Skip to content

UI surfaces consume the Determined design language

State: open/active

  • Every live UI surface styles itself through the Determined design language: --d-* tokens, dSemantics intents, and D* components — never raw palette classes, dark: variants, or Nuxt UI color/variant literals.
  • 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 :root shadow.
  • 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 for U* components). No raw Tailwind palette utilities, no hard-coded hexes, no dark: color variants.
  • Meaning routes through semantics: intent/status/tier props and the dSemantics helpers, not color/variant literals at call sites.
  • Dark mode is the .dark class owned by color-mode and read through useDTheme. Components and experiences never watch prefers-color-scheme while 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.

  • 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.md Phase 3): extend the layer, let the bridge restyle stock U* components, replace literals opportunistically.
  • A consumer never imports tailwindcss or @nuxt/ui itself, and never lists '@nuxt/ui' in modules. The layer supplies both; a second import ships Tailwind’s preflight/base layer twice.
  • Reviewers: a diff that adds bg-gray-*, text-red-*, a dark: color variant, an i-heroicons-* icon, or a color=/variant= literal in an experience is asking for a token, an intent, or a D* component instead.
  • scripts/guard-raw-palette.ts backstops review (repo tooling, not the sdlc gate registry; wired into lefthook pre-commit, CI, and just 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 no var(--d-*) that packages/ts/ui/app/assets/css/tokens.css does not declare.
  • Enrolment is the RINGS table at the top of that script: one entry per surface, a mode per rule, and a required reason on every mode that is not enforce. enforce means 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; the D* 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, .ts or .tsx file it reads (in a .ts/.tsx file, only inside a string literal — see stringLiteralRegions in the guard). A colour computed at runtime, an inline style bound to a runtime expression rather than a literal, an i-heroicons-* icon and a color=/variant= literal are all outside it. *.stories.ts and *.test.ts/*.spec.ts are 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.
  • class="bg-amber-50 dark:bg-amber-900/10" on a warning banner — that is surfaceClasses('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 covers soft/ghost only; solid is never a call-site variant.
  • window.matchMedia('(prefers-color-scheme: dark)') inside an experience to pick a canvas theme — that is useDTheme().resolvedTheme.
  • A bare :root { --d-bg-base: #0f1115 } in an app stylesheet — that is a theme preset registered on data-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-scheme and a local projection of the seed tokens.
  • Quiet status-colored UButton actions inside experiences: a color naming one of success, warning, error, or info, combined with variant="soft" or variant="ghost", may be written on UButton directly. 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 is DButton intent="ghost" (or DIconButton intent="ghost"); a neutral variant="soft" is DButton intent="neutral".

← Back to Standards