V1 Web PWA · Surface walkthrough

Shell: Design system and global tokens

A per-surface walkthrough of the V1 Web PWA: layout, states, interactions, data, and cross-references.

unspecified
11sections5 minread

On this page

Source: apps/oshun/web/src/design-system/lilith/tokens.ts, apps/oshun/web/src/design-system/lilith/lilith.css, apps/oshun/web/src/design-system/lilith/primitives.tsx, apps/oshun/web/src/design-system/lilith/pills.tsx, apps/oshun/web/src/design-system/lilith/shells.tsx, apps/oshun/web/src/design-system/lilith/edge-states.tsx, apps/oshun/web/src/design-system/lilith/index.ts, apps/oshun/web/src/design-system/lilith/LilithThemeBootstrap.tsx

Lilith is the design system — one paper-warm palette, one terracotta accent, three theme variants, two registers (manuscript for customer surfaces, plate-of-record for operator surfaces). The token contract is the load-bearing piece: literal L.* constants are locked at compile time, LV.* CSS-variable references react to the active theme. Walk this whenever a token is added, a theme variant changes, or a primitive grows new states.

The L object (compile-time) and LV (CSS vars)#

From tokens.ts. L is literal hex (locked to cream-paper). LV exposes the same keys as var(--l-…) references. Most files import LV as L from @/design-system/lilith so theme swaps take effect automatically.

  • L.bg #f1ebdd, L.paper #f7f1e2, L.paper2 #ede4cf (surfaces)
  • L.ink #1f1a14, L.ink2 #3b3325, L.soft #6b5d49 (foreground)
  • L.muted = var(--l-muted) — always a CSS-var, even on literal L, so contrast stays valid across variants
  • L.rule #d9cfb7, L.rule2 #e6dec6, L.grid #dfd4b4, L.chip #e8dfc7
  • L.accent #9a3e1c (terracotta), L.accent2 #c97247
  • L.ok / warn / alert = var(--l-…) (theme-aware status)
  • L.paperDark #221d14 — no CSS-var counterpart; LV.paperDark falls back to the literal
  • Every LV key resolves to a --l-* custom property in lilith.css

Theme variants#

L_VARIANTS in tokens.ts + CSS overrides in lilith.css:

  • cream — base register. Cream paper (#f1ebdd), terracotta accent (#9a3e1c), ink (#1f1a14). Default when no data-lilith-theme attribute.
  • cream-hc — high-contrast accessibility (≥7:1). Selector [data-lilith-theme='cream-hc']. Status colors lifted, ink to true black.
  • dusk — evening reading register (warm low-light, not a dark theme). Selector [data-lilith-theme='dusk']. color-scheme: dark, paper goes to #2c2418, accent lifts to #e58a5e, domain text accents and KG entity hues re-tuned for ≥4.5:1 against the dark paper.

LilithThemeBootstrap#

  • First visit — reads prefers-contrast: morecream-hc; else prefers-color-scheme: darkdusk; else cream
  • Stored choice at localStorage['oshun-lilith-theme'] wins over system preference
  • System changes apply only when no user choice stored
  • Cross-tab syncstorage event + custom oshun-lilith-theme-changed event re-apply variant
  • Attribute placementdocument.documentElement gets data-lilith-theme="cream-hc" | "dusk" (cream removes the attribute)

Font stacks#

From tokens.ts:

  • SERIF_L = "Cormorant Garamond", "EB Garamond", Georgia, serif
  • SANS_L = "Inter", system-ui, sans-serif
  • MONO_L = "JetBrains Mono", "IBM Plex Mono", ui-monospace, monospace
  • fonts alias = { serif, sans, mono }
  • type scale — display (40/56/72/96), headline (22/28/36/56), body (14/16/18), ui (9/10/11/12/14), mono (9/10/11/12)

Primitives (primitives.tsx)#

Inline-style components — token values come from LV as L, so theme-aware.

  • LEyebrow, LRule / LRule2 / LDotRule / LDoubleRule / LThickRule (hairlines), LColumnRule
  • LChip, LCode, LField (key/value), LSourceItem (citation row)
  • LDisplay, LDropCap, LOpenQuote, LMasthead, LPullQuote
  • LDomainMark — SVG glyph for tara | arete | veritas | nyx | nisaba | lilith | metis
  • LConfidence — five-dot rail (strong | moderate | mixed | contested | evolving)
  • LDots — segmented progress
  • LBtnprimary | accent | ghost | quiet × sm | md | lg. With no onClick and no href renders as placeholder: dimmed, aria-disabled="true", data-placeholder="true", tooltip "No handler wired"
  • LCard — paper card with optional left accent stripe

Pills (pills.tsx)#

  • LSeverityPill S3 | S2 | S1
  • LStatusPill ok | degraded | down | paused
  • LDecisionChip pending | approved | request | rejected
  • LValidationPill ok | warn | err | info
  • LAIDisclosure — persistent ✻ mark for AI-assisted surfaces
  • LSyncDot live | offline | queued

Shells (shells.tsx)#

  • LWebShell — locks the cream-paper register via LIGHT_LOCKED_VARS regardless of active Lilith theme (keeps broadsheet prose readable when user is in dusk)
  • LCustomerNav — primary nav (Today / Explore / Activity / Library), active accent underline, ⌘K search affordance, Ask Lilith, avatar
  • LSubNav — sub-nav row with active-item terracotta underline
  • LSectionHead — section heading with kicker + title + right slot
  • LPhoneShell / LStatusBar / LHomeIndicator / LTabBar — phone-mockup chrome (design comps + onboarding)
  • LGraphPaper, LOperatorBar, LDataCell, LDataRow — operator (plate-of-record) chrome
  • README references LMobileNav, LSidebar, LTopBar — not exported from shells.tsx today (see Open questions)

Edge states (edge-states.tsx)#

The calm-under-stress register — no shimmer, no alarming red, no spinners. Collectively the contract for empty / loading / drifted / completed states across the customer surface; per-view files reference these as LEdgeState.

  • LSkeleton (dotted-rule lines), LEmpty (kicker + serif italic title + body + single action), LPwaUpdate ("A new edition." ribbon)
  • LError — stamped notice (Notice · E.04), serif title, action, logged at timestamp
  • LOffline — full-bleed banner with LSyncDot offline
  • LPartialFailurePartial · 3 of 4 loaded + retry + missing list

Token contract — accessibility#

  • Focus ring:focus-visible two-stroke box-shadow (paper inset + ink outer) at border-radius: 4
  • prefers-reduced-motion: reduce--l-motion-* collapse to 0ms, all transitions/animations forced to 0.001ms
  • color-scheme:root light; [data-lilith-theme='dusk'] dark
  • Forced-colors — no explicit @media (forced-colors: active) block; cream-hc is the reliance point for users on prefers-contrast: more
  • WCAG ratioslilith.css comments annotate ≥4.5:1 (cream + dusk), ≥7:1 (cream-hc)
  • ::selection terracotta on paper; customer scrollbars scoped via [data-shell-kind='customer']; native dialog picks up Lilith chrome (paper bg, ink border, sans font, soft shadow)

When NOT to use Lilith primitives#

  • Studio / operator surfaces (/lilith-studio/*, admin inbox, the Bellona / Yemaya / Hathor workspaces) — workspace-scoped Studio* components under components/studio/ layer on top of LGraphPaper / LOperatorBar / LDataCell / LDataRow
  • Domain content kernels with authored visual fidelity (Veritas claim cards, Atelier bake/mix/cast) — consume tokens but not primitives
  • Marketing / public routes — minimal header variant bypasses LCustomerNav / LSubNav; tokens still in play

Cross-references#

Open questions / known gaps#

  • README references LMobileNav, LSidebar, LTopBar but only LCustomerNav is exported from shells.tsx — confirm whether the other three are coming online or whether the README is aspirational
  • No explicit @media (forced-colors: active) styles — verify the Windows high-contrast walkthrough in 06-keyboard-a11y.md is acceptable, or add a forced-colors block to lilith.css
  • --l-paper-rgb / --l-ink-rgb are referenced from overlay components for translucent surfaces — confirm every overlay reads RGB tuples (not hard-coded rgba(247,241,226,…)) so dusk doesn't break opacity stacks
  • LBtn placeholder treatment (data-placeholder="true" when no handler) is documented but not enforced by a lint — consider a codemod sweep to catch unwired buttons before they ship