UI Design System
- Tailwind CSS v4 — utility-first, configured via CSS (
src/app/globals.css) - shadcn/ui — component library (style:
radix-nova), source copied intosrc/components/ui/ - Radix UI — unstyled primitives underlying shadcn components
class-variance-authority— variant system used inside shadcn componentscn()— merge helper at@/lib/utils, combinesclsx+tailwind-merge
Design tokens
Section titled “Design tokens”Cinatra’s tokens live in :root in src/app/globals.css. shadcn expects a different token set; they are bridged via var() aliases in the same file — cinatra tokens are never removed.
Cinatra tokens (source of truth)
Section titled “Cinatra tokens (source of truth)”| Token | Value | Use |
|---|---|---|
--background | #f3f4f7 | Page background |
--foreground | #16181d | Default text |
--surface | rgba(255,255,255,0.82) | Frosted-glass panels |
--surface-strong | #ffffff | Solid white panels |
--surface-muted | #eef1f5 | Muted/secondary surfaces |
--muted | #69707d | Muted text color |
--accent | #17191f | Dark ink (primary action color) |
--accent-soft | #d9dee7 | Light soft tint |
--line | rgba(15,23,42,0.08) | Borders |
--shadow-soft / --shadow-strong | — | Box shadows |
--radius | 0.5rem | Base border radius |
shadcn compatibility aliases
Section titled “shadcn compatibility aliases”These are defined as var(--cinatra-token) aliases; they let shadcn components work without modification.
| shadcn token | Maps to |
|---|---|
--card / --card-foreground | --surface / --foreground |
--popover / --popover-foreground | --surface-strong / --foreground |
--primary / --primary-foreground | --accent / #ffffff |
--secondary / --secondary-foreground | --surface-muted / --foreground |
--muted-foreground | --muted |
--border / --input | --line |
--ring | --accent |
--destructive / --destructive-foreground | #a6384f (light) / oklch(0.704 0.191 22.216) (dark) |
--success / --success-foreground | oklch(0.50 0.17 145) (light) / oklch(0.65 0.20 145) (dark) — green status tier |
--warning / --warning-foreground | oklch(0.52 0.14 55) (light) / oklch(0.72 0.16 75) (dark) — amber status tier (note: dark-mode hue is intentionally shifted from 55→75 for warmer reading on dark surfaces) |
--info / --info-foreground | oklch(0.47 0.13 240) (light) / oklch(0.66 0.15 240) (dark) — blue status tier |
The four status-tier tokens (destructive, success, warning, info) all share the tinted-pill cva pattern (bg-{token}/10 text-{token} ... [a]:hover:bg-{token}/20) when used in <Badge variant="…">. Solid-fill alerts using these tokens must verify foreground contrast meets WCAG AA on the chosen surface before shipping. The canonical color-substitution rulebook for sweep waves lives in .planning/audits/UI-AUDIT.md § “Semantic Color Mapping (Phase 227.1 rulebook)”.
Tailwind utility remaps
Section titled “Tailwind utility remaps”In @theme inline, bg-muted maps to --surface-muted (light surface background, not the muted text color) and bg-accent maps to --accent-soft (subtle hover tint). This follows shadcn semantics. No existing code used bg-muted or bg-accent before this was set up.
--color-ink / --color-ink-foreground expose --accent (#17191f) as bg-ink / text-ink-foreground for use in cinatra-specific components that predate shadcn.
Adding components
Section titled “Adding components”Use the shadcn CLI — it reads components.json and writes source to src/components/ui/:
pnpm dlx shadcn@latest add <component>pnpm dlx shadcn@latest add table dialog tabs select separator skeletonTo preview changes before writing:
pnpm dlx shadcn@latest add <component> --dry-runpnpm dlx shadcn@latest add <component> --diffTo search available components:
pnpm dlx shadcn@latest search <keyword>Component rules (summary from .agents/skills/shadcn/)
Section titled “Component rules (summary from .agents/skills/shadcn/)”For the complete rules see .agents/skills/shadcn/SKILL.md. Key points:
- Use semantic color tokens (
bg-primary,text-muted-foreground), never raw values (bg-blue-500) - Use
cn()for conditional classes — not manual template literals - Use
flexwithgap-*— neverspace-x-*/space-y-* - Use
size-*when width and height are equal - Full Card composition:
CardHeader/CardTitle/CardDescription/CardContent/CardFooter - Dialogs always need a title (
DialogTitle, even ifsr-only) - Toast via
sonnertoast(), not custom markup Skeletonfor loading states,Badgefor labels,Separatorinstead of<hr>
Current components
Section titled “Current components”shadcn UI primitives (src/components/ui/)
Section titled “shadcn UI primitives (src/components/ui/)”Added via pnpm dlx shadcn@latest add <name>. Own the source — edit freely.
button.tsx— variants: default (dark ink), outline, secondary, ghost, destructive, linkcard.tsx— full composition with CardHeader, CardTitle, CardDescription, CardContent, CardFooter, CardActionbadge.tsx— inline labels with variantsinput.tsx— form input with ring/border tokens
App layout components (src/components/)
Section titled “App layout components (src/components/)”App-specific, not managed by shadcn CLI.
PageHeader — page title bar with optional description and right-side action slot:
import { PageHeader } from "@/components/page-header";
<PageHeader title="Accounts" description="Company-level records and enrichment signals." actions={<Button>New account</Button>}/>Props: title: string, description?: string, actions?: ReactNode, className?: string.
Uses text-foreground / text-muted-foreground so it responds to theme automatically.
PageContent — content wrapper matching PageHeader’s max-width and padding:
import { PageContent } from "@/components/page-content";
<PageContent> <Card>...</Card></PageContent>Props: children: ReactNode, className?: string (e.g. "pb-16" for pages with a floating action bar).
Use PageHeader + PageContent on every new page instead of repeating mx-auto max-w-7xl px-5 sm:px-8 lg:px-0 inline.
IconButton — square 44×44 icon button used in the AppShell header. Uses semantic tokens, adapts to all themes automatically:
import { IconButton } from "@/components/icon-button";
<IconButton onClick={handleClick} aria-label="Open menu"> <SomeIcon className="h-5 w-5" /></IconButton>Props: extends React.ComponentPropsWithoutRef<"button">. Add className to override specific utilities (e.g. "fixed right-5" for positioned variants).
Dark mode
Section titled “Dark mode”Implemented. next-themes manages the .dark class on <html>.
- Token overrides:
src/app/globals.css—.dark { ... }block overrides all cinatra tokens and the two shadcn-specific values (--primary-foreground,--destructive) that need different values in dark mode - Provider:
src/app/providers.tsxwraps the tree in<ThemeProvider attribute="class" defaultTheme="system" enableSystem> - Toggle:
src/components/theme-toggle.tsx— sun/moon button; rendered in the AppShell header between dev tools and notifications - shadcn components pick up dark mode automatically via semantic tokens — no extra work needed when adding new components
Dark token values (in .dark):
| Token | Dark value |
|---|---|
--background | #0e1016 |
--foreground | #e8eaef |
--surface | rgba(22,25,34,0.84) |
--surface-strong | #1a1d27 |
--surface-muted | #1e2130 |
--muted | #8891a0 |
--accent | #e8eaef (flipped to light for primary buttons) |
--accent-soft | #262b38 |
--line | rgba(255,255,255,0.07) |
Color themes (named palettes)
Section titled “Color themes (named palettes)”A named color theme is a [data-theme="X"] block in globals.css that overrides the cinatra design tokens. The ThemeColorSelector component (src/components/theme-color-selector.tsx) writes data-theme to <html> and persists to localStorage. A small inline script in src/app/layout.tsx restores the selection before React hydrates to prevent flash.
Currently defined themes: default (no data-theme attribute) and ocean ([data-theme="ocean"]).
To add a new color theme:
- Add a
[data-theme="my-theme"] { ... }block toglobals.cssoverriding the tokens you want to change - Add a
{ id: "my-theme", label: "My Theme", swatch: "#hex" }entry to theTHEMESarray intheme-color-selector.tsx
Named themes compose with dark mode — the .dark class and data-theme are independent. A dark “ocean” theme would need .dark[data-theme="ocean"] overrides if the default [data-theme="ocean"] values don’t look right in dark mode.
Best practices (mandatory rules)
Section titled “Best practices (mandatory rules)”These rules are enforced by the shadcn skill (.agents/skills/shadcn/SKILL.md) and apply to all new code in this project.
Never use hardcoded color classes
Section titled “Never use hardcoded color classes”Wrong:
<div className="bg-white text-slate-900 hover:bg-slate-900 hover:text-white" /><div className="dark:bg-gray-800 dark:text-gray-100" />Right:
<div className="bg-surface-strong text-foreground hover:bg-primary hover:text-primary-foreground" />The shadcn skill rule: “No manual dark: color overrides. Use semantic tokens.”
Semantic tokens (text-foreground, bg-surface-strong) adapt to every theme automatically — no dark: variants needed. Adding explicit dark: color pairs is the wrong fix; it bloats classNames and still breaks when a third theme is added.
Token cheat sheet (what to use instead of what)
Section titled “Token cheat sheet (what to use instead of what)”| Instead of | Use |
|---|---|
text-slate-900, text-slate-800, text-slate-700 | text-foreground |
text-slate-600, text-slate-500, text-slate-400 | text-muted-foreground |
bg-white | bg-surface-strong |
bg-white/80, bg-white/75, bg-white/70 | bg-surface |
hover:bg-slate-900 hover:text-white | hover:bg-primary hover:text-primary-foreground |
hover:bg-slate-100, hover:bg-slate-50 | hover:bg-surface-muted |
border-slate-200, border-slate-300 | border-line |
focus:border-slate-400 | focus:border-border |
When a semantic token doesn’t exist
Section titled “When a semantic token doesn’t exist”Add it to globals.css rather than hardcoding a value. See “Extending the theme” below.
Extending the theme
Section titled “Extending the theme”To add a new cinatra design token that shadcn components can also use:
- Add the raw value in
:rootinglobals.css - Add a corresponding override in
.dark { ... }if the dark value differs - Add a
var()alias under the shadcn section if it maps to a shadcn concept - Add a
--color-*entry in@theme inlineto expose it as a Tailwind utility