feat: add use cases marketing product with AI-generated content
Transform qualifying scenarios into marketing-ready use cases with: - AI-generated titles, descriptions, ROI estimates, business value - Persona/industry/category taxonomy for targeting - Browseable feed with filtering and ranking - SEO-optimized case study pages - Daily cron job for generation and ranking Database: - Add Persona, Industry, Category lookup tables - Add UseCase model with marketing content fields - Add junction tables for personas/industries/categories - Add SocialProof model for cached metrics API: - GET /api/use-cases - Global directory with filtering - GET /api/use-cases/[id] - Individual use case details - GET /api/public/users/[username]/collections/[slug]/use-cases - POST /api/cron/use-cases - Nightly generation job Frontend: - /use-cases - Global feed with persona dropdown - /use-cases/[slug] - SEO case study page - /[username]/collections/[slug]/use-cases - Collection feed - UseCasesFeed component - Sortable table component - UseCaseCaseStudy component - Full case study layout
This commit is contained in:
parent
5fafc90e3e
commit
48c41e8733
245 changed files with 4734 additions and 2075 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -2,8 +2,7 @@
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"quickfix.biome": "explicit",
|
"source.fixAll.biome": "explicit"
|
||||||
"source.organizeImports.biome": "explicit"
|
|
||||||
},
|
},
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createOpenAI } from '@ai-sdk/openai';
|
import { createOpenAI } from '@ai-sdk/openai';
|
||||||
import { searchTpmjsToolsTool } from '@tpmjs/search-registry';
|
import { searchTpmjsToolsTool } from '@tpmjs/search-registry';
|
||||||
import { type UIMessage, convertToModelMessages, stepCountIs, streamText } from 'ai';
|
import { convertToModelMessages, stepCountIs, streamText, type UIMessage } from 'ai';
|
||||||
import type { NextRequest } from 'next/server';
|
import type { NextRequest } from 'next/server';
|
||||||
import { env } from '~/env';
|
import { env } from '~/env';
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Analytics } from '@vercel/analytics/next';
|
import { Analytics } from '@vercel/analytics/next';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { ThemeProvider } from 'next-themes';
|
|
||||||
import { Space_Grotesk, Space_Mono } from 'next/font/google';
|
import { Space_Grotesk, Space_Mono } from 'next/font/google';
|
||||||
|
import { ThemeProvider } from 'next-themes';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
const spaceGrotesk = Space_Grotesk({
|
const spaceGrotesk = Space_Grotesk({
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const moduleCache = new Map();
|
||||||
/**
|
/**
|
||||||
* Health check endpoint
|
* Health check endpoint
|
||||||
*/
|
*/
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|
@ -192,7 +192,7 @@ app.post('/execute-tool', async (req, res) => {
|
||||||
/**
|
/**
|
||||||
* Clear module cache (for debugging)
|
* Clear module cache (for debugging)
|
||||||
*/
|
*/
|
||||||
app.post('/cache/clear', (req, res) => {
|
app.post('/cache/clear', (_req, res) => {
|
||||||
const size = moduleCache.size;
|
const size = moduleCache.size;
|
||||||
moduleCache.clear();
|
moduleCache.clear();
|
||||||
console.log(`🗑️ Cleared cache (${size} entries)`);
|
console.log(`🗑️ Cleared cache (${size} entries)`);
|
||||||
|
|
@ -206,7 +206,7 @@ app.post('/cache/clear', (req, res) => {
|
||||||
/**
|
/**
|
||||||
* Get cache statistics
|
* Get cache statistics
|
||||||
*/
|
*/
|
||||||
app.get('/cache/stats', (req, res) => {
|
app.get('/cache/stats', (_req, res) => {
|
||||||
const entries = Array.from(moduleCache.keys());
|
const entries = Array.from(moduleCache.keys());
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|
|
||||||
|
|
@ -947,7 +947,7 @@ async function handler(req: Request): Promise<Response> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
const port = Number.parseInt(Deno.env.get('PORT') || '3002');
|
const port = Number.parseInt(Deno.env.get('PORT') || '3002', 10);
|
||||||
|
|
||||||
console.log(`🚀 Railway Tool Executor (Deno) running on port ${port}`);
|
console.log(`🚀 Railway Tool Executor (Deno) running on port ${port}`);
|
||||||
console.log('📦 HTTP imports: ENABLED');
|
console.log('📦 HTTP imports: ENABLED');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { AgentExampleSlideshow } from '@/components/AgentExampleSlideshow';
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { AgentExampleSlideshow } from '@/components/AgentExampleSlideshow';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Real-World Agent Example | TPMJS Tutorials',
|
title: 'Real-World Agent Example | TPMJS Tutorials',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { FirstToolSlideshow } from '@/components/FirstToolSlideshow';
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { FirstToolSlideshow } from '@/components/FirstToolSlideshow';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Build Your First Tool | TPMJS Tutorials',
|
title: 'Build Your First Tool | TPMJS Tutorials',
|
||||||
|
|
|
||||||
|
|
@ -139,9 +139,9 @@
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
animation-duration: 0.01ms !important;
|
animation-duration: 0.01ms;
|
||||||
animation-iteration-count: 1 !important;
|
animation-iteration-count: 1;
|
||||||
transition-duration: 0.01ms !important;
|
transition-duration: 0.01ms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { OverviewSlideshow } from '@/components/OverviewSlideshow';
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { OverviewSlideshow } from '@/components/OverviewSlideshow';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Overview | TPMJS Tutorials',
|
title: 'Overview | TPMJS Tutorials',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { PlaygroundSlideshow } from '@/components/PlaygroundSlideshow';
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { PlaygroundSlideshow } from '@/components/PlaygroundSlideshow';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Interactive Playground | TPMJS Tutorials',
|
title: 'Interactive Playground | TPMJS Tutorials',
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSlideshow } from '@/hooks/useSlideshow';
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Slide } from './Slide';
|
import { useSlideshow } from '@/hooks/useSlideshow';
|
||||||
import { SlideNavigation } from './SlideNavigation';
|
|
||||||
import { SlideProgress } from './SlideProgress';
|
|
||||||
import { AgentCodeSlide } from './agent-example-slides/AgentCodeSlide';
|
import { AgentCodeSlide } from './agent-example-slides/AgentCodeSlide';
|
||||||
import { AgentExampleNextStepsSlide } from './agent-example-slides/AgentExampleNextStepsSlide';
|
import { AgentExampleNextStepsSlide } from './agent-example-slides/AgentExampleNextStepsSlide';
|
||||||
import { AgentExampleWelcomeSlide } from './agent-example-slides/AgentExampleWelcomeSlide';
|
import { AgentExampleWelcomeSlide } from './agent-example-slides/AgentExampleWelcomeSlide';
|
||||||
import { ExecutionFlowSlide } from './agent-example-slides/ExecutionFlowSlide';
|
import { ExecutionFlowSlide } from './agent-example-slides/ExecutionFlowSlide';
|
||||||
import { SetupCodeSlide } from './agent-example-slides/SetupCodeSlide';
|
import { SetupCodeSlide } from './agent-example-slides/SetupCodeSlide';
|
||||||
import { TheGoalSlide } from './agent-example-slides/TheGoalSlide';
|
import { TheGoalSlide } from './agent-example-slides/TheGoalSlide';
|
||||||
|
import { Slide } from './Slide';
|
||||||
|
import { SlideNavigation } from './SlideNavigation';
|
||||||
|
import { SlideProgress } from './SlideProgress';
|
||||||
|
|
||||||
const SLIDES = [
|
const SLIDES = [
|
||||||
{ id: 'welcome', Component: AgentExampleWelcomeSlide },
|
{ id: 'welcome', Component: AgentExampleWelcomeSlide },
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSlideshow } from '@/hooks/useSlideshow';
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useSlideshow } from '@/hooks/useSlideshow';
|
||||||
import { Slide } from './Slide';
|
import { Slide } from './Slide';
|
||||||
import { SlideNavigation } from './SlideNavigation';
|
import { SlideNavigation } from './SlideNavigation';
|
||||||
import { SlideProgress } from './SlideProgress';
|
import { SlideProgress } from './SlideProgress';
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSlideshow } from '@/hooks/useSlideshow';
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Slide } from './Slide';
|
import { useSlideshow } from '@/hooks/useSlideshow';
|
||||||
import { SlideNavigation } from './SlideNavigation';
|
|
||||||
import { SlideProgress } from './SlideProgress';
|
|
||||||
import { AuthorNextStepsSlide } from './author-slides/AuthorNextStepsSlide';
|
import { AuthorNextStepsSlide } from './author-slides/AuthorNextStepsSlide';
|
||||||
import { AuthorWelcomeSlide } from './author-slides/AuthorWelcomeSlide';
|
import { AuthorWelcomeSlide } from './author-slides/AuthorWelcomeSlide';
|
||||||
import { GetDiscoveredSlide } from './author-slides/GetDiscoveredSlide';
|
import { GetDiscoveredSlide } from './author-slides/GetDiscoveredSlide';
|
||||||
|
|
@ -15,6 +12,9 @@ import { PackageSetupSlide } from './author-slides/PackageSetupSlide';
|
||||||
import { QualityScoringSlide } from './author-slides/QualityScoringSlide';
|
import { QualityScoringSlide } from './author-slides/QualityScoringSlide';
|
||||||
import { ToolDefinitionSlide } from './author-slides/ToolDefinitionSlide';
|
import { ToolDefinitionSlide } from './author-slides/ToolDefinitionSlide';
|
||||||
import { WhatIsRegistrySlide } from './author-slides/WhatIsRegistrySlide';
|
import { WhatIsRegistrySlide } from './author-slides/WhatIsRegistrySlide';
|
||||||
|
import { Slide } from './Slide';
|
||||||
|
import { SlideNavigation } from './SlideNavigation';
|
||||||
|
import { SlideProgress } from './SlideProgress';
|
||||||
|
|
||||||
const SLIDES = [
|
const SLIDES = [
|
||||||
{ id: 'welcome', Component: AuthorWelcomeSlide },
|
{ id: 'welcome', Component: AuthorWelcomeSlide },
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSlideshow } from '@/hooks/useSlideshow';
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Slide } from './Slide';
|
import { useSlideshow } from '@/hooks/useSlideshow';
|
||||||
import { SlideNavigation } from './SlideNavigation';
|
|
||||||
import { SlideProgress } from './SlideProgress';
|
|
||||||
import { FirstToolNextStepsSlide } from './first-tool-slides/FirstToolNextStepsSlide';
|
import { FirstToolNextStepsSlide } from './first-tool-slides/FirstToolNextStepsSlide';
|
||||||
import { FirstToolWelcomeSlide } from './first-tool-slides/FirstToolWelcomeSlide';
|
import { FirstToolWelcomeSlide } from './first-tool-slides/FirstToolWelcomeSlide';
|
||||||
import { PackageJsonSlide } from './first-tool-slides/PackageJsonSlide';
|
import { PackageJsonSlide } from './first-tool-slides/PackageJsonSlide';
|
||||||
import { ProjectSetupSlide } from './first-tool-slides/ProjectSetupSlide';
|
import { ProjectSetupSlide } from './first-tool-slides/ProjectSetupSlide';
|
||||||
import { PublishSlide } from './first-tool-slides/PublishSlide';
|
import { PublishSlide } from './first-tool-slides/PublishSlide';
|
||||||
import { WriteToolSlide } from './first-tool-slides/WriteToolSlide';
|
import { WriteToolSlide } from './first-tool-slides/WriteToolSlide';
|
||||||
|
import { Slide } from './Slide';
|
||||||
|
import { SlideNavigation } from './SlideNavigation';
|
||||||
|
import { SlideProgress } from './SlideProgress';
|
||||||
|
|
||||||
const SLIDES = [
|
const SLIDES = [
|
||||||
{ id: 'welcome', Component: FirstToolWelcomeSlide },
|
{ id: 'welcome', Component: FirstToolWelcomeSlide },
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSlideshow } from '@/hooks/useSlideshow';
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Slide } from './Slide';
|
import { useSlideshow } from '@/hooks/useSlideshow';
|
||||||
import { SlideNavigation } from './SlideNavigation';
|
|
||||||
import { SlideProgress } from './SlideProgress';
|
|
||||||
import { ArchitectureSlide } from './overview-slides/ArchitectureSlide';
|
import { ArchitectureSlide } from './overview-slides/ArchitectureSlide';
|
||||||
import { EcosystemSlide } from './overview-slides/EcosystemSlide';
|
import { EcosystemSlide } from './overview-slides/EcosystemSlide';
|
||||||
import { ExploreSlide } from './overview-slides/ExploreSlide';
|
import { ExploreSlide } from './overview-slides/ExploreSlide';
|
||||||
import { OverviewWelcomeSlide } from './overview-slides/OverviewWelcomeSlide';
|
import { OverviewWelcomeSlide } from './overview-slides/OverviewWelcomeSlide';
|
||||||
import { UseCasesSlide } from './overview-slides/UseCasesSlide';
|
import { UseCasesSlide } from './overview-slides/UseCasesSlide';
|
||||||
import { WhatIsTpmjsSlide } from './overview-slides/WhatIsTpmjsSlide';
|
import { WhatIsTpmjsSlide } from './overview-slides/WhatIsTpmjsSlide';
|
||||||
|
import { Slide } from './Slide';
|
||||||
|
import { SlideNavigation } from './SlideNavigation';
|
||||||
|
import { SlideProgress } from './SlideProgress';
|
||||||
|
|
||||||
const SLIDES = [
|
const SLIDES = [
|
||||||
{ id: 'welcome', Component: OverviewWelcomeSlide },
|
{ id: 'welcome', Component: OverviewWelcomeSlide },
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSlideshow } from '@/hooks/useSlideshow';
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Slide } from './Slide';
|
import { useSlideshow } from '@/hooks/useSlideshow';
|
||||||
import { SlideNavigation } from './SlideNavigation';
|
|
||||||
import { SlideProgress } from './SlideProgress';
|
|
||||||
import { FillParametersSlide } from './playground-slides/FillParametersSlide';
|
import { FillParametersSlide } from './playground-slides/FillParametersSlide';
|
||||||
import { FindToolSlide } from './playground-slides/FindToolSlide';
|
import { FindToolSlide } from './playground-slides/FindToolSlide';
|
||||||
import { OpenPlaygroundSlide } from './playground-slides/OpenPlaygroundSlide';
|
import { OpenPlaygroundSlide } from './playground-slides/OpenPlaygroundSlide';
|
||||||
import { PlaygroundNextStepsSlide } from './playground-slides/PlaygroundNextStepsSlide';
|
import { PlaygroundNextStepsSlide } from './playground-slides/PlaygroundNextStepsSlide';
|
||||||
import { PlaygroundWelcomeSlide } from './playground-slides/PlaygroundWelcomeSlide';
|
import { PlaygroundWelcomeSlide } from './playground-slides/PlaygroundWelcomeSlide';
|
||||||
import { SeeResultsSlide } from './playground-slides/SeeResultsSlide';
|
import { SeeResultsSlide } from './playground-slides/SeeResultsSlide';
|
||||||
|
import { Slide } from './Slide';
|
||||||
|
import { SlideNavigation } from './SlideNavigation';
|
||||||
|
import { SlideProgress } from './SlideProgress';
|
||||||
|
|
||||||
const SLIDES = [
|
const SLIDES = [
|
||||||
{ id: 'welcome', Component: PlaygroundWelcomeSlide },
|
{ id: 'welcome', Component: PlaygroundWelcomeSlide },
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { type Variants, motion } from 'framer-motion';
|
import { motion, type Variants } from 'framer-motion';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
interface SlideProps {
|
interface SlideProps {
|
||||||
|
|
|
||||||
|
|
@ -107,12 +107,7 @@ export default function ForgotPasswordPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button type="submit" disabled={loading} loading={loading} className="w-full">
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
loading={loading}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{loading ? 'Sending...' : 'Send Reset Link'}
|
{loading ? 'Sending...' : 'Send Reset Link'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { AuthHeader } from '@/components/AuthHeader';
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { AuthHeader } from '@/components/AuthHeader';
|
||||||
|
|
||||||
export default function AuthLayout({ children }: { children: ReactNode }) {
|
export default function AuthLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,9 @@ export default function SignInPage() {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<Label htmlFor="password" className="mb-0">Password</Label>
|
<Label htmlFor="password" className="mb-0">
|
||||||
|
Password
|
||||||
|
</Label>
|
||||||
<Link
|
<Link
|
||||||
href="/forgot-password"
|
href="/forgot-password"
|
||||||
className="text-sm text-foreground-secondary hover:text-foreground hover:underline"
|
className="text-sm text-foreground-secondary hover:text-foreground hover:underline"
|
||||||
|
|
@ -122,12 +124,7 @@ export default function SignInPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button type="submit" disabled={loading} loading={loading} className="w-full">
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
loading={loading}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{loading ? 'Signing in...' : 'Sign In'}
|
{loading ? 'Signing in...' : 'Sign In'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { suggestUsername } from '@tpmjs/types/user';
|
||||||
import { Button } from '@tpmjs/ui/Button/Button';
|
import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
import { Input } from '@tpmjs/ui/Input/Input';
|
import { Input } from '@tpmjs/ui/Input/Input';
|
||||||
import { Label } from '@tpmjs/ui/Label/Label';
|
import { Label } from '@tpmjs/ui/Label/Label';
|
||||||
import { suggestUsername } from '@tpmjs/types/user';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { signUp } from '~/lib/auth-client';
|
import { signUp } from '~/lib/auth-client';
|
||||||
|
|
@ -222,9 +222,7 @@ export default function SignUpPage() {
|
||||||
{!checkingUsername &&
|
{!checkingUsername &&
|
||||||
usernameCheck &&
|
usernameCheck &&
|
||||||
!usernameCheck.available &&
|
!usernameCheck.available &&
|
||||||
username.length >= 3 && (
|
username.length >= 3 && <Icon icon="x" size="sm" className="text-error" />}
|
||||||
<Icon icon="x" size="sm" className="text-error" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Username availability message */}
|
{/* Username availability message */}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
/**
|
||||||
|
* Collection Use Cases Page
|
||||||
|
*
|
||||||
|
* Browseable/searchable feed of use cases for a specific collection
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { prisma } from '@tpmjs/db';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
import UseCasesFeed from '~/components/UseCasesFeed';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: Promise<{ username: string; slug: string }>;
|
||||||
|
searchParams: Promise<{ persona?: string; search?: string; sort?: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: Props) {
|
||||||
|
const { username, slug } = await params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const collection = await prisma.collection.findFirst({
|
||||||
|
where: {
|
||||||
|
slug,
|
||||||
|
user: { username },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return {
|
||||||
|
title: 'Collection Not Found - TPMJS',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `Use Cases - ${collection.name} - TPMJS`,
|
||||||
|
description: collection.description || `Use cases for ${collection.name}`,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
title: 'Collection Use Cases - TPMJS',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function CollectionUseCasesPage({ params, searchParams }: Props) {
|
||||||
|
const { username, slug } = await params;
|
||||||
|
const { persona, search, sort } = await searchParams;
|
||||||
|
|
||||||
|
// Verify collection exists and is public
|
||||||
|
const collection = await prisma.collection.findFirst({
|
||||||
|
where: {
|
||||||
|
slug,
|
||||||
|
user: { username },
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
description: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="border-b bg-card/50 backdrop-blur">
|
||||||
|
<div className="container mx-auto px-4 py-8 md:py-12">
|
||||||
|
<div className="mx-auto max-w-3xl text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<a
|
||||||
|
href={`/${collection.user.username}/collections/${collection.slug}`}
|
||||||
|
className="hover:text-foreground"
|
||||||
|
>
|
||||||
|
{collection.name}
|
||||||
|
</a>
|
||||||
|
{' / '}Use Cases
|
||||||
|
</p>
|
||||||
|
<h1 className="mt-4 text-4xl font-bold tracking-tight sm:text-5xl">Use Cases</h1>
|
||||||
|
<p className="mt-4 text-lg text-muted-foreground">
|
||||||
|
Proven workflows from this collection that deliver real business value.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<Suspense fallback={<UseCasesFeedSkeleton />}>
|
||||||
|
<UseCasesFeed
|
||||||
|
collectionId={collection.id}
|
||||||
|
initialPersona={persona}
|
||||||
|
initialSearch={search}
|
||||||
|
initialSort={sort}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UseCasesFeedSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
<div className="h-10 w-48 animate-pulse rounded-lg bg-muted" />
|
||||||
|
<div className="h-10 w-48 animate-pulse rounded-lg bg-muted" />
|
||||||
|
<div className="h-10 w-48 animate-pulse rounded-lg bg-muted" />
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border">
|
||||||
|
<div className="h-12 animate-pulse bg-muted/50" />
|
||||||
|
{Array.from({ length: 10 }).map((_, i) => (
|
||||||
|
<div key={i} className="h-16 animate-pulse border-t bg-muted/30" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -18,9 +18,9 @@ export default function AboutPage(): React.ReactElement {
|
||||||
|
|
||||||
<div className="prose prose-neutral dark:prose-invert max-w-none">
|
<div className="prose prose-neutral dark:prose-invert max-w-none">
|
||||||
<p className="text-lg text-foreground-secondary mb-6">
|
<p className="text-lg text-foreground-secondary mb-6">
|
||||||
TPMJS (Tool Package Manager for JavaScript) is the npm registry for AI agent tools.
|
TPMJS (Tool Package Manager for JavaScript) is the npm registry for AI agent tools. It
|
||||||
It was started in 2024 to make it easy for developers to publish and discover tools
|
was started in 2024 to make it easy for developers to publish and discover tools that AI
|
||||||
that AI agents can use.
|
agents can use.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 className="text-xl font-semibold mt-10 mb-4 text-foreground">Creator</h2>
|
<h2 className="text-xl font-semibold mt-10 mb-4 text-foreground">Creator</h2>
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,9 @@ export default function PublicAgentsPage(): React.ReactElement {
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon="terminal"
|
icon="terminal"
|
||||||
title="No agents found"
|
title="No agents found"
|
||||||
description={search ? 'Try adjusting your search terms' : 'Be the first to share a public agent!'}
|
description={
|
||||||
|
search ? 'Try adjusting your search terms' : 'Be the first to share a public agent!'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { prisma } from '@tpmjs/db';
|
import { prisma } from '@tpmjs/db';
|
||||||
import { AGENT_LIMITS, AddCollectionToAgentSchema } from '@tpmjs/types/agent';
|
import { AddCollectionToAgentSchema, AGENT_LIMITS } from '@tpmjs/types/agent';
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import { type NextRequest, NextResponse } from 'next/server';
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { prisma } from '@tpmjs/db';
|
import { prisma } from '@tpmjs/db';
|
||||||
import { AGENT_LIMITS, AddToolToAgentSchema } from '@tpmjs/types/agent';
|
import { AddToolToAgentSchema, AGENT_LIMITS } from '@tpmjs/types/agent';
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import { type NextRequest, NextResponse } from 'next/server';
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ export async function GET(request: NextRequest): Promise<NextResponse> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20'), 50);
|
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20', 10), 50);
|
||||||
const offset = Number.parseInt(searchParams.get('offset') || '0');
|
const offset = Number.parseInt(searchParams.get('offset') || '0', 10);
|
||||||
|
|
||||||
const agents = await prisma.agent.findMany({
|
const agents = await prisma.agent.findMany({
|
||||||
where: { userId: authResult.userId },
|
where: { userId: authResult.userId },
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { auth } from '@/lib/auth';
|
|
||||||
import { toNextJsHandler } from 'better-auth/next-js';
|
import { toNextJsHandler } from 'better-auth/next-js';
|
||||||
|
import { auth } from '@/lib/auth';
|
||||||
|
|
||||||
export const { GET, POST } = toNextJsHandler(auth);
|
export const { GET, POST } = toNextJsHandler(auth);
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
||||||
name: scenario.name,
|
name: scenario.name,
|
||||||
tags: scenario.tags,
|
tags: scenario.tags,
|
||||||
},
|
},
|
||||||
...(similarityResult && similarityResult.hasSimilar
|
...(similarityResult?.hasSimilar
|
||||||
? {
|
? {
|
||||||
similarity: {
|
similarity: {
|
||||||
hasSimilar: true,
|
hasSimilar: true,
|
||||||
|
|
|
||||||
98
apps/web/src/app/api/cron/use-cases/route.ts
Normal file
98
apps/web/src/app/api/cron/use-cases/route.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
/**
|
||||||
|
* Use Cases Generation Cron Endpoint
|
||||||
|
*
|
||||||
|
* Triggers the nightly use cases generation process to:
|
||||||
|
* 1. Find qualifying scenarios (qualityScore >= 0.3, totalRuns >= 1, lastRunStatus = 'pass')
|
||||||
|
* 2. Generate marketing content via AI
|
||||||
|
* 3. Create/update UseCase records
|
||||||
|
* 4. Update SocialProof cache
|
||||||
|
* 5. Compute rank scores
|
||||||
|
*
|
||||||
|
* Schedule: Daily at midnight (configured in vercel.json)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { env } from '~/env';
|
||||||
|
import {
|
||||||
|
computeRankScores,
|
||||||
|
generateUseCasesForQualifyingScenarios,
|
||||||
|
} from '~/lib/use-cases/generate';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
export const maxDuration = 300; // 5 minutes for AI generation
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// 1. Verify cron secret
|
||||||
|
const authHeader = request.headers.get('authorization');
|
||||||
|
const token = authHeader?.replace('Bearer ', '');
|
||||||
|
|
||||||
|
if (env.CRON_SECRET && token !== env.CRON_SECRET) {
|
||||||
|
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Generate use cases for qualifying scenarios
|
||||||
|
const generationResult = await generateUseCasesForQualifyingScenarios();
|
||||||
|
|
||||||
|
// 3. Compute rank scores for all use cases
|
||||||
|
const rankedCount = await computeRankScores();
|
||||||
|
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
|
||||||
|
// 4. Log the result
|
||||||
|
const { prisma } = await import('@tpmjs/db');
|
||||||
|
await prisma.syncLog.create({
|
||||||
|
data: {
|
||||||
|
source: 'use-cases-generation',
|
||||||
|
status: generationResult.errors > 0 ? 'partial' : 'success',
|
||||||
|
processed: generationResult.created + generationResult.updated,
|
||||||
|
skipped: generationResult.skipped,
|
||||||
|
errors: generationResult.errors,
|
||||||
|
message: `Created: ${generationResult.created}, Updated: ${generationResult.updated}, Skipped: ${generationResult.skipped}, Ranked: ${rankedCount}`,
|
||||||
|
metadata: {
|
||||||
|
durationMs,
|
||||||
|
rankedCount,
|
||||||
|
errorDetails: generationResult.errorDetails.slice(0, 5), // Keep first 5 errors
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
created: generationResult.created,
|
||||||
|
updated: generationResult.updated,
|
||||||
|
skipped: generationResult.skipped,
|
||||||
|
errors: generationResult.errors,
|
||||||
|
ranked: rankedCount,
|
||||||
|
durationMs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
|
||||||
|
// Log error
|
||||||
|
const { prisma } = await import('@tpmjs/db');
|
||||||
|
await prisma.syncLog.create({
|
||||||
|
data: {
|
||||||
|
source: 'use-cases-generation',
|
||||||
|
status: 'error',
|
||||||
|
processed: 0,
|
||||||
|
skipped: 0,
|
||||||
|
errors: 1,
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
metadata: {
|
||||||
|
durationMs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
/**
|
||||||
|
* Collection Use Cases API
|
||||||
|
*
|
||||||
|
* GET /api/public/users/[username]/collections/[slug]/use-cases
|
||||||
|
* Get all use cases for a specific collection
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { prisma } from '@tpmjs/db';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import { apiForbidden, apiInternalError, apiNotFound, apiSuccess } from '~/lib/api-response';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
type RouteContext = {
|
||||||
|
params: Promise<{ username: string; slug: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/public/users/[username]/collections/[slug]/use-cases
|
||||||
|
* Get all use cases for a specific collection
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest, context: RouteContext) {
|
||||||
|
const requestId = crypto.randomUUID();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { username: rawUsername, slug } = await context.params;
|
||||||
|
const username = rawUsername.startsWith('@') ? rawUsername.slice(1) : rawUsername;
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
|
// Parse query parameters
|
||||||
|
const persona = searchParams.get('persona');
|
||||||
|
const industry = searchParams.get('industry');
|
||||||
|
const category = searchParams.get('category');
|
||||||
|
const searchQuery = searchParams.get('search');
|
||||||
|
const sort = (searchParams.get('sort') || 'rank') as 'rank' | 'quality' | 'runs' | 'recent';
|
||||||
|
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20', 10), 100);
|
||||||
|
const offset = Number.parseInt(searchParams.get('offset') || '0', 10);
|
||||||
|
|
||||||
|
// Find the user first
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { username },
|
||||||
|
select: { id: true, username: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user || !user.username) {
|
||||||
|
return apiNotFound('User', requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the collection by slug belonging to this user
|
||||||
|
const collection = await prisma.collection.findFirst({
|
||||||
|
where: {
|
||||||
|
slug,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return apiNotFound('Collection', requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only return if public
|
||||||
|
if (!collection.isPublic) {
|
||||||
|
return apiForbidden('This collection is not public', requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build where clause for use cases
|
||||||
|
const where: Record<string, unknown> = {
|
||||||
|
scenario: {
|
||||||
|
collectionId: collection.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (persona) {
|
||||||
|
where.personas = {
|
||||||
|
some: {
|
||||||
|
persona: { slug: persona },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (industry) {
|
||||||
|
where.industries = {
|
||||||
|
some: {
|
||||||
|
industry: { slug: industry },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
where.categories = {
|
||||||
|
some: {
|
||||||
|
category: { slug: category },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery) {
|
||||||
|
where.OR = [
|
||||||
|
{ marketingTitle: { contains: searchQuery, mode: 'insensitive' } },
|
||||||
|
{ marketingDesc: { contains: searchQuery, mode: 'insensitive' } },
|
||||||
|
{ businessValue: { contains: searchQuery, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build order by
|
||||||
|
const orderBy: {
|
||||||
|
rankScore?: 'asc' | 'desc';
|
||||||
|
socialProof?: { qualityScore?: 'asc' | 'desc'; totalRuns?: 'asc' | 'desc' };
|
||||||
|
createdAt?: 'asc' | 'desc';
|
||||||
|
} = {};
|
||||||
|
if (sort === 'quality') {
|
||||||
|
orderBy.socialProof = { qualityScore: 'desc' };
|
||||||
|
} else if (sort === 'runs') {
|
||||||
|
orderBy.socialProof = { totalRuns: 'desc' };
|
||||||
|
} else if (sort === 'recent') {
|
||||||
|
orderBy.createdAt = 'desc';
|
||||||
|
} else {
|
||||||
|
orderBy.rankScore = 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
const total = await prisma.useCase.count({ where });
|
||||||
|
|
||||||
|
// Fetch use cases
|
||||||
|
const useCases = await prisma.useCase.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
personas: {
|
||||||
|
select: {
|
||||||
|
persona: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
industries: {
|
||||||
|
select: {
|
||||||
|
industry: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
select: {
|
||||||
|
category: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
type: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
socialProof: true,
|
||||||
|
scenario: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
prompt: true,
|
||||||
|
name: true,
|
||||||
|
tags: true,
|
||||||
|
qualityScore: true,
|
||||||
|
totalRuns: true,
|
||||||
|
lastRunStatus: true,
|
||||||
|
collection: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy,
|
||||||
|
take: limit,
|
||||||
|
skip: offset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return apiSuccess(
|
||||||
|
{
|
||||||
|
collection: {
|
||||||
|
id: collection.id,
|
||||||
|
slug: collection.slug,
|
||||||
|
name: collection.name,
|
||||||
|
description: collection.description,
|
||||||
|
},
|
||||||
|
useCases,
|
||||||
|
pagination: {
|
||||||
|
total,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
hasMore: offset + limit < total,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ requestId }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'[API Error] GET /api/public/users/[username]/collections/[slug]/use-cases:',
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return apiInternalError('Failed to fetch use cases', requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -64,7 +64,6 @@ export async function GET(request: NextRequest) {
|
||||||
case 'recent':
|
case 'recent':
|
||||||
orderBy = [{ createdAt: 'desc' }];
|
orderBy = [{ createdAt: 'desc' }];
|
||||||
break;
|
break;
|
||||||
case 'quality':
|
|
||||||
default:
|
default:
|
||||||
orderBy = [{ qualityScore: 'desc' }, { package: { npmDownloadsLastMonth: 'desc' } }];
|
orderBy = [{ qualityScore: 'desc' }, { package: { npmDownloadsLastMonth: 'desc' } }];
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { prisma } from '@tpmjs/db';
|
import { prisma } from '@tpmjs/db';
|
||||||
import { fetchChanges, fetchLatestPackageWithMetadata } from '@tpmjs/npm-client';
|
import { fetchChanges, fetchLatestPackageWithMetadata } from '@tpmjs/npm-client';
|
||||||
import { validateTpmjsField } from '@tpmjs/types/tpmjs';
|
|
||||||
import type { TpmjsToolDefinition } from '@tpmjs/types/tpmjs';
|
import type { TpmjsToolDefinition } from '@tpmjs/types/tpmjs';
|
||||||
|
import { validateTpmjsField } from '@tpmjs/types/tpmjs';
|
||||||
import { type NextRequest, NextResponse } from 'next/server';
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
import { env } from '~/env';
|
import { env } from '~/env';
|
||||||
import { performHealthCheck } from '~/lib/health-check/health-check-service';
|
import { performHealthCheck } from '~/lib/health-check/health-check-service';
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,8 @@ export async function GET(request: NextRequest) {
|
||||||
const category = searchParams.get('category') || '';
|
const category = searchParams.get('category') || '';
|
||||||
const minQuality = Number.parseFloat(searchParams.get('minQuality') || '0');
|
const minQuality = Number.parseFloat(searchParams.get('minQuality') || '0');
|
||||||
const verb = searchParams.get('verb') || '';
|
const verb = searchParams.get('verb') || '';
|
||||||
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '100'), 10000);
|
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '100', 10), 10000);
|
||||||
const offset = Number.parseInt(searchParams.get('offset') || '0');
|
const offset = Number.parseInt(searchParams.get('offset') || '0', 10);
|
||||||
|
|
||||||
const data = loadToolIdeas();
|
const data = loadToolIdeas();
|
||||||
let tools = data.tools;
|
let tools = data.tools;
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ export async function GET(
|
||||||
case 'lowest':
|
case 'lowest':
|
||||||
orderBy = [{ rating: 'asc' }, { createdAt: 'desc' }];
|
orderBy = [{ rating: 'asc' }, { createdAt: 'desc' }];
|
||||||
break;
|
break;
|
||||||
case 'recent':
|
|
||||||
default:
|
default:
|
||||||
orderBy = [{ createdAt: 'desc' }];
|
orderBy = [{ createdAt: 'desc' }];
|
||||||
}
|
}
|
||||||
|
|
@ -365,18 +364,18 @@ export async function POST(
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
id: review!.id,
|
id: review?.id,
|
||||||
title: review!.title,
|
title: review?.title,
|
||||||
content: review!.content,
|
content: review?.content,
|
||||||
rating: review!.rating,
|
rating: review?.rating,
|
||||||
helpfulCount: review!.helpfulCount,
|
helpfulCount: review?.helpfulCount,
|
||||||
createdAt: review!.createdAt.toISOString(),
|
createdAt: review?.createdAt.toISOString(),
|
||||||
updatedAt: review!.updatedAt.toISOString(),
|
updatedAt: review?.updatedAt.toISOString(),
|
||||||
user: {
|
user: {
|
||||||
id: review!.user.id,
|
id: review?.user.id,
|
||||||
name: review!.user.name,
|
name: review?.user.name,
|
||||||
image: review!.user.image,
|
image: review?.user.image,
|
||||||
username: review!.user.username,
|
username: review?.user.username,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId },
|
meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId },
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
* Executes TPMJS tools with AI agents and streams real-time progress
|
* Executes TPMJS tools with AI agents and streams real-time progress
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { checkRateLimit, getClientIP } from '@/lib/rate-limiter';
|
|
||||||
import { prisma } from '@tpmjs/db';
|
import { prisma } from '@tpmjs/db';
|
||||||
import { type NextRequest, NextResponse } from 'next/server';
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { checkRateLimit, getClientIP } from '@/lib/rate-limiter';
|
||||||
|
|
||||||
// Use Node.js runtime for SSE streaming
|
// Use Node.js runtime for SSE streaming
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { prisma } from '@tpmjs/db';
|
import { prisma } from '@tpmjs/db';
|
||||||
import { type NextRequest, NextResponse } from 'next/server';
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
import { STRICT_RATE_LIMIT, checkRateLimit } from '~/lib/rate-limit';
|
import { checkRateLimit, STRICT_RATE_LIMIT } from '~/lib/rate-limit';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
@ -93,7 +93,7 @@ export async function GET(request: NextRequest) {
|
||||||
// Accept both 'q' and 'query' parameters for flexibility
|
// Accept both 'q' and 'query' parameters for flexibility
|
||||||
const query = searchParams.get('q') || searchParams.get('query') || '';
|
const query = searchParams.get('q') || searchParams.get('query') || '';
|
||||||
const category = searchParams.get('category');
|
const category = searchParams.get('category');
|
||||||
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '10'), 100);
|
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '10', 10), 100);
|
||||||
|
|
||||||
// Parse excludeIds to filter out tools already in user's collection
|
// Parse excludeIds to filter out tools already in user's collection
|
||||||
const excludeIdsParam = searchParams.get('excludeIds');
|
const excludeIdsParam = searchParams.get('excludeIds');
|
||||||
|
|
@ -206,7 +206,8 @@ export async function GET(request: NextRequest) {
|
||||||
// Boost for package name match (when user searches by package name like @tpmjs/tools-unsandbox)
|
// Boost for package name match (when user searches by package name like @tpmjs/tools-unsandbox)
|
||||||
const packageNameBoost = hasPackageNameMatch(query, tool.package.npmPackageName) ? 50 : 0;
|
const packageNameBoost = hasPackageNameMatch(query, tool.package.npmPackageName) ? 50 : 0;
|
||||||
|
|
||||||
const finalScore = bm25Score + qualityBoost + downloadBoost + exactNameBoost + packageNameBoost;
|
const finalScore =
|
||||||
|
bm25Score + qualityBoost + downloadBoost + exactNameBoost + packageNameBoost;
|
||||||
|
|
||||||
return { tool, score: finalScore };
|
return { tool, score: finalScore };
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ export async function GET(request: NextRequest): Promise<NextResponse<ApiRespons
|
||||||
case 'month':
|
case 'month':
|
||||||
recencyPeriodMs = 30 * 24 * 60 * 60 * 1000;
|
recencyPeriodMs = 30 * 24 * 60 * 60 * 1000;
|
||||||
break;
|
break;
|
||||||
case 'all':
|
|
||||||
default:
|
default:
|
||||||
recencyPeriodMs = 365 * 24 * 60 * 60 * 1000; // 1 year
|
recencyPeriodMs = 365 * 24 * 60 * 60 * 1000; // 1 year
|
||||||
}
|
}
|
||||||
|
|
|
||||||
175
apps/web/src/app/api/use-cases/[id]/route.ts
Normal file
175
apps/web/src/app/api/use-cases/[id]/route.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
/**
|
||||||
|
* Individual Use Case API
|
||||||
|
*
|
||||||
|
* GET /api/use-cases/[id] - Get a single use case by ID or slug
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { prisma } from '@tpmjs/db';
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
interface UseCaseDetail {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
marketingTitle: string;
|
||||||
|
marketingDesc: string;
|
||||||
|
roiEstimate: string | null;
|
||||||
|
businessValue: string | null;
|
||||||
|
problemStatement: string | null;
|
||||||
|
solutionNarrative: string | null;
|
||||||
|
rankScore: number;
|
||||||
|
createdAt: Date;
|
||||||
|
lastRegeneratedAt: Date | null;
|
||||||
|
personas: Array<{
|
||||||
|
persona: { slug: string; name: string; description: string | null; icon: string | null };
|
||||||
|
}>;
|
||||||
|
industries: Array<{ industry: { slug: string; name: string; description: string | null } }>;
|
||||||
|
categories: Array<{
|
||||||
|
category: { slug: string; name: string; type: string; description: string | null };
|
||||||
|
}>;
|
||||||
|
socialProof: {
|
||||||
|
qualityScore: number;
|
||||||
|
totalRuns: number;
|
||||||
|
consecutivePasses: number;
|
||||||
|
lastRunStatus: string | null;
|
||||||
|
lastRunAt: Date | null;
|
||||||
|
successRate: number | null;
|
||||||
|
lastRunAgo: string | null;
|
||||||
|
} | null;
|
||||||
|
scenario: {
|
||||||
|
id: string;
|
||||||
|
prompt: string;
|
||||||
|
name: string | null;
|
||||||
|
description: string | null;
|
||||||
|
tags: string[];
|
||||||
|
qualityScore: number;
|
||||||
|
totalRuns: number;
|
||||||
|
lastRunStatus: string | null;
|
||||||
|
collection: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string | null;
|
||||||
|
user: {
|
||||||
|
username: string | null;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function GET(_request: NextRequest, { params }: Props) {
|
||||||
|
try {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
// Find by ID or slug
|
||||||
|
const useCase = await prisma.useCase.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [{ id }, { slug: id }],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
personas: {
|
||||||
|
include: {
|
||||||
|
persona: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
icon: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
industries: {
|
||||||
|
include: {
|
||||||
|
industry: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
include: {
|
||||||
|
category: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
type: true,
|
||||||
|
description: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
socialProof: true,
|
||||||
|
scenario: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
prompt: true,
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
tags: true,
|
||||||
|
qualityScore: true,
|
||||||
|
totalRuns: true,
|
||||||
|
lastRunStatus: true,
|
||||||
|
collection: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!useCase) {
|
||||||
|
return NextResponse.json({ success: false, error: 'Use case not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform response
|
||||||
|
const transformedUseCase: UseCaseDetail = {
|
||||||
|
id: useCase.id,
|
||||||
|
slug: useCase.slug,
|
||||||
|
marketingTitle: useCase.marketingTitle,
|
||||||
|
marketingDesc: useCase.marketingDesc,
|
||||||
|
roiEstimate: useCase.roiEstimate,
|
||||||
|
businessValue: useCase.businessValue,
|
||||||
|
problemStatement: useCase.problemStatement,
|
||||||
|
solutionNarrative: useCase.solutionNarrative,
|
||||||
|
rankScore: useCase.rankScore,
|
||||||
|
createdAt: useCase.createdAt,
|
||||||
|
lastRegeneratedAt: useCase.lastRegeneratedAt,
|
||||||
|
personas: useCase.personas,
|
||||||
|
industries: useCase.industries,
|
||||||
|
categories: useCase.categories,
|
||||||
|
socialProof: useCase.socialProof,
|
||||||
|
scenario: useCase.scenario,
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: transformedUseCase,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
219
apps/web/src/app/api/use-cases/route.ts
Normal file
219
apps/web/src/app/api/use-cases/route.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
/**
|
||||||
|
* Use Cases API - Global Directory
|
||||||
|
*
|
||||||
|
* GET /api/use-cases - List all use cases with filtering and pagination
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { prisma } from '@tpmjs/db';
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
interface UseCaseWithRelations {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
marketingTitle: string;
|
||||||
|
marketingDesc: string;
|
||||||
|
roiEstimate: string | null;
|
||||||
|
businessValue: string | null;
|
||||||
|
rankScore: number;
|
||||||
|
createdAt: Date;
|
||||||
|
lastRegeneratedAt: Date | null;
|
||||||
|
personas: Array<{ persona: { slug: string; name: string } }>;
|
||||||
|
industries: Array<{ industry: { slug: string; name: string } }>;
|
||||||
|
categories: Array<{ category: { slug: string; name: string; type: string } }>;
|
||||||
|
socialProof: {
|
||||||
|
qualityScore: number;
|
||||||
|
totalRuns: number;
|
||||||
|
consecutivePasses: number;
|
||||||
|
lastRunStatus: string | null;
|
||||||
|
lastRunAt: Date | null;
|
||||||
|
successRate: number | null;
|
||||||
|
lastRunAgo: string | null;
|
||||||
|
} | null;
|
||||||
|
scenario: {
|
||||||
|
id: string;
|
||||||
|
prompt: string;
|
||||||
|
name: string | null;
|
||||||
|
tags: string[];
|
||||||
|
collection: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string | null;
|
||||||
|
user: {
|
||||||
|
username: string | null;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
|
// Parse query parameters
|
||||||
|
const persona = searchParams.get('persona');
|
||||||
|
const industry = searchParams.get('industry');
|
||||||
|
const category = searchParams.get('category');
|
||||||
|
const search = searchParams.get('search');
|
||||||
|
const sort = (searchParams.get('sort') || 'rank') as 'rank' | 'quality' | 'runs' | 'recent';
|
||||||
|
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20', 10), 100);
|
||||||
|
const offset = Number.parseInt(searchParams.get('offset') || '0', 10);
|
||||||
|
|
||||||
|
// Build where clause
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (persona) {
|
||||||
|
where.personas = {
|
||||||
|
some: {
|
||||||
|
persona: { slug: persona },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (industry) {
|
||||||
|
where.industries = {
|
||||||
|
some: {
|
||||||
|
industry: { slug: industry },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
where.categories = {
|
||||||
|
some: {
|
||||||
|
category: { slug: category },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ marketingTitle: { contains: search, mode: 'insensitive' } },
|
||||||
|
{ marketingDesc: { contains: search, mode: 'insensitive' } },
|
||||||
|
{ businessValue: { contains: search, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build order by
|
||||||
|
const orderBy: {
|
||||||
|
rankScore?: 'asc' | 'desc';
|
||||||
|
socialProof?: { qualityScore?: 'asc' | 'desc'; totalRuns?: 'asc' | 'desc' };
|
||||||
|
createdAt?: 'asc' | 'desc';
|
||||||
|
} = {};
|
||||||
|
if (sort === 'quality') {
|
||||||
|
orderBy.socialProof = { qualityScore: 'desc' };
|
||||||
|
} else if (sort === 'runs') {
|
||||||
|
orderBy.socialProof = { totalRuns: 'desc' };
|
||||||
|
} else if (sort === 'recent') {
|
||||||
|
orderBy.createdAt = 'desc';
|
||||||
|
} else {
|
||||||
|
orderBy.rankScore = 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
const total = await prisma.useCase.count({ where });
|
||||||
|
|
||||||
|
// Fetch use cases
|
||||||
|
const useCases = await prisma.useCase.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
personas: {
|
||||||
|
select: {
|
||||||
|
persona: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
industries: {
|
||||||
|
select: {
|
||||||
|
industry: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
select: {
|
||||||
|
category: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
type: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
socialProof: true,
|
||||||
|
scenario: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
prompt: true,
|
||||||
|
name: true,
|
||||||
|
tags: true,
|
||||||
|
collection: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy,
|
||||||
|
take: limit,
|
||||||
|
skip: offset,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transform response
|
||||||
|
const transformedUseCases: UseCaseWithRelations[] = useCases.map((uc) => ({
|
||||||
|
id: uc.id,
|
||||||
|
slug: uc.slug,
|
||||||
|
marketingTitle: uc.marketingTitle,
|
||||||
|
marketingDesc: uc.marketingDesc,
|
||||||
|
roiEstimate: uc.roiEstimate,
|
||||||
|
businessValue: uc.businessValue,
|
||||||
|
rankScore: uc.rankScore,
|
||||||
|
createdAt: uc.createdAt,
|
||||||
|
lastRegeneratedAt: uc.lastRegeneratedAt,
|
||||||
|
personas: uc.personas,
|
||||||
|
industries: uc.industries,
|
||||||
|
categories: uc.categories,
|
||||||
|
socialProof: uc.socialProof,
|
||||||
|
scenario: uc.scenario,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
useCases: transformedUseCases,
|
||||||
|
pagination: {
|
||||||
|
total,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
hasMore: offset + limit < total,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { prisma } from '@tpmjs/db';
|
import { prisma } from '@tpmjs/db';
|
||||||
import { RESERVED_USERNAMES, USERNAME_REGEX, UpdateUserProfileSchema } from '@tpmjs/types/user';
|
import { RESERVED_USERNAMES, UpdateUserProfileSchema, USERNAME_REGEX } from '@tpmjs/types/user';
|
||||||
import { type NextRequest, NextResponse } from 'next/server';
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { authenticateRequest } from '~/lib/api-keys/middleware';
|
import { authenticateRequest } from '~/lib/api-keys/middleware';
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,10 @@ function CliAuthContent(): React.ReactElement {
|
||||||
</Link>
|
</Link>
|
||||||
<p className="text-sm text-foreground-tertiary mt-4">
|
<p className="text-sm text-foreground-tertiary mt-4">
|
||||||
Don't have an account?{' '}
|
Don't have an account?{' '}
|
||||||
<Link href={`/sign-up?redirect=${encodeURIComponent(returnUrl)}`} className="text-primary hover:underline">
|
<Link
|
||||||
|
href={`/sign-up?redirect=${encodeURIComponent(returnUrl)}`}
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
Sign up
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -147,23 +150,15 @@ function CliAuthContent(): React.ReactElement {
|
||||||
<div className="bg-surface-secondary border border-border rounded-lg p-4 mb-6">
|
<div className="bg-surface-secondary border border-border rounded-lg p-4 mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{user.image ? (
|
{user.image ? (
|
||||||
<img
|
<img src={user.image} alt="" className="w-10 h-10 rounded-full" />
|
||||||
src={user.image}
|
|
||||||
alt=""
|
|
||||||
className="w-10 h-10 rounded-full"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||||
<Icon icon="user" size="sm" className="text-primary" />
|
<Icon icon="user" size="sm" className="text-primary" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-foreground">
|
<p className="font-medium text-foreground">{user.name || 'User'}</p>
|
||||||
{user.name || 'User'}
|
<p className="text-sm text-foreground-secondary">{user.email}</p>
|
||||||
</p>
|
|
||||||
<p className="text-sm text-foreground-secondary">
|
|
||||||
{user.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -195,11 +190,7 @@ function CliAuthContent(): React.ReactElement {
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Button
|
<Button onClick={handleAuthorize} disabled={isAuthorizing} className="w-full">
|
||||||
onClick={handleAuthorize}
|
|
||||||
disabled={isAuthorizing}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{isAuthorizing ? (
|
{isAuthorizing ? (
|
||||||
<>
|
<>
|
||||||
<Icon icon="loader" size="sm" className="mr-2 animate-spin" />
|
<Icon icon="loader" size="sm" className="mr-2 animate-spin" />
|
||||||
|
|
@ -223,8 +214,8 @@ function CliAuthContent(): React.ReactElement {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-foreground-tertiary text-center mt-6">
|
<p className="text-xs text-foreground-tertiary text-center mt-6">
|
||||||
An API key will be created and sent to the CLI.
|
An API key will be created and sent to the CLI. You can revoke it anytime from your
|
||||||
You can revoke it anytime from your dashboard.
|
dashboard.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,11 @@ export default function PublicCollectionsPage(): React.ReactElement {
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon="folder"
|
icon="folder"
|
||||||
title="No collections found"
|
title="No collections found"
|
||||||
description={search ? 'Try adjusting your search terms' : 'Be the first to share a public collection!'}
|
description={
|
||||||
|
search
|
||||||
|
? 'Try adjusting your search terms'
|
||||||
|
: 'Be the first to share a public collection!'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -360,7 +360,7 @@ export default function AgentChatPage(): React.ReactElement {
|
||||||
// Scroll to bottom only when messages change or streaming content updates
|
// Scroll to bottom only when messages change or streaming content updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [messages.length, streamingContent]);
|
}, []);
|
||||||
|
|
||||||
const handleSelectConversation = (convSlug: string) => {
|
const handleSelectConversation = (convSlug: string) => {
|
||||||
router.push(`/dashboard/agents/${agentId}/chat/${convSlug}`);
|
router.push(`/dashboard/agents/${agentId}/chat/${convSlug}`);
|
||||||
|
|
|
||||||
|
|
@ -322,11 +322,7 @@ export default function NewAgentPage(): React.ReactElement {
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-error/10 border border-error/20 rounded-lg p-4">
|
<div className="bg-error/10 border border-error/20 rounded-lg p-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Icon
|
<Icon icon="alertCircle" size="sm" className="text-error mt-0.5" />
|
||||||
icon="alertCircle"
|
|
||||||
size="sm"
|
|
||||||
className="text-error mt-0.5"
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-error">{error}</p>
|
<p className="text-sm text-error">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -168,8 +168,7 @@ export default function AgentsPage(): React.ReactElement {
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
// Loading skeleton
|
// Loading skeleton
|
||||||
<>
|
[0, 1, 2].map((idx) => (
|
||||||
{[0, 1, 2].map((idx) => (
|
|
||||||
<TableRow key={`agent-skeleton-${idx}`}>
|
<TableRow key={`agent-skeleton-${idx}`}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|
@ -193,8 +192,7 @@ export default function AgentsPage(): React.ReactElement {
|
||||||
<div className="h-8 w-24 bg-surface-secondary rounded animate-pulse ml-auto" />
|
<div className="h-8 w-24 bg-surface-secondary rounded animate-pulse ml-auto" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))
|
||||||
</>
|
|
||||||
) : agents.length === 0 ? (
|
) : agents.length === 0 ? (
|
||||||
<TableEmpty
|
<TableEmpty
|
||||||
colSpan={5}
|
colSpan={5}
|
||||||
|
|
|
||||||
|
|
@ -192,8 +192,7 @@ export default function CollectionsPage(): React.ReactElement {
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
// Loading skeleton
|
// Loading skeleton
|
||||||
<>
|
[0, 1, 2].map((idx) => (
|
||||||
{[0, 1, 2].map((idx) => (
|
|
||||||
<TableRow key={`collection-skeleton-${idx}`}>
|
<TableRow key={`collection-skeleton-${idx}`}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|
@ -217,8 +216,7 @@ export default function CollectionsPage(): React.ReactElement {
|
||||||
<div className="h-8 w-8 bg-surface-secondary rounded animate-pulse ml-auto" />
|
<div className="h-8 w-8 bg-surface-secondary rounded animate-pulse ml-auto" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))
|
||||||
</>
|
|
||||||
) : collections.length === 0 ? (
|
) : collections.length === 0 ? (
|
||||||
<TableEmpty
|
<TableEmpty
|
||||||
colSpan={5}
|
colSpan={5}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { LikeButton } from '~/components/LikeButton';
|
|
||||||
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
||||||
|
import { LikeButton } from '~/components/LikeButton';
|
||||||
|
|
||||||
interface LikedAgent {
|
interface LikedAgent {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { LikeButton } from '~/components/LikeButton';
|
|
||||||
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
||||||
|
import { LikeButton } from '~/components/LikeButton';
|
||||||
|
|
||||||
interface LikedCollection {
|
interface LikedCollection {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { LikeButton } from '~/components/LikeButton';
|
|
||||||
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
||||||
|
import { LikeButton } from '~/components/LikeButton';
|
||||||
|
|
||||||
interface LikedTool {
|
interface LikedTool {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSession } from '@/lib/auth-client';
|
|
||||||
import { Button } from '@tpmjs/ui/Button/Button';
|
import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useSession } from '@/lib/auth-client';
|
||||||
import { DashboardActivityStream } from '~/components/DashboardActivityStream';
|
import { DashboardActivityStream } from '~/components/DashboardActivityStream';
|
||||||
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { Button } from '@tpmjs/ui/Button/Button';
|
import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
import { Input } from '@tpmjs/ui/Input/Input';
|
import { Input } from '@tpmjs/ui/Input/Input';
|
||||||
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
@ -13,6 +12,7 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@tpmjs/ui/Table/Table';
|
} from '@tpmjs/ui/Table/Table';
|
||||||
|
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
|
||||||
|
|
@ -268,8 +268,7 @@ ANOTHER_SECRET=xyz789"
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
[0, 1, 2].map((idx) => (
|
||||||
{[0, 1, 2].map((idx) => (
|
|
||||||
<TableRow key={`key-skeleton-${idx}`}>
|
<TableRow key={`key-skeleton-${idx}`}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="h-4 w-40 bg-surface-secondary rounded animate-pulse" />
|
<div className="h-4 w-40 bg-surface-secondary rounded animate-pulse" />
|
||||||
|
|
@ -284,8 +283,7 @@ ANOTHER_SECRET=xyz789"
|
||||||
<div className="h-8 w-8 bg-surface-secondary rounded animate-pulse ml-auto" />
|
<div className="h-8 w-8 bg-surface-secondary rounded animate-pulse ml-auto" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))
|
||||||
</>
|
|
||||||
) : keys.length === 0 ? (
|
) : keys.length === 0 ? (
|
||||||
<TableEmpty
|
<TableEmpty
|
||||||
colSpan={4}
|
colSpan={4}
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ export default function UsagePage(): React.ReactElement {
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{data.timeSeries.at(-1)?.periodStart &&
|
{data.timeSeries.at(-1)?.periodStart &&
|
||||||
new Date(data.timeSeries.at(-1)!.periodStart).toLocaleDateString()}
|
new Date(data.timeSeries.at(-1)?.periodStart).toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -214,8 +214,7 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
||||||
{/* GET /health */}
|
{/* GET /health */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||||
<code className="px-2 py-1 bg-success/10 text-success rounded">GET</code>{' '}
|
<code className="px-2 py-1 bg-success/10 text-success rounded">GET</code> /health
|
||||||
/health
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-foreground-secondary mb-4">
|
<p className="text-foreground-secondary mb-4">
|
||||||
Check executor health status. Used by TPMJS to verify the executor is reachable.
|
Check executor health status. Used by TPMJS to verify the executor is reachable.
|
||||||
|
|
|
||||||
|
|
@ -478,8 +478,8 @@ export default function PlatformGuidePage(): React.ReactElement {
|
||||||
with a complete audit trail
|
with a complete audit trail
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong className="text-foreground">Usage Analytics</strong> - Monitor API calls,
|
<strong className="text-foreground">Usage Analytics</strong> - Monitor API
|
||||||
tokens, and costs
|
calls, tokens, and costs
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
|
|
@ -792,7 +792,9 @@ Invalid usernames:
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Windows:</strong>{' '}
|
<strong>Windows:</strong>{' '}
|
||||||
<code className="text-primary">%APPDATA%\Claude\claude_desktop_config.json</code>
|
<code className="text-primary">
|
||||||
|
%APPDATA%\Claude\claude_desktop_config.json
|
||||||
|
</code>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
|
|
@ -934,7 +936,8 @@ Invalid usernames:
|
||||||
name: 'Temperature',
|
name: 'Temperature',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: false,
|
required: false,
|
||||||
description: 'Response randomness (0 = deterministic, 2 = creative). Default: 0.7',
|
description:
|
||||||
|
'Response randomness (0 = deterministic, 2 = creative). Default: 0.7',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Max Tool Calls',
|
name: 'Max Tool Calls',
|
||||||
|
|
@ -996,7 +999,9 @@ Always cite your sources and be transparent about limitations.`}
|
||||||
<ul className="list-disc list-inside space-y-2 text-foreground-secondary">
|
<ul className="list-disc list-inside space-y-2 text-foreground-secondary">
|
||||||
<li>Maximum 50 individual tools per agent</li>
|
<li>Maximum 50 individual tools per agent</li>
|
||||||
<li>Maximum 10 collections per agent</li>
|
<li>Maximum 10 collections per agent</li>
|
||||||
<li>Tools from attached collections don't count against the 50 tool limit</li>
|
<li>
|
||||||
|
Tools from attached collections don't count against the 50 tool limit
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
</DocSection>
|
</DocSection>
|
||||||
|
|
@ -1061,7 +1066,11 @@ Always cite your sources and be transparent about limitations.`}
|
||||||
{
|
{
|
||||||
name: 'Anthropic',
|
name: 'Anthropic',
|
||||||
desc: 'Claude models known for nuanced understanding',
|
desc: 'Claude models known for nuanced understanding',
|
||||||
models: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'],
|
models: [
|
||||||
|
'claude-sonnet-4-20250514',
|
||||||
|
'claude-3-5-haiku-20241022',
|
||||||
|
'claude-3-opus-20240229',
|
||||||
|
],
|
||||||
keyUrl: 'https://console.anthropic.com/settings/keys',
|
keyUrl: 'https://console.anthropic.com/settings/keys',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1073,7 +1082,11 @@ Always cite your sources and be transparent about limitations.`}
|
||||||
{
|
{
|
||||||
name: 'Groq',
|
name: 'Groq',
|
||||||
desc: 'Ultra-fast inference for open-source models',
|
desc: 'Ultra-fast inference for open-source models',
|
||||||
models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'],
|
models: [
|
||||||
|
'llama-3.3-70b-versatile',
|
||||||
|
'llama-3.1-8b-instant',
|
||||||
|
'mixtral-8x7b-32768',
|
||||||
|
],
|
||||||
keyUrl: 'https://console.groq.com/keys',
|
keyUrl: 'https://console.groq.com/keys',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1083,7 +1096,10 @@ Always cite your sources and be transparent about limitations.`}
|
||||||
keyUrl: 'https://console.mistral.ai/api-keys',
|
keyUrl: 'https://console.mistral.ai/api-keys',
|
||||||
},
|
},
|
||||||
].map((provider) => (
|
].map((provider) => (
|
||||||
<div key={provider.name} className="p-4 border border-border rounded-lg bg-surface">
|
<div
|
||||||
|
key={provider.name}
|
||||||
|
className="p-4 border border-border rounded-lg bg-surface"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h4 className="font-semibold text-foreground">{provider.name}</h4>
|
<h4 className="font-semibold text-foreground">{provider.name}</h4>
|
||||||
<a
|
<a
|
||||||
|
|
@ -1272,23 +1288,18 @@ Always cite your sources and be transparent about limitations.`}
|
||||||
|
|
||||||
<DocSection id="viewing-parent-updates" title="Viewing Parent Updates">
|
<DocSection id="viewing-parent-updates" title="Viewing Parent Updates">
|
||||||
<p className="text-foreground-secondary mb-6">
|
<p className="text-foreground-secondary mb-6">
|
||||||
After forking, you can check what the original parent has changed by visiting
|
After forking, you can check what the original parent has changed by visiting its
|
||||||
its page. This lets you see new tools or collections the parent author has added.
|
page. This lets you see new tools or collections the parent author has added.
|
||||||
</p>
|
</p>
|
||||||
<DocSubSection title="How to View Parent Changes">
|
<DocSubSection title="How to View Parent Changes">
|
||||||
<div className="space-y-3 text-foreground-secondary">
|
<div className="space-y-3 text-foreground-secondary">
|
||||||
|
<p>1. Go to your forked agent or collection in the dashboard</p>
|
||||||
|
<p>2. Click the "Forked from" badge to navigate to the parent</p>
|
||||||
<p>
|
<p>
|
||||||
1. Go to your forked agent or collection in the dashboard
|
3. The parent's page shows its <strong>current</strong> tools and
|
||||||
</p>
|
collections
|
||||||
<p>
|
|
||||||
2. Click the "Forked from" badge to navigate to the parent
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
3. The parent's page shows its <strong>current</strong> tools and collections
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
4. Compare with your fork to see what's different
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>4. Compare with your fork to see what's different</p>
|
||||||
</div>
|
</div>
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
<DocSubSection title="What You Can See">
|
<DocSubSection title="What You Can See">
|
||||||
|
|
@ -1324,18 +1335,10 @@ Always cite your sources and be transparent about limitations.`}
|
||||||
the parent. To incorporate parent changes:
|
the parent. To incorporate parent changes:
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-3 text-foreground-secondary">
|
<div className="space-y-3 text-foreground-secondary">
|
||||||
<p>
|
<p>1. View the parent's current tools/collections</p>
|
||||||
1. View the parent's current tools/collections
|
<p>2. Go to your fork's edit page</p>
|
||||||
</p>
|
<p>3. Manually add or remove tools to match (or improve upon) the parent</p>
|
||||||
<p>
|
<p>4. Update settings as needed</p>
|
||||||
2. Go to your fork's edit page
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
3. Manually add or remove tools to match (or improve upon) the parent
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
4. Update settings as needed
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 p-4 border border-border rounded-lg bg-surface-elevated">
|
<div className="mt-4 p-4 border border-border rounded-lg bg-surface-elevated">
|
||||||
<p className="text-sm text-foreground-secondary">
|
<p className="text-sm text-foreground-secondary">
|
||||||
|
|
@ -1447,9 +1450,9 @@ curl "https://tpmjs.com/api/public/users/{username}/agents/{uid}"
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
<div className="p-4 border border-warning/30 rounded-lg bg-warning/5 mt-4">
|
<div className="p-4 border border-warning/30 rounded-lg bg-warning/5 mt-4">
|
||||||
<p className="text-sm text-foreground-secondary">
|
<p className="text-sm text-foreground-secondary">
|
||||||
<strong className="text-warning">Important:</strong> Your API key is displayed only
|
<strong className="text-warning">Important:</strong> Your API key is displayed
|
||||||
once when created. Store it securely. If you lose it, you'll need to generate
|
only once when created. Store it securely. If you lose it, you'll need to
|
||||||
a new one.
|
generate a new one.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DocSection>
|
</DocSection>
|
||||||
|
|
@ -1508,7 +1511,10 @@ curl "https://tpmjs.com/api/public/users/{username}/agents/{uid}"
|
||||||
<p className="text-foreground-secondary mb-4">
|
<p className="text-foreground-secondary mb-4">
|
||||||
Include your API key in the Authorization header:
|
Include your API key in the Authorization header:
|
||||||
</p>
|
</p>
|
||||||
<CodeBlock language="bash" code="Authorization: Bearer tpmjs_sk_your_api_key_here" />
|
<CodeBlock
|
||||||
|
language="bash"
|
||||||
|
code="Authorization: Bearer tpmjs_sk_your_api_key_here"
|
||||||
|
/>
|
||||||
<p className="text-foreground-secondary mt-4">Example request:</p>
|
<p className="text-foreground-secondary mt-4">Example request:</p>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
language="bash"
|
language="bash"
|
||||||
|
|
@ -1645,8 +1651,8 @@ X-RateLimit-Reset: 1704067200`}
|
||||||
<DocSection id="access-model" title="Access Model: Public Access with Your Credentials">
|
<DocSection id="access-model" title="Access Model: Public Access with Your Credentials">
|
||||||
<p className="text-foreground-secondary mb-6">
|
<p className="text-foreground-secondary mb-6">
|
||||||
You can access public agents and collections using your own API key—but you must
|
You can access public agents and collections using your own API key—but you must
|
||||||
provide <strong className="text-foreground">all required credentials</strong>{' '}
|
provide <strong className="text-foreground">all required credentials</strong> (LLM
|
||||||
(LLM keys, tool environment variables) in the request. The owner's stored
|
keys, tool environment variables) in the request. The owner's stored
|
||||||
credentials are never shared.
|
credentials are never shared.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
|
|
|
||||||
|
|
@ -180,9 +180,7 @@ export default function CustomExecutorTutorialPage(): React.ReactElement {
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<Icon icon="alertCircle" className="w-5 h-5 text-warning flex-shrink-0 mt-0.5" />
|
<Icon icon="alertCircle" className="w-5 h-5 text-warning flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-warning">
|
<p className="font-medium text-warning">Security Recommendation</p>
|
||||||
Security Recommendation
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-foreground-secondary mt-1">
|
<p className="text-sm text-foreground-secondary mt-1">
|
||||||
Set <code className="px-1 bg-warning/20 rounded">EXECUTOR_API_KEY</code> to
|
Set <code className="px-1 bg-warning/20 rounded">EXECUTOR_API_KEY</code> to
|
||||||
require authentication. Without it, anyone with your executor URL can execute
|
require authentication. Without it, anyone with your executor URL can execute
|
||||||
|
|
|
||||||
|
|
@ -226,10 +226,14 @@
|
||||||
|
|
||||||
--shadow-none: none;
|
--shadow-none: none;
|
||||||
--shadow-xs: 0 1px 2px 0 hsl(var(--foreground) / 0.05);
|
--shadow-xs: 0 1px 2px 0 hsl(var(--foreground) / 0.05);
|
||||||
--shadow-sm: 0 1px 3px 0 hsl(var(--foreground) / 0.08), 0 1px 2px -1px hsl(var(--foreground) / 0.08);
|
--shadow-sm:
|
||||||
--shadow-md: 0 4px 6px -1px hsl(var(--foreground) / 0.08), 0 2px 4px -2px hsl(var(--foreground) / 0.08);
|
0 1px 3px 0 hsl(var(--foreground) / 0.08), 0 1px 2px -1px hsl(var(--foreground) / 0.08);
|
||||||
--shadow-lg: 0 10px 15px -3px hsl(var(--foreground) / 0.08), 0 4px 6px -4px hsl(var(--foreground) / 0.08);
|
--shadow-md:
|
||||||
--shadow-xl: 0 20px 25px -5px hsl(var(--foreground) / 0.08), 0 8px 10px -6px hsl(var(--foreground) / 0.08);
|
0 4px 6px -1px hsl(var(--foreground) / 0.08), 0 2px 4px -2px hsl(var(--foreground) / 0.08);
|
||||||
|
--shadow-lg:
|
||||||
|
0 10px 15px -3px hsl(var(--foreground) / 0.08), 0 4px 6px -4px hsl(var(--foreground) / 0.08);
|
||||||
|
--shadow-xl:
|
||||||
|
0 20px 25px -5px hsl(var(--foreground) / 0.08), 0 8px 10px -6px hsl(var(--foreground) / 0.08);
|
||||||
--shadow-2xl: 0 25px 50px -12px hsl(var(--foreground) / 0.15);
|
--shadow-2xl: 0 25px 50px -12px hsl(var(--foreground) / 0.15);
|
||||||
--shadow-inner: inset 0 2px 4px 0 hsl(var(--foreground) / 0.05);
|
--shadow-inner: inset 0 2px 4px 0 hsl(var(--foreground) / 0.05);
|
||||||
|
|
||||||
|
|
@ -745,9 +749,9 @@
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
animation-duration: 0.01ms !important;
|
animation-duration: 0.01ms;
|
||||||
animation-iteration-count: 1 !important;
|
animation-iteration-count: 1;
|
||||||
transition-duration: 0.01ms !important;
|
transition-duration: 0.01ms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,10 +194,7 @@ export default function StatsPage() {
|
||||||
if (error || !stats) {
|
if (error || !stats) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||||
<ErrorState
|
<ErrorState title="Failed to load statistics" message={error || 'Unknown error'} />
|
||||||
title="Failed to load statistics"
|
|
||||||
message={error || 'Unknown error'}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,12 @@ export default function StyleGuidePage(): React.ReactElement {
|
||||||
Design System
|
Design System
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Badge variant="default" size="sm">v2.0</Badge>
|
<Badge variant="default" size="sm">
|
||||||
<Badge variant="outline" size="sm">wcag aa</Badge>
|
v2.0
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" size="sm">
|
||||||
|
wcag aa
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -454,7 +454,9 @@ export default function TermsPage(): React.ReactElement {
|
||||||
<Button size="lg">Contact Us</Button>
|
<Button size="lg">Contact Us</Button>
|
||||||
</a>
|
</a>
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<Button variant="outline" size="lg">Back to Home</Button>
|
<Button variant="outline" size="lg">
|
||||||
|
Back to Home
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,9 @@ export function ToolIdeasClient() {
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 bg-surface p-4 rounded-lg border border-border">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 bg-surface p-4 rounded-lg border border-border">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="tool-search" className="text-xs mb-1.5">Search</Label>
|
<Label htmlFor="tool-search" className="text-xs mb-1.5">
|
||||||
|
Search
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="tool-search"
|
id="tool-search"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -240,7 +242,9 @@ export function ToolIdeasClient() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="tool-category" className="text-xs mb-1.5">Category</Label>
|
<Label htmlFor="tool-category" className="text-xs mb-1.5">
|
||||||
|
Category
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
id="tool-category"
|
id="tool-category"
|
||||||
value={category}
|
value={category}
|
||||||
|
|
@ -251,7 +255,9 @@ export function ToolIdeasClient() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="tool-verb" className="text-xs mb-1.5">Verb</Label>
|
<Label htmlFor="tool-verb" className="text-xs mb-1.5">
|
||||||
|
Verb
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
id="tool-verb"
|
id="tool-verb"
|
||||||
value={verb}
|
value={verb}
|
||||||
|
|
@ -262,7 +268,9 @@ export function ToolIdeasClient() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="tool-quality" className="text-xs mb-1.5">Min Quality</Label>
|
<Label htmlFor="tool-quality" className="text-xs mb-1.5">
|
||||||
|
Min Quality
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
id="tool-quality"
|
id="tool-quality"
|
||||||
value={minQuality}
|
value={minQuality}
|
||||||
|
|
|
||||||
|
|
@ -273,9 +273,7 @@ export function ToolDetailClient({ tool, slug }: ToolDetailClientProps): React.R
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<span className="text-xl mt-0.5">🔍</span>
|
<span className="text-xl mt-0.5">🔍</span>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="text-sm font-semibold text-warning mb-1">
|
<h3 className="text-sm font-semibold text-warning mb-1">Auto-discovered tool</h3>
|
||||||
Auto-discovered tool
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-warning/80">
|
<p className="text-sm text-warning/80">
|
||||||
This tool was automatically discovered from the package exports. The author did
|
This tool was automatically discovered from the package exports. The author did
|
||||||
not explicitly register it in their{' '}
|
not explicitly register it in their{' '}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ import { AppHeader } from '~/components/AppHeader';
|
||||||
import { CopyButton } from '~/components/CopyButton';
|
import { CopyButton } from '~/components/CopyButton';
|
||||||
import { LikeButton } from '~/components/LikeButton';
|
import { LikeButton } from '~/components/LikeButton';
|
||||||
import {
|
import {
|
||||||
PackageManagerSelector,
|
|
||||||
getInstallCommand,
|
getInstallCommand,
|
||||||
|
PackageManagerSelector,
|
||||||
usePackageManager,
|
usePackageManager,
|
||||||
} from '~/components/PackageManagerSelector';
|
} from '~/components/PackageManagerSelector';
|
||||||
import { type Tool, useTools } from '~/hooks/useTools';
|
import { type Tool, useTools } from '~/hooks/useTools';
|
||||||
|
|
@ -81,7 +81,11 @@ export default function ToolSearchPage(): React.ReactElement {
|
||||||
const [packageManager, setPackageManager] = usePackageManager();
|
const [packageManager, setPackageManager] = usePackageManager();
|
||||||
|
|
||||||
// Fetch tools from API using SWR
|
// Fetch tools from API using SWR
|
||||||
const { data: tools = [], isLoading: loading, error: swrError } = useTools({
|
const {
|
||||||
|
data: tools = [],
|
||||||
|
isLoading: loading,
|
||||||
|
error: swrError,
|
||||||
|
} = useTools({
|
||||||
category: categoryFilter !== 'all' ? categoryFilter : undefined,
|
category: categoryFilter !== 'all' ? categoryFilter : undefined,
|
||||||
importHealth: healthFilter === 'healthy' ? 'HEALTHY' : undefined,
|
importHealth: healthFilter === 'healthy' ? 'HEALTHY' : undefined,
|
||||||
executionHealth: healthFilter === 'healthy' ? 'HEALTHY' : undefined,
|
executionHealth: healthFilter === 'healthy' ? 'HEALTHY' : undefined,
|
||||||
|
|
@ -143,10 +147,7 @@ export default function ToolSearchPage(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<Link
|
<Link href={`/tool/${tool.package.npmPackageName}/${tool.name}`} className="block">
|
||||||
href={`/tool/${tool.package.npmPackageName}/${tool.name}`}
|
|
||||||
className="block"
|
|
||||||
>
|
|
||||||
<div className="font-semibold text-foreground group-hover:text-primary transition-colors">
|
<div className="font-semibold text-foreground group-hover:text-primary transition-colors">
|
||||||
{displayName}
|
{displayName}
|
||||||
{isBroken && (
|
{isBroken && (
|
||||||
|
|
@ -310,7 +311,9 @@ export default function ToolSearchPage(): React.ReactElement {
|
||||||
style={{ tableLayout: 'fixed' }}
|
style={{ tableLayout: 'fixed' }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
TableHead: (props) => <thead {...props} className="bg-surface-secondary sticky top-0 z-10" />,
|
TableHead: (props) => (
|
||||||
|
<thead {...props} className="bg-surface-secondary sticky top-0 z-10" />
|
||||||
|
),
|
||||||
TableBody: (props) => <tbody {...props} />,
|
TableBody: (props) => <tbody {...props} />,
|
||||||
TableRow: (props) => (
|
TableRow: (props) => (
|
||||||
<tr
|
<tr
|
||||||
|
|
|
||||||
104
apps/web/src/app/use-cases/[slug]/page.tsx
Normal file
104
apps/web/src/app/use-cases/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
/**
|
||||||
|
* Individual Use Case Page
|
||||||
|
*
|
||||||
|
* SEO-optimized case study page with hero, story, technical proof, and CTA
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
import UseCaseCaseStudy from '~/components/UseCaseCaseStudy';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: Props) {
|
||||||
|
const { slug } = await params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.BETTER_AUTH_URL || 'http://localhost:3000'}/api/use-cases/${slug}`,
|
||||||
|
{
|
||||||
|
cache: 'no-store',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
title: 'Use Case Not Found - TPMJS',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${data.marketingTitle} - TPMJS Use Case`,
|
||||||
|
description: data.marketingDesc,
|
||||||
|
openGraph: {
|
||||||
|
title: data.marketingTitle,
|
||||||
|
description: data.marketingDesc,
|
||||||
|
type: 'article',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
title: 'Use Case - TPMJS',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function UseCasePage({ params }: Props) {
|
||||||
|
const { slug } = await params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.BETTER_AUTH_URL || 'http://localhost:3000'}/api/use-cases/${slug}`,
|
||||||
|
{
|
||||||
|
cache: 'no-store',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await response.json();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<UseCaseSkeleton />}>
|
||||||
|
<UseCaseCaseStudy useCase={data} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function UseCaseSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* Hero skeleton */}
|
||||||
|
<div className="border-b bg-card/50 backdrop-blur">
|
||||||
|
<div className="container mx-auto px-4 py-12">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
<div className="h-8 w-32 animate-pulse rounded bg-muted" />
|
||||||
|
<div className="mt-4 h-12 w-3/4 animate-pulse rounded bg-muted" />
|
||||||
|
<div className="mt-4 h-24 w-full animate-pulse rounded bg-muted" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content skeleton */}
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="mx-auto max-w-3xl space-y-8">
|
||||||
|
<div className="h-64 animate-pulse rounded-lg bg-muted" />
|
||||||
|
<div className="h-64 animate-pulse rounded-lg bg-muted" />
|
||||||
|
<div className="h-64 animate-pulse rounded-lg bg-muted" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
apps/web/src/app/use-cases/page.tsx
Normal file
67
apps/web/src/app/use-cases/page.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Use Cases Directory Page
|
||||||
|
*
|
||||||
|
* Global browseable/searchable feed of all use cases with persona-based filtering
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
import UseCasesFeed from '~/components/UseCasesFeed';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'TPMJS Use Cases - Real AI Workflows That Work',
|
||||||
|
description:
|
||||||
|
'Explore 1000+ proven use cases for TPMJS tools. Filter by persona, industry, or category to find workflows that deliver real business value.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function UseCasesPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="border-b bg-card/50 backdrop-blur">
|
||||||
|
<div className="container mx-auto px-4 py-8 md:py-12">
|
||||||
|
<div className="mx-auto max-w-3xl text-center">
|
||||||
|
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||||
|
TPMJS Use Cases
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 text-lg text-muted-foreground md:text-xl">
|
||||||
|
Real AI workflows that work. Filter by persona, industry, or category to find proven
|
||||||
|
solutions for your business.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<Suspense fallback={<UseCasesFeedSkeleton />}>
|
||||||
|
<UseCasesFeed />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UseCasesFeedSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Filter skeleton */}
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
<div className="h-10 w-48 animate-pulse rounded-lg bg-muted" />
|
||||||
|
<div className="h-10 w-48 animate-pulse rounded-lg bg-muted" />
|
||||||
|
<div className="h-10 w-48 animate-pulse rounded-lg bg-muted" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table skeleton */}
|
||||||
|
<div className="rounded-lg border">
|
||||||
|
<div className="h-12 animate-pulse bg-muted/50" />
|
||||||
|
{Array.from({ length: 10 }).map((_, i) => (
|
||||||
|
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton loading state
|
||||||
|
<div key={i} className="h-16 animate-pulse border-t bg-muted/30" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSession } from '@/lib/auth-client';
|
|
||||||
import { Button } from '@tpmjs/ui/Button/Button';
|
import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useSession } from '@/lib/auth-client';
|
||||||
import { useLikeStatus } from '~/hooks/useLikeStatus';
|
import { useLikeStatus } from '~/hooks/useLikeStatus';
|
||||||
|
|
||||||
export type LikeEntityType = 'tool' | 'collection' | 'agent';
|
export type LikeEntityType = 'tool' | 'collection' | 'agent';
|
||||||
|
|
@ -36,12 +36,10 @@ export function LikeButton({
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
// Use SWR for like status with optimistic updates
|
// Use SWR for like status with optimistic updates
|
||||||
const { data, toggleLike } = useLikeStatus(
|
const { data, toggleLike } = useLikeStatus(entityType, entityId, !!session, {
|
||||||
entityType,
|
liked: initialLiked,
|
||||||
entityId,
|
likeCount: initialCount,
|
||||||
!!session,
|
});
|
||||||
{ liked: initialLiked, likeCount: initialCount }
|
|
||||||
);
|
|
||||||
|
|
||||||
const liked = data?.liked ?? initialLiked;
|
const liked = data?.liked ?? initialLiked;
|
||||||
const count = data?.likeCount ?? initialCount;
|
const count = data?.likeCount ?? initialCount;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
* Interactive playground for executing TPMJS tools with AI agents
|
* Interactive playground for executing TPMJS tools with AI agents
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { TokenBreakdown as TokenData } from '@/lib/ai-agent/tool-executor-agent';
|
|
||||||
import type { Package, Tool } from '@tpmjs/db';
|
import type { Package, Tool } from '@tpmjs/db';
|
||||||
import { Button } from '@tpmjs/ui/Button/Button';
|
import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import { Label } from '@tpmjs/ui/Label/Label';
|
import { Label } from '@tpmjs/ui/Label/Label';
|
||||||
|
|
@ -14,6 +13,7 @@ import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import type { TokenBreakdown as TokenData } from '@/lib/ai-agent/tool-executor-agent';
|
||||||
import { TokenBreakdown } from './TokenBreakdown';
|
import { TokenBreakdown } from './TokenBreakdown';
|
||||||
|
|
||||||
interface ToolPlaygroundProps {
|
interface ToolPlaygroundProps {
|
||||||
|
|
@ -240,7 +240,9 @@ export function ToolPlayground({ tool }: ToolPlaygroundProps): React.ReactElemen
|
||||||
{activeTab === 'input' && (
|
{activeTab === 'input' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="prompt" className="mb-2">Prompt</Label>
|
<Label htmlFor="prompt" className="mb-2">
|
||||||
|
Prompt
|
||||||
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="prompt"
|
id="prompt"
|
||||||
value={prompt}
|
value={prompt}
|
||||||
|
|
|
||||||
319
apps/web/src/components/UseCaseCaseStudy.tsx
Normal file
319
apps/web/src/components/UseCaseCaseStudy.tsx
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UseCaseCaseStudy Component
|
||||||
|
*
|
||||||
|
* Hero, story, technical proof, related use cases
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface UseCase {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
marketingTitle: string;
|
||||||
|
marketingDesc: string;
|
||||||
|
roiEstimate: string | null;
|
||||||
|
businessValue: string | null;
|
||||||
|
problemStatement: string | null;
|
||||||
|
solutionNarrative: string | null;
|
||||||
|
rankScore: number;
|
||||||
|
createdAt: Date;
|
||||||
|
lastRegeneratedAt: Date | null;
|
||||||
|
personas: Array<{
|
||||||
|
persona: {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
icon: string | null;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
industries: Array<{
|
||||||
|
industry: {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
categories: Array<{
|
||||||
|
category: {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
description: string | null;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
socialProof: {
|
||||||
|
qualityScore: number;
|
||||||
|
totalRuns: number;
|
||||||
|
consecutivePasses: number;
|
||||||
|
lastRunStatus: string | null;
|
||||||
|
lastRunAt: Date | null;
|
||||||
|
successRate: number | null;
|
||||||
|
lastRunAgo: string | null;
|
||||||
|
} | null;
|
||||||
|
scenario: {
|
||||||
|
id: string;
|
||||||
|
prompt: string;
|
||||||
|
name: string | null;
|
||||||
|
description: string | null;
|
||||||
|
tags: string[];
|
||||||
|
qualityScore: number;
|
||||||
|
totalRuns: number;
|
||||||
|
lastRunStatus: string | null;
|
||||||
|
collection: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string | null;
|
||||||
|
user: {
|
||||||
|
username: string | null;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseCaseCaseStudyProps {
|
||||||
|
useCase: UseCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: case study component
|
||||||
|
export default function UseCaseCaseStudy({ useCase }: UseCaseCaseStudyProps) {
|
||||||
|
const qualityPercent = useCase.socialProof
|
||||||
|
? Math.round(useCase.socialProof.qualityScore * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const successRate = useCase.socialProof?.successRate
|
||||||
|
? Math.round(useCase.socialProof.successRate * 100)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="border-b bg-card/50 backdrop-blur">
|
||||||
|
<div className="container mx-auto px-4 py-12">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
{useCase.scenario.collection && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<a
|
||||||
|
href={`/${useCase.scenario.collection.user.username}/collections/${useCase.scenario.collection.slug}/use-cases`}
|
||||||
|
className="hover:text-foreground"
|
||||||
|
>
|
||||||
|
{useCase.scenario.collection.name}
|
||||||
|
</a>
|
||||||
|
{' / '}Use Case
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h1 className="mt-4 text-4xl font-bold tracking-tight sm:text-5xl">
|
||||||
|
{useCase.marketingTitle}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="mt-4 text-xl text-muted-foreground">{useCase.marketingDesc}</p>
|
||||||
|
|
||||||
|
{/* Key stats */}
|
||||||
|
<div className="mt-8 flex flex-wrap gap-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Quality Score</p>
|
||||||
|
<p className="text-2xl font-bold text-green-600">{qualityPercent}%</p>
|
||||||
|
</div>
|
||||||
|
{useCase.socialProof && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Total Runs</p>
|
||||||
|
<p className="text-2xl font-bold">{useCase.socialProof.totalRuns}</p>
|
||||||
|
</div>
|
||||||
|
{successRate !== null && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Success Rate</p>
|
||||||
|
<p className="text-2xl font-bold">{successRate}%</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{useCase.roiEstimate && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">ROI Estimate</p>
|
||||||
|
<p className="text-2xl font-bold">{useCase.roiEstimate}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<a
|
||||||
|
href={`/playground?scenarioId=${useCase.scenario.id}`}
|
||||||
|
className="inline-flex items-center justify-center rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
Try This Use Case
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="container mx-auto px-4 py-12">
|
||||||
|
<div className="mx-auto max-w-3xl space-y-12">
|
||||||
|
{/* Problem Statement */}
|
||||||
|
{useCase.problemStatement && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold">The Problem</h2>
|
||||||
|
<p className="mt-4 text-lg leading-relaxed text-muted-foreground">
|
||||||
|
{useCase.problemStatement}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Solution Narrative */}
|
||||||
|
{useCase.solutionNarrative && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold">The Solution</h2>
|
||||||
|
<p className="mt-4 text-lg leading-relaxed text-muted-foreground">
|
||||||
|
{useCase.solutionNarrative}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Business Value */}
|
||||||
|
{useCase.businessValue && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold">Business Value</h2>
|
||||||
|
<p className="mt-4 text-lg leading-relaxed text-muted-foreground">
|
||||||
|
{useCase.businessValue}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Technical Proof */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold">Technical Proof</h2>
|
||||||
|
|
||||||
|
<div className="mt-4 rounded-lg border bg-card p-6">
|
||||||
|
{/* Scenario prompt */}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">Scenario Prompt</h3>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">{useCase.scenario.prompt}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quality metrics */}
|
||||||
|
<div className="mt-6 grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Quality</p>
|
||||||
|
<p className="text-lg font-semibold">
|
||||||
|
{Math.round(useCase.scenario.qualityScore * 100)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Runs</p>
|
||||||
|
<p className="text-lg font-semibold">{useCase.scenario.totalRuns}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Status</p>
|
||||||
|
<p
|
||||||
|
className={`text-lg font-semibold ${
|
||||||
|
useCase.scenario.lastRunStatus === 'pass' ? 'text-green-600' : 'text-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{useCase.scenario.lastRunStatus === 'pass' ? 'Passing' : 'Failing'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Last Run</p>
|
||||||
|
<p className="text-lg font-semibold">
|
||||||
|
{useCase.socialProof?.lastRunAgo || 'Never'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tools used */}
|
||||||
|
{useCase.scenario.collection && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<h3 className="font-semibold">Collection</h3>
|
||||||
|
<a
|
||||||
|
href={`/${useCase.scenario.collection.user.username}/collections/${useCase.scenario.collection.slug}`}
|
||||||
|
className="mt-2 block text-sm text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{useCase.scenario.collection.name}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
{useCase.scenario.tags.length > 0 && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold">Tags</h2>
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
|
{useCase.scenario.tags.map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="inline-flex items-center rounded-full bg-muted px-3 py-1 text-sm"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Personas */}
|
||||||
|
{useCase.personas.length > 0 && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold">For These Roles</h2>
|
||||||
|
<div className="mt-4 grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||||
|
{useCase.personas.map((p) => (
|
||||||
|
<div key={p.persona.slug} className="rounded-lg border bg-card p-4">
|
||||||
|
<p className="font-semibold">
|
||||||
|
{p.persona.icon && <span className="mr-2">{p.persona.icon}</span>}
|
||||||
|
{p.persona.name}
|
||||||
|
</p>
|
||||||
|
{p.persona.description && (
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">{p.persona.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Industries */}
|
||||||
|
{useCase.industries.length > 0 && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold">Industries</h2>
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
|
{useCase.industries.map((ind) => (
|
||||||
|
<span
|
||||||
|
key={ind.industry.slug}
|
||||||
|
className="inline-flex items-center rounded-full bg-blue-100 px-3 py-1 text-sm font-medium text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
{ind.industry.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Categories */}
|
||||||
|
{useCase.categories.length > 0 && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold">Categories</h2>
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
{useCase.categories.map((cat) => (
|
||||||
|
<span
|
||||||
|
key={cat.category.slug}
|
||||||
|
className="inline-flex items-center rounded-full bg-purple-100 px-3 py-1 text-sm font-medium text-purple-800 dark:bg-purple-900/30 dark:text-purple-400 mr-2"
|
||||||
|
>
|
||||||
|
{cat.category.name}
|
||||||
|
<span className="ml-1 text-xs opacity-70">({cat.category.type})</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
273
apps/web/src/components/UseCasesFeed.tsx
Normal file
273
apps/web/src/components/UseCasesFeed.tsx
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UseCasesFeed Component
|
||||||
|
*
|
||||||
|
* Table + search + persona dropdown for browsing use cases
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
interface UseCase {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
marketingTitle: string;
|
||||||
|
marketingDesc: string;
|
||||||
|
roiEstimate: string | null;
|
||||||
|
businessValue: string | null;
|
||||||
|
rankScore: number;
|
||||||
|
personas: Array<{ persona: { slug: string; name: string } }>;
|
||||||
|
industries: Array<{ industry: { slug: string; name: string } }>;
|
||||||
|
categories: Array<{ category: { slug: string; name: string; type: string } }>;
|
||||||
|
socialProof: {
|
||||||
|
qualityScore: number;
|
||||||
|
totalRuns: number;
|
||||||
|
consecutivePasses: number;
|
||||||
|
lastRunStatus: string | null;
|
||||||
|
successRate: number | null;
|
||||||
|
lastRunAgo: string | null;
|
||||||
|
} | null;
|
||||||
|
scenario: {
|
||||||
|
id: string;
|
||||||
|
collection: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string | null;
|
||||||
|
user: {
|
||||||
|
username: string | null;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseCasesFeedProps {
|
||||||
|
collectionId?: string;
|
||||||
|
initialPersona?: string;
|
||||||
|
initialSearch?: string;
|
||||||
|
initialSort?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERSONAS = [
|
||||||
|
{ slug: 'cto', name: 'CTO' },
|
||||||
|
{ slug: 'product-manager', name: 'Product Manager' },
|
||||||
|
{ slug: 'developer', name: 'Developer' },
|
||||||
|
{ slug: 'founder', name: 'Founder' },
|
||||||
|
{ slug: 'sales-lead', name: 'Sales Lead' },
|
||||||
|
{ slug: 'support-lead', name: 'Support Lead' },
|
||||||
|
{ slug: 'data-analyst', name: 'Data Analyst' },
|
||||||
|
{ slug: 'marketing-manager', name: 'Marketing Manager' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SORT_OPTIONS = [
|
||||||
|
{ value: 'rank', label: 'Relevance' },
|
||||||
|
{ value: 'quality', label: 'Quality Score' },
|
||||||
|
{ value: 'runs', label: 'Most Runs' },
|
||||||
|
{ value: 'recent', label: 'Recently Added' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function UseCasesFeed({
|
||||||
|
collectionId,
|
||||||
|
initialPersona = '',
|
||||||
|
initialSearch = '',
|
||||||
|
initialSort = 'rank',
|
||||||
|
}: UseCasesFeedProps) {
|
||||||
|
const [persona, setPersona] = useState(initialPersona);
|
||||||
|
const [searchQuery, setSearchQuery] = useState(initialSearch);
|
||||||
|
const [sort, setSort] = useState(initialSort);
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
|
||||||
|
// Build API URL
|
||||||
|
const apiUrl = collectionId
|
||||||
|
? `/api/public/users/${collectionId.split('-')[0]}/collections/${collectionId}/use-cases`
|
||||||
|
: '/api/use-cases';
|
||||||
|
|
||||||
|
// Fetch use cases
|
||||||
|
const fetchUrl = `${apiUrl}?persona=${persona}&search=${searchQuery}&sort=${sort}&limit=20&offset=${page * 20}`;
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useSWR(fetchUrl, fetcher, {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const useCases: UseCase[] = data?.data?.useCases || [];
|
||||||
|
const pagination = data?.data?.pagination || { total: 0, hasMore: false };
|
||||||
|
|
||||||
|
function fetcher(url: string) {
|
||||||
|
return fetch(url).then((res) => res.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset page on filter change
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="flex-1 min-w-[200px]">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search use cases..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 rounded-lg border bg-background"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Persona dropdown */}
|
||||||
|
<select
|
||||||
|
value={persona}
|
||||||
|
onChange={(e) => setPersona(e.target.value)}
|
||||||
|
className="px-4 py-2 rounded-lg border bg-background"
|
||||||
|
>
|
||||||
|
<option value="">All Personas</option>
|
||||||
|
{PERSONAS.map((p) => (
|
||||||
|
<option key={p.slug} value={p.slug}>
|
||||||
|
{p.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Sort dropdown */}
|
||||||
|
<select
|
||||||
|
value={sort}
|
||||||
|
onChange={(e) => setSort(e.target.value)}
|
||||||
|
className="px-4 py-2 rounded-lg border bg-background"
|
||||||
|
>
|
||||||
|
{SORT_OPTIONS.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results count */}
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{isLoading ? 'Loading...' : `${pagination.total} use cases found`}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
{error ? (
|
||||||
|
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-8 text-center">
|
||||||
|
<p className="text-destructive">Failed to load use cases</p>
|
||||||
|
</div>
|
||||||
|
) : useCases.length === 0 && !isLoading ? (
|
||||||
|
<div className="rounded-lg border p-8 text-center">
|
||||||
|
<p className="text-muted-foreground">No use cases found</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto rounded-lg border">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-muted/50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium">Use Case</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium">Quality</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium">Runs</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium hidden sm:table-cell">
|
||||||
|
Personas
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium hidden md:table-cell">
|
||||||
|
Collection
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{isLoading
|
||||||
|
? Array.from({ length: 10 }).map((_, i) => (
|
||||||
|
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton loading state
|
||||||
|
<tr key={i} className="border-t">
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="h-4 w-48 animate-pulse rounded bg-muted" />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="h-4 w-16 animate-pulse rounded bg-muted" />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="h-4 w-16 animate-pulse rounded bg-muted" />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 hidden sm:table-cell">
|
||||||
|
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 hidden md:table-cell">
|
||||||
|
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
: useCases.map((useCase) => (
|
||||||
|
<tr key={useCase.id} className="border-t hover:bg-muted/50">
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<a
|
||||||
|
href={`/use-cases/${useCase.slug}`}
|
||||||
|
className="font-medium hover:underline"
|
||||||
|
>
|
||||||
|
{useCase.marketingTitle}
|
||||||
|
</a>
|
||||||
|
{useCase.roiEstimate && (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{useCase.roiEstimate}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className="inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
||||||
|
{useCase.socialProof
|
||||||
|
? `${Math.round(useCase.socialProof.qualityScore * 100)}%`
|
||||||
|
: 'N/A'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm">{useCase.socialProof?.totalRuns || 0}</td>
|
||||||
|
<td className="px-4 py-3 hidden sm:table-cell">
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{useCase.personas.slice(0, 2).map((p) => (
|
||||||
|
<span
|
||||||
|
key={p.persona.slug}
|
||||||
|
className="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
{p.persona.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{useCase.personas.length > 2 && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
+{useCase.personas.length - 2}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 hidden md:table-cell">
|
||||||
|
{useCase.scenario.collection ? (
|
||||||
|
<a
|
||||||
|
href={`/${useCase.scenario.collection.user.username}/collections/${useCase.scenario.collection.slug}`}
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
{useCase.scenario.collection.name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground">Unknown</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{pagination.hasMore && (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPage(page + 1)}
|
||||||
|
className="px-4 py-2 rounded-lg border bg-background hover:bg-muted"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Loading...' : 'Load More'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -61,7 +61,8 @@ export function ChatToolsPanel({
|
||||||
const [expandedCollections, setExpandedCollections] = useState<Set<string>>(new Set());
|
const [expandedCollections, setExpandedCollections] = useState<Set<string>>(new Set());
|
||||||
const [collectionTools, setCollectionTools] = useState<Record<string, CollectionToolData>>({});
|
const [collectionTools, setCollectionTools] = useState<Record<string, CollectionToolData>>({});
|
||||||
|
|
||||||
const fetchCollectionTools = useCallback(async (collectionId: string) => {
|
const fetchCollectionTools = useCallback(
|
||||||
|
async (collectionId: string) => {
|
||||||
const existingData = collectionTools[collectionId];
|
const existingData = collectionTools[collectionId];
|
||||||
if (existingData?.tools && existingData.tools.length > 0) return;
|
if (existingData?.tools && existingData.tools.length > 0) return;
|
||||||
|
|
||||||
|
|
@ -99,7 +100,9 @@ export function ChatToolsPanel({
|
||||||
[collectionId]: { tools: [], loading: false, error: 'Failed to load tools' },
|
[collectionId]: { tools: [], loading: false, error: 'Failed to load tools' },
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [collectionTools]);
|
},
|
||||||
|
[collectionTools]
|
||||||
|
);
|
||||||
|
|
||||||
const toggleCollection = (collectionId: string) => {
|
const toggleCollection = (collectionId: string) => {
|
||||||
setExpandedCollections((prev) => {
|
setExpandedCollections((prev) => {
|
||||||
|
|
@ -117,10 +120,7 @@ export function ChatToolsPanel({
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const totalToolsFromCollections = collections.reduce(
|
const totalToolsFromCollections = collections.reduce((acc, c) => acc + c.collection.toolCount, 0);
|
||||||
(acc, c) => acc + c.collection.toolCount,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-72 border-l border-dashed border-border flex flex-col bg-surface">
|
<div className="w-72 border-l border-dashed border-border flex flex-col bg-surface">
|
||||||
|
|
@ -213,13 +213,15 @@ export function ChatToolsPanel({
|
||||||
<div className="border-t border-dashed border-border/50">
|
<div className="border-t border-dashed border-border/50">
|
||||||
{toolData?.loading ? (
|
{toolData?.loading ? (
|
||||||
<div className="p-3 flex items-center justify-center gap-2">
|
<div className="p-3 flex items-center justify-center gap-2">
|
||||||
<Icon icon="loader" size="xs" className="animate-spin text-foreground-tertiary" />
|
<Icon
|
||||||
|
icon="loader"
|
||||||
|
size="xs"
|
||||||
|
className="animate-spin text-foreground-tertiary"
|
||||||
|
/>
|
||||||
<span className="text-xs text-foreground-tertiary">loading...</span>
|
<span className="text-xs text-foreground-tertiary">loading...</span>
|
||||||
</div>
|
</div>
|
||||||
) : toolData?.error ? (
|
) : toolData?.error ? (
|
||||||
<div className="p-3 text-xs text-error text-center">
|
<div className="p-3 text-xs text-error text-center">{toolData.error}</div>
|
||||||
{toolData.error}
|
|
||||||
</div>
|
|
||||||
) : toolData?.tools.length === 0 ? (
|
) : toolData?.tools.length === 0 ? (
|
||||||
<div className="p-3 text-xs text-foreground-tertiary text-center">
|
<div className="p-3 text-xs text-foreground-tertiary text-center">
|
||||||
no tools in collection
|
no tools in collection
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,11 @@ export function ToolDetailsModal({ tool, open, onClose }: ToolDetailsModalProps)
|
||||||
|
|
||||||
{/* Optional parameters */}
|
{/* Optional parameters */}
|
||||||
{optionalParams.length > 0 && (
|
{optionalParams.length > 0 && (
|
||||||
<div className={requiredParams.length > 0 ? 'pt-4 border-t border-dashed border-border/50' : ''}>
|
<div
|
||||||
|
className={
|
||||||
|
requiredParams.length > 0 ? 'pt-4 border-t border-dashed border-border/50' : ''
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<div className="w-2 h-2 rounded-full bg-foreground-tertiary" />
|
<div className="w-2 h-2 rounded-full bg-foreground-tertiary" />
|
||||||
<span className="font-mono text-xs text-foreground-secondary uppercase tracking-wide">
|
<span className="font-mono text-xs text-foreground-secondary uppercase tracking-wide">
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { signOut } from '@/lib/auth-client';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { signOut } from '@/lib/auth-client';
|
||||||
|
|
||||||
export function SignOutButton() {
|
export function SignOutButton() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ export function AddToolSearch({
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Debounced search
|
// Debounced search
|
||||||
const search = useCallback(async (searchQuery: string) => {
|
const search = useCallback(
|
||||||
|
async (searchQuery: string) => {
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
setResults([]);
|
setResults([]);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
@ -83,7 +84,9 @@ export function AddToolSearch({
|
||||||
} finally {
|
} finally {
|
||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
}
|
}
|
||||||
}, [existingToolIds]);
|
},
|
||||||
|
[existingToolIds]
|
||||||
|
);
|
||||||
|
|
||||||
// Handle query changes with debounce
|
// Handle query changes with debounce
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -113,9 +113,7 @@ function AnimatedTerminal(): React.ReactElement {
|
||||||
{currentLine < lines.length && (
|
{currentLine < lines.length && (
|
||||||
<div className={`${getLineColor(lines[currentLine]?.type || 'input')} flex`}>
|
<div className={`${getLineColor(lines[currentLine]?.type || 'input')} flex`}>
|
||||||
<span>{displayedText}</span>
|
<span>{displayedText}</span>
|
||||||
{isTyping && (
|
{isTyping && <span className="ml-0.5 w-2 h-5 bg-primary animate-pulse" />}
|
||||||
<span className="ml-0.5 w-2 h-5 bg-primary animate-pulse" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -135,7 +133,13 @@ interface FeatureCardProps {
|
||||||
delay?: number;
|
delay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FeatureCard({ icon, title, description, stats, delay = 0 }: FeatureCardProps): React.ReactElement {
|
function FeatureCard({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
stats,
|
||||||
|
delay = 0,
|
||||||
|
}: FeatureCardProps): React.ReactElement {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -201,9 +205,7 @@ function FeatureCard({ icon, title, description, stats, delay = 0 }: FeatureCard
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<h3 className="font-mono text-lg font-semibold mb-2 text-foreground lowercase">
|
<h3 className="font-mono text-lg font-semibold mb-2 text-foreground lowercase">{title}</h3>
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<p className="font-sans text-sm text-foreground-secondary leading-relaxed mb-4">
|
<p className="font-sans text-sm text-foreground-secondary leading-relaxed mb-4">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -233,7 +235,12 @@ interface AnimatedCounterProps {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AnimatedCounter({ end, duration = 2000, suffix = '', prefix = '' }: AnimatedCounterProps): React.ReactElement {
|
function AnimatedCounter({
|
||||||
|
end,
|
||||||
|
duration = 2000,
|
||||||
|
suffix = '',
|
||||||
|
prefix = '',
|
||||||
|
}: AnimatedCounterProps): React.ReactElement {
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
const [hasStarted, setHasStarted] = useState(false);
|
const [hasStarted, setHasStarted] = useState(false);
|
||||||
const ref = useRef<HTMLSpanElement>(null);
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
|
|
@ -266,7 +273,7 @@ function AnimatedCounter({ end, duration = 2000, suffix = '', prefix = '' }: Ani
|
||||||
const progress = Math.min((timestamp - startTime) / duration, 1);
|
const progress = Math.min((timestamp - startTime) / duration, 1);
|
||||||
|
|
||||||
// Easing function for smooth animation
|
// Easing function for smooth animation
|
||||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
const easeOutQuart = 1 - (1 - progress) ** 4;
|
||||||
setCount(Math.floor(easeOutQuart * end));
|
setCount(Math.floor(easeOutQuart * end));
|
||||||
|
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
|
|
@ -280,7 +287,9 @@ function AnimatedCounter({ end, duration = 2000, suffix = '', prefix = '' }: Ani
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span ref={ref} className="tabular-nums">
|
<span ref={ref} className="tabular-nums">
|
||||||
{prefix}{count.toLocaleString()}{suffix}
|
{prefix}
|
||||||
|
{count.toLocaleString()}
|
||||||
|
{suffix}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -321,7 +330,8 @@ function FlowDiagram(): React.ReactElement {
|
||||||
className={`
|
className={`
|
||||||
w-16 h-16 flex items-center justify-center border-2 mb-3
|
w-16 h-16 flex items-center justify-center border-2 mb-3
|
||||||
transition-all duration-500
|
transition-all duration-500
|
||||||
${activeStep === i
|
${
|
||||||
|
activeStep === i
|
||||||
? 'border-primary bg-primary/10 shadow-[0_0_20px_rgba(166,89,45,0.3)]'
|
? 'border-primary bg-primary/10 shadow-[0_0_20px_rgba(166,89,45,0.3)]'
|
||||||
: 'border-dashed border-border bg-surface'
|
: 'border-dashed border-border bg-surface'
|
||||||
}
|
}
|
||||||
|
|
@ -333,12 +343,12 @@ function FlowDiagram(): React.ReactElement {
|
||||||
className={`transition-colors duration-500 ${activeStep === i ? 'text-primary' : 'text-foreground-tertiary'}`}
|
className={`transition-colors duration-500 ${activeStep === i ? 'text-primary' : 'text-foreground-tertiary'}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className={`font-mono text-sm font-medium transition-colors duration-500 ${activeStep === i ? 'text-primary' : 'text-foreground'}`}>
|
<span
|
||||||
|
className={`font-mono text-sm font-medium transition-colors duration-500 ${activeStep === i ? 'text-primary' : 'text-foreground'}`}
|
||||||
|
>
|
||||||
{step.label}
|
{step.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono text-xs text-foreground-tertiary mt-1">
|
<span className="font-mono text-xs text-foreground-tertiary mt-1">{step.desc}</span>
|
||||||
{step.desc}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Pulse ring when active */}
|
{/* Pulse ring when active */}
|
||||||
{activeStep === i && (
|
{activeStep === i && (
|
||||||
|
|
@ -454,8 +464,8 @@ export function FeaturesSection(): React.ReactElement {
|
||||||
powerful features
|
powerful features
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-foreground-secondary max-w-2xl mx-auto font-sans">
|
<p className="text-lg text-foreground-secondary max-w-2xl mx-auto font-sans">
|
||||||
From discovery to execution, TPMJS provides the complete infrastructure
|
From discovery to execution, TPMJS provides the complete infrastructure for AI tool
|
||||||
for AI tool development.
|
development.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -490,8 +500,8 @@ export function FeaturesSection(): React.ReactElement {
|
||||||
live execution
|
live execution
|
||||||
</legend>
|
</legend>
|
||||||
<p className="font-sans text-sm text-foreground-secondary mb-6">
|
<p className="font-sans text-sm text-foreground-secondary mb-6">
|
||||||
Execute any tool directly from your terminal or AI agent. Secure sandboxed
|
Execute any tool directly from your terminal or AI agent. Secure sandboxed execution
|
||||||
execution with real-time streaming output.
|
with real-time streaming output.
|
||||||
</p>
|
</p>
|
||||||
<AnimatedTerminal />
|
<AnimatedTerminal />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
@ -504,8 +514,8 @@ export function FeaturesSection(): React.ReactElement {
|
||||||
tool registry
|
tool registry
|
||||||
</legend>
|
</legend>
|
||||||
<p className="font-sans text-sm text-foreground-secondary mb-6">
|
<p className="font-sans text-sm text-foreground-secondary mb-6">
|
||||||
Browse 170+ tools across multiple categories. Each tool is validated,
|
Browse 170+ tools across multiple categories. Each tool is validated, documented,
|
||||||
documented, and ready to use.
|
and ready to use.
|
||||||
</p>
|
</p>
|
||||||
<InteractiveToolGrid />
|
<InteractiveToolGrid />
|
||||||
<div className="mt-6 flex justify-center">
|
<div className="mt-6 flex justify-center">
|
||||||
|
|
@ -534,8 +544,8 @@ export function FeaturesSection(): React.ReactElement {
|
||||||
tools → tpmjs → agents
|
tools → tpmjs → agents
|
||||||
</legend>
|
</legend>
|
||||||
<p className="font-sans text-sm text-foreground-secondary mb-6 text-center max-w-2xl mx-auto">
|
<p className="font-sans text-sm text-foreground-secondary mb-6 text-center max-w-2xl mx-auto">
|
||||||
TPMJS acts as the central hub connecting npm packages to AI agents.
|
TPMJS acts as the central hub connecting npm packages to AI agents. Watch data flow in
|
||||||
Watch data flow in real-time as tools serve agent requests.
|
real-time as tools serve agent requests.
|
||||||
</p>
|
</p>
|
||||||
<ToolConnectionViz />
|
<ToolConnectionViz />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export function AnimatedCounter({
|
||||||
const progress = Math.min(elapsed / duration, 1);
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
|
||||||
// Easing function (ease-out-expo)
|
// Easing function (ease-out-expo)
|
||||||
const easeOutExpo = 1 - Math.pow(2, -10 * progress);
|
const easeOutExpo = 1 - 2 ** (-10 * progress);
|
||||||
const current = startValue + (endValue - startValue) * easeOutExpo;
|
const current = startValue + (endValue - startValue) * easeOutExpo;
|
||||||
|
|
||||||
setDisplayValue(current);
|
setDisplayValue(current);
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ export function BarChart({
|
||||||
.attr('dominant-baseline', 'middle')
|
.attr('dominant-baseline', 'middle')
|
||||||
.attr('fill', colors.foregroundSecondary)
|
.attr('fill', colors.foregroundSecondary)
|
||||||
.style('font-size', '0.75rem')
|
.style('font-size', '0.75rem')
|
||||||
.text((d) => (d.label.length > 12 ? d.label.slice(0, 12) + '...' : d.label));
|
.text((d) => (d.label.length > 12 ? `${d.label.slice(0, 12)}...` : d.label));
|
||||||
|
|
||||||
// Values
|
// Values
|
||||||
if (showValues) {
|
if (showValues) {
|
||||||
|
|
@ -196,7 +196,7 @@ export function BarChart({
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('fill', colors.foregroundSecondary)
|
.attr('fill', colors.foregroundSecondary)
|
||||||
.style('font-size', '0.7rem')
|
.style('font-size', '0.7rem')
|
||||||
.text((d) => (d.label.length > 8 ? d.label.slice(0, 8) + '...' : d.label))
|
.text((d) => (d.label.length > 8 ? `${d.label.slice(0, 8)}...` : d.label))
|
||||||
.attr(
|
.attr(
|
||||||
'transform',
|
'transform',
|
||||||
(d) => `rotate(-45, ${(x(d.label) || 0) + x.bandwidth() / 2}, ${innerHeight + 20})`
|
(d) => `rotate(-45, ${(x(d.label) || 0) + x.bandwidth() / 2}, ${innerHeight + 20})`
|
||||||
|
|
|
||||||
|
|
@ -167,15 +167,21 @@ function A11yTable({ components, title }: { components: A11yRequirement[]; title
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{comp.focusTrap ? (
|
{comp.focusTrap ? (
|
||||||
<Badge variant="success" size="sm">yes</Badge>
|
<Badge variant="success" size="sm">
|
||||||
|
yes
|
||||||
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline" size="sm">no</Badge>
|
<Badge variant="outline" size="sm">
|
||||||
|
no
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{comp.keyboard.map((k, i) => (
|
{comp.keyboard.map((k, i) => (
|
||||||
<div key={i} className="text-xs text-foreground-secondary">{k}</div>
|
<div key={i} className="text-xs text-foreground-secondary">
|
||||||
|
{k}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -195,13 +201,14 @@ export function SectionA11yChecklists(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="19. accessibility checklists" id="a11y-checklists">
|
<FieldsetSection title="19. accessibility checklists" id="a11y-checklists">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Component-level accessibility requirements and implementation guides.
|
Component-level accessibility requirements and implementation guides. All components follow
|
||||||
All components follow WCAG 2.1 AA standards.
|
WCAG 2.1 AA standards.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="aria patterns & keyboard">
|
<SubSection title="aria patterns & keyboard">
|
||||||
<p className="font-sans text-sm text-foreground-secondary mb-6">
|
<p className="font-sans text-sm text-foreground-secondary mb-6">
|
||||||
Each component has specific ARIA patterns and keyboard interactions that must be implemented.
|
Each component has specific ARIA patterns and keyboard interactions that must be
|
||||||
|
implemented.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<A11yTable components={overlayComponents} title="overlay components" />
|
<A11yTable components={overlayComponents} title="overlay components" />
|
||||||
|
|
@ -240,7 +247,7 @@ export function SectionA11yChecklists(): React.ReactElement {
|
||||||
<div className="mt-6 bg-surface p-4 border border-dashed border-border">
|
<div className="mt-6 bg-surface p-4 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-3">implementation pattern</h4>
|
<h4 className="font-mono text-sm font-medium mb-3">implementation pattern</h4>
|
||||||
<pre className="text-xs font-mono text-foreground-secondary overflow-x-auto">
|
<pre className="text-xs font-mono text-foreground-secondary overflow-x-auto">
|
||||||
{`// Focus trap implementation
|
{`// Focus trap implementation
|
||||||
const dialogRef = useRef<HTMLDivElement>(null);
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
|
@ -300,19 +307,27 @@ const handleClose = () => {
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono text-sm">Toast shown</TableCell>
|
<TableCell className="font-mono text-sm">Toast shown</TableCell>
|
||||||
<TableCell className="text-sm text-foreground-secondary">"[message content]"</TableCell>
|
<TableCell className="text-sm text-foreground-secondary">
|
||||||
|
"[message content]"
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono text-sm">Modal opened</TableCell>
|
<TableCell className="font-mono text-sm">Modal opened</TableCell>
|
||||||
<TableCell className="text-sm text-foreground-secondary">"[dialog title], dialog"</TableCell>
|
<TableCell className="text-sm text-foreground-secondary">
|
||||||
|
"[dialog title], dialog"
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono text-sm">Form error</TableCell>
|
<TableCell className="font-mono text-sm">Form error</TableCell>
|
||||||
<TableCell className="text-sm text-foreground-secondary">"Error: [field name], [error message]"</TableCell>
|
<TableCell className="text-sm text-foreground-secondary">
|
||||||
|
"Error: [field name], [error message]"
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono text-sm">Loading complete</TableCell>
|
<TableCell className="font-mono text-sm">Loading complete</TableCell>
|
||||||
<TableCell className="text-sm text-foreground-secondary">"Loading complete, [N] results"</TableCell>
|
<TableCell className="text-sm text-foreground-secondary">
|
||||||
|
"Loading complete, [N] results"
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
@ -322,21 +337,28 @@ const handleClose = () => {
|
||||||
|
|
||||||
<SubSection title="color contrast requirements">
|
<SubSection title="color contrast requirements">
|
||||||
<p className="font-sans text-sm text-foreground-secondary mb-4">
|
<p className="font-sans text-sm text-foreground-secondary mb-4">
|
||||||
All text must meet WCAG AA contrast requirements (4.5:1 for normal text, 3:1 for large text).
|
All text must meet WCAG AA contrast requirements (4.5:1 for normal text, 3:1 for large
|
||||||
|
text).
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between p-3 bg-background border border-border">
|
<div className="flex items-center justify-between p-3 bg-background border border-border">
|
||||||
<span className="text-foreground">foreground on background</span>
|
<span className="text-foreground">foreground on background</span>
|
||||||
<Badge variant="success" size="sm">9.2:1</Badge>
|
<Badge variant="success" size="sm">
|
||||||
|
9.2:1
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between p-3 bg-surface border border-border">
|
<div className="flex items-center justify-between p-3 bg-surface border border-border">
|
||||||
<span className="text-foreground-secondary">secondary on surface</span>
|
<span className="text-foreground-secondary">secondary on surface</span>
|
||||||
<Badge variant="success" size="sm">5.8:1</Badge>
|
<Badge variant="success" size="sm">
|
||||||
|
5.8:1
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between p-3 bg-surface border border-border">
|
<div className="flex items-center justify-between p-3 bg-surface border border-border">
|
||||||
<span className="text-foreground-tertiary">tertiary on surface</span>
|
<span className="text-foreground-tertiary">tertiary on surface</span>
|
||||||
<Badge variant="warning" size="sm">4.5:1</Badge>
|
<Badge variant="warning" size="sm">
|
||||||
|
4.5:1
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface p-4 border border-dashed border-border">
|
<div className="bg-surface p-4 border border-dashed border-border">
|
||||||
|
|
@ -353,13 +375,13 @@ const handleClose = () => {
|
||||||
|
|
||||||
<SubSection title="reduced motion support">
|
<SubSection title="reduced motion support">
|
||||||
<p className="font-sans text-sm text-foreground-secondary mb-4">
|
<p className="font-sans text-sm text-foreground-secondary mb-4">
|
||||||
Respect the <code className="px-1 bg-surface-2 text-xs">prefers-reduced-motion</code> media query
|
Respect the <code className="px-1 bg-surface-2 text-xs">prefers-reduced-motion</code>{' '}
|
||||||
for users who experience motion sickness or vestibular disorders.
|
media query for users who experience motion sickness or vestibular disorders.
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-surface p-4 border border-dashed border-border">
|
<div className="bg-surface p-4 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-3">implementation</h4>
|
<h4 className="font-mono text-sm font-medium mb-3">implementation</h4>
|
||||||
<pre className="text-xs font-mono text-foreground-secondary overflow-x-auto">
|
<pre className="text-xs font-mono text-foreground-secondary overflow-x-auto">
|
||||||
{`// CSS approach
|
{`// CSS approach
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
* {
|
* {
|
||||||
animation-duration: 0.01ms !important;
|
animation-duration: 0.01ms !important;
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ export function SectionAccessibility(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="6. accessibility" id="accessibility">
|
<FieldsetSection title="6. accessibility" id="accessibility">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
TPMJS targets WCAG 2.1 AA compliance. Accessibility is not optional—it's
|
TPMJS targets WCAG 2.1 AA compliance. Accessibility is not optional—it's a core requirement
|
||||||
a core requirement for every component.
|
for every component.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="standards">
|
<SubSection title="standards">
|
||||||
|
|
@ -35,8 +35,8 @@ export function SectionAccessibility(): React.ReactElement {
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">focus ring design</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">focus ring design</h4>
|
||||||
<p className="font-sans text-sm text-foreground-secondary mb-4">
|
<p className="font-sans text-sm text-foreground-secondary mb-4">
|
||||||
Focus rings use the copper accent color with 2px width and 2px offset.
|
Focus rings use the copper accent color with 2px width and 2px offset. They are{' '}
|
||||||
They are <strong>never removed</strong> from interactive elements.
|
<strong>never removed</strong> from interactive elements.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
<Button className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
||||||
|
|
@ -49,10 +49,23 @@ export function SectionAccessibility(): React.ReactElement {
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">keyboard navigation</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">keyboard navigation</h4>
|
||||||
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
||||||
<li>• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Tab</kbd> moves focus forward through interactive elements</li>
|
<li>
|
||||||
<li>• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Shift+Tab</kbd> moves focus backward</li>
|
• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Tab</kbd> moves focus
|
||||||
<li>• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Enter</kbd> / <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Space</kbd> activates buttons and links</li>
|
forward through interactive elements
|
||||||
<li>• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Esc</kbd> closes modals and dropdowns</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Shift+Tab</kbd> moves
|
||||||
|
focus backward
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Enter</kbd> /{' '}
|
||||||
|
<kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Space</kbd> activates
|
||||||
|
buttons and links
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Esc</kbd> closes modals
|
||||||
|
and dropdowns
|
||||||
|
</li>
|
||||||
<li>• Arrow keys navigate within components (tabs, radios, menus)</li>
|
<li>• Arrow keys navigate within components (tabs, radios, menus)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ export function SectionColors(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="2. color system" id="colors">
|
<FieldsetSection title="2. color system" id="colors">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
A warm earthy palette with copper accent. Colors are designed for clarity,
|
A warm earthy palette with copper accent. Colors are designed for clarity, hierarchy, and
|
||||||
hierarchy, and accessibility.
|
accessibility.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Color Palette Display */}
|
{/* Color Palette Display */}
|
||||||
|
|
@ -33,31 +33,77 @@ export function SectionColors(): React.ReactElement {
|
||||||
<SubSection title="borders">
|
<SubSection title="borders">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
<ColorCard name="border" color="bg-border" hex="#CEC9C3" desc="default borders" />
|
<ColorCard name="border" color="bg-border" hex="#CEC9C3" desc="default borders" />
|
||||||
<ColorCard name="border-strong" color="bg-border-strong" hex="#857F77" desc="emphasized borders" />
|
<ColorCard
|
||||||
<ColorCard name="border-subtle" color="bg-border-subtle" hex="#E3E0DC" desc="subtle borders" />
|
name="border-strong"
|
||||||
|
color="bg-border-strong"
|
||||||
|
hex="#857F77"
|
||||||
|
desc="emphasized borders"
|
||||||
|
/>
|
||||||
|
<ColorCard
|
||||||
|
name="border-subtle"
|
||||||
|
color="bg-border-subtle"
|
||||||
|
hex="#E3E0DC"
|
||||||
|
desc="subtle borders"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
<SubSection title="text">
|
<SubSection title="text">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
<ColorCard name="text" color="bg-foreground" hex="#1A1715" desc="primary text" textLight />
|
<ColorCard
|
||||||
<ColorCard name="text-secondary" color="bg-foreground-secondary" hex="#5C564F" desc="secondary text" textLight />
|
name="text"
|
||||||
<ColorCard name="text-tertiary" color="bg-foreground-tertiary" hex="#817B74" desc="tertiary text" textLight />
|
color="bg-foreground"
|
||||||
<ColorCard name="text-muted" color="bg-foreground-muted" hex="#9A958F" desc="muted text" textLight />
|
hex="#1A1715"
|
||||||
|
desc="primary text"
|
||||||
|
textLight
|
||||||
|
/>
|
||||||
|
<ColorCard
|
||||||
|
name="text-secondary"
|
||||||
|
color="bg-foreground-secondary"
|
||||||
|
hex="#5C564F"
|
||||||
|
desc="secondary text"
|
||||||
|
textLight
|
||||||
|
/>
|
||||||
|
<ColorCard
|
||||||
|
name="text-tertiary"
|
||||||
|
color="bg-foreground-tertiary"
|
||||||
|
hex="#817B74"
|
||||||
|
desc="tertiary text"
|
||||||
|
textLight
|
||||||
|
/>
|
||||||
|
<ColorCard
|
||||||
|
name="text-muted"
|
||||||
|
color="bg-foreground-muted"
|
||||||
|
hex="#9A958F"
|
||||||
|
desc="muted text"
|
||||||
|
textLight
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
<SubSection title="accent (copper)">
|
<SubSection title="accent (copper)">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
<ColorCard name="accent" color="bg-accent" hex="#A6592D" desc="brand accent" textLight />
|
<ColorCard name="accent" color="bg-accent" hex="#A6592D" desc="brand accent" textLight />
|
||||||
<ColorCard name="accent-strong" color="bg-accent-strong" hex="#8F4722" desc="hover accent" textLight />
|
<ColorCard
|
||||||
|
name="accent-strong"
|
||||||
|
color="bg-accent-strong"
|
||||||
|
hex="#8F4722"
|
||||||
|
desc="hover accent"
|
||||||
|
textLight
|
||||||
|
/>
|
||||||
<ColorCard name="accent-muted" color="bg-accent-muted" hex="#EDE5DF" desc="soft tint" />
|
<ColorCard name="accent-muted" color="bg-accent-muted" hex="#EDE5DF" desc="soft tint" />
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
<SubSection title="status">
|
<SubSection title="status">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
<ColorCard name="success" color="bg-success" hex="#327D52" desc="success states" textLight />
|
<ColorCard
|
||||||
|
name="success"
|
||||||
|
color="bg-success"
|
||||||
|
hex="#327D52"
|
||||||
|
desc="success states"
|
||||||
|
textLight
|
||||||
|
/>
|
||||||
<ColorCard name="warning" color="bg-warning" hex="#D9A020" desc="warning states" />
|
<ColorCard name="warning" color="bg-warning" hex="#D9A020" desc="warning states" />
|
||||||
<ColorCard name="error" color="bg-error" hex="#C44545" desc="error states" textLight />
|
<ColorCard name="error" color="bg-error" hex="#C44545" desc="error states" textLight />
|
||||||
<ColorCard name="info" color="bg-info" hex="#3380CC" desc="info states" textLight />
|
<ColorCard name="info" color="bg-info" hex="#3380CC" desc="info states" textLight />
|
||||||
|
|
@ -66,8 +112,18 @@ export function SectionColors(): React.ReactElement {
|
||||||
|
|
||||||
<SubSection title="status (light backgrounds)">
|
<SubSection title="status (light backgrounds)">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
<ColorCard name="success-light" color="bg-success-light" hex="#E5F3EC" desc="success bg" />
|
<ColorCard
|
||||||
<ColorCard name="warning-light" color="bg-warning-light" hex="#F8F2E0" desc="warning bg" />
|
name="success-light"
|
||||||
|
color="bg-success-light"
|
||||||
|
hex="#E5F3EC"
|
||||||
|
desc="success bg"
|
||||||
|
/>
|
||||||
|
<ColorCard
|
||||||
|
name="warning-light"
|
||||||
|
color="bg-warning-light"
|
||||||
|
hex="#F8F2E0"
|
||||||
|
desc="warning bg"
|
||||||
|
/>
|
||||||
<ColorCard name="error-light" color="bg-error-light" hex="#F9EDED" desc="error bg" />
|
<ColorCard name="error-light" color="bg-error-light" hex="#F9EDED" desc="error bg" />
|
||||||
<ColorCard name="info-light" color="bg-info-light" hex="#EDF4FB" desc="info bg" />
|
<ColorCard name="info-light" color="bg-info-light" hex="#EDF4FB" desc="info bg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -79,9 +135,15 @@ export function SectionColors(): React.ReactElement {
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">when to use copper vs neutral</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">when to use copper vs neutral</h4>
|
||||||
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
||||||
<li>• <strong>Copper:</strong> Primary actions, active states, links, key metrics</li>
|
<li>
|
||||||
<li>• <strong>Neutral:</strong> Secondary actions, borders, backgrounds, body text</li>
|
• <strong>Copper:</strong> Primary actions, active states, links, key metrics
|
||||||
<li>• <strong>Rule:</strong> Copper should be max ~10% of visible screen area</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
• <strong>Neutral:</strong> Secondary actions, borders, backgrounds, body text
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <strong>Rule:</strong> Copper should be max ~10% of visible screen area
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
|
|
@ -148,26 +210,58 @@ export function SectionColors(): React.ReactElement {
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono">foreground / background</TableCell>
|
<TableCell className="font-mono">foreground / background</TableCell>
|
||||||
<TableCell className="font-mono">12.5:1</TableCell>
|
<TableCell className="font-mono">12.5:1</TableCell>
|
||||||
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
|
<TableCell>
|
||||||
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
|
<Badge variant="success" size="sm">
|
||||||
|
pass
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="success" size="sm">
|
||||||
|
pass
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono">accent / background</TableCell>
|
<TableCell className="font-mono">accent / background</TableCell>
|
||||||
<TableCell className="font-mono">5.2:1</TableCell>
|
<TableCell className="font-mono">5.2:1</TableCell>
|
||||||
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
|
<TableCell>
|
||||||
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
|
<Badge variant="success" size="sm">
|
||||||
|
pass
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="success" size="sm">
|
||||||
|
pass
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono">foreground-secondary / background</TableCell>
|
<TableCell className="font-mono">foreground-secondary / background</TableCell>
|
||||||
<TableCell className="font-mono">5.8:1</TableCell>
|
<TableCell className="font-mono">5.8:1</TableCell>
|
||||||
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
|
<TableCell>
|
||||||
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
|
<Badge variant="success" size="sm">
|
||||||
|
pass
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="success" size="sm">
|
||||||
|
pass
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono">foreground-tertiary / background</TableCell>
|
<TableCell className="font-mono">foreground-tertiary / background</TableCell>
|
||||||
<TableCell className="font-mono">3.9:1</TableCell>
|
<TableCell className="font-mono">3.9:1</TableCell>
|
||||||
<TableCell><Badge variant="warning" size="sm">large only</Badge></TableCell>
|
<TableCell>
|
||||||
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
|
<Badge variant="warning" size="sm">
|
||||||
|
large only
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="success" size="sm">
|
||||||
|
pass
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ export function SectionComponentAPIs(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="13. component apis" id="api">
|
<FieldsetSection title="13. component apis" id="api">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Complete API documentation for each component with props, types,
|
Complete API documentation for each component with props, types, and usage examples.
|
||||||
and usage examples.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="button api">
|
<SubSection title="button api">
|
||||||
|
|
@ -33,13 +32,18 @@ export function SectionComponentAPIs(): React.ReactElement {
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono">variant</TableCell>
|
<TableCell className="font-mono">variant</TableCell>
|
||||||
<TableCell className="font-mono text-xs">'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'</TableCell>
|
<TableCell className="font-mono text-xs">
|
||||||
|
'default' | 'secondary' | 'destructive' |
|
||||||
|
'outline' | 'ghost' | 'link'
|
||||||
|
</TableCell>
|
||||||
<TableCell className="font-mono">'default'</TableCell>
|
<TableCell className="font-mono">'default'</TableCell>
|
||||||
<TableCell>visual style</TableCell>
|
<TableCell>visual style</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono">size</TableCell>
|
<TableCell className="font-mono">size</TableCell>
|
||||||
<TableCell className="font-mono text-xs">'sm' | 'md' | 'lg' | 'icon'</TableCell>
|
<TableCell className="font-mono text-xs">
|
||||||
|
'sm' | 'md' | 'lg' | 'icon'
|
||||||
|
</TableCell>
|
||||||
<TableCell className="font-mono">'md'</TableCell>
|
<TableCell className="font-mono">'md'</TableCell>
|
||||||
<TableCell>button size</TableCell>
|
<TableCell>button size</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -94,13 +98,17 @@ export function SectionComponentAPIs(): React.ReactElement {
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono">state</TableCell>
|
<TableCell className="font-mono">state</TableCell>
|
||||||
<TableCell className="font-mono text-xs">'default' | 'error' | 'success'</TableCell>
|
<TableCell className="font-mono text-xs">
|
||||||
|
'default' | 'error' | 'success'
|
||||||
|
</TableCell>
|
||||||
<TableCell className="font-mono">'default'</TableCell>
|
<TableCell className="font-mono">'default'</TableCell>
|
||||||
<TableCell>visual state</TableCell>
|
<TableCell>visual state</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="font-mono">size</TableCell>
|
<TableCell className="font-mono">size</TableCell>
|
||||||
<TableCell className="font-mono text-xs">'sm' | 'md' | 'lg'</TableCell>
|
<TableCell className="font-mono text-xs">
|
||||||
|
'sm' | 'md' | 'lg'
|
||||||
|
</TableCell>
|
||||||
<TableCell className="font-mono">'md'</TableCell>
|
<TableCell className="font-mono">'md'</TableCell>
|
||||||
<TableCell>input size</TableCell>
|
<TableCell>input size</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from '@tpmjs/ui/Accordion/Accordion';
|
} from '@tpmjs/ui/Accordion/Accordion';
|
||||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||||
import { Breadcrumbs, BreadcrumbItem } from '@tpmjs/ui/Breadcrumbs/Breadcrumbs';
|
import { BreadcrumbItem, Breadcrumbs } from '@tpmjs/ui/Breadcrumbs/Breadcrumbs';
|
||||||
import { Button } from '@tpmjs/ui/Button/Button';
|
import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -270,7 +270,13 @@ export function SectionComponents({
|
||||||
{/* Slider */}
|
{/* Slider */}
|
||||||
<SubSection title="slider">
|
<SubSection title="slider">
|
||||||
<div className="max-w-md space-y-4">
|
<div className="max-w-md space-y-4">
|
||||||
<Slider value={sliderValue} onChange={(e) => setSliderValue(Number(e.target.value))} min={0} max={100} step={1} />
|
<Slider
|
||||||
|
value={sliderValue}
|
||||||
|
onChange={(e) => setSliderValue(Number(e.target.value))}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
<p className="font-mono text-xs text-foreground-secondary">Value: {sliderValue}</p>
|
<p className="font-mono text-xs text-foreground-secondary">Value: {sliderValue}</p>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
@ -350,11 +356,7 @@ export function SectionComponents({
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<SubSection title="pagination">
|
<SubSection title="pagination">
|
||||||
<Pagination
|
<Pagination page={currentPage} totalPages={10} onPageChange={setCurrentPage} />
|
||||||
page={currentPage}
|
|
||||||
totalPages={10}
|
|
||||||
onPageChange={setCurrentPage}
|
|
||||||
/>
|
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
{/* Dropdown Menu */}
|
||||||
|
|
@ -408,7 +410,9 @@ export function SectionComponents({
|
||||||
description="This is a modal dialog."
|
description="This is a modal dialog."
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button variant="outline" onClick={() => setShowModal(false)}>Cancel</Button>
|
<Button variant="outline" onClick={() => setShowModal(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
<Button onClick={() => setShowModal(false)}>Confirm</Button>
|
<Button onClick={() => setShowModal(false)}>Confirm</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
@ -419,12 +423,10 @@ export function SectionComponents({
|
||||||
|
|
||||||
{/* Drawer */}
|
{/* Drawer */}
|
||||||
<SubSection title="drawer">
|
<SubSection title="drawer">
|
||||||
<Button variant="outline" onClick={() => setShowDrawer(true)}>Open Drawer</Button>
|
<Button variant="outline" onClick={() => setShowDrawer(true)}>
|
||||||
<Drawer
|
Open Drawer
|
||||||
open={showDrawer}
|
</Button>
|
||||||
onClose={() => setShowDrawer(false)}
|
<Drawer open={showDrawer} onClose={() => setShowDrawer(false)} title="Drawer Content">
|
||||||
title="Drawer Content"
|
|
||||||
>
|
|
||||||
<p className="text-foreground-secondary">
|
<p className="text-foreground-secondary">
|
||||||
This is the drawer content. It slides in from the side.
|
This is the drawer content. It slides in from the side.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Button } from '@tpmjs/ui/Button/Button';
|
import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import {
|
import { Card, CardContent } from '@tpmjs/ui/Card/Card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
} from '@tpmjs/ui/Card/Card';
|
|
||||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
import { Spinner } from '@tpmjs/ui/Spinner/Spinner';
|
import { Spinner } from '@tpmjs/ui/Spinner/Spinner';
|
||||||
import { DoDontCard, FieldsetSection, SubSection } from './shared';
|
import { DoDontCard, FieldsetSection, SubSection } from './shared';
|
||||||
|
|
@ -13,8 +10,7 @@ export function SectionContent(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="8. content guidelines" id="content">
|
<FieldsetSection title="8. content guidelines" id="content">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Voice and tone guidelines for consistent, helpful communication
|
Voice and tone guidelines for consistent, helpful communication across the platform.
|
||||||
across the platform.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="voice & tone">
|
<SubSection title="voice & tone">
|
||||||
|
|
@ -22,19 +18,35 @@ export function SectionContent(): React.ReactElement {
|
||||||
<div className="bg-surface p-6 border-2 border-success">
|
<div className="bg-surface p-6 border-2 border-success">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4 text-success">we are</h4>
|
<h4 className="font-mono text-sm font-medium mb-4 text-success">we are</h4>
|
||||||
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
||||||
<li>• <strong>Technical:</strong> Precise, accurate, developer-friendly</li>
|
<li>
|
||||||
<li>• <strong>Direct:</strong> Say what needs to be said, no fluff</li>
|
• <strong>Technical:</strong> Precise, accurate, developer-friendly
|
||||||
<li>• <strong>Helpful:</strong> Guide users to success</li>
|
</li>
|
||||||
<li>• <strong>Neutral:</strong> Professional, not corporate</li>
|
<li>
|
||||||
|
• <strong>Direct:</strong> Say what needs to be said, no fluff
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <strong>Helpful:</strong> Guide users to success
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <strong>Neutral:</strong> Professional, not corporate
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface p-6 border-2 border-error">
|
<div className="bg-surface p-6 border-2 border-error">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4 text-error">we are not</h4>
|
<h4 className="font-mono text-sm font-medium mb-4 text-error">we are not</h4>
|
||||||
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
||||||
<li>• <strong>Marketing-speak:</strong> No "revolutionary" or "game-changing"</li>
|
<li>
|
||||||
<li>• <strong>Cute:</strong> No jokes, puns, or playful language</li>
|
• <strong>Marketing-speak:</strong> No "revolutionary" or "game-changing"
|
||||||
<li>• <strong>Vague:</strong> No "something went wrong"</li>
|
</li>
|
||||||
<li>• <strong>Condescending:</strong> No "simply" or "just"</li>
|
<li>
|
||||||
|
• <strong>Cute:</strong> No jokes, puns, or playful language
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <strong>Vague:</strong> No "something went wrong"
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <strong>Condescending:</strong> No "simply" or "just"
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,15 +58,23 @@ export function SectionContent(): React.ReactElement {
|
||||||
<DoDontCard type="do" title="Use action verbs for buttons">
|
<DoDontCard type="do" title="Use action verbs for buttons">
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button size="sm">publish</Button>
|
<Button size="sm">publish</Button>
|
||||||
<Button size="sm" variant="secondary">save draft</Button>
|
<Button size="sm" variant="secondary">
|
||||||
<Button size="sm" variant="destructive">delete</Button>
|
save draft
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="destructive">
|
||||||
|
delete
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DoDontCard>
|
</DoDontCard>
|
||||||
<DoDontCard type="dont" title="Avoid generic labels">
|
<DoDontCard type="dont" title="Avoid generic labels">
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button size="sm">submit</Button>
|
<Button size="sm">submit</Button>
|
||||||
<Button size="sm" variant="secondary">ok</Button>
|
<Button size="sm" variant="secondary">
|
||||||
<Button size="sm" variant="destructive">yes</Button>
|
ok
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="destructive">
|
||||||
|
yes
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DoDontCard>
|
</DoDontCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -67,7 +87,8 @@ export function SectionContent(): React.ReactElement {
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="p-3 bg-error-light border border-error text-sm">
|
<div className="p-3 bg-error-light border border-error text-sm">
|
||||||
<strong>Good:</strong> "API key is invalid. Generate a new key in your dashboard settings."
|
<strong>Good:</strong> "API key is invalid. Generate a new key in your dashboard
|
||||||
|
settings."
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-error-light border border-error text-sm">
|
<div className="p-3 bg-error-light border border-error text-sm">
|
||||||
<strong>Bad:</strong> "Error: Invalid credentials"
|
<strong>Bad:</strong> "Error: Invalid credentials"
|
||||||
|
|
@ -97,7 +118,9 @@ export function SectionContent(): React.ReactElement {
|
||||||
<p className="font-sans text-xs text-foreground-secondary mb-4">
|
<p className="font-sans text-xs text-foreground-secondary mb-4">
|
||||||
Try adjusting your search or filters.
|
Try adjusting your search or filters.
|
||||||
</p>
|
</p>
|
||||||
<Button size="sm" variant="outline">clear filters</Button>
|
<Button size="sm" variant="outline">
|
||||||
|
clear filters
|
||||||
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -108,7 +131,9 @@ export function SectionContent(): React.ReactElement {
|
||||||
<p className="font-sans text-xs text-foreground-secondary mb-4">
|
<p className="font-sans text-xs text-foreground-secondary mb-4">
|
||||||
Check your connection and try again.
|
Check your connection and try again.
|
||||||
</p>
|
</p>
|
||||||
<Button size="sm" variant="outline">retry</Button>
|
<Button size="sm" variant="outline">
|
||||||
|
retry
|
||||||
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -134,7 +159,9 @@ export function SectionContent(): React.ReactElement {
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Spinner size="sm" />
|
<Spinner size="sm" />
|
||||||
<span className="font-mono text-sm text-foreground-secondary">publishing tool...</span>
|
<span className="font-mono text-sm text-foreground-secondary">
|
||||||
|
publishing tool...
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,35 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@tpmjs/ui/Table/Table';
|
} from '@tpmjs/ui/Table/Table';
|
||||||
import { FieldsetSection, SubSection, DoDontCard } from './shared';
|
import { DoDontCard, FieldsetSection, SubSection } from './shared';
|
||||||
|
|
||||||
const glossaryTerms = [
|
const glossaryTerms = [
|
||||||
{ term: 'Tool', definition: 'A reusable MCP server or utility that can be installed via npm', avoid: 'Package, Module, Plugin' },
|
{
|
||||||
{ term: 'Agent', definition: 'An AI-powered assistant that uses tools to complete tasks', avoid: 'Bot, Assistant, AI' },
|
term: 'Tool',
|
||||||
|
definition: 'A reusable MCP server or utility that can be installed via npm',
|
||||||
|
avoid: 'Package, Module, Plugin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: 'Agent',
|
||||||
|
definition: 'An AI-powered assistant that uses tools to complete tasks',
|
||||||
|
avoid: 'Bot, Assistant, AI',
|
||||||
|
},
|
||||||
{ term: 'Collection', definition: 'A curated group of related tools', avoid: 'Bundle, Set, Kit' },
|
{ term: 'Collection', definition: 'A curated group of related tools', avoid: 'Bundle, Set, Kit' },
|
||||||
{ term: 'Publish', definition: 'Release a new version to the registry', avoid: 'Deploy, Ship, Push' },
|
{
|
||||||
{ term: 'Install', definition: 'Add a tool to your project dependencies', avoid: 'Download, Get, Add' },
|
term: 'Publish',
|
||||||
{ term: 'Execute', definition: 'Run a tool with specific parameters', avoid: 'Invoke, Call, Trigger' },
|
definition: 'Release a new version to the registry',
|
||||||
|
avoid: 'Deploy, Ship, Push',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: 'Install',
|
||||||
|
definition: 'Add a tool to your project dependencies',
|
||||||
|
avoid: 'Download, Get, Add',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
term: 'Execute',
|
||||||
|
definition: 'Run a tool with specific parameters',
|
||||||
|
avoid: 'Invoke, Call, Trigger',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const verbConsistency = [
|
const verbConsistency = [
|
||||||
|
|
@ -51,8 +71,8 @@ export function SectionContentGuidelines(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="20. content & writing" id="content-guidelines">
|
<FieldsetSection title="20. content & writing" id="content-guidelines">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Consistent language and terminology across the platform.
|
Consistent language and terminology across the platform. These guidelines ensure clarity for
|
||||||
These guidelines ensure clarity for users and maintainability for developers.
|
users and maintainability for developers.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="glossary">
|
<SubSection title="glossary">
|
||||||
|
|
@ -72,9 +92,13 @@ export function SectionContentGuidelines(): React.ReactElement {
|
||||||
{glossaryTerms.map((item) => (
|
{glossaryTerms.map((item) => (
|
||||||
<TableRow key={item.term}>
|
<TableRow key={item.term}>
|
||||||
<TableCell className="font-mono text-sm font-medium">{item.term}</TableCell>
|
<TableCell className="font-mono text-sm font-medium">{item.term}</TableCell>
|
||||||
<TableCell className="text-sm text-foreground-secondary">{item.definition}</TableCell>
|
<TableCell className="text-sm text-foreground-secondary">
|
||||||
|
{item.definition}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-xs text-foreground-tertiary line-through">{item.avoid}</span>
|
<span className="text-xs text-foreground-tertiary line-through">
|
||||||
|
{item.avoid}
|
||||||
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
@ -101,10 +125,14 @@ export function SectionContentGuidelines(): React.ReactElement {
|
||||||
<TableRow key={item.action}>
|
<TableRow key={item.action}>
|
||||||
<TableCell className="text-sm text-foreground-secondary">{item.action}</TableCell>
|
<TableCell className="text-sm text-foreground-secondary">{item.action}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="default" size="sm">{item.use}</Badge>
|
<Badge variant="default" size="sm">
|
||||||
|
{item.use}
|
||||||
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-xs text-foreground-tertiary line-through">{item.avoid}</span>
|
<span className="text-xs text-foreground-tertiary line-through">
|
||||||
|
{item.avoid}
|
||||||
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
@ -211,11 +239,15 @@ export function SectionContentGuidelines(): React.ReactElement {
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="p-3 bg-error-light border-l-2 border-error text-sm">
|
<div className="p-3 bg-error-light border-l-2 border-error text-sm">
|
||||||
<p className="font-medium">Invalid email format</p>
|
<p className="font-medium">Invalid email format</p>
|
||||||
<p className="text-foreground-secondary text-xs mt-1">Please enter a valid email address like user@example.com</p>
|
<p className="text-foreground-secondary text-xs mt-1">
|
||||||
|
Please enter a valid email address like user@example.com
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-error-light border-l-2 border-error text-sm">
|
<div className="p-3 bg-error-light border-l-2 border-error text-sm">
|
||||||
<p className="font-medium">Tool name already exists</p>
|
<p className="font-medium">Tool name already exists</p>
|
||||||
<p className="text-foreground-secondary text-xs mt-1">Choose a different name or update the existing tool</p>
|
<p className="text-foreground-secondary text-xs mt-1">
|
||||||
|
Choose a different name or update the existing tool
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DoDontCard>
|
</DoDontCard>
|
||||||
|
|
@ -242,14 +274,18 @@ export function SectionIconSystem(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="21. icon system" id="icon-system">
|
<FieldsetSection title="21. icon system" id="icon-system">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Icons from Lucide React, used consistently across components.
|
Icons from Lucide React, used consistently across components. All icons use 2px stroke
|
||||||
All icons use 2px stroke weight and are available in multiple sizes.
|
weight and are available in multiple sizes.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="icon library">
|
<SubSection title="icon library">
|
||||||
<p className="font-sans text-sm text-foreground-secondary mb-4">
|
<p className="font-sans text-sm text-foreground-secondary mb-4">
|
||||||
Source: <a href="https://lucide.dev" className="text-accent hover:underline">Lucide React</a>.
|
Source:{' '}
|
||||||
Icons should be imported from <code className="text-xs bg-surface-2 px-1">@tpmjs/ui/Icon/Icon</code>.
|
<a href="https://lucide.dev" className="text-accent hover:underline">
|
||||||
|
Lucide React
|
||||||
|
</a>
|
||||||
|
. Icons should be imported from{' '}
|
||||||
|
<code className="text-xs bg-surface-2 px-1">@tpmjs/ui/Icon/Icon</code>.
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-surface p-4 border border-dashed border-border">
|
<div className="bg-surface p-4 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">common icons</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">common icons</h4>
|
||||||
|
|
@ -316,7 +352,9 @@ export function SectionIconSystem(): React.ReactElement {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-foreground-secondary">{item.usage}</TableCell>
|
<TableCell className="text-sm text-foreground-secondary">{item.usage}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="outline" size="sm">{item.size}</Badge>
|
<Badge variant="outline" size="sm">
|
||||||
|
{item.size}
|
||||||
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
@ -383,15 +421,20 @@ export function SectionIconSystem(): React.ReactElement {
|
||||||
<ol className="space-y-3 text-sm text-foreground-secondary font-sans">
|
<ol className="space-y-3 text-sm text-foreground-secondary font-sans">
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="font-mono text-accent">1.</span>
|
<span className="font-mono text-accent">1.</span>
|
||||||
Check if a suitable icon exists in <a href="https://lucide.dev/icons" className="text-accent hover:underline">Lucide</a>
|
Check if a suitable icon exists in{' '}
|
||||||
|
<a href="https://lucide.dev/icons" className="text-accent hover:underline">
|
||||||
|
Lucide
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="font-mono text-accent">2.</span>
|
<span className="font-mono text-accent">2.</span>
|
||||||
Add the icon import to <code className="text-xs bg-surface-2 px-1">packages/ui/src/Icon/icons.ts</code>
|
Add the icon import to{' '}
|
||||||
|
<code className="text-xs bg-surface-2 px-1">packages/ui/src/Icon/icons.ts</code>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="font-mono text-accent">3.</span>
|
<span className="font-mono text-accent">3.</span>
|
||||||
Add the icon name to the <code className="text-xs bg-surface-2 px-1">IconName</code> type
|
Add the icon name to the <code className="text-xs bg-surface-2 px-1">IconName</code>{' '}
|
||||||
|
type
|
||||||
</li>
|
</li>
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="font-mono text-accent">4.</span>
|
<span className="font-mono text-accent">4.</span>
|
||||||
|
|
@ -399,9 +442,11 @@ export function SectionIconSystem(): React.ReactElement {
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<div className="mt-4 p-3 bg-surface-2">
|
<div className="mt-4 p-3 bg-surface-2">
|
||||||
<p className="font-mono text-xs text-foreground-tertiary mb-2">example: adding a new icon</p>
|
<p className="font-mono text-xs text-foreground-tertiary mb-2">
|
||||||
|
example: adding a new icon
|
||||||
|
</p>
|
||||||
<pre className="text-xs font-mono overflow-x-auto">
|
<pre className="text-xs font-mono overflow-x-auto">
|
||||||
{`// icons.ts
|
{`// icons.ts
|
||||||
import { Heart } from 'lucide-react';
|
import { Heart } from 'lucide-react';
|
||||||
|
|
||||||
export const icons = {
|
export const icons = {
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,14 @@ interface SectionDataVizProps {
|
||||||
onCounterChange: (value: number) => void;
|
onCounterChange: (value: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SectionDataViz({ counterValue, onCounterChange }: SectionDataVizProps): React.ReactElement {
|
export function SectionDataViz({
|
||||||
|
counterValue,
|
||||||
|
onCounterChange,
|
||||||
|
}: SectionDataVizProps): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="9. data visualization" id="data-viz">
|
<FieldsetSection title="9. data visualization" id="data-viz">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Guidelines for charts, metrics, and data displays. Semantic colors only—
|
Guidelines for charts, metrics, and data displays. Semantic colors only— no rainbow charts.
|
||||||
no rainbow charts.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="data color palette">
|
<SubSection title="data color palette">
|
||||||
|
|
@ -52,34 +54,10 @@ export function SectionDataViz({ counterValue, onCounterChange }: SectionDataViz
|
||||||
Tool quality scores use a consistent visual language across the platform.
|
Tool quality scores use a consistent visual language across the platform.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
<StatCard
|
<StatCard label="excellent" value={95} suffix="%" showBar barProgress={95} />
|
||||||
label="excellent"
|
<StatCard label="good" value={75} suffix="%" showBar barProgress={75} />
|
||||||
value={95}
|
<StatCard label="fair" value={50} suffix="%" showBar barProgress={50} />
|
||||||
suffix="%"
|
<StatCard label="poor" value={25} suffix="%" showBar barProgress={25} />
|
||||||
showBar
|
|
||||||
barProgress={95}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
label="good"
|
|
||||||
value={75}
|
|
||||||
suffix="%"
|
|
||||||
showBar
|
|
||||||
barProgress={75}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
label="fair"
|
|
||||||
value={50}
|
|
||||||
suffix="%"
|
|
||||||
showBar
|
|
||||||
barProgress={50}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
label="poor"
|
|
||||||
value={25}
|
|
||||||
suffix="%"
|
|
||||||
showBar
|
|
||||||
barProgress={25}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
|
|
@ -94,7 +72,9 @@ export function SectionDataViz({ counterValue, onCounterChange }: SectionDataViz
|
||||||
<ProgressBar value={100} variant="success" />
|
<ProgressBar value={100} variant="success" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-mono text-sm text-foreground-secondary mb-2">warning (approaching limit)</p>
|
<p className="font-mono text-sm text-foreground-secondary mb-2">
|
||||||
|
warning (approaching limit)
|
||||||
|
</p>
|
||||||
<ProgressBar value={85} variant="warning" />
|
<ProgressBar value={85} variant="warning" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -132,9 +112,23 @@ export function SectionDataViz({ counterValue, onCounterChange }: SectionDataViz
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 mt-6 justify-center">
|
<div className="flex gap-2 mt-6 justify-center">
|
||||||
<Button size="sm" onClick={() => onCounterChange(counterValue + 1000)}>+1000</Button>
|
<Button size="sm" onClick={() => onCounterChange(counterValue + 1000)}>
|
||||||
<Button size="sm" variant="outline" onClick={() => onCounterChange(Math.max(0, counterValue - 1000))}>-1000</Button>
|
+1000
|
||||||
<Button size="sm" variant="secondary" onClick={() => onCounterChange(Math.floor(Math.random() * 100000))}>random</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onCounterChange(Math.max(0, counterValue - 1000))}
|
||||||
|
>
|
||||||
|
-1000
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => onCounterChange(Math.floor(Math.random() * 100000))}
|
||||||
|
>
|
||||||
|
random
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
</FieldsetSection>
|
</FieldsetSection>
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,19 @@ export function SectionIcons(): React.ReactElement {
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">icon-only buttons</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">icon-only buttons</h4>
|
||||||
<p className="font-sans text-sm text-foreground-secondary mb-4">
|
<p className="font-sans text-sm text-foreground-secondary mb-4">
|
||||||
Only allowed when the action is universally understood
|
Only allowed when the action is universally understood (close, search, menu) AND space
|
||||||
(close, search, menu) AND space is limited.
|
is limited.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button size="icon" variant="ghost"><Icon icon="x" size="sm" /></Button>
|
<Button size="icon" variant="ghost">
|
||||||
<Button size="icon" variant="ghost"><Icon icon="search" size="sm" /></Button>
|
<Icon icon="x" size="sm" />
|
||||||
<Button size="icon" variant="ghost"><Icon icon="menu" size="sm" /></Button>
|
</Button>
|
||||||
|
<Button size="icon" variant="ghost">
|
||||||
|
<Icon icon="search" size="sm" />
|
||||||
|
</Button>
|
||||||
|
<Button size="icon" variant="ghost">
|
||||||
|
<Icon icon="menu" size="sm" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
|
|
@ -52,8 +58,12 @@ export function SectionIcons(): React.ReactElement {
|
||||||
Always include text labels when space permits for clarity.
|
Always include text labels when space permits for clarity.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button size="sm"><Icon icon="plus" size="sm" className="mr-2" /> add tool</Button>
|
<Button size="sm">
|
||||||
<Button size="sm" variant="outline"><Icon icon="upload" size="sm" className="mr-2" /> upload</Button>
|
<Icon icon="plus" size="sm" className="mr-2" /> add tool
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Icon icon="upload" size="sm" className="mr-2" /> upload
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,16 +93,51 @@ export function SectionIcons(): React.ReactElement {
|
||||||
<SubSection title="all icons">
|
<SubSection title="all icons">
|
||||||
<div className="grid grid-cols-6 md:grid-cols-8 lg:grid-cols-12 gap-6">
|
<div className="grid grid-cols-6 md:grid-cols-8 lg:grid-cols-12 gap-6">
|
||||||
{[
|
{[
|
||||||
'copy', 'github', 'check', 'x', 'chevronDown', 'chevronRight',
|
'copy',
|
||||||
'clock', 'link', 'sun', 'moon', 'discord', 'menu',
|
'github',
|
||||||
'folder', 'plus', 'trash', 'edit', 'search', 'loader',
|
'check',
|
||||||
'upload', 'alertCircle', 'globe', 'terminal', 'puzzle', 'message',
|
'x',
|
||||||
'key', 'info', 'send', 'home', 'user', 'heart',
|
'chevronDown',
|
||||||
'star', 'externalLink', 'arrowLeft', 'box', 'alertTriangle',
|
'chevronRight',
|
||||||
|
'clock',
|
||||||
|
'link',
|
||||||
|
'sun',
|
||||||
|
'moon',
|
||||||
|
'discord',
|
||||||
|
'menu',
|
||||||
|
'folder',
|
||||||
|
'plus',
|
||||||
|
'trash',
|
||||||
|
'edit',
|
||||||
|
'search',
|
||||||
|
'loader',
|
||||||
|
'upload',
|
||||||
|
'alertCircle',
|
||||||
|
'globe',
|
||||||
|
'terminal',
|
||||||
|
'puzzle',
|
||||||
|
'message',
|
||||||
|
'key',
|
||||||
|
'info',
|
||||||
|
'send',
|
||||||
|
'home',
|
||||||
|
'user',
|
||||||
|
'heart',
|
||||||
|
'star',
|
||||||
|
'externalLink',
|
||||||
|
'arrowLeft',
|
||||||
|
'box',
|
||||||
|
'alertTriangle',
|
||||||
].map((iconName) => (
|
].map((iconName) => (
|
||||||
<div key={iconName} className="flex flex-col items-center gap-2" title={iconName}>
|
<div key={iconName} className="flex flex-col items-center gap-2" title={iconName}>
|
||||||
<Icon icon={iconName as Parameters<typeof Icon>[0]['icon']} size="md" className="text-foreground" />
|
<Icon
|
||||||
<span className="font-mono text-[10px] text-foreground-tertiary truncate max-w-full">{iconName}</span>
|
icon={iconName as Parameters<typeof Icon>[0]['icon']}
|
||||||
|
size="md"
|
||||||
|
className="text-foreground"
|
||||||
|
/>
|
||||||
|
<span className="font-mono text-[10px] text-foreground-tertiary truncate max-w-full">
|
||||||
|
{iconName}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,15 @@ interface SectionLayoutProps {
|
||||||
onDensityChange: (density: 'compact' | 'comfortable' | 'spacious') => void;
|
onDensityChange: (density: 'compact' | 'comfortable' | 'spacious') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SectionLayout({ density, onDensityChange }: SectionLayoutProps): React.ReactElement {
|
export function SectionLayout({
|
||||||
|
density,
|
||||||
|
onDensityChange,
|
||||||
|
}: SectionLayoutProps): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="7. layout & responsiveness" id="layout">
|
<FieldsetSection title="7. layout & responsiveness" id="layout">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Mobile-first responsive design with clear breakpoints and density modes
|
Mobile-first responsive design with clear breakpoints and density modes for different use
|
||||||
for different use cases.
|
cases.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="breakpoints">
|
<SubSection title="breakpoints">
|
||||||
|
|
@ -37,18 +40,26 @@ export function SectionLayout({ density, onDensityChange }: SectionLayoutProps):
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">responsive behaviors</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">responsive behaviors</h4>
|
||||||
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
||||||
<li>• <strong>Sidebar:</strong> Collapsible on mobile, visible on lg+</li>
|
<li>
|
||||||
<li>• <strong>Tables:</strong> Horizontal scroll on mobile, full width on lg+</li>
|
• <strong>Sidebar:</strong> Collapsible on mobile, visible on lg+
|
||||||
<li>• <strong>Cards:</strong> Single column on mobile, grid on md+</li>
|
</li>
|
||||||
<li>• <strong>Navigation:</strong> Hamburger menu on mobile, horizontal on lg+</li>
|
<li>
|
||||||
|
• <strong>Tables:</strong> Horizontal scroll on mobile, full width on lg+
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <strong>Cards:</strong> Single column on mobile, grid on md+
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <strong>Navigation:</strong> Hamburger menu on mobile, horizontal on lg+
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
<SubSection title="density modes">
|
<SubSection title="density modes">
|
||||||
<p className="font-sans text-sm text-foreground-secondary mb-6">
|
<p className="font-sans text-sm text-foreground-secondary mb-6">
|
||||||
Density modes adjust spacing, font size, and row heights for different contexts.
|
Density modes adjust spacing, font size, and row heights for different contexts. Essential
|
||||||
Essential for data-dense developer tools.
|
for data-dense developer tools.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex gap-4 mb-6">
|
<div className="flex gap-4 mb-6">
|
||||||
|
|
@ -88,17 +99,29 @@ export function SectionLayout({ density, onDensityChange }: SectionLayoutProps):
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="density-cell font-mono">@tpmjs/parser</TableCell>
|
<TableCell className="density-cell font-mono">@tpmjs/parser</TableCell>
|
||||||
<TableCell className="density-cell font-mono">125,432</TableCell>
|
<TableCell className="density-cell font-mono">125,432</TableCell>
|
||||||
<TableCell className="density-cell"><Badge variant="success" size="sm">active</Badge></TableCell>
|
<TableCell className="density-cell">
|
||||||
|
<Badge variant="success" size="sm">
|
||||||
|
active
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="density-cell font-mono">@tpmjs/validator</TableCell>
|
<TableCell className="density-cell font-mono">@tpmjs/validator</TableCell>
|
||||||
<TableCell className="density-cell font-mono">89,231</TableCell>
|
<TableCell className="density-cell font-mono">89,231</TableCell>
|
||||||
<TableCell className="density-cell"><Badge variant="success" size="sm">active</Badge></TableCell>
|
<TableCell className="density-cell">
|
||||||
|
<Badge variant="success" size="sm">
|
||||||
|
active
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="density-cell font-mono">@tpmjs/transform</TableCell>
|
<TableCell className="density-cell font-mono">@tpmjs/transform</TableCell>
|
||||||
<TableCell className="density-cell font-mono">45,678</TableCell>
|
<TableCell className="density-cell font-mono">45,678</TableCell>
|
||||||
<TableCell className="density-cell"><Badge variant="warning" size="sm">beta</Badge></TableCell>
|
<TableCell className="density-cell">
|
||||||
|
<Badge variant="warning" size="sm">
|
||||||
|
beta
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
@ -126,7 +149,10 @@ export function SectionLayout({ density, onDensityChange }: SectionLayoutProps):
|
||||||
<p className="font-mono text-xs text-foreground-secondary mb-3">12-column grid</p>
|
<p className="font-mono text-xs text-foreground-secondary mb-3">12-column grid</p>
|
||||||
<div className="grid grid-cols-12 gap-2">
|
<div className="grid grid-cols-12 gap-2">
|
||||||
{[...Array(12)].map((_, i) => (
|
{[...Array(12)].map((_, i) => (
|
||||||
<div key={i} className="bg-surface border border-dashed border-border p-2 text-center">
|
<div
|
||||||
|
key={i}
|
||||||
|
className="bg-surface border border-dashed border-border p-2 text-center"
|
||||||
|
>
|
||||||
<span className="font-mono text-[10px] text-foreground-tertiary">{i + 1}</span>
|
<span className="font-mono text-[10px] text-foreground-tertiary">{i + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -134,7 +160,9 @@ export function SectionLayout({ density, onDensityChange }: SectionLayoutProps):
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="font-mono text-xs text-foreground-secondary mb-3">sidebar + content (3 + 9)</p>
|
<p className="font-mono text-xs text-foreground-secondary mb-3">
|
||||||
|
sidebar + content (3 + 9)
|
||||||
|
</p>
|
||||||
<div className="grid grid-cols-12 gap-4">
|
<div className="grid grid-cols-12 gap-4">
|
||||||
<div className="col-span-3 bg-surface border border-dashed border-border p-4">
|
<div className="col-span-3 bg-surface border border-dashed border-border p-4">
|
||||||
<span className="font-mono text-xs">sidebar</span>
|
<span className="font-mono text-xs">sidebar</span>
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ export function SectionMotion(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="5. motion" id="motion">
|
<FieldsetSection title="5. motion" id="motion">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Motion communicates state changes, not decoration. Animations are fast
|
Motion communicates state changes, not decoration. Animations are fast by default and
|
||||||
by default and respect user preferences.
|
respect user preferences.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="motion principles">
|
<SubSection title="motion principles">
|
||||||
|
|
@ -16,29 +16,29 @@ export function SectionMotion(): React.ReactElement {
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">fast by default</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">fast by default</h4>
|
||||||
<p className="font-sans text-sm text-foreground-secondary">
|
<p className="font-sans text-sm text-foreground-secondary">
|
||||||
Most transitions complete in 150-200ms. Users should never wait
|
Most transitions complete in 150-200ms. Users should never wait for animations to
|
||||||
for animations to finish before interacting.
|
finish before interacting.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">state communication</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">state communication</h4>
|
||||||
<p className="font-sans text-sm text-foreground-secondary">
|
<p className="font-sans text-sm text-foreground-secondary">
|
||||||
Motion indicates something changed: a panel opened, an item was
|
Motion indicates something changed: a panel opened, an item was selected, data
|
||||||
selected, data updated. Never animate just for visual interest.
|
updated. Never animate just for visual interest.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">linear for data</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">linear for data</h4>
|
||||||
<p className="font-sans text-sm text-foreground-secondary">
|
<p className="font-sans text-sm text-foreground-secondary">
|
||||||
Data updates (counters, progress bars, charts) use linear easing.
|
Data updates (counters, progress bars, charts) use linear easing. This feels more
|
||||||
This feels more precise and mechanical.
|
precise and mechanical.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">respect preferences</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">respect preferences</h4>
|
||||||
<p className="font-sans text-sm text-foreground-secondary">
|
<p className="font-sans text-sm text-foreground-secondary">
|
||||||
Honor prefers-reduced-motion. All animations disable when the
|
Honor prefers-reduced-motion. All animations disable when the user has requested
|
||||||
user has requested reduced motion.
|
reduced motion.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,11 +46,31 @@ export function SectionMotion(): React.ReactElement {
|
||||||
|
|
||||||
<SubSection title="duration tokens">
|
<SubSection title="duration tokens">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<TokenRow name="--motion-instant" value="0ms" preview={<div className="w-full h-2 bg-accent" />} />
|
<TokenRow
|
||||||
<TokenRow name="--motion-fast" value="150ms" preview={<div className="w-full h-2 bg-accent motion-fast" />} />
|
name="--motion-instant"
|
||||||
<TokenRow name="--motion-base" value="200ms" preview={<div className="w-full h-2 bg-accent motion-base" />} />
|
value="0ms"
|
||||||
<TokenRow name="--motion-slow" value="300ms" preview={<div className="w-full h-2 bg-accent motion-slow" />} />
|
preview={<div className="w-full h-2 bg-accent" />}
|
||||||
<TokenRow name="--motion-slower" value="500ms" preview={<div className="w-full h-2 bg-accent" />} />
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--motion-fast"
|
||||||
|
value="150ms"
|
||||||
|
preview={<div className="w-full h-2 bg-accent motion-fast" />}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--motion-base"
|
||||||
|
value="200ms"
|
||||||
|
preview={<div className="w-full h-2 bg-accent motion-base" />}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--motion-slow"
|
||||||
|
value="300ms"
|
||||||
|
preview={<div className="w-full h-2 bg-accent motion-slow" />}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--motion-slower"
|
||||||
|
value="500ms"
|
||||||
|
preview={<div className="w-full h-2 bg-accent" />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
|
|
@ -66,9 +86,7 @@ export function SectionMotion(): React.ReactElement {
|
||||||
|
|
||||||
<SubSection title="interactive demo">
|
<SubSection title="interactive demo">
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<Button className="motion-fast transition-all hover:scale-105">
|
<Button className="motion-fast transition-all hover:scale-105">fast (150ms)</Button>
|
||||||
fast (150ms)
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" className="motion-base transition-all hover:scale-105">
|
<Button variant="secondary" className="motion-base transition-all hover:scale-105">
|
||||||
base (200ms)
|
base (200ms)
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,17 @@ export function SectionPatternFeedback(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="16. feedback patterns" id="feedback-patterns">
|
<FieldsetSection title="16. feedback patterns" id="feedback-patterns">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Feedback patterns communicate status, progress, and system responses.
|
Feedback patterns communicate status, progress, and system responses. Choose the right
|
||||||
Choose the right pattern based on context and urgency.
|
pattern based on context and urgency.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="feedback decision tree">
|
<SubSection title="feedback decision tree">
|
||||||
<div className="bg-surface p-6 border border-dashed border-border mb-6">
|
<div className="bg-surface p-6 border border-dashed border-border mb-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<Badge variant="info" className="mt-1">Q1</Badge>
|
<Badge variant="info" className="mt-1">
|
||||||
|
Q1
|
||||||
|
</Badge>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-mono text-sm mb-2">Is this a page-level system message?</p>
|
<p className="font-mono text-sm mb-2">Is this a page-level system message?</p>
|
||||||
<p className="font-sans text-xs text-foreground-secondary">
|
<p className="font-sans text-xs text-foreground-secondary">
|
||||||
|
|
@ -28,7 +30,9 @@ export function SectionPatternFeedback(): React.ReactElement {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<Badge variant="info" className="mt-1">Q2</Badge>
|
<Badge variant="info" className="mt-1">
|
||||||
|
Q2
|
||||||
|
</Badge>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-mono text-sm mb-2">Is it a response to a user action?</p>
|
<p className="font-mono text-sm mb-2">Is it a response to a user action?</p>
|
||||||
<p className="font-sans text-xs text-foreground-secondary">
|
<p className="font-sans text-xs text-foreground-secondary">
|
||||||
|
|
@ -37,7 +41,9 @@ export function SectionPatternFeedback(): React.ReactElement {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<Badge variant="info" className="mt-1">Q3</Badge>
|
<Badge variant="info" className="mt-1">
|
||||||
|
Q3
|
||||||
|
</Badge>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-mono text-sm mb-2">Is it contextual to a specific element?</p>
|
<p className="font-mono text-sm mb-2">Is it contextual to a specific element?</p>
|
||||||
<p className="font-sans text-xs text-foreground-secondary">
|
<p className="font-sans text-xs text-foreground-secondary">
|
||||||
|
|
@ -142,7 +148,9 @@ export function SectionPatternFeedback(): React.ReactElement {
|
||||||
<p className="font-sans text-xs text-foreground-secondary">
|
<p className="font-sans text-xs text-foreground-secondary">
|
||||||
This version has known vulnerabilities. Update immediately.
|
This version has known vulnerabilities. Update immediately.
|
||||||
</p>
|
</p>
|
||||||
<Button size="sm" variant="destructive" className="mt-3">update now</Button>
|
<Button size="sm" variant="destructive" className="mt-3">
|
||||||
|
update now
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -174,7 +182,9 @@ export function SectionPatternFeedback(): React.ReactElement {
|
||||||
Service degradation detected. Some API calls may fail. We are investigating.
|
Service degradation detected. Some API calls may fail. We are investigating.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" className="font-mono text-sm underline hover:no-underline">status page</a>
|
<a href="#" className="font-mono text-sm underline hover:no-underline">
|
||||||
|
status page
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Announcement banner */}
|
{/* Announcement banner */}
|
||||||
|
|
@ -185,7 +195,9 @@ export function SectionPatternFeedback(): React.ReactElement {
|
||||||
New: AI-powered code review is now available for all tools!
|
New: AI-powered code review is now available for all tools!
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="secondary" size="sm">learn more</Button>
|
<Button variant="secondary" size="sm">
|
||||||
|
learn more
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
@ -238,11 +250,14 @@ export function SectionPatternFeedback(): React.ReactElement {
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="font-mono text-sm font-medium mb-1">failed to load tools</p>
|
<p className="font-mono text-sm font-medium mb-1">failed to load tools</p>
|
||||||
<p className="font-sans text-xs text-foreground-secondary mb-4">
|
<p className="font-sans text-xs text-foreground-secondary mb-4">
|
||||||
We couldn't connect to the server. This could be a network issue or the service may be temporarily unavailable.
|
We couldn't connect to the server. This could be a network issue or the service may
|
||||||
|
be temporarily unavailable.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button size="sm">retry</Button>
|
<Button size="sm">retry</Button>
|
||||||
<Button size="sm" variant="ghost">view details</Button>
|
<Button size="sm" variant="ghost">
|
||||||
|
view details
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ export function SectionPatternForms(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="15. form patterns" id="form-patterns">
|
<FieldsetSection title="15. form patterns" id="form-patterns">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Consistent form patterns for data entry, validation, and submission.
|
Consistent form patterns for data entry, validation, and submission. Forms should guide
|
||||||
Forms should guide users through tasks with clear feedback.
|
users through tasks with clear feedback.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="validation timing">
|
<SubSection title="validation timing">
|
||||||
|
|
@ -55,9 +55,21 @@ export function SectionPatternForms(): React.ReactElement {
|
||||||
<span className="font-mono text-sm font-medium text-error">please fix 3 errors</span>
|
<span className="font-mono text-sm font-medium text-error">please fix 3 errors</span>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-1 text-sm text-foreground-secondary font-sans">
|
<ul className="space-y-1 text-sm text-foreground-secondary font-sans">
|
||||||
<li><a href="#name" className="text-error hover:underline">Name is required</a></li>
|
<li>
|
||||||
<li><a href="#email" className="text-error hover:underline">Email format is invalid</a></li>
|
<a href="#name" className="text-error hover:underline">
|
||||||
<li><a href="#description" className="text-error hover:underline">Description must be at least 50 characters</a></li>
|
Name is required
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#email" className="text-error hover:underline">
|
||||||
|
Email format is invalid
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#description" className="text-error hover:underline">
|
||||||
|
Description must be at least 50 characters
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -79,10 +91,7 @@ export function SectionPatternForms(): React.ReactElement {
|
||||||
<SubSection title="help text placement">
|
<SubSection title="help text placement">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<DoDontCard type="do" title="Help text below the input">
|
<DoDontCard type="do" title="Help text below the input">
|
||||||
<FormField
|
<FormField label="api key" helperText="You can find this in your dashboard settings">
|
||||||
label="api key"
|
|
||||||
helperText="You can find this in your dashboard settings"
|
|
||||||
>
|
|
||||||
<Input placeholder="sk-xxxx..." />
|
<Input placeholder="sk-xxxx..." />
|
||||||
</FormField>
|
</FormField>
|
||||||
</DoDontCard>
|
</DoDontCard>
|
||||||
|
|
@ -101,7 +110,9 @@ export function SectionPatternForms(): React.ReactElement {
|
||||||
<SubSection title="required vs optional">
|
<SubSection title="required vs optional">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<p className="font-mono text-xs text-foreground-tertiary mb-4">when most fields are required</p>
|
<p className="font-mono text-xs text-foreground-tertiary mb-4">
|
||||||
|
when most fields are required
|
||||||
|
</p>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FormField label="name" required>
|
<FormField label="name" required>
|
||||||
<Input placeholder="Tool name" />
|
<Input placeholder="Tool name" />
|
||||||
|
|
@ -115,7 +126,9 @@ export function SectionPatternForms(): React.ReactElement {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<p className="font-mono text-xs text-foreground-tertiary mb-4">when most fields are optional</p>
|
<p className="font-mono text-xs text-foreground-tertiary mb-4">
|
||||||
|
when most fields are optional
|
||||||
|
</p>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FormField label="website">
|
<FormField label="website">
|
||||||
<Input placeholder="https://example.com" />
|
<Input placeholder="https://example.com" />
|
||||||
|
|
@ -131,8 +144,8 @@ export function SectionPatternForms(): React.ReactElement {
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface p-4 border border-dashed border-border">
|
<div className="bg-surface p-4 border border-dashed border-border">
|
||||||
<p className="font-sans text-sm text-foreground-secondary">
|
<p className="font-sans text-sm text-foreground-secondary">
|
||||||
<strong>Rule:</strong> Mark the minority. If most fields are required, mark optional fields.
|
<strong>Rule:</strong> Mark the minority. If most fields are required, mark optional
|
||||||
If most are optional, mark required fields.
|
fields. If most are optional, mark required fields.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
@ -213,7 +226,9 @@ export function SectionPatternForms(): React.ReactElement {
|
||||||
Once deleted, this tool cannot be recovered.
|
Once deleted, this tool cannot be recovered.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="destructive" size="sm">delete tool</Button>
|
<Button variant="destructive" size="sm">
|
||||||
|
delete tool
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between py-4 border-t border-dashed border-error">
|
<div className="flex items-center justify-between py-4 border-t border-dashed border-error">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -222,7 +237,9 @@ export function SectionPatternForms(): React.ReactElement {
|
||||||
Transfer this tool to another user or organization.
|
Transfer this tool to another user or organization.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm">transfer</Button>
|
<Button variant="outline" size="sm">
|
||||||
|
transfer
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||||
import { Breadcrumbs, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbPage } from '@tpmjs/ui/Breadcrumbs/Breadcrumbs';
|
import {
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
Breadcrumbs,
|
||||||
|
} from '@tpmjs/ui/Breadcrumbs/Breadcrumbs';
|
||||||
import { Button } from '@tpmjs/ui/Button/Button';
|
import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
import { Tabs } from '@tpmjs/ui/Tabs/Tabs';
|
import { Tabs } from '@tpmjs/ui/Tabs/Tabs';
|
||||||
|
|
@ -14,8 +20,8 @@ export function SectionPatternNavigation(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="14. navigation patterns" id="nav-patterns">
|
<FieldsetSection title="14. navigation patterns" id="nav-patterns">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Navigation patterns for consistent wayfinding across the platform. Each pattern
|
Navigation patterns for consistent wayfinding across the platform. Each pattern has specific
|
||||||
has specific use cases and accessibility requirements.
|
use cases and accessibility requirements.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="global navigation (header)">
|
<SubSection title="global navigation (header)">
|
||||||
|
|
@ -28,14 +34,36 @@ export function SectionPatternNavigation(): React.ReactElement {
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<span className="font-mono text-lg font-semibold">tpmjs</span>
|
<span className="font-mono text-lg font-semibold">tpmjs</span>
|
||||||
<nav className="flex items-center gap-6">
|
<nav className="flex items-center gap-6">
|
||||||
<a href="#" className="font-mono text-sm text-foreground hover:text-accent transition-colors">tools</a>
|
<a
|
||||||
<a href="#" className="font-mono text-sm text-foreground-secondary hover:text-accent transition-colors">agents</a>
|
href="#"
|
||||||
<a href="#" className="font-mono text-sm text-foreground-secondary hover:text-accent transition-colors">docs</a>
|
className="font-mono text-sm text-foreground hover:text-accent transition-colors"
|
||||||
<a href="#" className="font-mono text-sm text-foreground-secondary hover:text-accent transition-colors">pricing</a>
|
>
|
||||||
|
tools
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="font-mono text-sm text-foreground-secondary hover:text-accent transition-colors"
|
||||||
|
>
|
||||||
|
agents
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="font-mono text-sm text-foreground-secondary hover:text-accent transition-colors"
|
||||||
|
>
|
||||||
|
docs
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="font-mono text-sm text-foreground-secondary hover:text-accent transition-colors"
|
||||||
|
>
|
||||||
|
pricing
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="ghost" size="sm">sign in</Button>
|
<Button variant="ghost" size="sm">
|
||||||
|
sign in
|
||||||
|
</Button>
|
||||||
<Button size="sm">get started</Button>
|
<Button size="sm">get started</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -60,20 +88,34 @@ export function SectionPatternNavigation(): React.ReactElement {
|
||||||
<div className="bg-surface border border-dashed border-border p-4">
|
<div className="bg-surface border border-dashed border-border p-4">
|
||||||
<p className="font-mono text-xs text-foreground-tertiary mb-4">expanded state</p>
|
<p className="font-mono text-xs text-foreground-tertiary mb-4">expanded state</p>
|
||||||
<nav className="space-y-1">
|
<nav className="space-y-1">
|
||||||
<a href="#" className="flex items-center gap-3 px-3 py-2 bg-accent/10 text-accent font-mono text-sm">
|
<a
|
||||||
|
href="#"
|
||||||
|
className="flex items-center gap-3 px-3 py-2 bg-accent/10 text-accent font-mono text-sm"
|
||||||
|
>
|
||||||
<Icon icon="home" size="sm" />
|
<Icon icon="home" size="sm" />
|
||||||
<span>dashboard</span>
|
<span>dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" className="flex items-center gap-3 px-3 py-2 text-foreground-secondary hover:bg-surface-2 font-mono text-sm">
|
<a
|
||||||
|
href="#"
|
||||||
|
className="flex items-center gap-3 px-3 py-2 text-foreground-secondary hover:bg-surface-2 font-mono text-sm"
|
||||||
|
>
|
||||||
<Icon icon="puzzle" size="sm" />
|
<Icon icon="puzzle" size="sm" />
|
||||||
<span>tools</span>
|
<span>tools</span>
|
||||||
<Badge size="sm" variant="outline" className="ml-auto">12</Badge>
|
<Badge size="sm" variant="outline" className="ml-auto">
|
||||||
|
12
|
||||||
|
</Badge>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" className="flex items-center gap-3 px-3 py-2 text-foreground-secondary hover:bg-surface-2 font-mono text-sm">
|
<a
|
||||||
|
href="#"
|
||||||
|
className="flex items-center gap-3 px-3 py-2 text-foreground-secondary hover:bg-surface-2 font-mono text-sm"
|
||||||
|
>
|
||||||
<Icon icon="key" size="sm" />
|
<Icon icon="key" size="sm" />
|
||||||
<span>api keys</span>
|
<span>api keys</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" className="flex items-center gap-3 px-3 py-2 text-foreground-secondary hover:bg-surface-2 font-mono text-sm">
|
<a
|
||||||
|
href="#"
|
||||||
|
className="flex items-center gap-3 px-3 py-2 text-foreground-secondary hover:bg-surface-2 font-mono text-sm"
|
||||||
|
>
|
||||||
<Icon icon="user" size="sm" />
|
<Icon icon="user" size="sm" />
|
||||||
<span>settings</span>
|
<span>settings</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -102,10 +144,20 @@ export function SectionPatternNavigation(): React.ReactElement {
|
||||||
<div className="mt-4 bg-surface p-4 border border-dashed border-border">
|
<div className="mt-4 bg-surface p-4 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-3">keyboard shortcuts</h4>
|
<h4 className="font-mono text-sm font-medium mb-3">keyboard shortcuts</h4>
|
||||||
<div className="grid grid-cols-2 gap-4 text-sm text-foreground-secondary font-sans">
|
<div className="grid grid-cols-2 gap-4 text-sm text-foreground-secondary font-sans">
|
||||||
<div><kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">[</kbd> collapse/expand sidebar</div>
|
<div>
|
||||||
<div><kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">g then h</kbd> go to home</div>
|
<kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">[</kbd> collapse/expand
|
||||||
<div><kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">g then t</kbd> go to tools</div>
|
sidebar
|
||||||
<div><kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">g then s</kbd> go to settings</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">g then h</kbd> go to home
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">g then t</kbd> go to tools
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">g then s</kbd> go to
|
||||||
|
settings
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
@ -181,10 +233,23 @@ export function SectionPatternNavigation(): React.ReactElement {
|
||||||
<div className="bg-surface p-4 border border-dashed border-border">
|
<div className="bg-surface p-4 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-3">keyboard behavior</h4>
|
<h4 className="font-mono text-sm font-medium mb-3">keyboard behavior</h4>
|
||||||
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
||||||
<li>• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">←</kbd> / <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">→</kbd> navigate between tabs</li>
|
<li>
|
||||||
<li>• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Home</kbd> focus first tab</li>
|
• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">←</kbd> /{' '}
|
||||||
<li>• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">End</kbd> focus last tab</li>
|
<kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">→</kbd> navigate between
|
||||||
<li>• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Enter</kbd> / <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Space</kbd> activate focused tab</li>
|
tabs
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Home</kbd> focus first
|
||||||
|
tab
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">End</kbd> focus last tab
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Enter</kbd> /{' '}
|
||||||
|
<kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Space</kbd> activate
|
||||||
|
focused tab
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -205,13 +270,28 @@ export function SectionPatternNavigation(): React.ReactElement {
|
||||||
</div>
|
</div>
|
||||||
{/* Expanded menu preview */}
|
{/* Expanded menu preview */}
|
||||||
<div className="p-4 space-y-2">
|
<div className="p-4 space-y-2">
|
||||||
<a href="#" className="block px-3 py-2 font-mono text-sm text-foreground bg-accent/10">tools</a>
|
<a
|
||||||
<a href="#" className="block px-3 py-2 font-mono text-sm text-foreground-secondary">agents</a>
|
href="#"
|
||||||
<a href="#" className="block px-3 py-2 font-mono text-sm text-foreground-secondary">docs</a>
|
className="block px-3 py-2 font-mono text-sm text-foreground bg-accent/10"
|
||||||
<a href="#" className="block px-3 py-2 font-mono text-sm text-foreground-secondary">pricing</a>
|
>
|
||||||
|
tools
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-3 py-2 font-mono text-sm text-foreground-secondary">
|
||||||
|
agents
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-3 py-2 font-mono text-sm text-foreground-secondary">
|
||||||
|
docs
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-3 py-2 font-mono text-sm text-foreground-secondary">
|
||||||
|
pricing
|
||||||
|
</a>
|
||||||
<div className="pt-4 border-t border-border space-y-2">
|
<div className="pt-4 border-t border-border space-y-2">
|
||||||
<Button variant="outline" size="sm" className="w-full">sign in</Button>
|
<Button variant="outline" size="sm" className="w-full">
|
||||||
<Button size="sm" className="w-full">get started</Button>
|
sign in
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" className="w-full">
|
||||||
|
get started
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="18. search & filtering" id="search-patterns">
|
<FieldsetSection title="18. search & filtering" id="search-patterns">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Search and filter patterns help users find content quickly.
|
Search and filter patterns help users find content quickly. Design for progressive
|
||||||
Design for progressive disclosure and instant feedback.
|
disclosure and instant feedback.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="search box states">
|
<SubSection title="search box states">
|
||||||
|
|
@ -24,7 +24,11 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
<div className="bg-surface p-4 border border-dashed border-border">
|
<div className="bg-surface p-4 border border-dashed border-border">
|
||||||
<p className="font-mono text-xs text-foreground-tertiary mb-3">default</p>
|
<p className="font-mono text-xs text-foreground-tertiary mb-3">default</p>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon icon="search" size="sm" className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary" />
|
<Icon
|
||||||
|
icon="search"
|
||||||
|
size="sm"
|
||||||
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary"
|
||||||
|
/>
|
||||||
<Input placeholder="search tools..." className="pl-10" />
|
<Input placeholder="search tools..." className="pl-10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -33,7 +37,11 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
<div className="bg-surface p-4 border border-dashed border-border">
|
<div className="bg-surface p-4 border border-dashed border-border">
|
||||||
<p className="font-mono text-xs text-foreground-tertiary mb-3">active / has value</p>
|
<p className="font-mono text-xs text-foreground-tertiary mb-3">active / has value</p>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon icon="search" size="sm" className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary" />
|
<Icon
|
||||||
|
icon="search"
|
||||||
|
size="sm"
|
||||||
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary"
|
||||||
|
/>
|
||||||
<Input defaultValue="parser" className="pl-10 pr-10" />
|
<Input defaultValue="parser" className="pl-10 pr-10" />
|
||||||
<button className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-tertiary hover:text-foreground">
|
<button className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-tertiary hover:text-foreground">
|
||||||
<Icon icon="x" size="sm" />
|
<Icon icon="x" size="sm" />
|
||||||
|
|
@ -45,7 +53,11 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
<div className="bg-surface p-4 border border-dashed border-border">
|
<div className="bg-surface p-4 border border-dashed border-border">
|
||||||
<p className="font-mono text-xs text-foreground-tertiary mb-3">loading</p>
|
<p className="font-mono text-xs text-foreground-tertiary mb-3">loading</p>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon icon="loader" size="sm" className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary animate-spin" />
|
<Icon
|
||||||
|
icon="loader"
|
||||||
|
size="sm"
|
||||||
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary animate-spin"
|
||||||
|
/>
|
||||||
<Input defaultValue="validator" className="pl-10" readOnly />
|
<Input defaultValue="validator" className="pl-10" readOnly />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -54,7 +66,11 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
<div className="bg-surface p-4 border border-dashed border-border">
|
<div className="bg-surface p-4 border border-dashed border-border">
|
||||||
<p className="font-mono text-xs text-foreground-tertiary mb-3">no results</p>
|
<p className="font-mono text-xs text-foreground-tertiary mb-3">no results</p>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon icon="search" size="sm" className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary" />
|
<Icon
|
||||||
|
icon="search"
|
||||||
|
size="sm"
|
||||||
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary"
|
||||||
|
/>
|
||||||
<Input defaultValue="xyzabc123" className="pl-10 pr-10" />
|
<Input defaultValue="xyzabc123" className="pl-10 pr-10" />
|
||||||
<button className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-tertiary hover:text-foreground">
|
<button className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-tertiary hover:text-foreground">
|
||||||
<Icon icon="x" size="sm" />
|
<Icon icon="x" size="sm" />
|
||||||
|
|
@ -91,9 +107,7 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
</Badge>
|
</Badge>
|
||||||
<button className="font-mono text-xs text-accent hover:underline">clear all</button>
|
<button className="font-mono text-xs text-accent hover:underline">clear all</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-mono text-xs text-foreground-secondary">
|
<p className="font-mono text-xs text-foreground-secondary">showing 42 of 128 tools</p>
|
||||||
showing 42 of 128 tools
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
|
|
@ -107,7 +121,9 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">filters</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">filters</h4>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="font-mono text-xs text-foreground-secondary block mb-2">category</label>
|
<label className="font-mono text-xs text-foreground-secondary block mb-2">
|
||||||
|
category
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
placeholder="all categories"
|
placeholder="all categories"
|
||||||
options={[
|
options={[
|
||||||
|
|
@ -118,7 +134,9 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="font-mono text-xs text-foreground-secondary block mb-2">status</label>
|
<label className="font-mono text-xs text-foreground-secondary block mb-2">
|
||||||
|
status
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
placeholder="all statuses"
|
placeholder="all statuses"
|
||||||
options={[
|
options={[
|
||||||
|
|
@ -129,11 +147,15 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="font-mono text-xs text-foreground-secondary block mb-2">min downloads</label>
|
<label className="font-mono text-xs text-foreground-secondary block mb-2">
|
||||||
|
min downloads
|
||||||
|
</label>
|
||||||
<Input type="number" placeholder="0" />
|
<Input type="number" placeholder="0" />
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4 border-t border-dashed border-border">
|
<div className="pt-4 border-t border-dashed border-border">
|
||||||
<Button size="sm" className="w-full">apply filters</Button>
|
<Button size="sm" className="w-full">
|
||||||
|
apply filters
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -143,9 +165,14 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
<p className="font-mono text-xs text-foreground-tertiary mb-4">results area</p>
|
<p className="font-mono text-xs text-foreground-tertiary mb-4">results area</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{['@tpmjs/parser', '@tpmjs/validator', '@tpmjs/transform'].map((name) => (
|
{['@tpmjs/parser', '@tpmjs/validator', '@tpmjs/transform'].map((name) => (
|
||||||
<div key={name} className="p-3 border border-dashed border-border flex items-center justify-between">
|
<div
|
||||||
|
key={name}
|
||||||
|
className="p-3 border border-dashed border-border flex items-center justify-between"
|
||||||
|
>
|
||||||
<span className="font-mono text-sm">{name}</span>
|
<span className="font-mono text-sm">{name}</span>
|
||||||
<Badge variant="success" size="sm">active</Badge>
|
<Badge variant="success" size="sm">
|
||||||
|
active
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -161,7 +188,9 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-mono text-sm font-medium">saved views</span>
|
<span className="font-mono text-sm font-medium">saved views</span>
|
||||||
<Badge variant="outline" size="sm">3</Badge>
|
<Badge variant="outline" size="sm">
|
||||||
|
3
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" variant="ghost">
|
<Button size="sm" variant="ghost">
|
||||||
<Icon icon="plus" size="sm" className="mr-2" />
|
<Icon icon="plus" size="sm" className="mr-2" />
|
||||||
|
|
@ -172,17 +201,23 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
<button className="w-full text-left px-3 py-2 bg-accent/10 font-mono text-sm flex items-center justify-between">
|
<button className="w-full text-left px-3 py-2 bg-accent/10 font-mono text-sm flex items-center justify-between">
|
||||||
<span>my active tools</span>
|
<span>my active tools</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="outline" size="sm">42 results</Badge>
|
<Badge variant="outline" size="sm">
|
||||||
|
42 results
|
||||||
|
</Badge>
|
||||||
<Icon icon="check" size="sm" className="text-accent" />
|
<Icon icon="check" size="sm" className="text-accent" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button className="w-full text-left px-3 py-2 hover:bg-surface-2 font-mono text-sm flex items-center justify-between">
|
<button className="w-full text-left px-3 py-2 hover:bg-surface-2 font-mono text-sm flex items-center justify-between">
|
||||||
<span>deprecated packages</span>
|
<span>deprecated packages</span>
|
||||||
<Badge variant="outline" size="sm">8 results</Badge>
|
<Badge variant="outline" size="sm">
|
||||||
|
8 results
|
||||||
|
</Badge>
|
||||||
</button>
|
</button>
|
||||||
<button className="w-full text-left px-3 py-2 hover:bg-surface-2 font-mono text-sm flex items-center justify-between">
|
<button className="w-full text-left px-3 py-2 hover:bg-surface-2 font-mono text-sm flex items-center justify-between">
|
||||||
<span>high-download tools</span>
|
<span>high-download tools</span>
|
||||||
<Badge variant="outline" size="sm">15 results</Badge>
|
<Badge variant="outline" size="sm">
|
||||||
|
15 results
|
||||||
|
</Badge>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -225,8 +260,15 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
{/* Search input */}
|
{/* Search input */}
|
||||||
<div className="border-b border-border p-3">
|
<div className="border-b border-border p-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon icon="search" size="sm" className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary" />
|
<Icon
|
||||||
<Input placeholder="search or type a command..." className="pl-10 border-0 focus:ring-0" />
|
icon="search"
|
||||||
|
size="sm"
|
||||||
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="search or type a command..."
|
||||||
|
className="pl-10 border-0 focus:ring-0"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -234,7 +276,9 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
<div className="max-h-80 overflow-y-auto">
|
<div className="max-h-80 overflow-y-auto">
|
||||||
{/* Recent */}
|
{/* Recent */}
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<p className="font-mono text-[10px] text-foreground-tertiary uppercase tracking-wide px-2 py-1">recent</p>
|
<p className="font-mono text-[10px] text-foreground-tertiary uppercase tracking-wide px-2 py-1">
|
||||||
|
recent
|
||||||
|
</p>
|
||||||
<button className="w-full text-left px-3 py-2 hover:bg-surface-2 flex items-center gap-3">
|
<button className="w-full text-left px-3 py-2 hover:bg-surface-2 flex items-center gap-3">
|
||||||
<Icon icon="clock" size="sm" className="text-foreground-tertiary" />
|
<Icon icon="clock" size="sm" className="text-foreground-tertiary" />
|
||||||
<span className="font-mono text-sm">@tpmjs/parser</span>
|
<span className="font-mono text-sm">@tpmjs/parser</span>
|
||||||
|
|
@ -243,7 +287,9 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="p-2 border-t border-border">
|
<div className="p-2 border-t border-border">
|
||||||
<p className="font-mono text-[10px] text-foreground-tertiary uppercase tracking-wide px-2 py-1">actions</p>
|
<p className="font-mono text-[10px] text-foreground-tertiary uppercase tracking-wide px-2 py-1">
|
||||||
|
actions
|
||||||
|
</p>
|
||||||
<button className="w-full text-left px-3 py-2 hover:bg-surface-2 flex items-center justify-between">
|
<button className="w-full text-left px-3 py-2 hover:bg-surface-2 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Icon icon="plus" size="sm" className="text-foreground-tertiary" />
|
<Icon icon="plus" size="sm" className="text-foreground-tertiary" />
|
||||||
|
|
@ -271,16 +317,24 @@ export function SectionPatternSearch(): React.ReactElement {
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="border-t border-border px-3 py-2 flex items-center justify-between text-[10px] text-foreground-tertiary">
|
<div className="border-t border-border px-3 py-2 flex items-center justify-between text-[10px] text-foreground-tertiary">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span><kbd className="bg-surface-2 px-1.5 py-0.5">↑↓</kbd> navigate</span>
|
<span>
|
||||||
<span><kbd className="bg-surface-2 px-1.5 py-0.5">↵</kbd> select</span>
|
<kbd className="bg-surface-2 px-1.5 py-0.5">↑↓</kbd> navigate
|
||||||
<span><kbd className="bg-surface-2 px-1.5 py-0.5">esc</kbd> close</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
<kbd className="bg-surface-2 px-1.5 py-0.5">↵</kbd> select
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<kbd className="bg-surface-2 px-1.5 py-0.5">esc</kbd> close
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 text-center">
|
<div className="mt-4 text-center">
|
||||||
<kbd className="font-mono text-xs bg-surface px-3 py-1.5 border border-border">⌘K</kbd>
|
<kbd className="font-mono text-xs bg-surface px-3 py-1.5 border border-border">⌘K</kbd>
|
||||||
<span className="font-sans text-xs text-foreground-secondary ml-2">to open command palette</span>
|
<span className="font-sans text-xs text-foreground-secondary ml-2">
|
||||||
|
to open command palette
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
</FieldsetSection>
|
</FieldsetSection>
|
||||||
|
|
|
||||||
|
|
@ -21,24 +21,57 @@ export function SectionPatternTables(): React.ReactElement {
|
||||||
const [selectedRows, setSelectedRows] = useState<string[]>([]);
|
const [selectedRows, setSelectedRows] = useState<string[]>([]);
|
||||||
|
|
||||||
const mockData = [
|
const mockData = [
|
||||||
{ id: '1', name: '@tpmjs/parser', category: 'utility', downloads: 125432, status: 'active', score: 0.92 },
|
{
|
||||||
{ id: '2', name: '@tpmjs/validator', category: 'validation', downloads: 89231, status: 'active', score: 0.87 },
|
id: '1',
|
||||||
{ id: '3', name: '@tpmjs/transform', category: 'data', downloads: 45678, status: 'beta', score: 0.81 },
|
name: '@tpmjs/parser',
|
||||||
{ id: '4', name: '@tpmjs/executor', category: 'runtime', downloads: 34521, status: 'active', score: 0.78 },
|
category: 'utility',
|
||||||
{ id: '5', name: '@tpmjs/config', category: 'utility', downloads: 23456, status: 'deprecated', score: 0.65 },
|
downloads: 125432,
|
||||||
|
status: 'active',
|
||||||
|
score: 0.92,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: '@tpmjs/validator',
|
||||||
|
category: 'validation',
|
||||||
|
downloads: 89231,
|
||||||
|
status: 'active',
|
||||||
|
score: 0.87,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: '@tpmjs/transform',
|
||||||
|
category: 'data',
|
||||||
|
downloads: 45678,
|
||||||
|
status: 'beta',
|
||||||
|
score: 0.81,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: '@tpmjs/executor',
|
||||||
|
category: 'runtime',
|
||||||
|
downloads: 34521,
|
||||||
|
status: 'active',
|
||||||
|
score: 0.78,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: '@tpmjs/config',
|
||||||
|
category: 'utility',
|
||||||
|
downloads: 23456,
|
||||||
|
status: 'deprecated',
|
||||||
|
score: 0.65,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const toggleRow = (id: string) => {
|
const toggleRow = (id: string) => {
|
||||||
setSelectedRows(prev =>
|
setSelectedRows((prev) => (prev.includes(id) ? prev.filter((r) => r !== id) : [...prev, id]));
|
||||||
prev.includes(id) ? prev.filter(r => r !== id) : [...prev, id]
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAll = () => {
|
const toggleAll = () => {
|
||||||
if (selectedRows.length === mockData.length) {
|
if (selectedRows.length === mockData.length) {
|
||||||
setSelectedRows([]);
|
setSelectedRows([]);
|
||||||
} else {
|
} else {
|
||||||
setSelectedRows(mockData.map(d => d.id));
|
setSelectedRows(mockData.map((d) => d.id));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -77,7 +110,9 @@ export function SectionPatternTables(): React.ReactElement {
|
||||||
<TableRow key={row.id}>
|
<TableRow key={row.id}>
|
||||||
<TableCell className="font-mono">{row.name}</TableCell>
|
<TableCell className="font-mono">{row.name}</TableCell>
|
||||||
<TableCell>{row.category}</TableCell>
|
<TableCell>{row.category}</TableCell>
|
||||||
<TableCell className="text-right font-mono">{row.downloads.toLocaleString()}</TableCell>
|
<TableCell className="text-right font-mono">
|
||||||
|
{row.downloads.toLocaleString()}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-right font-mono">{row.score}</TableCell>
|
<TableCell className="text-right font-mono">{row.score}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
@ -98,8 +133,12 @@ export function SectionPatternTables(): React.ReactElement {
|
||||||
{selectedRows.length} item{selectedRows.length !== 1 ? 's' : ''} selected
|
{selectedRows.length} item{selectedRows.length !== 1 ? 's' : ''} selected
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button size="sm" variant="outline">export</Button>
|
<Button size="sm" variant="outline">
|
||||||
<Button size="sm" variant="destructive">delete</Button>
|
export
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="destructive">
|
||||||
|
delete
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -121,7 +160,10 @@ export function SectionPatternTables(): React.ReactElement {
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{mockData.map((row) => (
|
{mockData.map((row) => (
|
||||||
<TableRow key={row.id} className={selectedRows.includes(row.id) ? 'bg-accent/5' : ''}>
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
className={selectedRows.includes(row.id) ? 'bg-accent/5' : ''}
|
||||||
|
>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedRows.includes(row.id)}
|
checked={selectedRows.includes(row.id)}
|
||||||
|
|
@ -133,13 +175,21 @@ export function SectionPatternTables(): React.ReactElement {
|
||||||
<TableCell>{row.category}</TableCell>
|
<TableCell>{row.category}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<Badge
|
||||||
variant={row.status === 'active' ? 'success' : row.status === 'deprecated' ? 'error' : 'warning'}
|
variant={
|
||||||
|
row.status === 'active'
|
||||||
|
? 'success'
|
||||||
|
: row.status === 'deprecated'
|
||||||
|
? 'error'
|
||||||
|
: 'warning'
|
||||||
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{row.status}
|
{row.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right font-mono">{row.downloads.toLocaleString()}</TableCell>
|
<TableCell className="text-right font-mono">
|
||||||
|
{row.downloads.toLocaleString()}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|
@ -167,23 +217,13 @@ export function SectionPatternTables(): React.ReactElement {
|
||||||
{/* Simple pagination */}
|
{/* Simple pagination */}
|
||||||
<div className="bg-surface p-4 border border-dashed border-border">
|
<div className="bg-surface p-4 border border-dashed border-border">
|
||||||
<p className="font-mono text-xs text-foreground-tertiary mb-4">simple pagination</p>
|
<p className="font-mono text-xs text-foreground-tertiary mb-4">simple pagination</p>
|
||||||
<Pagination
|
<Pagination page={page} totalPages={10} onPageChange={setPage} variant="simple" />
|
||||||
page={page}
|
|
||||||
totalPages={10}
|
|
||||||
onPageChange={setPage}
|
|
||||||
variant="simple"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Minimal pagination */}
|
{/* Minimal pagination */}
|
||||||
<div className="bg-surface p-4 border border-dashed border-border">
|
<div className="bg-surface p-4 border border-dashed border-border">
|
||||||
<p className="font-mono text-xs text-foreground-tertiary mb-4">minimal pagination</p>
|
<p className="font-mono text-xs text-foreground-tertiary mb-4">minimal pagination</p>
|
||||||
<Pagination
|
<Pagination page={page} totalPages={10} onPageChange={setPage} variant="minimal" />
|
||||||
page={page}
|
|
||||||
totalPages={10}
|
|
||||||
onPageChange={setPage}
|
|
||||||
variant="minimal"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
@ -215,9 +255,15 @@ export function SectionPatternTables(): React.ReactElement {
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{[1, 2, 3].map((i) => (
|
{[1, 2, 3].map((i) => (
|
||||||
<TableRow key={i}>
|
<TableRow key={i}>
|
||||||
<TableCell><div className="h-4 bg-muted animate-pulse w-32" /></TableCell>
|
<TableCell>
|
||||||
<TableCell><div className="h-4 bg-muted animate-pulse w-20" /></TableCell>
|
<div className="h-4 bg-muted animate-pulse w-32" />
|
||||||
<TableCell className="text-right"><div className="h-4 bg-muted animate-pulse w-16 ml-auto" /></TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="h-4 bg-muted animate-pulse w-20" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="h-4 bg-muted animate-pulse w-16 ml-auto" />
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ export function SectionPrinciples(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="1. design principles" id="principles">
|
<FieldsetSection title="1. design principles" id="principles">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
These principles guide every design decision in TPMJS. They ensure consistency
|
These principles guide every design decision in TPMJS. They ensure consistency and prevent
|
||||||
and prevent drift as the system grows.
|
drift as the system grows.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-10">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-10">
|
||||||
|
|
@ -46,14 +46,14 @@ export function SectionPrinciples(): React.ReactElement {
|
||||||
<SubSection title="design philosophy">
|
<SubSection title="design philosophy">
|
||||||
<div className="bg-surface-2 p-6 border border-dashed border-border prose-width">
|
<div className="bg-surface-2 p-6 border border-dashed border-border prose-width">
|
||||||
<p className="font-sans text-foreground leading-relaxed mb-4">
|
<p className="font-sans text-foreground leading-relaxed mb-4">
|
||||||
TPMJS is a <strong>developer platform</strong> for AI tools. The design reflects
|
TPMJS is a <strong>developer platform</strong> for AI tools. The design reflects this
|
||||||
this through industrial aesthetics: sharp edges, technical typography, and
|
through industrial aesthetics: sharp edges, technical typography, and a muted palette
|
||||||
a muted palette with copper as the signal color.
|
with copper as the signal color.
|
||||||
</p>
|
</p>
|
||||||
<p className="font-sans text-foreground-secondary leading-relaxed">
|
<p className="font-sans text-foreground-secondary leading-relaxed">
|
||||||
Unlike consumer products that aim for delight, TPMJS aims for
|
Unlike consumer products that aim for delight, TPMJS aims for
|
||||||
<strong> efficiency and trust</strong>. Users should feel confident that the
|
<strong> efficiency and trust</strong>. Users should feel confident that the interface
|
||||||
interface will behave predictably and help them accomplish tasks quickly.
|
will behave predictably and help them accomplish tasks quickly.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ export function SectionSpacing(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="4. spacing" id="spacing">
|
<FieldsetSection title="4. spacing" id="spacing">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Consistent spacing creates visual rhythm. Use the 4px base unit
|
Consistent spacing creates visual rhythm. Use the 4px base unit and generous whitespace for
|
||||||
and generous whitespace for clarity.
|
clarity.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="spacing scale">
|
<SubSection title="spacing scale">
|
||||||
|
|
@ -15,10 +15,7 @@ export function SectionSpacing(): React.ReactElement {
|
||||||
{[1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64].map((space) => (
|
{[1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64].map((space) => (
|
||||||
<div key={space} className="flex items-center gap-4">
|
<div key={space} className="flex items-center gap-4">
|
||||||
<span className="font-mono text-xs text-foreground-secondary w-12">{space}</span>
|
<span className="font-mono text-xs text-foreground-secondary w-12">{space}</span>
|
||||||
<div
|
<div className="bg-accent h-4" style={{ width: `${space * 4}px` }} />
|
||||||
className="bg-accent h-4"
|
|
||||||
style={{ width: `${space * 4}px` }}
|
|
||||||
/>
|
|
||||||
<span className="font-mono text-xs text-foreground-tertiary">{space * 4}px</span>
|
<span className="font-mono text-xs text-foreground-tertiary">{space * 4}px</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,17 @@ export function SectionTheming(): React.ReactElement {
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<h4 className="font-mono text-sm font-medium mb-4">for sdk users embedding tpmjs ui</h4>
|
<h4 className="font-mono text-sm font-medium mb-4">for sdk users embedding tpmjs ui</h4>
|
||||||
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
|
||||||
<li>• Import the CSS variables from <code className="font-mono bg-surface-2 px-1">@tpmjs/ui/styles</code></li>
|
<li>
|
||||||
|
• Import the CSS variables from{' '}
|
||||||
|
<code className="font-mono bg-surface-2 px-1">@tpmjs/ui/styles</code>
|
||||||
|
</li>
|
||||||
<li>• Override semantic tokens in your own CSS to match your brand</li>
|
<li>• Override semantic tokens in your own CSS to match your brand</li>
|
||||||
<li>• Do not override core tokens (raw values)</li>
|
<li>• Do not override core tokens (raw values)</li>
|
||||||
<li>• Test both light and dark modes if supporting theme switching</li>
|
<li>• Test both light and dark modes if supporting theme switching</li>
|
||||||
<li>• Use <code className="font-mono bg-surface-2 px-1">data-density</code> attribute for density control</li>
|
<li>
|
||||||
|
• Use <code className="font-mono bg-surface-2 px-1">data-density</code> attribute for
|
||||||
|
density control
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ export function SectionTypography(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<FieldsetSection title="3. typography" id="typography">
|
<FieldsetSection title="3. typography" id="typography">
|
||||||
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
<p className="text-foreground-secondary mb-8 font-sans prose-width">
|
||||||
Two font families create clear hierarchy: monospace for headings and technical
|
Two font families create clear hierarchy: monospace for headings and technical content,
|
||||||
content, sans-serif for body text and descriptions.
|
sans-serif for body text and descriptions.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SubSection title="font families">
|
<SubSection title="font families">
|
||||||
|
|
@ -15,37 +15,97 @@ export function SectionTypography(): React.ReactElement {
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<p className="font-mono text-2xl mb-2">JetBrains Mono</p>
|
<p className="font-mono text-2xl mb-2">JetBrains Mono</p>
|
||||||
<p className="font-mono text-sm text-foreground-secondary mb-4">--font-mono</p>
|
<p className="font-mono text-sm text-foreground-secondary mb-4">--font-mono</p>
|
||||||
<p className="font-mono text-sm">Used for: headings, code, data, labels, technical content</p>
|
<p className="font-mono text-sm">
|
||||||
|
Used for: headings, code, data, labels, technical content
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<p className="font-sans text-2xl mb-2">Inter</p>
|
<p className="font-sans text-2xl mb-2">Inter</p>
|
||||||
<p className="font-mono text-sm text-foreground-secondary mb-4">--font-sans</p>
|
<p className="font-mono text-sm text-foreground-secondary mb-4">--font-sans</p>
|
||||||
<p className="font-sans text-sm">Used for: body text, descriptions, long-form content</p>
|
<p className="font-sans text-sm">
|
||||||
|
Used for: body text, descriptions, long-form content
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
<SubSection title="type scale">
|
<SubSection title="type scale">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<TokenRow name="--text-xs" value="12px / 0.75rem" preview={<span className="text-xs font-mono">Aa</span>} />
|
<TokenRow
|
||||||
<TokenRow name="--text-sm" value="14px / 0.875rem" preview={<span className="text-sm font-mono">Aa</span>} />
|
name="--text-xs"
|
||||||
<TokenRow name="--text-base" value="16px / 1rem" preview={<span className="text-base font-mono">Aa</span>} />
|
value="12px / 0.75rem"
|
||||||
<TokenRow name="--text-lg" value="18px / 1.125rem" preview={<span className="text-lg font-mono">Aa</span>} />
|
preview={<span className="text-xs font-mono">Aa</span>}
|
||||||
<TokenRow name="--text-xl" value="20px / 1.25rem" preview={<span className="text-xl font-mono">Aa</span>} />
|
/>
|
||||||
<TokenRow name="--text-2xl" value="24px / 1.5rem" preview={<span className="text-2xl font-mono">Aa</span>} />
|
<TokenRow
|
||||||
<TokenRow name="--text-3xl" value="32px / 2rem" preview={<span className="text-3xl font-mono">Aa</span>} />
|
name="--text-sm"
|
||||||
<TokenRow name="--text-4xl" value="40px / 2.5rem" preview={<span className="text-4xl font-mono">Aa</span>} />
|
value="14px / 0.875rem"
|
||||||
<TokenRow name="--text-5xl" value="48px / 3rem" preview={<span className="text-5xl font-mono">Aa</span>} />
|
preview={<span className="text-sm font-mono">Aa</span>}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--text-base"
|
||||||
|
value="16px / 1rem"
|
||||||
|
preview={<span className="text-base font-mono">Aa</span>}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--text-lg"
|
||||||
|
value="18px / 1.125rem"
|
||||||
|
preview={<span className="text-lg font-mono">Aa</span>}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--text-xl"
|
||||||
|
value="20px / 1.25rem"
|
||||||
|
preview={<span className="text-xl font-mono">Aa</span>}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--text-2xl"
|
||||||
|
value="24px / 1.5rem"
|
||||||
|
preview={<span className="text-2xl font-mono">Aa</span>}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--text-3xl"
|
||||||
|
value="32px / 2rem"
|
||||||
|
preview={<span className="text-3xl font-mono">Aa</span>}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--text-4xl"
|
||||||
|
value="40px / 2.5rem"
|
||||||
|
preview={<span className="text-4xl font-mono">Aa</span>}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--text-5xl"
|
||||||
|
value="48px / 3rem"
|
||||||
|
preview={<span className="text-5xl font-mono">Aa</span>}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
<SubSection title="line height">
|
<SubSection title="line height">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<TokenRow name="--leading-tight" value="1.2" preview={<div className="w-full h-3 bg-accent/20" />} />
|
<TokenRow
|
||||||
<TokenRow name="--leading-snug" value="1.4" preview={<div className="w-full h-4 bg-accent/20" />} />
|
name="--leading-tight"
|
||||||
<TokenRow name="--leading-normal" value="1.6" preview={<div className="w-full h-5 bg-accent/20" />} />
|
value="1.2"
|
||||||
<TokenRow name="--leading-relaxed" value="1.7" preview={<div className="w-full h-6 bg-accent/20" />} />
|
preview={<div className="w-full h-3 bg-accent/20" />}
|
||||||
<TokenRow name="--leading-loose" value="1.8" preview={<div className="w-full h-7 bg-accent/20" />} />
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--leading-snug"
|
||||||
|
value="1.4"
|
||||||
|
preview={<div className="w-full h-4 bg-accent/20" />}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--leading-normal"
|
||||||
|
value="1.6"
|
||||||
|
preview={<div className="w-full h-5 bg-accent/20" />}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--leading-relaxed"
|
||||||
|
value="1.7"
|
||||||
|
preview={<div className="w-full h-6 bg-accent/20" />}
|
||||||
|
/>
|
||||||
|
<TokenRow
|
||||||
|
name="--leading-loose"
|
||||||
|
value="1.8"
|
||||||
|
preview={<div className="w-full h-7 bg-accent/20" />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
||||||
|
|
@ -57,9 +117,9 @@ export function SectionTypography(): React.ReactElement {
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface p-6 border border-dashed border-border">
|
<div className="bg-surface p-6 border border-dashed border-border">
|
||||||
<p className="font-sans text-sm text-foreground-secondary leading-relaxed prose-width">
|
<p className="font-sans text-sm text-foreground-secondary leading-relaxed prose-width">
|
||||||
This paragraph is constrained to 65 characters per line, the optimal width
|
This paragraph is constrained to 65 characters per line, the optimal width for reading
|
||||||
for reading comprehension. Lines that are too long cause eye fatigue, while
|
comprehension. Lines that are too long cause eye fatigue, while lines that are too short
|
||||||
lines that are too short disrupt reading rhythm.
|
disrupt reading rhythm.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|
@ -91,10 +151,18 @@ export function SectionTypography(): React.ReactElement {
|
||||||
|
|
||||||
<SubSection title="heading hierarchy">
|
<SubSection title="heading hierarchy">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h1 className="font-mono text-5xl font-semibold tracking-tight lowercase">heading 1 (48px)</h1>
|
<h1 className="font-mono text-5xl font-semibold tracking-tight lowercase">
|
||||||
<h2 className="font-mono text-4xl font-semibold tracking-tight lowercase">heading 2 (40px)</h2>
|
heading 1 (48px)
|
||||||
<h3 className="font-mono text-3xl font-semibold tracking-tight lowercase">heading 3 (32px)</h3>
|
</h1>
|
||||||
<h4 className="font-mono text-2xl font-semibold tracking-tight lowercase">heading 4 (24px)</h4>
|
<h2 className="font-mono text-4xl font-semibold tracking-tight lowercase">
|
||||||
|
heading 2 (40px)
|
||||||
|
</h2>
|
||||||
|
<h3 className="font-mono text-3xl font-semibold tracking-tight lowercase">
|
||||||
|
heading 3 (32px)
|
||||||
|
</h3>
|
||||||
|
<h4 className="font-mono text-2xl font-semibold tracking-tight lowercase">
|
||||||
|
heading 4 (24px)
|
||||||
|
</h4>
|
||||||
<h5 className="font-mono text-xl font-medium lowercase">heading 5 (20px)</h5>
|
<h5 className="font-mono text-xl font-medium lowercase">heading 5 (20px)</h5>
|
||||||
<h6 className="font-mono text-lg font-medium lowercase">heading 6 (18px)</h6>
|
<h6 className="font-mono text-lg font-medium lowercase">heading 6 (18px)</h6>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,36 @@
|
||||||
// Shared helpers
|
// Shared helpers
|
||||||
export {
|
|
||||||
FieldsetSection,
|
|
||||||
SubSection,
|
|
||||||
ColorCard,
|
|
||||||
DoDontCard,
|
|
||||||
PrincipleCard,
|
|
||||||
TokenRow,
|
|
||||||
NavItem,
|
|
||||||
} from './shared';
|
|
||||||
|
|
||||||
// Foundation sections
|
|
||||||
export { SectionPrinciples } from './SectionPrinciples';
|
|
||||||
export { SectionColors } from './SectionColors';
|
|
||||||
export { SectionTypography } from './SectionTypography';
|
|
||||||
export { SectionSpacing } from './SectionSpacing';
|
|
||||||
export { SectionMotion } from './SectionMotion';
|
|
||||||
|
|
||||||
// Systems sections
|
|
||||||
export { SectionAccessibility } from './SectionAccessibility';
|
|
||||||
export { SectionLayout } from './SectionLayout';
|
|
||||||
export { SectionContent } from './SectionContent';
|
|
||||||
export { SectionDataViz } from './SectionDataViz';
|
|
||||||
export { SectionIcons } from './SectionIcons';
|
|
||||||
|
|
||||||
// Implementation sections
|
|
||||||
export { SectionTheming } from './SectionTheming';
|
|
||||||
export { SectionComponents } from './SectionComponents';
|
|
||||||
export { SectionComponentAPIs } from './SectionComponentAPIs';
|
|
||||||
|
|
||||||
// Pattern library sections
|
|
||||||
export { SectionPatternNavigation } from './SectionPatternNavigation';
|
|
||||||
export { SectionPatternForms } from './SectionPatternForms';
|
|
||||||
export { SectionPatternFeedback } from './SectionPatternFeedback';
|
|
||||||
export { SectionPatternTables } from './SectionPatternTables';
|
|
||||||
export { SectionPatternSearch } from './SectionPatternSearch';
|
|
||||||
|
|
||||||
// Additional sections
|
// Additional sections
|
||||||
export { SectionA11yChecklists } from './SectionA11yChecklists';
|
export { SectionA11yChecklists } from './SectionA11yChecklists';
|
||||||
|
// Systems sections
|
||||||
|
export { SectionAccessibility } from './SectionAccessibility';
|
||||||
|
export { SectionColors } from './SectionColors';
|
||||||
|
export { SectionComponentAPIs } from './SectionComponentAPIs';
|
||||||
|
export { SectionComponents } from './SectionComponents';
|
||||||
|
export { SectionContent } from './SectionContent';
|
||||||
export { SectionContentGuidelines, SectionIconSystem } from './SectionContentGuidelines';
|
export { SectionContentGuidelines, SectionIconSystem } from './SectionContentGuidelines';
|
||||||
|
export { SectionDataViz } from './SectionDataViz';
|
||||||
|
export { SectionIcons } from './SectionIcons';
|
||||||
|
export { SectionLayout } from './SectionLayout';
|
||||||
|
export { SectionMotion } from './SectionMotion';
|
||||||
|
export { SectionPatternFeedback } from './SectionPatternFeedback';
|
||||||
|
export { SectionPatternForms } from './SectionPatternForms';
|
||||||
|
// Pattern library sections
|
||||||
|
export { SectionPatternNavigation } from './SectionPatternNavigation';
|
||||||
|
export { SectionPatternSearch } from './SectionPatternSearch';
|
||||||
|
export { SectionPatternTables } from './SectionPatternTables';
|
||||||
|
// Foundation sections
|
||||||
|
export { SectionPrinciples } from './SectionPrinciples';
|
||||||
|
export { SectionSpacing } from './SectionSpacing';
|
||||||
|
// Implementation sections
|
||||||
|
export { SectionTheming } from './SectionTheming';
|
||||||
|
export { SectionTypography } from './SectionTypography';
|
||||||
|
export {
|
||||||
|
ColorCard,
|
||||||
|
DoDontCard,
|
||||||
|
FieldsetSection,
|
||||||
|
NavItem,
|
||||||
|
PrincipleCard,
|
||||||
|
SubSection,
|
||||||
|
TokenRow,
|
||||||
|
} from './shared';
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,10 @@ export function FieldsetSection({
|
||||||
className?: string;
|
className?: string;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<fieldset id={id} className={`border border-dashed border-border p-8 mb-16 scroll-mt-24 ${className}`}>
|
<fieldset
|
||||||
|
id={id}
|
||||||
|
className={`border border-dashed border-border p-8 mb-16 scroll-mt-24 ${className}`}
|
||||||
|
>
|
||||||
<legend className="font-mono text-sm text-foreground-secondary px-3 lowercase">
|
<legend className="font-mono text-sm text-foreground-secondary px-3 lowercase">
|
||||||
{title}
|
{title}
|
||||||
</legend>
|
</legend>
|
||||||
|
|
@ -62,13 +65,19 @@ export function ColorCard({
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<div className={`${color} p-4 border border-dashed border-border`}>
|
<div className={`${color} p-4 border border-dashed border-border`}>
|
||||||
<div className={`font-mono text-sm font-medium ${textLight ? 'text-white' : 'text-foreground'}`}>
|
<div
|
||||||
|
className={`font-mono text-sm font-medium ${textLight ? 'text-white' : 'text-foreground'}`}
|
||||||
|
>
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
<div className={`font-mono text-xs ${textLight ? 'text-white/80' : 'text-foreground-secondary'}`}>
|
<div
|
||||||
|
className={`font-mono text-xs ${textLight ? 'text-white/80' : 'text-foreground-secondary'}`}
|
||||||
|
>
|
||||||
{hex}
|
{hex}
|
||||||
</div>
|
</div>
|
||||||
<div className={`font-mono text-xs mt-2 ${textLight ? 'text-white/60' : 'text-foreground-tertiary'}`}>
|
<div
|
||||||
|
className={`font-mono text-xs mt-2 ${textLight ? 'text-white/60' : 'text-foreground-tertiary'}`}
|
||||||
|
>
|
||||||
{desc}
|
{desc}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -95,9 +104,7 @@ export function DoDontCard({
|
||||||
<span className="font-mono text-sm font-medium uppercase">{isDo ? 'do' : "don't"}</span>
|
<span className="font-mono text-sm font-medium uppercase">{isDo ? 'do' : "don't"}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-mono text-xs text-foreground-secondary mb-3">{title}</p>
|
<p className="font-mono text-xs text-foreground-secondary mb-3">{title}</p>
|
||||||
<div className="bg-surface p-3 border border-dashed border-border">
|
<div className="bg-surface p-3 border border-dashed border-border">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +125,11 @@ export function PrincipleCard({
|
||||||
<div className="border border-dashed border-border p-6">
|
<div className="border border-dashed border-border p-6">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="w-10 h-10 bg-accent/10 flex items-center justify-center">
|
<div className="w-10 h-10 bg-accent/10 flex items-center justify-center">
|
||||||
<Icon icon={icon as Parameters<typeof Icon>[0]['icon']} size="md" className="text-accent" />
|
<Icon
|
||||||
|
icon={icon as Parameters<typeof Icon>[0]['icon']}
|
||||||
|
size="md"
|
||||||
|
className="text-accent"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-mono text-base font-medium text-foreground">{title}</h4>
|
<h4 className="font-mono text-base font-medium text-foreground">{title}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,7 @@ export function useAgents(params: UseAgentsParams = {}) {
|
||||||
const queryString = searchParams.toString();
|
const queryString = searchParams.toString();
|
||||||
|
|
||||||
// Custom response handler since the API returns { success, data, pagination }
|
// Custom response handler since the API returns { success, data, pagination }
|
||||||
return useSWR<AgentsResponse>(
|
return useSWR<AgentsResponse>(`/api/public/agents?${queryString}`, async (url: string) => {
|
||||||
`/api/public/agents?${queryString}`,
|
|
||||||
async (url: string) => {
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
|
|
@ -61,6 +59,5 @@ export function useAgents(params: UseAgentsParams = {}) {
|
||||||
agents: json.data,
|
agents: json.data,
|
||||||
pagination: json.pagination,
|
pagination: json.pagination,
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,8 @@ export function useBundleSize(packageName: string | undefined, version?: string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return useSWR<BundleSizeData>(
|
return useSWR<BundleSizeData>(packageName ? `/api/bundlephobia?${params.toString()}` : null, {
|
||||||
packageName ? `/api/bundlephobia?${params.toString()}` : null,
|
|
||||||
{
|
|
||||||
// Don't retry on 404s (common for scoped packages)
|
// Don't retry on 404s (common for scoped packages)
|
||||||
shouldRetryOnError: false,
|
shouldRetryOnError: false,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue