feat(onyx): @paper/onyx design system — wave 1 - #759
Draft
johnleider wants to merge 15 commits into
Draft
Conversation
Shadcn-centric design system on @vuetify/v0: dark-by-default token pipeline (onyx + onyx-light themes, graphite identity), dependency-free CSS emitters shared by the stylesheet adapter and the theme prebake, --v0-* kit-interop aliases, and 16 On* component families composing v0 compound primitives — zero native form controls, data-attribute state styling, color-mix focus rings.
18-page docs app at /onyx built entirely from On* components: grouped drawer nav with theme toggle (dark default, light variant), foundation pages driven by live tokens, per-component exhibit pages with full interaction demos; registers the notifications and storage plugins the harness lacked.
Two real token-wiring bugs found while auditing every --onyx-* name foundations()/block() emit against what components actually reference: - OnToaster's .onyx-toast paired background: var(--onyx-popover, ...) with color: var(--onyx-foreground, ...) instead of the matching --onyx-popover-foreground. Same rendered value today (the two tokens happen to share values in both themes), but the wrong pairing would silently break if a future palette ever diverges them. - OnPagination's .onyx-pagination__item/__ellipsis hardcoded height/width: 36px instead of var(--onyx-control-md, 36px) — the exact value OnButton's own md size uses for the same "interactive control height" concept. SPEC.md: three accuracy fixes surfaced by the same audit — the warning/success/info severity tokens are directly consumed by OnAlert/OnToast for severity styling, not "alias-only" as previously documented; brand-foreground was missing from the brand reserved-token note; and the brand hue's color name was still "fuchsia" after the graphite rebrand. Remaining emitted-but-unreferenced tokens (brand/brand-foreground, font-mono, radius-xl, spacing-xl/2xl, the 2xl-4xl text sizes) are over-provisioned foundation scale, not bugs — no wave-1 component needs them yet. Full reasoning in .superpowers/sdd/gate-a-report.md.
…ference
Keyboard-map grid used grid-template-columns: repeat(auto-fit,
minmax(380px, 1fr)) — at a 390px viewport the container's available
content width (minus padding) is under 380px, so the fixed minimum forced
a track wider than the viewport (confirmed: scrollWidth 404 vs clientWidth
375). Wrapped the minimum in min(380px, 100%) so the track never demands
more than the container has, at any width — same layout at desktop sizes,
correctly collapses to one column on mobile instead of overflowing.
Also fixed a leftover "Hb" reference in the focus-visible alert's prose
("Every interactive Hb component..." -> "On component") — a leftover from
the earlier Hummingbird->Onyx rename that the identifier-only sweep didn't
catch since it's plain text, not a code identifier.
The root RouterView wrapper's bg-background/p-4 sandbox styling (meant for the Playground and create-overflow demo routes) was leaking through as a light-gray frame around every Onyx page in both themes, since OnyxDocs.vue owns its own full-bleed chrome. Bare-shell /onyx/* routes while leaving the Playground/create-overflow routes untouched.
Full palette and material-system replacement per the commissioned redesign spec (.superpowers/design/direction-a.md), applied to all 16 component families plus the token layer. Palette (theme.ts): the zinc/Inter ramp is replaced with nine named warm surface values (pitch, stone, stoneRaised, intaglio, hairline, hairlineStrong, mutedFg, bone, champagne) at OKLCH hue 56-62°/chroma 0.006-0.012 for the dark ramp — real onyx is warm brown-black, not blue-black. #000000 never appears anywhere in the system (hard rule) — every "black" reference in the material tokens is built from pitchDeep or a color-mix() of it; the spec's own worked CSS violates this rule in several spots (literal #000/rgb(0 0 0)), all substituted with the warm equivalent at the same alpha. The two shadow-xs/shadow-sm foundation tokens are the sole, spec-documented exception (kept for backward compatibility, unused by any component). Material system (css.ts, colors.ts): every raised surface gets a band (subtle top-lightening gradient), a girdle (1px inset top-edge line — the signature device: brighter = elevated, champagne = selected/active, colored = severity), and a pool (wide shadow darkening the ground beneath the object). Recessed surfaces (inputs, tab troughs, progress tracks) invert the light direction instead. colors.ts's palette() now merges each theme's semantic map with its material geometry map; a prefers-contrast: more block (required deliverable, not polish, per the spec's own accessibility floor) promotes --onyx-border to hairline-strong and raises the dark girdle to 0.14 alpha, emitted once by css.ts's new contrastMore(). Grafts from Direction B (direction-b.md), layered onto A's own system: explicit disabled colors everywhere (never opacity:0.5 — it also faded the focus ring and dropped text below the accessibility floor); a real 2px outline on every focusable control, additive to any girdle/glow decoration, never replaced by a box-shadow (direction-a.md's own §5.5 input recipe sets outline:none, which this overrides); OnInput's already- present recess lighting confirmed consistent with the graft's intent. Typography (theme.ts): Fraunces (serif, display, 3xl+) / Instrument Sans (sans, body) / IBM Plex Mono (mono, utility) replace the previous stack, each with a graceful system fallback baked into the token. Every fontSize step gained a letterSpacing value alongside size/line-height (the "buying air" scale). Font files are the docs agent's install, not this package's. Components explicitly specified by the redesign (Card, Button all 6 variants, Input, Dialog, Tabs, Toast) implement the spec's worked CSS directly (adapted for the #000 fix and this codebase's real class names). The remaining 10 families (Alert, Avatar, Badge, Banner, Breadcrumbs, Chip, List, Pagination, Progress, Table) extend the same material logic coherently: card-like raised surfaces (Alert, Table) get the full band/girdle/pool treatment; small inline elements (Badge, Chip, Avatar, breadcrumb links) deliberately don't get a girdle, per the spec's own discipline rule that only objects sitting above the ground catch light; OnBanner (a full-width layout fixture, not a floating object) follows the same reasoning. Judgment calls made where the spec has gaps (all documented in .superpowers/sdd/redesign-pkg-report.md): light-mode values for surface-raised/hairline-strong/champagne (not given explicit overrides in the spec's light block), popover-foreground and destructive-fg for light mode, and OnCard's new interactive/selected props (state hooks the spec's own appendix requires but never wires to a component API). Verification: typecheck, build, and a comprehensive #000/#000000/rgb(0 0 0) grep across all of src/ and dist/ (theme.css + style.css) all clean except the documented shadow-xs/shadow-sm exception. --v0-* alias count holds at 42. Browser-verified (isolated browser contexts, per the technique from the gate-A report) across buttons/cards/form-elements in both themes — zero console errors, girdle/pool/band and excavated-input recesses render correctly, disabled states show explicit flat colors, and a real 2px outline appears on Tab-focus. Does not touch dev/src/onyx — the docs agent owns those files in parallel.
Fonts: self-hosted Fraunces Variable (display), Instrument Sans Variable (body), IBM Plex Mono (utility) per direction-a.md §4.1, wired into the dev app entry and the root pnpm catalog. Docs shell (OnyxDocs.vue): the app root now hosts the case lamp once (`.onyx-app`); the nav rail switches to pitch-deep with no border and a 2px champagne active bar (the girdle rotated 90°) replacing the filled pill; group labels move to the shared `.onyx-hallmark` utility. Also adds `.onyx-exhibit`, the recessed "bench" treatment for component demo sections (direction-a.md §8, grafted per direction-c.md §7), applied across all 11 component-demo pages (Buttons, FormElements, Cards, Tabs, DialogsAndBanners, Chips, Toast, Lists, Progress, Tables, Breadcrumbs). Introduction.vue: full-bleed hero per §8 (hallmark eyebrow, Fraunces 5xl title, single champagne rule, one-sentence lede) — the shell's generic page title is suppressed on this one route so it doesn't stack a second heading above it. Provenance/name copy rewritten to match the actual shipped palette instead of the prior graphite-era draft. Depth.vue: full rewrite — the old Material shadow-ramp story is gone, replaced with three live-token exhibits (the four material layers built up incrementally, raised vs. recessed, and girdle-carries-state) documenting the real band/girdle/hairline/pool system honestly. Typography.vue: reflects the new three-face system and per-step tracking from live tokens, with correct serif/weight-300 switch at 3xl+.
The new 5xl type-scale step (72px) exposed a layout bug in the type-scale demo row: a single word in the Fraunces sample text at 3xl-5xl can be wider than the space left over after the fixed 220px label column, and a flex item can't shrink below the width of its longest unbreakable word — so the row (and the whole page) was forced wider than the viewport at 390px. Let the row wrap onto two lines when needed and give the sample text overflow-wrap: anywhere as a last-resort floor, so no single word can force the page wider than the viewport regardless of scale step or viewport width. Found via the final re-gate's 390px overflow sweep across all 9 half-A pages; every other page was clean.
materialLight defines an 'intaglio' key (light.muted, per direction-a.md §3.4's explicit light-mode intaglio fill) but materialDark never did, so --onyx-intaglio was entirely absent from the [data-theme="onyx"] block — every dark-mode recess (Input, Tabs trough, Progress track, Depth.vue's recessed swatch) fell through its own var(--onyx-intaglio, #090605) CSS fallback instead of reading the real token, landing visually indistinct from --onyx-muted whenever the fallback and the intended value happened to diverge in a future edit. Add the intaglio key to materialDark using the existing named surface const (#090605, already the fallback literal every consuming component already assumes) — restores emission symmetry with materialLight and makes the CSS custom property authoritative again instead of the inline fallback silently carrying the value.
The Onyx rename swept every mechanical identifier but left a provenance sentence intact: "Onyx recreates the *principles* of the 2020 Onyx Design System (johnleider/onyx — Vuetify 2, built for Medecision's Aerial platform)". That names a real client and a fabricated repo URL in a public spec file — a liability that must not exist at all, not just be reworded. Rewrite the Identity opening Onyx-native, with zero predecessor/client/URL references, mirroring how Introduction.vue already frames the same fact (built headless from the start on @vuetify/v0, visual language redrawn since a first flat-graphite pass) — same shadcn-centric technical content, no lineage claim. git grep -inE 'medecision|aerial' -- packages dev: zero matches. git grep -i 'hummingbird' -- packages dev: zero matches (unaffected).
The published Lc60 floor can't be met by any text-color choice against champagne-deep as a fill (verified: even pitch, the darkest available ink, only reaches Lc 44.2 there) — champagne-deep is simply too mid-toned a background to clear it in either polarity. Direction A's own light-mode law is "the material law inverts": the box no longer emits light, so the one object that reads as solid/metal in light mode isn't a gold fill with pale text, it's the dark stone itself with a cream face — same fashion- house logic as a black lacquer object photographed on a cream backdrop. light.primary is now ink (#1f1915, the same value as light.foreground — intentional, they're the same stone), light.primary-foreground stays bone. champagne-deep keeps every other role it already had (brand, ring, active markers, accents) — those are thin-line/accent contexts where APCA's requirements are looser and gold is the correct read. apca(bone, ink) = -99.4 — clears the floor with room to spare, and the Accessibility page's own live table (which reads the tokens directly, not a hardcoded number) now shows "Light — primary on background: Lc 97.1", identical to "foreground on background" since they're literally the same color now. Consumer check (grep for --onyx-primary / --onyx-primary-foreground): OnButton default (dark button on cream, intended), OnBadge default (dark ink badge, verified on the Chips page), OnProgress fill (dark ink bar, verified on the Progress page) all read correctly as the "dark stone" material — no consumer semantically wants gold instead. OnPagination's active-page marker uses the identical pattern but has no live consumer in the docs app yet to screenshot; reviewed at the source level only. Also fixes a regression this surfaces: OnButton's default-variant :hover mixed --onyx-primary toward --onyx-foreground, which in light mode are now the same color — a no-op hover with zero visible feedback (confirmed via pixel comparison, not just computed-style string). Retargeted the mix to --onyx-card (the same anchor :active already uses, at a lighter 95%/5% touch vs active's 88%/12%), which produces a real, visible hover state in both themes and makes hover/active move in the same direction, differing only by intensity — arguably more coherent than the previous opposite- direction hover/active pair.
Same class of leak as the earlier Identity-section fix: "Original: github.com/johnleider/onyx" is a fabricated repo URL that shouldn't exist in a public spec regardless of accuracy. Dropped the claim entirely, kept the captures pointer (renamed "Visual evidence" since "Original: ...; captures in ..." read as one clause).
…ng, drift-proofing 1. OnInput reveal toggle dropped focus to <body> on every activation. The :key="effectiveType" on Input.Root forces a full subtree remount to work around v0 #757 (Input.Root's `type` context field is built once, plain, in Root's own setup — createInput's context object is assembled once and never rebuilt, so it stays frozen even if a child re-renders; verified against InputRoot.vue directly, confirming keying Control instead would remount the wrong layer and do nothing). The remount destroys the just- clicked reveal button along with everything else in the subtree. Fix: wrap the reveal Button.Root in a display:contents ref (Button.Root doesn't defineExpose its element, so this mirrors the same querySelector workaround Buttons.vue's onFocusDemo already uses), and refocus it after nextTick once the remounted subtree has painted. Verified via real keyboard flow (Tab to the reveal button, Enter, read document.activeElement) across two consecutive toggles, not just one. 2. OnTable's active page-size pill lost its highlight the instant the pointer moved over it — OnButton's own [data-variant='outline']:hover: not([data-disabled]) rule (4 selectors) beat OnTable's bare [data-active] override (3 selectors). Repeated the override at matching :hover:not specificity so the active pill's colors hold under hover. Verified via computed-style before/after a real .hover() call — background and color are now bit-identical across the hover transition. 3. css.ts's block() validated color values against UNSAFE_CSS but never validated color KEYS — colors.ts's palette() output flows through createOnyxPlugin({ theme: { themes } }), which merges consumer-supplied theme objects, so a caller-controlled key can reach this loop (verified in plugin.ts: `themes: { ...defaults.themes, ...options.theme.themes }`). Mirrors the base v0 ThemeAdapter's own SAFE_IDENT key guard, which checks both key and value together. 4. bake-theme.ts hand-enumerated 'onyx' and 'onyx-light' as two separate block() calls; a future third theme in colors.ts's `themes` record could silently miss the bake. Now iterates Object.entries(themes) directly (name + dark flag) and keys the :root block to whichever entry is flagged `dark: true`, throwing loudly instead of guessing if none is. Confirmed behavior-preserving: dist/theme.css is byte-identical before/after (same 42 --v0-* aliases, both [data-theme] blocks, :root, and the prefers-contrast: more block).
Extends the earlier Identity/Reference provenance fixes: the Wave 1 table
listed 15 real predecessor component names verbatim (HdsBtn, HdsCard,
HdsDialog, HdsDataTable, ...) under an "Original" column, captioned
"mapped from the original Onyx's exported components" — a detailed
fingerprint of the real client's real design system sitting right next to
where the client name used to be, invisible to a "medecision|aerial|
johnleider|hummingbird" grep since it's a different string entirely.
Table becomes Component | v0 basis | Notes — no predecessor column. Folded/
dropped rows deleted outright (they only existed relative to the
predecessor); the deferred-DatePicker note stays but reworded as pure
v0-roadmap rationale with zero lineage framing (also corrected while
rewriting it: v0 has no DatePicker primitive at any maturity, composable
or component — verified via source grep, not carried over from the
imprecise "past draft maturity" phrasing the original note used). The
"15+3+1+1=20" accounting prose is gone; wave 1 is stated as what it is,
16 component families.
Also caught and fixed a second, same-class leftover the table removal
exposed as dangling: the Non-goals section's "Checkbox, Switch, Select,
Slider, Combobox — not in the original inventory" bullet referenced a
concept ("the original inventory") that no longer exists anywhere in this
file once the table's predecessor framing is gone.
git grep -n 'Hds': zero tracked matches.
git grep -inE 'medecision|aerial': zero matches (unaffected, still clean).
git grep -i 'hummingbird': zero matches (unaffected, still clean).
git grep -in 'original inventory|original onyx|original.*export': zero matches.
My previous fix over-corrected: I'd grepped packages/0/src for a source file and found nothing, then wrote "planned, not yet drafted" — but maturity.json (lines ~130/549) lists both createDatePicker and DatePicker at draft maturity. Both facts are true simultaneously: draft-registered, zero source implementation. QA's exact wording, verified against maturity.json.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
New
@paper/*design system: Onyx — dark-by-default, built entirely on@vuetify/v0headless primitives with a lapidary ("jewel-box") material language: warm stone surfaces, depth drawn with light (girdle + pool) instead of shadow ramps, champagne action color, Fraunces/Instrument Sans/IBM Plex Mono type.Surface
onyx(dark, default) +onyx-lightthemes;OnyxStyleSheetAdapter extends V0StyleSheetThemeAdapter; prebakeddist/theme.css(exported — zero-config path); dependency-free CSS emitters (src/css.ts) shared by adapter and bake;--v0-*kit-interop aliases (Genesis-compatible);prefers-contrast: more+prefers-reduced-motionsupport; #000-never palette rule.color-mix()focus rings./onyxin the dev harness — 18 pages built entirely from On components (live-token foundation pages, full interaction demos, APCA values computed live via v0'sapca). Registers the notifications + storage plugins the harness previously lacked.Verification
pnpm typecheck/repo:checkclean; zero-config render proof from:rootalone; token-wiring audit; 72-capture visual matrix (18 pages × 2 viewports × 2 themes) run twice (pre/post visual-language pass); 11 interaction categories verified via DOM-state reads; APCA floors verified in both themes.Notes
typenon-reactively) — worked around in OnInput via keyed remount, linked in source.tsdown'sdevExportsauto-sync is deliberately omitted: it prunes the./theme.cssexport the zero-config path requires (details in package SPEC.md; Emerald likely affected by the same mechanism).@paper/onyxminor.