Files
crewli/dev-docs/CREWLI-DESIGN-TOKENS.md
bert.hausmans 8330e93fe5 docs(design): add CREWLI-DESIGN-TOKENS.md token inventory (Plan 2.5 Track B)
Per RFC-WS-PRIMEVUE-PLAN-2-5 Track B (§6). Inventories and classifies
the design tokens live in the codebase (Brand-essential / Bespoke /
Generic per RFC §6.2) and records the Typography decision register
(AD-2.5-T1) end-to-end — including the historical Public Sans
removal across both the CSS path (P2, commit 41af1801) and the
webfontloader JS path (P2-followup, commit 641ca513).

Inventory covers:
- Tailwind v4 @theme + :root (font tokens + dark variant selector)
- PrimeVue Aura preset (full 11-step Crewli teal primary palette +
  light/dark colorScheme bindings; everything else inherits Aura)
- PrimeVue runtime config (darkModeSelector='.dark', cssLayer=false,
  empty pt defaults scaffold)
- Iconify (Tabler set, dash-naming convention)
- useShellUiStore runtime writers (.dark class, data-density)
- Workspace gradient palette (8 pairs, deterministic per org id)
- Brand-square recipe (32px / rounded-lg / px-4 / centring equation)
- Density axis (comfortable | compact, axis present but no
  component-level reaction yet — backlog DENSITY-AWARE-SPACING)

Drift items flagged for Plan 4 (no fix in P7 — read-only audit):
- Workspace gradient palette uses Tailwind palette anchors, not
  derivations of Crewli's #0D9394
- User-avatar gradient hardcodes #f472b6 (Tailwind pink) + a
  fallback #0d9488 that's NOT Crewli's #0D9394
- --topbar-h referenced with fallback only, never declared in :root
- 'density' axis attribute set but no component spacing reacts to it

Remaining token decisions (surface tones, focus-ring, radius scale,
font-size scale, spacing rhythm, density-aware spacing, shadow scale,
secondary palette) explicitly deferred to Plan 4 per RFC §6.4.

Read-only audit: zero code files touched (verified via git status).
Foundation document for Plan 4's template-layer token work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 02:19:22 +02:00

33 KiB
Raw Permalink Blame History

Crewli Design Tokens — Audit & Decision Register

Field Value
Source design system crewli-starter / PrimeVue Aura
Aura preset version @primeuix/themes 4.5.x
Tailwind version v4 (CSS-first @theme, no tailwind.config.ts)
Audit date 2026-05-21
Phase-1 decided tokens Typography (AD-2.5-T1)
Plan reference RFC-WS-PRIMEVUE-PLAN-2-5 §6 (Track B)
Related docs PRIMEVUE_COMPONENTS.md, RFC-WS-PRIMEVUE-PLAN-2-5.md

1. Purpose & scope

This document is the inventory and decision register for the design tokens currently live in the Crewli SPA. It is generated as Track B of RFC-WS-PRIMEVUE-PLAN-2-5 and serves as the foundation for Plan 4's template-layer token work.

What this doc does:

  • Inventories every token source (Tailwind v4 @theme, PrimeVue Aura preset, component-level CSS variables, store-managed runtime attributes).
  • Classifies each token as Brand-essential, Bespoke, or Generic per the RFC §6.2 framework.
  • Records the Typography Decision Register (AD-2.5-T1) end-to-end, including the historical Public Sans removal across both the CSS path and the webfontloader JS path.
  • Flags drift, inconsistencies, and follow-up items for Plan 4.

What this doc does NOT do:

  • Make new token decisions beyond Typography (per RFC §6.4, the rest is deferred to Plan 2.5b or absorbed into Plan 4).
  • Refactor, consolidate, or move any token value. This is a read-only audit; the doc records state without changing it.
  • Decide on density-aware component spacing, focus-ring tuning, surface-tone overrides, or radius-scale customization — all explicitly deferred.

If a token value changes after this audit date, update the relevant row + the Audit date field; do not silently let the inventory drift.

2. Classification scheme

Each token is one of (verbatim from RFC §6.2):

  • Brand-essential — token carries Crewli brand identity. Keep current value. Examples: --p-primary-color (teal), gradient definitions, Crewli wordmark colors.
  • Bespoke — token has a non-default value with deliberate design intent. Keep current value with rationale. Examples: the brand-square recipe (32px / rounded-lg / px-4), the density axis attribute, custom topbar height.
  • Generic — token has a non-default value with no documented intent. Default decision: revert to framework default. Override requires explicit rationale.

3. Token sources

Source Type Notes
apps/app/src/assets/styles/tailwind.css Tailwind v4 @theme + :root Single source for Tailwind tokens. Houses --font-sans (@theme), --crewli-font-family (:root), and the @custom-variant dark selector binding.
apps/app/src/plugins/primevue/theme.ts PrimeVue Aura preset (programmatic) definePreset(Aura, {...}) — overrides ONLY the semantic.primary palette + the light/dark colorScheme bindings. Everything else inherits Aura defaults.
apps/app/src/plugins/primevue/index.ts PrimeVue runtime config darkModeSelector: '.dark', cssLayer: false, Dutch locale, empty pt defaults.
apps/app/src/plugins/iconify.ts Iconify runtime Tabler set eager-loaded; icon name convention tabler-<slug> (dash, NOT Iconify standard colon).
apps/app/src/stores/useShellUiStore.ts Pinia store (runtime DOM attribute writer) Writes <html class="dark"> (theme) and <html data-density="..."> (density) via applyDomAttributes().
apps/app/src/utils/v2/gradient.ts Component data The 8-pair gradient palette consumed by the WorkspaceSwitcher avatar (per-org gradient via deterministic hash).
Component scoped CSS (<style scoped> blocks) Per-component Inset box-shadow recipe on the brand-square (RFC §7.4 last-resort — Tailwind has no inset directional utility).
apps/app/src/@core/scss/template/libs/vuetify/_variables.scss Vuetify SCSS Mirrors Inter via $font-family-custom so legacy Vuexy surfaces match the v2 shell during the F3F6 window.

4. Color tokens

4.1 Primary palette (Brand-essential)

Set by CrewliPreset in plugins/primevue/theme.ts. The full 11-step ramp is derived from Crewli teal #0D9394.

Token Value Aura default Classification Decision Note
primary.50 #E6F4F4 emerald-50 Brand-essential Keep Tinted teal scale; replaces Aura emerald.
primary.100 #CCE9EA emerald-100 Brand-essential Keep
primary.200 #99D3D4 emerald-200 Brand-essential Keep
primary.300 #66BDBE emerald-300 Brand-essential Keep
primary.400 #33A7A8 emerald-400 Brand-essential Keep
primary.500 #0D9394 emerald-500 Brand-essential Keep The Crewli teal. Single source of brand identity in the color system.
primary.600 #0B7F80 emerald-600 Brand-essential Keep Mirrors Vuetify's staticPrimaryDarkenColor (lock-stepped during F3F6).
primary.700 #086B6C emerald-700 Brand-essential Keep
primary.800 #055758 emerald-800 Brand-essential Keep
primary.900 #034344 emerald-900 Brand-essential Keep
primary.950 #012F30 emerald-950 Brand-essential Keep

4.2 Color-scheme bindings (Brand-essential)

Token Value Classification Note
light.primary.color {primary.500} Brand-essential Crewli teal as light-mode primary.
light.primary.contrastColor #ffffff Brand-essential White text on teal.
light.primary.hoverColor {primary.600} Brand-essential One step darker on hover.
light.primary.activeColor {primary.700} Brand-essential
dark.primary.color {primary.400} Brand-essential Lighter teal on dark surfaces.
dark.primary.contrastColor {surface.900} Brand-essential Inherits Aura surface scale.
dark.primary.hoverColor {primary.300} Brand-essential
dark.primary.activeColor {primary.200} Brand-essential

4.3 Dark-mode mechanism (Bespoke)

Token Value Classification Note
PrimeVue darkModeSelector .dark Bespoke Class-based, NOT prefers-color-scheme. AD-2.5-D1.
Tailwind @custom-variant dark (&:where(.dark, .dark *)) Bespoke Pins Tailwind dark: variants to the same class. Without this, PrimeVue and Tailwind would divergent (PrimeVue listens to class, Tailwind default listens to OS).
useShellUiStore.applyDomAttributes() write documentElement.classList.toggle('dark', theme==='dark') Bespoke Runtime write — single source of truth for the class.

4.4 Workspace gradient palette (Brand-essential pattern; values are data)

utils/v2/gradient.ts exports GRADIENT_PALETTE, an 8-pair list keyed by deterministic hash of org.id. Each entry is a [from, to] tuple for a 135° linear gradient.

Index From To Tailwind palette anchor
0 #0d9488 #0f766e teal-600 → teal-700
1 #0891b2 #0e7490 cyan-600 → cyan-700
2 #059669 #047857 emerald-600 → emerald-700
3 #10b981 #059669 emerald-500 → emerald-600
4 #0284c7 #0369a1 sky-600 → sky-700
5 #14b8a6 #0d9488 teal-500 → teal-600
6 #06b6d4 #0891b2 cyan-500 → cyan-600
7 #34d399 #10b981 emerald-400 → emerald-500

Classification: the pattern (per-org deterministic gradient) is Brand-essential. The specific palette values are data, not core design tokens — they sit in component code. ⚠ Drift: the palette uses Tailwind palette anchors (teal/cyan/emerald/sky from Tailwind defaults) rather than derivations of Crewli's actual #0D9394. crewli-starter's reference markup uses pairs like #0D9394, #075F60 (Crewli teal-500/-700 in the new ramp). Flagged for Plan 4 — see §10.

4.5 Avatar gradient leftover (drift)

AppTopbar.vue user avatar pt-style:

background: linear-gradient(135deg, #f472b6, var(--p-primary-500, #0d9488));
Aspect Value Note
Hardcoded #f472b6 Tailwind pink-400 Not a Crewli brand color. Vuexy-era leftover; mixed with brand teal in the gradient.
Fallback #0d9488 Tailwind teal-600 NOT the Crewli teal #0D9394. Inert today (the var() resolves), but masks the brand color if the CSS var ever fails.
Classification Generic (drift) Flagged for Plan 4 cleanup; not in P7 scope to change.

5. Typography tokens + Decision Register

5.1 Live state

Token Value Source Classification Decision
Tailwind --font-sans (@theme) "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif apps/app/src/assets/styles/tailwind.css Brand-essential Decided — AD-2.5-T1
:root --crewli-font-family same stack as above apps/app/src/assets/styles/tailwind.css Brand-essential Decided — AD-2.5-T1
html, body { font-family } var(--crewli-font-family) apps/app/src/assets/styles/tailwind.css Brand-essential Decided — AD-2.5-T1
Vuetify $font-family-custom "Inter", ... apps/app/src/@core/scss/template/libs/vuetify/_variables.scss Brand-essential Decided — AD-2.5-T1
Inter weights loaded 400 / 500 / 600 / 700 (via @fontsource/inter) apps/app/src/main.ts Brand-essential Decided — AD-2.5-T1
Inter load mechanism Local NPM package (@fontsource/inter) — no CDN apps/app/src/main.ts Brand-essential Decided — AD-2.5-T1

5.2 Decision Register — AD-2.5-T1 (Inter via @fontsource/inter)

Decision: Inter is Crewli's sans-serif typography across PrimeVue, Tailwind, and Vuetify surfaces. Loaded locally via @fontsource/inter (weights 400 / 500 / 600 / 700). No Google Fonts CDN, no CDN of any kind for the body font.

Status: complete (decided + implemented + regression-locked).

Rationale:

  1. Inter is PrimeVue's de-facto reference font — using it produces the cleanest visual default across the component set.
  2. Local loading avoids external Google Fonts requests, which carry GDPR/privacy implications and add a third-party network dependency to first paint.
  3. Single source — one font, three frameworks all reading from the same --crewli-font-family stack — prevents the dual-path drift that the initial Plan 2.5 P2 audit missed (see §5.3).

History:

Date / commit Event
Pre-Plan 2.5 Codebase loaded Public Sans via two independent paths: (a) CSS font-family declarations in tailwind.css and Vuexy _variables.scss; (b) a JS webfontloader plugin (apps/app/src/plugins/webfontloader.ts) that fetched Public+Sans:...&display=swap from the Google Fonts CDN at runtime. The two paths were independent; neither knew about the other.
Plan 2.5 P2 (commit 41af1801) CSS path reverted to Inter. tailwind.css @theme --font-sans + :root --crewli-font-family + the html, body cascade all changed to the Inter stack. Vuetify $font-family-custom matched. Regression-lock spec tests/unit/styles/typography.spec.ts added — but it only inspected CSS file contents.
P5 manual smoke (2026-05-20) The live DOM still carried wf-publicsans-n4-active wf-active classes on <html>. webfontloader was still fetching Public Sans from Google Fonts. The CSS-only regression-lock had missed the JS path.
P2-followup (commit 641ca513) The whole webfontloader.ts plugin removed (it loaded only Public Sans — no other families). webfontloader@1.6.28 + @types/webfontloader@1.6.38 removed from package.json. Regression-lock strengthened: typography.spec.ts now also scans every file in src/plugins/ for any reference to webfontloader, WebFont.load, Public Sans (any spelling), or fonts.googleapis.com. The plugin's auto-registration via Vuexy's registerPlugins() glob is severed by the file deletion alone — no manual unregister site to clean up.

Regression lock (current state):

Spec Asserts
tests/unit/styles/typography.spec.tsTypography regression lock (AD-2.5-T1) describe tailwind.css declares Inter first in every direct font stack; Vuexy $font-family-custom declares Inter first; no Public Sans (any case/separator) remains in either CSS file.
tests/unit/styles/typography.spec.tsJS font-loading path (AD-2.5-T1 completion) describe No file under src/plugins/ references webfontloader, WebFont.load, Public Sans (any case/separator), or fonts.googleapis.com; the webfontloader.ts plugin file does NOT exist.

If a future PR re-introduces any of these (a Vuexy template port that loads a Google Font, a Storybook addon that lazy-loads a webfontloader path, etc.) the spec catches it before merge.

5.3 Why Typography is the only fully-decided Phase-1 token

Per RFC §6.4: font choice has the largest visual surface impact (every text element in the app), the regression-lock pattern needed an anchor reference, and reverting from Public Sans to Inter was already a clear correctness call. Other tokens (focus-ring width, surface tones, density-aware spacing) are localized and benefit from waiting until more pages exist so visual impact is observable.

6. Spacing / sizing / radius tokens

6.1 The brand-square recipe (Bespoke)

The most consequential bespoke layout invariant in the shell. Encoded across SidebarHeader + WorkspaceSwitcher in P6-styling commits.

Property Value Note
Square size w-8 h-8 32px × 32px. Applies to the SidebarHeader brand mark AND the WorkspaceSwitcher avatar.
Square radius rounded-lg 8px. Set after manual smoke rejected rounded-xl (12px) as too soft.
Wrapper height h-[56px] Matches the topbar height; applied to the SidebarHeader brand row AND the WorkspaceSwitcher wrapper in collapsed state.
Wrapper horizontal padding px-4 16px each side. Constant across both states for both wrappers (collapse no longer toggles justify-center/px-0).
Inset shadow inset 0 -2px 0 rgb(0 0 0 / 8%) Scoped CSS in both components — no Tailwind utility for inset directional shadow at this granularity (RFC §7.4 last-resort).

Centring equation: collapsed rail (64px) = square (32px) + 2 × px-4 (2 × 16px). A left-aligned 32px square inside a px-4 row is also centred when the row is 64px wide. This is what keeps the brand logo stationary through the rail's width animation — no justify-content re-centring during the transition.

6.2 Sidebar rail widths (Bespoke)

Token Value Classification Note
Expanded rail width w-64 (256px) Bespoke Matches crewli-starter's --sidebar-w.
Collapsed rail width w-16 (64px) Bespoke Matches crewli-starter's --sidebar-w-collapsed. Locked to the brand-square recipe (see §6.1).
Width transition transition-[width] duration-200 Bespoke 200ms ease on the aside.
Aside overflow overflow-hidden Bespoke Clips content during the width animation — prevents whitespace-nowrap wordmark overflow.

6.3 Topbar height (Bespoke CSS var with fallback)

Token Value Note
--topbar-h var(--topbar-h, 56px) Consumed in AppTopbar's pt.root class. ⚠ Not declared in :root anywhere — only used with fallback. Effectively a hard-coded 56px with the option of being overridden. Worth either declaring properly or removing the var indirection in Plan 4.

6.4 Other component sizes (Generic — Tailwind defaults)

The shell uses Tailwind defaults for the rest: h-[38px] for icon buttons (search, density, dark, notifications) is a one-off literal; w-7 h-7 for the inline collapse chevron; gap-[10px] between brand row children; etc. None of these carry brand intent — they're sized to match crewli-starter visual reference and otherwise inherit Tailwind's default scale. Classified Generic; Plan 4 may consolidate via a --icon-btn-size token if patterns recur.

6.5 PrimeVue surface tones, focus-ring, border-radius scale (DEFERRED)

Inherited from Aura defaults — no overrides in CrewliPreset. Surface scales ({surface.0}{surface.950}), focus-ring width/style, default radius (var(--p-border-radius)) all come from @primeuix/themes/aura as-shipped. Plan 2.5 made no decisions here; deferred to Plan 4 (see §9).

7. Density tokens (Bespoke)

Token Value Source Note
density enum 'comfortable' | 'compact' (NOT comfy) useShellUiStore.ts Binary. The earlier RFC draft referenced a comfy value — incorrect; the runtime type is comfortable.
<html data-density> written by applyDomAttributes() useShellUiStore.ts Single DOM read for any consumer that wants to react to density.
toggleDensity() action binary flip + sync applyDomAttributes useShellUiStore.ts (P6 Fix 10) Topbar button delegates here.
Component-level reaction none yet No component CSS currently keys off [data-density=compact]. The attribute is set; the component-level spacing-aware styles are deferred (backlog DENSITY-AWARE-SPACING).

Classification: the axis itself is Bespoke (Crewli design intent — toggle between two interface densities). The component-level spacing recipes that respond to it are deferred and become Plan 4 work.

8. Icon tokens (Bespoke convention)

Token Value Note
Icon naming tabler-<slug> Dash-separated. Deviates from Iconify standard tabler:<slug> (colon) because Crewli's Icon.vue bridge expects dashes — a Vuexy-era convention preserved for the v1→v2 bridge.
Iconify collection @iconify-json/tabler Full set eager-loaded at app boot (addCollection in plugins/iconify.ts).
Default size per-call (:size="N") No global icon-size token. Each call sets its own size.

Classification: dash convention is Bespoke (a documented Crewli deviation); the set choice (Tabler) and the eager-load strategy are Bespoke decisions (size trade-off accepted for CSP compatibility — Iconify's runtime CDN is blocked).

9. Classification summary

Bucket Approximate count What this means for Plan 4
Brand-essential ~20 (full primary palette + scheme bindings + Typography stack) Sacred. Plan 4 should not change these without explicit brand-team sign-off. Inter and #0D9394 ARE Crewli.
Bespoke ~10 (dark-mode mechanism, brand-square recipe, rail widths, density axis, icon-dash convention, topbar height) Intentional Crewli deviations from defaults. Plan 4 may refactor / consolidate, but the rationale stays. Each Bespoke row in this doc carries a one-line "why".
Generic ~everything else (full Aura surface scale, default border radius, focus ring, font-size scale, spacing scale, default shadows, default transitions) These are framework defaults used as-is. Plan 4 decides which of these to override with Crewli-specific values. Default decision per RFC §6.2 is "revert to default" unless rationale exists.

10. Drift + follow-up items for Plan 4

These are not P7 decisions; they're flagged here so they don't get lost.

Item Severity Source Recommended action
Workspace gradient palette uses Tailwind palette anchors (teal-500/600, cyan-500/600, emerald, sky) rather than derivations of Crewli's primary #0D9394. Low (visual; data tier) utils/v2/gradient.ts line 14 Re-derive the 8 pairs from the Crewli primary ramp (primary.500/primary.700 plus complementary tints from a Crewli secondary palette, when Plan 4 defines one).
User-avatar gradient hardcodes #f472b6 (Tailwind pink-400) mixed with var(--p-primary-500). Medium (off-brand color baked into chrome) AppTopbar.vue line 385 + 400 Replace pink with a Crewli secondary/accent (Plan 4 decision). At minimum, drop the var fallback #0d9488 which is NOT the Crewli teal (#0D9394).
--topbar-h referenced with fallback only, never declared in :root. Low (correctness) AppTopbar.vue line 225 Either declare :root { --topbar-h: 56px; } in tailwind.css, or replace the var(--topbar-h, 56px) with the literal h-14. The current form is a half-finished abstraction.
density axis is set on <html> but no component spacing reacts to it. Medium (feature incomplete) useShellUiStore.ts + every shell component Plan 4 picks the spacing surfaces that should compress in compact mode (typically row heights in tables, sidebar nav item padding, topbar height). Tracked as DENSITY-AWARE-SPACING.
PrimeVue pt defaults file is empty. None (scaffold, by design) apps/app/src/plugins/primevue/defaults.ts F4 sub-packages populate as each Vuetify surface migrates. Not an immediate Plan 4 concern, but worth knowing this central override site exists.
The --crewli-font-family CSS variable duplicates the Tailwind @theme --font-sans stack. Low (redundancy) tailwind.css Intentional twin — @theme feeds Tailwind utilities; --crewli-font-family is the cascade-level named alias for non-Tailwind consumers (PrimeVue, Vuetify body inheritance). Plan 4 could collapse to a single source if the two-step is no longer needed after Vuetify is removed.

11. Deferred to Plan 4

Per RFC §6.4, Plan 2.5 deliberately stops Phase-1 at Typography. The following are NOT decided in this audit:

Deferred area Why deferred Where it'll land
Surface tone scale (--p-surface-*) Localized impact; needs page surfaces beyond the dashboard to evaluate. Plan 4 (or Plan 2.5b if priority bumps).
Focus-ring width / color Aura default works visually; needs accessibility audit before customization. Plan 4 + accessibility pass.
Border-radius scale (--p-border-radius family) Single-value change cascades through every rounded surface. Wait until more components are migrated. Plan 4.
Font-size scale (root rem base) Touches the typographic system; needs Plan 4's full type-scale design. Plan 4.
Spacing rhythm (--p-spacing-*) Plan 2.5 used Tailwind defaults; Plan 4 may introduce Crewli-specific spacing tokens. Plan 4.
Density-aware component spacing The axis exists (<html data-density>) but nothing reads it. Plan 4 picks the surfaces. Plan 4 (DENSITY-AWARE-SPACING).
Shadow scale Aura defaults; one bespoke inset-shadow exception on the brand square (RFC §7.4 last-resort). Plan 4 if shadow recipes recur.
Secondary / accent color palette Crewli's brand currently has only the teal primary defined. A secondary is needed for callouts (e.g., the off-brand pink in §10). Plan 4 + brand-team input.
Workspace gradient palette re-derivation See §10. Plan 4.

12. Maintenance

When a token decision lands in a future PR:

  1. Find the row in this doc; update the Decision column and the Note column.
  2. Add a new row to §5.2-style "Decision Register" if the token gets its own register entry.
  3. Update the Audit date field at the top.
  4. Add the corresponding regression-lock spec if the decision is enforceable in code (CSS file contents, computed style, store action). Cross-link the spec path here.

When a new token source appears (e.g., a pt default block populates):

  1. Add the source to §3.
  2. Inventory its tokens in the relevant section.
  3. Classify each one.

Drift catches: if a follow-up audit finds a token in the codebase not listed here, that's the doc to fix, not the codebase.