forked from cardosofelipe/fast-next-template
Add Dev Hub for interactive design system demos and /dev/forms with validation examples
- **Design System Hub:** Introduce `/dev` as a central hub for interactive design system showcases (components, layouts, spacing, etc.). Includes live demos, highlights, and documentation links. - **Forms Demo:** Add `/dev/forms` for reactive forms with `react-hook-form` and `Zod`. Demonstrate validation patterns, error handling, loading states, and accessibility best practices. - **Features:** Showcase reusable `Example`, `ExampleSection`, and `BeforeAfter` components for better UI demonstration and code previews.
This commit is contained in:
591
frontend/src/app/dev/forms/page.tsx
Normal file
591
frontend/src/app/dev/forms/page.tsx
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
/**
|
||||||
|
* Form Patterns Demo
|
||||||
|
* Interactive demonstrations of form patterns with validation
|
||||||
|
* Access: /dev/forms
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { ArrowLeft, CheckCircle2, Loader2 } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
|
import { Example, ExampleSection } from '@/components/dev/Example';
|
||||||
|
import { BeforeAfter } from '@/components/dev/BeforeAfter';
|
||||||
|
|
||||||
|
// Example schemas
|
||||||
|
const loginSchema = z.object({
|
||||||
|
email: z.string().email('Invalid email address'),
|
||||||
|
password: z.string().min(8, 'Password must be at least 8 characters'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const contactSchema = z.object({
|
||||||
|
name: z.string().min(1, 'Name is required'),
|
||||||
|
email: z.string().email('Invalid email address'),
|
||||||
|
message: z.string().min(10, 'Message must be at least 10 characters'),
|
||||||
|
category: z.string().min(1, 'Please select a category'),
|
||||||
|
});
|
||||||
|
|
||||||
|
type LoginForm = z.infer<typeof loginSchema>;
|
||||||
|
type ContactForm = z.infer<typeof contactSchema>;
|
||||||
|
|
||||||
|
export default function FormsPage() {
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||||
|
|
||||||
|
// Login form
|
||||||
|
const {
|
||||||
|
register: registerLogin,
|
||||||
|
handleSubmit: handleSubmitLogin,
|
||||||
|
formState: { errors: errorsLogin },
|
||||||
|
} = useForm<LoginForm>({
|
||||||
|
resolver: zodResolver(loginSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Contact form
|
||||||
|
const {
|
||||||
|
register: registerContact,
|
||||||
|
handleSubmit: handleSubmitContact,
|
||||||
|
formState: { errors: errorsContact },
|
||||||
|
setValue: setValueContact,
|
||||||
|
} = useForm<ContactForm>({
|
||||||
|
resolver: zodResolver(contactSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmitLogin = async (data: LoginForm) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setSubmitSuccess(false);
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
console.log('Login form data:', data);
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setSubmitSuccess(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmitContact = async (data: ContactForm) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setSubmitSuccess(false);
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
console.log('Contact form data:', data);
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setSubmitSuccess(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* 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 gap-4 px-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">Form Patterns</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
react-hook-form + Zod validation examples
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<main className="container mx-auto px-4 py-8">
|
||||||
|
<div className="space-y-12">
|
||||||
|
{/* Introduction */}
|
||||||
|
<div className="max-w-3xl space-y-4">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Complete form implementations using react-hook-form for state management
|
||||||
|
and Zod for validation. Includes error handling, loading states, and
|
||||||
|
accessibility features.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="outline">react-hook-form</Badge>
|
||||||
|
<Badge variant="outline">Zod</Badge>
|
||||||
|
<Badge variant="outline">Validation</Badge>
|
||||||
|
<Badge variant="outline">ARIA</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Basic Form */}
|
||||||
|
<ExampleSection
|
||||||
|
id="basic-form"
|
||||||
|
title="Basic Form with Validation"
|
||||||
|
description="Login form with email and password validation"
|
||||||
|
>
|
||||||
|
<Example
|
||||||
|
title="Login Form"
|
||||||
|
description="Validates on submit, shows field-level errors"
|
||||||
|
code={`const schema = z.object({
|
||||||
|
email: z.string().email('Invalid email'),
|
||||||
|
password: z.string().min(8, 'Min 8 chars'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState: { errors } } = useForm({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
{...register('email')}
|
||||||
|
aria-invalid={!!errors.email}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="text-sm text-destructive">
|
||||||
|
{errors.email.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? <Loader2 className="animate-spin" /> : 'Submit'}
|
||||||
|
</Button>
|
||||||
|
</form>`}
|
||||||
|
>
|
||||||
|
<div className="max-w-md mx-auto">
|
||||||
|
<form onSubmit={handleSubmitLogin(onSubmitLogin)} className="space-y-4">
|
||||||
|
{/* Email */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="login-email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="login-email"
|
||||||
|
type="email"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
{...registerLogin('email')}
|
||||||
|
aria-invalid={!!errorsLogin.email}
|
||||||
|
aria-describedby={
|
||||||
|
errorsLogin.email ? 'login-email-error' : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{errorsLogin.email && (
|
||||||
|
<p
|
||||||
|
id="login-email-error"
|
||||||
|
className="text-sm text-destructive"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{errorsLogin.email.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="login-password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="login-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
{...registerLogin('password')}
|
||||||
|
aria-invalid={!!errorsLogin.password}
|
||||||
|
aria-describedby={
|
||||||
|
errorsLogin.password ? 'login-password-error' : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{errorsLogin.password && (
|
||||||
|
<p
|
||||||
|
id="login-password-error"
|
||||||
|
className="text-sm text-destructive"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{errorsLogin.password.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
|
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||||
|
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
{isSubmitting ? 'Signing In...' : 'Sign In'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Success Message */}
|
||||||
|
{submitSuccess && (
|
||||||
|
<Alert className="border-green-500 text-green-600 dark:border-green-400 dark:text-green-400">
|
||||||
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
|
<AlertTitle>Success!</AlertTitle>
|
||||||
|
<AlertDescription>Form submitted successfully.</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Example>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* Complete Form */}
|
||||||
|
<ExampleSection
|
||||||
|
id="complete-form"
|
||||||
|
title="Complete Form Example"
|
||||||
|
description="Contact form with multiple field types"
|
||||||
|
>
|
||||||
|
<Example
|
||||||
|
title="Contact Form"
|
||||||
|
description="Text, textarea, select, and validation"
|
||||||
|
code={`const schema = z.object({
|
||||||
|
name: z.string().min(1, 'Name is required'),
|
||||||
|
email: z.string().email('Invalid email'),
|
||||||
|
message: z.string().min(10, 'Min 10 characters'),
|
||||||
|
category: z.string().min(1, 'Select a category'),
|
||||||
|
});
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{/* Input field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Name</Label>
|
||||||
|
<Input {...register('name')} />
|
||||||
|
{errors.name && <p className="text-sm text-destructive">{errors.name.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Textarea field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="message">Message</Label>
|
||||||
|
<Textarea {...register('message')} rows={4} />
|
||||||
|
{errors.message && <p className="text-sm text-destructive">{errors.message.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Select field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="category">Category</Label>
|
||||||
|
<Select onValueChange={(value) => setValue('category', value)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="support">Support</SelectItem>
|
||||||
|
<SelectItem value="sales">Sales</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</form>`}
|
||||||
|
>
|
||||||
|
<div className="max-w-md mx-auto">
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmitContact(onSubmitContact)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
{/* Name */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="contact-name">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="contact-name"
|
||||||
|
placeholder="John Doe"
|
||||||
|
{...registerContact('name')}
|
||||||
|
aria-invalid={!!errorsContact.name}
|
||||||
|
/>
|
||||||
|
{errorsContact.name && (
|
||||||
|
<p className="text-sm text-destructive" role="alert">
|
||||||
|
{errorsContact.name.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="contact-email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="contact-email"
|
||||||
|
type="email"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
{...registerContact('email')}
|
||||||
|
aria-invalid={!!errorsContact.email}
|
||||||
|
/>
|
||||||
|
{errorsContact.email && (
|
||||||
|
<p className="text-sm text-destructive" role="alert">
|
||||||
|
{errorsContact.email.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="contact-category">Category</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => setValueContact('category', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="contact-category">
|
||||||
|
<SelectValue placeholder="Select a category" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="support">Support</SelectItem>
|
||||||
|
<SelectItem value="sales">Sales</SelectItem>
|
||||||
|
<SelectItem value="feedback">Feedback</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{errorsContact.category && (
|
||||||
|
<p className="text-sm text-destructive" role="alert">
|
||||||
|
{errorsContact.category.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="contact-message">Message</Label>
|
||||||
|
<Textarea
|
||||||
|
id="contact-message"
|
||||||
|
placeholder="Type your message here..."
|
||||||
|
rows={4}
|
||||||
|
{...registerContact('message')}
|
||||||
|
aria-invalid={!!errorsContact.message}
|
||||||
|
/>
|
||||||
|
{errorsContact.message && (
|
||||||
|
<p className="text-sm text-destructive" role="alert">
|
||||||
|
{errorsContact.message.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
|
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||||
|
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
{isSubmitting ? 'Sending...' : 'Send Message'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Success Message */}
|
||||||
|
{submitSuccess && (
|
||||||
|
<Alert className="border-green-500 text-green-600 dark:border-green-400 dark:text-green-400">
|
||||||
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
|
<AlertTitle>Success!</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Your message has been sent successfully.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Example>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* Error States */}
|
||||||
|
<ExampleSection
|
||||||
|
id="error-states"
|
||||||
|
title="Error State Handling"
|
||||||
|
description="Proper ARIA attributes and visual feedback"
|
||||||
|
>
|
||||||
|
<BeforeAfter
|
||||||
|
title="Error State Best Practices"
|
||||||
|
description="Use aria-invalid and aria-describedby for accessibility"
|
||||||
|
before={{
|
||||||
|
caption: "No ARIA attributes, poor accessibility",
|
||||||
|
content: (
|
||||||
|
<div className="space-y-2 rounded-lg border p-4">
|
||||||
|
<div className="text-sm font-medium">Email</div>
|
||||||
|
<div className="h-10 rounded-md border border-destructive bg-background"></div>
|
||||||
|
<p className="text-sm text-destructive">Invalid email address</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
after={{
|
||||||
|
caption: "With ARIA, screen reader accessible",
|
||||||
|
content: (
|
||||||
|
<div className="space-y-2 rounded-lg border p-4">
|
||||||
|
<div className="text-sm font-medium">Email</div>
|
||||||
|
<div
|
||||||
|
className="h-10 rounded-md border border-destructive bg-background"
|
||||||
|
role="textbox"
|
||||||
|
aria-invalid="true"
|
||||||
|
aria-describedby="email-error"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Email input with error</span>
|
||||||
|
</div>
|
||||||
|
<p id="email-error" className="text-sm text-destructive" role="alert">
|
||||||
|
Invalid email address
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card className="mt-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Error Handling Checklist</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5" />
|
||||||
|
<span>Add <code className="text-xs">aria-invalid=true</code> to invalid inputs</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5" />
|
||||||
|
<span>Link errors with <code className="text-xs">aria-describedby</code></span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5" />
|
||||||
|
<span>Add <code className="text-xs">role="alert"</code> to error messages</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5" />
|
||||||
|
<span>Visual indicator (red border, icon)</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-600 mt-0.5" />
|
||||||
|
<span>Clear error message text</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* Loading States */}
|
||||||
|
<ExampleSection
|
||||||
|
id="loading-states"
|
||||||
|
title="Loading States"
|
||||||
|
description="Proper feedback during async operations"
|
||||||
|
>
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Button Loading State</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<code className="text-xs block">
|
||||||
|
{`<Button disabled={isLoading}>
|
||||||
|
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
{isLoading ? 'Saving...' : 'Save'}
|
||||||
|
</Button>`}
|
||||||
|
</code>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button size="sm">Save</Button>
|
||||||
|
<Button size="sm" disabled>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Form Disabled State</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<code className="text-xs block mb-3">
|
||||||
|
{`<fieldset disabled={isLoading}>
|
||||||
|
<Input />
|
||||||
|
<Button type="submit" />
|
||||||
|
</fieldset>`}
|
||||||
|
</code>
|
||||||
|
<div className="space-y-2 opacity-60">
|
||||||
|
<Input placeholder="Disabled input" disabled />
|
||||||
|
<Button size="sm" disabled className="w-full">
|
||||||
|
Disabled Button
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* Zod Patterns */}
|
||||||
|
<ExampleSection
|
||||||
|
id="zod-patterns"
|
||||||
|
title="Common Zod Validation Patterns"
|
||||||
|
description="Reusable validation schemas"
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Validation Pattern Library</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="font-medium text-sm">Required String</div>
|
||||||
|
<code className="block rounded bg-muted p-2 text-xs">
|
||||||
|
z.string().min(1, "Required")
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="font-medium text-sm">Email</div>
|
||||||
|
<code className="block rounded bg-muted p-2 text-xs">
|
||||||
|
z.string().email("Invalid email")
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="font-medium text-sm">Password (min length)</div>
|
||||||
|
<code className="block rounded bg-muted p-2 text-xs">
|
||||||
|
z.string().min(8, "Min 8 characters")
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="font-medium text-sm">Number Range</div>
|
||||||
|
<code className="block rounded bg-muted p-2 text-xs">
|
||||||
|
z.coerce.number().min(0).max(100)
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="font-medium text-sm">Optional Field</div>
|
||||||
|
<code className="block rounded bg-muted p-2 text-xs">
|
||||||
|
z.string().optional()
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="font-medium text-sm">Password Confirmation</div>
|
||||||
|
<code className="block rounded bg-muted p-2 text-xs">
|
||||||
|
{`z.object({
|
||||||
|
password: z.string().min(8),
|
||||||
|
confirmPassword: z.string()
|
||||||
|
}).refine(data => data.password === data.confirmPassword, {
|
||||||
|
message: "Passwords don't match",
|
||||||
|
path: ["confirmPassword"]
|
||||||
|
})`}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</ExampleSection>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="mt-16 border-t py-6">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Learn more:{' '}
|
||||||
|
<a
|
||||||
|
href="/docs/design-system/06-forms.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium hover:text-foreground"
|
||||||
|
>
|
||||||
|
Forms Documentation
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
524
frontend/src/app/dev/layouts/page.tsx
Normal file
524
frontend/src/app/dev/layouts/page.tsx
Normal file
@@ -0,0 +1,524 @@
|
|||||||
|
/**
|
||||||
|
* Layout Patterns Demo
|
||||||
|
* Interactive demonstrations of essential layout patterns
|
||||||
|
* Access: /dev/layouts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { ArrowLeft, Grid3x3, LayoutDashboard } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Example, ExampleSection } from '@/components/dev/Example';
|
||||||
|
import { BeforeAfter } from '@/components/dev/BeforeAfter';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Layout Patterns | Dev',
|
||||||
|
description: 'Essential layout patterns with before/after examples',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LayoutsPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* 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 gap-4 px-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">Layout Patterns</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Essential patterns for pages, dashboards, and forms
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<main className="container mx-auto px-4 py-8">
|
||||||
|
<div className="space-y-12">
|
||||||
|
{/* Introduction */}
|
||||||
|
<div className="max-w-3xl space-y-4">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
These 5 essential layout patterns cover 80% of interface needs. Each
|
||||||
|
pattern includes live examples, before/after comparisons, and copy-paste
|
||||||
|
code.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="outline">Grid vs Flex</Badge>
|
||||||
|
<Badge variant="outline">Responsive</Badge>
|
||||||
|
<Badge variant="outline">Mobile-first</Badge>
|
||||||
|
<Badge variant="outline">Best practices</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 1. Page Container */}
|
||||||
|
<ExampleSection
|
||||||
|
id="page-container"
|
||||||
|
title="1. Page Container"
|
||||||
|
description="Standard page layout with constrained width"
|
||||||
|
>
|
||||||
|
<Example
|
||||||
|
title="Page Container Pattern"
|
||||||
|
description="Responsive container with padding and max-width"
|
||||||
|
code={`<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
|
<h1 className="text-3xl font-bold">Page Title</h1>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Content Card</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p>Your main content goes here.</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>`}
|
||||||
|
>
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-2">
|
||||||
|
<div className="container mx-auto px-4 py-8 bg-background rounded">
|
||||||
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
|
<h2 className="text-2xl font-bold">Page Title</h2>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Content Card</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Constrained to max-w-4xl for readability
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Your main content goes here. The max-w-4xl constraint
|
||||||
|
ensures comfortable reading width.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Example>
|
||||||
|
|
||||||
|
<BeforeAfter
|
||||||
|
title="Common Mistake: No Width Constraint"
|
||||||
|
description="Content should not span full viewport width"
|
||||||
|
before={{
|
||||||
|
caption: "No max-width, hard to read on wide screens",
|
||||||
|
content: (
|
||||||
|
<div className="w-full space-y-4 bg-background p-4 rounded">
|
||||||
|
<h3 className="font-semibold">Full Width Content</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
This text spans the entire width, making it hard to read on
|
||||||
|
large screens. Lines become too long.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
after={{
|
||||||
|
caption: "Constrained with max-w for better readability",
|
||||||
|
content: (
|
||||||
|
<div className="max-w-2xl mx-auto space-y-4 bg-background p-4 rounded">
|
||||||
|
<h3 className="font-semibold">Constrained Content</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
This text has a max-width, creating comfortable line lengths
|
||||||
|
for reading.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* 2. Dashboard Grid */}
|
||||||
|
<ExampleSection
|
||||||
|
id="dashboard-grid"
|
||||||
|
title="2. Dashboard Grid"
|
||||||
|
description="Responsive card grid for metrics and data"
|
||||||
|
>
|
||||||
|
<Example
|
||||||
|
title="Responsive Grid Pattern"
|
||||||
|
description="1 → 2 → 3 columns progression with grid"
|
||||||
|
code={`<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{items.map(item => (
|
||||||
|
<Card key={item.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{item.title}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>{item.content}</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>`}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
|
<Card key={i}>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Metric {i}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{(Math.random() * 1000).toFixed(0)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
+{(Math.random() * 20).toFixed(1)}% from last month
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Example>
|
||||||
|
|
||||||
|
<BeforeAfter
|
||||||
|
title="Grid vs Flex for Equal Columns"
|
||||||
|
description="Use Grid for equal-width columns, not Flex"
|
||||||
|
before={{
|
||||||
|
caption: "flex with flex-1 - uneven wrapping",
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
<div className="flex-1 min-w-[200px] rounded border bg-background p-4">
|
||||||
|
<div className="text-xs">flex-1</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-[200px] rounded border bg-background p-4">
|
||||||
|
<div className="text-xs">flex-1</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-[200px] rounded border bg-background p-4">
|
||||||
|
<div className="text-xs">flex-1 (odd one out)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
after={{
|
||||||
|
caption: "grid with grid-cols - consistent sizing",
|
||||||
|
content: (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div className="rounded border bg-background p-4">
|
||||||
|
<div className="text-xs">grid-cols</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded border bg-background p-4">
|
||||||
|
<div className="text-xs">grid-cols</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded border bg-background p-4">
|
||||||
|
<div className="text-xs">grid-cols (perfect)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* 3. Form Layout */}
|
||||||
|
<ExampleSection
|
||||||
|
id="form-layout"
|
||||||
|
title="3. Form Layout"
|
||||||
|
description="Centered form with appropriate max-width"
|
||||||
|
>
|
||||||
|
<Example
|
||||||
|
title="Centered Form Pattern"
|
||||||
|
description="Form constrained to max-w-md"
|
||||||
|
code={`<div className="container mx-auto px-4 py-8">
|
||||||
|
<Card className="max-w-md mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Login</CardTitle>
|
||||||
|
<CardDescription>Enter your credentials</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form className="space-y-4">
|
||||||
|
{/* Form fields */}
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>`}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<Card className="max-w-md mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Login</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your credentials to continue
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Email</div>
|
||||||
|
<div className="h-10 rounded-md border bg-background"></div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Password</div>
|
||||||
|
<div className="h-10 rounded-md border bg-background"></div>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full">Sign In</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Example>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* 4. Sidebar Layout */}
|
||||||
|
<ExampleSection
|
||||||
|
id="sidebar-layout"
|
||||||
|
title="4. Sidebar Layout"
|
||||||
|
description="Two-column layout with fixed sidebar"
|
||||||
|
>
|
||||||
|
<Example
|
||||||
|
title="Sidebar + Main Content"
|
||||||
|
description="Grid with fixed sidebar width"
|
||||||
|
code={`<div className="grid lg:grid-cols-[240px_1fr] gap-6">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Navigation</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>{/* Nav items */}</CardContent>
|
||||||
|
</Card>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="space-y-4">
|
||||||
|
<Card>{/* Content */}</Card>
|
||||||
|
</main>
|
||||||
|
</div>`}
|
||||||
|
>
|
||||||
|
<div className="grid lg:grid-cols-[240px_1fr] gap-6">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Navigation</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
{['Dashboard', 'Settings', 'Profile'].map((item) => (
|
||||||
|
<div
|
||||||
|
key={item}
|
||||||
|
className="rounded-md bg-muted px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Main Content</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Fixed 240px sidebar, fluid main area
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
The sidebar remains 240px wide while the main content area
|
||||||
|
flexes to fill remaining space.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</Example>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* 5. Centered Content */}
|
||||||
|
<ExampleSection
|
||||||
|
id="centered-content"
|
||||||
|
title="5. Centered Content"
|
||||||
|
description="Vertically and horizontally centered layouts"
|
||||||
|
>
|
||||||
|
<Example
|
||||||
|
title="Center with Flexbox"
|
||||||
|
description="Full-height centered content"
|
||||||
|
code={`<div className="flex min-h-screen items-center justify-center">
|
||||||
|
<Card className="max-w-md w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Centered Card</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p>Content perfectly centered on screen.</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>`}
|
||||||
|
>
|
||||||
|
<div className="flex min-h-[400px] items-center justify-center rounded-lg border bg-muted/30 p-4">
|
||||||
|
<Card className="max-w-sm w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Centered Card</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Centered vertically and horizontally
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Perfect for login screens, error pages, and loading states.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Example>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* Decision Tree */}
|
||||||
|
<ExampleSection
|
||||||
|
id="decision-tree"
|
||||||
|
title="Decision Tree: Grid vs Flex"
|
||||||
|
description="When to use each layout method"
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Grid3x3 className="h-5 w-5 text-primary" />
|
||||||
|
<CardTitle>Grid vs Flex Quick Guide</CardTitle>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="default">Use Grid</Badge>
|
||||||
|
<span className="text-sm font-medium">When you need...</span>
|
||||||
|
</div>
|
||||||
|
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||||
|
<li>Equal-width columns (dashboard cards)</li>
|
||||||
|
<li>2D layout (rows AND columns)</li>
|
||||||
|
<li>Consistent grid structure</li>
|
||||||
|
<li>Auto-fill/auto-fit responsive grids</li>
|
||||||
|
</ul>
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||||
|
grid grid-cols-3 gap-6
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="secondary">Use Flex</Badge>
|
||||||
|
<span className="text-sm font-medium">When you need...</span>
|
||||||
|
</div>
|
||||||
|
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||||
|
<li>Variable-width items (buttons, tags)</li>
|
||||||
|
<li>1D layout (row OR column)</li>
|
||||||
|
<li>Center alignment</li>
|
||||||
|
<li>Space-between/around distribution</li>
|
||||||
|
</ul>
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||||
|
flex gap-4 items-center
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* Responsive Patterns */}
|
||||||
|
<ExampleSection
|
||||||
|
id="responsive"
|
||||||
|
title="Responsive Patterns"
|
||||||
|
description="Mobile-first breakpoint strategies"
|
||||||
|
>
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">1 → 2 → 3 Progression</CardTitle>
|
||||||
|
<CardDescription>Most common pattern</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<code className="text-xs">
|
||||||
|
grid-cols-1 md:grid-cols-2 lg:grid-cols-3
|
||||||
|
</code>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Mobile: 1 column
|
||||||
|
<br />
|
||||||
|
Tablet: 2 columns
|
||||||
|
<br />
|
||||||
|
Desktop: 3 columns
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">1 → 2 → 4 Progression</CardTitle>
|
||||||
|
<CardDescription>For smaller cards</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<code className="text-xs">
|
||||||
|
grid-cols-1 md:grid-cols-2 lg:grid-cols-4
|
||||||
|
</code>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Mobile: 1 column
|
||||||
|
<br />
|
||||||
|
Tablet: 2 columns
|
||||||
|
<br />
|
||||||
|
Desktop: 4 columns
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Stack → Row</CardTitle>
|
||||||
|
<CardDescription>Form buttons, toolbars</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<code className="text-xs">flex flex-col sm:flex-row</code>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Mobile: Stacked vertically
|
||||||
|
<br />
|
||||||
|
Tablet+: Horizontal row
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Hide Sidebar</CardTitle>
|
||||||
|
<CardDescription>Mobile navigation</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<code className="text-xs">
|
||||||
|
hidden lg:block
|
||||||
|
</code>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Mobile: Hidden (use menu)
|
||||||
|
<br />
|
||||||
|
Desktop: Visible sidebar
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</ExampleSection>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="mt-16 border-t py-6">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Learn more:{' '}
|
||||||
|
<a
|
||||||
|
href="/docs/design-system/03-layouts.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium hover:text-foreground"
|
||||||
|
>
|
||||||
|
Layout Documentation
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
309
frontend/src/app/dev/page.tsx
Normal file
309
frontend/src/app/dev/page.tsx
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
/**
|
||||||
|
* Design System Hub
|
||||||
|
* Central landing page for all interactive design system demonstrations
|
||||||
|
* Access: /dev
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import {
|
||||||
|
Palette,
|
||||||
|
Layout,
|
||||||
|
Ruler,
|
||||||
|
FileText,
|
||||||
|
BookOpen,
|
||||||
|
ArrowRight,
|
||||||
|
Sparkles,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Design System Hub | Dev',
|
||||||
|
description: 'Interactive demonstrations and documentation for the FastNext design system',
|
||||||
|
};
|
||||||
|
|
||||||
|
const demoPages = [
|
||||||
|
{
|
||||||
|
title: 'Components',
|
||||||
|
description: 'Explore all shadcn/ui components with live examples and copy-paste code',
|
||||||
|
href: '/dev/components',
|
||||||
|
icon: Palette,
|
||||||
|
status: 'enhanced' as const,
|
||||||
|
highlights: ['All variants', 'Interactive demos', 'Copy-paste code'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Layouts',
|
||||||
|
description: 'Essential layout patterns for pages, dashboards, forms, and content',
|
||||||
|
href: '/dev/layouts',
|
||||||
|
icon: Layout,
|
||||||
|
status: 'new' as const,
|
||||||
|
highlights: ['Grid vs Flex', 'Responsive patterns', 'Before/after examples'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Spacing',
|
||||||
|
description: 'Visual demonstrations of spacing philosophy and best practices',
|
||||||
|
href: '/dev/spacing',
|
||||||
|
icon: Ruler,
|
||||||
|
status: 'new' as const,
|
||||||
|
highlights: ['Parent-controlled', 'Gap vs Space-y', 'Anti-patterns'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Forms',
|
||||||
|
description: 'Complete form implementations with validation and error handling',
|
||||||
|
href: '/dev/forms',
|
||||||
|
icon: FileText,
|
||||||
|
status: 'new' as const,
|
||||||
|
highlights: ['react-hook-form', 'Zod validation', 'Loading states'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const documentationLinks = [
|
||||||
|
{
|
||||||
|
title: 'Quick Start',
|
||||||
|
description: '5-minute crash course',
|
||||||
|
href: '/docs/design-system/00-quick-start.md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Complete Documentation',
|
||||||
|
description: 'Full design system guide',
|
||||||
|
href: '/docs/design-system/README.md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'AI Guidelines',
|
||||||
|
description: 'Rules for AI code generation',
|
||||||
|
href: '/docs/design-system/08-ai-guidelines.md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Quick Reference',
|
||||||
|
description: 'Cheat sheet for lookups',
|
||||||
|
href: '/docs/design-system/99-reference.md',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function DesignSystemHub() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Sparkles className="h-8 w-8 text-primary" />
|
||||||
|
<h1 className="text-4xl font-bold tracking-tight">
|
||||||
|
Design System Hub
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg text-muted-foreground max-w-2xl">
|
||||||
|
Interactive demonstrations, live examples, and copy-paste code for
|
||||||
|
the FastNext design system. Built with shadcn/ui + Tailwind CSS 4.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="container mx-auto px-4 py-12">
|
||||||
|
<div className="space-y-12">
|
||||||
|
{/* Demo Pages Grid */}
|
||||||
|
<section className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-semibold tracking-tight">
|
||||||
|
Interactive Demonstrations
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
Explore live examples with copy-paste code snippets
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
{demoPages.map((page) => {
|
||||||
|
const Icon = page.icon;
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={page.href}
|
||||||
|
className="group relative overflow-hidden transition-all hover:border-primary/50"
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="rounded-lg bg-primary/10 p-2">
|
||||||
|
<Icon className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
{page.status === 'new' && (
|
||||||
|
<Badge variant="default" className="gap-1">
|
||||||
|
<Sparkles className="h-3 w-3" />
|
||||||
|
New
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{page.status === 'enhanced' && (
|
||||||
|
<Badge variant="secondary">Enhanced</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<CardTitle className="mt-4">{page.title}</CardTitle>
|
||||||
|
<CardDescription>{page.description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Highlights */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{page.highlights.map((highlight) => (
|
||||||
|
<Badge key={highlight} variant="outline" className="text-xs">
|
||||||
|
{highlight}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<Link href={page.href} className="block">
|
||||||
|
<Button className="w-full gap-2 group-hover:bg-primary/90">
|
||||||
|
Explore {page.title}
|
||||||
|
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Documentation Links */}
|
||||||
|
<section className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
|
||||||
|
<BookOpen className="h-6 w-6" />
|
||||||
|
Documentation
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
Comprehensive guides and reference materials
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{documentationLinks.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.href}
|
||||||
|
href={link.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<Card className="h-full transition-all hover:border-primary/50 hover:bg-accent/50">
|
||||||
|
<CardHeader className="space-y-1">
|
||||||
|
<CardTitle className="text-base group-hover:text-primary transition-colors">
|
||||||
|
{link.title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs">
|
||||||
|
{link.description}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Key Features */}
|
||||||
|
<section className="space-y-6">
|
||||||
|
<h2 className="text-2xl font-semibold tracking-tight">
|
||||||
|
Key Features
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">🎨 OKLCH Color System</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-sm text-muted-foreground">
|
||||||
|
Perceptually uniform colors with semantic tokens for consistent theming
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">📏 Parent-Controlled Spacing</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-sm text-muted-foreground">
|
||||||
|
Consistent spacing philosophy using gap and space-y utilities
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">♿ WCAG AA Compliant</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-sm text-muted-foreground">
|
||||||
|
Full accessibility support with keyboard navigation and screen readers
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">📱 Mobile-First Responsive</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-sm text-muted-foreground">
|
||||||
|
Tailwind breakpoints with progressive enhancement
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">🤖 AI-Optimized</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-sm text-muted-foreground">
|
||||||
|
Dedicated guidelines for Claude Code, Cursor, and GitHub Copilot
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">🚀 Production-Ready</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-sm text-muted-foreground">
|
||||||
|
Battle-tested patterns with real-world examples
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="mt-16 border-t py-8">
|
||||||
|
<div className="container mx-auto px-4 text-center text-sm text-muted-foreground">
|
||||||
|
<p>
|
||||||
|
FastNext Design System • 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
522
frontend/src/app/dev/spacing/page.tsx
Normal file
522
frontend/src/app/dev/spacing/page.tsx
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
/**
|
||||||
|
* Spacing Patterns Demo
|
||||||
|
* Interactive demonstrations of spacing philosophy and best practices
|
||||||
|
* Access: /dev/spacing
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { ArrowLeft, Ruler } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Example, ExampleSection } from '@/components/dev/Example';
|
||||||
|
import { BeforeAfter } from '@/components/dev/BeforeAfter';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Spacing Patterns | Dev',
|
||||||
|
description: 'Parent-controlled spacing philosophy and visual demonstrations',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SpacingPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* 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 gap-4 px-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">Spacing Patterns</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Parent-controlled spacing philosophy
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<main className="container mx-auto px-4 py-8">
|
||||||
|
<div className="space-y-12">
|
||||||
|
{/* Introduction */}
|
||||||
|
<div className="max-w-3xl space-y-4">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
The Golden Rule: <strong>Parents control spacing, not children.</strong>{' '}
|
||||||
|
Use gap, space-y, and space-x utilities on the parent container. Avoid
|
||||||
|
margins on children except for exceptions.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="outline">gap</Badge>
|
||||||
|
<Badge variant="outline">space-y</Badge>
|
||||||
|
<Badge variant="outline">space-x</Badge>
|
||||||
|
<Badge variant="destructive">avoid margin</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spacing Scale */}
|
||||||
|
<ExampleSection
|
||||||
|
id="spacing-scale"
|
||||||
|
title="Spacing Scale"
|
||||||
|
description="Multiples of 4px (Tailwind's base unit)"
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Common Spacing Values</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Use consistent spacing values from the scale
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[
|
||||||
|
{ class: '2', px: '8px', rem: '0.5rem', use: 'Tight (label → input)' },
|
||||||
|
{ class: '4', px: '16px', rem: '1rem', use: 'Standard (form fields)' },
|
||||||
|
{ class: '6', px: '24px', rem: '1.5rem', use: 'Section spacing' },
|
||||||
|
{ class: '8', px: '32px', rem: '2rem', use: 'Large gaps' },
|
||||||
|
{ class: '12', px: '48px', rem: '3rem', use: 'Section dividers' },
|
||||||
|
].map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.class}
|
||||||
|
className="grid grid-cols-[80px_80px_100px_1fr] items-center gap-4"
|
||||||
|
>
|
||||||
|
<code className="text-sm font-mono">gap-{item.class}</code>
|
||||||
|
<span className="text-sm text-muted-foreground">{item.px}</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{item.rem}</span>
|
||||||
|
<span className="text-sm">{item.use}</span>
|
||||||
|
<div className="col-span-4">
|
||||||
|
<div
|
||||||
|
className="h-2 rounded bg-primary"
|
||||||
|
style={{ width: item.px }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* Gap for Flex/Grid */}
|
||||||
|
<ExampleSection
|
||||||
|
id="gap"
|
||||||
|
title="Gap: For Flex and Grid"
|
||||||
|
description="Preferred method for spacing flex and grid children"
|
||||||
|
>
|
||||||
|
<Example
|
||||||
|
title="Gap with Flex"
|
||||||
|
description="Horizontal and vertical spacing"
|
||||||
|
code={`{/* Horizontal */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button>Cancel</Button>
|
||||||
|
<Button>Save</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vertical */}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div>Item 1</div>
|
||||||
|
<div>Item 2</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div className="grid grid-cols-3 gap-6">
|
||||||
|
{/* Cards */}
|
||||||
|
</div>`}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Horizontal */}
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium mb-2">Horizontal (gap-4)</p>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
<Button>Save</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vertical */}
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium mb-2">Vertical (gap-4)</p>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="rounded-lg border bg-muted p-3 text-sm">Item 1</div>
|
||||||
|
<div className="rounded-lg border bg-muted p-3 text-sm">Item 2</div>
|
||||||
|
<div className="rounded-lg border bg-muted p-3 text-sm">Item 3</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium mb-2">Grid (gap-6)</p>
|
||||||
|
<div className="grid grid-cols-3 gap-6">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="rounded-lg border bg-muted p-3 text-center text-sm"
|
||||||
|
>
|
||||||
|
Card {i}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Example>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* Space-y for Stacks */}
|
||||||
|
<ExampleSection
|
||||||
|
id="space-y"
|
||||||
|
title="Space-y: For Vertical Stacks"
|
||||||
|
description="Simple vertical spacing without flex/grid"
|
||||||
|
>
|
||||||
|
<Example
|
||||||
|
title="Space-y Pattern"
|
||||||
|
description="Adds margin-top to all children except first"
|
||||||
|
code={`<div className="space-y-4">
|
||||||
|
<div>First item (no margin)</div>
|
||||||
|
<div>Second item (mt-4)</div>
|
||||||
|
<div>Third item (mt-4)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form example */}
|
||||||
|
<form className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Email</Label>
|
||||||
|
<Input />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Password</Label>
|
||||||
|
<Input />
|
||||||
|
</div>
|
||||||
|
<Button>Submit</Button>
|
||||||
|
</form>`}
|
||||||
|
>
|
||||||
|
<div className="max-w-md space-y-6">
|
||||||
|
{/* Visual demo */}
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium mb-4">Visual Demo (space-y-4)</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-lg border bg-muted p-3 text-sm">
|
||||||
|
First item (no margin)
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border bg-muted p-3 text-sm">
|
||||||
|
Second item (mt-4)
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border bg-muted p-3 text-sm">
|
||||||
|
Third item (mt-4)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form example */}
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium mb-4">Form Example (space-y-4)</p>
|
||||||
|
<div className="space-y-4 rounded-lg border p-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Email</div>
|
||||||
|
<div className="h-10 rounded-md border bg-background"></div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Password</div>
|
||||||
|
<div className="h-10 rounded-md border bg-background"></div>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full">Submit</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Example>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* Anti-pattern: Child Margins */}
|
||||||
|
<ExampleSection
|
||||||
|
id="anti-patterns"
|
||||||
|
title="Anti-patterns to Avoid"
|
||||||
|
description="Common spacing mistakes"
|
||||||
|
>
|
||||||
|
<BeforeAfter
|
||||||
|
title="Don't Let Children Control Spacing"
|
||||||
|
description="Parent should control spacing, not children"
|
||||||
|
before={{
|
||||||
|
caption: "Children control their own spacing with mt-4",
|
||||||
|
content: (
|
||||||
|
<div className="space-y-2 rounded-lg border p-4">
|
||||||
|
<div className="rounded bg-muted p-2 text-xs">
|
||||||
|
<div>Child 1</div>
|
||||||
|
<code className="text-[10px] text-destructive">no margin</code>
|
||||||
|
</div>
|
||||||
|
<div className="rounded bg-muted p-2 text-xs">
|
||||||
|
<div>Child 2</div>
|
||||||
|
<code className="text-[10px] text-destructive">mt-4</code>
|
||||||
|
</div>
|
||||||
|
<div className="rounded bg-muted p-2 text-xs">
|
||||||
|
<div>Child 3</div>
|
||||||
|
<code className="text-[10px] text-destructive">mt-4</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
after={{
|
||||||
|
caption: "Parent controls spacing with space-y-4",
|
||||||
|
content: (
|
||||||
|
<div className="space-y-4 rounded-lg border p-4">
|
||||||
|
<div className="rounded bg-muted p-2 text-xs">
|
||||||
|
<div>Child 1</div>
|
||||||
|
<code className="text-[10px] text-green-600">
|
||||||
|
parent uses space-y-4
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<div className="rounded bg-muted p-2 text-xs">
|
||||||
|
<div>Child 2</div>
|
||||||
|
<code className="text-[10px] text-green-600">clean, no margin</code>
|
||||||
|
</div>
|
||||||
|
<div className="rounded bg-muted p-2 text-xs">
|
||||||
|
<div>Child 3</div>
|
||||||
|
<code className="text-[10px] text-green-600">clean, no margin</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BeforeAfter
|
||||||
|
title="Use Gap, Not Margin for Buttons"
|
||||||
|
description="Button groups should use gap, not margins"
|
||||||
|
before={{
|
||||||
|
caption: "Margin on children - harder to maintain",
|
||||||
|
content: (
|
||||||
|
<div className="flex rounded-lg border p-4">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" className="ml-4">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
after={{
|
||||||
|
caption: "Gap on parent - clean and flexible",
|
||||||
|
content: (
|
||||||
|
<div className="flex gap-4 rounded-lg border p-4">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="sm">Save</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* Decision Tree */}
|
||||||
|
<ExampleSection
|
||||||
|
id="decision-tree"
|
||||||
|
title="Decision Tree: Which Spacing Method?"
|
||||||
|
description="Choose the right spacing utility"
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Ruler className="h-5 w-5 text-primary" />
|
||||||
|
<CardTitle>Spacing Decision Tree</CardTitle>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Gap */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="default">Use gap</Badge>
|
||||||
|
<span className="text-sm font-medium">When...</span>
|
||||||
|
</div>
|
||||||
|
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||||
|
<li>Parent is flex or grid</li>
|
||||||
|
<li>All children need equal spacing</li>
|
||||||
|
<li>Responsive spacing (gap-4 md:gap-6)</li>
|
||||||
|
</ul>
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||||
|
flex gap-4
|
||||||
|
<br />
|
||||||
|
grid grid-cols-3 gap-6
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Space-y */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="secondary">Use space-y</Badge>
|
||||||
|
<span className="text-sm font-medium">When...</span>
|
||||||
|
</div>
|
||||||
|
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||||
|
<li>Vertical stack without flex/grid</li>
|
||||||
|
<li>Form fields</li>
|
||||||
|
<li>Content sections</li>
|
||||||
|
</ul>
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||||
|
space-y-4
|
||||||
|
<br />
|
||||||
|
space-y-6
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Margin */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="destructive">Use margin</Badge>
|
||||||
|
<span className="text-sm font-medium">Only when...</span>
|
||||||
|
</div>
|
||||||
|
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||||
|
<li>Exception case (one child needs different spacing)</li>
|
||||||
|
<li>Negative margin for overlap effects</li>
|
||||||
|
<li>Can't modify parent (external component)</li>
|
||||||
|
</ul>
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||||
|
mt-8 {/* exception */}
|
||||||
|
<br />
|
||||||
|
-mt-4 {/* overlap */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Padding */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline">Use padding</Badge>
|
||||||
|
<span className="text-sm font-medium">When...</span>
|
||||||
|
</div>
|
||||||
|
<ul className="ml-6 space-y-1 text-sm text-muted-foreground list-disc">
|
||||||
|
<li>Internal spacing within a component</li>
|
||||||
|
<li>Card/container padding</li>
|
||||||
|
<li>Button padding</li>
|
||||||
|
</ul>
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-3 font-mono text-xs">
|
||||||
|
p-4 {/* all sides */}
|
||||||
|
<br />
|
||||||
|
px-4 py-2 {/* horizontal & vertical */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</ExampleSection>
|
||||||
|
|
||||||
|
{/* Common Patterns */}
|
||||||
|
<ExampleSection
|
||||||
|
id="common-patterns"
|
||||||
|
title="Common Patterns"
|
||||||
|
description="Real-world spacing examples"
|
||||||
|
>
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Form Fields</CardTitle>
|
||||||
|
<CardDescription>Parent: space-y-4, Field: space-y-2</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<code className="text-xs block mb-3">
|
||||||
|
{`<form className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Email</Label>
|
||||||
|
<Input />
|
||||||
|
</div>
|
||||||
|
</form>`}
|
||||||
|
</code>
|
||||||
|
<div className="space-y-4 rounded-lg border bg-muted/30 p-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-medium">Email</div>
|
||||||
|
<div className="h-8 rounded border bg-background"></div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-medium">Password</div>
|
||||||
|
<div className="h-8 rounded border bg-background"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Button Group</CardTitle>
|
||||||
|
<CardDescription>Use flex gap-4</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<code className="text-xs block mb-3">
|
||||||
|
{`<div className="flex gap-4">
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
<Button>Save</Button>
|
||||||
|
</div>`}
|
||||||
|
</code>
|
||||||
|
<div className="flex gap-4 rounded-lg border bg-muted/30 p-4">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="sm">Save</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Card Grid</CardTitle>
|
||||||
|
<CardDescription>Use grid with gap-6</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<code className="text-xs block mb-3">
|
||||||
|
{`<div className="grid grid-cols-2 gap-6">
|
||||||
|
<Card>...</Card>
|
||||||
|
</div>`}
|
||||||
|
</code>
|
||||||
|
<div className="grid grid-cols-2 gap-4 rounded-lg border bg-muted/30 p-4">
|
||||||
|
<div className="h-16 rounded border bg-background"></div>
|
||||||
|
<div className="h-16 rounded border bg-background"></div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Content Stack</CardTitle>
|
||||||
|
<CardDescription>Use space-y-6 for sections</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<code className="text-xs block mb-3">
|
||||||
|
{`<div className="space-y-6">
|
||||||
|
<section>...</section>
|
||||||
|
<section>...</section>
|
||||||
|
</div>`}
|
||||||
|
</code>
|
||||||
|
<div className="space-y-4 rounded-lg border bg-muted/30 p-4">
|
||||||
|
<div className="h-12 rounded border bg-background"></div>
|
||||||
|
<div className="h-12 rounded border bg-background"></div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</ExampleSection>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="mt-16 border-t py-6">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Learn more:{' '}
|
||||||
|
<a
|
||||||
|
href="/docs/design-system/04-spacing-philosophy.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium hover:text-foreground"
|
||||||
|
>
|
||||||
|
Spacing Philosophy Documentation
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user