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.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit",
"source.organizeImports.biome": "explicit"
"source.fixAll.biome": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true

View file

@ -1,6 +1,6 @@
import { createOpenAI } from '@ai-sdk/openai';
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 { env } from '~/env';
import {

View file

@ -1,7 +1,7 @@
import { Analytics } from '@vercel/analytics/next';
import type { Metadata } from 'next';
import { ThemeProvider } from 'next-themes';
import { Space_Grotesk, Space_Mono } from 'next/font/google';
import { ThemeProvider } from 'next-themes';
import './globals.css';
const spaceGrotesk = Space_Grotesk({

View file

@ -19,7 +19,7 @@ const moduleCache = new Map();
/**
* Health check endpoint
*/
app.get('/health', (req, res) => {
app.get('/health', (_req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
@ -192,7 +192,7 @@ app.post('/execute-tool', async (req, res) => {
/**
* Clear module cache (for debugging)
*/
app.post('/cache/clear', (req, res) => {
app.post('/cache/clear', (_req, res) => {
const size = moduleCache.size;
moduleCache.clear();
console.log(`🗑️ Cleared cache (${size} entries)`);
@ -206,7 +206,7 @@ app.post('/cache/clear', (req, res) => {
/**
* Get cache statistics
*/
app.get('/cache/stats', (req, res) => {
app.get('/cache/stats', (_req, res) => {
const entries = Array.from(moduleCache.keys());
res.json({

View file

@ -947,7 +947,7 @@ async function handler(req: Request): Promise<Response> {
}
// 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('📦 HTTP imports: ENABLED');

View file

@ -1,5 +1,5 @@
import { AgentExampleSlideshow } from '@/components/AgentExampleSlideshow';
import type { Metadata } from 'next';
import { AgentExampleSlideshow } from '@/components/AgentExampleSlideshow';
export const metadata: Metadata = {
title: 'Real-World Agent Example | TPMJS Tutorials',

View file

@ -1,5 +1,5 @@
import { FirstToolSlideshow } from '@/components/FirstToolSlideshow';
import type { Metadata } from 'next';
import { FirstToolSlideshow } from '@/components/FirstToolSlideshow';
export const metadata: Metadata = {
title: 'Build Your First Tool | TPMJS Tutorials',

View file

@ -139,9 +139,9 @@
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms;
animation-iteration-count: 1;
transition-duration: 0.01ms;
}
}
}

View file

@ -1,5 +1,5 @@
import { OverviewSlideshow } from '@/components/OverviewSlideshow';
import type { Metadata } from 'next';
import { OverviewSlideshow } from '@/components/OverviewSlideshow';
export const metadata: Metadata = {
title: 'Overview | TPMJS Tutorials',

View file

@ -1,5 +1,5 @@
import { PlaygroundSlideshow } from '@/components/PlaygroundSlideshow';
import type { Metadata } from 'next';
import { PlaygroundSlideshow } from '@/components/PlaygroundSlideshow';
export const metadata: Metadata = {
title: 'Interactive Playground | TPMJS Tutorials',

View file

@ -1,17 +1,17 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AgentCodeSlide } from './agent-example-slides/AgentCodeSlide';
import { AgentExampleNextStepsSlide } from './agent-example-slides/AgentExampleNextStepsSlide';
import { AgentExampleWelcomeSlide } from './agent-example-slides/AgentExampleWelcomeSlide';
import { ExecutionFlowSlide } from './agent-example-slides/ExecutionFlowSlide';
import { SetupCodeSlide } from './agent-example-slides/SetupCodeSlide';
import { TheGoalSlide } from './agent-example-slides/TheGoalSlide';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
const SLIDES = [
{ id: 'welcome', Component: AgentExampleWelcomeSlide },

View file

@ -1,8 +1,8 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { useSlideshow } from '@/hooks/useSlideshow';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';

View file

@ -1,11 +1,8 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AuthorNextStepsSlide } from './author-slides/AuthorNextStepsSlide';
import { AuthorWelcomeSlide } from './author-slides/AuthorWelcomeSlide';
import { GetDiscoveredSlide } from './author-slides/GetDiscoveredSlide';
@ -15,6 +12,9 @@ import { PackageSetupSlide } from './author-slides/PackageSetupSlide';
import { QualityScoringSlide } from './author-slides/QualityScoringSlide';
import { ToolDefinitionSlide } from './author-slides/ToolDefinitionSlide';
import { WhatIsRegistrySlide } from './author-slides/WhatIsRegistrySlide';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
const SLIDES = [
{ id: 'welcome', Component: AuthorWelcomeSlide },

View file

@ -1,17 +1,17 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
import { useSlideshow } from '@/hooks/useSlideshow';
import { FirstToolNextStepsSlide } from './first-tool-slides/FirstToolNextStepsSlide';
import { FirstToolWelcomeSlide } from './first-tool-slides/FirstToolWelcomeSlide';
import { PackageJsonSlide } from './first-tool-slides/PackageJsonSlide';
import { ProjectSetupSlide } from './first-tool-slides/ProjectSetupSlide';
import { PublishSlide } from './first-tool-slides/PublishSlide';
import { WriteToolSlide } from './first-tool-slides/WriteToolSlide';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
const SLIDES = [
{ id: 'welcome', Component: FirstToolWelcomeSlide },

View file

@ -1,17 +1,17 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
import { useSlideshow } from '@/hooks/useSlideshow';
import { ArchitectureSlide } from './overview-slides/ArchitectureSlide';
import { EcosystemSlide } from './overview-slides/EcosystemSlide';
import { ExploreSlide } from './overview-slides/ExploreSlide';
import { OverviewWelcomeSlide } from './overview-slides/OverviewWelcomeSlide';
import { UseCasesSlide } from './overview-slides/UseCasesSlide';
import { WhatIsTpmjsSlide } from './overview-slides/WhatIsTpmjsSlide';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
const SLIDES = [
{ id: 'welcome', Component: OverviewWelcomeSlide },

View file

@ -1,17 +1,17 @@
'use client';
import { useSlideshow } from '@/hooks/useSlideshow';
import { AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
import { useSlideshow } from '@/hooks/useSlideshow';
import { FillParametersSlide } from './playground-slides/FillParametersSlide';
import { FindToolSlide } from './playground-slides/FindToolSlide';
import { OpenPlaygroundSlide } from './playground-slides/OpenPlaygroundSlide';
import { PlaygroundNextStepsSlide } from './playground-slides/PlaygroundNextStepsSlide';
import { PlaygroundWelcomeSlide } from './playground-slides/PlaygroundWelcomeSlide';
import { SeeResultsSlide } from './playground-slides/SeeResultsSlide';
import { Slide } from './Slide';
import { SlideNavigation } from './SlideNavigation';
import { SlideProgress } from './SlideProgress';
const SLIDES = [
{ id: 'welcome', Component: PlaygroundWelcomeSlide },

View file

@ -1,6 +1,6 @@
'use client';
import { type Variants, motion } from 'framer-motion';
import { motion, type Variants } from 'framer-motion';
import type { ReactNode } from 'react';
interface SlideProps {

View file

@ -107,12 +107,7 @@ export default function ForgotPasswordPage() {
/>
</div>
<Button
type="submit"
disabled={loading}
loading={loading}
className="w-full"
>
<Button type="submit" disabled={loading} loading={loading} className="w-full">
{loading ? 'Sending...' : 'Send Reset Link'}
</Button>
</form>

View file

@ -1,5 +1,5 @@
import { AuthHeader } from '@/components/AuthHeader';
import type { ReactNode } from 'react';
import { AuthHeader } from '@/components/AuthHeader';
export default function AuthLayout({ children }: { children: ReactNode }) {
return (

View file

@ -91,7 +91,9 @@ export default function SignInPage() {
<div>
<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
href="/forgot-password"
className="text-sm text-foreground-secondary hover:text-foreground hover:underline"
@ -122,12 +124,7 @@ export default function SignInPage() {
</div>
</div>
<Button
type="submit"
disabled={loading}
loading={loading}
className="w-full"
>
<Button type="submit" disabled={loading} loading={loading} className="w-full">
{loading ? 'Signing in...' : 'Sign In'}
</Button>
</form>

View file

@ -1,10 +1,10 @@
'use client';
import { suggestUsername } from '@tpmjs/types/user';
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Input } from '@tpmjs/ui/Input/Input';
import { Label } from '@tpmjs/ui/Label/Label';
import { suggestUsername } from '@tpmjs/types/user';
import Link from 'next/link';
import { useCallback, useEffect, useState } from 'react';
import { signUp } from '~/lib/auth-client';
@ -222,9 +222,7 @@ export default function SignUpPage() {
{!checkingUsername &&
usernameCheck &&
!usernameCheck.available &&
username.length >= 3 && (
<Icon icon="x" size="sm" className="text-error" />
)}
username.length >= 3 && <Icon icon="x" size="sm" className="text-error" />}
</div>
</div>
{/* 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">
<p className="text-lg text-foreground-secondary mb-6">
TPMJS (Tool Package Manager for JavaScript) is the npm registry for AI agent tools.
It was started in 2024 to make it easy for developers to publish and discover tools
that AI agents can use.
TPMJS (Tool Package Manager for JavaScript) is the npm registry for AI agent tools. It
was started in 2024 to make it easy for developers to publish and discover tools that AI
agents can use.
</p>
<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
icon="terminal"
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 { AGENT_LIMITS, AddCollectionToAgentSchema } from '@tpmjs/types/agent';
import { AddCollectionToAgentSchema, AGENT_LIMITS } from '@tpmjs/types/agent';
import { headers } from 'next/headers';
import { type NextRequest, NextResponse } from 'next/server';

View file

@ -1,5 +1,5 @@
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 { 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 limit = Math.min(Number.parseInt(searchParams.get('limit') || '20'), 50);
const offset = Number.parseInt(searchParams.get('offset') || '0');
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20', 10), 50);
const offset = Number.parseInt(searchParams.get('offset') || '0', 10);
const agents = await prisma.agent.findMany({
where: { userId: authResult.userId },

View file

@ -1,4 +1,4 @@
import { auth } from '@/lib/auth';
import { toNextJsHandler } from 'better-auth/next-js';
import { auth } from '@/lib/auth';
export const { GET, POST } = toNextJsHandler(auth);

View file

@ -178,7 +178,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
name: scenario.name,
tags: scenario.tags,
},
...(similarityResult && similarityResult.hasSimilar
...(similarityResult?.hasSimilar
? {
similarity: {
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':
orderBy = [{ createdAt: 'desc' }];
break;
case 'quality':
default:
orderBy = [{ qualityScore: 'desc' }, { package: { npmDownloadsLastMonth: 'desc' } }];
break;

View file

@ -1,7 +1,7 @@
import { prisma } from '@tpmjs/db';
import { fetchChanges, fetchLatestPackageWithMetadata } from '@tpmjs/npm-client';
import { validateTpmjsField } 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 { env } from '~/env';
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 minQuality = Number.parseFloat(searchParams.get('minQuality') || '0');
const verb = searchParams.get('verb') || '';
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '100'), 10000);
const offset = Number.parseInt(searchParams.get('offset') || '0');
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '100', 10), 10000);
const offset = Number.parseInt(searchParams.get('offset') || '0', 10);
const data = loadToolIdeas();
let tools = data.tools;

View file

@ -79,7 +79,6 @@ export async function GET(
case 'lowest':
orderBy = [{ rating: 'asc' }, { createdAt: 'desc' }];
break;
case 'recent':
default:
orderBy = [{ createdAt: 'desc' }];
}
@ -365,18 +364,18 @@ export async function POST(
return NextResponse.json({
success: true,
data: {
id: review!.id,
title: review!.title,
content: review!.content,
rating: review!.rating,
helpfulCount: review!.helpfulCount,
createdAt: review!.createdAt.toISOString(),
updatedAt: review!.updatedAt.toISOString(),
id: review?.id,
title: review?.title,
content: review?.content,
rating: review?.rating,
helpfulCount: review?.helpfulCount,
createdAt: review?.createdAt.toISOString(),
updatedAt: review?.updatedAt.toISOString(),
user: {
id: review!.user.id,
name: review!.user.name,
image: review!.user.image,
username: review!.user.username,
id: review?.user.id,
name: review?.user.name,
image: review?.user.image,
username: review?.user.username,
},
},
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
*/
import { checkRateLimit, getClientIP } from '@/lib/rate-limiter';
import { prisma } from '@tpmjs/db';
import { type NextRequest, NextResponse } from 'next/server';
import { checkRateLimit, getClientIP } from '@/lib/rate-limiter';
// Use Node.js runtime for SSE streaming
export const runtime = 'nodejs';

View file

@ -1,6 +1,6 @@
import { prisma } from '@tpmjs/db';
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 dynamic = 'force-dynamic';
@ -93,7 +93,7 @@ export async function GET(request: NextRequest) {
// Accept both 'q' and 'query' parameters for flexibility
const query = searchParams.get('q') || searchParams.get('query') || '';
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
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)
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 };
});

View file

@ -65,7 +65,6 @@ export async function GET(request: NextRequest): Promise<NextResponse<ApiRespons
case 'month':
recencyPeriodMs = 30 * 24 * 60 * 60 * 1000;
break;
case 'all':
default:
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 { 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 { authenticateRequest } from '~/lib/api-keys/middleware';

View file

@ -118,7 +118,10 @@ function CliAuthContent(): React.ReactElement {
</Link>
<p className="text-sm text-foreground-tertiary mt-4">
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
</Link>
</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="flex items-center gap-3">
{user.image ? (
<img
src={user.image}
alt=""
className="w-10 h-10 rounded-full"
/>
<img 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">
<Icon icon="user" size="sm" className="text-primary" />
</div>
)}
<div>
<p className="font-medium text-foreground">
{user.name || 'User'}
</p>
<p className="text-sm text-foreground-secondary">
{user.email}
</p>
<p className="font-medium text-foreground">{user.name || 'User'}</p>
<p className="text-sm text-foreground-secondary">{user.email}</p>
</div>
</div>
</div>
@ -195,11 +190,7 @@ function CliAuthContent(): React.ReactElement {
{/* Actions */}
<div className="flex flex-col gap-3">
<Button
onClick={handleAuthorize}
disabled={isAuthorizing}
className="w-full"
>
<Button onClick={handleAuthorize} disabled={isAuthorizing} className="w-full">
{isAuthorizing ? (
<>
<Icon icon="loader" size="sm" className="mr-2 animate-spin" />
@ -223,8 +214,8 @@ function CliAuthContent(): React.ReactElement {
</div>
<p className="text-xs text-foreground-tertiary text-center mt-6">
An API key will be created and sent to the CLI.
You can revoke it anytime from your dashboard.
An API key will be created and sent to the CLI. You can revoke it anytime from your
dashboard.
</p>
</div>
</div>

View file

@ -242,7 +242,11 @@ export default function PublicCollectionsPage(): React.ReactElement {
<EmptyState
icon="folder"
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
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages.length, streamingContent]);
}, []);
const handleSelectConversation = (convSlug: string) => {
router.push(`/dashboard/agents/${agentId}/chat/${convSlug}`);

View file

@ -322,11 +322,7 @@ export default function NewAgentPage(): React.ReactElement {
{error && (
<div className="bg-error/10 border border-error/20 rounded-lg p-4">
<div className="flex items-start gap-3">
<Icon
icon="alertCircle"
size="sm"
className="text-error mt-0.5"
/>
<Icon icon="alertCircle" size="sm" className="text-error mt-0.5" />
<p className="text-sm text-error">{error}</p>
</div>
</div>

View file

@ -168,8 +168,7 @@ export default function AgentsPage(): React.ReactElement {
<TableBody>
{isLoading ? (
// Loading skeleton
<>
{[0, 1, 2].map((idx) => (
[0, 1, 2].map((idx) => (
<TableRow key={`agent-skeleton-${idx}`}>
<TableCell>
<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" />
</TableCell>
</TableRow>
))}
</>
))
) : agents.length === 0 ? (
<TableEmpty
colSpan={5}

View file

@ -192,8 +192,7 @@ export default function CollectionsPage(): React.ReactElement {
<TableBody>
{isLoading ? (
// Loading skeleton
<>
{[0, 1, 2].map((idx) => (
[0, 1, 2].map((idx) => (
<TableRow key={`collection-skeleton-${idx}`}>
<TableCell>
<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" />
</TableCell>
</TableRow>
))}
</>
))
) : collections.length === 0 ? (
<TableEmpty
colSpan={5}

View file

@ -5,8 +5,8 @@ import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import Link from 'next/link';
import { useCallback, useEffect, useState } from 'react';
import { LikeButton } from '~/components/LikeButton';
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
import { LikeButton } from '~/components/LikeButton';
interface LikedAgent {
id: string;

View file

@ -5,8 +5,8 @@ import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import Link from 'next/link';
import { useCallback, useEffect, useState } from 'react';
import { LikeButton } from '~/components/LikeButton';
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
import { LikeButton } from '~/components/LikeButton';
interface LikedCollection {
id: string;

View file

@ -5,8 +5,8 @@ import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import Link from 'next/link';
import { useCallback, useEffect, useState } from 'react';
import { LikeButton } from '~/components/LikeButton';
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
import { LikeButton } from '~/components/LikeButton';
interface LikedTool {
id: string;

View file

@ -1,9 +1,9 @@
'use client';
import { useSession } from '@/lib/auth-client';
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import Link from 'next/link';
import { useSession } from '@/lib/auth-client';
import { DashboardActivityStream } from '~/components/DashboardActivityStream';
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';

View file

@ -3,7 +3,6 @@
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Input } from '@tpmjs/ui/Input/Input';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import {
Table,
TableBody,
@ -13,6 +12,7 @@ import {
TableHeader,
TableRow,
} from '@tpmjs/ui/Table/Table';
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { DashboardLayout } from '~/components/dashboard/DashboardLayout';
@ -268,8 +268,7 @@ ANOTHER_SECRET=xyz789"
</TableHeader>
<TableBody>
{isLoading ? (
<>
{[0, 1, 2].map((idx) => (
[0, 1, 2].map((idx) => (
<TableRow key={`key-skeleton-${idx}`}>
<TableCell>
<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" />
</TableCell>
</TableRow>
))}
</>
))
) : keys.length === 0 ? (
<TableEmpty
colSpan={4}

View file

@ -245,7 +245,7 @@ export default function UsagePage(): React.ReactElement {
</span>
<span>
{data.timeSeries.at(-1)?.periodStart &&
new Date(data.timeSeries.at(-1)!.periodStart).toLocaleDateString()}
new Date(data.timeSeries.at(-1)?.periodStart).toLocaleDateString()}
</span>
</div>
</div>

View file

@ -214,8 +214,7 @@ export default function ExecutorsDocsPage(): React.ReactElement {
{/* GET /health */}
<div className="mb-8">
<h3 className="text-lg font-medium text-foreground mb-2">
<code className="px-2 py-1 bg-success/10 text-success rounded">GET</code>{' '}
/health
<code className="px-2 py-1 bg-success/10 text-success rounded">GET</code> /health
</h3>
<p className="text-foreground-secondary mb-4">
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
</li>
<li>
<strong className="text-foreground">Usage Analytics</strong> - Monitor API calls,
tokens, and costs
<strong className="text-foreground">Usage Analytics</strong> - Monitor API
calls, tokens, and costs
</li>
</ul>
</DocSubSection>
@ -792,7 +792,9 @@ Invalid usernames:
</li>
<li>
<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>
</ul>
</DocSubSection>
@ -934,7 +936,8 @@ Invalid usernames:
name: 'Temperature',
type: 'number',
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',
@ -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">
<li>Maximum 50 individual tools 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>
</DocSubSection>
</DocSection>
@ -1061,7 +1066,11 @@ Always cite your sources and be transparent about limitations.`}
{
name: 'Anthropic',
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',
},
{
@ -1073,7 +1082,11 @@ Always cite your sources and be transparent about limitations.`}
{
name: 'Groq',
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',
},
{
@ -1083,7 +1096,10 @@ Always cite your sources and be transparent about limitations.`}
keyUrl: 'https://console.mistral.ai/api-keys',
},
].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">
<h4 className="font-semibold text-foreground">{provider.name}</h4>
<a
@ -1272,23 +1288,18 @@ Always cite your sources and be transparent about limitations.`}
<DocSection id="viewing-parent-updates" title="Viewing Parent Updates">
<p className="text-foreground-secondary mb-6">
After forking, you can check what the original parent has changed by visiting
its page. This lets you see new tools or collections the parent author has added.
After forking, you can check what the original parent has changed by visiting its
page. This lets you see new tools or collections the parent author has added.
</p>
<DocSubSection title="How to View Parent Changes">
<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>
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>
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
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>
</div>
</DocSubSection>
<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:
</p>
<div className="space-y-3 text-foreground-secondary">
<p>
1. View the parent&apos;s current tools/collections
</p>
<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>
<p>1. View the parent&apos;s current tools/collections</p>
<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 className="mt-4 p-4 border border-border rounded-lg bg-surface-elevated">
<p className="text-sm text-foreground-secondary">
@ -1447,9 +1450,9 @@ curl "https://tpmjs.com/api/public/users/{username}/agents/{uid}"
</DocSubSection>
<div className="p-4 border border-warning/30 rounded-lg bg-warning/5 mt-4">
<p className="text-sm text-foreground-secondary">
<strong className="text-warning">Important:</strong> Your API key is displayed only
once when created. Store it securely. If you lose it, you&apos;ll need to generate
a new one.
<strong className="text-warning">Important:</strong> Your API key is displayed
only once when created. Store it securely. If you lose it, you&apos;ll need to
generate a new one.
</p>
</div>
</DocSection>
@ -1508,7 +1511,10 @@ curl "https://tpmjs.com/api/public/users/{username}/agents/{uid}"
<p className="text-foreground-secondary mb-4">
Include your API key in the Authorization header:
</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>
<CodeBlock
language="bash"
@ -1645,8 +1651,8 @@ X-RateLimit-Reset: 1704067200`}
<DocSection id="access-model" title="Access Model: Public Access with Your Credentials">
<p className="text-foreground-secondary mb-6">
You can access public agents and collections using your own API keybut you must
provide <strong className="text-foreground">all required credentials</strong>{' '}
(LLM keys, tool environment variables) in the request. The owner&apos;s stored
provide <strong className="text-foreground">all required credentials</strong> (LLM
keys, tool environment variables) in the request. The owner&apos;s stored
credentials are never shared.
</p>
<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">
<Icon icon="alertCircle" className="w-5 h-5 text-warning flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-warning">
Security Recommendation
</p>
<p className="font-medium text-warning">Security Recommendation</p>
<p className="text-sm text-foreground-secondary mt-1">
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

View file

@ -226,10 +226,14 @@
--shadow-none: none;
--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-md: 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-sm:
0 1px 3px 0 hsl(var(--foreground) / 0.08), 0 1px 2px -1px hsl(var(--foreground) / 0.08);
--shadow-md:
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-inner: inset 0 2px 4px 0 hsl(var(--foreground) / 0.05);
@ -745,9 +749,9 @@
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms;
animation-iteration-count: 1;
transition-duration: 0.01ms;
}
}

View file

@ -194,10 +194,7 @@ export default function StatsPage() {
if (error || !stats) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<ErrorState
title="Failed to load statistics"
message={error || 'Unknown error'}
/>
<ErrorState title="Failed to load statistics" message={error || 'Unknown error'} />
</div>
);
}

View file

@ -132,8 +132,12 @@ export default function StyleGuidePage(): React.ReactElement {
Design System
</h2>
<div className="flex gap-2">
<Badge variant="default" size="sm">v2.0</Badge>
<Badge variant="outline" size="sm">wcag aa</Badge>
<Badge variant="default" size="sm">
v2.0
</Badge>
<Badge variant="outline" size="sm">
wcag aa
</Badge>
</div>
</div>

View file

@ -454,7 +454,9 @@ export default function TermsPage(): React.ReactElement {
<Button size="lg">Contact Us</Button>
</a>
<Link href="/">
<Button variant="outline" size="lg">Back to Home</Button>
<Button variant="outline" size="lg">
Back to Home
</Button>
</Link>
</div>
</div>

View file

@ -228,7 +228,9 @@ export function ToolIdeasClient() {
{/* 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>
<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
id="tool-search"
type="text"
@ -240,7 +242,9 @@ export function ToolIdeasClient() {
</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
id="tool-category"
value={category}
@ -251,7 +255,9 @@ export function ToolIdeasClient() {
</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
id="tool-verb"
value={verb}
@ -262,7 +268,9 @@ export function ToolIdeasClient() {
</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
id="tool-quality"
value={minQuality}

View file

@ -273,9 +273,7 @@ export function ToolDetailClient({ tool, slug }: ToolDetailClientProps): React.R
<div className="flex items-start gap-3">
<span className="text-xl mt-0.5">🔍</span>
<div className="flex-1">
<h3 className="text-sm font-semibold text-warning mb-1">
Auto-discovered tool
</h3>
<h3 className="text-sm font-semibold text-warning mb-1">Auto-discovered tool</h3>
<p className="text-sm text-warning/80">
This tool was automatically discovered from the package exports. The author did
not explicitly register it in their{' '}

View file

@ -17,8 +17,8 @@ import { AppHeader } from '~/components/AppHeader';
import { CopyButton } from '~/components/CopyButton';
import { LikeButton } from '~/components/LikeButton';
import {
PackageManagerSelector,
getInstallCommand,
PackageManagerSelector,
usePackageManager,
} from '~/components/PackageManagerSelector';
import { type Tool, useTools } from '~/hooks/useTools';
@ -81,7 +81,11 @@ export default function ToolSearchPage(): React.ReactElement {
const [packageManager, setPackageManager] = usePackageManager();
// 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,
importHealth: healthFilter === 'healthy' ? 'HEALTHY' : undefined,
executionHealth: healthFilter === 'healthy' ? 'HEALTHY' : undefined,
@ -143,10 +147,7 @@ export default function ToolSearchPage(): React.ReactElement {
return (
<>
<td className="px-4 py-3">
<Link
href={`/tool/${tool.package.npmPackageName}/${tool.name}`}
className="block"
>
<Link href={`/tool/${tool.package.npmPackageName}/${tool.name}`} className="block">
<div className="font-semibold text-foreground group-hover:text-primary transition-colors">
{displayName}
{isBroken && (
@ -310,7 +311,9 @@ export default function ToolSearchPage(): React.ReactElement {
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} />,
TableRow: (props) => (
<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';
import { useSession } from '@/lib/auth-client';
import { Button } from '@tpmjs/ui/Button/Button';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { useCallback, useState } from 'react';
import { useSession } from '@/lib/auth-client';
import { useLikeStatus } from '~/hooks/useLikeStatus';
export type LikeEntityType = 'tool' | 'collection' | 'agent';
@ -36,12 +36,10 @@ export function LikeButton({
const [isLoading, setIsLoading] = useState(false);
// Use SWR for like status with optimistic updates
const { data, toggleLike } = useLikeStatus(
entityType,
entityId,
!!session,
{ liked: initialLiked, likeCount: initialCount }
);
const { data, toggleLike } = useLikeStatus(entityType, entityId, !!session, {
liked: initialLiked,
likeCount: initialCount,
});
const liked = data?.liked ?? initialLiked;
const count = data?.likeCount ?? initialCount;

View file

@ -5,7 +5,6 @@
* 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 { Button } from '@tpmjs/ui/Button/Button';
import { Label } from '@tpmjs/ui/Label/Label';
@ -14,6 +13,7 @@ import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import type { TokenBreakdown as TokenData } from '@/lib/ai-agent/tool-executor-agent';
import { TokenBreakdown } from './TokenBreakdown';
interface ToolPlaygroundProps {
@ -240,7 +240,9 @@ export function ToolPlayground({ tool }: ToolPlaygroundProps): React.ReactElemen
{activeTab === 'input' && (
<div className="space-y-4">
<div>
<Label htmlFor="prompt" className="mb-2">Prompt</Label>
<Label htmlFor="prompt" className="mb-2">
Prompt
</Label>
<Textarea
id="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 [collectionTools, setCollectionTools] = useState<Record<string, CollectionToolData>>({});
const fetchCollectionTools = useCallback(async (collectionId: string) => {
const fetchCollectionTools = useCallback(
async (collectionId: string) => {
const existingData = collectionTools[collectionId];
if (existingData?.tools && existingData.tools.length > 0) return;
@ -99,7 +100,9 @@ export function ChatToolsPanel({
[collectionId]: { tools: [], loading: false, error: 'Failed to load tools' },
}));
}
}, [collectionTools]);
},
[collectionTools]
);
const toggleCollection = (collectionId: string) => {
setExpandedCollections((prev) => {
@ -117,10 +120,7 @@ export function ChatToolsPanel({
if (!isOpen) return null;
const totalToolsFromCollections = collections.reduce(
(acc, c) => acc + c.collection.toolCount,
0
);
const totalToolsFromCollections = collections.reduce((acc, c) => acc + c.collection.toolCount, 0);
return (
<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">
{toolData?.loading ? (
<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>
</div>
) : toolData?.error ? (
<div className="p-3 text-xs text-error text-center">
{toolData.error}
</div>
<div className="p-3 text-xs text-error text-center">{toolData.error}</div>
) : toolData?.tools.length === 0 ? (
<div className="p-3 text-xs text-foreground-tertiary text-center">
no tools in collection

View file

@ -99,7 +99,11 @@ export function ToolDetailsModal({ tool, open, onClose }: ToolDetailsModalProps)
{/* Optional parameters */}
{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="w-2 h-2 rounded-full bg-foreground-tertiary" />
<span className="font-mono text-xs text-foreground-secondary uppercase tracking-wide">

View file

@ -1,8 +1,8 @@
'use client';
import { signOut } from '@/lib/auth-client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { signOut } from '@/lib/auth-client';
export function SignOutButton() {
const router = useRouter();

View file

@ -47,7 +47,8 @@ export function AddToolSearch({
const containerRef = useRef<HTMLDivElement>(null);
// Debounced search
const search = useCallback(async (searchQuery: string) => {
const search = useCallback(
async (searchQuery: string) => {
if (!searchQuery.trim()) {
setResults([]);
setIsOpen(false);
@ -83,7 +84,9 @@ export function AddToolSearch({
} finally {
setIsSearching(false);
}
}, [existingToolIds]);
},
[existingToolIds]
);
// Handle query changes with debounce
useEffect(() => {

View file

@ -113,9 +113,7 @@ function AnimatedTerminal(): React.ReactElement {
{currentLine < lines.length && (
<div className={`${getLineColor(lines[currentLine]?.type || 'input')} flex`}>
<span>{displayedText}</span>
{isTyping && (
<span className="ml-0.5 w-2 h-5 bg-primary animate-pulse" />
)}
{isTyping && <span className="ml-0.5 w-2 h-5 bg-primary animate-pulse" />}
</div>
)}
</div>
@ -135,7 +133,13 @@ interface FeatureCardProps {
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 [isHovered, setIsHovered] = useState(false);
const ref = useRef<HTMLDivElement>(null);
@ -201,9 +205,7 @@ function FeatureCard({ icon, title, description, stats, delay = 0 }: FeatureCard
</div>
{/* Content */}
<h3 className="font-mono text-lg font-semibold mb-2 text-foreground lowercase">
{title}
</h3>
<h3 className="font-mono text-lg font-semibold mb-2 text-foreground lowercase">{title}</h3>
<p className="font-sans text-sm text-foreground-secondary leading-relaxed mb-4">
{description}
</p>
@ -233,7 +235,12 @@ interface AnimatedCounterProps {
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 [hasStarted, setHasStarted] = useState(false);
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);
// Easing function for smooth animation
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const easeOutQuart = 1 - (1 - progress) ** 4;
setCount(Math.floor(easeOutQuart * end));
if (progress < 1) {
@ -280,7 +287,9 @@ function AnimatedCounter({ end, duration = 2000, suffix = '', prefix = '' }: Ani
return (
<span ref={ref} className="tabular-nums">
{prefix}{count.toLocaleString()}{suffix}
{prefix}
{count.toLocaleString()}
{suffix}
</span>
);
}
@ -321,7 +330,8 @@ function FlowDiagram(): React.ReactElement {
className={`
w-16 h-16 flex items-center justify-center border-2 mb-3
transition-all duration-500
${activeStep === i
${
activeStep === i
? 'border-primary bg-primary/10 shadow-[0_0_20px_rgba(166,89,45,0.3)]'
: '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'}`}
/>
</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}
</span>
<span className="font-mono text-xs text-foreground-tertiary mt-1">
{step.desc}
</span>
<span className="font-mono text-xs text-foreground-tertiary mt-1">{step.desc}</span>
{/* Pulse ring when active */}
{activeStep === i && (
@ -454,8 +464,8 @@ export function FeaturesSection(): React.ReactElement {
powerful features
</h2>
<p className="text-lg text-foreground-secondary max-w-2xl mx-auto font-sans">
From discovery to execution, TPMJS provides the complete infrastructure
for AI tool development.
From discovery to execution, TPMJS provides the complete infrastructure for AI tool
development.
</p>
</div>
@ -490,8 +500,8 @@ export function FeaturesSection(): React.ReactElement {
live execution
</legend>
<p className="font-sans text-sm text-foreground-secondary mb-6">
Execute any tool directly from your terminal or AI agent. Secure sandboxed
execution with real-time streaming output.
Execute any tool directly from your terminal or AI agent. Secure sandboxed execution
with real-time streaming output.
</p>
<AnimatedTerminal />
</fieldset>
@ -504,8 +514,8 @@ export function FeaturesSection(): React.ReactElement {
tool registry
</legend>
<p className="font-sans text-sm text-foreground-secondary mb-6">
Browse 170+ tools across multiple categories. Each tool is validated,
documented, and ready to use.
Browse 170+ tools across multiple categories. Each tool is validated, documented,
and ready to use.
</p>
<InteractiveToolGrid />
<div className="mt-6 flex justify-center">
@ -534,8 +544,8 @@ export function FeaturesSection(): React.ReactElement {
tools tpmjs agents
</legend>
<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.
Watch data flow in real-time as tools serve agent requests.
TPMJS acts as the central hub connecting npm packages to AI agents. Watch data flow in
real-time as tools serve agent requests.
</p>
<ToolConnectionViz />
</fieldset>

View file

@ -36,7 +36,7 @@ export function AnimatedCounter({
const progress = Math.min(elapsed / duration, 1);
// 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;
setDisplayValue(current);

View file

@ -112,7 +112,7 @@ export function BarChart({
.attr('dominant-baseline', 'middle')
.attr('fill', colors.foregroundSecondary)
.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
if (showValues) {
@ -196,7 +196,7 @@ export function BarChart({
.attr('text-anchor', 'middle')
.attr('fill', colors.foregroundSecondary)
.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(
'transform',
(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>
{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>
<div className="space-y-1">
{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>
</TableCell>
@ -195,13 +201,14 @@ export function SectionA11yChecklists(): React.ReactElement {
return (
<FieldsetSection title="19. accessibility checklists" id="a11y-checklists">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Component-level accessibility requirements and implementation guides.
All components follow WCAG 2.1 AA standards.
Component-level accessibility requirements and implementation guides. All components follow
WCAG 2.1 AA standards.
</p>
<SubSection title="aria patterns & keyboard">
<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>
<A11yTable components={overlayComponents} title="overlay components" />
@ -240,7 +247,7 @@ export function SectionA11yChecklists(): React.ReactElement {
<div className="mt-6 bg-surface p-4 border border-dashed border-border">
<h4 className="font-mono text-sm font-medium mb-3">implementation pattern</h4>
<pre className="text-xs font-mono text-foreground-secondary overflow-x-auto">
{`// Focus trap implementation
{`// Focus trap implementation
const dialogRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
@ -300,19 +307,27 @@ const handleClose = () => {
<TableBody>
<TableRow>
<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>
<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>
<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>
<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>
</TableBody>
</Table>
@ -322,21 +337,28 @@ const handleClose = () => {
<SubSection title="color contrast requirements">
<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>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-background border border-border">
<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 className="flex items-center justify-between p-3 bg-surface border border-border">
<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 className="flex items-center justify-between p-3 bg-surface border border-border">
<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 className="bg-surface p-4 border border-dashed border-border">
@ -353,13 +375,13 @@ const handleClose = () => {
<SubSection title="reduced motion support">
<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
for users who experience motion sickness or vestibular disorders.
Respect the <code className="px-1 bg-surface-2 text-xs">prefers-reduced-motion</code>{' '}
media query for users who experience motion sickness or vestibular disorders.
</p>
<div className="bg-surface p-4 border border-dashed border-border">
<h4 className="font-mono text-sm font-medium mb-3">implementation</h4>
<pre className="text-xs font-mono text-foreground-secondary overflow-x-auto">
{`// CSS approach
{`// CSS approach
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;

View file

@ -9,8 +9,8 @@ export function SectionAccessibility(): React.ReactElement {
return (
<FieldsetSection title="6. accessibility" id="accessibility">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
TPMJS targets WCAG 2.1 AA compliance. Accessibility is not optionalit's
a core requirement for every component.
TPMJS targets WCAG 2.1 AA compliance. Accessibility is not optionalit's a core requirement
for every component.
</p>
<SubSection title="standards">
@ -35,8 +35,8 @@ export function SectionAccessibility(): React.ReactElement {
<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>
<p className="font-sans text-sm text-foreground-secondary mb-4">
Focus rings use the copper accent color with 2px width and 2px offset.
They are <strong>never removed</strong> from interactive elements.
Focus rings use the copper accent color with 2px width and 2px offset. They are{' '}
<strong>never removed</strong> from interactive elements.
</p>
<div className="flex gap-4">
<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">
<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">
<li> <kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Tab</kbd> moves focus forward through interactive elements</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>
<kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">Tab</kbd> moves focus
forward through interactive elements
</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>
</ul>
</div>

View file

@ -16,8 +16,8 @@ export function SectionColors(): React.ReactElement {
return (
<FieldsetSection title="2. color system" id="colors">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
A warm earthy palette with copper accent. Colors are designed for clarity,
hierarchy, and accessibility.
A warm earthy palette with copper accent. Colors are designed for clarity, hierarchy, and
accessibility.
</p>
{/* Color Palette Display */}
@ -33,31 +33,77 @@ export function SectionColors(): React.ReactElement {
<SubSection title="borders">
<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-strong" color="bg-border-strong" hex="#857F77" desc="emphasized borders" />
<ColorCard name="border-subtle" color="bg-border-subtle" hex="#E3E0DC" desc="subtle borders" />
<ColorCard
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>
</SubSection>
<SubSection title="text">
<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 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 />
<ColorCard
name="text"
color="bg-foreground"
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>
</SubSection>
<SubSection title="accent (copper)">
<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-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" />
</div>
</SubSection>
<SubSection title="status">
<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="error" color="bg-error" hex="#C44545" desc="error 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)">
<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 name="warning-light" color="bg-warning-light" hex="#F8F2E0" desc="warning bg" />
<ColorCard
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="info-light" color="bg-info-light" hex="#EDF4FB" desc="info bg" />
</div>
@ -79,9 +135,15 @@ export function SectionColors(): React.ReactElement {
<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>
<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> <strong>Neutral:</strong> Secondary actions, borders, backgrounds, body text</li>
<li> <strong>Rule:</strong> Copper should be max ~10% of visible screen area</li>
<li>
<strong>Copper:</strong> Primary actions, active states, links, key metrics
</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>
</div>
<div className="bg-surface p-6 border border-dashed border-border">
@ -148,26 +210,58 @@ export function SectionColors(): React.ReactElement {
<TableRow>
<TableCell className="font-mono">foreground / background</TableCell>
<TableCell className="font-mono">12.5:1</TableCell>
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
<TableCell>
<Badge variant="success" size="sm">
pass
</Badge>
</TableCell>
<TableCell>
<Badge variant="success" size="sm">
pass
</Badge>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono">accent / background</TableCell>
<TableCell className="font-mono">5.2:1</TableCell>
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
<TableCell>
<Badge variant="success" size="sm">
pass
</Badge>
</TableCell>
<TableCell>
<Badge variant="success" size="sm">
pass
</Badge>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono">foreground-secondary / background</TableCell>
<TableCell className="font-mono">5.8:1</TableCell>
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
<TableCell>
<Badge variant="success" size="sm">
pass
</Badge>
</TableCell>
<TableCell>
<Badge variant="success" size="sm">
pass
</Badge>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono">foreground-tertiary / background</TableCell>
<TableCell className="font-mono">3.9:1</TableCell>
<TableCell><Badge variant="warning" size="sm">large only</Badge></TableCell>
<TableCell><Badge variant="success" size="sm">pass</Badge></TableCell>
<TableCell>
<Badge variant="warning" size="sm">
large only
</Badge>
</TableCell>
<TableCell>
<Badge variant="success" size="sm">
pass
</Badge>
</TableCell>
</TableRow>
</TableBody>
</Table>

View file

@ -15,8 +15,7 @@ export function SectionComponentAPIs(): React.ReactElement {
return (
<FieldsetSection title="13. component apis" id="api">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Complete API documentation for each component with props, types,
and usage examples.
Complete API documentation for each component with props, types, and usage examples.
</p>
<SubSection title="button api">
@ -33,13 +32,18 @@ export function SectionComponentAPIs(): React.ReactElement {
<TableBody>
<TableRow>
<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>visual style</TableCell>
</TableRow>
<TableRow>
<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>button size</TableCell>
</TableRow>
@ -94,13 +98,17 @@ export function SectionComponentAPIs(): React.ReactElement {
<TableBody>
<TableRow>
<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>visual state</TableCell>
</TableRow>
<TableRow>
<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>input size</TableCell>
</TableRow>

View file

@ -7,7 +7,7 @@ import {
AccordionTrigger,
} from '@tpmjs/ui/Accordion/Accordion';
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 {
Card,
@ -270,7 +270,13 @@ export function SectionComponents({
{/* Slider */}
<SubSection title="slider">
<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>
</div>
</SubSection>
@ -350,11 +356,7 @@ export function SectionComponents({
{/* Pagination */}
<SubSection title="pagination">
<Pagination
page={currentPage}
totalPages={10}
onPageChange={setCurrentPage}
/>
<Pagination page={currentPage} totalPages={10} onPageChange={setCurrentPage} />
</SubSection>
{/* Dropdown Menu */}
@ -408,7 +410,9 @@ export function SectionComponents({
description="This is a modal dialog."
footer={
<>
<Button variant="outline" onClick={() => setShowModal(false)}>Cancel</Button>
<Button variant="outline" onClick={() => setShowModal(false)}>
Cancel
</Button>
<Button onClick={() => setShowModal(false)}>Confirm</Button>
</>
}
@ -419,12 +423,10 @@ export function SectionComponents({
{/* Drawer */}
<SubSection title="drawer">
<Button variant="outline" onClick={() => setShowDrawer(true)}>Open Drawer</Button>
<Drawer
open={showDrawer}
onClose={() => setShowDrawer(false)}
title="Drawer Content"
>
<Button variant="outline" onClick={() => setShowDrawer(true)}>
Open Drawer
</Button>
<Drawer open={showDrawer} onClose={() => setShowDrawer(false)} title="Drawer Content">
<p className="text-foreground-secondary">
This is the drawer content. It slides in from the side.
</p>

View file

@ -1,10 +1,7 @@
'use client';
import { Button } from '@tpmjs/ui/Button/Button';
import {
Card,
CardContent,
} from '@tpmjs/ui/Card/Card';
import { Card, CardContent } from '@tpmjs/ui/Card/Card';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Spinner } from '@tpmjs/ui/Spinner/Spinner';
import { DoDontCard, FieldsetSection, SubSection } from './shared';
@ -13,8 +10,7 @@ export function SectionContent(): React.ReactElement {
return (
<FieldsetSection title="8. content guidelines" id="content">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Voice and tone guidelines for consistent, helpful communication
across the platform.
Voice and tone guidelines for consistent, helpful communication across the platform.
</p>
<SubSection title="voice & tone">
@ -22,19 +18,35 @@ export function SectionContent(): React.ReactElement {
<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>
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
<li> <strong>Technical:</strong> Precise, accurate, developer-friendly</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>
<li>
<strong>Technical:</strong> Precise, accurate, developer-friendly
</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>
</div>
<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>
<ul className="space-y-2 text-sm text-foreground-secondary font-sans">
<li> <strong>Marketing-speak:</strong> No "revolutionary" or "game-changing"</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>
<li>
<strong>Marketing-speak:</strong> No "revolutionary" or "game-changing"
</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>
</div>
</div>
@ -46,15 +58,23 @@ export function SectionContent(): React.ReactElement {
<DoDontCard type="do" title="Use action verbs for buttons">
<div className="flex gap-3">
<Button size="sm">publish</Button>
<Button size="sm" variant="secondary">save draft</Button>
<Button size="sm" variant="destructive">delete</Button>
<Button size="sm" variant="secondary">
save draft
</Button>
<Button size="sm" variant="destructive">
delete
</Button>
</div>
</DoDontCard>
<DoDontCard type="dont" title="Avoid generic labels">
<div className="flex gap-3">
<Button size="sm">submit</Button>
<Button size="sm" variant="secondary">ok</Button>
<Button size="sm" variant="destructive">yes</Button>
<Button size="sm" variant="secondary">
ok
</Button>
<Button size="sm" variant="destructive">
yes
</Button>
</div>
</DoDontCard>
</div>
@ -67,7 +87,8 @@ export function SectionContent(): React.ReactElement {
</p>
<div className="space-y-3">
<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 className="p-3 bg-error-light border border-error text-sm">
<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">
Try adjusting your search or filters.
</p>
<Button size="sm" variant="outline">clear filters</Button>
<Button size="sm" variant="outline">
clear filters
</Button>
</CardContent>
</Card>
@ -108,7 +131,9 @@ export function SectionContent(): React.ReactElement {
<p className="font-sans text-xs text-foreground-secondary mb-4">
Check your connection and try again.
</p>
<Button size="sm" variant="outline">retry</Button>
<Button size="sm" variant="outline">
retry
</Button>
</CardContent>
</Card>
</div>
@ -134,7 +159,9 @@ export function SectionContent(): React.ReactElement {
</p>
<div className="flex items-center gap-4">
<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>

View file

@ -10,15 +10,35 @@ import {
TableHeader,
TableRow,
} from '@tpmjs/ui/Table/Table';
import { FieldsetSection, SubSection, DoDontCard } from './shared';
import { DoDontCard, FieldsetSection, SubSection } from './shared';
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: '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: 'Execute', definition: 'Run a tool with specific parameters', avoid: 'Invoke, Call, Trigger' },
{
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: 'Execute',
definition: 'Run a tool with specific parameters',
avoid: 'Invoke, Call, Trigger',
},
];
const verbConsistency = [
@ -51,8 +71,8 @@ export function SectionContentGuidelines(): React.ReactElement {
return (
<FieldsetSection title="20. content & writing" id="content-guidelines">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Consistent language and terminology across the platform.
These guidelines ensure clarity for users and maintainability for developers.
Consistent language and terminology across the platform. These guidelines ensure clarity for
users and maintainability for developers.
</p>
<SubSection title="glossary">
@ -72,9 +92,13 @@ export function SectionContentGuidelines(): React.ReactElement {
{glossaryTerms.map((item) => (
<TableRow key={item.term}>
<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>
<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>
</TableRow>
))}
@ -101,10 +125,14 @@ export function SectionContentGuidelines(): React.ReactElement {
<TableRow key={item.action}>
<TableCell className="text-sm text-foreground-secondary">{item.action}</TableCell>
<TableCell>
<Badge variant="default" size="sm">{item.use}</Badge>
<Badge variant="default" size="sm">
{item.use}
</Badge>
</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>
</TableRow>
))}
@ -211,11 +239,15 @@ export function SectionContentGuidelines(): React.ReactElement {
<div className="space-y-3">
<div className="p-3 bg-error-light border-l-2 border-error text-sm">
<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 className="p-3 bg-error-light border-l-2 border-error text-sm">
<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>
</DoDontCard>
@ -242,14 +274,18 @@ export function SectionIconSystem(): React.ReactElement {
return (
<FieldsetSection title="21. icon system" id="icon-system">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Icons from Lucide React, used consistently across components.
All icons use 2px stroke weight and are available in multiple sizes.
Icons from Lucide React, used consistently across components. All icons use 2px stroke
weight and are available in multiple sizes.
</p>
<SubSection title="icon library">
<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>.
Icons should be imported from <code className="text-xs bg-surface-2 px-1">@tpmjs/ui/Icon/Icon</code>.
Source:{' '}
<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>
<div className="bg-surface p-4 border border-dashed border-border">
<h4 className="font-mono text-sm font-medium mb-4">common icons</h4>
@ -316,7 +352,9 @@ export function SectionIconSystem(): React.ReactElement {
</TableCell>
<TableCell className="text-sm text-foreground-secondary">{item.usage}</TableCell>
<TableCell>
<Badge variant="outline" size="sm">{item.size}</Badge>
<Badge variant="outline" size="sm">
{item.size}
</Badge>
</TableCell>
</TableRow>
))}
@ -383,15 +421,20 @@ export function SectionIconSystem(): React.ReactElement {
<ol className="space-y-3 text-sm text-foreground-secondary font-sans">
<li className="flex gap-3">
<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 className="flex gap-3">
<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 className="flex gap-3">
<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 className="flex gap-3">
<span className="font-mono text-accent">4.</span>
@ -399,9 +442,11 @@ export function SectionIconSystem(): React.ReactElement {
</li>
</ol>
<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">
{`// icons.ts
{`// icons.ts
import { Heart } from 'lucide-react';
export const icons = {

View file

@ -11,12 +11,14 @@ interface SectionDataVizProps {
onCounterChange: (value: number) => void;
}
export function SectionDataViz({ counterValue, onCounterChange }: SectionDataVizProps): React.ReactElement {
export function SectionDataViz({
counterValue,
onCounterChange,
}: SectionDataVizProps): React.ReactElement {
return (
<FieldsetSection title="9. data visualization" id="data-viz">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Guidelines for charts, metrics, and data displays. Semantic colors only
no rainbow charts.
Guidelines for charts, metrics, and data displays. Semantic colors only no rainbow charts.
</p>
<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.
</p>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<StatCard
label="excellent"
value={95}
suffix="%"
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}
/>
<StatCard label="excellent" value={95} suffix="%" 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>
</SubSection>
@ -94,7 +72,9 @@ export function SectionDataViz({ counterValue, onCounterChange }: SectionDataViz
<ProgressBar value={100} variant="success" />
</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" />
</div>
<div>
@ -132,9 +112,23 @@ export function SectionDataViz({ counterValue, onCounterChange }: SectionDataViz
</div>
</div>
<div className="flex gap-2 mt-6 justify-center">
<Button size="sm" onClick={() => onCounterChange(counterValue + 1000)}>+1000</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>
<Button size="sm" onClick={() => onCounterChange(counterValue + 1000)}>
+1000
</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>
</SubSection>
</FieldsetSection>

View file

@ -37,13 +37,19 @@ export function SectionIcons(): React.ReactElement {
<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>
<p className="font-sans text-sm text-foreground-secondary mb-4">
Only allowed when the action is universally understood
(close, search, menu) AND space is limited.
Only allowed when the action is universally understood (close, search, menu) AND space
is limited.
</p>
<div className="flex gap-2">
<Button size="icon" variant="ghost"><Icon icon="x" size="sm" /></Button>
<Button size="icon" variant="ghost"><Icon icon="search" size="sm" /></Button>
<Button size="icon" variant="ghost"><Icon icon="menu" size="sm" /></Button>
<Button size="icon" variant="ghost">
<Icon icon="x" size="sm" />
</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 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.
</p>
<div className="flex gap-2">
<Button size="sm"><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>
<Button size="sm">
<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>
@ -83,16 +93,51 @@ export function SectionIcons(): React.ReactElement {
<SubSection title="all icons">
<div className="grid grid-cols-6 md:grid-cols-8 lg:grid-cols-12 gap-6">
{[
'copy', 'github', 'check', 'x', 'chevronDown', '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',
'copy',
'github',
'check',
'x',
'chevronDown',
'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) => (
<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" />
<span className="font-mono text-[10px] text-foreground-tertiary truncate max-w-full">{iconName}</span>
<Icon
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>

View file

@ -17,12 +17,15 @@ interface SectionLayoutProps {
onDensityChange: (density: 'compact' | 'comfortable' | 'spacious') => void;
}
export function SectionLayout({ density, onDensityChange }: SectionLayoutProps): React.ReactElement {
export function SectionLayout({
density,
onDensityChange,
}: SectionLayoutProps): React.ReactElement {
return (
<FieldsetSection title="7. layout & responsiveness" id="layout">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Mobile-first responsive design with clear breakpoints and density modes
for different use cases.
Mobile-first responsive design with clear breakpoints and density modes for different use
cases.
</p>
<SubSection title="breakpoints">
@ -37,18 +40,26 @@ export function SectionLayout({ density, onDensityChange }: SectionLayoutProps):
<div className="bg-surface p-6 border border-dashed border-border">
<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">
<li> <strong>Sidebar:</strong> Collapsible on mobile, visible 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>
<li>
<strong>Sidebar:</strong> Collapsible on mobile, visible 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>
</div>
</SubSection>
<SubSection title="density modes">
<p className="font-sans text-sm text-foreground-secondary mb-6">
Density modes adjust spacing, font size, and row heights for different contexts.
Essential for data-dense developer tools.
Density modes adjust spacing, font size, and row heights for different contexts. Essential
for data-dense developer tools.
</p>
<div className="flex gap-4 mb-6">
@ -88,17 +99,29 @@ export function SectionLayout({ density, onDensityChange }: SectionLayoutProps):
<TableRow>
<TableCell className="density-cell font-mono">@tpmjs/parser</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>
<TableCell className="density-cell font-mono">@tpmjs/validator</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>
<TableCell className="density-cell font-mono">@tpmjs/transform</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>
</TableBody>
</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>
<div className="grid grid-cols-12 gap-2">
{[...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>
</div>
))}
@ -134,7 +160,9 @@ export function SectionLayout({ density, onDensityChange }: SectionLayoutProps):
</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="col-span-3 bg-surface border border-dashed border-border p-4">
<span className="font-mono text-xs">sidebar</span>

View file

@ -7,8 +7,8 @@ export function SectionMotion(): React.ReactElement {
return (
<FieldsetSection title="5. motion" id="motion">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Motion communicates state changes, not decoration. Animations are fast
by default and respect user preferences.
Motion communicates state changes, not decoration. Animations are fast by default and
respect user preferences.
</p>
<SubSection title="motion principles">
@ -16,29 +16,29 @@ export function SectionMotion(): React.ReactElement {
<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>
<p className="font-sans text-sm text-foreground-secondary">
Most transitions complete in 150-200ms. Users should never wait
for animations to finish before interacting.
Most transitions complete in 150-200ms. Users should never wait for animations to
finish before interacting.
</p>
</div>
<div className="bg-surface p-6 border border-dashed border-border">
<h4 className="font-mono text-sm font-medium mb-4">state communication</h4>
<p className="font-sans text-sm text-foreground-secondary">
Motion indicates something changed: a panel opened, an item was
selected, data updated. Never animate just for visual interest.
Motion indicates something changed: a panel opened, an item was selected, data
updated. Never animate just for visual interest.
</p>
</div>
<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>
<p className="font-sans text-sm text-foreground-secondary">
Data updates (counters, progress bars, charts) use linear easing.
This feels more precise and mechanical.
Data updates (counters, progress bars, charts) use linear easing. This feels more
precise and mechanical.
</p>
</div>
<div className="bg-surface p-6 border border-dashed border-border">
<h4 className="font-mono text-sm font-medium mb-4">respect preferences</h4>
<p className="font-sans text-sm text-foreground-secondary">
Honor prefers-reduced-motion. All animations disable when the
user has requested reduced motion.
Honor prefers-reduced-motion. All animations disable when the user has requested
reduced motion.
</p>
</div>
</div>
@ -46,11 +46,31 @@ export function SectionMotion(): React.ReactElement {
<SubSection title="duration tokens">
<div className="space-y-4">
<TokenRow name="--motion-instant" value="0ms" 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" />} />
<TokenRow
name="--motion-instant"
value="0ms"
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>
</SubSection>
@ -66,9 +86,7 @@ export function SectionMotion(): React.ReactElement {
<SubSection title="interactive demo">
<div className="flex flex-wrap gap-4">
<Button className="motion-fast transition-all hover:scale-105">
fast (150ms)
</Button>
<Button className="motion-fast transition-all hover:scale-105">fast (150ms)</Button>
<Button variant="secondary" className="motion-base transition-all hover:scale-105">
base (200ms)
</Button>

View file

@ -11,15 +11,17 @@ export function SectionPatternFeedback(): React.ReactElement {
return (
<FieldsetSection title="16. feedback patterns" id="feedback-patterns">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Feedback patterns communicate status, progress, and system responses.
Choose the right pattern based on context and urgency.
Feedback patterns communicate status, progress, and system responses. Choose the right
pattern based on context and urgency.
</p>
<SubSection title="feedback decision tree">
<div className="bg-surface p-6 border border-dashed border-border mb-6">
<div className="space-y-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>
<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">
@ -28,7 +30,9 @@ export function SectionPatternFeedback(): React.ReactElement {
</div>
</div>
<div className="flex items-start gap-4">
<Badge variant="info" className="mt-1">Q2</Badge>
<Badge variant="info" className="mt-1">
Q2
</Badge>
<div>
<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">
@ -37,7 +41,9 @@ export function SectionPatternFeedback(): React.ReactElement {
</div>
</div>
<div className="flex items-start gap-4">
<Badge variant="info" className="mt-1">Q3</Badge>
<Badge variant="info" className="mt-1">
Q3
</Badge>
<div>
<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">
@ -142,7 +148,9 @@ export function SectionPatternFeedback(): React.ReactElement {
<p className="font-sans text-xs text-foreground-secondary">
This version has known vulnerabilities. Update immediately.
</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>
@ -174,7 +182,9 @@ export function SectionPatternFeedback(): React.ReactElement {
Service degradation detected. Some API calls may fail. We are investigating.
</span>
</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>
{/* Announcement banner */}
@ -185,7 +195,9 @@ export function SectionPatternFeedback(): React.ReactElement {
New: AI-powered code review is now available for all tools!
</span>
</div>
<Button variant="secondary" size="sm">learn more</Button>
<Button variant="secondary" size="sm">
learn more
</Button>
</div>
</div>
</SubSection>
@ -238,11 +250,14 @@ export function SectionPatternFeedback(): React.ReactElement {
<div className="flex-1">
<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">
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>
<div className="flex gap-2">
<Button size="sm">retry</Button>
<Button size="sm" variant="ghost">view details</Button>
<Button size="sm" variant="ghost">
view details
</Button>
</div>
</div>
</div>

View file

@ -13,8 +13,8 @@ export function SectionPatternForms(): React.ReactElement {
return (
<FieldsetSection title="15. form patterns" id="form-patterns">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Consistent form patterns for data entry, validation, and submission.
Forms should guide users through tasks with clear feedback.
Consistent form patterns for data entry, validation, and submission. Forms should guide
users through tasks with clear feedback.
</p>
<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>
</div>
<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><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>
<li>
<a href="#name" className="text-error hover:underline">
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>
</div>
@ -79,10 +91,7 @@ export function SectionPatternForms(): React.ReactElement {
<SubSection title="help text placement">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<DoDontCard type="do" title="Help text below the input">
<FormField
label="api key"
helperText="You can find this in your dashboard settings"
>
<FormField label="api key" helperText="You can find this in your dashboard settings">
<Input placeholder="sk-xxxx..." />
</FormField>
</DoDontCard>
@ -101,7 +110,9 @@ export function SectionPatternForms(): React.ReactElement {
<SubSection title="required vs optional">
<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">
<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">
<FormField label="name" required>
<Input placeholder="Tool name" />
@ -115,7 +126,9 @@ export function SectionPatternForms(): React.ReactElement {
</div>
</div>
<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">
<FormField label="website">
<Input placeholder="https://example.com" />
@ -131,8 +144,8 @@ export function SectionPatternForms(): React.ReactElement {
</div>
<div className="bg-surface p-4 border border-dashed border-border">
<p className="font-sans text-sm text-foreground-secondary">
<strong>Rule:</strong> Mark the minority. If most fields are required, mark optional fields.
If most are optional, mark required fields.
<strong>Rule:</strong> Mark the minority. If most fields are required, mark optional
fields. If most are optional, mark required fields.
</p>
</div>
</SubSection>
@ -213,7 +226,9 @@ export function SectionPatternForms(): React.ReactElement {
Once deleted, this tool cannot be recovered.
</p>
</div>
<Button variant="destructive" size="sm">delete tool</Button>
<Button variant="destructive" size="sm">
delete tool
</Button>
</div>
<div className="flex items-center justify-between py-4 border-t border-dashed border-error">
<div>
@ -222,7 +237,9 @@ export function SectionPatternForms(): React.ReactElement {
Transfer this tool to another user or organization.
</p>
</div>
<Button variant="outline" size="sm">transfer</Button>
<Button variant="outline" size="sm">
transfer
</Button>
</div>
</div>
</SubSection>

View file

@ -1,7 +1,13 @@
'use client';
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 { Icon } from '@tpmjs/ui/Icon/Icon';
import { Tabs } from '@tpmjs/ui/Tabs/Tabs';
@ -14,8 +20,8 @@ export function SectionPatternNavigation(): React.ReactElement {
return (
<FieldsetSection title="14. navigation patterns" id="nav-patterns">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Navigation patterns for consistent wayfinding across the platform. Each pattern
has specific use cases and accessibility requirements.
Navigation patterns for consistent wayfinding across the platform. Each pattern has specific
use cases and accessibility requirements.
</p>
<SubSection title="global navigation (header)">
@ -28,14 +34,36 @@ export function SectionPatternNavigation(): React.ReactElement {
<div className="flex items-center gap-8">
<span className="font-mono text-lg font-semibold">tpmjs</span>
<nav className="flex items-center gap-6">
<a href="#" className="font-mono text-sm text-foreground hover:text-accent transition-colors">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>
<a
href="#"
className="font-mono text-sm text-foreground hover:text-accent transition-colors"
>
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>
</div>
<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>
</div>
</div>
@ -60,20 +88,34 @@ export function SectionPatternNavigation(): React.ReactElement {
<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>
<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" />
<span>dashboard</span>
</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" />
<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 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" />
<span>api keys</span>
</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" />
<span>settings</span>
</a>
@ -102,10 +144,20 @@ export function SectionPatternNavigation(): React.ReactElement {
<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>
<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><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>
<kbd className="px-2 py-1 bg-surface-2 font-mono text-xs">[</kbd> collapse/expand
sidebar
</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>
</SubSection>
@ -181,10 +233,23 @@ export function SectionPatternNavigation(): React.ReactElement {
<div className="bg-surface p-4 border border-dashed border-border">
<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">
<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> <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>
<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>
<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>
</div>
</div>
@ -205,13 +270,28 @@ export function SectionPatternNavigation(): React.ReactElement {
</div>
{/* Expanded menu preview */}
<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 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>
<a
href="#"
className="block px-3 py-2 font-mono text-sm text-foreground bg-accent/10"
>
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">
<Button variant="outline" size="sm" className="w-full">sign in</Button>
<Button size="sm" className="w-full">get started</Button>
<Button variant="outline" size="sm" className="w-full">
sign in
</Button>
<Button size="sm" className="w-full">
get started
</Button>
</div>
</div>
</div>

View file

@ -11,8 +11,8 @@ export function SectionPatternSearch(): React.ReactElement {
return (
<FieldsetSection title="18. search & filtering" id="search-patterns">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Search and filter patterns help users find content quickly.
Design for progressive disclosure and instant feedback.
Search and filter patterns help users find content quickly. Design for progressive
disclosure and instant feedback.
</p>
<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">
<p className="font-mono text-xs text-foreground-tertiary mb-3">default</p>
<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" />
</div>
</div>
@ -33,7 +37,11 @@ export function SectionPatternSearch(): React.ReactElement {
<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>
<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" />
<button className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-tertiary hover:text-foreground">
<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">
<p className="font-mono text-xs text-foreground-tertiary mb-3">loading</p>
<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 />
</div>
</div>
@ -54,7 +66,11 @@ export function SectionPatternSearch(): React.ReactElement {
<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>
<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" />
<button className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-tertiary hover:text-foreground">
<Icon icon="x" size="sm" />
@ -91,9 +107,7 @@ export function SectionPatternSearch(): React.ReactElement {
</Badge>
<button className="font-mono text-xs text-accent hover:underline">clear all</button>
</div>
<p className="font-mono text-xs text-foreground-secondary">
showing 42 of 128 tools
</p>
<p className="font-mono text-xs text-foreground-secondary">showing 42 of 128 tools</p>
</div>
</SubSection>
@ -107,7 +121,9 @@ export function SectionPatternSearch(): React.ReactElement {
<h4 className="font-mono text-sm font-medium mb-4">filters</h4>
<div className="space-y-4">
<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
placeholder="all categories"
options={[
@ -118,7 +134,9 @@ export function SectionPatternSearch(): React.ReactElement {
/>
</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
placeholder="all statuses"
options={[
@ -129,11 +147,15 @@ export function SectionPatternSearch(): React.ReactElement {
/>
</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" />
</div>
<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>
@ -143,9 +165,14 @@ export function SectionPatternSearch(): React.ReactElement {
<p className="font-mono text-xs text-foreground-tertiary mb-4">results area</p>
<div className="space-y-2">
{['@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>
<Badge variant="success" size="sm">active</Badge>
<Badge variant="success" size="sm">
active
</Badge>
</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 gap-2">
<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>
<Button size="sm" variant="ghost">
<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">
<span>my active tools</span>
<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" />
</div>
</button>
<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>
<Badge variant="outline" size="sm">8 results</Badge>
<Badge variant="outline" size="sm">
8 results
</Badge>
</button>
<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>
<Badge variant="outline" size="sm">15 results</Badge>
<Badge variant="outline" size="sm">
15 results
</Badge>
</button>
</div>
</div>
@ -225,8 +260,15 @@ export function SectionPatternSearch(): React.ReactElement {
{/* Search input */}
<div className="border-b border-border p-3">
<div className="relative">
<Icon 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" />
<Icon
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>
@ -234,7 +276,9 @@ export function SectionPatternSearch(): React.ReactElement {
<div className="max-h-80 overflow-y-auto">
{/* Recent */}
<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">
<Icon icon="clock" size="sm" className="text-foreground-tertiary" />
<span className="font-mono text-sm">@tpmjs/parser</span>
@ -243,7 +287,9 @@ export function SectionPatternSearch(): React.ReactElement {
{/* Actions */}
<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">
<div className="flex items-center gap-3">
<Icon icon="plus" size="sm" className="text-foreground-tertiary" />
@ -271,16 +317,24 @@ export function SectionPatternSearch(): React.ReactElement {
{/* Footer */}
<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">
<span><kbd className="bg-surface-2 px-1.5 py-0.5"></kbd> navigate</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>
<span>
<kbd className="bg-surface-2 px-1.5 py-0.5"></kbd> navigate
</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 className="mt-4 text-center">
<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>
</SubSection>
</FieldsetSection>

View file

@ -21,24 +21,57 @@ export function SectionPatternTables(): React.ReactElement {
const [selectedRows, setSelectedRows] = useState<string[]>([]);
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: '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 },
{
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: '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) => {
setSelectedRows(prev =>
prev.includes(id) ? prev.filter(r => r !== id) : [...prev, id]
);
setSelectedRows((prev) => (prev.includes(id) ? prev.filter((r) => r !== id) : [...prev, id]));
};
const toggleAll = () => {
if (selectedRows.length === mockData.length) {
setSelectedRows([]);
} 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}>
<TableCell className="font-mono">{row.name}</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>
</TableRow>
))}
@ -98,8 +133,12 @@ export function SectionPatternTables(): React.ReactElement {
{selectedRows.length} item{selectedRows.length !== 1 ? 's' : ''} selected
</span>
<div className="flex gap-2">
<Button size="sm" variant="outline">export</Button>
<Button size="sm" variant="destructive">delete</Button>
<Button size="sm" variant="outline">
export
</Button>
<Button size="sm" variant="destructive">
delete
</Button>
</div>
</div>
)}
@ -121,7 +160,10 @@ export function SectionPatternTables(): React.ReactElement {
</TableHeader>
<TableBody>
{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>
<Checkbox
checked={selectedRows.includes(row.id)}
@ -133,13 +175,21 @@ export function SectionPatternTables(): React.ReactElement {
<TableCell>{row.category}</TableCell>
<TableCell>
<Badge
variant={row.status === 'active' ? 'success' : row.status === 'deprecated' ? 'error' : 'warning'}
variant={
row.status === 'active'
? 'success'
: row.status === 'deprecated'
? 'error'
: 'warning'
}
size="sm"
>
{row.status}
</Badge>
</TableCell>
<TableCell className="text-right font-mono">{row.downloads.toLocaleString()}</TableCell>
<TableCell className="text-right font-mono">
{row.downloads.toLocaleString()}
</TableCell>
</TableRow>
))}
</TableBody>
@ -167,23 +217,13 @@ export function SectionPatternTables(): React.ReactElement {
{/* Simple pagination */}
<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>
<Pagination
page={page}
totalPages={10}
onPageChange={setPage}
variant="simple"
/>
<Pagination page={page} totalPages={10} onPageChange={setPage} variant="simple" />
</div>
{/* Minimal pagination */}
<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>
<Pagination
page={page}
totalPages={10}
onPageChange={setPage}
variant="minimal"
/>
<Pagination page={page} totalPages={10} onPageChange={setPage} variant="minimal" />
</div>
</div>
</SubSection>
@ -215,9 +255,15 @@ export function SectionPatternTables(): React.ReactElement {
<TableBody>
{[1, 2, 3].map((i) => (
<TableRow key={i}>
<TableCell><div className="h-4 bg-muted animate-pulse w-32" /></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>
<TableCell>
<div className="h-4 bg-muted animate-pulse w-32" />
</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>
))}
</TableBody>

View file

@ -6,8 +6,8 @@ export function SectionPrinciples(): React.ReactElement {
return (
<FieldsetSection title="1. design principles" id="principles">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
These principles guide every design decision in TPMJS. They ensure consistency
and prevent drift as the system grows.
These principles guide every design decision in TPMJS. They ensure consistency and prevent
drift as the system grows.
</p>
<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">
<div className="bg-surface-2 p-6 border border-dashed border-border prose-width">
<p className="font-sans text-foreground leading-relaxed mb-4">
TPMJS is a <strong>developer platform</strong> for AI tools. The design reflects
this through industrial aesthetics: sharp edges, technical typography, and
a muted palette with copper as the signal color.
TPMJS is a <strong>developer platform</strong> for AI tools. The design reflects this
through industrial aesthetics: sharp edges, technical typography, and a muted palette
with copper as the signal color.
</p>
<p className="font-sans text-foreground-secondary leading-relaxed">
Unlike consumer products that aim for delight, TPMJS aims for
<strong> efficiency and trust</strong>. Users should feel confident that the
interface will behave predictably and help them accomplish tasks quickly.
<strong> efficiency and trust</strong>. Users should feel confident that the interface
will behave predictably and help them accomplish tasks quickly.
</p>
</div>
</SubSection>

View file

@ -6,8 +6,8 @@ export function SectionSpacing(): React.ReactElement {
return (
<FieldsetSection title="4. spacing" id="spacing">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Consistent spacing creates visual rhythm. Use the 4px base unit
and generous whitespace for clarity.
Consistent spacing creates visual rhythm. Use the 4px base unit and generous whitespace for
clarity.
</p>
<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) => (
<div key={space} className="flex items-center gap-4">
<span className="font-mono text-xs text-foreground-secondary w-12">{space}</span>
<div
className="bg-accent h-4"
style={{ width: `${space * 4}px` }}
/>
<div className="bg-accent h-4" style={{ width: `${space * 4}px` }} />
<span className="font-mono text-xs text-foreground-tertiary">{space * 4}px</span>
</div>
))}

View file

@ -82,11 +82,17 @@ export function SectionTheming(): React.ReactElement {
<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>
<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> Do not override core tokens (raw values)</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>
</div>
</SubSection>

View file

@ -6,8 +6,8 @@ export function SectionTypography(): React.ReactElement {
return (
<FieldsetSection title="3. typography" id="typography">
<p className="text-foreground-secondary mb-8 font-sans prose-width">
Two font families create clear hierarchy: monospace for headings and technical
content, sans-serif for body text and descriptions.
Two font families create clear hierarchy: monospace for headings and technical content,
sans-serif for body text and descriptions.
</p>
<SubSection title="font families">
@ -15,37 +15,97 @@ export function SectionTypography(): React.ReactElement {
<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-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 className="bg-surface p-6 border border-dashed border-border">
<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-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>
</SubSection>
<SubSection title="type scale">
<div className="space-y-4">
<TokenRow name="--text-xs" value="12px / 0.75rem" preview={<span className="text-xs font-mono">Aa</span>} />
<TokenRow name="--text-sm" value="14px / 0.875rem" 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>} />
<TokenRow
name="--text-xs"
value="12px / 0.75rem"
preview={<span className="text-xs font-mono">Aa</span>}
/>
<TokenRow
name="--text-sm"
value="14px / 0.875rem"
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>
</SubSection>
<SubSection title="line height">
<div className="space-y-4">
<TokenRow name="--leading-tight" value="1.2" preview={<div className="w-full h-3 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" />} />
<TokenRow
name="--leading-tight"
value="1.2"
preview={<div className="w-full h-3 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>
</SubSection>
@ -57,9 +117,9 @@ export function SectionTypography(): React.ReactElement {
</div>
<div className="bg-surface p-6 border border-dashed border-border">
<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
for reading comprehension. Lines that are too long cause eye fatigue, while
lines that are too short disrupt reading rhythm.
This paragraph is constrained to 65 characters per line, the optimal width for reading
comprehension. Lines that are too long cause eye fatigue, while lines that are too short
disrupt reading rhythm.
</p>
</div>
</SubSection>
@ -91,10 +151,18 @@ export function SectionTypography(): React.ReactElement {
<SubSection title="heading hierarchy">
<div className="space-y-6">
<h1 className="font-mono text-5xl font-semibold tracking-tight lowercase">heading 1 (48px)</h1>
<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>
<h1 className="font-mono text-5xl font-semibold tracking-tight lowercase">
heading 1 (48px)
</h1>
<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>
<h6 className="font-mono text-lg font-medium lowercase">heading 6 (18px)</h6>
</div>

View file

@ -1,40 +1,36 @@
// 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
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 { 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;
}): React.ReactElement {
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">
{title}
</legend>
@ -62,13 +65,19 @@ export function ColorCard({
}): React.ReactElement {
return (
<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}
</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}
</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}
</div>
</div>
@ -95,9 +104,7 @@ export function DoDontCard({
<span className="font-mono text-sm font-medium uppercase">{isDo ? 'do' : "don't"}</span>
</div>
<p className="font-mono text-xs text-foreground-secondary mb-3">{title}</p>
<div className="bg-surface p-3 border border-dashed border-border">
{children}
</div>
<div className="bg-surface p-3 border border-dashed border-border">{children}</div>
</div>
);
}
@ -118,7 +125,11 @@ export function PrincipleCard({
<div className="border border-dashed border-border p-6">
<div className="flex items-center gap-3 mb-4">
<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>
<h4 className="font-mono text-base font-medium text-foreground">{title}</h4>
</div>

View file

@ -47,9 +47,7 @@ export function useAgents(params: UseAgentsParams = {}) {
const queryString = searchParams.toString();
// Custom response handler since the API returns { success, data, pagination }
return useSWR<AgentsResponse>(
`/api/public/agents?${queryString}`,
async (url: string) => {
return useSWR<AgentsResponse>(`/api/public/agents?${queryString}`, async (url: string) => {
const res = await fetch(url);
const json = await res.json();
@ -61,6 +59,5 @@ export function useAgents(params: UseAgentsParams = {}) {
agents: json.data,
pagination: json.pagination,
};
}
);
});
}

View file

@ -21,11 +21,8 @@ export function useBundleSize(packageName: string | undefined, version?: string)
}
}
return useSWR<BundleSizeData>(
packageName ? `/api/bundlephobia?${params.toString()}` : null,
{
return useSWR<BundleSizeData>(packageName ? `/api/bundlephobia?${params.toString()}` : null, {
// Don't retry on 404s (common for scoped packages)
shouldRetryOnError: false,
}
);
});
}

Some files were not shown because too many files have changed in this diff Show more