Mercurial
changeset 258:60a876c4587a
[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 <[email protected]>
line wrap: on
line diff
--- 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-<component>-*` 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.
--- 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:
--- 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__"], )
--- 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); } ```
--- 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 }` |
--- 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.
--- 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 <zen-story name="Primary button"> @@ -54,11 +54,15 @@ <script type="module" src="/design-system/components/index.js"></script> ``` -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 ```
--- /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 +<zen-button size="lg"><button type="button">Publish</button></zen-button> +<zen-input size="sm"><input aria-label="Filter" /></zen-input> +``` + +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 +<zen-heading size="xl"><h2>Repository activity</h2></zen-heading> +<zen-text size="sm"><p>Updated two minutes ago.</p></zen-text> +<zen-box padding="lg">...</zen-box> +```
--- 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; } ```
--- /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-<component>-* +``` + +## 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 +<html data-zen-theme="paper" data-zen-density="compact"> +``` + +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-<component>-*` 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.
--- 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",
--- /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); + } +}
--- 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<!Object>} 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: `<zen-accordion> - <details open><summary>Is it accessible?</summary><p>It keeps native details and summary semantics.</p></details> - <details><summary>Can several open?</summary><p>Add the multiple attribute to allow it.</p></details> -</zen-accordion>`, - }, - { - 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: `<zen-alert-dialog> - <button type="button" data-zen-trigger>Delete deployment</button> - <dialog> - <h3>Delete deployment?</h3> - <p>This action cannot be undone.</p> - <button type="button" data-zen-close>Cancel</button> - <button type="button" value="confirm" data-zen-close>Delete</button> - </dialog> -</zen-alert-dialog>`, - }, - { - name: "Aspect Ratio", - slug: "aspect-ratio", - element: "zen-aspect-ratio", - group: "Layout", - description: "A media frame driven by a readable ratio attribute.", - markup: `<zen-aspect-ratio ratio="16/9"> - <div class="demo-media">16 / 9</div> -</zen-aspect-ratio>`, - }, - { - name: "Attachment", - slug: "attachment", - element: "zen-attachment", - group: "Chat", - description: "File and upload state presentation using ordinary links and buttons.", - markup: `<zen-attachment state="complete"> - <span aria-hidden="true">PDF</span> - <div><strong>release-notes.pdf</strong><small>248 KB · Ready</small></div> - <button type="button" aria-label="Remove attachment"><zen-icon name="close"></zen-icon></button> -</zen-attachment>`, - }, - { - name: "Avatar", - slug: "avatar", - element: "zen-avatar", - group: "Display", - description: "A compact identity image with a visible text fallback.", - markup: `<zen-avatar aria-label="June Park"><span>JP</span></zen-avatar>`, - }, - { - name: "Badge", - slug: "badge", - element: "zen-badge", - group: "Display", - description: "A small label for metadata, state, or counts.", - markup: `<zen-stack direction="row"> - <zen-badge>Default</zen-badge> - <zen-badge tone="success">Healthy</zen-badge> - <zen-badge tone="danger">Failed</zen-badge> -</zen-stack>`, - }, - { - name: "Breadcrumb", - slug: "breadcrumb", - element: "zen-breadcrumb", - group: "Navigation", - description: "A native navigation landmark for hierarchical location.", - markup: `<zen-breadcrumb> - <nav><ol> - <li><a href="/">Home</a></li> - <li><a href="/components">Components</a></li> - <li aria-current="page">Breadcrumb</li> - </ol></nav> -</zen-breadcrumb>`, - }, - { - name: "Bubble", - slug: "bubble", - element: "zen-bubble", - group: "Chat", - description: "A message bubble with incoming and outgoing alignment.", - markup: `<zen-stack> - <zen-bubble><p>The deployment is ready.</p></zen-bubble> - <zen-bubble align="end"><p>Ship it.</p></zen-bubble> -</zen-stack>`, - }, - { - 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: `<zen-button-group aria-label="Text alignment"> - <button type="button">Left</button> - <button type="button">Center</button> - <button type="button">Right</button> -</zen-button-group>`, - }, - { - name: "Calendar", - slug: "calendar", - element: "zen-calendar", - group: "Forms", - description: "A first-party date month grid backed by a native form value.", - markup: `<zen-calendar> - <label for="calendar-demo">Release date</label> - <input id="calendar-demo" type="date" value="2026-08-04" /> -</zen-calendar>`, - }, - { - 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: `<zen-carousel aria-label="Recent projects"> - <div data-zen-viewport> - <article>Mercurial forge</article> - <article>HLS player</article> - <article>LaTeX editor</article> - </div> - <button type="button" data-zen-prev aria-label="Previous slide"><zen-icon name="arrow-left"></zen-icon></button> - <button type="button" data-zen-next aria-label="Next slide"><zen-icon name="arrow-right"></zen-icon></button> -</zen-carousel>`, - }, - { - name: "Chart", - slug: "chart", - element: "zen-chart", - group: "Data", - description: "A dependency-free chart frame for semantic labels and custom marks.", - markup: `<zen-chart aria-label="Build minutes by day"> - <div data-zen-chart-plot> - <span style="--zen-chart-value: 42%" data-label="Mon"></span> - <span style="--zen-chart-value: 76%" data-label="Tue"></span> - <span style="--zen-chart-value: 58%" data-label="Wed"></span> - <span style="--zen-chart-value: 88%" data-label="Thu"></span> - </div> -</zen-chart>`, - }, - { - name: "Checkbox", - slug: "checkbox", - element: "zen-checkbox", - group: "Forms", - description: "A styled native checkbox that remains form-associated.", - markup: `<zen-checkbox> - <label><input type="checkbox" checked /> Include deployment artifacts</label> -</zen-checkbox>`, - }, - { - name: "Collapsible", - slug: "collapsible", - element: "zen-collapsible", - group: "Disclosure", - description: "A single native details disclosure.", - markup: `<zen-collapsible> - <details><summary>Three changed files</summary><p>server.c, styles.css, README.md</p></details> -</zen-collapsible>`, - }, - { - name: "Combobox", - slug: "combobox", - element: "zen-combobox", - group: "Forms", - description: "A filterable text input backed by an accessible option list.", - markup: `<zen-combobox> - <label for="repo-combobox">Repository</label> - <input id="repo-combobox" autocomplete="off" placeholder="Find repository" /> - <div role="listbox"> - <button type="button" role="option" data-value="zenbu">zenbu</button> - <button type="button" role="option" data-value="hg-web">hg-web</button> - <button type="button" role="option" data-value="seobeo">seobeo</button> - </div> -</zen-combobox>`, - }, - { - name: "Command", - slug: "command", - element: "zen-command", - group: "Navigation", - description: "A keyboard-first filterable command list.", - markup: `<zen-command> - <input type="search" aria-label="Commands" placeholder="Type a command" /> - <div role="menu"> - <button type="button" role="menuitem" data-value="new">New repository</button> - <button type="button" role="menuitem" data-value="deploy">Deploy release</button> - <button type="button" role="menuitem" data-value="theme">Toggle theme</button> - </div> -</zen-command>`, - }, - { - name: "Context Menu", - slug: "context-menu", - element: "zen-context-menu", - group: "Menus", - description: "A right-click menu with keyboard navigation and native buttons.", - markup: `<zen-context-menu> - <div data-zen-trigger tabindex="0">Right-click this repository</div> - <div role="menu" hidden> - <button type="button" role="menuitem">Open</button> - <button type="button" role="menuitem">Clone</button> - <button type="button" role="menuitem">Archive</button> - </div> -</zen-context-menu>`, - }, - { - name: "Data Table", - slug: "data-table", - element: "zen-data-table", - group: "Data", - description: "A native table enhanced with optional client-side sorting.", - markup: `<zen-data-table> - <table> - <thead><tr> - <th><button type="button" data-zen-sort="name">Repository</button></th> - <th><button type="button" data-zen-sort="commits">Commits</button></th> - </tr></thead> - <tbody> - <tr><td data-key="name">seobeo</td><td data-key="commits">87</td></tr> - <tr><td data-key="name">hg-web</td><td data-key="commits">142</td></tr> - <tr><td data-key="name">design-system</td><td data-key="commits">12</td></tr> - </tbody> - </table> -</zen-data-table>`, - }, - { - name: "Date Picker", - slug: "date-picker", - element: "zen-date-picker", - group: "Forms", - description: "A tokenized date trigger and first-party keyboard calendar.", - markup: `<zen-date-picker> - <label for="date-picker-demo">Deploy on</label> - <input id="date-picker-demo" type="date" /> -</zen-date-picker>`, - }, - { - name: "Dialog", - slug: "dialog", - element: "zen-dialog", - group: "Overlays", - description: "A native modal dialog with trigger and close wiring.", - markup: `<zen-dialog> - <button type="button" data-zen-trigger>Edit profile</button> - <dialog> - <h3>Edit profile</h3> - <label>Display name <input value="June" /></label> - <button type="button" data-zen-close>Done</button> - </dialog> -</zen-dialog>`, - }, - { - name: "Direction", - slug: "direction", - element: "zen-direction", - group: "Utilities", - description: "A light-DOM boundary for left-to-right or right-to-left content.", - markup: `<zen-direction dir="rtl"><p>مرحبا بالعالم</p></zen-direction>`, - }, - { - name: "Drawer", - slug: "drawer", - element: "zen-drawer", - group: "Overlays", - description: "A native dialog presented as an edge drawer.", - markup: `<zen-drawer placement="bottom"> - <button type="button" data-zen-trigger>Open drawer</button> - <dialog><h3>Build queue</h3><p>Two jobs are running.</p><button type="button" data-zen-close>Close</button></dialog> -</zen-drawer>`, - }, - { - name: "Dropdown Menu", - slug: "dropdown-menu", - element: "zen-dropdown-menu", - group: "Menus", - description: "A button-owned action menu with arrow-key navigation.", - markup: `<zen-dropdown-menu> - <button type="button" data-zen-trigger>Repository actions</button> - <div role="menu" hidden> - <button type="button" role="menuitem">Clone</button> - <button type="button" role="menuitem">Bookmark</button> - <button type="button" role="menuitem">Archive</button> - </div> -</zen-dropdown-menu>`, - }, - { - name: "Empty", - slug: "empty", - element: "zen-empty", - group: "Feedback", - description: "A structured empty state with an optional native action.", - markup: `<zen-empty> - <strong>No deployments yet</strong> - <p>Create the first release from the current default branch.</p> - <button type="button">Create deployment</button> -</zen-empty>`, - }, - { - 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: `<zen-form> - <form> - <zen-field><label>Repository name</label><input required /><small>Required</small></zen-field> - <button type="submit">Create</button> - </form> -</zen-form>`, - }, - { - name: "Hover Card", - slug: "hover-card", - element: "zen-hover-card", - group: "Overlays", - description: "Supplemental content shown on hover or keyboard focus.", - markup: `<zen-hover-card> - <a href="#" data-zen-trigger>@mrjunejune</a> - <div data-zen-content hidden><strong>June Park</strong><p>Building Zenbu in C and native HTML.</p></div> -</zen-hover-card>`, - }, - { - name: "Input", - slug: "input", - element: "zen-input", - group: "Forms", - description: "A styled native input with no replacement semantics.", - markup: `<zen-input><input type="email" placeholder="[email protected]" aria-label="Email" /></zen-input>`, - }, - { - name: "Input Group", - slug: "input-group", - element: "zen-input-group", - group: "Forms", - description: "A native input with prefix, suffix, or inline actions.", - markup: `<zen-input-group> - <span aria-hidden="true">https://</span> - <input aria-label="Host" value="zenbu.babocoder.com" /> - <button type="button">Copy</button> -</zen-input-group>`, - }, - { - name: "Input OTP", - slug: "input-otp", - element: "zen-input-otp", - group: "Forms", - description: "A single native one-time-code input with visual slots.", - markup: `<zen-input-otp> - <label for="otp-demo">Verification code</label> - <input id="otp-demo" inputmode="numeric" autocomplete="one-time-code" maxlength="6" /> - <div data-zen-otp-slots aria-hidden="true"></div> -</zen-input-otp>`, - }, - { - name: "Item", - slug: "item", - element: "zen-item", - group: "Display", - description: "A flexible list row for an icon, content, and actions.", - markup: `<zen-item> - <zen-icon name="repository"></zen-icon> - <div><strong>hg-web</strong><small>Updated two minutes ago</small></div> - <button type="button">Open</button> -</zen-item>`, - }, - { - name: "Kbd", - slug: "kbd", - element: "zen-kbd", - group: "Display", - description: "Keyboard shortcut presentation using native kbd elements.", - markup: `<p>Open commands with <zen-kbd><kbd>Ctrl</kbd><span>+</span><kbd>K</kbd></zen-kbd>.</p>`, - }, - { - name: "Label", - slug: "label", - element: "zen-label", - group: "Forms", - description: "A styled native label that keeps browser association.", - markup: `<zen-label><label for="label-demo">Branch name</label></zen-label> -<zen-input><input id="label-demo" value="default" /></zen-input>`, - }, - { - name: "Marker", - slug: "marker", - element: "zen-marker", - group: "Chat", - description: "An inline system marker for chat and activity streams.", - markup: `<zen-marker><span>Today</span></zen-marker>`, - }, - { - name: "Menubar", - slug: "menubar", - element: "zen-menubar", - group: "Menus", - description: "A horizontal application menu using native buttons and menu roles.", - markup: `<zen-menubar> - <button type="button" data-zen-trigger>File</button> - <div role="menu" hidden><button role="menuitem">New</button><button role="menuitem">Open</button></div> - <button type="button" data-zen-trigger>View</button> - <div role="menu" hidden><button role="menuitem">Graph</button><button role="menuitem">History</button></div> -</zen-menubar>`, - }, - { - name: "Message", - slug: "message", - element: "zen-message", - group: "Chat", - description: "An avatar, author, and content row for conversations.", - markup: `<zen-message> - <zen-avatar aria-label="June"><span>JP</span></zen-avatar> - <div><strong>June</strong><zen-bubble><p>Can we deploy this release?</p></zen-bubble></div> -</zen-message>`, - }, - { - 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: `<zen-message-scroller> - <div data-zen-viewport> - <p>Worker connected.</p><p>Build started.</p><p>Bundle created.</p><p>Deployment complete.</p> - </div> - <button type="button" data-zen-scroll-end hidden>Jump to latest</button> -</zen-message-scroller>`, - }, - { - name: "Native Select", - slug: "native-select", - element: "zen-native-select", - group: "Forms", - description: "A styled native select with full platform behavior.", - markup: `<zen-native-select><select aria-label="Environment"><option>Development</option><option>Production</option></select></zen-native-select>`, - }, - { - name: "Navigation Menu", - slug: "navigation-menu", - element: "zen-navigation-menu", - group: "Navigation", - description: "Top-level navigation with optional keyboard-accessible flyouts.", - markup: `<zen-navigation-menu> - <nav aria-label="Products"> - <button type="button" data-zen-trigger>Projects</button> - <div role="menu" hidden><a role="menuitem" href="/components">Zenbu UI</a><a role="menuitem" href="/">hg-web</a></div> - <a href="/tokens">Tokens</a> - </nav> -</zen-navigation-menu>`, - }, - { - name: "Pagination", - slug: "pagination", - element: "zen-pagination", - group: "Navigation", - description: "Page navigation built from native links.", - markup: `<zen-pagination> - <nav><a href="#">Previous</a><a href="#">1</a><a href="#" aria-current="page">2</a><a href="#">3</a><a href="#">Next</a></nav> -</zen-pagination>`, - }, - { - name: "Popover", - slug: "popover", - element: "zen-popover", - group: "Overlays", - description: "A lightweight panel using the native Popover API when available.", - markup: `<zen-popover> - <button type="button" data-zen-trigger>Open settings</button> - <div data-zen-content><strong>Dimensions</strong><label>Width <input value="100%" /></label></div> -</zen-popover>`, - }, - { - name: "Progress", - slug: "progress", - element: "zen-progress", - group: "Feedback", - description: "A styled native progress element.", - markup: `<zen-progress><label for="progress-demo">Uploading</label><progress id="progress-demo" value="68" max="100">68%</progress></zen-progress>`, - }, - { - name: "Radio Group", - slug: "radio-group", - element: "zen-radio-group", - group: "Forms", - description: "A fieldset of native radio inputs.", - markup: `<zen-radio-group> - <fieldset><legend>Visibility</legend> - <label><input type="radio" name="visibility" checked /> Public</label> - <label><input type="radio" name="visibility" /> Private</label> - </fieldset> -</zen-radio-group>`, - }, - { - name: "Resizable", - slug: "resizable", - element: "zen-resizable", - group: "Layout", - description: "Two native panels separated by a pointer and keyboard handle.", - markup: `<zen-resizable> - <section data-zen-panel>Repository tree</section> - <div data-zen-handle tabindex="0"></div> - <section data-zen-panel>File preview</section> -</zen-resizable>`, - }, - { - name: "Scroll Area", - slug: "scroll-area", - element: "zen-scroll-area", - group: "Layout", - description: "A bounded native scroll container with theme-aware scrollbars.", - markup: `<zen-scroll-area tabindex="0"> - <p>default</p><p>feature/design-system</p><p>release/2026-08</p><p>experiment/trading</p> -</zen-scroll-area>`, - }, - { - name: "Select", - slug: "select", - element: "zen-select", - group: "Forms", - description: "A native select styled as the default accessible implementation.", - markup: `<zen-select><label>Branch <select><option>default</option><option>stable</option></select></label></zen-select>`, - }, - { - name: "Separator", - slug: "separator", - element: "zen-separator", - group: "Layout", - description: "A native horizontal rule or ARIA separator.", - markup: `<div>Repository</div><zen-separator><hr /></zen-separator><div>Changesets</div>`, - }, - { - name: "Sheet", - slug: "sheet", - element: "zen-sheet", - group: "Overlays", - description: "A native dialog presented from a selected screen edge.", - markup: `<zen-sheet placement="right"> - <button type="button" data-zen-trigger>Open filters</button> - <dialog><h3>Filters</h3><label>Author <input /></label><button type="button" data-zen-close>Apply</button></dialog> -</zen-sheet>`, - }, - { - name: "Sidebar", - slug: "sidebar", - element: "zen-sidebar", - group: "Navigation", - description: "A collapsible navigation region with an explicit native trigger.", - markup: `<zen-sidebar open> - <button type="button" data-zen-sidebar-trigger>Toggle sidebar</button> - <aside data-zen-sidebar-panel><nav><a href="/">Overview</a><a href="/components">Components</a><a href="/tokens">Tokens</a></nav></aside> -</zen-sidebar>`, - }, - { - name: "Skeleton", - slug: "skeleton", - element: "zen-skeleton", - group: "Feedback", - description: "A reduced-motion-safe placeholder for pending content.", - markup: `<zen-stack> - <zen-skeleton shape="circle"></zen-skeleton> - <zen-skeleton></zen-skeleton> - <zen-skeleton width="60%"></zen-skeleton> -</zen-stack>`, - }, - { - name: "Slider", - slug: "slider", - element: "zen-slider", - group: "Forms", - description: "A native range input with browser keyboard and pointer behavior.", - markup: `<zen-slider><label for="slider-demo">Volume</label><input id="slider-demo" type="range" value="65" /></zen-slider>`, - }, - { - name: "Sonner", - slug: "sonner", - element: "zen-notifications", - group: "Feedback", - description: "Zenbu's bounded, scoped, Sonner-like notification stack.", - markup: `<zen-notifications aria-label="Sonner demo"> - <button type="button" data-notification-demo data-notification-id="sonner-demo" data-tone="success" data-message="Deployment complete">Show notification</button> -</zen-notifications>`, - }, - { - name: "Spinner", - slug: "spinner", - element: "zen-spinner", - group: "Feedback", - description: "A small CSS loading indicator with status semantics.", - markup: `<zen-spinner aria-label="Loading repositories"></zen-spinner>`, - }, - { - name: "Switch", - slug: "switch", - element: "zen-switch", - group: "Forms", - description: "A native checkbox presented with switch semantics.", - markup: `<zen-switch><label><input type="checkbox" checked /> Automatic deployment</label></zen-switch>`, - }, - { - name: "Table", - slug: "table", - element: "zen-table", - group: "Data", - description: "Responsive styling around a native HTML table.", - markup: `<zen-table><table> - <caption>Recent changesets</caption> - <thead><tr><th>Revision</th><th>Author</th></tr></thead> - <tbody><tr><td>253:fdf3816959cb</td><td>June</td></tr><tr><td>252:7a7581f040e8</td><td>June</td></tr></tbody> -</table></zen-table>`, - }, - { - name: "Tabs", - slug: "tabs", - element: "zen-tabs", - group: "Disclosure", - description: "ARIA tabs with automatic or manual keyboard activation.", - markup: `<zen-tabs> - <div role="tablist" aria-label="Repository views"> - <button type="button" role="tab" data-value="files">Files</button> - <button type="button" role="tab" data-value="history">History</button> - <button type="button" role="tab" data-value="graph">Graph</button> - </div> - <section role="tabpanel">Browse repository files.</section> - <section role="tabpanel">Inspect changesets.</section> - <section role="tabpanel">View branch topology.</section> -</zen-tabs>`, - }, - { - name: "Textarea", - slug: "textarea", - element: "zen-textarea", - group: "Forms", - description: "A styled native multiline text control.", - markup: `<zen-textarea><textarea aria-label="Commit message" placeholder="Describe this change"></textarea></zen-textarea>`, - }, - { - name: "Toast", - slug: "toast", - element: "zen-notifications", - group: "Feedback", - description: "The deprecated toast concept maps to zen-notifications.", - markup: `<zen-alert tone="warning"><p><strong>Toast is deprecated.</strong> Use <code>zen-notifications</code> for scoped transient messages.</p></zen-alert>`, - }, - { - name: "Toggle", - slug: "toggle", - element: "zen-toggle", - group: "Actions", - description: "A native button with managed pressed state.", - markup: `<zen-toggle><button type="button">Bold</button></zen-toggle>`, - }, - { - name: "Toggle Group", - slug: "toggle-group", - element: "zen-toggle-group", - group: "Actions", - description: "Single or multiple pressed-state buttons with roving keyboard focus.", - markup: `<zen-toggle-group type="single" value="center" aria-label="Alignment"> - <button type="button" value="left">Left</button> - <button type="button" value="center">Center</button> - <button type="button" value="right">Right</button> -</zen-toggle-group>`, - }, - { - name: "Tooltip", - slug: "tooltip", - element: "zen-tooltip", - group: "Overlays", - description: "A delayed, non-interactive description for hover and focus.", - markup: `<zen-tooltip> - <button type="button" data-zen-trigger aria-label="Add repository"><zen-icon name="add"></zen-icon></button> - <span data-zen-content hidden>Add repository</span> -</zen-tooltip>`, - }, - { - name: "Typography", - slug: "typography", - element: "zen-typography", - group: "Foundations", - description: "Readable defaults for native headings, paragraphs, lists, and code.", - markup: `<zen-typography> - <h2>The Mercurial forge</h2> - <p>Typography stays semantic and inherits Zenbu tokens.</p> - <blockquote>Own the stack, understand the stack.</blockquote> -</zen-typography>`, - }, -]; +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); +});
--- /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<!Object>} 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: `<zen-accordion> + <details open><summary>Is it accessible?</summary><p>It keeps native details and summary semantics.</p></details> + <details><summary>Can several open?</summary><p>Add the multiple attribute to allow it.</p></details> +</zen-accordion>`, + }, + { + 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: `<zen-alert-dialog> + <zen-button size="md" variant="danger"> + <button type="button" data-zen-trigger>Delete deployment</button> + </zen-button> + <dialog> + <h3>Delete deployment?</h3> + <p>This action cannot be undone.</p> + <zen-button size="md" variant="quiet"> + <button type="button" data-zen-close>Cancel</button> + </zen-button> + <zen-button size="md" variant="danger"> + <button type="button" value="confirm" data-zen-close>Delete</button> + </zen-button> + </dialog> +</zen-alert-dialog>`, + }, + { + name: "Aspect Ratio", + slug: "aspect-ratio", + element: "zen-aspect-ratio", + group: "Layout", + description: "A media frame driven by a readable ratio attribute.", + markup: `<zen-aspect-ratio ratio="16/9"> + <div class="demo-media">16 / 9</div> +</zen-aspect-ratio>`, + }, + { + name: "Attachment", + slug: "attachment", + element: "zen-attachment", + group: "Chat", + description: "File and upload state presentation using ordinary links and buttons.", + markup: `<zen-attachment state="complete"> + <span aria-hidden="true">PDF</span> + <div><strong>release-notes.pdf</strong><small>248 KB · Ready</small></div> + <button type="button" aria-label="Remove attachment"><zen-icon name="close"></zen-icon></button> +</zen-attachment>`, + }, + { + name: "Avatar", + slug: "avatar", + element: "zen-avatar", + group: "Display", + description: "A compact identity image with a visible text fallback.", + markup: `<zen-avatar aria-label="June Park"><span>JP</span></zen-avatar>`, + }, + { + name: "Badge", + slug: "badge", + element: "zen-badge", + group: "Display", + description: "A small label for metadata, state, or counts.", + markup: `<zen-stack direction="row"> + <zen-badge>Default</zen-badge> + <zen-badge tone="success">Healthy</zen-badge> + <zen-badge tone="danger">Failed</zen-badge> +</zen-stack>`, + }, + { + name: "Breadcrumb", + slug: "breadcrumb", + element: "zen-breadcrumb", + group: "Navigation", + description: "A native navigation landmark for hierarchical location.", + markup: `<zen-breadcrumb> + <nav><ol> + <li><a href="/">Home</a></li> + <li><a href="/components">Components</a></li> + <li aria-current="page">Breadcrumb</li> + </ol></nav> +</zen-breadcrumb>`, + }, + { + name: "Bubble", + slug: "bubble", + element: "zen-bubble", + group: "Chat", + description: "A message bubble with incoming and outgoing alignment.", + markup: `<zen-stack> + <zen-bubble><p>The deployment is ready.</p></zen-bubble> + <zen-bubble align="end"><p>Ship it.</p></zen-bubble> +</zen-stack>`, + }, + { + name: "Box", + slug: "box", + element: "zen-box", + group: "Layout", + description: "A container with canonical xs through xl padding.", + markup: `<zen-stack direction="row"> + <zen-box padding="xs">XS padding</zen-box> + <zen-box padding="sm">SM padding</zen-box> + <zen-box padding="md">MD padding</zen-box> + <zen-box padding="lg">LG padding</zen-box> + <zen-box padding="xl">XL padding</zen-box> +</zen-stack>`, + }, + { + 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: `<zen-button-group aria-label="Text alignment"> + <button type="button">Left</button> + <button type="button">Center</button> + <button type="button">Right</button> +</zen-button-group>`, + }, + { + name: "Calendar", + slug: "calendar", + element: "zen-calendar", + group: "Forms", + description: "A first-party date month grid backed by a native form value.", + markup: `<zen-calendar> + <label for="calendar-demo">Release date</label> + <input id="calendar-demo" type="date" value="2026-08-04" /> +</zen-calendar>`, + }, + { + 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: `<zen-carousel aria-label="Recent projects"> + <div data-zen-viewport> + <article>Mercurial forge</article> + <article>HLS player</article> + <article>LaTeX editor</article> + </div> + <button type="button" data-zen-prev aria-label="Previous slide"><zen-icon name="arrow-left"></zen-icon></button> + <button type="button" data-zen-next aria-label="Next slide"><zen-icon name="arrow-right"></zen-icon></button> +</zen-carousel>`, + }, + { + name: "Chart", + slug: "chart", + element: "zen-chart", + group: "Data", + description: "A dependency-free chart frame for semantic labels and custom marks.", + markup: `<zen-chart aria-label="Build minutes by day"> + <div data-zen-chart-plot> + <span style="--zen-chart-value: 42%" data-label="Mon"></span> + <span style="--zen-chart-value: 76%" data-label="Tue"></span> + <span style="--zen-chart-value: 58%" data-label="Wed"></span> + <span style="--zen-chart-value: 88%" data-label="Thu"></span> + </div> +</zen-chart>`, + }, + { + name: "Checkbox", + slug: "checkbox", + element: "zen-checkbox", + group: "Forms", + description: "A styled native checkbox that remains form-associated.", + markup: `<zen-checkbox> + <label><input type="checkbox" checked /> Include deployment artifacts</label> +</zen-checkbox>`, + }, + { + name: "Collapsible", + slug: "collapsible", + element: "zen-collapsible", + group: "Disclosure", + description: "A single native details disclosure.", + markup: `<zen-collapsible> + <details><summary>Three changed files</summary><p>server.c, styles.css, README.md</p></details> +</zen-collapsible>`, + }, + { + name: "Combobox", + slug: "combobox", + element: "zen-combobox", + group: "Forms", + description: "A filterable text input backed by an accessible option list.", + markup: `<zen-combobox> + <label for="repo-combobox">Repository</label> + <input id="repo-combobox" autocomplete="off" placeholder="Find repository" /> + <div role="listbox"> + <button type="button" role="option" data-value="zenbu">zenbu</button> + <button type="button" role="option" data-value="hg-web">hg-web</button> + <button type="button" role="option" data-value="seobeo">seobeo</button> + </div> +</zen-combobox>`, + }, + { + name: "Command", + slug: "command", + element: "zen-command", + group: "Navigation", + description: "A keyboard-first filterable command list.", + markup: `<zen-command> + <input type="search" aria-label="Commands" placeholder="Type a command" /> + <div role="menu"> + <button type="button" role="menuitem" data-value="new">New repository</button> + <button type="button" role="menuitem" data-value="deploy">Deploy release</button> + <button type="button" role="menuitem" data-value="theme">Toggle theme</button> + </div> +</zen-command>`, + }, + { + name: "Context Menu", + slug: "context-menu", + element: "zen-context-menu", + group: "Menus", + description: "A right-click menu with keyboard navigation and native buttons.", + markup: `<zen-context-menu> + <div data-zen-trigger tabindex="0">Right-click this repository</div> + <div role="menu" hidden> + <button type="button" role="menuitem">Open</button> + <button type="button" role="menuitem">Clone</button> + <button type="button" role="menuitem">Archive</button> + </div> +</zen-context-menu>`, + }, + { + name: "Data Table", + slug: "data-table", + element: "zen-data-table", + group: "Data", + description: "A native table enhanced with optional client-side sorting.", + markup: `<zen-data-table> + <table> + <thead><tr> + <th><button type="button" data-zen-sort="name">Repository</button></th> + <th><button type="button" data-zen-sort="commits">Commits</button></th> + </tr></thead> + <tbody> + <tr><td data-key="name">seobeo</td><td data-key="commits">87</td></tr> + <tr><td data-key="name">hg-web</td><td data-key="commits">142</td></tr> + <tr><td data-key="name">design-system</td><td data-key="commits">12</td></tr> + </tbody> + </table> +</zen-data-table>`, + }, + { + name: "Date Picker", + slug: "date-picker", + element: "zen-date-picker", + group: "Forms", + description: "A tokenized date trigger and first-party keyboard calendar.", + markup: `<zen-date-picker> + <label for="date-picker-demo">Deploy on</label> + <input id="date-picker-demo" type="date" /> +</zen-date-picker>`, + }, + { + name: "Dialog", + slug: "dialog", + element: "zen-dialog", + group: "Overlays", + description: "A native modal dialog with trigger and close wiring.", + markup: `<zen-dialog> + <zen-button size="md"> + <button type="button" data-zen-trigger>Edit profile</button> + </zen-button> + <dialog> + <h3>Edit profile</h3> + <zen-field> + <label>Display name</label> + <input value="June" /> + </zen-field> + <zen-button size="md"> + <button type="button" data-zen-close>Done</button> + </zen-button> + </dialog> +</zen-dialog>`, + }, + { + name: "Direction", + slug: "direction", + element: "zen-direction", + group: "Utilities", + description: "A light-DOM boundary for left-to-right or right-to-left content.", + markup: `<zen-direction dir="rtl"><p>مرحبا بالعالم</p></zen-direction>`, + }, + { + name: "Drawer", + slug: "drawer", + element: "zen-drawer", + group: "Overlays", + description: "A native dialog presented as an edge drawer.", + markup: `<zen-drawer placement="bottom"> + <zen-button size="md"> + <button type="button" data-zen-trigger>Open drawer</button> + </zen-button> + <dialog> + <h3>Build queue</h3> + <p>Two jobs are running.</p> + <zen-button size="md"> + <button type="button" data-zen-close>Close</button> + </zen-button> + </dialog> +</zen-drawer>`, + }, + { + name: "Dropdown Menu", + slug: "dropdown-menu", + element: "zen-dropdown-menu", + group: "Menus", + description: "A button-owned action menu with arrow-key navigation.", + markup: `<zen-dropdown-menu> + <button type="button" data-zen-trigger>Repository actions</button> + <div role="menu" hidden> + <button type="button" role="menuitem">Clone</button> + <button type="button" role="menuitem">Bookmark</button> + <button type="button" role="menuitem">Archive</button> + </div> +</zen-dropdown-menu>`, + }, + { + name: "Empty", + slug: "empty", + element: "zen-empty", + group: "Feedback", + description: "A structured empty state with an optional native action.", + markup: `<zen-empty> + <strong>No deployments yet</strong> + <p>Create the first release from the current default branch.</p> + <zen-button size="md"><button type="button">Create deployment</button></zen-button> +</zen-empty>`, + }, + { + 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: `<zen-form> + <form> + <zen-field><label>Repository name</label><input required /><small>Required</small></zen-field> + <zen-button size="md"><button type="submit">Create</button></zen-button> + </form> +</zen-form>`, + }, + { + name: "Hover Card", + slug: "hover-card", + element: "zen-hover-card", + group: "Overlays", + description: "Supplemental content shown on hover or keyboard focus.", + markup: `<zen-hover-card> + <a href="#" data-zen-trigger>@mrjunejune</a> + <div data-zen-content hidden><strong>June Park</strong><p>Building Zenbu in C and native HTML.</p></div> +</zen-hover-card>`, + }, + { + name: "Heading", + slug: "heading", + element: "zen-heading", + group: "Typography", + description: "Strong native headings on the canonical type scale.", + markup: `<zen-stack> + <zen-heading size="xs"><h3>Extra small heading</h3></zen-heading> + <zen-heading size="sm"><h3>Small heading</h3></zen-heading> + <zen-heading size="md"><h3>Medium heading</h3></zen-heading> + <zen-heading size="lg"><h3>Large heading</h3></zen-heading> + <zen-heading size="xl"><h3>Extra large heading</h3></zen-heading> +</zen-stack>`, + }, + { + name: "Input", + slug: "input", + element: "zen-input", + group: "Forms", + description: "A styled native input with no replacement semantics.", + markup: `<zen-input><input type="email" placeholder="[email protected]" aria-label="Email" /></zen-input>`, + }, + { + name: "Input Group", + slug: "input-group", + element: "zen-input-group", + group: "Forms", + description: "A native input with prefix, suffix, or inline actions.", + markup: `<zen-input-group> + <span aria-hidden="true">https://</span> + <input aria-label="Host" value="zenbu.babocoder.com" /> + <button type="button">Copy</button> +</zen-input-group>`, + }, + { + name: "Input OTP", + slug: "input-otp", + element: "zen-input-otp", + group: "Forms", + description: "A single native one-time-code input with visual slots.", + markup: `<zen-input-otp> + <label for="otp-demo">Verification code</label> + <input id="otp-demo" inputmode="numeric" autocomplete="one-time-code" maxlength="6" /> + <div data-zen-otp-slots aria-hidden="true"></div> +</zen-input-otp>`, + }, + { + name: "Item", + slug: "item", + element: "zen-item", + group: "Display", + description: "A flexible list row for an icon, content, and actions.", + markup: `<zen-item> + <zen-icon name="repository"></zen-icon> + <div><strong>hg-web</strong><small>Updated two minutes ago</small></div> + <zen-button size="sm" variant="quiet"><button type="button">Open</button></zen-button> +</zen-item>`, + }, + { + name: "Kbd", + slug: "kbd", + element: "zen-kbd", + group: "Display", + description: "Keyboard shortcut presentation using native kbd elements.", + markup: `<p>Open commands with <zen-kbd><kbd>Ctrl</kbd><span>+</span><kbd>K</kbd></zen-kbd>.</p>`, + }, + { + name: "Label", + slug: "label", + element: "zen-label", + group: "Forms", + description: "A styled native label that keeps browser association.", + markup: `<zen-label><label for="label-demo">Branch name</label></zen-label> +<zen-input><input id="label-demo" value="default" /></zen-input>`, + }, + { + name: "Link", + slug: "link", + element: "zen-link", + group: "Navigation", + description: "A native link with optional reusable first-party effects.", + markup: `<zen-link effect="paw"> + <a href="/components/link">A link with the paw effect</a> +</zen-link>`, + }, + { + name: "Marker", + slug: "marker", + element: "zen-marker", + group: "Chat", + description: "An inline system marker for chat and activity streams.", + markup: `<zen-marker><span>Today</span></zen-marker>`, + }, + { + name: "Menubar", + slug: "menubar", + element: "zen-menubar", + group: "Menus", + description: "A horizontal application menu using native buttons and menu roles.", + markup: `<zen-menubar> + <button type="button" data-zen-trigger>File</button> + <div role="menu" hidden><button role="menuitem">New</button><button role="menuitem">Open</button></div> + <button type="button" data-zen-trigger>View</button> + <div role="menu" hidden><button role="menuitem">Graph</button><button role="menuitem">History</button></div> +</zen-menubar>`, + }, + { + name: "Message", + slug: "message", + element: "zen-message", + group: "Chat", + description: "An avatar, author, and content row for conversations.", + markup: `<zen-message> + <zen-avatar aria-label="June"><span>JP</span></zen-avatar> + <div><strong>June</strong><zen-bubble><p>Can we deploy this release?</p></zen-bubble></div> +</zen-message>`, + }, + { + 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: `<zen-message-scroller> + <div data-zen-viewport> + <p>Worker connected.</p><p>Build started.</p><p>Bundle created.</p><p>Deployment complete.</p> + </div> + <button type="button" data-zen-scroll-end hidden>Jump to latest</button> +</zen-message-scroller>`, + }, + { + name: "Native Select", + slug: "native-select", + element: "zen-native-select", + group: "Forms", + description: "A styled native select with full platform behavior.", + markup: `<zen-native-select><select aria-label="Environment"><option>Development</option><option>Production</option></select></zen-native-select>`, + }, + { + name: "Navigation Menu", + slug: "navigation-menu", + element: "zen-navigation-menu", + group: "Navigation", + description: "Top-level navigation with optional keyboard-accessible flyouts.", + markup: `<zen-navigation-menu> + <nav aria-label="Products"> + <button type="button" data-zen-trigger>Projects</button> + <div role="menu" hidden><a role="menuitem" href="/components">Zenbu UI</a><a role="menuitem" href="/">hg-web</a></div> + <a href="/tokens">Tokens</a> + </nav> +</zen-navigation-menu>`, + }, + { + name: "Pagination", + slug: "pagination", + element: "zen-pagination", + group: "Navigation", + description: "Page navigation built from native links.", + markup: `<zen-pagination> + <nav><a href="#">Previous</a><a href="#">1</a><a href="#" aria-current="page">2</a><a href="#">3</a><a href="#">Next</a></nav> +</zen-pagination>`, + }, + { + name: "Popover", + slug: "popover", + element: "zen-popover", + group: "Overlays", + description: "A lightweight panel using the native Popover API when available.", + markup: `<zen-popover> + <zen-button size="md"> + <button type="button" data-zen-trigger>Open settings</button> + </zen-button> + <div data-zen-content> + <strong>Dimensions</strong> + <zen-field><label>Width</label><input value="100%" /></zen-field> + </div> +</zen-popover>`, + }, + { + name: "Progress", + slug: "progress", + element: "zen-progress", + group: "Feedback", + description: "A styled native progress element.", + markup: `<zen-progress><label for="progress-demo">Uploading</label><progress id="progress-demo" value="68" max="100">68%</progress></zen-progress>`, + }, + { + name: "Radio Group", + slug: "radio-group", + element: "zen-radio-group", + group: "Forms", + description: "A fieldset of native radio inputs.", + markup: `<zen-radio-group> + <fieldset><legend>Visibility</legend> + <label><input type="radio" name="visibility" checked /> Public</label> + <label><input type="radio" name="visibility" /> Private</label> + </fieldset> +</zen-radio-group>`, + }, + { + name: "Resizable", + slug: "resizable", + element: "zen-resizable", + group: "Layout", + description: "Two native panels separated by a pointer and keyboard handle.", + markup: `<zen-resizable> + <section data-zen-panel>Repository tree</section> + <div data-zen-handle tabindex="0"></div> + <section data-zen-panel>File preview</section> +</zen-resizable>`, + }, + { + name: "Scroll Area", + slug: "scroll-area", + element: "zen-scroll-area", + group: "Layout", + description: "A bounded native scroll container with theme-aware scrollbars.", + markup: `<zen-scroll-area tabindex="0"> + <p>default</p><p>feature/design-system</p><p>release/2026-08</p><p>experiment/trading</p> +</zen-scroll-area>`, + }, + { + name: "Select", + slug: "select", + element: "zen-select", + group: "Forms", + description: "A native select styled as the default accessible implementation.", + markup: `<zen-select><label>Branch <select><option>default</option><option>stable</option></select></label></zen-select>`, + }, + { + name: "Separator", + slug: "separator", + element: "zen-separator", + group: "Layout", + description: "A native horizontal rule or ARIA separator.", + markup: `<div>Repository</div><zen-separator><hr /></zen-separator><div>Changesets</div>`, + }, + { + name: "Sheet", + slug: "sheet", + element: "zen-sheet", + group: "Overlays", + description: "A native dialog presented from a selected screen edge.", + markup: `<zen-sheet placement="right"> + <zen-button size="md"> + <button type="button" data-zen-trigger>Open filters</button> + </zen-button> + <dialog> + <h3>Filters</h3> + <zen-field><label>Author</label><input /></zen-field> + <zen-button size="md"> + <button type="button" data-zen-close>Apply</button> + </zen-button> + </dialog> +</zen-sheet>`, + }, + { + name: "Sidebar", + slug: "sidebar", + element: "zen-sidebar", + group: "Navigation", + description: "A collapsible navigation region with an explicit native trigger.", + markup: `<zen-sidebar open> + <button type="button" data-zen-sidebar-trigger>Toggle sidebar</button> + <aside data-zen-sidebar-panel><nav><a href="/">Overview</a><a href="/components">Components</a><a href="/tokens">Tokens</a></nav></aside> +</zen-sidebar>`, + }, + { + name: "Skeleton", + slug: "skeleton", + element: "zen-skeleton", + group: "Feedback", + description: "A reduced-motion-safe placeholder for pending content.", + markup: `<zen-stack> + <zen-skeleton shape="circle"></zen-skeleton> + <zen-skeleton></zen-skeleton> + <zen-skeleton width="60%"></zen-skeleton> +</zen-stack>`, + }, + { + name: "Slider", + slug: "slider", + element: "zen-slider", + group: "Forms", + description: "A native range input with browser keyboard and pointer behavior.", + markup: `<zen-slider><label for="slider-demo">Volume</label><input id="slider-demo" type="range" value="65" /></zen-slider>`, + }, + { + 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: `<zen-spinner aria-label="Loading repositories"></zen-spinner>`, + }, + { + name: "Switch", + slug: "switch", + element: "zen-switch", + group: "Forms", + description: "A native checkbox presented with switch semantics.", + markup: `<zen-switch><label><input type="checkbox" checked /> Automatic deployment</label></zen-switch>`, + }, + { + name: "Table", + slug: "table", + element: "zen-table", + group: "Data", + description: "Responsive styling around a native HTML table.", + markup: `<zen-table><table> + <caption>Recent changesets</caption> + <thead><tr><th>Revision</th><th>Author</th></tr></thead> + <tbody><tr><td>253:fdf3816959cb</td><td>June</td></tr><tr><td>252:7a7581f040e8</td><td>June</td></tr></tbody> +</table></zen-table>`, + }, + { + name: "Tabs", + slug: "tabs", + element: "zen-tabs", + group: "Disclosure", + description: "ARIA tabs with automatic or manual keyboard activation.", + markup: `<zen-tabs> + <div role="tablist" aria-label="Repository views"> + <button type="button" role="tab" data-value="files">Files</button> + <button type="button" role="tab" data-value="history">History</button> + <button type="button" role="tab" data-value="graph">Graph</button> + </div> + <section role="tabpanel">Browse repository files.</section> + <section role="tabpanel">Inspect changesets.</section> + <section role="tabpanel">View branch topology.</section> +</zen-tabs>`, + }, + { + name: "Textarea", + slug: "textarea", + element: "zen-textarea", + group: "Forms", + description: "A styled native multiline text control.", + markup: `<zen-textarea><textarea aria-label="Commit message" placeholder="Describe this change"></textarea></zen-textarea>`, + }, + { + name: "Text", + slug: "text", + element: "zen-text", + group: "Typography", + description: "Native text on the canonical xs through xl type scale.", + markup: `<zen-stack> + <zen-text size="xs"><p>Extra small text</p></zen-text> + <zen-text size="sm"><p>Small text</p></zen-text> + <zen-text size="md"><p>Medium text</p></zen-text> + <zen-text size="lg"><p>Large text</p></zen-text> + <zen-text size="xl"><p>Extra large text</p></zen-text> +</zen-stack>`, + }, + { + name: "Toast", + slug: "toast", + element: "zen-notifications", + group: "Feedback", + description: "The deprecated toast concept maps to zen-notifications.", + markup: `<zen-alert tone="warning"><p><strong>Toast is deprecated.</strong> Use <code>zen-notifications</code> for scoped transient messages.</p></zen-alert>`, + }, + { + name: "Toggle", + slug: "toggle", + element: "zen-toggle", + group: "Actions", + description: "A native button with managed pressed state.", + markup: `<zen-toggle><button type="button">Bold</button></zen-toggle>`, + }, + { + name: "Toggle Group", + slug: "toggle-group", + element: "zen-toggle-group", + group: "Actions", + description: "Single or multiple pressed-state buttons with roving keyboard focus.", + markup: `<zen-toggle-group type="single" value="center" aria-label="Alignment"> + <button type="button" value="left">Left</button> + <button type="button" value="center">Center</button> + <button type="button" value="right">Right</button> +</zen-toggle-group>`, + }, + { + name: "Tooltip", + slug: "tooltip", + element: "zen-tooltip", + group: "Overlays", + description: "A delayed, non-interactive description for hover and focus.", + markup: `<zen-tooltip> + <zen-button size="sm" variant="quiet"> + <button type="button" data-zen-trigger aria-label="Add repository"><zen-icon name="add"></zen-icon></button> + </zen-button> + <span data-zen-content hidden>Add repository</span> +</zen-tooltip>`, + }, + { + name: "Typography", + slug: "typography", + element: "zen-typography", + group: "Foundations", + description: "Readable defaults for native headings, paragraphs, lists, and code.", + markup: `<zen-typography> + <h2>The Mercurial forge</h2> + <p>Typography stays semantic and inherits Zenbu tokens.</p> + <blockquote>Own the stack, understand the stack.</blockquote> +</zen-typography>`, + }, +]; + +/** @const {number} Number of component concepts in the catalog. */ +export const componentConceptCount = componentCatalog.length;
--- 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); } }
--- 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,
--- /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); +}
--- 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`, ); }
--- 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;
--- 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 @@ <link rel="stylesheet" href="/styles/tokens.css" /> <link rel="stylesheet" href="/styles/themes.css" /> <link rel="stylesheet" href="/styles/components.css" /> - <link rel="stylesheet" href="/storybook.css" /> - <script type="module" src="/storybook.js"></script> + <link rel="stylesheet" href="/catalog.css" /> + <script type="module" src="/catalog.js"></script> </head> <body> <div class="catalog-shell"> @@ -50,9 +50,11 @@ <a href="/components/stack" data-catalog-link>Stack</a> </nav> - <button id="themeToggle" class="theme-toggle" type="button"> - Toggle theme - </button> + <zen-button class="theme-toggle-wrap" appearance="plain" size="sm"> + <button id="themeToggle" class="theme-toggle" type="button"> + Toggle theme + </button> + </zen-button> </aside> <main id="catalogMain" class="catalog-main" tabindex="-1"> @@ -175,6 +177,25 @@ </zen-stack> </section> <section class="token-group"> + <h2>Size scale</h2> + <zen-stack gap="tight"> + <code>xs · sm · md · lg · xl</code> + <code>--zenbu-sys-control-height-*</code> + <code>--zenbu-sys-control-padding-*</code> + <code>--zenbu-sys-font-size-* / --zenbu-sys-line-height-*</code> + <code>--zenbu-sys-padding-* / --zenbu-sys-icon-size-*</code> + </zen-stack> + </section> + <section class="token-group token-group-wide"> + <h2>Token layers</h2> + <zen-stack direction="row"> + <code>--zenbu-ref-* · available values</code> + <code>--zenbu-sys-* · semantic intent</code> + <code>--zenbu-<component>-* · local aliases</code> + </zen-stack> + <p>Components consume semantic intent. Themes and density remap the system layer.</p> + </section> + <section class="token-group"> <h2>Shape</h2> <zen-stack gap="tight"> <code>--zen-radius-sm</code> @@ -210,6 +231,17 @@ </zen-stack> </template> </zen-story> + <zen-story name="Sizes"> + <template> + <zen-stack direction="row"> + <zen-button size="xs"><button type="button">Extra small</button></zen-button> + <zen-button size="sm"><button type="button">Small</button></zen-button> + <zen-button size="md"><button type="button">Medium</button></zen-button> + <zen-button size="lg"><button type="button">Large</button></zen-button> + <zen-button size="xl"><button type="button">Extra large</button></zen-button> + </zen-stack> + </template> + </zen-story> <zen-story name="States"> <template> <zen-stack direction="row">
--- a/design_system/src/storybook.css Tue Aug 04 16:49:11 2026 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,393 +0,0 @@ -* { - box-sizing: border-box; -} - -html { - min-height: 100%; - background: var(--zen-color-canvas); - color: var(--zen-color-text); - font-family: var(--zen-font-sans); -} - -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(--zen-space-5); - border-right: var(--zen-border-width) solid var(--zen-color-border); - background: var(--zen-color-surface); -} - -.catalog-brand { - display: inline-flex; - align-items: center; - gap: var(--zen-space-3); - color: inherit; - font-size: var(--zen-font-size-lg); - font-weight: 850; - text-decoration: none; -} - -.catalog-search { - width: 100%; - margin-top: var(--zen-space-5); -} - -.catalog-search > zen-icon { - align-self: center; - margin-inline-start: var(--zen-space-3); - color: var(--zen-color-text-muted); -} - -.catalog-search > kbd { - align-self: center; - margin-inline-end: var(--zen-space-2); - padding: 0 var(--zen-space-1); - border: 1px solid var(--zen-color-border-muted); - border-radius: var(--zen-radius-sm); - color: var(--zen-color-text-muted); - font: var(--zen-font-size-xs)/1.5 var(--zen-font-mono); -} - -.search-status { - min-height: 1.25rem; - margin: var(--zen-space-1) 0 0; - color: var(--zen-color-text-muted); - font-size: var(--zen-font-size-xs); -} - -.catalog-mark { - display: grid; - width: 2.5rem; - height: 2.5rem; - place-items: center; - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: 50%; - background: var(--zen-color-accent); - color: var(--zen-color-on-accent); -} - -.catalog-sidebar h2 { - margin: var(--zen-space-6) 0 var(--zen-space-2); - color: var(--zen-color-text-muted); - font-size: var(--zen-font-size-xs); - letter-spacing: 0.12em; - text-transform: uppercase; -} - -.catalog-nav { - display: grid; - gap: var(--zen-space-1); -} - -.catalog-nav a { - padding: var(--zen-space-2) var(--zen-space-3); - border-radius: var(--zen-radius-sm); - 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(--zen-color-accent); - color: var(--zen-color-on-accent); -} - -.theme-toggle { - width: 100%; - margin-top: var(--zen-space-6); - padding: var(--zen-space-2); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - background: transparent; - color: inherit; - cursor: pointer; -} - -.catalog-main { - width: min(100%, var(--zen-content-width)); - min-width: 0; - padding: var(--zen-space-7) clamp(1rem, 5vw, 5rem); - outline: none; -} - -.catalog-page { - min-width: 0; -} - -.catalog-page[hidden] { - display: none; -} - -.page-heading { - max-width: 52rem; - margin-bottom: var(--zen-space-7); -} - -.eyebrow { - margin: 0 0 var(--zen-space-2); - color: var(--zen-color-info); - font-size: var(--zen-font-size-sm); - font-weight: 800; - letter-spacing: 0.1em; - text-transform: uppercase; -} - -.page-heading h1 { - margin: 0 0 var(--zen-space-3); - font-size: var(--zen-font-size-xl); - line-height: 1.05; -} - -.page-heading p { - color: var(--zen-color-text-muted); - font-size: var(--zen-font-size-lg); -} - -.principles, -.component-grid, -.token-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); - gap: var(--zen-space-5); -} - -.principle, -.token-group { - padding: var(--zen-space-5); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-lg); - background: var(--zen-color-surface); -} - -.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(--zen-color-info); - font-size: var(--zen-font-size-xs); -} - -.component-grid p { - color: var(--zen-color-text-muted); -} - -.swatch-list { - display: grid; - gap: var(--zen-space-2); - margin: 0; -} - -.swatch-list div { - display: grid; - grid-template-columns: 2rem 1fr; - align-items: center; - gap: var(--zen-space-2); -} - -.swatch-list dt, -.swatch-list dd { - margin: 0; - font-family: var(--zen-font-mono); - font-size: var(--zen-font-size-xs); -} - -.swatch { - width: 2rem; - height: 2rem; - border: 1px solid var(--zen-color-border); - border-radius: 50%; - background: var(--zen-color-swatch); -} - -.token-group-wide { - grid-column: span 2; -} - -.token-group > p { - color: var(--zen-color-text-muted); -} - -.palette-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr)); - gap: var(--zen-space-3); -} - -.palette-grid > div { - display: grid; - gap: var(--zen-space-1); -} - -.palette-grid span { - min-height: 3.5rem; - border: 1px solid var(--zen-color-border-muted); - border-radius: var(--zen-radius-md); - background: var(--zen-color-swatch); -} - -.palette-grid code { - font-size: var(--zen-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(--zen-color-border); - border-radius: var(--zen-radius-md); -} - -.data-palette span { - background: var(--zen-color-swatch); -} - -.icon-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); - gap: var(--zen-space-3); -} - -.icon-grid figure { - display: grid; - min-height: 7rem; - margin: 0; - padding: var(--zen-space-3); - place-items: center; - gap: var(--zen-space-2); - border: 1px solid var(--zen-color-border-muted); - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface); -} - -.icon-grid figcaption { - max-width: 100%; - overflow: hidden; - color: var(--zen-color-text-muted); - font-size: var(--zen-font-size-xs); - text-overflow: ellipsis; -} - -zen-story { - display: block; - max-width: 100%; - margin-bottom: var(--zen-space-6); - overflow: hidden; - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-lg); - background: var(--zen-color-surface); -} - -zen-story > header { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--zen-space-3); - padding: var(--zen-space-3) var(--zen-space-4); - border-bottom: 1px solid var(--zen-color-border); -} - -zen-story > header h3 { - margin: 0; -} - -zen-story > header button { - padding: var(--zen-space-1) var(--zen-space-2); - border: 0; - border-radius: var(--zen-radius-sm); - background: transparent; - color: var(--zen-color-info); - cursor: pointer; -} - -.story-canvas { - min-height: 8rem; - padding: clamp(1rem, 4vw, 3rem); - background: - linear-gradient(90deg, var(--zen-color-gridline) 1px, transparent 1px), - linear-gradient(var(--zen-color-gridline) 1px, transparent 1px); - background-size: 1rem 1rem; -} - -zen-story > pre { - max-height: 24rem; - overflow: auto; - margin: 0; - padding: var(--zen-space-4); - border-top: 1px solid var(--zen-color-border); - background: var(--zen-color-code-surface); - color: var(--zen-color-code-text); - font: var(--zen-font-size-sm)/1.55 var(--zen-font-mono); -} - -.demo-card-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); - gap: var(--zen-space-5); -} - -@media (max-width: 760px) { - .catalog-shell { - grid-template-columns: 1fr; - } - - .catalog-sidebar { - position: static; - width: 100%; - height: auto; - border-right: 0; - border-bottom: var(--zen-border-width) solid var(--zen-color-border); - } - - .catalog-nav { - grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); - } - - .catalog-main { - padding: var(--zen-space-6) var(--zen-space-4); - } - - .token-group-wide { - grid-column: auto; - } - - .data-palette { - grid-template-columns: repeat(5, 1fr); - } -}
--- a/design_system/src/storybook.js Tue Aug 04 16:49:11 2026 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,304 +0,0 @@ -import "./components/index.js"; -import { componentCatalog } from "./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); -} - -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); -});
--- a/design_system/src/styles/components.css Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/src/styles/components.css Wed Aug 05 05:25:40 2026 -0700 @@ -1,4 +1,4 @@ -@import url("./shadcn.css"); +@import url("./elements.css"); :where( zen-button, @@ -9,78 +9,128 @@ zen-notifications ) { box-sizing: border-box; - font-family: var(--zen-font-sans); - color: var(--zen-color-text); + font-family: var(--zenbu-sys-font-family-ui); + color: var(--zenbu-sys-color-text-primary); } zen-button { display: inline-flex; } -zen-button > :where(button, a) { +zen-button:not([appearance="plain"]) > :where(button, a) { + --zenbu-button-background: + var(--zenbu-sys-color-action-primary-background); + --zenbu-button-background-hover: + var(--zenbu-sys-color-action-primary-background-hover); + --zenbu-button-background-pressed: + var(--zenbu-sys-color-action-primary-background-pressed); + --zenbu-button-border: + var(--zenbu-sys-color-action-primary-border); + --zenbu-button-foreground: + var(--zenbu-sys-color-action-primary-foreground); appearance: none; + box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; - gap: var(--zen-space-2); - min-height: 2.75rem; - padding: var(--zen-space-2) var(--zen-space-4); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - box-shadow: var(--zen-shadow-sm); - background: var(--zen-color-accent); - color: var(--zen-color-on-accent); - font: 700 var(--zen-font-size-md)/1 var(--zen-font-sans); + gap: var(--zenbu-control-gap); + min-height: var(--zenbu-control-height); + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); + border: var(--zenbu-sys-stroke-width) solid var(--zenbu-button-border); + border-radius: var(--zenbu-sys-radius-control); + box-shadow: var(--zenbu-sys-shadow-subtle); + background: var(--zenbu-button-background); + color: var(--zenbu-button-foreground); + font: + var(--zenbu-sys-font-weight-strong) + var(--zenbu-control-font-size) / + var(--zenbu-control-line-height) + var(--zenbu-sys-font-family-ui); text-decoration: none; cursor: pointer; transition: - background var(--zen-motion-fast) ease, - box-shadow var(--zen-motion-fast) ease, - transform var(--zen-motion-fast) ease; + background var(--zenbu-sys-motion-duration-state) + var(--zenbu-sys-motion-ease-state), + border-color var(--zenbu-sys-motion-duration-state) + var(--zenbu-sys-motion-ease-state), + box-shadow var(--zenbu-sys-motion-duration-feedback) + var(--zenbu-sys-motion-ease-state), + transform var(--zenbu-sys-motion-duration-feedback) + var(--zenbu-sys-motion-ease-state); } -zen-button > :where(button, a):hover { - background: var(--zen-color-accent-hover); +zen-button:not([appearance="plain"]) > :where(button, a):hover:not( + :disabled, + [aria-disabled="true"] +) { + background: var(--zenbu-button-background-hover); transform: translate(-1px, -1px); - box-shadow: var(--zen-shadow-hover); + box-shadow: var(--zenbu-sys-shadow-raised); } -zen-button > :where(button, a):active { +zen-button:not([appearance="plain"]) > :where(button, a):active:not( + :disabled, + [aria-disabled="true"] +) { + background: var(--zenbu-button-background-pressed); transform: translate(2px, 2px); box-shadow: none; } -zen-button > :where(button, a):focus-visible, +zen-button:not([appearance="plain"]) > :where(button, a):focus-visible, zen-alert [data-zen-dismiss]:focus-visible { - outline: 3px solid var(--zen-color-focus); - outline-offset: 3px; + outline: var(--zenbu-sys-focus-width) solid + var(--zenbu-sys-color-focus-outer); + outline-offset: var(--zenbu-sys-focus-offset); } -zen-field :where(input, select, textarea):focus-visible { +zen-field:not([appearance="plain"]) :where( + input, + select, + textarea +):focus-visible { outline: none; } -zen-button[variant="quiet"] > :where(button, a) { - background: var(--zen-color-surface); - color: var(--zen-color-text); +zen-button:not([appearance="plain"])[variant="quiet"] > :where(button, a) { + --zenbu-button-background: + var(--zenbu-sys-color-action-neutral-background); + --zenbu-button-background-hover: + var(--zenbu-sys-color-action-neutral-background-hover); + --zenbu-button-background-pressed: + var(--zenbu-sys-color-action-neutral-background-pressed); + --zenbu-button-border: + var(--zenbu-sys-color-action-neutral-border); + --zenbu-button-foreground: + var(--zenbu-sys-color-action-neutral-foreground); } -zen-button[variant="danger"] > :where(button, a) { - background: var(--zen-color-danger); - color: var(--zen-color-on-danger); +zen-button:not([appearance="plain"])[variant="danger"] > :where(button, a) { + --zenbu-button-background: + var(--zenbu-sys-color-action-danger-background); + --zenbu-button-background-hover: + var(--zenbu-sys-color-action-danger-background-hover); + --zenbu-button-background-pressed: + var(--zenbu-sys-color-action-danger-background-pressed); + --zenbu-button-border: + var(--zenbu-sys-color-action-danger-border); + --zenbu-button-foreground: + var(--zenbu-sys-color-action-danger-foreground); } -zen-button[disabled] > :where(button, a), -zen-button[loading] > :where(button, a) { - opacity: 0.55; +zen-button:not([appearance="plain"])[disabled] > :where(button, a), +zen-button:not([appearance="plain"])[loading] > :where(button, a) { + opacity: var(--zenbu-sys-opacity-disabled); box-shadow: none; cursor: not-allowed; transform: none; } -zen-button[loading] > :where(button, a)::before { - width: 0.85rem; - height: 0.85rem; +zen-button:not([appearance="plain"])[loading] > :where(button, a)::before { + width: var(--zenbu-control-icon-size); + height: var(--zenbu-control-icon-size); border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; @@ -96,24 +146,30 @@ display: block; } -zen-card > :where(article, section) { +zen-card:not([appearance="plain"]) > :where(article, section) { + --zenbu-card-background: var(--zenbu-sys-color-surface-raised); + --zenbu-card-border: var(--zenbu-sys-color-border-subtle); + --zenbu-card-foreground: var(--zenbu-sys-color-text-primary); height: 100%; - padding: var(--zen-space-5); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-lg); - box-shadow: var(--zen-shadow-md); - background: var(--zen-color-surface-raised); + padding: var(--zenbu-sys-padding-lg); + border: var(--zenbu-sys-stroke-width) solid var(--zenbu-card-border); + border-radius: var(--zenbu-sys-radius-container); + box-shadow: var(--zenbu-sys-shadow-raised); + background: var(--zenbu-card-background); + color: var(--zenbu-card-foreground); } -zen-card[interactive] > :where(article, section) { +zen-card:not([appearance="plain"])[interactive] > :where(article, section) { transition: - box-shadow var(--zen-motion-fast) ease, - transform var(--zen-motion-fast) ease; + box-shadow var(--zenbu-sys-motion-duration-state) + var(--zenbu-sys-motion-ease-state), + transform var(--zenbu-sys-motion-duration-state) + var(--zenbu-sys-motion-ease-state); } -zen-card[interactive] > :where(article, section):hover { +zen-card:not([appearance="plain"])[interactive] > :where(article, section):hover { transform: translate(-2px, -2px); - box-shadow: var(--zen-shadow-lifted); + box-shadow: var(--zenbu-sys-shadow-overlay); } zen-card :where(h2, h3, p) { @@ -121,85 +177,123 @@ } zen-alert { + --zenbu-alert-background: var(--zenbu-sys-color-info-background); + --zenbu-alert-border: var(--zenbu-sys-color-info-border); + --zenbu-alert-foreground: var(--zenbu-sys-color-info-foreground); display: grid; - grid-template-columns: 0.35rem minmax(0, 1fr) auto; + --zenbu-alert-indicator-width: var(--zenbu-ref-space-1-5); + grid-template-columns: + var(--zenbu-alert-indicator-width) + minmax(0, 1fr) + auto; align-items: start; - gap: var(--zen-space-3); - padding: var(--zen-space-4); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface-raised); + gap: var(--zenbu-sys-space-control); + padding: var(--zenbu-sys-padding-md); + border: var(--zenbu-sys-stroke-width) solid var(--zenbu-alert-border); + border-radius: var(--zenbu-sys-radius-container); + background: var(--zenbu-alert-background); + color: var(--zenbu-alert-foreground); } zen-alert::before { align-self: stretch; - border-radius: 999px; - background: var(--zen-color-info); + border-radius: var(--zenbu-sys-radius-pill); + background: var(--zenbu-alert-border); content: ""; } -zen-alert[tone="success"]::before { background: var(--zen-color-success); } -zen-alert[tone="warning"]::before { background: var(--zen-color-warning); } -zen-alert[tone="danger"]::before { background: var(--zen-color-danger); } +zen-alert[tone="success"] { + --zenbu-alert-background: var(--zenbu-sys-color-success-background); + --zenbu-alert-border: var(--zenbu-sys-color-success-border); + --zenbu-alert-foreground: var(--zenbu-sys-color-success-foreground); +} +zen-alert[tone="warning"] { + --zenbu-alert-background: var(--zenbu-sys-color-warning-background); + --zenbu-alert-border: var(--zenbu-sys-color-warning-border); + --zenbu-alert-foreground: var(--zenbu-sys-color-warning-foreground); +} +zen-alert[tone="danger"] { + --zenbu-alert-background: var(--zenbu-sys-color-danger-background); + --zenbu-alert-border: var(--zenbu-sys-color-danger-border); + --zenbu-alert-foreground: var(--zenbu-sys-color-danger-foreground); +} zen-alert > :where(p, div) { margin: 0; } zen-alert [data-zen-dismiss] { - min-width: 2rem; - min-height: 2rem; + min-width: var(--zenbu-sys-control-height-xs); + min-height: var(--zenbu-sys-control-height-xs); padding: 0; border: 0; - border-radius: 50%; + border-radius: var(--zenbu-sys-radius-pill); background: transparent; color: inherit; - font: 700 1.2rem/1 var(--zen-font-sans); + font: + var(--zenbu-sys-font-weight-bold) + var(--zenbu-sys-font-size-lg) / 1 + var(--zenbu-sys-font-family-ui); cursor: pointer; } zen-field { + --zenbu-field-textarea-min-height: + calc(var(--zenbu-ref-space-24) + var(--zenbu-ref-space-4)); display: grid; - gap: var(--zen-space-2); + gap: var(--zenbu-sys-space-icon-label); } -zen-field > label { - font-weight: 750; +zen-field[appearance="plain"] { + display: contents; +} + +zen-field:not([appearance="plain"]) > label { + font-family: var(--zenbu-sys-type-label-family); + font-size: var(--zenbu-sys-type-label-size); + font-weight: var(--zenbu-sys-type-label-weight); + line-height: var(--zenbu-sys-type-label-line-height); } -zen-field > :where(input, select, textarea) { +zen-field:not([appearance="plain"]) > :where(input, select, textarea) { width: 100%; - min-height: 2.75rem; + min-height: var(--zenbu-control-height); box-sizing: border-box; - padding: var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface); - color: var(--zen-color-text); - font: inherit; + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); + border: var(--zenbu-sys-stroke-width) solid + var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-default); + color: var(--zenbu-sys-color-text-primary); + font-family: inherit; + font-size: var(--zenbu-control-font-size); + line-height: var(--zenbu-control-line-height); } -zen-field > textarea { - min-height: 7rem; +zen-field:not([appearance="plain"]) > textarea { + min-height: var(--zenbu-field-textarea-min-height); resize: vertical; } -zen-field > :where(small, [data-help]) { - color: var(--zen-color-text-muted); +zen-field:not([appearance="plain"]) > :where(small, [data-help]) { + color: var(--zenbu-sys-color-text-secondary); } -zen-field[data-invalid] > :where(input, select, textarea) { - border-color: var(--zen-color-danger); +zen-field:not([appearance="plain"])[data-invalid] > :where(input, select, textarea) { + border-color: var(--zenbu-sys-color-danger-border); } -zen-field[data-invalid] > :where(small, [data-help]) { - color: var(--zen-color-danger); +zen-field:not([appearance="plain"])[data-invalid] > :where(small, [data-help]) { + color: var(--zenbu-sys-color-danger-foreground); } zen-stack { + --zenbu-stack-gap: var(--zenbu-sys-space-group); display: flex; flex-direction: column; - gap: var(--zen-stack-gap, var(--zen-space-4)); + gap: var(--zenbu-stack-gap); } zen-stack[direction="row"] { @@ -207,59 +301,89 @@ align-items: center; } -zen-stack[gap="tight"] { --zen-stack-gap: var(--zen-space-2); } -zen-stack[gap="loose"] { --zen-stack-gap: var(--zen-space-6); } +zen-stack[gap="tight"] { + --zenbu-stack-gap: var(--zenbu-sys-space-related); +} +zen-stack[gap="loose"] { + --zenbu-stack-gap: var(--zenbu-sys-space-container); +} zen-notifications > [data-zen-notification-stack] { + --zenbu-notification-z-index: var(--zenbu-sys-z-notification); + --zenbu-notification-inset: var(--zenbu-sys-padding-md); + --zenbu-notification-bottom-inset: var(--zenbu-sys-padding-md); + --zenbu-notification-collapsed-height: 0; + --zenbu-notification-expanded-height: 0; position: fixed; - z-index: var(--zen-notification-z-index, 1000); - inset-inline-end: var(--zen-notification-inset, var(--zen-space-4)); + z-index: var( + --zenbu-notification-z-index, + var(--zenbu-sys-z-notification) + ); + inset-inline-end: var( + --zenbu-notification-inset, + var(--zenbu-sys-padding-md) + ); inset-block-end: var( - --zen-notification-bottom-inset, - var(--zen-space-4) + --zenbu-notification-bottom-inset, + var(--zenbu-sys-padding-md) ); inline-size: min(24rem, calc(100vi - 2rem)); - height: var(--zen-notification-collapsed-height, 0); - --zen-notification-gap: 12px; + height: var(--zenbu-notification-collapsed-height, 0); + --zenbu-notification-gap: var(--zenbu-sys-space-control); pointer-events: auto; isolation: isolate; - transition: height 220ms cubic-bezier(0.2, 0.8, 0.2, 1); + transition: + height var(--zenbu-sys-motion-duration-layout) + var(--zenbu-sys-motion-ease-layout); } zen-notifications > [data-zen-notification-stack]:is( :hover, :focus-within ) { - height: var(--zen-notification-expanded-height, 0); + height: var(--zenbu-notification-expanded-height, 0); } zen-notifications > [data-zen-notification-stack] > article { + --zenbu-notification-collapsed-opacity: 1; + --zenbu-notification-collapsed-offset: 0; + --zenbu-notification-offset: 0; + --zenbu-notification-scale: 1; + --zenbu-notification-background: + var(--zenbu-sys-color-surface-raised); + --zenbu-notification-border: + var(--zenbu-sys-color-border-subtle); + --zenbu-notification-foreground: + var(--zenbu-sys-color-text-primary); position: absolute; inset-inline: 0; inset-block-end: 0; display: grid; grid-template-columns: 1.75rem minmax(0, 1fr) 2rem; align-items: start; - gap: var(--zen-space-3); - padding: var(--zen-space-4); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-lg); - box-shadow: var(--zen-shadow-md); - background: var(--zen-color-surface-raised); - color: var(--zen-color-text); - opacity: var(--zen-notification-collapsed-opacity, 1); + gap: var(--zenbu-sys-space-control); + padding: var(--zenbu-sys-padding-md); + border: var(--zenbu-sys-stroke-width) solid + var(--zenbu-notification-border); + border-radius: var(--zenbu-sys-radius-container); + box-shadow: var(--zenbu-sys-shadow-overlay); + background: var(--zenbu-notification-background); + color: var(--zenbu-notification-foreground); + opacity: var(--zenbu-notification-collapsed-opacity, 1); pointer-events: none; transform: translate3d( 0, - var(--zen-notification-collapsed-offset, 0), + var(--zenbu-notification-collapsed-offset, 0), 0 ) - scale(var(--zen-notification-scale, 1)); + scale(var(--zenbu-notification-scale, 1)); transform-origin: center bottom; transition: - opacity 180ms ease, - transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1); + opacity var(--zenbu-sys-motion-duration-exit) + var(--zenbu-sys-motion-ease-exit), + transform var(--zenbu-sys-motion-duration-layout) + var(--zenbu-sys-motion-ease-layout); will-change: transform; animation: zen-notification-enter 160ms ease-out; } @@ -277,7 +401,7 @@ transform: translate3d( 0, - var(--zen-notification-offset, 0px), + var(--zenbu-notification-offset, 0px), 0 ) scale(1); @@ -290,34 +414,38 @@ } .zen-notification-tone { + --zenbu-notification-tone-background: + var(--zenbu-sys-color-info-foreground); + --zenbu-notification-tone-foreground: + var(--zenbu-sys-color-text-inverse); display: grid; width: 1.75rem; height: 1.75rem; place-items: center; - border-radius: 50%; - background: var(--zen-color-info); - color: var(--zen-color-on-info); - font-weight: 900; + border-radius: var(--zenbu-sys-radius-pill); + background: var(--zenbu-notification-tone-background); + color: var(--zenbu-notification-tone-foreground); + font-weight: var(--zenbu-sys-font-weight-bold); } [data-tone="success"] > .zen-notification-tone { - background: var(--zen-color-success); - color: var(--zen-color-on-success); + --zenbu-notification-tone-background: + var(--zenbu-sys-color-success-foreground); } [data-tone="warning"] > .zen-notification-tone { - background: var(--zen-color-warning); - color: var(--zen-color-on-warning); + --zenbu-notification-tone-background: + var(--zenbu-sys-color-warning-foreground); } [data-tone="error"] > .zen-notification-tone { - background: var(--zen-color-danger); - color: var(--zen-color-on-danger); + --zenbu-notification-tone-background: + var(--zenbu-sys-color-danger-foreground); } .zen-notification-content { display: grid; - gap: var(--zen-space-1); + gap: var(--zenbu-sys-space-icon-label); } .zen-notification-message, @@ -326,12 +454,12 @@ } .zen-notification-message { - font-weight: 800; + font-weight: var(--zenbu-sys-font-weight-bold); } .zen-notification-description { - color: var(--zen-color-text-muted); - font-size: var(--zen-font-size-sm); + color: var(--zenbu-sys-color-text-secondary); + font-size: var(--zenbu-sys-font-size-sm); } .zen-notification-action, @@ -344,10 +472,10 @@ .zen-notification-action { justify-self: start; - margin-top: var(--zen-space-2); + margin-top: var(--zenbu-sys-space-related); padding: 0; - color: var(--zen-color-info); - font-weight: 800; + color: var(--zenbu-sys-color-text-link); + font-weight: var(--zenbu-sys-font-weight-bold); text-decoration: underline; text-underline-offset: 0.2em; } @@ -356,14 +484,15 @@ width: 2rem; height: 2rem; padding: 0; - border-radius: 50%; - font-size: 1.25rem; + border-radius: var(--zenbu-sys-radius-pill); + font-size: var(--zenbu-sys-font-size-xl); } .zen-notification-action:focus-visible, .zen-notification-dismiss:focus-visible { - outline: 3px solid var(--zen-color-focus); - outline-offset: 2px; + outline: var(--zenbu-sys-focus-width) solid + var(--zenbu-sys-color-focus-outer); + outline-offset: var(--zenbu-sys-focus-offset); } .zen-visually-hidden {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/src/styles/density.css Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,31 @@ +:root, +:root[data-zen-density="comfortable"] { + --zenbu-sys-control-height-xs: var(--zenbu-ref-size-control-xs); + --zenbu-sys-control-height-sm: var(--zenbu-ref-size-control-sm); + --zenbu-sys-control-height-md: var(--zenbu-ref-size-control-md); + --zenbu-sys-control-height-lg: var(--zenbu-ref-size-control-lg); + --zenbu-sys-control-height-xl: var(--zenbu-ref-size-control-xl); +} + +:root[data-zen-density="compact"] { + --zenbu-sys-control-height-xs: 1.75rem; + --zenbu-sys-control-height-sm: 2rem; + --zenbu-sys-control-height-md: 2.25rem; + --zenbu-sys-control-height-lg: 2.75rem; + --zenbu-sys-control-height-xl: 3.25rem; + --zenbu-sys-control-padding-block-xs: var(--zenbu-ref-space-0-5); + --zenbu-sys-control-padding-block-sm: var(--zenbu-ref-space-1); + --zenbu-sys-control-padding-block-md: var(--zenbu-ref-space-1-5); + --zenbu-sys-control-padding-block-lg: var(--zenbu-ref-space-2); + --zenbu-sys-control-padding-block-xl: var(--zenbu-ref-space-3); + --zenbu-sys-control-padding-inline-xs: var(--zenbu-ref-space-2); + --zenbu-sys-control-padding-inline-sm: var(--zenbu-ref-space-2-5); + --zenbu-sys-control-padding-inline-md: var(--zenbu-ref-space-3); + --zenbu-sys-control-padding-inline-lg: var(--zenbu-ref-space-4); + --zenbu-sys-control-padding-inline-xl: var(--zenbu-ref-space-5); + --zenbu-sys-control-gap-xs: var(--zenbu-ref-space-0-5); + --zenbu-sys-control-gap-sm: var(--zenbu-ref-space-1); + --zenbu-sys-control-gap-md: var(--zenbu-ref-space-1-5); + --zenbu-sys-control-gap-lg: var(--zenbu-ref-space-2); + --zenbu-sys-control-gap-xl: var(--zenbu-ref-space-3); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/src/styles/elements.css Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,2030 @@ +:where( + zen-accordion, + zen-alert-dialog, + zen-aspect-ratio, + zen-attachment, + zen-avatar, + zen-badge, + zen-breadcrumb, + zen-bubble, + zen-box, + zen-button-group, + zen-calendar, + zen-carousel, + zen-chart, + zen-checkbox, + zen-collapsible, + zen-combobox, + zen-command, + zen-context-menu, + zen-data-table, + zen-date-picker, + zen-dialog, + zen-direction, + zen-drawer, + zen-dropdown-menu, + zen-empty, + zen-form, + zen-hover-card, + zen-heading, + zen-icon, + zen-input, + zen-input-group, + zen-input-otp, + zen-item, + zen-kbd, + zen-label, + zen-marker, + zen-menubar, + zen-message, + zen-message-scroller, + zen-native-select, + zen-navigation-menu, + zen-pagination, + zen-popover, + zen-progress, + zen-radio-group, + zen-resizable, + zen-scroll-area, + zen-select, + zen-separator, + zen-sheet, + zen-sidebar, + zen-skeleton, + zen-slider, + zen-spinner, + zen-switch, + zen-table, + zen-text, + zen-tabs, + zen-textarea, + zen-toggle, + zen-toggle-group, + zen-tooltip, + zen-typography +) { + box-sizing: border-box; + color: var(--zenbu-sys-color-text-primary); + font-family: var(--zenbu-sys-font-family-ui); +} + +:where( + zen-button, + zen-calendar, + zen-checkbox, + zen-combobox, + zen-command, + zen-date-picker, + zen-field, + zen-form, + zen-input, + zen-input-group, + zen-input-otp, + zen-native-select, + zen-radio-group, + zen-select, + zen-slider, + zen-switch, + zen-textarea +) { + --zenbu-control-height: var(--zenbu-sys-control-height-md); + --zenbu-control-padding-block: var(--zenbu-sys-control-padding-block-md); + --zenbu-control-padding-inline: var(--zenbu-sys-control-padding-inline-md); + --zenbu-control-gap: var(--zenbu-sys-control-gap-md); + --zenbu-control-font-size: var(--zenbu-sys-font-size-md); + --zenbu-control-line-height: var(--zenbu-sys-control-line-height-md); + --zenbu-control-icon-size: var(--zenbu-sys-icon-size-md); +} + +:where( + zen-button, + zen-calendar, + zen-checkbox, + zen-combobox, + zen-command, + zen-date-picker, + zen-field, + zen-form, + zen-input, + zen-input-group, + zen-input-otp, + zen-native-select, + zen-radio-group, + zen-select, + zen-slider, + zen-switch, + zen-textarea +)[size="xs"] { + --zenbu-control-height: var(--zenbu-sys-control-height-xs); + --zenbu-control-padding-block: var(--zenbu-sys-control-padding-block-xs); + --zenbu-control-padding-inline: var(--zenbu-sys-control-padding-inline-xs); + --zenbu-control-gap: var(--zenbu-sys-control-gap-xs); + --zenbu-control-font-size: var(--zenbu-sys-font-size-xs); + --zenbu-control-line-height: var(--zenbu-sys-control-line-height-xs); + --zenbu-control-icon-size: var(--zenbu-sys-icon-size-xs); +} + +:where( + zen-button, + zen-calendar, + zen-checkbox, + zen-combobox, + zen-command, + zen-date-picker, + zen-field, + zen-form, + zen-input, + zen-input-group, + zen-input-otp, + zen-native-select, + zen-radio-group, + zen-select, + zen-slider, + zen-switch, + zen-textarea +)[size="sm"] { + --zenbu-control-height: var(--zenbu-sys-control-height-sm); + --zenbu-control-padding-block: var(--zenbu-sys-control-padding-block-sm); + --zenbu-control-padding-inline: var(--zenbu-sys-control-padding-inline-sm); + --zenbu-control-gap: var(--zenbu-sys-control-gap-sm); + --zenbu-control-font-size: var(--zenbu-sys-font-size-sm); + --zenbu-control-line-height: var(--zenbu-sys-control-line-height-sm); + --zenbu-control-icon-size: var(--zenbu-sys-icon-size-sm); +} + +:where( + zen-button, + zen-calendar, + zen-checkbox, + zen-combobox, + zen-command, + zen-date-picker, + zen-field, + zen-form, + zen-input, + zen-input-group, + zen-input-otp, + zen-native-select, + zen-radio-group, + zen-select, + zen-slider, + zen-switch, + zen-textarea +)[size="lg"] { + --zenbu-control-height: var(--zenbu-sys-control-height-lg); + --zenbu-control-padding-block: var(--zenbu-sys-control-padding-block-lg); + --zenbu-control-padding-inline: var(--zenbu-sys-control-padding-inline-lg); + --zenbu-control-gap: var(--zenbu-sys-control-gap-lg); + --zenbu-control-font-size: var(--zenbu-sys-font-size-lg); + --zenbu-control-line-height: var(--zenbu-sys-control-line-height-lg); + --zenbu-control-icon-size: var(--zenbu-sys-icon-size-lg); +} + +:where( + zen-button, + zen-calendar, + zen-checkbox, + zen-combobox, + zen-command, + zen-date-picker, + zen-field, + zen-form, + zen-input, + zen-input-group, + zen-input-otp, + zen-native-select, + zen-radio-group, + zen-select, + zen-slider, + zen-switch, + zen-textarea +)[size="xl"] { + --zenbu-control-height: var(--zenbu-sys-control-height-xl); + --zenbu-control-padding-block: var(--zenbu-sys-control-padding-block-xl); + --zenbu-control-padding-inline: var(--zenbu-sys-control-padding-inline-xl); + --zenbu-control-gap: var(--zenbu-sys-control-gap-xl); + --zenbu-control-font-size: var(--zenbu-sys-font-size-xl); + --zenbu-control-line-height: var(--zenbu-sys-control-line-height-xl); + --zenbu-control-icon-size: var(--zenbu-sys-icon-size-xl); +} + +zen-box { + display: block; + padding: var(--zenbu-box-padding, 0); +} + +zen-box[padding="xs"] { --zenbu-box-padding: var(--zenbu-sys-padding-xs); } +zen-box[padding="sm"] { --zenbu-box-padding: var(--zenbu-sys-padding-sm); } +zen-box[padding="md"] { --zenbu-box-padding: var(--zenbu-sys-padding-md); } +zen-box[padding="lg"] { --zenbu-box-padding: var(--zenbu-sys-padding-lg); } +zen-box[padding="xl"] { --zenbu-box-padding: var(--zenbu-sys-padding-xl); } + +:where(zen-text, zen-heading) { + display: contents; + --zenbu-type-font-size: var(--zenbu-sys-font-size-md); + --zenbu-type-line-height: var(--zenbu-sys-line-height-md); +} + +:where(zen-text, zen-heading)[size="xs"] { + --zenbu-type-font-size: var(--zenbu-sys-font-size-xs); + --zenbu-type-line-height: var(--zenbu-sys-line-height-xs); +} + +:where(zen-text, zen-heading)[size="sm"] { + --zenbu-type-font-size: var(--zenbu-sys-font-size-sm); + --zenbu-type-line-height: var(--zenbu-sys-line-height-sm); +} + +:where(zen-text, zen-heading)[size="md"] { + --zenbu-type-font-size: var(--zenbu-sys-font-size-md); + --zenbu-type-line-height: var(--zenbu-sys-line-height-md); +} + +:where(zen-text, zen-heading)[size="lg"] { + --zenbu-type-font-size: var(--zenbu-sys-font-size-lg); + --zenbu-type-line-height: var(--zenbu-sys-line-height-lg); +} + +:where(zen-text, zen-heading)[size="xl"] { + --zenbu-type-font-size: var(--zenbu-sys-font-size-xl); + --zenbu-type-line-height: var(--zenbu-sys-line-height-xl); +} + +zen-text > :where(span, p, small, strong) { + font-size: var(--zenbu-type-font-size); + line-height: var(--zenbu-type-line-height); +} + +zen-heading { + --zenbu-type-font-size: var(--zenbu-sys-font-size-2xl); + --zenbu-type-line-height: var(--zenbu-sys-line-height-xl); +} + +zen-heading > :where(h1, h2, h3, h4, h5, h6) { + font-size: var(--zenbu-type-font-size); + font-weight: var(--zenbu-sys-font-weight-display); + line-height: var(--zenbu-type-line-height); +} + +zen-icon { + --zenbu-icon-size: 1em; + display: inline-grid; + width: var(--zenbu-icon-size, 1em); + height: var(--zenbu-icon-size, 1em); + flex: 0 0 auto; + place-items: center; + color: inherit; + vertical-align: -0.125em; +} + +zen-icon > svg { + display: block; + width: 100%; + height: 100%; +} + +zen-link { + display: contents; +} + +zen-link[effect="paw"] > a > [data-zen-link-decoration] { + width: 1em; + height: 1em; + margin-inline-end: var(--zenbu-sys-space-related); + color: var(--zenbu-sys-color-link-decoration); + animation: zen-paw-step 600ms ease-out both; +} + +zen-link[effect="paw"] > a:hover > [data-zen-link-decoration] { + animation: zen-paw-wiggle 400ms ease-in-out infinite; +} + +:where( + zen-alert-dialog, + zen-calendar, + zen-combobox, + zen-command, + zen-date-picker, + zen-dialog, + zen-drawer, + zen-form, + zen-input-otp, + zen-popover, + zen-progress, + zen-radio-group, + zen-resizable, + zen-select, + zen-sheet, + zen-sidebar, + zen-slider, + zen-tabs +) { + display: block; +} + +:where( + zen-accordion, + zen-collapsible +) { + display: block; +} + +:where( + zen-accordion, + zen-collapsible +) details { + interpolate-size: allow-keywords; + border-block-end: 1px solid color-mix( + in srgb, + var(--zenbu-sys-color-border-default) 28%, + transparent + ); +} + +:where( + zen-accordion, + zen-collapsible +) summary { + display: flex; + align-items: center; + gap: var(--zenbu-sys-space-control); + padding: var(--zenbu-sys-space-control) 0; + font-weight: 800; + list-style: none; + cursor: pointer; +} + +:where( + zen-accordion, + zen-collapsible +) summary::marker, +:where( + zen-accordion, + zen-collapsible +) summary::-webkit-details-marker { + display: none; + content: ""; +} + +:where( + zen-accordion, + zen-collapsible +) summary > [data-zen-disclosure-icon] { + margin-inline-start: auto; + transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +:where( + zen-accordion, + zen-collapsible +) details[open] > summary > [data-zen-disclosure-icon] { + transform: rotate(180deg); +} + +:where( + zen-accordion, + zen-collapsible +) details::details-content { + block-size: 0; + overflow: clip; + opacity: 0; + transition: + block-size 220ms cubic-bezier(0.2, 0.8, 0.2, 1), + content-visibility 220ms allow-discrete, + opacity 180ms ease; +} + +:where( + zen-accordion, + zen-collapsible +) details[open]::details-content { + block-size: auto; + opacity: 1; +} + +:where( + zen-accordion, + zen-collapsible +) details > :not(summary) { + margin-block-start: 0; + padding-block-end: var(--zenbu-sys-space-control); + color: var(--zenbu-sys-color-text-secondary); +} + +:where( + zen-alert-dialog, + zen-dialog, + zen-drawer, + zen-sheet +) > [data-zen-trigger], +:where( + zen-dropdown-menu, + zen-menubar, + zen-navigation-menu, + zen-popover +) [data-zen-trigger], +zen-empty > button, +zen-form button, +zen-input-group button { + min-height: var(--zenbu-control-height); + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + box-shadow: var(--zenbu-sys-shadow-subtle); + background: var(--zenbu-sys-color-action-primary-background); + color: var(--zenbu-sys-color-action-primary-foreground); + font-weight: 800; + cursor: pointer; +} + +:where( + zen-alert-dialog, + zen-dialog, + zen-drawer, + zen-sheet +) dialog { + width: min(32rem, calc(100vi - 2rem)); + 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); + box-shadow: var(--zenbu-sys-shadow-raised); + background: var(--zenbu-sys-color-surface-raised); + color: var(--zenbu-sys-color-text-primary); +} + +:where( + zen-alert-dialog, + zen-dialog, + zen-drawer, + zen-sheet +) dialog[open], +:where( + zen-combobox, + zen-command, + zen-context-menu, + zen-dropdown-menu, + zen-hover-card, + zen-menubar, + zen-navigation-menu, + zen-popover, + zen-tooltip +) :where( + [data-zen-content], + [role="listbox"], + [role="menu"] +):not([hidden]) { + animation: zen-surface-open 160ms ease-out; +} + +:where( + zen-alert-dialog, + zen-dialog, + zen-drawer, + zen-sheet +) dialog::backdrop { + background: var(--zenbu-sys-color-scrim); + backdrop-filter: blur(3px); +} + +:where( + zen-alert-dialog, + zen-dialog, + zen-drawer, + zen-sheet +) dialog > h3 { + margin-block-start: 0; +} + +:where( + zen-alert-dialog, + zen-dialog, + zen-drawer, + zen-sheet +) dialog button { + min-height: 2.35rem; + margin-inline-end: var(--zenbu-sys-space-related); + padding-inline: var(--zenbu-sys-space-control); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control-small); + background: var(--zenbu-sys-color-surface-default); + color: var(--zenbu-sys-color-text-primary); + font-weight: 700; + cursor: pointer; +} + +zen-alert-dialog dialog button[value="confirm"] { + background: var(--zenbu-sys-color-danger-foreground); + color: var(--zenbu-sys-color-text-inverse); +} + +:where(zen-sheet, zen-drawer) dialog { + position: fixed; + max-width: none; + max-height: none; + margin: 0; +} + +:where(zen-sheet, zen-drawer)[placement="right"] dialog { + inset: 0 0 0 auto; + width: min(28rem, 92vi); + height: 100vb; + border-radius: var(--zenbu-sys-radius-container) 0 0 var(--zenbu-sys-radius-container); +} + +:where(zen-sheet, zen-drawer)[placement="left"] dialog { + inset: 0 auto 0 0; + width: min(28rem, 92vi); + height: 100vb; + border-radius: 0 var(--zenbu-sys-radius-container) var(--zenbu-sys-radius-container) 0; +} + +:where(zen-sheet, zen-drawer)[placement="bottom"] dialog, +zen-drawer:not([placement]) dialog { + inset: auto 0 0; + width: 100vi; + border-radius: var(--zenbu-sys-radius-container) var(--zenbu-sys-radius-container) 0 0; +} + +zen-aspect-ratio { + --zenbu-aspect-ratio: 16 / 9; + display: block; + aspect-ratio: var(--zenbu-aspect-ratio, 16 / 9); + 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-aspect-ratio > * { + width: 100%; + height: 100%; +} + +.demo-media { + display: grid; + place-items: center; + background: var(--zenbu-sys-color-surface-subtle); + color: var(--zenbu-sys-color-text-primary); + font-size: var(--zenbu-sys-font-size-lg); + font-weight: 900; +} + +zen-attachment { + display: grid; + grid-template-columns: 3rem minmax(0, 1fr) auto; + align-items: center; + gap: var(--zenbu-sys-space-control); + padding: var(--zenbu-sys-space-control); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-default); +} + +zen-attachment > span { + display: grid; + width: 3rem; + height: 3rem; + place-items: center; + border-radius: var(--zenbu-sys-radius-control-small); + background: var(--zenbu-sys-color-info-foreground); + color: var(--zenbu-sys-color-text-inverse); + font-size: var(--zenbu-sys-font-size-xs); + font-weight: 900; +} + +zen-attachment > div, +zen-item > div { + display: grid; +} + +:where(zen-attachment, zen-item) small { + color: var(--zenbu-sys-color-text-secondary); +} + +:where(zen-attachment, zen-item) > button { + border: 0; + background: transparent; + color: inherit; + cursor: pointer; +} + +zen-avatar { + --zenbu-avatar-size: var(--zenbu-sys-control-height-md); + display: inline-grid; + width: var(--zenbu-avatar-size, 2.75rem); + height: var(--zenbu-avatar-size, 2.75rem); + overflow: hidden; + 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); + font-weight: 900; +} + +zen-avatar > img { + width: 100%; + height: 100%; + object-fit: cover; +} + +zen-badge { + display: inline-flex; + align-items: center; + min-height: 1.75rem; + padding: 0 var(--zenbu-sys-space-related); + border: 1px solid var(--zenbu-sys-color-border-default); + border-radius: 999px; + background: var(--zenbu-sys-color-surface-default); + font-size: var(--zenbu-sys-font-size-xs); + font-weight: 850; +} + +zen-badge[tone="success"] { + background: var(--zenbu-sys-color-success-foreground); + color: var(--zenbu-sys-color-text-inverse); +} + +zen-badge[tone="danger"] { + background: var(--zenbu-sys-color-danger-foreground); + color: var(--zenbu-sys-color-text-inverse); +} + +:where(zen-breadcrumb, zen-pagination) nav { + display: flex; + align-items: center; + gap: var(--zenbu-sys-space-related); +} + +zen-breadcrumb ol { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--zenbu-sys-space-related); + margin: 0; + padding: 0; + list-style: none; +} + +zen-breadcrumb li:not(:last-child)::after { + margin-inline-start: var(--zenbu-sys-space-related); + color: var(--zenbu-sys-color-text-secondary); + content: "/"; +} + +:where(zen-breadcrumb, zen-pagination) a { + color: var(--zenbu-sys-color-info-foreground); + text-underline-offset: 0.2em; +} + +zen-bubble { + display: block; + width: fit-content; + max-width: min(42rem, 88%); + padding: var(--zenbu-sys-space-control) var(--zenbu-sys-space-group); + border-radius: var(--zenbu-sys-radius-container) var(--zenbu-sys-radius-container) + var(--zenbu-sys-radius-container) var(--zenbu-sys-radius-control-small); + background: var(--zenbu-sys-color-surface-raised); + box-shadow: var(--zenbu-sys-shadow-subtle); +} + +zen-bubble[align="end"] { + margin-inline-start: auto; + border-radius: var(--zenbu-sys-radius-container) var(--zenbu-sys-radius-container) + var(--zenbu-sys-radius-control-small) var(--zenbu-sys-radius-container); + background: var(--zenbu-sys-color-action-primary-background); + color: var(--zenbu-sys-color-action-primary-foreground); +} + +zen-bubble > :first-child { + margin-block-start: 0; +} + +zen-bubble > :last-child { + margin-block-end: 0; +} + +zen-button-group { + display: inline-flex; +} + +zen-button-group > :where(button, a) { + min-height: 2.5rem; + padding-inline: var(--zenbu-sys-space-control); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: 0; + background: var(--zenbu-sys-color-surface-default); + color: var(--zenbu-sys-color-text-primary); + font-weight: 750; + cursor: pointer; +} + +zen-button-group > :first-child { + border-radius: var(--zenbu-sys-radius-control) 0 0 var(--zenbu-sys-radius-control); +} + +zen-button-group > :last-child { + border-radius: 0 var(--zenbu-sys-radius-control) var(--zenbu-sys-radius-control) 0; +} + +zen-button-group > :not(:first-child) { + margin-inline-start: calc(-1 * var(--zenbu-sys-stroke-width-emphasis)); +} + +:where( + zen-calendar, + zen-date-picker, + zen-slider +) { + display: grid; + gap: var(--zenbu-sys-space-related); + max-width: 22rem; +} + +:where( + zen-calendar, + zen-date-picker, + zen-input, + zen-input-group, + zen-input-otp, + zen-native-select, + zen-select, + zen-slider, + zen-textarea +):not([appearance="plain"]) :where(input, select, textarea) { + appearance: none; + min-height: var(--zenbu-control-height); + box-sizing: border-box; + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-default); + color: var(--zenbu-sys-color-text-primary); + font-family: inherit; + font-size: var(--zenbu-control-font-size); + line-height: var(--zenbu-control-line-height); +} + +:where( + zen-input, + zen-input-group, + zen-input-otp, + zen-native-select, + zen-select, + zen-textarea, + zen-calendar, + zen-date-picker +):not([appearance="plain"]) :where(input, select, textarea), +zen-field:not([appearance="plain"]) > :where(input, select, textarea) { + appearance: none; + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-default); + color: var(--zenbu-sys-color-text-primary); +} + +:where(zen-native-select, zen-select) { + position: relative; +} + +:where(zen-native-select, zen-select) select { + width: 100%; + padding-inline-end: 2.75rem; + cursor: pointer; +} + +:where(zen-native-select, zen-select) > [data-zen-select-icon] { + position: absolute; + inset-inline-end: var(--zenbu-sys-space-control); + inset-block-end: 0.9rem; + pointer-events: none; +} + +:where( + zen-calendar, + zen-date-picker, + zen-input, + zen-native-select, + zen-select, + zen-textarea +):not([appearance="plain"]) { + display: grid; + gap: var(--zenbu-sys-space-related); +} + +zen-textarea:not([appearance="plain"]) textarea { + width: 100%; + min-height: 8rem; + resize: vertical; +} + +:where( + zen-checkbox, + zen-input, + zen-native-select, + zen-radio-group, + zen-select, + zen-slider, + zen-switch, + zen-textarea +)[appearance="plain"] { + display: contents; + color: inherit; + font-family: inherit; +} + +:where(zen-calendar, zen-date-picker) > input[data-zen-calendar-input] { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + pointer-events: none; +} + +:where(zen-calendar, zen-date-picker) [data-zen-generated="calendar"] { + display: grid; + gap: var(--zenbu-sys-space-related); +} + +zen-date-picker [data-zen-generated="calendar"] { + position: relative; +} + +:where(zen-calendar, zen-date-picker) :where( + [data-zen-date-trigger], + [data-zen-calendar-previous], + [data-zen-calendar-next], + [data-zen-calendar-day] +) { + appearance: none; + border: 0; + background: transparent; + color: var(--zenbu-sys-color-text-primary); + font: inherit; + cursor: pointer; +} + +zen-date-picker [data-zen-date-trigger] { + display: inline-flex; + min-height: var(--zenbu-control-height); + align-items: center; + gap: var(--zenbu-control-gap); + justify-content: flex-start; + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + box-shadow: var(--zenbu-sys-shadow-subtle); + background: var(--zenbu-sys-color-surface-default); +} + +zen-date-picker[data-invalid] [data-zen-date-trigger], +zen-calendar[data-invalid] [data-zen-calendar-view] { + border-color: var(--zenbu-sys-color-danger-foreground); +} + +zen-date-picker [data-zen-calendar-panel] { + position: absolute; + z-index: 40; + inset-block-start: calc(100% + var(--zenbu-sys-space-related)); + inset-inline-start: 0; + width: min(21rem, calc(100vi - 2rem)); + padding: var(--zenbu-sys-space-control); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-container); + box-shadow: var(--zenbu-sys-shadow-raised); + background: var(--zenbu-sys-color-surface-raised); + transform-origin: top left; + animation: zen-calendar-open 160ms ease-out; +} + +zen-date-picker [data-zen-calendar-panel][hidden] { + display: none; +} + +:where(zen-calendar, zen-date-picker) [data-zen-calendar-view] { + display: grid; + gap: var(--zenbu-sys-space-related); + padding: var(--zenbu-sys-space-control); + border: 1px solid var(--zenbu-sys-color-border-subtle); + border-radius: var(--zenbu-sys-radius-container); + background: var(--zenbu-sys-color-surface-default); +} + +zen-date-picker [data-zen-calendar-view] { + padding: 0; + border: 0; +} + +:where(zen-calendar, zen-date-picker) [data-zen-calendar-view] > header { + display: grid; + grid-template-columns: 2.5rem minmax(0, 1fr) 2.5rem; + align-items: center; + gap: var(--zenbu-sys-space-related); + text-align: center; +} + +:where(zen-calendar, zen-date-picker) :where( + [data-zen-calendar-previous], + [data-zen-calendar-next] +) { + display: grid; + width: 2.5rem; + height: 2.5rem; + place-items: center; + border: 1px solid var(--zenbu-sys-color-border-subtle); + border-radius: 50%; +} + +:where(zen-calendar, zen-date-picker) :where( + [data-zen-calendar-previous], + [data-zen-calendar-next] +):hover { + background: var(--zenbu-sys-color-action-neutral-background-hover); +} + +:where(zen-calendar, zen-date-picker) [data-zen-calendar-weekdays], +:where(zen-calendar, zen-date-picker) [data-zen-calendar-grid] > [role="row"] { + display: grid; + grid-template-columns: repeat(7, minmax(2rem, 1fr)); + gap: var(--zenbu-sys-space-icon-label); +} + +:where(zen-calendar, zen-date-picker) [data-zen-calendar-weekdays] > span { + padding-block: var(--zenbu-sys-space-icon-label); + color: var(--zenbu-sys-color-text-secondary); + font-size: var(--zenbu-sys-font-size-xs); + font-weight: 800; + text-align: center; +} + +:where(zen-calendar, zen-date-picker) [data-zen-calendar-grid] { + display: grid; + gap: var(--zenbu-sys-space-icon-label); +} + +:where(zen-calendar, zen-date-picker) [data-zen-calendar-day] { + aspect-ratio: 1; + min-width: 2rem; + border-radius: 50%; + font-size: var(--zenbu-sys-font-size-sm); +} + +:where(zen-calendar, zen-date-picker) [data-zen-calendar-day]:hover { + background: var(--zenbu-sys-color-action-neutral-background-hover); +} + +:where(zen-calendar, zen-date-picker) [data-zen-calendar-day][data-outside] { + color: var(--zenbu-sys-color-text-tertiary); + opacity: 0.55; +} + +:where(zen-calendar, zen-date-picker) [data-zen-calendar-day][data-today] { + outline: 1px solid var(--zenbu-sys-color-info-foreground); + outline-offset: -2px; +} + +:where(zen-calendar, zen-date-picker) [data-zen-calendar-day][aria-selected="true"] { + background: var(--zenbu-sys-color-action-primary-background); + color: var(--zenbu-sys-color-action-primary-foreground); + font-weight: 900; +} + +:where(zen-calendar, zen-date-picker) [data-zen-calendar-day]:disabled { + color: var(--zenbu-sys-color-text-disabled); + cursor: not-allowed; +} + +@keyframes zen-calendar-open { + from { + opacity: 0; + transform: translateY(calc(-1 * var(--zenbu-sys-space-icon-label))) scale(0.98); + } +} + +@keyframes zen-paw-step { + from { + opacity: 0; + transform: scale(0); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes zen-paw-wiggle { + 0%, + 100% { + transform: rotate(-15deg); + } + 50% { + transform: rotate(15deg); + } +} + +@keyframes zen-surface-open { + from { + opacity: 0; + transform: translateY(calc(-1 * var(--zenbu-sys-space-icon-label))) scale(0.98); + } +} + +@media print { + zen-link[effect="paw"] > a > [data-zen-link-decoration] { + display: none; + } +} + +zen-input-group { + display: flex; + align-items: stretch; + width: min(100%, 36rem); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-default); +} + +zen-input-group > :where(span, button) { + display: inline-flex; + align-items: center; + padding-inline: var(--zenbu-sys-space-control); +} + +zen-input-group input { + min-width: 0; + flex: 1; + border: 0; + border-radius: 0; +} + +zen-input-group button { + min-height: auto; + border-width: 0 0 0 var(--zenbu-sys-stroke-width-emphasis); + border-radius: 0 var(--zenbu-sys-radius-control-small) var(--zenbu-sys-radius-control-small) 0; + box-shadow: none; +} + +zen-carousel { + position: relative; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: var(--zenbu-sys-space-related); +} + +zen-carousel [data-zen-viewport] { + display: flex; + grid-column: 1 / -1; + gap: var(--zenbu-sys-space-control); + overflow: auto; + scroll-snap-type: inline mandatory; + scrollbar-width: none; +} + +zen-carousel [data-zen-viewport] > * { + min-width: min(22rem, 82vi); + min-height: 9rem; + 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-raised); + scroll-snap-align: start; +} + +zen-carousel :where([data-zen-prev], [data-zen-next]) { + width: 2.5rem; + height: 2.5rem; + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: 50%; + background: var(--zenbu-sys-color-surface-default); + color: var(--zenbu-sys-color-text-primary); + cursor: pointer; +} + +zen-carousel [data-zen-prev] { + grid-column: 1; +} + +zen-carousel [data-zen-next] { + grid-column: 3; +} + +zen-chart { + display: block; + min-height: 15rem; + padding: var(--zenbu-sys-space-group); + 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-chart [data-zen-chart-plot] { + display: flex; + height: 12rem; + align-items: end; + justify-content: space-around; + gap: var(--zenbu-sys-space-control); + border-block-end: 1px solid var(--zenbu-sys-color-border-default); +} + +zen-chart [data-zen-chart-plot] > span { + --zenbu-chart-value: 0%; + position: relative; + width: min(3rem, 15%); + height: var(--zenbu-chart-value); + min-height: 1rem; + border-radius: var(--zenbu-sys-radius-control-small) var(--zenbu-sys-radius-control-small) 0 0; + background: var(--zenbu-sys-color-data-1); +} + +zen-chart [data-zen-chart-plot] > span:nth-child(2) { + background: var(--zenbu-sys-color-data-2); +} + +zen-chart [data-zen-chart-plot] > span:nth-child(3) { + background: var(--zenbu-sys-color-data-3); +} + +zen-chart [data-zen-chart-plot] > span:nth-child(4) { + background: var(--zenbu-sys-color-data-4); +} + +zen-chart [data-zen-chart-plot] > span:nth-child(5) { + background: var(--zenbu-sys-color-data-5); +} + +zen-chart [data-zen-chart-plot] > span::after { + position: absolute; + inset-block-start: 100%; + inset-inline-start: 50%; + padding-block-start: var(--zenbu-sys-space-icon-label); + color: var(--zenbu-sys-color-text-secondary); + content: attr(data-label); + font-size: var(--zenbu-sys-font-size-xs); + transform: translateX(-50%); +} + +:where( + zen-checkbox:not([appearance="plain"]), + zen-switch:not([appearance="plain"]) +) label, +zen-radio-group:not([appearance="plain"]) label { + display: flex; + align-items: center; + gap: var(--zenbu-sys-space-related); + width: fit-content; + cursor: pointer; +} + +:where( + zen-checkbox:not([appearance="plain"]), + zen-radio-group:not([appearance="plain"]) +) input { + appearance: none; + display: inline-grid; + width: 1.2rem; + height: 1.2rem; + flex: 0 0 auto; + place-content: center; + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + background: var(--zenbu-sys-color-surface-default); + cursor: pointer; +} + +zen-checkbox:not([appearance="plain"]) input { + border-radius: var(--zenbu-sys-radius-control-small); +} + +zen-checkbox:not([appearance="plain"]) input::before { + width: 0.55rem; + height: 0.3rem; + border-block-end: 2px solid var(--zenbu-sys-color-action-primary-foreground); + border-inline-start: 2px solid var(--zenbu-sys-color-action-primary-foreground); + content: ""; + opacity: 0; + transform: translateY(-0.08rem) rotate(-45deg); +} + +zen-checkbox:not([appearance="plain"]) input:checked { + background: var(--zenbu-sys-color-action-primary-background); +} + +zen-checkbox:not([appearance="plain"]) input:checked::before { + opacity: 1; +} + +zen-radio-group:not([appearance="plain"]) input { + border-radius: 50%; +} + +zen-radio-group:not([appearance="plain"]) input::before { + width: 0.55rem; + height: 0.55rem; + border-radius: 50%; + background: var(--zenbu-sys-color-info-foreground); + content: ""; + opacity: 0; +} + +zen-radio-group:not([appearance="plain"]) input:checked::before { + opacity: 1; +} + +zen-radio-group:not([appearance="plain"]) fieldset { + display: grid; + gap: var(--zenbu-sys-space-related); + padding: var(--zenbu-sys-space-group); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); +} + +zen-switch:not([appearance="plain"]) input { + appearance: none; + width: 2.75rem; + height: 1.5rem; + margin: 0; + padding: 2px; + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: 999px; + background: var(--zenbu-sys-color-surface-default); + cursor: pointer; + transition: background var(--zenbu-sys-motion-duration-feedback) ease; +} + +zen-switch:not([appearance="plain"]) input::before { + display: block; + width: 1rem; + height: 1rem; + border-radius: 50%; + background: var(--zenbu-sys-color-border-default); + content: ""; + transition: transform var(--zenbu-sys-motion-duration-feedback) ease; +} + +zen-switch:not([appearance="plain"]) input:checked { + background: var(--zenbu-sys-color-success-foreground); +} + +zen-switch:not([appearance="plain"]) input:checked::before { + background: var(--zenbu-sys-color-text-inverse); + transform: translateX(1.2rem); +} + +:where(zen-combobox, zen-command) { + position: relative; + display: grid; + width: min(100%, 28rem); + gap: var(--zenbu-sys-space-related); +} + +:where(zen-combobox, zen-command) > input { + min-height: var(--zenbu-control-height); + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-default); + color: var(--zenbu-sys-color-text-primary); +} + +:where( + zen-combobox, + zen-command, + zen-context-menu, + zen-dropdown-menu, + zen-menubar, + zen-navigation-menu +) :where([role="listbox"], [role="menu"]) { + z-index: 20; + display: grid; + min-width: 12rem; + padding: var(--zenbu-sys-space-icon-label); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + box-shadow: var(--zenbu-sys-shadow-raised); + background: var(--zenbu-sys-color-surface-raised); +} + +:where( + zen-combobox, + zen-command, + zen-context-menu, + zen-dropdown-menu, + zen-menubar, + zen-navigation-menu +) :where([role="listbox"], [role="menu"])[hidden] { + display: none; +} + +:where( + zen-combobox, + zen-command, + zen-context-menu, + zen-dropdown-menu, + zen-menubar, + zen-navigation-menu +) :where([role="option"], [role="menuitem"]) { + display: block; + width: 100%; + padding: var(--zenbu-sys-space-related) var(--zenbu-sys-space-control); + border: 0; + border-radius: var(--zenbu-sys-radius-control-small); + background: transparent; + color: var(--zenbu-sys-color-text-primary); + text-align: start; + text-decoration: none; + cursor: pointer; +} + +:where( + zen-combobox, + zen-command, + zen-context-menu, + zen-dropdown-menu, + zen-menubar, + zen-navigation-menu +) :where([role="option"], [role="menuitem"]):is( + :hover, + :focus, + [aria-selected="true"] +) { + outline: none; + background: var(--zenbu-sys-color-action-primary-background); + color: var(--zenbu-sys-color-action-primary-foreground); +} + +:where( + zen-context-menu, + zen-dropdown-menu, + zen-navigation-menu +) { + position: relative; + display: inline-block; +} + +zen-context-menu > [data-zen-trigger] { + min-height: 8rem; + padding: var(--zenbu-sys-space-container); + border: 2px dashed var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-container); + background: var(--zenbu-sys-color-surface-default); +} + +:where( + zen-dropdown-menu, + zen-navigation-menu +) > [role="menu"], +zen-navigation-menu nav > [role="menu"] { + position: absolute; + inset-block-start: calc(100% + var(--zenbu-sys-space-related)); + inset-inline-start: 0; +} + +zen-context-menu > [role="menu"] { + position: fixed; +} + +zen-menubar { + position: relative; + display: flex; + width: fit-content; + padding: var(--zenbu-sys-space-icon-label); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-default); +} + +zen-menubar > [data-zen-trigger] { + border: 0; + box-shadow: none; + background: transparent; + color: var(--zenbu-sys-color-text-primary); +} + +zen-menubar > [role="menu"] { + position: absolute; + inset-block-start: calc(100% + var(--zenbu-sys-space-icon-label)); +} + +zen-data-table, +zen-table { + display: block; + max-width: 100%; + overflow: auto; +} + +:where(zen-data-table, zen-table) table { + width: 100%; + border-collapse: collapse; + background: var(--zenbu-sys-color-surface-default); +} + +:where(zen-data-table, zen-table) :where(th, td) { + padding: var(--zenbu-sys-space-control); + border-block-end: 1px solid color-mix( + in srgb, + var(--zenbu-sys-color-border-default) 30%, + transparent + ); + text-align: start; +} + +:where(zen-data-table, zen-table) th { + font-size: var(--zenbu-sys-font-size-sm); +} + +zen-data-table [data-zen-sort] { + padding: 0; + border: 0; + background: transparent; + color: inherit; + font-weight: 850; + cursor: pointer; +} + +zen-direction { + display: contents; +} + +zen-empty { + display: grid; + justify-items: center; + gap: var(--zenbu-sys-space-related); + padding: var(--zenbu-sys-space-section) var(--zenbu-sys-space-container); + border: var(--zenbu-sys-stroke-width-emphasis) dashed var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-container); + background: var(--zenbu-sys-color-surface-default); + text-align: center; +} + +zen-empty > p { + max-width: 34rem; + margin: 0; + color: var(--zenbu-sys-color-text-secondary); +} + +zen-form form { + display: grid; + max-width: 34rem; + gap: var(--zenbu-sys-space-group); +} + +zen-form[data-invalid] form { + padding-inline-start: var(--zenbu-sys-space-control); + border-inline-start: 0.35rem solid var(--zenbu-sys-color-danger-foreground); +} + +:where(zen-hover-card, zen-tooltip, zen-popover) { + position: relative; + display: inline-block; +} + +:where(zen-hover-card, zen-tooltip, zen-popover) [data-zen-content] { + position: absolute; + z-index: 30; + inset-block-start: calc(100% + var(--zenbu-sys-space-related)); + inset-inline-start: 0; + width: max-content; + max-width: min(22rem, calc(100vi - 2rem)); + padding: var(--zenbu-sys-space-control); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + box-shadow: var(--zenbu-sys-shadow-raised); + background: var(--zenbu-sys-color-surface-raised); + color: var(--zenbu-sys-color-text-primary); +} + +:where(zen-hover-card, zen-tooltip, zen-popover) [data-zen-content][hidden] { + display: none; +} + +zen-tooltip [data-zen-content] { + padding: var(--zenbu-sys-space-icon-label) var(--zenbu-sys-space-related); + background: var(--zenbu-sys-color-border-default); + color: var(--zenbu-sys-color-surface-default); + font-size: var(--zenbu-sys-font-size-xs); +} + +zen-input-otp [data-zen-otp-slots] { + display: flex; + gap: var(--zenbu-control-gap); +} + +zen-input-otp [data-zen-otp-slots] > span { + display: grid; + width: var(--zenbu-control-height); + height: var(--zenbu-control-height); + place-items: center; + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control-small); + background: var(--zenbu-sys-color-surface-default); + font-size: var(--zenbu-control-font-size); + font-weight: var(--zenbu-sys-font-weight-strong); +} + +zen-item { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: var(--zenbu-sys-space-control); + padding: var(--zenbu-sys-space-control); + border-block-end: 1px solid color-mix( + in srgb, + var(--zenbu-sys-color-border-default) 28%, + transparent + ); +} + +zen-kbd { + display: inline-flex; + align-items: center; + gap: var(--zenbu-sys-space-icon-label); +} + +zen-kbd kbd { + min-width: 1.75rem; + padding: 0.1rem var(--zenbu-sys-space-icon-label); + border: 1px solid var(--zenbu-sys-color-border-default); + border-block-end-width: 3px; + border-radius: var(--zenbu-sys-radius-control-small); + background: var(--zenbu-sys-color-surface-default); + font: 700 var(--zenbu-sys-font-size-xs)/1.5 var(--zenbu-sys-font-family-code); + text-align: center; +} + +zen-label { + display: block; + margin-block-end: var(--zenbu-sys-space-related); + font-weight: 800; +} + +zen-marker { + display: flex; + align-items: center; + gap: var(--zenbu-sys-space-related); + color: var(--zenbu-sys-color-text-secondary); + font-size: var(--zenbu-sys-font-size-xs); + text-transform: uppercase; +} + +zen-marker::before, +zen-marker::after { + height: 1px; + flex: 1; + background: color-mix( + in srgb, + var(--zenbu-sys-color-border-default) 32%, + transparent + ); + content: ""; +} + +zen-message { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: start; + gap: var(--zenbu-sys-space-control); +} + +zen-message > div { + display: grid; + gap: var(--zenbu-sys-space-icon-label); +} + +zen-message-scroller { + position: relative; + display: block; +} + +zen-message-scroller [data-zen-viewport] { + max-height: 12rem; + overflow: auto; + padding: var(--zenbu-sys-space-control); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-default); +} + +zen-message-scroller [data-zen-scroll-end] { + position: absolute; + inset-inline-end: var(--zenbu-sys-space-control); + inset-block-end: var(--zenbu-sys-space-control); +} + +zen-navigation-menu nav { + display: flex; + align-items: center; + gap: var(--zenbu-sys-space-control); +} + +zen-pagination nav { + flex-wrap: wrap; +} + +zen-pagination a { + display: inline-grid; + min-width: 2.35rem; + min-height: 2.35rem; + place-items: center; + padding-inline: var(--zenbu-sys-space-related); + border: 1px solid transparent; + border-radius: var(--zenbu-sys-radius-control-small); + text-decoration: none; +} + +zen-pagination a[aria-current] { + border-color: var(--zenbu-sys-color-border-default); + background: var(--zenbu-sys-color-action-primary-background); + color: var(--zenbu-sys-color-action-primary-foreground); +} + +zen-progress { + display: grid; + gap: var(--zenbu-sys-space-related); +} + +zen-progress progress { + appearance: none; + width: 100%; + height: 0.75rem; + overflow: hidden; + border: 0; + border-radius: 999px; + accent-color: var(--zenbu-sys-color-success-foreground); + background: var(--zenbu-sys-color-surface-default); +} + +zen-progress progress::-webkit-progress-bar { + background: var(--zenbu-sys-color-surface-subtle); +} + +zen-progress progress::-webkit-progress-value { + background: var(--zenbu-sys-color-success-foreground); +} + +zen-progress progress::-moz-progress-bar { + background: var(--zenbu-sys-color-success-foreground); +} + +zen-resizable { + display: flex; + min-height: 12rem; + overflow: hidden; + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-container); +} + +zen-resizable [data-zen-panel] { + min-width: 0; + flex: 1 1 50%; + padding: var(--zenbu-sys-space-group); + background: var(--zenbu-sys-color-surface-default); +} + +zen-resizable [data-zen-handle] { + width: 0.65rem; + flex: 0 0 0.65rem; + border-inline: 1px solid var(--zenbu-sys-color-border-default); + background: var(--zenbu-sys-color-action-primary-background); + cursor: col-resize; +} + +zen-scroll-area { + display: block; + max-height: 10rem; + overflow: auto; + padding: var(--zenbu-sys-space-control); + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-default); + scrollbar-color: var(--zenbu-sys-color-action-primary-background) transparent; +} + +zen-separator { + display: block; +} + +zen-separator hr { + margin-block: var(--zenbu-sys-space-group); + border: 0; + border-block-start: 1px solid var(--zenbu-sys-color-border-default); +} + +zen-separator[orientation="vertical"] { + display: inline-block; + width: 1px; + min-height: 1.5rem; + background: var(--zenbu-sys-color-border-default); +} + +zen-sidebar { + display: grid; + grid-template-columns: minmax(12rem, 17rem) minmax(0, 1fr); + gap: var(--zenbu-sys-space-control); +} + +zen-sidebar > [data-zen-sidebar-trigger] { + grid-column: 1 / -1; + justify-self: start; +} + +zen-sidebar [data-zen-sidebar-panel] { + padding: var(--zenbu-sys-space-control); + 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-sidebar [data-zen-sidebar-panel] nav { + display: grid; + gap: var(--zenbu-sys-space-icon-label); +} + +zen-sidebar [data-zen-sidebar-panel] a { + padding: var(--zenbu-sys-space-related); + border-radius: var(--zenbu-sys-radius-control-small); + color: inherit; + text-decoration: none; +} + +zen-sidebar [data-zen-sidebar-panel] a:hover { + background: var(--zenbu-sys-color-action-primary-background); + color: var(--zenbu-sys-color-action-primary-foreground); +} + +zen-skeleton { + --zenbu-skeleton-width: 100%; + display: block; + width: var(--zenbu-skeleton-width, 100%); + height: 1rem; + border-radius: var(--zenbu-sys-radius-control-small); + background: + linear-gradient( + 100deg, + color-mix(in srgb, var(--zenbu-sys-color-text-secondary) 18%, transparent) 20%, + color-mix(in srgb, var(--zenbu-sys-color-text-secondary) 8%, transparent) 38%, + color-mix(in srgb, var(--zenbu-sys-color-text-secondary) 18%, transparent) 56% + ); + background-size: 200% 100%; + animation: zen-skeleton-shimmer 1.4s linear infinite; +} + +zen-skeleton[shape="circle"] { + width: 3rem; + height: 3rem; + border-radius: 50%; +} + +zen-slider:not([appearance="plain"]) input { + appearance: none; + width: 100%; + height: 0.5rem; + padding-inline: 0; + border: 0; + border-radius: 999px; + background: var(--zenbu-sys-color-surface-sunken); + cursor: pointer; +} + +zen-slider:not([appearance="plain"]) input::-webkit-slider-thumb { + appearance: none; + width: 1.35rem; + height: 1.35rem; + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: 50%; + box-shadow: var(--zenbu-sys-shadow-subtle); + background: var(--zenbu-sys-color-action-primary-background); +} + +zen-slider:not([appearance="plain"]) input::-moz-range-thumb { + width: 1.35rem; + height: 1.35rem; + border: var(--zenbu-sys-stroke-width-emphasis) solid var(--zenbu-sys-color-border-default); + border-radius: 50%; + box-shadow: var(--zenbu-sys-shadow-subtle); + background: var(--zenbu-sys-color-action-primary-background); +} + +zen-spinner { + display: inline-block; + width: 1.35rem; + height: 1.35rem; + border: 3px solid color-mix( + in srgb, + var(--zenbu-sys-color-text-secondary) 28%, + transparent + ); + border-inline-end-color: var(--zenbu-sys-color-info-foreground); + border-radius: 50%; + animation: zen-element-spin 650ms linear infinite; +} + +zen-tabs [role="tablist"] { + display: flex; + width: fit-content; + gap: var(--zenbu-sys-space-icon-label); + padding: var(--zenbu-sys-space-icon-label); + border: 1px solid var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-default); +} + +zen-tabs [role="tab"] { + min-height: 2.25rem; + padding-inline: var(--zenbu-sys-space-control); + border: 0; + border-radius: var(--zenbu-sys-radius-control-small); + background: transparent; + color: var(--zenbu-sys-color-text-primary); + font-weight: 750; + cursor: pointer; +} + +zen-tabs [role="tab"][aria-selected="true"] { + background: var(--zenbu-sys-color-action-primary-background); + color: var(--zenbu-sys-color-action-primary-foreground); +} + +zen-tabs [role="tabpanel"] { + padding-block: var(--zenbu-sys-space-group); +} + +zen-tabs [role="tabpanel"][hidden] { + display: none; +} + +:where(zen-toggle, zen-toggle-group) { + display: inline-flex; +} + +:where(zen-toggle, zen-toggle-group) > button { + min-height: 2.5rem; + padding-inline: var(--zenbu-sys-space-control); + border: 1px solid transparent; + border-radius: var(--zenbu-sys-radius-control-small); + background: transparent; + color: var(--zenbu-sys-color-text-primary); + font-weight: 750; + cursor: pointer; +} + +:where(zen-toggle, zen-toggle-group) > button[aria-pressed="true"] { + border-color: var(--zenbu-sys-color-border-default); + background: var(--zenbu-sys-color-action-primary-background); + color: var(--zenbu-sys-color-action-primary-foreground); +} + +zen-typography { + display: block; + max-width: 65ch; + line-height: var(--zenbu-sys-type-body-line-height); +} + +zen-typography > :where(h1, h2, h3) { + line-height: 1.15; + text-wrap: balance; +} + +zen-typography blockquote { + margin-inline: 0; + padding-inline-start: var(--zenbu-sys-space-group); + border-inline-start: 0.35rem solid var(--zenbu-sys-color-action-primary-background); + color: var(--zenbu-sys-color-text-secondary); + font-style: italic; +} + +:where( + button, + summary, + [tabindex] +):focus-visible { + outline: 3px solid var(--zenbu-sys-color-focus-outer); + outline-offset: 3px; +} + +:where( + zen-calendar:not([appearance="plain"]), + zen-checkbox:not([appearance="plain"]), + zen-combobox:not([appearance="plain"]), + zen-command:not([appearance="plain"]), + zen-date-picker:not([appearance="plain"]), + zen-field:not([appearance="plain"]), + zen-form:not([appearance="plain"]), + zen-input:not([appearance="plain"]), + zen-input-group:not([appearance="plain"]), + zen-input-otp:not([appearance="plain"]), + zen-native-select:not([appearance="plain"]), + zen-radio-group:not([appearance="plain"]), + zen-select:not([appearance="plain"]), + zen-slider:not([appearance="plain"]), + zen-switch:not([appearance="plain"]), + zen-textarea:not([appearance="plain"]) +) :where(input, select, textarea):focus-visible { + outline: none; +} + +@keyframes zen-element-spin { + to { transform: rotate(1turn); } +} + +@keyframes zen-skeleton-shimmer { + to { background-position-x: -200%; } +} + +@media (max-width: 640px) { + zen-sidebar { + display: block; + } + + zen-sidebar > [data-zen-sidebar-trigger] { + margin-block-end: var(--zenbu-sys-space-related); + } + + :where(zen-calendar, zen-date-picker) { + width: 100%; + max-width: 100%; + } + + zen-date-picker [data-zen-calendar-panel] { + position: fixed; + inset: auto var(--zenbu-sys-space-control) var(--zenbu-sys-space-control); + width: auto; + max-height: calc(100vb - 2 * var(--zenbu-sys-space-control)); + overflow: auto; + transform-origin: bottom center; + } +} + +@media (forced-colors: active) { + :where( + zen-aspect-ratio, + zen-attachment, + zen-avatar, + zen-badge, + zen-empty, + zen-resizable, + zen-scroll-area + ) { + border: 1px solid CanvasText; + } +} + +@media (prefers-reduced-motion: reduce) { + zen-link[effect="paw"] > a > [data-zen-link-decoration], + zen-link[effect="paw"] > a:hover > [data-zen-link-decoration] { + animation: none; + } + + zen-skeleton, + zen-spinner { + animation: none; + } + + zen-switch input, + zen-switch input::before, + :where( + zen-accordion, + zen-collapsible + ) details::details-content, + :where( + zen-accordion, + zen-collapsible + ) summary > [data-zen-disclosure-icon] { + transition: none; + } + + zen-date-picker [data-zen-calendar-panel] { + animation: none; + } + + :where( + zen-alert-dialog, + zen-dialog, + zen-drawer, + zen-sheet + ) dialog[open], + :where( + zen-combobox, + zen-command, + zen-context-menu, + zen-dropdown-menu, + zen-hover-card, + zen-menubar, + zen-navigation-menu, + zen-popover, + zen-tooltip + ) :where( + [data-zen-content], + [role="listbox"], + [role="menu"] + ):not([hidden]) { + animation: none; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/src/styles/reference.css Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,167 @@ +:root { + --zenbu-ref-color-black: #000000; + --zenbu-ref-color-white: #ffffff; + --zenbu-ref-color-transparent: transparent; + + --zenbu-ref-color-neutral-50: #fffdf7; + --zenbu-ref-color-neutral-100: #f7f3e8; + --zenbu-ref-color-neutral-200: #ece5d5; + --zenbu-ref-color-neutral-300: #ddd2bd; + --zenbu-ref-color-neutral-400: #b9aa90; + --zenbu-ref-color-neutral-500: #8e8069; + --zenbu-ref-color-neutral-600: #6f6351; + --zenbu-ref-color-neutral-700: #564c3e; + --zenbu-ref-color-neutral-800: #3b352d; + --zenbu-ref-color-neutral-900: #27241f; + --zenbu-ref-color-neutral-950: #171613; + + --zenbu-ref-color-brand-100: #e4eaf2; + --zenbu-ref-color-brand-200: #cbd7e6; + --zenbu-ref-color-brand-300: #a6bad2; + --zenbu-ref-color-brand-400: #7b99b8; + --zenbu-ref-color-brand-500: #5c7d9d; + --zenbu-ref-color-brand-600: #48647f; + --zenbu-ref-color-brand-700: #3c5167; + --zenbu-ref-color-brand-800: #354655; + --zenbu-ref-color-brand-900: #303c49; + + --zenbu-ref-color-danger-100: #f9e9e2; + --zenbu-ref-color-danger-300: #dda995; + --zenbu-ref-color-danger-500: #a9553f; + --zenbu-ref-color-danger-600: #8f4132; + --zenbu-ref-color-danger-700: #71342a; + --zenbu-ref-color-danger-900: #4b2924; + --zenbu-ref-color-success-100: #e9edd5; + --zenbu-ref-color-success-300: #afbf79; + --zenbu-ref-color-success-500: #6d8139; + --zenbu-ref-color-success-600: #55672d; + --zenbu-ref-color-success-700: #424f27; + --zenbu-ref-color-success-900: #303923; + --zenbu-ref-color-warning-100: #fcebd1; + --zenbu-ref-color-warning-300: #efb670; + --zenbu-ref-color-warning-500: #c76a26; + --zenbu-ref-color-warning-600: #a94f1c; + --zenbu-ref-color-warning-700: #873d1b; + --zenbu-ref-color-warning-900: #5b2d1d; + --zenbu-ref-color-amber-50: #fffbea; + --zenbu-ref-color-amber-100: #f6efc6; + --zenbu-ref-color-amber-300: #d9c455; + --zenbu-ref-color-amber-600: #80651e; + --zenbu-ref-color-orange-100: #fcebd1; + --zenbu-ref-color-orange-300: #efb670; + --zenbu-ref-color-rose-100: #f8e7e7; + --zenbu-ref-color-rose-300: #dfa9ad; + --zenbu-ref-color-rose-600: #924550; + --zenbu-ref-color-violet-100: #eee7ee; + --zenbu-ref-color-violet-500: #916f91; + --zenbu-ref-color-violet-700: #5e465d; + --zenbu-ref-color-blue-600: #48647f; + --zenbu-ref-color-blue-900: #303c49; + --zenbu-ref-color-blue-950: #1c252e; + --zenbu-ref-color-accent: #ffb000; + --zenbu-ref-color-accent-hover: #ffca4a; + --zenbu-ref-color-accent-pressed: #9e8124; + --zenbu-ref-color-info: #2764d7; + --zenbu-ref-color-success: #18794e; + --zenbu-ref-color-warning: #9a5b00; + --zenbu-ref-color-danger: #c62f3e; + --zenbu-ref-color-focus: #7c3aed; + --zenbu-ref-color-link-decoration: #ae5b65; + --zenbu-ref-color-black-alpha-8: rgba(0, 0, 0, 0.08); + --zenbu-ref-color-black-alpha-60: rgba(0, 0, 0, 0.6); + + --zenbu-ref-space-0: 0; + --zenbu-ref-space-0-5: 0.125rem; + --zenbu-ref-space-1: 0.25rem; + --zenbu-ref-space-1-5: 0.375rem; + --zenbu-ref-space-2: 0.5rem; + --zenbu-ref-space-2-5: 0.625rem; + --zenbu-ref-space-3: 0.75rem; + --zenbu-ref-space-4: 1rem; + --zenbu-ref-space-5: 1.25rem; + --zenbu-ref-space-6: 1.5rem; + --zenbu-ref-space-8: 2rem; + --zenbu-ref-space-10: 2.5rem; + --zenbu-ref-space-12: 3rem; + --zenbu-ref-space-16: 4rem; + --zenbu-ref-space-20: 5rem; + --zenbu-ref-space-24: 6rem; + + --zenbu-ref-font-family-sans: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", sans-serif; + --zenbu-ref-font-family-mono: + "SFMono-Regular", Consolas, "Liberation Mono", monospace; + --zenbu-ref-font-size-xs: 0.75rem; + --zenbu-ref-font-size-sm: 0.875rem; + --zenbu-ref-font-size-md: 1rem; + --zenbu-ref-font-size-lg: 1.125rem; + --zenbu-ref-font-size-xl: 1.25rem; + --zenbu-ref-font-size-2xl: 1.5rem; + --zenbu-ref-font-size-3xl: 2rem; + --zenbu-ref-font-size-display: 2.5rem; + --zenbu-ref-font-weight-regular: 400; + --zenbu-ref-font-weight-medium: 500; + --zenbu-ref-font-weight-strong: 600; + --zenbu-ref-font-weight-bold: 700; + --zenbu-ref-line-height-tight: 1.15; + --zenbu-ref-line-height-heading: 1.25; + --zenbu-ref-line-height-ui: 1.4; + --zenbu-ref-line-height-body: 1.55; + --zenbu-ref-line-height-reading: 1.7; + --zenbu-ref-letter-spacing-tight: -0.02em; + --zenbu-ref-letter-spacing-normal: 0; + --zenbu-ref-letter-spacing-wide: 0.02em; + --zenbu-ref-letter-spacing-caps: 0.06em; + + --zenbu-ref-radius-0: 0; + --zenbu-ref-radius-1: 0.25rem; + --zenbu-ref-radius-2: 0.5rem; + --zenbu-ref-radius-3: 0.75rem; + --zenbu-ref-radius-4: 1rem; + --zenbu-ref-radius-5: 1.5rem; + --zenbu-ref-radius-6: 2rem; + --zenbu-ref-radius-round: 9999px; + --zenbu-ref-stroke-0: 0; + --zenbu-ref-stroke-1: 1px; + --zenbu-ref-stroke-2: 2px; + --zenbu-ref-stroke-3: 3px; + + --zenbu-ref-shadow-1: 3px 3px 0 rgba(23, 26, 33, 0.12); + --zenbu-ref-shadow-2: 5px 5px 0 rgba(23, 26, 33, 0.16); + --zenbu-ref-shadow-3: 7px 7px 0 rgba(23, 26, 33, 0.18); + --zenbu-ref-shadow-4: 10px 10px 0 rgba(23, 26, 33, 0.2); + + --zenbu-ref-duration-instant: 50ms; + --zenbu-ref-duration-fast: 120ms; + --zenbu-ref-duration-normal: 200ms; + --zenbu-ref-duration-slow: 300ms; + --zenbu-ref-duration-deliberate: 500ms; + --zenbu-ref-ease-linear: linear; + --zenbu-ref-ease-standard: cubic-bezier(0.2, 0, 0, 1); + --zenbu-ref-ease-enter: cubic-bezier(0, 0, 0, 1); + --zenbu-ref-ease-exit: cubic-bezier(0.3, 0, 1, 1); + --zenbu-ref-ease-emphasized: cubic-bezier(0.2, 0.8, 0.2, 1); + + --zenbu-ref-opacity-disabled: 0.45; + --zenbu-ref-opacity-muted: 0.7; + --zenbu-ref-opacity-backdrop: 0.6; + --zenbu-ref-size-icon-xs: 0.75rem; + --zenbu-ref-size-icon-sm: 0.875rem; + --zenbu-ref-size-icon-md: 1rem; + --zenbu-ref-size-icon-lg: 1.25rem; + --zenbu-ref-size-icon-xl: 1.5rem; + --zenbu-ref-size-control-xs: 2rem; + --zenbu-ref-size-control-sm: 2.25rem; + --zenbu-ref-size-control-md: 2.75rem; + --zenbu-ref-size-control-lg: 3.25rem; + --zenbu-ref-size-control-xl: 3.75rem; + --zenbu-ref-size-target-min: 2.75rem; + --zenbu-ref-z-base: 0; + --zenbu-ref-z-sticky: 100; + --zenbu-ref-z-dropdown: 300; + --zenbu-ref-z-popover: 400; + --zenbu-ref-z-notification: 600; + --zenbu-ref-z-modal: 800; + --zenbu-ref-z-critical: 1000; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/src/styles/semantic.css Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,245 @@ +:root { + --zenbu-font-editorial: var(--zenbu-ref-font-family-sans); + --zenbu-font-playful: var(--zenbu-ref-font-family-sans); + --zenbu-sys-font-family-ui: var(--zenbu-ref-font-family-sans); + --zenbu-sys-font-family-content: var(--zenbu-ref-font-family-sans); + --zenbu-sys-font-family-code: var(--zenbu-ref-font-family-mono); + --zenbu-sys-font-weight-regular: var(--zenbu-ref-font-weight-regular); + --zenbu-sys-font-weight-medium: var(--zenbu-ref-font-weight-medium); + --zenbu-sys-font-weight-strong: var(--zenbu-ref-font-weight-strong); + --zenbu-sys-font-weight-bold: var(--zenbu-ref-font-weight-bold); + --zenbu-sys-font-weight-display: var(--zenbu-ref-font-weight-bold); + --zenbu-sys-font-size-xs: var(--zenbu-ref-font-size-xs); + --zenbu-sys-font-size-sm: var(--zenbu-ref-font-size-sm); + --zenbu-sys-font-size-md: var(--zenbu-ref-font-size-md); + --zenbu-sys-font-size-lg: var(--zenbu-ref-font-size-lg); + --zenbu-sys-font-size-xl: var(--zenbu-ref-font-size-xl); + --zenbu-sys-font-size-2xl: var(--zenbu-ref-font-size-2xl); + --zenbu-sys-font-size-3xl: var(--zenbu-ref-font-size-3xl); + --zenbu-sys-line-height-xs: 1.25; + --zenbu-sys-line-height-sm: var(--zenbu-ref-line-height-ui); + --zenbu-sys-line-height-md: var(--zenbu-ref-line-height-body); + --zenbu-sys-line-height-lg: 1.5; + --zenbu-sys-line-height-xl: var(--zenbu-ref-line-height-ui); + + --zenbu-sys-type-caption-family: var(--zenbu-sys-font-family-ui); + --zenbu-sys-type-caption-size: var(--zenbu-ref-font-size-xs); + --zenbu-sys-type-caption-weight: var(--zenbu-sys-font-weight-regular); + --zenbu-sys-type-caption-line-height: var(--zenbu-ref-line-height-ui); + --zenbu-sys-type-label-family: var(--zenbu-sys-font-family-ui); + --zenbu-sys-type-label-size: var(--zenbu-ref-font-size-sm); + --zenbu-sys-type-label-weight: var(--zenbu-sys-font-weight-medium); + --zenbu-sys-type-label-line-height: var(--zenbu-ref-line-height-ui); + --zenbu-sys-type-body-family: var(--zenbu-sys-font-family-ui); + --zenbu-sys-type-body-size: var(--zenbu-ref-font-size-md); + --zenbu-sys-type-body-weight: var(--zenbu-sys-font-weight-regular); + --zenbu-sys-type-body-line-height: var(--zenbu-ref-line-height-body); + --zenbu-sys-type-body-large-family: var(--zenbu-sys-font-family-ui); + --zenbu-sys-type-body-large-size: var(--zenbu-ref-font-size-lg); + --zenbu-sys-type-body-large-weight: var(--zenbu-sys-font-weight-regular); + --zenbu-sys-type-body-large-line-height: var(--zenbu-ref-line-height-body); + --zenbu-sys-type-subtitle-family: var(--zenbu-sys-font-family-ui); + --zenbu-sys-type-subtitle-size: var(--zenbu-ref-font-size-xl); + --zenbu-sys-type-subtitle-weight: var(--zenbu-sys-font-weight-strong); + --zenbu-sys-type-subtitle-line-height: var(--zenbu-ref-line-height-heading); + --zenbu-sys-type-title-family: var(--zenbu-sys-font-family-ui); + --zenbu-sys-type-title-size: var(--zenbu-ref-font-size-2xl); + --zenbu-sys-type-title-weight: var(--zenbu-sys-font-weight-strong); + --zenbu-sys-type-title-line-height: var(--zenbu-ref-line-height-heading); + --zenbu-sys-type-page-title-family: var(--zenbu-sys-font-family-ui); + --zenbu-sys-type-page-title-size: + clamp(var(--zenbu-ref-font-size-3xl), 1.5rem + 2cqi, var(--zenbu-ref-font-size-display)); + --zenbu-sys-type-page-title-weight: var(--zenbu-sys-font-weight-strong); + --zenbu-sys-type-page-title-line-height: var(--zenbu-ref-line-height-tight); + --zenbu-sys-type-reading-family: var(--zenbu-sys-font-family-content); + --zenbu-sys-type-reading-size: var(--zenbu-ref-font-size-md); + --zenbu-sys-type-reading-weight: var(--zenbu-sys-font-weight-regular); + --zenbu-sys-type-reading-line-height: var(--zenbu-ref-line-height-reading); + --zenbu-sys-type-code-family: var(--zenbu-sys-font-family-code); + --zenbu-sys-type-code-size: var(--zenbu-ref-font-size-sm); + --zenbu-sys-type-code-weight: var(--zenbu-sys-font-weight-regular); + --zenbu-sys-type-code-line-height: var(--zenbu-ref-line-height-body); + + --zenbu-sys-space-icon-label: var(--zenbu-ref-space-1); + --zenbu-sys-space-related: var(--zenbu-ref-space-2); + --zenbu-sys-space-control: var(--zenbu-ref-space-3); + --zenbu-sys-space-group: var(--zenbu-ref-space-4); + --zenbu-sys-space-container: var(--zenbu-ref-space-6); + --zenbu-sys-space-content: var(--zenbu-ref-space-8); + --zenbu-sys-space-section: var(--zenbu-ref-space-12); + --zenbu-sys-space-page: var(--zenbu-ref-space-16); + --zenbu-sys-padding-xs: var(--zenbu-ref-space-2); + --zenbu-sys-padding-sm: var(--zenbu-ref-space-3); + --zenbu-sys-padding-md: var(--zenbu-ref-space-4); + --zenbu-sys-padding-lg: var(--zenbu-ref-space-6); + --zenbu-sys-padding-xl: var(--zenbu-ref-space-8); + + --zenbu-sys-radius-control-small: var(--zenbu-ref-radius-1); + --zenbu-sys-radius-control: var(--zenbu-ref-radius-2); + --zenbu-sys-radius-control-large: var(--zenbu-ref-radius-3); + --zenbu-sys-radius-container: var(--zenbu-ref-radius-3); + --zenbu-sys-radius-panel: var(--zenbu-ref-radius-4); + --zenbu-sys-radius-dialog: var(--zenbu-ref-radius-5); + --zenbu-sys-radius-pill: var(--zenbu-ref-radius-round); + --zenbu-sys-stroke-width: var(--zenbu-ref-stroke-1); + --zenbu-sys-stroke-width-emphasis: var(--zenbu-ref-stroke-2); + --zenbu-sys-focus-width: var(--zenbu-ref-stroke-3); + --zenbu-sys-focus-offset: var(--zenbu-ref-space-0-5); + --zenbu-sys-shadow-rest: none; + --zenbu-sys-shadow-subtle: var(--zenbu-ref-shadow-1); + --zenbu-sys-shadow-raised: var(--zenbu-ref-shadow-2); + --zenbu-sys-shadow-overlay: var(--zenbu-ref-shadow-3); + --zenbu-sys-shadow-dialog: var(--zenbu-ref-shadow-4); + + --zenbu-sys-motion-duration-feedback: var(--zenbu-ref-duration-fast); + --zenbu-sys-motion-duration-state: var(--zenbu-ref-duration-normal); + --zenbu-sys-motion-duration-enter: var(--zenbu-ref-duration-normal); + --zenbu-sys-motion-duration-exit: var(--zenbu-ref-duration-fast); + --zenbu-sys-motion-duration-layout: var(--zenbu-ref-duration-slow); + --zenbu-sys-motion-ease-state: var(--zenbu-ref-ease-standard); + --zenbu-sys-motion-ease-enter: var(--zenbu-ref-ease-enter); + --zenbu-sys-motion-ease-exit: var(--zenbu-ref-ease-exit); + --zenbu-sys-motion-ease-layout: var(--zenbu-ref-ease-emphasized); + --zenbu-sys-opacity-disabled: var(--zenbu-ref-opacity-disabled); + + --zenbu-sys-control-height-xs: var(--zenbu-ref-size-control-xs); + --zenbu-sys-control-height-sm: var(--zenbu-ref-size-control-sm); + --zenbu-sys-control-height-md: var(--zenbu-ref-size-control-md); + --zenbu-sys-control-height-lg: var(--zenbu-ref-size-control-lg); + --zenbu-sys-control-height-xl: var(--zenbu-ref-size-control-xl); + --zenbu-sys-control-padding-block-xs: var(--zenbu-ref-space-1); + --zenbu-sys-control-padding-block-sm: var(--zenbu-ref-space-1-5); + --zenbu-sys-control-padding-block-md: var(--zenbu-ref-space-2); + --zenbu-sys-control-padding-block-lg: var(--zenbu-ref-space-3); + --zenbu-sys-control-padding-block-xl: var(--zenbu-ref-space-4); + --zenbu-sys-control-padding-inline-xs: var(--zenbu-ref-space-2-5); + --zenbu-sys-control-padding-inline-sm: var(--zenbu-ref-space-3); + --zenbu-sys-control-padding-inline-md: var(--zenbu-ref-space-4); + --zenbu-sys-control-padding-inline-lg: var(--zenbu-ref-space-5); + --zenbu-sys-control-padding-inline-xl: var(--zenbu-ref-space-6); + --zenbu-sys-control-gap-xs: var(--zenbu-ref-space-1); + --zenbu-sys-control-gap-sm: var(--zenbu-ref-space-1-5); + --zenbu-sys-control-gap-md: var(--zenbu-ref-space-2); + --zenbu-sys-control-gap-lg: var(--zenbu-ref-space-3); + --zenbu-sys-control-gap-xl: var(--zenbu-ref-space-4); + --zenbu-sys-control-font-size-xs: var(--zenbu-ref-font-size-xs); + --zenbu-sys-control-font-size-sm: var(--zenbu-ref-font-size-sm); + --zenbu-sys-control-font-size-md: var(--zenbu-ref-font-size-md); + --zenbu-sys-control-font-size-lg: var(--zenbu-ref-font-size-lg); + --zenbu-sys-control-font-size-xl: var(--zenbu-ref-font-size-xl); + --zenbu-sys-control-line-height-xs: 1; + --zenbu-sys-control-line-height-sm: 1; + --zenbu-sys-control-line-height-md: 1; + --zenbu-sys-control-line-height-lg: 1; + --zenbu-sys-control-line-height-xl: 1; + --zenbu-sys-icon-size-xs: var(--zenbu-ref-size-icon-xs); + --zenbu-sys-icon-size-sm: var(--zenbu-ref-size-icon-sm); + --zenbu-sys-icon-size-md: var(--zenbu-ref-size-icon-md); + --zenbu-sys-icon-size-lg: var(--zenbu-ref-size-icon-lg); + --zenbu-sys-icon-size-xl: var(--zenbu-ref-size-icon-xl); + --zenbu-sys-size-target-min: var(--zenbu-ref-size-target-min); + --zenbu-sys-width-reading: 45rem; + --zenbu-sys-width-content: 64rem; + --zenbu-sys-width-page: 90rem; + --zenbu-sys-z-sticky: var(--zenbu-ref-z-sticky); + --zenbu-sys-z-dropdown: var(--zenbu-ref-z-dropdown); + --zenbu-sys-z-popover: var(--zenbu-ref-z-popover); + --zenbu-sys-z-notification: var(--zenbu-ref-z-notification); + --zenbu-sys-z-modal: var(--zenbu-ref-z-modal); + + --zenbu-sys-color-surface-page: var(--zenbu-ref-color-neutral-100); + --zenbu-sys-color-surface-canvas: var(--zenbu-ref-color-neutral-100); + --zenbu-sys-color-surface-default: var(--zenbu-ref-color-neutral-50); + --zenbu-sys-color-surface-subtle: var(--zenbu-ref-color-neutral-200); + --zenbu-sys-color-surface-raised: var(--zenbu-ref-color-white); + --zenbu-sys-color-surface-overlay: var(--zenbu-ref-color-white); + --zenbu-sys-color-surface-sunken: var(--zenbu-ref-color-neutral-200); + --zenbu-sys-color-scrim: var(--zenbu-ref-color-black-alpha-60); + --zenbu-sys-color-text-primary: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-text-secondary: var(--zenbu-ref-color-neutral-700); + --zenbu-sys-color-text-tertiary: var(--zenbu-ref-color-neutral-600); + --zenbu-sys-color-text-placeholder: var(--zenbu-ref-color-neutral-500); + --zenbu-sys-color-text-disabled: var(--zenbu-ref-color-neutral-400); + --zenbu-sys-color-text-inverse: var(--zenbu-ref-color-white); + --zenbu-sys-color-text-link: var(--zenbu-ref-color-brand-600); + --zenbu-sys-color-link-decoration: var(--zenbu-ref-color-link-decoration); + --zenbu-sys-color-icon-primary: var(--zenbu-ref-color-neutral-900); + --zenbu-sys-color-icon-secondary: var(--zenbu-ref-color-neutral-600); + --zenbu-sys-color-border-subtle: var(--zenbu-ref-color-neutral-300); + --zenbu-sys-color-border-default: var(--zenbu-ref-color-neutral-500); + --zenbu-sys-color-border-strong: var(--zenbu-ref-color-neutral-800); + --zenbu-sys-color-action-primary-background: var(--zenbu-ref-color-accent); + --zenbu-sys-color-action-primary-background-hover: var(--zenbu-ref-color-accent-hover); + --zenbu-sys-color-action-primary-background-pressed: var(--zenbu-ref-color-accent-pressed); + --zenbu-sys-color-action-primary-foreground: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-action-primary-border: var(--zenbu-sys-color-border-strong); + --zenbu-sys-color-action-neutral-background: var(--zenbu-sys-color-surface-default); + --zenbu-sys-color-action-neutral-background-hover: var(--zenbu-sys-color-surface-subtle); + --zenbu-sys-color-action-neutral-background-pressed: var(--zenbu-sys-color-surface-sunken); + --zenbu-sys-color-action-neutral-foreground: var(--zenbu-sys-color-text-primary); + --zenbu-sys-color-action-neutral-border: var(--zenbu-sys-color-border-default); + --zenbu-sys-color-action-danger-background: var(--zenbu-ref-color-danger); + --zenbu-sys-color-action-danger-background-hover: var(--zenbu-ref-color-danger-700); + --zenbu-sys-color-action-danger-background-pressed: var(--zenbu-ref-color-danger-900); + --zenbu-sys-color-action-danger-foreground: var(--zenbu-ref-color-white); + --zenbu-sys-color-action-danger-border: var(--zenbu-ref-color-danger); + --zenbu-sys-color-focus-inner: var(--zenbu-sys-color-surface-default); + --zenbu-sys-color-focus-outer: var(--zenbu-ref-color-focus); + --zenbu-sys-color-selection-background: var(--zenbu-ref-color-warning-300); + --zenbu-sys-color-selection-foreground: var(--zenbu-sys-color-text-primary); + --zenbu-sys-color-info-background: var(--zenbu-ref-color-brand-100); + --zenbu-sys-color-info-foreground: var(--zenbu-ref-color-brand-700); + --zenbu-sys-color-info-border: var(--zenbu-ref-color-brand-400); + --zenbu-sys-color-success-background: var(--zenbu-ref-color-success-100); + --zenbu-sys-color-success-foreground: var(--zenbu-ref-color-success); + --zenbu-sys-color-success-border: var(--zenbu-ref-color-success-500); + --zenbu-sys-color-warning-background: var(--zenbu-ref-color-warning-100); + --zenbu-sys-color-warning-foreground: var(--zenbu-ref-color-warning); + --zenbu-sys-color-warning-border: var(--zenbu-ref-color-warning-500); + --zenbu-sys-color-danger-background: var(--zenbu-ref-color-danger-100); + --zenbu-sys-color-danger-foreground: var(--zenbu-ref-color-danger); + --zenbu-sys-color-danger-border: var(--zenbu-ref-color-danger-500); + --zenbu-sys-color-code-surface: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-code-text: var(--zenbu-ref-color-neutral-50); + --zenbu-sys-color-gridline: var(--zenbu-ref-color-black-alpha-8); + --zenbu-sys-color-data-1: var(--zenbu-ref-color-brand-600); + --zenbu-sys-color-data-2: var(--zenbu-ref-color-warning-500); + --zenbu-sys-color-data-3: var(--zenbu-ref-color-success-600); + --zenbu-sys-color-data-4: var(--zenbu-ref-color-violet-500); + --zenbu-sys-color-data-5: var(--zenbu-ref-color-amber-600); +} + +@media (forced-colors: active) { + :root { + --zenbu-sys-color-surface-page: Canvas; + --zenbu-sys-color-surface-canvas: Canvas; + --zenbu-sys-color-surface-default: Canvas; + --zenbu-sys-color-surface-subtle: Canvas; + --zenbu-sys-color-surface-raised: Canvas; + --zenbu-sys-color-surface-overlay: Canvas; + --zenbu-sys-color-text-primary: CanvasText; + --zenbu-sys-color-text-secondary: CanvasText; + --zenbu-sys-color-text-tertiary: GrayText; + --zenbu-sys-color-text-disabled: GrayText; + --zenbu-sys-color-text-link: LinkText; + --zenbu-sys-color-border-subtle: CanvasText; + --zenbu-sys-color-border-default: CanvasText; + --zenbu-sys-color-border-strong: CanvasText; + --zenbu-sys-color-focus-inner: Canvas; + --zenbu-sys-color-focus-outer: CanvasText; + --zenbu-sys-shadow-subtle: none; + --zenbu-sys-shadow-raised: none; + --zenbu-sys-shadow-overlay: none; + --zenbu-sys-shadow-dialog: none; + } +} + +@media (prefers-reduced-motion: reduce) { + :root { + --zenbu-sys-motion-duration-feedback: 1ms; + --zenbu-sys-motion-duration-state: 1ms; + --zenbu-sys-motion-duration-enter: 1ms; + --zenbu-sys-motion-duration-exit: 1ms; + --zenbu-sys-motion-duration-layout: 1ms; + } +}
--- a/design_system/src/styles/shadcn.css Tue Aug 04 16:49:11 2026 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1744 +0,0 @@ -:where( - zen-accordion, - zen-alert-dialog, - zen-aspect-ratio, - zen-attachment, - zen-avatar, - zen-badge, - zen-breadcrumb, - zen-bubble, - zen-button-group, - zen-calendar, - zen-carousel, - zen-chart, - zen-checkbox, - zen-collapsible, - zen-combobox, - zen-command, - zen-context-menu, - zen-data-table, - zen-date-picker, - zen-dialog, - zen-direction, - zen-drawer, - zen-dropdown-menu, - zen-empty, - zen-form, - zen-hover-card, - zen-icon, - zen-input, - zen-input-group, - zen-input-otp, - zen-item, - zen-kbd, - zen-label, - zen-marker, - zen-menubar, - zen-message, - zen-message-scroller, - zen-native-select, - zen-navigation-menu, - zen-pagination, - zen-popover, - zen-progress, - zen-radio-group, - zen-resizable, - zen-scroll-area, - zen-select, - zen-separator, - zen-sheet, - zen-sidebar, - zen-skeleton, - zen-slider, - zen-spinner, - zen-switch, - zen-table, - zen-tabs, - zen-textarea, - zen-toggle, - zen-toggle-group, - zen-tooltip, - zen-typography -) { - box-sizing: border-box; - color: var(--zen-color-text); - font-family: var(--zen-font-sans); -} - -zen-icon { - display: inline-grid; - width: var(--zen-icon-size, 1em); - height: var(--zen-icon-size, 1em); - flex: 0 0 auto; - place-items: center; - color: inherit; - vertical-align: -0.125em; -} - -zen-icon > svg { - display: block; - width: 100%; - height: 100%; -} - -:where( - zen-alert-dialog, - zen-calendar, - zen-combobox, - zen-command, - zen-date-picker, - zen-dialog, - zen-drawer, - zen-form, - zen-input-otp, - zen-popover, - zen-progress, - zen-radio-group, - zen-resizable, - zen-select, - zen-sheet, - zen-sidebar, - zen-slider, - zen-tabs -) { - display: block; -} - -:where( - zen-accordion, - zen-collapsible -) { - display: block; -} - -:where( - zen-accordion, - zen-collapsible -) details { - interpolate-size: allow-keywords; - border-block-end: 1px solid color-mix( - in srgb, - var(--zen-color-border) 28%, - transparent - ); -} - -:where( - zen-accordion, - zen-collapsible -) summary { - display: flex; - align-items: center; - gap: var(--zen-space-3); - padding: var(--zen-space-3) 0; - font-weight: 800; - list-style: none; - cursor: pointer; -} - -:where( - zen-accordion, - zen-collapsible -) summary::marker, -:where( - zen-accordion, - zen-collapsible -) summary::-webkit-details-marker { - display: none; - content: ""; -} - -:where( - zen-accordion, - zen-collapsible -) summary > [data-zen-disclosure-icon] { - margin-inline-start: auto; - transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1); -} - -:where( - zen-accordion, - zen-collapsible -) details[open] > summary > [data-zen-disclosure-icon] { - transform: rotate(180deg); -} - -:where( - zen-accordion, - zen-collapsible -) details::details-content { - block-size: 0; - overflow: clip; - opacity: 0; - transition: - block-size 220ms cubic-bezier(0.2, 0.8, 0.2, 1), - content-visibility 220ms allow-discrete, - opacity 180ms ease; -} - -:where( - zen-accordion, - zen-collapsible -) details[open]::details-content { - block-size: auto; - opacity: 1; -} - -:where( - zen-accordion, - zen-collapsible -) details > :not(summary) { - margin-block-start: 0; - padding-block-end: var(--zen-space-3); - color: var(--zen-color-text-muted); -} - -:where( - zen-alert-dialog, - zen-dialog, - zen-drawer, - zen-sheet -) > [data-zen-trigger], -:where( - zen-dropdown-menu, - zen-menubar, - zen-navigation-menu, - zen-popover -) [data-zen-trigger], -zen-empty > button, -zen-form button, -zen-input-group button { - min-height: 2.5rem; - padding: var(--zen-space-2) var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - box-shadow: var(--zen-shadow-sm); - background: var(--zen-color-accent); - color: var(--zen-color-on-accent); - font-weight: 800; - cursor: pointer; -} - -:where( - zen-alert-dialog, - zen-dialog, - zen-drawer, - zen-sheet -) dialog { - width: min(32rem, calc(100vi - 2rem)); - padding: var(--zen-space-5); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-lg); - box-shadow: var(--zen-shadow-md); - background: var(--zen-color-surface-raised); - color: var(--zen-color-text); -} - -:where( - zen-alert-dialog, - zen-dialog, - zen-drawer, - zen-sheet -) dialog[open], -:where( - zen-combobox, - zen-command, - zen-context-menu, - zen-dropdown-menu, - zen-hover-card, - zen-menubar, - zen-navigation-menu, - zen-popover, - zen-tooltip -) :where( - [data-zen-content], - [role="listbox"], - [role="menu"] -):not([hidden]) { - animation: zen-surface-open 160ms ease-out; -} - -:where( - zen-alert-dialog, - zen-dialog, - zen-drawer, - zen-sheet -) dialog::backdrop { - background: var(--zen-color-surface-overlay); - backdrop-filter: blur(3px); -} - -:where( - zen-alert-dialog, - zen-dialog, - zen-drawer, - zen-sheet -) dialog > h3 { - margin-block-start: 0; -} - -:where( - zen-alert-dialog, - zen-dialog, - zen-drawer, - zen-sheet -) dialog button { - min-height: 2.35rem; - margin-inline-end: var(--zen-space-2); - padding-inline: var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-sm); - background: var(--zen-color-surface); - color: var(--zen-color-text); - font-weight: 700; - cursor: pointer; -} - -zen-alert-dialog dialog button[value="confirm"] { - background: var(--zen-color-danger); - color: var(--zen-color-on-danger); -} - -:where(zen-sheet, zen-drawer) dialog { - position: fixed; - max-width: none; - max-height: none; - margin: 0; -} - -:where(zen-sheet, zen-drawer)[placement="right"] dialog { - inset: 0 0 0 auto; - width: min(28rem, 92vi); - height: 100vb; - border-radius: var(--zen-radius-lg) 0 0 var(--zen-radius-lg); -} - -:where(zen-sheet, zen-drawer)[placement="left"] dialog { - inset: 0 auto 0 0; - width: min(28rem, 92vi); - height: 100vb; - border-radius: 0 var(--zen-radius-lg) var(--zen-radius-lg) 0; -} - -:where(zen-sheet, zen-drawer)[placement="bottom"] dialog, -zen-drawer:not([placement]) dialog { - inset: auto 0 0; - width: 100vi; - border-radius: var(--zen-radius-lg) var(--zen-radius-lg) 0 0; -} - -zen-aspect-ratio { - display: block; - aspect-ratio: var(--zen-aspect-ratio, 16 / 9); - overflow: hidden; - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-lg); - background: var(--zen-color-surface); -} - -zen-aspect-ratio > * { - width: 100%; - height: 100%; -} - -.demo-media { - display: grid; - place-items: center; - background: var(--zen-color-surface-muted); - color: var(--zen-color-text); - font-size: var(--zen-font-size-lg); - font-weight: 900; -} - -zen-attachment { - display: grid; - grid-template-columns: 3rem minmax(0, 1fr) auto; - align-items: center; - gap: var(--zen-space-3); - padding: var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface); -} - -zen-attachment > span { - display: grid; - width: 3rem; - height: 3rem; - place-items: center; - border-radius: var(--zen-radius-sm); - background: var(--zen-color-info); - color: var(--zen-color-on-info); - font-size: var(--zen-font-size-xs); - font-weight: 900; -} - -zen-attachment > div, -zen-item > div { - display: grid; -} - -:where(zen-attachment, zen-item) small { - color: var(--zen-color-text-muted); -} - -:where(zen-attachment, zen-item) > button { - border: 0; - background: transparent; - color: inherit; - cursor: pointer; -} - -zen-avatar { - display: inline-grid; - width: var(--zen-avatar-size, 2.75rem); - height: var(--zen-avatar-size, 2.75rem); - overflow: hidden; - place-items: center; - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: 50%; - background: var(--zen-color-accent); - color: var(--zen-color-on-accent); - font-weight: 900; -} - -zen-avatar > img { - width: 100%; - height: 100%; - object-fit: cover; -} - -zen-badge { - display: inline-flex; - align-items: center; - min-height: 1.75rem; - padding: 0 var(--zen-space-2); - border: 1px solid var(--zen-color-border); - border-radius: 999px; - background: var(--zen-color-surface); - font-size: var(--zen-font-size-xs); - font-weight: 850; -} - -zen-badge[tone="success"] { - background: var(--zen-color-success); - color: var(--zen-color-on-success); -} - -zen-badge[tone="danger"] { - background: var(--zen-color-danger); - color: var(--zen-color-on-danger); -} - -:where(zen-breadcrumb, zen-pagination) nav { - display: flex; - align-items: center; - gap: var(--zen-space-2); -} - -zen-breadcrumb ol { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--zen-space-2); - margin: 0; - padding: 0; - list-style: none; -} - -zen-breadcrumb li:not(:last-child)::after { - margin-inline-start: var(--zen-space-2); - color: var(--zen-color-text-muted); - content: "/"; -} - -:where(zen-breadcrumb, zen-pagination) a { - color: var(--zen-color-info); - text-underline-offset: 0.2em; -} - -zen-bubble { - display: block; - width: fit-content; - max-width: min(42rem, 88%); - padding: var(--zen-space-3) var(--zen-space-4); - border-radius: var(--zen-radius-lg) var(--zen-radius-lg) - var(--zen-radius-lg) var(--zen-radius-sm); - background: var(--zen-color-surface-raised); - box-shadow: var(--zen-shadow-sm); -} - -zen-bubble[align="end"] { - margin-inline-start: auto; - border-radius: var(--zen-radius-lg) var(--zen-radius-lg) - var(--zen-radius-sm) var(--zen-radius-lg); - background: var(--zen-color-accent); - color: var(--zen-color-on-accent); -} - -zen-bubble > :first-child { - margin-block-start: 0; -} - -zen-bubble > :last-child { - margin-block-end: 0; -} - -zen-button-group { - display: inline-flex; -} - -zen-button-group > :where(button, a) { - min-height: 2.5rem; - padding-inline: var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: 0; - background: var(--zen-color-surface); - color: var(--zen-color-text); - font-weight: 750; - cursor: pointer; -} - -zen-button-group > :first-child { - border-radius: var(--zen-radius-md) 0 0 var(--zen-radius-md); -} - -zen-button-group > :last-child { - border-radius: 0 var(--zen-radius-md) var(--zen-radius-md) 0; -} - -zen-button-group > :not(:first-child) { - margin-inline-start: calc(-1 * var(--zen-border-width)); -} - -:where( - zen-calendar, - zen-date-picker, - zen-slider -) { - display: grid; - gap: var(--zen-space-2); - max-width: 22rem; -} - -:where( - zen-calendar, - zen-date-picker, - zen-input, - zen-input-group, - zen-input-otp, - zen-native-select, - zen-select, - zen-slider, - zen-textarea -) :where(input, select, textarea) { - appearance: none; - min-height: 2.75rem; - box-sizing: border-box; - padding: var(--zen-space-2) var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface); - color: var(--zen-color-text); - font: inherit; -} - -:where( - zen-input, - zen-input-group, - zen-input-otp, - zen-native-select, - zen-select, - zen-textarea, - zen-calendar, - zen-date-picker -) :where(input, select, textarea), -zen-field > :where(input, select, textarea) { - appearance: none; - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface); - color: var(--zen-color-text); -} - -:where(zen-native-select, zen-select) { - position: relative; -} - -:where(zen-native-select, zen-select) select { - width: 100%; - padding-inline-end: 2.75rem; - cursor: pointer; -} - -:where(zen-native-select, zen-select) > [data-zen-select-icon] { - position: absolute; - inset-inline-end: var(--zen-space-3); - inset-block-end: 0.9rem; - pointer-events: none; -} - -:where( - zen-calendar, - zen-date-picker, - zen-input, - zen-native-select, - zen-select, - zen-textarea -) { - display: grid; - gap: var(--zen-space-2); -} - -zen-textarea textarea { - width: 100%; - min-height: 8rem; - resize: vertical; -} - -:where(zen-calendar, zen-date-picker) > input[data-zen-calendar-input] { - position: absolute; - width: 1px; - height: 1px; - overflow: hidden; - clip-path: inset(50%); - pointer-events: none; -} - -:where(zen-calendar, zen-date-picker) [data-zen-generated="calendar"] { - display: grid; - gap: var(--zen-space-2); -} - -zen-date-picker [data-zen-generated="calendar"] { - position: relative; -} - -:where(zen-calendar, zen-date-picker) :where( - [data-zen-date-trigger], - [data-zen-calendar-previous], - [data-zen-calendar-next], - [data-zen-calendar-day] -) { - appearance: none; - border: 0; - background: transparent; - color: var(--zen-color-text); - font: inherit; - cursor: pointer; -} - -zen-date-picker [data-zen-date-trigger] { - display: inline-flex; - min-height: 2.75rem; - align-items: center; - gap: var(--zen-space-2); - justify-content: flex-start; - padding: var(--zen-space-2) var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - box-shadow: var(--zen-shadow-sm); - background: var(--zen-color-surface); -} - -zen-date-picker[data-invalid] [data-zen-date-trigger], -zen-calendar[data-invalid] [data-zen-calendar-view] { - border-color: var(--zen-color-danger); -} - -zen-date-picker [data-zen-calendar-panel] { - position: absolute; - z-index: 40; - inset-block-start: calc(100% + var(--zen-space-2)); - inset-inline-start: 0; - width: min(21rem, calc(100vi - 2rem)); - padding: var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-lg); - box-shadow: var(--zen-shadow-md); - background: var(--zen-color-surface-raised); - transform-origin: top left; - animation: zen-calendar-open 160ms ease-out; -} - -zen-date-picker [data-zen-calendar-panel][hidden] { - display: none; -} - -:where(zen-calendar, zen-date-picker) [data-zen-calendar-view] { - display: grid; - gap: var(--zen-space-2); - padding: var(--zen-space-3); - border: 1px solid var(--zen-color-border-muted); - border-radius: var(--zen-radius-lg); - background: var(--zen-color-surface); -} - -zen-date-picker [data-zen-calendar-view] { - padding: 0; - border: 0; -} - -:where(zen-calendar, zen-date-picker) [data-zen-calendar-view] > header { - display: grid; - grid-template-columns: 2.5rem minmax(0, 1fr) 2.5rem; - align-items: center; - gap: var(--zen-space-2); - text-align: center; -} - -:where(zen-calendar, zen-date-picker) :where( - [data-zen-calendar-previous], - [data-zen-calendar-next] -) { - display: grid; - width: 2.5rem; - height: 2.5rem; - place-items: center; - border: 1px solid var(--zen-color-border-muted); - border-radius: 50%; -} - -:where(zen-calendar, zen-date-picker) :where( - [data-zen-calendar-previous], - [data-zen-calendar-next] -):hover { - background: var(--zen-color-accent-muted); -} - -:where(zen-calendar, zen-date-picker) [data-zen-calendar-weekdays], -:where(zen-calendar, zen-date-picker) [data-zen-calendar-grid] > [role="row"] { - display: grid; - grid-template-columns: repeat(7, minmax(2rem, 1fr)); - gap: var(--zen-space-1); -} - -:where(zen-calendar, zen-date-picker) [data-zen-calendar-weekdays] > span { - padding-block: var(--zen-space-1); - color: var(--zen-color-text-muted); - font-size: var(--zen-font-size-xs); - font-weight: 800; - text-align: center; -} - -:where(zen-calendar, zen-date-picker) [data-zen-calendar-grid] { - display: grid; - gap: var(--zen-space-1); -} - -:where(zen-calendar, zen-date-picker) [data-zen-calendar-day] { - aspect-ratio: 1; - min-width: 2rem; - border-radius: 50%; - font-size: var(--zen-font-size-sm); -} - -:where(zen-calendar, zen-date-picker) [data-zen-calendar-day]:hover { - background: var(--zen-color-accent-muted); -} - -:where(zen-calendar, zen-date-picker) [data-zen-calendar-day][data-outside] { - color: var(--zen-color-text-subtle); - opacity: 0.55; -} - -:where(zen-calendar, zen-date-picker) [data-zen-calendar-day][data-today] { - outline: 1px solid var(--zen-color-info); - outline-offset: -2px; -} - -:where(zen-calendar, zen-date-picker) [data-zen-calendar-day][aria-selected="true"] { - background: var(--zen-color-accent); - color: var(--zen-color-on-accent); - font-weight: 900; -} - -:where(zen-calendar, zen-date-picker) [data-zen-calendar-day]:disabled { - color: var(--zen-color-disabled); - cursor: not-allowed; -} - -@keyframes zen-calendar-open { - from { - opacity: 0; - transform: translateY(calc(-1 * var(--zen-space-1))) scale(0.98); - } - - @keyframes zen-surface-open { - from { - opacity: 0; - transform: translateY(calc(-1 * var(--zen-space-1))) scale(0.98); - } - } -} - -zen-input-group { - display: flex; - align-items: stretch; - width: min(100%, 36rem); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface); -} - -zen-input-group > :where(span, button) { - display: inline-flex; - align-items: center; - padding-inline: var(--zen-space-3); -} - -zen-input-group input { - min-width: 0; - flex: 1; - border: 0; - border-radius: 0; -} - -zen-input-group button { - min-height: auto; - border-width: 0 0 0 var(--zen-border-width); - border-radius: 0 var(--zen-radius-sm) var(--zen-radius-sm) 0; - box-shadow: none; -} - -zen-carousel { - position: relative; - display: grid; - grid-template-columns: auto minmax(0, 1fr) auto; - align-items: center; - gap: var(--zen-space-2); -} - -zen-carousel [data-zen-viewport] { - display: flex; - grid-column: 1 / -1; - gap: var(--zen-space-3); - overflow: auto; - scroll-snap-type: inline mandatory; - scrollbar-width: none; -} - -zen-carousel [data-zen-viewport] > * { - min-width: min(22rem, 82vi); - min-height: 9rem; - padding: var(--zen-space-5); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-lg); - background: var(--zen-color-surface-raised); - scroll-snap-align: start; -} - -zen-carousel :where([data-zen-prev], [data-zen-next]) { - width: 2.5rem; - height: 2.5rem; - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: 50%; - background: var(--zen-color-surface); - color: var(--zen-color-text); - cursor: pointer; -} - -zen-carousel [data-zen-prev] { - grid-column: 1; -} - -zen-carousel [data-zen-next] { - grid-column: 3; -} - -zen-chart { - display: block; - min-height: 15rem; - padding: var(--zen-space-4); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-lg); - background: var(--zen-color-surface); -} - -zen-chart [data-zen-chart-plot] { - display: flex; - height: 12rem; - align-items: end; - justify-content: space-around; - gap: var(--zen-space-3); - border-block-end: 1px solid var(--zen-color-border); -} - -zen-chart [data-zen-chart-plot] > span { - position: relative; - width: min(3rem, 15%); - height: var(--zen-chart-value); - min-height: 1rem; - border-radius: var(--zen-radius-sm) var(--zen-radius-sm) 0 0; - background: var(--zen-color-data-1); -} - -zen-chart [data-zen-chart-plot] > span:nth-child(2) { - background: var(--zen-color-data-2); -} - -zen-chart [data-zen-chart-plot] > span:nth-child(3) { - background: var(--zen-color-data-3); -} - -zen-chart [data-zen-chart-plot] > span:nth-child(4) { - background: var(--zen-color-data-4); -} - -zen-chart [data-zen-chart-plot] > span:nth-child(5) { - background: var(--zen-color-data-5); -} - -zen-chart [data-zen-chart-plot] > span::after { - position: absolute; - inset-block-start: 100%; - inset-inline-start: 50%; - padding-block-start: var(--zen-space-1); - color: var(--zen-color-text-muted); - content: attr(data-label); - font-size: var(--zen-font-size-xs); - transform: translateX(-50%); -} - -:where(zen-checkbox, zen-switch) label, -zen-radio-group label { - display: flex; - align-items: center; - gap: var(--zen-space-2); - width: fit-content; - cursor: pointer; -} - -:where(zen-checkbox, zen-radio-group) input { - appearance: none; - display: inline-grid; - width: 1.2rem; - height: 1.2rem; - flex: 0 0 auto; - place-content: center; - border: var(--zen-border-width) solid var(--zen-color-border); - background: var(--zen-color-surface); - cursor: pointer; -} - -zen-checkbox input { - border-radius: var(--zen-radius-sm); -} - -zen-checkbox input::before { - width: 0.55rem; - height: 0.3rem; - border-block-end: 2px solid var(--zen-color-on-accent); - border-inline-start: 2px solid var(--zen-color-on-accent); - content: ""; - opacity: 0; - transform: translateY(-0.08rem) rotate(-45deg); -} - -zen-checkbox input:checked { - background: var(--zen-color-accent); -} - -zen-checkbox input:checked::before { - opacity: 1; -} - -zen-radio-group input { - border-radius: 50%; -} - -zen-radio-group input::before { - width: 0.55rem; - height: 0.55rem; - border-radius: 50%; - background: var(--zen-color-info); - content: ""; - opacity: 0; -} - -zen-radio-group input:checked::before { - opacity: 1; -} - -zen-radio-group fieldset { - display: grid; - gap: var(--zen-space-2); - padding: var(--zen-space-4); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); -} - -zen-switch input { - appearance: none; - width: 2.75rem; - height: 1.5rem; - margin: 0; - padding: 2px; - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: 999px; - background: var(--zen-color-surface); - cursor: pointer; - transition: background var(--zen-motion-fast) ease; -} - -zen-switch input::before { - display: block; - width: 1rem; - height: 1rem; - border-radius: 50%; - background: var(--zen-color-border); - content: ""; - transition: transform var(--zen-motion-fast) ease; -} - -zen-switch input:checked { - background: var(--zen-color-success); -} - -zen-switch input:checked::before { - background: var(--zen-color-on-success); - transform: translateX(1.2rem); -} - -:where(zen-combobox, zen-command) { - position: relative; - display: grid; - width: min(100%, 28rem); - gap: var(--zen-space-2); -} - -:where(zen-combobox, zen-command) > input { - min-height: 2.75rem; - padding: var(--zen-space-2) var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface); - color: var(--zen-color-text); -} - -:where( - zen-combobox, - zen-command, - zen-context-menu, - zen-dropdown-menu, - zen-menubar, - zen-navigation-menu -) :where([role="listbox"], [role="menu"]) { - z-index: 20; - display: grid; - min-width: 12rem; - padding: var(--zen-space-1); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - box-shadow: var(--zen-shadow-md); - background: var(--zen-color-surface-raised); -} - -:where( - zen-combobox, - zen-command, - zen-context-menu, - zen-dropdown-menu, - zen-menubar, - zen-navigation-menu -) :where([role="listbox"], [role="menu"])[hidden] { - display: none; -} - -:where( - zen-combobox, - zen-command, - zen-context-menu, - zen-dropdown-menu, - zen-menubar, - zen-navigation-menu -) :where([role="option"], [role="menuitem"]) { - display: block; - width: 100%; - padding: var(--zen-space-2) var(--zen-space-3); - border: 0; - border-radius: var(--zen-radius-sm); - background: transparent; - color: var(--zen-color-text); - text-align: start; - text-decoration: none; - cursor: pointer; -} - -:where( - zen-combobox, - zen-command, - zen-context-menu, - zen-dropdown-menu, - zen-menubar, - zen-navigation-menu -) :where([role="option"], [role="menuitem"]):is( - :hover, - :focus, - [aria-selected="true"] -) { - outline: none; - background: var(--zen-color-accent); - color: var(--zen-color-on-accent); -} - -:where( - zen-context-menu, - zen-dropdown-menu, - zen-navigation-menu -) { - position: relative; - display: inline-block; -} - -zen-context-menu > [data-zen-trigger] { - min-height: 8rem; - padding: var(--zen-space-5); - border: 2px dashed var(--zen-color-border); - border-radius: var(--zen-radius-lg); - background: var(--zen-color-surface); -} - -:where( - zen-dropdown-menu, - zen-navigation-menu -) > [role="menu"], -zen-navigation-menu nav > [role="menu"] { - position: absolute; - inset-block-start: calc(100% + var(--zen-space-2)); - inset-inline-start: 0; -} - -zen-context-menu > [role="menu"] { - position: fixed; -} - -zen-menubar { - position: relative; - display: flex; - width: fit-content; - padding: var(--zen-space-1); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface); -} - -zen-menubar > [data-zen-trigger] { - border: 0; - box-shadow: none; - background: transparent; - color: var(--zen-color-text); -} - -zen-menubar > [role="menu"] { - position: absolute; - inset-block-start: calc(100% + var(--zen-space-1)); -} - -zen-data-table, -zen-table { - display: block; - max-width: 100%; - overflow: auto; -} - -:where(zen-data-table, zen-table) table { - width: 100%; - border-collapse: collapse; - background: var(--zen-color-surface); -} - -:where(zen-data-table, zen-table) :where(th, td) { - padding: var(--zen-space-3); - border-block-end: 1px solid color-mix( - in srgb, - var(--zen-color-border) 30%, - transparent - ); - text-align: start; -} - -:where(zen-data-table, zen-table) th { - font-size: var(--zen-font-size-sm); -} - -zen-data-table [data-zen-sort] { - padding: 0; - border: 0; - background: transparent; - color: inherit; - font-weight: 850; - cursor: pointer; -} - -zen-direction { - display: contents; -} - -zen-empty { - display: grid; - justify-items: center; - gap: var(--zen-space-2); - padding: var(--zen-space-7) var(--zen-space-5); - border: var(--zen-border-width) dashed var(--zen-color-border); - border-radius: var(--zen-radius-lg); - background: var(--zen-color-surface); - text-align: center; -} - -zen-empty > p { - max-width: 34rem; - margin: 0; - color: var(--zen-color-text-muted); -} - -zen-form form { - display: grid; - max-width: 34rem; - gap: var(--zen-space-4); -} - -zen-form[data-invalid] form { - padding-inline-start: var(--zen-space-3); - border-inline-start: 0.35rem solid var(--zen-color-danger); -} - -:where(zen-hover-card, zen-tooltip, zen-popover) { - position: relative; - display: inline-block; -} - -:where(zen-hover-card, zen-tooltip, zen-popover) [data-zen-content] { - position: absolute; - z-index: 30; - inset-block-start: calc(100% + var(--zen-space-2)); - inset-inline-start: 0; - width: max-content; - max-width: min(22rem, calc(100vi - 2rem)); - padding: var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - box-shadow: var(--zen-shadow-md); - background: var(--zen-color-surface-raised); - color: var(--zen-color-text); -} - -:where(zen-hover-card, zen-tooltip, zen-popover) [data-zen-content][hidden] { - display: none; -} - -zen-tooltip [data-zen-content] { - padding: var(--zen-space-1) var(--zen-space-2); - background: var(--zen-color-border); - color: var(--zen-color-surface); - font-size: var(--zen-font-size-xs); -} - -zen-input-otp [data-zen-otp-slots] { - display: flex; - gap: var(--zen-space-1); -} - -zen-input-otp [data-zen-otp-slots] > span { - display: grid; - width: 2.5rem; - height: 2.75rem; - place-items: center; - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-sm); - background: var(--zen-color-surface); - font-weight: 850; -} - -zen-item { - display: grid; - grid-template-columns: auto minmax(0, 1fr) auto; - align-items: center; - gap: var(--zen-space-3); - padding: var(--zen-space-3); - border-block-end: 1px solid color-mix( - in srgb, - var(--zen-color-border) 28%, - transparent - ); -} - -zen-kbd { - display: inline-flex; - align-items: center; - gap: var(--zen-space-1); -} - -zen-kbd kbd { - min-width: 1.75rem; - padding: 0.1rem var(--zen-space-1); - border: 1px solid var(--zen-color-border); - border-block-end-width: 3px; - border-radius: var(--zen-radius-sm); - background: var(--zen-color-surface); - font: 700 var(--zen-font-size-xs)/1.5 var(--zen-font-mono); - text-align: center; -} - -zen-label { - display: block; - margin-block-end: var(--zen-space-2); - font-weight: 800; -} - -zen-marker { - display: flex; - align-items: center; - gap: var(--zen-space-2); - color: var(--zen-color-text-muted); - font-size: var(--zen-font-size-xs); - text-transform: uppercase; -} - -zen-marker::before, -zen-marker::after { - height: 1px; - flex: 1; - background: color-mix( - in srgb, - var(--zen-color-border) 32%, - transparent - ); - content: ""; -} - -zen-message { - display: grid; - grid-template-columns: auto minmax(0, 1fr); - align-items: start; - gap: var(--zen-space-3); -} - -zen-message > div { - display: grid; - gap: var(--zen-space-1); -} - -zen-message-scroller { - position: relative; - display: block; -} - -zen-message-scroller [data-zen-viewport] { - max-height: 12rem; - overflow: auto; - padding: var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface); -} - -zen-message-scroller [data-zen-scroll-end] { - position: absolute; - inset-inline-end: var(--zen-space-3); - inset-block-end: var(--zen-space-3); -} - -zen-navigation-menu nav { - display: flex; - align-items: center; - gap: var(--zen-space-3); -} - -zen-pagination nav { - flex-wrap: wrap; -} - -zen-pagination a { - display: inline-grid; - min-width: 2.35rem; - min-height: 2.35rem; - place-items: center; - padding-inline: var(--zen-space-2); - border: 1px solid transparent; - border-radius: var(--zen-radius-sm); - text-decoration: none; -} - -zen-pagination a[aria-current] { - border-color: var(--zen-color-border); - background: var(--zen-color-accent); - color: var(--zen-color-on-accent); -} - -zen-progress { - display: grid; - gap: var(--zen-space-2); -} - -zen-progress progress { - appearance: none; - width: 100%; - height: 0.75rem; - overflow: hidden; - border: 0; - border-radius: 999px; - accent-color: var(--zen-color-success); - background: var(--zen-color-surface); -} - -zen-progress progress::-webkit-progress-bar { - background: var(--zen-color-surface-muted); -} - -zen-progress progress::-webkit-progress-value { - background: var(--zen-color-success); -} - -zen-progress progress::-moz-progress-bar { - background: var(--zen-color-success); -} - -zen-resizable { - display: flex; - min-height: 12rem; - overflow: hidden; - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-lg); -} - -zen-resizable [data-zen-panel] { - min-width: 0; - flex: 1 1 50%; - padding: var(--zen-space-4); - background: var(--zen-color-surface); -} - -zen-resizable [data-zen-handle] { - width: 0.65rem; - flex: 0 0 0.65rem; - border-inline: 1px solid var(--zen-color-border); - background: var(--zen-color-accent); - cursor: col-resize; -} - -zen-scroll-area { - display: block; - max-height: 10rem; - overflow: auto; - padding: var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface); - scrollbar-color: var(--zen-color-accent) transparent; -} - -zen-separator { - display: block; -} - -zen-separator hr { - margin-block: var(--zen-space-4); - border: 0; - border-block-start: 1px solid var(--zen-color-border); -} - -zen-separator[orientation="vertical"] { - display: inline-block; - width: 1px; - min-height: 1.5rem; - background: var(--zen-color-border); -} - -zen-sidebar { - display: grid; - grid-template-columns: minmax(12rem, 17rem) minmax(0, 1fr); - gap: var(--zen-space-3); -} - -zen-sidebar > [data-zen-sidebar-trigger] { - grid-column: 1 / -1; - justify-self: start; -} - -zen-sidebar [data-zen-sidebar-panel] { - padding: var(--zen-space-3); - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: var(--zen-radius-lg); - background: var(--zen-color-surface); -} - -zen-sidebar [data-zen-sidebar-panel] nav { - display: grid; - gap: var(--zen-space-1); -} - -zen-sidebar [data-zen-sidebar-panel] a { - padding: var(--zen-space-2); - border-radius: var(--zen-radius-sm); - color: inherit; - text-decoration: none; -} - -zen-sidebar [data-zen-sidebar-panel] a:hover { - background: var(--zen-color-accent); - color: var(--zen-color-on-accent); -} - -zen-skeleton { - display: block; - width: var(--zen-skeleton-width, 100%); - height: 1rem; - border-radius: var(--zen-radius-sm); - background: - linear-gradient( - 100deg, - color-mix(in srgb, var(--zen-color-text-muted) 18%, transparent) 20%, - color-mix(in srgb, var(--zen-color-text-muted) 8%, transparent) 38%, - color-mix(in srgb, var(--zen-color-text-muted) 18%, transparent) 56% - ); - background-size: 200% 100%; - animation: zen-skeleton-shimmer 1.4s linear infinite; -} - -zen-skeleton[shape="circle"] { - width: 3rem; - height: 3rem; - border-radius: 50%; -} - -zen-slider input { - appearance: none; - width: 100%; - height: 0.5rem; - padding-inline: 0; - border: 0; - border-radius: 999px; - background: var(--zen-color-surface-sunken); - cursor: pointer; -} - -zen-slider input::-webkit-slider-thumb { - appearance: none; - width: 1.35rem; - height: 1.35rem; - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: 50%; - box-shadow: var(--zen-shadow-sm); - background: var(--zen-color-accent); -} - -zen-slider input::-moz-range-thumb { - width: 1.35rem; - height: 1.35rem; - border: var(--zen-border-width) solid var(--zen-color-border); - border-radius: 50%; - box-shadow: var(--zen-shadow-sm); - background: var(--zen-color-accent); -} - -zen-spinner { - display: inline-block; - width: 1.35rem; - height: 1.35rem; - border: 3px solid color-mix( - in srgb, - var(--zen-color-text-muted) 28%, - transparent - ); - border-inline-end-color: var(--zen-color-info); - border-radius: 50%; - animation: zen-shadcn-spin 650ms linear infinite; -} - -zen-tabs [role="tablist"] { - display: flex; - width: fit-content; - gap: var(--zen-space-1); - padding: var(--zen-space-1); - border: 1px solid var(--zen-color-border); - border-radius: var(--zen-radius-md); - background: var(--zen-color-surface); -} - -zen-tabs [role="tab"] { - min-height: 2.25rem; - padding-inline: var(--zen-space-3); - border: 0; - border-radius: var(--zen-radius-sm); - background: transparent; - color: var(--zen-color-text); - font-weight: 750; - cursor: pointer; -} - -zen-tabs [role="tab"][aria-selected="true"] { - background: var(--zen-color-accent); - color: var(--zen-color-on-accent); -} - -zen-tabs [role="tabpanel"] { - padding-block: var(--zen-space-4); -} - -zen-tabs [role="tabpanel"][hidden] { - display: none; -} - -:where(zen-toggle, zen-toggle-group) { - display: inline-flex; -} - -:where(zen-toggle, zen-toggle-group) > button { - min-height: 2.5rem; - padding-inline: var(--zen-space-3); - border: 1px solid transparent; - border-radius: var(--zen-radius-sm); - background: transparent; - color: var(--zen-color-text); - font-weight: 750; - cursor: pointer; -} - -:where(zen-toggle, zen-toggle-group) > button[aria-pressed="true"] { - border-color: var(--zen-color-border); - background: var(--zen-color-accent); - color: var(--zen-color-on-accent); -} - -zen-typography { - display: block; - max-width: 65ch; - line-height: var(--zen-line-height); -} - -zen-typography > :where(h1, h2, h3) { - line-height: 1.15; - text-wrap: balance; -} - -zen-typography blockquote { - margin-inline: 0; - padding-inline-start: var(--zen-space-4); - border-inline-start: 0.35rem solid var(--zen-color-accent); - color: var(--zen-color-text-muted); - font-style: italic; -} - -:where( - button, - summary, - [tabindex] -):focus-visible { - outline: 3px solid var(--zen-color-focus); - outline-offset: 3px; -} - -:where( - zen-calendar, - zen-checkbox, - zen-combobox, - zen-command, - zen-date-picker, - zen-field, - zen-form, - zen-input, - zen-input-group, - zen-input-otp, - zen-native-select, - zen-radio-group, - zen-select, - zen-slider, - zen-switch, - zen-textarea -) :where(input, select, textarea):focus-visible { - outline: none; -} - -@keyframes zen-shadcn-spin { - to { transform: rotate(1turn); } -} - -@keyframes zen-skeleton-shimmer { - to { background-position-x: -200%; } -} - -@media (max-width: 640px) { - zen-sidebar { - display: block; - } - - zen-sidebar > [data-zen-sidebar-trigger] { - margin-block-end: var(--zen-space-2); - } - - :where(zen-calendar, zen-date-picker) { - width: 100%; - max-width: 100%; - } - - zen-date-picker [data-zen-calendar-panel] { - position: fixed; - inset: auto var(--zen-space-3) var(--zen-space-3); - width: auto; - max-height: calc(100vb - 2 * var(--zen-space-3)); - overflow: auto; - transform-origin: bottom center; - } -} - -@media (forced-colors: active) { - :where( - zen-aspect-ratio, - zen-attachment, - zen-avatar, - zen-badge, - zen-empty, - zen-resizable, - zen-scroll-area - ) { - border: 1px solid CanvasText; - } -} - -@media (prefers-reduced-motion: reduce) { - zen-skeleton, - zen-spinner { - animation: none; - } - - zen-switch input, - zen-switch input::before, - :where( - zen-accordion, - zen-collapsible - ) details::details-content, - :where( - zen-accordion, - zen-collapsible - ) summary > [data-zen-disclosure-icon] { - transition: none; - } - - zen-date-picker [data-zen-calendar-panel] { - animation: none; - } - - :where( - zen-alert-dialog, - zen-dialog, - zen-drawer, - zen-sheet - ) dialog[open], - :where( - zen-combobox, - zen-command, - zen-context-menu, - zen-dropdown-menu, - zen-hover-card, - zen-menubar, - zen-navigation-menu, - zen-popover, - zen-tooltip - ) :where( - [data-zen-content], - [role="listbox"], - [role="menu"] - ):not([hidden]) { - animation: none; - } -}
--- a/design_system/src/styles/themes.css Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/src/styles/themes.css Wed Aug 05 05:25:40 2026 -0700 @@ -18,12 +18,6 @@ --zen-color-warning: var(--zen-color-orange-600); --zen-color-danger: var(--zen-color-red-600); --zen-color-focus: var(--zen-color-violet-600); - --zen-font-sans: var( - --zen-font-editorial, - ui-rounded, - system-ui, - sans-serif - ); } :root[data-zen-theme="ink"] { @@ -52,12 +46,6 @@ --zen-color-focus: var(--zen-color-violet-300); --zen-color-on-accent: var(--zen-color-neutral-950); --zen-color-text-inverse: var(--zen-color-neutral-950); - --zen-font-sans: var( - --zen-font-editorial, - ui-rounded, - system-ui, - sans-serif - ); } :root[data-zen-theme="playful"] { @@ -83,13 +71,156 @@ --zen-color-on-warning: var(--zen-color-neutral-50); --zen-color-focus: var(--zen-color-violet-500); --zen-color-on-accent: var(--zen-color-blue-950); - --zen-font-sans: var( - --zen-font-playful, - ui-rounded, - system-ui, - sans-serif - ); --zen-radius-sm: 0.65rem; --zen-radius-md: 1rem; --zen-radius-lg: 1.5rem; } + +:root[data-zen-theme="paper"] { + --zenbu-sys-font-family-ui: var( + --zenbu-font-editorial, + var(--zenbu-ref-font-family-sans) + ); + --zenbu-sys-font-family-content: var(--zenbu-sys-font-family-ui); + --zenbu-sys-color-surface-page: var(--zenbu-ref-color-neutral-100); + --zenbu-sys-color-surface-canvas: var(--zenbu-ref-color-neutral-100); + --zenbu-sys-color-surface-default: var(--zenbu-ref-color-neutral-50); + --zenbu-sys-color-surface-subtle: var(--zenbu-ref-color-neutral-200); + --zenbu-sys-color-surface-raised: var(--zenbu-ref-color-neutral-50); + --zenbu-sys-color-surface-sunken: var(--zenbu-ref-color-neutral-200); + --zenbu-sys-color-text-primary: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-text-secondary: var(--zenbu-ref-color-neutral-700); + --zenbu-sys-color-text-tertiary: var(--zenbu-ref-color-neutral-500); + --zenbu-sys-color-border-subtle: var(--zenbu-ref-color-neutral-300); + --zenbu-sys-color-border-default: var(--zenbu-ref-color-neutral-500); + --zenbu-sys-color-border-strong: var(--zenbu-ref-color-neutral-800); + --zenbu-sys-color-action-primary-background: var(--zenbu-ref-color-amber-300); + --zenbu-sys-color-action-primary-background-hover: var(--zenbu-ref-color-amber-100); + --zenbu-sys-color-action-primary-background-pressed: var(--zenbu-ref-color-warning-600); + --zenbu-sys-color-action-primary-foreground: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-text-link: var(--zenbu-ref-color-brand-600); + --zenbu-sys-color-focus-outer: var(--zenbu-ref-color-focus); +} + +:root[data-zen-theme="ink"], +:root[data-zen-theme="dark"] { + --zenbu-sys-font-family-ui: var( + --zenbu-font-editorial, + var(--zenbu-ref-font-family-sans) + ); + --zenbu-sys-font-family-content: var(--zenbu-sys-font-family-ui); + --zenbu-sys-color-surface-page: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-surface-canvas: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-surface-default: var(--zenbu-ref-color-neutral-900); + --zenbu-sys-color-surface-subtle: var(--zenbu-ref-color-neutral-800); + --zenbu-sys-color-surface-raised: var(--zenbu-ref-color-neutral-800); + --zenbu-sys-color-surface-sunken: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-text-primary: var(--zenbu-ref-color-neutral-50); + --zenbu-sys-color-text-secondary: var(--zenbu-ref-color-neutral-300); + --zenbu-sys-color-text-tertiary: var(--zenbu-ref-color-neutral-400); + --zenbu-sys-color-text-inverse: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-text-link: var(--zenbu-ref-color-brand-300); + --zenbu-sys-color-border-subtle: var(--zenbu-ref-color-neutral-700); + --zenbu-sys-color-border-default: var(--zenbu-ref-color-neutral-600); + --zenbu-sys-color-border-strong: var(--zenbu-ref-color-neutral-200); + --zenbu-sys-color-action-primary-background: var(--zenbu-ref-color-amber-300); + --zenbu-sys-color-action-primary-background-hover: var(--zenbu-ref-color-amber-100); + --zenbu-sys-color-action-primary-background-pressed: var(--zenbu-ref-color-warning-600); + --zenbu-sys-color-action-primary-foreground: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-action-danger-background: var(--zenbu-ref-color-danger-300); + --zenbu-sys-color-action-danger-background-hover: var(--zenbu-ref-color-danger-500); + --zenbu-sys-color-action-danger-background-pressed: var(--zenbu-ref-color-danger-600); + --zenbu-sys-color-action-danger-foreground: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-focus-inner: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-focus-outer: var(--zenbu-ref-color-neutral-50); + --zenbu-sys-color-info-background: var(--zenbu-ref-color-neutral-800); + --zenbu-sys-color-info-foreground: var(--zenbu-ref-color-brand-300); + --zenbu-sys-color-success-background: var(--zenbu-ref-color-neutral-800); + --zenbu-sys-color-success-foreground: var(--zenbu-ref-color-success-300); + --zenbu-sys-color-warning-background: var(--zenbu-ref-color-neutral-800); + --zenbu-sys-color-warning-foreground: var(--zenbu-ref-color-warning-300); + --zenbu-sys-color-danger-background: var(--zenbu-ref-color-neutral-800); + --zenbu-sys-color-danger-foreground: var(--zenbu-ref-color-danger-300); +} + +:root[data-zen-theme="playful"] { + --zenbu-sys-font-family-ui: var( + --zenbu-font-playful, + var(--zenbu-ref-font-family-sans) + ); + --zenbu-sys-font-family-content: var(--zenbu-sys-font-family-ui); + --zenbu-sys-color-surface-page: var(--zenbu-ref-color-rose-100); + --zenbu-sys-color-surface-canvas: var(--zenbu-ref-color-rose-100); + --zenbu-sys-color-surface-default: var(--zenbu-ref-color-amber-50); + --zenbu-sys-color-surface-subtle: var(--zenbu-ref-color-orange-100); + --zenbu-sys-color-surface-raised: var(--zenbu-ref-color-neutral-50); + --zenbu-sys-color-surface-sunken: var(--zenbu-ref-color-violet-100); + --zenbu-sys-color-text-primary: var(--zenbu-ref-color-blue-900); + --zenbu-sys-color-text-secondary: var(--zenbu-ref-color-violet-700); + --zenbu-sys-color-text-tertiary: var(--zenbu-ref-color-blue-600); + --zenbu-sys-color-border-subtle: var(--zenbu-ref-color-rose-300); + --zenbu-sys-color-border-default: var(--zenbu-ref-color-violet-500); + --zenbu-sys-color-border-strong: var(--zenbu-ref-color-violet-700); + --zenbu-sys-color-action-primary-background: var(--zenbu-ref-color-orange-300); + --zenbu-sys-color-action-primary-background-hover: var(--zenbu-ref-color-amber-300); + --zenbu-sys-color-action-primary-background-pressed: var(--zenbu-ref-color-warning-600); + --zenbu-sys-color-action-primary-foreground: var(--zenbu-ref-color-blue-950); + --zenbu-sys-color-text-link: var(--zenbu-ref-color-blue-600); + --zenbu-sys-color-focus-outer: var(--zenbu-ref-color-violet-500); + --zenbu-sys-radius-control-small: var(--zenbu-ref-radius-2); + --zenbu-sys-radius-control: var(--zenbu-ref-radius-4); + --zenbu-sys-radius-container: var(--zenbu-ref-radius-5); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-zen-theme]) { + --zenbu-sys-color-surface-page: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-surface-canvas: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-surface-default: var(--zenbu-ref-color-neutral-900); + --zenbu-sys-color-surface-subtle: var(--zenbu-ref-color-neutral-800); + --zenbu-sys-color-surface-raised: var(--zenbu-ref-color-neutral-800); + --zenbu-sys-color-surface-sunken: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-text-primary: var(--zenbu-ref-color-neutral-50); + --zenbu-sys-color-text-secondary: var(--zenbu-ref-color-neutral-300); + --zenbu-sys-color-text-tertiary: var(--zenbu-ref-color-neutral-400); + --zenbu-sys-color-border-subtle: var(--zenbu-ref-color-neutral-700); + --zenbu-sys-color-border-default: var(--zenbu-ref-color-neutral-600); + --zenbu-sys-color-border-strong: var(--zenbu-ref-color-neutral-200); + --zenbu-sys-color-text-link: var(--zenbu-ref-color-brand-300); + --zenbu-sys-color-focus-inner: var(--zenbu-ref-color-neutral-950); + --zenbu-sys-color-focus-outer: var(--zenbu-ref-color-neutral-50); + } +} + +@media (forced-colors: active) { + :root, + :root[data-zen-theme], + :root:not([data-zen-theme]) { + --zenbu-sys-color-surface-page: Canvas; + --zenbu-sys-color-surface-canvas: Canvas; + --zenbu-sys-color-surface-default: Canvas; + --zenbu-sys-color-surface-subtle: Canvas; + --zenbu-sys-color-surface-raised: Canvas; + --zenbu-sys-color-surface-overlay: Canvas; + --zenbu-sys-color-surface-sunken: Canvas; + --zenbu-sys-color-text-primary: CanvasText; + --zenbu-sys-color-text-secondary: CanvasText; + --zenbu-sys-color-text-tertiary: GrayText; + --zenbu-sys-color-text-disabled: GrayText; + --zenbu-sys-color-text-link: LinkText; + --zenbu-sys-color-border-subtle: CanvasText; + --zenbu-sys-color-border-default: CanvasText; + --zenbu-sys-color-border-strong: CanvasText; + --zenbu-sys-color-action-primary-background: Highlight; + --zenbu-sys-color-action-primary-background-hover: Highlight; + --zenbu-sys-color-action-primary-background-pressed: Highlight; + --zenbu-sys-color-action-primary-foreground: HighlightText; + --zenbu-sys-color-action-primary-border: Highlight; + --zenbu-sys-color-focus-inner: Canvas; + --zenbu-sys-color-focus-outer: CanvasText; + --zenbu-sys-shadow-subtle: none; + --zenbu-sys-shadow-raised: none; + --zenbu-sys-shadow-overlay: none; + --zenbu-sys-shadow-dialog: none; + } +}
--- a/design_system/src/styles/tokens.css Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/src/styles/tokens.css Wed Aug 05 05:25:40 2026 -0700 @@ -1,3 +1,7 @@ +@import url("./reference.css"); +@import url("./semantic.css"); +@import url("./density.css"); + :root { color-scheme: light; @@ -170,6 +174,7 @@ --zen-color-danger-muted: var(--zen-color-red-100); --zen-color-disabled: var(--zen-color-neutral-300); --zen-color-selection: var(--zen-color-amber-200); + --zen-color-link-decoration: var(--zen-color-clay); --zen-color-code-surface: #111318; --zen-color-code-text: #f4f4f5; --zen-color-gridline: rgba(127, 127, 127, 0.08); @@ -185,22 +190,71 @@ --zen-color-data-9: var(--zen-color-clay); --zen-color-data-10: var(--zen-color-blue-400); - --zen-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - --zen-font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace; - --zen-font-size-xs: 0.75rem; - --zen-font-size-sm: 0.875rem; - --zen-font-size-md: 1rem; - --zen-font-size-lg: 1.25rem; - --zen-font-size-xl: clamp(1.75rem, 4vw, 3rem); - --zen-line-height: 1.55; + /* Compatibility aliases. New code consumes --zenbu-sys-* tokens. */ + --zen-font-sans: var(--zenbu-sys-font-family-ui); + --zen-font-mono: var(--zenbu-sys-font-family-code); + --zen-font-size-xs: var(--zenbu-sys-font-size-xs); + --zen-font-size-sm: var(--zenbu-sys-font-size-sm); + --zen-font-size-md: var(--zenbu-sys-font-size-md); + --zen-font-size-lg: var(--zenbu-sys-font-size-lg); + --zen-font-size-xl: var(--zenbu-sys-font-size-xl); + --zen-font-size-2xl: var(--zenbu-sys-font-size-2xl); + --zen-font-size-3xl: var(--zenbu-sys-type-page-title-size); + --zen-line-height-xs: var(--zenbu-sys-line-height-xs); + --zen-line-height-sm: var(--zenbu-sys-line-height-sm); + --zen-line-height-md: var(--zenbu-sys-line-height-md); + --zen-line-height-lg: var(--zenbu-sys-line-height-lg); + --zen-line-height-xl: var(--zenbu-sys-line-height-xl); + --zen-line-height: var(--zenbu-sys-type-body-line-height); + --zen-font-weight-regular: var(--zenbu-sys-font-weight-regular); + --zen-font-weight-medium: var(--zenbu-sys-font-weight-medium); + --zen-font-weight-strong: var(--zenbu-sys-font-weight-strong); + --zen-font-weight-display: var(--zenbu-sys-font-weight-display); + + --zen-space-1: var(--zenbu-ref-space-1); + --zen-space-2: var(--zenbu-ref-space-2); + --zen-space-3: var(--zenbu-ref-space-3); + --zen-space-4: var(--zenbu-ref-space-4); + --zen-space-5: var(--zenbu-ref-space-6); + --zen-space-6: var(--zenbu-ref-space-8); + --zen-space-7: var(--zenbu-ref-space-12); - --zen-space-1: 0.25rem; - --zen-space-2: 0.5rem; - --zen-space-3: 0.75rem; - --zen-space-4: 1rem; - --zen-space-5: 1.5rem; - --zen-space-6: 2rem; - --zen-space-7: 3rem; + --zen-padding-xs: var(--zenbu-sys-padding-xs); + --zen-padding-sm: var(--zenbu-sys-padding-sm); + --zen-padding-md: var(--zenbu-sys-padding-md); + --zen-padding-lg: var(--zenbu-sys-padding-lg); + --zen-padding-xl: var(--zenbu-sys-padding-xl); + + --zen-control-height-xs: var(--zenbu-sys-control-height-xs); + --zen-control-height-sm: var(--zenbu-sys-control-height-sm); + --zen-control-height-md: var(--zenbu-sys-control-height-md); + --zen-control-height-lg: var(--zenbu-sys-control-height-lg); + --zen-control-height-xl: var(--zenbu-sys-control-height-xl); + --zen-control-padding-block-xs: var(--zenbu-sys-control-padding-block-xs); + --zen-control-padding-block-sm: var(--zenbu-sys-control-padding-block-sm); + --zen-control-padding-block-md: var(--zenbu-sys-control-padding-block-md); + --zen-control-padding-block-lg: var(--zenbu-sys-control-padding-block-lg); + --zen-control-padding-block-xl: var(--zenbu-sys-control-padding-block-xl); + --zen-control-padding-inline-xs: var(--zenbu-sys-control-padding-inline-xs); + --zen-control-padding-inline-sm: var(--zenbu-sys-control-padding-inline-sm); + --zen-control-padding-inline-md: var(--zenbu-sys-control-padding-inline-md); + --zen-control-padding-inline-lg: var(--zenbu-sys-control-padding-inline-lg); + --zen-control-padding-inline-xl: var(--zenbu-sys-control-padding-inline-xl); + --zen-control-gap-xs: var(--zenbu-sys-control-gap-xs); + --zen-control-gap-sm: var(--zenbu-sys-control-gap-sm); + --zen-control-gap-md: var(--zenbu-sys-control-gap-md); + --zen-control-gap-lg: var(--zenbu-sys-control-gap-lg); + --zen-control-gap-xl: var(--zenbu-sys-control-gap-xl); + --zen-control-line-height-xs: var(--zenbu-sys-control-line-height-xs); + --zen-control-line-height-sm: var(--zenbu-sys-control-line-height-sm); + --zen-control-line-height-md: var(--zenbu-sys-control-line-height-md); + --zen-control-line-height-lg: var(--zenbu-sys-control-line-height-lg); + --zen-control-line-height-xl: var(--zenbu-sys-control-line-height-xl); + --zen-icon-size-xs: var(--zenbu-sys-icon-size-xs); + --zen-icon-size-sm: var(--zenbu-sys-icon-size-sm); + --zen-icon-size-md: var(--zenbu-sys-icon-size-md); + --zen-icon-size-lg: var(--zenbu-sys-icon-size-lg); + --zen-icon-size-xl: var(--zenbu-sys-icon-size-xl); --zen-radius-sm: 0.35rem; --zen-radius-md: 0.7rem;
--- a/design_system/test/BUILD Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/test/BUILD Wed Aug 05 05:25:40 2026 -0700 @@ -8,8 +8,8 @@ ) js_test( - name = "storybook_test", - entry_point = "storybook_test.js", + name = "catalog_test", + entry_point = "catalog_test.js", data = [ "//design_system:design_system_server", "//hg-web/e2e:node_modules/playwright-core",
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/test/catalog_test.js Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,2100 @@ +const assert = require('node:assert/strict'); +const http = require('node:http'); +const net = require('node:net'); +const path = require('node:path'); +const { spawn } = require('node:child_process'); + +const RUNFILES = process.env.JS_BINARY__RUNFILES; +const WORKSPACE = process.env.JS_BINARY__WORKSPACE; +const runfilesWorkspace = path.join(RUNFILES, WORKSPACE); +const playwrightPath = path.join( + runfilesWorkspace, + 'hg-web/e2e/node_modules/playwright-core', +); +const { chromium } = require(playwrightPath); + +function findFreePort() { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + server.close(error => { + if (error) reject(error); + else resolve(String(address.port)); + }); + }); + }); +} + +async function stopProcess(child) { + if (!child || child.exitCode !== null) return; + child.kill('SIGTERM'); + await new Promise(resolve => { + const timer = setTimeout(() => { + if (child.exitCode === null) child.kill('SIGKILL'); + }, 3000); + child.once('exit', () => { + clearTimeout(timer); + resolve(); + }); + }); +} + +async function waitForServer(server, baseUrl, logs) { + const deadline = Date.now() + 15000; + while (Date.now() < deadline) { + if (server.exitCode !== null) { + throw new Error(`Server exited with ${server.exitCode}\n${logs.join('')}`); + } + try { + const response = await fetch(baseUrl); + if (response.ok) return; + } catch { + // Keep waiting. + } + await new Promise(resolve => setTimeout(resolve, 100)); + } + throw new Error(`Server startup timed out\n${logs.join('')}`); +} + +function requestRawPath(port, requestPath) { + return new Promise((resolve, reject) => { + const request = http.request({ + host: '127.0.0.1', + port, + method: 'GET', + path: requestPath, + agent: false, + }, response => { + response.resume(); + response.on('end', () => resolve(response.statusCode)); + }); + request.on('error', reject); + request.end(); + }); +} + +(async () => { + assert.ok(RUNFILES); + assert.ok(WORKSPACE); + const serverBinary = path.join( + runfilesWorkspace, + 'design_system/design_system_server', + ); + const chromiumPath = path.resolve(process.env.CHROMIUM_PATH); + const port = await findFreePort(); + const baseUrl = `http://127.0.0.1:${port}`; + const logs = []; + let server; + let browser; + + try { + const invalidServer = spawn(serverBinary, [], { + cwd: runfilesWorkspace, + env: { + ...process.env, + DESIGN_SYSTEM_PORT: 'not-a-port', + }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + const invalidExit = await new Promise(resolve => { + invalidServer.once('exit', (code, signal) => resolve({ code, signal })); + }); + assert.equal(invalidExit.code, 1); + assert.equal(invalidExit.signal, null); + + server = spawn(serverBinary, [], { + cwd: runfilesWorkspace, + env: { + ...process.env, + DESIGN_SYSTEM_PORT: port, + }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + server.stdout.on('data', chunk => logs.push(chunk.toString())); + server.stderr.on('data', chunk => logs.push(chunk.toString())); + await waitForServer(server, baseUrl, logs); + assert.equal(await requestRawPath(port, '/../../MODULE.bazel'), 400); + + for (const route of [ + '/', + '/tokens', + '/icons', + '/components', + '/components/button', + '/components/card', + '/components/alert', + '/components/field', + '/components/notifications', + '/components/stack', + ]) { + const response = await fetch(`${baseUrl}${route}`); + assert.equal(response.status, 200, route); + assert.match( + response.headers.get('content-type') || '', + /^text\/html/, + ); + } + for (const asset of [ + '/styles/tokens.css', + '/styles/reference.css', + '/styles/semantic.css', + '/styles/density.css', + '/styles/themes.css', + '/styles/components.css', + '/styles/elements.css', + '/components/index.js', + '/components/icon.js', + '/catalog.js', + '/catalog.js', + ]) { + const response = await fetch(`${baseUrl}${asset}`); + assert.equal(response.status, 200, asset); + assert.ok((await response.text()).length > 100, asset); + } + + browser = await chromium.launch({ + executablePath: chromiumPath, + headless: true, + args: ['--no-sandbox'], + }); + const context = await browser.newContext({ colorScheme: 'light' }); + const page = await context.newPage(); + const errors = []; + const devtools = await context.newCDPSession(page); + await devtools.send('Runtime.enable'); + devtools.on('Runtime.exceptionThrown', event => { + const exception = event.exceptionDetails; + errors.push( + `${exception.text}: ${exception.exception?.description || ''} ` + + `${exception.url || ''}:${exception.lineNumber + 1}`, + ); + }); + page.on('pageerror', error => errors.push(error.stack || error.message)); + page.on('console', message => { + if (message.type() === 'error') { + const location = message.location(); + errors.push( + `${message.text()} ${location.url || ''}:${location.lineNumber + 1}`, + ); + } + }); + page.on('response', response => { + if (response.status() >= 400) { + errors.push(`${response.status()} ${response.url()}`); + } + }); + + await page.goto(`${baseUrl}/components/button`, { + waitUntil: 'networkidle', + }); + try { + await page.waitForFunction( + () => customElements.get('zen-button') && + customElements.get('zen-story'), + undefined, + { timeout: 5000 }, + ); + } catch (error) { + throw new Error(`${error.message}\n${errors.join('\n')}`); + } + const catalogState = await page.evaluate(async () => { + const links = [ + ...document.querySelectorAll( + '.catalog-nav[aria-label="Components"] a', + ), + ]; + const elements = [...new Set( + links.map(link => link.dataset.zenElement).filter(Boolean), + )]; + const statuses = await Promise.all( + links.map(async link => (await fetch(link.href)).status), + ); + return { + cards: document.querySelectorAll('.component-grid > a').length, + elements, + links: links.length, + missing: elements.filter(name => !customElements.get(name)), + routes: links.map(link => new URL(link.href).pathname), + statuses, + }; + }); + assert.equal(catalogState.cards, 70); + assert.equal(catalogState.links, 70); + assert.equal(catalogState.elements.length, 69); + assert.deepEqual(catalogState.missing, []); + assert.deepEqual( + [...new Set(catalogState.statuses)], + [200], + ); + const primitiveOwnership = await page.evaluate(() => { + const buttonOwners = [ + 'zen-alert', + 'zen-attachment', + 'zen-button', + 'zen-button-group', + 'zen-calendar', + 'zen-carousel', + 'zen-combobox', + 'zen-command', + 'zen-context-menu', + 'zen-data-table', + 'zen-date-picker', + 'zen-dropdown-menu', + 'zen-input-group', + 'zen-menubar', + 'zen-message-scroller', + 'zen-navigation-menu', + 'zen-notifications', + 'zen-sidebar', + 'zen-tabs', + 'zen-toggle', + 'zen-toggle-group', + ].join(','); + const controlOwners = [ + 'zen-calendar', + 'zen-checkbox', + 'zen-combobox', + 'zen-command', + 'zen-date-picker', + 'zen-field', + 'zen-form', + 'zen-input', + 'zen-input-group', + 'zen-input-otp', + 'zen-native-select', + 'zen-radio-group', + 'zen-select', + 'zen-slider', + 'zen-switch', + 'zen-textarea', + ].join(','); + return { + buttons: [...document.querySelectorAll('button')].filter(button => + !button.closest(buttonOwners) && + !button.matches('zen-story > header > button') + ).map(button => button.outerHTML), + controls: [ + ...document.querySelectorAll('input, select, textarea'), + ].filter(control => !control.closest(controlOwners)) + .map(control => control.outerHTML), + }; + }); + assert.deepEqual(primitiveOwnership, { + buttons: [], + controls: [], + }); + const brokenCatalogPages = await page.evaluate(routes => { + const failures = []; + const buttonOwners = [ + 'zen-alert', + 'zen-attachment', + 'zen-button', + 'zen-button-group', + 'zen-calendar', + 'zen-carousel', + 'zen-combobox', + 'zen-command', + 'zen-context-menu', + 'zen-data-table', + 'zen-date-picker', + 'zen-dropdown-menu', + 'zen-input-group', + 'zen-menubar', + 'zen-message-scroller', + 'zen-navigation-menu', + 'zen-notifications', + 'zen-sidebar', + 'zen-tabs', + 'zen-toggle', + 'zen-toggle-group', + ].join(','); + for (const route of ['/', '/tokens', '/icons', '/components', ...routes]) { + history.replaceState({}, '', route); + window.dispatchEvent(new PopStateEvent('popstate')); + const visible = [ + ...document.querySelectorAll('[data-catalog-page]:not([hidden])'), + ]; + const expected = route === '/' || route === '/components' + ? 'overview' + : route.slice(route.lastIndexOf('/') + 1); + if (visible.length !== 1 || + visible[0].dataset.catalogPage !== expected || + !visible[0].querySelector('h1')?.textContent.trim()) { + failures.push(route); + continue; + } + if (route.startsWith('/components/')) { + const story = visible[0].querySelector('zen-story'); + const canvas = story?.querySelector('.story-canvas'); + const source = story?.querySelector('pre code'); + if (!story || !canvas?.children.length || + !source?.textContent.trim()) { + failures.push(`${route}:empty-story`); + } + const unownedButtons = [...canvas?.querySelectorAll('button') || []] + .filter(button => !button.closest(buttonOwners)); + if (unownedButtons.length) { + failures.push(`${route}:unowned-button`); + } + } + } + history.replaceState({}, '', '/components/button'); + window.dispatchEvent(new PopStateEvent('popstate')); + return failures; + }, catalogState.routes); + assert.deepEqual(brokenCatalogPages, []); + await page.keyboard.press('/'); + assert.equal( + await page.locator('#componentSearch').evaluate( + input => document.activeElement === input, + ), + true, + ); + await page.locator('#componentSearch').fill('date'); + assert.equal( + await page.locator( + '.catalog-nav[aria-label="Components"] a:not([hidden])', + ).count(), + 2, + ); + assert.equal( + await page.locator('.component-grid > a:not([hidden])').count(), + 2, + ); + assert.match( + await page.locator('#componentSearchStatus').textContent(), + /^2 components found$/, + ); + await page.locator('#componentSearch').press('Escape'); + assert.equal(await page.locator('#componentSearch').inputValue(), ''); + assert.equal( + await page.locator( + '.catalog-nav[aria-label="Components"] a:not([hidden])', + ).count(), + 70, + ); + await page.locator('#componentSearch').fill('stack'); + assert.equal( + await page.locator( + '.catalog-nav[aria-label="Components"] a[href="/components/stack"]', + ).getAttribute('hidden'), + null, + ); + assert.equal( + await page.locator( + '.component-grid > a[href="/components/stack"]', + ).getAttribute('hidden'), + null, + ); + await page.locator('#componentSearch').press('Escape'); + assert.equal( + await page.locator('[data-catalog-page="button"]').isVisible(), + true, + ); + assert.equal( + await page.locator('.catalog-sidebar a[href="/components/button"]').getAttribute('aria-current'), + 'page', + ); + assert.match( + await page.locator('zen-story pre').first().textContent(), + /<zen-button>/, + ); + const plainPresentation = await page.evaluate(() => { + const style = document.createElement('style'); + style.textContent = ` + .application-button { + padding: 7px; + border: 3px solid currentColor; + box-shadow: none; + background: transparent; + } + .application-input { + appearance: auto; + padding: 9px; + border: 4px solid currentColor; + background: transparent; + } + `; + document.head.append(style); + const buttonHost = document.createElement('zen-button'); + buttonHost.setAttribute('appearance', 'plain'); + buttonHost.setAttribute('loading', ''); + const button = document.createElement('button'); + button.className = 'application-button'; + button.textContent = 'Plain'; + buttonHost.append(button); + + const card = document.createElement('zen-card'); + card.setAttribute('appearance', 'plain'); + const article = document.createElement('article'); + card.append(article); + + const field = document.createElement('zen-field'); + field.setAttribute('appearance', 'plain'); + const label = document.createElement('label'); + label.textContent = 'Plain field'; + const input = document.createElement('input'); + field.append(label, input); + const inputHost = document.createElement('zen-input'); + inputHost.setAttribute('appearance', 'plain'); + inputHost.setAttribute('size', 'lg'); + const applicationInput = document.createElement('input'); + applicationInput.className = 'application-input'; + inputHost.append(applicationInput); + document.body.append(buttonHost, card, field, inputHost); + + const buttonStyle = getComputedStyle(button); + const cardStyle = getComputedStyle(article); + return { + buttonBackground: buttonStyle.backgroundColor, + buttonBorder: buttonStyle.borderTopWidth, + buttonPadding: buttonStyle.paddingTop, + buttonShadow: buttonStyle.boxShadow, + buttonOpacity: buttonStyle.opacity, + buttonSpinner: getComputedStyle(button, '::before').content, + cardBackground: cardStyle.backgroundColor, + cardBorder: cardStyle.borderTopWidth, + cardShadow: cardStyle.boxShadow, + fieldDisplay: getComputedStyle(field).display, + inputAppearance: getComputedStyle(input).appearance, + plainInputAppearance: getComputedStyle(applicationInput).appearance, + plainInputBorder: getComputedStyle(applicationInput).borderTopWidth, + plainInputPadding: getComputedStyle(applicationInput).paddingTop, + labelFor: label.htmlFor, + inputId: input.id, + }; + }); + assert.deepEqual(plainPresentation, { + buttonBackground: 'rgba(0, 0, 0, 0)', + buttonBorder: '3px', + buttonPadding: '7px', + buttonShadow: 'none', + buttonOpacity: '1', + buttonSpinner: 'none', + cardBackground: 'rgba(0, 0, 0, 0)', + cardBorder: '0px', + cardShadow: 'none', + fieldDisplay: 'contents', + inputAppearance: 'auto', + plainInputAppearance: 'auto', + plainInputBorder: '4px', + plainInputPadding: '9px', + labelFor: plainPresentation.inputId, + inputId: plainPresentation.inputId, + }); + const buttonScale = await page.locator( + '[data-catalog-page="button"] zen-button[size] > button', + ).evaluateAll(buttons => buttons.map(button => { + const host = button.parentElement; + const style = getComputedStyle(button); + const hostStyle = getComputedStyle(host); + return { + fontSize: Number.parseFloat(style.fontSize), + height: button.getBoundingClientRect().height, + paddingInline: Number.parseFloat(style.paddingInlineStart), + size: host.getAttribute('size'), + variableHeight: Number.parseFloat( + hostStyle.getPropertyValue('--zenbu-control-height'), + ) * Number.parseFloat(getComputedStyle(document.documentElement).fontSize), + }; + })); + assert.deepEqual( + buttonScale.map(item => item.size), + ['xs', 'sm', 'md', 'lg', 'xl'], + ); + for (let index = 1; index < buttonScale.length; index++) { + assert.ok(buttonScale[index].height > buttonScale[index - 1].height); + assert.ok( + buttonScale[index].paddingInline > + buttonScale[index - 1].paddingInline, + ); + assert.ok( + buttonScale[index].fontSize > buttonScale[index - 1].fontSize, + ); + } + for (const item of buttonScale) { + assert.ok( + Math.abs(item.height - item.variableHeight) < 1, + JSON.stringify(buttonScale), + ); + } + const compactScale = await page.evaluate(() => { + document.documentElement.dataset.zenDensity = 'compact'; + return [...document.querySelectorAll( + '[data-catalog-page="button"] zen-button[size] > button', + )] + .map(button => button.getBoundingClientRect().height); + }); + assert.equal(compactScale.length, buttonScale.length); + for (let index = 0; index < compactScale.length; index++) { + assert.ok(compactScale[index] < buttonScale[index].height); + } + await page.evaluate(() => { + delete document.documentElement.dataset.zenDensity; + }); + + await page.emulateMedia({ reducedMotion: 'reduce' }); + assert.equal( + await page.evaluate(() => + getComputedStyle(document.documentElement) + .getPropertyValue('--zenbu-sys-motion-duration-state') + .trim() + ), + '1ms', + ); + await page.emulateMedia({ reducedMotion: 'no-preference' }); + + assert.equal( + await page.locator('zen-button[loading]:not([appearance="plain"])').getAttribute('inert'), + null, + ); + assert.equal( + await page.locator('zen-button[loading]:not([appearance="plain"]) button').getAttribute('aria-disabled'), + 'true', + ); + assert.equal( + await page.locator('zen-button[loading]:not([appearance="plain"]) button').getAttribute('aria-busy'), + 'true', + ); + await page.evaluate(() => { + window.__loadingClicks = 0; + document.querySelector('zen-button[loading]:not([appearance="plain"]) button') + .addEventListener('click', () => window.__loadingClicks++); + }); + await page.locator('zen-button[loading]:not([appearance="plain"]) button').evaluate( + button => button.click(), + ); + assert.equal(await page.evaluate(() => window.__loadingClicks), 0); + await page.locator('zen-button[loading]:not([appearance="plain"]) button').focus(); + assert.equal( + await page.locator('zen-button[loading]:not([appearance="plain"]) button').evaluate( + button => document.activeElement === button, + ), + true, + ); + await page.keyboard.press('Tab'); + assert.equal( + await page.locator('zen-button[loading]:not([appearance="plain"]) button').evaluate( + button => document.activeElement === button, + ), + false, + ); + const nativeDisabledPreserved = await page.evaluate(async () => { + const wrapper = document.createElement('zen-button'); + const button = document.createElement('button'); + button.disabled = true; + button.textContent = 'Native disabled'; + wrapper.append(button); + document.body.append(wrapper); + await customElements.whenDefined('zen-button'); + wrapper.setAttribute('disabled', ''); + wrapper.removeAttribute('disabled'); + return button.disabled; + }); + assert.equal(nativeDisabledPreserved, true); + const replacementButtonState = await page.evaluate(async () => { + const wrapper = document.createElement('zen-button'); + wrapper.setAttribute('disabled', ''); + const first = document.createElement('button'); + first.textContent = 'First'; + wrapper.append(first); + document.body.append(wrapper); + await new Promise(resolve => setTimeout(resolve)); + const second = document.createElement('button'); + second.textContent = 'Second'; + wrapper.replaceChildren(second); + await new Promise(resolve => setTimeout(resolve)); + return { + firstDisabled: first.disabled, + secondAriaDisabled: second.getAttribute('aria-disabled'), + wrapperInert: wrapper.hasAttribute('inert'), + }; + }); + assert.equal(replacementButtonState.firstDisabled, false); + assert.equal(replacementButtonState.secondAriaDisabled, 'true'); + assert.equal(replacementButtonState.wrapperInert, false); + const modifiedClickAllowed = await page.evaluate(() => { + const link = document.querySelector( + '.catalog-sidebar a[href="/components/card"]', + ); + return link.dispatchEvent(new MouseEvent('click', { + bubbles: true, + cancelable: true, + button: 0, + ctrlKey: true, + })); + }); + assert.equal(modifiedClickAllowed, true); + + await page.goto(`${baseUrl}/icons`, { waitUntil: 'networkidle' }); + const iconCount = await page.locator('#iconGrid figure').count(); + assert.ok(iconCount >= 30); + assert.equal( + await page.locator('#iconGrid figure zen-icon > svg').count(), + iconCount, + ); + const iconFallback = await page.evaluate(() => { + const icon = document.createElement('zen-icon'); + icon.setAttribute('name', 'not-a-real-icon'); + icon.setAttribute('label', 'Missing icon fallback'); + document.body.append(icon); + return { + fallback: icon.dataset.zenIcon, + hidden: icon.getAttribute('aria-hidden'), + label: icon.getAttribute('aria-label'), + role: icon.getAttribute('role'), + }; + }); + assert.deepEqual(iconFallback, { + fallback: 'alert', + hidden: null, + label: 'Missing icon fallback', + role: 'img', + }); + + await page.goto(`${baseUrl}/components/link`, { + waitUntil: 'networkidle', + }); + const linkPrimitive = page.locator('zen-link[effect="paw"]').first(); + assert.equal( + await linkPrimitive.locator( + ':scope > a > zen-icon[name="paw"][data-zen-link-decoration]', + ).count(), + 1, + ); + const pawMotion = await linkPrimitive.locator('zen-icon').evaluate(icon => { + const style = getComputedStyle(icon); + const keyframes = icon.getAnimations().some(animation => + animation.animationName === 'zen-paw-step' + ); + return { + animationDuration: style.animationDuration, + color: style.color, + keyframes, + }; + }); + assert.notEqual(pawMotion.animationDuration, '0s'); + assert.notEqual(pawMotion.color, 'rgba(0, 0, 0, 0)'); + assert.equal(pawMotion.keyframes, true); + await page.emulateMedia({ reducedMotion: 'reduce' }); + assert.equal( + await linkPrimitive.locator('zen-icon').evaluate( + icon => getComputedStyle(icon).animationName, + ), + 'none', + ); + await page.emulateMedia({ reducedMotion: 'no-preference' }); + + await page.goto(`${baseUrl}/components/text`, { + waitUntil: 'networkidle', + }); + const textScale = await page.locator('zen-text[size] > p').evaluateAll( + texts => texts.map(text => Number.parseFloat( + getComputedStyle(text).fontSize, + )), + ); + assert.equal(textScale.length, 5); + for (let index = 1; index < textScale.length; index++) { + assert.ok(textScale[index] > textScale[index - 1]); + } + + await page.goto(`${baseUrl}/components/heading`, { + waitUntil: 'networkidle', + }); + const headingScale = await page.locator( + 'zen-heading[size] > h3', + ).evaluateAll(headings => headings.map(heading => ({ + fontSize: Number.parseFloat(getComputedStyle(heading).fontSize), + weight: Number(getComputedStyle(heading).fontWeight), + }))); + assert.equal(headingScale.length, 5); + assert.ok(headingScale.every(heading => heading.weight >= 700)); + for (let index = 1; index < headingScale.length; index++) { + assert.ok( + headingScale[index].fontSize > headingScale[index - 1].fontSize, + ); + } + + await page.goto(`${baseUrl}/components/box`, { + waitUntil: 'networkidle', + }); + const boxScale = await page.locator('zen-box[padding]').evaluateAll( + boxes => boxes.map(box => Number.parseFloat( + getComputedStyle(box).paddingTop, + )), + ); + assert.equal(boxScale.length, 5); + for (let index = 1; index < boxScale.length; index++) { + assert.ok(boxScale[index] > boxScale[index - 1]); + } + + await page.goto(`${baseUrl}/components/skeleton`, { + waitUntil: 'networkidle', + }); + assert.equal( + await page.locator('zen-skeleton[width="60%"]').evaluate( + skeleton => skeleton.style.getPropertyValue( + '--zenbu-skeleton-width', + ), + ), + '60%', + ); + + await page.goto(`${baseUrl}/tokens`, { waitUntil: 'networkidle' }); + const colorTokenState = await page.evaluate(() => { + const root = document.documentElement; + const value = name => getComputedStyle(root) + .getPropertyValue(`--zen-color-${name}`) + .trim(); + const systemValue = name => getComputedStyle(root) + .getPropertyValue(`--zenbu-sys-${name}`) + .trim(); + const families = [ + 'neutral', + 'red', + 'orange', + 'amber', + 'green', + 'teal', + 'blue', + 'violet', + 'rose', + 'brown', + ]; + const steps = [ + '50', + '100', + '200', + '300', + '400', + '500', + '600', + '700', + '800', + '900', + '950', + ]; + const rampsComplete = families.every(family => + steps.every(step => value(`${family}-${step}`)) + ); + const data = Array.from( + { length: 10 }, + (_, index) => value(`data-${index + 1}`), + ); + const materials = [ + 'paper', + 'washi', + 'linen', + 'sumi', + 'brick', + 'persimmon', + 'ochre', + 'moss', + 'patina', + 'indigo', + 'plum', + 'clay', + 'wood', + ].map(value); + const lightMuted = systemValue('color-surface-subtle'); + root.dataset.zenTheme = 'dark'; + const darkMuted = systemValue('color-surface-subtle'); + const darkData = Array.from( + { length: 10 }, + (_, index) => value(`data-${index + 1}`), + ); + const themes = ['paper', 'ink', 'playful'].map(theme => { + root.dataset.zenTheme = theme; + const channels = value => value.match(/\d+(?:\.\d+)?/g) + .slice(0, 3) + .map(Number); + const luminance = value => { + const converted = channels(value).map(channel => { + const normalized = channel / 255; + return normalized <= 0.04045 + ? normalized / 12.92 + : ((normalized + 0.055) / 1.055) ** 2.4; + }); + return converted[0] * 0.2126 + + converted[1] * 0.7152 + + converted[2] * 0.0722; + }; + const contrasts = [ + 'info', + 'success', + 'warning', + 'danger', + ].map(tone => { + const probe = document.createElement('span'); + probe.style.background = + `var(--zenbu-sys-color-${tone}-background)`; + probe.style.color = + `var(--zenbu-sys-color-${tone}-foreground)`; + document.body.append(probe); + const probeStyle = getComputedStyle(probe); + const foreground = luminance(probeStyle.color); + const background = luminance(probeStyle.backgroundColor); + const contrast = (Math.max(foreground, background) + 0.05) / + (Math.min(foreground, background) + 0.05); + probe.remove(); + return contrast; + }); + return { + canvas: systemValue('color-surface-page'), + contrast: Math.min(...contrasts), + font: getComputedStyle(root) + .getPropertyValue('--zenbu-sys-font-family-ui') + .trim(), + name: theme, + radius: getComputedStyle(root) + .getPropertyValue('--zenbu-sys-radius-container') + .trim(), + }; + }); + delete root.dataset.zenTheme; + return { + darkData, + darkMuted, + data, + lightMuted, + materials, + rampsComplete, + themes, + }; + }); + assert.equal(colorTokenState.rampsComplete, true); + assert.equal(new Set(colorTokenState.materials).size, 13); + assert.equal(new Set(colorTokenState.data).size, 10); + assert.equal(new Set(colorTokenState.darkData).size, 10); + assert.notDeepEqual(colorTokenState.data, colorTokenState.darkData); + assert.equal( + new Set(colorTokenState.themes.map(theme => theme.canvas)).size, + 3, + ); + assert.notEqual( + colorTokenState.themes.find(theme => theme.name === 'paper').radius, + colorTokenState.themes.find(theme => theme.name === 'playful').radius, + ); + assert.ok( + colorTokenState.themes.every(theme => theme.contrast >= 4.5), + JSON.stringify(colorTokenState.themes), + ); + assert.notEqual( + colorTokenState.lightMuted, + colorTokenState.darkMuted, + ); + assert.equal(await page.locator('.palette-grid > div').count(), 13); + assert.equal(await page.locator('.data-palette > span').count(), 10); + await page.evaluate(() => { + document.documentElement.dataset.zenTheme = 'playful'; + }); + await page.emulateMedia({ forcedColors: 'active' }); + assert.equal( + await page.evaluate(() => + getComputedStyle(document.documentElement) + .getPropertyValue('--zenbu-sys-color-surface-page') + .trim() + .toLowerCase() + ), + 'canvas', + ); + await page.emulateMedia({ forcedColors: 'none' }); + await page.evaluate(() => { + delete document.documentElement.dataset.zenTheme; + }); + + await page.goto(`${baseUrl}/components/field`, { + waitUntil: 'networkidle', + }); + const fieldWiring = await page.locator('zen-field').first().evaluate(field => { + const label = field.querySelector('label'); + const input = field.querySelector('input'); + const help = field.querySelector('small'); + input.checkValidity(); + return { + describedBy: input.getAttribute('aria-describedby'), + helpId: help.id, + inputId: input.id, + invalid: field.hasAttribute('data-invalid'), + labelFor: label.htmlFor, + }; + }); + assert.ok(fieldWiring.inputId); + assert.equal(fieldWiring.labelFor, fieldWiring.inputId); + assert.equal(fieldWiring.describedBy, fieldWiring.helpId); + assert.equal(fieldWiring.invalid, true); + await page.locator('zen-field input').first().focus(); + assert.equal( + await page.locator('zen-field input').first().evaluate( + input => getComputedStyle(input).outlineStyle, + ), + 'none', + ); + const replacementField = await page.locator('zen-field').first().evaluate( + async field => { + const label = field.querySelector('label'); + const oldInput = field.querySelector('input'); + const blocker = document.createElement('div'); + blocker.id = 'zen-field-3'; + document.body.append(blocker); + const nextInput = document.createElement('input'); + nextInput.required = true; + oldInput.replaceWith(nextInput); + await new Promise(resolve => setTimeout(resolve)); + oldInput.dispatchEvent(new Event('invalid')); + const help = field.querySelector('small'); + nextInput.id = 'replacement-email'; + help.id = 'replacement-help'; + await new Promise(resolve => setTimeout(resolve)); + return { + describedBy: nextInput.getAttribute('aria-describedby'), + helpId: help.id, + labelFor: label.htmlFor, + nextId: nextInput.id, + blockerId: blocker.id, + }; + }, + ); + assert.ok(replacementField.nextId); + assert.notEqual(replacementField.nextId, replacementField.blockerId); + assert.equal(replacementField.labelFor, replacementField.nextId); + assert.equal(replacementField.describedBy, replacementField.helpId); + + await page.goto(`${baseUrl}/components/alert`, { + waitUntil: 'networkidle', + }); + await page.evaluate(() => { + window.__dismissed = 0; + document.addEventListener('zen-dismiss', () => { + window.__dismissed++; + }); + }); + const dismissible = page.locator('zen-alert[dismissible]'); + assert.equal(await dismissible.getAttribute('role'), 'alert'); + await dismissible.locator('[data-zen-dismiss]').click(); + assert.equal(await dismissible.count(), 0); + assert.equal(await page.evaluate(() => window.__dismissed), 1); + const authoredDismissState = await page.evaluate(async () => { + const alert = document.createElement('zen-alert'); + alert.setAttribute('dismissible', ''); + const message = document.createElement('p'); + message.textContent = 'Authored dismiss control'; + const dismiss = document.createElement('button'); + dismiss.dataset.zenDismiss = ''; + alert.append(message, dismiss); + document.body.append(alert); + await new Promise(resolve => setTimeout(resolve)); + let events = 0; + alert.addEventListener('zen-dismiss', () => events++); + alert.removeAttribute('dismissible'); + dismiss.click(); + return { + connected: alert.isConnected, + events, + }; + }); + assert.equal(authoredDismissState.connected, true); + assert.equal(authoredDismissState.events, 0); + + await page.goto(`${baseUrl}/components/accordion`, { + waitUntil: 'networkidle', + }); + const accordion = page.locator('zen-accordion').first(); + const disclosureIcon = accordion.locator( + 'summary > zen-icon[data-zen-disclosure-icon]', + ).nth(1); + const collapsedDisclosure = await disclosureIcon.evaluate(icon => { + const style = getComputedStyle(icon); + return { + duration: style.transitionDuration, + transform: style.transform, + }; + }); + assert.notEqual(collapsedDisclosure.duration, '0s'); + await accordion.locator('summary').nth(1).click(); + await page.waitForTimeout(250); + assert.notEqual( + await disclosureIcon.evaluate(icon => getComputedStyle(icon).transform), + collapsedDisclosure.transform, + ); + assert.equal( + await accordion.locator('details').first().getAttribute('open'), + null, + ); + assert.equal( + await accordion.locator('details').nth(1).getAttribute('open'), + '', + ); + await accordion.locator('summary').nth(1).press('ArrowUp'); + assert.equal( + await accordion.locator('summary').first().evaluate( + summary => document.activeElement === summary, + ), + true, + ); + + await page.goto(`${baseUrl}/components/aspect-ratio`, { + waitUntil: 'networkidle', + }); + const aspectColors = await page.locator('.demo-media').evaluate(media => { + const style = getComputedStyle(media); + const probe = document.createElement('span'); + probe.style.background = 'var(--zenbu-sys-color-surface-subtle)'; + document.body.append(probe); + const token = getComputedStyle(probe).backgroundColor; + probe.remove(); + return { + background: style.backgroundColor, + image: style.backgroundImage, + token, + }; + }); + assert.equal(aspectColors.image, 'none'); + assert.equal(aspectColors.background, aspectColors.token); + + await page.goto(`${baseUrl}/components/tabs`, { + waitUntil: 'networkidle', + }); + await page.evaluate(() => { + window.__tabValue = null; + document.querySelector('zen-tabs').addEventListener( + 'zen-change', + event => { + window.__tabValue = event.detail.value; + }, + ); + }); + await page.locator('zen-tabs [role="tab"]').nth(1).click(); + assert.equal( + await page.locator('zen-tabs [role="tab"]').nth(1) + .getAttribute('aria-selected'), + 'true', + ); + assert.equal( + await page.locator('zen-tabs [role="tabpanel"]').nth(1) + .getAttribute('hidden'), + null, + ); + assert.equal(await page.evaluate(() => window.__tabValue), 'history'); + + await page.goto(`${baseUrl}/components/dialog`, { + waitUntil: 'networkidle', + }); + await page.locator('zen-dialog [data-zen-trigger]').click(); + assert.equal(await page.locator('zen-dialog dialog').getAttribute('open'), ''); + await page.locator('zen-dialog [data-zen-close]').click(); + assert.equal(await page.locator('zen-dialog dialog').getAttribute('open'), null); + const dynamicOverlays = await page.evaluate(async () => { + const ownAction = button => { + const owner = document.createElement('zen-button'); + owner.setAttribute('size', 'md'); + owner.append(button); + return owner; + }; + const dialogHost = document.createElement('zen-dialog'); + const popoverHost = document.createElement('zen-popover'); + const tooltipHost = document.createElement('zen-tooltip'); + const menuHost = document.createElement('zen-dropdown-menu'); + document.body.append(dialogHost, popoverHost, tooltipHost, menuHost); + + const dialogTrigger = document.createElement('button'); + dialogTrigger.type = 'button'; + dialogTrigger.dataset.zenTrigger = ''; + const dialog = document.createElement('dialog'); + dialogHost.append(ownAction(dialogTrigger), dialog); + + const popoverTrigger = document.createElement('button'); + popoverTrigger.type = 'button'; + popoverTrigger.dataset.zenTrigger = ''; + const popover = document.createElement('div'); + popover.dataset.zenContent = ''; + popoverHost.append(ownAction(popoverTrigger), popover); + + const tooltipTrigger = document.createElement('button'); + tooltipTrigger.type = 'button'; + tooltipTrigger.dataset.zenTrigger = ''; + const tooltip = document.createElement('span'); + tooltip.dataset.zenContent = ''; + tooltipHost.append(ownAction(tooltipTrigger), tooltip); + + const menuTrigger = document.createElement('button'); + menuTrigger.type = 'button'; + menuTrigger.dataset.zenTrigger = ''; + const menu = document.createElement('div'); + menu.setAttribute('role', 'menu'); + const item = document.createElement('button'); + item.type = 'button'; + item.setAttribute('role', 'menuitem'); + menu.append(item); + menuHost.append(menuTrigger, menu); + + await new Promise(resolve => setTimeout(resolve)); + dialogTrigger.click(); + const dialogOpen = dialog.open; + dialog.close(); + popoverTrigger.click(); + menuTrigger.click(); + tooltipTrigger.focus(); + await new Promise(resolve => setTimeout(resolve, 375)); + return { + dialogControlled: Boolean(dialogTrigger.getAttribute('aria-controls')), + dialogOpen, + menuOpen: !menu.hidden, + popoverExpanded: popoverTrigger.getAttribute('aria-expanded'), + tooltipOpen: !tooltip.hidden, + }; + }); + assert.deepEqual(dynamicOverlays, { + dialogControlled: true, + dialogOpen: true, + menuOpen: true, + popoverExpanded: 'true', + tooltipOpen: true, + }); + + await page.goto(`${baseUrl}/components/dropdown-menu`, { + waitUntil: 'networkidle', + }); + const dropdownTrigger = page.locator( + 'zen-dropdown-menu [data-zen-trigger]', + ); + await dropdownTrigger.click(); + assert.equal( + await page.locator('zen-dropdown-menu [role="menu"]') + .getAttribute('hidden'), + null, + ); + await page.keyboard.press('Escape'); + assert.equal( + await page.locator('zen-dropdown-menu [role="menu"]') + .getAttribute('hidden'), + '', + ); + assert.equal( + await dropdownTrigger.evaluate( + trigger => document.activeElement === trigger, + ), + true, + ); + await dropdownTrigger.click(); + await page.locator('zen-dropdown-menu [role="menuitem"]').first().click(); + assert.equal( + await dropdownTrigger.evaluate( + trigger => document.activeElement === trigger, + ), + true, + ); + + await page.goto(`${baseUrl}/components/combobox`, { + waitUntil: 'networkidle', + }); + const comboboxInput = page.locator('zen-combobox input'); + await comboboxInput.fill('seo'); + assert.equal( + await page.locator('zen-combobox [role="option"]:not([hidden])').count(), + 1, + ); + await comboboxInput.press('ArrowDown'); + await comboboxInput.press('Enter'); + assert.equal(await comboboxInput.inputValue(), 'seobeo'); + assert.equal( + await page.locator('zen-combobox [role="listbox"]') + .getAttribute('hidden'), + '', + ); + + await page.goto(`${baseUrl}/components/date-picker`, { + waitUntil: 'networkidle', + }); + const dateInput = page.locator('zen-date-picker input[type="date"]'); + assert.equal(await dateInput.getAttribute('aria-hidden'), 'true'); + assert.equal( + await dateInput.evaluate(input => getComputedStyle(input).position), + 'absolute', + ); + await page.evaluate(() => { + window.__dateValue = null; + document.querySelector('zen-date-picker').addEventListener( + 'zen-change', + event => { + window.__dateValue = event.detail.value; + }, + ); + }); + const dateTrigger = page.locator( + 'zen-date-picker [data-zen-date-trigger]', + ); + await dateTrigger.click(); + assert.equal( + await page.locator('zen-date-picker [data-zen-calendar-panel]') + .getAttribute('hidden'), + null, + ); + assert.equal( + await page.locator( + 'zen-date-picker [data-zen-calendar-day]', + ).count(), + 42, + ); + const chosenDate = await page.locator( + 'zen-date-picker [data-zen-calendar-day]:not([data-outside]):not(:disabled)', + ).nth(14).getAttribute('data-zen-calendar-day'); + await page.locator( + `zen-date-picker [data-zen-calendar-day="${chosenDate}"]`, + ).click(); + assert.equal(await dateInput.inputValue(), chosenDate); + assert.equal(await page.evaluate(() => window.__dateValue), chosenDate); + assert.equal(await dateTrigger.getAttribute('aria-expanded'), 'false'); + const triggerName = await dateTrigger.evaluate(trigger => + (trigger.getAttribute('aria-labelledby') || '') + .split(/\s+/) + .map(id => document.getElementById(id)?.textContent || '') + .join(' ') + ); + assert.match(triggerName, /Deploy on/); + assert.equal( + await page.locator('zen-date-picker > label').getAttribute('for'), + await dateTrigger.getAttribute('id'), + ); + const minimumDate = `${chosenDate.slice(0, 8)}10`; + await dateInput.evaluate((input, minimum) => { + input.min = minimum; + }, minimumDate); + await page.waitForTimeout(0); + await dateTrigger.click(); + const focusedBoundary = page.locator( + `zen-date-picker [data-zen-calendar-day="${chosenDate}"]`, + ); + await focusedBoundary.press('PageUp'); + assert.equal( + await page.evaluate(() => + document.activeElement?.dataset.zenCalendarDay + ), + minimumDate, + ); + assert.equal( + await page.locator( + 'zen-date-picker [data-zen-calendar-day][tabindex="0"]:not(:disabled)', + ).count(), + 1, + ); + await page.keyboard.press('Escape'); + await dateInput.evaluate(input => { + input.disabled = true; + }); + await page.waitForTimeout(0); + assert.equal(await dateTrigger.isDisabled(), true); + await dateInput.evaluate(input => { + input.disabled = false; + input.readOnly = true; + }); + await page.waitForTimeout(0); + assert.equal(await dateTrigger.isDisabled(), true); + await dateInput.evaluate(input => { + input.readOnly = false; + input.required = true; + input.value = ''; + input.dispatchEvent(new Event('input', { bubbles: true })); + input.reportValidity(); + }); + await page.waitForTimeout(0); + assert.equal(await dateTrigger.getAttribute('aria-invalid'), 'true'); + assert.equal( + await dateTrigger.evaluate( + trigger => document.activeElement === trigger, + ), + true, + ); + assert.equal( + await page.locator('zen-date-picker').getAttribute('data-invalid'), + '', + ); + await dateInput.evaluate((input, value) => { + input.value = value; + input.dispatchEvent(new Event('input', { bubbles: true })); + }, chosenDate); + assert.equal(await dateTrigger.getAttribute('aria-invalid'), 'false'); + const earlyYear = await page.evaluate(async () => { + const calendar = document.createElement('zen-calendar'); + const input = document.createElement('input'); + input.type = 'date'; + input.value = '0001-01-01'; + input.setAttribute('aria-label', 'Early date'); + calendar.append(input); + document.body.append(calendar); + await new Promise(resolve => setTimeout(resolve)); + return { + selected: calendar.querySelector( + '[data-zen-calendar-day][aria-selected="true"]', + )?.dataset.zenCalendarDay, + value: input.value, + }; + }); + assert.deepEqual(earlyYear, { + selected: '0001-01-01', + value: '0001-01-01', + }); + const resetDateState = await page.evaluate(async () => { + const label = document.createElement('span'); + label.id = 'reset-date-label'; + label.textContent = 'Reset date'; + const form = document.createElement('form'); + const picker = document.createElement('zen-date-picker'); + const input = document.createElement('input'); + input.type = 'date'; + input.defaultValue = '2026-08-04'; + input.setAttribute('aria-labelledby', label.id); + picker.append(input); + form.append(picker); + document.body.append(label, form); + await new Promise(resolve => setTimeout(resolve)); + input.value = '2026-08-12'; + input.dispatchEvent(new Event('input', { bubbles: true })); + form.reset(); + await new Promise(resolve => setTimeout(resolve)); + const trigger = picker.querySelector('[data-zen-date-trigger]'); + return { + labelledBy: trigger.getAttribute('aria-labelledby'), + selected: picker.querySelector( + '[data-zen-calendar-day][aria-selected="true"]', + )?.dataset.zenCalendarDay, + value: input.value, + }; + }); + assert.match(resetDateState.labelledBy, /^reset-date-label /); + assert.deepEqual( + { + selected: resetDateState.selected, + value: resetDateState.value, + }, + { + selected: '2026-08-04', + value: '2026-08-04', + }, + ); + + await page.goto(`${baseUrl}/components/input-otp`, { + waitUntil: 'networkidle', + }); + await page.evaluate(() => { + window.__otpValue = null; + document.querySelector('zen-input-otp').addEventListener( + 'zen-complete', + event => { + window.__otpValue = event.detail.value; + }, + ); + }); + await page.locator('zen-input-otp input').evaluate(input => { + input.value = '12a3456'; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + assert.equal(await page.locator('zen-input-otp input').inputValue(), '123456'); + assert.equal( + await page.locator('zen-input-otp [data-zen-otp-slots] > span').count(), + 6, + ); + const otp = page.locator('zen-input-otp'); + await otp.evaluate(host => host.setAttribute('size', 'xs')); + const compactOtpSize = await otp.locator( + '[data-zen-otp-slots] > span', + ).first().evaluate(slot => slot.getBoundingClientRect().width); + await otp.evaluate(host => host.setAttribute('size', 'xl')); + const largeOtpSize = await otp.locator( + '[data-zen-otp-slots] > span', + ).first().evaluate(slot => slot.getBoundingClientRect().width); + assert.ok(largeOtpSize > compactOtpSize); + assert.equal(await page.evaluate(() => window.__otpValue), '123456'); + + await page.goto(`${baseUrl}/components/data-table`, { + waitUntil: 'networkidle', + }); + await page.locator('zen-data-table [data-zen-sort="commits"]').click(); + assert.equal( + await page.locator('zen-data-table th').nth(1).getAttribute('aria-sort'), + 'ascending', + ); + assert.equal( + await page.locator('zen-data-table tbody tr').first().locator( + '[data-key="commits"]', + ).textContent(), + '12', + ); + + await page.goto(`${baseUrl}/components/carousel`, { + waitUntil: 'networkidle', + }); + await page.evaluate(() => { + window.__carouselIndex = null; + document.querySelector('zen-carousel').addEventListener( + 'zen-change', + event => { + window.__carouselIndex = event.detail.index; + }, + ); + }); + await page.locator('zen-carousel [data-zen-next]').click(); + assert.equal(await page.evaluate(() => window.__carouselIndex), 1); + assert.equal( + await page.locator('zen-carousel [data-zen-prev]').isDisabled(), + false, + ); + + await page.goto(`${baseUrl}/components/resizable`, { + waitUntil: 'networkidle', + }); + const resizeHandle = page.locator('zen-resizable [data-zen-handle]'); + const initialPercent = Number( + await resizeHandle.getAttribute('aria-valuenow'), + ); + await resizeHandle.focus(); + await resizeHandle.press('ArrowRight'); + assert.ok( + Number(await resizeHandle.getAttribute('aria-valuenow')) > initialPercent, + ); + + await page.goto(`${baseUrl}/components/switch`, { + waitUntil: 'networkidle', + }); + const switchInput = page.locator('zen-switch input'); + assert.equal(await switchInput.getAttribute('role'), 'switch'); + assert.equal(await switchInput.getAttribute('aria-checked'), 'true'); + await switchInput.uncheck(); + assert.equal(await switchInput.getAttribute('aria-checked'), 'false'); + + await page.goto(`${baseUrl}/components/checkbox`, { + waitUntil: 'networkidle', + }); + assert.equal( + await page.locator('zen-checkbox input').evaluate( + input => getComputedStyle(input).appearance, + ), + 'none', + ); + await page.goto(`${baseUrl}/components/native-select`, { + waitUntil: 'networkidle', + }); + assert.equal( + await page.locator('zen-native-select select').evaluate( + select => getComputedStyle(select).appearance, + ), + 'none', + ); + assert.equal( + await page.locator( + 'zen-native-select > zen-icon[data-zen-select-icon]', + ).count(), + 1, + ); + await page.goto(`${baseUrl}/components/slider`, { + waitUntil: 'networkidle', + }); + assert.equal( + await page.locator('zen-slider input').evaluate( + input => getComputedStyle(input).appearance, + ), + 'none', + ); + + await page.goto(`${baseUrl}/components/toggle-group`, { + waitUntil: 'networkidle', + }); + await page.locator('zen-toggle-group button[value="right"]').click(); + assert.equal( + await page.locator('zen-toggle-group button[value="right"]') + .getAttribute('aria-pressed'), + 'true', + ); + assert.equal( + await page.locator('zen-toggle-group').getAttribute('value'), + 'right', + ); + + await page.goto(`${baseUrl}/components/sidebar`, { + waitUntil: 'networkidle', + }); + const sidebarTrigger = page.locator( + 'zen-sidebar [data-zen-sidebar-trigger]', + ); + const sidebarPanel = page.locator( + 'zen-sidebar [data-zen-sidebar-panel]', + ); + assert.equal(await sidebarTrigger.getAttribute('aria-expanded'), 'true'); + assert.equal(await sidebarPanel.getAttribute('hidden'), null); + await sidebarTrigger.click(); + assert.equal(await sidebarTrigger.getAttribute('aria-expanded'), 'false'); + assert.equal(await sidebarPanel.getAttribute('hidden'), ''); + + await page.goto(`${baseUrl}/components/notifications`, { + waitUntil: 'networkidle', + }); + const notificationScope = page.locator('zen-notifications').first(); + const notificationPosition = await notificationScope.locator( + '[data-zen-notification-stack]', + ).evaluate(stack => { + const style = getComputedStyle(stack); + return { + bottom: style.bottom, + position: style.position, + right: style.right, + }; + }); + assert.equal(notificationPosition.position, 'fixed'); + assert.notEqual(notificationPosition.bottom, 'auto'); + assert.notEqual(notificationPosition.right, 'auto'); + + const dedupe = await notificationScope.evaluate(scope => { + const source = scope.querySelector('[data-notification-demo]'); + const send = detail => source.dispatchEvent( + new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail, + }), + ); + send({ + version: 1, + id: 'dedupe', + tone: 'info', + message: 'First announcement', + announcement: 'polite', + persistent: true, + }); + send({ + version: 1, + id: 'dedupe', + tone: 'success', + message: 'Updated without duplication', + announcement: 'polite', + persistent: true, + }); + send({ version: 99, id: 'invalid' }); + return { + size: scope.size, + visible: scope.visibleCount, + }; + }); + assert.deepEqual(dedupe, { size: 1, visible: 1 }); + assert.equal( + await notificationScope.locator('article').count(), + 1, + ); + assert.match( + await notificationScope.locator('article').textContent(), + /Updated without duplication/, + ); + await page.waitForFunction(() => + document.querySelector( + 'zen-notifications [data-zen-live="polite"]', + )?.textContent === 'First announcement' + ); + + const bounded = await notificationScope.evaluate(scope => { + let accepted = 0; + for (let index = 0; index < 25; index++) { + if (scope.notify({ + version: 1, + id: `bounded-${index}`, + tone: 'info', + message: `Bounded ${index}`, + announcement: 'none', + durationMs: 120000, + })) accepted++; + } + return { + accepted, + size: scope.size, + visible: scope.visibleCount, + }; + }); + assert.deepEqual(bounded, { accepted: 25, size: 23, visible: 3 }); + assert.equal( + await notificationScope.locator('article').count(), + 3, + ); + await page.waitForFunction(() => + [...document.querySelectorAll( + 'zen-notifications article', + )].some(article => article.dataset.depth === '2') + ); + const collapsedStack = await notificationScope.locator( + '[data-zen-notification-stack]', + ).evaluate(stack => { + const articles = [...stack.querySelectorAll('article')]; + return { + depths: articles.map(article => article.dataset.depth), + height: stack.getBoundingClientRect().height, + positions: articles.map(article => + getComputedStyle(article).position + ), + transforms: articles.map(article => + getComputedStyle(article).transform + ), + }; + }); + assert.deepEqual(collapsedStack.depths, ['2', '1', '0']); + assert.deepEqual( + collapsedStack.positions, + ['absolute', 'absolute', 'absolute'], + ); + await notificationScope.locator('article').last().hover(); + await page.waitForTimeout(300); + const expandedStack = await notificationScope.locator( + '[data-zen-notification-stack]', + ).evaluate(stack => ({ + height: stack.getBoundingClientRect().height, + rects: [...stack.querySelectorAll('article')].map(article => { + const bounds = article.getBoundingClientRect(); + return { + bottom: bounds.bottom, + left: bounds.left, + right: bounds.right, + top: bounds.top, + }; + }), + tops: [...stack.querySelectorAll('article')].map( + article => Math.round(article.getBoundingClientRect().top), + ), + transforms: [...stack.querySelectorAll('article')].map( + article => getComputedStyle(article).transform, + ), + })); + assert.ok(expandedStack.height > collapsedStack.height); + assert.equal(new Set(expandedStack.tops).size, 3); + const expandedGaps = [ + expandedStack.rects[1].top - expandedStack.rects[0].bottom, + expandedStack.rects[2].top - expandedStack.rects[1].bottom, + ]; + for (const gap of expandedGaps) { + assert.ok(gap >= 10 && gap <= 14, JSON.stringify(expandedGaps)); + } + assert.notDeepEqual( + expandedStack.transforms, + collapsedStack.transforms, + ); + const gapX = + (expandedStack.rects[0].left + expandedStack.rects[0].right) / 2; + const gapY = + (expandedStack.rects[0].bottom + expandedStack.rects[1].top) / 2; + await page.mouse.move(gapX, gapY); + await page.waitForTimeout(100); + const gapState = await notificationScope.locator( + '[data-zen-notification-stack]', + ).evaluate(stack => ({ + height: stack.getBoundingClientRect().height, + hovered: stack.matches(':hover'), + })); + assert.equal(gapState.hovered, true); + assert.ok(gapState.height >= expandedStack.height - 1); + await page.mouse.move(0, 0); + + const actionScope = await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + scope.id = 'action-scope'; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + window.__notificationAction = null; + scope.addEventListener('zen-notification-action', event => { + window.__notificationAction = event.detail; + }); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'action', + tone: 'error', + message: 'Action required', + announcement: 'assertive', + persistent: true, + action: { + token: 'opaque-secret-token', + label: 'Retry', + }, + }, + })); + return scope.id; + }); + assert.equal( + await page.locator(`#${actionScope}`).evaluate( + scope => scope.records, + ), + undefined, + ); + const actionArticle = page.locator( + `#${actionScope} [data-zen-notification-id="action"]`, + ); + assert.doesNotMatch( + await actionArticle.evaluate(article => article.outerHTML), + /opaque-secret-token/, + ); + await actionArticle.locator('.zen-notification-action').focus(); + await page.locator(`#${actionScope}`).evaluate(scope => { + const source = scope.querySelector('button'); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'action-neighbor', + tone: 'info', + message: 'Neighbor', + announcement: 'none', + persistent: true, + }, + })); + }); + assert.equal( + await actionArticle.locator('.zen-notification-action').evaluate( + action => document.activeElement === action, + ), + true, + ); + await actionArticle.locator('.zen-notification-action').click(); + assert.deepEqual( + await page.evaluate(() => window.__notificationAction), + { + version: 1, + id: 'action', + token: 'opaque-secret-token', + }, + ); + await page.locator(`#${actionScope}`).evaluate(scope => { + const source = scope.querySelector('button'); + source.dispatchEvent(new CustomEvent( + 'zen-dismiss-notification', + { + bubbles: true, + composed: false, + detail: { version: 1, id: 'action' }, + }, + )); + }); + await actionArticle.waitFor({ state: 'detached' }); + + await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + scope.id = 'announcement-scope'; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + window.__announcements = []; + const region = scope.querySelector('[data-zen-live="polite"]'); + new MutationObserver(() => { + if (region.textContent) { + window.__announcements.push(region.textContent); + } + }).observe(region, { childList: true }); + for (let index = 0; index < 3; index++) { + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: `announcement-${index}`, + tone: 'info', + message: `Announcement ${index}`, + announcement: 'polite', + persistent: true, + }, + })); + } + }); + await page.waitForFunction(() => + window.__announcements?.length === 3 + ); + assert.deepEqual( + await page.evaluate(() => window.__announcements), + ['Announcement 0', 'Announcement 1', 'Announcement 2'], + ); + + const composedRejected = await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: true, + detail: { + version: 1, + id: 'composed', + tone: 'info', + message: 'Must be rejected', + announcement: 'none', + persistent: true, + }, + })); + return scope.size; + }); + assert.equal(composedRejected, 0); + + await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + scope.id = 'focus-timer-scope'; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'focus-timer', + tone: 'warning', + message: 'Focus timer', + announcement: 'none', + durationMs: 300, + action: { token: 'focus-token', label: 'Keep focused' }, + }, + })); + }); + const focusTimer = page.locator( + '#focus-timer-scope [data-zen-notification-id="focus-timer"]', + ); + await focusTimer.locator('.zen-notification-action').focus(); + await page.locator('#focus-timer-scope').evaluate(scope => { + const source = scope.querySelector(':scope > button'); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'focus-neighbor', + tone: 'info', + message: 'Focus neighbor', + announcement: 'none', + persistent: true, + }, + })); + }); + await page.waitForTimeout(500); + assert.equal(await focusTimer.count(), 1); + assert.equal( + await focusTimer.locator('.zen-notification-action').evaluate( + action => document.activeElement === action, + ), + true, + ); + await page.mouse.move(0, 0); + await page.locator('#catalogMain').focus(); + await page.waitForFunction(() => + !document.querySelector( + '#focus-timer-scope [data-zen-notification-id="focus-timer"]', + ) + ); + + await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + scope.id = 'timer-scope'; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'timer', + tone: 'info', + message: 'Paused timer', + announcement: 'none', + durationMs: 300, + }, + })); + }); + const timerArticle = page.locator('#timer-scope article'); + await timerArticle.hover(); + await page.waitForTimeout(500); + assert.equal(await timerArticle.count(), 1); + await page.mouse.move(0, 0); + await page.waitForFunction(() => + !document.querySelector('#timer-scope article') + ); + + await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + scope.id = 'hidden-timer-scope'; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'hidden-timer', + tone: 'info', + message: 'Hidden timer', + announcement: 'none', + durationMs: 300, + }, + })); + Object.defineProperty(document, 'hidden', { + configurable: true, + value: true, + }); + document.dispatchEvent(new Event('visibilitychange')); + }); + await page.waitForTimeout(500); + assert.equal( + await page.locator('#hidden-timer-scope article').count(), + 1, + ); + await page.evaluate(() => { + Object.defineProperty(document, 'hidden', { + configurable: true, + value: false, + }); + document.dispatchEvent(new Event('visibilitychange')); + }); + await page.waitForFunction(() => + !document.querySelector('#hidden-timer-scope article') + ); + + const isolated = await page.evaluate(() => { + const makeScope = id => { + const scope = document.createElement('zen-notifications'); + scope.id = id; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + return { scope, source }; + }; + const first = makeScope('scope-one'); + const second = makeScope('scope-two'); + first.source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'isolated', + tone: 'success', + message: 'Only first scope', + announcement: 'none', + persistent: true, + }, + })); + return [first.scope.size, second.scope.size]; + }); + assert.deepEqual(isolated, [1, 0]); + + const lightCanvas = await page.evaluate(() => + getComputedStyle(document.documentElement) + .getPropertyValue('--zenbu-sys-color-surface-page') + .trim() + ); + await page.locator('#themeToggle').click(); + const darkCanvas = await page.evaluate(() => + getComputedStyle(document.documentElement) + .getPropertyValue('--zenbu-sys-color-surface-page') + .trim() + ); + assert.notEqual(lightCanvas, darkCanvas); + assert.equal( + await page.evaluate(() => localStorage.getItem('zen-theme')), + 'dark', + ); + + assert.deepEqual(errors, []); + await context.close(); + + const darkContext = await browser.newContext({ colorScheme: 'dark' }); + const darkPage = await darkContext.newPage(); + await darkPage.goto(baseUrl, { waitUntil: 'networkidle' }); + assert.equal( + await darkPage.locator('#themeToggle').getAttribute('aria-pressed'), + 'true', + ); + const systemDarkCanvas = await darkPage.evaluate(() => + getComputedStyle(document.documentElement) + .getPropertyValue('--zenbu-sys-color-surface-page') + .trim() + ); + await darkPage.locator('#themeToggle').click(); + const explicitLightCanvas = await darkPage.evaluate(() => + getComputedStyle(document.documentElement) + .getPropertyValue('--zenbu-sys-color-surface-page') + .trim() + ); + assert.notEqual(systemDarkCanvas, explicitLightCanvas); + await darkContext.close(); + + const mobileContext = await browser.newContext({ + colorScheme: 'light', + viewport: { width: 390, height: 844 }, + }); + const mobilePage = await mobileContext.newPage(); + const mobileErrors = []; + mobilePage.on('pageerror', error => { + mobileErrors.push(error.stack || error.message); + }); + mobilePage.on('console', message => { + if (message.type() === 'error') mobileErrors.push(message.text()); + }); + await mobilePage.goto(baseUrl, { waitUntil: 'networkidle' }); + await mobilePage.waitForFunction(() => + customElements.get('zen-date-picker') && + document.querySelectorAll( + '.catalog-nav[aria-label="Components"] a', + ).length === 70 + ); + const mobileFailures = await mobilePage.evaluate(() => { + const routes = [ + '/', + '/tokens', + '/icons', + ...[...document.querySelectorAll( + '.catalog-nav[aria-label="Components"] a', + )].map(link => new URL(link.href).pathname), + ]; + const failures = []; + for (const route of routes) { + history.replaceState({}, '', route); + window.dispatchEvent(new PopStateEvent('popstate')); + const page = document.querySelector( + '[data-catalog-page]:not([hidden])', + ); + if (!page || + document.documentElement.scrollWidth > innerWidth + 1 || + page.getBoundingClientRect().right > innerWidth + 1) { + failures.push(route); + } + } + return failures; + }); + assert.deepEqual(mobileFailures, []); + await mobilePage.goto(`${baseUrl}/components/date-picker`, { + waitUntil: 'networkidle', + }); + await mobilePage.locator( + 'zen-date-picker [data-zen-date-trigger]', + ).click(); + const mobileCalendarBounds = await mobilePage.locator( + 'zen-date-picker [data-zen-calendar-panel]', + ).evaluate(panel => { + const bounds = panel.getBoundingClientRect(); + return { + bottom: bounds.bottom, + left: bounds.left, + right: bounds.right, + top: bounds.top, + }; + }); + assert.ok(mobileCalendarBounds.left >= 0); + assert.ok(mobileCalendarBounds.right <= 390); + assert.ok(mobileCalendarBounds.top >= 0); + assert.ok(mobileCalendarBounds.bottom <= 844); + assert.deepEqual(mobileErrors, []); + await mobileContext.close(); + } finally { + if (browser) await browser.close(); + await stopProcess(server); + } +})().catch(error => { + console.error(error.stack || error); + process.exitCode = 1; +});
--- a/design_system/test/design_system_policy_test.js Tue Aug 04 16:49:11 2026 -0700 +++ b/design_system/test/design_system_policy_test.js Wed Aug 05 05:25:40 2026 -0700 @@ -7,7 +7,8 @@ assert.ok(RUNFILES); assert.ok(WORKSPACE); -const sourceRoot = path.join(RUNFILES, WORKSPACE, 'design_system/src'); +const designRoot = path.join(RUNFILES, WORKSPACE, 'design_system'); +const sourceRoot = path.join(designRoot, 'src'); const files = []; function walk(directory) { @@ -19,6 +20,26 @@ } walk(sourceRoot); +function listMarkdown(directory) { + const markdown = []; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) markdown.push(...listMarkdown(entryPath)); + else if (entry.name.endsWith('.md')) markdown.push(entryPath); + } + return markdown; +} +const documentationFiles = [ + ...fs.readdirSync(designRoot) + .filter(name => name.endsWith('.md')) + .map(name => path.join(designRoot, name)), + ...listMarkdown(path.join(designRoot, 'wiki')), +]; +const namingFiles = [ + ...files, + ...documentationFiles, + path.join(designRoot, 'main.c'), +]; function validateCallableDocumentation( relative, @@ -58,13 +79,18 @@ ].join('|'), 'gi'); const forbiddenGlyph = /[×←→↑↓+◆◇✓✔✕✖★☆⚠]/gu; const pictographic = /\p{Extended_Pictographic}/gu; +const borrowedProductName = + /(?:shadcn|storybook|sonner|radix|tailwind|react)/gi; const failures = []; for (const file of files) { const relative = path.relative(sourceRoot, file); const source = fs.readFileSync(file, 'utf8'); - if (relative !== 'styles/tokens.css') { + if (![ + 'styles/reference.css', + 'styles/tokens.css', + ].includes(relative)) { for (const match of source.matchAll(rawColor)) { failures.push( `${relative}:${source.slice(0, match.index).split('\n').length} ` + @@ -157,7 +183,7 @@ !/(?:collapse|image|radius|spacing|style|width)/.test(property)); if (!colorProperty) continue; const withoutTokens = value.replace( - /var\(\s*--zen-[\w-]+\s*\)/g, + /var\(\s*--(?:zenbu|zen)-[\w-]+\s*\)/g, '', ); const words = withoutTokens.toLowerCase() @@ -168,7 +194,7 @@ )) { failures.push( `${relative}:${css.slice(0, declaration.index).split('\n').length} ` + - `${property} must use a --zen-* token`, + `${property} must use a --zenbu-sys-* token`, ); } } @@ -223,6 +249,54 @@ } } +const cssFiles = files.filter(file => file.endsWith('.css')); +const zenbuDeclarations = new Set(); +for (const file of cssFiles) { + const source = fs.readFileSync(file, 'utf8'); + for (const match of source.matchAll(/(--zenbu-[\w-]+)\s*:/g)) { + zenbuDeclarations.add(match[1]); + } +} +for (const file of cssFiles) { + const relative = path.relative(sourceRoot, file); + const source = fs.readFileSync(file, 'utf8'); + for (const match of source.matchAll(/var\(\s*(--zenbu-[\w-]+)/g)) { + if (!zenbuDeclarations.has(match[1])) { + failures.push(`${relative} references unknown token ${match[1]}`); + } + } + if (![ + 'styles/reference.css', + 'styles/semantic.css', + 'styles/themes.css', + 'styles/tokens.css', + ].includes(relative)) { + for (const match of source.matchAll( + /var\(\s*(--zenbu-ref-color-[\w-]+)/g, + )) { + failures.push( + `${relative} consumes reference color ${match[1]} directly`, + ); + } + } + if (![ + 'styles/themes.css', + 'styles/tokens.css', + ].includes(relative) && /var\(\s*--zen-/.test(source)) { + failures.push(`${relative} consumes deprecated --zen-* tokens`); + } +} + +for (const file of namingFiles) { + const source = fs.readFileSync(file, 'utf8'); + for (const match of source.matchAll(borrowedProductName)) { + failures.push( + `${path.relative(designRoot, file)} uses borrowed product name ` + + JSON.stringify(match[0]), + ); + } +} + for (const file of files.filter(file => file.endsWith('.js') )) {
--- a/design_system/test/storybook_test.js Tue Aug 04 16:49:11 2026 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1741 +0,0 @@ -const assert = require('node:assert/strict'); -const http = require('node:http'); -const net = require('node:net'); -const path = require('node:path'); -const { spawn } = require('node:child_process'); - -const RUNFILES = process.env.JS_BINARY__RUNFILES; -const WORKSPACE = process.env.JS_BINARY__WORKSPACE; -const runfilesWorkspace = path.join(RUNFILES, WORKSPACE); -const playwrightPath = path.join( - runfilesWorkspace, - 'hg-web/e2e/node_modules/playwright-core', -); -const { chromium } = require(playwrightPath); - -function findFreePort() { - return new Promise((resolve, reject) => { - const server = net.createServer(); - server.once('error', reject); - server.listen(0, '127.0.0.1', () => { - const address = server.address(); - server.close(error => { - if (error) reject(error); - else resolve(String(address.port)); - }); - }); - }); -} - -async function stopProcess(child) { - if (!child || child.exitCode !== null) return; - child.kill('SIGTERM'); - await new Promise(resolve => { - const timer = setTimeout(() => { - if (child.exitCode === null) child.kill('SIGKILL'); - }, 3000); - child.once('exit', () => { - clearTimeout(timer); - resolve(); - }); - }); -} - -async function waitForServer(server, baseUrl, logs) { - const deadline = Date.now() + 15000; - while (Date.now() < deadline) { - if (server.exitCode !== null) { - throw new Error(`Server exited with ${server.exitCode}\n${logs.join('')}`); - } - try { - const response = await fetch(baseUrl); - if (response.ok) return; - } catch { - // Keep waiting. - } - await new Promise(resolve => setTimeout(resolve, 100)); - } - throw new Error(`Server startup timed out\n${logs.join('')}`); -} - -function requestRawPath(port, requestPath) { - return new Promise((resolve, reject) => { - const request = http.request({ - host: '127.0.0.1', - port, - method: 'GET', - path: requestPath, - agent: false, - }, response => { - response.resume(); - response.on('end', () => resolve(response.statusCode)); - }); - request.on('error', reject); - request.end(); - }); -} - -(async () => { - assert.ok(RUNFILES); - assert.ok(WORKSPACE); - const serverBinary = path.join( - runfilesWorkspace, - 'design_system/design_system_server', - ); - const chromiumPath = path.resolve(process.env.CHROMIUM_PATH); - const port = await findFreePort(); - const baseUrl = `http://127.0.0.1:${port}`; - const logs = []; - let server; - let browser; - - try { - const invalidServer = spawn(serverBinary, [], { - cwd: runfilesWorkspace, - env: { - ...process.env, - DESIGN_SYSTEM_PORT: 'not-a-port', - }, - stdio: ['ignore', 'pipe', 'pipe'], - }); - const invalidExit = await new Promise(resolve => { - invalidServer.once('exit', (code, signal) => resolve({ code, signal })); - }); - assert.equal(invalidExit.code, 1); - assert.equal(invalidExit.signal, null); - - server = spawn(serverBinary, [], { - cwd: runfilesWorkspace, - env: { - ...process.env, - DESIGN_SYSTEM_PORT: port, - }, - stdio: ['ignore', 'pipe', 'pipe'], - }); - server.stdout.on('data', chunk => logs.push(chunk.toString())); - server.stderr.on('data', chunk => logs.push(chunk.toString())); - await waitForServer(server, baseUrl, logs); - assert.equal(await requestRawPath(port, '/../../MODULE.bazel'), 400); - - for (const route of [ - '/', - '/tokens', - '/icons', - '/components', - '/components/button', - '/components/card', - '/components/alert', - '/components/field', - '/components/notifications', - '/components/stack', - ]) { - const response = await fetch(`${baseUrl}${route}`); - assert.equal(response.status, 200, route); - assert.match( - response.headers.get('content-type') || '', - /^text\/html/, - ); - } - for (const asset of [ - '/styles/tokens.css', - '/styles/themes.css', - '/styles/components.css', - '/styles/shadcn.css', - '/components/index.js', - '/components/icon.js', - '/catalog.js', - '/storybook.js', - ]) { - const response = await fetch(`${baseUrl}${asset}`); - assert.equal(response.status, 200, asset); - assert.ok((await response.text()).length > 100, asset); - } - - browser = await chromium.launch({ - executablePath: chromiumPath, - headless: true, - args: ['--no-sandbox'], - }); - const context = await browser.newContext({ colorScheme: 'light' }); - const page = await context.newPage(); - const errors = []; - const devtools = await context.newCDPSession(page); - await devtools.send('Runtime.enable'); - devtools.on('Runtime.exceptionThrown', event => { - const exception = event.exceptionDetails; - errors.push( - `${exception.text}: ${exception.exception?.description || ''} ` + - `${exception.url || ''}:${exception.lineNumber + 1}`, - ); - }); - page.on('pageerror', error => errors.push(error.stack || error.message)); - page.on('console', message => { - if (message.type() === 'error') { - const location = message.location(); - errors.push( - `${message.text()} ${location.url || ''}:${location.lineNumber + 1}`, - ); - } - }); - page.on('response', response => { - if (response.status() >= 400) { - errors.push(`${response.status()} ${response.url()}`); - } - }); - - await page.goto(`${baseUrl}/components/button`, { - waitUntil: 'networkidle', - }); - try { - await page.waitForFunction( - () => customElements.get('zen-button') && - customElements.get('zen-story'), - undefined, - { timeout: 5000 }, - ); - } catch (error) { - throw new Error(`${error.message}\n${errors.join('\n')}`); - } - const catalogState = await page.evaluate(async () => { - const links = [ - ...document.querySelectorAll( - '.catalog-nav[aria-label="Components"] a', - ), - ]; - const elements = [...new Set( - links.map(link => link.dataset.zenElement).filter(Boolean), - )]; - const statuses = await Promise.all( - links.map(async link => (await fetch(link.href)).status), - ); - return { - cards: document.querySelectorAll('.component-grid > a').length, - elements, - links: links.length, - missing: elements.filter(name => !customElements.get(name)), - routes: links.map(link => new URL(link.href).pathname), - statuses, - }; - }); - assert.equal(catalogState.cards, 66); - assert.equal(catalogState.links, 66); - assert.equal(catalogState.elements.length, 65); - assert.deepEqual(catalogState.missing, []); - assert.deepEqual( - [...new Set(catalogState.statuses)], - [200], - ); - const brokenCatalogPages = await page.evaluate(routes => { - const failures = []; - for (const route of ['/', '/tokens', '/icons', '/components', ...routes]) { - history.replaceState({}, '', route); - window.dispatchEvent(new PopStateEvent('popstate')); - const visible = [ - ...document.querySelectorAll('[data-catalog-page]:not([hidden])'), - ]; - const expected = route === '/' || route === '/components' - ? 'overview' - : route.slice(route.lastIndexOf('/') + 1); - if (visible.length !== 1 || - visible[0].dataset.catalogPage !== expected || - !visible[0].querySelector('h1')?.textContent.trim()) { - failures.push(route); - continue; - } - if (route.startsWith('/components/')) { - const story = visible[0].querySelector('zen-story'); - const canvas = story?.querySelector('.story-canvas'); - const source = story?.querySelector('pre code'); - if (!story || !canvas?.children.length || - !source?.textContent.trim()) { - failures.push(`${route}:empty-story`); - } - } - } - history.replaceState({}, '', '/components/button'); - window.dispatchEvent(new PopStateEvent('popstate')); - return failures; - }, catalogState.routes); - assert.deepEqual(brokenCatalogPages, []); - await page.keyboard.press('/'); - assert.equal( - await page.locator('#componentSearch').evaluate( - input => document.activeElement === input, - ), - true, - ); - await page.locator('#componentSearch').fill('date'); - assert.equal( - await page.locator( - '.catalog-nav[aria-label="Components"] a:not([hidden])', - ).count(), - 2, - ); - assert.equal( - await page.locator('.component-grid > a:not([hidden])').count(), - 2, - ); - assert.match( - await page.locator('#componentSearchStatus').textContent(), - /^2 components found$/, - ); - await page.locator('#componentSearch').press('Escape'); - assert.equal(await page.locator('#componentSearch').inputValue(), ''); - assert.equal( - await page.locator( - '.catalog-nav[aria-label="Components"] a:not([hidden])', - ).count(), - 66, - ); - await page.locator('#componentSearch').fill('stack'); - assert.equal( - await page.locator( - '.catalog-nav[aria-label="Components"] a[href="/components/stack"]', - ).getAttribute('hidden'), - null, - ); - assert.equal( - await page.locator( - '.component-grid > a[href="/components/stack"]', - ).getAttribute('hidden'), - null, - ); - await page.locator('#componentSearch').press('Escape'); - assert.equal( - await page.locator('[data-catalog-page="button"]').isVisible(), - true, - ); - assert.equal( - await page.locator('.catalog-sidebar a[href="/components/button"]').getAttribute('aria-current'), - 'page', - ); - assert.match( - await page.locator('zen-story pre').first().textContent(), - /<zen-button>/, - ); - assert.equal( - await page.locator('zen-button[loading]').getAttribute('inert'), - null, - ); - assert.equal( - await page.locator('zen-button[loading] button').getAttribute('aria-disabled'), - 'true', - ); - assert.equal( - await page.locator('zen-button[loading] button').getAttribute('aria-busy'), - 'true', - ); - await page.evaluate(() => { - window.__loadingClicks = 0; - document.querySelector('zen-button[loading] button') - .addEventListener('click', () => window.__loadingClicks++); - }); - await page.locator('zen-button[loading] button').evaluate( - button => button.click(), - ); - assert.equal(await page.evaluate(() => window.__loadingClicks), 0); - await page.locator('zen-button[loading] button').focus(); - assert.equal( - await page.locator('zen-button[loading] button').evaluate( - button => document.activeElement === button, - ), - true, - ); - await page.keyboard.press('Tab'); - assert.equal( - await page.locator('zen-button[loading] button').evaluate( - button => document.activeElement === button, - ), - false, - ); - const nativeDisabledPreserved = await page.evaluate(async () => { - const wrapper = document.createElement('zen-button'); - const button = document.createElement('button'); - button.disabled = true; - button.textContent = 'Native disabled'; - wrapper.append(button); - document.body.append(wrapper); - await customElements.whenDefined('zen-button'); - wrapper.setAttribute('disabled', ''); - wrapper.removeAttribute('disabled'); - return button.disabled; - }); - assert.equal(nativeDisabledPreserved, true); - const replacementButtonState = await page.evaluate(async () => { - const wrapper = document.createElement('zen-button'); - wrapper.setAttribute('disabled', ''); - const first = document.createElement('button'); - first.textContent = 'First'; - wrapper.append(first); - document.body.append(wrapper); - await new Promise(resolve => setTimeout(resolve)); - const second = document.createElement('button'); - second.textContent = 'Second'; - wrapper.replaceChildren(second); - await new Promise(resolve => setTimeout(resolve)); - return { - firstDisabled: first.disabled, - secondAriaDisabled: second.getAttribute('aria-disabled'), - wrapperInert: wrapper.hasAttribute('inert'), - }; - }); - assert.equal(replacementButtonState.firstDisabled, false); - assert.equal(replacementButtonState.secondAriaDisabled, 'true'); - assert.equal(replacementButtonState.wrapperInert, false); - const modifiedClickAllowed = await page.evaluate(() => { - const link = document.querySelector( - '.catalog-sidebar a[href="/components/card"]', - ); - return link.dispatchEvent(new MouseEvent('click', { - bubbles: true, - cancelable: true, - button: 0, - ctrlKey: true, - })); - }); - assert.equal(modifiedClickAllowed, true); - - await page.goto(`${baseUrl}/icons`, { waitUntil: 'networkidle' }); - const iconCount = await page.locator('#iconGrid figure').count(); - assert.ok(iconCount >= 30); - assert.equal( - await page.locator('#iconGrid figure zen-icon > svg').count(), - iconCount, - ); - const iconFallback = await page.evaluate(() => { - const icon = document.createElement('zen-icon'); - icon.setAttribute('name', 'not-a-real-icon'); - icon.setAttribute('label', 'Missing icon fallback'); - document.body.append(icon); - return { - fallback: icon.dataset.zenIcon, - hidden: icon.getAttribute('aria-hidden'), - label: icon.getAttribute('aria-label'), - role: icon.getAttribute('role'), - }; - }); - assert.deepEqual(iconFallback, { - fallback: 'alert', - hidden: null, - label: 'Missing icon fallback', - role: 'img', - }); - - await page.goto(`${baseUrl}/tokens`, { waitUntil: 'networkidle' }); - const colorTokenState = await page.evaluate(() => { - const root = document.documentElement; - const value = name => getComputedStyle(root) - .getPropertyValue(`--zen-color-${name}`) - .trim(); - const families = [ - 'neutral', - 'red', - 'orange', - 'amber', - 'green', - 'teal', - 'blue', - 'violet', - 'rose', - 'brown', - ]; - const steps = [ - '50', - '100', - '200', - '300', - '400', - '500', - '600', - '700', - '800', - '900', - '950', - ]; - const rampsComplete = families.every(family => - steps.every(step => value(`${family}-${step}`)) - ); - const data = Array.from( - { length: 10 }, - (_, index) => value(`data-${index + 1}`), - ); - const materials = [ - 'paper', - 'washi', - 'linen', - 'sumi', - 'brick', - 'persimmon', - 'ochre', - 'moss', - 'patina', - 'indigo', - 'plum', - 'clay', - 'wood', - ].map(value); - const lightMuted = value('surface-muted'); - root.dataset.zenTheme = 'dark'; - const darkMuted = value('surface-muted'); - const darkData = Array.from( - { length: 10 }, - (_, index) => value(`data-${index + 1}`), - ); - const themes = ['paper', 'ink', 'playful'].map(theme => { - root.dataset.zenTheme = theme; - const channels = value => value.match(/\d+(?:\.\d+)?/g) - .slice(0, 3) - .map(Number); - const luminance = value => { - const converted = channels(value).map(channel => { - const normalized = channel / 255; - return normalized <= 0.04045 - ? normalized / 12.92 - : ((normalized + 0.055) / 1.055) ** 2.4; - }); - return converted[0] * 0.2126 + - converted[1] * 0.7152 + - converted[2] * 0.0722; - }; - const contrasts = [ - 'info', - 'success', - 'warning', - 'danger', - ].map(tone => { - const probe = document.createElement('span'); - probe.style.background = `var(--zen-color-${tone})`; - probe.style.color = `var(--zen-color-on-${tone})`; - document.body.append(probe); - const probeStyle = getComputedStyle(probe); - const foreground = luminance(probeStyle.color); - const background = luminance(probeStyle.backgroundColor); - const contrast = (Math.max(foreground, background) + 0.05) / - (Math.min(foreground, background) + 0.05); - probe.remove(); - return contrast; - }); - return { - canvas: value('canvas'), - contrast: Math.min(...contrasts), - font: getComputedStyle(root) - .getPropertyValue('--zen-font-sans') - .trim(), - name: theme, - radius: getComputedStyle(root) - .getPropertyValue('--zen-radius-lg') - .trim(), - }; - }); - delete root.dataset.zenTheme; - return { - darkData, - darkMuted, - data, - lightMuted, - materials, - rampsComplete, - themes, - }; - }); - assert.equal(colorTokenState.rampsComplete, true); - assert.equal(new Set(colorTokenState.materials).size, 13); - assert.equal(new Set(colorTokenState.data).size, 10); - assert.equal(new Set(colorTokenState.darkData).size, 10); - assert.notDeepEqual(colorTokenState.data, colorTokenState.darkData); - assert.equal( - new Set(colorTokenState.themes.map(theme => theme.canvas)).size, - 3, - ); - assert.notEqual( - colorTokenState.themes.find(theme => theme.name === 'paper').radius, - colorTokenState.themes.find(theme => theme.name === 'playful').radius, - ); - assert.ok( - colorTokenState.themes.every(theme => theme.contrast >= 4.5), - JSON.stringify(colorTokenState.themes), - ); - assert.notEqual( - colorTokenState.lightMuted, - colorTokenState.darkMuted, - ); - assert.equal(await page.locator('.palette-grid > div').count(), 13); - assert.equal(await page.locator('.data-palette > span').count(), 10); - - await page.goto(`${baseUrl}/components/field`, { - waitUntil: 'networkidle', - }); - const fieldWiring = await page.locator('zen-field').first().evaluate(field => { - const label = field.querySelector('label'); - const input = field.querySelector('input'); - const help = field.querySelector('small'); - input.checkValidity(); - return { - describedBy: input.getAttribute('aria-describedby'), - helpId: help.id, - inputId: input.id, - invalid: field.hasAttribute('data-invalid'), - labelFor: label.htmlFor, - }; - }); - assert.ok(fieldWiring.inputId); - assert.equal(fieldWiring.labelFor, fieldWiring.inputId); - assert.equal(fieldWiring.describedBy, fieldWiring.helpId); - assert.equal(fieldWiring.invalid, true); - await page.locator('zen-field input').first().focus(); - assert.equal( - await page.locator('zen-field input').first().evaluate( - input => getComputedStyle(input).outlineStyle, - ), - 'none', - ); - const replacementField = await page.locator('zen-field').first().evaluate( - async field => { - const label = field.querySelector('label'); - const oldInput = field.querySelector('input'); - const blocker = document.createElement('div'); - blocker.id = 'zen-field-3'; - document.body.append(blocker); - const nextInput = document.createElement('input'); - nextInput.required = true; - oldInput.replaceWith(nextInput); - await new Promise(resolve => setTimeout(resolve)); - oldInput.dispatchEvent(new Event('invalid')); - const help = field.querySelector('small'); - nextInput.id = 'replacement-email'; - help.id = 'replacement-help'; - await new Promise(resolve => setTimeout(resolve)); - return { - describedBy: nextInput.getAttribute('aria-describedby'), - helpId: help.id, - labelFor: label.htmlFor, - nextId: nextInput.id, - blockerId: blocker.id, - }; - }, - ); - assert.ok(replacementField.nextId); - assert.notEqual(replacementField.nextId, replacementField.blockerId); - assert.equal(replacementField.labelFor, replacementField.nextId); - assert.equal(replacementField.describedBy, replacementField.helpId); - - await page.goto(`${baseUrl}/components/alert`, { - waitUntil: 'networkidle', - }); - await page.evaluate(() => { - window.__dismissed = 0; - document.addEventListener('zen-dismiss', () => { - window.__dismissed++; - }); - }); - const dismissible = page.locator('zen-alert[dismissible]'); - assert.equal(await dismissible.getAttribute('role'), 'alert'); - await dismissible.locator('[data-zen-dismiss]').click(); - assert.equal(await dismissible.count(), 0); - assert.equal(await page.evaluate(() => window.__dismissed), 1); - const authoredDismissState = await page.evaluate(async () => { - const alert = document.createElement('zen-alert'); - alert.setAttribute('dismissible', ''); - const message = document.createElement('p'); - message.textContent = 'Authored dismiss control'; - const dismiss = document.createElement('button'); - dismiss.dataset.zenDismiss = ''; - alert.append(message, dismiss); - document.body.append(alert); - await new Promise(resolve => setTimeout(resolve)); - let events = 0; - alert.addEventListener('zen-dismiss', () => events++); - alert.removeAttribute('dismissible'); - dismiss.click(); - return { - connected: alert.isConnected, - events, - }; - }); - assert.equal(authoredDismissState.connected, true); - assert.equal(authoredDismissState.events, 0); - - await page.goto(`${baseUrl}/components/accordion`, { - waitUntil: 'networkidle', - }); - const accordion = page.locator('zen-accordion').first(); - const disclosureIcon = accordion.locator( - 'summary > zen-icon[data-zen-disclosure-icon]', - ).nth(1); - const collapsedDisclosure = await disclosureIcon.evaluate(icon => { - const style = getComputedStyle(icon); - return { - duration: style.transitionDuration, - transform: style.transform, - }; - }); - assert.notEqual(collapsedDisclosure.duration, '0s'); - await accordion.locator('summary').nth(1).click(); - await page.waitForTimeout(250); - assert.notEqual( - await disclosureIcon.evaluate(icon => getComputedStyle(icon).transform), - collapsedDisclosure.transform, - ); - assert.equal( - await accordion.locator('details').first().getAttribute('open'), - null, - ); - assert.equal( - await accordion.locator('details').nth(1).getAttribute('open'), - '', - ); - await accordion.locator('summary').nth(1).press('ArrowUp'); - assert.equal( - await accordion.locator('summary').first().evaluate( - summary => document.activeElement === summary, - ), - true, - ); - - await page.goto(`${baseUrl}/components/aspect-ratio`, { - waitUntil: 'networkidle', - }); - const aspectColors = await page.locator('.demo-media').evaluate(media => { - const style = getComputedStyle(media); - const probe = document.createElement('span'); - probe.style.background = 'var(--zen-color-surface-muted)'; - document.body.append(probe); - const token = getComputedStyle(probe).backgroundColor; - probe.remove(); - return { - background: style.backgroundColor, - image: style.backgroundImage, - token, - }; - }); - assert.equal(aspectColors.image, 'none'); - assert.equal(aspectColors.background, aspectColors.token); - - await page.goto(`${baseUrl}/components/tabs`, { - waitUntil: 'networkidle', - }); - await page.evaluate(() => { - window.__tabValue = null; - document.querySelector('zen-tabs').addEventListener( - 'zen-change', - event => { - window.__tabValue = event.detail.value; - }, - ); - }); - await page.locator('zen-tabs [role="tab"]').nth(1).click(); - assert.equal( - await page.locator('zen-tabs [role="tab"]').nth(1) - .getAttribute('aria-selected'), - 'true', - ); - assert.equal( - await page.locator('zen-tabs [role="tabpanel"]').nth(1) - .getAttribute('hidden'), - null, - ); - assert.equal(await page.evaluate(() => window.__tabValue), 'history'); - - await page.goto(`${baseUrl}/components/dialog`, { - waitUntil: 'networkidle', - }); - await page.locator('zen-dialog [data-zen-trigger]').click(); - assert.equal(await page.locator('zen-dialog dialog').getAttribute('open'), ''); - await page.locator('zen-dialog [data-zen-close]').click(); - assert.equal(await page.locator('zen-dialog dialog').getAttribute('open'), null); - const dynamicOverlays = await page.evaluate(async () => { - const dialogHost = document.createElement('zen-dialog'); - const popoverHost = document.createElement('zen-popover'); - const tooltipHost = document.createElement('zen-tooltip'); - const menuHost = document.createElement('zen-dropdown-menu'); - document.body.append(dialogHost, popoverHost, tooltipHost, menuHost); - - const dialogTrigger = document.createElement('button'); - dialogTrigger.type = 'button'; - dialogTrigger.dataset.zenTrigger = ''; - const dialog = document.createElement('dialog'); - dialogHost.append(dialogTrigger, dialog); - - const popoverTrigger = document.createElement('button'); - popoverTrigger.type = 'button'; - popoverTrigger.dataset.zenTrigger = ''; - const popover = document.createElement('div'); - popover.dataset.zenContent = ''; - popoverHost.append(popoverTrigger, popover); - - const tooltipTrigger = document.createElement('button'); - tooltipTrigger.type = 'button'; - tooltipTrigger.dataset.zenTrigger = ''; - const tooltip = document.createElement('span'); - tooltip.dataset.zenContent = ''; - tooltipHost.append(tooltipTrigger, tooltip); - - const menuTrigger = document.createElement('button'); - menuTrigger.type = 'button'; - menuTrigger.dataset.zenTrigger = ''; - const menu = document.createElement('div'); - menu.setAttribute('role', 'menu'); - const item = document.createElement('button'); - item.type = 'button'; - item.setAttribute('role', 'menuitem'); - menu.append(item); - menuHost.append(menuTrigger, menu); - - await new Promise(resolve => setTimeout(resolve)); - dialogTrigger.click(); - const dialogOpen = dialog.open; - dialog.close(); - popoverTrigger.click(); - menuTrigger.click(); - tooltipTrigger.focus(); - await new Promise(resolve => setTimeout(resolve, 375)); - return { - dialogControlled: Boolean(dialogTrigger.getAttribute('aria-controls')), - dialogOpen, - menuOpen: !menu.hidden, - popoverExpanded: popoverTrigger.getAttribute('aria-expanded'), - tooltipOpen: !tooltip.hidden, - }; - }); - assert.deepEqual(dynamicOverlays, { - dialogControlled: true, - dialogOpen: true, - menuOpen: true, - popoverExpanded: 'true', - tooltipOpen: true, - }); - - await page.goto(`${baseUrl}/components/dropdown-menu`, { - waitUntil: 'networkidle', - }); - const dropdownTrigger = page.locator( - 'zen-dropdown-menu [data-zen-trigger]', - ); - await dropdownTrigger.click(); - assert.equal( - await page.locator('zen-dropdown-menu [role="menu"]') - .getAttribute('hidden'), - null, - ); - await page.keyboard.press('Escape'); - assert.equal( - await page.locator('zen-dropdown-menu [role="menu"]') - .getAttribute('hidden'), - '', - ); - assert.equal( - await dropdownTrigger.evaluate( - trigger => document.activeElement === trigger, - ), - true, - ); - await dropdownTrigger.click(); - await page.locator('zen-dropdown-menu [role="menuitem"]').first().click(); - assert.equal( - await dropdownTrigger.evaluate( - trigger => document.activeElement === trigger, - ), - true, - ); - - await page.goto(`${baseUrl}/components/combobox`, { - waitUntil: 'networkidle', - }); - const comboboxInput = page.locator('zen-combobox input'); - await comboboxInput.fill('seo'); - assert.equal( - await page.locator('zen-combobox [role="option"]:not([hidden])').count(), - 1, - ); - await comboboxInput.press('ArrowDown'); - await comboboxInput.press('Enter'); - assert.equal(await comboboxInput.inputValue(), 'seobeo'); - assert.equal( - await page.locator('zen-combobox [role="listbox"]') - .getAttribute('hidden'), - '', - ); - - await page.goto(`${baseUrl}/components/date-picker`, { - waitUntil: 'networkidle', - }); - const dateInput = page.locator('zen-date-picker input[type="date"]'); - assert.equal(await dateInput.getAttribute('aria-hidden'), 'true'); - assert.equal( - await dateInput.evaluate(input => getComputedStyle(input).position), - 'absolute', - ); - await page.evaluate(() => { - window.__dateValue = null; - document.querySelector('zen-date-picker').addEventListener( - 'zen-change', - event => { - window.__dateValue = event.detail.value; - }, - ); - }); - const dateTrigger = page.locator( - 'zen-date-picker [data-zen-date-trigger]', - ); - await dateTrigger.click(); - assert.equal( - await page.locator('zen-date-picker [data-zen-calendar-panel]') - .getAttribute('hidden'), - null, - ); - assert.equal( - await page.locator( - 'zen-date-picker [data-zen-calendar-day]', - ).count(), - 42, - ); - const chosenDate = await page.locator( - 'zen-date-picker [data-zen-calendar-day]:not([data-outside]):not(:disabled)', - ).nth(14).getAttribute('data-zen-calendar-day'); - await page.locator( - `zen-date-picker [data-zen-calendar-day="${chosenDate}"]`, - ).click(); - assert.equal(await dateInput.inputValue(), chosenDate); - assert.equal(await page.evaluate(() => window.__dateValue), chosenDate); - assert.equal(await dateTrigger.getAttribute('aria-expanded'), 'false'); - const triggerName = await dateTrigger.evaluate(trigger => - (trigger.getAttribute('aria-labelledby') || '') - .split(/\s+/) - .map(id => document.getElementById(id)?.textContent || '') - .join(' ') - ); - assert.match(triggerName, /Deploy on/); - assert.equal( - await page.locator('zen-date-picker > label').getAttribute('for'), - await dateTrigger.getAttribute('id'), - ); - const minimumDate = `${chosenDate.slice(0, 8)}10`; - await dateInput.evaluate((input, minimum) => { - input.min = minimum; - }, minimumDate); - await page.waitForTimeout(0); - await dateTrigger.click(); - const focusedBoundary = page.locator( - `zen-date-picker [data-zen-calendar-day="${chosenDate}"]`, - ); - await focusedBoundary.press('PageUp'); - assert.equal( - await page.evaluate(() => - document.activeElement?.dataset.zenCalendarDay - ), - minimumDate, - ); - assert.equal( - await page.locator( - 'zen-date-picker [data-zen-calendar-day][tabindex="0"]:not(:disabled)', - ).count(), - 1, - ); - await page.keyboard.press('Escape'); - await dateInput.evaluate(input => { - input.disabled = true; - }); - await page.waitForTimeout(0); - assert.equal(await dateTrigger.isDisabled(), true); - await dateInput.evaluate(input => { - input.disabled = false; - input.readOnly = true; - }); - await page.waitForTimeout(0); - assert.equal(await dateTrigger.isDisabled(), true); - await dateInput.evaluate(input => { - input.readOnly = false; - input.required = true; - input.value = ''; - input.dispatchEvent(new Event('input', { bubbles: true })); - input.reportValidity(); - }); - await page.waitForTimeout(0); - assert.equal(await dateTrigger.getAttribute('aria-invalid'), 'true'); - assert.equal( - await dateTrigger.evaluate( - trigger => document.activeElement === trigger, - ), - true, - ); - assert.equal( - await page.locator('zen-date-picker').getAttribute('data-invalid'), - '', - ); - await dateInput.evaluate((input, value) => { - input.value = value; - input.dispatchEvent(new Event('input', { bubbles: true })); - }, chosenDate); - assert.equal(await dateTrigger.getAttribute('aria-invalid'), 'false'); - const earlyYear = await page.evaluate(async () => { - const calendar = document.createElement('zen-calendar'); - const input = document.createElement('input'); - input.type = 'date'; - input.value = '0001-01-01'; - input.setAttribute('aria-label', 'Early date'); - calendar.append(input); - document.body.append(calendar); - await new Promise(resolve => setTimeout(resolve)); - return { - selected: calendar.querySelector( - '[data-zen-calendar-day][aria-selected="true"]', - )?.dataset.zenCalendarDay, - value: input.value, - }; - }); - assert.deepEqual(earlyYear, { - selected: '0001-01-01', - value: '0001-01-01', - }); - const resetDateState = await page.evaluate(async () => { - const label = document.createElement('span'); - label.id = 'reset-date-label'; - label.textContent = 'Reset date'; - const form = document.createElement('form'); - const picker = document.createElement('zen-date-picker'); - const input = document.createElement('input'); - input.type = 'date'; - input.defaultValue = '2026-08-04'; - input.setAttribute('aria-labelledby', label.id); - picker.append(input); - form.append(picker); - document.body.append(label, form); - await new Promise(resolve => setTimeout(resolve)); - input.value = '2026-08-12'; - input.dispatchEvent(new Event('input', { bubbles: true })); - form.reset(); - await new Promise(resolve => setTimeout(resolve)); - const trigger = picker.querySelector('[data-zen-date-trigger]'); - return { - labelledBy: trigger.getAttribute('aria-labelledby'), - selected: picker.querySelector( - '[data-zen-calendar-day][aria-selected="true"]', - )?.dataset.zenCalendarDay, - value: input.value, - }; - }); - assert.match(resetDateState.labelledBy, /^reset-date-label /); - assert.deepEqual( - { - selected: resetDateState.selected, - value: resetDateState.value, - }, - { - selected: '2026-08-04', - value: '2026-08-04', - }, - ); - - await page.goto(`${baseUrl}/components/input-otp`, { - waitUntil: 'networkidle', - }); - await page.evaluate(() => { - window.__otpValue = null; - document.querySelector('zen-input-otp').addEventListener( - 'zen-complete', - event => { - window.__otpValue = event.detail.value; - }, - ); - }); - await page.locator('zen-input-otp input').evaluate(input => { - input.value = '12a3456'; - input.dispatchEvent(new Event('input', { bubbles: true })); - }); - assert.equal(await page.locator('zen-input-otp input').inputValue(), '123456'); - assert.equal( - await page.locator('zen-input-otp [data-zen-otp-slots] > span').count(), - 6, - ); - assert.equal(await page.evaluate(() => window.__otpValue), '123456'); - - await page.goto(`${baseUrl}/components/data-table`, { - waitUntil: 'networkidle', - }); - await page.locator('zen-data-table [data-zen-sort="commits"]').click(); - assert.equal( - await page.locator('zen-data-table th').nth(1).getAttribute('aria-sort'), - 'ascending', - ); - assert.equal( - await page.locator('zen-data-table tbody tr').first().locator( - '[data-key="commits"]', - ).textContent(), - '12', - ); - - await page.goto(`${baseUrl}/components/carousel`, { - waitUntil: 'networkidle', - }); - await page.evaluate(() => { - window.__carouselIndex = null; - document.querySelector('zen-carousel').addEventListener( - 'zen-change', - event => { - window.__carouselIndex = event.detail.index; - }, - ); - }); - await page.locator('zen-carousel [data-zen-next]').click(); - assert.equal(await page.evaluate(() => window.__carouselIndex), 1); - assert.equal( - await page.locator('zen-carousel [data-zen-prev]').isDisabled(), - false, - ); - - await page.goto(`${baseUrl}/components/resizable`, { - waitUntil: 'networkidle', - }); - const resizeHandle = page.locator('zen-resizable [data-zen-handle]'); - const initialPercent = Number( - await resizeHandle.getAttribute('aria-valuenow'), - ); - await resizeHandle.focus(); - await resizeHandle.press('ArrowRight'); - assert.ok( - Number(await resizeHandle.getAttribute('aria-valuenow')) > initialPercent, - ); - - await page.goto(`${baseUrl}/components/switch`, { - waitUntil: 'networkidle', - }); - const switchInput = page.locator('zen-switch input'); - assert.equal(await switchInput.getAttribute('role'), 'switch'); - assert.equal(await switchInput.getAttribute('aria-checked'), 'true'); - await switchInput.uncheck(); - assert.equal(await switchInput.getAttribute('aria-checked'), 'false'); - - await page.goto(`${baseUrl}/components/checkbox`, { - waitUntil: 'networkidle', - }); - assert.equal( - await page.locator('zen-checkbox input').evaluate( - input => getComputedStyle(input).appearance, - ), - 'none', - ); - await page.goto(`${baseUrl}/components/native-select`, { - waitUntil: 'networkidle', - }); - assert.equal( - await page.locator('zen-native-select select').evaluate( - select => getComputedStyle(select).appearance, - ), - 'none', - ); - assert.equal( - await page.locator( - 'zen-native-select > zen-icon[data-zen-select-icon]', - ).count(), - 1, - ); - await page.goto(`${baseUrl}/components/slider`, { - waitUntil: 'networkidle', - }); - assert.equal( - await page.locator('zen-slider input').evaluate( - input => getComputedStyle(input).appearance, - ), - 'none', - ); - - await page.goto(`${baseUrl}/components/toggle-group`, { - waitUntil: 'networkidle', - }); - await page.locator('zen-toggle-group button[value="right"]').click(); - assert.equal( - await page.locator('zen-toggle-group button[value="right"]') - .getAttribute('aria-pressed'), - 'true', - ); - assert.equal( - await page.locator('zen-toggle-group').getAttribute('value'), - 'right', - ); - - await page.goto(`${baseUrl}/components/sidebar`, { - waitUntil: 'networkidle', - }); - const sidebarTrigger = page.locator( - 'zen-sidebar [data-zen-sidebar-trigger]', - ); - const sidebarPanel = page.locator( - 'zen-sidebar [data-zen-sidebar-panel]', - ); - assert.equal(await sidebarTrigger.getAttribute('aria-expanded'), 'true'); - assert.equal(await sidebarPanel.getAttribute('hidden'), null); - await sidebarTrigger.click(); - assert.equal(await sidebarTrigger.getAttribute('aria-expanded'), 'false'); - assert.equal(await sidebarPanel.getAttribute('hidden'), ''); - - await page.goto(`${baseUrl}/components/notifications`, { - waitUntil: 'networkidle', - }); - const notificationScope = page.locator('zen-notifications').first(); - const notificationPosition = await notificationScope.locator( - '[data-zen-notification-stack]', - ).evaluate(stack => { - const style = getComputedStyle(stack); - return { - bottom: style.bottom, - position: style.position, - right: style.right, - }; - }); - assert.equal(notificationPosition.position, 'fixed'); - assert.notEqual(notificationPosition.bottom, 'auto'); - assert.notEqual(notificationPosition.right, 'auto'); - - const dedupe = await notificationScope.evaluate(scope => { - const source = scope.querySelector('[data-notification-demo]'); - const send = detail => source.dispatchEvent( - new CustomEvent('zen-notify', { - bubbles: true, - composed: false, - detail, - }), - ); - send({ - version: 1, - id: 'dedupe', - tone: 'info', - message: 'First announcement', - announcement: 'polite', - persistent: true, - }); - send({ - version: 1, - id: 'dedupe', - tone: 'success', - message: 'Updated without duplication', - announcement: 'polite', - persistent: true, - }); - send({ version: 99, id: 'invalid' }); - return { - size: scope.size, - visible: scope.visibleCount, - }; - }); - assert.deepEqual(dedupe, { size: 1, visible: 1 }); - assert.equal( - await notificationScope.locator('article').count(), - 1, - ); - assert.match( - await notificationScope.locator('article').textContent(), - /Updated without duplication/, - ); - await page.waitForFunction(() => - document.querySelector( - 'zen-notifications [data-zen-live="polite"]', - )?.textContent === 'First announcement' - ); - - const bounded = await notificationScope.evaluate(scope => { - let accepted = 0; - for (let index = 0; index < 25; index++) { - if (scope.notify({ - version: 1, - id: `bounded-${index}`, - tone: 'info', - message: `Bounded ${index}`, - announcement: 'none', - durationMs: 120000, - })) accepted++; - } - return { - accepted, - size: scope.size, - visible: scope.visibleCount, - }; - }); - assert.deepEqual(bounded, { accepted: 25, size: 23, visible: 3 }); - assert.equal( - await notificationScope.locator('article').count(), - 3, - ); - await page.waitForFunction(() => - [...document.querySelectorAll( - 'zen-notifications article', - )].some(article => article.dataset.depth === '2') - ); - const collapsedStack = await notificationScope.locator( - '[data-zen-notification-stack]', - ).evaluate(stack => { - const articles = [...stack.querySelectorAll('article')]; - return { - depths: articles.map(article => article.dataset.depth), - height: stack.getBoundingClientRect().height, - positions: articles.map(article => - getComputedStyle(article).position - ), - transforms: articles.map(article => - getComputedStyle(article).transform - ), - }; - }); - assert.deepEqual(collapsedStack.depths, ['2', '1', '0']); - assert.deepEqual( - collapsedStack.positions, - ['absolute', 'absolute', 'absolute'], - ); - await notificationScope.locator('article').last().hover(); - await page.waitForTimeout(300); - const expandedStack = await notificationScope.locator( - '[data-zen-notification-stack]', - ).evaluate(stack => ({ - height: stack.getBoundingClientRect().height, - rects: [...stack.querySelectorAll('article')].map(article => { - const bounds = article.getBoundingClientRect(); - return { - bottom: bounds.bottom, - left: bounds.left, - right: bounds.right, - top: bounds.top, - }; - }), - tops: [...stack.querySelectorAll('article')].map( - article => Math.round(article.getBoundingClientRect().top), - ), - transforms: [...stack.querySelectorAll('article')].map( - article => getComputedStyle(article).transform, - ), - })); - assert.ok(expandedStack.height > collapsedStack.height); - assert.equal(new Set(expandedStack.tops).size, 3); - const expandedGaps = [ - expandedStack.rects[1].top - expandedStack.rects[0].bottom, - expandedStack.rects[2].top - expandedStack.rects[1].bottom, - ]; - for (const gap of expandedGaps) { - assert.ok(gap >= 10 && gap <= 14, JSON.stringify(expandedGaps)); - } - assert.notDeepEqual( - expandedStack.transforms, - collapsedStack.transforms, - ); - const gapX = - (expandedStack.rects[0].left + expandedStack.rects[0].right) / 2; - const gapY = - (expandedStack.rects[0].bottom + expandedStack.rects[1].top) / 2; - await page.mouse.move(gapX, gapY); - await page.waitForTimeout(100); - const gapState = await notificationScope.locator( - '[data-zen-notification-stack]', - ).evaluate(stack => ({ - height: stack.getBoundingClientRect().height, - hovered: stack.matches(':hover'), - })); - assert.equal(gapState.hovered, true); - assert.ok(gapState.height >= expandedStack.height - 1); - await page.mouse.move(0, 0); - - const actionScope = await page.evaluate(() => { - const scope = document.createElement('zen-notifications'); - scope.id = 'action-scope'; - const source = document.createElement('button'); - scope.append(source); - document.body.append(scope); - window.__notificationAction = null; - scope.addEventListener('zen-notification-action', event => { - window.__notificationAction = event.detail; - }); - source.dispatchEvent(new CustomEvent('zen-notify', { - bubbles: true, - composed: false, - detail: { - version: 1, - id: 'action', - tone: 'error', - message: 'Action required', - announcement: 'assertive', - persistent: true, - action: { - token: 'opaque-secret-token', - label: 'Retry', - }, - }, - })); - return scope.id; - }); - assert.equal( - await page.locator(`#${actionScope}`).evaluate( - scope => scope.records, - ), - undefined, - ); - const actionArticle = page.locator( - `#${actionScope} [data-zen-notification-id="action"]`, - ); - assert.doesNotMatch( - await actionArticle.evaluate(article => article.outerHTML), - /opaque-secret-token/, - ); - await actionArticle.locator('.zen-notification-action').focus(); - await page.locator(`#${actionScope}`).evaluate(scope => { - const source = scope.querySelector('button'); - source.dispatchEvent(new CustomEvent('zen-notify', { - bubbles: true, - composed: false, - detail: { - version: 1, - id: 'action-neighbor', - tone: 'info', - message: 'Neighbor', - announcement: 'none', - persistent: true, - }, - })); - }); - assert.equal( - await actionArticle.locator('.zen-notification-action').evaluate( - action => document.activeElement === action, - ), - true, - ); - await actionArticle.locator('.zen-notification-action').click(); - assert.deepEqual( - await page.evaluate(() => window.__notificationAction), - { - version: 1, - id: 'action', - token: 'opaque-secret-token', - }, - ); - await page.locator(`#${actionScope}`).evaluate(scope => { - const source = scope.querySelector('button'); - source.dispatchEvent(new CustomEvent( - 'zen-dismiss-notification', - { - bubbles: true, - composed: false, - detail: { version: 1, id: 'action' }, - }, - )); - }); - await actionArticle.waitFor({ state: 'detached' }); - - await page.evaluate(() => { - const scope = document.createElement('zen-notifications'); - scope.id = 'announcement-scope'; - const source = document.createElement('button'); - scope.append(source); - document.body.append(scope); - window.__announcements = []; - const region = scope.querySelector('[data-zen-live="polite"]'); - new MutationObserver(() => { - if (region.textContent) { - window.__announcements.push(region.textContent); - } - }).observe(region, { childList: true }); - for (let index = 0; index < 3; index++) { - source.dispatchEvent(new CustomEvent('zen-notify', { - bubbles: true, - composed: false, - detail: { - version: 1, - id: `announcement-${index}`, - tone: 'info', - message: `Announcement ${index}`, - announcement: 'polite', - persistent: true, - }, - })); - } - }); - await page.waitForFunction(() => - window.__announcements?.length === 3 - ); - assert.deepEqual( - await page.evaluate(() => window.__announcements), - ['Announcement 0', 'Announcement 1', 'Announcement 2'], - ); - - const composedRejected = await page.evaluate(() => { - const scope = document.createElement('zen-notifications'); - const source = document.createElement('button'); - scope.append(source); - document.body.append(scope); - source.dispatchEvent(new CustomEvent('zen-notify', { - bubbles: true, - composed: true, - detail: { - version: 1, - id: 'composed', - tone: 'info', - message: 'Must be rejected', - announcement: 'none', - persistent: true, - }, - })); - return scope.size; - }); - assert.equal(composedRejected, 0); - - await page.evaluate(() => { - const scope = document.createElement('zen-notifications'); - scope.id = 'focus-timer-scope'; - const source = document.createElement('button'); - scope.append(source); - document.body.append(scope); - source.dispatchEvent(new CustomEvent('zen-notify', { - bubbles: true, - composed: false, - detail: { - version: 1, - id: 'focus-timer', - tone: 'warning', - message: 'Focus timer', - announcement: 'none', - durationMs: 300, - action: { token: 'focus-token', label: 'Keep focused' }, - }, - })); - }); - const focusTimer = page.locator( - '#focus-timer-scope [data-zen-notification-id="focus-timer"]', - ); - await focusTimer.locator('.zen-notification-action').focus(); - await page.locator('#focus-timer-scope').evaluate(scope => { - const source = scope.querySelector(':scope > button'); - source.dispatchEvent(new CustomEvent('zen-notify', { - bubbles: true, - composed: false, - detail: { - version: 1, - id: 'focus-neighbor', - tone: 'info', - message: 'Focus neighbor', - announcement: 'none', - persistent: true, - }, - })); - }); - await page.waitForTimeout(500); - assert.equal(await focusTimer.count(), 1); - assert.equal( - await focusTimer.locator('.zen-notification-action').evaluate( - action => document.activeElement === action, - ), - true, - ); - await page.mouse.move(0, 0); - await page.locator('#catalogMain').focus(); - await page.waitForFunction(() => - !document.querySelector( - '#focus-timer-scope [data-zen-notification-id="focus-timer"]', - ) - ); - - await page.evaluate(() => { - const scope = document.createElement('zen-notifications'); - scope.id = 'timer-scope'; - const source = document.createElement('button'); - scope.append(source); - document.body.append(scope); - source.dispatchEvent(new CustomEvent('zen-notify', { - bubbles: true, - composed: false, - detail: { - version: 1, - id: 'timer', - tone: 'info', - message: 'Paused timer', - announcement: 'none', - durationMs: 300, - }, - })); - }); - const timerArticle = page.locator('#timer-scope article'); - await timerArticle.hover(); - await page.waitForTimeout(500); - assert.equal(await timerArticle.count(), 1); - await page.mouse.move(0, 0); - await page.waitForFunction(() => - !document.querySelector('#timer-scope article') - ); - - await page.evaluate(() => { - const scope = document.createElement('zen-notifications'); - scope.id = 'hidden-timer-scope'; - const source = document.createElement('button'); - scope.append(source); - document.body.append(scope); - source.dispatchEvent(new CustomEvent('zen-notify', { - bubbles: true, - composed: false, - detail: { - version: 1, - id: 'hidden-timer', - tone: 'info', - message: 'Hidden timer', - announcement: 'none', - durationMs: 300, - }, - })); - Object.defineProperty(document, 'hidden', { - configurable: true, - value: true, - }); - document.dispatchEvent(new Event('visibilitychange')); - }); - await page.waitForTimeout(500); - assert.equal( - await page.locator('#hidden-timer-scope article').count(), - 1, - ); - await page.evaluate(() => { - Object.defineProperty(document, 'hidden', { - configurable: true, - value: false, - }); - document.dispatchEvent(new Event('visibilitychange')); - }); - await page.waitForFunction(() => - !document.querySelector('#hidden-timer-scope article') - ); - - const isolated = await page.evaluate(() => { - const makeScope = id => { - const scope = document.createElement('zen-notifications'); - scope.id = id; - const source = document.createElement('button'); - scope.append(source); - document.body.append(scope); - return { scope, source }; - }; - const first = makeScope('scope-one'); - const second = makeScope('scope-two'); - first.source.dispatchEvent(new CustomEvent('zen-notify', { - bubbles: true, - composed: false, - detail: { - version: 1, - id: 'isolated', - tone: 'success', - message: 'Only first scope', - announcement: 'none', - persistent: true, - }, - })); - return [first.scope.size, second.scope.size]; - }); - assert.deepEqual(isolated, [1, 0]); - - const lightCanvas = await page.evaluate(() => - getComputedStyle(document.documentElement) - .getPropertyValue('--zen-color-canvas') - .trim() - ); - await page.locator('#themeToggle').click(); - const darkCanvas = await page.evaluate(() => - getComputedStyle(document.documentElement) - .getPropertyValue('--zen-color-canvas') - .trim() - ); - assert.notEqual(lightCanvas, darkCanvas); - assert.equal( - await page.evaluate(() => localStorage.getItem('zen-theme')), - 'dark', - ); - - assert.deepEqual(errors, []); - await context.close(); - - const darkContext = await browser.newContext({ colorScheme: 'dark' }); - const darkPage = await darkContext.newPage(); - await darkPage.goto(baseUrl, { waitUntil: 'networkidle' }); - assert.equal( - await darkPage.locator('#themeToggle').getAttribute('aria-pressed'), - 'true', - ); - const systemDarkCanvas = await darkPage.evaluate(() => - getComputedStyle(document.documentElement) - .getPropertyValue('--zen-color-canvas') - .trim() - ); - await darkPage.locator('#themeToggle').click(); - const explicitLightCanvas = await darkPage.evaluate(() => - getComputedStyle(document.documentElement) - .getPropertyValue('--zen-color-canvas') - .trim() - ); - assert.notEqual(systemDarkCanvas, explicitLightCanvas); - await darkContext.close(); - - const mobileContext = await browser.newContext({ - colorScheme: 'light', - viewport: { width: 390, height: 844 }, - }); - const mobilePage = await mobileContext.newPage(); - const mobileErrors = []; - mobilePage.on('pageerror', error => { - mobileErrors.push(error.stack || error.message); - }); - mobilePage.on('console', message => { - if (message.type() === 'error') mobileErrors.push(message.text()); - }); - await mobilePage.goto(baseUrl, { waitUntil: 'networkidle' }); - await mobilePage.waitForFunction(() => - customElements.get('zen-date-picker') && - document.querySelectorAll( - '.catalog-nav[aria-label="Components"] a', - ).length === 66 - ); - const mobileFailures = await mobilePage.evaluate(() => { - const routes = [ - '/', - '/tokens', - '/icons', - ...[...document.querySelectorAll( - '.catalog-nav[aria-label="Components"] a', - )].map(link => new URL(link.href).pathname), - ]; - const failures = []; - for (const route of routes) { - history.replaceState({}, '', route); - window.dispatchEvent(new PopStateEvent('popstate')); - const page = document.querySelector( - '[data-catalog-page]:not([hidden])', - ); - if (!page || - document.documentElement.scrollWidth > innerWidth + 1 || - page.getBoundingClientRect().right > innerWidth + 1) { - failures.push(route); - } - } - return failures; - }); - assert.deepEqual(mobileFailures, []); - await mobilePage.goto(`${baseUrl}/components/date-picker`, { - waitUntil: 'networkidle', - }); - await mobilePage.locator( - 'zen-date-picker [data-zen-date-trigger]', - ).click(); - const mobileCalendarBounds = await mobilePage.locator( - 'zen-date-picker [data-zen-calendar-panel]', - ).evaluate(panel => { - const bounds = panel.getBoundingClientRect(); - return { - bottom: bounds.bottom, - left: bounds.left, - right: bounds.right, - top: bounds.top, - }; - }); - assert.ok(mobileCalendarBounds.left >= 0); - assert.ok(mobileCalendarBounds.right <= 390); - assert.ok(mobileCalendarBounds.top >= 0); - assert.ok(mobileCalendarBounds.bottom <= 844); - assert.deepEqual(mobileErrors, []); - await mobileContext.close(); - } finally { - if (browser) await browser.close(); - await stopProcess(server); - } -})().catch(error => { - console.error(error.stack || error); - process.exitCode = 1; -});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/wiki/HTML_AUTHORING.md Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,95 @@ +# HTML authoring + +## Native underneath + +Seeing a native `button`, `input`, `textarea`, `select`, or `a` inside a Zenbu +element is intentional: + +```html +<zen-button size="md"> + <button type="submit">Save</button> +</zen-button> +``` + +The native element retains form submission, keyboard activation, focus, +validation, autofill, link navigation, and browser APIs. The Zenbu host owns +shared behavior, sizing, tokens, and optional presentation. + +Do not replace the inner button with text-only custom-element content. + +## One owner per control + +An authored control must have one nearest owner: + +```html +<zen-field appearance="plain" size="lg"> + <label for="token">Access token</label> + <input id="token" required> +</zen-field> +``` + +Do not nest a control inside unrelated visual primitives merely to increase +component usage. A toggle button is already owned by `zen-toggle`. Overlay and +layout components only coordinate behavior, so their authored actions still +use the control primitive: + +```html +<zen-drawer> + <zen-button size="md"> + <button type="button" data-zen-trigger>Open drawer</button> + </zen-button> + <dialog> + <zen-button size="md"> + <button type="button" data-zen-close>Close</button> + </zen-button> + </dialog> +</zen-drawer> +``` + +## Plain integration + +Use plain appearance to preserve an application's visual language: + +```html +<zen-button appearance="plain" size="sm"> + <button class="existing-toolbar-button">Close</button> +</zen-button> +``` + +Plain mode: + +- retains component behavior and ARIA wiring; +- exposes inherited `--zenbu-control-*` aliases; +- does not add borders, shadows, colors, padding, or hover effects; +- leaves application selectors on the native element effective. + +## Structural HTML + +Keep semantic structure native unless a primitive adds a concrete contract: + +```html +<main> + <section aria-labelledby="activity-heading"> + <zen-heading size="xl"> + <h2 id="activity-heading">Activity</h2> + </zen-heading> + <zen-text size="md"><p>Latest repository events.</p></zen-text> + </section> +</main> +``` + +`main`, `section`, `nav`, lists, tables, paragraphs, and heading levels still +carry document meaning. Zenbu must not obscure that meaning. + +## Links + +Use `zen-link` when a reusable effect is requested: + +```html +<zen-link effect="paw"> + <a href="/blog">Blog</a> +</zen-link> +``` + +Use a native link without a Zenbu wrapper when no link behavior or effect is +being added. Button-like links belong in `zen-button`.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/wiki/PRIMITIVE_MAP.md Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,43 @@ +# Primitive map + +| Native authored element | Owning Zenbu primitive | +| --- | --- | +| `button`, button-like `a` | `zen-button` | +| text/email/file/etc. `input` | `zen-input` or `zen-field` | +| `textarea` | `zen-textarea` or `zen-field` | +| native `select` | `zen-native-select`, `zen-select`, or `zen-field` | +| checkbox input | `zen-checkbox` | +| radio inputs/fieldset | `zen-radio-group` | +| switch checkbox | `zen-switch` | +| range input | `zen-slider` | +| date value | `zen-calendar` or `zen-date-picker` | +| one-time-code input | `zen-input-otp` | +| filter input and options | `zen-combobox` | +| command input and items | `zen-command` | +| anchor with reusable effect | `zen-link` | +| heading with canonical type role | `zen-heading` | +| text with canonical type role | `zen-text` | +| padded container | `zen-box` | + +## Control-owning composites + +These behavior primitives already own their native buttons. Do not add a second +`zen-button`: + +- menus, context menus, menubars, and navigation menus; +- tabs, toggles, toggle groups, and sidebars; +- calendars, carousels, commands, and data tables; +- notifications and dismissible alerts. + +## Coordinating containers + +Dialogs, alert dialogs, sheets, drawers, popovers, tooltips, forms, empty +states, and items coordinate or arrange content. They do not own application +actions. Wrap authored action buttons in `zen-button`, then put the native +`button` inside it. + +## Generated controls + +Component implementation code may create native buttons for accessibility. +Generated controls are owned by that component. Application-authored controls +are checked separately.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/wiki/README.md Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,23 @@ +# Zenbu UI wiki + +This wiki is the source of truth for future HTML and component work. + +## Pages + +- [HTML authoring](HTML_AUTHORING.md): native-underneath markup and ownership. +- [Primitive map](PRIMITIVE_MAP.md): which primitive owns each native control. +- [Tokens and sizing](TOKENS_AND_SIZING.md): reference, semantic, local, theme, + density, and xs-xl contracts. +- [Review checklist](REVIEW_CHECKLIST.md): required searches, tests, and + accessibility checks. + +## Core rules + +1. Keep browser semantics. A Zenbu control contains the appropriate native + element rather than replacing it. +2. Give every authored interactive control one owning primitive. +3. Use `appearance="plain"` when only behavior and scale are shared. +4. Consume `--zenbu-sys-*` intent in application/component CSS. +5. Use reference values only while defining semantic mappings. +6. Keep business state and workflows outside the design system. +7. Add catalog and browser evidence for every public primitive.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/wiki/REVIEW_CHECKLIST.md Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,40 @@ +# Review checklist + +## Discovery + +- Read relevant Bazel `BUILD` dependencies before broad searches. +- Identify shared assets and downstream consumers. +- Search for existing primitives and semantic tokens before adding new ones. + +## HTML + +- Every authored action button belongs to `zen-button`. +- A behavior primitive owns a raw native button only when that button is the + primitive's semantic control, such as a tab, toggle, menu item, or carousel + control. +- Every authored form control belongs to a form-control primitive. +- Native semantics remain visible in source. +- There is only one owning primitive per control. +- Plain appearance preserves application CSS. + +## CSS + +- New variables use the `--zenbu-*` namespace. +- Components and applications consume `--zenbu-sys-*` roles. +- Reference colors are not used directly in component/application CSS. +- Repeated dimensions use the xs-xl or semantic spacing scale. +- Focus, disabled, hover, pressed, invalid, and selected states are covered. +- Reduced motion and forced colors remain usable. + +## Verification + +```bash +bazel test //design_system/test:design_system_policy_test +bazel test //design_system/test:catalog_test +bazel test //mrjunejune/test:theme_and_webp_test +bazel build //design_system:design_system_server_bundle +bazel build //mrjunejune:mrjunejune_server_bundle +``` + +Do not mark an integration complete when controls are merely present. Exercise +native forms, keyboard behavior, sizing, theme, density, and mobile layout.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/design_system/wiki/TOKENS_AND_SIZING.md Wed Aug 05 05:25:40 2026 -0700 @@ -0,0 +1,53 @@ +# Tokens and sizing + +## Three layers + +```text +--zenbu-ref-* available context-free values +--zenbu-sys-* theme/density-aware semantic intent +--zenbu-<component>-* local component aliases +``` + +Application CSS consumes semantic roles. Component CSS may define local aliases +that fall back to semantic roles. Reference colors are restricted to token and +theme definitions. + +## Five-step scale + +Controls, text, icons, gaps, and padding use: + +```text +xs sm md lg xl +``` + +Every sized control host exposes: + +```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. Plain components expose them to +application CSS. + +## Density + +Set density once at the application root: + +```html +<html data-zen-density="compact"> +``` + +Density remaps semantic control sizes and padding. It does not require +component-specific selectors. + +## Typography roles + +Prefer semantic type roles for page CSS and use `zen-text` / `zen-heading` +where a reusable type contract is useful. Font sizes remain in `rem`; line +heights remain unitless.
--- a/mrjunejune/src/base.css Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/base.css Wed Aug 05 05:25:40 2026 -0700 @@ -99,36 +99,37 @@ } :root { - --zen-font-editorial: "More Thin", "Roboto", sans-serif; - --zen-font-playful: "More", "More Thin", sans-serif; - --zen-font-sans: var(--zen-font-editorial); + --zenbu-font-editorial: "More Thin", "Roboto", sans-serif; + --zenbu-font-playful: "More", "More Thin", sans-serif; + --zenbu-sys-font-family-ui: var(--zenbu-font-editorial); + --zenbu-sys-font-family-content: var(--zenbu-font-editorial); } :root:not([data-zen-theme]), :root[data-zen-theme] { - --white: var(--zen-color-surface); - --black: var(--zen-color-text); - --darkgray: var(--zen-color-border-strong); - --lightgray: var(--zen-color-border-muted); - --gray-light: var(--zen-color-border-muted); - --green: var(--zen-color-success); - --orange: var(--zen-color-warning); - --purple: var(--zen-color-violet-400); - --red: var(--zen-color-danger); - --blue: var(--zen-color-info); - --darktext: var(--zen-color-text-muted); - --text: var(--zen-color-text); - --graytext: var(--zen-color-text-muted); - --lighttext: var(--zen-color-text-subtle); - --awesome: var(--zen-color-persimmon); - --accent: var(--zen-color-info); - --accent-dark: var(--zen-color-indigo); - --gray-dark: var(--zen-color-text); + --white: var(--zenbu-sys-color-surface-default); + --black: var(--zenbu-sys-color-text-primary); + --darkgray: var(--zenbu-sys-color-border-strong); + --lightgray: var(--zenbu-sys-color-border-subtle); + --gray-light: var(--zenbu-sys-color-border-subtle); + --green: var(--zenbu-sys-color-success-foreground); + --orange: var(--zenbu-sys-color-action-primary-background-hover); + --purple: var(--zenbu-sys-color-action-primary-background); + --red: var(--zenbu-sys-color-danger-foreground); + --blue: var(--zenbu-sys-color-info-foreground); + --darktext: var(--zenbu-sys-color-text-secondary); + --text: var(--zenbu-sys-color-text-primary); + --graytext: var(--zenbu-sys-color-text-secondary); + --lighttext: var(--zenbu-sys-color-text-tertiary); + --awesome: var(--zenbu-sys-color-action-danger-background); + --accent: var(--zenbu-sys-color-text-link); + --accent-dark: var(--zenbu-sys-color-action-primary-background-pressed); + --gray-dark: var(--zenbu-sys-color-text-primary); --gray-gradient: linear-gradient( - var(--zen-color-canvas), - var(--zen-color-surface) + var(--zenbu-sys-color-surface-page), + var(--zenbu-sys-color-surface-default) ); - --box-shadow: var(--zen-shadow-md); + --box-shadow: var(--zenbu-sys-shadow-raised); } /* --- fonts --- */ @@ -165,6 +166,7 @@ /* -- HTML Tags only --*/ html { + min-height: 100%; background: var(--white); } @@ -174,23 +176,26 @@ } body { - font-family: var(--zen-font-sans, "More Thin", sans-serif); + min-height: 100vh; + font-family: var(--zenbu-sys-font-family-content); margin: 0; padding: 0; text-align: left; background: var(--gray-gradient); - background-size: 100% 600px; + background-attachment: fixed; + background-repeat: no-repeat; + background-size: cover; word-wrap: break-word; overflow-wrap: break-word; color: var(--gray-dark); - font-size: 20px; - line-height: 1.7; + font-size: var(--zenbu-sys-font-size-xl); + line-height: var(--zenbu-sys-line-height-lg); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } main { - background: var(--white); + background: transparent; width: var(--main-width); max-width: calc(100% - 2em); margin: auto; @@ -208,23 +213,23 @@ } h1 { - font-size: 2.5em; + font-size: var(--zenbu-sys-font-size-3xl); } h2 { - font-size: 2em; + font-size: var(--zenbu-sys-font-size-2xl); } h3 { - font-size: 1.75em; + font-size: var(--zenbu-sys-font-size-xl); } h4 { - font-size: 1.5em; + font-size: var(--zenbu-sys-font-size-lg); } h5 { - font-size: 1.25em; + font-size: var(--zenbu-sys-font-size-md); } strong, @@ -245,6 +250,19 @@ font-size: 16px; } +zen-button[appearance="plain"][size] > :where(button, a) { + display: inline-flex; + min-height: var(--zenbu-control-height); + align-items: center; + justify-content: center; + gap: var(--zenbu-control-gap); + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); + font-size: var(--zenbu-control-font-size); + line-height: var(--zenbu-control-line-height); +} + input { font-size: 16px; } @@ -274,7 +292,8 @@ border-left: 4px solid var(--accent); padding: 0 0 0 20px; margin-bottom: 1.25em; - font-size: 1.333em; + font-size: var(--zenbu-sys-font-size-lg); + line-height: var(--zenbu-sys-line-height-lg); } ul { @@ -295,63 +314,35 @@ text-decoration: underline; } -a::before { - content: ""; - display: inline-block; - width: 1em; - height: 1em; - margin-right: 8px; - vertical-align: middle; - background-color: #FF69B4; - -webkit-mask: url('/public/paw.svg') no-repeat center; - mask: url('/public/paw.svg') no-repeat center; - animation: pawStep 0.6s ease-out forwards; -} - -@keyframes pawStep { - 0% { transform: scale(0); opacity: 0; } - 100% { transform: scale(1); opacity: 1; } -} - -a:hover::before { - animation: wiggle 0.4s ease-in-out infinite; -} - -@keyframes wiggle { - 0%, 100% { transform: rotate(-15deg); } - 50% { transform: rotate(15deg); } -} - - /* -- mobile -- */ @media (max-width: 720px) { body { - font-size: 18px; - line-height: 1.8; + font-size: var(--zenbu-sys-font-size-lg); + line-height: var(--zenbu-sys-line-height-md); } main { - padding: 1.5em 1em; + padding: var(--zenbu-sys-padding-lg) var(--zenbu-sys-padding-md); } h1 { - font-size: 2em; + font-size: var(--zenbu-sys-font-size-2xl); } h2 { - font-size: 1.75em; + font-size: var(--zenbu-sys-font-size-xl); } h3 { - font-size: 1.5em; + font-size: var(--zenbu-sys-font-size-lg); } h4 { - font-size: 1.25em; + font-size: var(--zenbu-sys-font-size-md); } h5 { - font-size: 1.1em; + font-size: var(--zenbu-sys-font-size-sm); } p { @@ -364,25 +355,19 @@ display: inline-block; } - button, input[type="submit"], input[type="button"] { - min-height: 44px; - padding: 0.75em 1.5em; - font-size: 1em; - } - input, textarea, select { font-size: 16px; /* Prevents zoom on iOS */ } /* Improve blockquote readability */ blockquote { - font-size: 1.1em; + font-size: var(--zenbu-sys-font-size-md); padding: 0.5em 0 0.5em 1em; } /* Better code block sizing */ code { - font-size: 0.9em; + font-size: var(--zenbu-sys-font-size-sm); } pre { @@ -397,34 +382,24 @@ position: fixed; bottom: 20px; right: 20px; - padding: 12px 24px 12px 36px; + min-height: var(--zenbu-control-height); + gap: var(--zenbu-control-gap); + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); background: #000000; color: #eeffee; border: none; border-radius: 8px; cursor: pointer; - font-size: 14px; + font-size: var(--zenbu-control-font-size); font-weight: bold; box-shadow: 0 4px 12px rgba(0,0,0,0.2); z-index: 1000; font-family: inherit; } -#pwa-install-btn.with-icon::before { - content: ""; - position: absolute; - left: 14px; - top: 50%; - transform: translateY(-50%); - width: 18px; - height: 18px; - - /* Masked icon */ - -webkit-mask: url('/public/paw.svg') no-repeat center; - mask: url('/public/paw.svg') no-repeat center; - mask-size: contain; - mask-repeat: no-repeat; - - /* Icon color = button text color */ - background-color: currentColor; +#pwa-install-btn zen-icon { + width: var(--zenbu-control-icon-size); + height: var(--zenbu-control-icon-size); }
--- a/mrjunejune/src/index.html Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/index.html Wed Aug 05 05:25:40 2026 -0700 @@ -65,27 +65,6 @@ cursor: pointer; } - .site-link-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); - gap: var(--zen-space-4); - } - - .site-link-grid > a { - color: inherit; - text-decoration: none; - } - - .site-link-grid zen-card, - .site-link-grid article { - height: 100%; - } - - .site-link-grid p { - margin: 0; - color: var(--zen-color-text-muted); - font-size: 0.9rem; - } </style> </head> <body> @@ -102,28 +81,12 @@ </div> <h2>Links</h2> - <div class="site-link-grid"> - <a href="https://zenbu.babocoder.com/file/tip"> - <zen-card interactive> - <article><h3>Repository</h3><p>Browse the code behind these experiments.</p></article> - </zen-card> - </a> - <a href="/blog"> - <zen-card interactive> - <article><h3>Blogs</h3><p>Thoughts, experiments, and technical notes.</p></article> - </zen-card> - </a> - <a href="/resume"> - <zen-card interactive> - <article><h3>Resume</h3><p>Professional experience and selected work.</p></article> - </zen-card> - </a> - <a href="/tools"> - <zen-card interactive> - <article><h3>Tools</h3><p>Small things I built for myself.</p></article> - </zen-card> - </a> - </div> + <ul> + <li><a href="https://zenbu.babocoder.com/file/tip">Repository</a> - Check out my code</li> + <li><a href="/blog">Blogs</a> - My thoughts / Experiments </li> + <li><a href="/resume">Resume</a> - My professional experiences </li> + <li><a href="/tools">Tools</a> - Things I made for myself </li> + </ul> <canvas id="background"></canvas> <canvas id="game"></canvas> <canvas id="gameUI"></canvas>
--- a/mrjunejune/src/index.js Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/index.js Wed Aug 05 05:25:40 2026 -0700 @@ -19,7 +19,7 @@ function updateThemeColor() { const probe = document.createElement('span'); - probe.style.background = 'var(--zen-color-canvas)'; + probe.style.background = 'var(--zenbu-sys-color-surface-page)'; document.body.append(probe); const color = getComputedStyle(probe).backgroundColor; probe.remove(); @@ -57,6 +57,34 @@ localStorage.setItem(THEME_STORAGE_KEY, currentPreference); } +function enhanceLinks(root = document) { + const links = root instanceof HTMLAnchorElement + ? [root] + : [...root.querySelectorAll?.('a') || []]; + for (const link of links) { + if (link.closest('zen-link, zen-button') || + link.dataset.zenLinkEffect === 'none') { + continue; + } + const wrapper = document.createElement('zen-link'); + wrapper.setAttribute('effect', 'paw'); + link.before(wrapper); + wrapper.append(link); + } +} + +window.addEventListener('DOMContentLoaded', () => { + enhanceLinks(); + const observer = new MutationObserver(records => { + for (const record of records) { + for (const node of record.addedNodes) { + if (node instanceof Element) enhanceLinks(node); + } + } + }); + observer.observe(document.body, { childList: true, subtree: true }); +}); + document.querySelector('#themeToggle')?.addEventListener('click', () => { const current = getThemePreference(); const next = THEMES[(THEMES.indexOf(current) + 1) % THEMES.length];
--- a/mrjunejune/src/notes/index.html Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/notes/index.html Wed Aug 05 05:25:40 2026 -0700 @@ -7,7 +7,7 @@ .notes-page { min-width: 70%; margin: 0 auto; - padding: 20px; + padding: var(--zenbu-sys-padding-lg); } .notes-header { display: flex; @@ -15,36 +15,36 @@ align-items: center; margin-bottom: 20px; flex-wrap: wrap; - gap: 12px; + gap: var(--zenbu-sys-space-control); } .notes-header h1 { margin: 0; display: flex; align-items: center; - gap: 12px; + gap: var(--zenbu-sys-space-control); } .note-id-display { - font-size: 14px; + font-size: var(--zenbu-sys-font-size-sm); color: #666; background: #f0f0f0; - padding: 4px 12px; - border-radius: 4px; + padding: + var(--zenbu-sys-control-padding-block-xs) + var(--zenbu-sys-control-padding-inline-sm); + border-radius: var(--zenbu-sys-radius-control-small); font-weight: normal; } .notes-actions { display: flex; - gap: 8px; + gap: var(--zenbu-sys-space-related); align-items: center; } .notes-actions button, .notes-actions a { - padding: 8px 16px; border: 1px solid #ccc; - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); background: #fff; cursor: pointer; text-decoration: none; color: #333; - font-size: 14px; } .notes-actions button:hover, .notes-actions a:hover { background: #f5f5f5; @@ -78,8 +78,8 @@ } .new-note-form { background: white; - padding: 24px; - border-radius: 8px; + padding: var(--zenbu-sys-padding-lg); + border-radius: var(--zenbu-sys-radius-control); width: 400px; max-width: 90%; } @@ -88,23 +88,21 @@ } .new-note-form input { width: 100%; - padding: 10px; + padding: var(--zenbu-sys-padding-sm); border: 1px solid #ccc; - border-radius: 4px; - font-size: 16px; + border-radius: var(--zenbu-sys-radius-control-small); + font-size: var(--zenbu-sys-font-size-md); box-sizing: border-box; margin-bottom: 16px; } .new-note-form .form-actions { display: flex; - gap: 8px; + gap: var(--zenbu-sys-space-related); justify-content: flex-end; } .new-note-form button { - padding: 8px 16px; - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); cursor: pointer; - font-size: 14px; } .new-note-form .btn-cancel { background: #f5f5f5; @@ -116,7 +114,7 @@ border: none; } .note-hint { - font-size: 12px; + font-size: var(--zenbu-sys-font-size-xs); color: #666; margin-top: -12px; margin-bottom: 16px; @@ -133,8 +131,12 @@ <span class="note-id-display" id="note-id-display">index</span> </h1> <div class="notes-actions"> - <button onclick="goHome()"> Home </button> - <button onclick="logout()" class="btn-danger">Logout</button> + <zen-button appearance="plain" size="sm"> + <button onclick="goHome()"> Home </button> + </zen-button> + <zen-button appearance="plain" size="sm"> + <button onclick="logout()" class="btn-danger">Logout</button> + </zen-button> </div> </div> @@ -145,11 +147,17 @@ <div class="new-note-dialog" id="new-note-dialog"> <div class="new-note-form"> <h2>Create New Note</h2> - <input type="text" id="new-note-id" placeholder="note-id (e.g., my-ideas)"> + <zen-input appearance="plain" size="md"> + <input type="text" id="new-note-id" placeholder="note-id (e.g., my-ideas)"> + </zen-input> <p class="note-hint">Use lowercase letters, numbers, and hyphens only</p> <div class="form-actions"> - <button class="btn-cancel" onclick="hideNewNoteDialog()">Cancel</button> - <button class="btn-create" onclick="createNewNote()">Create & Open</button> + <zen-button appearance="plain" size="sm"> + <button class="btn-cancel" onclick="hideNewNoteDialog()">Cancel</button> + </zen-button> + <zen-button appearance="plain" size="sm"> + <button class="btn-create" onclick="createNewNote()">Create & Open</button> + </zen-button> </div> </div> </div>
--- a/mrjunejune/src/notes/login.html Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/notes/login.html Wed Aug 05 05:25:40 2026 -0700 @@ -7,18 +7,18 @@ .login-page { max-width: 400px; margin: 100px auto; - padding: 20px; + padding: var(--zenbu-sys-padding-lg); } .login-box { background: #fff; border: 1px solid #ddd; - border-radius: 8px; - padding: 32px; + border-radius: var(--zenbu-sys-radius-control); + padding: var(--zenbu-sys-padding-xl); box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .login-box h1 { margin: 0 0 24px 0; - font-size: 24px; + font-size: var(--zenbu-sys-font-size-2xl); text-align: center; } .login-box label { @@ -28,10 +28,14 @@ } .login-box input { width: 100%; - padding: 12px; + min-height: var(--zenbu-control-height); + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); border: 1px solid #ccc; - border-radius: 4px; - font-size: 16px; + border-radius: var(--zenbu-sys-radius-control-small); + font-size: var(--zenbu-control-font-size); + line-height: var(--zenbu-control-line-height); box-sizing: border-box; } .login-box input:focus { @@ -40,15 +44,16 @@ } .login-box button { width: 100%; - padding: 12px; margin-top: 16px; background: #0078ff; color: white; border: none; - border-radius: 4px; - font-size: 16px; + border-radius: var(--zenbu-sys-radius-control-small); cursor: pointer; } + .login-box zen-button { + width: 100%; + } .login-box button:hover { background: #0066dd; } @@ -67,9 +72,13 @@ <div class="login-box"> <h1>Notes Login</h1> <form id="login-form"> - <label for="token">Access Token</label> - <input type="password" id="token" placeholder="Enter your access token" required> - <button type="submit">Login</button> + <zen-field appearance="plain" size="lg"> + <label for="token">Access Token</label> + <input type="password" id="token" placeholder="Enter your access token" required> + </zen-field> + <zen-button appearance="plain" size="lg"> + <button type="submit">Login</button> + </zen-button> <p class="error-msg" id="error-msg">Invalid token</p> </form> </div>
--- a/mrjunejune/src/offline.html Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/offline.html Wed Aug 05 05:25:40 2026 -0700 @@ -3,6 +3,13 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" href="/public/design-system/styles/tokens.css"> + <link rel="stylesheet" href="/public/design-system/styles/themes.css"> + <link rel="stylesheet" href="/public/design-system/styles/components.css"> + <script + type="module" + src="/public/design-system/components/index.js" + ></script> <title>Offline - MrJuneJune</title> <style> body { @@ -13,30 +20,34 @@ justify-content: center; min-height: 100vh; margin: 0; - padding: 20px; + padding: var(--zenbu-sys-padding-lg); background: linear-gradient(135deg, #544e43 0%, #3a3630 100%); color: white; text-align: center; } h1 { - font-size: 3em; + font-size: var(--zenbu-sys-font-size-3xl); margin: 0 0 0.5em 0; } p { - font-size: 1.2em; + font-size: var(--zenbu-sys-font-size-lg); + line-height: var(--zenbu-sys-line-height-lg); margin: 0 0 2em 0; opacity: 0.9; } button { - padding: 12px 32px; - font-size: 1em; + min-height: var(--zenbu-control-height); + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); + font-size: var(--zenbu-control-font-size); background: white; color: #544e43; border: none; - border-radius: 8px; + border-radius: var(--zenbu-sys-radius-control); cursor: pointer; font-weight: bold; transition: transform 0.2s; @@ -53,9 +64,13 @@ </style> </head> <body> - <div class="icon">📡</div> + <div class="icon"> + <zen-icon name="offline" label="Offline"></zen-icon> + </div> <h1>You're Offline</h1> <p>It looks like you've lost your internet connection.<br>Don't worry, some cached pages might still work!</p> - <button onclick="window.location.reload()">Try Again</button> + <zen-button appearance="plain" size="lg"> + <button onclick="window.location.reload()">Try Again</button> + </zen-button> </body> </html>
--- a/mrjunejune/src/parts/header.html Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/parts/header.html Wed Aug 05 05:25:40 2026 -0700 @@ -18,16 +18,11 @@ } #themeToggle { + display: flex; flex-direction: column; - min-width: 4.5rem; - padding: 0.35rem; - border-radius: 1.5rem; - } - - #themeName { - font-family: "More", sans-serif; - font-size: 0.65rem; - line-height: 1; + padding: 0; + border: 0; + background: transparent; } /* Professional header */ @@ -35,7 +30,6 @@ margin: auto; padding: 1.5em 1em; font-family: "More", sans-serif; - box-shadow: var(--zen-shadow-sm); width: 720px; max-width: calc(100% - 2em); text-align: center; @@ -137,7 +131,7 @@ } </style> -<zen-button id="themeControl" variant="quiet"> +<zen-button id="themeControl" appearance="plain" size="xl"> <button id="themeToggle" type="button" @@ -151,11 +145,11 @@ height="50" width="50" > - <span id="themeName" aria-live="polite">Auto</span> + <span id="themeName" class="zen-visually-hidden" aria-live="polite">Auto</span> </button> </zen-button> <header> - <h1><a href="/">MrJuneJune</a></h1> + <h1><a href="/" data-zen-link-effect="none">MrJuneJune</a></h1> </header> <script src="/index.js"></script>
--- a/mrjunejune/src/public/pwa-register.js Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/public/pwa-register.js Wed Aug 05 05:25:40 2026 -0700 @@ -57,7 +57,11 @@ const installBtn = document.createElement('button'); installBtn.classList.add('with-icon'); installBtn.id = 'pwa-install-btn'; - installBtn.textContent = 'Install as App'; + const installIcon = document.createElement('zen-icon'); + installIcon.setAttribute('name', 'download'); + const installLabel = document.createElement('span'); + installLabel.textContent = 'Install as App'; + installBtn.append(installIcon, installLabel); installBtn.addEventListener('click', async () => { if (!deferredPrompt) return; @@ -70,12 +74,16 @@ installBtn.remove(); }); - document.body.appendChild(installBtn); + const wrapper = document.createElement('zen-button'); + wrapper.setAttribute('appearance', 'plain'); + wrapper.setAttribute('size', 'md'); + wrapper.append(installBtn); + document.body.appendChild(wrapper); setTimeout(() => { installBtn.style.opacity = '0'; installBtn.style.transition = 'opacity 0.3s'; - setTimeout(() => installBtn.remove(), 300); + setTimeout(() => wrapper.remove(), 300); }, 5000); } @@ -84,5 +92,5 @@ deferredPrompt = null; const installBtn = document.getElementById('pwa-install-btn'); - if (installBtn) installBtn.remove(); + if (installBtn) installBtn.closest('zen-button')?.remove(); });
--- a/mrjunejune/src/resume/index.html Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/resume/index.html Wed Aug 05 05:25:40 2026 -0700 @@ -10,7 +10,7 @@ {{/parts/header.html}} <main> <div class="info"> - <a href="/public/resume.pdf">Download PDF</a> + <a href="/public/resume.pdf" data-zen-link-effect="none">Download PDF</a> <hr> <p><span class="header-firstname-style"> JUNTAE </span><span class="header-lastname-style">PARK</span><p> <p class="header-position-style"> MEMBER OF TECHNICAL STAFF · SOFTWARE ENGINEER </p>
--- a/mrjunejune/src/sw.js Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/sw.js Wed Aug 05 05:25:40 2026 -0700 @@ -1,16 +1,20 @@ // Root-scoped Service Worker for MrJuneJune PWA -const CACHE_VERSION = 'v5-zenbu-themes'; +const CACHE_VERSION = 'v6-zenbu-headless'; const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`; // Files to cache immediately on install const STATIC_CACHE = [ '/', + '/offline.html', '/index.js', '/base.css', '/public/design-system/styles/tokens.css', + '/public/design-system/styles/reference.css', + '/public/design-system/styles/semantic.css', + '/public/design-system/styles/density.css', '/public/design-system/styles/themes.css', '/public/design-system/styles/components.css', - '/public/design-system/styles/shadcn.css', + '/public/design-system/styles/elements.css', '/public/design-system/components/index.js', '/public/design-system/components/alert.js', '/public/design-system/components/button.js', @@ -20,6 +24,7 @@ '/public/design-system/components/field.js', '/public/design-system/components/forms.js', '/public/design-system/components/icon.js', + '/public/design-system/components/link.js', '/public/design-system/components/notifications.js', '/public/design-system/components/overlays.js', '/public/design-system/components/primitives.js',
--- a/mrjunejune/src/talk/index.html Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/talk/index.html Wed Aug 05 05:25:40 2026 -0700 @@ -5,8 +5,8 @@ <title>Talk!</title> {{/parts/base_head.html}} <style> - body { font-family: sans-serif; padding: 20px; } - #messages { height: 200px; border: 1px solid #ccc; overflow-y: scroll; margin-bottom: 10px; padding: 10px; } + body { font-family: sans-serif; padding: var(--zenbu-sys-padding-lg); } + #messages { height: 200px; border: 1px solid #ccc; overflow-y: scroll; margin-bottom: var(--zenbu-sys-space-control); padding: var(--zenbu-sys-padding-md); } #chat { display: flex; gap: 10px; } input { flex-grow: 1; } </style> @@ -19,8 +19,12 @@ <div id="messages"></div> <div id="chat"> - <input type="text" id="messageInput" placeholder="Type a message..."> - <button id="sendBtn">Send</button> + <zen-input appearance="plain" size="md"> + <input type="text" id="messageInput" placeholder="Type a message..."> + </zen-input> + <zen-button appearance="plain" size="md"> + <button id="sendBtn">Send</button> + </zen-button> </div> </main> {{/parts/footer.html}}
--- a/mrjunejune/src/tools/file_converter/index.html Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/tools/file_converter/index.html Wed Aug 05 05:25:40 2026 -0700 @@ -9,13 +9,13 @@ .container { max-width: 800px; margin: 2rem auto; - padding: 2rem; + padding: var(--zenbu-sys-padding-xl); } .converter-section { - border-radius: 8px; + border-radius: var(--zenbu-sys-radius-control); border: 2px solid var(--lightgray); - padding: 2rem; + padding: var(--zenbu-sys-padding-xl); margin-bottom: 2rem; } @@ -35,11 +35,11 @@ } input[type="file"] { - padding: 0.75rem; + padding: var(--zenbu-sys-padding-sm); border: 2px dashed #ccc; - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); background: white; - font-size: 16px; + font-size: var(--zenbu-sys-font-size-md); } button { @@ -47,12 +47,9 @@ font-family: "More Thin", sans-serif; color: var(--gray-gradient); border: none; - padding: 0.75rem 1.5rem; - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); cursor: pointer; - font-size: 1rem; margin-top: 1rem; - min-height: 44px; } button:hover { @@ -66,9 +63,9 @@ .result { margin-top: 1rem; - padding: 1rem; + padding: var(--zenbu-sys-padding-md); background: white; - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); display: none; } @@ -107,30 +104,28 @@ /* Mobile responsive */ @media (max-width: 720px) { .container { - padding: 1rem; + padding: var(--zenbu-sys-padding-md); margin: 1rem auto; } .converter-section { - padding: 1.5rem 1rem; + padding: + var(--zenbu-sys-padding-lg) + var(--zenbu-sys-padding-md); } .converter-section h2 { - font-size: 1.5rem; + font-size: var(--zenbu-sys-font-size-2xl); } .file-input-wrapper label { - font-size: 1rem; + font-size: var(--zenbu-sys-font-size-md); } - button { - font-size: 1.1rem; - padding: 1rem 1.5rem; - } .result { - padding: 1rem; - font-size: 1rem; + padding: var(--zenbu-sys-padding-md); + font-size: var(--zenbu-sys-font-size-md); } } </style> @@ -148,10 +143,14 @@ <div class="file-input-wrapper"> <label for="imageInput">Choose an image file:</label> - <input type="file" id="imageInput" accept="image/*"> + <zen-input appearance="plain" size="md"> + <input type="file" id="imageInput" accept="image/*"> + </zen-input> </div> - <button id="convertImageBtn" onclick="convertImageToWebP()">Convert to WebP</button> + <zen-button appearance="plain" size="md"> + <button id="convertImageBtn" onclick="convertImageToWebP()">Convert to WebP</button> + </zen-button> <div class="loading" id="imageLoading">Converting... Please wait.</div> @@ -167,10 +166,14 @@ <div class="file-input-wrapper"> <label for="videoInput">Choose a video file:</label> - <input type="file" id="videoInput" accept="video/*"> + <zen-input appearance="plain" size="md"> + <input type="file" id="videoInput" accept="video/*"> + </zen-input> </div> - <button id="convertVideoBtn" onclick="convertVideoToMP4()">Convert to MP4</button> + <zen-button appearance="plain" size="md"> + <button id="convertVideoBtn" onclick="convertVideoToMP4()">Convert to MP4</button> + </zen-button> <div class="loading" id="videoLoading">Converting... Please wait.</div>
--- a/mrjunejune/src/tools/hls_player/index.css Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/tools/hls_player/index.css Wed Aug 05 05:25:40 2026 -0700 @@ -14,27 +14,28 @@ gap: 0.6rem; } -.hls-url-row input, -.hls-url-row button { - min-height: 44px; - font: inherit; +.hls-url-row input { + min-height: var(--zenbu-sys-control-height-md); + font-size: var(--zenbu-sys-font-size-md); + line-height: var(--zenbu-sys-line-height-md); } .hls-url-row input { width: 100%; - padding: 0.65rem 0.8rem; + padding: + var(--zenbu-sys-control-padding-block-md) + var(--zenbu-sys-control-padding-inline-md); color: var(--gray-dark); background: var(--white); border: 1px solid var(--darkgray); - border-radius: 6px; + border-radius: var(--zenbu-sys-radius-control-small); } .hls-url-row button { - padding: 0.65rem 1rem; color: var(--white); background: var(--darkgray); border: 1px solid var(--darkgray); - border-radius: 6px; + border-radius: var(--zenbu-sys-radius-control-small); cursor: pointer; } @@ -48,7 +49,7 @@ .hls-player-shell { overflow: hidden; border: 1px solid var(--darkgray); - border-radius: 8px; + border-radius: var(--zenbu-sys-radius-control); background: #10131a; } @@ -56,7 +57,7 @@ margin: 1rem 0 1.5rem; padding: 0.9rem; border: 1px solid var(--gray-light); - border-radius: 8px; + border-radius: var(--zenbu-sys-radius-control); } .hls-local-picker legend { @@ -77,11 +78,15 @@ .hls-file-button { display: inline-flex; align-items: center; - min-height: 44px; - padding: 0.65rem 1rem; + min-height: var(--zenbu-sys-control-height-md); + padding: + var(--zenbu-sys-control-padding-block-md) + var(--zenbu-sys-control-padding-inline-md); + font-size: var(--zenbu-sys-font-size-md); + line-height: var(--zenbu-sys-line-height-md); color: var(--white); background: var(--darkgray); - border-radius: 6px; + border-radius: var(--zenbu-sys-radius-control-small); cursor: pointer; } @@ -107,9 +112,9 @@ } .hls-status { - min-height: 2.75rem; + min-height: var(--zenbu-sys-control-height-md); margin: 0; - padding: 0.65rem 0.9rem; + padding: var(--zenbu-sys-padding-sm); color: #f3f5f8; background: #171b24; } @@ -138,7 +143,7 @@ .hls-details div { padding: 0.7rem; border: 1px solid var(--gray-light); - border-radius: 6px; + border-radius: var(--zenbu-sys-radius-control-small); } .hls-details dt {
--- a/mrjunejune/src/tools/hls_player/index.html Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/tools/hls_player/index.html Wed Aug 05 05:25:40 2026 -0700 @@ -16,17 +16,23 @@ <form id="hlsForm" class="hls-form"> <label for="hlsUrl">Playlist URL</label> <div class="hls-url-row"> - <input - id="hlsUrl" - name="url" - type="text" - inputmode="url" - value="/public/hls-sample/h264-ts-stream.m3u8" - placeholder="https://example.com/stream.m3u8" - required - /> - <button type="submit">Load</button> - <button id="sampleButton" type="button">Sample</button> + <zen-input appearance="plain" size="md"> + <input + id="hlsUrl" + name="url" + type="text" + inputmode="url" + value="/public/hls-sample/h264-ts-stream.m3u8" + placeholder="https://example.com/stream.m3u8" + required + /> + </zen-input> + <zen-button appearance="plain" size="md"> + <button type="submit">Load</button> + </zen-button> + <zen-button appearance="plain" size="md"> + <button id="sampleButton" type="button">Sample</button> + </zen-button> </div> </form> @@ -36,21 +42,25 @@ <div class="hls-local-actions"> <label class="hls-file-button"> Choose HLS files - <input - id="hlsFiles" - type="file" - accept=".m3u8,.m4s,.mp4,.ts,.aac,.vtt,.key,application/vnd.apple.mpegurl" - multiple - /> + <zen-input appearance="plain" size="md"> + <input + id="hlsFiles" + type="file" + accept=".m3u8,.m4s,.mp4,.ts,.aac,.vtt,.key,application/vnd.apple.mpegurl" + multiple + /> + </zen-input> </label> <label class="hls-file-button"> Choose HLS folder - <input - id="hlsFolder" - type="file" - webkitdirectory - multiple - /> + <zen-input appearance="plain" size="md"> + <input + id="hlsFolder" + type="file" + webkitdirectory + multiple + /> + </zen-input> </label> </div> </fieldset>
--- a/mrjunejune/src/tools/latex_editor/index.css Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/tools/latex_editor/index.css Wed Aug 05 05:25:40 2026 -0700 @@ -25,12 +25,14 @@ gap: 0.65rem; } +.latex-actions zen-button { + display: inline-flex; +} + .latex-actions button, .button-link { - min-height: 42px; - padding: 0.65rem 1rem; border: 2px solid var(--black); - border-radius: 8px; + border-radius: var(--zenbu-sys-radius-control); background: var(--awesome); color: var(--black); font: inherit; @@ -86,7 +88,7 @@ min-width: 0; overflow: hidden; border: 2px solid var(--black); - border-radius: 12px; + border-radius: var(--zenbu-sys-radius-panel); background: var(--white); box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.12); } @@ -100,9 +102,11 @@ display: flex; align-items: center; justify-content: space-between; - gap: 1rem; - min-height: 48px; - padding: 0.55rem 0.8rem; + gap: var(--zenbu-sys-control-gap-md); + min-height: var(--zenbu-sys-control-height-lg); + padding: + var(--zenbu-sys-control-padding-block-sm) + var(--zenbu-sys-control-padding-inline-md); border-bottom: 2px solid var(--black); background: var(--gray-gradient); } @@ -110,7 +114,7 @@ .panel-heading h2, .panel-heading span { margin: 0; - font-size: 0.95rem; + font-size: var(--zenbu-sys-font-size-sm); } #latexSource { @@ -120,13 +124,15 @@ min-height: 540px; resize: vertical; box-sizing: border-box; - padding: 1rem; + padding: var(--zenbu-sys-padding-md); border: 0; outline: 0; background: #111318; color: #f4f4f5; caret-color: #f4f4f5; - font: 15px/1.55 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: var(--zenbu-sys-font-size-sm); + line-height: var(--zenbu-sys-line-height-md); tab-size: 2; } @@ -198,4 +204,12 @@ flex: 1 1 45%; text-align: center; } + + .latex-actions zen-button { + flex: 1 1 45%; + } + + .latex-actions zen-button > :where(button, a) { + width: 100%; + } }
--- a/mrjunejune/src/tools/latex_editor/index.html Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/tools/latex_editor/index.html Wed Aug 05 05:25:40 2026 -0700 @@ -15,13 +15,21 @@ <p>Write LaTeX here. My server compiles it inside a locked-down sandbox and sends the PDF back to this preview.</p> </div> <div class="latex-actions"> - <label class="auto-compile"> - <input id="autoCompile" type="checkbox" checked /> - Auto compile - </label> - <button id="resetButton" type="button" class="secondary">Reset</button> - <button id="compileButton" type="button">Compile PDF</button> - <a id="downloadButton" class="button-link disabled" aria-disabled="true">Download</a> + <zen-checkbox appearance="plain" size="sm"> + <label class="auto-compile"> + <input id="autoCompile" type="checkbox" checked /> + Auto compile + </label> + </zen-checkbox> + <zen-button appearance="plain" size="md"> + <button id="resetButton" type="button" class="secondary">Reset</button> + </zen-button> + <zen-button appearance="plain" size="md"> + <button id="compileButton" type="button">Compile PDF</button> + </zen-button> + <zen-button appearance="plain" size="md"> + <a id="downloadButton" class="button-link disabled" aria-disabled="true">Download</a> + </zen-button> </div> </div> @@ -35,7 +43,8 @@ <h2 id="sourceHeading">document.tex</h2> <span id="sourceSize">0 / 65,536 bytes</span> </div> - <textarea id="latexSource" spellcheck="false" autocomplete="off" aria-label="LaTeX source" placeholder="Type your LaTeX document here...">\documentclass[11pt]{article} + <zen-textarea appearance="plain" size="md"> + <textarea id="latexSource" spellcheck="false" autocomplete="off" aria-label="LaTeX source" placeholder="Type your LaTeX document here...">\documentclass[11pt]{article} \usepackage[margin=1in]{geometry} \usepackage{amsmath} \usepackage{xcolor} @@ -57,6 +66,7 @@ \end{document} </textarea> + </zen-textarea> </section> <section class="latex-panel preview-panel" aria-labelledby="previewHeading">
--- a/mrjunejune/src/tools/markdown_to_html/index.css Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/tools/markdown_to_html/index.css Wed Aug 05 05:25:40 2026 -0700 @@ -6,7 +6,7 @@ body { line-height: 1.6; - padding: 20px; + padding: var(--zenbu-sys-padding-lg); max-width: 1400px; margin: 0 auto; } @@ -24,7 +24,7 @@ .container { display: grid; grid-template-columns: 1fr 1fr; - gap: 20px; + gap: var(--zenbu-sys-space-container); height: calc(100vh - 200px); min-height: 500px; } @@ -33,9 +33,9 @@ display: flex; flex-direction: column; background: var(--white); - border-radius: 8px; + border-radius: var(--zenbu-sys-radius-control); border: 1px solid rgba(var(--gray), 0.3); - padding: 15px; + padding: var(--zenbu-sys-padding-md); overflow: hidden; } @@ -50,7 +50,7 @@ .label { font-weight: 600; color: var(--gray-dark); - font-size: 14px; + font-size: var(--zenbu-sys-font-size-sm); text-transform: uppercase; letter-spacing: 0.5px; } @@ -59,10 +59,8 @@ background: var(--accent); color: var(--white); border: none; - padding: 8px 16px; - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); cursor: pointer; - font-size: 14px; transition: background 0.2s; } @@ -74,13 +72,14 @@ textarea { flex: 1; width: 100%; - padding: 15px; + padding: var(--zenbu-sys-padding-md); background: var(--gray-light); color: var(--gray-dark); border: 1px solid rgba(var(--gray), 0.3); - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; - font-size: 14px; + font-size: var(--zenbu-sys-font-size-sm); + line-height: var(--zenbu-sys-line-height-sm); line-height: 1.5; resize: none; } @@ -93,13 +92,14 @@ /* Output panel */ #output { flex: 1; - padding: 15px; + padding: var(--zenbu-sys-padding-md); border: 1px solid rgba(var(--gray), 0.3); - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); background: var(--white); color: var(--gray-dark); overflow-y: auto; - font-size: 15px; + font-size: var(--zenbu-sys-font-size-sm); + line-height: var(--zenbu-sys-line-height-sm); } /* Markdown output styles */ @@ -134,7 +134,7 @@ #output code { background: var(--gray-light); padding: 2px 6px; - border-radius: 3px; + border-radius: var(--zenbu-sys-radius-control-small); font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; font-size: 0.9em; color: var(--accent); @@ -144,7 +144,7 @@ background: var(--black); color: var(--white); padding: 15px; - border-radius: 6px; + border-radius: var(--zenbu-sys-radius-control-small); overflow-x: auto; margin: 1em 0; line-height: 1.4; @@ -183,7 +183,7 @@ #output img { max-width: 100%; height: auto; - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); } #output strong {
--- a/mrjunejune/src/tools/markdown_to_html/index.html Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/src/tools/markdown_to_html/index.html Wed Aug 05 05:25:40 2026 -0700 @@ -15,7 +15,8 @@ <div class="container"> <div class="panel"> - <div class="label">Markdown Input</div> + <zen-field appearance="plain" size="md"> + <label class="label" for="input">Markdown Input</label> <textarea id="input" placeholder="Type your markdown here..."># Welcome to Markdown Converter ## Features @@ -57,12 +58,15 @@  **Try editing this text!**</textarea> + </zen-field> </div> <div class="panel"> <div class="title"> <div class="label">HTML Output</div> - <button id="copy"> Copy </button> + <zen-button appearance="plain" size="sm"> + <button id="copy"> Copy </button> + </zen-button> </div> <div id="output"></div> </div>
--- a/mrjunejune/test/snapshots/blog.snapshot Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/test/snapshots/blog.snapshot Wed Aug 05 05:25:40 2026 -0700 @@ -53,16 +53,11 @@ } #themeToggle { + display: flex; flex-direction: column; - min-width: 4.5rem; - padding: 0.35rem; - border-radius: 1.5rem; - } - - #themeName { - font-family: "More", sans-serif; - font-size: 0.65rem; - line-height: 1; + padding: 0; + border: 0; + background: transparent; } /* Professional header */ @@ -70,7 +65,6 @@ margin: auto; padding: 1.5em 1em; font-family: "More", sans-serif; - box-shadow: var(--zen-shadow-sm); width: 720px; max-width: calc(100% - 2em); text-align: center; @@ -172,7 +166,7 @@ } </style> -<zen-button id="themeControl" variant="quiet"> +<zen-button id="themeControl" appearance="plain" size="xl"> <button id="themeToggle" type="button" @@ -186,12 +180,12 @@ height="50" width="50" > - <span id="themeName" aria-live="polite">Auto</span> + <span id="themeName" class="zen-visually-hidden" aria-live="polite">Auto</span> </button> </zen-button> <header> - <h1><a href="/">MrJuneJune</a></h1> + <h1><a href="/" data-zen-link-effect="none">MrJuneJune</a></h1> </header> <script src="/index.js"></script>
--- a/mrjunejune/test/snapshots/resume.snapshot Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/test/snapshots/resume.snapshot Wed Aug 05 05:25:40 2026 -0700 @@ -56,16 +56,11 @@ } #themeToggle { + display: flex; flex-direction: column; - min-width: 4.5rem; - padding: 0.35rem; - border-radius: 1.5rem; - } - - #themeName { - font-family: "More", sans-serif; - font-size: 0.65rem; - line-height: 1; + padding: 0; + border: 0; + background: transparent; } /* Professional header */ @@ -73,7 +68,6 @@ margin: auto; padding: 1.5em 1em; font-family: "More", sans-serif; - box-shadow: var(--zen-shadow-sm); width: 720px; max-width: calc(100% - 2em); text-align: center; @@ -175,7 +169,7 @@ } </style> -<zen-button id="themeControl" variant="quiet"> +<zen-button id="themeControl" appearance="plain" size="xl"> <button id="themeToggle" type="button" @@ -189,18 +183,18 @@ height="50" width="50" > - <span id="themeName" aria-live="polite">Auto</span> + <span id="themeName" class="zen-visually-hidden" aria-live="polite">Auto</span> </button> </zen-button> <header> - <h1><a href="/">MrJuneJune</a></h1> + <h1><a href="/" data-zen-link-effect="none">MrJuneJune</a></h1> </header> <script src="/index.js"></script> <main> <div class="info"> - <a href="/public/resume.pdf">Download PDF</a> + <a href="/public/resume.pdf" data-zen-link-effect="none">Download PDF</a> <hr> <p><span class="header-firstname-style"> JUNTAE </span><span class="header-lastname-style">PARK</span><p> <p class="header-position-style"> MEMBER OF TECHNICAL STAFF · SOFTWARE ENGINEER </p>
--- a/mrjunejune/test/snapshots/root.snapshot Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/test/snapshots/root.snapshot Wed Aug 05 05:25:40 2026 -0700 @@ -94,27 +94,6 @@ cursor: pointer; } - .site-link-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); - gap: var(--zen-space-4); - } - - .site-link-grid > a { - color: inherit; - text-decoration: none; - } - - .site-link-grid zen-card, - .site-link-grid article { - height: 100%; - } - - .site-link-grid p { - margin: 0; - color: var(--zen-color-text-muted); - font-size: 0.9rem; - } </style> </head> <body> @@ -138,16 +117,11 @@ } #themeToggle { + display: flex; flex-direction: column; - min-width: 4.5rem; - padding: 0.35rem; - border-radius: 1.5rem; - } - - #themeName { - font-family: "More", sans-serif; - font-size: 0.65rem; - line-height: 1; + padding: 0; + border: 0; + background: transparent; } /* Professional header */ @@ -155,7 +129,6 @@ margin: auto; padding: 1.5em 1em; font-family: "More", sans-serif; - box-shadow: var(--zen-shadow-sm); width: 720px; max-width: calc(100% - 2em); text-align: center; @@ -257,7 +230,7 @@ } </style> -<zen-button id="themeControl" variant="quiet"> +<zen-button id="themeControl" appearance="plain" size="xl"> <button id="themeToggle" type="button" @@ -271,12 +244,12 @@ height="50" width="50" > - <span id="themeName" aria-live="polite">Auto</span> + <span id="themeName" class="zen-visually-hidden" aria-live="polite">Auto</span> </button> </zen-button> <header> - <h1><a href="/">MrJuneJune</a></h1> + <h1><a href="/" data-zen-link-effect="none">MrJuneJune</a></h1> </header> <script src="/index.js"></script> @@ -292,28 +265,12 @@ </div> <h2>Links</h2> - <div class="site-link-grid"> - <a href="https://zenbu.babocoder.com/file/tip"> - <zen-card interactive> - <article><h3>Repository</h3><p>Browse the code behind these experiments.</p></article> - </zen-card> - </a> - <a href="/blog"> - <zen-card interactive> - <article><h3>Blogs</h3><p>Thoughts, experiments, and technical notes.</p></article> - </zen-card> - </a> - <a href="/resume"> - <zen-card interactive> - <article><h3>Resume</h3><p>Professional experience and selected work.</p></article> - </zen-card> - </a> - <a href="/tools"> - <zen-card interactive> - <article><h3>Tools</h3><p>Small things I built for myself.</p></article> - </zen-card> - </a> - </div> + <ul> + <li><a href="https://zenbu.babocoder.com/file/tip">Repository</a> - Check out my code</li> + <li><a href="/blog">Blogs</a> - My thoughts / Experiments </li> + <li><a href="/resume">Resume</a> - My professional experiences </li> + <li><a href="/tools">Tools</a> - Things I made for myself </li> + </ul> <canvas id="background"></canvas> <canvas id="game"></canvas> <canvas id="gameUI"></canvas>
--- a/mrjunejune/test/snapshots/talk.snapshot Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/test/snapshots/talk.snapshot Wed Aug 05 05:25:40 2026 -0700 @@ -34,8 +34,8 @@ <script src="/public/pwa-register.js" defer></script> <style> - body { font-family: sans-serif; padding: 20px; } - #messages { height: 200px; border: 1px solid #ccc; overflow-y: scroll; margin-bottom: 10px; padding: 10px; } + body { font-family: sans-serif; padding: var(--zenbu-sys-padding-lg); } + #messages { height: 200px; border: 1px solid #ccc; overflow-y: scroll; margin-bottom: var(--zenbu-sys-space-control); padding: var(--zenbu-sys-padding-md); } #chat { display: flex; gap: 10px; } input { flex-grow: 1; } </style> @@ -61,16 +61,11 @@ } #themeToggle { + display: flex; flex-direction: column; - min-width: 4.5rem; - padding: 0.35rem; - border-radius: 1.5rem; - } - - #themeName { - font-family: "More", sans-serif; - font-size: 0.65rem; - line-height: 1; + padding: 0; + border: 0; + background: transparent; } /* Professional header */ @@ -78,7 +73,6 @@ margin: auto; padding: 1.5em 1em; font-family: "More", sans-serif; - box-shadow: var(--zen-shadow-sm); width: 720px; max-width: calc(100% - 2em); text-align: center; @@ -180,7 +174,7 @@ } </style> -<zen-button id="themeControl" variant="quiet"> +<zen-button id="themeControl" appearance="plain" size="xl"> <button id="themeToggle" type="button" @@ -194,12 +188,12 @@ height="50" width="50" > - <span id="themeName" aria-live="polite">Auto</span> + <span id="themeName" class="zen-visually-hidden" aria-live="polite">Auto</span> </button> </zen-button> <header> - <h1><a href="/">MrJuneJune</a></h1> + <h1><a href="/" data-zen-link-effect="none">MrJuneJune</a></h1> </header> <script src="/index.js"></script> @@ -209,8 +203,12 @@ <div id="messages"></div> <div id="chat"> - <input type="text" id="messageInput" placeholder="Type a message..."> - <button id="sendBtn">Send</button> + <zen-input appearance="plain" size="md"> + <input type="text" id="messageInput" placeholder="Type a message..."> + </zen-input> + <zen-button appearance="plain" size="md"> + <button id="sendBtn">Send</button> + </zen-button> </div> </main> <div style="display: flex; align-items: center; justify-content: center; margin: 30px 0px;">
--- a/mrjunejune/test/snapshots/tools.snapshot Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/test/snapshots/tools.snapshot Wed Aug 05 05:25:40 2026 -0700 @@ -54,16 +54,11 @@ } #themeToggle { + display: flex; flex-direction: column; - min-width: 4.5rem; - padding: 0.35rem; - border-radius: 1.5rem; - } - - #themeName { - font-family: "More", sans-serif; - font-size: 0.65rem; - line-height: 1; + padding: 0; + border: 0; + background: transparent; } /* Professional header */ @@ -71,7 +66,6 @@ margin: auto; padding: 1.5em 1em; font-family: "More", sans-serif; - box-shadow: var(--zen-shadow-sm); width: 720px; max-width: calc(100% - 2em); text-align: center; @@ -173,7 +167,7 @@ } </style> -<zen-button id="themeControl" variant="quiet"> +<zen-button id="themeControl" appearance="plain" size="xl"> <button id="themeToggle" type="button" @@ -187,12 +181,12 @@ height="50" width="50" > - <span id="themeName" aria-live="polite">Auto</span> + <span id="themeName" class="zen-visually-hidden" aria-live="polite">Auto</span> </button> </zen-button> <header> - <h1><a href="/">MrJuneJune</a></h1> + <h1><a href="/" data-zen-link-effect="none">MrJuneJune</a></h1> </header> <script src="/index.js"></script>
--- a/mrjunejune/test/snapshots/tools_file_converter.snapshot Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/test/snapshots/tools_file_converter.snapshot Wed Aug 05 05:25:40 2026 -0700 @@ -38,13 +38,13 @@ .container { max-width: 800px; margin: 2rem auto; - padding: 2rem; + padding: var(--zenbu-sys-padding-xl); } .converter-section { - border-radius: 8px; + border-radius: var(--zenbu-sys-radius-control); border: 2px solid var(--lightgray); - padding: 2rem; + padding: var(--zenbu-sys-padding-xl); margin-bottom: 2rem; } @@ -64,11 +64,11 @@ } input[type="file"] { - padding: 0.75rem; + padding: var(--zenbu-sys-padding-sm); border: 2px dashed #ccc; - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); background: white; - font-size: 16px; + font-size: var(--zenbu-sys-font-size-md); } button { @@ -76,12 +76,9 @@ font-family: "More Thin", sans-serif; color: var(--gray-gradient); border: none; - padding: 0.75rem 1.5rem; - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); cursor: pointer; - font-size: 1rem; margin-top: 1rem; - min-height: 44px; } button:hover { @@ -95,9 +92,9 @@ .result { margin-top: 1rem; - padding: 1rem; + padding: var(--zenbu-sys-padding-md); background: white; - border-radius: 4px; + border-radius: var(--zenbu-sys-radius-control-small); display: none; } @@ -136,30 +133,28 @@ /* Mobile responsive */ @media (max-width: 720px) { .container { - padding: 1rem; + padding: var(--zenbu-sys-padding-md); margin: 1rem auto; } .converter-section { - padding: 1.5rem 1rem; + padding: + var(--zenbu-sys-padding-lg) + var(--zenbu-sys-padding-md); } .converter-section h2 { - font-size: 1.5rem; + font-size: var(--zenbu-sys-font-size-2xl); } .file-input-wrapper label { - font-size: 1rem; + font-size: var(--zenbu-sys-font-size-md); } - button { - font-size: 1.1rem; - padding: 1rem 1.5rem; - } .result { - padding: 1rem; - font-size: 1rem; + padding: var(--zenbu-sys-padding-md); + font-size: var(--zenbu-sys-font-size-md); } } </style> @@ -185,16 +180,11 @@ } #themeToggle { + display: flex; flex-direction: column; - min-width: 4.5rem; - padding: 0.35rem; - border-radius: 1.5rem; - } - - #themeName { - font-family: "More", sans-serif; - font-size: 0.65rem; - line-height: 1; + padding: 0; + border: 0; + background: transparent; } /* Professional header */ @@ -202,7 +192,6 @@ margin: auto; padding: 1.5em 1em; font-family: "More", sans-serif; - box-shadow: var(--zen-shadow-sm); width: 720px; max-width: calc(100% - 2em); text-align: center; @@ -304,7 +293,7 @@ } </style> -<zen-button id="themeControl" variant="quiet"> +<zen-button id="themeControl" appearance="plain" size="xl"> <button id="themeToggle" type="button" @@ -318,12 +307,12 @@ height="50" width="50" > - <span id="themeName" aria-live="polite">Auto</span> + <span id="themeName" class="zen-visually-hidden" aria-live="polite">Auto</span> </button> </zen-button> <header> - <h1><a href="/">MrJuneJune</a></h1> + <h1><a href="/" data-zen-link-effect="none">MrJuneJune</a></h1> </header> <script src="/index.js"></script> @@ -338,10 +327,14 @@ <div class="file-input-wrapper"> <label for="imageInput">Choose an image file:</label> - <input type="file" id="imageInput" accept="image/*"> + <zen-input appearance="plain" size="md"> + <input type="file" id="imageInput" accept="image/*"> + </zen-input> </div> - <button id="convertImageBtn" onclick="convertImageToWebP()">Convert to WebP</button> + <zen-button appearance="plain" size="md"> + <button id="convertImageBtn" onclick="convertImageToWebP()">Convert to WebP</button> + </zen-button> <div class="loading" id="imageLoading">Converting... Please wait.</div> @@ -357,10 +350,14 @@ <div class="file-input-wrapper"> <label for="videoInput">Choose a video file:</label> - <input type="file" id="videoInput" accept="video/*"> + <zen-input appearance="plain" size="md"> + <input type="file" id="videoInput" accept="video/*"> + </zen-input> </div> - <button id="convertVideoBtn" onclick="convertVideoToMP4()">Convert to MP4</button> + <zen-button appearance="plain" size="md"> + <button id="convertVideoBtn" onclick="convertVideoToMP4()">Convert to MP4</button> + </zen-button> <div class="loading" id="videoLoading">Converting... Please wait.</div>
--- a/mrjunejune/test/snapshots/tools_hls_player.snapshot Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/test/snapshots/tools_hls_player.snapshot Wed Aug 05 05:25:40 2026 -0700 @@ -57,16 +57,11 @@ } #themeToggle { + display: flex; flex-direction: column; - min-width: 4.5rem; - padding: 0.35rem; - border-radius: 1.5rem; - } - - #themeName { - font-family: "More", sans-serif; - font-size: 0.65rem; - line-height: 1; + padding: 0; + border: 0; + background: transparent; } /* Professional header */ @@ -74,7 +69,6 @@ margin: auto; padding: 1.5em 1em; font-family: "More", sans-serif; - box-shadow: var(--zen-shadow-sm); width: 720px; max-width: calc(100% - 2em); text-align: center; @@ -176,7 +170,7 @@ } </style> -<zen-button id="themeControl" variant="quiet"> +<zen-button id="themeControl" appearance="plain" size="xl"> <button id="themeToggle" type="button" @@ -190,12 +184,12 @@ height="50" width="50" > - <span id="themeName" aria-live="polite">Auto</span> + <span id="themeName" class="zen-visually-hidden" aria-live="polite">Auto</span> </button> </zen-button> <header> - <h1><a href="/">MrJuneJune</a></h1> + <h1><a href="/" data-zen-link-effect="none">MrJuneJune</a></h1> </header> <script src="/index.js"></script> @@ -206,17 +200,23 @@ <form id="hlsForm" class="hls-form"> <label for="hlsUrl">Playlist URL</label> <div class="hls-url-row"> - <input - id="hlsUrl" - name="url" - type="text" - inputmode="url" - value="/public/hls-sample/h264-ts-stream.m3u8" - placeholder="https://example.com/stream.m3u8" - required - /> - <button type="submit">Load</button> - <button id="sampleButton" type="button">Sample</button> + <zen-input appearance="plain" size="md"> + <input + id="hlsUrl" + name="url" + type="text" + inputmode="url" + value="/public/hls-sample/h264-ts-stream.m3u8" + placeholder="https://example.com/stream.m3u8" + required + /> + </zen-input> + <zen-button appearance="plain" size="md"> + <button type="submit">Load</button> + </zen-button> + <zen-button appearance="plain" size="md"> + <button id="sampleButton" type="button">Sample</button> + </zen-button> </div> </form> @@ -226,21 +226,25 @@ <div class="hls-local-actions"> <label class="hls-file-button"> Choose HLS files - <input - id="hlsFiles" - type="file" - accept=".m3u8,.m4s,.mp4,.ts,.aac,.vtt,.key,application/vnd.apple.mpegurl" - multiple - /> + <zen-input appearance="plain" size="md"> + <input + id="hlsFiles" + type="file" + accept=".m3u8,.m4s,.mp4,.ts,.aac,.vtt,.key,application/vnd.apple.mpegurl" + multiple + /> + </zen-input> </label> <label class="hls-file-button"> Choose HLS folder - <input - id="hlsFolder" - type="file" - webkitdirectory - multiple - /> + <zen-input appearance="plain" size="md"> + <input + id="hlsFolder" + type="file" + webkitdirectory + multiple + /> + </zen-input> </label> </div> </fieldset>
--- a/mrjunejune/test/snapshots/tools_latex_editor.snapshot Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/test/snapshots/tools_latex_editor.snapshot Wed Aug 05 05:25:40 2026 -0700 @@ -56,16 +56,11 @@ } #themeToggle { + display: flex; flex-direction: column; - min-width: 4.5rem; - padding: 0.35rem; - border-radius: 1.5rem; - } - - #themeName { - font-family: "More", sans-serif; - font-size: 0.65rem; - line-height: 1; + padding: 0; + border: 0; + background: transparent; } /* Professional header */ @@ -73,7 +68,6 @@ margin: auto; padding: 1.5em 1em; font-family: "More", sans-serif; - box-shadow: var(--zen-shadow-sm); width: 720px; max-width: calc(100% - 2em); text-align: center; @@ -175,7 +169,7 @@ } </style> -<zen-button id="themeControl" variant="quiet"> +<zen-button id="themeControl" appearance="plain" size="xl"> <button id="themeToggle" type="button" @@ -189,12 +183,12 @@ height="50" width="50" > - <span id="themeName" aria-live="polite">Auto</span> + <span id="themeName" class="zen-visually-hidden" aria-live="polite">Auto</span> </button> </zen-button> <header> - <h1><a href="/">MrJuneJune</a></h1> + <h1><a href="/" data-zen-link-effect="none">MrJuneJune</a></h1> </header> <script src="/index.js"></script> @@ -205,13 +199,21 @@ <p>Write LaTeX here. My server compiles it inside a locked-down sandbox and sends the PDF back to this preview.</p> </div> <div class="latex-actions"> - <label class="auto-compile"> - <input id="autoCompile" type="checkbox" checked /> - Auto compile - </label> - <button id="resetButton" type="button" class="secondary">Reset</button> - <button id="compileButton" type="button">Compile PDF</button> - <a id="downloadButton" class="button-link disabled" aria-disabled="true">Download</a> + <zen-checkbox appearance="plain" size="sm"> + <label class="auto-compile"> + <input id="autoCompile" type="checkbox" checked /> + Auto compile + </label> + </zen-checkbox> + <zen-button appearance="plain" size="md"> + <button id="resetButton" type="button" class="secondary">Reset</button> + </zen-button> + <zen-button appearance="plain" size="md"> + <button id="compileButton" type="button">Compile PDF</button> + </zen-button> + <zen-button appearance="plain" size="md"> + <a id="downloadButton" class="button-link disabled" aria-disabled="true">Download</a> + </zen-button> </div> </div> @@ -225,7 +227,8 @@ <h2 id="sourceHeading">document.tex</h2> <span id="sourceSize">0 / 65,536 bytes</span> </div> - <textarea id="latexSource" spellcheck="false" autocomplete="off" aria-label="LaTeX source" placeholder="Type your LaTeX document here...">\documentclass[11pt]{article} + <zen-textarea appearance="plain" size="md"> + <textarea id="latexSource" spellcheck="false" autocomplete="off" aria-label="LaTeX source" placeholder="Type your LaTeX document here...">\documentclass[11pt]{article} \usepackage[margin=1in]{geometry} \usepackage{amsmath} \usepackage{xcolor} @@ -247,6 +250,7 @@ \end{document} </textarea> + </zen-textarea> </section> <section class="latex-panel preview-panel" aria-labelledby="previewHeading">
--- a/mrjunejune/test/snapshots/tools_markdown_to_html.snapshot Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/test/snapshots/tools_markdown_to_html.snapshot Wed Aug 05 05:25:40 2026 -0700 @@ -57,16 +57,11 @@ } #themeToggle { + display: flex; flex-direction: column; - min-width: 4.5rem; - padding: 0.35rem; - border-radius: 1.5rem; - } - - #themeName { - font-family: "More", sans-serif; - font-size: 0.65rem; - line-height: 1; + padding: 0; + border: 0; + background: transparent; } /* Professional header */ @@ -74,7 +69,6 @@ margin: auto; padding: 1.5em 1em; font-family: "More", sans-serif; - box-shadow: var(--zen-shadow-sm); width: 720px; max-width: calc(100% - 2em); text-align: center; @@ -176,7 +170,7 @@ } </style> -<zen-button id="themeControl" variant="quiet"> +<zen-button id="themeControl" appearance="plain" size="xl"> <button id="themeToggle" type="button" @@ -190,12 +184,12 @@ height="50" width="50" > - <span id="themeName" aria-live="polite">Auto</span> + <span id="themeName" class="zen-visually-hidden" aria-live="polite">Auto</span> </button> </zen-button> <header> - <h1><a href="/">MrJuneJune</a></h1> + <h1><a href="/" data-zen-link-effect="none">MrJuneJune</a></h1> </header> <script src="/index.js"></script> @@ -205,7 +199,8 @@ <div class="container"> <div class="panel"> - <div class="label">Markdown Input</div> + <zen-field appearance="plain" size="md"> + <label class="label" for="input">Markdown Input</label> <textarea id="input" placeholder="Type your markdown here..."># Welcome to Markdown Converter ## Features @@ -247,12 +242,15 @@  **Try editing this text!**</textarea> + </zen-field> </div> <div class="panel"> <div class="title"> <div class="label">HTML Output</div> - <button id="copy"> Copy </button> + <zen-button appearance="plain" size="sm"> + <button id="copy"> Copy </button> + </zen-button> </div> <div id="output"></div> </div>
--- a/mrjunejune/test/theme_and_webp_test.js Tue Aug 04 16:49:11 2026 -0700 +++ b/mrjunejune/test/theme_and_webp_test.js Wed Aug 05 05:25:40 2026 -0700 @@ -56,6 +56,72 @@ return files; } +function primitiveOwnershipViolations(source) { + const violations = []; + const stack = []; + const voidElements = new Set([ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', + 'meta', 'param', 'source', 'track', 'wbr', + ]); + const ownerMap = { + button: new Set(['zen-button']), + select: new Set(['zen-field', 'zen-native-select', 'zen-select']), + textarea: new Set(['zen-field', 'zen-textarea']), + }; + + for (const token of source.matchAll( + /<!--[\s\S]*?-->|<\/?([a-zA-Z][\w-]*)\b([^>]*)>/g, + )) { + if (!token[1]) continue; + const name = token[1].toLowerCase(); + if (token[0].startsWith('</')) { + while (stack.length) { + if (stack.pop() === name) break; + } + continue; + } + + let owners = ownerMap[name]; + if (name === 'input') { + const type = /\btype\s*=\s*["']?([^"'\s>]+)/i.exec(token[2])?.[1] + ?.toLowerCase() || 'text'; + if (type === 'checkbox') { + owners = new Set(['zen-checkbox', 'zen-field', 'zen-switch']); + } else if (type === 'radio') { + owners = new Set(['zen-field', 'zen-radio-group']); + } else if (type === 'range') { + owners = new Set(['zen-field', 'zen-slider']); + } else if (type === 'date') { + owners = new Set(['zen-calendar', 'zen-date-picker', 'zen-field']); + } else { + owners = new Set([ + 'zen-combobox', + 'zen-command', + 'zen-field', + 'zen-input', + 'zen-input-group', + 'zen-input-otp', + ]); + } + } + if (owners) { + const matchingOwners = stack.filter(ancestor => owners.has(ancestor)); + if (matchingOwners.length !== 1) { + violations.push({ + control: name, + owners: matchingOwners, + source: token[0], + }); + } + } + + if (!voidElements.has(name) && !token[0].endsWith('/>')) { + stack.push(name); + } + } + return violations; +} + async function sampleTheme(browser, theme) { const context = await browser.newContext({ colorScheme: 'dark' }); await context.addInitScript(value => { @@ -125,19 +191,46 @@ converted[2] * 0.0722; }; const bodyStyle = getComputedStyle(document.body); - const foreground = colorLuminance(bodyStyle.color); - const background = colorLuminance( - getComputedStyle(document.querySelector('main')).backgroundColor, + const themeButtonStyle = getComputedStyle( + document.querySelector('#themeToggle'), ); + const foreground = colorLuminance(bodyStyle.color); + const contrastProbe = document.createElement('span'); + contrastProbe.style.background = 'var(--zen-color-surface)'; + document.body.append(contrastProbe); + const background = colorLuminance( + getComputedStyle(contrastProbe).backgroundColor, + ); + contrastProbe.remove(); return { - cardCount: document.querySelectorAll('.site-link-grid zen-card').length, - componentReady: Boolean(customElements.get('zen-card')), + backgroundRepeat: bodyStyle.backgroundRepeat, + backgroundSize: bodyStyle.backgroundSize, + bodyCoversViewport: document.body.getBoundingClientRect().height >= + innerHeight, + componentReady: Boolean(customElements.get('zen-button')), count, fontFamily: getComputedStyle(document.body).fontFamily, + headerPaws: document.querySelectorAll( + 'header [data-zen-link-decoration]', + ).length, luminance: count ? luminance / count : 0, textContrast: (Math.max(foreground, background) + 0.05) / (Math.min(foreground, background) + 0.05), rootTheme: document.documentElement.dataset.zenTheme || 'auto', + enhancedLinks: document.querySelectorAll( + 'zen-link[effect="paw"] > a > zen-icon[name="paw"]', + ).length, + links: document.querySelectorAll('a').length, + pawLinks: [...document.querySelectorAll('a')].filter(link => + !link.closest('zen-button') && + link.dataset.zenLinkEffect !== 'none' + ).length, + mainBackground: getComputedStyle( + document.querySelector('main'), + ).backgroundColor, + themeButtonBackground: themeButtonStyle.backgroundColor, + themeButtonBorder: themeButtonStyle.borderTopWidth, + themeButtonShadow: themeButtonStyle.boxShadow, themeLabel: document.querySelector('#themeName')?.textContent, }; }); @@ -168,6 +261,118 @@ await context.close(); } +async function testPlainField(browser) { + const page = await browser.newPage(); + await page.goto(`${baseUrl}/tools/markdown_to_html`, { + waitUntil: 'networkidle', + }); + await page.waitForFunction(() => customElements.get('zen-field')); + const state = await page.locator('zen-field[appearance="plain"]').evaluate( + field => { + const label = field.querySelector('label'); + const textarea = field.querySelector('textarea'); + const probe = document.createElement('span'); + probe.style.border = '1px solid var(--accent)'; + document.body.append(probe); + const accent = getComputedStyle(probe).borderColor; + probe.remove(); + textarea.focus(); + return { + borderColor: getComputedStyle(textarea).borderColor, + display: getComputedStyle(field).display, + labelFor: label.htmlFor, + accent, + textareaId: textarea.id, + }; + }, + ); + assert.deepEqual(state, { + accent: state.borderColor, + borderColor: state.borderColor, + display: 'contents', + labelFor: 'input', + textareaId: 'input', + }); + await page.close(); +} + +async function testButtonScale(browser) { + const page = await browser.newPage(); + for (const route of [ + '/talk', + '/notes/login', + '/tools/hls_player', + '/tools/latex_editor', + '/tools/markdown_to_html', + '/tools/file_converter', + '/offline.html', + ]) { + await page.goto(`${baseUrl}${route}`, { waitUntil: 'networkidle' }); + await page.waitForFunction(() => customElements.get('zen-button')); + const controls = await page.locator( + 'zen-button[size] > :is(button, a):not(#themeToggle)', + ).evaluateAll(elements => elements.map(control => { + const host = control.parentElement; + const style = getComputedStyle(control); + const hostStyle = getComputedStyle(host); + return { + height: control.getBoundingClientRect().height, + minimum: Number.parseFloat( + hostStyle.getPropertyValue('--zenbu-control-height'), + ) * Number.parseFloat(getComputedStyle(document.documentElement).fontSize), + paddingBlock: style.paddingBlockStart, + paddingVariable: Number.parseFloat( + hostStyle.getPropertyValue('--zenbu-control-padding-block'), + ) * Number.parseFloat(getComputedStyle(document.documentElement).fontSize), + size: host.getAttribute('size'), + }; + })); + assert.ok(controls.length > 0, route); + for (const control of controls) { + assert.match(control.size, /^(?:xs|sm|md|lg|xl)$/); + assert.ok(control.height >= control.minimum - 1, JSON.stringify(control)); + assert.ok( + Math.abs(Number.parseFloat(control.paddingBlock) - + control.paddingVariable) < 1, + JSON.stringify(control), + ); + } + } + await page.close(); +} + +async function testDynamicButtonOwnership(browser) { + const page = await browser.newPage(); + await page.goto(baseUrl, { waitUntil: 'networkidle' }); + await page.evaluate(() => { + window.dispatchEvent(new Event('beforeinstallprompt', { + cancelable: true, + })); + }); + const state = await page.locator('#pwa-install-btn').evaluate(button => ({ + ownerCount: button.closest('zen-button') ? 1 : 0, + appearance: button.closest('zen-button')?.getAttribute('appearance'), + size: button.closest('zen-button')?.getAttribute('size'), + })); + assert.deepEqual(state, { + appearance: 'plain', + ownerCount: 1, + size: 'md', + }); + await page.close(); +} + +async function testResumePrint(browser) { + const page = await browser.newPage(); + await page.emulateMedia({ media: 'print' }); + await page.goto(`${baseUrl}/resume`, { waitUntil: 'networkidle' }); + assert.equal( + await page.locator('.info > a[href="/public/resume.pdf"]').isVisible(), + false, + ); + await page.close(); +} + async function testHlsPlayer(browser, siteRoot) { const page = await browser.newPage(); const errors = []; @@ -423,6 +628,34 @@ baseUrl = `http://127.0.0.1:${port}`; const pngFiles = listFiles(siteRoot).filter(file => file.endsWith('.png')); assert.deepEqual(pngFiles, [], `PNG files leaked into runfiles: ${pngFiles}`); + for (const file of listFiles(siteRoot).filter(file => + file.endsWith('.html') + )) { + const source = fs.readFileSync(file, 'utf8'); + for (const primitive of source.matchAll(/<zen-button\b([^>]*)>/g)) { + assert.match( + primitive[1], + /\bsize=["'](?:xs|sm|md|lg|xl)["']/, + `${path.relative(siteRoot, file)} has an unsized zen-button`, + ); + } + assert.deepEqual( + primitiveOwnershipViolations(source), + [], + `${path.relative(siteRoot, file)} violates primitive ownership`, + ); + } + for (const file of listFiles(siteRoot).filter(file => + /\.(?:css|html|js)$/.test(file) && + !file.includes(`${path.sep}public${path.sep}design-system${path.sep}`) + )) { + const source = fs.readFileSync(file, 'utf8'); + assert.doesNotMatch( + source, + /var\(\s*--zen-/, + `${path.relative(siteRoot, file)} consumes a deprecated token`, + ); + } server = spawn(serverBinary, [], { cwd: runfilesWorkspace, @@ -446,7 +679,7 @@ for (const source of [home, dogGame, manifest]) { assert.doesNotMatch(source, /\.png(?:["')]|$)/i); } - assert.match(serviceWorker, /v5-zenbu-themes/); + assert.match(serviceWorker, /v6-zenbu-headless/); assert.match( await ( await fetch(`${baseUrl}/public/pwa-register.js`) @@ -486,15 +719,28 @@ assert.equal(automatic.themeLabel, 'Auto'); for (const sample of [paper, ink, playful, automatic]) { assert.ok(sample.count > 0); + assert.equal(sample.backgroundRepeat, 'no-repeat'); + assert.equal(sample.backgroundSize, 'cover'); + assert.equal(sample.bodyCoversViewport, true); assert.equal(sample.componentReady, true); - assert.equal(sample.cardCount, 4); + assert.ok(sample.links > 0); + assert.equal(sample.enhancedLinks, sample.pawLinks); assert.match(sample.fontFamily, /More/); + assert.equal(sample.headerPaws, 0); assert.ok(sample.textContrast >= 4.5, JSON.stringify(sample)); + assert.equal(sample.mainBackground, 'rgba(0, 0, 0, 0)'); + assert.equal(sample.themeButtonBackground, 'rgba(0, 0, 0, 0)'); + assert.equal(sample.themeButtonBorder, '0px'); + assert.equal(sample.themeButtonShadow, 'none'); } assert.ok(paper.luminance < 175, JSON.stringify(paper)); assert.ok(playful.luminance < 175, JSON.stringify(playful)); assert.ok(ink.luminance > 200, JSON.stringify(ink)); await testThemeCycle(browser); + await testPlainField(browser); + await testButtonScale(browser); + await testDynamicButtonOwnership(browser); + await testResumePrint(browser); await testHlsPlayer(browser, siteRoot); } finally { if (browser) await browser.close();