CharDesk Docs

Visual System

Token, recipe, primitive, and product composition pipeline.

HEAD a63778e

The 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.css owns semantic color, surface, elevation, and viewport-layer tokens.
  • packages/ui/src/recipes.ts owns reusable visual and interaction recipes.
  • packages/ui/src owns 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:

  • active identifies the current editor tool or navigation mode.
  • pressed identifies a two-state control and owns aria-pressed.
  • open identifies an expanded panel or transient surface and owns aria-expanded when Radix does not provide it.
  • selected identifies 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:

  • success identifies a completed operation or healthy ongoing state.
  • warning identifies a degraded state that can continue.
  • error identifies a failed operation, invalid state, or lost connection.
  • neutral identifies 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.mjs rejects legacy recipes, raw palette colors, component-local dark: 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.tsx and context-menu.test.tsx

On this page