Design System — AI Software Factory
The visual + interaction contract for
packages/uiand every screen inapps/web. Implements the UX requirements (ux-requirements.md) and the theming rule PR-164 (tokens only, no hard-coded colors). Token tables are the source of truth for values; the implementation lives inpackages/ui/src/tokens/*.css(CSS variables) and is consumed via Tailwind'stheme.extendmapping (hex values appear only in the token files — an eslint rule blocks hex/rgb literals elsewhere inapps/web).
1. Design tokens
1.1 Color — semantic tokens (the only names app code may use)
| Token | Light | Dark | Purpose |
|---|---|---|---|
--color-bg |
#FFFFFF |
#191919 |
app background |
--color-surface |
#FFFFFF |
#202020 |
cards, dialogs, menus |
--color-surface-2 |
#F7F6F3 |
#262626 |
sidebar, hover surfaces, code bg |
--color-surface-3 |
#F1F0EC |
#2E2E2E |
active hover, pressed, inset panels |
--color-border |
#E9E8E3 |
#333333 |
hairlines, dividers |
--color-border-strong |
#D3D1C9 |
#454545 |
input borders, focused menus |
--color-text |
#37352F |
#FFFFFF |
primary text |
--color-text-secondary |
#6B6B6B |
#B4B4B4 |
secondary text, placeholders |
--color-text-tertiary |
#9B9A97 |
#808080 |
muted text, disabled, timestamps |
--color-accent |
#2383E2 |
#3B8EE6 |
primary actions, links, active states |
--color-accent-hover |
#1A73C7 |
#4C9BE8 |
|
--color-accent-soft |
#E7F2FC |
#1C2E40 |
active tab bg, selection bg, info chips |
--color-success |
#0F7B6C |
#3DB6A0 |
success text/icon |
--color-success-soft |
#E3F4F0 |
#14332E |
|
--color-warning |
#B97324 |
#D99A4E |
warnings |
--color-warning-soft |
#FBF0E2 |
#3A2C18 |
|
--color-danger |
#E03E3E |
#F2545B |
destructive, errors |
--color-danger-soft |
#FCEBEB |
#3D2222 |
|
--color-focus-ring |
#2383E2 |
#3B8EE6 |
focus outline (2 px) |
--color-shadow |
rgba(15,15,15,0.08) |
rgba(0,0,0,0.5) |
shadow color |
--color-scrim |
rgba(15,15,15,0.4) |
rgba(0,0,0,0.6) |
modal overlay |
--color-selection |
rgba(35,131,226,0.25) |
rgba(59,142,230,0.35) |
text/block selection |
--color-skeleton |
#F1F0EC |
#2E2E2E |
skeleton shimmer base |
Contrast check (WCAG 2.2 AA): --color-text on --color-bg ≥ 12.6:1 in
both themes; --color-text-secondary ≥ 4.6:1; --color-accent on
--color-bg 4.9:1 (light) / 5.4:1 (dark) — verified by the token test
(packages/ui/src/tokens/contrast.test.ts, fails the build on a token edit
that breaks a pair).
1.2 Color — database option palette (28 named, stable across themes)
Select / Multi Select / Status / group colors. Each has a base + soft (background) variant; the base is used for text/dots, the soft for pills.
| Name | Base (L) | Soft (L) | Base (D) | Soft (D) |
|---|---|---|---|---|
default (gray) |
#9B9A97 |
#F1F0EC |
#9B9A97 |
#2E2E2E |
gray |
#787774 |
#E9E8E3 |
#B4B4B4 |
#333333 |
brown |
#9F6B53 |
#F3EAE5 |
#C58C74 |
#3A2A22 |
orange |
#D9730D |
#FBEBDD |
#E8944A |
#3D2A14 |
yellow |
#B99724 |
#F9F1D8 |
#D9BE4E |
#3A3418 |
green |
#0F7B46 |
#E3F4EB |
#3DB678 |
#143326 |
blue |
#2383E2 |
#E7F2FC |
#3B8EE6 |
#1C2E40 |
purple |
#6B57D0 |
#EEEAFB |
#8F7FE6 |
#282240 |
pink |
#C2398B |
#F9E8F4 |
#E05FB0 |
#3D1F33 |
red |
#E03E3E |
#FCEBEB |
#F2545B |
#3D2222 |
brown_bg … |
(the soft column above doubles as the _bg token) |
(Each name also exists as <name>-bg = the soft value; e.g.
--color-option-blue-bg. 10 hues × base/bg = 20, + default pair + 8
"extra" light variants for gallery covers = 28 palette entries.)
1.3 Typography
Font stack: Inter, -apple-system, "Segoe UI", Roboto, sans-serif; mono:
"JetBrains Mono", ui-monospace, "SF Mono", monospace.
| Token | Size | Line-height | Weight | Use |
|---|---|---|---|---|
--text-title |
26 px | 1.25 | 650 | page title (H1) |
--text-h1 |
26 px | 1.25 | 600 | heading_1 |
--text-h2 |
20 px | 1.3 | 600 | heading_2 |
--text-h3 |
16 px | 1.3 | 600 | heading_3 |
--text-body-lg |
16 px | 1.5 | 400 | editor body, primary UI text |
--text-body |
15 px | 1.5 | 400 | default text (tables, lists, menus) |
--text-small |
13 px | 1.4 | 400 | secondary UI, table cells compact |
--text-tiny |
12 px | 1.3 | 400 | badges, timestamps, captions |
--text-mono |
13 px | 1.5 | 400 | code blocks, formula input |
--text-weight-medium |
— | — | 500 | |
--text-weight-semibold |
— | — | 600 | |
--text-weight-title |
— | — | 650 | |
--text-placeholder |
— | — | 400 | color: --color-text-tertiary |
1.4 Spacing (4 px scale)
| Token | Value | Use |
|---|---|---|
--space-1 |
4 px | icon↔label, chip inner |
--space-2 |
8 px | tight gaps, menu item padding-y |
--space-3 |
12 px | standard control padding |
--space-4 |
16 px | block vertical rhythm (editor line gaps) |
--space-5 |
24 px | section gaps, dialog padding |
--space-6 |
32 px | page content top/bottom |
--space-7 |
48 px | hero/empty-state padding |
--space-8 |
64 px | large layout padding |
Editor rhythm: block margin-top = --space-4 (paragraph), headings add
--space-4 above (h1) / --space-3 (h2, h3); toggle children indent
--space-5 + a 24 px gutter; list indent per level = --space-5.
1.5 Radii
| Token | Value | Use |
|---|---|---|
--radius-sm |
4 px | chips, badges, small buttons |
--radius-md |
6 px | inputs, buttons, menu items |
--radius-lg |
8 px | cards, dialogs, popovers, kanban cards |
--radius-xl |
12 px | drawers, command palette |
--radius-full |
999 px | pills, avatars, toggles |
1.6 Shadows
| Token | Value (light) | Value (dark) | Use |
|---|---|---|---|
--shadow-sm |
0 1px 2px var(--color-shadow) |
same | cards at rest, rows hover |
--shadow-md |
0 2px 8px var(--color-shadow) |
0 2px 8px var(--color-shadow) |
lifted cards, hover lift, drag preview |
--shadow-lg |
0 8px 24px var(--color-shadow) |
0 8px 24px var(--color-shadow) |
menus, popovers, dialogs |
--shadow-focus |
0 0 0 2px var(--color-focus-ring) |
same | focus ring (combined with border) |
1.7 Z-index (layering contract — ux-requirements §11)
| Token | Value | Use |
|---|---|---|
--z-content |
0 | page content |
--z-sticky |
10 | sticky table headers, top-of-page toolbar |
--z-toolbar |
30 | floating selection toolbar, block hover controls |
--z-drawer |
40 | settings drawer (view settings, right rail) |
--z-topbar |
50 | top bar |
--z-sidebar-overlay |
90 | mobile sidebar drawer |
--z-palette |
100 | command palette |
--z-toast |
110 | toasts |
--z-dialog |
120 | confirm dialogs, modals |
1.8 Motion
| Token | Value | Use |
|---|---|---|
--motion-instant |
0 ms | reduced-motion: everything |
--motion-fast |
120 ms | hover, focus ring, press |
--motion-base |
150 ms | menus, dropdowns, toasts in/out |
--motion-slow |
200 ms | drawers, palette open/close, view switch |
--motion-ease |
cubic-bezier(0.25, 0.6, 0.3, 1) |
standard ease-out |
--motion-spring |
cubic-bezier(0.34, 1.3, 0.64, 1) |
drag settle, card drop (slight overshoot) |
Rules: only opacity and transform animate (compositor-friendly); layout
properties never animate except drawer width; reduced-motion (OS or
manual) maps every token to --motion-instant and disables drag
auto-scroll animation (jump-cut instead) and skeleton shimmer (static fill).
2. Theming implementation
packages/ui/src/tokens/
├── tokens.light.css # :root / .light { --color-bg: #fff; … } (ALL hex lives here)
├── tokens.dark.css # .dark { --color-bg: #191919; … }
├── tokens.hc.css # .hc (high contrast override set, v1.1)
├── tokens.typography.css # font/spacing/radii/shadows/z-index/motion (theme-independent)
└── contrast.test.ts # WCAG pair checks on the color tables above
- Mode:
next-themesinapps/web(attribute="class",defaultTheme="system",enableSystem). The user setting (users.theme: light/dark/system) is applied on boot by setting theclasson<html>before first paint (inline script in the root layout to avoid FOUC); the palette toggle writes the setting via the API (persisted) and flips the class locally (optimistic). - Consumption:
packages/uiexports Tailwind theme extensions mapping utility names to tokens (bg-surface→var(--color-surface),text-primary→var(--color-text),border-default→var(--color-border),rounded-card→var(--radius-lg),shadow-pop→var(--shadow-lg),duration-base→var(--motion-base)). App code uses only these utilities (or the raw CSS vars in inline styles for dynamic values like option colors:style={{ background: \var(--color-option-${color}-bg)` }}`). - Apps that consume:
apps/web(all).packages/uicomponents are theme-agnostic (they reference tokens, never hex). The admin console (Phase 14) reuses the same token set. - Option colors at runtime: property values store the name
(
"blue"); the renderer resolvesvar(--color-option-blue)— so a new theme variant only touches the token files. - High-contrast (§8): a third class
.hcthat overrides the color tokens to a pure-black/white 2-px-border set; toggled in Settings (v1.1, flagFEATURE_HC), required for enterprise (ux-requirements §9).
2.1 As-built (apps/web) — token format contract + conflict record
Recorded per AGENTS.md rule 29 (2026-10). The package above (
packages/uihex tokens +next-themes) is the target design; the shipped web app runs its own local theming. Both are described so the contract is unambiguous:
- State:
apps/web/src/stores/ui-store.ts(Zustand) —themelight/dark/system, persisted tolocalStorage["factory-theme"], applied byapplyTheme()(toggles.darkon<html>) fromcomponents/layout/theme-provider.tsx;useSystemThemeSync()listens toprefers-color-schemewhiletheme === "system". The Appearance page (/w/:workspace/settings/appearance) is the sole user entry point. - Token format (the load-bearing contract): the 19 core tokens in
apps/web/src/app/globals.cssare stored as raw oklch channel triples (--background: 0.985 0.002 247.839), andtailwind.config.tsreferences them asoklch(var(--token) / <alpha-value>). Storing a full color (oklch(…)in the var) produces the invalid nestedoklch(oklch(…)), which the browser resolves to transparent — everybg-*/text-*/border-*utility silently paints nothing and dark mode looks identical to light. Code needing the finished color must wrap explicitly:oklch(var(--primary))/oklch(var(--primary) / a)(see theaccent-[oklch(var(--primary))]checkbox utilities and the scrollbaroklch(var(--muted-foreground) / 0.35)). - Exempt tokens:
--factory-*(code + 18 block colors) are fulloklch()colors by design — consumed directly viavar()in inline styles, never through Tailwind. - Guards:
apps/web/src/app/__tests__/theme-tokens.test.tspins the raw-triple + config-wrapping contract;tests/e2e/specs/appearance-theme.spec.tspins the observable behavior (dark paints an opaque, distinct background; persists across reload; system follows the OS). - Realtime status: the app-wide WS singleton is opened in
components/layout/workspace-shell.tsx(keyed on the auth token; disconnect on logout), so the Live/Offline chip + offline banner are correct on every authenticated route, not only page/database routes.
3. Component inventory (packages/ui)
Props sketches are TypeScript-ish and show the public API. All components forward
ref, acceptclassName(tailwind-merge), and are keyboard-accessible per ux-requirements §12. Radix primitives underpin the overlay components (menu/dialog/popover/tooltip/tabs/accordion).
3.1 Button
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "ghost" | "danger" | "danger-soft";
size?: "sm" | "md" | "lg"; // 28 / 32 / 40 px height
loading?: boolean; // spinner + disabled (see §5 states)
iconOnly?: boolean; // aria-label required
leadingIcon?: React.ReactNode; // 16 px icon, --space-2 gap
}
- primary:
bg-accent text-white, hoverbg-accent-hover; secondary:bg-surface-2 border-border; ghost: transparent, hoverbg-surface-3; danger:bg-danger text-white; danger-soft:bg-danger-soft text-danger. - Loading:
opacity-60 pointer-events-none+ 14 px spinner replacing leadingIcon; aria-busy.
3.2 Input / Textarea / Select / Checkbox / Switch / Slider
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string; // renders <label> + id wiring (a11y)
error?: string; // renders --color-danger message, border-danger, aria-invalid
hint?: string; // helper line, --color-text-tertiary
leadingIcon?: React.ReactNode;
truncate?: boolean; // single-line ellipsis mode (table cells)
}
- Input: height 32 px,
rounded-md,border-border→ focusborder-accent + shadow-focus; errorborder-danger. - Textarea: auto-grow (min 1 line, max 8, then scroll).
- Select: native
<select>styled (a11y + perf); for > 20 options or searchable cases useCommandSelect(§3.3). - Checkbox: 16 px,
rounded-sm, checked =bg-accent+ white check; indeterminate = dash. Switch: 28×16 track,--radius-full, knob 12 px. - Slider (column resize, spacer height, chart values): 2 px track, 12 px thumb, value tooltip on drag.
3.3 Command (searchable select / the command-list primitive)
<Command onSearch={fn} onOpen={fn}>
<Command.Input placeholder="Search…" />
<Command.List>
<Command.Group label="Text">
<Command.Item value="h1" onSelect={fn} shortcut="⌘⇧1" icon={<Icon/>}>Heading 1</Command.Item>
</Command.Group>
</Command.List>
</Command>
- Powers: slash menu, command palette, property pickers, page move-to, user pickers (PERSON), relation pickers. Fuzzy: subsequence match on labels (case-insensitive), ranked by start-position + group order; 150 ms debounce; max 50 items rendered (virtualized beyond 30).
- Keyboard: ↑↓ (loop), Enter (select), Esc (close+return focus), Tab
(group cycle),
/prefix filtering. ARIA: combobox + listbox pattern (ux-requirements §12).
3.4 Dialog / Drawer / Popover
<Dialog open onOpenChange title description size="sm|md|lg">…</Dialog>
// role=dialog aria-modal, scrim, focus trap, Esc, footer: Cancel/Confirm
<Drawer side="right" width={320} title>…</Drawer> // view settings, filters
<Popover trigger={node} align="start" width={260}>…</Popover> // cell editors
- Dialog sizes: sm 400 px / md 520 px / lg 720 px; full-screen sheet below
640 px (slide up + grab handle). Confirm variant:
ConfirmDialogwith danger support + typed confirmation (input "DELETE") for irreversible ops. - Drawer: right side,
--motion-slowslide; content scrollable, footer pinned.
3.5 DropdownMenu / ContextMenu / Menu primitives
<DropdownMenu trigger={node} align="start">
<Menu.Item icon onSelect>…</Menu.Item>
<Menu.Sub label="Turn into">…</Menu.Sub>
<Menu.Separator/>
<Menu.Item destructive>Delete</Menu.Item>
</DropdownMenu>
<ContextMenu>…same item tree…</ContextMenu> // right-click / ⋮⋮
- 260 px,
shadow-lg,rounded-lg, items 32 px, icon 16 px--space-3gap; submenu opens on hover (300 ms close grace — 1.4.13) and on focus; max-height 320 px scroll; destructive itemstext-danger; checkable items show ✓; items the caller can't use are not rendered (permission-gated at the call site).
3.6 Tooltip / Toaster / Toast
<Tooltip content="Copy link" side="bottom">…</Tooltip>
// 500 ms open delay, 0 close; aria-describedby; never on touch (tap = action)
<Toaster/> // portal, stack max 4, --z-toast
toast({ title, description?, kind: "info"|"success"|"warning"|"error",
action?: {label, onClick}, duration? }) // 4s / 7s w/ action
3.7 Avatar / Badge / Chip
- Avatar: 20/24/32 px circles; image or initial-fallback (background =
stable hue hash of the user id, from the option palette); presence dot
(10 px,
--color-success) for "here now"; stacked groups overlap −8 px with max 5 + "+n". - Badge: 18 px pill,
text-tiny, soft-variant bg (option colors or semantic). Chip (removable): badge + × (16 px hit → 24 px touch).
3.8 Card / KanbanCard
- Card:
rounded-lg border bg-surface, padding--space-4; hovershadow-sm. - KanbanCard (board view): cover (16:9,
rounded-t-lg), icon + title (text-body, 2-line clamp), ≤ 3 property rows (badge/date/avatar renderers), drag handle implicit (whole card),--shadow-md+ 2° tilt while dragging.
3.9 Table / VirtualizedList
<DataTable<T>
rows={T[]} rowKey={fn} onRowClick? onRowSelect?
selection="none|single|multiple"
columns={{ id, header, cell(row), width?, minWidth=80, maxWidth=500,
sortable?, sticky?, renderCell?(ctx) }}
virtualize={{ rowHeight: 48|64, overscan: 8 }}
density="compact|comfortable" />
- Backs the table view, responses view, run history, audit viewer, session
list.
@tanstack/react-virtualfor rows and horizontal columns; sticky first column + header row (both, independently); sort indicators onaria-sortheaders; row selection: checkbox column (multiple) or row-click (single); empty → theEmptyStateslot; loading → 12 skeleton rows. - VirtualizedList: the generic primitive (sidebar tree, comment list, notifications) — same virtualization core without the column model.
3.10 Tabs
<Tabs value onChange items={{id, label, icon?, closable?}} addMenu?/>
- View tabs (databases), settings sections, dialog steps. Active:
border-b-2 border-accent text-primary; 36 px height; add button ("+") at the end (opens the add menu, e.g. new view type).
3.11 Sidebar (page tree)
<SidebarTree nodes={TreeNode[]} onSelect activeId collapsed
onDragReorder? renderItem={row => (<PageRow …/>)}/>
role=tree/treeitem; rows 32 px: icon (16 px) + title (1-line clamp)- hover actions (⋯ menu, + child); expand chevron on parents
(
aria-expanded); sections (Home, Inbox…, Teamspaces…) are fixed headers with count badges; fractional-order drag (dnd-kit vertical list + reparent); collapsed mode = 56 px icon rail (icons only, hover-expands).
- hover actions (⋯ menu, + child); expand chevron on parents
(
3.12 Skeleton / EmptyState / ErrorState / Spinner
- Skeleton:
bg-skeleton+ shimmer (gradient sweep 1.2 s, disabled in reduced motion); shapes:SkeletonText lines=3,SkeletonRows n=12,SkeletonCircle,SkeletonBlock h. - EmptyState: 96 px monochrome illustration slot + headline (
text-body semibold) + one line body + CTA row (primary + secondary buttons). - ErrorState: icon (24 px danger) + message + Retry button (secondary).
- Spinner: 14/20/28 px, 1.1 s rotate; never the sole loading indicator for content areas (skeletons), only for controls.
3.13 CommandPalette (app composite, built on Command)
The ⌘K overlay (ux-requirements §3): 640 px, --z-palette, groups
(Create / Go to / Actions / Workspace / AI / Data), recent-items memory,
full keyboard model, per-item permission gating. Lives in apps/web
(composes packages/ui Command) because its item catalog is app-specific.
3.14 BlockToolbar / BlockHoverControls (editor composites)
BlockHoverControls: renders the gutter⋮⋮ +on hover (150 ms), exposes drag (dnd-kituseDraggableadapter) + the keyboard-lift API (Spacelift / arrows / drop — ux-requirements §2.2).SelectionToolbar: floats over a multi-block selection (--z-toolbar): duplicate · copy · delete · color · move-to; 100 ms appear.SlashMenu:Commandinstance wired to the editor state (insert at caret, database property-picker flow).ColorPopover: the 28-option palette + "no color"; used by text color, background, callout bg, status options.
3.15 Form controls (builder + public forms)
FormField wrapper (label, required *, hint, error, aria wiring) +
per-type editors: text, textarea, number (spinbuttons), email, date
(popup calendar: month grid, role=grid, keyboard per ux §12), checkbox,
radio/segment (single select), multi-select (Command + chips), rating
(5 stars, aria-valuenow), file (dropzone + file chip list). Public-form
render reuses the same editors in read-submit mode (no builder chrome).
3.16 Media
ImageBlock: lazy<img>(nativeloading=lazy+ IntersectionObserver for the skeleton), width handle (drag → % snap 25/50/66/75/100), caption, upload-progress overlay,altprompt at upload.VideoPlayer/AudioPlayer: native controls (video) / custom minimal (audio: play, seek bar, time); poster from file thumbnail.FileCard: mime icon, name (1-line), size (human), download button (signed URL), preview link (if previewable).BookmarkCard: favicon (16 px), domain (text-tinytertiary), title (text-bodymedium, accent), description (2-line clamp).CodeBlock: language select (top-right on hover), line-number gutter (40 px), copy button,--text-mono, horizontal scroll, no wrap.EquationBlock: KaTeX render; edit mode = raw LaTeX input with live preview + error border on invalid.Calendar(database view): the month/week/day grid (ux §4.3), day cellrole=gridcell, record chips (option-color dot + 1-line title), span rendering for ranges.Timeline: Gantt canvas (HTML grid, not SVG, for 100k-friendliness up to the virtualization cap of 500 visible bars), barsrounded-md, drag/ resize handles (8 px), today line--color-accent2 px.GalleryGrid: responsive card grid (ux §4.4).ChartRender: wraps the chart library (bar/line/area/pie/donut/number) with the config mapping (x/y/group/agg/color/legend); color series from the option palette; tooltip = title + value; "number" renders a KPI (48 px value + 13 px label + delta).
4. Iconography
- Set: a single linear icon set, 16 px grid (1.5 px stroke), currentColor
(icons inherit
text-*tokens — no colored icons except semantic states which set the text color). ~180 icons shipped (navigation, file types, block types, actions, states). Naming:icon/arrow-up.tsxetc.;Iconcomponent:<Icon name="arrow-up" size={16} className/>. - Emoji: used as content (page icons, callout icons, block icons, button icons, avatar-less mentions) — rendered at the content font size, inside a 1.25 em box for baseline alignment. The icon picker is a searchable emoji grid (~350 curated) with category tabs (smileys, gestures, animals, food, activities, travel, objects, symbols, flags) + a "recent" row (16) + "upload image" footer.
- File-type icons: derived from mime (image/pdf/doc/sheet/slides/audio/ video/archive/code/other) — 24 px duotone (soft bg + base icon, from the option palette: pdf=red, sheet=green, slides=orange, doc=blue, image=purple, audio=pink, video=brown, archive=gray).
- No icon-only button without
aria-label(a11y) and without a tooltip (desktop) / visible label (mobile).
5. State styles (per control)
| State | Style |
|---|---|
| default | as specified |
| hover | bg-surface-3 (ghost) / color token -hover (primary) / shadow-sm (cards) |
| focus-visible | 2 px --color-focus-ring outline, 2 px offset (keyboard only — :focus-visible) |
| active/pressed | bg-surface-3 + 1 px translateY (buttons) |
| disabled | opacity-50 pointer-events-none (keeps layout) |
| selected | bg-accent-soft text-accent (list rows) / bg-accent text-white (buttons) |
| error | border-danger + message below + aria-invalid |
| loading | Button: spinner + opacity-60; rows: value kept + pulse overlay; areas: skeletons |
| drag | source: opacity-40; preview: shadow-md rotate-[2deg] scale(1.02); drop target: border-accent bg-accent-soft/40 (line indicator for sibling drops: 2 px accent bar with 8 px end caps) |
6. Block type visual specs (all 28 authoring + 2 structural)
Gutter convention (all blocks): 40 px left gutter (24 px content padding +
16 px handle zone); ⋮⋮ + appear on hover; block hover bg = transparent
except in selection (bg-accent/10 + 2 px left border-accent).
| # | Type | Layout & controls |
|---|---|---|
| 1 | paragraph | text-body; empty placeholder per ux §2.1 |
| 2 | heading_1 | --text-h1, margin-top --space-4; hover: anchor # link (when showAnchor) |
| 3 | heading_2 | --text-h2, margin-top --space-3 |
| 4 | heading_3 | --text-h3, margin-top --space-3 |
| 5 | quote | 3 px left border (--color-border-strong), padding-left --space-3, text-body --color-text-secondary |
| 6 | callout | rounded-lg bg-surface-2 (or properties.background option soft), padding --space-4, icon 20 px left (--space-3 gap), text text-body |
| 7 | bulleted_list | bullet = 6 px dot --color-text-tertiary in a 24 px indent box; line-height 1.5 |
| 8 | numbered_list | number in the indent box (text-tiny tertiary, right-aligned, max 3 digits); numbering from consecutive same-level siblings |
| 9 | todo | 16 px checkbox in the indent box; checked → text --color-text-tertiary + line-through; "All (3/5)" summary row above a todo run |
| 10 | toggle | row: chevron (16 px, rotates 90° on open, --motion-fast) + ⋮⋮ on hover + title text-body medium; children indented --space-5 + 24 px; closed = children hidden (virtualized out) |
| 11 | image | per §3.16 ImageBlock; max-width 100 %; width handle: 8 px circles at mid-left/right edges on hover (drag) |
| 12 | video | 16:9 max, rounded-lg, native controls; upload = file pipeline |
| 13 | audio | 40 px tall player: play (32 px), seek (2 px track), time (text-tiny) |
| 14 | file | FileCard per §3.16 (row layout: icon 24 px + name/size + actions) |
| 15 | bookmark | BookmarkCard per §3.16, max-width 420 px |
| 16 | embed | iframe, rounded-lg border, height from properties.height (default 400 px, resize handle bottom-center); sandbox attributes per functional §4.2 |
| 17 | code | CodeBlock per §3.16; language menu top-right (hover) |
| 18 | equation | KaTeX, text-body-lg, centered; edit on click (inline input replaces render) |
| 19 | column_list | flex row, gap: --space-4; child columns; divider handle between columns (4 px hit → 12 px, cursor col-resize, drag live-resizes, min width 10 %); stacks vertically < 768 px (handles hidden) |
| 20 | column | transparent container; holds blocks; width = column_list.widths[i] |
| 21 | divider | 1 px --color-border, margin --space-4 0 |
| 22 | spacer | invisible; height from properties.height; hover: 8 px handle bar centered (drag = height, snap 8 px) |
| 23 | database | full database render (tabs + view, §7); the block frame is borderless (the view owns its chrome) |
| 24 | linked_database | same as database with a "linked to <db>" chip in the top-right + read-only badge |
| 25 | table | bordered grid (1 px --color-border), header row bg-surface-2 text-small medium; cells 32 px min-height, text-body; add-row/col ghost buttons at the edge on hover; cell = inline text input |
| 26 | chart | ChartRender per §3.16, 280 px default height |
| 27 | form | public-form render (§3.15) inline; draft state shows "Publish to collect responses" |
| 28 | synced_block | renders master content + "Synced" badge (12 px pill, top-right on hover); copies show a link icon in the gutter instead of ⋮⋮ (drag disabled — break-sync via menu) |
| — | child_page | sidebar-style row: icon + title + → on hover; click navigates; used in TOCs/templates |
| — | child_database | same row style + database icon; click opens the record list |
7. Database view visual specs
Shared chrome (all views): tab bar (36 px: tabs text-body medium,
active border-b-2 border-accent; "+" opens the 9-type picker; tab
right-click = rename/duplicate/delete) + toolbar row (32 px: left =
view name (inline edit) + record count (text-tiny tertiary, "128");
right = filter/sort/group indicator chips (click → drawer) + settings gear
(opens the right drawer, ux §4)).
| View | Visual rules |
|---|---|
| Table | row height 48 px (comfortable) / 36 px (compact) from density; column borders 1 px --color-border; title column 240 px sticky; cell padding --space-3; row hover bg-surface-2; selected row bg-accent-soft; sort arrow 12 px in header; "New row" ghost row at bottom (text-tertiary, "+ New row"); bulk bar appears above the table on selection (bg-surface-2 border-b: count + actions) |
| List | row = title (medium) + 2 secondary properties (tertiary, text-small) + 16 px icon; 40 px rows; same filter/sort/group drawer |
| Board | columns 280 px wide, gap --space-4, horizontal scroll; column header 40 px (bg-surface-2 rounded-t-lg: name medium + count badge + ⋯); column body bg-surface-2 rounded-lg padding --space-2; cards stacked 8 px gap; add-card ghost at column bottom; WIP line optional (dashed --color-border-strong at the limit, over-limit cards get a --color-warning count) |
| Calendar | toolbar per ux §4.3; month grid: 7 cols, 5–6 rows, day cell min 96 px; today cell: accent ring 2 px; record chip: 24 px, option-color dot + 1-line title, text-small; week: rows per day (88 px), records as timeline chips; day: list with time-of-day order |
| Gallery | grid auto-fill minmax(220px, 1fr), gap --space-4; card: cover 16:9 (bg-surface-2 + centered icon fallback), padding --space-3, title medium 2-line clamp, ≤ 3 property rows; hover: shadow-md 150 ms |
| Timeline | axis 48 px (month ticks text-tiny); lanes 56 px (bg-surface-2 alternating, lane label sticky left 160 px); bars 32 px rounded-md in the record's status color (option palette), label inside when > 120 px else right; drag ghost = --color-accent-soft bar; today line 2 px accent full-height; zoom toolbar (Day/Week/Month/Quarter) top-right |
| Chart | ChartRender full-width (min 320 px height) + "Configure" ghost button (drawer: type, axes, aggregation, filter, legend) |
| Dashboard | widget grid (12 cols × 32 px rows), gap --space-3; widget = card chrome + header (type icon + title + ⋯: settings/remove) + body render; add widget = dashed placeholder card on empty slot hover; public mode = read-only badge |
| Form | two-pane (builder: question list left 320 px drag-reorder + live preview right) / public render (single column, 480 px max, intro + fields + submit, brand colors from config) |
8. Dark mode rules
- Every surface maps through tokens — a component is dark-mode-correct by construction if it uses only token utilities; the visual regression suite (Playwright screenshots, both themes, per view) catches token leaks.
- Shadows in dark mode use the higher-alpha
--color-shadow(dark surfaces need stronger definition); elevation is also carried bysurface-2/3steps (the dark palette is only 3 surface steps — use them in order, never skip). - Images/media get a 4 % dark scrim on hover-adjacent chrome (covers are displayed as-is; no forced dimming).
- Option colors: the dark bases are lightened (see table §1.2) so pills keep 4.5:1 text contrast on their soft bg in both themes.
- Focus ring:
--color-focus-ringis tuned per theme (slightly lighter in dark); never use the accent directly for focus in dark (4.9:1 → 5.4:1 difference matters at 2 px). - Code blocks: always
--color-surface-2(both themes) — code syntax themes are fixed (one syntax palette that works on both: the--syntax-*token set, 8 colors, contrast-verified onsurface-2in both themes).
9. Reduced motion
- Trigger:
prefers-reduced-motion: reduce(OS) or manual toggle (Settings → Accessibility) →.reduced-motionon<html>. - Effects: all
--motion-*→ 0 ms; drag auto-scroll becomes instant jumps; skeleton shimmer → static; card hover lift → border change only; board card drop → no spring (straight placement); toasts → fade only, 200 ms; no auto-playing anything. - Never disabled: focus indication, error states, content visibility (reduced motion never hides information or interaction).
10. Responsive rules (per component category)
| Category | ≥ 1024 | 768–1023 | 640–767 | < 640 |
|---|---|---|---|---|
| Layout shell | sidebar 240 px + content | sidebar auto-collapses to 56 px rail (hover-expand) | overlay drawer (hamburger) | overlay drawer; top bar search → icon (full-screen) |
| Buttons/controls | standard sizes | standard | 44 px min touch height | 44 px min touch height; icon buttons get 44×44 hit areas |
| Menus/tooltips | hover + right-click | hover + tap (⋯) | long-press / ⋯ per row; menus full-width bottom sheets for confirmation | same; tooltips never (use inline labels) |
| Dialogs | centered 400–720 px | centered | centered, 90 % width | full-screen sheet (slide up, 24 px grab handle) |
| Table view | virtualized both axes | horizontal scroll + sticky title | forced list view | forced list view |
| Board | columns 280 px | columns 260 px, horizontal scroll | cards 80 % viewport width | same + snap scroll per column |
| Calendar | month grid | week default | day-list | day-list |
| Gallery | minmax(220px) | minmax(200px) | 2-up | 1–2 up (minmax(160px)) |
| Timeline | full | horizontal scroll, axis sticky | day zoom default | day list fallback |
| Slash/palette menus | anchored, 260–640 px | anchored | centered modal | full-screen (palette) / bottom sheet (slash) |
| Columns (editor) | side-by-side | ≤ 2-up | stacked | stacked |
| Comments panel | right rail 360 px (page-level) | overlay drawer | full overlay | full-screen |
| Text | body 15 px | 15 px | 15 px | 15 px (never < 14 px) |
11. Accessibility (visual + interaction contract)
- Focus:
:focus-visible2 px--color-focus-ring, 2 px offset, on every focusable (components ship it; the a11y e2e project asserts). - Contrast: token pairs verified by
contrast.test.ts(4.5:1 text, 3:1 UI); option-color pills carry text in the base color on the soft bg (verified pair per hue, both themes). - Non-color signals: every state change that uses color also changes shape or text (error: border + icon + message; status pills: color + name text; selected row: color + left border; drag target: border + line indicator).
- Menus/dialogs: ARIA per ux-requirements §12 (roving tabindex, focus trap, live regions for toasts and selection announcements).
- Touch: 44×44 px minimums below 1024 px, 8 px separation; drag handles enlarged (44 px hit) and the keyboard/context-menu paths are always available (non-drag DnD, ux §10).
- Screen-reader landmarks + skip link (ux §12 global); block editor exposes
the per-block roles (ux §12 table) — implemented in
packages/editor, specified here for the component team. - High-contrast mode (§2) ships as a token override; the a11y suite runs
axe with the
hcclass as a separate pass in v1.1.