UI surfaces consume the Determined design language
State: open/active
Summary
Section titled “Summary”- Every live UI surface styles itself through the Determined design language:
--d-*tokens,dSemanticsintents, andD*components — never raw palette classes,dark:variants, or Nuxt UIcolor/variantliterals. - Theming happens through the language’s seams: a theme preset registered via
registerTheme(), a token override outside the cascade layer — never a component fork or a:rootshadow. - Named escape hatches only (see Notes); everything else is a violation.
A surface with a UI (app, product, experience, site, generated report) expresses appearance through the design language:
- Colors and chrome come from
--d-*tokens (directly, or via the--ui-* ← --d-*bridge forU*components). No raw Tailwind palette utilities, no hard-coded hexes, nodark:color variants. - Meaning routes through semantics:
intent/status/tierprops and thedSemanticshelpers, notcolor/variantliterals at call sites. - Dark mode is the
.darkclass owned by color-mode and read throughuseDTheme. Components and experiences never watchprefers-color-schemewhile running inside a shell. - Icons are Lucide via Iconify (
i-lucide-*). - Themes are data: a solution or product that wants its own look
registers a preset (Tier-1 seeds keyed on
data-theme) through the same public API the built-ins use. If the vocabulary cannot express the theme, the vocabulary is extended (spec first) — components are never forked.
The canonical vocabulary is packages/ts/ui/docs/design-system.md; the
laws and their rationale are D-DLNG-design-language; the styling idiom each
layer writes those tokens in — and the rule that code moved between layers is
restyled to the destination’s idiom — is design-system.md §15.
Five apps sharing one framework stack diverged into four palettes, three icon sets, and three dark-mode mechanisms because nothing bound them to the shared layer. The design language exists, is themable, and restyles correctly only when appearance flows through its tokens — every literal is a pixel the theme system cannot reach. One edit retuning the whole suite is the property this rule preserves.
How to apply
Section titled “How to apply”- New surfaces extend
@determined/ui(directly or via a host layer such as@determined/shell) from the first commit. - Existing live surfaces migrate per the adoption plan
(
docs/planning/notes/N-R926-design-language-plan.mdPhase 3): extend the layer, let the bridge restyle stockU*components, replace literals opportunistically. - A consumer never imports
tailwindcssor@nuxt/uiitself, and never lists'@nuxt/ui'inmodules. The layer supplies both; a second import ships Tailwind’s preflight/base layer twice. - Reviewers: a diff that adds
bg-gray-*,text-red-*, adark:color variant, ani-heroicons-*icon, or acolor=/variant=literal in an experience is asking for a token, an intent, or aD*component instead. scripts/guard-raw-palette.tsbackstops review (repo tooling, not the sdlc gate registry; wired into lefthook pre-commit, CI, andjust ci-guards). It asserts five rules over the enrolled surfaces: no raw hex in a<style>block; no bare colour keyword (white,gray, …) in a painted property; no colour in functional notation (rgb(),rgba(),hsl(),oklch(), …); no Tailwind palette class,dark:colour variant or arbitrary value (bg-[#fff]) in markup; and novar(--d-*)thatpackages/ts/ui/app/assets/css/tokens.cssdoes not declare.- Enrolment is the
RINGStable at the top of that script: one entry per surface, a mode per rule, and a required reason on every mode that is notenforce.enforcemeans the count must stay at zero; a numeric baseline is a committed measurement that fails only on GROWTH, so a legacy debt is held flat while it burns down instead of blocking every commit. When a count comes in under its baseline the guard prints the number to tighten it to; tightening is a reviewed edit to that table. - The enrolled set is every surface that consumes the design language,
directly or through
@determined/shell.packages/ts/ui’s token and exploration stylesheets are excluded (a token has to resolve to a real colour somewhere) and so are the frozen surfaces named below; theD*components are enrolled.solutions/determined/apps/*is out because those apps consume neither package yet. - The guard catches the MECHANICAL literal only — one it can see written
down in a
.vue,.css,.tsor.tsxfile it reads (in a.ts/.tsxfile, only inside a string literal — seestringLiteralRegionsin the guard). A colour computed at runtime, an inlinestylebound to a runtime expression rather than a literal, ani-heroicons-*icon and acolor=/variant=literal are all outside it.*.stories.tsand*.test.ts/*.spec.tsare excluded outright — dev-only fixtures, never shipped. Review still carries the rest of this standard. - Code relocated between layers (chrome to
@determined/ui, an experience component down into the frame) is restyled to the destination layer’s idiom in the same change — design-system.md §15.
Anti-examples
Section titled “Anti-examples”class="bg-amber-50 dark:bg-amber-900/10"on a warning banner — that issurfaceClasses('warning')’s job, or a status token.<UButton color="error" variant="solid">at a call site — that is<DButton intent="danger">. The quiet status-color allowance in Notes coverssoft/ghostonly;solidis never a call-site variant.window.matchMedia('(prefers-color-scheme: dark)')inside an experience to pick a canvas theme — that isuseDTheme().resolvedTheme.- A bare
:root { --d-bg-base: #0f1115 }in an app stylesheet — that is a theme preset registered ondata-theme, not a root shadow.
Named escape hatches:
- Frozen/legacy surfaces (
apps/ontological-legacy) are exempt until retired. - The Obsidian plugin (
solutions/agent-pants) inherits the host vault’s theme by design and ships no styling of its own. - Standalone static surfaces with no shell (single-file dashboards,
generated static pages) may use
prefers-color-schemeand a local projection of the seed tokens. - Quiet status-colored
UButtonactions inside experiences: acolornaming one ofsuccess,warning,error, orinfo, combined withvariant="soft"orvariant="ghost", may be written onUButtondirectly. Either prop alone is still a violation. This is the one gap the intent list cannot express; design-system.md §5 states the rule and the emphasis axis that will retire it.variant="ghost"on its own isDButton intent="ghost"(orDIconButton intent="ghost"); a neutralvariant="soft"isDButton intent="neutral".