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:
Ajax Davis 2026-01-20 16:17:35 +10:00
parent 5fafc90e3e
commit 48c41e8733
245 changed files with 4734 additions and 2075 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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({

View file

@ -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({

View file

@ -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');

View file

@ -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',

View file

@ -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',

View file

@ -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;
} }
} }
} }

View file

@ -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',

View file

@ -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',

View file

@ -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 },

View file

@ -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';

View file

@ -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 },

View file

@ -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 },

View file

@ -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 },

View file

@ -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 },

View file

@ -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 {

View file

@ -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>

View file

@ -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 (

View file

@ -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>

View file

@ -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 */}

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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!'
}
/> />
) : ( ) : (
<> <>

View file

@ -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';

View file

@ -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';

View file

@ -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 },

View file

@ -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);

View file

@ -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,

View 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 }
);
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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';

View file

@ -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;

View file

@ -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 },

View file

@ -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';

View file

@ -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 };
}); });

View file

@ -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
} }

View 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 }
);
}
}

View 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 }
);
}
}

View file

@ -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';

View file

@ -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&apos;t have an account?{' '} Don&apos;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>

View file

@ -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!'
}
/> />
) : ( ) : (
<> <>

View file

@ -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}`);

View file

@ -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>

View file

@ -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}

View file

@ -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}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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';

View file

@ -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}

View file

@ -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>

View file

@ -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.

View file

@ -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&apos;t count against the 50 tool limit</li> <li>
Tools from attached collections don&apos;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 &quot;Forked from&quot; badge to navigate to the parent</p>
<p> <p>
1. Go to your forked agent or collection in the dashboard 3. The parent&apos;s page shows its <strong>current</strong> tools and
</p> collections
<p>
2. Click the &quot;Forked from&quot; badge to navigate to the parent
</p>
<p>
3. The parent&apos;s page shows its <strong>current</strong> tools and collections
</p>
<p>
4. Compare with your fork to see what&apos;s different
</p> </p>
<p>4. Compare with your fork to see what&apos;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&apos;s current tools/collections</p>
1. View the parent&apos;s current tools/collections <p>2. Go to your fork&apos;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&apos;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&apos;ll need to generate only once when created. Store it securely. If you lose it, you&apos;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 keybut you must You can access public agents and collections using your own API keybut 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&apos;s stored keys, tool environment variables) in the request. The owner&apos;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">

View file

@ -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

View file

@ -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;
} }
} }

View file

@ -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>
); );
} }

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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{' '}

View file

@ -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

View 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>
);
}

View 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>
);
}

View file

@ -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;

View file

@ -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}

View 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>
);
}

View 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>
);
}

View file

@ -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

View file

@ -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">

View file

@ -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();

View file

@ -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(() => {

View file

@ -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>

View file

@ -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);

View file

@ -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})`

View file

@ -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" />
@ -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,8 +375,8 @@ 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>

View file

@ -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 optionalit's TPMJS targets WCAG 2.1 AA compliance. Accessibility is not optionalit'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>

View file

@ -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>

View file

@ -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">&apos;default&apos; | &apos;secondary&apos; | &apos;destructive&apos; | &apos;outline&apos; | &apos;ghost&apos; | &apos;link&apos;</TableCell> <TableCell className="font-mono text-xs">
&apos;default&apos; | &apos;secondary&apos; | &apos;destructive&apos; |
&apos;outline&apos; | &apos;ghost&apos; | &apos;link&apos;
</TableCell>
<TableCell className="font-mono">&apos;default&apos;</TableCell> <TableCell className="font-mono">&apos;default&apos;</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">&apos;sm&apos; | &apos;md&apos; | &apos;lg&apos; | &apos;icon&apos;</TableCell> <TableCell className="font-mono text-xs">
&apos;sm&apos; | &apos;md&apos; | &apos;lg&apos; | &apos;icon&apos;
</TableCell>
<TableCell className="font-mono">&apos;md&apos;</TableCell> <TableCell className="font-mono">&apos;md&apos;</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">&apos;default&apos; | &apos;error&apos; | &apos;success&apos;</TableCell> <TableCell className="font-mono text-xs">
&apos;default&apos; | &apos;error&apos; | &apos;success&apos;
</TableCell>
<TableCell className="font-mono">&apos;default&apos;</TableCell> <TableCell className="font-mono">&apos;default&apos;</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">&apos;sm&apos; | &apos;md&apos; | &apos;lg&apos;</TableCell> <TableCell className="font-mono text-xs">
&apos;sm&apos; | &apos;md&apos; | &apos;lg&apos;
</TableCell>
<TableCell className="font-mono">&apos;md&apos;</TableCell> <TableCell className="font-mono">&apos;md&apos;</TableCell>
<TableCell>input size</TableCell> <TableCell>input size</TableCell>
</TableRow> </TableRow>

View file

@ -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>

View file

@ -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>

View file

@ -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,7 +442,9 @@ 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';

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>
))} ))}

View file

@ -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>

View file

@ -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>

View file

@ -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';

View file

@ -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>

View file

@ -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,
}; };
} });
);
} }

View file

@ -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