A token-conscious approach using type-safe classname utilities instead of component wrappers for layout primitives.
Components for behavior. Classnames for styling.
| Use Components | Use Classname Helpers |
|---|---|
| Dialog, Popover, Form | Flex, Grid, Stack, Box |
| Tooltip, Select, Tabs | Spacing, Typography |
| Anything with state/a11y logic | Pure CSS composition |
This keeps token count flat while maximizing composability.
┌─────────────────────────────────────────────────────┐
│ Application Components │
│ (features, pages, domain-specific) │
├─────────────────────────────────────────────────────┤
│ Behavioral Components (Radix wrappers) │
│ Form, Dialog, WithTooltip, ListItem │
├─────────────────────────────────────────────────────┤
│ Classname Helpers (type-safe utilities) │
│ flex(), stack(), inline(), box(), grid() │
├─────────────────────────────────────────────────────┤
│ Foundation │
│ cn(), spacing scale, color tokens │
└─────────────────────────────────────────────────────┘
// dashboard/src/ui/helpers/types.ts
export const spacingScale = ['0', '0.5', '1', '1.5', '2', '3', '4', '6', '8', '10', '12'] as const
export type Spacing = typeof spacingScale[number]
export type Alignment = 'start' | 'center' | 'end' | 'stretch' | 'baseline'
export type Justify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'// dashboard/src/ui/helpers/flex.ts
import { cn } from '@/lib/utils'
import type { Spacing, Alignment, Justify } from './types'
export interface FlexOptions {
direction?: 'row' | 'column' | 'row-reverse' | 'column-reverse'
gap?: Spacing
align?: Alignment
justify?: Justify
wrap?: boolean | 'reverse'
inline?: boolean
}
export const flex = (opts: FlexOptions = {}): string => {
const {
direction,
gap,
align,
justify,
wrap,
inline = false,
} = opts
return cn(
inline ? 'inline-flex' : 'flex',
direction === 'column' && 'flex-col',
direction === 'row-reverse' && 'flex-row-reverse',
direction === 'column-reverse' && 'flex-col-reverse',
// Default: row direction gets items-center (our convention)
!direction && !align && 'items-center',
gap !== undefined && `gap-${gap}`,
align && `items-${align}`,
justify && `justify-${justify}`,
wrap === true && 'flex-wrap',
wrap === 'reverse' && 'flex-wrap-reverse',
)
}Usage:
<div className={flex({ gap: '2' })}>
<Icon />
<span>Label</span>
</div>
<nav className={cn(flex({ direction: 'column', gap: '1' }), 'p-4')}>
<NavItem />
<NavItem />
</nav>
// Semantic elements, no wrapper needed
<header className={flex({ justify: 'between' })}>
<Logo />
<UserMenu />
</header>Vertical flex with sensible defaults. The most common layout pattern.
// dashboard/src/ui/helpers/stack.ts
import { cn } from '@/lib/utils'
import type { Spacing, Alignment } from './types'
export interface StackOptions {
gap?: Spacing
align?: Alignment // cross-axis (horizontal)
}
export const stack = (opts: StackOptions = {}): string => {
const { gap = '4', align } = opts
return cn(
'flex flex-col',
`gap-${gap}`,
align && `items-${align}`,
)
}Usage:
<div className={stack()}>
<Heading />
<Paragraph />
<Button />
</div>
<form className={stack({ gap: '6' })}>
<FieldGroup />
<FieldGroup />
<SubmitButton />
</form>Inline-flex for icon + text patterns.
// dashboard/src/ui/helpers/inline.ts
import { cn } from '@/lib/utils'
import type { Spacing } from './types'
export interface InlineOptions {
gap?: Spacing
align?: 'start' | 'center' | 'end' | 'baseline'
}
export const inline = (opts: InlineOptions = {}): string => {
const { gap = '1.5', align = 'center' } = opts
return cn(
'inline-flex',
`gap-${gap}`,
`items-${align}`,
)
}Usage:
<span className={inline()}>
<IconCheck className="size-4" />
Completed
</span>
<button className={cn(inline({ gap: '2' }), 'px-3 py-1.5')}>
<IconPlus className="size-4" />
Add Item
</button>Padding, sizing, and common box patterns.
// dashboard/src/ui/helpers/box.ts
import { cn } from '@/lib/utils'
import type { Spacing } from './types'
export interface BoxOptions {
p?: Spacing // padding all
px?: Spacing // padding x
py?: Spacing // padding y
m?: Spacing // margin all
mx?: Spacing // margin x
my?: Spacing // margin y
fill?: boolean // w-full h-full
center?: boolean // flex items-center justify-center
}
export const box = (opts: BoxOptions = {}): string => {
const { p, px, py, m, mx, my, fill, center } = opts
return cn(
p !== undefined && `p-${p}`,
px !== undefined && `px-${px}`,
py !== undefined && `py-${py}`,
m !== undefined && `m-${m}`,
mx !== undefined && `mx-${mx}`,
my !== undefined && `my-${my}`,
fill && 'w-full h-full',
center && 'flex items-center justify-center',
)
}Usage:
<div className={box({ p: '4', fill: true })}>
<Content />
</div>
<div className={box({ center: true, fill: true })}>
<Spinner />
</div>
// Combine with other helpers
<section className={cn(box({ px: '6', py: '8' }), stack({ gap: '4' }))}>
<Heading />
<Content />
</section>CSS Grid layouts.
// dashboard/src/ui/helpers/grid.ts
import { cn } from '@/lib/utils'
import type { Spacing } from './types'
export interface GridOptions {
cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12 | 'none'
rows?: 1 | 2 | 3 | 4 | 5 | 6 | 'none'
gap?: Spacing
gapX?: Spacing
gapY?: Spacing
flow?: 'row' | 'col' | 'dense' | 'row-dense' | 'col-dense'
}
export const grid = (opts: GridOptions = {}): string => {
const { cols, rows, gap, gapX, gapY, flow } = opts
return cn(
'grid',
cols && `grid-cols-${cols}`,
rows && `grid-rows-${rows}`,
gap !== undefined && `gap-${gap}`,
gapX !== undefined && `gap-x-${gapX}`,
gapY !== undefined && `gap-y-${gapY}`,
flow && `grid-flow-${flow}`,
)
}Usage:
<div className={grid({ cols: 3, gap: '4' })}>
<Card />
<Card />
<Card />
</div>
<div className={grid({ cols: 12, gap: '6' })}>
<aside className="col-span-3">Sidebar</aside>
<main className="col-span-9">Content</main>
</div>// dashboard/src/ui/helpers/index.ts
export { flex, type FlexOptions } from './flex'
export { stack, type StackOptions } from './stack'
export { inline, type InlineOptions } from './inline'
export { box, type BoxOptions } from './box'
export { grid, type GridOptions } from './grid'
export { spacingScale, type Spacing, type Alignment, type Justify } from './types'These have real behavior that justifies component abstraction.
Convenience wrapper for adding tooltips to any element.
// dashboard/src/ui/tooltip.tsx (add to existing)
interface WithTooltipProps {
content: ReactNode
side?: 'top' | 'right' | 'bottom' | 'left'
sideOffset?: number
disabled?: boolean
children: ReactElement
}
function WithTooltip({
content,
side = 'top',
sideOffset = 4,
disabled,
children
}: WithTooltipProps) {
if (disabled) return children
return (
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent side={side} sideOffset={sideOffset}>
{content}
</TooltipContent>
</Tooltip>
)
}Usage:
<WithTooltip content="Delete item" side="bottom">
<Button variant="ghost"><IconTrash /></Button>
</WithTooltip>
// Disabled state skips tooltip entirely
<WithTooltip content="Not available" disabled={!hasPermission}>
<Button>Action</Button>
</WithTooltip>Solves: 387 tooltip usages with boilerplate
Semantic clickable primitive. Auto-detects button vs link based on props.
// dashboard/src/ui/action/action.tsx
import { Slot } from '@radix-ui/react-slot'
import { Link } from 'react-router-dom'
interface ActionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
href?: string
external?: boolean
variant?: 'default' | 'subtle' | 'ghost'
asChild?: boolean
}
function Action({
children,
onClick,
href,
external,
variant = 'ghost',
disabled,
className,
asChild,
...props
}: ActionProps) {
const classes = cn(
'cursor-pointer inline-flex items-center',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'disabled:cursor-not-allowed disabled:opacity-50',
variant === 'default' && 'hover:bg-darken-200 active:bg-darken-300 rounded-md',
variant === 'subtle' && 'hover:bg-darken-100 active:bg-darken-200 rounded-md',
className
)
if (asChild) {
return <Slot className={classes} {...props}>{children}</Slot>
}
if (href) {
const isExternal = external || href.startsWith('http')
if (isExternal) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={classes}
{...props}
>
{children}
</a>
)
}
return <Link to={href} className={classes} {...props}>{children}</Link>
}
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
className={classes}
{...props}
>
{children}
</button>
)
}Usage:
// Button behavior
<Action onClick={handleClick}>Click me</Action>
// Internal link
<Action href="/settings">Go to settings</Action>
// External link (auto-detected or explicit)
<Action href="https://example.com">External link</Action>
<Action href="/api/download" external>Download</Action>
// With asChild for custom elements
<Action asChild variant="subtle">
<div>Custom clickable</div>
</Action>Solves: 136 cursor-pointer divs
Wraps @radix-ui/react-form with our styling conventions.
// dashboard/src/ui/form.tsx
import * as FormPrimitive from '@radix-ui/react-form'
const Form = FormPrimitive.Root
const FormField = React.forwardRef<
React.ElementRef<typeof FormPrimitive.Field>,
React.ComponentPropsWithoutRef<typeof FormPrimitive.Field>
>(({ className, ...props }, ref) => (
<FormPrimitive.Field
ref={ref}
className={cn('flex flex-col gap-1.5', className)}
{...props}
/>
))
FormField.displayName = 'FormField'
const FormLabel = React.forwardRef<
React.ElementRef<typeof FormPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof FormPrimitive.Label> & { required?: boolean }
>(({ className, required, children, ...props }, ref) => (
<FormPrimitive.Label
ref={ref}
className={cn('text-sm font-medium', className)}
{...props}
>
{children}
{required && <span className="text-destructive ml-0.5">*</span>}
</FormPrimitive.Label>
))
FormLabel.displayName = 'FormLabel'
const FormControl = React.forwardRef<
React.ElementRef<typeof FormPrimitive.Control>,
React.ComponentPropsWithoutRef<typeof FormPrimitive.Control>
>(({ className, ...props }, ref) => (
<FormPrimitive.Control
ref={ref}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1',
'text-sm shadow-sm transition-colors',
'placeholder:text-muted-foreground',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
'disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
/>
))
FormControl.displayName = 'FormControl'
const FormMessage = React.forwardRef<
React.ElementRef<typeof FormPrimitive.Message>,
React.ComponentPropsWithoutRef<typeof FormPrimitive.Message>
>(({ className, ...props }, ref) => (
<FormPrimitive.Message
ref={ref}
className={cn('text-xs text-destructive', className)}
{...props}
/>
))
FormMessage.displayName = 'FormMessage'
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-xs text-text-tertiary', className)} {...props} />
))
FormDescription.displayName = 'FormDescription'
const FormSubmit = FormPrimitive.Submit
const FormValidityState = FormPrimitive.ValidityState
export {
Form,
FormField,
FormLabel,
FormControl,
FormMessage,
FormDescription,
FormSubmit,
FormValidityState
}Usage:
<Form onSubmit={handleSubmit}>
<FormField name="email">
<FormLabel required>Email</FormLabel>
<FormControl type="email" required placeholder="[email protected]" />
<FormMessage match="valueMissing">Email is required</FormMessage>
<FormMessage match="typeMismatch">Please enter a valid email</FormMessage>
</FormField>
<FormField name="password">
<FormLabel required>Password</FormLabel>
<FormControl type="password" required minLength={8} />
<FormMessage match="valueMissing">Password is required</FormMessage>
<FormMessage match="tooShort">Password must be at least 8 characters</FormMessage>
<FormDescription>Must be at least 8 characters</FormDescription>
</FormField>
<FormSubmit asChild>
<Button>Sign Up</Button>
</FormSubmit>
</Form>What Radix Form handles:
- Label-input linking (accessibility)
- Native HTML5 validation with nice API
- Built-in validity matchers:
valueMissing,typeMismatch,tooShort,tooLong,patternMismatch, etc. - Server-side validation support (
serverInvalidprop) - Custom async validators
Semantic badge for status, categories, or counts.
// dashboard/src/ui/badge/badge.tsx
type BadgeVariant = 'default' | 'success' | 'warning' | 'error' | 'info'
const variantStyles: Record<BadgeVariant, string> = {
default: 'bg-darken-100 text-text-secondary',
success: 'bg-green-50 text-green-700',
warning: 'bg-yellow-50 text-yellow-700',
error: 'bg-red-50 text-red-700',
info: 'bg-blue-50 text-blue-700',
}
interface BadgeProps {
variant?: BadgeVariant
size?: 'sm' | 'md'
icon?: ReactNode
className?: string
children?: ReactNode
}
function Badge({
variant = 'default',
size = 'sm',
icon,
children,
className
}: BadgeProps) {
return (
<span
className={cn(
inline({ gap: '1' }),
'rounded-full font-medium',
size === 'sm' && 'px-2 py-0.5 text-xs',
size === 'md' && 'px-2.5 py-1 text-sm',
variantStyles[variant],
className
)}
>
{icon}
{children}
</span>
)
}Usage:
<Badge>Default</Badge>
<Badge variant="success">Active</Badge>
<Badge variant="error" icon={<IconAlert className="size-3" />}>Failed</Badge>
<Badge variant="info" size="md">12 items</Badge>Small colored dot indicating status. Optionally shows a label.
// dashboard/src/ui/status-dot/status-dot.tsx
type Status = 'success' | 'warning' | 'error' | 'info' | 'offline' | 'neutral'
const statusColors: Record<Status, string> = {
success: 'bg-green-500',
warning: 'bg-yellow-500',
error: 'bg-red-500',
info: 'bg-blue-500',
offline: 'bg-gray-400',
neutral: 'bg-gray-300',
}
const sizes = {
sm: 'size-1.5',
md: 'size-2',
lg: 'size-2.5',
}
interface StatusDotProps {
status: Status
size?: 'sm' | 'md' | 'lg'
pulse?: boolean
label?: string
className?: string
}
function StatusDot({
status,
size = 'md',
pulse,
label,
className
}: StatusDotProps) {
const dot = (
<span
className={cn(
'rounded-full inline-block flex-shrink-0',
sizes[size],
statusColors[status],
pulse && 'animate-pulse',
className
)}
/>
)
if (label) {
return (
<span className={inline({ gap: '1.5' })}>
{dot}
<span className="text-sm text-text-secondary">{label}</span>
</span>
)
}
return dot
}Usage:
<StatusDot status="success" />
<StatusDot status="error" pulse />
<StatusDot status="success" label="Online" />
<StatusDot status="offline" size="lg" label="Disconnected" />Loading placeholder with shimmer animation.
// dashboard/src/ui/skeleton/skeleton.tsx
interface SkeletonProps {
variant?: 'text' | 'circular' | 'rectangular'
width?: string | number
height?: string | number
className?: string
}
function Skeleton({
variant = 'text',
width,
height,
className
}: SkeletonProps) {
return (
<div
className={cn(
'animate-pulse bg-darken-200',
variant === 'text' && 'h-4 rounded',
variant === 'circular' && 'rounded-full',
variant === 'rectangular' && 'rounded-md',
className
)}
style={{ width, height }}
/>
)
}Usage:
// Text placeholder
<Skeleton width="60%" />
// Avatar placeholder
<Skeleton variant="circular" width={40} height={40} />
// Card placeholder
<Skeleton variant="rectangular" width="100%" height={120} />Centered empty state with icon, title, description, and action.
// dashboard/src/ui/empty-state/empty-state.tsx
interface EmptyStateProps {
icon?: ReactNode
title: string
description?: string
action?: ReactNode
className?: string
}
function EmptyState({
icon,
title,
description,
action,
className
}: EmptyStateProps) {
return (
<div
className={cn(
box({ center: true, fill: true }),
stack({ gap: '4', align: 'center' }),
'py-12 px-6',
className
)}
>
{icon && (
<div className={cn(box({ center: true }), 'size-12 rounded-full bg-darken-100 text-text-secondary')}>
{icon}
</div>
)}
<div className={cn(stack({ gap: '1', align: 'center' }), 'text-center max-w-sm')}>
<h3 className="text-base font-semibold">{title}</h3>
{description && (
<p className="text-sm text-text-secondary">{description}</p>
)}
</div>
{action}
</div>
)
}Usage:
<EmptyState
icon={<IconFolder className="size-6" />}
title="No files"
description="Upload your first file to get started"
action={<Button>Upload File</Button>}
/>
// Minimal
<EmptyState title="No results found" />Clickable row with optional selection state. Compound component.
// dashboard/src/ui/list-item/list-item.tsx
interface ListItemProps {
selected?: boolean
disabled?: boolean
onClick?: () => void
className?: string
children?: ReactNode
}
function ListItem({
selected,
disabled,
onClick,
className,
children,
...props
}: ListItemProps) {
return (
<div
role={onClick ? 'button' : undefined}
tabIndex={onClick && !disabled ? 0 : undefined}
onClick={disabled ? undefined : onClick}
className={cn(
flex({ gap: '3' }),
'px-2 py-2 rounded-lg transition-colors',
onClick && 'cursor-pointer hover:bg-surface-hover',
selected && 'bg-surface-hover',
disabled && 'opacity-50 cursor-not-allowed',
className
)}
{...props}
>
{children}
</div>
)
}
function ListItemContent({ className, children }: { className?: string; children?: ReactNode }) {
return <div className={cn('flex-1 min-w-0', className)}>{children}</div>
}
function ListItemTitle({ className, children }: { className?: string; children?: ReactNode }) {
return <div className={cn('text-sm font-medium truncate', className)}>{children}</div>
}
function ListItemSubtitle({ className, children }: { className?: string; children?: ReactNode }) {
return <div className={cn('text-xs text-text-secondary truncate', className)}>{children}</div>
}
function ListItemAction({ className, children }: { className?: string; children?: ReactNode }) {
return <div className={cn('flex-shrink-0', className)}>{children}</div>
}
ListItem.Content = ListItemContent
ListItem.Title = ListItemTitle
ListItem.Subtitle = ListItemSubtitle
ListItem.Action = ListItemActionUsage:
<ListItem onClick={handleClick} selected={isActive}>
<Avatar src={user.avatar} />
<ListItem.Content>
<ListItem.Title>{user.name}</ListItem.Title>
<ListItem.Subtitle>Last active 2h ago</ListItem.Subtitle>
</ListItem.Content>
<ListItem.Action>
<IconChevron className="size-4" />
</ListItem.Action>
</ListItem>Groups buttons together, optionally attached with shared borders.
// dashboard/src/ui/button-group/button-group.tsx
interface ButtonGroupProps {
attached?: boolean
className?: string
children?: ReactNode
}
function ButtonGroup({ attached, className, children }: ButtonGroupProps) {
return (
<div
className={cn(
flex({ gap: attached ? '0' : '1' }),
attached && [
'[&>*:not(:first-child)]:rounded-l-none',
'[&>*:not(:last-child)]:rounded-r-none',
'[&>*:not(:first-child)]:-ml-px',
],
className
)}
>
{children}
</div>
)
}Usage:
// Spaced
<ButtonGroup>
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
// Attached
<ButtonGroup attached>
<Button variant="outline">Left</Button>
<Button variant="outline">Center</Button>
<Button variant="outline">Right</Button>
</ButtonGroup>Text component with semantic variants. Enhanced with truncation.
// dashboard/src/ui/text.tsx (enhance existing)
interface TextProps {
variant?: 'default' | 'title' | 'title2' | 'title3' | 'subheadline' | 'body' | 'callout' | 'caption'
color?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger'
weight?: 'normal' | 'medium' | 'semibold' | 'bold'
truncate?: boolean | number // true = single line, number = line clamp
as?: 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
emphasized?: boolean
className?: string
children?: ReactNode
}
function Text({
variant = 'default',
color = 'default',
weight,
truncate,
as: Component = 'span',
emphasized,
className,
children,
...props
}: TextProps) {
return (
<Component
className={cn(
variantStyles[variant],
colorStyles[color],
weight && weightStyles[weight],
emphasized && 'italic',
truncate === true && 'truncate',
typeof truncate === 'number' && `line-clamp-${truncate}`,
className
)}
{...props}
>
{children}
</Component>
)
}Usage:
<Text variant="title">Page Title</Text>
<Text variant="body" color="secondary">Description text</Text>
<Text truncate>This will truncate if too long...</Text>
<Text truncate={2}>This will clamp to 2 lines...</Text>PR 1: Layout Helpers
flex(),stack(),inline(),box(),grid()- Shared types and spacing scale
- Unit tests for all helpers
- Update AGENTS.md with usage patterns
PR 2: WithTooltip
- Convenience wrapper around Radix Tooltip
- Solves 387 tooltip usages with boilerplate
PR 3: Action Primitive
- Semantic clickable (button/link polymorphism)
- Uses
@radix-ui/react-slotforasChild - Solves 136
cursor-pointerdivs
PR 4: Radix Form Wrapper
- Install
@radix-ui/react-form - FormField, FormLabel, FormControl, FormMessage, FormDescription, FormSubmit
- Uses
stack()internally for field layout
PR 5: Badge + StatusDot
- Badge with variants: default, success, warning, error, info
- StatusDot with optional label and pulse animation
- Both use
inline()internally
PR 6: Skeleton + EmptyState
- Skeleton with variants: text, circular, rectangular
- EmptyState uses
stack()+box({ center: true })
PR 7: ListItem Compound Component
- Uses
flex()internally - Compound: ListItem.Content, .Title, .Subtitle, .Action
- Selection and disabled states
PR 8: ButtonGroup + Text Enhancement
- ButtonGroup with optional attached styling
- Text enhanced with
truncateprop (single line or line-clamp)
PR 9: Migration + Docs
- Migrate top 50
flex items-centerpatterns - Complete AGENTS.md documentation
- Storybook examples for all helpers and components
dashboard/src/ui/
├── helpers/
│ ├── flex.ts
│ ├── flex.test.ts
│ ├── stack.ts
│ ├── stack.test.ts
│ ├── inline.ts
│ ├── inline.test.ts
│ ├── box.ts
│ ├── box.test.ts
│ ├── grid.ts
│ ├── grid.test.ts
│ ├── types.ts
│ └── index.ts
├── action/
│ ├── action.tsx
│ ├── action.test.tsx
│ └── action.stories.tsx
├── badge/
│ ├── badge.tsx
│ ├── badge.test.tsx
│ └── badge.stories.tsx
├── button-group/
│ ├── button-group.tsx
│ ├── button-group.test.tsx
│ └── button-group.stories.tsx
├── empty-state/
│ ├── empty-state.tsx
│ ├── empty-state.test.tsx
│ └── empty-state.stories.tsx
├── list-item/
│ ├── list-item.tsx
│ ├── list-item.test.tsx
│ └── list-item.stories.tsx
├── skeleton/
│ ├── skeleton.tsx
│ ├── skeleton.test.tsx
│ └── skeleton.stories.tsx
├── status-dot/
│ ├── status-dot.tsx
│ ├── status-dot.test.tsx
│ └── status-dot.stories.tsx
├── form.tsx # Radix Form wrapper
├── text.tsx # Enhanced with truncate
├── tooltip.tsx # + WithTooltip
└── index.ts
No new dependencies for helpers. They're pure functions using cn().
Add for Form:
npm install @radix-ui/react-form| Metric | Target |
|---|---|
| Helper bundle size | < 1KB gzipped |
| Type coverage | 100% |
| Token count vs components | ≤ 105% (no worse than 5% more) |
Raw flex items-center |
< 400 (from 1,200) |
cursor-pointer divs |
< 20 (from 136) |
| Storybook coverage | 100% |
For each PR:
- Implementation with full TypeScript types
- Unit tests (vitest)
- Storybook stories
- Export from
ui/index.tsorui/helpers/index.ts - AGENTS.md updated
- Lint passes
-
classNameescape hatch on all components