Skip to content

Add @vsms/ui: daisyUI theming over Radix behaviour, and the eleven-state status system (T6) - #128

Merged
stephane-segning merged 1 commit into
mainfrom
claude/dashboard-t6-ui
Aug 8, 2026
Merged

Add @vsms/ui: daisyUI theming over Radix behaviour, and the eleven-state status system (T6)#128
stephane-segning merged 1 commit into
mainfrom
claude/dashboard-t6-ui

Conversation

@stephane-segning

Copy link
Copy Markdown
Contributor

Summary

Builds packages/ui (@vsms/ui) per T6 of the dashboard architecture plan: a daisyUI v5-themed, Radix-behaviour component library with the eleven-state message status system as its centerpiece, plus a component gallery replacing admin/app/page.tsx's placeholder.

Intent

Source of truth: dashboard-architecture-plan.md §2/§3 and its DECISIONS section (owner, 2026-08-08) — specifically §4 ("shadcn/ui is OUT... daisyUI + Radix headless for behaviour") and §5 (design-system overrides: dark default, 2px radius, eleven states, delivered gets a green pill) — and dashboard-design-system.md in full, especially §3 (tokens), §4 (the status system), §5 (component inventory).

This branch is layered on origin/claude/dashboard-t2-t5, not main — the Next.js scaffold (admin/) and @vsms/env are T6's prerequisite and are not yet merged (tracked in #127). Until #127 merges, this PR's diff includes T2/T5's files too; only the files listed under Scope below are new/T6-owned.

Scope

New:

  • packages/ui/ — the full @vsms/ui package (see below for inventory)

Modified (all required to make T6 actually buildable/verifiable, not just plumbing added around it):

  • admin/app/globals.css — imports packages/ui/src/styles/theme.css, keeps the load-bearing @source directive
  • admin/next.config.ts@vsms/ui added to transpilePackages
  • admin/app/page.tsx — replaced the scaffold placeholder with an honest component gallery
  • admin/package.json, admin/postcss.config.mjs (new) — @tailwindcss/postcss was never wired into the T2/T5 scaffold, so Tailwind was never actually processed by the build; this is a pre-existing gap, patched because it blocks T6's own verification
  • biome.json, root package.json, packages/env/src/index.ts, admin/app/layout.tsx, admin/tsconfig.json@biomejs/biome was configured (biome.json committed) but never added as a dependency or run, so the existing scaffold was already out of step with its own lint config (single vs. double quotes, a stale schema version, a files.ignores key biome 2.5.7 removed). Fixed so pnpm biome check . is clean, as required by this task's own verification step.

Verification

  • pnpm install — clean, no ERR_PNPM_IGNORED_BUILDS (the allowBuilds: { sharp: true } config is intact).
  • pnpm --filter admin run build — succeeds.
  • pnpm --filter admin run devvisually inspected in the Browser pane, both themes, via a real running server (not just a build). See Screenshots below.
  • pnpm biome check . — clean.
  • cargo check --workspace — passes, Rust workspace undisturbed (sms-encoding, sms-provider, sms-api, sms-worker, sms-gateway, etc. all still compile).

Screenshots/Evidence

Full-page gallery renders, both themes (captured from a live next dev server in the sandboxed Browser pane):

Dark (default): all eleven status pills rendering with distinct silhouette + interior mark + hue (quiet: accepted/queued/routed/submitted/delivered/cancelled; loud: uncertain/undelivered/failed/expired/rejected), buttons, form controls, table with a LiveRow wash demo, tabs, dialog/dropdown/tooltip/popover/drawer/command-menu/toast triggers, payload inspector with real accordion+tabs, the state timeline (the epic-gate component) with its uncertain annotation node, and the encoding preview flagging ç and offering transliteration.

Light (peer, toggled client-side): same gallery, full contrast and layout verified — cards separated by border not fill (per the design doc's own light-theme intent), status hues re-tuned for AA on white.

Accessibility gate (§4.6): the gallery has a live "render at grayscale(1)" checkbox over the status-pill section — checked it and confirmed all eleven states stay distinguishable by silhouette + interior mark alone, no hue.

One real bug caught by this process, not by reading code: Button's primary variant was wired to daisyUI's btn-neutral class on the assumption that neutral resolved to the same achromatic-inverse-fill value as primary in this theme. It doesn't — neutral is a separate, deliberately quieter surface-3-ish token used for badges — and it read as a washed-out primary button in light theme specifically. Fixed to btn-primary, the actual inverse-fill token. Caught by looking at the rendered light theme, exactly the failure mode pnpm build cannot see.

Risk Assessment

Low, additive-only: a new leaf package with zero internal dependencies (verified: no @vsms/api, @vsms/env, or @vsms/gateway imports anywhere in packages/ui), plus the minimum plumbing in admin/ needed to consume it and to make the existing scaffold's own lint config actually enforceable. No Rust changes. The two "pre-existing gap" fixes (@tailwindcss/postcss, @biomejs/biome) are config-only and were verified against a real build/lint run each, not assumed.

AI Usage Declaration

Authored by a Claude Sonnet 5 agent session, working directly (no sub-delegation) against the two design documents named above plus a live read of ~/dev/cratestack/examples/react-nextjs-daisyui/ for the daisyUI v5 CSS-first theming form and the postcss.config.mjs wiring it uses.

  • I read both design documents in full (not skimmed) before writing any component.
  • I fetched daisyUI v5's actual custom-theme documentation (via Context7) rather than guessing the @plugin "daisyui/theme" syntax from memory — training data skews toward daisyUI v4's data-theme JS-config form, which is a different mechanism.
  • I ran pnpm build, pnpm biome check ., and cargo check --workspace myself and pasted real output, not assumed-passing output.
  • I opened a real browser against a real next dev server and looked at the rendered page, in both themes, before writing "confirmed" anywhere — including catching and fixing the btn-neutral/btn-primary bug above, which no static check would have caught.
  • Two components (EncodingPreview, LiveRow) are deliberately lighter than the design doc's full spec, per the design doc's own allowance ("can be stubs with correct props if the data shape isn't settled") — documented as such in code comments and below, not silently under-built.
  • I take responsibility for the design judgement calls below that the source documents left open.

Reviewer Focus

What I took from ~/dev/cratestack/examples/react-nextjs-daisyui/: the exact daisyUI v5 CSS-first theming form (@import "tailwindcss"; @plugin "daisyui"; ...), that @tailwindcss/postcss needs an explicit postcss.config.mjs (absent from the T2/T5 scaffold — see Scope above), and the data-theme + bg-base-*/text-base-content styling convention on <html>/<body>. I did not reuse its theme values (it ships daisyUI's stock emerald/dark themes; this console needs the design doc's own bespoke token set) or its wasm/napi/PWA machinery, which is unrelated to T6.

The exact daisyUI 5 theming form used: two @plugin "daisyui/theme" { name: "dark"|"light"; ... } blocks in packages/ui/src/styles/theme.css, reusing daisyUI's own built-in theme names (so admin/app/layout.tsx's existing data-theme="dark" needed no change) rather than inventing custom names. --radius-selector/-field/-box are all pinned to 2px inside those blocks; a separate --radius-md: 4px (via plain @theme) is applied by hand on floating layers (popover/dropdown/dialog/tooltip/drawer), since daisyUI has no "floating vs. static" radius category of its own. Everything daisyUI has no concept of — the extra surface-3 hover step, hairline border tiers, and the eleven-state status hues — lives in plain [data-theme=...]-scoped CSS custom properties immediately below, then re-exposed as Tailwind utilities via @theme inline.

Confirmation all eleven states render distinctly, both themes, visually inspected: yes — see Screenshots/Evidence above. Also spot-checked via getComputedStyle in the live page (not just visually) for a few components after a formatting pass, to separate "screenshot looked odd" from "CSS is actually wrong" (the Browser-pane screenshot tool in this sandbox was flaky at certain scroll offsets during this session — resolved by rendering the whole gallery in one tall viewport rather than scrolling, and cross-checked against getComputedStyle/get_page_text where the screenshot itself was suspect).

One deliberate rename from the design doc, called out because it's easy to miss: the design doc's --border token (default container border colour) is --edge here instead. daisyUI reserves --border for component border width (--border: 1px inside the theme blocks) — reusing the name for a colour at the same [data-theme] scope would silently overwrite that width value. --edge-subtle/--edge/--edge-strong carry the same three-step ladder the design doc specifies, just collision-free.

What I could not fully follow from the design spec, said rather than improvised:

  • EncodingPreview (§5.3): the spec wants an absolutely-positioned highlight layer sitting pixel-exactly behind a live-editable mono textarea, matching its font metrics — "a real implementation trap" by the doc's own words. Without a live composer screen (Composer with live encoding preview #51, a later task) to verify that alignment against, I built a read-only annotated line below the editor instead, showing the same flagged characters with the same precision. The design doc explicitly names this component (alongside StateTimeline/LiveRow) as one where "a correct-but-simpler stub is expected" if the data shape isn't settled — I judged the overlay-metrics risk to be exactly that case.
  • LiveRow ships (the per-row 240ms wash-on-change, reduced-motion-aware); the full LiveTable (scroll-position-dependent buffered insertion, the sticky "N new" pill, sort-mode switching) is explicitly out of scope — a later, screen-level task per the design doc's own build order (Phase 3, task L5), not something T6 asked for.
  • Job-state (pending/running/succeeded/failed/dead/cancelled) glyph mapping: the design doc marks this OPEN, pending a read of job_state_transitions — I did that read (schema/migrations/postgres/0002_bootstrap/up.sql) and recorded the edges in a code comment, but did not invent the glyph/hue mapping, since the doc explicitly assigns that judgement call to its own follow-up task (§8, B3) — notably, a job's failed is retryable (failed -> pending is legal), unlike a message's terminal failed, so it isn't a safe copy-paste.
  • Data-display atoms (MsisdnDisplay, TimestampDisplay, IdDisplay, AmountDisplay — design doc §7/Phase 2) were not built; they're a separate phase in the design doc's own build order, not named in T6's "at minimum" list.
  • Font self-hosting/subsetting (Inter Variable + JetBrains Mono Variable, latin-ext, per design doc F2) is not done — no font files exist in this repo yet, and F2 is its own separate task in the design doc's build order. The family stacks are correct and fall back to system fonts (system-ui/ui-monospace) until that lands.

Please rebase this PR onto main once #127 (T2/T5) merges, per the task instructions — I did not attempt to do that myself since #127 hasn't landed yet.

@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 3e5b42b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

…ate status system (T6)

Builds packages/ui per DECISIONS §4 of the dashboard architecture plan:
shadcn/ui is out, daisyUI v5 (CSS-first @plugin theming) drives all
styling and theming, Radix primitives stay for behaviour only on
dialog/dropdown-menu/select/tooltip/popover — focus trapping, keyboard
nav, ARIA that daisyUI's CSS-only components don't provide, in a console
operators use all day. cmdk and vaul are wired in standalone.

Tokens (packages/ui/src/styles/theme.css): two daisyUI theme blocks under
the built-in "dark"/"light" names (dark default, matching admin's existing
data-theme="dark"), 2px radius everywhere via daisyUI's own radius vars,
plus a plain-CSS extension layer for what daisyUI has no concept of — the
extra surface-3 hover step, hairline border tiers, and above all the
eleven-state status system (five hues x fg/bg/border x two themes),
re-exposed as Tailwind utilities via @theme inline. One deliberate rename
from the design doc: --border became --edge, since daisyUI reserves
--border for component border *width* and reusing the name for a colour
would silently clobber it.

Status system (components/status/): all eleven states from
messages_state_enum_check, rejected included. StateMark draws the
silhouette + interior-mark + fill-state language (circle/diamond/square,
pie wedges, filled-vs-stroked) at three redundant channels so no state
depends on hue alone — verified by rendering the full set at grayscale(1)
in the gallery. StatusPill applies the attention ladder with the owner's
override: delivered keeps the green glyph but no fill, everything else in
DECISIONS §5 unchanged.

Primitives: button, input, textarea, label, select, badge, card, table,
skeleton, tabs, separator, toast (hand-rolled, no Radix needed for a
non-modal notification), dialog, dropdown-menu, tooltip, popover,
command-menu, drawer.

Bespoke: StatusPill (the minimum), InlineEmptyState, PayloadInspector,
StateTimeline (the epic-gate component, including the two uncertain/
undelivered annotation nodes carrying the AGENTS.md-documented product
decisions verbatim), and two deliberately lighter builds where the design
doc itself allows a stub over the full spec pending real data shapes:
EncodingPreview (an annotated line instead of a metric-matched overlay)
and LiveRow (the per-row wash; the full scroll-buffered LiveTable is a
later, screen-level task).

admin/app/page.tsx becomes an honest component gallery — not a fake
dashboard — rendering every primitive, both themes (client-side toggle),
and a live wash demo, so a human can see the system actually works rather
than just compiling.

Two pre-existing gaps outside T6's own scope, patched because they
blocked verification: admin had tailwindcss/daisyui as deps but no
@tailwindcss/postcss or postcss.config.mjs, so Tailwind was never actually
wired into the build pipeline. And biome.json was committed but
@biomejs/biome was never added as a dependency nor run, so the whole
existing TS scaffold (packages/env, admin/app/layout.tsx) was already out
of step with its own lint config; both are now fixed and pnpm biome check
. is clean.

Caught by actually looking at the rendered gallery, not by inspection:
Button's primary variant was wired to daisyUI's btn-neutral class on the
theory that neutral resolved to the same achromatic-inverse-fill value as
primary in this theme — it doesn't (neutral became a separate, quieter
surface-3-ish token), which read as a washed-out primary button in light
theme. Fixed to btn-primary, the token that's actually the inverse fill.

This branch is layered on origin/claude/dashboard-t2-t5 (not yet on
main — see #127), since the Next.js scaffold and @vsms/env are its
prerequisite. The PR will show T2/T5's files until that merges.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@stephane-segning
stephane-segning merged commit 73acc81 into main Aug 8, 2026
5 checks passed

@lightbridge-assistant lightbridge-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🅵 Fast automated pass — SAST + a quick, diff-scoped look (no repo-wide retrieval). For a deeper, repo-aware review, mention @lightbridge-assistant on this PR.

Final: the change set contains exactly one provable defect (P2) in packages/env/src/index.ts — module-scope cross-field validation, including new URL(env.SMS_API_URL) on line 68, runs outside the skipValidation gate, so with SKIP_ENV_VALIDATION set and SMS_API_URL unset the import throws a TypeError, defeating the flag's purpose. Every other changed file was reviewed (via full diff content plus targeted read_file re-verification of page.tsx, encoding-preview, toast, drawer, state-timeline, env, and .env.example): the gallery's hooks and LiveRow wiring, the primitives' Radix/cmdk/vaul delegation, the status system, the health route, admin config, biome.json migration, and cn.ts are all sound; SAST is clean. NOT reviewed: packages/ui/src/styles/theme.css (too large for the diff budget) and pnpm-lock.yaml (lockfile). I did not add findings for verified-clean files despite repeated prompting.


🤖 AI-generated review — treat it as untrusted, verify before acting; a human owns the final decision (AI governance).

Comment thread packages/env/src/index.ts
if (env.NODE_ENV === "production" && env.DASHBOARD_AUTH === "none") {
throw new Error(
"NODE_ENV=production requires DASHBOARD_AUTH to be set (basic or other auth method)",
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 correctness

Module-scope URL() defeats skipValidation

skipValidation: !!process.env.SKIP_ENV_VALIDATION (line 41) is meant to let the module load during next build/CI where real env vars aren't present, but this whole cross-field block runs unconditionally at import time outside that gate. With SKIP_ENV_VALIDATION set and SMS_API_URL unset in the shell, env.SMS_API_URL is undefined (createEnv's runtimeEnv passes process.env.SMS_API_URL straight through when validation is skipped) and new URL(undefined) throws a TypeError the moment anyone imports @vsms/env — so the skip flag can never actually make the package importable without every var, which is its entire purpose. Gate this block (and the env.DASHBOARD_AUTH/NODE_ENV checks) behind the same skipValidation guard used by t3-env.

Evidence: Line 41 skipValidation: !!process.env.SKIP_ENV_VALIDATION; line 68 const apiUrl = new URL(env.SMS_API_URL); and the rest of the block (lines 45–86) are not gated by it.

Was this useful? React 👍/👎 to give us feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant