# HG changeset patch # User MrJuneJune # Date 1785932740 25200 # Node ID 60a876c4587a4e191ee5e9c984d705f8c3514dd0 # Parent 609d3c6aff4effe30b78f058d85d74d821647085 [ui] Add semantic primitive ownership Build a layered Zenbu token and sizing system, make authored controls use native-underneath primitives, migrate mrjunejune without imposing visual surfaces, and document/enforce HTML ownership in the catalog and wiki. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> diff -r 609d3c6aff4e -r 60a876c4587a .claude/skills/zenbu-design-system/SKILL.md --- a/.claude/skills/zenbu-design-system/SKILL.md Tue Aug 04 16:49:11 2026 -0700 +++ b/.claude/skills/zenbu-design-system/SKILL.md Wed Aug 05 05:25:40 2026 -0700 @@ -8,16 +8,27 @@ ## Structure - `design_system/src/components/`: dependency-free ES module Web Components. -- `design_system/src/styles/tokens.css`: shared `--zen-*` design tokens. -- `design_system/src/styles/components.css`: component presentation. +- `design_system/src/styles/reference.css`: context-free `--zenbu-ref-*` scales. +- `design_system/src/styles/semantic.css`: theme-aware `--zenbu-sys-*` roles. +- `design_system/src/styles/density.css`: comfortable/compact role mappings. +- `design_system/src/styles/components.css` and `elements.css`: presentation. +- `design_system/src/styles/tokens.css`: compatibility aliases only. - `design_system/src/index.html`: declarative component stories. -- `design_system/src/storybook.js` and `storybook.css`: catalog shell. +- `design_system/src/catalog.js`, `component_catalog.js`, and `catalog.css`: + catalog shell and story inventory. - `design_system/main.c`: Seobeo catalog server. ## Component conventions - Stay close to native HTML and use light DOM. - Wrap native interactive elements instead of recreating form/link semantics. +- Keep behavior reusable without forcing presentation. Support + `appearance="plain"` for button/card/field integrations, and require visual + surfaces, borders, shadows, and backgrounds to be deliberate opt-ins at the + application boundary. +- Do not wrap existing content in a visual component merely to demonstrate + design-system adoption; use components only where they add behavior, + semantics, or an explicitly requested visual recipe. - Do not add framework or runtime dependencies. - Keep application state outside components. - Keep notification action tokens opaque and out of DOM attributes; use scoped, @@ -25,10 +36,21 @@ - Keep `zen-notifications` behavior aligned with `design_system/NOTIFICATIONS.md`. - Notification presentation uses a measured collapsed depth stack that expands - on hover/focus; do not import Sonner or Radix. + on hover/focus and remains entirely first-party. - Preserve keyboard, form, validity, label, and ARIA behavior. -- Prefix custom elements and tokens with `zen-` / `--zen-`. +- Prefix custom elements and canonical tokens with `zen-` / `--zenbu-`. - Keep selectors low-specificity so applications can override tokens. +- Use the canonical `xs` / `sm` / `md` / `lg` / `xl` scale for typography, + control heights, block/inline padding, gaps, icons, and containers. Do not + introduce one-off component dimensions when a scale token fits. +- Plain controls inherit `--zenbu-control-*` variables from their size attribute + without receiving visual CSS; consuming applications should build their own + appearance from those variables. +- Keep the token graph layered: `--zenbu-ref-*` defines context-free values, + `--zenbu-sys-*` defines theme/density-aware intent, and private + `--zenbu--*` aliases simplify variants. New component and + application CSS must consume semantic roles rather than reference colors or + deprecated `--zen-*` aliases. - Support light, dark, reduced-motion, desktop, and mobile behavior. - Disclosure, overlay, and collapsing surfaces require smooth tokenized motion plus a reduced-motion fallback. Native disclosure markers must be replaced by @@ -43,10 +65,11 @@ - Treat themes as semantic-token compositions. Preserve the built-in Paper, Ink, and Playful themes plus automatic system light/dark behavior; components must not branch on theme names. -- Use semantic `--zen-color-*` aliases in component CSS. Use primitive ramps - or the ten data tokens only when a semantic role does not fit; never encode - meaning through color alone. -- Raw color literals are forbidden outside `src/styles/tokens.css`. Run +- Use semantic `--zenbu-sys-*` aliases in component CSS. Add a semantic role + instead of consuming reference colors directly; never encode meaning through + color alone. +- Raw color literals are forbidden outside `src/styles/reference.css` and the + compatibility-only `src/styles/tokens.css`. Run `//design_system/test:design_system_policy_test` whenever styles, components, stories, or catalog markup changes. - Render every interface icon with the first-party `zen-icon` component. Emoji, @@ -60,7 +83,7 @@ ```bash bazel run //design_system:dev -bazel test //design_system/test:storybook_test +bazel test //design_system/test:catalog_test bazel test //design_system/test:design_system_policy_test bazel build //design_system:design_system_server_bundle ``` @@ -76,3 +99,9 @@ Write stories declaratively with `zen-story` and a direct `template`. Every component state should have a rendered example and visible source. Browser tests must cover native semantics and interactions rather than only element presence. + +## Wiki requirement + +Read `design_system/wiki/README.md` before authoring application HTML. Follow +its one-owner native-underneath rules and update the wiki whenever a public +primitive or authoring contract changes. diff -r 609d3c6aff4e -r 60a876c4587a .claude/skills/zenbu-personal-site/SKILL.md --- a/.claude/skills/zenbu-personal-site/SKILL.md Tue Aug 04 16:49:11 2026 -0700 +++ b/.claude/skills/zenbu-personal-site/SKILL.md Wed Aug 05 05:25:40 2026 -0700 @@ -16,6 +16,16 @@ - Zenbu UI assets are copied from `//design_system:components` and `//design_system:styles` into `src/public/design-system/` by Bazel. Do not hand-copy or fork shared component files. +- Preserve the site's established transparent content and plain header/icon + presentation. Use `appearance="plain"` when shared behavior is useful, and + do not introduce cards, shadows, borders, or surfaces without a concrete UX + reason. +- Every site-owned `zen-button` declares an `xs` / `sm` / `md` / `lg` / `xl` + size. Site CSS must consume inherited `--zenbu-control-*` aliases plus + `--zenbu-sys-padding-*` and typography roles instead of hardcoding control + padding or height. +- New site CSS uses `--zenbu-sys-*` semantic roles. Do not consume + `--zenbu-ref-color-*` or add new `--zen-*` compatibility-token usage. - `mrjunejune/test/`: integration tests and snapshots. - `mrjunejune/BUILD`: Bazel build, bundle, asset movement, and test-visible filegroups. - `mrjunejune/.config` and `.config.development`: server configuration. Do not commit secrets. @@ -34,6 +44,11 @@ The simple template renderer replaces `{{...}}` tokens with file contents loaded through `Seobeo_Web_LoadFile`. +Before adding or changing HTML controls, read +`design_system/wiki/HTML_AUTHORING.md` and `PRIMITIVE_MAP.md`. Every authored +interactive control must have exactly one appropriate Zenbu owner while +retaining its native element underneath. + ## Routes and features Existing route areas include: diff -r 609d3c6aff4e -r 60a876c4587a design_system/BUILD --- a/design_system/BUILD Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/BUILD Wed Aug 05 05:25:40 2026 -0700 @@ -30,7 +30,11 @@ filegroup( name = "policy_sources", - srcs = glob(["src/**"]), + srcs = glob([ + "*.md", + "src/**", + "wiki/**/*.md", + ]) + ["main.c"], visibility = ["//design_system/test:__pkg__"], ) diff -r 609d3c6aff4e -r 60a876c4587a design_system/COLORS.md --- a/design_system/COLORS.md Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/COLORS.md Wed Aug 05 05:25:40 2026 -0700 @@ -4,28 +4,23 @@ most surfaces; restrained natural colors provide emphasis without making the interface feel synthetic or overly saturated. -## Primitive ramps +## Reference colors -Each family has `50`, `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, -`900`, and `950` steps: +`reference.css` exposes the steps required by current themes and components: -- `--zen-color-neutral-*`: warm paper, linen, charcoal, and ink; -- `--zen-color-red-*`: brick and iron oxide; -- `--zen-color-orange-*`: persimmon; -- `--zen-color-amber-*`: straw and ochre; -- `--zen-color-green-*`: matcha and moss; -- `--zen-color-teal-*`: aged copper and patina; -- `--zen-color-blue-*`: faded workwear and indigo; -- `--zen-color-violet-*`: muted plum; -- `--zen-color-rose-*`: clay and dusty rose; -- `--zen-color-brown-*`: kraft paper and wood. +- `--zenbu-ref-color-neutral-*`: paper, linen, charcoal, and ink; +- `--zenbu-ref-color-brand-*`: faded workwear and indigo; +- `--zenbu-ref-color-danger-*`: brick and iron oxide; +- `--zenbu-ref-color-success-*`: matcha and moss; +- `--zenbu-ref-color-warning-*`: persimmon and ochre. -Use a primitive when an application needs a precise ramp step. Primitive values -do not change between light and dark themes. +Reference values do not change between themes. They are consumed by semantic +theme mappings, not directly by application components. -## Material aliases +## Legacy material aliases -Material aliases make one-off composition readable: +These compatibility aliases preserve existing visual names while migration is +in progress: ```css var(--zen-color-paper) @@ -43,21 +38,26 @@ var(--zen-color-wood) ``` -## Semantic aliases +Do not use them in new component CSS; map the intended role through a +`--zenbu-sys-color-*` token instead. + +## Semantic roles Components should prefer semantic tokens because these adapt to the current theme: -- surfaces: `canvas`, `surface`, `surface-raised`, `surface-muted`, - `surface-sunken`, and `surface-overlay`; -- text: `text`, `text-muted`, `text-subtle`, and `text-inverse`; -- borders: `border`, `border-muted`, and `border-strong`; -- action: `accent`, `accent-hover`, `accent-muted`, `focus`, `disabled`, and - `selection`; -- status: `info`, `success`, `warning`, `danger`, their `on-*` foregrounds, - and their `*-muted` surfaces. +- surfaces: `--zenbu-sys-color-surface-*`; +- text and icons: `--zenbu-sys-color-text-*` and + `--zenbu-sys-color-icon-*`; +- borders: `--zenbu-sys-color-border-*`; +- actions and states: `--zenbu-sys-color-action-*`; +- status: `--zenbu-sys-color-info-*`, `success-*`, `warning-*`, and + `danger-*`; +- selection and focus: `--zenbu-sys-color-selection-*` and + `--zenbu-sys-color-focus-*`. -All names use the `--zen-color-` prefix. +Legacy `--zen-color-*` values remain compatibility aliases. New code uses the +`--zenbu-ref-*` / `--zenbu-sys-*` architecture. Component and catalog styles may not contain raw color literals. Add or reuse a token in `tokens.css`, then reference it with `var()`. The Bazel target @@ -66,7 +66,8 @@ ## Data colors -`--zen-color-data-1` through `--zen-color-data-10` form a categorical palette. +`--zen-color-data-1` through `--zen-color-data-10` remain the current +categorical compatibility palette. They deliberately vary in both hue and lightness. Keep adjacent series in numerical order and provide labels or patterns; color alone must never carry meaning. @@ -75,12 +76,13 @@ ```css .repository-summary { - border: 1px solid var(--zen-color-border-muted); - background: var(--zen-color-surface-muted); - color: var(--zen-color-text); + border: var(--zenbu-sys-stroke-width) solid + var(--zenbu-sys-color-border-subtle); + background: var(--zenbu-sys-color-surface-subtle); + color: var(--zenbu-sys-color-text-primary); } .repository-summary strong { - color: var(--zen-color-indigo); + color: var(--zenbu-sys-color-text-link); } ``` diff -r 609d3c6aff4e -r 60a876c4587a design_system/COMPONENTS.md --- a/design_system/COMPONENTS.md Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/COMPONENTS.md Wed Aug 05 05:25:40 2026 -0700 @@ -1,12 +1,12 @@ # Zenbu UI component contracts -Zenbu UI covers the 65 concepts in the shadcn/ui component catalog without -copying its React, Radix, or third-party runtime. Every Zenbu component uses -light DOM and keeps the corresponding native HTML control visible. +Zenbu UI provides first-party component concepts and composition patterns. +Every component uses light DOM and keeps the corresponding native HTML control +visible. ## Inventory -| shadcn/ui concept | Zenbu UI element | +| Component concept | Zenbu UI element | | --- | --- | | Accordion | `zen-accordion` | | Alert | `zen-alert` | @@ -17,6 +17,7 @@ | Badge | `zen-badge` | | Breadcrumb | `zen-breadcrumb` | | Bubble | `zen-bubble` | +| Box | `zen-box` | | Button | `zen-button` | | Button Group | `zen-button-group` | | Calendar | `zen-calendar` | @@ -38,12 +39,14 @@ | Field | `zen-field` | | Form | `zen-form` | | Hover Card | `zen-hover-card` | +| Heading | `zen-heading` | | Input | `zen-input` | | Input Group | `zen-input-group` | | Input OTP | `zen-input-otp` | | Item | `zen-item` | | Kbd | `zen-kbd` | | Label | `zen-label` | +| Link | `zen-link` | | Marker | `zen-marker` | | Menubar | `zen-menubar` | | Message | `zen-message` | @@ -62,12 +65,13 @@ | Sidebar | `zen-sidebar` | | Skeleton | `zen-skeleton` | | Slider | `zen-slider` | -| Sonner | `zen-notifications` | +| Notifications | `zen-notifications` | | Spinner | `zen-spinner` | | Switch | `zen-switch` | | Table | `zen-table` | | Tabs | `zen-tabs` | | Textarea | `zen-textarea` | +| Text | `zen-text` | | Toast (deprecated) | `zen-notifications` | | Toggle | `zen-toggle` | | Toggle Group | `zen-toggle-group` | @@ -96,12 +100,14 @@ | Elements | Required light-DOM hooks | Emitted event | | --- | --- | --- | -| `zen-dialog`, `zen-alert-dialog`, `zen-sheet`, `zen-drawer` | `[data-zen-trigger]`, `dialog`, optional `[data-zen-close]` | native `close` | -| `zen-popover`, `zen-hover-card`, `zen-tooltip` | `[data-zen-trigger]`, `[data-zen-content]` | native toggle/focus events | +| `zen-dialog`, `zen-alert-dialog`, `zen-sheet`, `zen-drawer` | `zen-button > button[data-zen-trigger]`, `dialog`, optional `zen-button > button[data-zen-close]` | native `close` | +| `zen-popover`, `zen-tooltip` | `zen-button > button[data-zen-trigger]`, `[data-zen-content]` | native toggle/focus events | +| `zen-hover-card` | interactive `[data-zen-trigger]`, `[data-zen-content]` | native toggle/focus events | | Menu elements | `[data-zen-trigger]`, `[role="menu"]`, `[role="menuitem"]` | native click | | `zen-tabs` | `[role="tablist"]`, `[role="tab"]`, `[role="tabpanel"]` | `zen-change` with `{ value }` | | `zen-toggle` | direct `button` | `zen-change` with `{ pressed }` | | `zen-toggle-group` | direct buttons with `value` | `zen-change` with `{ values }` | +| `zen-link` | direct native anchor; optional `effect="paw"` | native link events | | `zen-sidebar` | `[data-zen-sidebar-trigger]`, `[data-zen-sidebar-panel]` | `zen-change` with `{ open }` | | `zen-combobox` | `input`, `[role="listbox"]`, `[role="option"]` | native `input`/`change` and `zen-change` | | `zen-command` | `input`, `[role="menu"]`, `[role="menuitem"]` | `zen-command` with `{ value }` | diff -r 609d3c6aff4e -r 60a876c4587a design_system/NOTIFICATIONS.md --- a/design_system/NOTIFICATIONS.md Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/NOTIFICATIONS.md Wed Aug 05 05:25:40 2026 -0700 @@ -77,7 +77,7 @@ - Twenty additional records may remain queued. - The idle stack layers older records behind the newest with depth and scale. - Hovering the stack or focusing an action expands all visible records using - their measured heights; no Sonner/Radix runtime or CSS is used. + their measured heights and first-party component code. - Overflow evicts the oldest finite record; all-persistent queues reject new records. - Queued records do not count down. diff -r 609d3c6aff4e -r 60a876c4587a design_system/README.md --- a/design_system/README.md Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/README.md Wed Aug 05 05:25:40 2026 -0700 @@ -4,10 +4,11 @@ It stays close to ordinary HTML: components wrap native controls and sectioning elements instead of replacing their semantics. -The catalog includes native Zenbu implementations of all 65 concepts currently -listed by shadcn/ui. These are original Web Components using Zenbu tokens, not -ports of shadcn's React, Radix, or third-party runtime. See +The catalog includes native Zenbu component concepts and composition patterns. +They are original dependency-free Web Components using Zenbu tokens. +See [`COMPONENTS.md`](COMPONENTS.md) for the complete mapping and HTML contracts. +Future HTML work follows the [`Zenbu UI wiki`](wiki/README.md). ## Catalog @@ -20,8 +21,7 @@ Open `http://127.0.0.1:6980`. Override the port with `DESIGN_SYSTEM_PORT`. -The catalog is intentionally Storybook-like without depending on Storybook. Its -stories are declared as HTML templates: +The first-party catalog renders stories declared as HTML templates: ```html @@ -54,11 +54,15 @@ ``` -Every visual token is a `--zen-*` custom property and can be overridden by the -application. See [`COLORS.md`](COLORS.md) for the complete warm color ramps, -material aliases, semantic roles, dark-theme behavior, and data palette. +Canonical tokens use the layered +[`--zenbu-ref-*` / `--zenbu-sys-*` architecture](TOKENS.md) and can be +overridden at an application boundary; older `--zen-*` names remain +compatibility aliases. See [`COLORS.md`](COLORS.md) for color roles and theme +behavior. Named [`themes`](THEMES.md) compose those tokens as Paper, Ink, and Playful experiences in addition to automatic system light/dark behavior. +The [`sizing scale`](SIZING.md) defines shared `xs` through `xl` typography, +control, icon, gap, and padding primitives. Use [`zen-icon`](ICONS.md) for all interface iconography. Design-system source does not use emoji, ad hoc SVG, icon fonts, or third-party icon packages. @@ -74,10 +78,11 @@ | Component family | Native content | Attributes | Events | | --- | --- | --- | --- | -| `zen-button` | direct `button` or `a` | `variant`, `loading`, `disabled` | native click/form events | -| `zen-card` | direct `article` or `section` | `interactive` | native descendant events | +| `zen-button` | direct `button` or `a` | `appearance`, `variant`, `loading`, `disabled` | native click/form events | +| `zen-card` | direct `article` or `section` | `appearance`, `interactive` | native descendant events | +| `zen-link` | direct `a` | `effect` | native navigation events | | `zen-alert` | message HTML | `tone`, `dismissible` | `zen-dismiss` | -| `zen-field` | direct `label`, form control, help text | native control attributes | native input/change/invalid | +| `zen-field` | direct `label`, form control, help text | `appearance`, native control attributes | native input/change/invalid | | `zen-notifications` | descendant event producers | `aria-label`, `dismiss-label` | `zen-notification-action`, `zen-notification-removed` | | `zen-stack` | any HTML | `direction`, `gap` | native descendant events | | Disclosure and tabs | `details`, `summary`, buttons, sections | `multiple`, `activation` | `zen-change` | @@ -89,6 +94,14 @@ presentation, small accessibility wiring, and interaction affordances while applications retain data and workflow ownership. +Use `appearance="plain"` when an application needs component behavior without +the Zenbu visual recipe. Plain buttons remove browser chrome, plain cards add +no surface treatment, and plain fields keep label/validity wiring while leaving +the application's control CSS untouched. + +`zen-link effect="paw"` adds the first-party paw decoration and hover motion +while preserving the native anchor and all application-authored link styles. + ### Notifications See [`NOTIFICATIONS.md`](NOTIFICATIONS.md) for the complete event contract, @@ -128,6 +141,6 @@ ## Tests ```bash -bazel test //design_system/test:storybook_test +bazel test //design_system/test:catalog_test bazel build //design_system:design_system_server_bundle ``` diff -r 609d3c6aff4e -r 60a876c4587a design_system/SIZING.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/SIZING.md Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,65 @@ +# Zenbu UI sizing + +Zenbu UI uses one five-step scale everywhere: + +| Size | Intended use | +| --- | --- | +| `xs` | dense metadata and compact actions | +| `sm` | secondary controls and toolbars | +| `md` | default body text and controls | +| `lg` | prominent actions and lead text | +| `xl` | touch-forward actions and display text | + +## Tokens + +Each step defines: + +- `--zenbu-sys-font-size-*` and `--zenbu-sys-line-height-*`; +- `--zenbu-sys-control-height-*`; +- `--zenbu-sys-control-padding-block-*` and + `--zenbu-sys-control-padding-inline-*`; +- `--zenbu-sys-control-gap-*`; +- `--zenbu-sys-icon-size-*`; +- `--zenbu-sys-padding-*` for containers. + +Component hosts expose the selected values through inherited variables: + +```css +--zenbu-control-height +--zenbu-control-padding-block +--zenbu-control-padding-inline +--zenbu-control-gap +--zenbu-control-font-size +--zenbu-control-line-height +--zenbu-control-icon-size +``` + +Styled components consume these automatically: + +```html + + +``` + +Plain components expose the same variables without styling the native control. +Applications can preserve their visual identity while sharing the scale: + +```css +zen-button[appearance="plain"] > button { + min-height: var(--zenbu-control-height); + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); + font-size: var(--zenbu-control-font-size); +} +``` + +## Typography and containers + +Use native semantics inside the primitives: + +```html +

Repository activity

+

Updated two minutes ago.

+... +``` diff -r 609d3c6aff4e -r 60a876c4587a design_system/THEMES.md --- a/design_system/THEMES.md Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/THEMES.md Wed Aug 05 05:25:40 2026 -0700 @@ -18,8 +18,8 @@ ```css :root { - --zen-font-editorial: "More Thin", sans-serif; - --zen-font-playful: "More", sans-serif; + --zenbu-font-editorial: "More Thin", sans-serif; + --zenbu-font-playful: "More", sans-serif; } ``` diff -r 609d3c6aff4e -r 60a876c4587a design_system/TOKENS.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/TOKENS.md Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,83 @@ +# Zenbu UI token architecture + +Zenbu UI uses three CSS custom-property layers: + +```text +Reference values Semantic system roles Local component aliases +--zenbu-ref-* --zenbu-sys-* --zenbu--* +``` + +## Reference values + +`reference.css` defines context-free values: color ramps, spacing, type sizes, +weights, line heights, radii, strokes, shadows, durations, easing, opacity, +icon sizes, control sizes, and z-index levels. + +Reference values answer what exists in the system. Application and component +CSS must not consume reference colors directly. + +## Semantic roles + +`semantic.css` maps reference values to stable intent: + +```css +var(--zenbu-sys-color-text-primary) +var(--zenbu-sys-color-surface-raised) +var(--zenbu-sys-color-action-primary-background) +var(--zenbu-sys-type-body-size) +var(--zenbu-sys-space-container) +var(--zenbu-sys-radius-control) +var(--zenbu-sys-motion-duration-state) +``` + +Themes and density change semantic assignments. Component selectors continue +to consume the same roles. + +## Local aliases + +Components use private aliases where variants remap one internal role: + +```css +zen-button:not([appearance="plain"]) > button { + --zenbu-button-background: + var(--zenbu-sys-color-action-primary-background); + background: var(--zenbu-button-background); +} +``` + +Do not turn every internal declaration into public API. A local alias becomes a +documented host override only when multiple applications need that stable +customization point. + +## Themes and density + +Themes use `data-zen-theme`; density uses `data-zen-density`: + +```html + +``` + +Available densities: + +- no attribute or `comfortable`: canonical five-step control sizes; +- `compact`: reduced visual heights, padding, and gaps. + +Explicit component `size="xs|sm|md|lg|xl"` maps through density-aware semantic +tokens. + +## Compatibility + +Older `--zen-*` variables remain temporarily in `tokens.css` as aliases. New +code must use `--zenbu-ref-*`, `--zenbu-sys-*`, or a documented local +`--zenbu--*` alias. + +## Enforcement + +`//design_system/test:design_system_policy_test` checks: + +- raw colors appear only in token source files; +- every referenced `--zenbu-*` property is declared; +- component CSS does not consume reference colors directly; +- source and documentation use first-party naming; +- exported JavaScript APIs retain typed documentation; +- icon and dependency policies remain intact. diff -r 609d3c6aff4e -r 60a876c4587a design_system/main.c --- a/design_system/main.c Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/main.c Wed Aug 05 05:25:40 2026 -0700 @@ -13,6 +13,7 @@ "/components/badge", "/components/breadcrumb", "/components/bubble", + "/components/box", "/components/button", "/components/button-group", "/components/calendar", @@ -34,12 +35,14 @@ "/components/field", "/components/form", "/components/hover-card", + "/components/heading", "/components/input", "/components/input-group", "/components/input-otp", "/components/item", "/components/kbd", "/components/label", + "/components/link", "/components/marker", "/components/menubar", "/components/message", @@ -59,13 +62,13 @@ "/components/sidebar", "/components/skeleton", "/components/slider", - "/components/sonner", "/components/spinner", "/components/stack", "/components/switch", "/components/table", "/components/tabs", "/components/textarea", + "/components/text", "/components/toast", "/components/toggle", "/components/toggle-group", diff -r 609d3c6aff4e -r 60a876c4587a design_system/src/catalog.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/src/catalog.css Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,401 @@ +* { + box-sizing: border-box; +} + +html { + min-height: 100%; + background: var(--zenbu-sys-color-surface-canvas); + color: var(--zenbu-sys-color-text-primary); + font-family: var(--zenbu-sys-font-family-ui); +} + +body { + min-height: 100vh; + margin: 0; +} + +button, +input, +select, +textarea { + font: inherit; +} + +.catalog-shell { + display: grid; + grid-template-columns: 17rem minmax(0, 1fr); + min-height: 100vh; +} + +.catalog-sidebar { + position: sticky; + top: 0; + height: 100vh; + overflow: auto; + padding: var(--zenbu-sys-space-container); + border-right: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + background: var(--zenbu-sys-color-surface-default); +} + +.catalog-brand { + display: inline-flex; + align-items: center; + gap: var(--zenbu-sys-space-control); + color: inherit; + font-size: var(--zenbu-sys-font-size-lg); + font-weight: 850; + text-decoration: none; +} + +.catalog-search { + width: 100%; + margin-top: var(--zenbu-sys-space-container); +} + +.catalog-search > zen-icon { + align-self: center; + margin-inline-start: var(--zenbu-sys-space-control); + color: var(--zenbu-sys-color-text-secondary); +} + +.catalog-search > kbd { + align-self: center; + margin-inline-end: var(--zenbu-sys-space-related); + padding: 0 var(--zenbu-sys-space-icon-label); + border: 1px solid var(--zenbu-sys-color-border-subtle); + border-radius: var(--zenbu-sys-radius-control-small); + color: var(--zenbu-sys-color-text-secondary); + font: var(--zenbu-sys-font-size-xs)/1.5 var(--zenbu-sys-font-family-code); +} + +.search-status { + min-height: 1.25rem; + margin: var(--zenbu-sys-space-icon-label) 0 0; + color: var(--zenbu-sys-color-text-secondary); + font-size: var(--zenbu-sys-font-size-xs); +} + +.catalog-mark { + display: grid; + width: 2.5rem; + height: 2.5rem; + place-items: center; + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: 50%; + background: var(--zenbu-sys-color-action-primary-background); + color: var(--zenbu-sys-color-action-primary-foreground); +} + +.catalog-sidebar h2 { + margin: var(--zenbu-sys-space-content) 0 var(--zenbu-sys-space-related); + color: var(--zenbu-sys-color-text-secondary); + font-size: var(--zenbu-sys-font-size-xs); + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.catalog-nav { + display: grid; + gap: var(--zenbu-sys-space-icon-label); +} + +.catalog-nav a { + padding: var(--zenbu-sys-space-related) var(--zenbu-sys-space-control); + border-radius: var(--zenbu-sys-radius-control-small); + color: inherit; + text-decoration: none; +} + +.catalog-nav a[hidden], +.component-grid > a[hidden] { + display: none; +} + +.catalog-nav a:hover, +.catalog-nav a[aria-current] { + background: var(--zenbu-sys-color-action-primary-background); + color: var(--zenbu-sys-color-action-primary-foreground); +} + +.theme-toggle { + width: 100%; + margin-top: var(--zenbu-sys-space-content); + padding: var(--zenbu-sys-space-related); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + background: transparent; + color: inherit; + cursor: pointer; +} + +.theme-toggle-wrap { + display: flex; + width: 100%; +} + +.catalog-main { + width: min(100%, var(--zenbu-sys-width-page)); + min-width: 0; + padding: var(--zenbu-sys-space-section) clamp(1rem, 5vw, 5rem); + outline: none; +} + +.catalog-page { + min-width: 0; +} + +.catalog-page[hidden] { + display: none; +} + +.page-heading { + max-width: 52rem; + margin-bottom: var(--zenbu-sys-space-section); +} + +.eyebrow { + margin: 0 0 var(--zenbu-sys-space-related); + color: var(--zenbu-sys-color-info-foreground); + font-size: var(--zenbu-sys-font-size-sm); + font-weight: 800; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.page-heading h1 { + margin: 0 0 var(--zenbu-sys-space-control); + font-size: var(--zenbu-sys-type-page-title-size); + line-height: 1.05; +} + +.page-heading p { + color: var(--zenbu-sys-color-text-secondary); + font-size: var(--zenbu-sys-font-size-lg); +} + +.principles, +.component-grid, +.token-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); + gap: var(--zenbu-sys-space-container); +} + +.principle, +.token-group { + padding: var(--zenbu-sys-space-container); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-container); + background: var(--zenbu-sys-color-surface-default); +} + +.principle h2, +.token-group h2 { + margin-top: 0; +} + +.component-grid a { + color: inherit; + text-decoration: none; +} + +.component-grid zen-card { + height: 100%; +} + +.component-grid code { + color: var(--zenbu-sys-color-info-foreground); + font-size: var(--zenbu-sys-font-size-xs); +} + +.component-grid p { + color: var(--zenbu-sys-color-text-secondary); +} + +.swatch-list { + display: grid; + gap: var(--zenbu-sys-space-related); + margin: 0; +} + +.swatch-list div { + display: grid; + grid-template-columns: 2rem 1fr; + align-items: center; + gap: var(--zenbu-sys-space-related); +} + +.swatch-list dt, +.swatch-list dd { + margin: 0; + font-family: var(--zenbu-sys-font-family-code); + font-size: var(--zenbu-sys-font-size-xs); +} + +.swatch { + --zenbu-color-swatch: transparent; + width: 2rem; + height: 2rem; + border: 1px solid var(--zenbu-sys-color-border-default); + border-radius: 50%; + background: var(--zenbu-color-swatch); +} + +.token-group-wide { + grid-column: span 2; +} + +.token-group > p { + color: var(--zenbu-sys-color-text-secondary); +} + +.palette-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr)); + gap: var(--zenbu-sys-space-control); +} + +.palette-grid > div { + display: grid; + gap: var(--zenbu-sys-space-icon-label); +} + +.palette-grid span { + --zenbu-color-swatch: transparent; + min-height: 3.5rem; + border: 1px solid var(--zenbu-sys-color-border-subtle); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-color-swatch); +} + +.palette-grid code { + font-size: var(--zenbu-sys-font-size-xs); +} + +.data-palette { + display: grid; + grid-template-columns: repeat(10, minmax(1.75rem, 1fr)); + min-height: 4rem; + overflow: hidden; + border: 1px solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); +} + +.data-palette span { + --zenbu-color-swatch: transparent; + background: var(--zenbu-color-swatch); +} + +.icon-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); + gap: var(--zenbu-sys-space-control); +} + +.icon-grid figure { + display: grid; + min-height: 7rem; + margin: 0; + padding: var(--zenbu-sys-space-control); + place-items: center; + gap: var(--zenbu-sys-space-related); + border: 1px solid var(--zenbu-sys-color-border-subtle); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-default); +} + +.icon-grid figcaption { + max-width: 100%; + overflow: hidden; + color: var(--zenbu-sys-color-text-secondary); + font-size: var(--zenbu-sys-font-size-xs); + text-overflow: ellipsis; +} + +zen-story { + display: block; + max-width: 100%; + margin-bottom: var(--zenbu-sys-space-content); + overflow: hidden; + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-container); + background: var(--zenbu-sys-color-surface-default); +} + +zen-story > header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--zenbu-sys-space-control); + padding: var(--zenbu-sys-space-control) var(--zenbu-sys-space-group); + border-bottom: 1px solid var(--zenbu-sys-color-border-default); +} + +zen-story > header h3 { + margin: 0; +} + +zen-story > header button { + padding: var(--zenbu-sys-space-icon-label) var(--zenbu-sys-space-related); + border: 0; + border-radius: var(--zenbu-sys-radius-control-small); + background: transparent; + color: var(--zenbu-sys-color-info-foreground); + cursor: pointer; +} + +.story-canvas { + min-height: 8rem; + padding: clamp(1rem, 4vw, 3rem); + background: + linear-gradient(90deg, var(--zenbu-sys-color-gridline) 1px, transparent 1px), + linear-gradient(var(--zenbu-sys-color-gridline) 1px, transparent 1px); + background-size: 1rem 1rem; +} + +zen-story > pre { + max-height: 24rem; + overflow: auto; + margin: 0; + padding: var(--zenbu-sys-space-group); + border-top: 1px solid var(--zenbu-sys-color-border-default); + background: var(--zenbu-sys-color-code-surface); + color: var(--zenbu-sys-color-code-text); + font: var(--zenbu-sys-font-size-sm)/1.55 var(--zenbu-sys-font-family-code); +} + +.demo-card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + gap: var(--zenbu-sys-space-container); +} + +@media (max-width: 760px) { + .catalog-shell { + grid-template-columns: 1fr; + } + + .catalog-sidebar { + position: static; + width: 100%; + height: auto; + border-right: 0; + border-bottom: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + } + + .catalog-nav { + grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); + } + + .catalog-main { + padding: var(--zenbu-sys-space-content) var(--zenbu-sys-space-group); + } + + .token-group-wide { + grid-column: auto; + } + + .data-palette { + grid-template-columns: repeat(5, 1fr); + } +} diff -r 609d3c6aff4e -r 60a876c4587a design_system/src/catalog.js --- a/design_system/src/catalog.js Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/src/catalog.js Wed Aug 05 05:25:40 2026 -0700 @@ -1,732 +1,304 @@ -/** @const {!ReadonlyArray} shadcn concepts shown by the catalog. */ -export const componentCatalog = [ - { - name: "Accordion", - slug: "accordion", - element: "zen-accordion", - group: "Disclosure", - description: "A group of native details elements with optional single-open behavior.", - markup: ` -
Is it accessible?

It keeps native details and summary semantics.

-
Can several open?

Add the multiple attribute to allow it.

-
`, - }, - { - name: "Alert", - slug: "alert", - element: "zen-alert", - group: "Feedback", - description: "Status messaging with semantic tones and optional dismissal.", - existing: true, - }, - { - name: "Alert Dialog", - slug: "alert-dialog", - element: "zen-alert-dialog", - group: "Overlays", - description: "A native modal dialog for decisions that require confirmation.", - markup: ` - - -

Delete deployment?

-

This action cannot be undone.

- - -
-
`, - }, - { - name: "Aspect Ratio", - slug: "aspect-ratio", - element: "zen-aspect-ratio", - group: "Layout", - description: "A media frame driven by a readable ratio attribute.", - markup: ` -
16 / 9
-
`, - }, - { - name: "Attachment", - slug: "attachment", - element: "zen-attachment", - group: "Chat", - description: "File and upload state presentation using ordinary links and buttons.", - markup: ` - -
release-notes.pdf248 KB · Ready
- -
`, - }, - { - name: "Avatar", - slug: "avatar", - element: "zen-avatar", - group: "Display", - description: "A compact identity image with a visible text fallback.", - markup: `JP`, - }, - { - name: "Badge", - slug: "badge", - element: "zen-badge", - group: "Display", - description: "A small label for metadata, state, or counts.", - markup: ` - Default - Healthy - Failed -`, - }, - { - name: "Breadcrumb", - slug: "breadcrumb", - element: "zen-breadcrumb", - group: "Navigation", - description: "A native navigation landmark for hierarchical location.", - markup: ` - -`, - }, - { - name: "Bubble", - slug: "bubble", - element: "zen-bubble", - group: "Chat", - description: "A message bubble with incoming and outgoing alignment.", - markup: ` -

The deployment is ready.

-

Ship it.

-
`, - }, - { - name: "Button", - slug: "button", - element: "zen-button", - group: "Actions", - description: "Native buttons and links with shared variants and states.", - existing: true, - }, - { - name: "Button Group", - slug: "button-group", - element: "zen-button-group", - group: "Actions", - description: "Related native buttons joined into one visual control.", - markup: ` - - - -`, - }, - { - name: "Calendar", - slug: "calendar", - element: "zen-calendar", - group: "Forms", - description: "A first-party date month grid backed by a native form value.", - markup: ` - - -`, - }, - { - name: "Card", - slug: "card", - element: "zen-card", - group: "Display", - description: "A visual container around native article or section content.", - existing: true, - }, - { - name: "Carousel", - slug: "carousel", - element: "zen-carousel", - group: "Collections", - description: "A scroll-snap collection with native buttons and keyboard controls.", - markup: ` -
-
Mercurial forge
-
HLS player
-
LaTeX editor
-
- - -
`, - }, - { - name: "Chart", - slug: "chart", - element: "zen-chart", - group: "Data", - description: "A dependency-free chart frame for semantic labels and custom marks.", - markup: ` -
- - - - -
-
`, - }, - { - name: "Checkbox", - slug: "checkbox", - element: "zen-checkbox", - group: "Forms", - description: "A styled native checkbox that remains form-associated.", - markup: ` - -`, - }, - { - name: "Collapsible", - slug: "collapsible", - element: "zen-collapsible", - group: "Disclosure", - description: "A single native details disclosure.", - markup: ` -
Three changed files

server.c, styles.css, README.md

-
`, - }, - { - name: "Combobox", - slug: "combobox", - element: "zen-combobox", - group: "Forms", - description: "A filterable text input backed by an accessible option list.", - markup: ` - - -
- - - -
-
`, - }, - { - name: "Command", - slug: "command", - element: "zen-command", - group: "Navigation", - description: "A keyboard-first filterable command list.", - markup: ` - -
- - - -
-
`, - }, - { - name: "Context Menu", - slug: "context-menu", - element: "zen-context-menu", - group: "Menus", - description: "A right-click menu with keyboard navigation and native buttons.", - markup: ` -
Right-click this repository
- -
`, - }, - { - name: "Data Table", - slug: "data-table", - element: "zen-data-table", - group: "Data", - description: "A native table enhanced with optional client-side sorting.", - markup: ` - - - - - - - - - - -
seobeo87
hg-web142
design-system12
-
`, - }, - { - name: "Date Picker", - slug: "date-picker", - element: "zen-date-picker", - group: "Forms", - description: "A tokenized date trigger and first-party keyboard calendar.", - markup: ` - - -`, - }, - { - name: "Dialog", - slug: "dialog", - element: "zen-dialog", - group: "Overlays", - description: "A native modal dialog with trigger and close wiring.", - markup: ` - - -

Edit profile

- - -
-
`, - }, - { - name: "Direction", - slug: "direction", - element: "zen-direction", - group: "Utilities", - description: "A light-DOM boundary for left-to-right or right-to-left content.", - markup: `

مرحبا بالعالم

`, - }, - { - name: "Drawer", - slug: "drawer", - element: "zen-drawer", - group: "Overlays", - description: "A native dialog presented as an edge drawer.", - markup: ` - -

Build queue

Two jobs are running.

-
`, - }, - { - name: "Dropdown Menu", - slug: "dropdown-menu", - element: "zen-dropdown-menu", - group: "Menus", - description: "A button-owned action menu with arrow-key navigation.", - markup: ` - - -`, - }, - { - name: "Empty", - slug: "empty", - element: "zen-empty", - group: "Feedback", - description: "A structured empty state with an optional native action.", - markup: ` - No deployments yet -

Create the first release from the current default branch.

- -
`, - }, - { - name: "Field", - slug: "field", - element: "zen-field", - group: "Forms", - description: "Label, help text, validity, and a native form control.", - existing: true, - }, - { - name: "Form", - slug: "form", - element: "zen-form", - group: "Forms", - description: "A native form boundary with visible browser validity state.", - markup: ` -
- Required - -
-
`, - }, - { - name: "Hover Card", - slug: "hover-card", - element: "zen-hover-card", - group: "Overlays", - description: "Supplemental content shown on hover or keyboard focus.", - markup: ` - @mrjunejune - -`, - }, - { - name: "Input", - slug: "input", - element: "zen-input", - group: "Forms", - description: "A styled native input with no replacement semantics.", - markup: ``, - }, - { - name: "Input Group", - slug: "input-group", - element: "zen-input-group", - group: "Forms", - description: "A native input with prefix, suffix, or inline actions.", - markup: ` - - - -`, - }, - { - name: "Input OTP", - slug: "input-otp", - element: "zen-input-otp", - group: "Forms", - description: "A single native one-time-code input with visual slots.", - markup: ` - - - -`, - }, - { - name: "Item", - slug: "item", - element: "zen-item", - group: "Display", - description: "A flexible list row for an icon, content, and actions.", - markup: ` - -
hg-webUpdated two minutes ago
- -
`, - }, - { - name: "Kbd", - slug: "kbd", - element: "zen-kbd", - group: "Display", - description: "Keyboard shortcut presentation using native kbd elements.", - markup: `

Open commands with Ctrl+K.

`, - }, - { - name: "Label", - slug: "label", - element: "zen-label", - group: "Forms", - description: "A styled native label that keeps browser association.", - markup: ` -`, - }, - { - name: "Marker", - slug: "marker", - element: "zen-marker", - group: "Chat", - description: "An inline system marker for chat and activity streams.", - markup: `Today`, - }, - { - name: "Menubar", - slug: "menubar", - element: "zen-menubar", - group: "Menus", - description: "A horizontal application menu using native buttons and menu roles.", - markup: ` - - - - -`, - }, - { - name: "Message", - slug: "message", - element: "zen-message", - group: "Chat", - description: "An avatar, author, and content row for conversations.", - markup: ` - JP -
June

Can we deploy this release?

-
`, - }, - { - name: "Message Scroller", - slug: "message-scroller", - element: "zen-message-scroller", - group: "Chat", - description: "A chat viewport that follows new content until the reader scrolls away.", - markup: ` -
-

Worker connected.

Build started.

Bundle created.

Deployment complete.

-
- -
`, - }, - { - name: "Native Select", - slug: "native-select", - element: "zen-native-select", - group: "Forms", - description: "A styled native select with full platform behavior.", - markup: ``, - }, - { - name: "Navigation Menu", - slug: "navigation-menu", - element: "zen-navigation-menu", - group: "Navigation", - description: "Top-level navigation with optional keyboard-accessible flyouts.", - markup: ` - -`, - }, - { - name: "Pagination", - slug: "pagination", - element: "zen-pagination", - group: "Navigation", - description: "Page navigation built from native links.", - markup: ` - -`, - }, - { - name: "Popover", - slug: "popover", - element: "zen-popover", - group: "Overlays", - description: "A lightweight panel using the native Popover API when available.", - markup: ` - -
Dimensions
-
`, - }, - { - name: "Progress", - slug: "progress", - element: "zen-progress", - group: "Feedback", - description: "A styled native progress element.", - markup: `68%`, - }, - { - name: "Radio Group", - slug: "radio-group", - element: "zen-radio-group", - group: "Forms", - description: "A fieldset of native radio inputs.", - markup: ` -
Visibility - - -
-
`, - }, - { - name: "Resizable", - slug: "resizable", - element: "zen-resizable", - group: "Layout", - description: "Two native panels separated by a pointer and keyboard handle.", - markup: ` -
Repository tree
-
-
File preview
-
`, - }, - { - name: "Scroll Area", - slug: "scroll-area", - element: "zen-scroll-area", - group: "Layout", - description: "A bounded native scroll container with theme-aware scrollbars.", - markup: ` -

default

feature/design-system

release/2026-08

experiment/trading

-
`, - }, - { - name: "Select", - slug: "select", - element: "zen-select", - group: "Forms", - description: "A native select styled as the default accessible implementation.", - markup: ``, - }, - { - name: "Separator", - slug: "separator", - element: "zen-separator", - group: "Layout", - description: "A native horizontal rule or ARIA separator.", - markup: `
Repository

Changesets
`, - }, - { - name: "Sheet", - slug: "sheet", - element: "zen-sheet", - group: "Overlays", - description: "A native dialog presented from a selected screen edge.", - markup: ` - -

Filters

-
`, - }, - { - name: "Sidebar", - slug: "sidebar", - element: "zen-sidebar", - group: "Navigation", - description: "A collapsible navigation region with an explicit native trigger.", - markup: ` - - -`, - }, - { - name: "Skeleton", - slug: "skeleton", - element: "zen-skeleton", - group: "Feedback", - description: "A reduced-motion-safe placeholder for pending content.", - markup: ` - - - -`, - }, - { - name: "Slider", - slug: "slider", - element: "zen-slider", - group: "Forms", - description: "A native range input with browser keyboard and pointer behavior.", - markup: ``, - }, - { - name: "Sonner", - slug: "sonner", - element: "zen-notifications", - group: "Feedback", - description: "Zenbu's bounded, scoped, Sonner-like notification stack.", - markup: ` - -`, - }, - { - name: "Spinner", - slug: "spinner", - element: "zen-spinner", - group: "Feedback", - description: "A small CSS loading indicator with status semantics.", - markup: ``, - }, - { - name: "Switch", - slug: "switch", - element: "zen-switch", - group: "Forms", - description: "A native checkbox presented with switch semantics.", - markup: ``, - }, - { - name: "Table", - slug: "table", - element: "zen-table", - group: "Data", - description: "Responsive styling around a native HTML table.", - markup: ` - - - -
Recent changesets
RevisionAuthor
253:fdf3816959cbJune
252:7a7581f040e8June
`, - }, - { - name: "Tabs", - slug: "tabs", - element: "zen-tabs", - group: "Disclosure", - description: "ARIA tabs with automatic or manual keyboard activation.", - markup: ` -
- - - -
-
Browse repository files.
-
Inspect changesets.
-
View branch topology.
-
`, - }, - { - name: "Textarea", - slug: "textarea", - element: "zen-textarea", - group: "Forms", - description: "A styled native multiline text control.", - markup: ``, - }, - { - name: "Toast", - slug: "toast", - element: "zen-notifications", - group: "Feedback", - description: "The deprecated toast concept maps to zen-notifications.", - markup: `

Toast is deprecated. Use zen-notifications for scoped transient messages.

`, - }, - { - name: "Toggle", - slug: "toggle", - element: "zen-toggle", - group: "Actions", - description: "A native button with managed pressed state.", - markup: ``, - }, - { - name: "Toggle Group", - slug: "toggle-group", - element: "zen-toggle-group", - group: "Actions", - description: "Single or multiple pressed-state buttons with roving keyboard focus.", - markup: ` - - - -`, - }, - { - name: "Tooltip", - slug: "tooltip", - element: "zen-tooltip", - group: "Overlays", - description: "A delayed, non-interactive description for hover and focus.", - markup: ` - - -`, - }, - { - name: "Typography", - slug: "typography", - element: "zen-typography", - group: "Foundations", - description: "Readable defaults for native headings, paragraphs, lists, and code.", - markup: ` -

The Mercurial forge

-

Typography stays semantic and inherits Zenbu tokens.

-
Own the stack, understand the stack.
-
`, - }, -]; +import "./components/index.js"; +import { componentCatalog } from "./component_catalog.js"; +import { ICON_NAMES } from "./components/icon.js"; + +const stackComponent = { + name: "Stack", + slug: "stack", + element: "zen-stack", + group: "Layout", + description: "A Zenbu-specific flex layout primitive with tokenized gaps.", + existing: true, +}; +const displayedComponents = [...componentCatalog, stackComponent]; + +const pages = new Map([ + ["/", "overview"], + ["/components", "overview"], + ["/tokens", "tokens"], + ["/icons", "icons"], + ["/components/button", "button"], + ["/components/card", "card"], + ["/components/alert", "alert"], + ["/components/field", "field"], + ["/components/notifications", "notifications"], + ["/components/stack", "stack"], +]); + +for (const component of componentCatalog) { + pages.set(`/components/${component.slug}`, component.slug); +} + +function createCatalogLink(component) { + const link = document.createElement("a"); + link.href = `/components/${component.slug}`; + link.dataset.catalogLink = ""; + link.dataset.zenElement = component.element; + link.textContent = component.name; + return link; +} + +function createComponentCard(component) { + const link = createCatalogLink(component); + const card = document.createElement("zen-card"); + card.setAttribute("interactive", ""); + const article = document.createElement("article"); + const heading = document.createElement("h3"); + heading.textContent = component.name; + const description = document.createElement("p"); + description.textContent = component.description; + const element = document.createElement("code"); + element.textContent = component.element; + article.append(heading, description, element); + card.append(article); + link.replaceChildren(card); + return link; +} + +function createCatalogPage(component) { + const section = document.createElement("section"); + section.className = "catalog-page"; + section.dataset.catalogPage = component.slug; + section.hidden = true; + + const header = document.createElement("header"); + header.className = "page-heading"; + const eyebrow = document.createElement("p"); + eyebrow.className = "eyebrow"; + eyebrow.textContent = component.group; + const heading = document.createElement("h1"); + heading.textContent = component.name; + const description = document.createElement("p"); + description.textContent = component.description; + header.append(eyebrow, heading, description); + + const story = document.createElement("zen-story"); + story.setAttribute("name", `${component.name} example`); + const template = document.createElement("template"); + template.innerHTML = component.markup; + story.append(template); + section.append(header, story); + return section; +} + +function installComponentCatalog() { + const navigation = document.querySelector( + '.catalog-nav[aria-label="Components"]', + ); + const componentGrid = document.querySelector(".component-grid"); + const main = document.querySelector("#catalogMain"); + if (!navigation || !componentGrid || !main) return; + + navigation.replaceChildren( + ...displayedComponents.map(createCatalogLink), + ); + componentGrid.replaceChildren( + ...displayedComponents.map(createComponentCard), + ); + + for (const component of componentCatalog) { + if (component.existing) continue; + main.append(createCatalogPage(component)); + } +} + +function installIconCatalog() { + const grid = document.querySelector("#iconGrid"); + if (!grid) return; + const items = ICON_NAMES.map(name => { + const figure = document.createElement("figure"); + const icon = document.createElement("zen-icon"); + icon.setAttribute("name", name); + icon.setAttribute("size", "2rem"); + const caption = document.createElement("figcaption"); + const code = document.createElement("code"); + code.textContent = name; + caption.append(code); + figure.append(icon, caption); + return figure; + }); + grid.replaceChildren(...items); +} -/** @const {number} Number of mapped shadcn component concepts. */ -export const shadcnComponentCount = componentCatalog.length; +function installComponentSearch() { + const input = document.querySelector("#componentSearch"); + const status = document.querySelector("#componentSearchStatus"); + if (!input || !status) return; + const links = [ + ...document.querySelectorAll( + '.catalog-nav[aria-label="Components"] a', + ), + ]; + const cards = [...document.querySelectorAll(".component-grid > a")]; + const descriptions = new Map( + displayedComponents.map(component => [ + component.slug, + [ + component.name, + component.element, + component.group, + component.description, + ].join(" ").toLocaleLowerCase(), + ]), + ); + const filter = () => { + const query = input.value.trim().toLocaleLowerCase(); + let matches = 0; + for (const link of links) { + const slug = new URL(link.href).pathname.split("/").pop(); + const visible = !query || descriptions.get(slug)?.includes(query); + link.hidden = !visible; + if (visible) matches++; + } + for (const card of cards) { + const slug = new URL(card.href).pathname.split("/").pop(); + card.hidden = Boolean(query) && + !descriptions.get(slug)?.includes(query); + } + status.textContent = query + ? `${matches} component${matches === 1 ? "" : "s"} found` + : `${links.length} components`; + }; + + input.addEventListener("input", filter); + input.addEventListener("keydown", event => { + if (event.key !== "Escape" || !input.value) return; + input.value = ""; + filter(); + }); + document.addEventListener("keydown", event => { + const target = event.target; + const typing = target instanceof HTMLElement && + target.matches("input, textarea, select, [contenteditable]"); + if (event.key !== "/" || typing || event.metaKey || event.ctrlKey || + event.altKey) return; + event.preventDefault(); + input.focus(); + input.select(); + }); + filter(); +} + +function showPage(pathname) { + const pageName = pages.get(pathname) || "overview"; + for (const page of document.querySelectorAll("[data-catalog-page]")) { + page.hidden = page.dataset.catalogPage !== pageName; + } + for (const link of document.querySelectorAll( + ".catalog-sidebar [data-catalog-link]", + )) { + const active = new URL(link.href).pathname === pathname; + if (active) link.setAttribute("aria-current", "page"); + else link.removeAttribute("aria-current"); + } + const heading = document.querySelector( + `[data-catalog-page="${pageName}"] h1`, + ); + if (heading) document.title = `${heading.textContent} · Zenbu UI`; +} + +window.addEventListener("DOMContentLoaded", () => { + installComponentCatalog(); + installIconCatalog(); + installComponentSearch(); + const root = document.documentElement; + const systemTheme = matchMedia("(prefers-color-scheme: dark)"); + const storedTheme = localStorage.getItem("zen-theme"); + if (storedTheme) root.dataset.zenTheme = storedTheme; + + const themeToggle = document.querySelector("#themeToggle"); + const isDark = () => { + if (root.dataset.zenTheme) { + return root.dataset.zenTheme === "dark"; + } + return systemTheme.matches; + }; + const updateThemeButton = () => { + const dark = isDark(); + themeToggle?.setAttribute("aria-pressed", String(dark)); + if (themeToggle) { + themeToggle.textContent = dark + ? "Use light theme" + : "Use dark theme"; + } + }; + updateThemeButton(); + themeToggle?.addEventListener("click", () => { + const next = isDark() ? "light" : "dark"; + root.dataset.zenTheme = next; + localStorage.setItem("zen-theme", next); + updateThemeButton(); + }); + systemTheme.addEventListener("change", () => { + if (!root.dataset.zenTheme) updateThemeButton(); + }); + + document.addEventListener("click", event => { + const notificationDemo = event.target.closest( + "[data-notification-demo]", + ); + if (notificationDemo) { + const scope = notificationDemo.closest("zen-notifications"); + if (!scope) return; + const tone = notificationDemo.dataset.tone || "info"; + const baseId = notificationDemo.dataset.notificationId || tone; + const count = notificationDemo.dataset.burst === "true" ? 5 : 1; + for (let index = 0; index < count; index++) { + notificationDemo.dispatchEvent(new CustomEvent("zen-notify", { + bubbles: true, + composed: false, + detail: { + version: 1, + id: count > 1 ? `${baseId}-${Date.now()}-${index}` : baseId, + tone, + message: notificationDemo.dataset.message || "Notification", + description: notificationDemo.dataset.description, + announcement: tone === "error" ? "assertive" : "polite", + durationMs: 5000, + persistent: notificationDemo.dataset.persistent === "true", + action: notificationDemo.dataset.action === "true" + ? { token: `catalog-action-${Date.now()}`, label: "Retry" } + : undefined, + }, + })); + } + return; + } + + const dismissDemo = event.target.closest( + "[data-notification-dismiss-demo]", + ); + if (dismissDemo) { + dismissDemo.dispatchEvent(new CustomEvent( + "zen-dismiss-notification", + { + bubbles: true, + composed: false, + detail: { + version: 1, + id: dismissDemo.dataset.notificationDismissDemo, + }, + }, + )); + return; + } + + const link = event.target.closest("a[data-catalog-link]"); + if (!link || + link.origin !== location.origin || + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey || + link.hasAttribute("download") || + link.target) return; + event.preventDefault(); + history.pushState({}, "", link.href); + showPage(location.pathname); + document.querySelector("#catalogMain")?.focus(); + }); + window.addEventListener("popstate", () => showPage(location.pathname)); + showPage(location.pathname); +}); diff -r 609d3c6aff4e -r 60a876c4587a design_system/src/component_catalog.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/src/component_catalog.js Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,818 @@ +/** @const {!ReadonlyArray} Component concepts shown by the catalog. */ +export const componentCatalog = [ + { + name: "Accordion", + slug: "accordion", + element: "zen-accordion", + group: "Disclosure", + description: "A group of native details elements with optional single-open behavior.", + markup: ` +
Is it accessible?

It keeps native details and summary semantics.

+
Can several open?

Add the multiple attribute to allow it.

+
`, + }, + { + name: "Alert", + slug: "alert", + element: "zen-alert", + group: "Feedback", + description: "Status messaging with semantic tones and optional dismissal.", + existing: true, + }, + { + name: "Alert Dialog", + slug: "alert-dialog", + element: "zen-alert-dialog", + group: "Overlays", + description: "A native modal dialog for decisions that require confirmation.", + markup: ` + + + + +

Delete deployment?

+

This action cannot be undone.

+ + + + + + +
+
`, + }, + { + name: "Aspect Ratio", + slug: "aspect-ratio", + element: "zen-aspect-ratio", + group: "Layout", + description: "A media frame driven by a readable ratio attribute.", + markup: ` +
16 / 9
+
`, + }, + { + name: "Attachment", + slug: "attachment", + element: "zen-attachment", + group: "Chat", + description: "File and upload state presentation using ordinary links and buttons.", + markup: ` + +
release-notes.pdf248 KB · Ready
+ +
`, + }, + { + name: "Avatar", + slug: "avatar", + element: "zen-avatar", + group: "Display", + description: "A compact identity image with a visible text fallback.", + markup: `JP`, + }, + { + name: "Badge", + slug: "badge", + element: "zen-badge", + group: "Display", + description: "A small label for metadata, state, or counts.", + markup: ` + Default + Healthy + Failed +`, + }, + { + name: "Breadcrumb", + slug: "breadcrumb", + element: "zen-breadcrumb", + group: "Navigation", + description: "A native navigation landmark for hierarchical location.", + markup: ` + +`, + }, + { + name: "Bubble", + slug: "bubble", + element: "zen-bubble", + group: "Chat", + description: "A message bubble with incoming and outgoing alignment.", + markup: ` +

The deployment is ready.

+

Ship it.

+
`, + }, + { + name: "Box", + slug: "box", + element: "zen-box", + group: "Layout", + description: "A container with canonical xs through xl padding.", + markup: ` + XS padding + SM padding + MD padding + LG padding + XL padding +`, + }, + { + name: "Button", + slug: "button", + element: "zen-button", + group: "Actions", + description: "Native buttons and links with shared variants and states.", + existing: true, + }, + { + name: "Button Group", + slug: "button-group", + element: "zen-button-group", + group: "Actions", + description: "Related native buttons joined into one visual control.", + markup: ` + + + +`, + }, + { + name: "Calendar", + slug: "calendar", + element: "zen-calendar", + group: "Forms", + description: "A first-party date month grid backed by a native form value.", + markup: ` + + +`, + }, + { + name: "Card", + slug: "card", + element: "zen-card", + group: "Display", + description: "A visual container around native article or section content.", + existing: true, + }, + { + name: "Carousel", + slug: "carousel", + element: "zen-carousel", + group: "Collections", + description: "A scroll-snap collection with native buttons and keyboard controls.", + markup: ` +
+
Mercurial forge
+
HLS player
+
LaTeX editor
+
+ + +
`, + }, + { + name: "Chart", + slug: "chart", + element: "zen-chart", + group: "Data", + description: "A dependency-free chart frame for semantic labels and custom marks.", + markup: ` +
+ + + + +
+
`, + }, + { + name: "Checkbox", + slug: "checkbox", + element: "zen-checkbox", + group: "Forms", + description: "A styled native checkbox that remains form-associated.", + markup: ` + +`, + }, + { + name: "Collapsible", + slug: "collapsible", + element: "zen-collapsible", + group: "Disclosure", + description: "A single native details disclosure.", + markup: ` +
Three changed files

server.c, styles.css, README.md

+
`, + }, + { + name: "Combobox", + slug: "combobox", + element: "zen-combobox", + group: "Forms", + description: "A filterable text input backed by an accessible option list.", + markup: ` + + +
+ + + +
+
`, + }, + { + name: "Command", + slug: "command", + element: "zen-command", + group: "Navigation", + description: "A keyboard-first filterable command list.", + markup: ` + +
+ + + +
+
`, + }, + { + name: "Context Menu", + slug: "context-menu", + element: "zen-context-menu", + group: "Menus", + description: "A right-click menu with keyboard navigation and native buttons.", + markup: ` +
Right-click this repository
+ +
`, + }, + { + name: "Data Table", + slug: "data-table", + element: "zen-data-table", + group: "Data", + description: "A native table enhanced with optional client-side sorting.", + markup: ` + + + + + + + + + + +
seobeo87
hg-web142
design-system12
+
`, + }, + { + name: "Date Picker", + slug: "date-picker", + element: "zen-date-picker", + group: "Forms", + description: "A tokenized date trigger and first-party keyboard calendar.", + markup: ` + + +`, + }, + { + name: "Dialog", + slug: "dialog", + element: "zen-dialog", + group: "Overlays", + description: "A native modal dialog with trigger and close wiring.", + markup: ` + + + + +

Edit profile

+ + + + + + + +
+
`, + }, + { + name: "Direction", + slug: "direction", + element: "zen-direction", + group: "Utilities", + description: "A light-DOM boundary for left-to-right or right-to-left content.", + markup: `

مرحبا بالعالم

`, + }, + { + name: "Drawer", + slug: "drawer", + element: "zen-drawer", + group: "Overlays", + description: "A native dialog presented as an edge drawer.", + markup: ` + + + + +

Build queue

+

Two jobs are running.

+ + + +
+
`, + }, + { + name: "Dropdown Menu", + slug: "dropdown-menu", + element: "zen-dropdown-menu", + group: "Menus", + description: "A button-owned action menu with arrow-key navigation.", + markup: ` + + +`, + }, + { + name: "Empty", + slug: "empty", + element: "zen-empty", + group: "Feedback", + description: "A structured empty state with an optional native action.", + markup: ` + No deployments yet +

Create the first release from the current default branch.

+ +
`, + }, + { + name: "Field", + slug: "field", + element: "zen-field", + group: "Forms", + description: "Label, help text, validity, and a native form control.", + existing: true, + }, + { + name: "Form", + slug: "form", + element: "zen-form", + group: "Forms", + description: "A native form boundary with visible browser validity state.", + markup: ` +
+ Required + +
+
`, + }, + { + name: "Hover Card", + slug: "hover-card", + element: "zen-hover-card", + group: "Overlays", + description: "Supplemental content shown on hover or keyboard focus.", + markup: ` + @mrjunejune + +`, + }, + { + name: "Heading", + slug: "heading", + element: "zen-heading", + group: "Typography", + description: "Strong native headings on the canonical type scale.", + markup: ` +

Extra small heading

+

Small heading

+

Medium heading

+

Large heading

+

Extra large heading

+
`, + }, + { + name: "Input", + slug: "input", + element: "zen-input", + group: "Forms", + description: "A styled native input with no replacement semantics.", + markup: ``, + }, + { + name: "Input Group", + slug: "input-group", + element: "zen-input-group", + group: "Forms", + description: "A native input with prefix, suffix, or inline actions.", + markup: ` + + + +`, + }, + { + name: "Input OTP", + slug: "input-otp", + element: "zen-input-otp", + group: "Forms", + description: "A single native one-time-code input with visual slots.", + markup: ` + + + +`, + }, + { + name: "Item", + slug: "item", + element: "zen-item", + group: "Display", + description: "A flexible list row for an icon, content, and actions.", + markup: ` + +
hg-webUpdated two minutes ago
+ +
`, + }, + { + name: "Kbd", + slug: "kbd", + element: "zen-kbd", + group: "Display", + description: "Keyboard shortcut presentation using native kbd elements.", + markup: `

Open commands with Ctrl+K.

`, + }, + { + name: "Label", + slug: "label", + element: "zen-label", + group: "Forms", + description: "A styled native label that keeps browser association.", + markup: ` +`, + }, + { + name: "Link", + slug: "link", + element: "zen-link", + group: "Navigation", + description: "A native link with optional reusable first-party effects.", + markup: ` + A link with the paw effect +`, + }, + { + name: "Marker", + slug: "marker", + element: "zen-marker", + group: "Chat", + description: "An inline system marker for chat and activity streams.", + markup: `Today`, + }, + { + name: "Menubar", + slug: "menubar", + element: "zen-menubar", + group: "Menus", + description: "A horizontal application menu using native buttons and menu roles.", + markup: ` + + + + +`, + }, + { + name: "Message", + slug: "message", + element: "zen-message", + group: "Chat", + description: "An avatar, author, and content row for conversations.", + markup: ` + JP +
June

Can we deploy this release?

+
`, + }, + { + name: "Message Scroller", + slug: "message-scroller", + element: "zen-message-scroller", + group: "Chat", + description: "A chat viewport that follows new content until the reader scrolls away.", + markup: ` +
+

Worker connected.

Build started.

Bundle created.

Deployment complete.

+
+ +
`, + }, + { + name: "Native Select", + slug: "native-select", + element: "zen-native-select", + group: "Forms", + description: "A styled native select with full platform behavior.", + markup: ``, + }, + { + name: "Navigation Menu", + slug: "navigation-menu", + element: "zen-navigation-menu", + group: "Navigation", + description: "Top-level navigation with optional keyboard-accessible flyouts.", + markup: ` + +`, + }, + { + name: "Pagination", + slug: "pagination", + element: "zen-pagination", + group: "Navigation", + description: "Page navigation built from native links.", + markup: ` + +`, + }, + { + name: "Popover", + slug: "popover", + element: "zen-popover", + group: "Overlays", + description: "A lightweight panel using the native Popover API when available.", + markup: ` + + + +
+ Dimensions + +
+
`, + }, + { + name: "Progress", + slug: "progress", + element: "zen-progress", + group: "Feedback", + description: "A styled native progress element.", + markup: `68%`, + }, + { + name: "Radio Group", + slug: "radio-group", + element: "zen-radio-group", + group: "Forms", + description: "A fieldset of native radio inputs.", + markup: ` +
Visibility + + +
+
`, + }, + { + name: "Resizable", + slug: "resizable", + element: "zen-resizable", + group: "Layout", + description: "Two native panels separated by a pointer and keyboard handle.", + markup: ` +
Repository tree
+
+
File preview
+
`, + }, + { + name: "Scroll Area", + slug: "scroll-area", + element: "zen-scroll-area", + group: "Layout", + description: "A bounded native scroll container with theme-aware scrollbars.", + markup: ` +

default

feature/design-system

release/2026-08

experiment/trading

+
`, + }, + { + name: "Select", + slug: "select", + element: "zen-select", + group: "Forms", + description: "A native select styled as the default accessible implementation.", + markup: ``, + }, + { + name: "Separator", + slug: "separator", + element: "zen-separator", + group: "Layout", + description: "A native horizontal rule or ARIA separator.", + markup: `
Repository

Changesets
`, + }, + { + name: "Sheet", + slug: "sheet", + element: "zen-sheet", + group: "Overlays", + description: "A native dialog presented from a selected screen edge.", + markup: ` + + + + +

Filters

+ + + + +
+
`, + }, + { + name: "Sidebar", + slug: "sidebar", + element: "zen-sidebar", + group: "Navigation", + description: "A collapsible navigation region with an explicit native trigger.", + markup: ` + + +`, + }, + { + name: "Skeleton", + slug: "skeleton", + element: "zen-skeleton", + group: "Feedback", + description: "A reduced-motion-safe placeholder for pending content.", + markup: ` + + + +`, + }, + { + name: "Slider", + slug: "slider", + element: "zen-slider", + group: "Forms", + description: "A native range input with browser keyboard and pointer behavior.", + markup: ``, + }, + { + name: "Notifications", + slug: "notifications", + element: "zen-notifications", + group: "Feedback", + description: "A bounded, scoped notification stack.", + existing: true, + }, + { + name: "Spinner", + slug: "spinner", + element: "zen-spinner", + group: "Feedback", + description: "A small CSS loading indicator with status semantics.", + markup: ``, + }, + { + name: "Switch", + slug: "switch", + element: "zen-switch", + group: "Forms", + description: "A native checkbox presented with switch semantics.", + markup: ``, + }, + { + name: "Table", + slug: "table", + element: "zen-table", + group: "Data", + description: "Responsive styling around a native HTML table.", + markup: ` + + + +
Recent changesets
RevisionAuthor
253:fdf3816959cbJune
252:7a7581f040e8June
`, + }, + { + name: "Tabs", + slug: "tabs", + element: "zen-tabs", + group: "Disclosure", + description: "ARIA tabs with automatic or manual keyboard activation.", + markup: ` +
+ + + +
+
Browse repository files.
+
Inspect changesets.
+
View branch topology.
+
`, + }, + { + name: "Textarea", + slug: "textarea", + element: "zen-textarea", + group: "Forms", + description: "A styled native multiline text control.", + markup: ``, + }, + { + name: "Text", + slug: "text", + element: "zen-text", + group: "Typography", + description: "Native text on the canonical xs through xl type scale.", + markup: ` +

Extra small text

+

Small text

+

Medium text

+

Large text

+

Extra large text

+
`, + }, + { + name: "Toast", + slug: "toast", + element: "zen-notifications", + group: "Feedback", + description: "The deprecated toast concept maps to zen-notifications.", + markup: `

Toast is deprecated. Use zen-notifications for scoped transient messages.

`, + }, + { + name: "Toggle", + slug: "toggle", + element: "zen-toggle", + group: "Actions", + description: "A native button with managed pressed state.", + markup: ``, + }, + { + name: "Toggle Group", + slug: "toggle-group", + element: "zen-toggle-group", + group: "Actions", + description: "Single or multiple pressed-state buttons with roving keyboard focus.", + markup: ` + + + +`, + }, + { + name: "Tooltip", + slug: "tooltip", + element: "zen-tooltip", + group: "Overlays", + description: "A delayed, non-interactive description for hover and focus.", + markup: ` + + + + +`, + }, + { + name: "Typography", + slug: "typography", + element: "zen-typography", + group: "Foundations", + description: "Readable defaults for native headings, paragraphs, lists, and code.", + markup: ` +

The Mercurial forge

+

Typography stays semantic and inherits Zenbu tokens.

+
Own the stack, understand the stack.
+
`, + }, +]; + +/** @const {number} Number of component concepts in the catalog. */ +export const componentConceptCount = componentCatalog.length; diff -r 609d3c6aff4e -r 60a876c4587a design_system/src/components/icon.js --- a/design_system/src/components/icon.js Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/src/components/icon.js Wed Aug 05 05:25:40 2026 -0700 @@ -28,7 +28,21 @@ menu: ["M4 6h16", "M4 12h16", "M4 18h16"], moon: ["M20 15a8 8 0 0 1-11-11 9 9 0 1 0 11 11Z"], more: ["M5 12h.01", "M12 12h.01", "M19 12h.01"], + offline: [ + "M5 12a7 7 0 0 1 10.5-6", + "M19 12a7 7 0 0 0-.7-3", + "M8.5 15.5a5 5 0 0 1 7 0", + "M11.5 19h1", + "M3 3l18 18", + ], pause: ["M8 5v14", "M16 5v14"], + paw: [ + "M8 11c-1.7 0-3-1.5-3-3.3S6 4.5 7.7 4.5s2.5 1.5 2.5 3.3S9.7 11 8 11Z", + "M16 11c-1.7 0-2.2-1.5-2.2-3.2s.8-3.3 2.5-3.3S19 6 19 7.7 17.7 11 16 11Z", + "M5.3 15c-1.5 0-2.8-1.2-2.8-2.7s1.2-2.8 2.7-2.8S8 10.7 8 12.2 6.8 15 5.3 15Z", + "M18.7 15c-1.5 0-2.7-1.2-2.7-2.8s1.2-2.7 2.8-2.7 2.7 1.2 2.7 2.8-1.3 2.7-2.8 2.7Z", + "M7 18.5c0-3 2.2-5.5 5-5.5s5 2.5 5 5.5c0 1.5-1.2 2.5-2.7 2.5-.8 0-1.5-.4-2.3-.4s-1.5.4-2.3.4C8.2 21 7 20 7 18.5Z", + ], play: ["m8 5 11 7-11 7Z"], repository: ["M4 4h13a3 3 0 0 1 3 3v13H7a3 3 0 0 1-3-3Z", "M8 4v16"], retry: ["M4 4v6h6", "M5 9a8 8 0 1 1 2 8"], @@ -99,7 +113,10 @@ this.setAttribute("aria-hidden", "true"); } this.dataset.zenIcon = known ? requested : "alert"; - this.style.setProperty("--zen-icon-size", safeSize(this.getAttribute("size"))); + this.style.setProperty( + "--zenbu-icon-size", + safeSize(this.getAttribute("size")), + ); this.replaceChildren(svg); } } diff -r 609d3c6aff4e -r 60a876c4587a design_system/src/components/index.js --- a/design_system/src/components/index.js Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/src/components/index.js Wed Aug 05 05:25:40 2026 -0700 @@ -30,6 +30,7 @@ ZenSwitch, } from "./forms.js"; export { ICON_NAMES, ZenIcon } from "./icon.js"; +export { ZenLink } from "./link.js"; export { ZenNotifications, validateNotification, diff -r 609d3c6aff4e -r 60a876c4587a design_system/src/components/link.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/src/components/link.js Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,53 @@ +/** + * Adds an optional reusable effect to one direct native link. + * + * @extends {HTMLElement} + */ +export class ZenLink extends HTMLElement { + static get observedAttributes() { + return ["effect"]; + } + + connectedCallback() { + this._observer ||= new MutationObserver(() => this.sync()); + this._observer.observe(this, { childList: true }); + this.sync(); + } + + disconnectedCallback() { + this._observer?.disconnect(); + this.removeGeneratedDecoration(); + } + + attributeChangedCallback() { + if (this.isConnected) this.sync(); + } + + removeGeneratedDecoration() { + this.querySelector( + ':scope > a > zen-icon[data-zen-link-decoration][data-zen-generated="true"]', + )?.remove(); + } + + sync() { + const link = this.querySelector(":scope > a"); + if (!link || this.getAttribute("effect") !== "paw") { + this.removeGeneratedDecoration(); + return; + } + let decoration = link.querySelector( + ":scope > zen-icon[data-zen-link-decoration]", + ); + if (!decoration) { + decoration = document.createElement("zen-icon"); + decoration.dataset.zenLinkDecoration = ""; + decoration.dataset.zenGenerated = "true"; + decoration.setAttribute("name", "paw"); + link.prepend(decoration); + } + } +} + +if (!customElements.get("zen-link")) { + customElements.define("zen-link", ZenLink); +} diff -r 609d3c6aff4e -r 60a876c4587a design_system/src/components/notifications.js --- a/design_system/src/components/notifications.js Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/src/components/notifications.js Wed Aug 05 05:25:40 2026 -0700 @@ -13,6 +13,17 @@ (allowEmpty || value.trim().length > 0); } +function resolveCssLength(element, property, fallback) { + const probe = document.createElement("span"); + probe.style.position = "absolute"; + probe.style.inlineSize = `var(${property})`; + probe.style.visibility = "hidden"; + element.append(probe); + const value = Number.parseFloat(getComputedStyle(probe).inlineSize); + probe.remove(); + return Number.isFinite(value) ? value : fallback; +} + /** * Validates and normalizes a notification event record. * @@ -85,7 +96,7 @@ } /** - * Renders a bounded, scoped, Sonner-like notification queue. + * Renders a bounded, scoped notification queue. * * @extends {HTMLElement} */ @@ -496,13 +507,11 @@ const visible = [...this.#records.values()] .slice(-MAX_VISIBLE) .filter(record => record.view?.article.isConnected); - const styles = getComputedStyle(this.stack); - const configuredGap = Number.parseFloat( - styles.getPropertyValue("--zen-notification-gap"), + const gap = resolveCssLength( + this.stack, + "--zenbu-notification-gap", + 12, ); - const gap = Number.isFinite(configuredGap) - ? configuredGap - : 12; let expandedHeight = 0; let newestHeight = 0; @@ -513,24 +522,24 @@ const height = article.offsetHeight; if (depth === 0) newestHeight = height; article.style.setProperty( - "--zen-notification-depth", + "--zenbu-notification-depth", String(depth), ); article.style.setProperty( - "--zen-notification-collapsed-offset", + "--zenbu-notification-collapsed-offset", `${-depth * 10}px`, ); article.style.setProperty( - "--zen-notification-collapsed-opacity", + "--zenbu-notification-collapsed-opacity", String(Math.max(0.55, 1 - depth * 0.18)), ); article.dataset.depth = String(depth); article.style.setProperty( - "--zen-notification-offset", + "--zenbu-notification-offset", `${-expandedHeight}px`, ); article.style.setProperty( - "--zen-notification-scale", + "--zenbu-notification-scale", String(Math.max(0.88, 1 - depth * 0.055)), ); article.style.zIndex = String(visible.length - depth); @@ -539,11 +548,11 @@ this.stack.dataset.count = String(visible.length); this.stack.style.setProperty( - "--zen-notification-collapsed-height", + "--zenbu-notification-collapsed-height", `${newestHeight + Math.max(0, visible.length - 1) * 10}px`, ); this.stack.style.setProperty( - "--zen-notification-expanded-height", + "--zenbu-notification-expanded-height", `${Math.max(0, expandedHeight - gap)}px`, ); } diff -r 609d3c6aff4e -r 60a876c4587a design_system/src/components/primitives.js --- a/design_system/src/components/primitives.js Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/src/components/primitives.js Wed Aug 05 05:25:40 2026 -0700 @@ -6,6 +6,7 @@ "zen-badge", "zen-breadcrumb", "zen-bubble", + "zen-box", "zen-button-group", "zen-chart", "zen-direction", @@ -15,6 +16,7 @@ "zen-item", "zen-kbd", "zen-label", + "zen-heading", "zen-marker", "zen-message", "zen-native-select", @@ -25,6 +27,7 @@ "zen-skeleton", "zen-spinner", "zen-table", + "zen-text", "zen-textarea", "zen-typography", ]; @@ -60,7 +63,7 @@ switch (this.localName) { case "zen-aspect-ratio": this.style.setProperty( - "--zen-aspect-ratio", + "--zenbu-aspect-ratio", parseRatio(this.getAttribute("ratio")), ); break; @@ -118,11 +121,11 @@ case "zen-skeleton": if (this.hasAttribute("width")) { this.style.setProperty( - "--zen-skeleton-width", + "--zenbu-skeleton-width", this.getAttribute("width"), ); } else { - this.style.removeProperty("--zen-skeleton-width"); + this.style.removeProperty("--zenbu-skeleton-width"); } if (!this.hasAttribute("role")) this.setAttribute("aria-hidden", "true"); break; diff -r 609d3c6aff4e -r 60a876c4587a design_system/src/index.html --- a/design_system/src/index.html Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/src/index.html Wed Aug 05 05:25:40 2026 -0700 @@ -8,8 +8,8 @@ - - + +
@@ -50,9 +50,11 @@ Stack - + + +
@@ -175,6 +177,25 @@
+

Size scale

+ + xs · sm · md · lg · xl + --zenbu-sys-control-height-* + --zenbu-sys-control-padding-* + --zenbu-sys-font-size-* / --zenbu-sys-line-height-* + --zenbu-sys-padding-* / --zenbu-sys-icon-size-* + +
+
+

Token layers

+ + --zenbu-ref-* · available values + --zenbu-sys-* · semantic intent + --zenbu-<component>-* · local aliases + +

Components consume semantic intent. Themes and density remap the system layer.

+
+

Shape

--zen-radius-sm @@ -210,6 +231,17 @@ + + +