Visual System
Token, recipe, primitive, and product composition pipeline.
HEAD a63778eThe visual ownership path has one direction:
@chardesk/ui theme -> shared rx recipe -> shared UI primitive -> product composition
UiProvider is the Host runtime boundary. It owns localized primitive messages
and light/dark/system theme state; the shipped default is light. Host code uses
useUiTheme and never imports next-themes. Sonner, Radix, and Base UI are
package internals. Domain-facing feedback keeps its application adapter but
publishes through the package's stable notify API.
packages/ui/theme.cssowns semantic color, surface, elevation, and viewport-layer tokens.packages/ui/src/recipes.tsowns reusable visual and interaction recipes.packages/ui/srcowns every domain-neutral primitive and composition. Products consume the package root; they do not maintain an application-local primitive layer.
Canvas DOM and Canvas-rendered interaction affordances share the same Host
contract. readUiRuntimeTheme is the only bridge from CSS semantics to
imperative drawing values. CanvasAppearanceBridge publishes one typed theme
snapshot and revision to the main renderer, Minimap, slide preview, and imperative
commands so theme changes invalidate every affected layer together. Selection,
cursor, handles, picker, viewport chrome, and preview defaults belong to this
bridge. Document cell colors, ANSI palettes,
and CharGraph render themes remain artifact-owned. Freeform Canvas projects
those colors through the Host-resolved CanvasArtifactPalette; the canonical
#000000 foreground follows the Host only on cells without an explicit
background. Foregrounds paired with a background and all other colors remain
literal. Main Canvas, CellFrame previews, Minimap, and PNG export consume that
same runtime snapshot. Slide pages retain their white-paper palette. Theme state
does not enter document, history, session, or collaboration data.
Display owns paired Light and Dark content palettes shared by CharGraph and theme-aware Canvas templates. Paste and template Drop capture the resolved Host theme once, then materialize ordinary literal Cell colors. Sidebar and drag previews follow the active content palette, including its user overrides; switching Host theme never recolors existing Freeform content. Display settings edit the overrides for the currently resolved Host theme, while renderer feature toggles remain shared. Profiles saved by the former Light-only renderer migrate their color overrides to Light; Dark starts from its built-in palette. Template tokens are catalog metadata and never enter document or collaboration data.
Freeform and Slide use one terminal Cell cursor for both Navigate and Text Edit. The global Host preference selects block, bar, or underline and independently enables the 600ms edit blink; it is local UI state, not document or history state. Navigate is steady. Text Edit blinks only while its Canvas owns input focus, becoming steady on focus loss. A Range hides the cursor, and inactive split views draw neither cursor nor Range. One Canvas Cell presentation resolver owns this precedence and emits one terminal-cursor or Range visual intent. Selecting and resting Ranges use a surface only; moving Ranges add the selection border. Dark Range surfaces use low-strength contrast compositing over final pixels so literal Cell colors remain readable without introducing a resting border. Canvas and Cell UI adapt their own theme values to the shared rendering contracts.
Static Grid separates command targets from interaction semantics. The Selection
domain publishes a target plus one navigate, text-edit, or range
interaction. A default one-Cell target remains navigate; only an explicit
GridSelectionState.mode === "range" publishes Range geometry. Renderers must
consume this discriminant and never infer a Range from non-empty target areas,
geometry, or Cell count.
Third-party UI integrations live behind product adapters. The onboarding adapter is the sole Driver.js owner and maps shared overlay, motion, radius, and reduced-motion semantics into Driver configuration. Product onboarding code owns only steps, copy, workflow state, and lifecycle callbacks.
shadow-host, shadow-overlay, and shadow-dialog are the elevation ladder.
surface, control, and item are the radius ladder. Surface and dialog
shells use 12px; controls, fields, menu items, and selectable items use 8px;
color swatches remain circular.
ANSI swatches keep their values and order across appearances. Curated presets
are owned by @chardesk/ui and resolve to separate Light and Dark literal
palettes before product widgets consume them. Picking a swatch always
materializes its hex value; changing Host theme never recolors existing content.
Page backgrounds, embedded regions, floating hosts, overlays, and dialogs form
one neutral surface ladder. Embedded regions use tone without a contour;
floating, overlay, and modal surfaces use the shared low-contrast ring and
their corresponding elevation token.
Viewport surfaces use semantic layer tokens; product instances do not assign raw
overlay tiers or recreate shared surfaces. Presentation, presence, field, and
swatch colors also have semantic tokens instead of component-local theme branches.
Portal primitives inherit their layer from the nearest modal surface. Ordinary
portals stay below modal surfaces; portals declared inside Dialog, AlertDialog,
or Sheet render above that modal and below toast feedback.
Surface
is the product composition boundary. Its embedded, floating, overlay, and
transparent kinds map to the shared surface recipe. Toolbars, Inspector,
Minimap, and the desktop Sidebar compose Surface; transient portal primitives
consume the same overlay recipe internally.
FloatingSurface
provides two product compositions. control-bar contains 32px controls with
4px shell padding and a 4px gap. panel clips its shell and uses
SurfaceContent for compact 10px or default 16px padding; full-bleed media omits
the content wrapper. Deck is the panel spacing reference, while Zoom and Dock
are the control-bar density reference.
PageShell owns the clipped viewport surface and page gutter. PageFrame owns
the centered frame side borders and optional contained or viewport-bleeding
structural boundaries. CharGraph and Docs share the page compositions; each
product owns only its frame width, content, and responsive layout. Docs maps
Fumadocs colors through its shadcn adapter and keeps navigation, search,
sidebar, and table-of-contents behavior in Fumadocs.
Shared compositions own repeated shell geometry and content density. Widget
className owns product-specific position, size, scrolling, responsive
visibility, and typography. Shared primitives and recipes own background,
border, radius, elevation, focus, hover, active state, and default icon sizing.
A widget must express those decisions through typed primitive props and must not
import @chardesk/ui/styles or its compatibility recipe path directly.
border is the low-contrast contour for controls and explicit component
boundaries. Floating contours use surface-ring so they do not change layout.
The separator token owns structural-divider color, while the
primitive or recipe owns geometry. Static separators, including menus and the
presentation toolbar, are 2px and rounded. Resizable pane boundaries are 2px
and straight; table and data-grid row boundaries remain 1px and straight.
Interaction may strengthen a separator transiently but does not replace its
resting token.
StripeDivider owns the 32px diagonal section pattern and its two structural
boundaries. Its optional bleed extent applies to the pattern and both
boundaries as one contract; products own only placement, frame width, and side
borders. A bleeding divider must live inside a horizontally clipped viewport
surface.
Keyboard focus uses one shared recipe: a 2px inset ring at 45% opacity with
no offset. Controls, fields, scroll viewports, resize handles, and reorderable
items consume that recipe; appearance variants do not redefine its geometry.
Invalid controls may replace the ring color with invalid-ring. Focus,
selection, active state, and validation remain independent semantics, and
product code does not define local ring width, opacity, placement, or offset.
Interaction state has stable vocabulary:
activeidentifies the current editor tool or navigation mode.pressedidentifies a two-state control and ownsaria-pressed.openidentifies an expanded panel or transient surface and ownsaria-expandedwhen Radix does not provide it.selectedidentifies an item in a collection.data-state="highlighted|checked|on"remains owned by the relevant Radix primitive.
Operation and health status form a separate visual axis:
successidentifies a completed operation or healthy ongoing state.warningidentifies a degraded state that can continue.erroridentifies a failed operation, invalid state, or lost connection.neutralidentifies idle, loading, waiting, and ordinary informational state.
Status colors affect result icons and status text, not interaction surfaces.
Controls express transient outcomes through feedback; persistent labels and
indicators use StatusText and StatusDot. active never means success, and
destructive describes action intent rather than an operation result. Color is
always paired with an icon, text, or accessible live-region message.
Persistent state strength is active > pressed > open > hover. Each level has
its own light and dark semantic surface token. subordinate keeps an associated
control quiet at rest while its recipe restores full opacity for hover and every
persistent state. Hover, focus, and Radix highlight never replace a persistent
surface or its text color. Product code does not implement that behavior with
opacity utilities.
Button, IconButton, SelectableItem, SwatchButton, TabsTrigger, and the
menu family translate those states into shared recipes. SelectableItem is a
fixed compact row (12px text, 16px line height, 28px minimum height); Widgets
may adjust its layout but do not override its typography. Other controls expose
only their documented size or density options. Default 32px controls and fields
use 13px text; compact 28px controls use 12px. Segmented controls use joined;
option sets use ToggleGroup. Presentation playback is the only themed
override and marks that boundary with data-visual-contract="presentation".
Enabled activatable controls use the pointer cursor; disabled controls use the default cursor. Canvas editing, resizing, color picking, and reordering retain their task-specific text, resize, crosshair, move, and grab cursors. Shared primitives own control cursors; a Widget only declares a cursor for an intentional raw interaction surface such as the Slide playback canvas.
Dropdown primitives own the overlay surface, collision behavior, animation,
viewport limits, and a compact minimum width. The product instance owns only
its semantic width. Popovers expose compact (10px) and default (16px)
content density; editor chrome defaults to compact. Dropdown, Context Menu, and Select items share the same
compact/default density, radius, highlight, checked, disabled, destructive,
and separator recipes. Dropdowns are reserved for command choices and grouped
options; persistent lists, forms, status, and multi-action widgets use a
Popover with ordinary buttons and dialog focus behavior.
Motion uses three semantic durations: fast 120ms for control feedback, standard 180ms for floating surfaces and Sidebar transitions, and slow 240ms for modal surfaces. Ordinary controls transition color, background, opacity, and shadow without scaling or changing geometry. Transient Dropdown, Context Menu, Popover, and Select surfaces fade with at most a 4px directional shift; modal surfaces retain their separate slow zoom transition. Components transition only the properties they own and disable non-essential motion under reduced-motion preferences.
Evidence
scripts/quality/check-style-api.mjsrejects legacy recipes, raw palette colors, component-localdark:branches,space-x/y, raw viewport tiers, raw Widget buttons, Widget-owned interaction states, direct icon sizing, square-control geometry overrides, and Widget-owned shared surfaces. It also enforces the UI-package dependency facade, the sole Driver.js adapter, runtime-theme token declarations, and the exact native file-input and Canvas-textarea escape hatches.- Surface evidence:
dialog.test.tsxandcontext-menu.test.tsx