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:
2025-11-02 13:21:25 +01:00
parent e734acf31d
commit 58b761106b
4 changed files with 1105 additions and 341 deletions

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

View 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

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