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>;
|
||||
}
|
||||
@@ -2,17 +2,18 @@
|
||||
|
||||
/**
|
||||
* Component Showcase
|
||||
* Comprehensive display of all design system components
|
||||
* Comprehensive display of all design system components with copy-paste code
|
||||
* This file is excluded from coverage as it's a demo/showcase page
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Moon, Sun, Mail, User,
|
||||
Settings, LogOut, Shield, AlertCircle, Info,
|
||||
CheckCircle2, AlertTriangle, Trash2
|
||||
CheckCircle2, AlertTriangle, Trash2, ArrowLeft
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -66,20 +67,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
|
||||
/**
|
||||
* Section wrapper component
|
||||
*/
|
||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold text-foreground">{title}</h2>
|
||||
<div className="rounded-lg border bg-card p-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Example, ExampleGrid, ExampleSection } from './Example';
|
||||
|
||||
/**
|
||||
* Component showcase
|
||||
@@ -98,9 +86,16 @@ export function ComponentShowcase() {
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/dev">
|
||||
<Button variant="ghost" size="icon" aria-label="Back to hub">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Component Showcase</h1>
|
||||
<p className="text-sm text-muted-foreground">Development Preview</p>
|
||||
<p className="text-sm text-muted-foreground">All shadcn/ui components with code</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -117,91 +112,68 @@ export function ComponentShowcase() {
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<div className="space-y-12">
|
||||
{/* Colors */}
|
||||
<Section title="Colors">
|
||||
<ExampleSection
|
||||
id="colors"
|
||||
title="Colors"
|
||||
description="Semantic color tokens using OKLCH color space"
|
||||
>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<div className="h-20 rounded-lg bg-background border"></div>
|
||||
<p className="text-sm font-medium">Background</p>
|
||||
<p className="text-sm font-medium">bg-background</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-20 rounded-lg bg-foreground"></div>
|
||||
<p className="text-sm font-medium">Foreground</p>
|
||||
<p className="text-sm font-medium">bg-foreground</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-20 rounded-lg bg-card border"></div>
|
||||
<p className="text-sm font-medium">Card</p>
|
||||
<p className="text-sm font-medium">bg-card</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-20 rounded-lg bg-primary"></div>
|
||||
<p className="text-sm font-medium">Primary</p>
|
||||
<p className="text-sm font-medium">bg-primary</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-20 rounded-lg bg-secondary"></div>
|
||||
<p className="text-sm font-medium">Secondary</p>
|
||||
<p className="text-sm font-medium">bg-secondary</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-20 rounded-lg bg-muted"></div>
|
||||
<p className="text-sm font-medium">Muted</p>
|
||||
<p className="text-sm font-medium">bg-muted</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-20 rounded-lg bg-accent"></div>
|
||||
<p className="text-sm font-medium">Accent</p>
|
||||
<p className="text-sm font-medium">bg-accent</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-20 rounded-lg bg-destructive"></div>
|
||||
<p className="text-sm font-medium">Destructive</p>
|
||||
<p className="text-sm font-medium">bg-destructive</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-20 rounded-lg border-2 border-border"></div>
|
||||
<p className="text-sm font-medium">Border</p>
|
||||
<p className="text-sm font-medium">border-border</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Typography */}
|
||||
<Section title="Typography">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold">Heading 1</h1>
|
||||
<p className="text-sm text-muted-foreground">text-4xl font-bold</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-3xl font-semibold">Heading 2</h2>
|
||||
<p className="text-sm text-muted-foreground">text-3xl font-semibold</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">Heading 3</h3>
|
||||
<p className="text-sm text-muted-foreground">text-2xl font-semibold</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xl font-medium">Heading 4</h4>
|
||||
<p className="text-sm text-muted-foreground">text-xl font-medium</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-base">Body text - The quick brown fox jumps over the lazy dog</p>
|
||||
<p className="text-sm text-muted-foreground">text-base</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Small text - The quick brown fox jumps over the lazy dog
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">text-sm text-muted-foreground</p>
|
||||
</div>
|
||||
<div>
|
||||
<code className="rounded bg-muted px-2 py-1 font-mono text-sm">
|
||||
const example = true;
|
||||
</code>
|
||||
<p className="text-sm text-muted-foreground mt-1">Code / Mono</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Buttons */}
|
||||
<Section title="Buttons">
|
||||
<div className="space-y-6">
|
||||
{/* Variants */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-medium">Variants</h3>
|
||||
<ExampleSection
|
||||
id="buttons"
|
||||
title="Buttons"
|
||||
description="All button variants and sizes with states"
|
||||
>
|
||||
<ExampleGrid>
|
||||
<Example
|
||||
title="Button Variants"
|
||||
description="All available button styles"
|
||||
code={`<Button variant="default">Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="link">Link</Button>`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="default">Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
@@ -210,11 +182,18 @@ export function ComponentShowcase() {
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
{/* Sizes */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-medium">Sizes</h3>
|
||||
<Example
|
||||
title="Button Sizes"
|
||||
description="Small, default, large, and icon-only"
|
||||
code={`<Button size="sm">Small</Button>
|
||||
<Button size="default">Default</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button size="icon">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>`}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="default">Default</Button>
|
||||
@@ -223,11 +202,20 @@ export function ComponentShowcase() {
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
{/* With Icons */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-medium">With Icons</h3>
|
||||
<Example
|
||||
title="Buttons with Icons"
|
||||
description="Icons can be placed before or after text"
|
||||
code={`<Button>
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
Email
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Profile
|
||||
</Button>`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button>
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
@@ -242,32 +230,65 @@ export function ComponentShowcase() {
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
{/* States */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-medium">States</h3>
|
||||
<Example
|
||||
title="Button States"
|
||||
description="Normal and disabled states"
|
||||
code={`<Button>Normal</Button>
|
||||
<Button disabled>Disabled</Button>`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button>Normal</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</Example>
|
||||
</ExampleGrid>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Form Inputs */}
|
||||
<Section title="Form Inputs">
|
||||
<div className="max-w-md space-y-6">
|
||||
<ExampleSection
|
||||
id="form-inputs"
|
||||
title="Form Inputs"
|
||||
description="Text inputs, textareas, selects, and checkboxes"
|
||||
>
|
||||
<Example
|
||||
title="Form Components"
|
||||
description="Basic form field examples"
|
||||
code={`<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" placeholder="you@example.com" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea id="message" placeholder="Type here..." rows={4} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="country">Country</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="country">
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="us">United States</SelectItem>
|
||||
<SelectItem value="uk">United Kingdom</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="terms" />
|
||||
<Label htmlFor="terms">Accept terms</Label>
|
||||
</div>`}
|
||||
>
|
||||
<div className="max-w-md space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" placeholder="you@example.com" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" placeholder="••••••••" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea id="message" placeholder="Type your message here..." rows={4} />
|
||||
@@ -283,7 +304,6 @@ export function ComponentShowcase() {
|
||||
<SelectItem value="us">United States</SelectItem>
|
||||
<SelectItem value="uk">United Kingdom</SelectItem>
|
||||
<SelectItem value="ca">Canada</SelectItem>
|
||||
<SelectItem value="au">Australia</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -294,14 +314,29 @@ export function ComponentShowcase() {
|
||||
Accept terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Button className="w-full">Submit</Button>
|
||||
</div>
|
||||
</Section>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Cards */}
|
||||
<Section title="Cards">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<ExampleSection
|
||||
id="cards"
|
||||
title="Cards"
|
||||
description="Card component with header, content, and footer sections"
|
||||
>
|
||||
<ExampleGrid>
|
||||
<Example
|
||||
title="Simple Card"
|
||||
code={`<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>Card description</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Card content goes here.</p>
|
||||
</CardContent>
|
||||
</Card>`}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Simple Card</CardTitle>
|
||||
@@ -313,7 +348,23 @@ export function ComponentShowcase() {
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Example>
|
||||
|
||||
<Example
|
||||
title="Card with Footer"
|
||||
code={`<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card with Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Card content here.</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Save</Button>
|
||||
</CardFooter>
|
||||
</Card>`}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card with Footer</CardTitle>
|
||||
@@ -329,23 +380,50 @@ export function ComponentShowcase() {
|
||||
<Button>Save</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
</Example>
|
||||
</ExampleGrid>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Badges */}
|
||||
<Section title="Badges">
|
||||
<ExampleSection
|
||||
id="badges"
|
||||
title="Badges"
|
||||
description="Status indicators and labels"
|
||||
>
|
||||
<Example
|
||||
title="Badge Variants"
|
||||
code={`<Badge>Default</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>Default</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>
|
||||
<Badge className="bg-green-600 hover:bg-green-700">Success</Badge>
|
||||
<Badge className="bg-yellow-600 hover:bg-yellow-700">Warning</Badge>
|
||||
</div>
|
||||
</Section>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Avatars */}
|
||||
<Section title="Avatars">
|
||||
<ExampleSection
|
||||
id="avatars"
|
||||
title="Avatars"
|
||||
description="User avatars in different sizes"
|
||||
>
|
||||
<Example
|
||||
title="Avatar Sizes"
|
||||
code={`<Avatar>
|
||||
<AvatarFallback>AB</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarFallback>CD</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="h-16 w-16">
|
||||
<AvatarFallback>EF</AvatarFallback>
|
||||
</Avatar>`}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<Avatar>
|
||||
<AvatarFallback>AB</AvatarFallback>
|
||||
@@ -357,10 +435,34 @@ export function ComponentShowcase() {
|
||||
<AvatarFallback>EF</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</Section>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Alerts */}
|
||||
<Section title="Alerts">
|
||||
<ExampleSection
|
||||
id="alerts"
|
||||
title="Alerts"
|
||||
description="Contextual feedback messages"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<Example
|
||||
title="Alert Variants"
|
||||
code={`<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertTitle>Information</AlertTitle>
|
||||
<AlertDescription>
|
||||
This is an informational alert.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>
|
||||
Something went wrong.
|
||||
</AlertDescription>
|
||||
</Alert>`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
@@ -377,27 +479,36 @@ export function ComponentShowcase() {
|
||||
Something went wrong. Please try again.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert className="border-green-600 text-green-600 dark:border-green-400 dark:text-green-400">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
<AlertTitle>Success</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your changes have been saved successfully.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert className="border-yellow-600 text-yellow-600 dark:border-yellow-400 dark:text-yellow-400">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
<AlertDescription>
|
||||
Please review your changes before proceeding.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</Section>
|
||||
</Example>
|
||||
</div>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
<Section title="Dropdown Menu">
|
||||
<ExampleSection
|
||||
id="dropdown"
|
||||
title="Dropdown Menu"
|
||||
description="Contextual menus triggered by a button"
|
||||
>
|
||||
<Example
|
||||
title="Dropdown Example"
|
||||
code={`<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Open Menu</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-destructive">
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>`}
|
||||
centered
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Open Menu</Button>
|
||||
@@ -424,10 +535,36 @@ export function ComponentShowcase() {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Section>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Dialog */}
|
||||
<Section title="Dialog">
|
||||
<ExampleSection
|
||||
id="dialog"
|
||||
title="Dialog"
|
||||
description="Modal dialogs for user interactions"
|
||||
>
|
||||
<Example
|
||||
title="Dialog Example"
|
||||
code={`<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button variant="destructive">Delete</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>`}
|
||||
centered
|
||||
>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
@@ -446,27 +583,47 @@ export function ComponentShowcase() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Section>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Tabs */}
|
||||
<Section title="Tabs">
|
||||
<Tabs defaultValue="account" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 md:w-[400px]">
|
||||
<ExampleSection
|
||||
id="tabs"
|
||||
title="Tabs"
|
||||
description="Organize content into tabbed sections"
|
||||
>
|
||||
<Example
|
||||
title="Tabs Example"
|
||||
code={`<Tabs defaultValue="account">
|
||||
<TabsList>
|
||||
<TabsTrigger value="account">Account</TabsTrigger>
|
||||
<TabsTrigger value="password">Password</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="account" className="space-y-4">
|
||||
<TabsContent value="account">
|
||||
Account content
|
||||
</TabsContent>
|
||||
<TabsContent value="password">
|
||||
Password content
|
||||
</TabsContent>
|
||||
</Tabs>`}
|
||||
>
|
||||
<Tabs defaultValue="account" className="w-full">
|
||||
<TabsList className="grid w-full max-w-[400px] grid-cols-2">
|
||||
<TabsTrigger value="account">Account</TabsTrigger>
|
||||
<TabsTrigger value="password">Password</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="account" className="space-y-4 mt-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Make changes to your account here. Click save when you're done.
|
||||
Make changes to your account here.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" placeholder="John Doe" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="password" className="space-y-4">
|
||||
<TabsContent value="password" className="space-y-4 mt-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Change your password here. After saving, you'll be logged out.
|
||||
Change your password here.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="current">Current Password</Label>
|
||||
@@ -474,12 +631,36 @@ export function ComponentShowcase() {
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</Section>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Table */}
|
||||
<Section title="Table">
|
||||
<ExampleSection
|
||||
id="table"
|
||||
title="Table"
|
||||
description="Data tables with headers and cells"
|
||||
>
|
||||
<Example
|
||||
title="Table Example"
|
||||
code={`<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Invoice</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>INV001</TableCell>
|
||||
<TableCell><Badge>Paid</Badge></TableCell>
|
||||
<TableCell className="text-right">$250.00</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>`}
|
||||
>
|
||||
<Table>
|
||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
||||
<TableCaption>A list of recent invoices.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Invoice</TableHead>
|
||||
@@ -515,10 +696,28 @@ export function ComponentShowcase() {
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Skeletons */}
|
||||
<Section title="Skeleton Loading">
|
||||
<ExampleSection
|
||||
id="skeleton"
|
||||
title="Skeleton Loading"
|
||||
description="Loading placeholders for content"
|
||||
>
|
||||
<Example
|
||||
title="Skeleton Examples"
|
||||
code={`<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-12 w-3/4" />
|
||||
<Skeleton className="h-12 w-1/2" />
|
||||
<div className="flex space-x-4">
|
||||
<Skeleton className="h-12 w-12 rounded-full" />
|
||||
<div className="space-y-2 flex-1">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-4/5" />
|
||||
</div>
|
||||
</div>`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-12 w-3/4" />
|
||||
@@ -531,25 +730,56 @@ export function ComponentShowcase() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
|
||||
{/* Separator */}
|
||||
<Section title="Separator">
|
||||
<ExampleSection
|
||||
id="separator"
|
||||
title="Separator"
|
||||
description="Visual dividers between content sections"
|
||||
>
|
||||
<Example
|
||||
title="Separator Example"
|
||||
code={`<div>
|
||||
<p>Section 1</p>
|
||||
<Separator className="my-4" />
|
||||
<p>Section 2</p>
|
||||
</div>`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Section 1</p>
|
||||
<Separator className="my-2" />
|
||||
<Separator />
|
||||
<p className="text-sm text-muted-foreground">Section 2</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</Example>
|
||||
</ExampleSection>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-12 border-t py-6">
|
||||
<div className="container mx-auto px-4 text-center text-sm text-muted-foreground">
|
||||
<p>Design System v1.0 • Built with shadcn/ui + Tailwind CSS 4</p>
|
||||
<p>
|
||||
Design System v1.0 • Built with{' '}
|
||||
<a
|
||||
href="https://ui.shadcn.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium hover:text-foreground"
|
||||
>
|
||||
shadcn/ui
|
||||
</a>
|
||||
{' + '}
|
||||
<a
|
||||
href="https://tailwindcss.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium hover:text-foreground"
|
||||
>
|
||||
Tailwind CSS 4
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
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