ADR 0011: Shared workspace appearance
- Status: Accepted
- Date: 2026-10-30
Context
Section titled “Context”A workspace needs a shared visual identity. Users keep their preferred light/dark/system mode. xObject already uses shadcn Base UI Nova and semantic CSS variables; design customization must preserve these components and remain available through API/MCP.
Decision
Section titled “Decision”Store a strictly validated appearance JSONB object and positive appearance_version on sys_workspaces. New workspaces receive the complete current default appearance. Administrators and the workspace’s Designers may call set_workspace_appearance; ordinary workspace users can inspect the saved settings through list_workspaces and the read-only settings page.
Settings contain baseColor, theme, chartColor, optional light primaryColor, dark primaryColorDark, light secondaryColor and dark secondaryColorDark (#RRGGBB), radius, menuColor, menuAccent, font, fontHeading (inherit or a catalog font) and logoIcon (a fixed Lucide catalog icon). Base colors control surfaces, Theme controls accents and Chart color independently controls --chart-1 through --chart-5. Each explicit primary color overrides only its own mode; an empty value uses that mode’s selected Theme. Each secondary override likewise applies only to its own mode. Empty values independently use that mode’s theme. Custom primary colors also control the corresponding focus/sidebar rings. The preview checks custom primary link contrast against background/card/popover surfaces (4.5:1) and full-color focus borders against those surfaces and the sidebar (3:1), using browser-resolved sRGB colors. An inline advisory identifies insufficient contrast without changing or blocking the chosen color; it is not a whole-interface accessibility certification. Menu surfaces use the shadcn choices default, inverted, default-translucent, inverted-translucent; accents are subtle or bold. Inverted menus use dark surfaces (as shadcn’s dark-class transformation does), even on a light page. Translucent surfaces use a 70% fill and backdrop blur/saturation; bold accents use primary/primary-foreground on menu items. These overrides are scoped to menu surfaces rather than changing general page accents. Heading fonts apply to semantic HTML headings and the standard shadcn heading token, including dialog and card titles. The catalog is pinned to shadcn 4.23.0: eight base palettes, 25 Theme/chart palette choices, five radius choices and its 26 font choices plus the approved locally served Orbitron display font. default and medium currently use the same 0.625rem radius. These are theme tokens, not switches between Nova/Vega component source styles. The fixed Lucide logo-icon catalog is saved in the same versioned appearance object and shown for the active workspace and selection rows. A disabled own-logo upload action is preparation only; no file input, image URL or upload endpoint is enabled. No custom fonts, remote font services, CSS strings or arbitrary URLs are accepted.
Registry HSL channel triplets are stored as complete hsl(...) CSS expressions, so all base/theme choices remain valid in the token consumer.
The web client uses a snapshot of shadcn’s paired light/dark registry palettes. Tonal Theme/chart entries come from the base-color registry snapshots; chromatic entries come from isolated CLI apply --only theme runs using Nova/Neutral, because the current shadcn init endpoint rejects tonal Theme/chart combinations even though they are in the preset catalog. The Gray registry entry omits all sidebar roles. Its pinned snapshot binds sidebar surfaces to its own primary-foreground, foreground, accent, border and ring roles, with its own primary pair for sidebar primary actions. Every base owns the same complete token set; no surface inherits the global Neutral palette or a previous workspace. This is a shared base-palette correction, not a brand-specific override. Portal menus explicitly apply the scoped interface font and color scheme, including unsaved preview drafts. The preview uses the application’s Sidebar, Card, Table, Button, Input, Checkbox and menu primitives; navigation/table selection are isolated local examples. The live app never calls the registry. The current schema contains the complete appearance defaults. Font names, families and dependencies come from @shadcn/font-* registry items. Packages are served locally; alternate font stylesheets load on demand, and browsers request matching subsets. Font licenses ship at /licenses/fonts.txt. Catalog updates require reviewing the contracts, palette/font metadata and dependencies together. They do not happen from a remote registry at runtime.
Apply the current workspace’s tokens at the document root so portalled dialogs and menus inherit them. Observe personal dark-mode changes and reapply the corresponding palette. Cleanup restores prior root tokens when switching workspace or signing out. Font failures retain system fallbacks. The personal theme controls remain unchanged.
The settings page keeps a draft and shows scoped light/dark samples using the actual standard Sidebar, Card, Table, Checkbox, Button and Input components, semantic headings and five chart-color swatches. Real DropdownMenu and Select examples retain local selection state and include disabled entries. A React context carries the chosen workspace/draft design and preview mode into portals; popup-local CSS variables cover DropdownMenu (including submenus), Select and Command surfaces, while native OS selectors remain native. Caller style overrides and component state styles remain composable. Light previews are isolated from the surrounding personal dark mode. It commits only on Save and retains existing unsaved-change guards. Custom color text uses the higher-contrast black or white foreground, guaranteeing at least 4.5:1 on the solid fill. This calculation does not establish a full accessibility audit of hover, disabled, focus or surrounding surfaces; use the scoped focus tokens and test relevant combinations.
set_workspace_appearance requires the current version. A conflicting write returns 409 instead of overwriting someone else’s settings. Refresh the workspace query after conflicts; Reset can load the refreshed design without silently replacing a dirty draft. Update the JSON and version and journal each changed property under appearance.<property> using one change ID in the common transaction. A journal error rolls everything back. No-op writes produce neither events nor version increments.
Consequences and verification
Section titled “Consequences and verification”There is one saved design per workspace and no personal color/font override. Existing clients adopt saved changes on workspace refetch (including window focus); realtime push synchronization is not implemented. Light/dark mode is still local to the browser.
Tests cover ordinary-user denial, Designer workspace boundaries, API/MCP read/write parity, all font IDs, invalid inputs, no-op/stale writes, concurrent updates, transactional journal rollback, initialization retention and solid-fill contrast. Web behavior tests also verify that surfaces, accents, chart colors, heading fonts and manual overrides remain independent. Menu tests cover all surface/accent combinations in both modes, isolation from page tokens, initialization retention and journaled API/MCP persistence. Build/typecheck and focused browser checks verify client composition; record any incomplete browser acceptance separately.