Add reusable Example, ExampleGrid, and ExampleSection components for live UI demonstrations with code previews. Refactor ComponentShowcase to use new components, improving structure, maintainability, and documentation coverage. Include semantic updates to labels and descriptions.
This commit is contained in:
136
frontend/src/components/dev/BeforeAfter.tsx
Normal file
136
frontend/src/components/dev/BeforeAfter.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
/**
|
||||
* BeforeAfter Component
|
||||
* Side-by-side comparison component for demonstrating anti-patterns vs best practices
|
||||
* This file is excluded from coverage as it's a demo/showcase component
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { AlertTriangle, CheckCircle2 } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface BeforeAfterProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
before: {
|
||||
label?: string;
|
||||
content: React.ReactNode;
|
||||
caption?: string;
|
||||
};
|
||||
after: {
|
||||
label?: string;
|
||||
content: React.ReactNode;
|
||||
caption?: string;
|
||||
};
|
||||
vertical?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* BeforeAfter - Side-by-side comparison component
|
||||
*
|
||||
* @example
|
||||
* <BeforeAfter
|
||||
* title="Spacing Anti-pattern"
|
||||
* description="Parent should control spacing, not children"
|
||||
* before={{
|
||||
* content: <div className="mt-4">Child with margin</div>,
|
||||
* caption: "Child controls its own spacing"
|
||||
* }}
|
||||
* after={{
|
||||
* content: <div className="space-y-4"><div>Child</div></div>,
|
||||
* caption: "Parent controls spacing with gap/space-y"
|
||||
* }}
|
||||
* />
|
||||
*/
|
||||
export function BeforeAfter({
|
||||
title,
|
||||
description,
|
||||
before,
|
||||
after,
|
||||
vertical = false,
|
||||
className,
|
||||
}: BeforeAfterProps) {
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
{/* Header */}
|
||||
{(title || description) && (
|
||||
<div className="space-y-2">
|
||||
{title && <h3 className="text-xl font-semibold">{title}</h3>}
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Comparison Grid */}
|
||||
<div
|
||||
className={cn(
|
||||
'grid gap-4',
|
||||
vertical ? 'grid-cols-1' : 'grid-cols-1 lg:grid-cols-2'
|
||||
)}
|
||||
>
|
||||
{/* Before (Anti-pattern) */}
|
||||
<Card className="border-destructive/50">
|
||||
<CardHeader className="space-y-2 pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">
|
||||
{before.label || '❌ Before (Anti-pattern)'}
|
||||
</CardTitle>
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
Avoid
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* Demo content */}
|
||||
<div className="rounded-lg border border-destructive/50 bg-muted/50 p-4">
|
||||
{before.content}
|
||||
</div>
|
||||
{/* Caption */}
|
||||
{before.caption && (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
{before.caption}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* After (Best practice) */}
|
||||
<Card className="border-green-500/50 dark:border-green-400/50">
|
||||
<CardHeader className="space-y-2 pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">
|
||||
{after.label || '✅ After (Best practice)'}
|
||||
</CardTitle>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="gap-1 border-green-500 text-green-600 dark:border-green-400 dark:text-green-400"
|
||||
>
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
Correct
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* Demo content */}
|
||||
<div className="rounded-lg border border-green-500/50 bg-green-500/5 p-4 dark:border-green-400/50 dark:bg-green-400/5">
|
||||
{after.content}
|
||||
</div>
|
||||
{/* Caption */}
|
||||
{after.caption && (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
{after.caption}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
178
frontend/src/components/dev/CodeSnippet.tsx
Normal file
178
frontend/src/components/dev/CodeSnippet.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
/**
|
||||
* CodeSnippet Component
|
||||
* Displays syntax-highlighted code with copy-to-clipboard functionality
|
||||
* This file is excluded from coverage as it's a demo/showcase component
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface CodeSnippetProps {
|
||||
code: string;
|
||||
language?: 'tsx' | 'typescript' | 'javascript' | 'css' | 'bash' | 'json';
|
||||
title?: string;
|
||||
showLineNumbers?: boolean;
|
||||
highlightLines?: number[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodeSnippet - Syntax-highlighted code block with copy button
|
||||
*
|
||||
* @example
|
||||
* <CodeSnippet
|
||||
* title="Button Component"
|
||||
* language="tsx"
|
||||
* code={`<Button variant="default">Click me</Button>`}
|
||||
* showLineNumbers
|
||||
* />
|
||||
*/
|
||||
export function CodeSnippet({
|
||||
code,
|
||||
language = 'tsx',
|
||||
title,
|
||||
showLineNumbers = false,
|
||||
highlightLines = [],
|
||||
className,
|
||||
}: CodeSnippetProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy code:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const lines = code.split('\n');
|
||||
|
||||
return (
|
||||
<div className={cn('relative group', className)}>
|
||||
{/* Header */}
|
||||
{(title || language) && (
|
||||
<div className="flex items-center justify-between rounded-t-lg border border-b-0 bg-muted/50 px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{title && (
|
||||
<span className="text-sm font-medium text-foreground">{title}</span>
|
||||
)}
|
||||
{language && (
|
||||
<span className="text-xs text-muted-foreground">({language})</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="h-7 gap-1 px-2 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
aria-label="Copy code"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-3 w-3" />
|
||||
<span className="text-xs">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-3 w-3" />
|
||||
<span className="text-xs">Copy</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Code Block */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-x-auto rounded-lg border bg-muted/30',
|
||||
title || language ? 'rounded-t-none' : ''
|
||||
)}
|
||||
>
|
||||
{/* Copy button (when no header) */}
|
||||
{!title && !language && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="absolute right-2 top-2 z-10 h-7 gap-1 px-2 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
aria-label="Copy code"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-3 w-3" />
|
||||
<span className="text-xs">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-3 w-3" />
|
||||
<span className="text-xs">Copy</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<pre className="p-4 text-sm">
|
||||
<code className={cn('font-mono', `language-${language}`)}>
|
||||
{showLineNumbers ? (
|
||||
<div className="flex">
|
||||
{/* Line numbers */}
|
||||
<div className="mr-4 select-none border-r pr-4 text-right text-muted-foreground">
|
||||
{lines.map((_, idx) => (
|
||||
<div key={idx} className="leading-6">
|
||||
{idx + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Code lines */}
|
||||
<div className="flex-1">
|
||||
{lines.map((line, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={cn(
|
||||
'leading-6',
|
||||
highlightLines.includes(idx + 1) &&
|
||||
'bg-accent/20 -mx-4 px-4'
|
||||
)}
|
||||
>
|
||||
{line || ' '}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
code
|
||||
)}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* CodeGroup - Group multiple related code snippets
|
||||
*
|
||||
* @example
|
||||
* <CodeGroup>
|
||||
* <CodeSnippet title="Component" code="..." />
|
||||
* <CodeSnippet title="Usage" code="..." />
|
||||
* </CodeGroup>
|
||||
*/
|
||||
export function CodeGroup({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn('space-y-4', className)}>{children}</div>;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
220
frontend/src/components/dev/Example.tsx
Normal file
220
frontend/src/components/dev/Example.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
/**
|
||||
* Example Component
|
||||
* Container for live component demonstrations with optional code display
|
||||
* This file is excluded from coverage as it's a demo/showcase component
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Code2, Eye } from 'lucide-react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { CodeSnippet } from './CodeSnippet';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ExampleProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
code?: string;
|
||||
variant?: 'default' | 'compact';
|
||||
className?: string;
|
||||
centered?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Example - Live component demonstration container
|
||||
*
|
||||
* @example
|
||||
* <Example
|
||||
* title="Primary Button"
|
||||
* description="Default button variant for primary actions"
|
||||
* code={`<Button variant="default">Click me</Button>`}
|
||||
* >
|
||||
* <Button variant="default">Click me</Button>
|
||||
* </Example>
|
||||
*/
|
||||
export function Example({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
code,
|
||||
variant = 'default',
|
||||
className,
|
||||
centered = false,
|
||||
tags,
|
||||
}: ExampleProps) {
|
||||
const [showCode, setShowCode] = useState(false);
|
||||
|
||||
// Compact variant - no card wrapper
|
||||
if (variant === 'compact') {
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
{/* Header */}
|
||||
{(title || description || tags) && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{title && <h3 className="text-lg font-semibold">{title}</h3>}
|
||||
{tags && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Demo */}
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg border bg-card p-6',
|
||||
centered && 'flex items-center justify-center'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Code */}
|
||||
{code && <CodeSnippet code={code} language="tsx" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default variant - full card with tabs
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
{title && <CardTitle>{title}</CardTitle>}
|
||||
{tags && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{code ? (
|
||||
<Tabs defaultValue="preview" className="w-full">
|
||||
<TabsList className="grid w-full max-w-[240px] grid-cols-2">
|
||||
<TabsTrigger value="preview" className="gap-1.5">
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
Preview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="code" className="gap-1.5">
|
||||
<Code2 className="h-3.5 w-3.5" />
|
||||
Code
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="preview" className="mt-4">
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg border bg-muted/30 p-6',
|
||||
centered && 'flex items-center justify-center'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="code" className="mt-4">
|
||||
<CodeSnippet code={code} language="tsx" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg border bg-muted/30 p-6',
|
||||
centered && 'flex items-center justify-center'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ExampleGrid - Grid layout for multiple examples
|
||||
*
|
||||
* @example
|
||||
* <ExampleGrid>
|
||||
* <Example title="Example 1">...</Example>
|
||||
* <Example title="Example 2">...</Example>
|
||||
* </ExampleGrid>
|
||||
*/
|
||||
export function ExampleGrid({
|
||||
children,
|
||||
cols = 2,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
cols?: 1 | 2 | 3;
|
||||
className?: string;
|
||||
}) {
|
||||
const colsClass = {
|
||||
1: 'grid-cols-1',
|
||||
2: 'grid-cols-1 lg:grid-cols-2',
|
||||
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
}[cols];
|
||||
|
||||
return (
|
||||
<div className={cn('grid gap-6', colsClass, className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ExampleSection - Section wrapper with title
|
||||
*
|
||||
* @example
|
||||
* <ExampleSection title="Button Variants" description="All available button styles">
|
||||
* <ExampleGrid>...</ExampleGrid>
|
||||
* </ExampleSection>
|
||||
*/
|
||||
export function ExampleSection({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
id,
|
||||
className,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<section id={id} className={cn('space-y-6', className)}>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{title}</h2>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user