Introduce AppShell, DashboardPage, ListPage, and FormPage template components with Storybook recipe stories for AI agent consumption. Thoroughly update DESIGN.md with all missing components, corrected token values, and page layout conventions. Fix SideNav button items defaulting to centered text. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
export interface FormPageStep {
|
|
label: string
|
|
status: 'complete' | 'current' | 'upcoming'
|
|
}
|
|
|
|
export interface FormPageProps extends HTMLAttributes<HTMLDivElement> {
|
|
/** PageHeader or custom header section */
|
|
header?: ReactNode
|
|
/** Action bar above the form content (e.g. dropdowns, buttons) */
|
|
actions?: ReactNode
|
|
/** Vertical stepper steps — renders a progress indicator alongside the form */
|
|
steps?: FormPageStep[]
|
|
/** Form content area */
|
|
children: ReactNode
|
|
}
|
|
|
|
const StepIndicator = ({ step, index }: { step: FormPageStep; index: number }) => {
|
|
const base = 'flex size-8 shrink-0 items-center justify-center rounded-full text-small font-bold'
|
|
const styles = {
|
|
complete: 'bg-success text-white',
|
|
current: 'bg-info text-white',
|
|
upcoming: 'bg-grey-04 text-text-secondary',
|
|
}
|
|
|
|
return (
|
|
<div className="flex items-start gap-3">
|
|
<div className={cn(base, styles[step.status])}>
|
|
{step.status === 'complete' ? (
|
|
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round">
|
|
<polyline points="20 6 9 17 4 12" />
|
|
</svg>
|
|
) : (
|
|
index + 1
|
|
)}
|
|
</div>
|
|
<span className={cn(
|
|
'pt-1 text-small',
|
|
step.status === 'current' ? 'font-bold text-text' : 'text-text-secondary',
|
|
)}>
|
|
{step.label}
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const FormPage = forwardRef<HTMLDivElement, FormPageProps>(
|
|
({ header, actions, steps, className, children, ...props }, ref) => {
|
|
return (
|
|
<div ref={ref} className={cn('flex flex-col', className)} {...props}>
|
|
{header}
|
|
|
|
{actions && (
|
|
<div className="flex items-center justify-between gap-4 border-b border-border px-6 py-3">
|
|
{actions}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex-1 p-6">
|
|
<div className="mx-auto max-w-3xl">
|
|
{steps ? (
|
|
<div className="flex gap-8">
|
|
<nav className="flex w-48 shrink-0 flex-col gap-4" aria-label="Form steps">
|
|
{steps.map((step, i) => (
|
|
<StepIndicator key={step.label} step={step} index={i} />
|
|
))}
|
|
</nav>
|
|
<div className="min-w-0 flex-1">{children}</div>
|
|
</div>
|
|
) : (
|
|
children
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
},
|
|
)
|
|
FormPage.displayName = 'FormPage'
|