From 49e11e900ba528e38c53ee0682db20bcca6dc1b4 Mon Sep 17 00:00:00 2001 From: WeekendsuperHero <4048475+WeekendSuperhero@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:12:45 -0700 Subject: [PATCH 1/7] updated things --- .storybook/preview.ts | 2 +- CHANGELOG.md | 8 + README.md | 4 +- app/components/page.tsx | 7 + app/docs/theming/page.tsx | 29 +- app/globals.css | 6 +- app/layout.tsx | 15 +- app/site.css | 14 + app/theme/engine.css | 174 +++-- app/theme/materials.css | 90 +-- app/theme/presets.css | 29 +- app/theme/registrations.css | 27 +- app/theme/sonner.css | 44 -- app/theme/styles.css | 73 +- app/theme/tokens.css | 24 +- app/theme/utilities.css | 51 +- biome.json | 2 +- bun.lock | 663 +++++++++--------- components/auto-foreground.tsx | 11 +- components/component-preview.tsx | 227 +++++- components/docs-sidebar.tsx | 7 + components/foreground-tester.tsx | 6 +- components/glass-tint-switcher.tsx | 7 +- components/pattern-background.tsx | 98 +-- components/ui/button.tsx | 2 +- components/ui/chart.tsx | 77 +- components/ui/combobox.tsx | 282 ++++++++ components/ui/command.tsx | 20 +- components/ui/context-menu.tsx | 39 +- components/ui/dialog.tsx | 71 +- components/ui/dropdown-menu.tsx | 39 +- components/ui/empty-state.tsx | 3 + components/ui/empty.tsx | 115 +++ components/ui/field.tsx | 215 ++++++ components/ui/input.tsx | 1 - components/ui/item.tsx | 170 +++++ components/ui/kbd.tsx | 27 + components/ui/menu-bar.tsx | 10 +- components/ui/menubar.tsx | 300 ++++++++ components/ui/navigation-menu.tsx | 4 +- components/ui/progress.tsx | 31 + components/ui/select.tsx | 2 +- components/ui/skeleton.tsx | 2 +- components/ui/sonner.tsx | 155 +++- components/ui/switch.tsx | 2 +- components/ui/table.tsx | 16 +- components/ui/textarea.tsx | 1 - lib/component-examples.ts | 175 ++++- lib/material.ts | 14 +- lib/oklch-utils.ts | 13 + lib/snap-popper.ts | 33 +- lib/storybook-url.ts | 9 +- package.json | 57 +- public/r/accordion.json | 2 +- public/r/alert-dialog.json | 2 +- public/r/alert.json | 2 +- public/r/auto-foreground.json | 2 +- public/r/avatar.json | 2 +- public/r/background-controller.json | 2 +- public/r/badge.json | 2 +- public/r/breadcrumb.json | 2 +- public/r/button-group.json | 2 +- public/r/button.json | 4 +- public/r/calendar.json | 2 +- public/r/card.json | 2 +- public/r/carousel.json | 2 +- public/r/chart.json | 4 +- public/r/checkbox.json | 2 +- public/r/collapsible.json | 2 +- public/r/combobox.json | 45 ++ public/r/command.json | 4 +- public/r/context-menu.json | 6 +- public/r/cropper.json | 2 +- public/r/date-picker-input.json | 2 +- public/r/dialog.json | 4 +- public/r/drawer.json | 2 +- public/r/dropdown-menu.json | 6 +- public/r/empty-state.json | 6 +- public/r/empty.json | 41 ++ public/r/field.json | 43 ++ public/r/hover-card.json | 4 +- public/r/input-group.json | 2 +- public/r/input-otp.json | 2 +- public/r/input.json | 4 +- public/r/item.json | 43 ++ public/r/kbd.json | 41 ++ public/r/label.json | 2 +- public/r/material.json | 2 +- public/r/menu-bar.json | 6 +- public/r/menubar.json | 49 ++ public/r/mode-toggle.json | 2 +- public/r/navigation-menu.json | 4 +- public/r/oklch-utils.json | 2 +- public/r/pagination.json | 2 +- public/r/popover.json | 4 +- public/r/progress.json | 42 ++ public/r/radio-group.json | 2 +- public/r/registry.json | 272 ++++++- public/r/scroll-area.json | 2 +- public/r/select.json | 6 +- public/r/separator.json | 2 +- public/r/sheet.json | 2 +- public/r/sidebar.json | 2 +- public/r/skeleton.json | 4 +- public/r/slider.json | 2 +- public/r/sonner.json | 4 +- public/r/spinner.json | 2 +- public/r/switch.json | 4 +- public/r/table.json | 4 +- public/r/tabs.json | 2 +- public/r/textarea.json | 4 +- public/r/theme.json | 2 +- public/r/toggle-group.json | 2 +- public/r/toggle.json | 2 +- public/r/tooltip.json | 4 +- registry.json | 272 ++++++- registry/theme/globals.css | 517 +++++++------- scripts/check-contrast.mjs | 34 +- scripts/check-gamut.mjs | 23 +- scripts/check-patterns.mjs | 36 +- scripts/check-theme.mjs | 151 +++- stories/Combobox.stories.tsx | 95 +++ stories/Dialog.stories.tsx | 38 +- stories/Empty.stories.tsx | 53 ++ stories/Field.stories.tsx | 94 +++ stories/Item.stories.tsx | 75 ++ stories/Kbd.stories.tsx | 38 + ....stories.tsx => MenuBarLegacy.stories.tsx} | 2 +- stories/Menubar.stories.tsx | 89 +++ stories/Progress.stories.tsx | 38 + stories/Table.stories.tsx | 32 + tests/auto-foreground-lapis.test.tsx | 162 +++++ tests/chart-container.test.tsx | 152 ++++ tests/combobox.test.tsx | 60 ++ tests/command.test.tsx | 75 ++ tests/dialog.test.tsx | 87 ++- tests/field.test.tsx | 83 +++ tests/item.test.tsx | 68 ++ tests/material.test.tsx | 128 ++++ tests/menu-sub-portal.test.tsx | 96 +++ tests/menubar.test.tsx | 84 +++ tests/pattern-background.test.tsx | 110 +++ tests/progress.test.tsx | 37 + tests/snap-popper.test.tsx | 71 +- tests/sonner.test.tsx | 111 +++ tests/table.test.tsx | 90 +++ 146 files changed, 6146 insertions(+), 1090 deletions(-) create mode 100644 app/site.css delete mode 100644 app/theme/sonner.css create mode 100644 components/ui/combobox.tsx create mode 100644 components/ui/empty.tsx create mode 100644 components/ui/field.tsx create mode 100644 components/ui/item.tsx create mode 100644 components/ui/kbd.tsx create mode 100644 components/ui/menubar.tsx create mode 100644 components/ui/progress.tsx create mode 100644 public/r/combobox.json create mode 100644 public/r/empty.json create mode 100644 public/r/field.json create mode 100644 public/r/item.json create mode 100644 public/r/kbd.json create mode 100644 public/r/menubar.json create mode 100644 public/r/progress.json create mode 100644 stories/Combobox.stories.tsx create mode 100644 stories/Empty.stories.tsx create mode 100644 stories/Field.stories.tsx create mode 100644 stories/Item.stories.tsx create mode 100644 stories/Kbd.stories.tsx rename stories/{MenuBar.stories.tsx => MenuBarLegacy.stories.tsx} (97%) create mode 100644 stories/Menubar.stories.tsx create mode 100644 stories/Progress.stories.tsx create mode 100644 tests/auto-foreground-lapis.test.tsx create mode 100644 tests/chart-container.test.tsx create mode 100644 tests/combobox.test.tsx create mode 100644 tests/command.test.tsx create mode 100644 tests/field.test.tsx create mode 100644 tests/item.test.tsx create mode 100644 tests/material.test.tsx create mode 100644 tests/menu-sub-portal.test.tsx create mode 100644 tests/menubar.test.tsx create mode 100644 tests/pattern-background.test.tsx create mode 100644 tests/progress.test.tsx create mode 100644 tests/sonner.test.tsx create mode 100644 tests/table.test.tsx diff --git a/.storybook/preview.ts b/.storybook/preview.ts index ae181fed..9a301900 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,5 +1,5 @@ import type { Preview } from "@storybook/react"; -import "../app/globals.css"; +import "../app/site.css"; const preview: Preview = { parameters: { diff --git a/CHANGELOG.md b/CHANGELOG.md index ed25528c..e7d88f23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +- **Theme design fixes for consumers (BREAKING for custom toasters)**: *`@apply glass` surfaces* — the opaque page style now drops the solidify layer through a token on its own scope (`--srf-solidify: none`), so it reaches surfaces with no `.glass` class, and `glass-veil` restores the layer with a non-inheriting `--veil-solidify`; element-level page-style rules also accept a documented `data-surface` marker (`data-surface="veil"` when veiled). New invariants `[surface-marker]` and `[solidify-owner]`. *Toasts* — every `[data-sonner-toast]` rule left the theme (engine blocks, status presets, page styles and `sonner.css`); `Toaster` is now `unstyled`, draws each toast with `glassMaterial({ border, veil, size: "lg" })` and sonner's layout in classes, and stamps `data-glass-tint` from sonner's `data-type` (`error` → `destructive`), so status toasts use the theme's ordinary status blocks. Visible changes: rounder corners, status title colours apply (sonner's unlayered `color: inherit` used to beat them), status edges in their own hue, and default toasts follow the page tint. A consumer with its own Toaster must adopt the new `sonner.tsx`. New invariant `[vendor-free]`; `[status]` reads `TOAST_TINT`. *Scoped gloss* — `data-gloss` now works on any element, not just ``: the gloss and the stacks that bake it are composed in a new engine block C on `:root, .dark, [data-glass-tint], [data-gloss]`, flavors are two leaf switches (`--glass-gloss-is-white` / `--glass-gloss-is-hue`), and a nested `data-glass-tint` keeps the inherited flavor instead of resetting it to tonal. New invariant `[gloss-scope]`; `check-gamut` seeds the hue flavor's L74 for its variants. - **Chakra renames (BREAKING)**: renamed the tint presets Emerald → Aventurine, Bone → Moonstone, and Neutral → Selenite to align the jewel palette with the chakra-stone set. The demo switcher migrates stored legacy names automatically, but consumers targeting `[data-glass-tint="emerald"]` / `[data-glass-tint="bone"]` must update to the new names. - **Material system (BREAKING)**: redesigned the glass API around four **materials** (`glass`, `frosted`, `crystal`, `opaque`) selected with a `material` prop (undefined = adaptive glass, the default; `material="none"` = plain, non-glass), composed with orthogonal **axes**, `border`, `veil` (the legibility floor for menus/overlays), `gradient`, `glow`, `sheen`, while `size` stays the component's own dimension. On raw elements the surface is the single structural `glass` utility plus the `glass-border` / `glass-veil` / `glass-sm` / `glass-lg` / `glass-gradient` / `glass-glow` / `glass-glow-lg` / `glass-sheen` axis classes, with the material set via a `data-material` attribute. Components now live in **one tree** (`components/ui/*`) and resolve their surface through `materialSurface` / `glassMaterial` in `lib/material.ts` instead of hardcoded recipe-class strings. Renamed the crystal gloss attribute `data-crystal` → `data-gloss`. The page-style `data-glass` and tint `data-glass-tint` attributes are unchanged. @@ -15,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Legacy glass API (BREAKING)**: removed the `variant="glass|frosted|crystal|opaque|surface|solid"` surface tiers, the recipe utility classes (`glass-bg`, `glass-surface`, `glass-solid`, `glass-opaque`, `glass-frosted`, `glass-crystal`), the per-component `glass={{ color, blur, transparency, outline }}` prop, replaced by the `glassVars({ tintH, tintC, tintA, opacity, blur, solidA })` helper that emits CSS custom properties, and the `components/ui/glass/*` wrapper tree. ### Added +- **Seven shadcn items, material-aware**: `kbd` (theme tokens and the glass hairline instead of a surface, no tooltip inversion), `empty` (shadcn's anatomy; plain by default, `variant="glass"` for its own surface, a bordered chip for `EmptyMedia variant="icon"`; supersedes the legacy `empty-state`), `progress` (Slider's track and `--glass-accent` fill), `item` (a surface role per variant: plain `default`, bordered-glass `outline`, borderless-glass `muted`), `field` (plain layout, with shadcn's dead `has-data-checked:` / `group-has-data-horizontal/field:` variants fixed and the separator drawn as two rules instead of a `bg-background` knock-out), `menubar` (Radix Menubar; a bordered glass bar whose menus and sub menus are portaled, veiled glass with the popper snap and `bg-accent` rows; the legacy `menu-bar` story is now `Sistine/MenuBarLegacy`) and `combobox` (Base UI, as shadcn now uses, via the new `@base-ui/react@^1.9.0` dependency; veiled glass popup, a bordered small-tier chips field, and shadcn's never-matching `data-open:data-closed:animate-out` fixed). Each ships as a registry item with a docs preview, a code example, a story and tests (`menubar`, `combobox`, `field`, `progress`, `item`). `form` and `split-pane` were not adopted: Field with a form library's Controller covers the first, `resizable` the second. +- **`borderColor="ink"` axis**: the material edge is a near-white rim at the material's alpha, which reads as glass on a dark page and as nothing on most light themes at any weight, so `border="rim" | "frame"` had no visible use there. `borderColor="ink"` (class `glass-border-ink`) draws the border in the element's own text colour, `--foreground` at `--glass-border-ink-a` (0.5), through `MaterialProps`, `splitAxisProps`, `glassMaterial` and role defaults in `materialSurface`; it also colours the opaque page's uniform panel edge. `--brd-ink` is registered non-inheriting, so an inked panel does not ink the borders inside it. `check-contrast` gains an `[ink]` check that the line clears the APCA non-text floor (30 Lc) on the sheer and opaque surface of every preset in both modes (tightest 31.6); `tests/material.test.tsx` pins the class output, role inheritance and that the prop never reaches the DOM. - **ARC Bronze contrast**: introduced APCA contrast banding targeting for size-tiered text foregrounds and icons. - **Perceptual hue helpers**: added `complement` and `harmony` OKLCH hue helpers for theme-complementary accents. - **Glass surface tiers**: added `surface` and `solid` variants across all glass UI components. @@ -29,6 +32,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Horizon scenes**: added three pure-CSS animated pattern backgrounds on the shared 42% horizon, all drawn from photographic references rather than illustration: `clouds` (a deep-blue altitude sky over a parallax cloud deck, with a placeable moon), `dune` (golden-hour ridges receding into haze, blowing sand, a placeable moon), and `aurora` (borealis curtains mirrored in rippling water below the waterline). None of the three draws a horizon *line*: the sky dissolves into the ground across a mottled haze band, and distant layers wash toward the sky's own value. All are light/dark aware, take a half-strength theme cast in the mid-sky, move on compositor-only transforms, and respect reduced motion. `clouds` and `dune` swap the disc per mode: a **sun** by day (a hot core with a wide bloom, sitting low in the warm band for `dune`) and a prominent moon by night, from one shared rule, and `dune` adds a **blowing-sand control** (`breeze` / `storm` / `still`) driving wind veils that stream across the ridges. ### Fixed +- **Component one-liners from the first consumer's patch list**: `CommandItem` dropped its bare `data-[disabled]:` variants, which matched the `data-disabled="false"` cmdk puts on every enabled item and made no picker entry clickable by mouse (items are now `cursor-pointer`; pinned by `tests/command.test.tsx`). `DropdownMenuSubContent` and `ContextMenuSubContent` are portaled like their parent content, so a glass menu's `backdrop-filter` no longer clips its sub menu. `navigation-menu`'s `bg-foreground/10/50` classes, which Tailwind drops silently, are `bg-accent/50`, and `check-theme` gains a `[double-slash]` guard. Every menu row (dropdown, context, select, command, navigation, menu-bar) now highlights in `bg-accent`, so a consumer's `--accent` reaches it. The `ghost` button is borderless (`outline` is the bordered one, as in shadcn). `Input` and `Textarea` no longer scale up on focus; the ring marks it. `Switch` gains a hover state. `auto-foreground` exports `FG_STORAGE_KEY`, `RAMP_KEY` and `FG_EVENT`. +- **Theme one-liners from the same list**: the theme no longer maps `--font-sans` / `--font-mono` to `--font-geist-*`, which only this site defines, so a consumer without Geist gets Tailwind's default stacks instead of nothing; the mapping moved to the site-only `app/site.css`, and the site's next/font variables moved to ``, where `:root` resolves them (the site itself had been rendering the system stack). `--veil-floor` is registered `inherits: false`, so a glass surface nested in a veiled one no longer paints its parent's veil. `@keyframes shimmer` is namespaced `sistine-shimmer` and animates `translate`, so the skeleton's highlight sweeps -100% → 100% instead of composing with `-translate-x-full` and parking on top of it. Selenite gets a `[data-glass-tint="selenite"]` block carrying its `--harmony-h: 0` anchor, so a CSS-only consumer has one. Comments: `bun run` instead of `pnpm`, and the browserslist floor is documented as Sistine's own, which consumers should declare for themselves. +- **Component behaviour**: `Table`'s glass variant no longer puts `overflow-hidden` on the `` (a scroll container, so a sticky header bound to it and rode along with the rows); the container clips the corners with `overflow-clip` instead, and a new `containerClassName` makes it the scroller (`max-h-96 overflow-y-auto` + a `sticky top-0` header). `ChartContainer` measures its plot box with one ResizeObserver and hands recharts a fixed size from an absolute layer, so the `minHeight={150}` feedback loop ("ResizeObserver loop completed with undelivered notifications") and the zero-size warnings for charts in hidden tabs are gone; the 150px floor moved to the container as `min-h-[150px]`. A new `DialogBody` makes only the body scroll, with the header and footer fixed around it, so a footer button's glow is no longer sliced by the scroll box; dialogs without it behave as before, and every dialog's children can now shrink below their content width. The popper snap also resets the `will-change: transform` Floating UI writes at DPR ≥ 1.5, the actual cause of soft popper text in WebKit (it composites the wrapper and drops subpixel antialiasing). +- **Pattern scenes stop instead of pausing**: reduced motion and Speed 0 now both REMOVE a scene's animations (`animation: none; will-change: auto`) through one exported helper, `stopRules()`, keyed on `prefers-reduced-motion` and a `data-pat-static` attribute on the scene root. The old `--pat-play` / `animation-play-state: paused` knob left every layer animating, so WebKit kept re-rendering six full-viewport blurred, masked layers at Speed 0, and its `will-change` resolved to the invalid `auto, auto`. Ported from the first consumer's fork with its `tileScale` prop and the `TILE_SCALE` / `GEOMETRIC_TILE` exports (so a small preview can draw several dots or grid cells). Pinned by `tests/pattern-background.test.tsx`; `check-patterns` now fails on `animation-play-state` / `--pat-play` and on any animated scene stylesheet that does not call `stopRules(`. +- **Body text no longer trades its hue for an unreadable sliver of contrast**: `pickTonalInBand`'s legibility guard handed back the raw ramp's black end whenever the tonal pick missed the 75 Lc body floor, even when black missed it too. On lapis over a darker solidify backing that meant `#000002` for 1.8 Lc. Now a tonal pick within `TONAL_HUE_SLACK_LC` (3) of the floor is kept unless the fallback actually clears the floor; a fallback that clears it still always wins, and the mid-tone dead zone (tonal ~43 Lc) still takes the extreme, so `check-contrast` is unchanged. Reproduced first in `tests/auto-foreground-lapis.test.tsx`: the shipped L92 backing was never affected (L20 renders `#09014e` at 79.7 Lc, 78.8 at `--glass-opacity: 0.8`). The consumer's second hunk, dropping ramp steps by measured luminance, is not ported: it removes exactly the L20 step that keeps lapis ink-blue. - **Blurry text on popper surfaces**: `dropdown-menu`, `popover`, `tooltip`, `select`, `hover-card` and `context-menu` are positioned by an inline `transform` that Radix does not round. Combined with the glass `backdrop-filter` (which promotes the surface to its own compositing layer), a fractional offset rasterized every glyph off the device pixel grid. A new `lib/snap-popper.ts` rounds the wrapper's translate to whole pixels and re-applies after each reposition. Same defect the dialogs had; there the fix was to centre without a transform, which a popper can't do because its position *is* the transform. - **Moonstone night read as brown**: at the default opacity every surface (cards, inputs, the nav pill) paints `--glass-opaque-bg`, and moonstone's dark floor derived `oklch(0.8 0.047 75)`, a sand/tobacco tan rather than the pale silvery stone the preset is named for. Lightness alone could not fix it: h75 is a warm hue, so every lightness along it lands somewhere in the tan family; cutting the CHROMA is what moves it off that family. `.dark[data-glass-tint="moonstone"]` now sets `--glass-opaque-l: 84` (from 80: the "slightly lighter" half) plus `--glass-opaque-c-scale: 0.45`, deriving `oklch(0.84 0.021 75)`. Contrast improves rather than regresses: all five static `--foreground-*-opaque` tiers gain **+7.1 Lc** against the lighter floor, and `--foreground-strong-opaque` now clears the APCA body threshold at 75.0. Light mode is untouched (still `L 94` / `c-scale 1`, where the same hue reads as cream). The wash knobs are unchanged, so the `[moonstone-sync]` mirror in `auto-foreground.tsx` needs no update. - **Gradient and canvas backdrops were unreadable at one end**: the ramps in `lib/oklch-utils.ts` deliberately cover their FULL range, which is what makes them useful as swatches; as a full-bleed wallpaper it meant `linear-gradient` ran from pure black (L 0) to pure white (L 100) across the viewport, so a single solved foreground could only be legible at one end. Backdrops now fit their ramp into `readableLightnessBand()`: the lightness range where text over them clears APCA 60, binary-searched against the live `--foreground` (so it tracks AutoForeground, the tint and the mode). Measured: light mode bands to L 83–93, dark to L 7–63. The band stops short of L 0 / L 100 on the high-contrast end, which costs no readability and keeps the tint from washing out to pure black or white at one edge. `rampAxisColors` is untouched, so the docs swatches still show the full range. diff --git a/README.md b/README.md index 58d37379..32e1f51f 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Visit `http://localhost:6006` after starting Storybook. Every component lives in a single tree (`components/ui/[name].tsx`, published under `registry/ui/`) and draws its surface from the **material system** in `lib/material.ts`: - **Five materials** (`glass`, `frosted`, `crystal`, `chakra`, `opaque`) via the `material` prop. Leave it off and the surface is **adaptive**: it follows the page's `data-glass` style; an explicit material pins the element under any page style. -- **Orthogonal axes** (`border`, `veil`, `diffuse`, `gradient`, `glow`, `sheen`) layered on top of any material. Booleans with typed refinements: `border="rim" | "frame"` (2px / 4px), `diffuse="stained"` (dyed glass), `glow="lg"`. +- **Orthogonal axes** (`border`, `veil`, `diffuse`, `gradient`, `glow`, `sheen`) layered on top of any material. Booleans with typed refinements: `border="rim" | "frame"` (2px / 4px), `borderColor="ink"` (the border in the text colour rather than the material's near-white edge, which reads as nothing on light themes), `diffuse="stained"` (dyed glass), `glow="lg"`. - **Hover effects**: `effect="glow | shimmer | ripple | lift | scale"` from `lib/hover-effects.ts`. - Accessibility from Radix UI primitives, fully typed, and themed through CSS tokens. @@ -141,6 +141,8 @@ This floor is derived from what the theme actually uses, not chosen. The binding Tailwind v4's own baseline (Safari 16.4 / Chrome 111 / Firefox 128) sits just under this, so the floor is set by Sistine's CSS rather than by Tailwind. +That `browserslist` belongs to this repository: it is what Sistine's own build targets and what the floor above was measured against. It does not travel with the registry items, so a consuming app is not covered by it. Declare your own `browserslist` at or above this floor; without one, your CSS toolchain falls back to its defaults, which may target engines the theme does not render in. + Below the floor there is **one safety net, not a polyfill**: `app/theme/tokens.css` ends with an `@supports not (color: oklch(…))` block that restores a legible background, foreground and panel fill. Because every colour is an oklch value passed through a custom property, an engine without oklch resolves those properties to invalid at substitution time and computes them to `unset`, so surfaces go transparent rather than falling back. The block prevents that; it does not reproduce the palette. ## 🎨 Customization diff --git a/app/components/page.tsx b/app/components/page.tsx index d0b1f3c0..dd1d0a17 100644 --- a/app/components/page.tsx +++ b/app/components/page.tsx @@ -24,6 +24,13 @@ const newComponents = new Set([ "date-picker-input", "context-menu", "carousel", + "kbd", + "empty", + "progress", + "item", + "field", + "menubar", + "combobox", ]); // Semantic Button variants (behavior styles), shown side-by-side. diff --git a/app/docs/theming/page.tsx b/app/docs/theming/page.tsx index 28c0e0f7..2106b180 100644 --- a/app/docs/theming/page.tsx +++ b/app/docs/theming/page.tsx @@ -47,9 +47,9 @@ export default function ThemingPage() { below).
  • - Axes (orthogonal, so they compose freely on any material): border, veil (a legible floor for menus - / overlays), diffuse (a readability blur floor), gradient (brand wash), glow, sheen (hover - shimmer). Each is a prop on a component, or a glass-* class on a bare element. + Axes (orthogonal, so they compose freely on any material): border (and its colour, borderColor),{" "} + veil (a legible floor for menus / overlays), diffuse (a readability blur floor), gradient (brand wash),{" "} + glow, sheen (hover shimmer). Each is a prop on a component, or a glass-* class on a bare element.
  • The five materials. Pin one with the prop, or on a raw element with the attribute:

    @@ -141,6 +141,12 @@ export default function ThemingPage() { "glass-border / -rim / -frame", "material edge: hairline 1px (0.5px under frosted) · rim 2px · frame 4px", ], + [ + "border colour", + 'borderColor="ink"', + "glass-border-ink", + "draw the border in the text colour (--foreground at --glass-border-ink-a, 0.5) instead of the material's near-white edge, which disappears on light themes; pairs with border", + ], [ "veil", "veil", @@ -279,8 +285,10 @@ export default function ThemingPage() {

    - Crystal's shine has three flavors, set with data-gloss on any ancestor (default / unset = tonal). It composes - with material="crystal" and the data-glass="crystal" page style: + Crystal's shine has three flavors, set with data-gloss on <html> or on any element below it (default / + unset = tonal). A scoped flavor recomposes the gloss for its own subtree, in the page's tint, and a nested{" "} + data-glass-tint keeps the flavor it inherits. It composes with material="crystal",{" "} + material="chakra" and the data-glass="crystal" page style:

    @@ -416,6 +424,17 @@ export default function ThemingPage() { data-material attribute directly:

    {`
    Legible overlay
    `}
    +

    + Wearing the material through @apply instead of the class? The page styles find surfaces by the glass class for the + parts they apply per element (the crystal and chakra text tiers, the opaque page's edge and text tier), and an @apply{" "} + leaves no class behind. Mark the element with data-surface, or data-surface="veil" when it also applies{" "} + glass-veil. Everything else (the stacks, the blur, the solid floor the opaque page swaps in) is inherited and needs no marker. +

    + {`.selection-pill { @apply glass glass-border; } +.menu-panel { @apply glass glass-border glass-veil; } + + +
    …
    `}
    diff --git a/app/globals.css b/app/globals.css index 52cc8a32..631bd3d4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -6,8 +6,9 @@ engine default by SOURCE ORDER at equal specificity), - inside tokens.css, .dark comes after :root (same-element ties resolve by source order). Consumers install a FLATTENED single-file build of this graph (registry/theme/globals.css, generated - by scripts/build-theme.mjs) — run `pnpm registry:check` after editing any partial and commit - registry/theme + public/r. + by scripts/build-theme.mjs) — run `bun run registry:check` after editing any partial and commit + registry/theme + public/r. Site-only CSS (the Geist font mapping) lives in app/site.css, which imports + this file, so nothing the documentation site defines leaks into the shipped theme. */ @import "tailwindcss"; @import "tw-animate-css"; @@ -20,4 +21,3 @@ @import "./theme/styles.css"; @import "./theme/presets.css"; @import "./theme/frescoes.css"; -@import "./theme/sonner.css"; diff --git a/app/layout.tsx b/app/layout.tsx index aa6beea6..ecdb181b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,7 +6,7 @@ import { BackgroundProvider } from "@/components/background-provider"; import { Footer } from "@/components/footer"; import { Header } from "@/components/header"; import { Toaster } from "@/components/ui/sonner"; -import "./globals.css"; +import "./site.css"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -34,8 +34,17 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - + /* The font variables go on , not : site.css maps --font-sans to --font-geist-sans in the + theme layer, which resolves on :root, so a variable defined lower down leaves it unresolved and + the page on the UA's system stack. */ + + diff --git a/app/site.css b/app/site.css new file mode 100644 index 00000000..e2523449 --- /dev/null +++ b/app/site.css @@ -0,0 +1,14 @@ +/* + The documentation site's own stylesheet: the Sistine theme plus what only this site defines. + app/globals.css is the theme a consumer installs (flattened into registry/theme/globals.css), so it + must not reference anything the consumer's app may not have. Geist is loaded by app/layout.tsx via + next/font, which defines --font-geist-sans / --font-geist-mono on ; mapping Tailwind's font + tokens to it belongs here, beside that loader. A consumer without these lines keeps Tailwind's + default stacks, or sets --font-sans / --font-mono in their own @theme. +*/ +@import "./globals.css"; + +@theme inline { + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} diff --git a/app/theme/engine.css b/app/theme/engine.css index b2c14c5a..c6afcee7 100644 --- a/app/theme/engine.css +++ b/app/theme/engine.css @@ -1,5 +1,5 @@ /* ── Glass engine ───────────────────────────────────────────────────────────── - TWO grouped blocks, split by what must re-resolve where (the var-composition-resolves-at-declaration + THREE grouped blocks, split by what must re-resolve where (the var-composition-resolves-at-declaration gotcha — see scripts/check-theme.mjs): A. TINT ANCHORS (:root, [data-glass-tint], …) — the tint knobs plus everything mode-INVARIANT that @@ -13,12 +13,14 @@ scopes. `.dark` IS in this selector so a dark wrapper inside a light page re-composes its surfaces from the wrapper's own dark knobs (same-element cascade) — "toggle .dark anywhere" keeps working. + C. GLOSS (:root, .dark, [data-glass-tint], [data-gloss]) — the crystal gloss and the stacks that bake + it, which also have to recompose wherever a `data-gloss` flavor is set. See block C itself. + Foreground tokens stay on bare :root/.dark (AutoForeground owns them). Preset blocks below override --glass-tint-h/c/a by source order. Shadow composites are mode-TWINNED on bare :root/.dark (their light↔dark ratios are irregular — knobbing them would change values). */ :root, -[data-glass-tint], -[data-sonner-toast][data-type] { +[data-glass-tint] { /* Deliberately restrained glassmorphism — enough to read as glass, never enough to fog what is behind it. */ /* Glass tint — recolor ALL glass by changing these; presets set them via [data-glass-tint]. -h = hue, -c = chroma (gradient), -a = wash alpha. The gradient alone is too sheer @@ -60,8 +62,10 @@ land at the sRGB edge at some hues and get browser-gamut-mapped — prefer the hue tokens + a conservative chroma when that matters. */ /* Harmonic anchor hue — the wheel origin the harmonies rotate from. Normally the content hue, but the two - "hue-less" themes (selenite + moonstone) anchor at 0° instead — set by the tint switcher (selenite) + moonstone's - block below — so their accents come out a colorful red-based harmony, not a warp of a near-grey hue. */ + "hue-less" themes (selenite + moonstone) anchor at 0° instead — each preset block in presets.css declares + it, so a CSS-only consumer gets the anchor too (the tint switcher also inlines it, for a custom tint + dragged to chroma 0) — and their accents come out a colorful red-based harmony, not a warp of a + near-grey hue. */ --harmony-h: var(--glass-fg-h); --hue-base: var(--harmony-h); --hue-complement: calc(var(--harmony-h) + 180); @@ -99,17 +103,9 @@ /* Crystal edge — identical in both modes, so it lives here with the anchors (the crystal floor + shadows are mode-varying: --glass-crystal-bg in the surfaces block below, shadows on :root/.dark). */ --glass-crystal-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / 0.3); - /* Crystal gloss — the 3-part light effect (top highlight + diagonal streak + bottom glow) baked into - --crystal-stack-bg (the crystal material + the [data-glass="crystal"] page style) and layered by - glass-sheen. Each is a full gradient, so it composes in a background-image list AND in pseudo layers. - THREE FLAVORS, selected by data-gloss on any ancestor (default, unset = tonal — see the [data-gloss] - blocks further down): - white → flat white specular (the classic glass shine) - tonal → a tonally-close single-hue tint of the theme (--glass-gloss-l / --glass-gloss-tint) - hue → iridescent: the highlight sweeps hues shifted ± --glass-gloss-hue-span around the tint hue - White/tonal share the gradient SHAPES below (only --glass-gloss-ink, the L C H triple, differs); hue - redefines the three gradients to interpolate across hue. Each layer applies its own alpha. */ - /* --glass-gloss-l / --glass-gloss-tint live in tokens.css (66 / 4.25) — do NOT re-declare here: + /* Crystal gloss KNOBS — leaves only. The gloss itself (ink, the three gradients) and every stack that + bakes it are composed in block C below, which also recomposes at each [data-gloss] scope. + --glass-gloss-l / --glass-gloss-tint live in tokens.css (66 / 4.25) — do NOT re-declare here: engine.css imports AFTER tokens.css, so a duplicate at equal :root specificity silently wins and pure-CSS consumers get a stale gloss. */ --glass-gloss-hue-span: 40; /* hue: degrees the iridescent sweep shifts up & down from the tint hue */ @@ -121,15 +117,6 @@ renders what it asks for. */ --glass-gloss-c: 0.12; --glass-gloss-hue-dir: 1; /* gloss direction (±1) — flips the TONAL streak diagonal (calc angle) + the HUE sweep order (span × dir); toggled in the crystal demo */ - --glass-gloss-ink: calc(var(--glass-gloss-l, 94) * 1%) min(calc(var(--glass-tint-c) * var(--glass-gloss-tint, 2)), var(--glass-gloss-c-max, 0.3)) - var(--glass-tint-h); - --glass-gloss-top: linear-gradient(to bottom, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.4), transparent 30%); - --glass-gloss-streak: linear-gradient( - calc(135deg * var(--glass-gloss-hue-dir, 1)), - oklch(var(--glass-gloss-ink, 100% 0 0) / 0.15), - transparent 50% - ); - --glass-gloss-glow: radial-gradient(circle at 50% 120%, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.2), transparent 70%); --blur-crystal: 2px; } @@ -138,8 +125,7 @@ re-composing with their own knobs. */ :root, .dark, -[data-glass-tint], -[data-sonner-toast][data-type] { +[data-glass-tint] { /* Solid colored floor under the sheer gradient (see --glass-tint-a) — mode via --glass-wash-l/-c-mult. */ --glass-tint-wash: oklch(var(--glass-wash-l) calc(var(--glass-tint-c) * var(--glass-wash-c-mult)) var(--glass-tint-h) / var(--glass-tint-a)); /* Solid accent that tracks the FOREGROUND hue at a fixed vivid chroma — used by active controls @@ -276,18 +262,7 @@ oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) calc(var(--glass-fg-h) + 63.53)) 0%, oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) var(--glass-fg-h)) 100% ); - /* Crystal floor — clear glass; the alpha is the mode dial (--glass-crystal-bg-a). */ - /* Crystal body. The lightness is a KNOB, not 100%, and that is the whole point: at L100 sRGB holds - ZERO chroma, so this surface asked for 0.015 and delivered exactly none — crystal rendered pure - white under every preset, in both modes (measured: #fffdff amethyst, #fffef4 moonstone, - #fbffff lapis; RGB spread 2–11). check-gamut even reported it as "no attainable chroma at all" - and skipped the pair rather than failing it. - --glass-crystal-l is 96 because that is the lowest lightness at which the TIGHTEST of the 21 hues - this theme uses (h268, lapis) can still carry the 0.015 this asks for: ceiling 0.0182, 1.22x - headroom. L96.5 is the exact crossover at 1.06x, too thin given maxChromaFor already sits 3% inside - the boundary to stop Safari clamping toward grey. The cost is small — at the 0.3 light-mode alpha - the composite moves ~1.2 L, and 0.4 L in dark. */ - --glass-crystal-bg: oklch(calc(var(--glass-crystal-l) * 1%) calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / var(--glass-crystal-bg-a)); + /* (The crystal floor, --glass-crystal-bg, is in block C: the hue gloss flavor repaints it.) */ /* ── Chakra — the step-cut gem surface ──────────────────────────────────────────────────────── BODY: the stone itself. The one tint-composing token here, so it is the only one with a gamut budget — min() against --glass-chakra-c-max (not a multiplier) for the same reason --border uses @@ -298,12 +273,7 @@ calc(var(--glass-chakra-l) * 1%) min(var(--glass-tint-c), var(--glass-chakra-c-max)) var(--glass-tint-h) / var(--glass-chakra-a) ); - /* SURFACE STACK: the same gloss triple crystal bakes, so `data-gloss` white / tonal / hue reskin - chakra exactly as they reskin crystal (those blocks remap --glass-gloss-*, which these read), over - a fresco slot and the tint wash. Nothing here is inset, so nothing here has a corner of its own — - the element's border-radius clips the whole stack, which is the point (see the facet bands below). */ - --glass-chakra-stack-bg: - var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer); + /* (The chakra SURFACE STACK, --glass-chakra-stack-bg, is in block C: it bakes the gloss.) */ /* THE CUT — four sides, two steps, as INSET SHADOWS rather than inset background layers. A background layer is always a RECTANGLE: background-size shrinks it but gives it no radius of its own, so an inset gradient paints square corners over a rounded surface — a card with hard-cornered @@ -343,6 +313,120 @@ /* Frosted rides the wash in the IMAGE stack (sheet + wash), leaving the background-color channel free for the veil floor. */ --frosted-stack-bg: var(--glass-frosted-bg), var(--glass-wash-layer); + /* (--crystal-stack-bg is in block C: it bakes the gloss.) */ +} + +/* C. GLOSS — the crystal highlight and everything that bakes it: the ink, the three gradients, the + crystal floor, and the crystal + chakra stacks. Declared on every scope that can CHANGE it: :root and + .dark (the mode knobs), [data-glass-tint] (the tint), and [data-gloss] (the flavor). The last is what + lets `data-gloss` work below : these tokens are composed values, which bake where they are + declared, so a [data-gloss] section that only re-pinned an input (as the flavor blocks used to) had + its children inherit 's gradients and stacks pre-composed, and painted the page's gloss. + THREE FLAVORS, chosen by two plain switches the [data-gloss] blocks in materials.css set to `1` or + `initial` (unset = tonal): + white → flat white specular (the classic glass shine) --glass-gloss-is-white: 1 + tonal → a tonally-close single-hue tint of the theme (--glass-gloss-l / --glass-gloss-tint) + hue → iridescent: the highlight sweeps hues shifted ± --glass-gloss-hue-span around the tint + hue, and repaints the crystal floor in the sweep --glass-gloss-is-hue: 1 + A switch selects nothing by itself: each flavor's variant (block C′ below) multiplies an alpha by it, + so with the switch `initial` the variant is invalid at computed-value time and the var() fallback here, + the tonal shape, applies. The switches are leaves, so they inherit, and a nested [data-gloss] sets both + to pick its own flavor; a nested [data-glass-tint] recomposes from the inherited switches and keeps the + page's flavor (it used to reset it to tonal). Each layer applies its own alpha. */ +:root, +.dark, +[data-glass-tint], +[data-gloss] { + /* The tonal ink as its own token (not inline in the fallback below) so check-gamut's scraper, which + reads `--x: ` declarations, still sweeps it. */ + --glass-gloss-ink-tonal: calc(var(--glass-gloss-l, 94) * 1%) + min(calc(var(--glass-tint-c) * var(--glass-gloss-tint, 2)), var(--glass-gloss-c-max, 0.3)) var(--glass-tint-h); + --glass-gloss-ink: var(--glass-gloss-ink-white, var(--glass-gloss-ink-tonal)); + --glass-gloss-top: var(--glass-gloss-top-hue, linear-gradient(to bottom, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.4), transparent 30%)); + --glass-gloss-streak: var( + --glass-gloss-streak-hue, + linear-gradient(calc(135deg * var(--glass-gloss-hue-dir, 1)), oklch(var(--glass-gloss-ink, 100% 0 0) / 0.15), transparent 50%) + ); + --glass-gloss-glow: var( + --glass-gloss-glow-hue, + radial-gradient(circle at 50% 120%, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.2), transparent 70%) + ); + /* Crystal floor — clear glass; the alpha is the mode dial (--glass-crystal-bg-a). + Crystal body. The lightness is a KNOB, not 100%, and that is the whole point: at L100 sRGB holds + ZERO chroma, so this surface asked for 0.015 and delivered exactly none — crystal rendered pure + white under every preset, in both modes (measured: #fffdff amethyst, #fffef4 moonstone, + #fbffff lapis; RGB spread 2–11). check-gamut even reported it as "no attainable chroma at all" + and skipped the pair rather than failing it. + --glass-crystal-l is 96 because that is the lowest lightness at which the TIGHTEST of the 21 hues + this theme uses (h268, lapis) can still carry the 0.015 this asks for: ceiling 0.0182, 1.22x + headroom. L96.5 is the exact crossover at 1.06x, too thin given maxChromaFor already sits 3% inside + the boundary to stop Safari clamping toward grey. The cost is small — at the 0.3 light-mode alpha + the composite moves ~1.2 L, and 0.4 L in dark. */ + --glass-crystal-bg: var( + --glass-crystal-bg-hue, + oklch(calc(var(--glass-crystal-l) * 1%) calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / var(--glass-crystal-bg-a)) + ); + /* SURFACE STACKS: crystal and chakra bake the same gloss triple, so `data-gloss` white / tonal / hue + reskin both, over a fresco slot and the tint wash. Nothing in the chakra stack is inset, so nothing + has a corner of its own — the element's border-radius clips the whole stack (see the facet bands in + block B). Baked into the background, not ::before/::after: the crystal PAGE style re-skins every + surface, and pseudo layers would need position:relative, which breaks sticky/fixed surfaces. */ --crystal-stack-bg: var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer); + --glass-chakra-stack-bg: + var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer); +} + +/* C′. The FLAVOR VARIANTS block C falls back from, each valid only while its switch is `1`. Kept in a + sibling block (same scopes) so scripts/check-gamut.mjs can model the hue variants at the hue flavor's + own L74 (the [data-gloss="hue"] block pins --glass-gloss-l there) without misreading block C's tonal + ink at it. + hue: each highlight sweeps (tint hue − span) → tint hue → (+ span), interpolated IN OKLCH so the arc + passes through the base hue. Top leans to the low end, glow to the high end, streak spans both. The + floor is replaced by a hue-carrying one at the gloss lightness (so Light deepens it): the default + crystal floor sits near white, where chroma collapses, so the sweep would read over a white sheet. */ +:root, +.dark, +[data-glass-tint], +[data-gloss] { + --glass-gloss-ink-white: calc(var(--glass-gloss-is-white) * 100%) 0 0; + --glass-gloss-top-hue: linear-gradient( + to bottom in oklch, + oklch( + calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) + calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / + calc(0.4 * var(--glass-gloss-is-hue)) + ), + transparent 35% + ); + --glass-gloss-streak-hue: linear-gradient( + 115deg in oklch, + oklch( + calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) + calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / + calc(0.32 * var(--glass-gloss-is-hue)) + ) + 0%, + oklch(calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) var(--glass-tint-h) / calc(0.12 * var(--glass-gloss-is-hue))) 45%, + oklch( + calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) + calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / + calc(0.32 * var(--glass-gloss-is-hue)) + ) + 85%, + transparent + ); + --glass-gloss-glow-hue: radial-gradient( + circle at 50% 120%, + oklch( + calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) + calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / + calc(0.25 * var(--glass-gloss-is-hue)) + ), + transparent 70% + ); + --glass-crystal-bg-hue: oklch( + calc(var(--glass-gloss-l, 94) * 1%) calc(var(--glass-tint-c) * 1.2) var(--glass-tint-h) / + calc(0.4 * var(--glass-gloss-is-hue)) + ); } diff --git a/app/theme/materials.css b/app/theme/materials.css index 1dab80c9..85701b6a 100644 --- a/app/theme/materials.css +++ b/app/theme/materials.css @@ -6,8 +6,8 @@ NOTE these are inherited custom properties — a data-material element also acts as a SCOPED default for adaptive `glass` descendants (the mechanism DialogContent's old data-glass hack hand-rolled). Pin a child back with its own data-material. - Import order: after engine.css (the [data-gloss] blocks must beat the engine's gloss defaults by - source order at equal specificity on ). */ + Import order: after tokens.css — [data-gloss="hue"] re-pins --glass-gloss-l and the crystal shadows, + which tokens.css declares on :root/.dark at equal specificity, so source order decides on . */ [data-material="glass"] { /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify @@ -62,7 +62,7 @@ /* Crystal text tiers — the baked gloss locally LIGHTENS the title zone (worst in dark mode), so text inside crystal remaps to the -crystal set AutoForeground bands against the glossed crystal surface. VEILED crystal is excluded: the veil floor is exactly what the normal tiers are banded for. */ -[data-material="crystal"]:not(.glass-veil) { +[data-material="crystal"]:not(.glass-veil, [data-surface="veil"]) { --foreground: var(--foreground-crystal); --foreground-soft: var(--foreground-soft-crystal); --foreground-strong: var(--foreground-strong-crystal); @@ -99,7 +99,7 @@ /* Chakra text tiers — content sits on the body, whose lightness (--glass-chakra-l) is its own dial and can sit well away from the page's. Same reasoning as opaque/crystal; veiled chakra is excluded because the veil floor is exactly what the NORMAL tiers are banded for. */ -[data-material="chakra"]:not(.glass-veil) { +[data-material="chakra"]:not(.glass-veil, [data-surface="veil"]) { --foreground: var(--foreground-chakra); --foreground-soft: var(--foreground-soft-chakra); --foreground-strong: var(--foreground-strong-chakra); @@ -116,9 +116,10 @@ background-COLOR of the same --glass-opaque-bg at alpha 1 — i.e. C over C, which composites to C for ANY alpha. Measured across tints and both modes it moves every channel by exactly 0. So the layer was one full gradient rasterisation (and one dither pass) for provably zero pixels, on the one material - with no backdrop-filter behind it to mask that noise. Restored below when the surface is VEILED — - there the background-color is the neutral veil floor, not --glass-opaque-bg, so the layer is real - (dropping it there would shift up to 31/255 in light and 42/255 in dark). */ + with no backdrop-filter behind it to mask that noise. Restored when the surface is VEILED (glass-veil + sets --veil-solidify, which `glass` reads first) — there the background-color is the neutral veil + floor, not --glass-opaque-bg, so the layer is real (dropping it there would shift up to 31/255 in + light and 42/255 in dark). */ --srf-solidify: none; /* NO sheet either — opaque now paints a FLAT background-color and nothing else. --glass-bg is a 5-stop gradient whose total range, composited over this floor, is 2–4 sRGB codes across an entire card in @@ -147,26 +148,28 @@ --muted-foreground: var(--muted-foreground-opaque); } -/* Veiled opaque restores the solidify floor: glass-veil moves the background-COLOR channel to the - neutral --veil-floor, so the layer is no longer painting --glass-opaque-bg over itself and carries - the surface's entire tint. Mirrors crystal's `:not(.glass-veil)` carve-out. */ -[data-material="opaque"].glass-veil { - --srf-solidify: initial; -} - /* ── Gloss flavors ──────────────────────────────────────────────────────────── - data-gloss on (or any ancestor) swaps the crystal gloss. Default (unset) = tonal. Token - remaps only — they compose with [data-material="crystal"] AND the [data-glass="crystal"] page - style, in both modes. */ + data-gloss on or ANY element swaps the crystal (and chakra) gloss for that subtree. Default + (unset) = tonal. These blocks set LEAVES only — the two flavor switches, plus the hue flavor's own + lightness and shadows — and engine.css block C recomposes the gloss on every [data-gloss] element from + them. That split is what makes a scoped `
    ` work: a composed value bakes + where it is declared, so a block here that declared one would hand its children 's gloss. + Each block sets BOTH switches, so a nested flavor overrides an ancestor's instead of combining with it. + They compose with [data-material="crystal"] AND the [data-glass="crystal"] page style, in both modes. + Known edge: hue's pinned --glass-gloss-l and bevel-less crystal shadows are mode-twinned leaves that a + white/tonal section nested INSIDE a hue scope inherits (no mode-aware reset exists for them here). */ [data-gloss="white"] { - --glass-gloss-ink: var(--shadow-highlight); /* 100% 0 0 — the classic flat white specular */ + --glass-gloss-is-white: 1; /* the ink becomes the flat white specular, 100% 0 0 */ + --glass-gloss-is-hue: initial; } [data-gloss="tonal"] { - /* Explicit tonal (also the default) — re-asserted so it can override a white/hue set on an ancestor. */ - --glass-gloss-ink: calc(var(--glass-gloss-l, 94) * 1%) min(calc(var(--glass-tint-c) * var(--glass-gloss-tint, 2)), var(--glass-gloss-c-max, 0.3)) - var(--glass-tint-h); + /* Explicit tonal (also the default) — both switches off, so it can override a white/hue ancestor. */ + --glass-gloss-is-white: initial; + --glass-gloss-is-hue: initial; } [data-gloss="hue"] { + --glass-gloss-is-white: initial; + --glass-gloss-is-hue: 1; /* Hue OWNS its lightness rather than sharing the tonal twin. The two flavors want opposite things from the same number: tonal wants the brightest value the surface allows (97 in light) so it reads as a specular, while hue needs chroma headroom — its stops ask --glass-gloss-c 0.16, and the sRGB @@ -180,49 +183,10 @@ ceiling. Measured in WebKit it CLIPS instead (see --glass-gloss-c-max in tokens.css), so 0.16 did not deliver 81% of itself — it drove the tightest hues to a pinned channel. 0.12 sits just under 0.1240, the L-74 ceiling for turquoise (h190) and the tightest of the presets, so the sweep now - renders every hue it asks for. */ + renders every hue it asks for. scripts/check-gamut.mjs reads this 74 to score the variants. */ --glass-gloss-l: 74; - /* Iridescent — each highlight sweeps (tint hue − span) → tint hue → (+ span), interpolated IN OKLCH so the - arc passes through the base hue. Top leans to the low end, glow to the high end, streak spans both. */ - --glass-gloss-top: linear-gradient( - to bottom in oklch, - oklch( - calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) - calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / - 0.4 - ), - transparent 35% - ); - --glass-gloss-streak: linear-gradient( - 115deg in oklch, - oklch( - calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) - calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / - 0.32 - ) - 0%, - oklch(calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) var(--glass-tint-h) / 0.12) 45%, - oklch( - calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) - calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / - 0.32 - ) - 85%, - transparent - ); - --glass-gloss-glow: radial-gradient( - circle at 50% 120%, - oklch( - calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) - calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / - 0.25 - ), - transparent 70% - ); - /* Kill the white: the default crystal floor is oklch(100% …) = PURE white (chroma collapses at L100), and - the crystal shadow carries a white inset bevel. In the iridescent flavor, replace the floor with a - hue-carrying one at the gloss lightness (so Light deepens it) and drop the bevel — leaving only swept hue. */ - --glass-crystal-bg: oklch(calc(var(--glass-gloss-l, 94) * 1%) calc(var(--glass-tint-c) * 1.2) var(--glass-tint-h) / 0.4); + /* Kill the white bevel: the crystal shadow carries a white inset highlight, which would sit on top of + the swept hue. (The white floor is replaced in engine.css block C′.) */ --glass-crystal-shadow: var(--glass-shadow); --glass-crystal-shadow-hover: var(--glass-shadow-lg); } diff --git a/app/theme/presets.css b/app/theme/presets.css index 3300b60e..94abda29 100644 --- a/app/theme/presets.css +++ b/app/theme/presets.css @@ -3,6 +3,15 @@ Most presets just move the tint hue/chroma; "sistine" is a bespoke multi-hue fresco (gilded gold → terracotta flesh tones → azure sky), echoing the Sistine Chapel ceiling. Wash + all tint-derived tokens re-resolve per-scope via the :root,[data-glass-tint] split above. */ +/* Selenite — the neutral stone: no chroma and no wash, so the glass is the plain clear sheet. The tint + values equal the engine default's hue with the colour switched off; the block exists so a CSS-only + consumer (no tint switcher) gets selenite's colour-wheel anchor, which a hue-less theme needs. */ +[data-glass-tint="selenite"] { + --glass-tint-h: 250; + --glass-tint-c: 0; + --glass-tint-a: 0; + --harmony-h: 0; +} [data-glass-tint="sapphire"] { --glass-tint-h: 255; --glass-tint-c: 0.075; @@ -232,33 +241,29 @@ --muted-foreground-chakra: oklch(0.96 0.03 75); } -/* Semantic surfaces (Alert info/success/warning/destructive; destructive Button/Badge) render - data-glass-tint="", and sonner toasts hit the SAME hues via their data-type attribute - (destructive pairs with sonner's "error") — one block per status, joining the grouped engine - re-resolution above so --glass-bg / wash re-resolve at the alert/toast. */ -[data-glass-tint="info"], -[data-sonner-toast][data-type="info"] { +/* Semantic surfaces (Alert info/success/warning/destructive; destructive Button/Badge; status toasts, + which the Toaster stamps from sonner's data-type — see TOAST_TINT in components/ui/sonner.tsx) render + data-glass-tint="" — one block per status, joining the grouped engine re-resolution above so + --glass-bg / wash re-resolve at the alert/toast. */ +[data-glass-tint="info"] { --glass-tint-h: 255; --glass-tint-c: 0.075; --glass-tint-a: 0.18; --glass-wash-l: 63%; } -[data-glass-tint="success"], -[data-sonner-toast][data-type="success"] { +[data-glass-tint="success"] { --glass-tint-h: 150; --glass-tint-c: 0.088; --glass-tint-a: 0.18; --glass-wash-l: 87%; } -[data-glass-tint="warning"], -[data-sonner-toast][data-type="warning"] { +[data-glass-tint="warning"] { --glass-tint-h: 85; --glass-tint-c: 0.063; --glass-tint-a: 0.18; --glass-wash-l: 84%; } -[data-glass-tint="destructive"], -[data-sonner-toast][data-type="error"] { +[data-glass-tint="destructive"] { --glass-tint-h: 25; --glass-tint-c: 0.094; --glass-tint-a: 0.18; diff --git a/app/theme/registrations.css b/app/theme/registrations.css index 929c74f0..d0f2b01f 100644 --- a/app/theme/registrations.css +++ b/app/theme/registrations.css @@ -7,14 +7,35 @@ @custom-variant dark (&:where(.dark, .dark *)); /* Composable, NON-inheriting glow layer: the `glow` effect/prop sets it, and every glass box-shadow - folds in `var(--glow-layer, …)` so the glow rides INSIDE each style's shadow — surviving the - per-style `!important` box-shadow overrides instead of being replaced. inherits:false keeps a - glowing surface from lighting up its glass children. */ + folds in `var(--glow-layer, …)` so the glow rides INSIDE each material's and page style's shadow + (they set --srf-shadow; the glow is appended after it) instead of being replaced by it. inherits:false + keeps a glowing surface from lighting up its glass children. */ @property --glow-layer { syntax: "*"; inherits: false; } +/* The veil floor, NON-inheriting for the same reason as the glow: `glass-veil` sets it on the element + and `glass` paints it as that element's background-color. Inherited, a plain glass surface nested in + a veiled menu, popover, tooltip, select or card painted its PARENT's veil as its own floor (under the + Opaque page style: flat white in light, black in dark). A child without a veil of its own now falls + back to its material's --srf-bg-color, as it would anywhere else. */ +@property --veil-floor { + syntax: "*"; + inherits: false; +} +/* Its twin for the solidify layer (see @utility glass-veil): a veiled parent must not restore the layer + on a child that dropped it. */ +@property --veil-solidify { + syntax: "*"; + inherits: false; +} +/* The ink border colour (glass-border-ink): an inked panel must not ink the borders of the glass inside it. */ +@property --brd-ink { + syntax: "*"; + inherits: false; +} + /* Gradient-text tokens are consumed ONLY from JS inline styles (components/gradient-text.tsx) — no CSS rule references them, so Lightning CSS's optimizer would silently drop them (the same failure mode as the harmonic tokens below). Registered here to pin them. DO NOT REMOVE. */ diff --git a/app/theme/sonner.css b/app/theme/sonner.css deleted file mode 100644 index 99aba936..00000000 --- a/app/theme/sonner.css +++ /dev/null @@ -1,44 +0,0 @@ -/* Sonner Toast Notifications - Glass Effect Overrides */ -[data-sonner-toast] { - /* Glass gradient layered over a mostly-opaque surface so toast text stays legible. - Lower the popover % to make toasts more see-through, raise it to make them more solid. */ - background: var(--glass-bg), color-mix(in oklch, var(--popover) 78%, transparent) !important; - backdrop-filter: blur(var(--blur)) saturate(180%) !important; - border: 1px solid var(--glass-border) !important; - box-shadow: var(--glass-shadow-lg), var(--glow-layer, 0 0 0 0 transparent) !important; - color: var(--foreground) !important; -} - -/* Status toast borders — derived from the toast's own status tint (the merged status blocks above set - --glass-tint-h at the toast) instead of four hardcoded tailwind-500 literals; within ~0.1 L of the - originals on a 1px 30%-alpha border. NOT bare [data-type]: default/loading toasts keep the plain - glass border. */ -[data-sonner-toast][data-type="success"], -[data-sonner-toast][data-type="error"], -[data-sonner-toast][data-type="warning"], -[data-sonner-toast][data-type="info"] { - border-color: oklch(0.7 0.18 var(--glass-tint-h) / 0.3) !important; -} - -/* Toast description text */ -[data-sonner-toast] [data-description] { - color: var(--muted-foreground) !important; -} - -/* Toast action / cancel / close buttons — one glass recipe (the hover fade now covers all three; - previously only the action button transitioned). Cancel dims to muted below. */ -[data-sonner-toast] :is([data-button], [data-cancel], [data-close-button]) { - background: var(--glass-bg) !important; - backdrop-filter: blur(var(--blur-sm)) !important; - border: 1px solid var(--glass-border) !important; - color: var(--foreground) !important; - transition: opacity 0.2s ease !important; -} - -[data-sonner-toast] [data-cancel] { - color: var(--muted-foreground) !important; -} - -[data-sonner-toast] :is([data-button], [data-cancel], [data-close-button]):hover { - opacity: 0.9 !important; -} diff --git a/app/theme/styles.css b/app/theme/styles.css index 802d4a5c..95011de6 100644 --- a/app/theme/styles.css +++ b/app/theme/styles.css @@ -14,15 +14,14 @@ data-material) by remapping the --srf-* tokens it reads. An element-level [data-material] pins its own tokens and beats these by cascade physics — reinforced by the :where() wrapper, which drops every remap (incl. its cross-product legs) to specificity (0,0,0), so a co-located data-material on - a toast or tint-scope element always wins. The cross-product legs keep the swapped stacks - re-resolving inside scoped tints and status toasts (a bare [data-glass] rule would bake 's - tint into descendants' surfaces). Veiled surfaces (glass-veil) keep their element-composed floor. */ -:where( - [data-glass="frosted"], - [data-glass="frosted"] [data-glass-tint], - [data-glass="frosted"][data-glass-tint], - [data-glass="frosted"] [data-sonner-toast][data-type] -) { + a tint-scope element always wins. The cross-product legs keep the swapped stacks re-resolving inside + scoped tints, status toasts included (a bare [data-glass] rule would bake 's tint into + descendants' surfaces). Veiled surfaces (glass-veil) keep their element-composed floor. + The ELEMENT-level rules further down (text tiers, the opaque edge) need to know which elements are + surfaces. `.glass` says so for the utility class; an element that wears the material through + `@apply glass` has no class, so it opts in with a `data-surface` attribute (`data-surface="veil"` + when it also applies glass-veil). Everything ABOVE is inherited tokens and needs no marker. */ +:where([data-glass="frosted"], [data-glass="frosted"] [data-glass-tint], [data-glass="frosted"][data-glass-tint]) { --srf-bg-image: var(--frosted-stack-bg); /* NO --srf-bg-color wash here: the wash already rides the IMAGE stack (--frosted-stack-bg), same as the frosted material — a color-channel copy double-washed tinted presets and stole the veil @@ -32,12 +31,7 @@ --srf-border-w: var(--brd-w, 0.5px); --srf-shadow: var(--glass-frosted-shadow); } -:where( - [data-glass="crystal"], - [data-glass="crystal"] [data-glass-tint], - [data-glass="crystal"][data-glass-tint], - [data-glass="crystal"] [data-sonner-toast][data-type] -) { +:where([data-glass="crystal"], [data-glass="crystal"] [data-glass-tint], [data-glass="crystal"][data-glass-tint]) { /* Gloss rides inside --crystal-stack-bg (baked — sticky/fixed-safe). Shimmer stays the opt-in glass-sheen. No saturate(), matching the material. */ --srf-bg-image: var(--crystal-stack-bg); @@ -48,7 +42,7 @@ } /* Crystal page: ADAPTIVE glass (no explicit material, not veiled) gets the -crystal text tiers, same as the material remap in materials.css — the baked gloss lightens title zones there too. */ -[data-glass="crystal"] :where(.glass):not([data-material]):not(.glass-veil) { +[data-glass="crystal"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface="veil"]) { --foreground: var(--foreground-crystal); --foreground-soft: var(--foreground-soft-crystal); --foreground-strong: var(--foreground-strong-crystal); @@ -57,12 +51,7 @@ } /* Chakra page — every adaptive surface becomes a step-cut gem. The cut rides --srf-shadow, so it follows each surface's own border-radius with nothing to keep in sync. */ -:where( - [data-glass="chakra"], - [data-glass="chakra"] [data-glass-tint], - [data-glass="chakra"][data-glass-tint], - [data-glass="chakra"] [data-sonner-toast][data-type] -) { +:where([data-glass="chakra"], [data-glass="chakra"] [data-glass-tint], [data-glass="chakra"][data-glass-tint]) { --srf-bg-image: var(--glass-chakra-stack-bg); --srf-bg-color: var(--glass-chakra-bg); --srf-filter: blur(max(var(--blur-chakra), var(--dif, 0px))); @@ -71,7 +60,7 @@ } /* Chakra page: adaptive glass (no explicit material, not veiled) takes the -chakra text tiers, same as the material remap in materials.css. */ -[data-glass="chakra"] :where(.glass):not([data-material]):not(.glass-veil) { +[data-glass="chakra"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface="veil"]) { --foreground: var(--foreground-chakra); --foreground-soft: var(--foreground-soft-chakra); --foreground-strong: var(--foreground-strong-chakra); @@ -80,12 +69,7 @@ } /* Opaque — solid tinted panels for adaptive surfaces. --glass-solid-a: 1 makes any veiled overlay (glass-veil) compute a FULLY solid floor here too, so menus/toasts stay legible on an opaque page. */ -:where( - [data-glass="opaque"], - [data-glass="opaque"] [data-glass-tint], - [data-glass="opaque"][data-glass-tint], - [data-glass="opaque"] [data-sonner-toast][data-type] -) { +:where([data-glass="opaque"], [data-glass="opaque"] [data-glass-tint], [data-glass="opaque"][data-glass-tint]) { /* Flat floor, no sheet — same reasoning as [data-material="opaque"] in materials.css: with --srf-filter: none there is no blurred backdrop to mask the dither of a 2–7 code gradient, and the sheet's uniform lift is folded into --glass-opaque-l instead. */ @@ -93,6 +77,12 @@ --srf-bg-color: var(--glass-opaque-bg); --srf-filter: none; --srf-shadow: var(--glass-shadow); + /* Same provable no-op as [data-material="opaque"]: --glass-opaque-bg sits in the background-COLOR + channel, so the solidify layer would paint that colour over itself. A TOKEN here (it used to be an + element rule keyed on `.glass`), so it reaches an `@apply glass` surface too, which kept the light + solidify backing at 70% over the opaque floor and read nearly white. An explicit material pins the + slot back (`initial`), and a veiled surface restores the layer with --veil-solidify. */ + --srf-solidify: none; } /* --glass-solid-a CANNOT ride in the :where() block above. :where() is specificity ZERO by design (so a page style never beats an explicit material), and every other declaration up there survives that @@ -108,37 +98,30 @@ [data-glass="opaque"] { --glass-solid-a: 1; } -/* Opaque page: element-level touches for ADAPTIVE surfaces only (`.glass` with no explicit material — - an explicit frosted/crystal card keeps its translucency AND its normal foreground). Declared ON the - element because (a) --glass-opaque-outline is settable on any ancestor and must resolve where it's - read, and (b) the foreground remap must not leak into explicit materials. */ -[data-glass="opaque"] :where(.glass):not([data-material]):not(.glass-veil) { - /* Same provable no-op as [data-material="opaque"]: this page remap puts --glass-opaque-bg in the - background-COLOR channel, so the solidify layer paints that colour over itself. Element-level and - :not([data-material]) so it cannot leak into an explicit material, :not(.glass-veil) because a - veiled floor is neutral and the layer is real there. */ - --srf-solidify: none; -} -/* The uniform EDGE applies to every adaptive panel, veiled or not — a veiled card under the opaque page +/* Opaque page: element-level touches for ADAPTIVE surfaces only (a `.glass` or `[data-surface]` element + with no explicit material — an explicit frosted/crystal card keeps its translucency AND its normal + foreground). Declared ON the element because (a) --glass-opaque-outline is settable on any ancestor + and must resolve where it's read, and (b) the foreground remap must not leak into explicit materials. + The uniform EDGE applies to every adaptive panel, veiled or not — a veiled card under the opaque page still reads as a solid panel and needs the same outline. */ -[data-glass="opaque"] :where(.glass):not([data-material]) { +[data-glass="opaque"] :where(.glass, [data-surface]):not([data-material]) { --srf-border-color: var(--glass-opaque-outline, var(--glass-border)); --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px)); /* Paint the border HERE, not just the tokens: borderless roles (Card default) never render glass-border, so without this only bordered roles showed the outline — "some cards get it, some don't". Under the opaque page every solid panel takes the uniform edge; bordered elements resolve the same vars, so nothing double-draws. */ - border: var(--srf-border-w) solid var(--srf-border-color); + border: var(--srf-border-w) solid var(--brd-ink, var(--srf-border-color)); } /* The FOREGROUND remap is :not(.glass-veil) — the same carve-out crystal and chakra make, and the same - one the --srf-solidify rule above already makes for this page style. A veiled card does not paint + one --veil-solidify makes for the solidify layer on this page style. A veiled card does not paint --glass-opaque-bg: glass-veil moves the background-COLOR channel to the neutral --veil-floor, so its text sits on the veil surface the BASE tokens are banded for, not on the opaque floor. Remapping it here handed those cards ink solved for a surface they do not have — visible as a hue jump, since the opaque floor is the one surface with nothing composited over it and so sits at the raw --glass-tint-h while every sheer surface lands a few degrees off it (measured on the /colors panel: 22.0 vs 14.6). Splitting the rule is what keeps the edge treatment while dropping the foreground one. */ -[data-glass="opaque"] :where(.glass):not([data-material]):not(.glass-veil) { +[data-glass="opaque"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface="veil"]) { --foreground: var(--foreground-opaque); --foreground-soft: var(--foreground-soft-opaque); --foreground-strong: var(--foreground-strong-opaque); diff --git a/app/theme/tokens.css b/app/theme/tokens.css index ffe48c0b..c6891a72 100644 --- a/app/theme/tokens.css +++ b/app/theme/tokens.css @@ -25,8 +25,6 @@ --color-foreground-soft: var(--foreground-soft); --color-foreground-strong: var(--foreground-strong); --color-foreground-ui: var(--foreground-ui); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -116,6 +114,10 @@ --glass-sheet-a1: 0.04; /* the sheet's TOP-stop offset (dark tapers it to 0.03) */ --glass-frost-boost: 0.1; /* frosted = the sheet lifted by this much */ --glass-border-a: 0.16; /* glass edge alpha — a white rim needs more presence on light backdrops */ + /* Ink border alpha (borderColor="ink" / glass-border-ink): how much of --foreground the line takes. + Mode-invariant because --foreground already flips polarity per mode; 0.5 keeps the line clearly + softer than text while clearing the APCA non-text floor on every preset (check-contrast [ink]). */ + --glass-border-ink-a: 0.5; /* Crystal gloss defaults (the demo's Light/Tint sliders override inline). --glass-gloss-tint is mode-shared; --glass-gloss-l is NOT — see its twin below. AutoForeground's crystal tiers mirror --glass-gloss-l (97 light / 66 dark) as their snapshot-path fallback — keep all three in step. */ @@ -368,9 +370,10 @@ it drops to 76.0 Lc, under the floor+margin, while L93 holds 76.9. */ --accent-bg-l: 0.93; --accent-bg-c-max: 0.032; /* ≤ 0.0323, the L-93 ceiling floor (lapis h268) */ - /* Stays --foreground: this token is text on a SURFACE-lightness background, and it has two callers with - that shape — the accent fill here, and the bg-foreground/10 wash the menus use (dropdown, context, - navigation, command, menubar, select). They only share a text colour while --accent stays near the + /* Stays --foreground: this token is text on a SURFACE-lightness background. Its callers all have that + shape — the ghost button's hover, toggles, and the highlighted row of every menu (dropdown, context, + navigation, command, menu-bar, select), which paint bg-accent so a consumer's --accent reaches them + (the shadcn contract). Text and fill only stay legible together while --accent sits near the surface, which is precisely why the fill was moved by chroma rather than by weight. */ --accent-foreground: var(--foreground); @@ -643,10 +646,12 @@ } /* ── Below the floor ────────────────────────────────────────────────────────── - package.json's browserslist is the supported floor (Safari 17.5 / Chrome 120 / Firefox 128 / Edge - 120). It is set by what this theme actually uses, not by preference — the binding constraints are - text-wrap:balance (Safari 17.5), bare-`&` CSS nesting and unprefixed mask-composite (Chrome 120), - and @property (Firefox 128). Tailwind v4's own baseline sits just under it. + The supported floor is Safari 17.5 / Chrome 120 / Firefox 128 / Edge 120, declared as the browserslist + in SISTINE's package.json. It is set by what this theme actually uses, not by preference — the binding + constraints are text-wrap:balance (Safari 17.5), bare-`&` CSS nesting and unprefixed mask-composite + (Chrome 120), and @property (Firefox 128). Tailwind v4's own baseline sits just under it. That + browserslist does not ship with this file: a consuming app should declare its own, at or above the + floor, or its toolchain targets whatever its defaults are. Every colour in this theme is oklch(), fed through custom properties. On an engine without oklch the custom property still PARSES (custom props accept anything) and only fails when substituted — so @@ -668,6 +673,7 @@ color: #17161c; } .glass, + [data-surface], [data-material] { background-color: #ffffffe6; border: 1px solid #17161c26; diff --git a/app/theme/utilities.css b/app/theme/utilities.css index 7e86bf8d..12d3ae8f 100644 --- a/app/theme/utilities.css +++ b/app/theme/utilities.css @@ -34,9 +34,12 @@ gradient rasteriser (and its dither) to deliver zero pixels. The solidify floor (--glass-opacity dial) is composed HERE, at the element — never in a token context (the var-composition-resolves-at-declaration gotcha; see scripts/check-theme.mjs). The - --srf-solidify slot lets a material DROP that layer when it is provably a no-op; materials that - want the normal floor pin the slot back to `initial` (guaranteed-invalid → the var() fallback - below composes --glass-solidify at the element, exactly as before). */ + --srf-solidify slot lets a material DROP that layer when it is provably a no-op (the opaque material, + and the opaque PAGE style, which sets it on its own scope so it reaches `@apply glass` surfaces that + carry no `.glass` class); materials that want the normal floor pin the slot back to `initial` + (guaranteed-invalid → the var() fallback composes --glass-solidify at the element). --veil-solidify + leads the chain: glass-veil sets it (non-inheriting), because a VEILED surface paints the neutral + veil floor in its background-colour channel, so the layer is real there whatever the slot says. */ @utility glass { /* Fallback 0.7, not 0: every glass surface now sits on a 70% solidify floor unless a consumer dials it back. Measured before changing it, because AutoForeground does NOT read --glass-opacity — it @@ -54,7 +57,8 @@ what --accent was set to. No amount of tuning the token could fix that; it is a layer-order problem. Same no-op `none` slot pattern as --acc-gradient below it. */ background-image: - var(--srf-hover, none), var(--acc-gradient, none), var(--srf-bg-image, var(--glass-stack-bg)), var(--srf-solidify, var(--glass-solidify)); + var(--srf-hover, none), var(--acc-gradient, none), var(--srf-bg-image, var(--glass-stack-bg)), + var(--veil-solidify, var(--srf-solidify, var(--glass-solidify))); background-color: var(--veil-floor, var(--srf-bg-color, transparent)); /* Author ONLY the standard backdrop-filter (no -webkit- twin: Lightning dedupes the pair keeping the PREFIXED one, which Chromium never aliased — the [no-webkit-twin] invariant guards this). @@ -63,10 +67,11 @@ box-shadow: var(--srf-shadow, var(--srf-elev, var(--glass-shadow))), var(--glow-layer, 0 0 0 0 transparent); } -/* Border axis — the material picks the color; width resolves material pin (--srf-border-w, which - itself reads the --brd-w weight slot) → weight slot directly (adaptive surfaces) → 1px hairline. */ +/* Border axis — the material picks the color unless the element asks for ink (--brd-ink, the + borderColor axis below); width resolves material pin (--srf-border-w, which itself reads the --brd-w + weight slot) → weight slot directly (adaptive surfaces) → 1px hairline. */ @utility glass-border { - border: var(--srf-border-w, var(--brd-w, 1px)) solid var(--srf-border-color, var(--glass-border)); + border: var(--srf-border-w, var(--brd-w, 1px)) solid var(--brd-ink, var(--srf-border-color, var(--glass-border))); } /* Border WEIGHTS — token setters for the --brd-w slot (order-safe; same-element composition, like glass-sm's --srf-blur). Hairline is the unmarked default (1px; 0.5px under frosted); rim = 2px, frame = 4px. */ @@ -76,6 +81,15 @@ @utility glass-border-frame { --brd-w: 4px; } +/* Border COLOUR — ink. The material edge is a near-white rim at the material's alpha: it reads as cut + glass on a dark page and as nothing at all on most light themes, at hairline, rim or frame weight + alike, so a weight axis whose colour disappears went unused. Ink is the element's own text colour + (--foreground, after any per-material text remap on this element), mixed toward transparent by + --glass-border-ink-a: a visible line anywhere text is legible. Element-composed and registered + non-inheriting (registrations.css), so a child border keeps its own material colour. */ +@utility glass-border-ink { + --brd-ink: color-mix(in oklch, var(--foreground) calc(var(--glass-border-ink-a, 0.5) * 100%), transparent); +} /* Diffuse axis — a READABILITY BLUR FLOOR (not a blur value): every translucent material computes blur(max(, --dif)), so crystal stays crystal until a text-dense surface opts in. @@ -99,6 +113,9 @@ @utility glass-veil { --veil-floor: oklch(var(--glass-solid-l) 0 0 / var(--glass-solid-a)); + /* The veil keeps the solidify layer even where the material or page dropped it (--srf-solidify: none): + those drops are only no-ops over the opaque floor, and a veiled surface paints the veil instead. */ + --veil-solidify: var(--glass-solidify); } /* THE PAGE BACKDROP LAYER — the fixed, full-viewport ground every sheer material blurs. Put this on the @@ -191,12 +208,18 @@ } } -@keyframes shimmer { - 0% { - transform: translateX(-100%); +/* Namespaced: a bare `shimmer` is a global name, and a consumer's own @keyframes shimmer (or another + library's) replaced this one for everyone, whichever stylesheet loaded last. Animates `translate`, not + `transform`: Tailwind v4's `-translate-x-full` (the skeleton's resting position) writes the `translate` + property, which COMPOSES with an animated transform rather than being replaced by it, so the sweep ran + from -200% to 0% and parked the highlight on top of the skeleton. Animating the same property replaces + the resting value while running, and with the animation off it falls back to that resting value. */ +@keyframes sistine-shimmer { + from { + translate: -100% 0; } - 100% { - transform: translateX(100%); + to { + translate: 100% 0; } } @@ -218,8 +241,8 @@ rule outranks any layered one regardless of order, so this needs neither !important nor a specificity hack. */ @media (prefers-reduced-motion: reduce) { - .animate-\[shimmer_2s_infinite\], - .before\:animate-\[shimmer_2s_infinite\]::before { + .animate-\[sistine-shimmer_2s_infinite\], + .before\:animate-\[sistine-shimmer_2s_infinite\]::before { animation: none; } diff --git a/biome.json b/biome.json index 372f7b8c..1ea25aa3 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.10/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.15/schema.json", "root": true, "vcs": { "enabled": true, diff --git a/bun.lock b/bun.lock index 0bb04528..3f9c3b6f 100644 --- a/bun.lock +++ b/bun.lock @@ -5,52 +5,53 @@ "": { "name": "@weekendsuperhero-io/sistine", "dependencies": { + "@base-ui/react": "^1.9.0", "@phosphor-icons/react": "^2.1.10", - "@tanstack/react-table": "^9.1.2", + "@tanstack/react-table": "^9.3.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.4.0", - "framer-motion": "^13.1.1", + "framer-motion": "^13.5.1", "input-otp": "^1.5.0", - "next": "^16.3.3", + "next": "^16.4.2", "next-themes": "^0.4.6", - "radix-ui": "^1.6.7", - "react": "^19.2.8", - "react-day-picker": "^10.0.1", - "react-dom": "^19.2.8", - "react-easy-crop": "^6.2.3", - "react-resizable-panels": "^4.12.3", + "radix-ui": "^1.8.1", + "react": "^19.3.1", + "react-day-picker": "^10.0.2", + "react-dom": "^19.3.1", + "react-easy-crop": "^6.2.4", + "react-resizable-panels": "^4.14.6", "recharts": "^3.10.1", - "shadcn": "^4.19.0", + "shadcn": "^4.22.1", "sonner": "^2.0.8", - "tailwind-merge": "^3.6.0", + "tailwind-merge": "^3.7.0", "tw-animate-css": "^1.4.0", "vaul": "^1.1.2", - "zod": "^4.4.3", + "zod": "^4.6.7", }, "devDependencies": { - "@biomejs/biome": "^2.5.10", - "@chromatic-com/storybook": "^5.3.0", - "@storybook/addon-a11y": "^10.5.10", - "@storybook/addon-docs": "^10.5.10", - "@storybook/addon-onboarding": "^10.5.10", - "@storybook/addon-vitest": "^10.5.10", - "@storybook/nextjs": "^10.5.10", + "@biomejs/biome": "^2.5.15", + "@chromatic-com/storybook": "^5.5.1", + "@storybook/addon-a11y": "^10.6.2", + "@storybook/addon-docs": "^10.6.2", + "@storybook/addon-onboarding": "^10.6.2", + "@storybook/addon-vitest": "^10.6.2", + "@storybook/nextjs": "^10.6.2", "@tailwindcss/postcss": "^4.3.3", "@testing-library/jest-dom": "^7.0.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.6", - "@types/node": "^24.13.3", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.5", - "@vitejs/plugin-react": "^6.1.0", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.8", + "@types/node": "^24.19.2", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", "apca-w3": "^0.1.9", - "jsdom": "^30.0.1", - "storybook": "^10.5.10", + "jsdom": "^30.1.3", + "storybook": "^10.6.2", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", - "vitest": "^4.1.11", + "vitest": "^4.1.12", }, }, }, @@ -61,11 +62,11 @@ "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.3.0", "", {}, "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA=="], - "@asamuzakjp/css-color": ["@asamuzakjp/css-color@6.0.7", "", { "dependencies": { "@csstools/css-calc": "^3.3.0", "@csstools/css-color-parser": "^4.1.10", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.5.2" } }, "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@7.1.7", "", { "dependencies": { "@csstools/css-calc": "^3.4.3", "@csstools/css-color-parser": "^4.2.6", "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2", "lru-cache": "^11.5.3" } }, "sha512-Q7ioeear07HwixdIitIn9KS1LZi97CEDFCSjPNm9v+y8KNbsu9q4Lrd4zUQW4faED+E7qxvfeVnbNRW46wFKuw=="], - "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@8.3.2", "", { "dependencies": { "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2" } }, "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q=="], + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@10.0.4", "", { "dependencies": { "bidi-js": "^1.1.0", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.3" } }, "sha512-A6f1nKy99I6tyoK82U6kBHvQQZmdHmTmPzJYPY/0eaOW0r8/qPmEOwjGHs+hL/vCgOwQTupu4LT1XtXNUuDruA=="], "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], @@ -81,7 +82,7 @@ "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], - "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="], + "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.10", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.5.3", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-/YY9soQuWa8TURtHXHT+GbklpbAA+dZErxFGHIr6qgDg96xuGWrTAs1+MfjgVJPF0YLQsOVsOYs8lhgsPbOinQ=="], "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.8", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA=="], @@ -111,9 +112,9 @@ "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw=="], - "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + "@babel/helpers": ["@babel/helpers@7.29.10", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.8" } }, "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg=="], - "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + "@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w=="], @@ -247,7 +248,7 @@ "@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A=="], - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg=="], "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA=="], @@ -265,47 +266,51 @@ "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="], - "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + "@babel/runtime": ["@babel/runtime@7.29.10", "", {}, "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA=="], "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], + "@babel/traverse": ["@babel/traverse@7.29.10", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.9", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ=="], "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], - "@biomejs/biome": ["@biomejs/biome@2.5.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.10", "@biomejs/cli-darwin-x64": "2.5.10", "@biomejs/cli-linux-arm64": "2.5.10", "@biomejs/cli-linux-arm64-musl": "2.5.10", "@biomejs/cli-linux-x64": "2.5.10", "@biomejs/cli-linux-x64-musl": "2.5.10", "@biomejs/cli-win32-arm64": "2.5.10", "@biomejs/cli-win32-x64": "2.5.10" }, "bin": { "biome": "bin/biome" } }, "sha512-WRKXARA3kTuiV5sxqTpobJ/I0MVd4vk3pOL6wnp5az4LntFIhWTj1RWZq3DI9PCEN3lXcqy7p5aqUHzvq8AXyQ=="], + "@base-ui/react": ["@base-ui/react@1.9.0", "", { "dependencies": { "@babel/runtime": "^7.29.7 || ^8.0.0", "@base-ui/utils": "0.5.0", "@floating-ui/react-dom": "^2.1.9", "@floating-ui/utils": "^0.2.12", "use-sync-external-store": "^1.7.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-S8CSYosASBKtpL8SWUfS78rFElfLijiCgA7oHOK22jCJ95FULsYyKwBNFOgudt0RA+/7yNc7F81bOvQnRgbBiQ=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ItCrxKK6SXVT6flYs0qIuBd4AA3TTTl4d66Re6YI2FuGZnN85NmuYNzkiTJUyYw8qBLv69L5zTUB6uyWd++h3Q=="], + "@base-ui/utils": ["@base-ui/utils@0.5.0", "", { "dependencies": { "@babel/runtime": "^7.29.7 || ^8.0.0", "@floating-ui/utils": "^0.2.12", "reselect": "^5.3.0", "use-sync-external-store": "^1.7.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-dYGGcEttjkYiMoa5nHm8vLaJ2UcuTWxo3BFAwOYXeRg/q7UVVvA6cacEIXEs3FgBfK1CGtW1ptElqs7wsx+t9A=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-yLsPU9pAmtChXDu8vhKAzErqe+LeeYuwuUB2FZMkRitsmdodxsYRa9KHrFispsUHzzOu+9HB3nP/TQxyia+Sjw=="], + "@biomejs/biome": ["@biomejs/biome@2.5.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.15", "@biomejs/cli-darwin-x64": "2.5.15", "@biomejs/cli-linux-arm64": "2.5.15", "@biomejs/cli-linux-arm64-musl": "2.5.15", "@biomejs/cli-linux-x64": "2.5.15", "@biomejs/cli-linux-x64-musl": "2.5.15", "@biomejs/cli-win32-arm64": "2.5.15", "@biomejs/cli-win32-x64": "2.5.15" }, "bin": { "biome": "bin/biome" } }, "sha512-WZTW4slm/pdkh92K6t/3aEN++44JD1PQ7squ7RCMLI1flHGl43DVOIGXvCjHiZgBMO1V4uYxoFNtqrIA4qGuIQ=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-VG8uQW/86a1roLaIFvtIbEigxIdzdJ190oGyg1tV7VYeQtOS+x10sflk7WbuXgw91EtZX5DlIIIej1YqkNLlcg=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BZVzFhJ/mUvTLMYbc9x6el0o2Uv5zP7bACYyhFb6fSc1giroXm7PL0a5KqMJ9F+/BnkYRjagD/ROSDVOZ6eapg=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-t1QAKZwQJRB4dvgJSgFiQ4BNfNPChg69BNonz854qLVxnjT3UvDzQg9mbkTJRu35ZqU0Rw10A73J8Urgbg2RPw=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-V5Kw63V+fVGNFhFrizDxMbGXAzZCh8kYzJpy3GI6gTapWDg7bZK9oYC9CbVgQSEdTnupz0U5Efz8Ev+5vdBBXw=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-4O6T0eq2heoHZN0a9UX+rWQoxXEBaKf+lRi2hbsGlHneUz9BWXM76nEWMK7Eeq8gzMxR1khQB6BFpAASpeXqGg=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-XaG7P7eeSLYETD3K9grfB0mQmpLQZjygdbsxekWBJAg4am79fGAtRpfRyNnTvXB2fZFNnJZG3nXm7HBat0+VUw=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-pgDDqp9JybHm2I0KRgzN6i4+lt8xu4iqxUwLzglUMmOmyRTU1AYBGKzh9sNMOtIjah7xoWvKHlLVetvyifzoiQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-tGzZUTcJCV7tj5Adh/Gn6nR4MycqA3iohq2LEcrarAgRBkU0h0OKd2UzCRkOsOYOuUGaUJb4YmqRqyy2feGGSw=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-pxAbxduPO4xq/Cvgaa2lOrs9BB0hEXmmDqfMNP4ZOffGOkUrD1/QGw9UAMpFQpX2P8MqTIIRuQKcmetum4Oa6A=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.15", "", { "os": "linux", "cpu": "x64" }, "sha512-xE4iEW/3LqlYj9GFgGrFsFSH51dEEpUbYWBfeOv0q87WkUDxK2o/HhreSb7qMJqck70RVM6Lg96hgedcAYcOkA=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-M+2dgBsl3lXRiTfgPVc2p3anS4Tocojke4rzFLScZ2Y/wmF+36dRb1iHCLiyGqOzQGyTplZH1HnEYviiAqi3nA=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.15", "", { "os": "linux", "cpu": "x64" }, "sha512-IlxUcyxilVGPsE008x13pWdkTbU3nQpP2i9b5UrbOYj6goBkKsRX1XB7yJLcV+O1H9LBchIIm4adOaVLZBX0ZQ=="], - "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-2kPKzhNlm8C+Ru3GcO0Me2ODkCBLrVOUNuyi84RJyWDVKAA4+Kjj3jMjL938lF6BRfvltC4vB2nPY9SvN51Ovg=="], - "@chromatic-com/storybook": ["@chromatic-com/storybook@5.3.0", "", { "dependencies": { "@neoconfetti/react": "^1.0.0", "chromatic": "^18.0.1", "jsonfile": "^6.1.0", "strip-ansi": "^7.1.0" }, "peerDependencies": { "storybook": "^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0" } }, "sha512-gMAfVYJnF/tjdY8C3Q54KYz/low2NFjPIZMpCqSFuMUiTJ2DNWCTb7mFhuuzZ1iduWFDhewlJyilFmvKcEDYRA=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.15", "", { "os": "win32", "cpu": "x64" }, "sha512-yAzh4UqEImV6Hcy0zjUqNfJAsUhoD64FBQCdTG8Md/Z2wQeVP/ZmJ3XRYAs1g8J9fjTFyl2fCtc5FB5til47+g=="], - "@csstools/color-helpers": ["@csstools/color-helpers@6.1.1", "", {}, "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w=="], + "@bramus/specificity": ["@bramus/specificity@2.4.3", "", { "dependencies": { "css-tree": "^3.2.1" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw=="], - "@csstools/css-calc": ["@csstools/css-calc@3.3.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ=="], + "@chromatic-com/storybook": ["@chromatic-com/storybook@5.5.1", "", { "dependencies": { "@neoconfetti/react": "^1.0.0", "chromatic": "^18.0.1", "jsonfile": "^6.1.0", "strip-ansi": "^7.1.0" }, "peerDependencies": { "storybook": "^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0 || ^11.0.0-0" } }, "sha512-DOtUEBIJHmqsOWg+Hi1t5m5FTNlyzL0NpZUwQob+ex30nDvL4p+MrYctmvEqH/QT72mf9mLXbz3Uj8aoWD2OYA=="], - "@csstools/css-color-parser": ["@csstools/css-color-parser@4.2.1", "", { "dependencies": { "@csstools/color-helpers": "^6.1.1", "@csstools/css-calc": "^3.3.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-YpAJZhaHplYQkG8ib+/Fx5Y0eF2lVWi3tIvMJA6i39TLyUNp2439cifzW8VMjhlqrBjHzK5hVGugRRm2zTKI/A=="], + "@csstools/color-helpers": ["@csstools/color-helpers@6.1.2", "", {}, "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow=="], - "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + "@csstools/css-calc": ["@csstools/css-calc@3.4.3", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA=="], - "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.9", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A=="], + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.2.6", "", { "dependencies": { "@csstools/color-helpers": "^6.1.2", "@csstools/css-calc": "^3.4.3" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg=="], - "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.2", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.2" } }, "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.15", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.2", "", {}, "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw=="], "@date-fns/tz": ["@date-fns/tz@1.5.0", "", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="], @@ -371,7 +376,7 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], - "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="], + "@exodus/bytes": ["@exodus/bytes@1.16.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA=="], "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], @@ -381,61 +386,61 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], - "@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], + "@hono/node-server": ["@hono/node-server@2.1.5", "", { "peerDependencies": { "hono": "^4 || ^5.0.0-0" } }, "sha512-MgP/I06GdvsilDspMk/qmRzpk8LOUStP6jFAbSRi40W09DhoyUn+Mj7eyKICPqeYKybOjVFLVIAQzvu1bfIa7Q=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.4" }, "os": "darwin", "cpu": "x64" }, "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw=="], - "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="], + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "os": "freebsd" }, "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.4", "", { "os": "linux", "cpu": "arm" }, "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A=="], - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ=="], - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.4", "", { "os": "linux", "cpu": "none" }, "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.4" }, "os": "linux", "cpu": "arm" }, "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ=="], - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA=="], - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.4" }, "os": "linux", "cpu": "none" }, "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.4" }, "os": "linux", "cpu": "s390x" }, "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.5", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ=="], - "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="], + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "cpu": "none" }, "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA=="], - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="], + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="], + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -445,35 +450,35 @@ "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.32.1", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA=="], "@neoconfetti/react": ["@neoconfetti/react@1.0.0", "", {}, "sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A=="], - "@next/env": ["@next/env@16.3.3", "", {}, "sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g=="], + "@next/env": ["@next/env@16.4.2", "", {}, "sha512-nTUTFH7G5/J/fmpkjVLegOkqyKOP0CEsJcMsyO4UCYeN1su+YMW/MfaSErnJskL7XM+UDK6aTvslxsRCyya/sA=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.4.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ONkhFvZNFWlsEHOM/81AtxdyuJP2KRYKYR0JxPwvcu1AuPMMMgcL+wW2luldJvMtZwKtUxoSkCychW5wIeoYSw=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.4.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-luPCMbOsCoaF+kXJEUjmjmOZg1lC3BflhmUu+ZmW4tBu2vW/4AXak8JBFvePizGkxVmNiAw/r9ra+6UviCWtWQ=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-xhwFTAncJyaCNy5KqKKuKaIIdCy1/1ZV1s5S1G0hhT7wGMkDTlfJ6aQbfRFnIG/s3/kDrxQ8aycVe+c5QnP8ow=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-T1PNmfbPcxr5ndtWUL/3wQtLXsz808lq6sc2GrNT5k2oquF7UyqZddHSuAewHVsWsGXe6nsl9HoN1YNriVjuSg=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-N9cDUOCGtkJG5PNKDFZUF3xXndrJJcpLp121uAGjIGvO/U2p9tfdNOM9htj7+5f72l0m8rMw/cvYacUJBQoudg=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-yY2uh1YhX6ufuYy2FCbw1dqblbhI57GLYAJfCkqD9auTv6pLrH67Uccjc3QMKBeCYAG6lkgu08ExmX1bbjejjQ=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.4.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-j/ArWRIHpMMB6wYLMhr2puTguBt70lNU/Cmjl9p6dTDE8DGknYJMP9PdwPk2dqpCNL/KIX9yfngDvIesxJPmsg=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.4.2", "", { "os": "win32", "cpu": "x64" }, "sha512-AA4edyWHDko3NCSo0x6GomsdgFb0oR1J3MXU1b5xt6FRFg4AucrMPW3S0hCe/9onU0O1BbpGQBsqo1r0Gneu1g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -567,109 +572,109 @@ "@radix-ui/number": ["@radix-ui/number@1.1.3", "", {}, "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.2.0", "", {}, "sha512-KABY+VMIRPJL+Vyzv/335FEhXXD/xDv8BMb1kl48/aXJZMH0LtHCTx5ga5RnxaEP3nZHtmTLf1P9vanmFX46bw=="], - "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.15", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WTQwcAvQf5sOcuUyi90lKPbhwcvQ+j55cjrSmeaN+L2vKU3DooOvlKw2MDeiJ5IkV5N905KW0/fGojKOBhD11A=="], + "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.2.0", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.13" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-BB4+HHkFl3HipEAhm/mLhrpXV+2kRX7MvY4AaImUcQVeWQt5KhV1JEByNs8NGfJOcr95RcQGijt9+OrCo68xGA=="], - "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.20", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collapsible": "1.1.20", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg=="], + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.3.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-collapsible": "1.2.1", "@radix-ui/react-collection": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-controllable-state": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lCZ6sbGS/7WO0j84yx4srIgOf0MRJ/sZ13RonADXxvKBym8xrxNCq5f6DibGcr38sIlwPk8/52uYdXeOPsbEDw=="], - "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dialog": "1.1.23", "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA=="], + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-dialog": "1.3.1", "@radix-ui/react-primitive": "2.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MWC4N7RY+qsbNrFNo7yQ9ZbdoZEW+B5DtpWxJfUnVCqcQxxqqg3qis3RMPTDbCNaTIdHJSn5XDkl9ZdVWXknLg=="], - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.17", "", { "dependencies": { "@radix-ui/react-primitive": "2.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-67jjcVvxjXCCStnTW3Z+M7jmaFMuMR7M2JgpDzH4w7JSRrikmzhjfYgP/09ECb8n7whnsgh1ifJ+Ie96qEj+pQ=="], - "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-fy+dyVR+90nelK8rqIznFlxzx7uPcGbhxH8Nfr2bHb4UfSe+e3hklOC0luK0hDwVwnRX7xTRySpsrQVeW+/oNQ=="], + "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.2.0", "", { "dependencies": { "@radix-ui/react-primitive": "2.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gHEU+Lx8DlT/FYIjZQvvBJrFSwUngXTjh7FeL9QvJaNALDLez6pgDj2axCUihBjtPBy82LN0gNb25+sTcSXNmA=="], - "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA=="], + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.3.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-callback-ref": "1.1.5", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ssZOkfQNkFpYVmCPQlr978xSaU7moSrGXdwg4j2Xs2WlGl/1KsSZtfpK+WnLnx0Rwll1BAh6oUrAy/uR31SBvQ=="], - "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ=="], + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.4.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-size": "1.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-KWtwhh+98ML3seZ9vO9yRY2OwaQG90dCiDAxjpSNY2AaZ3Hik3B0dsk6m0ILywo/no3A9/SozadyPMNweVT8yw=="], - "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.20", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-fSzLb/qy1wuZSxQBcxpW0hNXYHDMWiIXDGyJGdA4+FSLknOWZsNRrTCUavxb4PI8b8lvHo/CysO6NVi5zLpo5g=="], - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.15", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-slot": "1.5.0", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+yiQzqcc7Sy7/5s0jrMCjpPRFB3538VNrXPWdTLZhE/SCfgEws4aI6lHWN/vvcnyhHndROWBzqmqB4QBrITZ9Q=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], - "@radix-ui/react-context": ["@radix-ui/react-context@1.2.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA=="], + "@radix-ui/react-context": ["@radix-ui/react-context@1.3.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-v5AYEocMISkbJ+1fTXzZpiFv7NRr3D2WiYbLIKf+FkmJ8VkP6d503eV2n34MFussHXY1pcoq3yfJiQH77dc4Mg=="], - "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.3.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-menu": "2.1.24", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew=="], + "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.4.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-menu": "2.2.1", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-controllable-state": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Pddn3Z8lio3/ChxyXaEP3jHTTo1LTHCWwF8+mG1chQlWijP+ttVHBKwezrQTiV6Mj670ojvumqvuGh+mdv33iQ=="], - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.3.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-dismissable-layer": "1.2.1", "@radix-ui/react-focus-guards": "1.2.0", "@radix-ui/react-focus-scope": "1.3.1", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-portal": "1.2.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-slot": "1.5.0", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+0E4qvVTwjAWra9sEhJ+9BTsvHE00zkUuNw1VGh7S+Gc13dfYk+S1PVQdchWDj+XYHhAai7Z7IpzXVDCGsDplg=="], - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FtRA52a/r+LKStFT+neLUm+8vSe7wbzkbQGGeU8ixFiwC8NU14Evoq9ATECqL3eGbcUzJR4fyfA4KkK/q6dfqg=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-callback-ref": "1.1.5", "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-KIfDcAsx+oVylFZ4xryVKusJUrLLhTQmfnJW4kwk4Km5myvhHCrj04hnWWUC166+xD4DHdH8tQGylwYX1h6+Pg=="], - "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.24", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-menu": "2.1.24", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g=="], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-menu": "2.2.1", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-controllable-state": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XsHcmYG2AcqnvTqrOXXWcMrhquEIlqucizuDH1MV/qMRW+DndpBCMvNeTRXOxqmG1H5j89M/+I0/34Zdd+zDcQ=="], - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.6", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MPm5R0iRGUDrDnPNxmLBkv+X7pN4XKzt+MbWJEuJQsH3NFH4omrbpXpucyYCCKTSt+na/ZxGhEWQrWTZ0285cA=="], - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.16", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ=="], + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.3.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-callback-ref": "1.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ugeMhbXt4y5S2T1GnIqbWDKEN962PDwnX1fN0eKOOgf5f63Dh2mn8F1FUySlgW0iF6GiVsRF32DzuDIWTUyQJg=="], - "@radix-ui/react-form": ["@radix-ui/react-form@0.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-label": "2.1.15", "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Q4TLEn2A7TAypxwmd6R9EwrlXDvkfYSDMrq9/887AXAGh+G1rH+kYJKSTv+Si9Y0JPKTwKYv6PviAJosysNimA=="], + "@radix-ui/react-form": ["@radix-ui/react-form@0.3.0", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-label": "2.1.17", "@radix-ui/react-primitive": "2.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-KnmrCM27m7Jo1R8S6cFDPrEeq20Jfn5dqde0SZrJr2lVXlj6/QZSUCyFoYh1d8ti0uUrMNK5Lx6dUDuINT+f4Q=="], - "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ=="], + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-dismissable-layer": "1.2.1", "@radix-ui/react-focus-scope": "1.3.1", "@radix-ui/react-popper": "1.4.0", "@radix-ui/react-portal": "1.2.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-controllable-state": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4QooHxgmpCKRjj1Pft+hETKidYcrbPKcVwMGtk2Oa5DcWY2IH1fHuQt9LG6q5hUQBBV2MhQxSvtx6B/C1xfiTw=="], "@radix-ui/react-id": ["@radix-ui/react-id@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA=="], - "@radix-ui/react-label": ["@radix-ui/react-label@2.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.17", "", { "dependencies": { "@radix-ui/react-primitive": "2.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xmj7MN/drx1oUAXkfiFIvyDmGNUIN/omGobvA5oAoNqu9zO2MkISYGZOkBcqSzs/UZXXwV+/3ACRE7q7mGcmkA=="], - "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.24", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-callback-ref": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA=="], + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-collection": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-dismissable-layer": "1.2.1", "@radix-ui/react-focus-guards": "1.2.0", "@radix-ui/react-focus-scope": "1.3.1", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.4.0", "@radix-ui/react-portal": "1.2.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-roving-focus": "1.2.1", "@radix-ui/react-slot": "1.5.0", "@radix-ui/react-use-callback-ref": "1.1.5", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G34BsU90SBj5foYpz8pQqq/Q0Dlfyyl6KitWEfRwXC0QvfwhWNAwS49DWP2+rRJ0iwv20Z2Qx7Sk+T03bGo5JA=="], - "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.24", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-menu": "2.1.24", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA=="], + "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-collection": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-menu": "2.2.1", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-roving-focus": "1.2.1", "@radix-ui/react-use-controllable-state": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bRTYtS9JITV2dUYqYZb7+N5SooQBkZrNRejfmyrSKe3Y57aXhKgx//kBUP0kY7pEpsWR/U1AHsAJRzjwCrK3PA=="], - "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.22", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-previous": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg=="], + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.4.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-collection": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-dismissable-layer": "1.2.1", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-slot": "1.5.0", "@radix-ui/react-use-callback-ref": "1.1.5", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-previous": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.13" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XPPZjkeyCSMNNvt0FyDv/PUauG7Ow4GwHQ/XZSwW36F/TS7rR4HFTsXH8ewLnXTOLn3x96/pmTPNQIVoTX/86g=="], - "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.16", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Tj9P6ntAJEw52oq/F0AGknXR4XncxEt7XU47O3xJQOiWfLzEy3d9gtgKfvjSzGxzHkfL+VzvxGu2KTFsloJqXw=="], + "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.2.1", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.2.0", "@radix-ui/react-collection": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-roving-focus": "1.2.1", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-KtHdcgnaAK6rohQ/MMJoGYcbgSRJsIuGBdHrFeTB1/9SaKlDs14oSOniW6BL6M43HqvgCO3u4WuVdFLkuc6sqw=="], - "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-is-hydrated": "0.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4gvFnmDXu3dgj21CqsufzIameRvlRd4SBqaWhcrlrNhRo0Y5i/49AmRJYe1fdAM3G2VNBbmin4b0D6cdQocwgw=="], + "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-is-hydrated": "0.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vu4+ZoJAaFjdI7l1lDSZRs+4dEv83Jmu7lyzajPrAeQzX74y84n0rUNWbWSgN9XB5NE8IY86Z02kGH6oRrzR+w=="], - "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ=="], + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.3.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-dismissable-layer": "1.2.1", "@radix-ui/react-focus-guards": "1.2.0", "@radix-ui/react-focus-scope": "1.3.1", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.4.0", "@radix-ui/react-portal": "1.2.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-slot": "1.5.0", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZNxvXRx5W2Wos4fXCXb5lpdqN0Z5chf+mStOUOTr4ik7d+62MRvoe9rzCA/eGNg36lxXqSrKEKmwuZEcUrIstw=="], - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-rect": "1.1.4", "@radix-ui/react-use-size": "1.1.4", "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.4.0", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.17", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-callback-ref": "1.1.5", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-rect": "1.1.4", "@radix-ui/react-use-size": "1.1.5", "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yCWRvLeGv9Pz3+efwHqGPp70mZ56r3kfIgQVF56Oj2aAa4uP/Tb+IE59wNcc+758rctqbCKx4LbK8II/r7Bn0Q=="], - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.17", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.2.0", "", { "dependencies": { "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-StX7xXnaRU41VvFtMm9zlhVGz8XF6GmWWTXftkQaY+tqVkbs/R9UmAp5mLzHHapk5hM9RMZQvO/3usyIP/g42g=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.10", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.11", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.2.0", "", { "dependencies": { "@radix-ui/react-slot": "1.5.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HCTSPMNqv55Qk0IX4geJbpyXazmqulcimwuyjxTkKi29v+QZbG2Zho9BnbBq4YS2PF+KlNb2CXoLz66uUmblkg=="], - "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.16", "", { "dependencies": { "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg=="], + "@radix-ui/react-progress": ["@radix-ui/react-progress@1.2.1", "", { "dependencies": { "@radix-ui/react-context": "1.3.0", "@radix-ui/react-primitive": "2.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yQBiM6J3YM8Ums1orkEFpbznK47geKxa0igikzcaS1xHgdba3DdAQBsGbCTt5BVfR8zhEvgxQ0poGcINaD3lvw=="], - "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.4.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ=="], + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.5.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-roving-focus": "1.2.1", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-size": "1.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QbyVT1ddnjqB9H+CUYKkNWdU9fIK3QHCYtjf7mS6ekSqiuXkWtWn5UZW9mhA8qtl3vrVyIsmR9gePstAtKrqkw=="], - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-collection": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-callback-ref": "1.1.5", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UwraORzVUeEPMclhO1p0vz0qI+TbdDLO9i+FiOqhJyHKwdpCRCck9yVEscUnAMIzCiuNoQa8D4A9bIpOPXGt6Q=="], - "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.18", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA=="], + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.4.0", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-slot": "1.5.0", "@radix-ui/react-use-callback-ref": "1.1.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-CFrLqHNejWq5FhKAcj9V/WPGRsol76HBQto81NdOBVFEOqIEkJkKIJt0Z824A/9hHLCtoiR/Kw3euRgzXP2pxg=="], - "@radix-ui/react-select": ["@radix-ui/react-select@2.3.7", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-previous": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg=="], + "@radix-ui/react-select": ["@radix-ui/react-select@2.4.1", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.2.0", "@radix-ui/react-collection": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-dismissable-layer": "1.2.1", "@radix-ui/react-focus-guards": "1.2.0", "@radix-ui/react-focus-scope": "1.3.1", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.4.0", "@radix-ui/react-portal": "1.2.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-slot": "1.5.0", "@radix-ui/react-use-callback-ref": "1.1.5", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.13", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-888ZwW0Md6GyptuqYABRX9nLKgoYfcooCyBBvVaX5Jp3e+8Jp2i4Zo/Uy50Hm+Ysn2JfloQXstW4U36w8teVtg=="], - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.2.0", "", { "dependencies": { "@radix-ui/react-primitive": "2.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1jUAap0wZEqVP05Ou7kJgRxe/W+Qftng+rUNWGa0qkKbv9r5pWTG3E7a3ELKo2eJoHKMECnzuYrZpSyO1YgcYQ=="], - "@radix-ui/react-slider": ["@radix-ui/react-slider@1.4.7", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-previous": "1.1.4", "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg=="], + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.6.1", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.2.0", "@radix-ui/react-collection": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-size": "1.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IRNRR90moPqxfwCJ0XbMJIxo70W0/1ZExT/sNCxEHMaPx0SF9HoQrcPEC75+H28cFKa0b+KH63anfLYJmwKacw=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.5.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-tyYd9ddPXAysAbdbD9eYxgwpCSv6f3PncqVSNKQFSR5lcJVLz3cb/pANDoYrLiHBpXv7skortyUgGUWsYuPPFQ=="], - "@radix-ui/react-switch": ["@radix-ui/react-switch@1.3.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw=="], + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.4.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-size": "1.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NpBGwc40rY02ZmSvuyeKk0Bvc8ZsNeTMi4qHvJ+ZH4EiYCv0zIXEgdCW4bVdzgIUeP9xxbLJSxKOUfqITYNmkA=="], - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.21", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog=="], + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-roving-focus": "1.2.1", "@radix-ui/react-use-controllable-state": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-hRoJlode+hg5ZNlR+3tkfkUliIybdTpD5lkAvHQ9yaXseGf7XVO9Ib2GPEIOuaJJbo7YWYbNpDDRfo8Dhf6/xA=="], - "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg=="], + "@radix-ui/react-toast": ["@radix-ui/react-toast@1.3.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-collection": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-dismissable-layer": "1.2.1", "@radix-ui/react-portal": "1.2.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-callback-ref": "1.1.5", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.13" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-iQvL0J7XK4HtJNGkkWMyYrboNseI7vtotFeUkIno8YNxVA9oM7B6//fh0Mc6TSKI96+zioLWi98lQLso88yJmQ=="], - "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.18", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug=="], + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-use-controllable-state": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+3MUq+WyiBplXYqQ0EIYlq8R4sLQ0T5NbOK7DMdDLCKO93pe2uE0DUJTWWfTBprpJJXxfoF3eMW0xpakVZrYsg=="], - "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-toggle": "1.1.18", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA=="], + "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-roving-focus": "1.2.1", "@radix-ui/react-toggle": "1.2.1", "@radix-ui/react-use-controllable-state": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8Ldx5ao4p4KoW+DO8/PmkOe5mdKNi3F2SseqjVj88wvDGDHe60cUmO0Zm/uHsKrvNgQGV8Efpuh95Tht/h4vw=="], - "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-separator": "1.1.15", "@radix-ui/react-toggle-group": "1.1.19" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw=="], + "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.2.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-roving-focus": "1.2.1", "@radix-ui/react-separator": "1.2.0", "@radix-ui/react-toggle-group": "1.2.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Cm8m7JTEeUdbfA6T9xgyu8HfOpyvSx8pB/wj0DlEuWaJe9FilIs4Az9wci/lU9RbthV/UkltszMw5WnnD+m/Ug=="], - "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.4.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-dismissable-layer": "1.2.1", "@radix-ui/react-focus-scope": "1.3.1", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.4.0", "@radix-ui/react-portal": "1.2.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-slot": "1.5.0", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.13" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NxLFGSFtZ78zVfcoOhTwR1xwqo5qx1Jh8Pw6FMZ9McCaqfRzz7/gIRahs2Oy5XlPgQ+8QkUcKbN5B0U44J/kCg=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y5GBf4NKeADoXI1geiMlMuMoURT3k9MizzzKhGb/m55H94aGigZ01PIpkvsO6lML13BA6XYAXSJ6gza7vTn5nw=="], - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.3.0", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TAcBI7bw2ljYCbv4+zuTSd6zICu8pRt6SDN2vJ/Z06K2MXh6I9WYFe/A9ovlFDtR5HVHGm5iDxDoABm/++dbKg=="], "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="], - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.5", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ge3ipobwSXTj4JyVtswQ7qZj0ZHdtbGuOno/LrgAAeSxtsJ6Vs4Gz5IkPH2bmqpjcLUFoqGhA/mueuIf63UXlA=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.6", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-9cRullnQRv+kqnRmFlmh0WC3kAPuGmHiQ9VwsXdrYk//sBhgCKEttcpPEPgIQbyx6TGUmooVEHYTyohwz/lSJg=="], "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw=="], @@ -679,81 +684,81 @@ "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.4", "", { "dependencies": { "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ=="], - "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw=="], + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-V1McDQkMGzgivwqdu7u96APs7oi0Jvj9LTzgq3WWrDZqxWdAqU9yZnZQqk5OizGFR8badUQwZsrlao7MOlWYOA=="], - "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ=="], + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-feijiN1oPk4rck4u9Dm9tsU4mT6Wp9oeEEhaz1WUEs1VuqLf8xzsFG+Qk+uWq6TnMH9O+WRECRg0NftyP9E4Fw=="], "@radix-ui/rect": ["@radix-ui/rect@1.1.3", "", {}, "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw=="], - "@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="], + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.13.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-f6jwVRfwW30+tnllz4YcsHHpPYYOE7abcHkyUCovD+P7DyhdAI5prz2Fh/8TLhakjpN+7aIEion1s5L/tvlYlA=="], - "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.5", "", { "os": "android", "cpu": "arm" }, "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA=="], + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.14", "", { "os": "android", "cpu": "arm" }, "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.5", "", { "os": "android", "cpu": "arm64" }, "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.14", "", { "os": "android", "cpu": "arm64" }, "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.5", "", { "os": "linux", "cpu": "arm" }, "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.14", "", { "os": "linux", "cpu": "arm" }, "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.14", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.14", "", { "os": "linux", "cpu": "s390x" }, "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.14", "", { "os": "linux", "cpu": "x64" }, "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.14", "", { "os": "linux", "cpu": "x64" }, "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.5", "", { "os": "none", "cpu": "arm64" }, "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.14", "", { "os": "none", "cpu": "arm64" }, "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.14", "", { "os": "win32", "cpu": "x64" }, "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + "@shadcn/registry": ["@shadcn/registry@0.2.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "cn": "^0.2.4", "code-block-writer": "^13.0.3", "deepmerge": "^4.3.1", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "kleur": "^4.1.5", "ora": "^8.2.0", "postcss": "^8.5.23", "prompts": "^2.4.2", "recast": "^0.23.11", "socks": "^2.8.8", "stringify-object": "^5.0.0", "tsconfig-paths": "^4.2.0", "type-fest": "^4.41.0", "undici": "^7.29.1", "zod": "^4.2.0" } }, "sha512-m3TeqU/+av2kgOLfESHkWzsmhuQuY4NXZd5Y1Rd4lzy4IX3JJ5+k2mYfkq6OT8oMuk2W4GQPAkO7+CC7KlpSbw=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@storybook/addon-a11y": ["@storybook/addon-a11y@10.5.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.5.10" } }, "sha512-RpRQV5xUbrl6hCiNrd5FSMIo6pnRZ0VZxWvEW/ASLcreGkKUW5jl2AeLCe5YROE2i80s/dU+6VPzOYKrwWNFbQ=="], - - "@storybook/addon-docs": ["@storybook/addon-docs@10.5.10", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.5.10", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.5.10", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.10" }, "optionalPeers": ["@types/react"] }, "sha512-06JoK3/a7FWI/6GzuidJP9iHp1/Vejboe6lzS1jW+d8ItpecriBt+oXh1VNmUM7i7PjI6pZnet+j51QnLyeOoQ=="], + "@storybook/addon-a11y": ["@storybook/addon-a11y@10.6.2", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.6.2" } }, "sha512-zleS+btwrTRcUf3opbZntKsEt6PTqAtxsek08tx5jh5fRvd6wWmtnwW3uaBcypfIKnheTB3KGljJwFX+Mc+Umg=="], - "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.5.10", "", { "peerDependencies": { "storybook": "^10.5.10" } }, "sha512-Qm1JyjM1wOabeZJykSuyT8mSyZE0/WdjT5wLjOnelE31nCWCh4DsBOgj8A1QDRt+3Hijq9/AW00olowJtf+eYA=="], + "@storybook/addon-docs": ["@storybook/addon-docs@10.6.2", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.6.2", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0", "unplugin": "^2.3.5" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.2" }, "optionalPeers": ["@types/react"] }, "sha512-f7Rv/rzX1sk4lUDTeoIRkXWcWvNwLv7vBnfpFGb3f88MliB+M4ZekGKNiKtrmi44uLFt52gzwwPzj2eGDu0DQQ=="], - "@storybook/addon-vitest": ["@storybook/addon-vitest@10.5.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", "storybook": "^10.5.10", "vitest": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-JNQ9DSkLfxC8qqytBCej91zBExIZ7z97B410U2zgfQPki4HkI9Ffz97a15f5yhVZ79ppIrZ/ssI+WcyWn0ykXQ=="], + "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.6.2", "", { "peerDependencies": { "storybook": "^10.6.2" } }, "sha512-3X+PD7MB/wHX8TkTfkkmKtYeVsrxwPZY3T0GkedJ5N8RQfdr5A3QrhTBiAnk1L1K1WdpAjdFxxmYivtvq7s8ig=="], - "@storybook/builder-webpack5": ["@storybook/builder-webpack5@10.5.10", "", { "dependencies": { "@storybook/core-webpack": "10.5.10", "case-sensitive-paths-webpack-plugin": "^2.4.0", "cjs-module-lexer": "^1.2.3", "css-loader": "^7.1.2", "es-module-lexer": "^1.5.0", "fork-ts-checker-webpack-plugin": "^9.1.0", "html-webpack-plugin": "^5.5.0", "magic-string": "^0.30.5", "semver": "^7.7.3", "style-loader": "^4.0.0", "terser-webpack-plugin": "^5.3.17", "ts-dedent": "^2.0.0", "webpack": "5", "webpack-dev-middleware": "^6.1.2", "webpack-hot-middleware": "^2.25.1", "webpack-virtual-modules": "^0.6.0" }, "peerDependencies": { "storybook": "^10.5.10", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-9ZBRusBwZStoCj3Wm4wVpSpKV8OlLMpadEQ+vvOVrtDQdVJWM5xdLQpqrG21r/cJC4ZX53ZpbrDL+NMsIp1Rkw=="], + "@storybook/addon-vitest": ["@storybook/addon-vitest@10.6.2", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0 || ^5.0.0", "@vitest/browser-playwright": "^4.0.0 || ^5.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0 || ^5.0.0", "storybook": "^10.6.2", "vitest": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-gyyBzncvUyEgWQR3LinnGuI4UsWIUYLyw1u8i244aTxmY4uxCYjsDmUNkaSr8xQzNz8nh2Ckh3G4bO4SvJ6nJQ=="], - "@storybook/core-webpack": ["@storybook/core-webpack@10.5.10", "", { "dependencies": { "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.5.10" } }, "sha512-CiUo4AZRKm3nVb6KWw+yfV6g6VSTA97msiVN9bJlBOFS1dax2ulrRNJnHJxR/U1AT51hONfU4dZOpCXRwaobTA=="], + "@storybook/builder-webpack5": ["@storybook/builder-webpack5@10.6.2", "", { "dependencies": { "@storybook/core-webpack": "10.6.2", "case-sensitive-paths-webpack-plugin": "^2.4.0", "cjs-module-lexer": "^1.2.3", "css-loader": "^7.1.2", "es-module-lexer": "^1.5.0", "fork-ts-checker-webpack-plugin": "^9.1.0", "html-webpack-plugin": "^5.5.0", "magic-string": "^1.1.0", "semver": "^7.7.3", "style-loader": "^4.0.0", "terser-webpack-plugin": "^5.3.17", "ts-dedent": "^2.0.0", "webpack": "5", "webpack-dev-middleware": "^6.1.2", "webpack-hot-middleware": "^2.25.1", "webpack-virtual-modules": "^0.6.0" }, "peerDependencies": { "storybook": "^10.6.2", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-rlXEjV1ZH+AKMVvlqUjm+pmimmi0asdGv0kTxNERQPu5Kaer9NmNmyYIUJvBYyeQfAg1KB4OntF3ik71jJl3HA=="], - "@storybook/csf-plugin": ["@storybook/csf-plugin@10.5.10", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.5.10", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-TaCLBrqVEr767+w58QDotDUiCTuE5cyRJuRcDlsKQyUIyGBv+lYD3lu8wBiVCYxgIjB/gu9HmqiC+0yx1rHzaw=="], + "@storybook/core-webpack": ["@storybook/core-webpack@10.6.2", "", { "dependencies": { "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.6.2" } }, "sha512-6nEmDApLKSKr6SWWFJUCI4wIAhR91TV7KpoYkegzS04GpJ6gRW0tZjUdGbCJuKt1hepxULtZxSU4pxddAtNoaA=="], "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], - "@storybook/icons": ["@storybook/icons@2.1.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg=="], + "@storybook/icons": ["@storybook/icons@2.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-FpUB/xizIrV1ILhCIYFo+TtQNN9YEoLjfcnAW0JtOXL+BcM2aisSysI0B54JMWDnljriXySBHgN6ps4s9lb75A=="], - "@storybook/nextjs": ["@storybook/nextjs@10.5.10", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-runtime": "^7.28.5", "@babel/preset-env": "^7.28.5", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@babel/runtime": "^7.28.4", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@storybook/builder-webpack5": "10.5.10", "@storybook/preset-react-webpack": "10.5.10", "@storybook/react": "10.5.10", "@types/semver": "^7.7.1", "babel-loader": "^9.1.3", "css-loader": "^6.7.3", "image-size": "^2.0.2", "loader-utils": "^3.2.1", "node-polyfill-webpack-plugin": "^2.0.1", "postcss": "^8.4.38", "postcss-loader": "^8.1.1", "react-refresh": "^0.14.0", "resolve-url-loader": "^5.0.0", "sass-loader": "^16.0.5", "semver": "^7.7.3", "style-loader": "^3.3.1", "styled-jsx": "^5.1.6", "tsconfig-paths": "^4.0.0", "tsconfig-paths-webpack-plugin": "^4.0.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "next": "^14.1.0 || ^15.0.0 || ^16.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.10", "typescript": "*", "webpack": "^5.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom", "typescript", "webpack"] }, "sha512-nLc+nFEo1pN0PlFIQTk3Ouj09lR2i0/rNtMPu0N8R1IN3ugK/9hA2bimnly9XEaaDznJVYguZCA2v3msFe/TeQ=="], + "@storybook/nextjs": ["@storybook/nextjs@10.6.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-runtime": "^7.28.5", "@babel/preset-env": "^7.28.5", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@babel/runtime": "^7.28.4", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@storybook/builder-webpack5": "10.6.2", "@storybook/preset-react-webpack": "10.6.2", "@storybook/react": "10.6.2", "@types/semver": "^7.7.1", "babel-loader": "^9.1.3", "css-loader": "^6.7.3", "loader-utils": "^3.2.1", "node-polyfill-webpack-plugin": "^2.0.1", "postcss": "^8.4.38", "postcss-loader": "^8.1.1", "probe-image-size": "^7.3.0", "react-refresh": "^0.14.0", "resolve-url-loader": "^5.0.0", "sass-loader": "^16.0.5", "semver": "^7.7.3", "style-loader": "^3.3.1", "styled-jsx": "^5.1.6", "tsconfig-paths": "^4.0.0", "tsconfig-paths-webpack-plugin": "^4.0.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "next": "^14.1.0 || ^15.0.0 || ^16.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.2", "typescript": "*", "webpack": "^5.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom", "typescript", "webpack"] }, "sha512-T1qokw4brqv7uZh30GlVyqcVTCriYpNPyfpb/joUlDtU6i6bzmAiIg4ZW3aXe/aDM1RDZhrVOQ562BsVzh1MvA=="], - "@storybook/preset-react-webpack": ["@storybook/preset-react-webpack@10.5.10", "", { "dependencies": { "@storybook/core-webpack": "10.5.10", "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", "@types/semver": "^7.7.1", "magic-string": "^0.30.5", "react-docgen": "^8.0.2", "resolve": "^1.22.8", "semver": "^7.7.3", "tsconfig-paths": "^4.2.0", "webpack": "5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.10", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-erAyPdNKqzOQOQVHm/kMSO4Nrr/g279KoA5LyzMOTJAQJHjDdsqmYdGXRDVfcnDL+H+Qm9pWaP+re2S8YG6JNg=="], + "@storybook/preset-react-webpack": ["@storybook/preset-react-webpack@10.6.2", "", { "dependencies": { "@storybook/core-webpack": "10.6.2", "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", "@types/semver": "^7.7.1", "magic-string": "^1.1.0", "react-docgen": "^8.0.2", "resolve": "^1.22.8", "semver": "^7.7.3", "tsconfig-paths": "^4.2.0", "webpack": "5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.2", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-6gIpay0hCKEr/7rS8S+7KWcEvKPj895WCL/e10OiLoaAlPL3RpLIOyLZDN4CRxLhSJcJoSpXhhRqJWG8O5R+HQ=="], - "@storybook/react": ["@storybook/react@10.5.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "10.5.10", "react-docgen": "^8.0.2", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.10", "typescript": ">= 4.9.x" }, "optionalPeers": ["@types/react", "@types/react-dom", "typescript"] }, "sha512-4MBV5e1SXIMfPynLHzr+Mp0dwGv/FW1bklWAsS4ynBOAbC98W9p/I9vqBnUctsvE3BJkhzHQQyPwMHL5tTcHVA=="], + "@storybook/react": ["@storybook/react@10.6.2", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "10.6.2", "react-docgen": "^8.0.2", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.2", "typescript": ">= 4.9.x" }, "optionalPeers": ["@types/react", "@types/react-dom", "typescript"] }, "sha512-kNWUU6Q8TrLmEqAzYmNYhC9A3i3kbjFxqIcLL3eu+o4gDu0afp1Of6Sy6jIF2qPGH05A3r6FWMzD03y4Mm5xRw=="], "@storybook/react-docgen-typescript-plugin": ["@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0", "", { "dependencies": { "debug": "^4.1.1", "endent": "^2.0.1", "find-cache-dir": "^3.3.1", "flat-cache": "^3.0.4", "micromatch": "^4.0.2", "react-docgen-typescript": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "typescript": ">= 4.x", "webpack": ">= 4" } }, "sha512-KUqXC3oa9JuQ0kZJLBhVdS4lOneKTOopnNBK4tUAgoxWQ3u/IjzdueZjFr7gyBrXMoU6duutk3RQR9u8ZpYJ4Q=="], - "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.5.10", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.5.10" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-rbu62ILo/VE3iXKmu+kWXFpD1H1Lwi0f19q/x7JnDsD2dxKS9w5znLEqPIq2qxpzi/wjjIb2iUP1cRG1d/9W5A=="], + "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.6.2", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.2" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-k0262Vqxjk3wHeg3g/QO/ENDrkC7kkbuwsFSXOdYr7Sl888tsF1DAS7uQ/gr+s/BRz1FLYRX7wBY3smEUV4MJg=="], "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], @@ -787,25 +792,25 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="], - "@tanstack/react-store": ["@tanstack/react-store@0.11.1", "", { "dependencies": { "@tanstack/store": "0.11.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ=="], + "@tanstack/react-store": ["@tanstack/react-store@0.11.2", "", { "dependencies": { "@tanstack/store": "0.11.2", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-oj5O5DmiMWE+ih+SioX2ayC1kbK7d3fpLx0B75fPJr9nY3zU+hoW010yr64qXsY/9fyiLVE2bn5mGNV3owm8RQ=="], - "@tanstack/react-table": ["@tanstack/react-table@9.1.2", "", { "dependencies": { "@tanstack/react-store": "^0.11.0", "@tanstack/table-core": "9.1.2" }, "peerDependencies": { "react": ">=18" } }, "sha512-YQPZFJ1nIi/bjjwsPZVouABgahDcl7Gdm33CdTStUJBn0DjEVJ2uhSTVmIoWt9MVKdQziXGAsXipSzy949Hygg=="], + "@tanstack/react-table": ["@tanstack/react-table@9.3.1", "", { "dependencies": { "@tanstack/react-store": "^0.11.2", "@tanstack/table-core": "9.3.1" }, "peerDependencies": { "react": ">=18" } }, "sha512-E6Ya5TnY6EaGyZYIUqig/29Lr/gJSAubQkLJFqiWkUl5nRCr0Xr9hvJot+NiRTtG+TzGxHTiqUW3bnLtQxlHug=="], - "@tanstack/store": ["@tanstack/store@0.11.1", "", {}, "sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA=="], + "@tanstack/store": ["@tanstack/store@0.11.2", "", {}, "sha512-sJ4mjol8uQsHV0gOJzzjwXfh2Fwm+Sz0+8deqiTm4jGbMdjzNSW+xZCFm0kUa870uhd8yi+DpKnZb5Kc4apa0Q=="], - "@tanstack/table-core": ["@tanstack/table-core@9.1.2", "", { "dependencies": { "@tanstack/store": "^0.11.0" } }, "sha512-ONpWQeass1sfg80CWF1NSwQ8r3GiqxA2lT/EdqIcrDEPZ0Z+0mM94eQoFYLPN0Kztzj8TQVb2+PrSZSItqA61g=="], + "@tanstack/table-core": ["@tanstack/table-core@9.3.1", "", { "dependencies": { "@tanstack/store": "^0.11.2" } }, "sha512-zQvlxSNi2qGM3L4jh85Z6hqMfdIR5qOfD9KHOU32nuKFJGN0RlJ7UvV1lsPb1fPVhpFYI0VP3jJGxLdLDpTKjQ=="], - "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + "@testing-library/dom": ["@testing-library/dom@10.4.2", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q=="], "@testing-library/jest-dom": ["@testing-library/jest-dom@7.0.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" }, "peerDependencies": { "@testing-library/dom": ">=10 <11", "vitest": ">= 0.32" }, "optionalPeers": ["vitest"] }, "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw=="], - "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], + "@testing-library/react": ["@testing-library/react@16.3.3", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg=="], - "@testing-library/user-event": ["@testing-library/user-event@14.6.6", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw=="], + "@testing-library/user-event": ["@testing-library/user-event@14.6.8", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ=="], "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], @@ -849,11 +854,11 @@ "@types/mdx": ["@types/mdx@2.0.14", "", {}, "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg=="], - "@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + "@types/node": ["@types/node@24.19.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ=="], - "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], + "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="], - "@types/react-dom": ["@types/react-dom@19.2.5", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg=="], + "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="], "@types/resolve": ["@types/resolve@1.20.6", "", {}, "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ=="], @@ -903,21 +908,21 @@ "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.0", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.152.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw=="], "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - "@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="], + "@vitest/mocker": ["@vitest/mocker@4.1.12", "", { "dependencies": { "@vitest/spy": "4.1.12", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.12", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA=="], - "@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="], + "@vitest/runner": ["@vitest/runner@4.1.12", "", { "dependencies": { "@vitest/utils": "4.1.12", "pathe": "^2.0.3" } }, "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="], + "@vitest/snapshot": ["@vitest/snapshot@4.1.12", "", { "dependencies": { "@vitest/pretty-format": "4.1.12", "@vitest/utils": "4.1.12", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw=="], "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - "@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="], + "@vitest/utils": ["@vitest/utils@4.1.12", "", { "dependencies": { "@vitest/pretty-format": "4.1.12", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA=="], "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], @@ -959,7 +964,7 @@ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + "acorn": ["acorn@8.19.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw=="], "adjust-sourcemap-loader": ["adjust-sourcemap-loader@4.0.0", "", { "dependencies": { "loader-utils": "^2.0.0", "regex-parser": "^2.2.11" } }, "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A=="], @@ -975,7 +980,7 @@ "ansi-html-community": ["ansi-html-community@0.0.8", "", { "bin": { "ansi-html": "bin/ansi-html" } }, "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw=="], - "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], + "ansi-regex": ["ansi-regex@6.4.0", "", {}, "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -993,13 +998,13 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + "ast-types": ["ast-types@0.16.3", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA=="], "atomically": ["atomically@1.7.0", "", {}, "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "axe-core": ["axe-core@4.13.0", "", {}, "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A=="], + "axe-core": ["axe-core@4.14.0", "", {}, "sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ=="], "babel-loader": ["babel-loader@9.2.1", "", { "dependencies": { "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" }, "peerDependencies": { "@babel/core": "^7.12.0", "webpack": ">=5" } }, "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA=="], @@ -1013,9 +1018,9 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.29", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w=="], - "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + "bidi-js": ["bidi-js@1.1.0", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg=="], "big.js": ["big.js@5.2.2", "", {}, "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="], @@ -1025,7 +1030,7 @@ "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - "brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], + "brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -1043,7 +1048,7 @@ "browserify-zlib": ["browserify-zlib@0.2.0", "", { "dependencies": { "pako": "~1.0.5" } }, "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA=="], - "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], + "browserslist": ["browserslist@4.29.4", "", { "dependencies": { "baseline-browser-mapping": "^2.11.29", "caniuse-lite": "^1.0.30001816", "electron-to-chromium": "^1.5.459", "node-releases": "^2.0.59", "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], @@ -1053,7 +1058,7 @@ "builtin-status-codes": ["builtin-status-codes@3.0.0", "", {}, "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ=="], - "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + "bundle-name": ["bundle-name@4.1.1", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -1067,7 +1072,7 @@ "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], - "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001816", "", {}, "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw=="], "case-sensitive-paths-webpack-plugin": ["case-sensitive-paths-webpack-plugin@2.4.0", "", {}, "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw=="], @@ -1079,7 +1084,7 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "chromatic": ["chromatic@18.6.0", "", { "dependencies": { "semver": "^7.3.5" }, "peerDependencies": { "@chromatic-com/cypress": "^0.*.* || ^1.0.0", "@chromatic-com/playwright": "^0.*.* || ^1.0.0", "@chromatic-com/vitest": "^0.*.* || ^1.0.0" }, "optionalPeers": ["@chromatic-com/cypress", "@chromatic-com/playwright", "@chromatic-com/vitest"], "bin": { "chroma": "dist/bin.cjs", "chromatic": "dist/bin.cjs", "chromatic-cli": "dist/bin.cjs" } }, "sha512-AfZ/rscek7V5SiBOQ2A5Xkns+URf/YoYXGz4ZLUhEu9aCZXGmUE1TY6P+wkjRYxeqQ7+nq5tpZ0dB9UGEQd8WA=="], + "chromatic": ["chromatic@18.13.0", "", { "dependencies": { "semver": "^7.3.5" }, "peerDependencies": { "@chromatic-com/cypress": "^0.*.* || ^1.0.0", "@chromatic-com/playwright": "^0.*.* || ^1.0.0", "@chromatic-com/vitest": "^0.*.* || ^1.0.0" }, "optionalPeers": ["@chromatic-com/cypress", "@chromatic-com/playwright", "@chromatic-com/vitest"], "bin": { "chroma": "dist/bin.cjs", "chromatic": "dist/bin.cjs", "chromatic-cli": "dist/bin.cjs" } }, "sha512-AdN6tHB9anx/sdJKIMQ7JkQQV2RhVe1iTvXOFLlwFBRecbl9rp50h/25N0sZJDdpx22gG7Y81HoUALOORfMuxQ=="], "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], @@ -1101,6 +1106,8 @@ "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + "cn": ["cn@0.2.6", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ=="], + "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -1125,7 +1132,7 @@ "constants-browserify": ["constants-browserify@1.0.0", "", {}, "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ=="], - "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + "content-disposition": ["content-disposition@2.0.1", "", {}, "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -1135,9 +1142,9 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "core-js-compat": ["core-js-compat@3.50.0", "", { "dependencies": { "browserslist": "^4.28.7" } }, "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q=="], + "core-js-compat": ["core-js-compat@3.51.0", "", { "dependencies": { "browserslist": "^4.29.3" } }, "sha512-52r6fkXgxv4GLMjmFcMWRx7X9bw071xTTdrDcdyPGRqU2oE+9pCG0bAOMifSBdLw0JRBZKEF0q3B55qPlLuE8g=="], - "core-js-pure": ["core-js-pure@3.50.0", "", {}, "sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA=="], + "core-js-pure": ["core-js-pure@3.51.0", "", {}, "sha512-NE4SFDJ+qzQeqHG2vVSNwopzS6zZNPYJ5kinIqs+OofTqGzOmICgT89E/yvS9XY7rwb3eU1utWXTJ9nVFqv74A=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -1191,7 +1198,7 @@ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], + "data-urls": ["data-urls@8.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^17.1.2" } }, "sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ=="], "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], @@ -1259,7 +1266,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.414", "", {}, "sha512-aYlviXiaXBbzvKgyALpcMmqa3Np3sDr0XnZbEG62n2UpZFbEcjQ4EEMOLGzVPhwVnwTz0lvKY+GcARbunuHekw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.459", "", {}, "sha512-4SC9HHSFjO/liAhifpdOL5hG9b4BP7/5G3y3UPZVO8trlAXr1CaUEEJL0gWK3kOcYcZrOA5rkwG45Gra/lTIgg=="], "elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="], @@ -1271,11 +1278,11 @@ "endent": ["endent@2.1.0", "", { "dependencies": { "dedent": "^0.7.0", "fast-json-parse": "^1.0.3", "objectorarray": "^1.0.5" } }, "sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w=="], - "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], + "enhanced-resolve": ["enhanced-resolve@5.26.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], - "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], + "entities": ["entities@8.1.0", "", {}, "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA=="], "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], @@ -1291,7 +1298,7 @@ "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], - "es-toolkit": ["es-toolkit@1.51.0", "", {}, "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw=="], + "es-toolkit": ["es-toolkit@1.52.0", "", {}, "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA=="], "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], @@ -1299,14 +1306,8 @@ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], @@ -1329,9 +1330,9 @@ "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "express": ["express@5.3.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.3.0", "content-disposition": "^2.0.1", "content-type": "^2.0.0", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.8", "qs": "^6.16.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.1.0", "vary": "^1.1.2" } }, "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g=="], - "express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], + "express-rate-limit": ["express-rate-limit@8.7.1", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -1341,9 +1342,9 @@ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - "fast-uri": ["fast-uri@3.1.6", "", {}, "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q=="], + "fast-uri": ["fast-uri@3.1.8", "", {}, "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg=="], - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -1369,11 +1370,11 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "framer-motion": ["framer-motion@13.1.1", "", { "dependencies": { "motion-dom": "^13.1.1", "motion-utils": "^13.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-B/xn2TPS4f61cEBLFjiYlQFnBZUW1YVj/LM+C+N4OP8Rs95VLEI2ot/RlfBg111la/EiyECFaJJi/A3FWA8MUA=="], + "framer-motion": ["framer-motion@13.5.1", "", { "dependencies": { "motion-dom": "13.5.1", "motion-utils": "13.5.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-HeWOaeHD3kyTSIEMT6+nw7fBn4saIfxxabZyAITbycAl4xr1kzGJKIvGt/OWghSlvIA9qTnZ1zTGelHAe6S8DA=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], + "fs-extra": ["fs-extra@11.4.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA=="], "fs-monkey": ["fs-monkey@1.1.0", "", {}, "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw=="], @@ -1389,7 +1390,7 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -1427,9 +1428,9 @@ "hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], - "hono": ["hono@4.13.4", "", {}, "sha512-AGEwKIyRMHRv1t8Wjwa3LHxQ61X5CqrdFT+4BRNTpqS5aJNnpl5WLjADb7vFlJzI/8uK7T5QLVApCMQKNa3LgQ=="], + "hono": ["hono@4.13.13", "", {}, "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA=="], - "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + "html-encoding-sniffer": ["html-encoding-sniffer@7.0.0", "", { "dependencies": { "@exodus/bytes": "^1.15.1" } }, "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w=="], "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], @@ -1445,7 +1446,7 @@ "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], - "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="], @@ -1453,9 +1454,7 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="], - - "immer": ["immer@11.1.18", "", {}, "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ=="], + "immer": ["immer@11.1.21", "", {}, "sha512-bZSadeCPGdaY5/qRLgr6mDSH1ra21rD3rqpiWk7+QTr4ayYPTE9qZYdLh/NdPhMe0wSrTvHD/Lk8q5EwhUs0aw=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -1469,7 +1468,7 @@ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="], + "ip-address": ["ip-address@10.7.4", "", {}, "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -1479,7 +1478,7 @@ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + "is-core-module": ["is-core-module@2.17.0", "", { "dependencies": { "hasown": "^2.0.4" } }, "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA=="], "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], @@ -1527,13 +1526,13 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], + "jose": ["jose@6.2.12", "", {}, "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], + "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], - "jsdom": ["jsdom@30.0.1", "", { "dependencies": { "@asamuzakjp/css-color": "^6.0.5", "@asamuzakjp/dom-selector": "^8.3.0", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.7", "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.2", "undici": "^8.9.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^17.1.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.2.3" }, "optionalPeers": ["canvas"] }, "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA=="], + "jsdom": ["jsdom@30.1.3", "", { "dependencies": { "@asamuzakjp/css-color": "^7.1.3", "@asamuzakjp/dom-selector": "^10.0.3", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.15", "@exodus/bytes": "^1.16.0", "css-tree": "^3.2.1", "data-urls": "^8.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^7.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.3", "parse5": "^8.0.1", "saxes": "^6.0.0", "tough-cookie": "^6.0.2", "undici": "^8.12.1", "w3c-xmlserializer": "^6.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^17.2.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.2.3" }, "optionalPeers": ["canvas"] }, "sha512-hQMUPvRgN7KxsfIytDQji7bimuJeRQa6Qo5+iuiZQk5CafeRxLIA10/cG5YCBFNpD9RO3oqIiN4CO1Y44kxqXg=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -1589,13 +1588,15 @@ "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], - "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -1641,21 +1642,23 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minimizer-webpack-plugin": ["minimizer-webpack-plugin@5.6.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "@minify-html/node": "*", "@swc/core": "*", "@swc/css": "*", "@swc/html": "*", "clean-css": "*", "cssnano": "*", "csso": "*", "esbuild": "*", "html-minifier-terser": "*", "lightningcss": "*", "postcss": "*", "uglify-js": "*", "webpack": "^5.1.0" }, "optionalPeers": ["@minify-html/node", "@swc/core", "@swc/css", "@swc/html", "clean-css", "cssnano", "csso", "esbuild", "html-minifier-terser", "lightningcss", "postcss", "uglify-js"] }, "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw=="], + "minimizer-webpack-plugin": ["minimizer-webpack-plugin@5.13.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "jest-worker": "^27.4.5", "schema-utils": "^4.6.0", "terser": "^5.51.0" }, "peerDependencies": { "@minify-html/node": "*", "@napi-rs/image": "*", "@swc/core": "*", "@swc/css": "*", "@swc/html": "*", "clean-css": "*", "cssnano": "*", "csso": "*", "esbuild": "*", "html-minifier-terser": "*", "imagemin": "*", "lightningcss": "*", "postcss": "*", "sharp": "*", "svgo": "*", "uglify-js": "*", "webpack": "^5.1.0" }, "optionalPeers": ["@minify-html/node", "@napi-rs/image", "@swc/core", "@swc/css", "@swc/html", "clean-css", "cssnano", "csso", "esbuild", "html-minifier-terser", "imagemin", "lightningcss", "postcss", "sharp", "svgo", "uglify-js"] }, "sha512-1mhAfvachWYYOm8tpmany5NLuvP+F2wxZWBSX8DC0uFS2wJpM9q3rWgN9wBooAH9d5V12wX0BjyJe4fMjHMarQ=="], - "motion-dom": ["motion-dom@13.1.1", "", { "dependencies": { "motion-utils": "^13.0.0" } }, "sha512-XSf8VYWSB6G/0IY3rWVbyLcxWXtAVHkN1PQE2agTaCv3u8RGvbwu56TyyR/MNzBqqNavEBTZzErcxI1TxBrjcA=="], + "motion-dom": ["motion-dom@13.5.1", "", { "dependencies": { "motion-utils": "13.5.0" } }, "sha512-/7CVCD20Mz1KobktorpdgvowV0V41J7UxqE+qWS3GkSJqrRr5ft+2RloOYtEKv+wvBp3JaSFBclhxt4ja8ovHg=="], - "motion-utils": ["motion-utils@13.0.0", "", {}, "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ=="], + "motion-utils": ["motion-utils@13.5.0", "", {}, "sha512-Hqe/sqjIKliPZg8sVjWAkdySDPJNIeKH+euVkDOG8ri3DUUU7ZjndmHsGqEPz2iHpiyVaV5I22iguCMHU2he+Q=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + "nanoid": ["nanoid@3.3.20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="], + + "needle": ["needle@2.9.1", "", { "dependencies": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" }, "bin": { "needle": "./bin/needle" } }, "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ=="], "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "next": ["next@16.3.3", "", { "dependencies": { "@next/env": "16.3.3", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.3", "@next/swc-darwin-x64": "16.3.3", "@next/swc-linux-arm64-gnu": "16.3.3", "@next/swc-linux-arm64-musl": "16.3.3", "@next/swc-linux-x64-gnu": "16.3.3", "@next/swc-linux-x64-musl": "16.3.3", "@next/swc-win32-arm64-msvc": "16.3.3", "@next/swc-win32-x64-msvc": "16.3.3", "sharp": "^0.35.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g=="], + "next": ["next@16.4.2", "", { "dependencies": { "@next/env": "16.4.2", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.4.2", "@next/swc-darwin-x64": "16.4.2", "@next/swc-linux-arm64-gnu": "16.4.2", "@next/swc-linux-arm64-musl": "16.4.2", "@next/swc-linux-x64-gnu": "16.4.2", "@next/swc-linux-x64-musl": "16.4.2", "@next/swc-win32-arm64-msvc": "16.4.2", "@next/swc-win32-x64-msvc": "16.4.2", "sharp": "^0.35.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], @@ -1665,7 +1668,7 @@ "node-polyfill-webpack-plugin": ["node-polyfill-webpack-plugin@2.0.1", "", { "dependencies": { "assert": "^2.0.0", "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", "console-browserify": "^1.2.0", "constants-browserify": "^1.0.0", "crypto-browserify": "^3.12.0", "domain-browser": "^4.22.0", "events": "^3.3.0", "filter-obj": "^2.0.2", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "process": "^0.11.10", "punycode": "^2.1.1", "querystring-es3": "^0.2.1", "readable-stream": "^4.0.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string_decoder": "^1.3.0", "timers-browserify": "^2.0.12", "tty-browserify": "^0.0.1", "type-fest": "^2.14.0", "url": "^0.11.0", "util": "^0.12.4", "vm-browserify": "^1.1.2" }, "peerDependencies": { "webpack": ">=5" } }, "sha512-ZUMiCnZkP1LF0Th2caY6J/eKKoA0TefpoVa68m/LQU1I/mE8rGt4fNYGgNuCcK+aG8P8P43nbeJ2RqJMOL/Y1A=="], - "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], + "node-releases": ["node-releases@2.0.59", "", {}, "sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA=="], "normalize-wheel": ["normalize-wheel@1.0.1", "", {}, "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA=="], @@ -1687,7 +1690,7 @@ "objectorarray": ["objectorarray@1.0.5", "", {}, "sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg=="], - "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + "obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -1717,7 +1720,7 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - "parse-asn1": ["parse-asn1@5.1.9", "", { "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", "evp_bytestokey": "^1.0.3", "pbkdf2": "^3.1.5", "safe-buffer": "^5.2.1" } }, "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg=="], + "parse-asn1": ["parse-asn1@5.1.10", "", { "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", "evp_bytestokey": "^1.0.3", "pbkdf2": "^3.1.7", "safe-buffer": "^5.2.1" } }, "sha512-gBlTKLbYnAJN9WjmawVOMfaGvVoy4lX0LH18t7ccwtEG9RgAa4ETf2eu3d2J4e/5SoCjCms0A0UF4tTw/Eyn6Q=="], "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], @@ -1747,7 +1750,7 @@ "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - "pbkdf2": ["pbkdf2@3.1.6", "", { "dependencies": { "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", "sha.js": "^2.4.12", "to-buffer": "^1.2.2" } }, "sha512-BT6eelPB1EyGHo8pC0o9Bl6k6SYVhKO1jEbd3lcTrtr7XHdjP8BW1YpfCV3G9Kwkxgattk+S5q2/RvuttCsS1g=="], + "pbkdf2": ["pbkdf2@3.1.7", "", { "dependencies": { "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", "sha.js": "^2.4.12", "to-buffer": "^1.2.2" } }, "sha512-nS4mvFgVwUrecPTRdpdseM2fwpdfnX0/HibA8DgBRQtihUAzHQYJFU2l+OlEtiXtpqkrMvCPDwyXMUnASDt6Qw=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -1761,7 +1764,7 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + "postcss": ["postcss@8.5.31", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg=="], "postcss-loader": ["postcss-loader@8.2.1", "", { "dependencies": { "cosmiconfig": "^9.0.0", "jiti": "^2.5.1", "semver": "^7.6.2" }, "peerDependencies": { "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-k98jtRzthjj3f76MYTs9JTpRqV1RaaMhEU0Lpw9OTmQZQdppg4B30VZ74BojuBHt3F4KyubHJoXCMUeM8Bqeow=="], @@ -1773,17 +1776,19 @@ "postcss-modules-values": ["postcss-modules-values@4.0.0", "", { "dependencies": { "icss-utils": "^5.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ=="], - "postcss-selector-parser": ["postcss-selector-parser@7.1.5", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw=="], + "postcss-selector-parser": ["postcss-selector-parser@7.1.6", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - "powershell-utils": ["powershell-utils@0.2.0", "", {}, "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw=="], + "powershell-utils": ["powershell-utils@0.2.1", "", {}, "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A=="], "pretty-error": ["pretty-error@4.0.0", "", { "dependencies": { "lodash": "^4.17.20", "renderkid": "^3.0.0" } }, "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw=="], "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], - "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "pretty-ms": ["pretty-ms@9.3.1", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA=="], + + "probe-image-size": ["probe-image-size@7.4.0", "", { "dependencies": { "lodash.merge": "^4.6.2", "needle": "^2.5.2", "stream-parser": "~0.3.1" } }, "sha512-cdEprVtZxV+awMde9X+4jILBFYh4CARxVrQaMl4wY4YcPWbul9jntXrIW95NInBDyJwcVUP3U0T6yukN8rMBaQ=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], @@ -1791,19 +1796,19 @@ "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "proxy-addr": ["proxy-addr@2.0.8", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ=="], "public-encrypt": ["public-encrypt@4.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + "qs": ["qs@6.16.1", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg=="], "querystring-es3": ["querystring-es3@0.2.1", "", {}, "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "radix-ui": ["radix-ui@1.6.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-accessible-icon": "1.1.15", "@radix-ui/react-accordion": "1.2.20", "@radix-ui/react-alert-dialog": "1.1.23", "@radix-ui/react-arrow": "1.1.15", "@radix-ui/react-aspect-ratio": "1.1.15", "@radix-ui/react-avatar": "1.2.6", "@radix-ui/react-checkbox": "1.3.11", "@radix-ui/react-collapsible": "1.1.20", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-context-menu": "2.3.7", "@radix-ui/react-dialog": "1.1.23", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-dropdown-menu": "2.1.24", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-form": "0.1.16", "@radix-ui/react-hover-card": "1.1.23", "@radix-ui/react-label": "2.1.15", "@radix-ui/react-menu": "2.1.24", "@radix-ui/react-menubar": "1.1.24", "@radix-ui/react-navigation-menu": "1.2.22", "@radix-ui/react-one-time-password-field": "0.1.16", "@radix-ui/react-password-toggle-field": "0.1.11", "@radix-ui/react-popover": "1.1.23", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-progress": "1.1.16", "@radix-ui/react-radio-group": "1.4.7", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-scroll-area": "1.2.18", "@radix-ui/react-select": "2.3.7", "@radix-ui/react-separator": "1.1.15", "@radix-ui/react-slider": "1.4.7", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-switch": "1.3.7", "@radix-ui/react-tabs": "1.1.21", "@radix-ui/react-toast": "1.2.23", "@radix-ui/react-toggle": "1.1.18", "@radix-ui/react-toggle-group": "1.1.19", "@radix-ui/react-toolbar": "1.1.19", "@radix-ui/react-tooltip": "1.2.16", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-escape-keydown": "1.1.5", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-size": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA=="], + "radix-ui": ["radix-ui@1.8.1", "", { "dependencies": { "@radix-ui/primitive": "1.2.0", "@radix-ui/react-accessible-icon": "1.2.0", "@radix-ui/react-accordion": "1.3.1", "@radix-ui/react-alert-dialog": "1.2.1", "@radix-ui/react-arrow": "1.1.17", "@radix-ui/react-aspect-ratio": "1.2.0", "@radix-ui/react-avatar": "1.3.1", "@radix-ui/react-checkbox": "1.4.1", "@radix-ui/react-collapsible": "1.2.1", "@radix-ui/react-collection": "1.2.0", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.3.0", "@radix-ui/react-context-menu": "2.4.1", "@radix-ui/react-dialog": "1.3.1", "@radix-ui/react-direction": "1.2.0", "@radix-ui/react-dismissable-layer": "1.2.1", "@radix-ui/react-dropdown-menu": "2.2.1", "@radix-ui/react-focus-guards": "1.2.0", "@radix-ui/react-focus-scope": "1.3.1", "@radix-ui/react-form": "0.3.0", "@radix-ui/react-hover-card": "1.2.1", "@radix-ui/react-label": "2.1.17", "@radix-ui/react-menu": "2.2.1", "@radix-ui/react-menubar": "1.2.1", "@radix-ui/react-navigation-menu": "1.4.1", "@radix-ui/react-one-time-password-field": "0.2.1", "@radix-ui/react-password-toggle-field": "0.2.1", "@radix-ui/react-popover": "1.3.1", "@radix-ui/react-popper": "1.4.0", "@radix-ui/react-portal": "1.2.0", "@radix-ui/react-presence": "1.1.11", "@radix-ui/react-primitive": "2.2.0", "@radix-ui/react-progress": "1.2.1", "@radix-ui/react-radio-group": "1.5.1", "@radix-ui/react-roving-focus": "1.2.1", "@radix-ui/react-scroll-area": "1.4.0", "@radix-ui/react-select": "2.4.1", "@radix-ui/react-separator": "1.2.0", "@radix-ui/react-slider": "1.6.1", "@radix-ui/react-slot": "1.5.0", "@radix-ui/react-switch": "1.4.1", "@radix-ui/react-tabs": "1.2.1", "@radix-ui/react-toast": "1.3.1", "@radix-ui/react-toggle": "1.2.1", "@radix-ui/react-toggle-group": "1.2.1", "@radix-ui/react-toolbar": "1.2.1", "@radix-ui/react-tooltip": "1.4.1", "@radix-ui/react-use-callback-ref": "1.1.5", "@radix-ui/react-use-controllable-state": "1.3.0", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-escape-keydown": "1.1.6", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-size": "1.1.5", "@radix-ui/react-visually-hidden": "1.2.13" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HPfpVGizO9xANlsFSauruLhRe/3+iOt3vPX0MXYle/UQi3gorQrSYtBcdiv0BZYewcl2rvL59qrIuLFMGAmUrw=="], "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], @@ -1813,17 +1818,17 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], + "react": ["react@19.3.1", "", {}, "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw=="], - "react-day-picker": ["react-day-picker@10.0.1", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "react": ">=16.8.0" }, "optionalPeers": ["@types/react"] }, "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w=="], + "react-day-picker": ["react-day-picker@10.0.2", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "react": ">=16.8.0" }, "optionalPeers": ["@types/react"] }, "sha512-QetiKtZWE9Uaw/pM/SYNv6WPYOTRnSJ2puL0GgwkmUFMIwiY0dtjH1W4B7DOVXVNQM0ZOClGpH/heOfBSzWeiA=="], - "react-docgen": ["react-docgen@8.0.3", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.2", "@types/babel__core": "^7.20.5", "@types/babel__traverse": "^7.20.7", "@types/doctrine": "^0.0.9", "@types/resolve": "^1.20.2", "doctrine": "^3.0.0", "resolve": "^1.22.1", "strip-indent": "^4.0.0" } }, "sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w=="], + "react-docgen": ["react-docgen@8.0.4", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.2", "@types/babel__core": "^7.20.5", "@types/babel__traverse": "^7.20.7", "@types/doctrine": "^0.0.9", "@types/resolve": "^1.20.2", "doctrine": "^3.0.0", "resolve": "^1.22.1", "strip-indent": "^4.0.0" } }, "sha512-gIYjtVTU4NCWZnb/LXVSWfClZXnjzX3+xxi69Tkfu4YsmYuqiOk8giLkKt/WwxWqh+U66CTh8Kqbha1uPv8/qQ=="], "react-docgen-typescript": ["react-docgen-typescript@2.4.0", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg=="], - "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], + "react-dom": ["react-dom@19.3.1", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.1" } }, "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ=="], - "react-easy-crop": ["react-easy-crop@6.2.3", "", { "dependencies": { "normalize-wheel": "^1.0.1" }, "peerDependencies": { "react": ">=16.4.0", "react-dom": ">=16.4.0" } }, "sha512-ebimG3OGlzizjxEZ77Cj9CVLcg5vDZ6QVz8ud1rx4WmsCDWHIROEhgMi0GpJ/jKAuwHrH0M+QZUK4hyvxbYhOA=="], + "react-easy-crop": ["react-easy-crop@6.2.4", "", { "dependencies": { "normalize-wheel": "^1.0.1" }, "peerDependencies": { "react": ">=16.4.0", "react-dom": ">=16.4.0" } }, "sha512-OWdnFYctzUNlekjob+bQfloG9wyhIBkkmeISnM5XVE4Byqn0jmVxDF7oz85ovUF+EIPQyj0yAHbBEFtnxwGv4g=="], "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], @@ -1835,11 +1840,11 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-resizable-panels": ["react-resizable-panels@4.12.3", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-GHMJWnDXui/3RX4bT+cgBP+N3N2nkwcoZATr/2xLFpqQQe7TlBrE0cGM0dUa9ceT7A90tUrSRsiqgRG+g0/2AA=="], + "react-resizable-panels": ["react-resizable-panels@4.14.6", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-YqkTuTDV1ncknnR5YN/iI8icpClippc9/10job9zJIppt2r8yUwaVfVxhzFNO+3DIxrnAwhcMMs1FdX5xt9C9A=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "readable-stream": ["readable-stream@4.8.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], @@ -1853,17 +1858,17 @@ "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], - "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], + "regenerate": ["regenerate@1.5.0", "", {}, "sha512-1s+CxXjJ71i7LNVy8nYwg47XV4y6lAnOEhsMtSKq3WpmPfhl9LuKlTqKIn/3yUpE8lBz+W+TT88Bxyhnr0U4yw=="], - "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + "regenerate-unicode-properties": ["regenerate-unicode-properties@10.3.0", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-9ns8odR8e9q3otKeHj3DE80jZYKg6zVeZKE+zQmRKXFoVUcuiTGeO6iusDhPRoIQQPVPRvH+SfBhrTud4eczuw=="], "regex-parser": ["regex-parser@2.3.1", "", {}, "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ=="], - "regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + "regexpu-core": ["regexpu-core@6.5.3", "", { "dependencies": { "regenerate": "^1.5.0", "regenerate-unicode-properties": "^10.3.0", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-E8VgUoSK+LMQrLboDxavm76MTBGrQhx9RiNuomRmYnZ10Uw12HrI8fkxOOncktb2hBpzIZ9SV/347euQHG5z+g=="], "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="], - "regjsparser": ["regjsparser@0.13.2", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ=="], + "regjsparser": ["regjsparser@0.13.4", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-EWV80YgFaVgFrjGCAsV7oybp4GOxbls5RzBg8OB0nKEVXsBJR1JlV8Di1JhCROg3MzTfe1RI8yitIm1eOs68iA=="], "relateurl": ["relateurl@0.2.7", "", {}, "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog=="], @@ -1873,7 +1878,7 @@ "reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], - "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + "resolve": ["resolve@1.22.13", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.17.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -1887,7 +1892,7 @@ "ripemd160": ["ripemd160@2.0.3", "", { "dependencies": { "hash-base": "^3.1.2", "inherits": "^2.0.4" } }, "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA=="], - "rolldown": ["rolldown@1.2.5", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="], + "rolldown": ["rolldown@1.2.14", "", { "dependencies": { "@oxc-project/types": "=0.154.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.14", "@rolldown/binding-android-arm64": "1.2.14", "@rolldown/binding-darwin-arm64": "1.2.14", "@rolldown/binding-darwin-x64": "1.2.14", "@rolldown/binding-freebsd-x64": "1.2.14", "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", "@rolldown/binding-linux-arm64-gnu": "1.2.14", "@rolldown/binding-linux-arm64-musl": "1.2.14", "@rolldown/binding-linux-ppc64-gnu": "1.2.14", "@rolldown/binding-linux-s390x-gnu": "1.2.14", "@rolldown/binding-linux-x64-gnu": "1.2.14", "@rolldown/binding-linux-x64-musl": "1.2.14", "@rolldown/binding-openharmony-arm64": "1.2.14", "@rolldown/binding-win32-arm64-msvc": "1.2.14", "@rolldown/binding-win32-x64-msvc": "1.2.14" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -1903,11 +1908,13 @@ "sass-loader": ["sass-loader@16.0.8", "", { "dependencies": { "neo-async": "^2.6.2" }, "peerDependencies": { "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", "sass": "^1.3.0", "sass-embedded": "*", "webpack": "^5.0.0" }, "optionalPeers": ["@rspack/core", "node-sass", "sass", "sass-embedded", "webpack"] }, "sha512-hcov4ZwZJIGbEuyNr9EmiTmZueyrxSToE6GOzoZnq5JM7ecRO7ttyvilPn+VmRsqiP16+VYZzVnGZj/hzZgKBA=="], + "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], - "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], + "schema-utils": ["schema-utils@4.8.1", "", { "dependencies": { "@types/json-schema": "^7.0.15", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", "ajv-keywords": "^5.1.0" }, "peerDependencies": { "webpack": "^5.112.0" }, "optionalPeers": ["webpack"], "bin": { "schema-utils": "bin/schema-utils.js" } }, "sha512-x39pYlJPyH7Uge5QO64B9XGP9zLDc9WNmiQZc+CE3IzZPvMu3lHOuAMGpYNoPixEL7MSWlGhYTg5UlLSL8G6iw=="], "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], @@ -1923,9 +1930,9 @@ "sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" } }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="], - "shadcn": ["shadcn@4.19.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "kleur": "^4.1.5", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "socks": "^2.8.8", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "undici": "^7.27.2", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-EQF6R+CUXTsEP2BpyhxrUEAFesrtFD1POvVOf5jM+wkgtA4kG1EW1+1Wlmi9LqiprSL681JJXBcS0u8WkVVVyQ=="], + "shadcn": ["shadcn@4.22.1", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.32.0", "@shadcn/registry": "0.2.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.29.3", "cn": "^0.2.4", "commander": "^14.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "kleur": "^4.1.5", "open": "^11.0.4", "ora": "^8.2.0", "postcss": "^8.5.23", "postcss-selector-parser": "^7.1.6", "prompts": "^2.4.2", "recast": "^0.23.11", "socks": "^2.8.8", "stringify-object": "^5.0.0", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "undici": "^7.29.1", "validate-npm-package-name": "^7.0.1", "zod": "^4.2.0" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-LbB4Lro+b0qSVki/tgL5edtKylEnXe1ppxMJC12d7NHPgaT+eSgJGMQULwP72V7X9tbiUq7xfx3VDAfB7jdGoA=="], - "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="], + "sharp": ["sharp@0.35.5", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.5", "@img/sharp-darwin-x64": "0.35.5", "@img/sharp-freebsd-wasm32": "0.35.5", "@img/sharp-libvips-darwin-arm64": "1.3.4", "@img/sharp-libvips-darwin-x64": "1.3.4", "@img/sharp-libvips-linux-arm": "1.3.4", "@img/sharp-libvips-linux-arm64": "1.3.4", "@img/sharp-libvips-linux-ppc64": "1.3.4", "@img/sharp-libvips-linux-riscv64": "1.3.4", "@img/sharp-libvips-linux-s390x": "1.3.4", "@img/sharp-libvips-linux-x64": "1.3.4", "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", "@img/sharp-libvips-linuxmusl-x64": "1.3.4", "@img/sharp-linux-arm": "0.35.5", "@img/sharp-linux-arm64": "0.35.5", "@img/sharp-linux-ppc64": "0.35.5", "@img/sharp-linux-riscv64": "0.35.5", "@img/sharp-linux-s390x": "0.35.5", "@img/sharp-linux-x64": "0.35.5", "@img/sharp-linuxmusl-arm64": "0.35.5", "@img/sharp-linuxmusl-x64": "0.35.5", "@img/sharp-webcontainers-wasm32": "0.35.5", "@img/sharp-win32-arm64": "0.35.5", "@img/sharp-win32-ia32": "0.35.5", "@img/sharp-win32-x64": "0.35.5" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1947,13 +1954,13 @@ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], - "socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="], + "socks": ["socks@2.8.10", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ=="], "sonner": ["sonner@2.0.8", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], @@ -1963,16 +1970,18 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], + "std-env": ["std-env@4.3.0", "", {}, "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag=="], "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - "storybook": ["storybook@10.5.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "11.21.2", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Rz8k9ejFHsi7lbtJTaxZlhCUz4GkbJIKEoKDjXeLfr/ZhXip73E6keKxW0KH8iGeKiCqHAbJCV4YIQrxTOLiig=="], + "storybook": ["storybook@10.6.2", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.3", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "11.21.2", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-qHDINMiapxFUxYb+xC3j8u7Y+85X3u+/PlytE/wmnyg+bU0agCW3jUmWP4vIh2MioO2jTK/2B3Lcw6IpRFzQBA=="], "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], "stream-http": ["stream-http@3.2.0", "", { "dependencies": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.4", "readable-stream": "^3.6.0", "xtend": "^4.0.2" } }, "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A=="], + "stream-parser": ["stream-parser@0.3.1", "", { "dependencies": { "debug": "2" } }, "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -1995,17 +2004,15 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + "systeminformation": ["systeminformation@5.33.16", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-R+Lh69IJXfJj6bfyH1In5cIR9FTr4hBS39YvlZHZjsK1p9cuELPq7hplZZIb56ZESMMb+DzdKqqE6Iomi01Mvg=="], - "systeminformation": ["systeminformation@5.33.3", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-CFdkRa2yfWBXtRtinSm7QbFGRntq1nC2V2hPAjfRTGwncZbNw/UWyiKDT8rZQz6MB70/3ndzP0scIR0dJHJjBA=="], - - "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], + "tailwind-merge": ["tailwind-merge@3.7.0", "", {}, "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA=="], "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "terser": ["terser@5.50.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w=="], + "terser": ["terser@5.51.2", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q=="], "terser-webpack-plugin": ["terser-webpack-plugin@5.6.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "@minify-html/node": "*", "@swc/core": "*", "@swc/css": "*", "@swc/html": "*", "clean-css": "*", "cssnano": "*", "csso": "*", "esbuild": "*", "html-minifier-terser": "*", "lightningcss": "*", "postcss": "*", "uglify-js": "*", "webpack": "^5.1.0" }, "optionalPeers": ["@minify-html/node", "@swc/core", "@swc/css", "@swc/html", "clean-css", "cssnano", "csso", "esbuild", "html-minifier-terser", "lightningcss", "postcss", "uglify-js"] }, "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ=="], @@ -2015,17 +2022,17 @@ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], + "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], + "tinyrainbow": ["tinyrainbow@3.2.0", "", {}, "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A=="], - "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + "tinyspy": ["tinyspy@4.0.6", "", {}, "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg=="], - "tldts": ["tldts@7.4.11", "", { "dependencies": { "tldts-core": "^7.4.11" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw=="], + "tldts": ["tldts@7.4.18", "", { "dependencies": { "tldts-core": "^7.4.18" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-somxA8xKAUzlAxl0TX/jMSYIbRNFIVGnI96pVGJPeMc5BsHmfVXn6mOJT6U++ELnl04eGfzpfcEuZe8WiASkXg=="], - "tldts-core": ["tldts-core@7.4.11", "", {}, "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg=="], + "tldts-core": ["tldts-core@7.4.18", "", {}, "sha512-KI2PKvCmvQI/xD6ypPqkU0i7oxNXR/O58RLRaHR4svsgVUQ2JE/XC8BtMbG6PSehD27I8sZJRcC1ASnCy6lp3g=="], "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], @@ -2035,7 +2042,7 @@ "tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="], - "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + "tr46": ["tr46@7.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg=="], "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], @@ -2059,9 +2066,9 @@ "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], - "undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="], + "undici": ["undici@8.12.3", "", {}, "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw=="], - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], @@ -2079,7 +2086,7 @@ "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], - "update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="], + "update-browserslist-db": ["update-browserslist-db@1.3.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -2089,7 +2096,7 @@ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], - "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="], "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], @@ -2105,41 +2112,41 @@ "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], - "vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="], + "vite": ["vite@8.3.4", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.29", "rolldown": "~1.2.12", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ=="], - "vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="], + "vitest": ["vitest@4.1.12", "", { "dependencies": { "@vitest/expect": "4.1.12", "@vitest/mocker": "4.1.12", "@vitest/pretty-format": "4.1.12", "@vitest/runner": "4.1.12", "@vitest/snapshot": "4.1.12", "@vitest/spy": "4.1.12", "@vitest/utils": "4.1.12", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.12", "@vitest/browser-preview": "4.1.12", "@vitest/browser-webdriverio": "^4.1.11", "@vitest/coverage-istanbul": "4.1.12", "@vitest/coverage-v8": "4.1.12", "@vitest/ui": "4.1.12", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA=="], "vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="], - "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + "w3c-xmlserializer": ["w3c-xmlserializer@6.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q=="], - "watchpack": ["watchpack@2.5.2", "", { "dependencies": { "graceful-fs": "^4.1.2" } }, "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg=="], + "watchpack": ["watchpack@2.6.3", "", { "dependencies": { "graceful-fs": "^4.1.2" } }, "sha512-s3NZy0QpTY9qGqzc0wtz4qI28z3+5qKbbAec4MV4G9N3CfgZ9wgvQRr0cl2jh/tqkWG1NWHkmVMHZpCamev5Wg=="], "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], - "webpack": ["webpack@5.109.2", "", { "dependencies": { "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.24.4", "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "graceful-fs": "^4.2.11", "mime-db": "^1.54.0", "minimizer-webpack-plugin": "^5.6.1", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "watchpack": "^2.5.2", "webpack-sources": "^3.5.1" }, "peerDependencies": { "webpack-cli": "*" }, "optionalPeers": ["webpack-cli"], "bin": { "webpack": "bin/webpack.js" } }, "sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw=="], + "webpack": ["webpack@5.112.0", "", { "dependencies": { "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.26.0", "es-module-lexer": "^2.1.0", "events": "^3.2.0", "graceful-fs": "^4.2.11", "mime-db": "^1.54.0", "minimizer-webpack-plugin": "^5.13.0", "schema-utils": "^4.5.0", "tapable": "^2.3.0", "watchpack": "^2.6.1", "webpack-sources": "^3.6.0" }, "peerDependencies": { "webpack-cli": "*" }, "optionalPeers": ["webpack-cli"], "bin": { "webpack": "bin/webpack.js" } }, "sha512-NIBpsRuCll036haDDt/CfcTDfsWNR9Z782nMRMI9D377BsGcYzZDTNU5TyzhCJzPIcihAxMBaMAhq2WSaDrH8Q=="], "webpack-dev-middleware": ["webpack-dev-middleware@6.1.3", "", { "dependencies": { "colorette": "^2.0.10", "memfs": "^3.4.12", "mime-types": "^2.1.31", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "peerDependencies": { "webpack": "^5.0.0" }, "optionalPeers": ["webpack"] }, "sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw=="], "webpack-hot-middleware": ["webpack-hot-middleware@2.26.1", "", { "dependencies": { "ansi-html-community": "0.0.8", "html-entities": "^2.1.0", "strip-ansi": "^6.0.0" } }, "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A=="], - "webpack-sources": ["webpack-sources@3.5.1", "", {}, "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw=="], + "webpack-sources": ["webpack-sources@3.6.0", "", {}, "sha512-EIMmPVvNI0CYXyRPp34F2Qk7W37/BZVZIofS6LAUnrNjCVahS+hO4mxwFJDjQjODTGZDtVzLYoN2+1dYkhk9qA=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], - "whatwg-url": ["whatwg-url@17.1.0", "", { "dependencies": { "@exodus/bytes": "^1.15.1", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw=="], + "whatwg-url": ["whatwg-url@17.3.0", "", { "dependencies": { "@exodus/bytes": "^1.16.0", "tr46": "^7.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-KwOAJrpf+GTgBvcBQJcP38sMF0DsQKdwtG+TxM2pW44P6E5k0hDbWrDmyuJDIF0Uef0aQq0ZoJs+5oI+k8ddhg=="], "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], - "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], + "which-typed-array": ["which-typed-array@1.1.24", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="], "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], @@ -2157,7 +2164,7 @@ "yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], @@ -2177,13 +2184,19 @@ "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@base-ui/react/@babel/runtime": ["@babel/runtime@8.0.7", "", {}, "sha512-k0hagDDq/6w4v+0Sp4wFvIz/yswH5O7Va6aRTnD7N62Tnborp4X+YB+H3XUcWVSR4aE47WPEXiIIZchBs0GmYg=="], + + "@base-ui/utils/@babel/runtime": ["@babel/runtime@8.0.7", "", {}, "sha512-k0hagDDq/6w4v+0Sp4wFvIz/yswH5O7Va6aRTnD7N62Tnborp4X+YB+H3XUcWVSR4aE47WPEXiIIZchBs0GmYg=="], + + "@base-ui/utils/reselect": ["reselect@5.3.0", "", {}, "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg=="], + "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "@dotenvx/dotenvx/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - "@dotenvx/dotenvx/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + "@dotenvx/dotenvx/undici": ["undici@7.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="], "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], @@ -2195,12 +2208,20 @@ "@reduxjs/toolkit/reselect": ["reselect@5.3.0", "", {}, "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg=="], - "@storybook/builder-webpack5/css-loader": ["css-loader@7.1.4", "", { "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.40", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.6.3" }, "peerDependencies": { "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "webpack": "^5.27.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw=="], + "@shadcn/registry/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "@shadcn/registry/undici": ["undici@7.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="], + + "@storybook/builder-webpack5/css-loader": ["css-loader@7.1.5", "", { "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.40", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.6.3" }, "peerDependencies": { "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "webpack": "^5.27.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-Q7iAfQkU2twNBryKX/vGAlE+GAmkF7quhSzAGNK8fBimxk3+tqg245rH52UPb9dioBolBc8rP0ihmHBaDTJQBA=="], "@storybook/builder-webpack5/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "@storybook/builder-webpack5/magic-string": ["magic-string@1.4.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ=="], + "@storybook/builder-webpack5/style-loader": ["style-loader@4.0.0", "", { "peerDependencies": { "webpack": "^5.27.0" } }, "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA=="], + "@storybook/preset-react-webpack/magic-string": ["magic-string@1.4.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ=="], + "@storybook/react-docgen-typescript-plugin/find-cache-dir": ["find-cache-dir@3.3.2", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], @@ -2225,7 +2246,7 @@ "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="], + "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.12", "", {}, "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg=="], "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], @@ -2237,6 +2258,8 @@ "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "body-parser/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + "browserify-sign/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -2249,8 +2272,6 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "data-urls/whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], - "diffie-hellman/bn.js": ["bn.js@4.12.5", "", {}, "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ=="], "dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], @@ -2263,7 +2284,7 @@ "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "express/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], @@ -2277,7 +2298,7 @@ "htmlparser2/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], - "jest-worker/@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="], + "jest-worker/@types/node": ["@types/node@26.6.5", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg=="], "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], @@ -2295,6 +2316,8 @@ "mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "needle/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "next/postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], @@ -2317,6 +2340,8 @@ "public-encrypt/bn.js": ["bn.js@4.12.5", "", {}, "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ=="], + "raw-body/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + "react-docgen/strip-indent": ["strip-indent@4.1.1", "", {}, "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA=="], "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -2333,17 +2358,13 @@ "ripemd160/hash-base": ["hash-base@3.1.2", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.1" } }, "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg=="], - "rolldown/@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], - - "schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "rolldown/@oxc-project/types": ["@oxc-project/types@0.154.0", "", {}, "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA=="], "send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "shadcn/open": ["open@11.0.1", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.0", "wsl-utils": "^1.0.0" } }, "sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw=="], - - "shadcn/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + "shadcn/open": ["open@11.0.4", "", { "dependencies": { "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.1", "wsl-utils": "^1.0.0" } }, "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg=="], - "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "shadcn/undici": ["undici@7.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -2353,6 +2374,8 @@ "stream-http/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "stream-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], @@ -2365,9 +2388,9 @@ "vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - "vitest/@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="], + "vitest/@vitest/expect": ["@vitest/expect@4.1.12", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.12", "@vitest/utils": "4.1.12", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ=="], - "vitest/@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="], + "vitest/@vitest/spy": ["@vitest/spy@4.1.12", "", {}, "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg=="], "webpack-hot-middleware/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -2393,7 +2416,9 @@ "@storybook/react-docgen-typescript-plugin/find-cache-dir/pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], - "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], + + "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], @@ -2409,7 +2434,7 @@ "fork-ts-checker-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], - "jest-worker/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + "jest-worker/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], "md5.js/hash-base/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -2419,7 +2444,9 @@ "ripemd160/hash-base/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "shadcn/open/wsl-utils": ["wsl-utils@1.0.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA=="], + "shadcn/open/wsl-utils": ["wsl-utils@1.0.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.2.0" } }, "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ=="], + + "stream-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], @@ -2443,7 +2470,7 @@ "vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], - "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "vitest/@vitest/expect/chai": ["chai@6.3.0", "", {}, "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg=="], "webpack-hot-middleware/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -2465,8 +2492,6 @@ "ripemd160/hash-base/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "shadcn/open/wsl-utils/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], - "@storybook/react-docgen-typescript-plugin/find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "@storybook/react-docgen-typescript-plugin/find-cache-dir/pkg-dir/find-up/path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], diff --git a/components/auto-foreground.tsx b/components/auto-foreground.tsx index 65eda93f..d9504b66 100644 --- a/components/auto-foreground.tsx +++ b/components/auto-foreground.tsx @@ -20,9 +20,14 @@ import { type ThemeForegroundOptions, } from "@/lib/oklch-utils"; -const FG_STORAGE_KEY = "sistine-fg"; -const RAMP_KEY = "sistine-ramp"; -const FG_EVENT = "sistine-fg"; +/** localStorage key holding the persisted {@link FgConfig}. Exported so an app that reads or seeds the + * config (a settings screen, a pre-hydration script) never retypes the string. */ +export const FG_STORAGE_KEY = "sistine-fg"; +/** localStorage key holding the persisted {@link RampConfig}. */ +export const RAMP_KEY = "sistine-ramp"; +/** Window event AutoForeground listens for to re-band: dispatch it after changing the tint, the mode or + * the stored config (`window.dispatchEvent(new Event(FG_EVENT))`). */ +export const FG_EVENT = "sistine-fg"; // Dev instrumentation: set localStorage["sistine-fg-debug"] = "1" (then reload) to log each update()'s // wall-time and how far it nudges --foreground off the CSS baseline (the "marginal bump"). Off by default. diff --git a/components/component-preview.tsx b/components/component-preview.tsx index 74d28a74..79b6da4b 100644 --- a/components/component-preview.tsx +++ b/components/component-preview.tsx @@ -1,6 +1,8 @@ "use client"; import { + BellIcon, + FolderOpenIcon, GearIcon, HouseIcon, TrayIcon as Inbox, @@ -37,6 +39,7 @@ import { Carousel } from "@/components/ui/carousel"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; import { Checkbox } from "@/components/ui/checkbox"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "@/components/ui/combobox"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { ContextMenu, @@ -48,7 +51,17 @@ import { } from "@/components/ui/context-menu"; import { Cropper } from "@/components/ui/cropper"; import { DatePickerInput } from "@/components/ui/date-picker-input"; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { + Dialog, + DialogBody, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; import { Drawer, DrawerClose, @@ -67,13 +80,30 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"; import { EmptyState, EmptyStateDescription, EmptyStateIcon, EmptyStateTitle } from "@/components/ui/empty-state"; +import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet } from "@/components/ui/field"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; import { Input } from "@/components/ui/input"; import { InputGroup } from "@/components/ui/input-group"; import { InputOTP } from "@/components/ui/input-otp"; +import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from "@/components/ui/item"; +import { Kbd, KbdGroup } from "@/components/ui/kbd"; import { Label } from "@/components/ui/label"; import { MenuBar, MenuBarItem } from "@/components/ui/menu-bar"; +import { + Menubar, + MenubarCheckboxItem, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarSeparator, + MenubarShortcut, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +} from "@/components/ui/menubar"; import { ModeToggle } from "@/components/ui/mode-toggle"; import { NavigationMenu, @@ -93,6 +123,7 @@ import { PaginationPrevious, } from "@/components/ui/pagination"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Progress } from "@/components/ui/progress"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { ScrollArea } from "@/components/ui/scroll-area"; @@ -129,6 +160,25 @@ function DatePickerInputPreview() { return ; } +const COMBOBOX_FRUITS = [ + "Apple", + "Banana", + "Blueberry", + "Cherry", + "Grape", + "Mango", + "Pineapple", +]; + +function ProgressDemo() { + const [value, setValue] = React.useState(13); + React.useEffect(() => { + const t = setTimeout(() => setValue(66), 500); + return () => clearTimeout(t); + }, []); + return ; +} + function PaginationDemo() { const [page, setPage] = React.useState(2); const go = (e: React.MouseEvent, p: number) => { @@ -463,6 +513,15 @@ export function ComponentPreview({ componentName }: { componentName: string }) { This action cannot be undone. This will permanently delete your account and remove your data from our servers. + +

    Your projects, files and settings are deleted with the account.

    +
    + + + + + + ); @@ -952,6 +1011,41 @@ export function ComponentPreview({ componentName }: { componentName: string }) { Show Error Toast + + + + + + + + + ); + + case "progress": + return ; + + case "item": + return ( + + + + + + + Notifications + Mentions, replies and invitations. + + + + + + + + Weekly digest + A summary of the week, every Monday. + + + + + + + ); + + case "field": + return ( +
    +
    + Profile + How you appear to others. + + + Name + + + or + + + Show my profile publicly + + +
    +
    + ); + + case "menubar": + return ( + + + File + + + New tab ⌘T + + + New window ⌘N + + + + Share + + Email link + Messages + + + + + + View + + Show bookmarks bar + Show full URLs + + + + ); + + case "combobox": + return ( + + + + No fruit found. + + {(item: string) => ( + + {item} + + )} + + + + ); + case "mode-toggle": return (
    diff --git a/components/docs-sidebar.tsx b/components/docs-sidebar.tsx index ff739eb4..f778c503 100644 --- a/components/docs-sidebar.tsx +++ b/components/docs-sidebar.tsx @@ -21,6 +21,13 @@ const newComponents = new Set([ "date-picker-input", "context-menu", "carousel", + "kbd", + "empty", + "progress", + "item", + "field", + "menubar", + "combobox", ]); type NavItem = { diff --git a/components/foreground-tester.tsx b/components/foreground-tester.tsx index 97e78583..590e5106 100644 --- a/components/foreground-tester.tsx +++ b/components/foreground-tester.tsx @@ -2,7 +2,7 @@ import { BellIcon, GearIcon, HeartIcon, MagnifyingGlassIcon, StarIcon } from "@phosphor-icons/react"; import * as React from "react"; -import { type FgPalette, readFgConfig, readRampConfig, writeFgConfig } from "@/components/auto-foreground"; +import { FG_EVENT, type FgPalette, readFgConfig, readRampConfig, writeFgConfig } from "@/components/auto-foreground"; import { Card, CardContent } from "@/components/ui/card"; import { Slider } from "@/components/ui/slider"; import { @@ -224,10 +224,10 @@ export function ForegroundTester({ live = false, palettes = DEFAULT_PALETTES }: "style", ], }); - window.addEventListener("sistine-fg", read); + window.addEventListener(FG_EVENT, read); return () => { obs.disconnect(); - window.removeEventListener("sistine-fg", read); + window.removeEventListener(FG_EVENT, read); }; }, []); diff --git a/components/glass-tint-switcher.tsx b/components/glass-tint-switcher.tsx index 56d95e58..ffe09c70 100644 --- a/components/glass-tint-switcher.tsx +++ b/components/glass-tint-switcher.tsx @@ -1,6 +1,7 @@ "use client"; import * as React from "react"; +import { FG_EVENT } from "@/components/auto-foreground"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Slider } from "@/components/ui/slider"; @@ -336,7 +337,7 @@ function emitFg(v: { h: number; c: number; a: number; base: PresetValue; l: numb "--accent-c": v.accC, }; window.dispatchEvent( - new CustomEvent("sistine-fg", { + new CustomEvent(FG_EVENT, { detail, }), ); @@ -490,7 +491,7 @@ export function GlassTintSwitcher() { const computed = Number.parseFloat(getComputedStyle(root).getPropertyValue("--glass-opaque-l")); setLightness(Number.isFinite(computed) ? computed : mode === "dark" ? 36.4 : 88); } - window.dispatchEvent(new Event("sistine-fg")); + window.dispatchEvent(new Event(FG_EVENT)); }; apply(); const obs = new MutationObserver(apply); @@ -543,7 +544,7 @@ export function GlassTintSwitcher() { from the surface you can see rather than from the preset table's light-mode value. */ const ea = Number.parseFloat(cs.getPropertyValue("--glass-tint-a")); if (Number.isFinite(ea)) setA(ea); - window.dispatchEvent(new Event("sistine-fg")); + window.dispatchEvent(new Event(FG_EVENT)); }; // Dragging a slider keeps a bespoke base (the frescoes) — data-glass-tint stays — diff --git a/components/pattern-background.tsx b/components/pattern-background.tsx index 4e2ec315..e9d1d425 100644 --- a/components/pattern-background.tsx +++ b/components/pattern-background.tsx @@ -24,7 +24,28 @@ export const PATTERN_STYLES: PatternStyle[] = [ "chase", ]; -/** Patterns that animate (CSS keyframes) — gates the speed control and shares the --pat-dur/--pat-play pace. */ +/** + * The two ways a scene stops: reduced motion, and Speed 0. + * + * Both REMOVE the animation rather than pausing it. `animation-play-state: paused` + * looks like it should be free — the transform never advances — but a paused + * animation still leaves the element in an animating state, and WebKit keeps + * re-rendering its layer. On `.au-curtain` that layer carries a `filter: blur()` + * and a two-layer `mask-composite`, six of them at full-viewport size, so "paused" + * was measurably NOT free (AMXSF-559). `animation: none` also drops `will-change`, + * releasing the promoted layer — `will-change` on a stopped animation is a + * promoted layer doing no work. + * + * Emitted from one helper so the two conditions cannot drift apart: a scene that + * respects reduced motion but not Speed 0 was exactly the bug. + */ +export function stopRules(...selectors: string[]): string { + const stop = "{ animation: none; will-change: auto; }"; + return `@media (prefers-reduced-motion: reduce) { ${selectors.join(", ")} ${stop} } +${selectors.map((s) => `[data-pat-static] ${s}`).join(", ")} ${stop}`; +} + +/** Patterns that animate (CSS keyframes) — gates the speed control and shares the --pat-dur pace. */ export const ANIMATED_PATTERNS = new Set([ "synthwave", "moonrise", @@ -68,7 +89,7 @@ const DENSITY_SCALE: Record = { }; /** Tile-size scale for the geometric tiles (dots / grid) — a much airier ladder: the old sparse (1.4) * is the new DENSE, with medium and sparse extrapolated outward on the same ~1.4× step. */ -const TILE_SCALE: Record = { +export const TILE_SCALE: Record = { sparse: 2.8, medium: 2, dense: 1.4, @@ -588,7 +609,6 @@ const SYNTHWAVE_STYLES = ` .sw-grid-rungs { background-image: linear-gradient(to bottom, var(--sw-line) 0 2px, transparent 2px); animation: sw-grid-scroll var(--pat-dur, 8s) linear infinite; - animation-play-state: var(--pat-play, running); will-change: var(--pat-wc, transform); } .sw-grid-rails { @@ -598,9 +618,7 @@ const SYNTHWAVE_STYLES = ` from { transform: rotateX(78deg) translateY(0); } to { transform: rotateX(78deg) translateY(4vmin); } } -@media (prefers-reduced-motion: reduce) { - .sw-grid-rungs { animation: none; will-change: auto; } -}`; +${stopRules(".sw-grid-rungs")}`; /* The moonrise variant — a purple-blue moon over a midnight sky, no scanlines — shares every structural class; its [data-pattern="moonrise"] palette blocks swap the colors, and its disc is the shared .scene-moon from SKY_STYLES (whose placement defaults ARE moonrise's horizon-dipping moon). */ @@ -720,7 +738,6 @@ const CLOUDS_STYLES = ` background-repeat: repeat-x; background-size: var(--cl-tile) 100%; animation: cl-drift calc(var(--pat-dur, 8s) * var(--cl-k)) linear infinite; - animation-play-state: var(--pat-play, running); will-change: var(--pat-wc, transform); } @keyframes cl-drift { @@ -821,9 +838,7 @@ const CLOUDS_STYLES = ` height: 26%; background: linear-gradient(to bottom, transparent 0%, var(--cl-haze) 42%, transparent 100%); } -@media (prefers-reduced-motion: reduce) { - .cl-deck { animation: none; will-change: auto; } -}`; +${stopRules(".cl-deck")}`; // Dune — drawn from the aerial golden-hour references: ridges receding into haze at the shared 42% // horizon. The ground is the farthest sand sheet and SIX SVG-masked tiers stack in front. The scene's @@ -1136,7 +1151,6 @@ const DUNE_STYLES = ` background-repeat: repeat; background-size: var(--du-tx) var(--du-ty); animation: du-blow calc(var(--pat-dur, 8s) * var(--du-k)) linear infinite; - animation-play-state: var(--pat-play, running); will-change: var(--pat-wc, transform); } @keyframes du-blow { @@ -1167,7 +1181,6 @@ const DUNE_STYLES = ` background-repeat: repeat-x; background-size: var(--du-vt) 100%; animation: du-veil-drift calc(var(--pat-dur, 8s) * var(--du-vk)) linear infinite; - animation-play-state: var(--pat-play, running); will-change: var(--pat-wc, transform); } @keyframes du-veil-drift { @@ -1229,9 +1242,7 @@ const DUNE_STYLES = ` radial-gradient(1px 1px at 64% 82%, var(--du-grain), transparent), radial-gradient(1.1px 1px at 90% 26%, var(--du-grain), transparent); } -@media (prefers-reduced-motion: reduce) { - .du-grain-tile, .du-veil { animation: none; will-change: auto; } -}`; +${stopRules(".du-grain-tile", ".du-veil")}`; // Aurora — the borealis over a MIRROR-CALM WATERLINE, no moon (a moonless sky is when the lights are // most vivid; the pattern shares only the stars). Three curtain layers paint slanted ray columns: each is @@ -1379,7 +1390,6 @@ const AURORA_STYLES = ` au-sway calc(var(--pat-dur, 8s) * var(--au-ks, 3)) ease-in-out infinite alternate, au-shimmer calc(var(--pat-dur, 8s) * var(--au-kp, 2)) ease-in-out infinite alternate; animation-delay: var(--au-delay, 0s); - animation-play-state: var(--pat-play, running); will-change: var(--pat-wc, transform), var(--pat-wc, opacity); } @keyframes au-sway { @@ -1538,7 +1548,6 @@ const AURORA_STYLES = ` repeating-linear-gradient(to bottom, var(--au-rip-hi) 0 1px, transparent 1px 8px), repeating-linear-gradient(to bottom, transparent 0 14px, var(--au-rip-lo) 14px 16px, transparent 16px 24px); animation: au-ripple-drift calc(var(--pat-dur, 8s) * 1.5) linear infinite; - animation-play-state: var(--pat-play, running); will-change: var(--pat-wc, transform); } @keyframes au-ripple-drift { @@ -1620,9 +1629,7 @@ const AURORA_STYLES = ` background: linear-gradient(to bottom, transparent 0%, var(--au-glow) 42%, transparent 100%); opacity: 0.75; } -@media (prefers-reduced-motion: reduce) { - .au-curtain, .au-ripples { animation: none; will-change: auto; } -}`; +${stopRules(".au-curtain", ".au-ripples")}`; // Starfield — a deep night field by night, a "morning haze" by day: pastel accent sky, the nebulae as // bright wisps, and the glints SOFTENED but kept (identity over astronomy — hiding the stars would @@ -2070,21 +2077,22 @@ const PAC_STYLES = ` 0%, 100% { transform: translateX(0); } 50% { transform: translateX(4px); } } -.pac-runner, .pac-man, .pac-ghost-runner, .pac-ghost, -.pac-vrunner, .pac-vman, .pac-vghost-runner, .pac-vghost { - animation-play-state: var(--pat-play, running); -} -@media (prefers-reduced-motion: reduce) { - .pac-runner, .pac-man, .pac-ghost-runner, .pac-ghost, - .pac-vrunner, .pac-vman, .pac-vghost-runner, .pac-vghost { animation: none; will-change: auto; } -}`; +${stopRules(".pac-runner", ".pac-man", ".pac-ghost-runner", ".pac-ghost", ".pac-vrunner", ".pac-vman", ".pac-vghost-runner", ".pac-vghost")}`; + +/** The geometric tiles' size at scale 1, in px: the grid's cell and the dots' spacing. Exported so a + * preview can size its tiles from the same numbers the wallpaper uses (Agent Muse's PatternSwatch). */ +export const GEOMETRIC_TILE = { + grid: 44, + dots: 24, +} as const; -function styleFor(style: PatternStyle, density: PatternDensity): CSSProperties { +function styleFor(style: PatternStyle, density: PatternDensity, tileScale?: number): CSSProperties { const bg = "var(--background)"; // Ink follows the accent (fallback: tint hue) so every geometric pattern recolors with the accent knob. const ink = "oklch(0.6 0.16 var(--accent-h, var(--glass-tint-h)) / 0.5)"; // Density scales the tile size (dots/grid use the airier TILE_SCALE ladder; chase has its own cells). - const d = TILE_SCALE[density]; + // `tileScale` overrides it for a preview that must fit several tiles in a small box. + const d = tileScale ?? TILE_SCALE[density]; const px = (n: number) => `${Math.round(n * d)}px`; const sq = (n: number) => `${px(n)} ${px(n)}`; @@ -2093,13 +2101,13 @@ function styleFor(style: PatternStyle, density: PatternDensity): CSSProperties { return { backgroundColor: bg, backgroundImage: `linear-gradient(${ink} 1px, transparent 1px), linear-gradient(90deg, ${ink} 1px, transparent 1px)`, - backgroundSize: sq(44), + backgroundSize: sq(GEOMETRIC_TILE.grid), }; case "dots": return { backgroundColor: bg, backgroundImage: `radial-gradient(${ink} 1.6px, transparent 1.8px)`, - backgroundSize: sq(24), + backgroundSize: sq(GEOMETRIC_TILE.dots), }; case "mesh": { // Mesh blobs center on the accent (hue + vividness) when it's on, else the tint's content hue — @@ -2159,6 +2167,7 @@ export function PatternBackground({ speed = 8, disc = "right", sand = "breeze", + tileScale, }: { style?: PatternStyle; density?: PatternDensity; @@ -2166,13 +2175,16 @@ export function PatternBackground({ disc?: "left" | "center" | "right"; /** Blowing-sand intensity for the dune scene — drives the wind veils and the airborne specks. */ sand?: "still" | "breeze" | "storm"; + /** The geometric tiles' (dots, grid) scale, in place of the density's: a picker chip draws smaller + * tiles than the wallpaper, or a 48px chip shows one dot of a 48px-spaced field and no grid line. */ + tileScale?: number; }) { - // Animated patterns share one pace: --pat-dur (loop seconds) + --pat-play (paused when speed is 0/static). + // Animated patterns share one pace: --pat-dur (loop seconds). const patDur = speed > 0 ? `${speed}s` : "8s"; - const patPlay = speed > 0 ? "running" : "paused"; - // Static means nothing moves, so stop asking the compositor to keep a layer ready for it — - // `will-change` on a paused animation is a promoted layer doing no work. Reduced motion gets - // the same release from each scene's prefers-reduced-motion block. + // Static means nothing moves, so the animation is REMOVED, not paused — `data-pat-static` + // is what `stopRules` keys on, and it takes `will-change` down with it. This replaced a + // `--pat-play: paused` knob that left every scene layer live; see stopRules for why. + const patStatic = speed > 0 ? undefined : ""; const patWc = speed > 0 ? "transform" : "auto"; // Sun/moon horizontal placement for the horizon scenes (off-center clears centered page content). const discX = { @@ -2187,12 +2199,12 @@ export function PatternBackground({ style={ { "--pat-dur": patDur, - "--pat-play": patPlay, "--pat-wc": patWc, "--pac-cell": `${Math.round(34 * DENSITY_SCALE[density])}px`, } as CSSProperties } data-pattern="chase" + data-pat-static={patStatic} >
    @@ -2276,11 +2288,11 @@ export function PatternBackground({ "--sw-disc-x": discX, // Grid scroll pace — one cell every --pat-dur seconds (shared pace across animated patterns). "--pat-dur": patDur, - "--pat-play": patPlay, "--pat-wc": patWc, } as CSSProperties } data-pattern={style} + data-pat-static={patStatic} > @@ -2316,11 +2328,11 @@ export function PatternBackground({ // CLOUDS_STYLES' [data-pattern] palette blocks so the .dark day/night switch can override them. "--sw-disc-x": discX, "--pat-dur": patDur, - "--pat-play": patPlay, "--pat-wc": patWc, } as CSSProperties } data-pattern="clouds" + data-pat-static={patStatic} > @@ -2378,11 +2390,11 @@ export function PatternBackground({ "--du-sand-o": sandVeil, "--du-grain-o": sandGrain, "--pat-dur": patDur, - "--pat-play": patPlay, "--pat-wc": patWc, } as CSSProperties } data-pattern="dune" + data-pat-static={patStatic} > @@ -2440,11 +2452,11 @@ export function PatternBackground({ // Sun placement (day only — the scene is moonless at night) plus the shared pace vars. "--sw-disc-x": discX, "--pat-dur": patDur, - "--pat-play": patPlay, "--pat-wc": patWc, } as CSSProperties } data-pattern="aurora" + data-pat-static={patStatic} > @@ -2506,7 +2518,7 @@ export function PatternBackground({ return (
    ); diff --git a/components/ui/button.tsx b/components/ui/button.tsx index d5452a5b..9bf8836e 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -35,7 +35,7 @@ const buttonVariants = cva( "text-foreground border-2 border-foreground/20 hover:border-foreground/40 dark:border-white/40 dark:hover:border-white/60 dark:text-white transition active:border-foreground/50 active:shadow-[var(--press-shadow-sm)]", secondary: "text-foreground hover:opacity-90 transition active:opacity-80 active:shadow-[var(--press-shadow-strong)]", ghost: - "border border-border hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 active:bg-accent/80 dark:active:bg-accent/60 active:shadow-[var(--press-shadow-sm)]", + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 active:bg-accent/80 dark:active:bg-accent/60 active:shadow-[var(--press-shadow-sm)]", link: "text-primary underline-offset-4 hover:underline active:opacity-80", }, size: { diff --git a/components/ui/chart.tsx b/components/ui/chart.tsx index adf432f3..90ef1605 100644 --- a/components/ui/chart.tsx +++ b/components/ui/chart.tsx @@ -57,6 +57,62 @@ function useChart() { return context; } +type PlotSize = { + width: number; + height: number; +}; + +/** + * The plot box's size, measured by ONE ResizeObserver on an element whose size comes from CSS alone. + * + * Recharts' percent-sized ResponsiveContainer observes its own div, and the container used to give that + * div `minHeight={150}` inside an `aspect-video` box: measure, grow to the floor, measure again — a + * ResizeObserver feedback loop WebKit reports as "ResizeObserver loop completed with undelivered + * notifications" and re-enters every frame. Here the measured box never contains the chart in flow (the + * chart sits in an absolute layer), so what it measures cannot change what it measures. + * + * - `null` until the first non-empty measurement: nothing renders at 0×0, which is what a chart in a + * hidden (display: none) tab measures, and why recharts warned "width(0) and height(0) of chart + * should be greater than 0" on every pass. + * - A 0×0 measurement after that is ignored, so a tab that hides keeps its last size rather than + * unmounting the chart. + * - `"unobserved"` when there is no ResizeObserver: the caller falls back to the percent container. + */ +function usePlotSize(ref: React.RefObject): PlotSize | "unobserved" | null { + const [size, setSize] = React.useState(null); + + React.useEffect(() => { + const el = ref.current; + if (!el) return; + if (typeof ResizeObserver === "undefined") { + setSize("unobserved"); + return; + } + const observer = new ResizeObserver((entries) => { + const box = entries[entries.length - 1]?.contentRect; + if (!box) return; + /* Floor, not round: a chart half a pixel wider than its layer would overflow it. */ + const width = Math.floor(box.width); + const height = Math.floor(box.height); + if (width <= 0 || height <= 0) return; + setSize((prev) => + prev !== null && prev !== "unobserved" && prev.width === width && prev.height === height + ? prev + : { + width, + height, + }, + ); + }); + observer.observe(el); + return () => observer.disconnect(); + }, [ + ref, + ]); + + return size; +} + const ChartContainer = React.forwardRef< HTMLDivElement, React.ComponentProps<"div"> & { @@ -77,6 +133,8 @@ const ChartContainer = React.forwardRef< }; const m = materialSurface(variant === "default" ? null : SURFACE_ROLE, axes); + const plotRef = React.useRef(null); + const plot = usePlotSize(plotRef); return ( in both Recharts 2 and 3, so it survives the structure change. (A CSS `fill` beats the // fill presentation attribute Recharts sets, so no !important is needed.) - "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick-value]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none", + /* ONE size contract: the caller's height (`h-[300px]`), else the aspect. `min-h-[150px]` is the + floor the old `minHeight={150}` gave the chart, moved onto this box so it no longer feeds back + through what the observer measures; a caller's min-h-* still wins. */ + "flex min-h-[150px] aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick-value]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none", variants[variant], className, )} {...rest} > - {children} +
    + {plot === "unobserved" ? ( + {children} + ) : plot ? ( + /* Absolute, so the chart never sizes the box being measured. Numeric width + height take + recharts' fixed-size path: no size-detector div and no second observer. */ +
    + + {children} + +
    + ) : null} +
    ); diff --git a/components/ui/combobox.tsx b/components/ui/combobox.tsx new file mode 100644 index 00000000..98f0a542 --- /dev/null +++ b/components/ui/combobox.tsx @@ -0,0 +1,282 @@ +"use client"; + +import { Combobox as ComboboxPrimitive } from "@base-ui/react"; +import { CaretDownIcon, CheckIcon, XIcon } from "@phosphor-icons/react"; +import * as React from "react"; + +import { type MaterialAxisProps, materialSurface, splitAxisProps } from "@/lib/material"; +import { cn } from "@/lib/utils"; +import { Button } from "./button"; +import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "./input-group"; + +/** + * An input with a filtered list of options, single or multiple (chips), on Base UI's Combobox: the + * primitive shadcn's combobox now uses, because Radix has none. The field is Sistine's InputGroup; the + * popup is bordered, veiled glass like every other menu; the chips field is a bordered small-tier glass + * surface like Input. + * + * Base UI marks state with presence attributes (`data-open`, `data-highlighted`, `data-disabled`) and + * omits them when off, so the bare `data-disabled:` variant is safe here, unlike cmdk (see command.tsx). + */ + +/* Roles: the popup is a read-through overlay; the chips field is an input-sized surface. */ +const POPUP_ROLE = { + border: true, + veil: true, +}; +const CHIPS_ROLE = { + border: true, + size: "sm" as const, +}; + +const Combobox = ComboboxPrimitive.Root; + +function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { + return ; +} + +function ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) { + return ( + + {children} + + + ); +} + +function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { + return ( + } + className={cn(className)} + {...props} + > + + + ); +} + +function ComboboxInput({ + className, + children, + disabled = false, + showTrigger = true, + showClear = false, + ...props +}: ComboboxPrimitive.Input.Props & { + showTrigger?: boolean; + showClear?: boolean; +}) { + return ( + + } {...props} /> + + {showTrigger && ( + + + + )} + {showClear && } + + {children} + + ); +} + +function ComboboxContent({ + className, + side = "bottom", + sideOffset = 6, + align = "start", + alignOffset = 0, + anchor, + variant = "glass", + ...props +}: ComboboxPrimitive.Popup.Props & + Pick & + MaterialAxisProps & { + variant?: "default" | "glass"; + }) { + const [axes, rest] = splitAxisProps(props); + const m = materialSurface(variant === "default" ? null : POPUP_ROLE, axes); + + return ( + + + + + + ); +} + +function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { + return ( + + ); +} + +function ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) { + return ( + + {children} + }> + + + + ); +} + +function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { + return ; +} + +function ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) { + return ( + + ); +} + +function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { + return ; +} + +function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { + return ( + + ); +} + +function ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) { + return ; +} + +function ComboboxChips({ + className, + ...props +}: React.ComponentPropsWithRef & ComboboxPrimitive.Chips.Props & MaterialAxisProps) { + const [axes, rest] = splitAxisProps(props); + const m = materialSurface(CHIPS_ROLE, axes); + + return ( + + ); +} + +function ComboboxChip({ + className, + children, + showRemove = true, + ...props +}: ComboboxPrimitive.Chip.Props & { + showRemove?: boolean; +}) { + return ( + + {children} + {showRemove && ( + } + className="-ml-1 opacity-50 hover:opacity-100" + data-slot="combobox-chip-remove" + > + + + )} + + ); +} + +function ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) { + return ( + + ); +} + +function useComboboxAnchor() { + return React.useRef(null); +} + +export { + Combobox, + ComboboxChip, + ComboboxChips, + ComboboxChipsInput, + ComboboxClear, + ComboboxCollection, + ComboboxContent, + ComboboxEmpty, + ComboboxGroup, + ComboboxInput, + ComboboxItem, + ComboboxLabel, + ComboboxList, + ComboboxSeparator, + ComboboxTrigger, + ComboboxValue, + useComboboxAnchor, +}; diff --git a/components/ui/command.tsx b/components/ui/command.tsx index faeafe7c..a1f57f83 100644 --- a/components/ui/command.tsx +++ b/components/ui/command.tsx @@ -144,13 +144,31 @@ const CommandSeparator = React.forwardRef< )); CommandSeparator.displayName = CommandPrimitive.Separator.displayName; +/** + * ⚠️ The disabled styles MUST be `data-[disabled=true]:`, never bare + * `data-[disabled]:` — see `tests/command.test.tsx`. + * + * cmdk renders `data-disabled={!!disabled}` unconditionally, and React + * stringifies booleans on `data-*` attributes, so an ENABLED item still carries + * `data-disabled="false"` in the DOM. Tailwind compiles the bare variant to the + * attribute-PRESENCE selector `[data-disabled]`, which matches `"false"` just as + * happily as `"true"` — so `pointer-events: none` landed on every item and no + * entry in a picker could be clicked. Keyboard selection still worked, which is + * what made it look like a mouse-only quirk rather than a stylesheet bug: cmdk + * dispatches its own `cmdk-item-select` event straight at the node on Enter, and + * that bypasses hit-testing entirely. + * + * The Radix menus next door keep the bare variant safely — Radix omits the + * attribute outright when the item is enabled. cmdk is the one that always + * emits it. + */ const CommandItem = React.forwardRef, React.ComponentPropsWithoutRef>( ({ className, ...props }, ref) => ( + + + ); }); ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; @@ -171,7 +176,7 @@ const ContextMenuItem = React.forwardRef< data-inset={inset} data-variant={variant} className={cn( - "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-foreground/10 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", + "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", inset && "pl-8", className, )} @@ -188,7 +193,7 @@ const ContextMenuCheckboxItem = React.forwardRef< ref={ref} data-slot="context-menu-checkbox-item" className={cn( - "relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-foreground/10 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} checked={checked} @@ -212,7 +217,7 @@ const ContextMenuRadioItem = React.forwardRef< ref={ref} data-slot="context-menu-radio-item" className={cn( - "relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-foreground/10 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 98b29fbe..200d92f5 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -58,9 +58,9 @@ const DialogContent = React.forwardRef< MaterialAxisProps & { /** Elevated backdrop blur (folded from the glass wrapper). */ animated?: boolean; - /** Styles the inner scrolling wrapper around `children`. `className` targets the OUTER surface, - * so without this a consumer has no way to change the body's layout — swap the grid for flex, - * drop the gap, or take the scrolling over entirely. + /** Styles the layout wrapper around `children` (`data-slot="dialog-layout"`). `className` targets + * the OUTER surface, so without this a consumer has no way to change the body's layout — swap the + * grid for flex, drop the gap, or take the scrolling over entirely. * Its `-m-6 p-6` is a matched pair that cancels the dialog's padding so the scroll container * stops clipping child shadows; override one and you must override the other. */ bodyClassName?: string; @@ -105,7 +105,17 @@ const DialogContent = React.forwardRef< )} {...rest} > - {/* min-h-0 is load-bearing: a grid item defaults to min-height:auto, which refuses to shrink + {/* The layout wrapper has two modes. + WITHOUT a DialogBody child it is the scroll box, as it always was: header, content and footer + scroll together. + WITH one (`has-[>[data-slot=dialog-body]]:`) it becomes a non-clipping flex column and only + the body scrolls: the header and footer are chrome, not content, and inside a scroll box a + glass button's glow was sliced at whichever edge it touched (overflow-y:auto clips on all + four sides, see below), and the footer scrolled away with the text. + min-w-0 + [&>*]:min-w-0: grid and flex items default to min-width:auto, so one unbreakable + child (a long path with `truncate`) set the track WIDER than the dialog and every line laid + out to that phantom width, clipped at the border. Letting the items shrink fixes wrap/truncate. + min-h-0 is load-bearing: a grid item defaults to min-height:auto, which refuses to shrink below its content, so without it this never scrolls and the dialog just clips again. -m-6 p-6 cancels the dialog's own p-6 so this box's edges land ON the dialog's padding box instead of its content box. That matters because `overflow-y: auto` is not one-axis: when @@ -117,7 +127,16 @@ const DialogContent = React.forwardRef< overflow-hidden already clips, which does the same job and follows the rounded corners. The two utilities are a pair: overriding padding alone via bodyClassName re-offsets the body by the leftover negative margin. */} -
    {children}
    +
    *]:min-w-0", + "has-[>[data-slot=dialog-body]]:flex has-[>[data-slot=dialog-body]]:flex-col has-[>[data-slot=dialog-body]]:overflow-visible", + bodyClassName, + )} + > + {children} +
    {showCloseButton && ( ) => ( -
    +
    ); DialogHeader.displayName = "DialogHeader"; +/** + * The scrolling region of a dialog. Optional: put it between DialogHeader and DialogFooter and only it + * scrolls, with the header and footer fixed around it; leave it out and the whole dialog content scrolls + * as one, as before. + * `-mx-6 px-6` runs the scroll box out to the dialog's edges (a scroll container clips at its padding + * box, so sitting at the content box it sheared child shadows 24px inside the edge), and `-my-2 py-2` + * gives a glow 8px of room above and below without moving the content. + */ +const DialogBody = ({ className, ...props }: React.HTMLAttributes) => ( +
    *]:min-w-0", className)} + {...props} + /> +); +DialogBody.displayName = "DialogBody"; + const DialogFooter = ({ className, showCloseButton = false, @@ -148,7 +188,10 @@ const DialogFooter = ({ }) => (
    {children} @@ -181,4 +224,16 @@ const DialogDescription = React.forwardRef< )); DialogDescription.displayName = DialogPrimitive.Description.displayName; -export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger }; +export { + Dialog, + DialogBody, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx index bfe3205c..d59fa19c 100644 --- a/components/ui/dropdown-menu.tsx +++ b/components/ui/dropdown-menu.tsx @@ -87,7 +87,7 @@ const DropdownMenuSubTrigger = React.forwardRef< data-slot="dropdown-menu-sub-trigger" data-inset={inset} className={cn( - "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-foreground/10 focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-foreground/10 data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", + "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", inset && "pl-8", className, )} @@ -108,20 +108,25 @@ const DropdownMenuSubContent = React.forwardRef< const composedRef = useComposedRefs(ref, snapRef); const m = materialSurface(variant === "default" ? null : ROLE, axes); + /* Portaled like DropdownMenuContent. A glass menu's backdrop-filter makes it the containing block of + every fixed-position descendant, so a sub menu rendered inside it is placed against the menu and + clipped by its overflow instead of floating beside it. */ return ( - + + + ); }); DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; @@ -169,7 +174,7 @@ const DropdownMenuItem = React.forwardRef< data-inset={inset} data-variant={variant} className={cn( - "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-foreground/10 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", + "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", inset && "pl-8", className, )} @@ -186,7 +191,7 @@ const DropdownMenuCheckboxItem = React.forwardRef< ref={ref} data-slot="dropdown-menu-checkbox-item" className={cn( - "relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-foreground/10 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} checked={checked} @@ -210,7 +215,7 @@ const DropdownMenuRadioItem = React.forwardRef< ref={ref} data-slot="dropdown-menu-radio-item" className={cn( - "relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-foreground/10 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} diff --git a/components/ui/empty-state.tsx b/components/ui/empty-state.tsx index cabf80c3..0af1dbd5 100644 --- a/components/ui/empty-state.tsx +++ b/components/ui/empty-state.tsx @@ -4,6 +4,9 @@ import { type HoverEffect, hoverEffects } from "@/lib/hover-effects"; import { type MaterialAxisProps, materialSurface, splitAxisProps } from "@/lib/material"; import { cn } from "@/lib/utils"; +/* Legacy: prefer `empty` (components/ui/empty.tsx), which follows shadcn's anatomy. Kept so existing + imports keep working. */ + /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */ const emptyStateVariants = cva("", { variants: { diff --git a/components/ui/empty.tsx b/components/ui/empty.tsx new file mode 100644 index 00000000..4b421f69 --- /dev/null +++ b/components/ui/empty.tsx @@ -0,0 +1,115 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import type * as React from "react"; + +import { type MaterialAxisProps, materialSurface, splitAxisProps } from "@/lib/material"; +import { cn } from "@/lib/utils"; + +/** + * An empty state: a centred message where content will be, under shadcn's name and anatomy (Empty, + * EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent). `empty-state` is the older, + * Sistine-only take on the same thing; prefer this for new work. + * + * The root is plain layout by default, because an empty state usually sits INSIDE a surface that is + * already glass (a card, a table, a panel); `variant="glass"` makes it a surface of its own. + */ + +/* Role: borderless adaptive glass, only when asked for. */ +const ROLE = {}; +/* The icon tile: a small bordered chip. */ +const MEDIA_ROLE = { + border: true, + size: "sm" as const, +}; + +function Empty({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & + MaterialAxisProps & { + variant?: "default" | "glass"; + }) { + const [axes, rest] = splitAxisProps(props); + const m = materialSurface(variant === "glass" ? ROLE : null, axes); + + return ( +
    + ); +} + +function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { + return
    ; +} + +/* Variant classes carry BEHAVIOR only; the icon tile's surface comes from materialSurface. */ +const emptyMediaVariants = cva("mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", { + variants: { + variant: { + default: "bg-transparent", + icon: "size-10 rounded-lg text-foreground-ui [&_svg:not([class*='size-'])]:size-6", + }, + }, + defaultVariants: { + variant: "default", + }, +}); + +function EmptyMedia({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & VariantProps & MaterialAxisProps) { + const [axes, rest] = splitAxisProps(props); + const m = materialSurface(variant === "icon" ? MEDIA_ROLE : null, axes); + + return ( +
    + ); +} + +function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { + return
    ; +} + +function EmptyDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", className)} + {...props} + /> + ); +} + +function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ); +} + +export { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle }; diff --git a/components/ui/field.tsx b/components/ui/field.tsx new file mode 100644 index 00000000..ef344f2e --- /dev/null +++ b/components/ui/field.tsx @@ -0,0 +1,215 @@ +"use client"; + +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; +import { Label } from "./label"; +import { Separator } from "./separator"; + +/** + * Form field layout: a label, the control, its description and its error, laid out vertically, + * horizontally, or responsively by the width of the surrounding FieldGroup. Plain layout with no + * surface of its own: the controls inside it (Input, Checkbox, Select, …) carry the material. + * + * Pairs with any form library: render FieldError with the library's errors (`errors={[…]}`), or use + * react-hook-form's Controller around the control. + */ + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
    [data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", className)} + {...props} + /> + ); +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { + variant?: "legend" | "label"; +}) { + return ( + + ); +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ); +} + +const fieldVariants = cva("group/field flex w-full gap-3 data-[invalid=true]:text-destructive", { + variants: { + orientation: { + vertical: "flex-col *:w-full [&>.sr-only]:w-auto", + horizontal: + "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + responsive: + "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + }, + }, + defaultVariants: { + orientation: "vertical", + }, +}); + +function Field({ className, orientation = "vertical", ...props }: React.ComponentProps<"div"> & VariantProps) { + return ( +
    + ); +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return
    ; +} + +/* A label wrapping a whole Field becomes a selectable choice card. Its checked state reads the Radix + control inside it, which marks itself `data-state="checked"`: shadcn's `has-data-checked:` looked for a + `data-checked` attribute nothing renders, so the card never lit up. */ +function FieldLabel({ className, ...props }: React.ComponentProps) { + return ( +
    a scroll container, and a + sticky header binds to its nearest one, so it rode along with the rows. The container clips the + corners instead (see below). */ + glass: "rounded-lg", }, }, defaultVariants: { @@ -29,8 +32,11 @@ const Table = React.forwardRef< MaterialAxisProps & { /** When true, rows alternate in brightness and the per-row dividers are removed. */ striped?: boolean; + /** Classes for the wrapping container, which is the element that scrolls: pass a height and an + * overflow (`max-h-96 overflow-y-auto`) and a `sticky top-0` TableHeader pins to it. */ + containerClassName?: string; } ->(({ className, variant = "glass", striped = false, ...props }, ref) => { +>(({ className, containerClassName, variant = "glass", striped = false, ...props }, ref) => { const [axes, rest] = splitAxisProps(props); const m = materialSurface(variant === "default" ? null : ROLE, axes); @@ -39,7 +45,11 @@ const Table = React.forwardRef< : ""; return ( -
    + /* `overflow-clip`, not `overflow-hidden`: it clips the header and row fills to the rounded corners + without making the container a scroll container, so it never captures a sticky header on its own. + A consumer's `overflow-y-auto` (containerClassName) makes it the scroller on purpose; the x axis + then computes to hidden and keeps the corners clipped. */ +
    + {/* Optional. With a DialogBody only the body scrolls; the header and + footer stay put around it. Without one, everything scrolls together. */} + +

    Your projects, files and settings are deleted with the account.

    +
    + + + + + + ) @@ -988,6 +1009,158 @@ export function Example() { ) }`; + case "kbd": + return `import { Kbd, KbdGroup } from "@/components/ui/kbd" + +export function Example() { + return ( +

    + Search with{" "} + + ⌘ + K + +

    + ) +}`; + + case "empty": + return `import { FolderOpenIcon } from "@phosphor-icons/react" +import { Button } from "@/components/ui/button" +import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty" + +export function Example() { + return ( + // Plain layout by default (it usually sits inside a card); variant="glass" gives it its own surface. + + + + + + No projects yet + Create your first project to get started. + + + + + + ) +}`; + + case "progress": + return `import { Progress } from "@/components/ui/progress" + +export function Example() { + return +}`; + + case "item": + return `import { BellIcon } from "@phosphor-icons/react" +import { Item, ItemActions, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "@/components/ui/item" +import { Switch } from "@/components/ui/switch" + +export function Example() { + return ( + // variant: "default" (plain row) · "outline" (bordered glass) · "muted" (borderless glass) + + + + + + Notifications + Mentions, replies and invitations. + + + + + + ) +}`; + + case "field": + return `import { Checkbox } from "@/components/ui/checkbox" +import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet } from "@/components/ui/field" +import { Input } from "@/components/ui/input" + +export function Example() { + return ( +
    + Profile + How you appear to others. + + + Name + + + + + + Show my profile publicly + + +
    + ) +}`; + + case "menubar": + return `import { + Menubar, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarSeparator, + MenubarShortcut, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +} from "@/components/ui/menubar" + +export function Example() { + return ( + + + File + + + New tab ⌘T + + + + Share + + Email link + + + + + + ) +}`; + + case "combobox": + return `import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "@/components/ui/combobox" + +const fruits = ["Apple", "Banana", "Blueberry", "Cherry", "Grape"] + +export function Example() { + return ( + + + + No fruit found. + + {(item: string) => ( + + {item} + + )} + + + + ) +}`; + case "mode-toggle": return `import { ModeToggle } from "@/components/ui/mode-toggle" diff --git a/lib/material.ts b/lib/material.ts index c57efebc..14ec9c0c 100644 --- a/lib/material.ts +++ b/lib/material.ts @@ -4,7 +4,7 @@ import type * as React from "react"; * The Sistine material system — the single source of the material → markup mapping. * * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in - * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen) + * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen) * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes). * @@ -25,6 +25,11 @@ export interface MaterialProps { /** Material border. Three weights: `true`/"hairline" = the material's edge (1px; 0.5px under * frosted), "rim" = 2px, "frame" = 4px. */ border?: boolean | "hairline" | "rim" | "frame"; + /** The border's COLOUR. "glass" (the default) = the material's own edge, a near-white rim at the + * material's alpha, which reads as glass but disappears on light themes at any weight. "ink" = the + * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible. + * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */ + borderColor?: "glass" | "ink"; /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */ veil?: boolean; /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop @@ -60,11 +65,12 @@ export function splitAxisProps

    ( MaterialAxisProps, SplitAxisRest

    , ] { - const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props; + const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props; return [ { material, border, + borderColor, veil, diffuse, gradient, @@ -85,7 +91,7 @@ export interface MaterialAttrs { } export function glassMaterial(props: MaterialProps = {}): MaterialAttrs { - const { material, border, veil, size, diffuse, gradient, glow, sheen } = props; + const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props; if (material === "none") { return { className: "", @@ -98,6 +104,7 @@ export function glassMaterial(props: MaterialProps = {}): MaterialAttrs { border && "glass-border", border === "rim" && "glass-border-rim", border === "frame" && "glass-border-frame", + borderColor === "ink" && "glass-border-ink", veil && "glass-veil", size === "sm" && "glass-sm", size === "lg" && "glass-lg", @@ -127,6 +134,7 @@ export function materialSurface(role: MaterialProps | null, props: MaterialProps return glassMaterial({ material: props.material, border: props.border ?? role.border, + borderColor: props.borderColor ?? role.borderColor, veil: props.veil ?? role.veil, size: props.size ?? role.size, diffuse: props.diffuse ?? role.diffuse, diff --git a/lib/oklch-utils.ts b/lib/oklch-utils.ts index f079ce36..d97c102e 100644 --- a/lib/oklch-utils.ts +++ b/lib/oklch-utils.ts @@ -1224,6 +1224,10 @@ export const TONAL_MIN_L = 18; * dark twin: near black a whole 18 points of lightness read as one colour and cost ~1.5 Lc to give up, * while near white a single 3-point step is worth 6–8 Lc. */ export const TONAL_MAX_L = 97; +/** How far under the body floor a tonal pick may sit and still be kept over an extreme that does not + * clear the floor either. 72 Lc and 74.8 Lc are not a difference a reader can use in body text; ink-blue + * versus #000002 is one they see. */ +export const TONAL_HUE_SLACK_LC = 3; /** Uncertainty margin at a fully-sheer floor, decaying to 0 as the floor becomes exactly modelable. */ export const LC_MARGIN = 12; /** Baseline aim for a floor that IS modelled exactly, so certainty is not a reason to aim at the bare @@ -1313,6 +1317,14 @@ export function foregroundRamp(o: ForegroundRampOptions): ForegroundRamp { * satisfy its FLOOR, legibility outranks hue and the full ramp comes back. Guard the FLOOR, not each * band's aspiration: the `small` band asks Lc 90, which no light surface reaches at all, so testing * against band.floor there would hand the extreme straight back on every light preset. + * + * The trade is bounded from the other side too. When the tonal pick is within {@link TONAL_HUE_SLACK_LC} + * of the floor and the extreme does NOT clear it either, the extreme is only "less unreadable" by under + * that much, and the hue stays: that was body text solving to #000002 on a dark-wash preset (lapis) over + * a darker backing, where black bought 1.8 Lc and still missed the floor. A fallback that DOES clear the + * floor always wins, so nothing that was readable becomes less so. And a tonal pick far under the floor + * (the mid-tone dead zone, ~43 Lc) still hands back the extreme: there the guard's job is only to take + * whatever contrast is left. */ export function pickTonalInBand(ramp: ForegroundRamp, surface: OklchColor | string, band: ContrastBand): OklchColor { const best = pickInBand(ramp.tonal, surface, band); @@ -1320,6 +1332,7 @@ export function pickTonalInBand(ramp: ForegroundRamp, surface: OklchColor | stri const lc = (c: OklchColor) => Math.abs(apcaContrast(c, surface)); if (lc(best) >= READABLE_USAGE.body.floor) return best; const full = pickInBand(ramp.raw, surface, band); + if (lc(best) >= READABLE_USAGE.body.floor - TONAL_HUE_SLACK_LC && lc(full) < READABLE_USAGE.body.floor) return best; return lc(full) > lc(best) ? full : best; } diff --git a/lib/snap-popper.ts b/lib/snap-popper.ts index 149c2e8a..90c021c5 100644 --- a/lib/snap-popper.ts +++ b/lib/snap-popper.ts @@ -6,20 +6,23 @@ import * as React from "react"; * Keeps text crisp on Radix popper surfaces (dropdown-menu, popover, tooltip, select, hover-card, * context-menu). * - * THE DEFECT: Radix positions those surfaces by writing an inline `transform: translate(x, y)` onto a - * wrapper element. Floating UI does not round x/y, so they land on fractions whenever the trigger does - * — under browser zoom, fractional DPR, or any flex/grid trigger whose box isn't on a whole pixel. On - * its own that would be harmless, but sistine's popper content carries `backdrop-filter` (it's glass), - * which promotes it to its own compositing layer. A promoted layer at a fractional offset has its - * glyphs rasterized off the device pixel grid and the text renders visibly soft. + * THE DEFECT: on a high-density display the text in a popper rendered visibly soft. Radix positions the + * surface with an inline `transform: translate(x, y)` on a wrapper element, written from Floating UI's + * `floatingStyles`, and at a device pixel ratio of 1.5 or more Floating UI adds `will-change: transform` + * to the same style (`@floating-ui/react-dom`, `floatingStyles`). `will-change` promotes the wrapper to + * its own compositing layer, and WebKit drops subpixel (LCD) antialiasing for text on a composited layer; + * sistine's popper content is glass, so a blurred backdrop sits right behind that text. Resetting + * `will-change` to `auto` is the fix. The cost is a repaint on reposition instead of a layer move, for a + * surface that is open for seconds. * - * This is the same defect the dialogs had. There the fix was to stop using a transform to center; a - * popper can't do that, because its position IS the transform. So instead: round the translate to whole - * pixels. Sub-pixel placement is imperceptible; blurred text is not. + * The ROUNDING below predates that diagnosis. Its premise was that unrounded offsets put glyphs off the + * pixel grid, but Floating UI already rounds x/y to the device pixel grid (`roundByDPR`) before writing + * them. It is kept for browser zoom, where the device grid and the CSS-pixel grid diverge, which has not + * been verified either way; it is cheap and harmless, and removing it is a separate decision. * - * Radix rewrites that transform on scroll, resize and collision flips, so a MutationObserver re-snaps - * after each write. The observer is re-entrant — our own write retriggers it — so the write is skipped - * when the value is already integral, which terminates the loop on the first pass. + * Radix rewrites the style on scroll, resize and collision flips, so a MutationObserver re-applies both + * after each write. The observer is re-entrant (our own write retriggers it), so each write is skipped + * when the value is already what we want, which terminates the loop on the second pass at the latest. */ /** * The whole snap, as a pure string transform: given the wrapper's SPECIFIED inline `transform`, return @@ -30,7 +33,7 @@ import * as React from "react"; * - the value is already integral, so there is nothing to do and the loop terminates on pass one; * - the value isn't a translate we recognise (Radix hasn't positioned yet), so we must not invent one. * - * Split out of the hook so it can be checked without a DOM — see scripts/check-units.mjs. + * Split out of the hook so it can be checked without a DOM — see tests/snap-popper.test.tsx. */ export function snapTransform(specified: string): string | null { const parts = /^translate(?:3d)?\(\s*(-?[\d.]+)px\s*,\s*(-?[\d.]+)px/.exec(specified); @@ -57,6 +60,10 @@ export function useSnappedPopper(): React.RefCallback // re-enter forever. const next = snapTransform(wrapper.style.transform); if (next !== null) wrapper.style.transform = next; + // Floating UI's will-change (DPR >= 1.5) is what costs the text its antialiasing. Only written when + // something else is set, so the pass our own write triggers sees `auto` and stops. + const willChange = wrapper.style.willChange; + if (willChange !== "" && willChange !== "auto") wrapper.style.willChange = "auto"; }; snap(); diff --git a/lib/storybook-url.ts b/lib/storybook-url.ts index dba518e9..9549cf12 100644 --- a/lib/storybook-url.ts +++ b/lib/storybook-url.ts @@ -5,6 +5,13 @@ import { siteUrl } from "@/lib/site-url"; * In production, uses NEXT_PUBLIC_STORYBOOK_URL env var * In development, uses localhost:6006 */ +/* Registry names whose hyphen-stripped form collides with another's: `menu-bar` (the legacy button row) + and `menubar` (the Radix menu system) both become "menubar", so the legacy one's story is titled + Sistine/MenuBarLegacy. */ +const STORY_ID_OVERRIDES: Record = { + "menu-bar": "menubarlegacy", +}; + export function getStorybookUrl(componentName: string): string { /* Storybook ships inside the site at /storybook (scripts/export-storybook.js copies it into out/), so its default follows the site origin rather than restating a domain. NEXT_PUBLIC_STORYBOOK_URL still @@ -14,6 +21,6 @@ export function getStorybookUrl(componentName: string): string { // Storybook converts "Sistine/AlertDialog" to "sistine-alertdialog" // Component names from registry are kebab-case (e.g., "alert-dialog") // But Storybook URLs use lowercase without hyphens (e.g., "alertdialog") - const storybookComponentName = componentName.toLowerCase().replace(/-/g, ""); + const storybookComponentName = STORY_ID_OVERRIDES[componentName] ?? componentName.toLowerCase().replace(/-/g, ""); return `${baseUrl}/?path=/docs/sistine-${storybookComponentName}--docs`; } diff --git a/package.json b/package.json index dac9735b..d23cecfa 100644 --- a/package.json +++ b/package.json @@ -29,52 +29,53 @@ "Edge >= 120" ], "dependencies": { + "@base-ui/react": "^1.9.0", "@phosphor-icons/react": "^2.1.10", - "@tanstack/react-table": "^9.1.2", + "@tanstack/react-table": "^9.3.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.4.0", - "framer-motion": "^13.1.1", + "framer-motion": "^13.5.1", "input-otp": "^1.5.0", - "next": "^16.3.3", + "next": "^16.4.2", "next-themes": "^0.4.6", - "radix-ui": "^1.6.7", - "react": "^19.2.8", - "react-day-picker": "^10.0.1", - "react-dom": "^19.2.8", - "react-easy-crop": "^6.2.3", - "react-resizable-panels": "^4.12.3", + "radix-ui": "^1.8.1", + "react": "^19.3.1", + "react-day-picker": "^10.0.2", + "react-dom": "^19.3.1", + "react-easy-crop": "^6.2.4", + "react-resizable-panels": "^4.14.6", "recharts": "^3.10.1", - "shadcn": "^4.19.0", + "shadcn": "^4.22.1", "sonner": "^2.0.8", - "tailwind-merge": "^3.6.0", + "tailwind-merge": "^3.7.0", "tw-animate-css": "^1.4.0", "vaul": "^1.1.2", - "zod": "^4.4.3" + "zod": "^4.6.7" }, "devDependencies": { - "@biomejs/biome": "^2.5.10", - "@chromatic-com/storybook": "^5.3.0", - "@storybook/addon-a11y": "^10.5.10", - "@storybook/addon-docs": "^10.5.10", - "@storybook/addon-onboarding": "^10.5.10", - "@storybook/addon-vitest": "^10.5.10", - "@storybook/nextjs": "^10.5.10", + "@biomejs/biome": "^2.5.15", + "@chromatic-com/storybook": "^5.5.1", + "@storybook/addon-a11y": "^10.6.2", + "@storybook/addon-docs": "^10.6.2", + "@storybook/addon-onboarding": "^10.6.2", + "@storybook/addon-vitest": "^10.6.2", + "@storybook/nextjs": "^10.6.2", "@tailwindcss/postcss": "^4.3.3", "@testing-library/jest-dom": "^7.0.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.6", - "@types/node": "^24.13.3", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.5", - "@vitejs/plugin-react": "^6.1.0", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.8", + "@types/node": "^24.19.2", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", "apca-w3": "^0.1.9", - "jsdom": "^30.0.1", - "storybook": "^10.5.10", + "jsdom": "^30.1.3", + "storybook": "^10.6.2", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", - "vitest": "^4.1.11" + "vitest": "^4.1.12" }, "overrides": { "@types/react": "^19.2.18", diff --git a/public/r/accordion.json b/public/r/accordion.json index d875ef05..43f33db5 100644 --- a/public/r/accordion.json +++ b/public/r/accordion.json @@ -22,7 +22,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/alert-dialog.json b/public/r/alert-dialog.json index e0fe392e..f2ecb195 100644 --- a/public/r/alert-dialog.json +++ b/public/r/alert-dialog.json @@ -22,7 +22,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/alert.json b/public/r/alert.json index 046141c0..69cce48d 100644 --- a/public/r/alert.json +++ b/public/r/alert.json @@ -20,7 +20,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/auto-foreground.json b/public/r/auto-foreground.json index 93004192..bf5fc1d5 100644 --- a/public/r/auto-foreground.json +++ b/public/r/auto-foreground.json @@ -11,7 +11,7 @@ "files": [ { "path": "components/auto-foreground.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n showThrough as backdropShowThrough,\n boostBand,\n type ContrastBand,\n compositeSurface,\n foregroundRamp,\n formatOklch,\n glassSolidSurface,\n HARMONIC_OFFSETS,\n type HarmonicName,\n harmonicHue,\n LC_AIM_KNOWN,\n LC_MARGIN,\n pickTonalInBand,\n READABLE_USAGE,\n readableTonal,\n type ThemeForegroundOptions,\n} from \"@/lib/oklch-utils\";\n\nconst FG_STORAGE_KEY = \"sistine-fg\";\nconst RAMP_KEY = \"sistine-ramp\";\nconst FG_EVENT = \"sistine-fg\";\n\n// Dev instrumentation: set localStorage[\"sistine-fg-debug\"] = \"1\" (then reload) to log each update()'s\n// wall-time and how far it nudges --foreground off the CSS baseline (the \"marginal bump\"). Off by default.\nconst fgDebug = () => {\n try {\n return typeof localStorage !== \"undefined\" && localStorage.getItem(\"sistine-fg-debug\") === \"1\";\n } catch {\n return false;\n }\n};\n\nexport type FgPalette = ThemeForegroundOptions[\"palette\"];\nexport interface FgConfig {\n palette: FgPalette;\n /** Icon foreground hue for `--foreground-ui`: a number (0–360) pins a hue; a harmonic name\n * (\"complement\" | \"triad-1\" | \"split-2\" | … — a color-wheel relationship rotated off --harmony-h,\n * tracked live); null → icons follow the theme/text color. Always contrast-solved (APCA/ARC). */\n iconHue: number | HarmonicName | null;\n /** Heading/large-text hue for `--foreground-soft` — same semantics as iconHue (number pins, a harmonic\n * name rotates off the theme live, null → follow the chosen palette ramp). */\n softHue: number | HarmonicName | null;\n /** Fine/small-text hue for `--foreground-strong` — same semantics (null → follow the palette ramp). */\n strongHue: number | HarmonicName | null;\n}\n/** The /colors ramp generator's base color + step count, shared with the foreground. */\nexport interface RampConfig {\n l: number;\n c: number;\n h: number;\n count: number;\n}\n\nconst FG_PALETTES: FgPalette[] = [\n \"tonal\",\n \"lightness\",\n \"hue\",\n \"chroma\",\n];\nconst DEFAULT_FG: FgConfig = {\n palette: \"lightness\", // linear ramp — holds the theme's chroma, so high-contrast text reads as a soft tinted white, not gray\n iconHue: null,\n softHue: null,\n strongHue: null,\n};\n/* The tonal clip bounds, the margins and the pick/solve themselves now live in lib/oklch-utils — see\n the \"Foreground solve\" section there. They were duplicated into components/foreground-tester.tsx and\n scripts/check-contrast.mjs, and every copy had drifted from this one in a way no test could see. */\nconst DEFAULT_RAMP: RampConfig = {\n l: 60,\n c: 0.15,\n h: 255,\n count: 12, // finest ramp (12 steps/side) — the most cohesive foreground set in practice\n};\n\n/** Normalize a stored hue choice: a harmonic relationship name, a pinned number, else null. */\nconst hueChoice = (v: unknown): number | HarmonicName | null =>\n typeof v === \"string\" && v in HARMONIC_OFFSETS ? (v as HarmonicName) : typeof v === \"number\" ? v : null;\n\n/** Read the persisted foreground palette; falls back to the default (Linear). */\nexport function readFgConfig(): FgConfig {\n try {\n const raw = localStorage.getItem(FG_STORAGE_KEY);\n if (raw) {\n const parsed = JSON.parse(raw) as Partial;\n if (FG_PALETTES.includes(parsed.palette as FgPalette)) {\n return {\n palette: parsed.palette as FgPalette,\n iconHue: hueChoice(parsed.iconHue),\n softHue: hueChoice(parsed.softHue),\n strongHue: hueChoice(parsed.strongHue),\n };\n }\n }\n } catch {\n // ignore parse / storage failures\n }\n return DEFAULT_FG;\n}\n\n/** Persist the foreground config + notify AutoForeground to re-apply it site-wide. */\nexport function writeFgConfig(config: Partial): void {\n try {\n localStorage.setItem(\n FG_STORAGE_KEY,\n JSON.stringify({\n ...readFgConfig(),\n ...config,\n }),\n );\n } catch {\n // ignore storage failures\n }\n window.dispatchEvent(new Event(FG_EVENT));\n}\n\n/** Read the ramp generator's base color + count (shared from the /colors ramp card). */\nexport function readRampConfig(): RampConfig {\n try {\n const raw = localStorage.getItem(RAMP_KEY);\n if (raw) {\n const p = JSON.parse(raw) as Partial;\n if (\n [\n p.l,\n p.c,\n p.h,\n p.count,\n ].every((n) => typeof n === \"number\")\n ) {\n return {\n l: p.l as number,\n c: p.c as number,\n h: p.h as number,\n count: p.count as number,\n };\n }\n }\n } catch {\n // ignore\n }\n return DEFAULT_RAMP;\n}\n\n/** Persist the ramp base color + count + notify AutoForeground (called by the ramp generator). */\nexport function writeRampConfig(config: RampConfig): void {\n try {\n localStorage.setItem(RAMP_KEY, JSON.stringify(config));\n } catch {\n // ignore\n }\n window.dispatchEvent(new Event(FG_EVENT));\n}\n\nexport interface AutoForegroundProps {\n /** Foreground ramp palette. Overrides the persisted config when set. */\n palette?: FgPalette;\n /** Ramp base color + step count. Overrides the persisted ramp when set. */\n ramp?: RampConfig;\n}\n\n/**\n * Sets the foreground tokens on by drawing COLORS from the chosen OKLCH ramp (palette + base\n * color + step count): `--foreground`, `--muted-foreground`, and the ARC-Bronze size tiers\n * `--foreground-soft` (large) / `--foreground-strong` (fine), plus the icon foreground `--foreground-ui`\n * (ui band, optional hue). Each is picked from that ramp to hit its contrast target on the glass-SOLID\n * surface text sits on — so foregrounds are real theme colors, not neutral gray, and track light/dark +\n * tint automatically. globals.css carries static fallbacks (no flash); the tiers are exposed as the\n * `text-foreground-soft` / `-strong` / `-ui` utilities.\n *\n * Configure declaratively — `` — or, with no\n * props, it reads a persisted config (`writeRampConfig`, e.g. the /colors generator) and re-applies on the\n * `sistine-fg` event. Mount it once at the app root. The foreground level is contrast-target-driven\n * (the ARC-Bronze band per tier), not a manual ramp index.\n */\nexport function AutoForeground({ palette: paletteProp, ramp: rampProp }: AutoForegroundProps = {}) {\n const rl = rampProp?.l;\n const rc = rampProp?.c;\n const rh = rampProp?.h;\n const rcount = rampProp?.count;\n\n React.useEffect(() => {\n const root = document.documentElement;\n\n const update = (inputs?: Record) => {\n const dbg = fgDebug();\n const t0 = dbg ? performance.now() : 0;\n const dark = root.classList.contains(\"dark\");\n const storedFg = readFgConfig();\n const storedRamp = readRampConfig();\n const palette = paletteProp ?? storedFg.palette;\n const count = rcount ?? storedRamp.count;\n // A snapshot from the switcher (the drag hot path) lets us skip getComputedStyle — the read-after-write\n // forced reflow measured at 7–22ms. Fall back to the DOM for mount / mode toggle / frescoes (no snapshot).\n const cs = inputs ? null : getComputedStyle(root);\n const fgBefore = dbg ? (cs ? cs.getPropertyValue(\"--foreground\").trim() : root.style.getPropertyValue(\"--foreground\").trim()) : \"\";\n const num = (name: string, fb: number) => {\n const v = inputs ? inputs[name] : cs ? Number.parseFloat(cs.getPropertyValue(name)) : Number.NaN;\n return v == null || Number.isNaN(v) ? fb : v;\n };\n // Foregrounds FOLLOW THE CHOSEN FOREGROUND HUE: the ramp's hue is --glass-fg-h (which defaults to\n // the glass tint --glass-tint-h, but frescoes set it apart so text anchors off their surface).\n // Lightness + chroma (vividness) and step count come from the /colors ramp config. Picks are\n // measured on the glass-SOLID surface body text sits on — a known surface, so a real Lc.\n const tintH = num(\"--glass-fg-h\", num(\"--glass-tint-h\", rh ?? storedRamp.h));\n const tintA = num(\"--glass-tint-a\", 0);\n // Colorfulness gate — CHROMA is the single master now that Wash is retired: tint-c > 0 → colored\n // foregrounds (at the ramp's vividness) + hue-tracking harmonics; chroma 0 → achromatic. (tintA still\n // feeds the glass-solid SURFACE color below, but no longer decides \"is this theme colored?\".)\n const tintC = num(\"--glass-tint-c\", 0);\n // Harmony anchor — the wheel origin the icon/foreground harmonics rotate from. Mirrors the CSS\n // --harmony-h (content hue, or 0 for the hue-less selenite/moonstone themes set inline by the tint switcher);\n // falls back to the content hue when unset (jewels), so JS harmonics land on the SAME angle as the\n // --hue-* swatches. harmonicHue(fgHarmonyH, name) below matches calc(var(--harmony-h) + N) exactly.\n const harmonyH = num(\"--harmony-h\", tintH);\n const cfgC = rc ?? storedRamp.c;\n // User accent: on the hue-LESS themes only (selenite + moonstone anchor --harmony-h at 0), a chosen accent\n // colors ALL text tiers — its hue + vividness drive the ramp base, so foregrounds tint toward the accent\n // instead of gray (selenite) / warm-moonstone. Frescoes (--harmony-h != 0) are untouched. Band-picking below\n // still hits each tier's ARC-Bronze APCA target, so accent-tinted text stays legible.\n const accentH = num(\"--accent-h\", Number.NaN);\n const accentC = num(\"--accent-c\", 0.15);\n // Uncertainty-aware contrast margin. The normal tiers are banded against the veiled floor MODEL,\n // whose only unknown is the backdrop showing through — and the backdrop's weight in that mix is\n // exactly (1 − solidA)·(1 − tintA) (see glassSolidSurface). The more the backdrop shows, the less\n // the model can be trusted, so each band's TARGET gets a safety margin of up to +LC_MARGIN\n // (≈ one ARC band step) at fully-sheer, decaying to +0 at a fully-known floor (solidA 1 — e.g. the\n // opaque page style sets --glass-solid-a: 1). Ceilings still cap the pick (anti-harshness).\n // (LC_MARGIN / LC_AIM_KNOWN are exported from lib/oklch-utils so every consumer aims the same.)\n // Parity aim for a FULLY-KNOWN floor. LC_MARGIN above is an UNCERTAINTY margin — it decays to 0 as the\n // floor becomes exactly modelable, which is right on its own terms but leaves the one surface we model\n // exactly (opaque) aiming at the BARE band target while every other surface aims 3.6–5.7 Lc above it.\n // Measured at the shipped defaults that made opaque the lowest-contrast surface in the system for every\n // tint in BOTH modes (body 80.0 vs 82.0–87.3, muted 72.0 vs 73.0–79.3) — the \"soft / out-of-focus\"\n // opaque card. Certainty about the floor is no reason to aim at the minimum, so a known floor gets this\n // baseline aim instead. Sized to the MIDDLE of the other surfaces' effective margins so opaque lands\n // LEVEL with them; the full LC_MARGIN here would pin body+muted to the band ceiling and just invert the\n // asymmetry (opaque becomes the harshest surface). Ceilings still cap the pick.\n const solidA = num(\"--glass-solid-a\", 0.65);\n // Wash knobs — moonstone is the ONE preset that overrides them at night (--glass-wash-l: 72%,\n // --glass-wash-c-mult: 2 — its pale-cream character), which the hardcoded model missed and\n // every moonstone-night surface banded ~3 L too dark (bright page, faint text). num() reads the\n // truth on the computed path; snapshots can't carry these, so the FALLBACK is moonstone-aware via\n // the data-glass-tint attribute (cheap, race-free on both paths). Every other theme resolves\n // to the standard mode values either way — this is a moonstone-only correction by construction.\n // check-theme [moonstone-sync] keeps these mirrored constants equal to presets.css.\n const moonstone = root.dataset.glassTint === \"moonstone\";\n const washL = num(\"--glass-wash-l\", moonstone && dark ? 64 : dark ? 58 : 72);\n const washCMult = num(\"--glass-wash-c-mult\", moonstone && dark ? 2 : 2.5);\n // THE SOLIDIFY FLOOR — `glass` paints --glass-solidify (the --glass-opacity dial, default 0.7) as the\n // bottom background-image layer of EVERY sheer material, so it is 70% of what text actually sits on.\n // The models below used to skip it entirely and band against the sheer floor alone. On most themes the\n // opaque floor sits the same side of mid-grey as the page, so that cost a few Lc (utilities.css measured\n // body 91.5 → 86.5). Moonstone NIGHT is the case that breaks it: a cream L84.9 opaque floor under an L20\n // page, i.e. the two OPPOSE, so the crystal model landed 27.5 L too dark, called for near-white text, and\n // reported Lc 87.1 for a surface that actually delivers 52.2 — below the body floor of 75, silently.\n // Reading it here fixes every sheer surface at once, and the fallbacks mirror tokens.css.\n const glassOpacity = Math.min(Math.max(num(\"--glass-opacity\", 0.7), 0), 1);\n /* The CAP is not cosmetic: engine.css paints this floor as\n `min(--glass-tint-c * --glass-opaque-c-scale, --glass-opaque-c-max)`, because a jewel's tint chroma\n scaled up would leave the sRGB gamut at the opaque floor's lightness and WebKit clips out-of-gamut\n oklch() per channel rather than reducing chroma — which trades lightness away and drifts hue. The\n cap is what each preset's own ceiling is FOR. Modelling the floor uncapped bands text against a\n surface more colourful (and so slightly darker) than the one actually painted; scripts/\n check-contrast.mjs already clamps here, so an uncapped model here also silently disagrees with the\n guard that signs the presets off. Keep the three in step. */\n const opaqueCMax = num(\"--glass-opaque-c-max\", dark ? 0.12 : 0.055);\n /* --glass-solidify-*, NOT --glass-opaque-*: the backing under sheer glass is its own surface (see\n tokens.css). It is the one high-weight term in the composite that is not part of a preset's\n declared identity, which is why LIGHT lifts it to 92 to buy body-text contrast without touching\n a single tint token. Dark pins both back to the opaque floor, so this reads identically there.\n Fallbacks mirror tokens.css: the derived cap is --glass-opaque-c-max × 0.65 in light. */\n const solidifyFloor = {\n l: num(\"--glass-solidify-l\", dark ? 36.4 : 92),\n c: Math.min(tintC * num(\"--glass-opaque-c-scale\", dark ? 1.05 : 0.85), num(\"--glass-solidify-c-max\", dark ? opaqueCMax : opaqueCMax * 0.65)),\n a: glassOpacity,\n };\n /** The solidify floor as a compositable layer — the layer order `glass` paints. */\n const solidifyLayer = {\n l: solidifyFloor.l,\n c: solidifyFloor.c,\n h: tintH,\n a: solidifyFloor.a,\n };\n /* --glass-tint-c-hi (engine.css): the HIGHLIGHT chroma budget, min(--glass-tint-c, 0.017). The\n sheet stops and the crystal floor are all scaled off this, not off raw --glass-tint-c — at\n near-white lightnesses the gamut ceiling collapses, so the cap is what keeps those layers\n rendering what they ask for. check-theme [gamut] holds the 0.017 in step with engine.css. */\n const TINT_C_HI_MAX = 0.017;\n const tintCHi = Math.min(tintC, TINT_C_HI_MAX);\n /* The gloss triple (top / streak / glow) — crystal AND chakra bake the same --glass-gloss-ink, so\n it is derived once here. Only the TOP highlight is modeled: it peaks at 0.4α and fades out by\n 30% height, so ≈0.2 is its mean across the title zone (modeling the 0.4 peak would make the band\n unsatisfiable on mid-gray). The streak (0.15α on a 135° diagonal) and glow (0.2α radial centred\n at 50% 120%, i.e. BELOW the card) are left out: the glow is past its 70% fade before it reaches\n the title zone, and the streak's mean there is a geometry estimate rather than a measurement.\n Both would only ADD light, so omitting them is the conservative direction. */\n const GLOSS_TOP_A = 0.2;\n const glossLayer = {\n /* Mode-aware fallback: --glass-gloss-l is a twin (97 light / 66 dark), so a single 66 here would\n model the light crystal surface ~6 L darker than it renders and band text too weak. */\n l: num(\"--glass-gloss-l\", dark ? 66 : 97),\n // --glass-gloss-ink is TINTED — min(--glass-tint-c × --glass-gloss-tint, --glass-gloss-c-max).\n // Modeling it achromatic dropped the gloss's colour from every crystal/chakra band.\n c: Math.min(tintC * num(\"--glass-gloss-tint\", 4.25), num(\"--glass-gloss-c-max\", dark ? 0.109 : 0.013)),\n h: tintH,\n a: GLOSS_TOP_A,\n };\n const normalLcBoost = LC_MARGIN * backdropShowThrough(solidA, tintA, glassOpacity);\n const huelessAccent = harmonyH === 0 && !Number.isNaN(accentH);\n // The wheel origin harmonics rotate from — the accent on hue-less+accent, else the CSS --harmony-h.\n const fgHarmonyH = huelessAccent ? accentH : harmonyH;\n /* The veiled floor body text sits on (page + translucent/veiled cards). Derived HERE, above the\n ramp, because the ramp's hue comes from it — see below. */\n const normalSurface = glassSolidSurface(\n dark,\n {\n h: tintH,\n c: tintC,\n a: tintA,\n },\n solidA,\n washL,\n washCMult,\n solidifyFloor,\n );\n /* TEXT FOLLOWS THE SURFACE'S HUE, NOT THE TINT TOKEN'S. --glass-tint-h is what the WASH declares;\n what a reader sees is the wash composited over the solid/solidify floor, and that mix does not\n travel a radial path — the floor is near-neutral, so it has almost no hue to interpolate toward\n and the result lands a few degrees off the declared angle (measured light: rose 8 → 2.5, goldstone\n 22 → 17.1, lapis 268 → 271.7). Seeding the ramp from tintH therefore painted text at an angle the\n surface underneath it never actually occupies. Opaque is the one surface unaffected: it is a solid\n painted colour with nothing composited over it, so its hue IS tintH and surface.h returns exactly\n that. The residual spread BETWEEN materials is ≤5.5°, which at the ink's chroma (~0.08) sits well\n under a just-noticeable difference — far smaller than the ink-vs-surface mismatch it removes. */\n const surfaceH = huelessAccent ? accentH : normalSurface.h;\n // A neutral tint → ACHROMATIC foregrounds (black/white/gray by lightness). EXCEPTION: the Hue palette\n // stays a full-spectrum color wheel even when neutral. A hue-less accent overrides both — it hue +\n // vividnesses the whole ramp so every text tier tints toward the chosen accent.\n const base = {\n l: rl ?? storedRamp.l,\n c: huelessAccent ? accentC || cfgC : palette === \"hue\" ? cfgC || 0.15 : tintC > 0 ? cfgC : 0,\n h: surfaceH,\n };\n /* Draw every foreground from the chosen tonal/lightness ramp — real theme COLORS, not neutral gray —\n each picked to hit its ARC-Bronze contrast target on the surface text actually sits on. Only the\n READABLE half (count + 1 levels) is built: the far side of the base is display-only, and picking\n from it would offer steps the surface can never make legible.\n foregroundRamp also drops both achromatic ends on tinted themes — see lib/oklch-utils for why the\n two thresholds are not symmetric. Before that clip, --foreground-strong was #000000 on 20 of 21\n presets in light and #ffffff on 18 of 21 in dark, and body text ALSO went black on 10 of 21. */\n const ramp = foregroundRamp({\n palette,\n count,\n base,\n dark,\n });\n const iconHue = storedFg.iconHue;\n /* Set the full foreground tier set against a given SURFACE, under a var suffix. Run FOUR times, once\n per modeled surface: \"\" (the normal glass-SOLID floor), \"-opaque\", \"-crystal\", \"-chakra\". Each\n material's [data-material] block remaps --foreground* → its own suffixed set, so a LIGHT opaque\n floor (dark-mode moonstone cream) gets DARK card text while the dark page keeps light text — one\n global foreground cannot do both.\n FIVE materials share those four sets: `glass` and `frosted` both fall through to \"\". They are not\n quite the same surface — frosted paints --glass-frosted-bg, the same sheet lifted by\n --glass-frost-boost (0.1 light / 0.06 dark) — and the \"\" model deliberately stops at the wash, so\n NEITHER sheet is in it. Both are therefore banded against a floor darker than what renders, which\n is the safe direction (measured light: text lands 2.2–2.6 Lc over-contrasted on glass, 4.2–4.6 on\n frosted, and no tier breaches its band ceiling). Frosted is the loosest fit in the system; giving\n it a fifth set would tighten it. */\n const applyTiers = (\n surface: {\n l: number;\n c: number;\n h: number;\n },\n suffix: string,\n adaptive: boolean,\n lcBoost = 0,\n // Baseline aim, kept SEPARATE from lcBoost so the two stay honest: lcBoost answers \"how much don't\n // we know about this floor\", lcAim answers \"how far above the bare minimum should we aim on a floor\n // we DO know\". Only the opaque set passes it (see LC_AIM_KNOWN); every other surface earns its\n // margin from uncertainty and leaves this 0.\n lcAim = 0,\n ) => {\n // Lift the band's TARGET toward its ceiling by the uncertainty boost + the baseline aim. Floors and\n // ceilings stay — the margin aims higher, it never legalizes a harsher pick than the band allowed.\n const boost = (band: ContrastBand) => boostBand(band, lcBoost, lcAim);\n /* Pick from the TONAL ramp, never at the cost of the band's floor — see pickTonalInBand in\n lib/oklch-utils. Today no band needs the fallback (light body bottoms out at 75.4 Lc on the\n clipped ramp), but /colors lets a consumer re-base the ramp entirely, so the guard exists so\n the hue preference can never become a readability regression. */\n /* The adaptive twin, readableTonal, also lives in lib/oklch-utils: an unreachable aim makes\n readableForeground return the lightness EXTREME, where the gamut annihilates chroma, so every\n jewel's --foreground-opaque solved to pure black in light. Same clip, same ~1 Lc cost. */\n // Band-aware pick: honor each tier's floor (minimum) + ceiling (anti-spike), aiming for target. Normal\n // surfaces draw a COLORED pick from the theme ramp; `adaptive` (opaque floors) uses readableForeground\n // instead, which flips the lightness DIRECTION to whatever the floor needs — the ramp only spans the\n // readable half (white→base in dark mode), so it can't produce DARK text for a light floor (moonstone cream).\n const tier = (rawBand: ContrastBand) => {\n const band = boost(rawBand);\n return formatOklch(\n adaptive\n ? readableTonal(surface, band, {\n // Opaque cards on the hue-less themes (selenite + moonstone) follow the chosen accent too — same as\n // the normal surface above — so moonstone/selenite opaque-card text tints toward the accent.\n // Otherwise THIS surface's own composited hue, not the tint token's (see surfaceH): each\n // adaptive material composites a different stack, so each gets ink at its own angle.\n hue: huelessAccent ? accentH : surface.h,\n chroma: huelessAccent ? accentC : tintC > 0 ? cfgC : 0,\n // Accent path ONLY — the one case where the background CAN hold the kept chroma, so the\n // minChroma search does not bail to the extreme (see readableTonal).\n minChroma: huelessAccent ? 0.08 : 0,\n })\n : pickTonalInBand(ramp, surface, band),\n );\n };\n root.style.setProperty(`--foreground${suffix}`, tier(READABLE_USAGE.body));\n root.style.setProperty(\n `--muted-foreground${suffix}`,\n tier({\n // Raised from 45/60/75 → muted lands darker (≈L30 on a light opaque floor) — a firmer secondary,\n // not a faint one. Global + computed per-hue (moonstone, sistine, every jewel), both surfaces + opaque.\n floor: 60,\n target: 72,\n ceiling: 84,\n }),\n );\n // Size tiers default to a palette-ramp pick (tier). softHue/strongHue (mirroring iconHue) optionally\n // pin a tier to its OWN readable hue — a number, \"complement\" (theme's opposite, live), or null =\n // follow the palette — so headings / fine text can be tinted independently of body text.\n const tierAtHue = (usage: \"large\" | \"small\", choice: number | HarmonicName | null) =>\n choice == null\n ? tier(READABLE_USAGE[usage])\n : formatOklch(\n // Clipped like every other solve: a PINNED hue is the one case where collapsing to the\n // achromatic extreme is most obviously wrong — the whole point of pinning is to see it.\n readableTonal(surface, boost(READABLE_USAGE[usage]), {\n hue: typeof choice === \"string\" ? harmonicHue(fgHarmonyH, choice) : choice,\n chroma: 0.15,\n }),\n );\n root.style.setProperty(`--foreground-soft${suffix}`, tierAtHue(\"large\", storedFg.softHue));\n // REACH-LIMITED IN LIGHT MODE, by design of the surfaces themselves: the small band floors at Lc 90,\n // and a mid-light floor simply cannot deliver that much even with pure black text. Light opaque (L90)\n // tops out at 82.6–87.6 across the tints and light chakra (L88) at 82.0–83.7, so --foreground-strong\n // on those two surfaces lands 2.4–8.0 Lc under its floor. readableForeground's documented\n // reach < floor fallback covers it — it returns the MOST contrast available rather than failing —\n // and no margin can close the gap. Only raising --glass-opaque-l past ~94.5 / --glass-chakra-l past\n // ~92.5 would (measurably paler cards); that is a design call, not a banding bug. Dark mode has\n // headroom to spare (reach 96–104) and hits the floor everywhere.\n root.style.setProperty(`--foreground-strong${suffix}`, tierAtHue(\"small\", storedFg.strongHue));\n // Icons get their own foreground: a ui-band-legible color (lightness solved for contrast) at an\n // OPTIONAL chosen hue — so icons can be tinted/cycled while staying readable, independent of the\n // text palette. iconHue null → follow the theme (neutral → gray, tinted → the tint hue).\n // null → follow the theme, which means THIS surface's composited hue (see surfaceH), so an icon\n // sits at the same angle as the material behind it. A pinned number / harmonic is a deliberate\n // choice and overrides that.\n const iconH = typeof iconHue === \"string\" ? harmonicHue(fgHarmonyH, iconHue) : typeof iconHue === \"number\" ? iconHue : surface.h;\n root.style.setProperty(\n `--foreground-ui${suffix}`,\n formatOklch(\n readableTonal(surface, boost(READABLE_USAGE.ui), {\n hue: iconH,\n chroma: iconHue != null ? 0.15 : tintC > 0 ? cfgC : 0,\n }),\n ),\n );\n };\n\n // Normal surface: the veiled floor body text sits on (page + translucent/veiled cards), with the\n // show-through margin lifting each band target as the floor gets sheerer. Derived above the ramp\n // (see normalSurface) because the ramp's hue is read off it.\n applyTiers(normalSurface, \"\", false, normalLcBoost);\n /* Opaque cards paint --glass-opaque-bg, so band a second set against THAT — derived here from\n --glass-opaque-l / -c-max rather than reused from solidifyFloor. Those two used to be the same\n colour, and this call read `solidifyFloor` on exactly that basis; they are separate surfaces now\n (tokens.css), so sharing would band opaque-card text against the sheer backing instead of the\n card. In dark the two still resolve identically; in light the card stays at L88 while the backing\n sits at L92. Keep in step with tokens.css — an earlier version of this re-derivation drifted,\n carrying its own `* 0.9` multiplier (matching neither mode) and no chroma cap at all.\n `adaptive` so a LIGHT floor (moonstone cream) gets DARK text — the theme ramp only spans the\n readable half and can't. */\n applyTiers(\n {\n l: num(\"--glass-opaque-l\", dark ? 36.4 : 88),\n c: Math.min(tintC * num(\"--glass-opaque-c-scale\", dark ? 1.05 : 0.85), opaqueCMax),\n h: tintH,\n },\n \"-opaque\",\n true,\n 0, // no uncertainty — --glass-opaque-l models this floor exactly\n LC_AIM_KNOWN,\n );\n // Crystal cards: the specular gloss is baked UNDER content, so title-zone text sits on a locally\n // LIGHTENED surface — worst in dark mode, where the ~L94 highlight over a dark floor pulls the\n // local surface toward mid-gray. Band a THIRD set against the crystal surface + the title zone's\n // mean gloss term (see glossLayer). [data-material=\"crystal\"] and the crystal page style remap the\n // tiers to this set — except veiled crystal, whose floor is what the NORMAL tiers are banded for.\n // Same show-through margin logic: the backdrop's weight in this mix is (1−crysA)(1−tintA)(1−glossA).\n {\n const crysA = num(\"--glass-crystal-bg-a\", dark ? 0.1 : 0.3);\n // Layer order: --glass-crystal-bg (background-COLOR) → solidify → wash → gloss. The solidify step\n // was missing, which is what put moonstone-night crystal 27.5 L below what it paints. Each layer\n // now composites in sRGB (see compositeSurface) rather than lerping OKLCH coordinates — on this\n // surface that error CHANGED SIGN by preset, so nothing shorter than a real composite fixes it.\n const uCrystal = Math.min(Math.max((1 - crysA) * (1 - tintA) * (1 - GLOSS_TOP_A) * (1 - glassOpacity), 0), 1);\n applyTiers(\n compositeSurface(\n {\n l: dark ? 20 : 95,\n c: 0,\n h: tintH,\n },\n [\n // --glass-crystal-bg, engine.css: oklch(--glass-crystal-l, --glass-tint-c-hi × 0.6). This\n // modeled a flat L100 at the RAW tint chroma — 100 carries no chroma at all (which is why\n // the token pins 96), and skipping the -c-hi cap asked for up to 6× the colour that\n // renders (tourmaline 0.0636 vs the 0.0102 ceiling).\n {\n l: num(\"--glass-crystal-l\", 96),\n c: tintCHi * 0.6,\n h: tintH,\n a: crysA,\n },\n solidifyLayer,\n {\n l: washL,\n c: tintC * washCMult,\n h: tintH,\n a: tintA,\n },\n glossLayer,\n ],\n ),\n \"-crystal\",\n true,\n LC_MARGIN * uCrystal,\n );\n }\n // Chakra cards: content sits on the translucent body, banded against the whole stack it paints.\n // The facet bands need no term — they live in box-shadow, ride the outer few px of the edge, and\n // pair a highlight against an ink on opposite sides, so they contribute nothing where text\n // actually sits. Show-through is (1 − body a)(1 − tintA)(1 − glossA)(1 − glassOpacity) — the same\n // four-term form crystal uses, since chakra bakes the same gloss. `adaptive` for the same reason\n // opaque is: an L88 body can sit on a dark page, and the theme ramp only spans the readable half.\n {\n const bodyA = num(\"--glass-chakra-a\", dark ? 0.58 : 0.62);\n /* --glass-chakra-bg is the background-COLOR and it is TRANSLUCENT (--glass-chakra-a, 0.62 light\n / 0.58 dark), so --glass-chakra-l was never the banding lightness on its own — the page shows\n through it. And --glass-chakra-stack-bg is the SAME stack crystal bakes (gloss triple, fresco\n slot, wash layer) with solidify composed under it, so chakra takes the tint wash and the gloss\n too. Modeling only `chakra-l → solidify` dropped the page, the body's own alpha, the entire\n wash and the gloss; it agreed with the truth on moonstone night by coincidence, because that\n preset pins --glass-chakra-l (84) near its cream opaque floor. Full stack, real paint order. */\n /* (1 − GLOSS_TOP_A) belongs here for the same reason it does on crystal: show-through is the\n backdrop's SURVIVING weight, so every layer painted above it attenuates — and chakra bakes the\n gloss now. Omitting it overstated the backdrop's share by 1/(1−0.2) = 1.25×, which inflated an\n UNCERTAINTY margin (the boost is LC_MARGIN × this) on a surface that is in fact better known\n than the model claimed. */\n const uChakra = Math.min(Math.max((1 - bodyA) * (1 - tintA) * (1 - GLOSS_TOP_A) * (1 - glassOpacity), 0), 1);\n applyTiers(\n compositeSurface(\n {\n l: dark ? 20 : 95,\n c: 0,\n h: tintH,\n },\n [\n {\n l: num(\"--glass-chakra-l\", dark ? 28 : 88),\n c: Math.min(tintC, num(\"--glass-chakra-c-max\", dark ? 0.046 : 0.055)),\n h: tintH,\n a: bodyA,\n },\n solidifyLayer,\n {\n l: washL,\n c: tintC * washCMult,\n h: tintH,\n a: tintA,\n },\n glossLayer,\n ],\n ),\n \"-chakra\",\n true,\n LC_MARGIN * uChakra,\n );\n }\n if (dbg) {\n const dur = performance.now() - t0;\n // Wall-time = the getComputedStyle forced recalc + the JS solve (58 µs). The post-write recalc from\n // the setProperty calls shows separately as \"Recalculate Style\" in the Performance panel.\n console.debug(\n `[AutoForeground] update ${dur.toFixed(2)}ms · --foreground ${fgBefore || \"(unset)\"} → ${root.style.getPropertyValue(\"--foreground\")}`,\n );\n try {\n performance.measure(\"AutoForeground.update\", {\n start: t0,\n duration: dur,\n });\n } catch {\n // options form of performance.measure not supported\n }\n }\n };\n\n update();\n // Mode toggle (class) uses the DOM-read fallback — the one place we still pay the recalc, by design.\n // Tint / accent / lightness changes arrive via FG_EVENT carrying a JS snapshot (no getComputedStyle).\n // Inline STYLE mutations re-band ONLY when a surface-model input actually changed: --glass-solid-a\n // (the veil-floor slider) or --glass-gloss-l (the crystal gloss-boldness slider) — the two inputs\n // with no FG_EVENT. The old-vs-new gate keeps tint drags on the event fast path AND breaks the\n // self-trigger loop from our own --foreground* writes (which touch neither).\n const STYLE_INPUTS = [\n /--glass-solid-a:\\s*([^;]+)/,\n /--glass-gloss-l:\\s*([^;]+)/,\n // --glass-opacity is a surface-model input now that the solidify floor is banded against, and the\n // component-opacity slider writes it inline with no FG_EVENT — without this the tiers go stale the\n // moment a consumer dials solidity, which is exactly when the floor moves most.\n /--glass-opacity:\\s*([^;]+)/,\n ];\n const observer = new MutationObserver((muts) => {\n for (const m of muts) {\n if (m.attributeName === \"class\") {\n update();\n return;\n }\n if (m.attributeName === \"style\") {\n const now = root.getAttribute(\"style\") ?? \"\";\n const was = m.oldValue ?? \"\";\n if (STYLE_INPUTS.some((re) => re.exec(was)?.[1]?.trim() !== re.exec(now)?.[1]?.trim())) {\n update();\n return;\n }\n }\n }\n });\n observer.observe(root, {\n attributes: true,\n attributeOldValue: true,\n attributeFilter: [\n \"class\",\n \"style\",\n ],\n });\n const onFg = (e: Event) => update((e as CustomEvent>).detail ?? undefined);\n window.addEventListener(FG_EVENT, onFg);\n return () => {\n observer.disconnect();\n window.removeEventListener(FG_EVENT, onFg);\n };\n }, [\n paletteProp,\n rl,\n rc,\n rh,\n rcount,\n ]);\n\n return null;\n}\n", + "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n showThrough as backdropShowThrough,\n boostBand,\n type ContrastBand,\n compositeSurface,\n foregroundRamp,\n formatOklch,\n glassSolidSurface,\n HARMONIC_OFFSETS,\n type HarmonicName,\n harmonicHue,\n LC_AIM_KNOWN,\n LC_MARGIN,\n pickTonalInBand,\n READABLE_USAGE,\n readableTonal,\n type ThemeForegroundOptions,\n} from \"@/lib/oklch-utils\";\n\n/** localStorage key holding the persisted {@link FgConfig}. Exported so an app that reads or seeds the\n * config (a settings screen, a pre-hydration script) never retypes the string. */\nexport const FG_STORAGE_KEY = \"sistine-fg\";\n/** localStorage key holding the persisted {@link RampConfig}. */\nexport const RAMP_KEY = \"sistine-ramp\";\n/** Window event AutoForeground listens for to re-band: dispatch it after changing the tint, the mode or\n * the stored config (`window.dispatchEvent(new Event(FG_EVENT))`). */\nexport const FG_EVENT = \"sistine-fg\";\n\n// Dev instrumentation: set localStorage[\"sistine-fg-debug\"] = \"1\" (then reload) to log each update()'s\n// wall-time and how far it nudges --foreground off the CSS baseline (the \"marginal bump\"). Off by default.\nconst fgDebug = () => {\n try {\n return typeof localStorage !== \"undefined\" && localStorage.getItem(\"sistine-fg-debug\") === \"1\";\n } catch {\n return false;\n }\n};\n\nexport type FgPalette = ThemeForegroundOptions[\"palette\"];\nexport interface FgConfig {\n palette: FgPalette;\n /** Icon foreground hue for `--foreground-ui`: a number (0–360) pins a hue; a harmonic name\n * (\"complement\" | \"triad-1\" | \"split-2\" | … — a color-wheel relationship rotated off --harmony-h,\n * tracked live); null → icons follow the theme/text color. Always contrast-solved (APCA/ARC). */\n iconHue: number | HarmonicName | null;\n /** Heading/large-text hue for `--foreground-soft` — same semantics as iconHue (number pins, a harmonic\n * name rotates off the theme live, null → follow the chosen palette ramp). */\n softHue: number | HarmonicName | null;\n /** Fine/small-text hue for `--foreground-strong` — same semantics (null → follow the palette ramp). */\n strongHue: number | HarmonicName | null;\n}\n/** The /colors ramp generator's base color + step count, shared with the foreground. */\nexport interface RampConfig {\n l: number;\n c: number;\n h: number;\n count: number;\n}\n\nconst FG_PALETTES: FgPalette[] = [\n \"tonal\",\n \"lightness\",\n \"hue\",\n \"chroma\",\n];\nconst DEFAULT_FG: FgConfig = {\n palette: \"lightness\", // linear ramp — holds the theme's chroma, so high-contrast text reads as a soft tinted white, not gray\n iconHue: null,\n softHue: null,\n strongHue: null,\n};\n/* The tonal clip bounds, the margins and the pick/solve themselves now live in lib/oklch-utils — see\n the \"Foreground solve\" section there. They were duplicated into components/foreground-tester.tsx and\n scripts/check-contrast.mjs, and every copy had drifted from this one in a way no test could see. */\nconst DEFAULT_RAMP: RampConfig = {\n l: 60,\n c: 0.15,\n h: 255,\n count: 12, // finest ramp (12 steps/side) — the most cohesive foreground set in practice\n};\n\n/** Normalize a stored hue choice: a harmonic relationship name, a pinned number, else null. */\nconst hueChoice = (v: unknown): number | HarmonicName | null =>\n typeof v === \"string\" && v in HARMONIC_OFFSETS ? (v as HarmonicName) : typeof v === \"number\" ? v : null;\n\n/** Read the persisted foreground palette; falls back to the default (Linear). */\nexport function readFgConfig(): FgConfig {\n try {\n const raw = localStorage.getItem(FG_STORAGE_KEY);\n if (raw) {\n const parsed = JSON.parse(raw) as Partial;\n if (FG_PALETTES.includes(parsed.palette as FgPalette)) {\n return {\n palette: parsed.palette as FgPalette,\n iconHue: hueChoice(parsed.iconHue),\n softHue: hueChoice(parsed.softHue),\n strongHue: hueChoice(parsed.strongHue),\n };\n }\n }\n } catch {\n // ignore parse / storage failures\n }\n return DEFAULT_FG;\n}\n\n/** Persist the foreground config + notify AutoForeground to re-apply it site-wide. */\nexport function writeFgConfig(config: Partial): void {\n try {\n localStorage.setItem(\n FG_STORAGE_KEY,\n JSON.stringify({\n ...readFgConfig(),\n ...config,\n }),\n );\n } catch {\n // ignore storage failures\n }\n window.dispatchEvent(new Event(FG_EVENT));\n}\n\n/** Read the ramp generator's base color + count (shared from the /colors ramp card). */\nexport function readRampConfig(): RampConfig {\n try {\n const raw = localStorage.getItem(RAMP_KEY);\n if (raw) {\n const p = JSON.parse(raw) as Partial;\n if (\n [\n p.l,\n p.c,\n p.h,\n p.count,\n ].every((n) => typeof n === \"number\")\n ) {\n return {\n l: p.l as number,\n c: p.c as number,\n h: p.h as number,\n count: p.count as number,\n };\n }\n }\n } catch {\n // ignore\n }\n return DEFAULT_RAMP;\n}\n\n/** Persist the ramp base color + count + notify AutoForeground (called by the ramp generator). */\nexport function writeRampConfig(config: RampConfig): void {\n try {\n localStorage.setItem(RAMP_KEY, JSON.stringify(config));\n } catch {\n // ignore\n }\n window.dispatchEvent(new Event(FG_EVENT));\n}\n\nexport interface AutoForegroundProps {\n /** Foreground ramp palette. Overrides the persisted config when set. */\n palette?: FgPalette;\n /** Ramp base color + step count. Overrides the persisted ramp when set. */\n ramp?: RampConfig;\n}\n\n/**\n * Sets the foreground tokens on by drawing COLORS from the chosen OKLCH ramp (palette + base\n * color + step count): `--foreground`, `--muted-foreground`, and the ARC-Bronze size tiers\n * `--foreground-soft` (large) / `--foreground-strong` (fine), plus the icon foreground `--foreground-ui`\n * (ui band, optional hue). Each is picked from that ramp to hit its contrast target on the glass-SOLID\n * surface text sits on — so foregrounds are real theme colors, not neutral gray, and track light/dark +\n * tint automatically. globals.css carries static fallbacks (no flash); the tiers are exposed as the\n * `text-foreground-soft` / `-strong` / `-ui` utilities.\n *\n * Configure declaratively — `` — or, with no\n * props, it reads a persisted config (`writeRampConfig`, e.g. the /colors generator) and re-applies on the\n * `sistine-fg` event. Mount it once at the app root. The foreground level is contrast-target-driven\n * (the ARC-Bronze band per tier), not a manual ramp index.\n */\nexport function AutoForeground({ palette: paletteProp, ramp: rampProp }: AutoForegroundProps = {}) {\n const rl = rampProp?.l;\n const rc = rampProp?.c;\n const rh = rampProp?.h;\n const rcount = rampProp?.count;\n\n React.useEffect(() => {\n const root = document.documentElement;\n\n const update = (inputs?: Record) => {\n const dbg = fgDebug();\n const t0 = dbg ? performance.now() : 0;\n const dark = root.classList.contains(\"dark\");\n const storedFg = readFgConfig();\n const storedRamp = readRampConfig();\n const palette = paletteProp ?? storedFg.palette;\n const count = rcount ?? storedRamp.count;\n // A snapshot from the switcher (the drag hot path) lets us skip getComputedStyle — the read-after-write\n // forced reflow measured at 7–22ms. Fall back to the DOM for mount / mode toggle / frescoes (no snapshot).\n const cs = inputs ? null : getComputedStyle(root);\n const fgBefore = dbg ? (cs ? cs.getPropertyValue(\"--foreground\").trim() : root.style.getPropertyValue(\"--foreground\").trim()) : \"\";\n const num = (name: string, fb: number) => {\n const v = inputs ? inputs[name] : cs ? Number.parseFloat(cs.getPropertyValue(name)) : Number.NaN;\n return v == null || Number.isNaN(v) ? fb : v;\n };\n // Foregrounds FOLLOW THE CHOSEN FOREGROUND HUE: the ramp's hue is --glass-fg-h (which defaults to\n // the glass tint --glass-tint-h, but frescoes set it apart so text anchors off their surface).\n // Lightness + chroma (vividness) and step count come from the /colors ramp config. Picks are\n // measured on the glass-SOLID surface body text sits on — a known surface, so a real Lc.\n const tintH = num(\"--glass-fg-h\", num(\"--glass-tint-h\", rh ?? storedRamp.h));\n const tintA = num(\"--glass-tint-a\", 0);\n // Colorfulness gate — CHROMA is the single master now that Wash is retired: tint-c > 0 → colored\n // foregrounds (at the ramp's vividness) + hue-tracking harmonics; chroma 0 → achromatic. (tintA still\n // feeds the glass-solid SURFACE color below, but no longer decides \"is this theme colored?\".)\n const tintC = num(\"--glass-tint-c\", 0);\n // Harmony anchor — the wheel origin the icon/foreground harmonics rotate from. Mirrors the CSS\n // --harmony-h (content hue, or 0 for the hue-less selenite/moonstone themes set inline by the tint switcher);\n // falls back to the content hue when unset (jewels), so JS harmonics land on the SAME angle as the\n // --hue-* swatches. harmonicHue(fgHarmonyH, name) below matches calc(var(--harmony-h) + N) exactly.\n const harmonyH = num(\"--harmony-h\", tintH);\n const cfgC = rc ?? storedRamp.c;\n // User accent: on the hue-LESS themes only (selenite + moonstone anchor --harmony-h at 0), a chosen accent\n // colors ALL text tiers — its hue + vividness drive the ramp base, so foregrounds tint toward the accent\n // instead of gray (selenite) / warm-moonstone. Frescoes (--harmony-h != 0) are untouched. Band-picking below\n // still hits each tier's ARC-Bronze APCA target, so accent-tinted text stays legible.\n const accentH = num(\"--accent-h\", Number.NaN);\n const accentC = num(\"--accent-c\", 0.15);\n // Uncertainty-aware contrast margin. The normal tiers are banded against the veiled floor MODEL,\n // whose only unknown is the backdrop showing through — and the backdrop's weight in that mix is\n // exactly (1 − solidA)·(1 − tintA) (see glassSolidSurface). The more the backdrop shows, the less\n // the model can be trusted, so each band's TARGET gets a safety margin of up to +LC_MARGIN\n // (≈ one ARC band step) at fully-sheer, decaying to +0 at a fully-known floor (solidA 1 — e.g. the\n // opaque page style sets --glass-solid-a: 1). Ceilings still cap the pick (anti-harshness).\n // (LC_MARGIN / LC_AIM_KNOWN are exported from lib/oklch-utils so every consumer aims the same.)\n // Parity aim for a FULLY-KNOWN floor. LC_MARGIN above is an UNCERTAINTY margin — it decays to 0 as the\n // floor becomes exactly modelable, which is right on its own terms but leaves the one surface we model\n // exactly (opaque) aiming at the BARE band target while every other surface aims 3.6–5.7 Lc above it.\n // Measured at the shipped defaults that made opaque the lowest-contrast surface in the system for every\n // tint in BOTH modes (body 80.0 vs 82.0–87.3, muted 72.0 vs 73.0–79.3) — the \"soft / out-of-focus\"\n // opaque card. Certainty about the floor is no reason to aim at the minimum, so a known floor gets this\n // baseline aim instead. Sized to the MIDDLE of the other surfaces' effective margins so opaque lands\n // LEVEL with them; the full LC_MARGIN here would pin body+muted to the band ceiling and just invert the\n // asymmetry (opaque becomes the harshest surface). Ceilings still cap the pick.\n const solidA = num(\"--glass-solid-a\", 0.65);\n // Wash knobs — moonstone is the ONE preset that overrides them at night (--glass-wash-l: 72%,\n // --glass-wash-c-mult: 2 — its pale-cream character), which the hardcoded model missed and\n // every moonstone-night surface banded ~3 L too dark (bright page, faint text). num() reads the\n // truth on the computed path; snapshots can't carry these, so the FALLBACK is moonstone-aware via\n // the data-glass-tint attribute (cheap, race-free on both paths). Every other theme resolves\n // to the standard mode values either way — this is a moonstone-only correction by construction.\n // check-theme [moonstone-sync] keeps these mirrored constants equal to presets.css.\n const moonstone = root.dataset.glassTint === \"moonstone\";\n const washL = num(\"--glass-wash-l\", moonstone && dark ? 64 : dark ? 58 : 72);\n const washCMult = num(\"--glass-wash-c-mult\", moonstone && dark ? 2 : 2.5);\n // THE SOLIDIFY FLOOR — `glass` paints --glass-solidify (the --glass-opacity dial, default 0.7) as the\n // bottom background-image layer of EVERY sheer material, so it is 70% of what text actually sits on.\n // The models below used to skip it entirely and band against the sheer floor alone. On most themes the\n // opaque floor sits the same side of mid-grey as the page, so that cost a few Lc (utilities.css measured\n // body 91.5 → 86.5). Moonstone NIGHT is the case that breaks it: a cream L84.9 opaque floor under an L20\n // page, i.e. the two OPPOSE, so the crystal model landed 27.5 L too dark, called for near-white text, and\n // reported Lc 87.1 for a surface that actually delivers 52.2 — below the body floor of 75, silently.\n // Reading it here fixes every sheer surface at once, and the fallbacks mirror tokens.css.\n const glassOpacity = Math.min(Math.max(num(\"--glass-opacity\", 0.7), 0), 1);\n /* The CAP is not cosmetic: engine.css paints this floor as\n `min(--glass-tint-c * --glass-opaque-c-scale, --glass-opaque-c-max)`, because a jewel's tint chroma\n scaled up would leave the sRGB gamut at the opaque floor's lightness and WebKit clips out-of-gamut\n oklch() per channel rather than reducing chroma — which trades lightness away and drifts hue. The\n cap is what each preset's own ceiling is FOR. Modelling the floor uncapped bands text against a\n surface more colourful (and so slightly darker) than the one actually painted; scripts/\n check-contrast.mjs already clamps here, so an uncapped model here also silently disagrees with the\n guard that signs the presets off. Keep the three in step. */\n const opaqueCMax = num(\"--glass-opaque-c-max\", dark ? 0.12 : 0.055);\n /* --glass-solidify-*, NOT --glass-opaque-*: the backing under sheer glass is its own surface (see\n tokens.css). It is the one high-weight term in the composite that is not part of a preset's\n declared identity, which is why LIGHT lifts it to 92 to buy body-text contrast without touching\n a single tint token. Dark pins both back to the opaque floor, so this reads identically there.\n Fallbacks mirror tokens.css: the derived cap is --glass-opaque-c-max × 0.65 in light. */\n const solidifyFloor = {\n l: num(\"--glass-solidify-l\", dark ? 36.4 : 92),\n c: Math.min(tintC * num(\"--glass-opaque-c-scale\", dark ? 1.05 : 0.85), num(\"--glass-solidify-c-max\", dark ? opaqueCMax : opaqueCMax * 0.65)),\n a: glassOpacity,\n };\n /** The solidify floor as a compositable layer — the layer order `glass` paints. */\n const solidifyLayer = {\n l: solidifyFloor.l,\n c: solidifyFloor.c,\n h: tintH,\n a: solidifyFloor.a,\n };\n /* --glass-tint-c-hi (engine.css): the HIGHLIGHT chroma budget, min(--glass-tint-c, 0.017). The\n sheet stops and the crystal floor are all scaled off this, not off raw --glass-tint-c — at\n near-white lightnesses the gamut ceiling collapses, so the cap is what keeps those layers\n rendering what they ask for. check-theme [gamut] holds the 0.017 in step with engine.css. */\n const TINT_C_HI_MAX = 0.017;\n const tintCHi = Math.min(tintC, TINT_C_HI_MAX);\n /* The gloss triple (top / streak / glow) — crystal AND chakra bake the same --glass-gloss-ink, so\n it is derived once here. Only the TOP highlight is modeled: it peaks at 0.4α and fades out by\n 30% height, so ≈0.2 is its mean across the title zone (modeling the 0.4 peak would make the band\n unsatisfiable on mid-gray). The streak (0.15α on a 135° diagonal) and glow (0.2α radial centred\n at 50% 120%, i.e. BELOW the card) are left out: the glow is past its 70% fade before it reaches\n the title zone, and the streak's mean there is a geometry estimate rather than a measurement.\n Both would only ADD light, so omitting them is the conservative direction. */\n const GLOSS_TOP_A = 0.2;\n const glossLayer = {\n /* Mode-aware fallback: --glass-gloss-l is a twin (97 light / 66 dark), so a single 66 here would\n model the light crystal surface ~6 L darker than it renders and band text too weak. */\n l: num(\"--glass-gloss-l\", dark ? 66 : 97),\n // --glass-gloss-ink is TINTED — min(--glass-tint-c × --glass-gloss-tint, --glass-gloss-c-max).\n // Modeling it achromatic dropped the gloss's colour from every crystal/chakra band.\n c: Math.min(tintC * num(\"--glass-gloss-tint\", 4.25), num(\"--glass-gloss-c-max\", dark ? 0.109 : 0.013)),\n h: tintH,\n a: GLOSS_TOP_A,\n };\n const normalLcBoost = LC_MARGIN * backdropShowThrough(solidA, tintA, glassOpacity);\n const huelessAccent = harmonyH === 0 && !Number.isNaN(accentH);\n // The wheel origin harmonics rotate from — the accent on hue-less+accent, else the CSS --harmony-h.\n const fgHarmonyH = huelessAccent ? accentH : harmonyH;\n /* The veiled floor body text sits on (page + translucent/veiled cards). Derived HERE, above the\n ramp, because the ramp's hue comes from it — see below. */\n const normalSurface = glassSolidSurface(\n dark,\n {\n h: tintH,\n c: tintC,\n a: tintA,\n },\n solidA,\n washL,\n washCMult,\n solidifyFloor,\n );\n /* TEXT FOLLOWS THE SURFACE'S HUE, NOT THE TINT TOKEN'S. --glass-tint-h is what the WASH declares;\n what a reader sees is the wash composited over the solid/solidify floor, and that mix does not\n travel a radial path — the floor is near-neutral, so it has almost no hue to interpolate toward\n and the result lands a few degrees off the declared angle (measured light: rose 8 → 2.5, goldstone\n 22 → 17.1, lapis 268 → 271.7). Seeding the ramp from tintH therefore painted text at an angle the\n surface underneath it never actually occupies. Opaque is the one surface unaffected: it is a solid\n painted colour with nothing composited over it, so its hue IS tintH and surface.h returns exactly\n that. The residual spread BETWEEN materials is ≤5.5°, which at the ink's chroma (~0.08) sits well\n under a just-noticeable difference — far smaller than the ink-vs-surface mismatch it removes. */\n const surfaceH = huelessAccent ? accentH : normalSurface.h;\n // A neutral tint → ACHROMATIC foregrounds (black/white/gray by lightness). EXCEPTION: the Hue palette\n // stays a full-spectrum color wheel even when neutral. A hue-less accent overrides both — it hue +\n // vividnesses the whole ramp so every text tier tints toward the chosen accent.\n const base = {\n l: rl ?? storedRamp.l,\n c: huelessAccent ? accentC || cfgC : palette === \"hue\" ? cfgC || 0.15 : tintC > 0 ? cfgC : 0,\n h: surfaceH,\n };\n /* Draw every foreground from the chosen tonal/lightness ramp — real theme COLORS, not neutral gray —\n each picked to hit its ARC-Bronze contrast target on the surface text actually sits on. Only the\n READABLE half (count + 1 levels) is built: the far side of the base is display-only, and picking\n from it would offer steps the surface can never make legible.\n foregroundRamp also drops both achromatic ends on tinted themes — see lib/oklch-utils for why the\n two thresholds are not symmetric. Before that clip, --foreground-strong was #000000 on 20 of 21\n presets in light and #ffffff on 18 of 21 in dark, and body text ALSO went black on 10 of 21. */\n const ramp = foregroundRamp({\n palette,\n count,\n base,\n dark,\n });\n const iconHue = storedFg.iconHue;\n /* Set the full foreground tier set against a given SURFACE, under a var suffix. Run FOUR times, once\n per modeled surface: \"\" (the normal glass-SOLID floor), \"-opaque\", \"-crystal\", \"-chakra\". Each\n material's [data-material] block remaps --foreground* → its own suffixed set, so a LIGHT opaque\n floor (dark-mode moonstone cream) gets DARK card text while the dark page keeps light text — one\n global foreground cannot do both.\n FIVE materials share those four sets: `glass` and `frosted` both fall through to \"\". They are not\n quite the same surface — frosted paints --glass-frosted-bg, the same sheet lifted by\n --glass-frost-boost (0.1 light / 0.06 dark) — and the \"\" model deliberately stops at the wash, so\n NEITHER sheet is in it. Both are therefore banded against a floor darker than what renders, which\n is the safe direction (measured light: text lands 2.2–2.6 Lc over-contrasted on glass, 4.2–4.6 on\n frosted, and no tier breaches its band ceiling). Frosted is the loosest fit in the system; giving\n it a fifth set would tighten it. */\n const applyTiers = (\n surface: {\n l: number;\n c: number;\n h: number;\n },\n suffix: string,\n adaptive: boolean,\n lcBoost = 0,\n // Baseline aim, kept SEPARATE from lcBoost so the two stay honest: lcBoost answers \"how much don't\n // we know about this floor\", lcAim answers \"how far above the bare minimum should we aim on a floor\n // we DO know\". Only the opaque set passes it (see LC_AIM_KNOWN); every other surface earns its\n // margin from uncertainty and leaves this 0.\n lcAim = 0,\n ) => {\n // Lift the band's TARGET toward its ceiling by the uncertainty boost + the baseline aim. Floors and\n // ceilings stay — the margin aims higher, it never legalizes a harsher pick than the band allowed.\n const boost = (band: ContrastBand) => boostBand(band, lcBoost, lcAim);\n /* Pick from the TONAL ramp, never at the cost of the band's floor — see pickTonalInBand in\n lib/oklch-utils. Today no band needs the fallback (light body bottoms out at 75.4 Lc on the\n clipped ramp), but /colors lets a consumer re-base the ramp entirely, so the guard exists so\n the hue preference can never become a readability regression. */\n /* The adaptive twin, readableTonal, also lives in lib/oklch-utils: an unreachable aim makes\n readableForeground return the lightness EXTREME, where the gamut annihilates chroma, so every\n jewel's --foreground-opaque solved to pure black in light. Same clip, same ~1 Lc cost. */\n // Band-aware pick: honor each tier's floor (minimum) + ceiling (anti-spike), aiming for target. Normal\n // surfaces draw a COLORED pick from the theme ramp; `adaptive` (opaque floors) uses readableForeground\n // instead, which flips the lightness DIRECTION to whatever the floor needs — the ramp only spans the\n // readable half (white→base in dark mode), so it can't produce DARK text for a light floor (moonstone cream).\n const tier = (rawBand: ContrastBand) => {\n const band = boost(rawBand);\n return formatOklch(\n adaptive\n ? readableTonal(surface, band, {\n // Opaque cards on the hue-less themes (selenite + moonstone) follow the chosen accent too — same as\n // the normal surface above — so moonstone/selenite opaque-card text tints toward the accent.\n // Otherwise THIS surface's own composited hue, not the tint token's (see surfaceH): each\n // adaptive material composites a different stack, so each gets ink at its own angle.\n hue: huelessAccent ? accentH : surface.h,\n chroma: huelessAccent ? accentC : tintC > 0 ? cfgC : 0,\n // Accent path ONLY — the one case where the background CAN hold the kept chroma, so the\n // minChroma search does not bail to the extreme (see readableTonal).\n minChroma: huelessAccent ? 0.08 : 0,\n })\n : pickTonalInBand(ramp, surface, band),\n );\n };\n root.style.setProperty(`--foreground${suffix}`, tier(READABLE_USAGE.body));\n root.style.setProperty(\n `--muted-foreground${suffix}`,\n tier({\n // Raised from 45/60/75 → muted lands darker (≈L30 on a light opaque floor) — a firmer secondary,\n // not a faint one. Global + computed per-hue (moonstone, sistine, every jewel), both surfaces + opaque.\n floor: 60,\n target: 72,\n ceiling: 84,\n }),\n );\n // Size tiers default to a palette-ramp pick (tier). softHue/strongHue (mirroring iconHue) optionally\n // pin a tier to its OWN readable hue — a number, \"complement\" (theme's opposite, live), or null =\n // follow the palette — so headings / fine text can be tinted independently of body text.\n const tierAtHue = (usage: \"large\" | \"small\", choice: number | HarmonicName | null) =>\n choice == null\n ? tier(READABLE_USAGE[usage])\n : formatOklch(\n // Clipped like every other solve: a PINNED hue is the one case where collapsing to the\n // achromatic extreme is most obviously wrong — the whole point of pinning is to see it.\n readableTonal(surface, boost(READABLE_USAGE[usage]), {\n hue: typeof choice === \"string\" ? harmonicHue(fgHarmonyH, choice) : choice,\n chroma: 0.15,\n }),\n );\n root.style.setProperty(`--foreground-soft${suffix}`, tierAtHue(\"large\", storedFg.softHue));\n // REACH-LIMITED IN LIGHT MODE, by design of the surfaces themselves: the small band floors at Lc 90,\n // and a mid-light floor simply cannot deliver that much even with pure black text. Light opaque (L90)\n // tops out at 82.6–87.6 across the tints and light chakra (L88) at 82.0–83.7, so --foreground-strong\n // on those two surfaces lands 2.4–8.0 Lc under its floor. readableForeground's documented\n // reach < floor fallback covers it — it returns the MOST contrast available rather than failing —\n // and no margin can close the gap. Only raising --glass-opaque-l past ~94.5 / --glass-chakra-l past\n // ~92.5 would (measurably paler cards); that is a design call, not a banding bug. Dark mode has\n // headroom to spare (reach 96–104) and hits the floor everywhere.\n root.style.setProperty(`--foreground-strong${suffix}`, tierAtHue(\"small\", storedFg.strongHue));\n // Icons get their own foreground: a ui-band-legible color (lightness solved for contrast) at an\n // OPTIONAL chosen hue — so icons can be tinted/cycled while staying readable, independent of the\n // text palette. iconHue null → follow the theme (neutral → gray, tinted → the tint hue).\n // null → follow the theme, which means THIS surface's composited hue (see surfaceH), so an icon\n // sits at the same angle as the material behind it. A pinned number / harmonic is a deliberate\n // choice and overrides that.\n const iconH = typeof iconHue === \"string\" ? harmonicHue(fgHarmonyH, iconHue) : typeof iconHue === \"number\" ? iconHue : surface.h;\n root.style.setProperty(\n `--foreground-ui${suffix}`,\n formatOklch(\n readableTonal(surface, boost(READABLE_USAGE.ui), {\n hue: iconH,\n chroma: iconHue != null ? 0.15 : tintC > 0 ? cfgC : 0,\n }),\n ),\n );\n };\n\n // Normal surface: the veiled floor body text sits on (page + translucent/veiled cards), with the\n // show-through margin lifting each band target as the floor gets sheerer. Derived above the ramp\n // (see normalSurface) because the ramp's hue is read off it.\n applyTiers(normalSurface, \"\", false, normalLcBoost);\n /* Opaque cards paint --glass-opaque-bg, so band a second set against THAT — derived here from\n --glass-opaque-l / -c-max rather than reused from solidifyFloor. Those two used to be the same\n colour, and this call read `solidifyFloor` on exactly that basis; they are separate surfaces now\n (tokens.css), so sharing would band opaque-card text against the sheer backing instead of the\n card. In dark the two still resolve identically; in light the card stays at L88 while the backing\n sits at L92. Keep in step with tokens.css — an earlier version of this re-derivation drifted,\n carrying its own `* 0.9` multiplier (matching neither mode) and no chroma cap at all.\n `adaptive` so a LIGHT floor (moonstone cream) gets DARK text — the theme ramp only spans the\n readable half and can't. */\n applyTiers(\n {\n l: num(\"--glass-opaque-l\", dark ? 36.4 : 88),\n c: Math.min(tintC * num(\"--glass-opaque-c-scale\", dark ? 1.05 : 0.85), opaqueCMax),\n h: tintH,\n },\n \"-opaque\",\n true,\n 0, // no uncertainty — --glass-opaque-l models this floor exactly\n LC_AIM_KNOWN,\n );\n // Crystal cards: the specular gloss is baked UNDER content, so title-zone text sits on a locally\n // LIGHTENED surface — worst in dark mode, where the ~L94 highlight over a dark floor pulls the\n // local surface toward mid-gray. Band a THIRD set against the crystal surface + the title zone's\n // mean gloss term (see glossLayer). [data-material=\"crystal\"] and the crystal page style remap the\n // tiers to this set — except veiled crystal, whose floor is what the NORMAL tiers are banded for.\n // Same show-through margin logic: the backdrop's weight in this mix is (1−crysA)(1−tintA)(1−glossA).\n {\n const crysA = num(\"--glass-crystal-bg-a\", dark ? 0.1 : 0.3);\n // Layer order: --glass-crystal-bg (background-COLOR) → solidify → wash → gloss. The solidify step\n // was missing, which is what put moonstone-night crystal 27.5 L below what it paints. Each layer\n // now composites in sRGB (see compositeSurface) rather than lerping OKLCH coordinates — on this\n // surface that error CHANGED SIGN by preset, so nothing shorter than a real composite fixes it.\n const uCrystal = Math.min(Math.max((1 - crysA) * (1 - tintA) * (1 - GLOSS_TOP_A) * (1 - glassOpacity), 0), 1);\n applyTiers(\n compositeSurface(\n {\n l: dark ? 20 : 95,\n c: 0,\n h: tintH,\n },\n [\n // --glass-crystal-bg, engine.css: oklch(--glass-crystal-l, --glass-tint-c-hi × 0.6). This\n // modeled a flat L100 at the RAW tint chroma — 100 carries no chroma at all (which is why\n // the token pins 96), and skipping the -c-hi cap asked for up to 6× the colour that\n // renders (tourmaline 0.0636 vs the 0.0102 ceiling).\n {\n l: num(\"--glass-crystal-l\", 96),\n c: tintCHi * 0.6,\n h: tintH,\n a: crysA,\n },\n solidifyLayer,\n {\n l: washL,\n c: tintC * washCMult,\n h: tintH,\n a: tintA,\n },\n glossLayer,\n ],\n ),\n \"-crystal\",\n true,\n LC_MARGIN * uCrystal,\n );\n }\n // Chakra cards: content sits on the translucent body, banded against the whole stack it paints.\n // The facet bands need no term — they live in box-shadow, ride the outer few px of the edge, and\n // pair a highlight against an ink on opposite sides, so they contribute nothing where text\n // actually sits. Show-through is (1 − body a)(1 − tintA)(1 − glossA)(1 − glassOpacity) — the same\n // four-term form crystal uses, since chakra bakes the same gloss. `adaptive` for the same reason\n // opaque is: an L88 body can sit on a dark page, and the theme ramp only spans the readable half.\n {\n const bodyA = num(\"--glass-chakra-a\", dark ? 0.58 : 0.62);\n /* --glass-chakra-bg is the background-COLOR and it is TRANSLUCENT (--glass-chakra-a, 0.62 light\n / 0.58 dark), so --glass-chakra-l was never the banding lightness on its own — the page shows\n through it. And --glass-chakra-stack-bg is the SAME stack crystal bakes (gloss triple, fresco\n slot, wash layer) with solidify composed under it, so chakra takes the tint wash and the gloss\n too. Modeling only `chakra-l → solidify` dropped the page, the body's own alpha, the entire\n wash and the gloss; it agreed with the truth on moonstone night by coincidence, because that\n preset pins --glass-chakra-l (84) near its cream opaque floor. Full stack, real paint order. */\n /* (1 − GLOSS_TOP_A) belongs here for the same reason it does on crystal: show-through is the\n backdrop's SURVIVING weight, so every layer painted above it attenuates — and chakra bakes the\n gloss now. Omitting it overstated the backdrop's share by 1/(1−0.2) = 1.25×, which inflated an\n UNCERTAINTY margin (the boost is LC_MARGIN × this) on a surface that is in fact better known\n than the model claimed. */\n const uChakra = Math.min(Math.max((1 - bodyA) * (1 - tintA) * (1 - GLOSS_TOP_A) * (1 - glassOpacity), 0), 1);\n applyTiers(\n compositeSurface(\n {\n l: dark ? 20 : 95,\n c: 0,\n h: tintH,\n },\n [\n {\n l: num(\"--glass-chakra-l\", dark ? 28 : 88),\n c: Math.min(tintC, num(\"--glass-chakra-c-max\", dark ? 0.046 : 0.055)),\n h: tintH,\n a: bodyA,\n },\n solidifyLayer,\n {\n l: washL,\n c: tintC * washCMult,\n h: tintH,\n a: tintA,\n },\n glossLayer,\n ],\n ),\n \"-chakra\",\n true,\n LC_MARGIN * uChakra,\n );\n }\n if (dbg) {\n const dur = performance.now() - t0;\n // Wall-time = the getComputedStyle forced recalc + the JS solve (58 µs). The post-write recalc from\n // the setProperty calls shows separately as \"Recalculate Style\" in the Performance panel.\n console.debug(\n `[AutoForeground] update ${dur.toFixed(2)}ms · --foreground ${fgBefore || \"(unset)\"} → ${root.style.getPropertyValue(\"--foreground\")}`,\n );\n try {\n performance.measure(\"AutoForeground.update\", {\n start: t0,\n duration: dur,\n });\n } catch {\n // options form of performance.measure not supported\n }\n }\n };\n\n update();\n // Mode toggle (class) uses the DOM-read fallback — the one place we still pay the recalc, by design.\n // Tint / accent / lightness changes arrive via FG_EVENT carrying a JS snapshot (no getComputedStyle).\n // Inline STYLE mutations re-band ONLY when a surface-model input actually changed: --glass-solid-a\n // (the veil-floor slider) or --glass-gloss-l (the crystal gloss-boldness slider) — the two inputs\n // with no FG_EVENT. The old-vs-new gate keeps tint drags on the event fast path AND breaks the\n // self-trigger loop from our own --foreground* writes (which touch neither).\n const STYLE_INPUTS = [\n /--glass-solid-a:\\s*([^;]+)/,\n /--glass-gloss-l:\\s*([^;]+)/,\n // --glass-opacity is a surface-model input now that the solidify floor is banded against, and the\n // component-opacity slider writes it inline with no FG_EVENT — without this the tiers go stale the\n // moment a consumer dials solidity, which is exactly when the floor moves most.\n /--glass-opacity:\\s*([^;]+)/,\n ];\n const observer = new MutationObserver((muts) => {\n for (const m of muts) {\n if (m.attributeName === \"class\") {\n update();\n return;\n }\n if (m.attributeName === \"style\") {\n const now = root.getAttribute(\"style\") ?? \"\";\n const was = m.oldValue ?? \"\";\n if (STYLE_INPUTS.some((re) => re.exec(was)?.[1]?.trim() !== re.exec(now)?.[1]?.trim())) {\n update();\n return;\n }\n }\n }\n });\n observer.observe(root, {\n attributes: true,\n attributeOldValue: true,\n attributeFilter: [\n \"class\",\n \"style\",\n ],\n });\n const onFg = (e: Event) => update((e as CustomEvent>).detail ?? undefined);\n window.addEventListener(FG_EVENT, onFg);\n return () => {\n observer.disconnect();\n window.removeEventListener(FG_EVENT, onFg);\n };\n }, [\n paletteProp,\n rl,\n rc,\n rh,\n rcount,\n ]);\n\n return null;\n}\n", "type": "registry:component", "target": "components/auto-foreground.tsx" } diff --git a/public/r/avatar.json b/public/r/avatar.json index a5b03605..07da483b 100644 --- a/public/r/avatar.json +++ b/public/r/avatar.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/background-controller.json b/public/r/background-controller.json index 034ccecf..ec9b2e47 100644 --- a/public/r/background-controller.json +++ b/public/r/background-controller.json @@ -22,7 +22,7 @@ }, { "path": "components/pattern-background.tsx", - "content": "import type { CSSProperties } from \"react\";\n\n/**\n * Pure-CSS pattern backgrounds — tileable wallpapers built entirely from `background-image` layers\n * (multiple backgrounds + gradients) and SVG `mask-image` silhouettes, all colored off the live\n * OKLCH tint tokens so they recolor with the theme. No canvas, no rAF loop: each static style is a\n * single GPU paint; the animated scenes (synthwave / moonrise grid scroll, the clouds / dune / aurora\n * drifts, the Pac-Man chase) move on compositor-only transforms. Sibling of GradientBackground\n * (CSS gradient) and CanvasBackground (JS canvas).\n */\n\nexport type PatternStyle = \"dots\" | \"grid\" | \"mesh\" | \"starfield\" | \"synthwave\" | \"moonrise\" | \"clouds\" | \"dune\" | \"aurora\" | \"chase\";\n\nexport const PATTERN_STYLES: PatternStyle[] = [\n \"dots\",\n \"grid\",\n \"mesh\",\n \"starfield\",\n \"synthwave\",\n \"moonrise\",\n \"clouds\",\n \"dune\",\n \"aurora\",\n \"chase\",\n];\n\n/** Patterns that animate (CSS keyframes) — gates the speed control and shares the --pat-dur/--pat-play pace. */\nexport const ANIMATED_PATTERNS = new Set([\n \"synthwave\",\n \"moonrise\",\n \"clouds\",\n \"dune\",\n \"aurora\",\n \"chase\",\n]);\n\n/** Horizon scenes with a sun/moon disc that can be placed left / center / right. */\nexport const DISC_PATTERNS = new Set([\n \"synthwave\",\n \"moonrise\",\n \"clouds\",\n \"dune\",\n \"aurora\",\n]);\n\n/** Scenes with a blowing-sand control (still / breeze / storm). */\nexport const SAND_PATTERNS = new Set([\n \"dune\",\n]);\n\n/** Star-field density — how many of the star field's points render (a \"range of stars\"). */\nexport type PatternDensity = \"sparse\" | \"medium\" | \"dense\";\nexport const PATTERN_DENSITIES: PatternDensity[] = [\n \"sparse\",\n \"medium\",\n \"dense\",\n];\nconst STAR_COUNT: Record = {\n sparse: 16,\n medium: 33,\n dense: 50,\n};\n/** Chase pellet-cell scale per density — sparse = larger cells (fewer per screen), dense = smaller (more). */\nconst DENSITY_SCALE: Record = {\n sparse: 1.4,\n medium: 1,\n dense: 0.68,\n};\n/** Tile-size scale for the geometric tiles (dots / grid) — a much airier ladder: the old sparse (1.4)\n * is the new DENSE, with medium and sparse extrapolated outward on the same ~1.4× step. */\nconst TILE_SCALE: Record = {\n sparse: 2.8,\n medium: 2,\n dense: 1.4,\n};\n\n// SVG silhouettes as mask sources. White fill + transparent ground works whether the browser samples\n// the mask by alpha or luminance. Wrapped in url(...) after encoding so it's a valid CSS value.\n// `stretch: true` sets preserveAspectRatio='none' so a wide silhouette (the dune crests) fills the\n// element's 100% × 100% mask box instead of letterboxing to the viewBox aspect (the SVG default —\n// which renders a full-width mask as a centered strip with cliff edges).\nconst svgMask = (body: string, viewBox = \"0 0 100 100\", stretch = false) =>\n `url(\"data:image/svg+xml,${encodeURIComponent(`${body}`)}\")`;\n\n// The ghost silhouette — used by the chase scene's fleeing ghosts.\nconst GHOST = svgMask(\n \"\",\n);\n\n// Scattered \"points\" for the galaxy — a fixed set so it's deterministic (SSR-safe, no Math.random).\n// Kept as an array so the density control can render just a slice of them.\nconst STAR_GRADIENTS = [\n [\n 8,\n 12,\n ],\n [\n 22,\n 64,\n ],\n [\n 15,\n 88,\n ],\n [\n 35,\n 26,\n ],\n [\n 44,\n 72,\n ],\n [\n 52,\n 16,\n ],\n [\n 61,\n 52,\n ],\n [\n 68,\n 84,\n ],\n [\n 73,\n 34,\n ],\n [\n 82,\n 66,\n ],\n [\n 88,\n 20,\n ],\n [\n 92,\n 80,\n ],\n [\n 28,\n 44,\n ],\n [\n 57,\n 90,\n ],\n [\n 4,\n 40,\n ],\n [\n 12,\n 6,\n ],\n [\n 19,\n 30,\n ],\n [\n 26,\n 78,\n ],\n [\n 33,\n 54,\n ],\n [\n 40,\n 20,\n ],\n [\n 47,\n 46,\n ],\n [\n 54,\n 68,\n ],\n [\n 63,\n 12,\n ],\n [\n 66,\n 40,\n ],\n [\n 70,\n 60,\n ],\n [\n 77,\n 78,\n ],\n [\n 80,\n 8,\n ],\n [\n 84,\n 48,\n ],\n [\n 90,\n 60,\n ],\n [\n 95,\n 34,\n ],\n [\n 6,\n 70,\n ],\n [\n 48,\n 4,\n ],\n [\n 58,\n 30,\n ],\n [\n 76,\n 22,\n ],\n [\n 2,\n 22,\n ],\n [\n 16,\n 50,\n ],\n [\n 24,\n 14,\n ],\n [\n 38,\n 88,\n ],\n [\n 50,\n 40,\n ],\n [\n 56,\n 58,\n ],\n [\n 64,\n 74,\n ],\n [\n 72,\n 46,\n ],\n [\n 86,\n 38,\n ],\n [\n 94,\n 12,\n ],\n [\n 10,\n 82,\n ],\n [\n 30,\n 68,\n ],\n [\n 42,\n 34,\n ],\n [\n 60,\n 6,\n ],\n [\n 74,\n 90,\n ],\n [\n 96,\n 66,\n ],\n].map(([x, y], i) => {\n // a range of stars — occasional bright glints, most fine\n const r = i % 7 === 0 ? \"2px\" : i % 4 === 0 ? \"1.6px\" : i % 3 === 0 ? \"1.3px\" : \"1px\";\n /* --star-core is the fraction of the dot that stays SOLID before the falloff starts. Default 0%\n reproduces the original `#fff, transparent` exactly (white at 0, transparent at 100% — a pure\n linear falloff), so every scene that does not set it is untouched.\n It exists because \"full opacity\" was not the same as \"not faded\": a linear falloff averages only\n alpha 0.333 across the dot's area, so on a light sky the stars delivered ~+7 L rather than the\n +22 their peak suggests. On a near-black night sky that is invisible as a problem — any white\n reads — which is why it only ever showed up by day. */\n return `radial-gradient(${r} ${r} at ${x}% ${y}%, #fff 0 var(--star-core, 0%), transparent)`;\n});\n\n// Easter egg: the \"Muse\" — the Agent mascot's head (fedora + visored helmet) traced as a sparse star\n// constellation in every star field (starfield / synthwave / moonrise / clouds / dune / aurora). The dots are anchor points sampled\n// from the mascot SVG's bezier paths, cropped to the head (the coat sprawled into unreadable scatter),\n// thinned to ~45 (downsample, dedupe) and re-normalized; rendered as stars only — no fill, no lines — so the\n// fedora-and-helmet silhouette hides among the real stars until you catch it. Hue follows the accent-or-tint.\n// biome-ignore format: keep the sampled dots as one compact \"x,y x,y …\" string (not a 400-line array).\nconst MUSE_DOTS = \"67,66.5 65.1,52.1 66.7,42.3 75.8,38.4 11,48.8 82.9,45.3 92.3,42.7 100,36.8 84.4,32.8 85.2,58.8 75.5,65.3 71.9,58.3 51.9,42.9 26.4,48.3 60.9,59.3 38.5,42.3 0,50.4 18.3,38 19.3,28.2 19.9,14.7 25.7,5.9 37.5,2.8 51.1,0 62.9,0.5 72.8,5.6 77.5,15.3 79.7,25.7 96.2,90.9 67,78 43.4,85.6 87.4,72.2 87.4,82.7 16.9,65.6 48.2,59.2 37.5,62 28.1,64 19,87 27.2,88.7 88.4,92.4 38.6,70.3 51.5,81.1 21.5,73 33.6,81 53.7,65.1 30.7,40.5\".split(\" \").map((p) => p.split(\",\").map(Number));\nconst MUSE_STYLES = `\n.star-muse {\n position: absolute;\n left: 6%;\n top: 3%;\n width: 58vh;\n height: 53.6vh;\n pointer-events: none;\n --muse-h: var(--accent-h, var(--glass-tint-h));\n filter: drop-shadow(0 0 1px oklch(0.72 0.17 var(--muse-h) / 0.5));\n /* Rides the star gate: hidden with the stars in the Tron scenes' day mode (they set it to 0). In\n starfield the gate is 1 in BOTH modes now, so this sits at a flat 0.9 — the constellation is an\n easter egg and reads a touch under the field it hides in, rather than being mode-dependent. */\n opacity: calc(0.9 * var(--sw-stars-o, 1));\n}\n.star-muse circle {\n fill: oklch(0.96 0.04 var(--muse-h));\n}`;\n\nfunction MuseConstellation() {\n return (\n <>\n \n \n {MUSE_DOTS.map(([x, y], i) => (\n \n ))}\n \n \n );\n}\n\n// Shared sky furniture for the horizon scenes — the star layer (upper sky, fading toward the horizon,\n// day-hidden via the --sw-stars-o gate the palettes flip) and the palette-driven DISC. Every disc color is\n// a scene var (--sw-moon-1/2/3 core, --sw-maria L C H triple, --sw-disc-glow halo) set per mode by the\n// scene's [data-pattern] blocks, and --sw-disc-x carries the left/center/right placement control — so one\n// rule paints a rising violet moon, a desert silver moon, or a SUN with zero reskins. The sun is not a\n// second element: --sw-maria-o zeroes the craters (a sun has none) and --sw-disc-bloom adds the wide\n// second halo a bright source throws, which a moon does not. Both default to the moon's behaviour, so\n// moonrise and every night palette are untouched by their existence.\n// Placement knobs default to moonrise's horizon-dipping moon (40vh disc sitting ON the 42% horizon);\n// sky-borne scenes (clouds / dune) lift it with --moon-bottom and cancel the dip.\nconst SKY_STYLES = `\n.sw-stars {\n position: absolute;\n inset: 0 0 42% 0;\n background-repeat: no-repeat;\n opacity: var(--sw-stars-o, 1);\n -webkit-mask-image: linear-gradient(to bottom, #000 50%, transparent 100%);\n mask-image: linear-gradient(to bottom, #000 50%, transparent 100%);\n}\n.scene-moon {\n position: absolute;\n left: var(--sw-disc-x, 50%);\n bottom: var(--moon-bottom, 42%);\n width: var(--moon-size, 40vh);\n height: var(--moon-size, 40vh);\n transform: translate(-50%, var(--moon-dip, 30%));\n border-radius: 50%;\n background:\n radial-gradient(34% 30% at 63% 30%, oklch(var(--sw-maria) / calc(0.55 * var(--sw-maria-o, 1))) 0, transparent 62%),\n radial-gradient(20% 18% at 38% 56%, oklch(var(--sw-maria) / calc(0.5 * var(--sw-maria-o, 1))) 0, transparent 62%),\n radial-gradient(13% 12% at 56% 72%, oklch(var(--sw-maria) / calc(0.45 * var(--sw-maria-o, 1))) 0, transparent 62%),\n radial-gradient(circle at 42% 38%, var(--sw-moon-1) 0%, var(--sw-moon-2) 52%, var(--sw-moon-3) 100%);\n /* Two halos. The first is the moon's; the second is the sun's wide atmospheric bloom, a no-op\n (0 radius, transparent) unless a palette opts in. */\n filter:\n drop-shadow(0 0 var(--sw-disc-blur, 5vh) var(--sw-disc-glow))\n drop-shadow(0 0 var(--sw-disc-bloom, 0) var(--sw-disc-bloom-c, transparent));\n opacity: var(--sw-moon-o, 1);\n}\n/* The OUTRUN SUN — synthwave's disc, promoted to shared furniture so clouds / dune / aurora can put the\n same low banded sun on their day horizon. It sits ON the 42% line and dips below it; whatever the scene\n draws next (cloud deck, sand, water) covers the dipped half, so it reads as sitting in the horizon.\n Both discs render in every scene that wants either; --sw-sun-o / --sw-moon-o pick which one shows per\n mode, the same way --sw-stars-o gates the stars. Defaults are 1, so synthwave and moonrise — which\n choose their disc in JSX instead — are untouched. */\n.sw-sun {\n position: absolute;\n left: var(--sw-disc-x, 50%);\n bottom: 42%;\n width: var(--sun-size, 46vh);\n height: var(--sun-size, 46vh);\n transform: translate(-50%, 33%);\n border-radius: 50%;\n opacity: var(--sw-sun-o, 1);\n background: linear-gradient(\n to top,\n var(--sw-disc-1) 0%,\n var(--sw-disc-2) 24%,\n var(--sw-disc-3) 52%,\n var(--sw-disc-4) 78%,\n var(--sw-disc-5) 100%\n );\n -webkit-mask-image:\n linear-gradient(to bottom, #000 0 42%, transparent 42%),\n repeating-linear-gradient(to bottom, #000 0 var(--sun-band, 0.85vh), transparent var(--sun-band, 0.85vh) calc(var(--sun-band, 0.85vh) * 2));\n mask-image:\n linear-gradient(to bottom, #000 0 42%, transparent 42%),\n repeating-linear-gradient(to bottom, #000 0 var(--sun-band, 0.85vh), transparent var(--sun-band, 0.85vh) calc(var(--sun-band, 0.85vh) * 2));\n filter: drop-shadow(0 0 5vh var(--sw-disc-glow));\n}`;\n\n// Synthwave / Tron horizon — two variants: `synthwave` (outrun sun) and `moonrise` (purple-blue moon),\n// each with a DAY and a NIGHT palette so the scene follows the light/dark toggle. Every scene color is a\n// --sw-* custom property: day values sit on [data-pattern=\"…\"], night values on `.dark [data-pattern=\"…\"]`\n// (the injected \n

    \n
    \n {PAC_WALLS.map((w) => (\n \n ))}\n {PAC_LANES.map((lane) => (\n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n ))}\n {PAC_VLANES.map((lane) => (\n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n ))}\n
    \n
    \n );\n }\n if (style === \"synthwave\" || style === \"moonrise\") {\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n const moon = style === \"moonrise\";\n return (\n \n \n \n
    \n \n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n );\n }\n if (style === \"clouds\") {\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n return (\n \n \n \n
    \n \n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n );\n }\n if (style === \"dune\") {\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n // Dune faces light toward the disc: the diagonal face gradients read --du-light, so moving the\n // moon left/center/right relights every dune (and the ground sheet) to match.\n const duneLight = {\n left: \"160deg\",\n center: \"180deg\",\n right: \"200deg\",\n }[disc];\n // Sand level → veil opacity + speck multiplier. \"still\" zeroes BOTH (a calm desert has no airborne\n // sand at all); it is not the same as the speed control's \"static\", which only pauses motion.\n const [sandVeil, sandGrain] = {\n still: [\n \"0\",\n \"0\",\n ],\n breeze: [\n \"0.55\",\n \"1\",\n ],\n storm: [\n \"1\",\n \"1.8\",\n ],\n }[sand];\n return (\n \n \n \n
    \n \n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n {/* Wind veils ride ABOVE the ridges and BELOW the haze — sand in the air, not on the ground. */}\n
    \n
    \n
    \n
    \n {/* Haze LAST-but-one: it must wash over the far ridges, not sit behind them. */}\n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n );\n }\n if (style === \"aurora\") {\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n return (\n \n \n \n
    \n \n \n
    \n
    \n
    \n
    \n {/* AFTER the curtains: behind them, three layers of translucent light wash the disc green and\n it stops reading as a sun. Still before the glow and water, so the haze and waterline\n cover it the way they cover everything else at the horizon. */}\n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n );\n }\n if (style === \"starfield\") {\n // Scene colors live in STARFIELD_STYLES' palette blocks (an inline background would beat the .dark\n // day/night flip); only the star GLINTS — white in both modes — are painted inline, on their own\n // layer so the palette's --sw-stars-o can dim them by day.\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n return (\n \n \n
    \n \n \n
    \n );\n }\n return (\n \n );\n}\n", + "content": "import type { CSSProperties } from \"react\";\n\n/**\n * Pure-CSS pattern backgrounds — tileable wallpapers built entirely from `background-image` layers\n * (multiple backgrounds + gradients) and SVG `mask-image` silhouettes, all colored off the live\n * OKLCH tint tokens so they recolor with the theme. No canvas, no rAF loop: each static style is a\n * single GPU paint; the animated scenes (synthwave / moonrise grid scroll, the clouds / dune / aurora\n * drifts, the Pac-Man chase) move on compositor-only transforms. Sibling of GradientBackground\n * (CSS gradient) and CanvasBackground (JS canvas).\n */\n\nexport type PatternStyle = \"dots\" | \"grid\" | \"mesh\" | \"starfield\" | \"synthwave\" | \"moonrise\" | \"clouds\" | \"dune\" | \"aurora\" | \"chase\";\n\nexport const PATTERN_STYLES: PatternStyle[] = [\n \"dots\",\n \"grid\",\n \"mesh\",\n \"starfield\",\n \"synthwave\",\n \"moonrise\",\n \"clouds\",\n \"dune\",\n \"aurora\",\n \"chase\",\n];\n\n/**\n * The two ways a scene stops: reduced motion, and Speed 0.\n *\n * Both REMOVE the animation rather than pausing it. `animation-play-state: paused`\n * looks like it should be free — the transform never advances — but a paused\n * animation still leaves the element in an animating state, and WebKit keeps\n * re-rendering its layer. On `.au-curtain` that layer carries a `filter: blur()`\n * and a two-layer `mask-composite`, six of them at full-viewport size, so \"paused\"\n * was measurably NOT free (AMXSF-559). `animation: none` also drops `will-change`,\n * releasing the promoted layer — `will-change` on a stopped animation is a\n * promoted layer doing no work.\n *\n * Emitted from one helper so the two conditions cannot drift apart: a scene that\n * respects reduced motion but not Speed 0 was exactly the bug.\n */\nexport function stopRules(...selectors: string[]): string {\n const stop = \"{ animation: none; will-change: auto; }\";\n return `@media (prefers-reduced-motion: reduce) { ${selectors.join(\", \")} ${stop} }\n${selectors.map((s) => `[data-pat-static] ${s}`).join(\", \")} ${stop}`;\n}\n\n/** Patterns that animate (CSS keyframes) — gates the speed control and shares the --pat-dur pace. */\nexport const ANIMATED_PATTERNS = new Set([\n \"synthwave\",\n \"moonrise\",\n \"clouds\",\n \"dune\",\n \"aurora\",\n \"chase\",\n]);\n\n/** Horizon scenes with a sun/moon disc that can be placed left / center / right. */\nexport const DISC_PATTERNS = new Set([\n \"synthwave\",\n \"moonrise\",\n \"clouds\",\n \"dune\",\n \"aurora\",\n]);\n\n/** Scenes with a blowing-sand control (still / breeze / storm). */\nexport const SAND_PATTERNS = new Set([\n \"dune\",\n]);\n\n/** Star-field density — how many of the star field's points render (a \"range of stars\"). */\nexport type PatternDensity = \"sparse\" | \"medium\" | \"dense\";\nexport const PATTERN_DENSITIES: PatternDensity[] = [\n \"sparse\",\n \"medium\",\n \"dense\",\n];\nconst STAR_COUNT: Record = {\n sparse: 16,\n medium: 33,\n dense: 50,\n};\n/** Chase pellet-cell scale per density — sparse = larger cells (fewer per screen), dense = smaller (more). */\nconst DENSITY_SCALE: Record = {\n sparse: 1.4,\n medium: 1,\n dense: 0.68,\n};\n/** Tile-size scale for the geometric tiles (dots / grid) — a much airier ladder: the old sparse (1.4)\n * is the new DENSE, with medium and sparse extrapolated outward on the same ~1.4× step. */\nexport const TILE_SCALE: Record = {\n sparse: 2.8,\n medium: 2,\n dense: 1.4,\n};\n\n// SVG silhouettes as mask sources. White fill + transparent ground works whether the browser samples\n// the mask by alpha or luminance. Wrapped in url(...) after encoding so it's a valid CSS value.\n// `stretch: true` sets preserveAspectRatio='none' so a wide silhouette (the dune crests) fills the\n// element's 100% × 100% mask box instead of letterboxing to the viewBox aspect (the SVG default —\n// which renders a full-width mask as a centered strip with cliff edges).\nconst svgMask = (body: string, viewBox = \"0 0 100 100\", stretch = false) =>\n `url(\"data:image/svg+xml,${encodeURIComponent(`${body}`)}\")`;\n\n// The ghost silhouette — used by the chase scene's fleeing ghosts.\nconst GHOST = svgMask(\n \"\",\n);\n\n// Scattered \"points\" for the galaxy — a fixed set so it's deterministic (SSR-safe, no Math.random).\n// Kept as an array so the density control can render just a slice of them.\nconst STAR_GRADIENTS = [\n [\n 8,\n 12,\n ],\n [\n 22,\n 64,\n ],\n [\n 15,\n 88,\n ],\n [\n 35,\n 26,\n ],\n [\n 44,\n 72,\n ],\n [\n 52,\n 16,\n ],\n [\n 61,\n 52,\n ],\n [\n 68,\n 84,\n ],\n [\n 73,\n 34,\n ],\n [\n 82,\n 66,\n ],\n [\n 88,\n 20,\n ],\n [\n 92,\n 80,\n ],\n [\n 28,\n 44,\n ],\n [\n 57,\n 90,\n ],\n [\n 4,\n 40,\n ],\n [\n 12,\n 6,\n ],\n [\n 19,\n 30,\n ],\n [\n 26,\n 78,\n ],\n [\n 33,\n 54,\n ],\n [\n 40,\n 20,\n ],\n [\n 47,\n 46,\n ],\n [\n 54,\n 68,\n ],\n [\n 63,\n 12,\n ],\n [\n 66,\n 40,\n ],\n [\n 70,\n 60,\n ],\n [\n 77,\n 78,\n ],\n [\n 80,\n 8,\n ],\n [\n 84,\n 48,\n ],\n [\n 90,\n 60,\n ],\n [\n 95,\n 34,\n ],\n [\n 6,\n 70,\n ],\n [\n 48,\n 4,\n ],\n [\n 58,\n 30,\n ],\n [\n 76,\n 22,\n ],\n [\n 2,\n 22,\n ],\n [\n 16,\n 50,\n ],\n [\n 24,\n 14,\n ],\n [\n 38,\n 88,\n ],\n [\n 50,\n 40,\n ],\n [\n 56,\n 58,\n ],\n [\n 64,\n 74,\n ],\n [\n 72,\n 46,\n ],\n [\n 86,\n 38,\n ],\n [\n 94,\n 12,\n ],\n [\n 10,\n 82,\n ],\n [\n 30,\n 68,\n ],\n [\n 42,\n 34,\n ],\n [\n 60,\n 6,\n ],\n [\n 74,\n 90,\n ],\n [\n 96,\n 66,\n ],\n].map(([x, y], i) => {\n // a range of stars — occasional bright glints, most fine\n const r = i % 7 === 0 ? \"2px\" : i % 4 === 0 ? \"1.6px\" : i % 3 === 0 ? \"1.3px\" : \"1px\";\n /* --star-core is the fraction of the dot that stays SOLID before the falloff starts. Default 0%\n reproduces the original `#fff, transparent` exactly (white at 0, transparent at 100% — a pure\n linear falloff), so every scene that does not set it is untouched.\n It exists because \"full opacity\" was not the same as \"not faded\": a linear falloff averages only\n alpha 0.333 across the dot's area, so on a light sky the stars delivered ~+7 L rather than the\n +22 their peak suggests. On a near-black night sky that is invisible as a problem — any white\n reads — which is why it only ever showed up by day. */\n return `radial-gradient(${r} ${r} at ${x}% ${y}%, #fff 0 var(--star-core, 0%), transparent)`;\n});\n\n// Easter egg: the \"Muse\" — the Agent mascot's head (fedora + visored helmet) traced as a sparse star\n// constellation in every star field (starfield / synthwave / moonrise / clouds / dune / aurora). The dots are anchor points sampled\n// from the mascot SVG's bezier paths, cropped to the head (the coat sprawled into unreadable scatter),\n// thinned to ~45 (downsample, dedupe) and re-normalized; rendered as stars only — no fill, no lines — so the\n// fedora-and-helmet silhouette hides among the real stars until you catch it. Hue follows the accent-or-tint.\n// biome-ignore format: keep the sampled dots as one compact \"x,y x,y …\" string (not a 400-line array).\nconst MUSE_DOTS = \"67,66.5 65.1,52.1 66.7,42.3 75.8,38.4 11,48.8 82.9,45.3 92.3,42.7 100,36.8 84.4,32.8 85.2,58.8 75.5,65.3 71.9,58.3 51.9,42.9 26.4,48.3 60.9,59.3 38.5,42.3 0,50.4 18.3,38 19.3,28.2 19.9,14.7 25.7,5.9 37.5,2.8 51.1,0 62.9,0.5 72.8,5.6 77.5,15.3 79.7,25.7 96.2,90.9 67,78 43.4,85.6 87.4,72.2 87.4,82.7 16.9,65.6 48.2,59.2 37.5,62 28.1,64 19,87 27.2,88.7 88.4,92.4 38.6,70.3 51.5,81.1 21.5,73 33.6,81 53.7,65.1 30.7,40.5\".split(\" \").map((p) => p.split(\",\").map(Number));\nconst MUSE_STYLES = `\n.star-muse {\n position: absolute;\n left: 6%;\n top: 3%;\n width: 58vh;\n height: 53.6vh;\n pointer-events: none;\n --muse-h: var(--accent-h, var(--glass-tint-h));\n filter: drop-shadow(0 0 1px oklch(0.72 0.17 var(--muse-h) / 0.5));\n /* Rides the star gate: hidden with the stars in the Tron scenes' day mode (they set it to 0). In\n starfield the gate is 1 in BOTH modes now, so this sits at a flat 0.9 — the constellation is an\n easter egg and reads a touch under the field it hides in, rather than being mode-dependent. */\n opacity: calc(0.9 * var(--sw-stars-o, 1));\n}\n.star-muse circle {\n fill: oklch(0.96 0.04 var(--muse-h));\n}`;\n\nfunction MuseConstellation() {\n return (\n <>\n \n \n {MUSE_DOTS.map(([x, y], i) => (\n \n ))}\n \n \n );\n}\n\n// Shared sky furniture for the horizon scenes — the star layer (upper sky, fading toward the horizon,\n// day-hidden via the --sw-stars-o gate the palettes flip) and the palette-driven DISC. Every disc color is\n// a scene var (--sw-moon-1/2/3 core, --sw-maria L C H triple, --sw-disc-glow halo) set per mode by the\n// scene's [data-pattern] blocks, and --sw-disc-x carries the left/center/right placement control — so one\n// rule paints a rising violet moon, a desert silver moon, or a SUN with zero reskins. The sun is not a\n// second element: --sw-maria-o zeroes the craters (a sun has none) and --sw-disc-bloom adds the wide\n// second halo a bright source throws, which a moon does not. Both default to the moon's behaviour, so\n// moonrise and every night palette are untouched by their existence.\n// Placement knobs default to moonrise's horizon-dipping moon (40vh disc sitting ON the 42% horizon);\n// sky-borne scenes (clouds / dune) lift it with --moon-bottom and cancel the dip.\nconst SKY_STYLES = `\n.sw-stars {\n position: absolute;\n inset: 0 0 42% 0;\n background-repeat: no-repeat;\n opacity: var(--sw-stars-o, 1);\n -webkit-mask-image: linear-gradient(to bottom, #000 50%, transparent 100%);\n mask-image: linear-gradient(to bottom, #000 50%, transparent 100%);\n}\n.scene-moon {\n position: absolute;\n left: var(--sw-disc-x, 50%);\n bottom: var(--moon-bottom, 42%);\n width: var(--moon-size, 40vh);\n height: var(--moon-size, 40vh);\n transform: translate(-50%, var(--moon-dip, 30%));\n border-radius: 50%;\n background:\n radial-gradient(34% 30% at 63% 30%, oklch(var(--sw-maria) / calc(0.55 * var(--sw-maria-o, 1))) 0, transparent 62%),\n radial-gradient(20% 18% at 38% 56%, oklch(var(--sw-maria) / calc(0.5 * var(--sw-maria-o, 1))) 0, transparent 62%),\n radial-gradient(13% 12% at 56% 72%, oklch(var(--sw-maria) / calc(0.45 * var(--sw-maria-o, 1))) 0, transparent 62%),\n radial-gradient(circle at 42% 38%, var(--sw-moon-1) 0%, var(--sw-moon-2) 52%, var(--sw-moon-3) 100%);\n /* Two halos. The first is the moon's; the second is the sun's wide atmospheric bloom, a no-op\n (0 radius, transparent) unless a palette opts in. */\n filter:\n drop-shadow(0 0 var(--sw-disc-blur, 5vh) var(--sw-disc-glow))\n drop-shadow(0 0 var(--sw-disc-bloom, 0) var(--sw-disc-bloom-c, transparent));\n opacity: var(--sw-moon-o, 1);\n}\n/* The OUTRUN SUN — synthwave's disc, promoted to shared furniture so clouds / dune / aurora can put the\n same low banded sun on their day horizon. It sits ON the 42% line and dips below it; whatever the scene\n draws next (cloud deck, sand, water) covers the dipped half, so it reads as sitting in the horizon.\n Both discs render in every scene that wants either; --sw-sun-o / --sw-moon-o pick which one shows per\n mode, the same way --sw-stars-o gates the stars. Defaults are 1, so synthwave and moonrise — which\n choose their disc in JSX instead — are untouched. */\n.sw-sun {\n position: absolute;\n left: var(--sw-disc-x, 50%);\n bottom: 42%;\n width: var(--sun-size, 46vh);\n height: var(--sun-size, 46vh);\n transform: translate(-50%, 33%);\n border-radius: 50%;\n opacity: var(--sw-sun-o, 1);\n background: linear-gradient(\n to top,\n var(--sw-disc-1) 0%,\n var(--sw-disc-2) 24%,\n var(--sw-disc-3) 52%,\n var(--sw-disc-4) 78%,\n var(--sw-disc-5) 100%\n );\n -webkit-mask-image:\n linear-gradient(to bottom, #000 0 42%, transparent 42%),\n repeating-linear-gradient(to bottom, #000 0 var(--sun-band, 0.85vh), transparent var(--sun-band, 0.85vh) calc(var(--sun-band, 0.85vh) * 2));\n mask-image:\n linear-gradient(to bottom, #000 0 42%, transparent 42%),\n repeating-linear-gradient(to bottom, #000 0 var(--sun-band, 0.85vh), transparent var(--sun-band, 0.85vh) calc(var(--sun-band, 0.85vh) * 2));\n filter: drop-shadow(0 0 5vh var(--sw-disc-glow));\n}`;\n\n// Synthwave / Tron horizon — two variants: `synthwave` (outrun sun) and `moonrise` (purple-blue moon),\n// each with a DAY and a NIGHT palette so the scene follows the light/dark toggle. Every scene color is a\n// --sw-* custom property: day values sit on [data-pattern=\"…\"], night values on `.dark [data-pattern=\"…\"]`\n// (the injected \n
    \n
    \n {PAC_WALLS.map((w) => (\n \n ))}\n {PAC_LANES.map((lane) => (\n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n ))}\n {PAC_VLANES.map((lane) => (\n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n ))}\n
    \n
    \n );\n }\n if (style === \"synthwave\" || style === \"moonrise\") {\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n const moon = style === \"moonrise\";\n return (\n \n \n \n
    \n \n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n );\n }\n if (style === \"clouds\") {\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n return (\n \n \n \n
    \n \n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n );\n }\n if (style === \"dune\") {\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n // Dune faces light toward the disc: the diagonal face gradients read --du-light, so moving the\n // moon left/center/right relights every dune (and the ground sheet) to match.\n const duneLight = {\n left: \"160deg\",\n center: \"180deg\",\n right: \"200deg\",\n }[disc];\n // Sand level → veil opacity + speck multiplier. \"still\" zeroes BOTH (a calm desert has no airborne\n // sand at all); it is not the same as the speed control's \"static\", which only pauses motion.\n const [sandVeil, sandGrain] = {\n still: [\n \"0\",\n \"0\",\n ],\n breeze: [\n \"0.55\",\n \"1\",\n ],\n storm: [\n \"1\",\n \"1.8\",\n ],\n }[sand];\n return (\n \n \n \n
    \n \n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n {/* Wind veils ride ABOVE the ridges and BELOW the haze — sand in the air, not on the ground. */}\n
    \n
    \n
    \n
    \n {/* Haze LAST-but-one: it must wash over the far ridges, not sit behind them. */}\n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n );\n }\n if (style === \"aurora\") {\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n return (\n \n \n \n
    \n \n \n
    \n
    \n
    \n
    \n {/* AFTER the curtains: behind them, three layers of translucent light wash the disc green and\n it stops reading as a sun. Still before the glow and water, so the haze and waterline\n cover it the way they cover everything else at the horizon. */}\n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n );\n }\n if (style === \"starfield\") {\n // Scene colors live in STARFIELD_STYLES' palette blocks (an inline background would beat the .dark\n // day/night flip); only the star GLINTS — white in both modes — are painted inline, on their own\n // layer so the palette's --sw-stars-o can dim them by day.\n const stars = STAR_GRADIENTS.slice(0, STAR_COUNT[density]).join(\", \");\n return (\n \n \n
    \n \n \n
    \n );\n }\n return (\n \n );\n}\n", "type": "registry:component", "target": "components/pattern-background.tsx" }, diff --git a/public/r/badge.json b/public/r/badge.json index 0a741a65..b3ebd5ef 100644 --- a/public/r/badge.json +++ b/public/r/badge.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/breadcrumb.json b/public/r/breadcrumb.json index bd6dbdd4..bf16d7cd 100644 --- a/public/r/breadcrumb.json +++ b/public/r/breadcrumb.json @@ -22,7 +22,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/button-group.json b/public/r/button-group.json index 082bf0e5..03e45d57 100644 --- a/public/r/button-group.json +++ b/public/r/button-group.json @@ -22,7 +22,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/button.json b/public/r/button.json index 01146da3..455a7421 100644 --- a/public/r/button.json +++ b/public/r/button.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -33,7 +33,7 @@ }, { "path": "components/ui/button.tsx", - "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { type HoverEffect, hoverEffects } from \"@/lib/hover-effects\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* SEMANTIC variants only — each carries BEHAVIOR (text, hover, press shadows); the SURFACE comes from\n the material system (materialSurface below). The five materials + axes are the material/border/veil/\n gradient/glow/sheen props, not variants. */\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive active:scale-[0.96] active:transition-transform\",\n {\n variants: {\n variant: {\n glass: \"text-foreground hover:opacity-90 transition active:opacity-80 active:shadow-[var(--press-shadow-strong)]\",\n /* The one FILLED action (better-colors: one primary per view, colour on the background not the\n label). It now follows the page material: --srf-bg-image / --srf-shadow are inherited custom\n properties that styles.css sets on per [data-glass] and materials.css sets on any\n [data-material] ancestor, so reading them with the glass stack as fallback makes this button\n wear crystal / chakra / opaque like everything else. It previously hardcoded var(--glass-bg)\n and var(--glass-shadow), pinning the GLASS sheet no matter what the page was wearing.\n The FILL stays --primary/--primary-foreground rather than moving to the gradient accent: that\n pair is contrast-solved (measured worst Lc 87.4 light / 60.9 dark across all 17 presets),\n where the gradient sits at L 60–68 — a mid-tone where text of EITHER polarity runs out of\n room. Measured worst there was Lc 55.5 with text-foreground and no better with any other\n tier or alpha, against an ARC Bronze body floor of 60.\n It deliberately does NOT take the `glass` utility: that utility writes background-color, which\n would override bg-primary and leave the fill fighting it for nothing. */\n default:\n \"bg-primary text-primary-foreground [background-image:var(--srf-bg-image,var(--glass-stack-bg))] shadow-[var(--srf-shadow,var(--glass-shadow))] hover:brightness-105 dark:hover:brightness-95 transition active:opacity-90 active:shadow-[var(--press-shadow)]\",\n destructive:\n \"text-destructive border border-destructive/60 hover:opacity-90 transition active:opacity-80 focus-visible:ring-destructive/20 active:shadow-[var(--press-shadow-strong)]\",\n outline:\n \"text-foreground border-2 border-foreground/20 hover:border-foreground/40 dark:border-white/40 dark:hover:border-white/60 dark:text-white transition active:border-foreground/50 active:shadow-[var(--press-shadow-sm)]\",\n secondary: \"text-foreground hover:opacity-90 transition active:opacity-80 active:shadow-[var(--press-shadow-strong)]\",\n ghost:\n \"border border-border hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 active:bg-accent/80 dark:active:bg-accent/60 active:shadow-[var(--press-shadow-sm)]\",\n link: \"text-primary underline-offset-4 hover:underline active:opacity-80\",\n },\n size: {\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n size: \"default\",\n },\n },\n);\n\n/* Each semantic variant's surface role: glass = borderless adaptive glass; destructive/outline the\n same (their chrome is their own border); secondary a bordered surface; default/ghost/link render no\n glass surface. */\nconst SURFACE_ROLE: Record = {\n glass: {},\n destructive: {},\n outline: {\n size: \"sm\",\n },\n secondary: {\n border: true,\n },\n /* null = no `glass` utility, deliberately (see the variant above: it would overwrite bg-primary's\n background-color). `default` still tracks the page material, but through the INHERITED --srf-*\n tokens rather than the utility. The trade is that a per-instance material prop does not apply to\n this one variant — it follows the page, not the button. */\n default: null,\n ghost: null,\n link: null,\n};\n\nfunction Button({\n className,\n variant = \"glass\",\n size = \"default\",\n asChild = false,\n effect,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps & {\n asChild?: boolean;\n } & MaterialAxisProps & {\n effect?: HoverEffect;\n }) {\n const Comp = asChild ? SlotPrimitive.Slot : \"button\";\n\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(SURFACE_ROLE[variant ?? \"glass\"] ?? null, axes);\n\n return (\n \n );\n}\n\nexport { Button, buttonVariants };\n", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { type HoverEffect, hoverEffects } from \"@/lib/hover-effects\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* SEMANTIC variants only — each carries BEHAVIOR (text, hover, press shadows); the SURFACE comes from\n the material system (materialSurface below). The five materials + axes are the material/border/veil/\n gradient/glow/sheen props, not variants. */\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive active:scale-[0.96] active:transition-transform\",\n {\n variants: {\n variant: {\n glass: \"text-foreground hover:opacity-90 transition active:opacity-80 active:shadow-[var(--press-shadow-strong)]\",\n /* The one FILLED action (better-colors: one primary per view, colour on the background not the\n label). It now follows the page material: --srf-bg-image / --srf-shadow are inherited custom\n properties that styles.css sets on per [data-glass] and materials.css sets on any\n [data-material] ancestor, so reading them with the glass stack as fallback makes this button\n wear crystal / chakra / opaque like everything else. It previously hardcoded var(--glass-bg)\n and var(--glass-shadow), pinning the GLASS sheet no matter what the page was wearing.\n The FILL stays --primary/--primary-foreground rather than moving to the gradient accent: that\n pair is contrast-solved (measured worst Lc 87.4 light / 60.9 dark across all 17 presets),\n where the gradient sits at L 60–68 — a mid-tone where text of EITHER polarity runs out of\n room. Measured worst there was Lc 55.5 with text-foreground and no better with any other\n tier or alpha, against an ARC Bronze body floor of 60.\n It deliberately does NOT take the `glass` utility: that utility writes background-color, which\n would override bg-primary and leave the fill fighting it for nothing. */\n default:\n \"bg-primary text-primary-foreground [background-image:var(--srf-bg-image,var(--glass-stack-bg))] shadow-[var(--srf-shadow,var(--glass-shadow))] hover:brightness-105 dark:hover:brightness-95 transition active:opacity-90 active:shadow-[var(--press-shadow)]\",\n destructive:\n \"text-destructive border border-destructive/60 hover:opacity-90 transition active:opacity-80 focus-visible:ring-destructive/20 active:shadow-[var(--press-shadow-strong)]\",\n outline:\n \"text-foreground border-2 border-foreground/20 hover:border-foreground/40 dark:border-white/40 dark:hover:border-white/60 dark:text-white transition active:border-foreground/50 active:shadow-[var(--press-shadow-sm)]\",\n secondary: \"text-foreground hover:opacity-90 transition active:opacity-80 active:shadow-[var(--press-shadow-strong)]\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 active:bg-accent/80 dark:active:bg-accent/60 active:shadow-[var(--press-shadow-sm)]\",\n link: \"text-primary underline-offset-4 hover:underline active:opacity-80\",\n },\n size: {\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n size: \"default\",\n },\n },\n);\n\n/* Each semantic variant's surface role: glass = borderless adaptive glass; destructive/outline the\n same (their chrome is their own border); secondary a bordered surface; default/ghost/link render no\n glass surface. */\nconst SURFACE_ROLE: Record = {\n glass: {},\n destructive: {},\n outline: {\n size: \"sm\",\n },\n secondary: {\n border: true,\n },\n /* null = no `glass` utility, deliberately (see the variant above: it would overwrite bg-primary's\n background-color). `default` still tracks the page material, but through the INHERITED --srf-*\n tokens rather than the utility. The trade is that a per-instance material prop does not apply to\n this one variant — it follows the page, not the button. */\n default: null,\n ghost: null,\n link: null,\n};\n\nfunction Button({\n className,\n variant = \"glass\",\n size = \"default\",\n asChild = false,\n effect,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps & {\n asChild?: boolean;\n } & MaterialAxisProps & {\n effect?: HoverEffect;\n }) {\n const Comp = asChild ? SlotPrimitive.Slot : \"button\";\n\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(SURFACE_ROLE[variant ?? \"glass\"] ?? null, axes);\n\n return (\n \n );\n}\n\nexport { Button, buttonVariants };\n", "type": "registry:component", "target": "components/ui/button.tsx" } diff --git a/public/r/calendar.json b/public/r/calendar.json index 70b77f73..43f7c756 100644 --- a/public/r/calendar.json +++ b/public/r/calendar.json @@ -23,7 +23,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/card.json b/public/r/card.json index c9b0dc11..8c11340c 100644 --- a/public/r/card.json +++ b/public/r/card.json @@ -20,7 +20,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/carousel.json b/public/r/carousel.json index 4354b0d3..792f46c9 100644 --- a/public/r/carousel.json +++ b/public/r/carousel.json @@ -22,7 +22,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/chart.json b/public/r/chart.json index b38b870c..e65e66d3 100644 --- a/public/r/chart.json +++ b/public/r/chart.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -33,7 +33,7 @@ }, { "path": "components/ui/chart.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as RechartsPrimitive from \"recharts\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = {\n light: \"\",\n dark: \".dark\",\n} as const;\n\n/* Roles: the container and legend are bordered adaptive glass;\n the tooltip is bordered + veiled. */\nconst SURFACE_ROLE = {\n border: true,\n};\nconst TOOLTIP_ROLE = {\n border: true,\n veil: true,\n};\n\nexport type ChartConfig = {\n [key in string]: {\n label?: React.ReactNode;\n icon?: React.ComponentType;\n } & (\n | {\n color?: string;\n theme?: never;\n }\n | {\n color?: never;\n theme: {\n light?: string;\n dark?: string;\n };\n }\n );\n};\n\ntype ChartContextProps = {\n config: ChartConfig;\n};\n\nconst ChartContext = React.createContext(null);\n\nfunction useChart() {\n const context = React.useContext(ChartContext);\n\n if (!context) {\n throw new Error(\"useChart must be used within a \");\n }\n\n return context;\n}\n\nconst ChartContainer = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n config: ChartConfig;\n children: React.ComponentProps[\"children\"];\n } & MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }\n>(({ id, className, children, config, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const uniqueId = React.useId();\n const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`;\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"\",\n glass: \"rounded-lg p-4\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : SURFACE_ROLE, axes);\n\n return (\n \n \n // .recharts-cartesian-axis-tick-label > text). The old descendant selector matched nothing after\n // the upgrade, so every tick kept Recharts' own `fill=\"#666\"` — grey in all 17 themes, in both\n // modes, which is what made the axes unreadable. .recharts-cartesian-axis-tick-value sits ON the\n // in both Recharts 2 and 3, so it survives the structure change. (A CSS `fill` beats the\n // fill presentation attribute Recharts sets, so no !important is needed.)\n \"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick-value]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none\",\n variants[variant],\n className,\n )}\n {...rest}\n >\n \n {children}\n

    \n \n );\n});\nChartContainer.displayName = \"Chart\";\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);\n\n if (!colorConfig.length) {\n return null;\n }\n\n return (\n `\n${prefix} [data-chart=${id}] {\n${colorConfig\n .map(([key, itemConfig]) => {\n const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;\n return color ? ` --color-${key}: ${color};` : null;\n })\n .join(\"\\n\")}\n}\n`,\n )\n .join(\"\\n\"),\n }}\n />\n );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\ntype TooltipProps = React.ComponentProps & {\n active?: boolean;\n payload?: Array<{\n dataKey?: string;\n name?: string;\n type?: string;\n value?: number | string;\n color?: string;\n payload?: {\n fill?: string;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n }>;\n};\n\ntype TooltipPayloadItem = NonNullable[number];\n\nconst ChartTooltipContent = React.forwardRef<\n HTMLDivElement,\n Omit &\n React.ComponentProps<\"div\"> &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n indicator?: \"dot\" | \"line\" | \"dashed\";\n hideLabel?: boolean;\n hideIndicator?: boolean;\n nameKey?: string;\n labelKey?: string;\n label?: React.ReactNode;\n labelClassName?: string;\n color?: string;\n labelFormatter?: (value: React.ReactNode, payload: TooltipPayloadItem[]) => React.ReactNode;\n formatter?: (\n value: TooltipPayloadItem[\"value\"],\n name: TooltipPayloadItem[\"name\"],\n item: TooltipPayloadItem,\n index: number,\n payload: TooltipPayloadItem[\"payload\"],\n ) => React.ReactNode;\n }\n>(\n (\n {\n active,\n payload,\n className,\n indicator = \"dot\",\n variant = \"glass\",\n hideLabel = false,\n hideIndicator = false,\n label,\n labelFormatter,\n labelClassName,\n formatter,\n color,\n nameKey,\n labelKey,\n ...props\n },\n ref,\n ) => {\n /* This component never forwards unknown props to the DOM — only the axes are consumed. */\n const [axes] = splitAxisProps(props);\n const { config } = useChart();\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : TOOLTIP_ROLE, axes);\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null;\n }\n\n const [item] = payload;\n const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n const value = !labelKey && typeof label === \"string\" ? config[label]?.label || label : configItem?.label;\n\n if (labelFormatter) {\n return
    {labelFormatter(value, payload)}
    ;\n }\n\n if (!value) {\n return null;\n }\n\n return
    {value}
    ;\n }, [\n label,\n labelFormatter,\n payload,\n hideLabel,\n labelClassName,\n config,\n labelKey,\n ]);\n\n if (!active || !payload?.length) {\n return null;\n }\n\n const nestLabel = payload.length === 1 && indicator !== \"dot\";\n\n return (\n \n {!nestLabel ? tooltipLabel : null}\n
    \n {payload\n ?.filter((item) => item.type !== \"none\")\n .map((item, index) => {\n const key = `${nameKey || item.name || item.dataKey || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n const indicatorColor =\n color ||\n (\n item.payload as\n | {\n fill?: string;\n }\n | undefined\n )?.fill ||\n item.color;\n\n return (\n svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground\",\n indicator === \"dot\" && \"items-center\",\n )}\n >\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, item.payload)\n ) : (\n <>\n {configItem?.icon ? (\n \n ) : (\n !hideIndicator && (\n \n )\n )}\n
    \n
    \n {nestLabel ? tooltipLabel : null}\n {configItem?.label || item.name}\n
    \n {item.value != null && (\n \n {typeof item.value === \"number\" ? item.value.toLocaleString() : String(item.value)}\n \n )}\n
    \n \n )}\n
    \n );\n })}\n
    \n
    \n );\n },\n);\nChartTooltipContent.displayName = \"ChartTooltip\";\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\ntype LegendProps = {\n payload?: Array<{\n value?: string;\n dataKey?: string;\n type?: string;\n color?: string;\n [key: string]: unknown;\n }>;\n verticalAlign?: \"top\" | \"bottom\";\n};\n\nconst ChartLegendContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> &\n LegendProps &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n hideIcon?: boolean;\n nameKey?: string;\n }\n>(({ className, payload, verticalAlign = \"bottom\", variant = \"glass\", hideIcon = false, nameKey, ...props }, ref) => {\n /* This component never forwards unknown props to the DOM — only the axes are consumed. */\n const [axes] = splitAxisProps(props);\n const { config } = useChart();\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"\",\n glass: \"rounded-lg px-2 py-1\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : SURFACE_ROLE, axes);\n\n if (!payload?.length) {\n return null;\n }\n\n return (\n \n {payload\n .filter((item) => item.type !== \"none\")\n .map((item, index) => {\n const key = `${nameKey || item.dataKey || item.value || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n\n return (\n
    svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground\")}>\n {configItem?.icon && !hideIcon ? (\n \n ) : (\n \n )}\n {configItem?.label}\n
    \n );\n })}\n
    \n );\n});\nChartLegendContent.displayName = \"ChartLegend\";\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {\n if (typeof payload !== \"object\" || payload === null) {\n return undefined;\n }\n\n const payloadPayload = \"payload\" in payload && typeof payload.payload === \"object\" && payload.payload !== null ? payload.payload : undefined;\n\n let configLabelKey: string = key;\n\n if (key in payload && typeof payload[key as keyof typeof payload] === \"string\") {\n configLabelKey = payload[key as keyof typeof payload] as string;\n } else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\") {\n configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;\n }\n\n return configLabelKey in config ? config[configLabelKey] : config[key];\n}\n\nexport { ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent };\n", + "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as RechartsPrimitive from \"recharts\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = {\n light: \"\",\n dark: \".dark\",\n} as const;\n\n/* Roles: the container and legend are bordered adaptive glass;\n the tooltip is bordered + veiled. */\nconst SURFACE_ROLE = {\n border: true,\n};\nconst TOOLTIP_ROLE = {\n border: true,\n veil: true,\n};\n\nexport type ChartConfig = {\n [key in string]: {\n label?: React.ReactNode;\n icon?: React.ComponentType;\n } & (\n | {\n color?: string;\n theme?: never;\n }\n | {\n color?: never;\n theme: {\n light?: string;\n dark?: string;\n };\n }\n );\n};\n\ntype ChartContextProps = {\n config: ChartConfig;\n};\n\nconst ChartContext = React.createContext(null);\n\nfunction useChart() {\n const context = React.useContext(ChartContext);\n\n if (!context) {\n throw new Error(\"useChart must be used within a \");\n }\n\n return context;\n}\n\ntype PlotSize = {\n width: number;\n height: number;\n};\n\n/**\n * The plot box's size, measured by ONE ResizeObserver on an element whose size comes from CSS alone.\n *\n * Recharts' percent-sized ResponsiveContainer observes its own div, and the container used to give that\n * div `minHeight={150}` inside an `aspect-video` box: measure, grow to the floor, measure again — a\n * ResizeObserver feedback loop WebKit reports as \"ResizeObserver loop completed with undelivered\n * notifications\" and re-enters every frame. Here the measured box never contains the chart in flow (the\n * chart sits in an absolute layer), so what it measures cannot change what it measures.\n *\n * - `null` until the first non-empty measurement: nothing renders at 0×0, which is what a chart in a\n * hidden (display: none) tab measures, and why recharts warned \"width(0) and height(0) of chart\n * should be greater than 0\" on every pass.\n * - A 0×0 measurement after that is ignored, so a tab that hides keeps its last size rather than\n * unmounting the chart.\n * - `\"unobserved\"` when there is no ResizeObserver: the caller falls back to the percent container.\n */\nfunction usePlotSize(ref: React.RefObject): PlotSize | \"unobserved\" | null {\n const [size, setSize] = React.useState(null);\n\n React.useEffect(() => {\n const el = ref.current;\n if (!el) return;\n if (typeof ResizeObserver === \"undefined\") {\n setSize(\"unobserved\");\n return;\n }\n const observer = new ResizeObserver((entries) => {\n const box = entries[entries.length - 1]?.contentRect;\n if (!box) return;\n /* Floor, not round: a chart half a pixel wider than its layer would overflow it. */\n const width = Math.floor(box.width);\n const height = Math.floor(box.height);\n if (width <= 0 || height <= 0) return;\n setSize((prev) =>\n prev !== null && prev !== \"unobserved\" && prev.width === width && prev.height === height\n ? prev\n : {\n width,\n height,\n },\n );\n });\n observer.observe(el);\n return () => observer.disconnect();\n }, [\n ref,\n ]);\n\n return size;\n}\n\nconst ChartContainer = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n config: ChartConfig;\n children: React.ComponentProps[\"children\"];\n } & MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }\n>(({ id, className, children, config, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const uniqueId = React.useId();\n const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`;\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"\",\n glass: \"rounded-lg p-4\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : SURFACE_ROLE, axes);\n const plotRef = React.useRef(null);\n const plot = usePlotSize(plotRef);\n\n return (\n \n \n // .recharts-cartesian-axis-tick-label > text). The old descendant selector matched nothing after\n // the upgrade, so every tick kept Recharts' own `fill=\"#666\"` — grey in all 17 themes, in both\n // modes, which is what made the axes unreadable. .recharts-cartesian-axis-tick-value sits ON the\n // in both Recharts 2 and 3, so it survives the structure change. (A CSS `fill` beats the\n // fill presentation attribute Recharts sets, so no !important is needed.)\n /* ONE size contract: the caller's height (`h-[300px]`), else the aspect. `min-h-[150px]` is the\n floor the old `minHeight={150}` gave the chart, moved onto this box so it no longer feeds back\n through what the observer measures; a caller's min-h-* still wins. */\n \"flex min-h-[150px] aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick-value]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none\",\n variants[variant],\n className,\n )}\n {...rest}\n >\n \n
    \n {plot === \"unobserved\" ? (\n {children}\n ) : plot ? (\n /* Absolute, so the chart never sizes the box being measured. Numeric width + height take\n recharts' fixed-size path: no size-detector div and no second observer. */\n
    \n \n {children}\n \n
    \n ) : null}\n
    \n
    \n \n );\n});\nChartContainer.displayName = \"Chart\";\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);\n\n if (!colorConfig.length) {\n return null;\n }\n\n return (\n `\n${prefix} [data-chart=${id}] {\n${colorConfig\n .map(([key, itemConfig]) => {\n const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;\n return color ? ` --color-${key}: ${color};` : null;\n })\n .join(\"\\n\")}\n}\n`,\n )\n .join(\"\\n\"),\n }}\n />\n );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\ntype TooltipProps = React.ComponentProps & {\n active?: boolean;\n payload?: Array<{\n dataKey?: string;\n name?: string;\n type?: string;\n value?: number | string;\n color?: string;\n payload?: {\n fill?: string;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n }>;\n};\n\ntype TooltipPayloadItem = NonNullable[number];\n\nconst ChartTooltipContent = React.forwardRef<\n HTMLDivElement,\n Omit &\n React.ComponentProps<\"div\"> &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n indicator?: \"dot\" | \"line\" | \"dashed\";\n hideLabel?: boolean;\n hideIndicator?: boolean;\n nameKey?: string;\n labelKey?: string;\n label?: React.ReactNode;\n labelClassName?: string;\n color?: string;\n labelFormatter?: (value: React.ReactNode, payload: TooltipPayloadItem[]) => React.ReactNode;\n formatter?: (\n value: TooltipPayloadItem[\"value\"],\n name: TooltipPayloadItem[\"name\"],\n item: TooltipPayloadItem,\n index: number,\n payload: TooltipPayloadItem[\"payload\"],\n ) => React.ReactNode;\n }\n>(\n (\n {\n active,\n payload,\n className,\n indicator = \"dot\",\n variant = \"glass\",\n hideLabel = false,\n hideIndicator = false,\n label,\n labelFormatter,\n labelClassName,\n formatter,\n color,\n nameKey,\n labelKey,\n ...props\n },\n ref,\n ) => {\n /* This component never forwards unknown props to the DOM — only the axes are consumed. */\n const [axes] = splitAxisProps(props);\n const { config } = useChart();\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : TOOLTIP_ROLE, axes);\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null;\n }\n\n const [item] = payload;\n const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n const value = !labelKey && typeof label === \"string\" ? config[label]?.label || label : configItem?.label;\n\n if (labelFormatter) {\n return
    {labelFormatter(value, payload)}
    ;\n }\n\n if (!value) {\n return null;\n }\n\n return
    {value}
    ;\n }, [\n label,\n labelFormatter,\n payload,\n hideLabel,\n labelClassName,\n config,\n labelKey,\n ]);\n\n if (!active || !payload?.length) {\n return null;\n }\n\n const nestLabel = payload.length === 1 && indicator !== \"dot\";\n\n return (\n \n {!nestLabel ? tooltipLabel : null}\n
    \n {payload\n ?.filter((item) => item.type !== \"none\")\n .map((item, index) => {\n const key = `${nameKey || item.name || item.dataKey || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n const indicatorColor =\n color ||\n (\n item.payload as\n | {\n fill?: string;\n }\n | undefined\n )?.fill ||\n item.color;\n\n return (\n svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground\",\n indicator === \"dot\" && \"items-center\",\n )}\n >\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, item.payload)\n ) : (\n <>\n {configItem?.icon ? (\n \n ) : (\n !hideIndicator && (\n \n )\n )}\n
    \n
    \n {nestLabel ? tooltipLabel : null}\n {configItem?.label || item.name}\n
    \n {item.value != null && (\n \n {typeof item.value === \"number\" ? item.value.toLocaleString() : String(item.value)}\n \n )}\n
    \n \n )}\n
    \n );\n })}\n
    \n
    \n );\n },\n);\nChartTooltipContent.displayName = \"ChartTooltip\";\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\ntype LegendProps = {\n payload?: Array<{\n value?: string;\n dataKey?: string;\n type?: string;\n color?: string;\n [key: string]: unknown;\n }>;\n verticalAlign?: \"top\" | \"bottom\";\n};\n\nconst ChartLegendContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> &\n LegendProps &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n hideIcon?: boolean;\n nameKey?: string;\n }\n>(({ className, payload, verticalAlign = \"bottom\", variant = \"glass\", hideIcon = false, nameKey, ...props }, ref) => {\n /* This component never forwards unknown props to the DOM — only the axes are consumed. */\n const [axes] = splitAxisProps(props);\n const { config } = useChart();\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"\",\n glass: \"rounded-lg px-2 py-1\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : SURFACE_ROLE, axes);\n\n if (!payload?.length) {\n return null;\n }\n\n return (\n \n {payload\n .filter((item) => item.type !== \"none\")\n .map((item, index) => {\n const key = `${nameKey || item.dataKey || item.value || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n\n return (\n
    svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground\")}>\n {configItem?.icon && !hideIcon ? (\n \n ) : (\n \n )}\n {configItem?.label}\n
    \n );\n })}\n
    \n );\n});\nChartLegendContent.displayName = \"ChartLegend\";\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {\n if (typeof payload !== \"object\" || payload === null) {\n return undefined;\n }\n\n const payloadPayload = \"payload\" in payload && typeof payload.payload === \"object\" && payload.payload !== null ? payload.payload : undefined;\n\n let configLabelKey: string = key;\n\n if (key in payload && typeof payload[key as keyof typeof payload] === \"string\") {\n configLabelKey = payload[key as keyof typeof payload] as string;\n } else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\") {\n configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;\n }\n\n return configLabelKey in config ? config[configLabelKey] : config[key];\n}\n\nexport { ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent };\n", "type": "registry:component", "target": "components/ui/chart.tsx" } diff --git a/public/r/checkbox.json b/public/r/checkbox.json index 47a0a3e8..52c2d54c 100644 --- a/public/r/checkbox.json +++ b/public/r/checkbox.json @@ -22,7 +22,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/collapsible.json b/public/r/collapsible.json index 82e4c9d8..b628cbda 100644 --- a/public/r/collapsible.json +++ b/public/r/collapsible.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/combobox.json b/public/r/combobox.json new file mode 100644 index 00000000..2887acfc --- /dev/null +++ b/public/r/combobox.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "combobox", + "title": "Combobox", + "description": "An input with a filtered list of options, single or multiple with chips, on Base UI's Combobox with a veiled glass popup.", + "dependencies": [ + "@base-ui/react@^1.9.0", + "@phosphor-icons/react", + "class-variance-authority", + "clsx", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/button", + "@sistine/input-group", + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "content": "import { cva } from \"class-variance-authority\";\n\n/**\n * Shared hover effect variants for all Sistine components\n *\n * Effects:\n * - none: No hover effect\n * - glow: Themed glow shadow (--glass-glow, follows the tint hue) that intensifies on hover\n * - shimmer: Moving shimmer effect across the component\n * - ripple: Ripple effect that scales outward on hover\n * - lift: Subtle lift effect with shadow increase\n * - scale: Scale up slightly on hover\n */\nexport const hoverEffects = cva(\"transition duration-300\", {\n variants: {\n hover: {\n none: \"\",\n glow: \"glass-glow hover:glass-glow-lg\",\n shimmer:\n \"relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent hover:before:translate-x-full before:transition-transform before:duration-1000\",\n ripple:\n \"relative overflow-hidden after:absolute after:inset-0 after:scale-0 after:rounded-full after:bg-white/30 after:transition-transform after:duration-500 hover:after:scale-150\",\n lift: \"hover:-translate-y-1 hover:shadow-lg\",\n scale: \"hover:scale-105\",\n },\n },\n defaultVariants: {\n hover: \"none\",\n },\n});\n\nexport type HoverEffect = \"none\" | \"glow\" | \"shimmer\" | \"ripple\" | \"lift\" | \"scale\";\n", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "components/ui/combobox.tsx", + "content": "\"use client\";\n\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react\";\nimport { CaretDownIcon, CheckIcon, XIcon } from \"@phosphor-icons/react\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\nimport { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from \"./input-group\";\n\n/**\n * An input with a filtered list of options, single or multiple (chips), on Base UI's Combobox: the\n * primitive shadcn's combobox now uses, because Radix has none. The field is Sistine's InputGroup; the\n * popup is bordered, veiled glass like every other menu; the chips field is a bordered small-tier glass\n * surface like Input.\n *\n * Base UI marks state with presence attributes (`data-open`, `data-highlighted`, `data-disabled`) and\n * omits them when off, so the bare `data-disabled:` variant is safe here, unlike cmdk (see command.tsx).\n */\n\n/* Roles: the popup is a read-through overlay; the chips field is an input-sized surface. */\nconst POPUP_ROLE = {\n border: true,\n veil: true,\n};\nconst CHIPS_ROLE = {\n border: true,\n size: \"sm\" as const,\n};\n\nconst Combobox = ComboboxPrimitive.Root;\n\nfunction ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {\n return ;\n}\n\nfunction ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) {\n return (\n \n {children}\n \n \n );\n}\n\nfunction ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {\n return (\n }\n className={cn(className)}\n {...props}\n >\n \n \n );\n}\n\nfunction ComboboxInput({\n className,\n children,\n disabled = false,\n showTrigger = true,\n showClear = false,\n ...props\n}: ComboboxPrimitive.Input.Props & {\n showTrigger?: boolean;\n showClear?: boolean;\n}) {\n return (\n \n } {...props} />\n \n {showTrigger && (\n \n \n \n )}\n {showClear && }\n \n {children}\n \n );\n}\n\nfunction ComboboxContent({\n className,\n side = \"bottom\",\n sideOffset = 6,\n align = \"start\",\n alignOffset = 0,\n anchor,\n variant = \"glass\",\n ...props\n}: ComboboxPrimitive.Popup.Props &\n Pick &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : POPUP_ROLE, axes);\n\n return (\n \n \n \n \n \n );\n}\n\nfunction ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {\n return (\n \n );\n}\n\nfunction ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) {\n return (\n \n {children}\n }>\n \n \n \n );\n}\n\nfunction ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {\n return ;\n}\n\nfunction ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {\n return (\n \n );\n}\n\nfunction ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {\n return ;\n}\n\nfunction ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {\n return (\n \n );\n}\n\nfunction ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) {\n return ;\n}\n\nfunction ComboboxChips({\n className,\n ...props\n}: React.ComponentPropsWithRef & ComboboxPrimitive.Chips.Props & MaterialAxisProps) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(CHIPS_ROLE, axes);\n\n return (\n \n );\n}\n\nfunction ComboboxChip({\n className,\n children,\n showRemove = true,\n ...props\n}: ComboboxPrimitive.Chip.Props & {\n showRemove?: boolean;\n}) {\n return (\n \n {children}\n {showRemove && (\n }\n className=\"-ml-1 opacity-50 hover:opacity-100\"\n data-slot=\"combobox-chip-remove\"\n >\n \n \n )}\n \n );\n}\n\nfunction ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) {\n return (\n \n );\n}\n\nfunction useComboboxAnchor() {\n return React.useRef(null);\n}\n\nexport {\n Combobox,\n ComboboxChip,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxClear,\n ComboboxCollection,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxGroup,\n ComboboxInput,\n ComboboxItem,\n ComboboxLabel,\n ComboboxList,\n ComboboxSeparator,\n ComboboxTrigger,\n ComboboxValue,\n useComboboxAnchor,\n};\n", + "type": "registry:component", + "target": "components/ui/combobox.tsx" + } + ], + "type": "registry:component" +} \ No newline at end of file diff --git a/public/r/command.json b/public/r/command.json index e34eebd6..5b258f5b 100644 --- a/public/r/command.json +++ b/public/r/command.json @@ -24,7 +24,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -36,7 +36,7 @@ }, { "path": "components/ui/command.tsx", - "content": "\"use client\";\n\nimport { MagnifyingGlassIcon } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport type { Dialog as DialogPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"./dialog\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst commandVariants = cva(\"flex h-full w-full flex-col overflow-hidden rounded-xl\", {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst Command = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n );\n});\nCommand.displayName = CommandPrimitive.displayName;\n\nconst CommandDialog = ({\n title = \"Command Palette\",\n description = \"Search for a command to run...\",\n children,\n className,\n showCloseButton = true,\n ...props\n}: DialogPrimitive.DialogProps & {\n title?: string;\n description?: string;\n className?: string;\n showCloseButton?: boolean;\n}) => {\n return (\n

    \n {/* Radix a11y: DialogContent requires a title + description. */}\n \n {title}\n {description}\n \n \n \n {children}\n \n \n \n );\n};\n\nconst CommandInput = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n
    \n \n \n
    \n ),\n);\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef, React.ComponentPropsWithoutRef>(\n (props, ref) => ,\n);\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\nconst CommandItem = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => {\n return ;\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut };\n", + "content": "\"use client\";\n\nimport { MagnifyingGlassIcon } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport type { Dialog as DialogPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"./dialog\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst commandVariants = cva(\"flex h-full w-full flex-col overflow-hidden rounded-xl\", {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst Command = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n );\n});\nCommand.displayName = CommandPrimitive.displayName;\n\nconst CommandDialog = ({\n title = \"Command Palette\",\n description = \"Search for a command to run...\",\n children,\n className,\n showCloseButton = true,\n ...props\n}: DialogPrimitive.DialogProps & {\n title?: string;\n description?: string;\n className?: string;\n showCloseButton?: boolean;\n}) => {\n return (\n \n {/* Radix a11y: DialogContent requires a title + description. */}\n \n {title}\n {description}\n \n \n \n {children}\n \n \n \n );\n};\n\nconst CommandInput = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n
    \n \n \n
    \n ),\n);\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef, React.ComponentPropsWithoutRef>(\n (props, ref) => ,\n);\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\n/**\n * ⚠️ The disabled styles MUST be `data-[disabled=true]:`, never bare\n * `data-[disabled]:` — see `tests/command.test.tsx`.\n *\n * cmdk renders `data-disabled={!!disabled}` unconditionally, and React\n * stringifies booleans on `data-*` attributes, so an ENABLED item still carries\n * `data-disabled=\"false\"` in the DOM. Tailwind compiles the bare variant to the\n * attribute-PRESENCE selector `[data-disabled]`, which matches `\"false\"` just as\n * happily as `\"true\"` — so `pointer-events: none` landed on every item and no\n * entry in a picker could be clicked. Keyboard selection still worked, which is\n * what made it look like a mouse-only quirk rather than a stylesheet bug: cmdk\n * dispatches its own `cmdk-item-select` event straight at the node on Enter, and\n * that bypasses hit-testing entirely.\n *\n * The Radix menus next door keep the bare variant safely — Radix omits the\n * attribute outright when the item is enabled. cmdk is the one that always\n * emits it.\n */\nconst CommandItem = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => {\n return ;\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut };\n", "type": "registry:component", "target": "components/ui/command.tsx" } diff --git a/public/r/context-menu.json b/public/r/context-menu.json index 53662902..78a3532c 100644 --- a/public/r/context-menu.json +++ b/public/r/context-menu.json @@ -22,7 +22,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -34,13 +34,13 @@ }, { "path": "lib/snap-popper.ts", - "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Keeps text crisp on Radix popper surfaces (dropdown-menu, popover, tooltip, select, hover-card,\n * context-menu).\n *\n * THE DEFECT: Radix positions those surfaces by writing an inline `transform: translate(x, y)` onto a\n * wrapper element. Floating UI does not round x/y, so they land on fractions whenever the trigger does\n * — under browser zoom, fractional DPR, or any flex/grid trigger whose box isn't on a whole pixel. On\n * its own that would be harmless, but sistine's popper content carries `backdrop-filter` (it's glass),\n * which promotes it to its own compositing layer. A promoted layer at a fractional offset has its\n * glyphs rasterized off the device pixel grid and the text renders visibly soft.\n *\n * This is the same defect the dialogs had. There the fix was to stop using a transform to center; a\n * popper can't do that, because its position IS the transform. So instead: round the translate to whole\n * pixels. Sub-pixel placement is imperceptible; blurred text is not.\n *\n * Radix rewrites that transform on scroll, resize and collision flips, so a MutationObserver re-snaps\n * after each write. The observer is re-entrant — our own write retriggers it — so the write is skipped\n * when the value is already integral, which terminates the loop on the first pass.\n */\n/**\n * The whole snap, as a pure string transform: given the wrapper's SPECIFIED inline `transform`, return\n * what it should be rewritten to, or `null` to leave it alone.\n *\n * Returning null is load-bearing in two cases and both are re-entrancy guards, since our own write\n * retriggers the MutationObserver that called us:\n * - the value is already integral, so there is nothing to do and the loop terminates on pass one;\n * - the value isn't a translate we recognise (Radix hasn't positioned yet), so we must not invent one.\n *\n * Split out of the hook so it can be checked without a DOM — see scripts/check-units.mjs.\n */\nexport function snapTransform(specified: string): string | null {\n const parts = /^translate(?:3d)?\\(\\s*(-?[\\d.]+)px\\s*,\\s*(-?[\\d.]+)px/.exec(specified);\n if (!parts) return null;\n const next = `translate(${Math.round(Number(parts[1]))}px, ${Math.round(Number(parts[2]))}px)`;\n return specified === next ? null : next;\n}\n\nexport function useSnappedPopper(): React.RefCallback {\n const cleanupRef = React.useRef<(() => void) | null>(null);\n\n return React.useCallback((node: T | null) => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n if (!node) return;\n\n const wrapper = node.closest(\"[data-radix-popper-content-wrapper]\");\n if (!wrapper) return; // not popper-positioned (e.g. an inline/anchored render) — nothing to snap\n\n const snap = () => {\n // Read and write the SPECIFIED value, never the computed one. Under browser `zoom` the computed\n // transform is in a scaled space, so rounding it and writing the result back as authored px\n // never converges — snapTransform's equality guard would never hold and the observer would\n // re-enter forever.\n const next = snapTransform(wrapper.style.transform);\n if (next !== null) wrapper.style.transform = next;\n };\n\n snap();\n const observer = new MutationObserver(snap);\n observer.observe(wrapper, {\n attributes: true,\n attributeFilter: [\n \"style\",\n ],\n });\n cleanupRef.current = () => observer.disconnect();\n }, []);\n}\n\n/**\n * Merge the forwarded ref with the snapping ref, MEMOIZED — use this, not `composeRefs`, in JSX.\n *\n * A ref callback written inline gets a fresh identity on every render, and React responds by detaching\n * the old one (calling it with `null`) and attaching the new one. For a plain ref that is merely\n * wasteful; here the detach runs `useSnappedPopper`'s cleanup, so the MutationObserver watching the\n * popper wrapper is torn down and rebuilt on every single render of the surface.\n *\n * Same shape as Radix's own `useComposedRefs` — `useCallback` over the composed callback, with the\n * refs themselves as the dependency array, so the identity holds as long as the individual refs do.\n * Kept local rather than importing @radix-ui/react-compose-refs: these components ship through the\n * registry, and a direct dependency would have to be declared on all six of them.\n */\nexport function useComposedRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n /* The spread refs ARE the dependency list — the identity holds while each individual ref does. */\n return React.useCallback(composeRefs(...refs), refs);\n}\n\n/** Merge the forwarded ref with the snapping ref — both need the same node. Prefer the memoized\n * {@link useComposedRefs} anywhere the result is handed to JSX. */\nexport function composeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n return (node: T | null) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.MutableRefObject).current = node;\n }\n };\n}\n", + "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Keeps text crisp on Radix popper surfaces (dropdown-menu, popover, tooltip, select, hover-card,\n * context-menu).\n *\n * THE DEFECT: on a high-density display the text in a popper rendered visibly soft. Radix positions the\n * surface with an inline `transform: translate(x, y)` on a wrapper element, written from Floating UI's\n * `floatingStyles`, and at a device pixel ratio of 1.5 or more Floating UI adds `will-change: transform`\n * to the same style (`@floating-ui/react-dom`, `floatingStyles`). `will-change` promotes the wrapper to\n * its own compositing layer, and WebKit drops subpixel (LCD) antialiasing for text on a composited layer;\n * sistine's popper content is glass, so a blurred backdrop sits right behind that text. Resetting\n * `will-change` to `auto` is the fix. The cost is a repaint on reposition instead of a layer move, for a\n * surface that is open for seconds.\n *\n * The ROUNDING below predates that diagnosis. Its premise was that unrounded offsets put glyphs off the\n * pixel grid, but Floating UI already rounds x/y to the device pixel grid (`roundByDPR`) before writing\n * them. It is kept for browser zoom, where the device grid and the CSS-pixel grid diverge, which has not\n * been verified either way; it is cheap and harmless, and removing it is a separate decision.\n *\n * Radix rewrites the style on scroll, resize and collision flips, so a MutationObserver re-applies both\n * after each write. The observer is re-entrant (our own write retriggers it), so each write is skipped\n * when the value is already what we want, which terminates the loop on the second pass at the latest.\n */\n/**\n * The whole snap, as a pure string transform: given the wrapper's SPECIFIED inline `transform`, return\n * what it should be rewritten to, or `null` to leave it alone.\n *\n * Returning null is load-bearing in two cases and both are re-entrancy guards, since our own write\n * retriggers the MutationObserver that called us:\n * - the value is already integral, so there is nothing to do and the loop terminates on pass one;\n * - the value isn't a translate we recognise (Radix hasn't positioned yet), so we must not invent one.\n *\n * Split out of the hook so it can be checked without a DOM — see tests/snap-popper.test.tsx.\n */\nexport function snapTransform(specified: string): string | null {\n const parts = /^translate(?:3d)?\\(\\s*(-?[\\d.]+)px\\s*,\\s*(-?[\\d.]+)px/.exec(specified);\n if (!parts) return null;\n const next = `translate(${Math.round(Number(parts[1]))}px, ${Math.round(Number(parts[2]))}px)`;\n return specified === next ? null : next;\n}\n\nexport function useSnappedPopper(): React.RefCallback {\n const cleanupRef = React.useRef<(() => void) | null>(null);\n\n return React.useCallback((node: T | null) => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n if (!node) return;\n\n const wrapper = node.closest(\"[data-radix-popper-content-wrapper]\");\n if (!wrapper) return; // not popper-positioned (e.g. an inline/anchored render) — nothing to snap\n\n const snap = () => {\n // Read and write the SPECIFIED value, never the computed one. Under browser `zoom` the computed\n // transform is in a scaled space, so rounding it and writing the result back as authored px\n // never converges — snapTransform's equality guard would never hold and the observer would\n // re-enter forever.\n const next = snapTransform(wrapper.style.transform);\n if (next !== null) wrapper.style.transform = next;\n // Floating UI's will-change (DPR >= 1.5) is what costs the text its antialiasing. Only written when\n // something else is set, so the pass our own write triggers sees `auto` and stops.\n const willChange = wrapper.style.willChange;\n if (willChange !== \"\" && willChange !== \"auto\") wrapper.style.willChange = \"auto\";\n };\n\n snap();\n const observer = new MutationObserver(snap);\n observer.observe(wrapper, {\n attributes: true,\n attributeFilter: [\n \"style\",\n ],\n });\n cleanupRef.current = () => observer.disconnect();\n }, []);\n}\n\n/**\n * Merge the forwarded ref with the snapping ref, MEMOIZED — use this, not `composeRefs`, in JSX.\n *\n * A ref callback written inline gets a fresh identity on every render, and React responds by detaching\n * the old one (calling it with `null`) and attaching the new one. For a plain ref that is merely\n * wasteful; here the detach runs `useSnappedPopper`'s cleanup, so the MutationObserver watching the\n * popper wrapper is torn down and rebuilt on every single render of the surface.\n *\n * Same shape as Radix's own `useComposedRefs` — `useCallback` over the composed callback, with the\n * refs themselves as the dependency array, so the identity holds as long as the individual refs do.\n * Kept local rather than importing @radix-ui/react-compose-refs: these components ship through the\n * registry, and a direct dependency would have to be declared on all six of them.\n */\nexport function useComposedRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n /* The spread refs ARE the dependency list — the identity holds while each individual ref does. */\n return React.useCallback(composeRefs(...refs), refs);\n}\n\n/** Merge the forwarded ref with the snapping ref — both need the same node. Prefer the memoized\n * {@link useComposedRefs} anywhere the result is handed to JSX. */\nexport function composeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n return (node: T | null) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.MutableRefObject).current = node;\n }\n };\n}\n", "type": "registry:lib", "target": "lib/snap-popper.ts" }, { "path": "components/ui/context-menu.tsx", - "content": "\"use client\";\n\nimport { CaretRightIcon, Check, Circle } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { ContextMenu as ContextMenuPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { useComposedRefs, useSnappedPopper } from \"@/lib/snap-popper\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst contextMenuSubContentVariants = cva(\n \"z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-xl p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\nconst contextMenuContentVariants = cva(\n \"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst ContextMenu = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenu.displayName = ContextMenuPrimitive.Root.displayName;\n\nconst ContextMenuTrigger = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuTrigger.displayName = ContextMenuPrimitive.Trigger.displayName;\n\nconst ContextMenuGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuGroup.displayName = ContextMenuPrimitive.Group.displayName;\n\nconst ContextMenuPortal = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuPortal.displayName = ContextMenuPrimitive.Portal.displayName;\n\nconst ContextMenuSub = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuSub.displayName = ContextMenuPrimitive.Sub.displayName;\n\nconst ContextMenuRadioGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuRadioGroup.displayName = ContextMenuPrimitive.RadioGroup.displayName;\n\nconst ContextMenuSubTrigger = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, children, ...props }, ref) => (\n \n {children}\n \n \n));\nContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;\n\nconst ContextMenuSubContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n );\n});\nContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;\n\nconst ContextMenuContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", style, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n );\n});\nContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;\n\nconst ContextMenuItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n }\n>(({ className, inset, variant = \"default\", ...props }, ref) => (\n \n));\nContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;\n\nconst ContextMenuCheckboxItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, checked, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;\n\nconst ContextMenuRadioItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;\n\nconst ContextMenuLabel = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, ...props }, ref) => (\n \n));\nContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;\n\nconst ContextMenuSeparator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;\n\nconst ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {\n return ;\n};\nContextMenuShortcut.displayName = \"ContextMenuShortcut\";\n\nexport {\n ContextMenu,\n ContextMenuCheckboxItem,\n ContextMenuContent,\n ContextMenuGroup,\n ContextMenuItem,\n ContextMenuLabel,\n ContextMenuPortal,\n ContextMenuRadioGroup,\n ContextMenuRadioItem,\n ContextMenuSeparator,\n ContextMenuShortcut,\n ContextMenuSub,\n ContextMenuSubContent,\n ContextMenuSubTrigger,\n ContextMenuTrigger,\n};\n", + "content": "\"use client\";\n\nimport { CaretRightIcon, Check, Circle } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { ContextMenu as ContextMenuPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { useComposedRefs, useSnappedPopper } from \"@/lib/snap-popper\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst contextMenuSubContentVariants = cva(\n \"z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-xl p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\nconst contextMenuContentVariants = cva(\n \"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst ContextMenu = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenu.displayName = ContextMenuPrimitive.Root.displayName;\n\nconst ContextMenuTrigger = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuTrigger.displayName = ContextMenuPrimitive.Trigger.displayName;\n\nconst ContextMenuGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuGroup.displayName = ContextMenuPrimitive.Group.displayName;\n\nconst ContextMenuPortal = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuPortal.displayName = ContextMenuPrimitive.Portal.displayName;\n\nconst ContextMenuSub = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuSub.displayName = ContextMenuPrimitive.Sub.displayName;\n\nconst ContextMenuRadioGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuRadioGroup.displayName = ContextMenuPrimitive.RadioGroup.displayName;\n\nconst ContextMenuSubTrigger = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, children, ...props }, ref) => (\n \n {children}\n \n \n));\nContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;\n\nconst ContextMenuSubContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n /* Portaled like ContextMenuContent. A glass menu's backdrop-filter makes it the containing block of\n every fixed-position descendant, so a sub menu rendered inside it is placed against the menu and\n clipped by its overflow instead of floating beside it. */\n return (\n \n \n \n );\n});\nContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;\n\nconst ContextMenuContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", style, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n );\n});\nContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;\n\nconst ContextMenuItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n }\n>(({ className, inset, variant = \"default\", ...props }, ref) => (\n \n));\nContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;\n\nconst ContextMenuCheckboxItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, checked, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;\n\nconst ContextMenuRadioItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;\n\nconst ContextMenuLabel = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, ...props }, ref) => (\n \n));\nContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;\n\nconst ContextMenuSeparator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;\n\nconst ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {\n return ;\n};\nContextMenuShortcut.displayName = \"ContextMenuShortcut\";\n\nexport {\n ContextMenu,\n ContextMenuCheckboxItem,\n ContextMenuContent,\n ContextMenuGroup,\n ContextMenuItem,\n ContextMenuLabel,\n ContextMenuPortal,\n ContextMenuRadioGroup,\n ContextMenuRadioItem,\n ContextMenuSeparator,\n ContextMenuShortcut,\n ContextMenuSub,\n ContextMenuSubContent,\n ContextMenuSubTrigger,\n ContextMenuTrigger,\n};\n", "type": "registry:component", "target": "components/ui/context-menu.tsx" } diff --git a/public/r/cropper.json b/public/r/cropper.json index dd780624..67cb59f2 100644 --- a/public/r/cropper.json +++ b/public/r/cropper.json @@ -24,7 +24,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/date-picker-input.json b/public/r/date-picker-input.json index 9bcc654e..7a277e28 100644 --- a/public/r/date-picker-input.json +++ b/public/r/date-picker-input.json @@ -26,7 +26,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/dialog.json b/public/r/dialog.json index 15450100..41351dc8 100644 --- a/public/r/dialog.json +++ b/public/r/dialog.json @@ -23,7 +23,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -35,7 +35,7 @@ }, { "path": "components/ui/dialog.tsx", - "content": "\"use client\";\n\nimport { X } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst dialogContentVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"bg-background border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass at the elevated blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"lg\",\n};\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n VariantProps &\n MaterialAxisProps & {\n /** Elevated backdrop blur (folded from the glass wrapper). */\n animated?: boolean;\n /** Styles the inner scrolling wrapper around `children`. `className` targets the OUTER surface,\n * so without this a consumer has no way to change the body's layout — swap the grid for flex,\n * drop the gap, or take the scrolling over entirely.\n * Its `-m-6 p-6` is a matched pair that cancels the dialog's padding so the scroll container\n * stops clipping child shadows; override one and you must override the other. */\n bodyClassName?: string;\n showCloseButton?: boolean;\n }\n>(({ className, variant = \"glass\", children, animated = true, bodyClassName, showCloseButton = true, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n {/* min-h-0 is load-bearing: a grid item defaults to min-height:auto, which refuses to shrink\n below its content, so without it this never scrolls and the dialog just clips again.\n -m-6 p-6 cancels the dialog's own p-6 so this box's edges land ON the dialog's padding box\n instead of its content box. That matters because `overflow-y: auto` is not one-axis: when\n either axis is non-visible the other computes visible -> auto, so this is a scroll\n container HORIZONTALLY too, and a scroll container clips at its padding box. Sitting at the\n content box it sheared every child's box-shadow flat 24px inside the dialog edge — a hard\n vertical cut down the side of the last footer button and a hard horizontal one under the\n footer row. Cancelling the padding moves that clip out to where the dialog's own\n overflow-hidden already clips, which does the same job and follows the rounded corners.\n The two utilities are a pair: overriding padding alone via bodyClassName re-offsets the\n body by the leftover negative margin. */}\n

    {children}
    \n {showCloseButton && (\n \n \n Close\n \n )}\n \n \n );\n});\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (\n
    \n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({\n className,\n showCloseButton = false,\n children,\n ...props\n}: React.HTMLAttributes & {\n showCloseButton?: boolean;\n}) => (\n \n {children}\n {showCloseButton && (\n \n \n \n )}\n
    \n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger };\n", + "content": "\"use client\";\n\nimport { X } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst dialogContentVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"bg-background border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass at the elevated blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"lg\",\n};\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n VariantProps &\n MaterialAxisProps & {\n /** Elevated backdrop blur (folded from the glass wrapper). */\n animated?: boolean;\n /** Styles the layout wrapper around `children` (`data-slot=\"dialog-layout\"`). `className` targets\n * the OUTER surface, so without this a consumer has no way to change the body's layout — swap the\n * grid for flex, drop the gap, or take the scrolling over entirely.\n * Its `-m-6 p-6` is a matched pair that cancels the dialog's padding so the scroll container\n * stops clipping child shadows; override one and you must override the other. */\n bodyClassName?: string;\n showCloseButton?: boolean;\n }\n>(({ className, variant = \"glass\", children, animated = true, bodyClassName, showCloseButton = true, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n {/* The layout wrapper has two modes.\n WITHOUT a DialogBody child it is the scroll box, as it always was: header, content and footer\n scroll together.\n WITH one (`has-[>[data-slot=dialog-body]]:`) it becomes a non-clipping flex column and only\n the body scrolls: the header and footer are chrome, not content, and inside a scroll box a\n glass button's glow was sliced at whichever edge it touched (overflow-y:auto clips on all\n four sides, see below), and the footer scrolled away with the text.\n min-w-0 + [&>*]:min-w-0: grid and flex items default to min-width:auto, so one unbreakable\n child (a long path with `truncate`) set the track WIDER than the dialog and every line laid\n out to that phantom width, clipped at the border. Letting the items shrink fixes wrap/truncate.\n min-h-0 is load-bearing: a grid item defaults to min-height:auto, which refuses to shrink\n below its content, so without it this never scrolls and the dialog just clips again.\n -m-6 p-6 cancels the dialog's own p-6 so this box's edges land ON the dialog's padding box\n instead of its content box. That matters because `overflow-y: auto` is not one-axis: when\n either axis is non-visible the other computes visible -> auto, so this is a scroll\n container HORIZONTALLY too, and a scroll container clips at its padding box. Sitting at the\n content box it sheared every child's box-shadow flat 24px inside the dialog edge — a hard\n vertical cut down the side of the last footer button and a hard horizontal one under the\n footer row. Cancelling the padding moves that clip out to where the dialog's own\n overflow-hidden already clips, which does the same job and follows the rounded corners.\n The two utilities are a pair: overriding padding alone via bodyClassName re-offsets the\n body by the leftover negative margin. */}\n *]:min-w-0\",\n \"has-[>[data-slot=dialog-body]]:flex has-[>[data-slot=dialog-body]]:flex-col has-[>[data-slot=dialog-body]]:overflow-visible\",\n bodyClassName,\n )}\n >\n {children}\n
    \n {showCloseButton && (\n \n \n Close\n \n )}\n \n \n );\n});\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (\n \n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/**\n * The scrolling region of a dialog. Optional: put it between DialogHeader and DialogFooter and only it\n * scrolls, with the header and footer fixed around it; leave it out and the whole dialog content scrolls\n * as one, as before.\n * `-mx-6 px-6` runs the scroll box out to the dialog's edges (a scroll container clips at its padding\n * box, so sitting at the content box it sheared child shadows 24px inside the edge), and `-my-2 py-2`\n * gives a glow 8px of room above and below without moving the content.\n */\nconst DialogBody = ({ className, ...props }: React.HTMLAttributes) => (\n *]:min-w-0\", className)}\n {...props}\n />\n);\nDialogBody.displayName = \"DialogBody\";\n\nconst DialogFooter = ({\n className,\n showCloseButton = false,\n children,\n ...props\n}: React.HTMLAttributes & {\n showCloseButton?: boolean;\n}) => (\n \n {children}\n {showCloseButton && (\n \n \n \n )}\n
    \n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n Dialog,\n DialogBody,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n", "type": "registry:component", "target": "components/ui/dialog.tsx" } diff --git a/public/r/drawer.json b/public/r/drawer.json index abbf37d6..4773f00c 100644 --- a/public/r/drawer.json +++ b/public/r/drawer.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/dropdown-menu.json b/public/r/dropdown-menu.json index 7d9035df..9f5837f7 100644 --- a/public/r/dropdown-menu.json +++ b/public/r/dropdown-menu.json @@ -22,7 +22,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -34,13 +34,13 @@ }, { "path": "lib/snap-popper.ts", - "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Keeps text crisp on Radix popper surfaces (dropdown-menu, popover, tooltip, select, hover-card,\n * context-menu).\n *\n * THE DEFECT: Radix positions those surfaces by writing an inline `transform: translate(x, y)` onto a\n * wrapper element. Floating UI does not round x/y, so they land on fractions whenever the trigger does\n * — under browser zoom, fractional DPR, or any flex/grid trigger whose box isn't on a whole pixel. On\n * its own that would be harmless, but sistine's popper content carries `backdrop-filter` (it's glass),\n * which promotes it to its own compositing layer. A promoted layer at a fractional offset has its\n * glyphs rasterized off the device pixel grid and the text renders visibly soft.\n *\n * This is the same defect the dialogs had. There the fix was to stop using a transform to center; a\n * popper can't do that, because its position IS the transform. So instead: round the translate to whole\n * pixels. Sub-pixel placement is imperceptible; blurred text is not.\n *\n * Radix rewrites that transform on scroll, resize and collision flips, so a MutationObserver re-snaps\n * after each write. The observer is re-entrant — our own write retriggers it — so the write is skipped\n * when the value is already integral, which terminates the loop on the first pass.\n */\n/**\n * The whole snap, as a pure string transform: given the wrapper's SPECIFIED inline `transform`, return\n * what it should be rewritten to, or `null` to leave it alone.\n *\n * Returning null is load-bearing in two cases and both are re-entrancy guards, since our own write\n * retriggers the MutationObserver that called us:\n * - the value is already integral, so there is nothing to do and the loop terminates on pass one;\n * - the value isn't a translate we recognise (Radix hasn't positioned yet), so we must not invent one.\n *\n * Split out of the hook so it can be checked without a DOM — see scripts/check-units.mjs.\n */\nexport function snapTransform(specified: string): string | null {\n const parts = /^translate(?:3d)?\\(\\s*(-?[\\d.]+)px\\s*,\\s*(-?[\\d.]+)px/.exec(specified);\n if (!parts) return null;\n const next = `translate(${Math.round(Number(parts[1]))}px, ${Math.round(Number(parts[2]))}px)`;\n return specified === next ? null : next;\n}\n\nexport function useSnappedPopper(): React.RefCallback {\n const cleanupRef = React.useRef<(() => void) | null>(null);\n\n return React.useCallback((node: T | null) => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n if (!node) return;\n\n const wrapper = node.closest(\"[data-radix-popper-content-wrapper]\");\n if (!wrapper) return; // not popper-positioned (e.g. an inline/anchored render) — nothing to snap\n\n const snap = () => {\n // Read and write the SPECIFIED value, never the computed one. Under browser `zoom` the computed\n // transform is in a scaled space, so rounding it and writing the result back as authored px\n // never converges — snapTransform's equality guard would never hold and the observer would\n // re-enter forever.\n const next = snapTransform(wrapper.style.transform);\n if (next !== null) wrapper.style.transform = next;\n };\n\n snap();\n const observer = new MutationObserver(snap);\n observer.observe(wrapper, {\n attributes: true,\n attributeFilter: [\n \"style\",\n ],\n });\n cleanupRef.current = () => observer.disconnect();\n }, []);\n}\n\n/**\n * Merge the forwarded ref with the snapping ref, MEMOIZED — use this, not `composeRefs`, in JSX.\n *\n * A ref callback written inline gets a fresh identity on every render, and React responds by detaching\n * the old one (calling it with `null`) and attaching the new one. For a plain ref that is merely\n * wasteful; here the detach runs `useSnappedPopper`'s cleanup, so the MutationObserver watching the\n * popper wrapper is torn down and rebuilt on every single render of the surface.\n *\n * Same shape as Radix's own `useComposedRefs` — `useCallback` over the composed callback, with the\n * refs themselves as the dependency array, so the identity holds as long as the individual refs do.\n * Kept local rather than importing @radix-ui/react-compose-refs: these components ship through the\n * registry, and a direct dependency would have to be declared on all six of them.\n */\nexport function useComposedRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n /* The spread refs ARE the dependency list — the identity holds while each individual ref does. */\n return React.useCallback(composeRefs(...refs), refs);\n}\n\n/** Merge the forwarded ref with the snapping ref — both need the same node. Prefer the memoized\n * {@link useComposedRefs} anywhere the result is handed to JSX. */\nexport function composeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n return (node: T | null) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.MutableRefObject).current = node;\n }\n };\n}\n", + "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Keeps text crisp on Radix popper surfaces (dropdown-menu, popover, tooltip, select, hover-card,\n * context-menu).\n *\n * THE DEFECT: on a high-density display the text in a popper rendered visibly soft. Radix positions the\n * surface with an inline `transform: translate(x, y)` on a wrapper element, written from Floating UI's\n * `floatingStyles`, and at a device pixel ratio of 1.5 or more Floating UI adds `will-change: transform`\n * to the same style (`@floating-ui/react-dom`, `floatingStyles`). `will-change` promotes the wrapper to\n * its own compositing layer, and WebKit drops subpixel (LCD) antialiasing for text on a composited layer;\n * sistine's popper content is glass, so a blurred backdrop sits right behind that text. Resetting\n * `will-change` to `auto` is the fix. The cost is a repaint on reposition instead of a layer move, for a\n * surface that is open for seconds.\n *\n * The ROUNDING below predates that diagnosis. Its premise was that unrounded offsets put glyphs off the\n * pixel grid, but Floating UI already rounds x/y to the device pixel grid (`roundByDPR`) before writing\n * them. It is kept for browser zoom, where the device grid and the CSS-pixel grid diverge, which has not\n * been verified either way; it is cheap and harmless, and removing it is a separate decision.\n *\n * Radix rewrites the style on scroll, resize and collision flips, so a MutationObserver re-applies both\n * after each write. The observer is re-entrant (our own write retriggers it), so each write is skipped\n * when the value is already what we want, which terminates the loop on the second pass at the latest.\n */\n/**\n * The whole snap, as a pure string transform: given the wrapper's SPECIFIED inline `transform`, return\n * what it should be rewritten to, or `null` to leave it alone.\n *\n * Returning null is load-bearing in two cases and both are re-entrancy guards, since our own write\n * retriggers the MutationObserver that called us:\n * - the value is already integral, so there is nothing to do and the loop terminates on pass one;\n * - the value isn't a translate we recognise (Radix hasn't positioned yet), so we must not invent one.\n *\n * Split out of the hook so it can be checked without a DOM — see tests/snap-popper.test.tsx.\n */\nexport function snapTransform(specified: string): string | null {\n const parts = /^translate(?:3d)?\\(\\s*(-?[\\d.]+)px\\s*,\\s*(-?[\\d.]+)px/.exec(specified);\n if (!parts) return null;\n const next = `translate(${Math.round(Number(parts[1]))}px, ${Math.round(Number(parts[2]))}px)`;\n return specified === next ? null : next;\n}\n\nexport function useSnappedPopper(): React.RefCallback {\n const cleanupRef = React.useRef<(() => void) | null>(null);\n\n return React.useCallback((node: T | null) => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n if (!node) return;\n\n const wrapper = node.closest(\"[data-radix-popper-content-wrapper]\");\n if (!wrapper) return; // not popper-positioned (e.g. an inline/anchored render) — nothing to snap\n\n const snap = () => {\n // Read and write the SPECIFIED value, never the computed one. Under browser `zoom` the computed\n // transform is in a scaled space, so rounding it and writing the result back as authored px\n // never converges — snapTransform's equality guard would never hold and the observer would\n // re-enter forever.\n const next = snapTransform(wrapper.style.transform);\n if (next !== null) wrapper.style.transform = next;\n // Floating UI's will-change (DPR >= 1.5) is what costs the text its antialiasing. Only written when\n // something else is set, so the pass our own write triggers sees `auto` and stops.\n const willChange = wrapper.style.willChange;\n if (willChange !== \"\" && willChange !== \"auto\") wrapper.style.willChange = \"auto\";\n };\n\n snap();\n const observer = new MutationObserver(snap);\n observer.observe(wrapper, {\n attributes: true,\n attributeFilter: [\n \"style\",\n ],\n });\n cleanupRef.current = () => observer.disconnect();\n }, []);\n}\n\n/**\n * Merge the forwarded ref with the snapping ref, MEMOIZED — use this, not `composeRefs`, in JSX.\n *\n * A ref callback written inline gets a fresh identity on every render, and React responds by detaching\n * the old one (calling it with `null`) and attaching the new one. For a plain ref that is merely\n * wasteful; here the detach runs `useSnappedPopper`'s cleanup, so the MutationObserver watching the\n * popper wrapper is torn down and rebuilt on every single render of the surface.\n *\n * Same shape as Radix's own `useComposedRefs` — `useCallback` over the composed callback, with the\n * refs themselves as the dependency array, so the identity holds as long as the individual refs do.\n * Kept local rather than importing @radix-ui/react-compose-refs: these components ship through the\n * registry, and a direct dependency would have to be declared on all six of them.\n */\nexport function useComposedRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n /* The spread refs ARE the dependency list — the identity holds while each individual ref does. */\n return React.useCallback(composeRefs(...refs), refs);\n}\n\n/** Merge the forwarded ref with the snapping ref — both need the same node. Prefer the memoized\n * {@link useComposedRefs} anywhere the result is handed to JSX. */\nexport function composeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n return (node: T | null) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.MutableRefObject).current = node;\n }\n };\n}\n", "type": "registry:lib", "target": "lib/snap-popper.ts" }, { "path": "components/ui/dropdown-menu.tsx", - "content": "\"use client\";\n\nimport { CaretRightIcon, Check, Circle } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { useComposedRefs, useSnappedPopper } from \"@/lib/snap-popper\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst dropdownMenuSubContentVariants = cva(\n \"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-xl p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\nconst dropdownMenuContentVariants = cva(\n \"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst DropdownMenu = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenu.displayName = DropdownMenuPrimitive.Root.displayName;\n\nconst DropdownMenuTrigger = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenuTrigger.displayName = DropdownMenuPrimitive.Trigger.displayName;\n\nconst DropdownMenuGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenuGroup.displayName = DropdownMenuPrimitive.Group.displayName;\n\nconst DropdownMenuPortal = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenuPortal.displayName = DropdownMenuPrimitive.Portal.displayName;\n\nconst DropdownMenuSub = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenuSub.displayName = DropdownMenuPrimitive.Sub.displayName;\n\nconst DropdownMenuRadioGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenuRadioGroup.displayName = DropdownMenuPrimitive.RadioGroup.displayName;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, children, ...props }, ref) => (\n \n {children}\n \n \n));\nDropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n );\n});\nDropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, sideOffset = 4, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n );\n});\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n }\n>(({ className, inset, variant = \"default\", ...props }, ref) => (\n \n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, checked, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nDropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, ...props }, ref) => (\n \n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {\n return ;\n};\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\";\n\nexport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuPortal,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n};\n", + "content": "\"use client\";\n\nimport { CaretRightIcon, Check, Circle } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { useComposedRefs, useSnappedPopper } from \"@/lib/snap-popper\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst dropdownMenuSubContentVariants = cva(\n \"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-xl p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\nconst dropdownMenuContentVariants = cva(\n \"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst DropdownMenu = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenu.displayName = DropdownMenuPrimitive.Root.displayName;\n\nconst DropdownMenuTrigger = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenuTrigger.displayName = DropdownMenuPrimitive.Trigger.displayName;\n\nconst DropdownMenuGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenuGroup.displayName = DropdownMenuPrimitive.Group.displayName;\n\nconst DropdownMenuPortal = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenuPortal.displayName = DropdownMenuPrimitive.Portal.displayName;\n\nconst DropdownMenuSub = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenuSub.displayName = DropdownMenuPrimitive.Sub.displayName;\n\nconst DropdownMenuRadioGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nDropdownMenuRadioGroup.displayName = DropdownMenuPrimitive.RadioGroup.displayName;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, children, ...props }, ref) => (\n \n {children}\n \n \n));\nDropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n /* Portaled like DropdownMenuContent. A glass menu's backdrop-filter makes it the containing block of\n every fixed-position descendant, so a sub menu rendered inside it is placed against the menu and\n clipped by its overflow instead of floating beside it. */\n return (\n \n \n \n );\n});\nDropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, sideOffset = 4, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n );\n});\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n }\n>(({ className, inset, variant = \"default\", ...props }, ref) => (\n \n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, checked, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nDropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, ...props }, ref) => (\n \n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {\n return ;\n};\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\";\n\nexport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuPortal,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n};\n", "type": "registry:component", "target": "components/ui/dropdown-menu.tsx" } diff --git a/public/r/empty-state.json b/public/r/empty-state.json index 0a07b66a..dd3f8993 100644 --- a/public/r/empty-state.json +++ b/public/r/empty-state.json @@ -2,7 +2,7 @@ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "empty-state", "title": "Empty State", - "description": "An empty state component, with the glass material system.", + "description": "An empty state component, with the glass material system. Legacy: prefer `empty`, which follows shadcn's anatomy (Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent).", "dependencies": [ "class-variance-authority", "clsx", @@ -20,7 +20,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -32,7 +32,7 @@ }, { "path": "components/ui/empty-state.tsx", - "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { type HoverEffect, hoverEffects } from \"@/lib/hover-effects\";\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst emptyStateVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground border shadow-sm\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: borderless adaptive glass. */\nconst ROLE = {};\n\nexport interface EmptyStateProps extends React.HTMLAttributes, VariantProps, MaterialAxisProps {\n effect?: HoverEffect;\n}\n\nconst EmptyState = React.forwardRef(({ className, variant = \"glass\", effect, children, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n {children}\n

    \n );\n});\nEmptyState.displayName = \"EmptyState\";\n\nfunction EmptyStateIcon({ className, ...props }: React.HTMLAttributes) {\n return
    ;\n}\n\nfunction EmptyStateTitle({ className, ...props }: React.HTMLAttributes) {\n return

    ;\n}\n\nfunction EmptyStateDescription({ className, ...props }: React.HTMLAttributes) {\n return

    ;\n}\n\nexport { EmptyState, EmptyStateDescription, EmptyStateIcon, EmptyStateTitle };\n", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { type HoverEffect, hoverEffects } from \"@/lib/hover-effects\";\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Legacy: prefer `empty` (components/ui/empty.tsx), which follows shadcn's anatomy. Kept so existing\n imports keep working. */\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst emptyStateVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground border shadow-sm\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: borderless adaptive glass. */\nconst ROLE = {};\n\nexport interface EmptyStateProps extends React.HTMLAttributes, VariantProps, MaterialAxisProps {\n effect?: HoverEffect;\n}\n\nconst EmptyState = React.forwardRef(({ className, variant = \"glass\", effect, children, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n {children}\n

    \n );\n});\nEmptyState.displayName = \"EmptyState\";\n\nfunction EmptyStateIcon({ className, ...props }: React.HTMLAttributes) {\n return
    ;\n}\n\nfunction EmptyStateTitle({ className, ...props }: React.HTMLAttributes) {\n return

    ;\n}\n\nfunction EmptyStateDescription({ className, ...props }: React.HTMLAttributes) {\n return

    ;\n}\n\nexport { EmptyState, EmptyStateDescription, EmptyStateIcon, EmptyStateTitle };\n", "type": "registry:component", "target": "components/ui/empty-state.tsx" } diff --git a/public/r/empty.json b/public/r/empty.json new file mode 100644 index 00000000..2bfd2ccd --- /dev/null +++ b/public/r/empty.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "empty", + "title": "Empty", + "description": "An empty state for a list, table or panel with nothing in it yet: media, title, description and actions, as plain layout or on its own glass surface.", + "dependencies": [ + "class-variance-authority", + "clsx", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "content": "import { cva } from \"class-variance-authority\";\n\n/**\n * Shared hover effect variants for all Sistine components\n *\n * Effects:\n * - none: No hover effect\n * - glow: Themed glow shadow (--glass-glow, follows the tint hue) that intensifies on hover\n * - shimmer: Moving shimmer effect across the component\n * - ripple: Ripple effect that scales outward on hover\n * - lift: Subtle lift effect with shadow increase\n * - scale: Scale up slightly on hover\n */\nexport const hoverEffects = cva(\"transition duration-300\", {\n variants: {\n hover: {\n none: \"\",\n glow: \"glass-glow hover:glass-glow-lg\",\n shimmer:\n \"relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent hover:before:translate-x-full before:transition-transform before:duration-1000\",\n ripple:\n \"relative overflow-hidden after:absolute after:inset-0 after:scale-0 after:rounded-full after:bg-white/30 after:transition-transform after:duration-500 hover:after:scale-150\",\n lift: \"hover:-translate-y-1 hover:shadow-lg\",\n scale: \"hover:scale-105\",\n },\n },\n defaultVariants: {\n hover: \"none\",\n },\n});\n\nexport type HoverEffect = \"none\" | \"glow\" | \"shimmer\" | \"ripple\" | \"lift\" | \"scale\";\n", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "components/ui/empty.tsx", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * An empty state: a centred message where content will be, under shadcn's name and anatomy (Empty,\n * EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent). `empty-state` is the older,\n * Sistine-only take on the same thing; prefer this for new work.\n *\n * The root is plain layout by default, because an empty state usually sits INSIDE a surface that is\n * already glass (a card, a table, a panel); `variant=\"glass\"` makes it a surface of its own.\n */\n\n/* Role: borderless adaptive glass, only when asked for. */\nconst ROLE = {};\n/* The icon tile: a small bordered chip. */\nconst MEDIA_ROLE = {\n border: true,\n size: \"sm\" as const,\n};\n\nfunction Empty({\n className,\n variant = \"default\",\n ...props\n}: React.ComponentProps<\"div\"> &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"glass\" ? ROLE : null, axes);\n\n return (\n \n );\n}\n\nfunction EmptyHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return

    ;\n}\n\n/* Variant classes carry BEHAVIOR only; the icon tile's surface comes from materialSurface. */\nconst emptyMediaVariants = cva(\"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0\", {\n variants: {\n variant: {\n default: \"bg-transparent\",\n icon: \"size-10 rounded-lg text-foreground-ui [&_svg:not([class*='size-'])]:size-6\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n});\n\nfunction EmptyMedia({\n className,\n variant = \"default\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps & MaterialAxisProps) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"icon\" ? MEDIA_ROLE : null, axes);\n\n return (\n \n );\n}\n\nfunction EmptyTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return
    ;\n}\n\nfunction EmptyDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary\", className)}\n {...props}\n />\n );\n}\n\nfunction EmptyContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nexport { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle };\n", + "type": "registry:component", + "target": "components/ui/empty.tsx" + } + ], + "type": "registry:component" +} \ No newline at end of file diff --git a/public/r/field.json b/public/r/field.json new file mode 100644 index 00000000..5a1932d8 --- /dev/null +++ b/public/r/field.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "field", + "title": "Field", + "description": "Form field layout (label, control, description, error) in vertical, horizontal or responsive orientation, with fieldsets, legends and separators.", + "dependencies": [ + "class-variance-authority", + "clsx", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/label", + "@sistine/separator", + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "content": "import { cva } from \"class-variance-authority\";\n\n/**\n * Shared hover effect variants for all Sistine components\n *\n * Effects:\n * - none: No hover effect\n * - glow: Themed glow shadow (--glass-glow, follows the tint hue) that intensifies on hover\n * - shimmer: Moving shimmer effect across the component\n * - ripple: Ripple effect that scales outward on hover\n * - lift: Subtle lift effect with shadow increase\n * - scale: Scale up slightly on hover\n */\nexport const hoverEffects = cva(\"transition duration-300\", {\n variants: {\n hover: {\n none: \"\",\n glow: \"glass-glow hover:glass-glow-lg\",\n shimmer:\n \"relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent hover:before:translate-x-full before:transition-transform before:duration-1000\",\n ripple:\n \"relative overflow-hidden after:absolute after:inset-0 after:scale-0 after:rounded-full after:bg-white/30 after:transition-transform after:duration-500 hover:after:scale-150\",\n lift: \"hover:-translate-y-1 hover:shadow-lg\",\n scale: \"hover:scale-105\",\n },\n },\n defaultVariants: {\n hover: \"none\",\n },\n});\n\nexport type HoverEffect = \"none\" | \"glow\" | \"shimmer\" | \"ripple\" | \"lift\" | \"scale\";\n", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "components/ui/field.tsx", + "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Label } from \"./label\";\nimport { Separator } from \"./separator\";\n\n/**\n * Form field layout: a label, the control, its description and its error, laid out vertically,\n * horizontally, or responsively by the width of the surrounding FieldGroup. Plain layout with no\n * surface of its own: the controls inside it (Input, Checkbox, Select, …) carry the material.\n *\n * Pairs with any form library: render FieldError with the library's errors (`errors={[…]}`), or use\n * react-hook-form's Controller around the control.\n */\n\nfunction FieldSet({ className, ...props }: React.ComponentProps<\"fieldset\">) {\n return (\n [data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3\", className)}\n {...props}\n />\n );\n}\n\nfunction FieldLegend({\n className,\n variant = \"legend\",\n ...props\n}: React.ComponentProps<\"legend\"> & {\n variant?: \"legend\" | \"label\";\n}) {\n return (\n \n );\n}\n\nfunction FieldGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nconst fieldVariants = cva(\"group/field flex w-full gap-3 data-[invalid=true]:text-destructive\", {\n variants: {\n orientation: {\n vertical: \"flex-col *:w-full [&>.sr-only]:w-auto\",\n horizontal:\n \"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n responsive:\n \"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n },\n },\n defaultVariants: {\n orientation: \"vertical\",\n },\n});\n\nfunction Field({ className, orientation = \"vertical\", ...props }: React.ComponentProps<\"div\"> & VariantProps) {\n return (\n \n );\n}\n\nfunction FieldContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return

    ;\n}\n\n/* A label wrapping a whole Field becomes a selectable choice card. Its checked state reads the Radix\n control inside it, which marks itself `data-state=\"checked\"`: shadcn's `has-data-checked:` looked for a\n `data-checked` attribute nothing renders, so the card never lit up. */\nfunction FieldLabel({ className, ...props }: React.ComponentProps) {\n return (\n [data-slot=field]]:rounded-xl has-[>[data-slot=field]]:border has-[>[data-slot=field]]:border-[var(--glass-border)] *:data-[slot=field]:p-4\",\n \"has-data-[state=checked]:border-primary/30 has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:border-primary/20 dark:has-data-[state=checked]:bg-primary/10\",\n \"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction FieldTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\n/* `group-data-[orientation=horizontal]/field:` — the Field marks its orientation that way; shadcn's\n `group-has-data-horizontal/field:` asked for a `data-horizontal` attribute that does not exist. */\nfunction FieldDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/* With a label, the rule runs on either side of it instead of behind it. shadcn knocks the line out with\n a `bg-background` patch under the label, which on glass paints an opaque page-coloured block. */\nfunction FieldSeparator({\n children,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n children?: React.ReactNode;\n}) {\n return (\n \n \n {children && (\n <>\n \n {children}\n \n \n \n )}\n
    \n );\n}\n\nfunction FieldError({\n className,\n children,\n errors,\n ...props\n}: React.ComponentProps<\"div\"> & {\n errors?: Array<\n | {\n message?: string;\n }\n | undefined\n >;\n}) {\n const content = React.useMemo(() => {\n if (children) return children;\n if (!errors?.length) return null;\n\n const uniqueErrors = [\n ...new Map(\n errors.map((error) => [\n error?.message,\n error,\n ]),\n ).values(),\n ];\n if (uniqueErrors.length === 1) return uniqueErrors[0]?.message;\n\n return (\n
      \n {uniqueErrors.map((error) => error?.message &&
    • {error.message}
    • )}\n
    \n );\n }, [\n children,\n errors,\n ]);\n\n if (!content) return null;\n\n return (\n
    \n {content}\n
    \n );\n}\n\nexport { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTitle };\n", + "type": "registry:component", + "target": "components/ui/field.tsx" + } + ], + "type": "registry:component" +} \ No newline at end of file diff --git a/public/r/hover-card.json b/public/r/hover-card.json index 2feb5701..7dc2db26 100644 --- a/public/r/hover-card.json +++ b/public/r/hover-card.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -33,7 +33,7 @@ }, { "path": "lib/snap-popper.ts", - "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Keeps text crisp on Radix popper surfaces (dropdown-menu, popover, tooltip, select, hover-card,\n * context-menu).\n *\n * THE DEFECT: Radix positions those surfaces by writing an inline `transform: translate(x, y)` onto a\n * wrapper element. Floating UI does not round x/y, so they land on fractions whenever the trigger does\n * — under browser zoom, fractional DPR, or any flex/grid trigger whose box isn't on a whole pixel. On\n * its own that would be harmless, but sistine's popper content carries `backdrop-filter` (it's glass),\n * which promotes it to its own compositing layer. A promoted layer at a fractional offset has its\n * glyphs rasterized off the device pixel grid and the text renders visibly soft.\n *\n * This is the same defect the dialogs had. There the fix was to stop using a transform to center; a\n * popper can't do that, because its position IS the transform. So instead: round the translate to whole\n * pixels. Sub-pixel placement is imperceptible; blurred text is not.\n *\n * Radix rewrites that transform on scroll, resize and collision flips, so a MutationObserver re-snaps\n * after each write. The observer is re-entrant — our own write retriggers it — so the write is skipped\n * when the value is already integral, which terminates the loop on the first pass.\n */\n/**\n * The whole snap, as a pure string transform: given the wrapper's SPECIFIED inline `transform`, return\n * what it should be rewritten to, or `null` to leave it alone.\n *\n * Returning null is load-bearing in two cases and both are re-entrancy guards, since our own write\n * retriggers the MutationObserver that called us:\n * - the value is already integral, so there is nothing to do and the loop terminates on pass one;\n * - the value isn't a translate we recognise (Radix hasn't positioned yet), so we must not invent one.\n *\n * Split out of the hook so it can be checked without a DOM — see scripts/check-units.mjs.\n */\nexport function snapTransform(specified: string): string | null {\n const parts = /^translate(?:3d)?\\(\\s*(-?[\\d.]+)px\\s*,\\s*(-?[\\d.]+)px/.exec(specified);\n if (!parts) return null;\n const next = `translate(${Math.round(Number(parts[1]))}px, ${Math.round(Number(parts[2]))}px)`;\n return specified === next ? null : next;\n}\n\nexport function useSnappedPopper(): React.RefCallback {\n const cleanupRef = React.useRef<(() => void) | null>(null);\n\n return React.useCallback((node: T | null) => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n if (!node) return;\n\n const wrapper = node.closest(\"[data-radix-popper-content-wrapper]\");\n if (!wrapper) return; // not popper-positioned (e.g. an inline/anchored render) — nothing to snap\n\n const snap = () => {\n // Read and write the SPECIFIED value, never the computed one. Under browser `zoom` the computed\n // transform is in a scaled space, so rounding it and writing the result back as authored px\n // never converges — snapTransform's equality guard would never hold and the observer would\n // re-enter forever.\n const next = snapTransform(wrapper.style.transform);\n if (next !== null) wrapper.style.transform = next;\n };\n\n snap();\n const observer = new MutationObserver(snap);\n observer.observe(wrapper, {\n attributes: true,\n attributeFilter: [\n \"style\",\n ],\n });\n cleanupRef.current = () => observer.disconnect();\n }, []);\n}\n\n/**\n * Merge the forwarded ref with the snapping ref, MEMOIZED — use this, not `composeRefs`, in JSX.\n *\n * A ref callback written inline gets a fresh identity on every render, and React responds by detaching\n * the old one (calling it with `null`) and attaching the new one. For a plain ref that is merely\n * wasteful; here the detach runs `useSnappedPopper`'s cleanup, so the MutationObserver watching the\n * popper wrapper is torn down and rebuilt on every single render of the surface.\n *\n * Same shape as Radix's own `useComposedRefs` — `useCallback` over the composed callback, with the\n * refs themselves as the dependency array, so the identity holds as long as the individual refs do.\n * Kept local rather than importing @radix-ui/react-compose-refs: these components ship through the\n * registry, and a direct dependency would have to be declared on all six of them.\n */\nexport function useComposedRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n /* The spread refs ARE the dependency list — the identity holds while each individual ref does. */\n return React.useCallback(composeRefs(...refs), refs);\n}\n\n/** Merge the forwarded ref with the snapping ref — both need the same node. Prefer the memoized\n * {@link useComposedRefs} anywhere the result is handed to JSX. */\nexport function composeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n return (node: T | null) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.MutableRefObject).current = node;\n }\n };\n}\n", + "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Keeps text crisp on Radix popper surfaces (dropdown-menu, popover, tooltip, select, hover-card,\n * context-menu).\n *\n * THE DEFECT: on a high-density display the text in a popper rendered visibly soft. Radix positions the\n * surface with an inline `transform: translate(x, y)` on a wrapper element, written from Floating UI's\n * `floatingStyles`, and at a device pixel ratio of 1.5 or more Floating UI adds `will-change: transform`\n * to the same style (`@floating-ui/react-dom`, `floatingStyles`). `will-change` promotes the wrapper to\n * its own compositing layer, and WebKit drops subpixel (LCD) antialiasing for text on a composited layer;\n * sistine's popper content is glass, so a blurred backdrop sits right behind that text. Resetting\n * `will-change` to `auto` is the fix. The cost is a repaint on reposition instead of a layer move, for a\n * surface that is open for seconds.\n *\n * The ROUNDING below predates that diagnosis. Its premise was that unrounded offsets put glyphs off the\n * pixel grid, but Floating UI already rounds x/y to the device pixel grid (`roundByDPR`) before writing\n * them. It is kept for browser zoom, where the device grid and the CSS-pixel grid diverge, which has not\n * been verified either way; it is cheap and harmless, and removing it is a separate decision.\n *\n * Radix rewrites the style on scroll, resize and collision flips, so a MutationObserver re-applies both\n * after each write. The observer is re-entrant (our own write retriggers it), so each write is skipped\n * when the value is already what we want, which terminates the loop on the second pass at the latest.\n */\n/**\n * The whole snap, as a pure string transform: given the wrapper's SPECIFIED inline `transform`, return\n * what it should be rewritten to, or `null` to leave it alone.\n *\n * Returning null is load-bearing in two cases and both are re-entrancy guards, since our own write\n * retriggers the MutationObserver that called us:\n * - the value is already integral, so there is nothing to do and the loop terminates on pass one;\n * - the value isn't a translate we recognise (Radix hasn't positioned yet), so we must not invent one.\n *\n * Split out of the hook so it can be checked without a DOM — see tests/snap-popper.test.tsx.\n */\nexport function snapTransform(specified: string): string | null {\n const parts = /^translate(?:3d)?\\(\\s*(-?[\\d.]+)px\\s*,\\s*(-?[\\d.]+)px/.exec(specified);\n if (!parts) return null;\n const next = `translate(${Math.round(Number(parts[1]))}px, ${Math.round(Number(parts[2]))}px)`;\n return specified === next ? null : next;\n}\n\nexport function useSnappedPopper(): React.RefCallback {\n const cleanupRef = React.useRef<(() => void) | null>(null);\n\n return React.useCallback((node: T | null) => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n if (!node) return;\n\n const wrapper = node.closest(\"[data-radix-popper-content-wrapper]\");\n if (!wrapper) return; // not popper-positioned (e.g. an inline/anchored render) — nothing to snap\n\n const snap = () => {\n // Read and write the SPECIFIED value, never the computed one. Under browser `zoom` the computed\n // transform is in a scaled space, so rounding it and writing the result back as authored px\n // never converges — snapTransform's equality guard would never hold and the observer would\n // re-enter forever.\n const next = snapTransform(wrapper.style.transform);\n if (next !== null) wrapper.style.transform = next;\n // Floating UI's will-change (DPR >= 1.5) is what costs the text its antialiasing. Only written when\n // something else is set, so the pass our own write triggers sees `auto` and stops.\n const willChange = wrapper.style.willChange;\n if (willChange !== \"\" && willChange !== \"auto\") wrapper.style.willChange = \"auto\";\n };\n\n snap();\n const observer = new MutationObserver(snap);\n observer.observe(wrapper, {\n attributes: true,\n attributeFilter: [\n \"style\",\n ],\n });\n cleanupRef.current = () => observer.disconnect();\n }, []);\n}\n\n/**\n * Merge the forwarded ref with the snapping ref, MEMOIZED — use this, not `composeRefs`, in JSX.\n *\n * A ref callback written inline gets a fresh identity on every render, and React responds by detaching\n * the old one (calling it with `null`) and attaching the new one. For a plain ref that is merely\n * wasteful; here the detach runs `useSnappedPopper`'s cleanup, so the MutationObserver watching the\n * popper wrapper is torn down and rebuilt on every single render of the surface.\n *\n * Same shape as Radix's own `useComposedRefs` — `useCallback` over the composed callback, with the\n * refs themselves as the dependency array, so the identity holds as long as the individual refs do.\n * Kept local rather than importing @radix-ui/react-compose-refs: these components ship through the\n * registry, and a direct dependency would have to be declared on all six of them.\n */\nexport function useComposedRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n /* The spread refs ARE the dependency list — the identity holds while each individual ref does. */\n return React.useCallback(composeRefs(...refs), refs);\n}\n\n/** Merge the forwarded ref with the snapping ref — both need the same node. Prefer the memoized\n * {@link useComposedRefs} anywhere the result is handed to JSX. */\nexport function composeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n return (node: T | null) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.MutableRefObject).current = node;\n }\n };\n}\n", "type": "registry:lib", "target": "lib/snap-popper.ts" }, diff --git a/public/r/input-group.json b/public/r/input-group.json index ef9f5f73..20383a9a 100644 --- a/public/r/input-group.json +++ b/public/r/input-group.json @@ -23,7 +23,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/input-otp.json b/public/r/input-otp.json index 1a64618e..4c6caf9a 100644 --- a/public/r/input-otp.json +++ b/public/r/input-otp.json @@ -22,7 +22,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/input.json b/public/r/input.json index a3ef3632..35f68087 100644 --- a/public/r/input.json +++ b/public/r/input.json @@ -20,7 +20,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -32,7 +32,7 @@ }, { "path": "components/ui/input.tsx", - "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst inputVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"dark:bg-input/30 border-input bg-transparent shadow-xs\",\n glass: \"\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass at the small blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"sm\",\n};\n\nfunction Input({\n className,\n type,\n variant = \"glass\",\n icon,\n error,\n ...props\n}: React.ComponentProps<\"input\"> &\n VariantProps &\n MaterialAxisProps & {\n /** Leading icon (folded from the glass wrapper). */\n icon?: React.ReactNode;\n /** Destructive border + focus ring (folded from the glass wrapper). */\n error?: boolean;\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n const input = (\n \n );\n\n if (icon) {\n return (\n

    \n
    {icon}
    \n {input}\n
    \n );\n }\n\n return input;\n}\n\nexport { Input };\n", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst inputVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"dark:bg-input/30 border-input bg-transparent shadow-xs\",\n glass: \"\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass at the small blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"sm\",\n};\n\nfunction Input({\n className,\n type,\n variant = \"glass\",\n icon,\n error,\n ...props\n}: React.ComponentProps<\"input\"> &\n VariantProps &\n MaterialAxisProps & {\n /** Leading icon (folded from the glass wrapper). */\n icon?: React.ReactNode;\n /** Destructive border + focus ring (folded from the glass wrapper). */\n error?: boolean;\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n const input = (\n \n );\n\n if (icon) {\n return (\n
    \n
    {icon}
    \n {input}\n
    \n );\n }\n\n return input;\n}\n\nexport { Input };\n", "type": "registry:component", "target": "components/ui/input.tsx" } diff --git a/public/r/item.json b/public/r/item.json new file mode 100644 index 00000000..415a493f --- /dev/null +++ b/public/r/item.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "item", + "title": "Item", + "description": "A versatile row for lists and settings: media, title, description and actions, plain or on bordered or borderless glass.", + "dependencies": [ + "class-variance-authority", + "clsx", + "radix-ui", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/separator", + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "content": "import { cva } from \"class-variance-authority\";\n\n/**\n * Shared hover effect variants for all Sistine components\n *\n * Effects:\n * - none: No hover effect\n * - glow: Themed glow shadow (--glass-glow, follows the tint hue) that intensifies on hover\n * - shimmer: Moving shimmer effect across the component\n * - ripple: Ripple effect that scales outward on hover\n * - lift: Subtle lift effect with shadow increase\n * - scale: Scale up slightly on hover\n */\nexport const hoverEffects = cva(\"transition duration-300\", {\n variants: {\n hover: {\n none: \"\",\n glow: \"glass-glow hover:glass-glow-lg\",\n shimmer:\n \"relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent hover:before:translate-x-full before:transition-transform before:duration-1000\",\n ripple:\n \"relative overflow-hidden after:absolute after:inset-0 after:scale-0 after:rounded-full after:bg-white/30 after:transition-transform after:duration-500 hover:after:scale-150\",\n lift: \"hover:-translate-y-1 hover:shadow-lg\",\n scale: \"hover:scale-105\",\n },\n },\n defaultVariants: {\n hover: \"none\",\n },\n});\n\nexport type HoverEffect = \"none\" | \"glow\" | \"shimmer\" | \"ripple\" | \"lift\" | \"scale\";\n", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "components/ui/item.tsx", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\n\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Separator } from \"./separator\";\n\nfunction ItemGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction ItemSeparator({ className, ...props }: React.ComponentProps) {\n return ;\n}\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. The plain variants keep a\n transparent 1px border so all three share one box size with `outline`, whose edge is glass-border. */\nconst itemVariants = cva(\n \"group/item flex w-full flex-wrap items-center rounded-xl text-sm text-foreground transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-accent\",\n {\n variants: {\n variant: {\n default: \"border border-transparent\",\n outline: \"\",\n muted: \"border border-transparent\",\n },\n size: {\n default: \"gap-3.5 px-4 py-3.5\",\n sm: \"gap-3.5 px-3.5 py-3\",\n xs: \"gap-2.5 px-3 py-2.5 in-data-[slot=dropdown-menu-content]:p-0\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\n/* Each variant's surface role: `default` is a plain row (no surface: items usually sit inside a card or a\n menu that is already glass), `outline` a bordered glass row, `muted` a borderless glass row. */\nconst SURFACE_ROLE: Record = {\n default: null,\n outline: {\n border: true,\n },\n muted: {},\n};\n\nfunction Item({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n ...props\n}: React.ComponentProps<\"div\"> &\n VariantProps &\n MaterialAxisProps & {\n asChild?: boolean;\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(SURFACE_ROLE[variant ?? \"default\"] ?? null, axes);\n const Comp = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n );\n}\n\nconst itemMediaVariants = cva(\n \"flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n icon: \"text-foreground-ui [&_svg:not([class*='size-'])]:size-4\",\n image:\n \"size-10 overflow-hidden rounded-lg group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 group-data-[size=xs]/item:rounded-md [&_img]:size-full [&_img]:object-cover\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n },\n);\n\nfunction ItemMedia({ className, variant = \"default\", ...props }: React.ComponentProps<\"div\"> & VariantProps) {\n return (\n \n );\n}\n\nfunction ItemContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction ItemTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction ItemDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction ItemActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return

    ;\n}\n\nfunction ItemHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return
    ;\n}\n\nfunction ItemFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return
    ;\n}\n\nexport { Item, ItemActions, ItemContent, ItemDescription, ItemFooter, ItemGroup, ItemHeader, ItemMedia, ItemSeparator, ItemTitle };\n", + "type": "registry:component", + "target": "components/ui/item.tsx" + } + ], + "type": "registry:component" +} \ No newline at end of file diff --git a/public/r/kbd.json b/public/r/kbd.json new file mode 100644 index 00000000..9f8e6df2 --- /dev/null +++ b/public/r/kbd.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "kbd", + "title": "Kbd", + "description": "A keyboard key cap for shortcuts and key combinations, drawn in the theme's text colour with the material's hairline edge.", + "dependencies": [ + "class-variance-authority", + "clsx", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "content": "import { cva } from \"class-variance-authority\";\n\n/**\n * Shared hover effect variants for all Sistine components\n *\n * Effects:\n * - none: No hover effect\n * - glow: Themed glow shadow (--glass-glow, follows the tint hue) that intensifies on hover\n * - shimmer: Moving shimmer effect across the component\n * - ripple: Ripple effect that scales outward on hover\n * - lift: Subtle lift effect with shadow increase\n * - scale: Scale up slightly on hover\n */\nexport const hoverEffects = cva(\"transition duration-300\", {\n variants: {\n hover: {\n none: \"\",\n glow: \"glass-glow hover:glass-glow-lg\",\n shimmer:\n \"relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent hover:before:translate-x-full before:transition-transform before:duration-1000\",\n ripple:\n \"relative overflow-hidden after:absolute after:inset-0 after:scale-0 after:rounded-full after:bg-white/30 after:transition-transform after:duration-500 hover:after:scale-150\",\n lift: \"hover:-translate-y-1 hover:shadow-lg\",\n scale: \"hover:scale-105\",\n },\n },\n defaultVariants: {\n hover: \"none\",\n },\n});\n\nexport type HoverEffect = \"none\" | \"glow\" | \"shimmer\" | \"ripple\" | \"lift\" | \"scale\";\n", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "components/ui/kbd.tsx", + "content": "import type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/* A key cap. Deliberately NOT a glass surface: a cap is a few pixels of chrome inside text, and a blur\n that small reads as noise. A wash of the text colour plus the material's hairline edge carries it on\n every material and both modes. shadcn's version inverts its colours inside a tooltip because its\n tooltip is a solid dark chip; Sistine's tooltip is veiled glass in the page's own text colour, so the\n plain treatment already reads there and the inversion would only turn the cap into a hole. */\nfunction Kbd({ className, ...props }: React.ComponentProps<\"kbd\">) {\n return (\n \n );\n}\n\nfunction KbdGroup({ className, ...props }: React.ComponentProps<\"kbd\">) {\n return ;\n}\n\nexport { Kbd, KbdGroup };\n", + "type": "registry:component", + "target": "components/ui/kbd.tsx" + } + ], + "type": "registry:component" +} \ No newline at end of file diff --git a/public/r/label.json b/public/r/label.json index 7807f279..d67f1f34 100644 --- a/public/r/label.json +++ b/public/r/label.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/material.json b/public/r/material.json index 1295bdd1..8731dbf9 100644 --- a/public/r/material.json +++ b/public/r/material.json @@ -8,7 +8,7 @@ "files": [ { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" } diff --git a/public/r/menu-bar.json b/public/r/menu-bar.json index 4587469c..d8128fd9 100644 --- a/public/r/menu-bar.json +++ b/public/r/menu-bar.json @@ -2,7 +2,7 @@ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "menu-bar", "title": "Menu Bar", - "description": "A menu bar component, with the glass material system.", + "description": "A row of glass buttons for app-style top bars. Legacy, and not a menu system: for File / Edit / View menus with keyboard navigation, use `menubar`.", "dependencies": [ "class-variance-authority", "clsx", @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -33,7 +33,7 @@ }, { "path": "components/ui/menu-bar.tsx", - "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { type HoverEffect, hoverEffects } from \"@/lib/hover-effects\";\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst menuBarVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground border shadow-sm\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: borderless adaptive glass. */\nconst ROLE = {};\n\nexport interface MenuBarProps extends React.HTMLAttributes, VariantProps, MaterialAxisProps {\n effect?: HoverEffect;\n}\n\nconst MenuBar = React.forwardRef(({ className, variant = \"glass\", effect, children, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n {children}\n

    \n );\n});\nMenuBar.displayName = \"MenuBar\";\n\nexport interface MenuBarItemProps extends React.ComponentProps {\n active?: boolean;\n}\n\nconst MenuBarItem = React.forwardRef(({ className, active, variant = \"ghost\", ...props }, ref) => {\n return (\n \n );\n});\nMenuBarItem.displayName = \"MenuBarItem\";\n\nexport { MenuBar, MenuBarItem };\n", + "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { type HoverEffect, hoverEffects } from \"@/lib/hover-effects\";\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\n\n/* Legacy: a row of glass buttons, not a menu system. For File / Edit / View menus with keyboard roving\n and sub menus, use `menubar` (components/ui/menubar.tsx). */\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst menuBarVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground border shadow-sm\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: borderless adaptive glass. */\nconst ROLE = {};\n\nexport interface MenuBarProps extends React.HTMLAttributes, VariantProps, MaterialAxisProps {\n effect?: HoverEffect;\n}\n\nconst MenuBar = React.forwardRef(({ className, variant = \"glass\", effect, children, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n {children}\n
    \n );\n});\nMenuBar.displayName = \"MenuBar\";\n\nexport interface MenuBarItemProps extends React.ComponentProps {\n active?: boolean;\n}\n\nconst MenuBarItem = React.forwardRef(({ className, active, variant = \"ghost\", ...props }, ref) => {\n return (\n

    \n));\nTableHeader.displayName = \"TableHeader\";\n\nconst TableBody = React.forwardRef>(({ className, ...props }, ref) => (\n \n));\nTableBody.displayName = \"TableBody\";\n\nconst TableFooter = React.forwardRef>(({ className, ...props }, ref) => (\n tr]:last:border-b-0\", className)} {...props} />\n));\nTableFooter.displayName = \"TableFooter\";\n\nconst TableRow = React.forwardRef>(({ className, ...props }, ref) => (\n \n));\nTableRow.displayName = \"TableRow\";\n\nconst TableHead = React.forwardRef>(({ className, ...props }, ref) => (\n [role=checkbox]]:translate-y-[2px]\",\n className,\n )}\n {...props}\n />\n));\nTableHead.displayName = \"TableHead\";\n\nconst TableCell = React.forwardRef>(({ className, ...props }, ref) => (\n [role=checkbox]]:translate-y-[2px]\", className)}\n {...props}\n />\n));\nTableCell.displayName = \"TableCell\";\n\nconst TableCaption = React.forwardRef>(({ className, ...props }, ref) => (\n
    \n));\nTableCaption.displayName = \"TableCaption\";\n\nexport { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };\n", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst tableVariants = cva(\"w-full caption-bottom text-sm\", {\n variants: {\n variant: {\n default: \"\",\n /* Rounded, but NOT overflow-hidden: `overflow: hidden` makes the a scroll container, and a\n sticky header binds to its nearest one, so it rode along with the rows. The container clips the\n corners instead (see below). */\n glass: \"rounded-lg\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass. */\nconst ROLE = {\n border: true,\n};\n\nconst Table = React.forwardRef<\n HTMLTableElement,\n React.HTMLAttributes &\n VariantProps &\n MaterialAxisProps & {\n /** When true, rows alternate in brightness and the per-row dividers are removed. */\n striped?: boolean;\n /** Classes for the wrapping container, which is the element that scrolls: pass a height and an\n * overflow (`max-h-96 overflow-y-auto`) and a `sticky top-0` TableHeader pins to it. */\n containerClassName?: string;\n }\n>(({ className, containerClassName, variant = \"glass\", striped = false, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n const stripedClass = striped\n ? \"[&_tbody_tr]:border-0 [&_tbody_tr:nth-child(even)]:bg-black/[0.07] dark:[&_tbody_tr:nth-child(even)]:bg-black/[0.22] [&_tbody_tr:nth-child(even):hover]:bg-muted/50\"\n : \"\";\n\n return (\n /* `overflow-clip`, not `overflow-hidden`: it clips the header and row fills to the rounded corners\n without making the container a scroll container, so it never captures a sticky header on its own.\n A consumer's `overflow-y-auto` (containerClassName) makes it the scroller on purpose; the x axis\n then computes to hidden and keeps the corners clipped. */\n
    \n \n
    \n );\n});\nTable.displayName = \"Table\";\n\nconst TableHeader = React.forwardRef>(({ className, ...props }, ref) => (\n
    \n));\nTableHeader.displayName = \"TableHeader\";\n\nconst TableBody = React.forwardRef>(({ className, ...props }, ref) => (\n \n));\nTableBody.displayName = \"TableBody\";\n\nconst TableFooter = React.forwardRef>(({ className, ...props }, ref) => (\n tr]:last:border-b-0\", className)} {...props} />\n));\nTableFooter.displayName = \"TableFooter\";\n\nconst TableRow = React.forwardRef>(({ className, ...props }, ref) => (\n \n));\nTableRow.displayName = \"TableRow\";\n\nconst TableHead = React.forwardRef>(({ className, ...props }, ref) => (\n [role=checkbox]]:translate-y-[2px]\",\n className,\n )}\n {...props}\n />\n));\nTableHead.displayName = \"TableHead\";\n\nconst TableCell = React.forwardRef>(({ className, ...props }, ref) => (\n [role=checkbox]]:translate-y-[2px]\", className)}\n {...props}\n />\n));\nTableCell.displayName = \"TableCell\";\n\nconst TableCaption = React.forwardRef>(({ className, ...props }, ref) => (\n
    \n));\nTableCaption.displayName = \"TableCaption\";\n\nexport { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };\n", "type": "registry:component", "target": "components/ui/table.tsx" } diff --git a/public/r/tabs.json b/public/r/tabs.json index cc4c3aa1..9e64d6bd 100644 --- a/public/r/tabs.json +++ b/public/r/tabs.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/textarea.json b/public/r/textarea.json index cfdf2819..00d02205 100644 --- a/public/r/textarea.json +++ b/public/r/textarea.json @@ -20,7 +20,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -32,7 +32,7 @@ }, { "path": "components/ui/textarea.tsx", - "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst textareaVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"dark:bg-input/30 border-input bg-transparent shadow-xs\",\n glass: \"\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass at the small blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"sm\",\n};\n\nfunction Textarea({\n className,\n variant = \"glass\",\n icon,\n error,\n ...props\n}: React.ComponentProps<\"textarea\"> &\n VariantProps &\n MaterialAxisProps & {\n /** Leading icon (folded from the glass wrapper). */\n icon?: React.ReactNode;\n /** Destructive border + focus ring (folded from the glass wrapper). */\n error?: boolean;\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n const textarea = (\n \n );\n\n if (icon) {\n return (\n

    \n
    {icon}
    \n {textarea}\n
    \n );\n }\n\n return textarea;\n}\n\nexport { Textarea };\n", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst textareaVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"dark:bg-input/30 border-input bg-transparent shadow-xs\",\n glass: \"\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass at the small blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"sm\",\n};\n\nfunction Textarea({\n className,\n variant = \"glass\",\n icon,\n error,\n ...props\n}: React.ComponentProps<\"textarea\"> &\n VariantProps &\n MaterialAxisProps & {\n /** Leading icon (folded from the glass wrapper). */\n icon?: React.ReactNode;\n /** Destructive border + focus ring (folded from the glass wrapper). */\n error?: boolean;\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n const textarea = (\n \n );\n\n if (icon) {\n return (\n
    \n
    {icon}
    \n {textarea}\n
    \n );\n }\n\n return textarea;\n}\n\nexport { Textarea };\n", "type": "registry:component", "target": "components/ui/textarea.tsx" } diff --git a/public/r/theme.json b/public/r/theme.json index 55c63781..3c86ecca 100644 --- a/public/r/theme.json +++ b/public/r/theme.json @@ -10,7 +10,7 @@ "files": [ { "path": "registry/theme/globals.css", - "content": "/*\n Sistine theme — aggregator. The theme is authored as focused partials under app/theme/; Tailwind v4\n bundles these relative imports itself (no postcss-import needed). ORDER MATTERS:\n - package imports come first (CSS requires @import before other rules),\n - engine.css must precede presets.css / frescoes.css (a preset's --glass-tint-h overrides the\n engine default by SOURCE ORDER at equal specificity),\n - inside tokens.css, .dark comes after :root (same-element ties resolve by source order).\n Consumers install a FLATTENED single-file build of this graph (registry/theme/globals.css, generated\n by scripts/build-theme.mjs) — run `pnpm registry:check` after editing any partial and commit\n registry/theme + public/r.\n*/\n@import \"tailwindcss\";\n@import \"tw-animate-css\";\n/* ── Registrations — @custom-variant + @property pins for JS-only tokens ──────\n Loaded first so the dark variant and typed properties exist before anything composes them. */\n\n/* Class-based dark mode, v4-standard form: matches the `.dark` element ITSELF and its descendants, so\n consumers can toggle `.dark` on any wrapper (not just / next-themes), at zero specificity\n (`:where`) so `dark:` overrides by cascade order like every other variant. */\n@custom-variant dark (&:where(.dark, .dark *));\n\n/* Composable, NON-inheriting glow layer: the `glow` effect/prop sets it, and every glass box-shadow\n folds in `var(--glow-layer, …)` so the glow rides INSIDE each style's shadow — surviving the\n per-style `!important` box-shadow overrides instead of being replaced. inherits:false keeps a\n glowing surface from lighting up its glass children. */\n@property --glow-layer {\n syntax: \"*\";\n inherits: false;\n}\n\n/* Gradient-text tokens are consumed ONLY from JS inline styles (components/gradient-text.tsx) — no CSS\n rule references them, so Lightning CSS's optimizer would silently drop them (the same failure mode as\n the harmonic tokens below). Registered here to pin them. DO NOT REMOVE. */\n@property --gradient-text {\n syntax: \"*\";\n inherits: true;\n}\n@property --gradient-text-contrast {\n syntax: \"*\";\n inherits: true;\n}\n/* ── Harmonic color registrations — compacted one-liners (biome's CSS formatter is excluded for this\n file; see biome.json). WHY these exist: the comment below. */\n\n/* ── Harmonic token registration ──────────────────────────────────────────────────────────────\n The harmonic tokens (--harmony-h, --hue-*, --color-*, --mono-*, defined in engine.css) are\n consumed ONLY from JS / inline styles (HarmonicSwatchesDemo, AutoForeground) — never referenced by\n a CSS rule. Tailwind v4's Lightning CSS optimizer constant-folds + dead-code-eliminates any custom\n property no CSS rule uses, which silently dropped this ENTIRE subgraph from the COMPILED stylesheet\n (so every `oklch(L C var(--hue-*))` / `var(--color-*)` resolved to nothing at runtime). Registering\n them via @property marks them as first-class typed API the optimizer must preserve. DO NOT REMOVE. */\n@property --harmony-h { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --color-chroma { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --mono-chroma { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-base { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-complement { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-analogous-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-analogous-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-split-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-split-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-triad-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-triad-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-3 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-3 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --color-complement { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-analogous-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-analogous-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-split-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-split-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-triad-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-triad-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-3 { syntax: \"\"; inherits: true; initial-value: transparent; }\n/* ── Design tokens — @theme mapping, base shadcn colors, mode knobs + shadow twins, base layer ─────\n :root carries light values + the LEAF mode knobs the engine composes; .dark carries the dark twins.\n .dark must come after :root (same-element ties resolve by source order on html.dark). */\n\n@theme inline {\n /* Easing. Tailwind's built-ins are deliberately gentle; these are the stronger variants UI motion\n wants, exposed as `ease-out` / `ease-in-out` / `ease-drawer` utilities.\n out — anything ENTERING or EXITING. Starts fast, so the first frame (the one the user is\n actually watching) already moves.\n in-out — things MOVING on screen that neither enter nor leave: carousels, sliders.\n drawer — a slower settle for panels that slide in from an edge, so a large surface arrives with\n weight instead of snapping.\n There is deliberately no `ease-in` token: it delays the first frame, which is the one moment the\n interface most needs to feel responsive. */\n --ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);\n --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);\n /* Anything with a bare `transition` and no `ease-*` inherits this. Tailwind ships\n cubic-bezier(0.4, 0, 0.2, 1) — symmetric, so it eases IN as well, and the first frame barely\n moves. Same curve as --ease-out, written literally because @theme inline doesn't emit the vars\n it inlines. */\n --default-transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1);\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-foreground-soft: var(--foreground-soft);\n --color-foreground-strong: var(--foreground-strong);\n --color-foreground-ui: var(--foreground-ui);\n --font-sans: var(--font-geist-sans);\n --font-mono: var(--font-geist-mono);\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n --radius: 1.25rem;\n /* Shadow ink — the base black/white that every shadow, pressed-inset, and bevel highlight is built\n from. Mode-invariant (only the ALPHA changes per surface/mode), so apply alpha at the call site:\n oklch(var(--shadow-ink) / ). One knob to retint every shadow at once (e.g. a cooler, less-flat\n ink) instead of hunting down dozens of oklch(0% 0 0) / oklch(100% 0 0) literals. */\n --shadow-ink: 0% 0 0; /* drop + pressed-inset shadow color */\n --shadow-highlight: 100% 0 0; /* inner bevel / top highlight */\n /* Active/pressed shadow SCALE, built from the ink above and mode-aware (deeper in dark; overridden in\n .dark), so components reference these with NO dark: twin. Depth ladder — all inset \"push-in\" presses\n except --active-shadow, which LIFTS the selected tab/toggle. --press-shadow-strong shares the\n standard press's 0.2 primary and stacks a contact layer (was 0.15; raised so the two-layer presses\n build ON the single-layer one instead of undercutting it):\n --press-shadow-sm — subtle press (ghost, outline)\n --press-shadow — standard press (default, gradient, opaque)\n --press-shadow-strong — firm two-layer press (glass, destructive, secondary, frosted)\n --press-shadow-deep — deepest press (crystal)\n --active-shadow — selected-control LIFT (tabs, toggle) */\n --press-shadow-sm: inset 0 1px 2px oklch(var(--shadow-ink) / 0.1);\n --press-shadow: inset 0 2px 4px oklch(var(--shadow-ink) / 0.2);\n --press-shadow-strong: inset 0 2px 4px oklch(var(--shadow-ink) / 0.2), inset 0 1px 2px oklch(var(--shadow-ink) / 0.15);\n --press-shadow-deep: inset 0 2px 4px oklch(var(--shadow-ink) / 0.25), inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --active-shadow: 0 1px 3px oklch(var(--shadow-ink) / 0.1), inset 0 1px 1px oklch(var(--shadow-highlight) / 0.5);\n /* Selected-control FILL — the twin of --active-shadow, which had the lift but no surface, so tabs and\n toggle-group each hardcoded `bg-white/80 dark:bg-white/20`. The colour is composed in engine.css;\n these are its knobs.\n The two modes source their hue by DIFFERENT routes, and that asymmetry is the whole design.\n DARK is structurally theme-driven: the fill is chroma 0 at alpha 0.2, so 100% of the colour is the\n tinted surface showing through — the chip literally IS the theme surface, lifted. Nothing can make\n it drift off-theme.\n LIGHT cannot use that route. Show-through only carries hue if the surface has hue to give, and a\n light glass surface measures around chroma 0.018 (the sheet is itself capped by --glass-tint-c-hi\n at 0.025 near white). So at alpha 0.9 the fill supplies ~94% of the chip's colour and has to carry\n the tint ITSELF.\n Which makes the fill's own lightness the binding constraint, because the near-white ceiling\n collapses hue-dependently — 0.0323 at L 93, 0.061 at L 87, and only 0.0091 by L 98. At the old\n L 93 the cap was 0.031 and the chip delivered an RGB spread of 20: nominally theme-tinted, visibly\n neutral, which is why day did not read as theme-driven while night did. L 87 roughly doubles the\n available chroma and the delivered spread (37), at the cost of sitting 3.6 L BELOW the tab list\n rather than 1.8 above it — the lift is carried by --active-shadow, so the chip still reads\n selected, now as a tinted fill rather than a white one.\n Capped with min() like --border and the chakra body, so selenite (chroma 0) stays exactly neutral\n and no preset can push the fill out of gamut. */\n --active-l: 87%;\n --active-c-max: 0.055; /* ≤ 0.061, the L-87 ceiling floor (lapis h268) */\n --active-a: 0.9;\n /* ── Mode knobs ──────────────────────────────────────────────────────────\n Single-number dials the glass engine's mode-derived block composes (light values here, dark twins in\n .dark). LEAVES ONLY — none composes a tint var (check-theme invariant 1), so they may live on the bare\n blocks and inherit into tint scopes, where the engine re-resolves the derived tokens. */\n --glass-sheet-a: 0.11; /* sheer glass sheet base alpha (per-stop offsets ride on top) */\n --glass-sheet-a1: 0.04; /* the sheet's TOP-stop offset (dark tapers it to 0.03) */\n --glass-frost-boost: 0.1; /* frosted = the sheet lifted by this much */\n --glass-border-a: 0.16; /* glass edge alpha — a white rim needs more presence on light backdrops */\n /* Crystal gloss defaults (the demo's Light/Tint sliders override inline). --glass-gloss-tint is\n mode-shared; --glass-gloss-l is NOT — see its twin below. AutoForeground's crystal tiers mirror\n --glass-gloss-l (97 light / 66 dark) as their snapshot-path fallback — keep all three in step. */\n --glass-diffuse: 12px; /* readability blur FLOOR the glass-diffuse axis raises surfaces to (opaque ignores it) */\n /* Gloss ink lightness (\"Light\" in the demo) — a MODE TWIN, because a gloss is defined relative to\n the surface under it and those surfaces are 68 L apart. The crystal floor computes to L 96.5 in\n light and L 28 in dark, so a single shared value cannot be a highlight in both: at 66 the top stop\n lifts dark by +15.2 L (a specular) and darkens light by −12.2 L (a smudge — the same \"grey\n speculars read as grime\" failure the shadow bevel comment warns about).\n 97 is the light twin: composited it lands +0.2 L over the surface, which is all the headroom that\n exists above L 96.5. The cost is a chroma ceiling, and it is strongly HUE-dependent rather than\n uniform — at L 97 it ranges from 0.0686 for peridot (h128) down to 0.0136 for lapis (h268).\n CORRECTION, measured in WebKit: an over-ceiling oklch() is NOT gamut-mapped down to that ceiling.\n It is CLIPPED per channel, which lands FURTHER from the request, not nearer:\n oklch(97% 0.0686 128) -> 232,255,205 (the ceiling — what the model predicts)\n oklch(97% 0.2975 128) -> 185,255,0 (what actually shipped: pure chartreuse)\n So the Tint multiplier did not \"do real work up to each hue's own ceiling\"; above the ceiling it\n drove the ink to a fully saturated primary. Every preset was affected — turquoise and aquamarine\n both clipped to 0,255,255, i.e. the two were indistinguishable — and moonstone reached 255,222,33,\n the egg-yolk cast that made light mode read yellow. --glass-gloss-c-max below is the fix, sized the\n way --glass-chakra-c-max is: the TIGHTEST ceiling across every preset hue, so no preset clips and\n all of them deliver 100% of what they ask for. */\n --glass-gloss-l: 97;\n /* Gloss chroma cap — the L-97 twin. 0.0136 is lapis (h268), the tightest of the 17 presets; 0.013\n sits just under it. Light gloss is therefore near-achromatic by necessity, not by choice: sRGB\n simply holds no more colour that close to white, and asking for more clips rather than tints. */\n --glass-gloss-c-max: 0.013;\n /* Gloss chroma multiplier over the tint (\"Tint\"). Calibrated for the DEFAULT tint (0.018 × 4.25 =\n 0.077, comfortably in gamut); a preset multiplies up from a much higher base, so the product is\n clamped to 0.3 where --glass-gloss-ink composes it — sRGB holds ~0.32 chroma at its very best hue\n and far less at L=66, so beyond that the browser, not the theme, is picking the color. Jewels\n (0.07 × 4.25 = 0.298) sit under the clamp and are unaffected; only the status tints hit it. */\n --glass-gloss-tint: 4.25;\n --glass-wash-l: 72%; /* tint wash lightness — deeper in dark so the tint reads over dark backdrops */\n --glass-wash-c-mult: 2.5; /* tint wash chroma multiplier (moonstone night quiets it to 2) */\n --glass-accent-l: 0.6; /* accent envelope — lifted in dark so the hue pops */\n --glass-accent-c: 0.15;\n --glass-glow-lc: 0.62 0.2; /* glow L+C pair */\n --glass-glow-a: 0.45;\n --harmonic-l: 0.6; /* --color-* accent-tier lightness */\n --mono-l-1: 0.75; /* mono ramp steps (irregular light↔dark lifts — three dials, not one) */\n --mono-l-2: 0.6;\n --mono-l-3: 0.45;\n --gradient-l: 0.6; /* brand-gradient envelope (matches --glass-accent-l/-c) */\n --gradient-c: 0.15;\n --gradient-contrast-l: 0.32; /* contrast gradient text: dark-on-light here, light-on-dark in .dark */\n --gradient-contrast-c: 0.14;\n --glass-crystal-l: 96; /* crystal body lightness — see --glass-crystal-bg; 100 carries zero chroma */\n --glass-crystal-bg-a: 0.3; /* crystal floor alpha (near-clear in dark) */\n /* glass-solid floor — the \"how solid\" dial (0 = sheer, 1 = fully solid; shared by light/dark) + the\n mode floor lightness; composed in @utility glass-veil so a scoped --glass-solid-a resolves there. */\n --glass-solid-a: 0.65;\n --glass-solid-l: 99%;\n /* Opaque tint BODY lightness (plain number → % in the engine). The header L slider drives this; the\n opaque floor color AND AutoForeground's --foreground-opaque banding both read it, so they can't\n drift. LOWER = deeper tint.\n L AND the c-scale below together absorb the retired opaque SHEET (see materials.css). --glass-bg was a\n near-white gradient at ~0.11 alpha, so it did two things to this floor: lightened it AND desaturated\n it. Compensating with lightness alone reproduced only half of that — it left the floor too saturated,\n and pushing L further toward white also SHRINKS the sRGB chroma ceiling, which blew the opaque body\n past check-gamut's 4.6× budget (lapis hit 5.46×). Moving both knobs reproduces both effects and stays\n inside the budget at 4.47×. Fitted across the 14 presets: worst OKLab dE 0.0021, mean 0.0009 — an\n order of magnitude under a just-noticeable difference, so the surface is unchanged to the eye.\n Moonstone pins its own pair (presets.css); its floor is near-white, where the sheet did far less. */\n /* L88, not 90.9. Near-white is where the sRGB chroma ceiling collapses, so an opaque card at 90.9 had\n two problems at once: it read as washed-toward-white, and it could not hold enough chroma to carry\n its own tint (lapis and sapphire were at 97–99% of a ceiling worth only 0.043). Dropping 2.9 L\n roughly doubles every hue's ceiling while keeping body text at Lc 80.0 — the floor is still firmly\n on the light side, so text polarity never changes. */\n --glass-opaque-l: 88;\n /* 0.85, down from 1.26. The old value only looked large: it was immediately clipped to the shared\n 0.042 cap, so every preset rendered the SAME chroma regardless of its tint. Presets now pin their\n own --glass-opaque-c-max (their hue's real ceiling at L88), and 0.85 is the multiplier that lets\n each one reach it. Net effect vs the old flat 0.042: +7% to +88% depending on hue. */\n --glass-opaque-c-scale: 0.85;\n /* Opaque floor chroma cap — the near-white sibling of --glass-tint-c-hi / --glass-chakra-c-max /\n --glass-gloss-c-max, and the last surface that lacked one. Without a cap the floor multiplied raw\n tint chroma this close to white and clipped for most of the palette.\n FALLBACK ONLY: the 12 jewels each pin their own above (their hue's real ceiling at L88); this\n covers the 9 scopes that don't — the 4 frescoes, the 4 status tints and moonstone. Sizing the\n SHARED cap to the tightest hue is the mistake that came before: one number low enough for amber\n (h75, ceiling 0.036 at L88) left peridot at 18% of its own much larger ceiling. So this is\n deliberately not the tightest — per-hue pins are, and scripts/check-gamut.mjs sweeps every scope\n against its real ceiling rather than trusting this number. */\n --glass-opaque-c-max: 0.055;\n /* ── Solidify floor — the opaque backing UNDER sheer glass ───────────────────\n `glass` paints --glass-solidify (this colour at --glass-opacity) as the bottom background-image\n layer of every SHEER material, so it is ~70% of what page/crystal/chakra text actually sits on.\n It used to be --glass-opaque-bg itself, which conflated two different jobs: the colour an OPAQUE\n CARD shows, and the backing that sets sheer text contrast. Opaque cards explicitly drop this layer\n (materials.css, --srf-solidify: none — it would be C over C), so the two were never actually the\n same surface; they only shared a token.\n Splitting them is what lets the sheer floor rise without repainting opaque cards. It is the right\n lever for text contrast because it is the only one with real WEIGHT in the composite (0.7) that is\n not part of a preset's declared identity: --glass-wash-l is the theme COLOUR (moving it changes\n what lapis IS), and --glass-tint-a is already per-mode and desaturates. Measured across all 17\n scopes, L92 is what puts every preset's body text over the 75 floor with the 1.5 Lc margin, with\n no preset's tint tokens touched. DARK needs none of this (it clears everywhere already), so it\n pins both back to the opaque floor below and nothing changes there. */\n --glass-solidify-l: 92;\n /* The floor's chroma ceiling collapses as it lightens, so the L88 caps above would clip here. Rather\n than 17 new per-preset numbers, derive from the ones each preset already declares: across the\n palette the L92/L88 ceiling ratio runs 0.60–0.90, and 0.65 lands every scope INSIDE its real\n ceiling (worst 0.99×, verified by scripts/check-gamut.mjs). Hues that peak light — peridot,\n turquoise, aventurine, amber, aquamarine — are not bound by it at all and keep 100% of their\n surface chroma; the dark-peaking arc keeps 80–86%. */\n --glass-solidify-c-max: calc(var(--glass-opaque-c-max, 0.055) * 0.65);\n /* ── Chakra — the step-cut material (between opaque and crystal) ─────────────\n Every other material is lit from ONE direction: --glass-shadow carries a bright top bevel and a\n dark bottom shade, so surfaces read as a lit dome. Chakra lights all FOUR edges as mitered facets\n (a conic-gradient with hard stops on the diagonals, composed in engine.css), so it reads as a\n step-cut gem instead.\n The facets are pure LUMINANCE overlays — --shadow-highlight / --shadow-ink at these alphas, chroma\n 0 — not tinted colors. Two reasons, and they are the same reason: (1) the theme hue already comes\n through from the tinted body floor beneath, and a translucent white/black over it shifts lightness\n while preserving hue; (2) a bright facet sits near L96 in light mode, where the sRGB chroma ceiling\n across the 17 presets collapses to 0.0182 — a tinted highlight there would be gamut-mapped by the\n browser at nearly every preset. This is the theme's existing rule for the top bevel (\"a specular,\n not a shadow; grey speculars read as grime\"), applied to all four sides.\n The alphas are irregular mode TWINS, not one knob: the same alpha buys a very different lightness\n step over an L88 body than over an L28 one. They are solved so BOTH modes span 18 L end to end —\n light 94.0 > 90.5 > 88.0 > 83.0 > 76.0, dark 38.0 > 32.0 > 28.0 > 24.0 > 20.0 (top, left, table,\n right, bottom). Light comes from the top-left, matching the 135° axis of --glass-bg. */\n --glass-chakra-l: 88; /* body/table lightness */\n /* Body alpha — the \"between crystal and opaque\" dial, and now genuinely that across the WHOLE\n surface: crystal's floor sits at 0.3, opaque at 1. Nothing is painted over the middle any more\n (the cut moved into box-shadow), so this alone decides how much backdrop shows, and glass-diffuse\n / --glass-opacity modulate it the same way they do on crystal. */\n --glass-chakra-a: 0.62;\n --glass-chakra-c-max: 0.055; /* body chroma cap — ≤ 0.0564, the L-88 ceiling floor (lapis h268) */\n --glass-chakra-step: 3px; /* width of ONE bevel step (the frame is two) */\n --glass-chakra-facet-t-a: 0.5; /* top — highlight, +6.0 L */\n --glass-chakra-facet-l-a: 0.208; /* left — highlight, +2.5 L */\n --glass-chakra-facet-r-a: 0.057; /* right — ink, −5.0 L */\n --glass-chakra-facet-b-a: 0.136; /* bottom — ink, −12.0 L */\n /* ── Glass shadows — mode TWINS (not knobs: the light↔dark alpha ratios AND layer structures are\n irregular). They compose only --shadow-ink/-highlight, never tint vars, so bare placement is legal.\n LIGHT twins carry extra definition layers dark mode gets for free (a light surface on a dark page\n separates itself): a tight CONTACT drop + 1px dark outer HAIRLINE define the edge on same-lightness\n backdrops, and a directional BEVEL (bright top inner edge, ink bottom inner edge) makes pills read\n convex instead of printed. The last layer flips family per mode: dark's white inner GLOW (luminous\n glass) becomes an ink VIGNETTE in light — a white wash on a light surface brightens without\n defining, while a faint vignette reads as glass thickness. (Frosted keeps its white glow in both\n modes: milkiness IS that material.) The top bevel stays WHITE in both — it's a specular, not a\n shadow; grey speculars read as grime. Layer order: ambient drop, contact drop, hairline, top\n bevel, bottom shade, inner glow/vignette. */\n --glass-shadow:\n 0 4px 16px oklch(var(--shadow-ink) / 0.13), 0 1px 3px oklch(var(--shadow-ink) / 0.11), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.4) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.08) inset,\n 0 0 12px oklch(var(--shadow-ink) / 0.05) inset;\n --glass-shadow-lg:\n 0 8px 32px oklch(var(--shadow-ink) / 0.16), 0 2px 6px oklch(var(--shadow-ink) / 0.12), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.45) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.09) inset,\n 0 0 20px oklch(var(--shadow-ink) / 0.06) inset;\n --glass-shadow-sm:\n 0 2px 8px oklch(var(--shadow-ink) / 0.1), 0 1px 2px oklch(var(--shadow-ink) / 0.09), 0 0 0 1px oklch(var(--shadow-ink) / 0.04),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.35) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.07) inset,\n 0 0 8px oklch(var(--shadow-ink) / 0.04) inset;\n --glass-frosted-border: oklch(100% 0 0 / 0.3);\n --glass-frosted-shadow:\n 0 8px 32px oklch(var(--shadow-ink) / 0.17), 0 2px 6px oklch(var(--shadow-ink) / 0.13), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.5) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.09) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.18) inset;\n --glass-crystal-shadow: var(--glass-shadow), 0 1px 1px oklch(var(--shadow-highlight) / 0.4) inset;\n --glass-crystal-shadow-hover: var(--glass-shadow-lg), 0 1px 1px oklch(var(--shadow-highlight) / 0.45) inset;\n /* Base colors - Light mode with dark text */\n --background: transparent;\n --foreground: oklch(\n 0.2 0.035 var(--glass-fg-h)\n ); /* CSS baseline (tinted, ~body target) so first paint has no gray→tint flash; AutoForeground refines to the exact APCA solve after */\n /* Size-tiered foregrounds (ARC Bronze Simple Mode bands) — static fallback; AutoForeground re-bands\n these against the live glass surface at runtime. text-foreground-soft / -strong utilities. */\n --foreground-soft: oklch(\n 0.45 0.077 var(--glass-fg-h)\n ); /* large/heading — baseline matched to AutoForeground's median pick; chroma is the LOWEST any preset hue solves to, so no hue clips before JS refines */\n --foreground-strong: oklch(\n 0.2 0.034 var(--glass-fg-h)\n ); /* fine/small — the ramp's tonal clip put this at L20, not L12; the old value flashed dark-then-tinted on every load */\n --foreground-ui: oklch(0.42 0.071 var(--glass-fg-h)); /* icons — AutoForeground re-bands (+ optional hue) */\n /* Static baselines for the OPAQUE + CRYSTAL tier sets (the [data-material] remaps read these) —\n WITHOUT them, a no-JS consumer (or SSR first paint) would substitute an unset var and INHERIT the\n page foreground onto those floors. Light mode: the opaque floor (L90) and crystal surface share\n the page's direction, so the normal literals apply; AutoForeground refines when mounted. */\n --foreground-opaque: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-opaque: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-opaque: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-opaque: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-opaque: oklch(0.14 0.04 var(--glass-fg-h));\n --foreground-crystal: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-crystal: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-crystal: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-crystal: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-crystal: oklch(0.14 0.04 var(--glass-fg-h));\n /* Chakra tier baselines — text sits on the TABLE (the flat centre, L 88), not on the facets, so in\n light mode the direction matches the page and the normal literals apply. AutoForeground re-bands\n against the composited table when mounted. */\n --foreground-chakra: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-chakra: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-chakra: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-chakra: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-chakra: oklch(0.14 0.04 var(--glass-fg-h));\n --card: transparent;\n --card-foreground: var(--foreground);\n --popover: transparent;\n --popover-foreground: var(--foreground);\n\n /* Primary colors - Light mode */\n --primary-l: 0.42; /* primary ink lightness (dark pill on light pages) — the color itself is composed in the engine, following the theme hue */\n /* Ceiling on the primary ink's chroma. The ×1.2 in the engine is intent — primary should read MORE\n saturated than the base tint — but uncapped it asked past the sRGB edge for five presets in light\n (turquoise, aquamarine, success, warning, aurora) and two in dark (lapis, info), where the browser\n gamut-maps and the delivered colour stops tracking the authored one.\n Measured against --glass-fg-h, not --glass-tint-h: --primary follows the FOREGROUND hue, and the\n Muse/Gloaming frescoes decouple the two (fg 290 / 18), so those hues are in the sweep — aurora only\n shows up as out-of-gamut once you check the fg hue. */\n --primary-c-max: 0.069; /* ≤ 0.0697, the L-42 ceiling floor — h205 (aurora) is now tighter than h190 */\n --primary-foreground: oklch(0.98 0 0); /* White text on the primary ink — Lc 88.1 at the worst hue, turquoise (measured) */\n\n /* Secondary colors - Light mode */\n /* ── Flat-neutral shadcn SURFACES, now theme-tinted ──────────────────────────\n --accent, --secondary, --muted and --sidebar were the last chroma-0 tokens in the theme, which is\n why a toggle's on-state read as near-white in light and near-black in dark no matter which preset\n was active: the fill simply had no hue to give. Same treatment as --border — lightness stays put\n (pure recolor), the colour moves to engine.css so it re-resolves per tint scope, and chroma is\n min(the preset's own, a cap) so a preset that declares less keeps its restraint and selenite stays\n exactly neutral.\n Each cap is measured against the tightest sRGB ceiling across all 17 presets AT THAT LIGHTNESS, and\n they differ by a factor of eight because the ceiling collapses toward white: 0.0714 at L 85, but\n only 0.0091 at L 98. One shared cap would have to be the smallest, tinting nothing. */\n --secondary-l: 0.96;\n --secondary-c-max: 0.018; /* ≤ 0.0182, the L-96 ceiling floor (lapis h268) */\n --secondary-foreground: var(--foreground);\n\n /* Muted colors - Light mode */\n --muted-l: 0.85;\n --muted-c-max: 0.071; /* ≤ 0.0714, the L-85 ceiling floor (lapis h268) */\n --muted-foreground: oklch(\n 0.35 0.06 var(--glass-fg-h)\n ); /* Level 1 — L35, not L14: the widest gap of the set, and the one doing most of the first-paint flash */\n\n /* Accent colors - Light mode. NOTE the knob prefix: --accent-c / --accent-h are already taken as the\n user-facing overrides for --glass-accent (the vivid switch/slider fill), a different role. */\n /* L93, down from 0.96, and the cap raised with it. The hover fill was reading as plain white: at L96 the\n sRGB gamut allows a chroma of only 0.0182, an RGB spread of 19, so \"tinted\" was arithmetically\n invisible. Chroma near the extremes is not a free parameter — it is whatever the gamut leaves — so the\n only way to a visible tint is to step off the extreme. Three points of lightness buys 1.7x the spread\n (19 -> 32) and moves the fill DARKER than the surface, the direction a highlight should go on a light\n page. It cannot go much further: --accent is also painted at 50% (bg-accent/50 on calendar's \"today\",\n button's active state, ToggleGroup hover), and that composite over an L88 card is what binds — at L92\n it drops to 76.0 Lc, under the floor+margin, while L93 holds 76.9. */\n --accent-bg-l: 0.93;\n --accent-bg-c-max: 0.032; /* ≤ 0.0323, the L-93 ceiling floor (lapis h268) */\n /* Stays --foreground: this token is text on a SURFACE-lightness background, and it has two callers with\n that shape — the accent fill here, and the bg-foreground/10 wash the menus use (dropdown, context,\n navigation, command, menubar, select). They only share a text colour while --accent stays near the\n surface, which is precisely why the fill was moved by chroma rather than by weight. */\n --accent-foreground: var(--foreground);\n\n /* Destructive colors - WCAG AA compliant (4.5:1 contrast) */\n --destructive: oklch(0.55 0.22 25); /* Red - 4.5:1 with white */\n --destructive-foreground: oklch(0.98 0 0); /* White text on red */\n\n /* Border / input LIGHTNESS + chroma-CAP knobs — the COLOR is composed in engine.css so the edge\n follows the theme tint like every other surface token (leaves, so they inherit into tint scopes\n where the engine re-resolves).\n L 0.85, not the 0.88 the flat neutral carried: sRGB's chroma ceiling collapses as lightness climbs,\n and at 0.88 the binding ceiling across all 17 presets is 0.0564 — the most colorful in-gamut edge\n possible there still reads as grey (mean RGB spread 51). Dropping 3 points of L raises the ceiling\n to 0.0714 and nearly doubles delivered colorfulness (mean spread 65) while moving the grey\n reference only #d7d7d7 → #cecece, which is below the threshold of a perceived weight change.\n --border-c-max is the CAP, not the value: chroma is min(tint chroma, cap), so a preset that\n declares less than the cap keeps its own restraint — selenite (c 0) stays exactly neutral, which is\n the whole point of that theme — while lapis (c 0.15) clamps instead of being gamut-mapped by the\n browser. A flat multiplier cannot do both: any factor small enough to keep lapis in gamut drags the\n 0.07 jewels below where they are today. */\n --border-l: 0.85;\n --input-l: 0.85;\n --border-c-max: 0.07; /* ≤ 0.0714, the L-0.85 ceiling floor (lapis h268) */\n --ring: oklch(0.5 0.2 250); /* Focus ring */\n\n /* Chart colors - WCAG AA compliant */\n --chart-1: oklch(0.6 0.2 250); /* Blue */\n --chart-2: oklch(0.55 0.15 150); /* Green */\n --chart-3: oklch(0.5 0.2 50); /* Yellow */\n --chart-4: oklch(0.65 0.2 300); /* Purple */\n --chart-5: oklch(0.6 0.2 20); /* Orange */\n\n /* Sidebar colors - Light mode */\n --sidebar-l: 0.98;\n --sidebar-c-max: 0.009; /* ≤ 0.0091, the L-98 ceiling floor — near white holds almost no hue */\n --sidebar-foreground: var(--foreground);\n --sidebar-primary: oklch(0.5 0.2 250); /* Blue */\n --sidebar-primary-foreground: oklch(0.98 0 0); /* White text on primary */\n --sidebar-accent: oklch(0.15 0 0 / 0.1); /* Subtle glass overlay (≈ foreground/10), not a solid block */\n --sidebar-accent-foreground: var(--foreground);\n --sidebar-border: var(--border); /* Same divider role, same value it already carried — follow the role token */\n --sidebar-ring: oklch(0.5 0.2 250); /* Blue ring - 3:1 contrast */\n}\n\n.dark {\n /* Base colors — light text on a dark ground */\n --background: transparent;\n /* Pressed/active shadow SCALE — dark overrides: primaries deepen +0.1, contact layers +0.05, so the\n push-in still reads on dark surfaces (see :root for the light ladder + rationale). */\n --press-shadow-sm: inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --press-shadow: inset 0 2px 4px oklch(var(--shadow-ink) / 0.3);\n --press-shadow-strong: inset 0 2px 4px oklch(var(--shadow-ink) / 0.3), inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --press-shadow-deep: inset 0 2px 4px oklch(var(--shadow-ink) / 0.35), inset 0 1px 2px oklch(var(--shadow-ink) / 0.25);\n --active-shadow:\n 0 2px 6px oklch(var(--shadow-ink) / 0.4), inset 0 1px 2px oklch(var(--shadow-highlight) / 0.15),\n inset 0 0 8px oklch(var(--shadow-highlight) / 0.1);\n /* Selected-control fill — dark twin. Deliberately UNCHANGED in behaviour from the `bg-white/20` it\n replaces: chroma cap 0 makes min() resolve to 0, so this is a pure white lift and the hue arrives\n by show-through from the surface below, which is exactly why dark already read correctly. Light\n has to source its hue directly (see :root); the two modes reach the same result by different\n routes because the near-white gamut only permits one of them. */\n --active-l: 100%;\n --active-c-max: 0;\n --active-a: 0.2;\n /* ── Mode knobs — dark twins (see :root for what each dial does). */\n --glass-sheet-a: 0.05;\n --glass-sheet-a1: 0.03;\n --glass-frost-boost: 0.06;\n --glass-border-a: 0.15;\n --glass-wash-l: 58%;\n --glass-accent-l: 0.68;\n --glass-accent-c: 0.14;\n --glass-glow-lc: 0.7 0.18;\n --glass-glow-a: 0.5;\n --harmonic-l: 0.68;\n --mono-l-1: 0.82;\n --mono-l-2: 0.68;\n --mono-l-3: 0.5;\n --gradient-l: 0.68;\n --gradient-c: 0.14;\n --gradient-contrast-l: 0.9;\n --gradient-contrast-c: 0.09;\n --glass-crystal-l: 96; /* shared with light: the ceiling that forces 96 is hue-driven, not mode-driven */\n --glass-crystal-bg-a: 0.1;\n /* Gloss ink lightness — dark twin. Over an L-28 floor there is 72 L of headroom, so 66 lifts +15.2\n and still holds 59% of the tonal ink's chroma: a specular that is genuinely tinted. Light cannot\n have both (see :root). */\n --glass-gloss-l: 66;\n /* Gloss chroma cap — the L-66 twin. Far more headroom this far from white: 0.1094 is aurora (h205),\n the tightest across all 23 preset scopes — the FRESCOES count here too, which is why this is not\n turquoise's 0.1106. Dark gloss keeps real colour; it was still clipping before (turquoise\n 0,193,186 and carnelian 255,0,0 both had a pinned channel). */\n --glass-gloss-c-max: 0.109;\n /* L lifted from 22% (less extreme light↔dark switch); the opaque chroma scale (0.9) keeps warm hues\n like amber in-gamut instead of hard-clipping to rust. */\n --glass-solid-l: 18%;\n /* The retired sheet's compensation, dark twin (see :root). Much the larger move of the two modes: a\n near-white sheet over a DARK floor lifts it ~12 codes where over the light floor it lifts ~2–5. Same\n two-knob fit, worst OKLab dE 0.0045 across the 14 presets. */\n --glass-opaque-l: 36.4;\n /* 1.05, up from 0.85. The dark floor was never cap-limited — every preset sat at 31–79% of its own\n ceiling, so the MULTIPLIER was the throttle and dark opaque cards lost their tint. 1.05 is the most\n the tightest hue (turquoise) can take without clipping; it lifts every preset +24% at Lc 80.0. */\n --glass-opaque-c-scale: 1.05;\n /* Opaque floor chroma cap, dark twin (see :root). A GUARD, not a throttle — the distinction matters\n and this value used to get it wrong. It was 0.06, the tightest ceiling across the preset scopes,\n which was correct while every jewel shared --glass-tint-c: 0.07 and the floor multiplied it blind.\n Once each preset tuned its own chroma to its own hue, that shared floor-of-the-tightest started\n costing the hues WITH headroom: at 0.06 amethyst lost 30% of its ask, tourmaline 33%, rose 26% —\n against own ceilings of 0.189, 0.152 and 0.142. Measured, every preset's dark ask now sits at\n 0.31–0.79× of its own ceiling, so nothing clips with no cap at all. 0.095 clears the largest ask\n (tourmaline 0.0901) and therefore throttles nobody, while still bounding a future preset that\n pushes chroma far past today's values. check-gamut scores each preset against its own ceiling. */\n /* Guard, not a throttle: the largest dark ask is tourmaline at 0.111, so this binds nobody. */\n --glass-opaque-c-max: 0.12;\n /* Dark keeps the solidify floor IDENTICAL to the opaque floor — the split exists to buy LIGHT-mode\n text contrast, and dark already clears the body floor on every preset with margin. Pinning to the\n opaque values (rather than leaving :root's L92 / ×0.65 to inherit) is what makes this a light-only\n change: a lighter backing under dark glass would wash the whole mode out. */\n --glass-solidify-l: var(--glass-opaque-l);\n --glass-solidify-c-max: var(--glass-opaque-c-max);\n /* ── Chakra dark twins (see :root for what each dial does + how the alphas were solved).\n The facet alphas invert in character: over an L28 body there is 72 L of headroom UP and only 28\n DOWN, so the highlights need far less alpha and the inks far more than their light-mode twins to\n land the same 18 L span. The targets also skew brighter here (+10/−8 rather than +6/−12) — a dark\n gem reads as cut by catching light, where a light one reads by casting shade. */\n --glass-chakra-l: 28;\n --glass-chakra-a: 0.58;\n --glass-chakra-c-max: 0.046; /* ≤ 0.0471, the L-28 ceiling floor (turquoise h190) */\n --glass-chakra-facet-t-a: 0.139; /* top — highlight, +10.0 L */\n --glass-chakra-facet-l-a: 0.056; /* left — highlight, +4.0 L */\n --glass-chakra-facet-r-a: 0.143; /* right — ink, −4.0 L */\n --glass-chakra-facet-b-a: 0.286; /* bottom — ink, −8.0 L */\n /* ── Glass shadows — dark twins (deeper drops, brighter bevels). */\n --glass-shadow:\n 0 4px 16px oklch(var(--shadow-ink) / 0.3), 0 1px 4px oklch(var(--shadow-ink) / 0.15), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.12) inset,\n 0 0 12px oklch(var(--shadow-highlight) / 0.15) inset;\n --glass-shadow-lg:\n 0 8px 32px oklch(var(--shadow-ink) / 0.4), 0 2px 8px oklch(var(--shadow-ink) / 0.2), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.15) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.18) inset;\n --glass-shadow-sm:\n 0 2px 8px oklch(var(--shadow-ink) / 0.25), 0 1px 2px oklch(var(--shadow-ink) / 0.12), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.1) inset,\n 0 0 8px oklch(var(--shadow-highlight) / 0.12) inset;\n --glass-frosted-border: oklch(100% 0 0 / 0.25);\n --glass-frosted-shadow:\n 0 8px 32px oklch(var(--shadow-ink) / 0.5), 0 2px 8px oklch(var(--shadow-ink) / 0.3), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.2) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.22) inset;\n --glass-crystal-shadow: var(--glass-shadow), 0 1px 1px oklch(var(--shadow-highlight) / 0.2) inset;\n --glass-crystal-shadow-hover: var(--glass-shadow-lg), 0 1px 1px oklch(var(--shadow-highlight) / 0.25) inset;\n --foreground: oklch(\n 0.95 0.03 var(--glass-fg-h)\n ); /* CSS baseline (tinted, ~body target) so first paint has no gray→tint flash; AutoForeground refines to the exact APCA solve after */\n /* Size-tiered foregrounds (ARC Bronze Simple Mode bands) — static fallback; AutoForeground re-bands\n these against the live glass surface at runtime. text-foreground-soft / -strong utilities. */\n --foreground-soft: oklch(0.83 0.065 var(--glass-fg-h)); /* large/heading — refined at runtime */\n --foreground-strong: oklch(0.97 0.015 var(--glass-fg-h)); /* fine/small — refined at runtime */\n --foreground-ui: oklch(0.87 0.057 var(--glass-fg-h)); /* icons — AutoForeground re-bands (+ optional hue) */\n /* Static OPAQUE + CRYSTAL tier baselines, dark twins — the standard dark opaque floor (L32) and\n crystal surface keep the page's light-text direction. Presets whose dark floors flip LIGHT (moonstone\n cream) override the -opaque set in their own block; AutoForeground refines when mounted. */\n --foreground-opaque: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-opaque: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-opaque: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-opaque: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-opaque: oklch(0.89 0.04 var(--glass-fg-h));\n --foreground-crystal: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-crystal: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-crystal: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-crystal: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-crystal: oklch(0.89 0.04 var(--glass-fg-h));\n /* Chakra tier baselines, dark twins — the L-28 table keeps the page's light-text direction. */\n --foreground-chakra: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-chakra: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-chakra: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-chakra: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-chakra: oklch(0.89 0.04 var(--glass-fg-h));\n --card: transparent;\n --card-foreground: var(--foreground);\n --popover: transparent;\n --popover-foreground: var(--foreground);\n\n /* Primary colors — dark mode */\n --primary-l: 0.78; /* primary ink lightness in dark mode (light pill) — color composed in the engine */\n --primary-c-max: 0.107; /* ≤ 0.1077, the L-78 ceiling floor (h268 lapis) */\n /* Near-black text on the primary ink — Lc 62.9 at the worst hue, sistine (measured). It was 60.9\n before --primary-c-max: capping the chroma the browser used to gamut-map moved the worst case back\n above the 62 this comment had been claiming. */\n --primary-foreground: oklch(0.15 0 0);\n\n /* Secondary colors — dark mode */\n /* Flat-neutral surfaces — dark twins (see :root for why these are knobs now). Lightness unchanged;\n the caps are the dark ceilings, which are tightest at the DARKEST step, not the lightest. */\n --secondary-l: 0.25;\n --secondary-c-max: 0.041; /* ≤ 0.0417, the L-25 ceiling floor — h205 (aurora) is now tighter than h190 */\n --secondary-foreground: var(--foreground);\n\n /* Muted colors — dark mode */\n --muted-l: 0.18;\n --muted-c-max: 0.03; /* ≤ 0.0307, the L-18 ceiling floor (turquoise h190) */\n --muted-foreground: oklch(0.9 0.031 var(--glass-fg-h)); /* Level 1 — one ramp step toward the chosen color */\n\n /* Accent colors — dark mode */\n /* L34, up from 0.25 — the same fix as light, mirrored. At L25 the ceiling was 0.0417 (spread 29), so the\n fill read as near-black; L34 allows 0.0565 (spread 42) and moves it BRIGHTER than the surface, which is\n the direction a highlight goes on a dark page. Dark has more room than light here (its 50% composite\n sits at 77.5 Lc rather than 76.0), because the fill and the L36.4 card floor are close together. */\n --accent-bg-l: 0.34;\n --accent-bg-c-max: 0.056; /* ≤ 0.0565, the L-34 ceiling floor (aurora h205) */\n --accent-foreground: var(--foreground);\n\n /* Destructive colors - WCAG AA compliant (4.5:1 contrast) */\n --destructive: oklch(0.65 0.22 25); /* Light red - 4.5:1 with dark background */\n --destructive-foreground: oklch(1 0 0); /* White text on red — APCA Lc ~67 (near-black was Lc 42) */\n\n /* Border / input lightness + chroma-cap — dark twins (see :root; color composed in engine.css).\n L is UNCHANGED here: in dark mode the edge sits at 0.30/0.25 on a dark page, so lowering it would\n cost contrast rather than buy color. Only the cap moves, up to the tightest dark ceiling — 0.0422\n at L 0.25 (turquoise h190), which --input hits first, so one cap serves both. */\n --border-l: 0.3;\n --input-l: 0.25;\n --border-c-max: 0.041; /* ≤ 0.0422, the L-0.25 ceiling floor (turquoise h190) */\n --ring: oklch(0.6 0.2 250); /* Focus ring */\n\n /* Chart colors - WCAG AA compliant */\n --chart-1: oklch(0.65 0.2 250); /* Light blue */\n --chart-2: oklch(0.7 0.15 150); /* Light green */\n --chart-3: oklch(0.75 0.2 50); /* Light yellow */\n --chart-4: oklch(0.7 0.2 300); /* Light purple */\n --chart-5: oklch(0.68 0.2 20); /* Light orange */\n\n /* Sidebar colors — dark mode */\n --sidebar-l: 0.18;\n --sidebar-c-max: 0.03; /* ≤ 0.0307, the L-18 ceiling floor (turquoise h190) */\n --sidebar-foreground: var(--foreground);\n --sidebar-primary: oklch(0.56 0.16 250); /* Match --primary; white text clears APCA */\n --sidebar-primary-foreground: oklch(1 0 0); /* White on the sidebar's brand fill */\n --sidebar-accent: oklch(1 0 0 / 0.1); /* Subtle glass overlay (≈ foreground/10), not a solid block */\n --sidebar-accent-foreground: var(--foreground);\n --sidebar-border: var(--border); /* Same divider role, same value it already carried — follow the role token */\n --sidebar-ring: oklch(0.6 0.2 250); /* Light blue ring - 3:1 contrast */\n}\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in oklch, var(--foreground) 22%, transparent) transparent;\n }\n body {\n /* Text uses the band-picked --foreground (set on by AutoForeground on theme/tint change;\n globals carries a static fallback so there's no flash). */\n color: var(--foreground);\n background-color: transparent;\n }\n /* Thin, glass-friendly scrollbars (replaces the chunky native bar) */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: color-mix(in oklch, var(--foreground) 22%, transparent);\n border-radius: 9999px;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: color-mix(in oklch, var(--foreground) 38%, transparent);\n }\n}\n\n/* ── Below the floor ──────────────────────────────────────────────────────────\n package.json's browserslist is the supported floor (Safari 17.5 / Chrome 120 / Firefox 128 / Edge\n 120). It is set by what this theme actually uses, not by preference — the binding constraints are\n text-wrap:balance (Safari 17.5), bare-`&` CSS nesting and unprefixed mask-composite (Chrome 120),\n and @property (Firefox 128). Tailwind v4's own baseline sits just under it.\n\n Every colour in this theme is oklch(), fed through custom properties. On an engine without oklch the\n custom property still PARSES (custom props accept anything) and only fails when substituted — so\n `background-color: var(--color-background)` resolves to an invalid value and computes to `unset`,\n i.e. transparent, rather than to a fallback. Surfaces disappear; text usually survives on the UA\n default. This is not a polyfill and does not try to reproduce the palette — it guarantees a legible\n page instead of transparent panels on stacked backgrounds.\n\n UNVERIFIED IN A REAL ENGINE: no browser in the test matrix lacks oklch, so this branch has only been\n confirmed to compile, not to render. Treat it as a floor, not a tested path. */\n@supports not (color: oklch(0% 0 0)) {\n :root {\n color-scheme: light;\n background-color: #f7f7fb;\n color: #17161c;\n }\n body {\n background-color: #f7f7fb;\n color: #17161c;\n }\n .glass,\n [data-material] {\n background-color: #ffffffe6;\n border: 1px solid #17161c26;\n backdrop-filter: none;\n }\n}\n/* ── Glass engine ─────────────────────────────────────────────────────────────\n TWO grouped blocks, split by what must re-resolve where (the var-composition-resolves-at-declaration\n gotcha — see scripts/check-theme.mjs):\n\n A. TINT ANCHORS (:root, [data-glass-tint], …) — the tint knobs plus everything mode-INVARIANT that\n composes them, declared on :root AND every tint scope so a scoped data-glass-tint re-resolves the\n whole subgraph. Deliberately NOT on `.dark`: re-declaring --glass-tint-h / --glass-fg-h at a bare\n `.dark` wrapper would reset a page tint inherited from above.\n\n B. MODE-DERIVED SURFACES (:root, .dark, [data-glass-tint], …) — every tint-composing token whose\n light/dark difference is a handful of numbers. Each is declared ONCE, composing the mode knobs\n (--glass-sheet-a, --glass-wash-l, … — see :root/.dark) which are LEAVES that inherit into tint\n scopes. `.dark` IS in this selector so a dark wrapper inside a light page re-composes its surfaces\n from the wrapper's own dark knobs (same-element cascade) — \"toggle .dark anywhere\" keeps working.\n\n Foreground tokens stay on bare :root/.dark (AutoForeground owns them). Preset blocks below override\n --glass-tint-h/c/a by source order. Shadow composites are mode-TWINNED on bare :root/.dark (their\n light↔dark ratios are irregular — knobbing them would change values). */\n:root,\n[data-glass-tint],\n[data-sonner-toast][data-type] {\n /* Deliberately restrained glassmorphism — enough to read as glass, never enough to fog what is behind it. */\n /* Glass tint — recolor ALL glass by changing these; presets set them via [data-glass-tint].\n -h = hue, -c = chroma (gradient), -a = wash alpha. The gradient alone is too sheer\n (~6–12% opacity) to show a hue, so -a paints a solid colored floor (--glass-tint-wash)\n that reads over any backdrop. Neutral keeps -a:0, so the default glass is unchanged. */\n --glass-tint-h: 250;\n --glass-tint-c: 0.018;\n --glass-tint-a: 0;\n /* The same chroma, capped for NEAR-WHITE surfaces. The sheet stops, the borders and the crystal\n floor sit at L 95–100%, where sRGB's chroma ceiling collapses AND becomes strongly hue-dependent:\n at L=97% it is ~0.071 for peridot (h128) but ~0.014 for lapis (h268). Feeding those the raw\n --glass-tint-c means each preset lands on whatever fraction of its declared value its hue happens\n to permit — lapis was delivering 9% of its 0.15 where peridot delivered 98% of its 0.07 — and the\n shortfall is then resolved by the browser's gamut mapping rather than by us. That is the same\n class of bug the 3% margin in lib/oklch-utils guards against on the computed path (\"foregrounds\n went grey in Safari while staying tinted in Chrome\"); this is its CSS-side counterpart.\n Capping the BASE (rather than lowering any preset) keeps chroma where it has headroom — a jewel's\n depth lives at mid lightness, in --primary and --glass-tint-wash, which are untouched by this.\n Guarded by scripts/check-gamut.mjs.\n 0.025 was still too generous, because the sheet MULTIPLIES this cap: its stops are\n calc(--glass-tint-c-hi * 0.3…1.3), so the top stop asked 0.0325 against an L-95 ceiling of 0.0229\n at the tightest hue — the cap itself clipped. 0.017 is that ceiling divided by the 1.3 multiplier,\n so the sheet now lands inside the gamut at every preset hue instead of at whatever the browser\n chops it to. Cap-then-multiply is the order that matters here; see check-gamut's SCALE/CAP/BOTH\n shapes, which model each form separately for exactly this reason. */\n --glass-tint-c-hi: min(var(--glass-tint-c), 0.017);\n /* Foreground hue — drives every FOREGROUND-keyed token (body/heading text via AutoForeground,\n --muted-foreground, --glass-accent, --glass-glow, --gradient*), kept SEPARATE from the surface\n hue so tuning text never shifts the glass/fresco surface. Defaults to the tint hue (jewels: text\n == surface); frescoes override it to anchor text off their multi-hue surface (Muse night: surface\n 222, text 310). AutoForeground reads this, falling back to --glass-tint-h for older consumers. */\n --glass-fg-h: var(--glass-tint-h);\n /* ── Harmonic color system ────────────────────────────────────────────────────────────────────\n Color-wheel relationships derived from the CONTENT hue (--glass-fg-h) — the same hue --glass-accent\n and --gradient track — so they rotate with the theme/tint automatically (a generalization of the\n --gradient's +63.53° offset). Hues are mode-invariant ANGLES; CSS Color 4 wraps them past 360°, so use\n anywhere as oklch(L C var(--hue-*)); choosing your own L/C also keeps them in-gamut. The ready --color-*\n tokens sit at the --glass-accent envelope (0.6/0.15; .dark lifts them). NOTE: a fixed-chroma harmonic can\n land at the sRGB edge at some hues and get browser-gamut-mapped — prefer the hue tokens + a conservative\n chroma when that matters. */\n /* Harmonic anchor hue — the wheel origin the harmonies rotate from. Normally the content hue, but the two\n \"hue-less\" themes (selenite + moonstone) anchor at 0° instead — set by the tint switcher (selenite) + moonstone's\n block below — so their accents come out a colorful red-based harmony, not a warp of a near-grey hue. */\n --harmony-h: var(--glass-fg-h);\n --hue-base: var(--harmony-h);\n --hue-complement: calc(var(--harmony-h) + 180);\n --hue-analogous-1: calc(var(--harmony-h) - 30);\n --hue-analogous-2: calc(var(--harmony-h) + 30);\n --hue-split-1: calc(var(--harmony-h) + 150);\n --hue-split-2: calc(var(--harmony-h) + 210);\n --hue-triad-1: calc(var(--harmony-h) + 120);\n --hue-triad-2: calc(var(--harmony-h) + 240);\n --hue-tetrad-1: calc(var(--harmony-h) + 60); /* rectangle: base, +60, +180, +240 */\n --hue-tetrad-2: calc(var(--harmony-h) + 180);\n --hue-tetrad-3: calc(var(--harmony-h) + 240);\n --hue-square-1: calc(var(--harmony-h) + 90); /* square: base, +90, +180, +270 */\n --hue-square-2: calc(var(--harmony-h) + 180);\n --hue-square-3: calc(var(--harmony-h) + 270);\n /* Harmonic chroma — a fixed vivid accent chroma so the palette stays colorful in EVERY theme (selenite +\n moonstone included; they get their color from the hue-0 anchor above, not from chroma). --mono-chroma is\n quieter (0.6×) for the subtle mono ramp. */\n --color-chroma: 0.15;\n --mono-chroma: calc(var(--color-chroma) * 0.6);\n /* (--color-* / --mono-* ready colors live in the mode-derived surfaces block below — their L is the\n mode envelope; the hue angles above are the mode-invariant part.) */\n /* (The solidify floor — the --glass-opacity \"component opacity\" dial — is composed INSIDE each sheer\n glass utility, at the element. Composing it here would bake :root's --glass-opacity into the value\n and silently ignore a per-element glass={{opacity}} — the var-composition gotcha again.) */\n /* Blur values — moderate by design; per-variant sizes (mode-invariant). */\n --blur: 2px;\n --blur-sm: 1px;\n --blur-lg: 8px;\n --blur-xl: 12px;\n --blur-frosted: 25px;\n /* Chakra reads as a THICK stone, so it diffuses more than crystal's near-clear 2px even though its\n body is more solid — the two axes are independent (how much light passes vs how scattered it is). */\n --blur-chakra: 6px;\n /* Crystal edge — identical in both modes, so it lives here with the anchors (the crystal floor +\n shadows are mode-varying: --glass-crystal-bg in the surfaces block below, shadows on :root/.dark). */\n --glass-crystal-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / 0.3);\n /* Crystal gloss — the 3-part light effect (top highlight + diagonal streak + bottom glow) baked into\n --crystal-stack-bg (the crystal material + the [data-glass=\"crystal\"] page style) and layered by\n glass-sheen. Each is a full gradient, so it composes in a background-image list AND in pseudo layers.\n THREE FLAVORS, selected by data-gloss on any ancestor (default, unset = tonal — see the [data-gloss]\n blocks further down):\n white → flat white specular (the classic glass shine)\n tonal → a tonally-close single-hue tint of the theme (--glass-gloss-l / --glass-gloss-tint)\n hue → iridescent: the highlight sweeps hues shifted ± --glass-gloss-hue-span around the tint hue\n White/tonal share the gradient SHAPES below (only --glass-gloss-ink, the L C H triple, differs); hue\n redefines the three gradients to interpolate across hue. Each layer applies its own alpha. */\n /* --glass-gloss-l / --glass-gloss-tint live in tokens.css (66 / 4.25) — do NOT re-declare here:\n engine.css imports AFTER tokens.css, so a duplicate at equal :root specificity silently wins and\n pure-CSS consumers get a stale gloss. */\n --glass-gloss-hue-span: 40; /* hue: degrees the iridescent sweep shifts up & down from the tint hue */\n /* hue: chroma of the iridescent stops, at the flavor's own L 74. Was 0.16, chosen believing an\n over-ceiling request is gamut-MAPPED down to the ceiling (\"holds 81% of that chroma\"). WebKit clips\n per channel instead (see --glass-gloss-c-max in tokens.css), so 0.16 pinned a channel on the\n tightest hues rather than delivering 81% of itself. 0.12 sits just under 0.1226 — aurora (h205) at\n L 74, the tightest across all 23 preset scopes (frescoes included) — so every hue in the sweep now\n renders what it asks for. */\n --glass-gloss-c: 0.12;\n --glass-gloss-hue-dir: 1; /* gloss direction (±1) — flips the TONAL streak diagonal (calc angle) + the HUE sweep order (span × dir); toggled in the crystal demo */\n --glass-gloss-ink: calc(var(--glass-gloss-l, 94) * 1%) min(calc(var(--glass-tint-c) * var(--glass-gloss-tint, 2)), var(--glass-gloss-c-max, 0.3))\n var(--glass-tint-h);\n --glass-gloss-top: linear-gradient(to bottom, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.4), transparent 30%);\n --glass-gloss-streak: linear-gradient(\n calc(135deg * var(--glass-gloss-hue-dir, 1)),\n oklch(var(--glass-gloss-ink, 100% 0 0) / 0.15),\n transparent 50%\n );\n --glass-gloss-glow: radial-gradient(circle at 50% 120%, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.2), transparent 70%);\n --blur-crystal: 2px;\n}\n\n/* B. MODE-DERIVED SURFACES — see the engine header above. Each tint-composing token is declared ONCE,\n parameterized by the mode knobs on bare :root/.dark; `.dark` in this selector keeps dark wrappers\n re-composing with their own knobs. */\n:root,\n.dark,\n[data-glass-tint],\n[data-sonner-toast][data-type] {\n /* Solid colored floor under the sheer gradient (see --glass-tint-a) — mode via --glass-wash-l/-c-mult. */\n --glass-tint-wash: oklch(var(--glass-wash-l) calc(var(--glass-tint-c) * var(--glass-wash-c-mult)) var(--glass-tint-h) / var(--glass-tint-a));\n /* Solid accent that tracks the FOREGROUND hue at a fixed vivid chroma — used by active controls\n (switch \"on\", slider range). The --accent-h/-c knob overrides apply in BOTH modes (the old dark\n twin dropped them — that was a bug). */\n --glass-accent: oklch(var(--glass-accent-l) var(--accent-c, var(--glass-accent-c)) var(--accent-h, var(--glass-fg-h)));\n /* Stained-glass — grayscale collapses the backdrop to pure LUMINANCE (akin to alpha: only lighter/\n darker survives), and the glass's OWN tint stack supplies the color — OKLCH-true to the theme, no\n hue-rotate approximation. --glass-stain-amount (0..1) sets how completely the backdrop's color is\n stripped; --glass-stain-invert (0..1, default 1) flips the surviving luminance so bright backdrop\n patches stop fighting dark tinted glass (set 0 to disable). The glass-stained utility and the switcher's Stained toggle both reference THIS token. */\n --glass-stain: grayscale(var(--glass-stain-amount, 1)) invert(var(--glass-stain-invert, 0));\n /* Colored glow for the `glow` prop — follows the foreground hue; override to recolor the halo. */\n --glass-glow: oklch(var(--glass-glow-lc) var(--glass-fg-h) / var(--glass-glow-a));\n /* Selected-control FILL (active tab, pressed toggle) — the surface twin of --active-shadow. Composed\n here, not on :root, because it reads the tint: a scoped data-glass-tint must re-resolve it, and it\n is why the chip tracks whichever theme the subtree carries. Mode knobs in tokens.css carry the two\n routes to the same look (light tints the fill, dark lets the surface show through a white lift). */\n --active-bg: oklch(var(--active-l) min(var(--glass-tint-c), var(--active-c-max)) var(--glass-tint-h) / var(--active-a));\n /* Solid PRIMARY ink (default Button, link text) — themed at the tint's OWN chroma (×1.2): selenite\n stays grey, moonstone whispers warm, jewels read clearly tinted. Lightness is the --primary-l mode knob\n (0.42 light / 0.78 dark). Chroma is min(tint × 1.2, --primary-c-max): the ×1.2 is intent — primary\n should read more saturated than the base tint — but uncapped it asked past the sRGB edge at five\n presets in light and two in dark, where the browser, not the theme, picked the delivered colour.\n Measured after capping: --primary-foreground holds Lc 88.1 light / 62.9 dark at the worst hue. */\n --primary: oklch(var(--primary-l) min(calc(var(--glass-tint-c) * 1.2), var(--primary-c-max)) var(--glass-fg-h));\n /* Ready accent-tier colors (hue angles from the anchors block; L is the mode envelope --harmonic-l). */\n --color-complement: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-complement));\n --color-analogous-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-analogous-1));\n --color-analogous-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-analogous-2));\n --color-split-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-split-1));\n --color-split-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-split-2));\n --color-triad-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-triad-1));\n --color-triad-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-triad-2));\n /* Monochromatic — the harmony anchor hue at stepped lightness (a subtle, low-chroma ramp). */\n --mono-1: oklch(var(--mono-l-1) var(--mono-chroma) var(--harmony-h));\n --mono-2: oklch(var(--mono-l-2) var(--mono-chroma) var(--harmony-h));\n --mono-3: oklch(var(--mono-l-3) var(--mono-chroma) var(--harmony-h));\n /* Opaque glass — a fully solid, faintly tinted floor (no see-through) for the \"opaque\" style.\n --glass-opaque-l is the body-lightness knob (:root/.dark; the header L slider drives it; the floor\n AND AutoForeground's --foreground-opaque banding both read it, so they can't drift). */\n /* Chroma is CAPPED now (--glass-opaque-c-max, tokens.css). This floor sat at L 90.9 in light (it is\n L88 now, for the same reason) — near-white territory where the sRGB ceiling collapses — yet it was\n the one near-white surface\n multiplying RAW --glass-tint-c (×1.26) with no cap, while the sheet had --glass-tint-c-hi, chakra\n had --glass-chakra-c-max and the gloss had --glass-gloss-c-max. That made it the binding surface\n for 7 of the 12 jewels: it clipped first, and every preset's chroma budget was set by it. Capping\n it is what let the tints below be normalised to their hue's real capacity rather than to this\n floor's. Multiply-THEN-cap, matching --glass-gloss-ink. */\n --glass-opaque-bg: oklch(\n calc(var(--glass-opaque-l) * 1%) min(calc(var(--glass-tint-c) * var(--glass-opaque-c-scale)), var(--glass-opaque-c-max, 0.3)) var(--glass-tint-h)\n );\n /* The same floor at the SOLIDIFY lightness/cap — the backing `glass` paints under every sheer\n material (see --glass-solidify in utilities.css). Split from --glass-opaque-bg because an opaque\n CARD and the backing under sheer glass are different surfaces with different jobs; tokens.css has\n the reasoning. Same scale, so a preset's tint still drives it; only the lightness and cap differ. */\n --glass-solidify-bg: oklch(\n calc(var(--glass-solidify-l, var(--glass-opaque-l)) * 1%)\n min(calc(var(--glass-tint-c) * var(--glass-opaque-c-scale)), var(--glass-solidify-c-max, var(--glass-opaque-c-max, 0.3))) var(--glass-tint-h)\n );\n /* Sheer glass sheet — hue/chroma from the tint vars, chroma scaled per stop so the highlights stay\n glassy and the body carries the color. Stop alphas ride the mode base --glass-sheet-a (+ fixed\n offsets; the top stop's offset is its own knob --glass-sheet-a1 — dark tapers it to 0.03);\n frosted = the same sheet lifted by --glass-frost-boost. */\n --glass-bg: linear-gradient(\n 135deg,\n oklch(100% calc(var(--glass-tint-c-hi) * 0.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-sheet-a1))) 0%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.01)) 25%,\n oklch(97% calc(var(--glass-tint-c-hi) * 1) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.02)) 50%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-sheet-a)) 75%,\n oklch(95% calc(var(--glass-tint-c-hi) * 1.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.01)) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg,\n oklch(100% calc(var(--glass-tint-c-hi) * 0.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.04)) 0%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.01)) 25%,\n oklch(97% calc(var(--glass-tint-c-hi) * 1) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.02)) 50%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost))) 75%,\n oklch(95% calc(var(--glass-tint-c-hi) * 1.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.01)) 100%\n );\n /* Very subtle white border — the lit edge of a cut sheet (alpha is the mode dial) */\n --glass-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-border-a));\n /* Opaque CHROME edge — the shadcn --border/--input role tokens (dividers, ghost/input outlines, any\n bare `border-border`). Previously flat neutral (oklch(0.88 0 0) / oklch(0.3 0 0)), which made them\n the ONLY chrome in the theme that did not move with the tint: a ghost Button kept a grey edge under\n lapis while its `outline` sibling (border-foreground/20) tracked the theme. Lightness moves only in\n LIGHT mode (0.88 → --border-l 0.85) to buy chroma headroom; see the knob comment in tokens.css.\n Chroma is min(tint chroma, --border-c-max) — a CAP, deliberately not --glass-tint-c-hi and not a\n multiplier. --glass-tint-c-hi (0.025) is the NEAR-WHITE cap, sized for the L 95–100% sheet stops\n and crystal floor; borrowing it here spent only 66% of the ceiling on amethyst and 17% on peridot,\n which is why the edge still read grey. A flat multiplier fails the other way: lapis declares\n c 0.15, so any factor small enough to keep it in gamut pushes the 0.07 jewels below where they\n started. min() gives every preset its full declared chroma up to the ceiling floor and clamps only\n the ones that overreach — and leaves c 0 at exactly 0, so selenite stays the neutral theme it is.\n Declaring them HERE (rather than on :root) also puts them under scripts/check-gamut.mjs, which\n scrapes engine.css for both the calc() and min() chroma shapes — the suite went 20 → 24 surface/mode\n pairs when these landed, so a later retune of the caps is measured, not asserted.\n SURFACE hue (--glass-tint-h), not --glass-fg-h: an edge belongs to the surface it bounds, matching\n --glass-border / --glass-crystal-border. In a fresco that splits the two (Muse night: surface 222,\n text 310) the border follows the panel, not the type. */\n --border: oklch(var(--border-l) min(var(--glass-tint-c), var(--border-c-max)) var(--glass-tint-h));\n --input: oklch(var(--input-l) min(var(--glass-tint-c), var(--border-c-max)) var(--glass-tint-h));\n /* The remaining shadcn SURFACE tokens — the last chroma-0 values in the theme. Same shape as --border\n for the same reason: a toggle's on-state, a hover wash and a sidebar panel all take their fill from\n these, so while they were flat neutrals those surfaces read near-white in light and near-black in\n dark in every preset. Each carries its OWN cap because the sRGB ceiling is a function of lightness\n and these sit from L 18 to L 98 — a shared cap would have to be the smallest of them (0.009, at the\n near-white sidebar) and would tint nothing. See the knobs in tokens.css. */\n --accent: oklch(var(--accent-bg-l) min(var(--glass-tint-c), var(--accent-bg-c-max)) var(--glass-tint-h));\n /* The hover wash as an IMAGE layer (glass-hover-accent). Translucent so the material still reads\n through it — a hover that paints a flat fill stops looking like glass. */\n --glass-hover-wash: color-mix(in oklch, var(--accent) 70%, transparent);\n --secondary: oklch(var(--secondary-l) min(var(--glass-tint-c), var(--secondary-c-max)) var(--glass-tint-h));\n --muted: oklch(var(--muted-l) min(var(--glass-tint-c), var(--muted-c-max)) var(--glass-tint-h));\n --sidebar: oklch(var(--sidebar-l) min(var(--glass-tint-c), var(--sidebar-c-max)) var(--glass-tint-h));\n /* Theme-aware brand gradient — follows --glass-fg-h, from the hue 3 ramp-steps out\n (8/side → step 360/17 ≈ 63.53°) to the base hue, at the mode envelope --gradient-l/-c + 0.5 alpha.\n (Axis/direction variants are computed in JS from lib/oklch-utils.ts — see the /components demo.) */\n --gradient: linear-gradient(\n 135deg,\n oklch(var(--gradient-l) var(--gradient-c) calc(var(--glass-fg-h) + 63.53) / 0.5) 0%,\n oklch(var(--gradient-l) var(--gradient-c) var(--glass-fg-h) / 0.5) 100%\n );\n /* Full-opacity twin of --gradient for gradient TEXT (background-clip: text) — no wash alpha so glyphs stay crisp. */\n --gradient-text: linear-gradient(\n 135deg,\n oklch(var(--gradient-l) var(--gradient-c) calc(var(--glass-fg-h) + 63.53)) 0%,\n oklch(var(--gradient-l) var(--gradient-c) var(--glass-fg-h)) 100%\n );\n /* High-contrast gradient text for printing ON a tinted theme surface: DARK tint-hue gradient in light\n mode, LIGHT one in .dark (--gradient-contrast-l/-c), so legibility comes from lightness. */\n --gradient-text-contrast: linear-gradient(\n 135deg,\n oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) calc(var(--glass-fg-h) + 63.53)) 0%,\n oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) var(--glass-fg-h)) 100%\n );\n /* Crystal floor — clear glass; the alpha is the mode dial (--glass-crystal-bg-a). */\n /* Crystal body. The lightness is a KNOB, not 100%, and that is the whole point: at L100 sRGB holds\n ZERO chroma, so this surface asked for 0.015 and delivered exactly none — crystal rendered pure\n white under every preset, in both modes (measured: #fffdff amethyst, #fffef4 moonstone,\n #fbffff lapis; RGB spread 2–11). check-gamut even reported it as \"no attainable chroma at all\"\n and skipped the pair rather than failing it.\n --glass-crystal-l is 96 because that is the lowest lightness at which the TIGHTEST of the 21 hues\n this theme uses (h268, lapis) can still carry the 0.015 this asks for: ceiling 0.0182, 1.22x\n headroom. L96.5 is the exact crossover at 1.06x, too thin given maxChromaFor already sits 3% inside\n the boundary to stop Safari clamping toward grey. The cost is small — at the 0.3 light-mode alpha\n the composite moves ~1.2 L, and 0.4 L in dark. */\n --glass-crystal-bg: oklch(calc(var(--glass-crystal-l) * 1%) calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / var(--glass-crystal-bg-a));\n /* ── Chakra — the step-cut gem surface ────────────────────────────────────────────────────────\n BODY: the stone itself. The one tint-composing token here, so it is the only one with a gamut\n budget — min() against --glass-chakra-c-max (not a multiplier) for the same reason --border uses\n one: lapis declares c 0.15 and would be gamut-mapped, while any multiplier small enough to hold it\n drags the 0.07 jewels below where they should sit. Caps are measured per mode against the tightest\n ceiling across all 17 presets, so every preset delivers 100% of its declared chroma. */\n --glass-chakra-bg: oklch(\n calc(var(--glass-chakra-l) * 1%) min(var(--glass-tint-c), var(--glass-chakra-c-max)) var(--glass-tint-h) /\n var(--glass-chakra-a)\n );\n /* SURFACE STACK: the same gloss triple crystal bakes, so `data-gloss` white / tonal / hue reskin\n chakra exactly as they reskin crystal (those blocks remap --glass-gloss-*, which these read), over\n a fresco slot and the tint wash. Nothing here is inset, so nothing here has a corner of its own —\n the element's border-radius clips the whole stack, which is the point (see the facet bands below). */\n --glass-chakra-stack-bg:\n var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer);\n /* THE CUT — four sides, two steps, as INSET SHADOWS rather than inset background layers.\n A background layer is always a RECTANGLE: background-size shrinks it but gives it no radius of its\n own, so an inset gradient paints square corners over a rounded surface — a card with hard-cornered\n rectangles stacked on it. box-shadow has no such problem; an inset shadow is the border box's own\n rounded rect, offset, so every band follows --radius at whatever value the element carries, for\n free and at any size.\n Each side is a pair. The step-1 band (offset --glass-chakra-step) paints OVER the step-2 band\n (offset twice that), so the visible result is two concentric bands per side: 0→N shows step 1\n composited on step 2, N→2N shows step 2 alone. That is the step cut, and because step 2 is\n literally the same colour at half alpha it cannot drift out of sync with step 1.\n Order matters — box-shadow paints first-listed on top, so every step-1 band precedes its step-2\n twin. Corners: adjacent bands overlap and composite rather than mitering to a hard 45° line, which\n is the trade for following the radius. On a gem that reads as the corner facet catching both\n neighbours, and it is what the existing --glass-shadow bevel already does at top and bottom. */\n --glass-chakra-facets:\n inset 0 var(--glass-chakra-step) 0 oklch(var(--shadow-highlight) / var(--glass-chakra-facet-t-a)),\n inset var(--glass-chakra-step) 0 0 oklch(var(--shadow-highlight) / var(--glass-chakra-facet-l-a)),\n inset calc(var(--glass-chakra-step) * -1) 0 0 oklch(var(--shadow-ink) / var(--glass-chakra-facet-r-a)),\n inset 0 calc(var(--glass-chakra-step) * -1) 0 oklch(var(--shadow-ink) / var(--glass-chakra-facet-b-a)),\n inset 0 calc(var(--glass-chakra-step) * 2) 0 oklch(var(--shadow-highlight) / calc(var(--glass-chakra-facet-t-a) * 0.5)),\n inset calc(var(--glass-chakra-step) * 2) 0 0 oklch(var(--shadow-highlight) / calc(var(--glass-chakra-facet-l-a) * 0.5)),\n inset calc(var(--glass-chakra-step) * -2) 0 0 oklch(var(--shadow-ink) / calc(var(--glass-chakra-facet-r-a) * 0.5)),\n inset 0 calc(var(--glass-chakra-step) * -2) 0 oklch(var(--shadow-ink) / calc(var(--glass-chakra-facet-b-a) * 0.5));\n /* Edge + depth: the standard drop plus the cut. --glass-shadow's own top/bottom bevel is left out —\n the facet bands ARE the bevel, and doubling them would flatten the step read. */\n --glass-chakra-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-border-a));\n --glass-chakra-shadow: var(--glass-shadow), var(--glass-chakra-facets);\n --glass-chakra-shadow-hover: var(--glass-shadow-lg), var(--glass-chakra-facets);\n /* Precomposed background STACKS — the background-image compositions, single-sourced. The surface\n utilities consume them via --srf-* fallbacks (sheer utilities append the element-composed solidify\n layer underneath), and the [data-glass] style remaps below swap them wholesale. Live here so they\n re-resolve per tint scope and per mode wrapper. The crystal stack bakes the gloss into the\n background (the site-wide re-skin can't use ::before/::after — those need position:relative, which\n would break sticky/fixed surfaces). */\n --glass-wash-layer: linear-gradient(var(--glass-tint-wash), var(--glass-tint-wash));\n --glass-stack-bg: var(--glass-bg), var(--glass-wash-layer);\n /* Frosted rides the wash in the IMAGE stack (sheet + wash), leaving the background-color channel\n free for the veil floor. */\n --frosted-stack-bg: var(--glass-frosted-bg), var(--glass-wash-layer);\n --crystal-stack-bg:\n var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer);\n}\n/* ── Glass surface utilities (@utility — tree-shaken per consumer usage) ──────\n The material system: ONE structural `glass` utility + orthogonal axis classes (border, veil, sm/lg,\n gradient, glow, sheen). Materials live in materials.css as [data-material] token sets; page styles\n in styles.css remap the same --srf-* tokens. No recipe utilities, no !important. */\n\n/* The glow itself — a punchy two-layer colored halo, centralized so its strength is tunable in ONE\n place. `glass-glow` is the resting glow; `glass-glow-lg` (used on hover) intensifies it. Both set\n --glow-layer, which every glass box-shadow folds in, so the glow shows under EVERY glass style.\n Full mechanism + tuning + troubleshooting: docs/glow.md */\n@utility glass-glow {\n --glow-layer: 0 0 8px 1px var(--glass-glow), 0 0 18px 3px var(--glass-glow);\n}\n@utility glass-glow-lg {\n --glow-layer: 0 0 12px 2px var(--glass-glow), 0 0 28px 5px var(--glass-glow);\n}\n\n/* ── THE MATERIAL SYSTEM (Sistine 2.0) ────────────────────────────────────────\n ONE structural utility (`glass`) + orthogonal axis classes. The material (a token set) comes from\n [data-material] (materials.css) or the inherited [data-glass] page remap (styles.css); with\n neither, the --srf-* fallbacks render the default glass material. Axis utilities only SET custom\n props (or properties `glass` never touches), so Tailwind's emission order can never flip a result.\n (The legacy recipe utilities below coexist during the migration and die with it.) */\n\n/* Structural glass — background/filter/shadow plumbing only; borderless (add glass-border).\n Channels, outermost first:\n background-image = [--srf-hover state slot] + [--acc-gradient accent slot] + [--srf-bg-image material stack] + [solidify]\n background-color = --veil-floor (element veil) > --srf-bg-color (material floor) > transparent\n backdrop-filter = --srf-filter (material) > blur(--srf-blur > --blur) saturate(180%)\n box-shadow = --srf-shadow (material) > --srf-elev (size axis) > --glass-shadow, + glow\n The leading gradient slot is a no-op layer so `glass-gradient` composes over ANY material (same\n pattern as the fresco slot inside --crystal-stack-bg). Both no-op slots resolve to `none` rather\n than linear-gradient(transparent, transparent): `none` is a valid , holds the layer\n position identically, and paints nothing — where a transparent GRADIENT still goes through the\n gradient rasteriser (and its dither) to deliver zero pixels.\n The solidify floor (--glass-opacity dial) is composed HERE, at the element — never in a token\n context (the var-composition-resolves-at-declaration gotcha; see scripts/check-theme.mjs). The\n --srf-solidify slot lets a material DROP that layer when it is provably a no-op; materials that\n want the normal floor pin the slot back to `initial` (guaranteed-invalid → the var() fallback\n below composes --glass-solidify at the element, exactly as before). */\n@utility glass {\n /* Fallback 0.7, not 0: every glass surface now sits on a 70% solidify floor unless a consumer dials\n it back. Measured before changing it, because AutoForeground does NOT read --glass-opacity — it\n bands text against the sheer floor, so a more solid surface than it modelled could have cost\n contrast. Body text goes 91.5 -> 86.5 Lc in light and 88.0 -> 85.8 in dark, both far above the\n 75 body floor (it still clears at a full 1.0). Set --glass-opacity: 0 for the old sheer default. */\n /* --glass-solidify-BG, not --glass-opaque-bg: the backing under sheer glass is its own surface now\n (engine.css / tokens.css). In dark the two resolve identically, so this is a light-mode change. */\n --glass-solidify-fill: color-mix(in oklch, var(--glass-solidify-bg, var(--glass-opaque-bg)) calc(var(--glass-opacity, 0.7) * 100%), transparent);\n --glass-solidify: linear-gradient(var(--glass-solidify-fill), var(--glass-solidify-fill));\n /* --srf-hover leads the stack because a hover/selection wash has to sit ON the material, not under it.\n `hover:bg-accent` sets background-COLOR, which is the bottom of the box — beneath all 14 image layers\n the glass stack paints, of which the solidify floor alone is 70% opaque. At most ~30% of the accent\n survived to the eye, so the hover read as the material's own colour (white in light mode) no matter\n what --accent was set to. No amount of tuning the token could fix that; it is a layer-order problem.\n Same no-op `none` slot pattern as --acc-gradient below it. */\n background-image:\n var(--srf-hover, none), var(--acc-gradient, none), var(--srf-bg-image, var(--glass-stack-bg)), var(--srf-solidify, var(--glass-solidify));\n background-color: var(--veil-floor, var(--srf-bg-color, transparent));\n /* Author ONLY the standard backdrop-filter (no -webkit- twin: Lightning dedupes the pair keeping\n the PREFIXED one, which Chromium never aliased — the [no-webkit-twin] invariant guards this).\n The max() composes the diffuse FLOOR for adaptive surfaces. */\n backdrop-filter: var(--srf-filter, blur(max(var(--srf-blur, var(--blur)), var(--dif, 0px))) saturate(180%)) var(--srf-stain,);\n box-shadow: var(--srf-shadow, var(--srf-elev, var(--glass-shadow))), var(--glow-layer, 0 0 0 0 transparent);\n}\n\n/* Border axis — the material picks the color; width resolves material pin (--srf-border-w, which\n itself reads the --brd-w weight slot) → weight slot directly (adaptive surfaces) → 1px hairline. */\n@utility glass-border {\n border: var(--srf-border-w, var(--brd-w, 1px)) solid var(--srf-border-color, var(--glass-border));\n}\n/* Border WEIGHTS — token setters for the --brd-w slot (order-safe; same-element composition, like\n glass-sm's --srf-blur). Hairline is the unmarked default (1px; 0.5px under frosted); rim = 2px, frame = 4px. */\n@utility glass-border-rim {\n --brd-w: 2px;\n}\n@utility glass-border-frame {\n --brd-w: 4px;\n}\n\n/* Diffuse axis — a READABILITY BLUR FLOOR (not a blur value): every translucent material computes\n blur(max(, --dif)), so crystal stays crystal until a text-dense surface opts in.\n Token-setter (order-safe); OPAQUE never reads the slot (--srf-filter: none), so the short-circuit\n is inherited — no GPU spend on solid floors. Tune the floor via --glass-diffuse. */\n@utility glass-diffuse {\n --dif: var(--glass-diffuse, 12px);\n}\n\n/* Veil axis — the legibility floor for read-through overlays (menus, tooltips, toasts).\n Composed AT THE ELEMENT so a scoped --glass-solid-a resolves here (check-theme invariant 2b).\n The veil rides the background-COLOR channel, under the material's image stack, and — being\n element-declared — beats any inherited --srf-bg-color remap. */\n/* Stained axis — REAL stained-glass optics: grayscale collapses the backdrop to pure luminance\n (lighter/darker only, akin to alpha), so the glass's OWN tint supplies every drop of color and\n whatever sits behind renders as tonal shades of the theme. One color-matrix op — free next to the\n blur. --glass-stain-amount (0..1) dials how completely the backdrop's color is stripped. */\n@utility glass-stained {\n --srf-stain: var(--glass-stain);\n}\n\n@utility glass-veil {\n --veil-floor: oklch(var(--glass-solid-l) 0 0 / var(--glass-solid-a));\n}\n\n/* THE PAGE BACKDROP LAYER — the fixed, full-viewport ground every sheer material blurs. Put this on the\n `fixed inset-0 -z-10` container each background component renders.\n It exists to force a compositing layer. `backdrop-filter` re-samples everything painted beneath it,\n and this layer sits at z-index -10 under the entire document; unpromoted, the compositor has to\n re-rasterise it against the scroll position every frame, which is where the scroll artifact on the\n sheer materials comes from. `opaque` is the ONE material that sets --srf-filter: none — and the one\n material that never showed the artifact, which is the correlation that points here.\n Measured on the deployed page, this element resolved will-change:auto / transform:none /\n contain:none — nothing was promoting it. Promoting is safe specifically because it has ZERO\n position:fixed descendants and no background-attachment:fixed; a transform/will-change would\n otherwise become their containing block and reparent them. Re-check that before adding children.\n will-change (rather than a translateZ hack) because the hint is honest here: this layer is on screen\n and being sampled for the whole session, so there is nothing to hint OFF later. */\n@utility glass-backdrop-layer {\n will-change: transform;\n}\n\n/* Size axes — blur + elevation tiers. These feed the GLASS material only: frosted/crystal/opaque\n (and page remaps) set --srf-filter/--srf-shadow wholesale, which flattens the tiers — matching\n the old system, where the [data-glass] styles flattened glass-surface-sm/-lg. */\n@utility glass-sm {\n --srf-blur: var(--blur-sm);\n --srf-elev: var(--glass-shadow-sm);\n}\n@utility glass-lg {\n --srf-blur: var(--blur-lg);\n --srf-elev: var(--glass-shadow-lg);\n}\n@utility glass-xl {\n --srf-blur: var(--blur-xl);\n --srf-elev: var(--glass-shadow-lg);\n}\n\n/* ── Gradient ACCENT — a brand wash ON a material, not a material. Fills the no-op --acc-gradient\n slot at the top of every `glass` background, so it composes over glass, frosted, crystal, and\n opaque alike. A token-setter (no property `glass` sets), so it's immune to utility emission order. */\n@utility glass-gradient {\n --acc-gradient: var(--gradient);\n}\n\n/* ── Hover/selection WASH — the interactive twin of glass-gradient. Fills the --srf-hover slot at the\n very top of the background stack, which is the only place a state colour is actually visible on a\n glass surface: `hover:bg-accent` writes background-COLOR, and every one of the material's image\n layers paints over it (solidify alone is 70% opaque), so the accent arrived at ~30% strength blended\n into the material and read as plain white on a light theme.\n The wash is deliberately translucent rather than a flat fill — glass that turns solid on hover stops\n looking like glass. 70% of --accent over the material keeps the wash, gloss and blur reading through\n it while still shifting the surface unmistakably toward the theme colour.\n Pair with `hover:bg-accent` rather than replacing it: the background-color still does the work on the\n NON-glass variants (outline/default paint no material, so nothing covers it there). */\n@utility glass-hover-accent {\n &:hover {\n --srf-hover: linear-gradient(var(--glass-hover-wash), var(--glass-hover-wash));\n }\n}\n\n/* Sheen — the OPT-IN hover shimmer (the gloss slides across on hover). Self-contained (highlight + diagonal\n + glow + the 200%/hover shift), so it works on any glass surface AND, layered on glass-crystal (which\n already has the static gloss), its ::before/::after override crystal's to add the motion. Pseudo-element\n based → position:relative; avoid on sticky/fixed surfaces. */\n@utility glass-sheen {\n position: relative;\n\n &::before,\n &::after {\n content: \"\";\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n }\n\n &::before {\n background: var(--glass-gloss-top), var(--glass-gloss-streak);\n background-size: 200% 200%;\n background-position: 0% 0%;\n opacity: 0.7;\n transition: background-position 0.3s ease;\n }\n\n &::after {\n background: var(--glass-gloss-glow);\n opacity: 0.8;\n }\n\n &:hover::before {\n background-position: 100% 100%;\n }\n}\n\n@keyframes shimmer {\n 0% {\n transform: translateX(-100%);\n }\n 100% {\n transform: translateX(100%);\n }\n}\n\n/* ── Reduced motion ───────────────────────────────────────────────────────────\n CONTRIBUTING says \"Respect prefers-reduced-motion\"; the pattern scenes did and nothing else did.\n This closes that, but NOT with the blunt global `*, ::before, ::after { animation: none !important }`\n reset — that would also flatten the pattern scenes' own carefully scoped rules and every enter/exit\n transition in the library.\n\n The treatment differs by what the animation actually IS, because the preference is about MOTION, not\n about animation in general:\n - shimmer → translation across a surface. Real motion, purely decorative: stopped. The skeleton\n still reads as a skeleton from its base fill.\n - spin → rotation. Real motion, but it is the ONLY signal that a loader is alive, so freezing\n it reads as \"hung\" rather than \"calm\". Slowed hard instead of stopped.\n - pulse → opacity only. Nothing moves, so there is nothing to trigger a vestibular response.\n Left alone deliberately; it also carries meaning (the input-otp caret is a blink).\n Unlayered on purpose: Tailwind's generated utilities sit inside @layer utilities, and an unlayered\n rule outranks any layered one regardless of order, so this needs neither !important nor a specificity\n hack. */\n@media (prefers-reduced-motion: reduce) {\n .animate-\\[shimmer_2s_infinite\\],\n .before\\:animate-\\[shimmer_2s_infinite\\]::before {\n animation: none;\n }\n\n .animate-spin {\n animation-duration: 3s;\n }\n}\n/* ── Materials — [data-material] token sets + gloss flavors ───────────────────\n Four materials, one attribute. Each block declares the FULL --srf-* pin set ON the element, so an\n explicit material always beats an inherited [data-glass] page remap (element-declared custom props\n beat inherited ones). One material per element is structural: attributes can't stack, so there is\n no !important anywhere in the system.\n NOTE these are inherited custom properties — a data-material element also acts as a SCOPED default\n for adaptive `glass` descendants (the mechanism DialogContent's old data-glass hack hand-rolled).\n Pin a child back with its own data-material.\n Import order: after engine.css (the [data-gloss] blocks must beat the engine's gloss defaults by\n source order at equal specificity on ). */\n\n[data-material=\"glass\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--glass-stack-bg);\n --srf-bg-color: transparent;\n /* Composed on the element, so the glass-sm/glass-lg axes still resolve here (same-element\n setter/reader is safe — the composition gotcha only bites pre-composed inherited values). */\n --srf-filter: blur(max(var(--srf-blur, var(--blur)), var(--dif, 0px))) saturate(180%);\n --srf-border-color: var(--glass-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--srf-elev, var(--glass-shadow));\n}\n\n/* Frosted — the tint wash rides the IMAGE stack (--frosted-stack-bg = sheet + wash), leaving the\n background-color channel free for the veil floor. */\n[data-material=\"frosted\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--frosted-stack-bg);\n --srf-bg-color: transparent;\n --srf-filter: blur(max(var(--blur-frosted), var(--dif, 0px))) saturate(200%);\n --srf-border-color: var(--glass-frosted-border);\n --srf-border-w: var(--brd-w, 0.5px);\n --srf-shadow: var(--glass-frosted-shadow);\n}\n\n/* Crystal — gloss BAKED into the background stack (sticky/fixed-safe; no pseudo-elements, no\n position:relative). --crystal-stack-bg carries the --glass-crystal-fresco pass-through. The hover\n sparkle is a plain token swap; box-shadow transitions on the real property. No saturate(),\n matching the old variant. */\n[data-material=\"crystal\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--crystal-stack-bg);\n --srf-bg-color: var(--glass-crystal-bg);\n --srf-filter: blur(max(var(--blur-crystal), var(--dif, 0px)));\n --srf-border-color: var(--glass-crystal-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--glass-crystal-shadow);\n transition: box-shadow 0.3s ease;\n}\n[data-material=\"crystal\"]:hover {\n --srf-shadow: var(--glass-crystal-shadow-hover);\n}\n/* Crystal text tiers — the baked gloss locally LIGHTENS the title zone (worst in dark mode), so text\n inside crystal remaps to the -crystal set AutoForeground bands against the glossed crystal surface.\n VEILED crystal is excluded: the veil floor is exactly what the normal tiers are banded for. */\n[data-material=\"crystal\"]:not(.glass-veil) {\n --foreground: var(--foreground-crystal);\n --foreground-soft: var(--foreground-soft-crystal);\n --foreground-strong: var(--foreground-strong-crystal);\n --foreground-ui: var(--foreground-ui-crystal);\n --muted-foreground: var(--muted-foreground-crystal);\n}\n\n/* Chakra — the step-cut gem: a translucent tinted body with a two-step facet frame on all four edges,\n sitting between crystal (near-clear) and opaque (solid). Like crystal it bakes everything into the\n background stack and the shadow — no pseudo-elements, so it is safe on sticky/fixed surfaces, and it\n carries the same gloss triple, so `data-gloss` white / tonal / hue reskin it too.\n The cut lives entirely in --srf-shadow (see --glass-chakra-facets in engine.css). Nothing here is an\n inset BACKGROUND layer, deliberately: background layers are rectangles with no radius of their own,\n so an inset gradient stacks square corners on a rounded surface. Inset shadows follow --radius.\n Because the stack is plain full-bleed layers, the sheer-glass axes work untouched — glass-diffuse\n raises --dif into the blur below, and the --glass-opacity solidify floor composes under the stack\n exactly as it does for crystal. */\n[data-material=\"chakra\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--glass-chakra-stack-bg);\n --srf-bg-color: var(--glass-chakra-bg);\n --srf-filter: blur(max(var(--blur-chakra), var(--dif, 0px)));\n --srf-border-color: var(--glass-chakra-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--glass-chakra-shadow);\n transition: box-shadow 0.3s ease;\n}\n[data-material=\"chakra\"]:hover {\n --srf-shadow: var(--glass-chakra-shadow-hover);\n}\n/* Chakra text tiers — content sits on the body, whose lightness (--glass-chakra-l) is its own dial and\n can sit well away from the page's. Same reasoning as opaque/crystal; veiled chakra is excluded\n because the veil floor is exactly what the NORMAL tiers are banded for. */\n[data-material=\"chakra\"]:not(.glass-veil) {\n --foreground: var(--foreground-chakra);\n --foreground-soft: var(--foreground-soft-chakra);\n --foreground-strong: var(--foreground-strong-chakra);\n --foreground-ui: var(--foreground-ui-chakra);\n --muted-foreground: var(--muted-foreground-chakra);\n}\n\n/* Opaque — solid tinted floor, nothing to blur through. --glass-opaque-outline stays settable on any\n ancestor: it's composed HERE, on the element, so it resolves where it's read. The floor can be\n LIGHT while the page is dark, so the foreground tiers remap to the -opaque set AutoForeground\n bands against that floor (inherits into the card's content). */\n[data-material=\"opaque\"] {\n /* DROP the solidify layer: it is `--glass-opaque-bg` at --glass-opacity alpha painted directly over a\n background-COLOR of the same --glass-opaque-bg at alpha 1 — i.e. C over C, which composites to C for\n ANY alpha. Measured across tints and both modes it moves every channel by exactly 0. So the layer was\n one full gradient rasterisation (and one dither pass) for provably zero pixels, on the one material\n with no backdrop-filter behind it to mask that noise. Restored below when the surface is VEILED —\n there the background-color is the neutral veil floor, not --glass-opaque-bg, so the layer is real\n (dropping it there would shift up to 31/255 in light and 42/255 in dark). */\n --srf-solidify: none;\n /* NO sheet either — opaque now paints a FLAT background-color and nothing else. --glass-bg is a 5-stop\n gradient whose total range, composited over this floor, is 2–4 sRGB codes across an entire card in\n light and 5–7 in dark: one code per ~100–200px, far too shallow to draw cleanly in 8-bit, so the\n rasteriser dithers rather than bands. On every other material a real backdrop-filter supplies blurred\n page content that masks that noise; opaque sets --srf-filter: none, so there is nothing behind it and\n the dither is the only variation on the card — read as grain, worst over text.\n The sheet's GRADIENT was therefore invisible, but its uniform LIFT was not (+1.7 L light, +4.8 L dark),\n so it is folded into --glass-opaque-l rather than dropped: see tokens.css. AutoForeground reads that\n same token, so the -opaque text tiers re-band against the new floor automatically. */\n --srf-bg-image: none;\n --srf-bg-color: var(--glass-opaque-bg);\n --srf-filter: none;\n --srf-border-color: var(--glass-opaque-outline, var(--glass-border));\n /* Outline WEIGHT: an element rim/frame class (--brd-w) beats the page-level outline weight (the\n switcher's --glass-opaque-outline-w on ), which beats the 1px hairline. */\n --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px));\n --srf-shadow: var(--glass-shadow);\n /* Veiled overlays INSIDE an opaque element compute a fully solid floor — same guarantee the opaque\n PAGE style makes (menus/toasts must stay legible on a solid ground). */\n --glass-solid-a: 1;\n --foreground: var(--foreground-opaque);\n --foreground-soft: var(--foreground-soft-opaque);\n --foreground-strong: var(--foreground-strong-opaque);\n --foreground-ui: var(--foreground-ui-opaque);\n --muted-foreground: var(--muted-foreground-opaque);\n}\n\n/* Veiled opaque restores the solidify floor: glass-veil moves the background-COLOR channel to the\n neutral --veil-floor, so the layer is no longer painting --glass-opaque-bg over itself and carries\n the surface's entire tint. Mirrors crystal's `:not(.glass-veil)` carve-out. */\n[data-material=\"opaque\"].glass-veil {\n --srf-solidify: initial;\n}\n\n/* ── Gloss flavors ────────────────────────────────────────────────────────────\n data-gloss on (or any ancestor) swaps the crystal gloss. Default (unset) = tonal. Token\n remaps only — they compose with [data-material=\"crystal\"] AND the [data-glass=\"crystal\"] page\n style, in both modes. */\n[data-gloss=\"white\"] {\n --glass-gloss-ink: var(--shadow-highlight); /* 100% 0 0 — the classic flat white specular */\n}\n[data-gloss=\"tonal\"] {\n /* Explicit tonal (also the default) — re-asserted so it can override a white/hue set on an ancestor. */\n --glass-gloss-ink: calc(var(--glass-gloss-l, 94) * 1%) min(calc(var(--glass-tint-c) * var(--glass-gloss-tint, 2)), var(--glass-gloss-c-max, 0.3))\n var(--glass-tint-h);\n}\n[data-gloss=\"hue\"] {\n /* Hue OWNS its lightness rather than sharing the tonal twin. The two flavors want opposite things\n from the same number: tonal wants the brightest value the surface allows (97 in light) so it reads\n as a specular, while hue needs chroma headroom — its stops ask --glass-gloss-c 0.16, and the sRGB\n ceiling at L 97 is 0.0136, so the iridescent sweep would deliver 8% of its colour and wash out to\n near-white exactly where it is supposed to be most vivid.\n 74 is mode-invariant on purpose: it holds 81% of that chroma, and it lands on the right side of\n both surfaces anyway (+18.4 L over the dark floor, −9.0 over the light one — for an iridescent\n sweep, being SEEN matters more than which direction it moves). The demo's flavor switcher already\n seeded 74 at runtime for this reason; this makes a no-JS consumer get it too.\n \"Holds 81% of that chroma\" assumed the browser gamut-MAPS an over-ceiling request down to the\n ceiling. Measured in WebKit it CLIPS instead (see --glass-gloss-c-max in tokens.css), so 0.16 did\n not deliver 81% of itself — it drove the tightest hues to a pinned channel. 0.12 sits just under\n 0.1240, the L-74 ceiling for turquoise (h190) and the tightest of the presets, so the sweep now\n renders every hue it asks for. */\n --glass-gloss-l: 74;\n /* Iridescent — each highlight sweeps (tint hue − span) → tint hue → (+ span), interpolated IN OKLCH so the\n arc passes through the base hue. Top leans to the low end, glow to the high end, streak spans both. */\n --glass-gloss-top: linear-gradient(\n to bottom in oklch,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n 0.4\n ),\n transparent 35%\n );\n --glass-gloss-streak: linear-gradient(\n 115deg in oklch,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n 0.32\n )\n 0%,\n oklch(calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) var(--glass-tint-h) / 0.12) 45%,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n 0.32\n )\n 85%,\n transparent\n );\n --glass-gloss-glow: radial-gradient(\n circle at 50% 120%,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n 0.25\n ),\n transparent 70%\n );\n /* Kill the white: the default crystal floor is oklch(100% …) = PURE white (chroma collapses at L100), and\n the crystal shadow carries a white inset bevel. In the iridescent flavor, replace the floor with a\n hue-carrying one at the gloss lightness (so Light deepens it) and drop the bevel — leaving only swept hue. */\n --glass-crystal-bg: oklch(calc(var(--glass-gloss-l, 94) * 1%) calc(var(--glass-tint-c) * 1.2) var(--glass-tint-h) / 0.4);\n --glass-crystal-shadow: var(--glass-shadow);\n --glass-crystal-shadow-hover: var(--glass-shadow-lg);\n}\n/* ── Site-wide style remaps + vendor cascade fixes ──────────────────────────── */\n\n/* Keep react-day-picker from painting over the glass background */\n@layer utilities {\n .glass[class*=\"rdp\"],\n .glass .rdp,\n .glass .rdp-month {\n background-color: transparent !important;\n }\n}\n\n/* ── Site-wide glass style: token remaps ─────────────────────────────────────\n data-glass on (frosted | crystal | opaque) re-skins every ADAPTIVE surface (`glass` with no\n data-material) by remapping the --srf-* tokens it reads. An element-level [data-material] pins its\n own tokens and beats these by cascade physics — reinforced by the :where() wrapper, which drops\n every remap (incl. its cross-product legs) to specificity (0,0,0), so a co-located data-material on\n a toast or tint-scope element always wins. The cross-product legs keep the swapped stacks\n re-resolving inside scoped tints and status toasts (a bare [data-glass] rule would bake 's\n tint into descendants' surfaces). Veiled surfaces (glass-veil) keep their element-composed floor. */\n:where(\n [data-glass=\"frosted\"],\n [data-glass=\"frosted\"] [data-glass-tint],\n [data-glass=\"frosted\"][data-glass-tint],\n [data-glass=\"frosted\"] [data-sonner-toast][data-type]\n) {\n --srf-bg-image: var(--frosted-stack-bg);\n /* NO --srf-bg-color wash here: the wash already rides the IMAGE stack (--frosted-stack-bg), same\n as the frosted material — a color-channel copy double-washed tinted presets and stole the veil\n floor's channel. */\n --srf-filter: blur(max(var(--blur-frosted), var(--dif, 0px))) saturate(200%);\n --srf-border-color: var(--glass-frosted-border);\n --srf-border-w: var(--brd-w, 0.5px);\n --srf-shadow: var(--glass-frosted-shadow);\n}\n:where(\n [data-glass=\"crystal\"],\n [data-glass=\"crystal\"] [data-glass-tint],\n [data-glass=\"crystal\"][data-glass-tint],\n [data-glass=\"crystal\"] [data-sonner-toast][data-type]\n) {\n /* Gloss rides inside --crystal-stack-bg (baked — sticky/fixed-safe). Shimmer stays the opt-in\n glass-sheen. No saturate(), matching the material. */\n --srf-bg-image: var(--crystal-stack-bg);\n --srf-bg-color: var(--glass-crystal-bg);\n --srf-filter: blur(max(var(--blur-crystal), var(--dif, 0px)));\n --srf-border-color: var(--glass-crystal-border);\n --srf-shadow: var(--glass-crystal-shadow);\n}\n/* Crystal page: ADAPTIVE glass (no explicit material, not veiled) gets the -crystal text tiers, same\n as the material remap in materials.css — the baked gloss lightens title zones there too. */\n[data-glass=\"crystal\"] :where(.glass):not([data-material]):not(.glass-veil) {\n --foreground: var(--foreground-crystal);\n --foreground-soft: var(--foreground-soft-crystal);\n --foreground-strong: var(--foreground-strong-crystal);\n --foreground-ui: var(--foreground-ui-crystal);\n --muted-foreground: var(--muted-foreground-crystal);\n}\n/* Chakra page — every adaptive surface becomes a step-cut gem. The cut rides --srf-shadow, so it\n follows each surface's own border-radius with nothing to keep in sync. */\n:where(\n [data-glass=\"chakra\"],\n [data-glass=\"chakra\"] [data-glass-tint],\n [data-glass=\"chakra\"][data-glass-tint],\n [data-glass=\"chakra\"] [data-sonner-toast][data-type]\n) {\n --srf-bg-image: var(--glass-chakra-stack-bg);\n --srf-bg-color: var(--glass-chakra-bg);\n --srf-filter: blur(max(var(--blur-chakra), var(--dif, 0px)));\n --srf-border-color: var(--glass-chakra-border);\n --srf-shadow: var(--glass-chakra-shadow);\n}\n/* Chakra page: adaptive glass (no explicit material, not veiled) takes the -chakra text tiers, same\n as the material remap in materials.css. */\n[data-glass=\"chakra\"] :where(.glass):not([data-material]):not(.glass-veil) {\n --foreground: var(--foreground-chakra);\n --foreground-soft: var(--foreground-soft-chakra);\n --foreground-strong: var(--foreground-strong-chakra);\n --foreground-ui: var(--foreground-ui-chakra);\n --muted-foreground: var(--muted-foreground-chakra);\n}\n/* Opaque — solid tinted panels for adaptive surfaces. --glass-solid-a: 1 makes any veiled overlay\n (glass-veil) compute a FULLY solid floor here too, so menus/toasts stay legible on an opaque page. */\n:where(\n [data-glass=\"opaque\"],\n [data-glass=\"opaque\"] [data-glass-tint],\n [data-glass=\"opaque\"][data-glass-tint],\n [data-glass=\"opaque\"] [data-sonner-toast][data-type]\n) {\n /* Flat floor, no sheet — same reasoning as [data-material=\"opaque\"] in materials.css: with\n --srf-filter: none there is no blurred backdrop to mask the dither of a 2–7 code gradient, and the\n sheet's uniform lift is folded into --glass-opaque-l instead. */\n --srf-bg-image: none;\n --srf-bg-color: var(--glass-opaque-bg);\n --srf-filter: none;\n --srf-shadow: var(--glass-shadow);\n}\n/* --glass-solid-a CANNOT ride in the :where() block above. :where() is specificity ZERO by design (so a\n page style never beats an explicit material), and every other declaration up there survives that\n because nothing else declares them — they are material SLOTS with no competing value. This one is\n different: tokens.css declares --glass-solid-a on :root, which is (0,1,0), so the :where() version\n lost every time and the opaque page never actually got its solid floor. Measured: --glass-solid-a\n resolved to 0.65 on under [data-glass=\"opaque\"], identical to every other page style.\n The visible symptom is any VEILED surface on an opaque page staying translucent while the solid\n panels around it go opaque — menus, toasts and popovers, which is exactly what the block above says\n this is for. A bare attribute selector is (0,1,0), which ties :root and wins on source order because\n globals.css imports styles.css after tokens.css. It is a plain literal, so it simply inherits into\n the tint / toast scopes and needs none of their re-resolution. */\n[data-glass=\"opaque\"] {\n --glass-solid-a: 1;\n}\n/* Opaque page: element-level touches for ADAPTIVE surfaces only (`.glass` with no explicit material —\n an explicit frosted/crystal card keeps its translucency AND its normal foreground). Declared ON the\n element because (a) --glass-opaque-outline is settable on any ancestor and must resolve where it's\n read, and (b) the foreground remap must not leak into explicit materials. */\n[data-glass=\"opaque\"] :where(.glass):not([data-material]):not(.glass-veil) {\n /* Same provable no-op as [data-material=\"opaque\"]: this page remap puts --glass-opaque-bg in the\n background-COLOR channel, so the solidify layer paints that colour over itself. Element-level and\n :not([data-material]) so it cannot leak into an explicit material, :not(.glass-veil) because a\n veiled floor is neutral and the layer is real there. */\n --srf-solidify: none;\n}\n/* The uniform EDGE applies to every adaptive panel, veiled or not — a veiled card under the opaque page\n still reads as a solid panel and needs the same outline. */\n[data-glass=\"opaque\"] :where(.glass):not([data-material]) {\n --srf-border-color: var(--glass-opaque-outline, var(--glass-border));\n --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px));\n /* Paint the border HERE, not just the tokens: borderless roles (Card default) never render\n glass-border, so without this only bordered roles showed the outline — \"some cards get it,\n some don't\". Under the opaque page every solid panel takes the uniform edge; bordered elements\n resolve the same vars, so nothing double-draws. */\n border: var(--srf-border-w) solid var(--srf-border-color);\n}\n/* The FOREGROUND remap is :not(.glass-veil) — the same carve-out crystal and chakra make, and the same\n one the --srf-solidify rule above already makes for this page style. A veiled card does not paint\n --glass-opaque-bg: glass-veil moves the background-COLOR channel to the neutral --veil-floor, so its\n text sits on the veil surface the BASE tokens are banded for, not on the opaque floor. Remapping it\n here handed those cards ink solved for a surface they do not have — visible as a hue jump, since the\n opaque floor is the one surface with nothing composited over it and so sits at the raw --glass-tint-h\n while every sheer surface lands a few degrees off it (measured on the /colors panel: 22.0 vs 14.6).\n Splitting the rule is what keeps the edge treatment while dropping the foreground one. */\n[data-glass=\"opaque\"] :where(.glass):not([data-material]):not(.glass-veil) {\n --foreground: var(--foreground-opaque);\n --foreground-soft: var(--foreground-soft-opaque);\n --foreground-strong: var(--foreground-strong-opaque);\n --foreground-ui: var(--foreground-ui-opaque);\n --muted-foreground: var(--muted-foreground-opaque);\n}\n/* ── Glass tint presets ─────────────────────────────────────────────────────\n Set data-glass-tint on any element (commonly ) to recolor the glass in that subtree.\n Most presets just move the tint hue/chroma; \"sistine\" is a bespoke multi-hue fresco (gilded\n gold → terracotta flesh tones → azure sky), echoing the Sistine Chapel ceiling.\n Wash + all tint-derived tokens re-resolve per-scope via the :root,[data-glass-tint] split above. */\n[data-glass-tint=\"sapphire\"] {\n --glass-tint-h: 255;\n --glass-tint-c: 0.075;\n --glass-tint-a: 0.19;\n --glass-wash-l: 63%;\n --glass-opaque-c-max: 0.056;\n}\n[data-glass-tint=\"aventurine\"] {\n --glass-tint-h: 158;\n --glass-tint-c: 0.076;\n --glass-tint-a: 0.54;\n --glass-wash-l: 88%;\n --glass-opaque-c-max: 0.195;\n}\n[data-glass-tint=\"amethyst\"] {\n --glass-tint-h: 300;\n --glass-tint-c: 0.101;\n --glass-tint-a: 0.15;\n --glass-wash-l: 55%;\n --glass-opaque-c-max: 0.065;\n}\n[data-glass-tint=\"rose\"] {\n --glass-tint-h: 8;\n --glass-tint-c: 0.095;\n --glass-tint-a: 0.17;\n --glass-wash-l: 64%;\n --glass-opaque-c-max: 0.062;\n}\n[data-glass-tint=\"amber\"] {\n --glass-tint-h: 75;\n --glass-tint-c: 0.062;\n --glass-tint-a: 0.38;\n --glass-wash-l: 81%;\n --glass-opaque-c-max: 0.096;\n}\n[data-glass-tint=\"carnelian\"] {\n --glass-tint-h: 38;\n --glass-tint-c: 0.081;\n --glass-tint-a: 0.19;\n --glass-wash-l: 67%;\n --glass-opaque-c-max: 0.062;\n}\n[data-glass-tint=\"peridot\"] {\n --glass-tint-h: 128;\n --glass-tint-c: 0.083;\n --glass-tint-a: 0.54;\n --glass-wash-l: 92%;\n --glass-opaque-c-max: 0.218;\n}\n[data-glass-tint=\"turquoise\"] {\n --glass-tint-h: 190;\n --glass-tint-c: 0.057;\n --glass-tint-a: 0.54;\n --glass-wash-l: 90%;\n --glass-opaque-c-max: 0.144;\n}\n[data-glass-tint=\"aquamarine\"] {\n --glass-tint-h: 215;\n --glass-tint-c: 0.053;\n --glass-tint-a: 0.5;\n --glass-wash-l: 82%;\n --glass-opaque-c-max: 0.091;\n}\n[data-glass-tint=\"tourmaline\"] {\n --glass-tint-h: 342;\n --glass-tint-c: 0.106;\n --glass-tint-a: 0.18;\n --glass-wash-l: 67%;\n --glass-opaque-c-max: 0.079;\n}\n[data-glass-tint=\"lapis\"] {\n /* Lapis lazuli IS the deep saturated ultramarine, and at plain jewel chroma it reads as just another\n blue next to sapphire. It used to buy that depth by declaring c 0.15 — far past the sRGB ceiling —\n and letting the browser clip. That DID deliver a deep blue, but by accident rather than by design:\n clipping chops channels, which drops LIGHTNESS, and the saturation came from landing darker.\n Measured, oklch(58% 0.375 268) renders L48.1 C0.2941 h271 — a colour nobody wrote down.\n The wash lightness below asks for that colour on purpose instead. h268's chroma ceiling PEAKS at\n L46 (0.2979) and collapses to 0.1293 by L74, so the shared 72/58 wash was sitting on the downslope;\n at L46 the same depth is available inside the gamut. oklch(45% 0.291 268) renders L44.9 C0.2909\n h267.9 — the same deep ultramarine, hue exact, nothing pinned.\n So lapis is no longer an exception to anything: it is the clearest case of the rule the whole\n palette now follows — put the wash where the hue can actually hold chroma, then ask for it. */\n --glass-tint-h: 268;\n --glass-tint-c: 0.085;\n --glass-tint-a: 0.13;\n --glass-wash-l: 46%;\n --glass-opaque-c-max: 0.055;\n}\n[data-glass-tint=\"goldstone\"] {\n --glass-tint-h: 22;\n --glass-tint-c: 0.094;\n --glass-tint-a: 0.17;\n --glass-wash-l: 63%;\n --glass-opaque-c-max: 0.06;\n}\n/* ── Per-mode wash ALPHA ──────────────────────────────────────────────────────\n Alpha slides the surface along a straight line from the material floor toward the wash colour, so\n how much a preset can take is set by how far its wash sits from THAT MODE's floor (L36.4 dark /\n L88 light). A wash near the floor barely moves the surface and tolerates a lot; a wash far from it\n crosses the mid-tone band where no text polarity reaches the body floor, and tolerates little.\n Because every wash now sits at its hue's chroma PEAK, dark-peaking hues (lapis L46, amethyst L55)\n sit near the DARK floor and take high alpha there, while light-peaking hues (peridot L92, turquoise\n L90) sit near the LIGHT floor and take high alpha there. The two columns are near mirrors.\n Each value is HALF that mode's measured headroom, so every preset keeps body text >= 75 Lc in both\n modes with room to spare. A single shared alpha cannot serve both ends: at 0.55 everywhere, five\n presets lose dark-mode text and lapis/amethyst lose light-mode text.\n The switcher no longer inlines --glass-tint-a for a named preset (see applyTint), so THESE are the\n values that render — nothing shadows them. */\n.dark[data-glass-tint=\"rose\"] {\n --glass-tint-a: 0.49;\n}\n.dark[data-glass-tint=\"goldstone\"] {\n --glass-tint-a: 0.5;\n}\n.dark[data-glass-tint=\"carnelian\"] {\n --glass-tint-a: 0.45;\n}\n.dark[data-glass-tint=\"amber\"] {\n --glass-tint-a: 0.34;\n}\n.dark[data-glass-tint=\"peridot\"] {\n --glass-tint-a: 0.28;\n}\n.dark[data-glass-tint=\"aventurine\"] {\n --glass-tint-a: 0.29;\n}\n.dark[data-glass-tint=\"turquoise\"] {\n --glass-tint-a: 0.28;\n}\n.dark[data-glass-tint=\"aquamarine\"] {\n --glass-tint-a: 0.31;\n}\n.dark[data-glass-tint=\"sapphire\"] {\n --glass-tint-a: 0.48;\n}\n.dark[data-glass-tint=\"lapis\"] {\n --glass-tint-a: 0.54;\n}\n.dark[data-glass-tint=\"amethyst\"] {\n --glass-tint-a: 0.54;\n}\n.dark[data-glass-tint=\"tourmaline\"] {\n --glass-tint-a: 0.46;\n}\n[data-glass-tint=\"moonstone\"] {\n /* Moonstone — a warm, very-low-chroma off-white glass (reads as cream, not yellow). A normal translucent\n tint: texture-preserving and light-text in both modes, just a desaturated warm hue. Anchors its harmony\n wheel at 0° (like selenite) so accents come out a colorful set, not a warp of the near-neutral hue. */\n --glass-tint-h: 75;\n --glass-tint-c: 0.047;\n --glass-tint-a: 0.18;\n --harmony-h: 0;\n /* Gloss chroma, moonstone's own. The global --glass-gloss-tint is 4.25 — sized for the JEWELS, where a\n 0.07–0.1 tint wants a vivid gem highlight. Moonstone declares 0.047 precisely so it \"reads as cream,\n not yellow\" (above), and 4.25× asks for 0.1998.\n --glass-gloss-c-max (tokens.css) is what stops that becoming a clipped primary, and in LIGHT it does\n the whole job: 0.013 caps both 0.1998 and 0.047 to the same value, so this override changes nothing\n there. It earns its place in DARK, where the cap is a roomy 0.11 — enough that the multiplier would\n still put moonstone's highlight at 2.3× its declared chroma, i.e. amber on a stone that is supposed\n to be pale. 1× holds the specular at the tint's own chroma and lets its LIGHTNESS do the work. */\n --glass-gloss-tint: 1;\n /* Moonstone daytime body lightness. Fitted for the retired opaque sheet (tokens.css) on moonstone's OWN\n floor rather than the global pair: this cream is already near-white, where a near-white sheet moved it\n far less than it moved the standard L90 floor. dE 0.0002 — indistinguishable from the old surface. */\n --glass-opaque-l: 94.5;\n /* NO pinned --glass-opaque-bg: the floor must stay DERIVED from --glass-opaque-l (engine), because the\n lightness slider / stored per-mode L land as an INLINE --glass-opaque-l that shadows this block — a\n literal pin would freeze the paint while AutoForeground keeps banding text against the token (light\n text on cream). Was c-scale 1 — the value that made the derived color exactly the original cream when\n the sheet still painted over it; 0.92 is that same cream now that the sheet is gone (it desaturated as\n well as lightened, so both knobs carry part of it). */\n --glass-opaque-c-scale: 0.92;\n}\n.dark[data-glass-tint=\"moonstone\"] {\n /* Moonstone night. The wash sits a few points above the standard 58% (warmth without glare; h75\n carries more real luminance per OKLCH point than the cool tints, so pale-warm washes read brighter\n than their numbers). AutoForeground reads BOTH wash knobs live (+ the [moonstone-sync] mirrored\n fallback), so page text and per-card dark-cream text re-band automatically.\n\n The FLOOR is what you actually see: at the default opacity every surface — cards, inputs, the nav\n pill — paints --glass-opaque-bg, and the wash only tints what shows through. It used to derive\n oklch(0.8 0.047 75), which is a sand/tobacco tan, so moonstone night read as brown rather than as\n the pale silvery stone it is named for. Lightness alone does not fix that: h75 is warm, so every\n lightness along it lands somewhere in the tan family. Cutting the CHROMA is what moves it off that\n family. Light mode is untouched (it keeps c-scale 1 at L94, where the same hue reads as cream).\n\n L52, down from a cream L84.9 — moonstone night is a DARK stone now, and that is forced rather than\n preferred. This floor feeds two surfaces at once: opaque cards paint it directly, and it composites\n at --glass-opacity over the dark veil to become the floor sheer glass sits on. Both have to clear\n the body floor, and for h75 nothing in L55–L84 reaches it in EITHER text polarity — that band is the\n mid-tone dead zone. A cream L84.9 put the opaque card just above it (Lc 77.1) but dragged the sheer\n surface to L65, dead centre, where body text topped out at 65.4 Lc and the preset had to be pinned\n as a known exception. Going lighter cannot fix it: the veil is 30% of the composite, which caps the\n solidify floor at L75.6 no matter how light this value gets, so the sheer surface can never climb\n out the top. Only the bottom exit is reachable. L52 is the PALEST value where both surfaces clear:\n sheer body text lands 80.0 Lc (was 65.4) and the opaque card 82.9 (was 77.1), and it also restores\n --foreground-strong, which could not reach its 90 target on the cream floor either. Still stone —\n oklch(0.52 0.021 75) is #70675c, a warm grey — and still well clear of the jewels' L36.4. */\n --glass-wash-l: 64%;\n --glass-wash-c-mult: 2;\n /* Fitted for the retired opaque sheet, like the daytime floor above and likewise not the global dark\n pair: moonstone night is a LIGHT floor, so the near-white sheet barely moved it. dE 0.0002. */\n --glass-opaque-l: 52;\n --glass-opaque-c-scale: 0.44;\n /* Static -opaque tier baselines. These flipped polarity with the floor: on the old cream L84.9 they were\n dark warm literals, and on an L52 stone they have to be light or SSR's first paint is dark-on-dark.\n Values are what AutoForeground itself solves for this surface, so the mounted refine is a no-op. */\n --foreground-opaque: oklch(1 0 75);\n --foreground-soft-opaque: oklch(0.9 0.05 75);\n --foreground-strong-opaque: oklch(1 0 75);\n --foreground-ui-opaque: oklch(0.91 0.05 75);\n --muted-foreground-opaque: oklch(0.97 0.02 75);\n /* Chakra needs the SAME treatment opaque gets above, and did not have it: chakra was added after this\n block was written, so it kept the global dark body (L28) and rendered oklch(0.28 0.046 75) = #36250c,\n a dark brown — the one surface where moonstone still read as tobacco rather than stone. Matching\n opaque's pair — now L51, chroma x0.45 → 0.021, landing on #6d6559, the same stone as the opaque\n floor one point above it.\n The hue stays 75 deliberately. Cutting CHROMA is what moves this off the tan family; lightness alone\n does not, and a cool hue would contradict the warm cream light mode keeps at L94. */\n --glass-chakra-l: 51;\n --glass-chakra-c-max: 0.021;\n /* Same reasoning as the -opaque baselines above, and flipped with them: an L51 body needs LIGHT text. */\n --foreground-chakra: oklch(1 0 75);\n --foreground-soft-chakra: oklch(0.9 0.05 75);\n --foreground-strong-chakra: oklch(1 0 75);\n --foreground-ui-chakra: oklch(0.91 0.05 75);\n --muted-foreground-chakra: oklch(0.96 0.03 75);\n}\n\n/* Semantic surfaces (Alert info/success/warning/destructive; destructive Button/Badge) render\n data-glass-tint=\"\", and sonner toasts hit the SAME hues via their data-type attribute\n (destructive pairs with sonner's \"error\") — one block per status, joining the grouped engine\n re-resolution above so --glass-bg / wash re-resolve at the alert/toast. */\n[data-glass-tint=\"info\"],\n[data-sonner-toast][data-type=\"info\"] {\n --glass-tint-h: 255;\n --glass-tint-c: 0.075;\n --glass-tint-a: 0.18;\n --glass-wash-l: 63%;\n}\n[data-glass-tint=\"success\"],\n[data-sonner-toast][data-type=\"success\"] {\n --glass-tint-h: 150;\n --glass-tint-c: 0.088;\n --glass-tint-a: 0.18;\n --glass-wash-l: 87%;\n}\n[data-glass-tint=\"warning\"],\n[data-sonner-toast][data-type=\"warning\"] {\n --glass-tint-h: 85;\n --glass-tint-c: 0.063;\n --glass-tint-a: 0.18;\n --glass-wash-l: 84%;\n}\n[data-glass-tint=\"destructive\"],\n[data-sonner-toast][data-type=\"error\"] {\n --glass-tint-h: 25;\n --glass-tint-c: 0.094;\n --glass-tint-a: 0.18;\n --glass-wash-l: 63%;\n}\n/* FRESCOES (muse / sistine / aurora / gloaming): each block's --glass-tint-h/c/a MUST mirror its\n GlassTintSwitcher preset (components/glass-tint-switcher.tsx). The switcher INLINES those onto ,\n shadowing these blocks on any page with the switcher mounted — so these are the static-consumer fallback.\n Keep them identical, or the demo and a static page render different surfaces. (--glass-bg gradients and\n --glass-fg-h are CSS-only — no preset counterpart.) */\n\n/* Muse — the cityscape signature, mode-aware: cool daytime blues in light, the neon night sweep in\n dark, so it tracks the day/night background. Pick the look with the light/dark/system toggle. */\n/* Muse is now ONE palette in both modes: the neon city — cyan → violet → magenta → pink → amber. The\n old day sweep (a pale blue → cyan → teal cityscape) is gone; its hues live on in Aurora, which took\n them over.\n\n What differs between the modes is ALPHA, not colour. A day surface sits on a light page and needs\n more of the gradient to register; night sits on a dark one and needs less before the neon blows out.\n Lightness is shared because these stops are already bright (L 78–85), so they read on either page.\n\n Stop 2's chroma is 0.095, not the 0.17 night shipped with: h290 at L80 has an sRGB ceiling of 0.1052,\n so 0.17 was 162% over and has been browser-gamut-mapped this whole time — which also means it\n rendered differently in Safari (per-channel clamp toward grey) than in Chrome (hue-preserving map).\n 0.095 is 90% of its own ceiling, the same rule the rest of the stops follow. */\n[data-glass-tint=\"muse\"] {\n --glass-tint-h: 230;\n --glass-fg-h: 290; /* text/accent anchor: violet, matching night so the two modes share one identity */\n --glass-tint-c: 0.056;\n --glass-tint-a: 0.16;\n --glass-wash-l: 76%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.32) 0%,\n oklch(80% 0.095 290 / 0.3) 22%,\n oklch(78% 0.2 326 / 0.28) 45%,\n oklch(79% 0.12 350 / 0.28) 70%,\n oklch(84% 0.12 74 / 0.3) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.46) 0%,\n oklch(80% 0.095 290 / 0.44) 22%,\n oklch(78% 0.2 326 / 0.42) 45%,\n oklch(79% 0.12 350 / 0.42) 70%,\n oklch(84% 0.12 74 / 0.44) 100%\n );\n /* Light-mode body: the night anchor's hue (288) at a day-legible lightness. Chroma is 90% of the\n ceiling at L91 h288 (0.0446), because a light surface has very little chroma headroom. */\n --glass-opaque-bg: oklch(91% 0.04 288);\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"muse\"] {\n /* Night: the same neon palette as day, at night's lower alphas (a dark page needs less of the\n gradient before the neon blows out). Only alpha and the solid body differ from the block above. */\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.24) 0%,\n oklch(80% 0.095 290 / 0.22) 22%,\n oklch(78% 0.2 326 / 0.2) 45%,\n oklch(79% 0.12 350 / 0.2) 70%,\n oklch(84% 0.12 74 / 0.22) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.36) 0%,\n oklch(80% 0.095 290 / 0.34) 22%,\n oklch(78% 0.2 326 / 0.32) 45%,\n oklch(79% 0.12 350 / 0.32) 70%,\n oklch(84% 0.12 74 / 0.34) 100%\n );\n --glass-opaque-bg: oklch(34% 0.12 288);\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Sistine — the signature: a painterly fresco of four jewels (amber → rose → sapphire → aventurine,\n in that order), interpolated `in oklch` so the warm→cool transitions stay vivid instead of\n averaging to grey. Border + crystal + accent pick up the rose base via the tint vars. */\n[data-glass-tint=\"sistine\"] {\n --glass-tint-h: 75; /* surface: mirrors the Sistine switcher preset */\n --glass-fg-h: 18; /* text/accent anchor: crimson (hand-selected) — hue 18 reads as deep red, not the rose at h8 */\n --glass-tint-c: 0.062;\n --glass-tint-a: 0.16;\n --glass-wash-l: 81%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(90% 0.11 75 / 0.34) 0%,\n oklch(86% 0.11 8 / 0.3) 33%,\n oklch(86% 0.11 255 / 0.3) 66%,\n oklch(87% 0.11 158 / 0.32) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(90% 0.11 75 / 0.46) 0%,\n oklch(86% 0.11 8 / 0.42) 33%,\n oklch(86% 0.11 255 / 0.42) 66%,\n oklch(87% 0.11 158 / 0.44) 100%\n );\n /* Crystal composes its own background (not --glass-bg), so hand it the fresco explicitly. */\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"sistine\"] {\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.11 75 / 0.22) 0%,\n oklch(82% 0.11 8 / 0.2) 33%,\n oklch(82% 0.11 255 / 0.2) 66%,\n oklch(83% 0.11 158 / 0.22) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.11 75 / 0.32) 0%,\n oklch(82% 0.11 8 / 0.3) 33%,\n oklch(82% 0.11 255 / 0.3) 66%,\n oklch(83% 0.11 158 / 0.32) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Aurora — northern lights: spring-green → teal → violet, interpolated `in oklch` so the cool sweep\n stays vivid. Border + crystal + accent pick up the teal base via the tint vars.\n DAY was authored at L 90/89/84 — within ~5 L of the L-95 page it composites onto — so after the\n alpha only 2.2 L of separation survived and the bands read as haze rather than bands. Night works\n because its stops sit 60+ L above an L-20 page and land 13.6 L clear of it. The fix is that\n separation, not more colour: day already delivered MORE chroma than night (spread 26/29/19 against\n 14/15/13) and still looked washed out, because colour without lightness structure reads as fog.\n Stops now sit at L 76.3/74.7/71.8, which also buys chroma headroom, so each takes ~90% of its OWN\n hue's sRGB ceiling rather than one shared number — h292 violet was clipped to 65% at L 84 and is\n fully in gamut here. Composited: spread 48/40/38 (was 26/29/19), separation 8.0 L (was 2.2).\n How far this could go is bound by TEXT, not by taste. AutoForeground models a fresco surface from\n the tint wash (--glass-tint-c/-a), never from this gradient literal, so it does not re-band when\n these stops move and the contrast has to hold statically. --foreground-soft (the L-40 heading tier)\n is the weakest pair on a light surface and sets the floor: at a composite of L 84 it measures 59.5\n and fails, so the stops are placed to land no darker than ~L 86. Worst measured here is 63.5.\n Frosted gets its OWN alphas rather than the old ~1.37x boost over --glass-bg: with stops this much\n darker, that multiplier drove the violet composite to L 83.4 and soft to 58.4. These land all three\n near L 86.4 instead. The authored shape is unchanged — green, then teal, then violet darkest. */\n/* Aurora now sweeps Muse Day's hues — blue 235 → cyan 205 → teal-green 155 — instead of the old\n green → cyan → violet. LIGHTNESS is untouched, because it is not a style choice here: the day stops\n sit at L 71.8–76.3 to satisfy the --foreground-soft ≥60 Lc bind described above, and Muse Day's own\n L 91–94 is precisely the band these were moved away from when day read as haze.\n\n CHROMA had to be re-derived, it could not travel with the hues. Each stop is 90% of that hue's OWN\n sRGB ceiling at its lightness, which is the rule the previous stops already followed (all three\n measured exactly 90%). A flat number cannot survive a hue change: the old 0.175 is 90% of green's\n ceiling at L76.3 but 127% of blue's, so carrying it over would have clipped stop 1 outright.\n\n Night is re-derived the same way, which also fixes a pre-existing clip: its violet stop asked 0.13\n against a 0.1068 ceiling at h292/L80 — 122% — so that stop has been gamut-mapped by the browser all\n along. */\n[data-glass-tint=\"aurora\"] {\n --glass-tint-h: 205; /* the sweep's midpoint, was 178 */\n --glass-tint-c: 0.054;\n --glass-tint-a: 0.16;\n --glass-wash-l: 86%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(76.3% 0.124 235 / 0.4) 0%,\n oklch(74.7% 0.111 205 / 0.38) 50%,\n oklch(71.8% 0.155 155 / 0.38) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(76.3% 0.124 235 / 0.46) 0%,\n oklch(74.7% 0.111 205 / 0.43) 50%,\n oklch(71.8% 0.155 155 / 0.37) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"aurora\"] {\n --glass-bg: linear-gradient(135deg in oklch, oklch(86% 0.071 235 / 0.22) 0%, oklch(85% 0.127 205 / 0.2) 50%, oklch(80% 0.173 155 / 0.22) 100%);\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.071 235 / 0.32) 0%,\n oklch(85% 0.127 205 / 0.3) 50%,\n oklch(80% 0.173 155 / 0.32) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Gloaming — dusk: gold → rose → indigo, interpolated `in oklch` so the warm→cool fade keeps its\n glow. Night keeps the warm glow; day uses a dusk-rose anchor (the warm hue reads muddy as light-mode text). */\n[data-glass-tint=\"gloaming\"] {\n --glass-tint-h: 32; /* surface: mirrors the Gloaming switcher preset; text shares it (--glass-fg-h defaults to it) */\n --glass-tint-c: 0.089;\n --glass-tint-a: 0.16;\n --glass-wash-l: 64%;\n --glass-bg: linear-gradient(135deg in oklch, oklch(89% 0.12 62 / 0.32) 0%, oklch(84% 0.14 350 / 0.3) 50%, oklch(78% 0.13 278 / 0.3) 100%);\n --glass-frosted-bg: linear-gradient(135deg in oklch, oklch(89% 0.12 62 / 0.44) 0%, oklch(84% 0.14 350 / 0.42) 50%, oklch(78% 0.13 278 / 0.42) 100%);\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"gloaming\"] {\n --glass-tint-h: 32; /* night keeps the warm glow; day uses the dusk-rose above */\n --glass-bg: linear-gradient(135deg in oklch, oklch(86% 0.12 62 / 0.22) 0%, oklch(80% 0.14 350 / 0.2) 50%, oklch(74% 0.13 278 / 0.22) 100%);\n --glass-frosted-bg: linear-gradient(135deg in oklch, oklch(86% 0.12 62 / 0.32) 0%, oklch(80% 0.14 350 / 0.3) 50%, oklch(74% 0.13 278 / 0.32) 100%);\n --glass-crystal-fresco: var(--glass-bg);\n}\n/* Sonner Toast Notifications - Glass Effect Overrides */\n[data-sonner-toast] {\n /* Glass gradient layered over a mostly-opaque surface so toast text stays legible.\n Lower the popover % to make toasts more see-through, raise it to make them more solid. */\n background: var(--glass-bg), color-mix(in oklch, var(--popover) 78%, transparent) !important;\n backdrop-filter: blur(var(--blur)) saturate(180%) !important;\n border: 1px solid var(--glass-border) !important;\n box-shadow: var(--glass-shadow-lg), var(--glow-layer, 0 0 0 0 transparent) !important;\n color: var(--foreground) !important;\n}\n\n/* Status toast borders — derived from the toast's own status tint (the merged status blocks above set\n --glass-tint-h at the toast) instead of four hardcoded tailwind-500 literals; within ~0.1 L of the\n originals on a 1px 30%-alpha border. NOT bare [data-type]: default/loading toasts keep the plain\n glass border. */\n[data-sonner-toast][data-type=\"success\"],\n[data-sonner-toast][data-type=\"error\"],\n[data-sonner-toast][data-type=\"warning\"],\n[data-sonner-toast][data-type=\"info\"] {\n border-color: oklch(0.7 0.18 var(--glass-tint-h) / 0.3) !important;\n}\n\n/* Toast description text */\n[data-sonner-toast] [data-description] {\n color: var(--muted-foreground) !important;\n}\n\n/* Toast action / cancel / close buttons — one glass recipe (the hover fade now covers all three;\n previously only the action button transitioned). Cancel dims to muted below. */\n[data-sonner-toast] :is([data-button], [data-cancel], [data-close-button]) {\n background: var(--glass-bg) !important;\n backdrop-filter: blur(var(--blur-sm)) !important;\n border: 1px solid var(--glass-border) !important;\n color: var(--foreground) !important;\n transition: opacity 0.2s ease !important;\n}\n\n[data-sonner-toast] [data-cancel] {\n color: var(--muted-foreground) !important;\n}\n\n[data-sonner-toast] :is([data-button], [data-cancel], [data-close-button]):hover {\n opacity: 0.9 !important;\n}\n", + "content": "/*\n Sistine theme — aggregator. The theme is authored as focused partials under app/theme/; Tailwind v4\n bundles these relative imports itself (no postcss-import needed). ORDER MATTERS:\n - package imports come first (CSS requires @import before other rules),\n - engine.css must precede presets.css / frescoes.css (a preset's --glass-tint-h overrides the\n engine default by SOURCE ORDER at equal specificity),\n - inside tokens.css, .dark comes after :root (same-element ties resolve by source order).\n Consumers install a FLATTENED single-file build of this graph (registry/theme/globals.css, generated\n by scripts/build-theme.mjs) — run `bun run registry:check` after editing any partial and commit\n registry/theme + public/r. Site-only CSS (the Geist font mapping) lives in app/site.css, which imports\n this file, so nothing the documentation site defines leaks into the shipped theme.\n*/\n@import \"tailwindcss\";\n@import \"tw-animate-css\";\n/* ── Registrations — @custom-variant + @property pins for JS-only tokens ──────\n Loaded first so the dark variant and typed properties exist before anything composes them. */\n\n/* Class-based dark mode, v4-standard form: matches the `.dark` element ITSELF and its descendants, so\n consumers can toggle `.dark` on any wrapper (not just / next-themes), at zero specificity\n (`:where`) so `dark:` overrides by cascade order like every other variant. */\n@custom-variant dark (&:where(.dark, .dark *));\n\n/* Composable, NON-inheriting glow layer: the `glow` effect/prop sets it, and every glass box-shadow\n folds in `var(--glow-layer, …)` so the glow rides INSIDE each material's and page style's shadow\n (they set --srf-shadow; the glow is appended after it) instead of being replaced by it. inherits:false\n keeps a glowing surface from lighting up its glass children. */\n@property --glow-layer {\n syntax: \"*\";\n inherits: false;\n}\n\n/* The veil floor, NON-inheriting for the same reason as the glow: `glass-veil` sets it on the element\n and `glass` paints it as that element's background-color. Inherited, a plain glass surface nested in\n a veiled menu, popover, tooltip, select or card painted its PARENT's veil as its own floor (under the\n Opaque page style: flat white in light, black in dark). A child without a veil of its own now falls\n back to its material's --srf-bg-color, as it would anywhere else. */\n@property --veil-floor {\n syntax: \"*\";\n inherits: false;\n}\n/* Its twin for the solidify layer (see @utility glass-veil): a veiled parent must not restore the layer\n on a child that dropped it. */\n@property --veil-solidify {\n syntax: \"*\";\n inherits: false;\n}\n/* The ink border colour (glass-border-ink): an inked panel must not ink the borders of the glass inside it. */\n@property --brd-ink {\n syntax: \"*\";\n inherits: false;\n}\n\n/* Gradient-text tokens are consumed ONLY from JS inline styles (components/gradient-text.tsx) — no CSS\n rule references them, so Lightning CSS's optimizer would silently drop them (the same failure mode as\n the harmonic tokens below). Registered here to pin them. DO NOT REMOVE. */\n@property --gradient-text {\n syntax: \"*\";\n inherits: true;\n}\n@property --gradient-text-contrast {\n syntax: \"*\";\n inherits: true;\n}\n/* ── Harmonic color registrations — compacted one-liners (biome's CSS formatter is excluded for this\n file; see biome.json). WHY these exist: the comment below. */\n\n/* ── Harmonic token registration ──────────────────────────────────────────────────────────────\n The harmonic tokens (--harmony-h, --hue-*, --color-*, --mono-*, defined in engine.css) are\n consumed ONLY from JS / inline styles (HarmonicSwatchesDemo, AutoForeground) — never referenced by\n a CSS rule. Tailwind v4's Lightning CSS optimizer constant-folds + dead-code-eliminates any custom\n property no CSS rule uses, which silently dropped this ENTIRE subgraph from the COMPILED stylesheet\n (so every `oklch(L C var(--hue-*))` / `var(--color-*)` resolved to nothing at runtime). Registering\n them via @property marks them as first-class typed API the optimizer must preserve. DO NOT REMOVE. */\n@property --harmony-h { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --color-chroma { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --mono-chroma { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-base { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-complement { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-analogous-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-analogous-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-split-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-split-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-triad-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-triad-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-3 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-3 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --color-complement { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-analogous-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-analogous-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-split-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-split-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-triad-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-triad-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-3 { syntax: \"\"; inherits: true; initial-value: transparent; }\n/* ── Design tokens — @theme mapping, base shadcn colors, mode knobs + shadow twins, base layer ─────\n :root carries light values + the LEAF mode knobs the engine composes; .dark carries the dark twins.\n .dark must come after :root (same-element ties resolve by source order on html.dark). */\n\n@theme inline {\n /* Easing. Tailwind's built-ins are deliberately gentle; these are the stronger variants UI motion\n wants, exposed as `ease-out` / `ease-in-out` / `ease-drawer` utilities.\n out — anything ENTERING or EXITING. Starts fast, so the first frame (the one the user is\n actually watching) already moves.\n in-out — things MOVING on screen that neither enter nor leave: carousels, sliders.\n drawer — a slower settle for panels that slide in from an edge, so a large surface arrives with\n weight instead of snapping.\n There is deliberately no `ease-in` token: it delays the first frame, which is the one moment the\n interface most needs to feel responsive. */\n --ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);\n --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);\n /* Anything with a bare `transition` and no `ease-*` inherits this. Tailwind ships\n cubic-bezier(0.4, 0, 0.2, 1) — symmetric, so it eases IN as well, and the first frame barely\n moves. Same curve as --ease-out, written literally because @theme inline doesn't emit the vars\n it inlines. */\n --default-transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1);\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-foreground-soft: var(--foreground-soft);\n --color-foreground-strong: var(--foreground-strong);\n --color-foreground-ui: var(--foreground-ui);\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n --radius: 1.25rem;\n /* Shadow ink — the base black/white that every shadow, pressed-inset, and bevel highlight is built\n from. Mode-invariant (only the ALPHA changes per surface/mode), so apply alpha at the call site:\n oklch(var(--shadow-ink) / ). One knob to retint every shadow at once (e.g. a cooler, less-flat\n ink) instead of hunting down dozens of oklch(0% 0 0) / oklch(100% 0 0) literals. */\n --shadow-ink: 0% 0 0; /* drop + pressed-inset shadow color */\n --shadow-highlight: 100% 0 0; /* inner bevel / top highlight */\n /* Active/pressed shadow SCALE, built from the ink above and mode-aware (deeper in dark; overridden in\n .dark), so components reference these with NO dark: twin. Depth ladder — all inset \"push-in\" presses\n except --active-shadow, which LIFTS the selected tab/toggle. --press-shadow-strong shares the\n standard press's 0.2 primary and stacks a contact layer (was 0.15; raised so the two-layer presses\n build ON the single-layer one instead of undercutting it):\n --press-shadow-sm — subtle press (ghost, outline)\n --press-shadow — standard press (default, gradient, opaque)\n --press-shadow-strong — firm two-layer press (glass, destructive, secondary, frosted)\n --press-shadow-deep — deepest press (crystal)\n --active-shadow — selected-control LIFT (tabs, toggle) */\n --press-shadow-sm: inset 0 1px 2px oklch(var(--shadow-ink) / 0.1);\n --press-shadow: inset 0 2px 4px oklch(var(--shadow-ink) / 0.2);\n --press-shadow-strong: inset 0 2px 4px oklch(var(--shadow-ink) / 0.2), inset 0 1px 2px oklch(var(--shadow-ink) / 0.15);\n --press-shadow-deep: inset 0 2px 4px oklch(var(--shadow-ink) / 0.25), inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --active-shadow: 0 1px 3px oklch(var(--shadow-ink) / 0.1), inset 0 1px 1px oklch(var(--shadow-highlight) / 0.5);\n /* Selected-control FILL — the twin of --active-shadow, which had the lift but no surface, so tabs and\n toggle-group each hardcoded `bg-white/80 dark:bg-white/20`. The colour is composed in engine.css;\n these are its knobs.\n The two modes source their hue by DIFFERENT routes, and that asymmetry is the whole design.\n DARK is structurally theme-driven: the fill is chroma 0 at alpha 0.2, so 100% of the colour is the\n tinted surface showing through — the chip literally IS the theme surface, lifted. Nothing can make\n it drift off-theme.\n LIGHT cannot use that route. Show-through only carries hue if the surface has hue to give, and a\n light glass surface measures around chroma 0.018 (the sheet is itself capped by --glass-tint-c-hi\n at 0.025 near white). So at alpha 0.9 the fill supplies ~94% of the chip's colour and has to carry\n the tint ITSELF.\n Which makes the fill's own lightness the binding constraint, because the near-white ceiling\n collapses hue-dependently — 0.0323 at L 93, 0.061 at L 87, and only 0.0091 by L 98. At the old\n L 93 the cap was 0.031 and the chip delivered an RGB spread of 20: nominally theme-tinted, visibly\n neutral, which is why day did not read as theme-driven while night did. L 87 roughly doubles the\n available chroma and the delivered spread (37), at the cost of sitting 3.6 L BELOW the tab list\n rather than 1.8 above it — the lift is carried by --active-shadow, so the chip still reads\n selected, now as a tinted fill rather than a white one.\n Capped with min() like --border and the chakra body, so selenite (chroma 0) stays exactly neutral\n and no preset can push the fill out of gamut. */\n --active-l: 87%;\n --active-c-max: 0.055; /* ≤ 0.061, the L-87 ceiling floor (lapis h268) */\n --active-a: 0.9;\n /* ── Mode knobs ──────────────────────────────────────────────────────────\n Single-number dials the glass engine's mode-derived block composes (light values here, dark twins in\n .dark). LEAVES ONLY — none composes a tint var (check-theme invariant 1), so they may live on the bare\n blocks and inherit into tint scopes, where the engine re-resolves the derived tokens. */\n --glass-sheet-a: 0.11; /* sheer glass sheet base alpha (per-stop offsets ride on top) */\n --glass-sheet-a1: 0.04; /* the sheet's TOP-stop offset (dark tapers it to 0.03) */\n --glass-frost-boost: 0.1; /* frosted = the sheet lifted by this much */\n --glass-border-a: 0.16; /* glass edge alpha — a white rim needs more presence on light backdrops */\n /* Ink border alpha (borderColor=\"ink\" / glass-border-ink): how much of --foreground the line takes.\n Mode-invariant because --foreground already flips polarity per mode; 0.5 keeps the line clearly\n softer than text while clearing the APCA non-text floor on every preset (check-contrast [ink]). */\n --glass-border-ink-a: 0.5;\n /* Crystal gloss defaults (the demo's Light/Tint sliders override inline). --glass-gloss-tint is\n mode-shared; --glass-gloss-l is NOT — see its twin below. AutoForeground's crystal tiers mirror\n --glass-gloss-l (97 light / 66 dark) as their snapshot-path fallback — keep all three in step. */\n --glass-diffuse: 12px; /* readability blur FLOOR the glass-diffuse axis raises surfaces to (opaque ignores it) */\n /* Gloss ink lightness (\"Light\" in the demo) — a MODE TWIN, because a gloss is defined relative to\n the surface under it and those surfaces are 68 L apart. The crystal floor computes to L 96.5 in\n light and L 28 in dark, so a single shared value cannot be a highlight in both: at 66 the top stop\n lifts dark by +15.2 L (a specular) and darkens light by −12.2 L (a smudge — the same \"grey\n speculars read as grime\" failure the shadow bevel comment warns about).\n 97 is the light twin: composited it lands +0.2 L over the surface, which is all the headroom that\n exists above L 96.5. The cost is a chroma ceiling, and it is strongly HUE-dependent rather than\n uniform — at L 97 it ranges from 0.0686 for peridot (h128) down to 0.0136 for lapis (h268).\n CORRECTION, measured in WebKit: an over-ceiling oklch() is NOT gamut-mapped down to that ceiling.\n It is CLIPPED per channel, which lands FURTHER from the request, not nearer:\n oklch(97% 0.0686 128) -> 232,255,205 (the ceiling — what the model predicts)\n oklch(97% 0.2975 128) -> 185,255,0 (what actually shipped: pure chartreuse)\n So the Tint multiplier did not \"do real work up to each hue's own ceiling\"; above the ceiling it\n drove the ink to a fully saturated primary. Every preset was affected — turquoise and aquamarine\n both clipped to 0,255,255, i.e. the two were indistinguishable — and moonstone reached 255,222,33,\n the egg-yolk cast that made light mode read yellow. --glass-gloss-c-max below is the fix, sized the\n way --glass-chakra-c-max is: the TIGHTEST ceiling across every preset hue, so no preset clips and\n all of them deliver 100% of what they ask for. */\n --glass-gloss-l: 97;\n /* Gloss chroma cap — the L-97 twin. 0.0136 is lapis (h268), the tightest of the 17 presets; 0.013\n sits just under it. Light gloss is therefore near-achromatic by necessity, not by choice: sRGB\n simply holds no more colour that close to white, and asking for more clips rather than tints. */\n --glass-gloss-c-max: 0.013;\n /* Gloss chroma multiplier over the tint (\"Tint\"). Calibrated for the DEFAULT tint (0.018 × 4.25 =\n 0.077, comfortably in gamut); a preset multiplies up from a much higher base, so the product is\n clamped to 0.3 where --glass-gloss-ink composes it — sRGB holds ~0.32 chroma at its very best hue\n and far less at L=66, so beyond that the browser, not the theme, is picking the color. Jewels\n (0.07 × 4.25 = 0.298) sit under the clamp and are unaffected; only the status tints hit it. */\n --glass-gloss-tint: 4.25;\n --glass-wash-l: 72%; /* tint wash lightness — deeper in dark so the tint reads over dark backdrops */\n --glass-wash-c-mult: 2.5; /* tint wash chroma multiplier (moonstone night quiets it to 2) */\n --glass-accent-l: 0.6; /* accent envelope — lifted in dark so the hue pops */\n --glass-accent-c: 0.15;\n --glass-glow-lc: 0.62 0.2; /* glow L+C pair */\n --glass-glow-a: 0.45;\n --harmonic-l: 0.6; /* --color-* accent-tier lightness */\n --mono-l-1: 0.75; /* mono ramp steps (irregular light↔dark lifts — three dials, not one) */\n --mono-l-2: 0.6;\n --mono-l-3: 0.45;\n --gradient-l: 0.6; /* brand-gradient envelope (matches --glass-accent-l/-c) */\n --gradient-c: 0.15;\n --gradient-contrast-l: 0.32; /* contrast gradient text: dark-on-light here, light-on-dark in .dark */\n --gradient-contrast-c: 0.14;\n --glass-crystal-l: 96; /* crystal body lightness — see --glass-crystal-bg; 100 carries zero chroma */\n --glass-crystal-bg-a: 0.3; /* crystal floor alpha (near-clear in dark) */\n /* glass-solid floor — the \"how solid\" dial (0 = sheer, 1 = fully solid; shared by light/dark) + the\n mode floor lightness; composed in @utility glass-veil so a scoped --glass-solid-a resolves there. */\n --glass-solid-a: 0.65;\n --glass-solid-l: 99%;\n /* Opaque tint BODY lightness (plain number → % in the engine). The header L slider drives this; the\n opaque floor color AND AutoForeground's --foreground-opaque banding both read it, so they can't\n drift. LOWER = deeper tint.\n L AND the c-scale below together absorb the retired opaque SHEET (see materials.css). --glass-bg was a\n near-white gradient at ~0.11 alpha, so it did two things to this floor: lightened it AND desaturated\n it. Compensating with lightness alone reproduced only half of that — it left the floor too saturated,\n and pushing L further toward white also SHRINKS the sRGB chroma ceiling, which blew the opaque body\n past check-gamut's 4.6× budget (lapis hit 5.46×). Moving both knobs reproduces both effects and stays\n inside the budget at 4.47×. Fitted across the 14 presets: worst OKLab dE 0.0021, mean 0.0009 — an\n order of magnitude under a just-noticeable difference, so the surface is unchanged to the eye.\n Moonstone pins its own pair (presets.css); its floor is near-white, where the sheet did far less. */\n /* L88, not 90.9. Near-white is where the sRGB chroma ceiling collapses, so an opaque card at 90.9 had\n two problems at once: it read as washed-toward-white, and it could not hold enough chroma to carry\n its own tint (lapis and sapphire were at 97–99% of a ceiling worth only 0.043). Dropping 2.9 L\n roughly doubles every hue's ceiling while keeping body text at Lc 80.0 — the floor is still firmly\n on the light side, so text polarity never changes. */\n --glass-opaque-l: 88;\n /* 0.85, down from 1.26. The old value only looked large: it was immediately clipped to the shared\n 0.042 cap, so every preset rendered the SAME chroma regardless of its tint. Presets now pin their\n own --glass-opaque-c-max (their hue's real ceiling at L88), and 0.85 is the multiplier that lets\n each one reach it. Net effect vs the old flat 0.042: +7% to +88% depending on hue. */\n --glass-opaque-c-scale: 0.85;\n /* Opaque floor chroma cap — the near-white sibling of --glass-tint-c-hi / --glass-chakra-c-max /\n --glass-gloss-c-max, and the last surface that lacked one. Without a cap the floor multiplied raw\n tint chroma this close to white and clipped for most of the palette.\n FALLBACK ONLY: the 12 jewels each pin their own above (their hue's real ceiling at L88); this\n covers the 9 scopes that don't — the 4 frescoes, the 4 status tints and moonstone. Sizing the\n SHARED cap to the tightest hue is the mistake that came before: one number low enough for amber\n (h75, ceiling 0.036 at L88) left peridot at 18% of its own much larger ceiling. So this is\n deliberately not the tightest — per-hue pins are, and scripts/check-gamut.mjs sweeps every scope\n against its real ceiling rather than trusting this number. */\n --glass-opaque-c-max: 0.055;\n /* ── Solidify floor — the opaque backing UNDER sheer glass ───────────────────\n `glass` paints --glass-solidify (this colour at --glass-opacity) as the bottom background-image\n layer of every SHEER material, so it is ~70% of what page/crystal/chakra text actually sits on.\n It used to be --glass-opaque-bg itself, which conflated two different jobs: the colour an OPAQUE\n CARD shows, and the backing that sets sheer text contrast. Opaque cards explicitly drop this layer\n (materials.css, --srf-solidify: none — it would be C over C), so the two were never actually the\n same surface; they only shared a token.\n Splitting them is what lets the sheer floor rise without repainting opaque cards. It is the right\n lever for text contrast because it is the only one with real WEIGHT in the composite (0.7) that is\n not part of a preset's declared identity: --glass-wash-l is the theme COLOUR (moving it changes\n what lapis IS), and --glass-tint-a is already per-mode and desaturates. Measured across all 17\n scopes, L92 is what puts every preset's body text over the 75 floor with the 1.5 Lc margin, with\n no preset's tint tokens touched. DARK needs none of this (it clears everywhere already), so it\n pins both back to the opaque floor below and nothing changes there. */\n --glass-solidify-l: 92;\n /* The floor's chroma ceiling collapses as it lightens, so the L88 caps above would clip here. Rather\n than 17 new per-preset numbers, derive from the ones each preset already declares: across the\n palette the L92/L88 ceiling ratio runs 0.60–0.90, and 0.65 lands every scope INSIDE its real\n ceiling (worst 0.99×, verified by scripts/check-gamut.mjs). Hues that peak light — peridot,\n turquoise, aventurine, amber, aquamarine — are not bound by it at all and keep 100% of their\n surface chroma; the dark-peaking arc keeps 80–86%. */\n --glass-solidify-c-max: calc(var(--glass-opaque-c-max, 0.055) * 0.65);\n /* ── Chakra — the step-cut material (between opaque and crystal) ─────────────\n Every other material is lit from ONE direction: --glass-shadow carries a bright top bevel and a\n dark bottom shade, so surfaces read as a lit dome. Chakra lights all FOUR edges as mitered facets\n (a conic-gradient with hard stops on the diagonals, composed in engine.css), so it reads as a\n step-cut gem instead.\n The facets are pure LUMINANCE overlays — --shadow-highlight / --shadow-ink at these alphas, chroma\n 0 — not tinted colors. Two reasons, and they are the same reason: (1) the theme hue already comes\n through from the tinted body floor beneath, and a translucent white/black over it shifts lightness\n while preserving hue; (2) a bright facet sits near L96 in light mode, where the sRGB chroma ceiling\n across the 17 presets collapses to 0.0182 — a tinted highlight there would be gamut-mapped by the\n browser at nearly every preset. This is the theme's existing rule for the top bevel (\"a specular,\n not a shadow; grey speculars read as grime\"), applied to all four sides.\n The alphas are irregular mode TWINS, not one knob: the same alpha buys a very different lightness\n step over an L88 body than over an L28 one. They are solved so BOTH modes span 18 L end to end —\n light 94.0 > 90.5 > 88.0 > 83.0 > 76.0, dark 38.0 > 32.0 > 28.0 > 24.0 > 20.0 (top, left, table,\n right, bottom). Light comes from the top-left, matching the 135° axis of --glass-bg. */\n --glass-chakra-l: 88; /* body/table lightness */\n /* Body alpha — the \"between crystal and opaque\" dial, and now genuinely that across the WHOLE\n surface: crystal's floor sits at 0.3, opaque at 1. Nothing is painted over the middle any more\n (the cut moved into box-shadow), so this alone decides how much backdrop shows, and glass-diffuse\n / --glass-opacity modulate it the same way they do on crystal. */\n --glass-chakra-a: 0.62;\n --glass-chakra-c-max: 0.055; /* body chroma cap — ≤ 0.0564, the L-88 ceiling floor (lapis h268) */\n --glass-chakra-step: 3px; /* width of ONE bevel step (the frame is two) */\n --glass-chakra-facet-t-a: 0.5; /* top — highlight, +6.0 L */\n --glass-chakra-facet-l-a: 0.208; /* left — highlight, +2.5 L */\n --glass-chakra-facet-r-a: 0.057; /* right — ink, −5.0 L */\n --glass-chakra-facet-b-a: 0.136; /* bottom — ink, −12.0 L */\n /* ── Glass shadows — mode TWINS (not knobs: the light↔dark alpha ratios AND layer structures are\n irregular). They compose only --shadow-ink/-highlight, never tint vars, so bare placement is legal.\n LIGHT twins carry extra definition layers dark mode gets for free (a light surface on a dark page\n separates itself): a tight CONTACT drop + 1px dark outer HAIRLINE define the edge on same-lightness\n backdrops, and a directional BEVEL (bright top inner edge, ink bottom inner edge) makes pills read\n convex instead of printed. The last layer flips family per mode: dark's white inner GLOW (luminous\n glass) becomes an ink VIGNETTE in light — a white wash on a light surface brightens without\n defining, while a faint vignette reads as glass thickness. (Frosted keeps its white glow in both\n modes: milkiness IS that material.) The top bevel stays WHITE in both — it's a specular, not a\n shadow; grey speculars read as grime. Layer order: ambient drop, contact drop, hairline, top\n bevel, bottom shade, inner glow/vignette. */\n --glass-shadow:\n 0 4px 16px oklch(var(--shadow-ink) / 0.13), 0 1px 3px oklch(var(--shadow-ink) / 0.11), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.4) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.08) inset,\n 0 0 12px oklch(var(--shadow-ink) / 0.05) inset;\n --glass-shadow-lg:\n 0 8px 32px oklch(var(--shadow-ink) / 0.16), 0 2px 6px oklch(var(--shadow-ink) / 0.12), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.45) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.09) inset,\n 0 0 20px oklch(var(--shadow-ink) / 0.06) inset;\n --glass-shadow-sm:\n 0 2px 8px oklch(var(--shadow-ink) / 0.1), 0 1px 2px oklch(var(--shadow-ink) / 0.09), 0 0 0 1px oklch(var(--shadow-ink) / 0.04),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.35) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.07) inset,\n 0 0 8px oklch(var(--shadow-ink) / 0.04) inset;\n --glass-frosted-border: oklch(100% 0 0 / 0.3);\n --glass-frosted-shadow:\n 0 8px 32px oklch(var(--shadow-ink) / 0.17), 0 2px 6px oklch(var(--shadow-ink) / 0.13), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.5) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.09) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.18) inset;\n --glass-crystal-shadow: var(--glass-shadow), 0 1px 1px oklch(var(--shadow-highlight) / 0.4) inset;\n --glass-crystal-shadow-hover: var(--glass-shadow-lg), 0 1px 1px oklch(var(--shadow-highlight) / 0.45) inset;\n /* Base colors - Light mode with dark text */\n --background: transparent;\n --foreground: oklch(\n 0.2 0.035 var(--glass-fg-h)\n ); /* CSS baseline (tinted, ~body target) so first paint has no gray→tint flash; AutoForeground refines to the exact APCA solve after */\n /* Size-tiered foregrounds (ARC Bronze Simple Mode bands) — static fallback; AutoForeground re-bands\n these against the live glass surface at runtime. text-foreground-soft / -strong utilities. */\n --foreground-soft: oklch(\n 0.45 0.077 var(--glass-fg-h)\n ); /* large/heading — baseline matched to AutoForeground's median pick; chroma is the LOWEST any preset hue solves to, so no hue clips before JS refines */\n --foreground-strong: oklch(\n 0.2 0.034 var(--glass-fg-h)\n ); /* fine/small — the ramp's tonal clip put this at L20, not L12; the old value flashed dark-then-tinted on every load */\n --foreground-ui: oklch(0.42 0.071 var(--glass-fg-h)); /* icons — AutoForeground re-bands (+ optional hue) */\n /* Static baselines for the OPAQUE + CRYSTAL tier sets (the [data-material] remaps read these) —\n WITHOUT them, a no-JS consumer (or SSR first paint) would substitute an unset var and INHERIT the\n page foreground onto those floors. Light mode: the opaque floor (L90) and crystal surface share\n the page's direction, so the normal literals apply; AutoForeground refines when mounted. */\n --foreground-opaque: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-opaque: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-opaque: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-opaque: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-opaque: oklch(0.14 0.04 var(--glass-fg-h));\n --foreground-crystal: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-crystal: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-crystal: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-crystal: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-crystal: oklch(0.14 0.04 var(--glass-fg-h));\n /* Chakra tier baselines — text sits on the TABLE (the flat centre, L 88), not on the facets, so in\n light mode the direction matches the page and the normal literals apply. AutoForeground re-bands\n against the composited table when mounted. */\n --foreground-chakra: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-chakra: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-chakra: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-chakra: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-chakra: oklch(0.14 0.04 var(--glass-fg-h));\n --card: transparent;\n --card-foreground: var(--foreground);\n --popover: transparent;\n --popover-foreground: var(--foreground);\n\n /* Primary colors - Light mode */\n --primary-l: 0.42; /* primary ink lightness (dark pill on light pages) — the color itself is composed in the engine, following the theme hue */\n /* Ceiling on the primary ink's chroma. The ×1.2 in the engine is intent — primary should read MORE\n saturated than the base tint — but uncapped it asked past the sRGB edge for five presets in light\n (turquoise, aquamarine, success, warning, aurora) and two in dark (lapis, info), where the browser\n gamut-maps and the delivered colour stops tracking the authored one.\n Measured against --glass-fg-h, not --glass-tint-h: --primary follows the FOREGROUND hue, and the\n Muse/Gloaming frescoes decouple the two (fg 290 / 18), so those hues are in the sweep — aurora only\n shows up as out-of-gamut once you check the fg hue. */\n --primary-c-max: 0.069; /* ≤ 0.0697, the L-42 ceiling floor — h205 (aurora) is now tighter than h190 */\n --primary-foreground: oklch(0.98 0 0); /* White text on the primary ink — Lc 88.1 at the worst hue, turquoise (measured) */\n\n /* Secondary colors - Light mode */\n /* ── Flat-neutral shadcn SURFACES, now theme-tinted ──────────────────────────\n --accent, --secondary, --muted and --sidebar were the last chroma-0 tokens in the theme, which is\n why a toggle's on-state read as near-white in light and near-black in dark no matter which preset\n was active: the fill simply had no hue to give. Same treatment as --border — lightness stays put\n (pure recolor), the colour moves to engine.css so it re-resolves per tint scope, and chroma is\n min(the preset's own, a cap) so a preset that declares less keeps its restraint and selenite stays\n exactly neutral.\n Each cap is measured against the tightest sRGB ceiling across all 17 presets AT THAT LIGHTNESS, and\n they differ by a factor of eight because the ceiling collapses toward white: 0.0714 at L 85, but\n only 0.0091 at L 98. One shared cap would have to be the smallest, tinting nothing. */\n --secondary-l: 0.96;\n --secondary-c-max: 0.018; /* ≤ 0.0182, the L-96 ceiling floor (lapis h268) */\n --secondary-foreground: var(--foreground);\n\n /* Muted colors - Light mode */\n --muted-l: 0.85;\n --muted-c-max: 0.071; /* ≤ 0.0714, the L-85 ceiling floor (lapis h268) */\n --muted-foreground: oklch(\n 0.35 0.06 var(--glass-fg-h)\n ); /* Level 1 — L35, not L14: the widest gap of the set, and the one doing most of the first-paint flash */\n\n /* Accent colors - Light mode. NOTE the knob prefix: --accent-c / --accent-h are already taken as the\n user-facing overrides for --glass-accent (the vivid switch/slider fill), a different role. */\n /* L93, down from 0.96, and the cap raised with it. The hover fill was reading as plain white: at L96 the\n sRGB gamut allows a chroma of only 0.0182, an RGB spread of 19, so \"tinted\" was arithmetically\n invisible. Chroma near the extremes is not a free parameter — it is whatever the gamut leaves — so the\n only way to a visible tint is to step off the extreme. Three points of lightness buys 1.7x the spread\n (19 -> 32) and moves the fill DARKER than the surface, the direction a highlight should go on a light\n page. It cannot go much further: --accent is also painted at 50% (bg-accent/50 on calendar's \"today\",\n button's active state, ToggleGroup hover), and that composite over an L88 card is what binds — at L92\n it drops to 76.0 Lc, under the floor+margin, while L93 holds 76.9. */\n --accent-bg-l: 0.93;\n --accent-bg-c-max: 0.032; /* ≤ 0.0323, the L-93 ceiling floor (lapis h268) */\n /* Stays --foreground: this token is text on a SURFACE-lightness background. Its callers all have that\n shape — the ghost button's hover, toggles, and the highlighted row of every menu (dropdown, context,\n navigation, command, menu-bar, select), which paint bg-accent so a consumer's --accent reaches them\n (the shadcn contract). Text and fill only stay legible together while --accent sits near the\n surface, which is precisely why the fill was moved by chroma rather than by weight. */\n --accent-foreground: var(--foreground);\n\n /* Destructive colors - WCAG AA compliant (4.5:1 contrast) */\n --destructive: oklch(0.55 0.22 25); /* Red - 4.5:1 with white */\n --destructive-foreground: oklch(0.98 0 0); /* White text on red */\n\n /* Border / input LIGHTNESS + chroma-CAP knobs — the COLOR is composed in engine.css so the edge\n follows the theme tint like every other surface token (leaves, so they inherit into tint scopes\n where the engine re-resolves).\n L 0.85, not the 0.88 the flat neutral carried: sRGB's chroma ceiling collapses as lightness climbs,\n and at 0.88 the binding ceiling across all 17 presets is 0.0564 — the most colorful in-gamut edge\n possible there still reads as grey (mean RGB spread 51). Dropping 3 points of L raises the ceiling\n to 0.0714 and nearly doubles delivered colorfulness (mean spread 65) while moving the grey\n reference only #d7d7d7 → #cecece, which is below the threshold of a perceived weight change.\n --border-c-max is the CAP, not the value: chroma is min(tint chroma, cap), so a preset that\n declares less than the cap keeps its own restraint — selenite (c 0) stays exactly neutral, which is\n the whole point of that theme — while lapis (c 0.15) clamps instead of being gamut-mapped by the\n browser. A flat multiplier cannot do both: any factor small enough to keep lapis in gamut drags the\n 0.07 jewels below where they are today. */\n --border-l: 0.85;\n --input-l: 0.85;\n --border-c-max: 0.07; /* ≤ 0.0714, the L-0.85 ceiling floor (lapis h268) */\n --ring: oklch(0.5 0.2 250); /* Focus ring */\n\n /* Chart colors - WCAG AA compliant */\n --chart-1: oklch(0.6 0.2 250); /* Blue */\n --chart-2: oklch(0.55 0.15 150); /* Green */\n --chart-3: oklch(0.5 0.2 50); /* Yellow */\n --chart-4: oklch(0.65 0.2 300); /* Purple */\n --chart-5: oklch(0.6 0.2 20); /* Orange */\n\n /* Sidebar colors - Light mode */\n --sidebar-l: 0.98;\n --sidebar-c-max: 0.009; /* ≤ 0.0091, the L-98 ceiling floor — near white holds almost no hue */\n --sidebar-foreground: var(--foreground);\n --sidebar-primary: oklch(0.5 0.2 250); /* Blue */\n --sidebar-primary-foreground: oklch(0.98 0 0); /* White text on primary */\n --sidebar-accent: oklch(0.15 0 0 / 0.1); /* Subtle glass overlay (≈ foreground/10), not a solid block */\n --sidebar-accent-foreground: var(--foreground);\n --sidebar-border: var(--border); /* Same divider role, same value it already carried — follow the role token */\n --sidebar-ring: oklch(0.5 0.2 250); /* Blue ring - 3:1 contrast */\n}\n\n.dark {\n /* Base colors — light text on a dark ground */\n --background: transparent;\n /* Pressed/active shadow SCALE — dark overrides: primaries deepen +0.1, contact layers +0.05, so the\n push-in still reads on dark surfaces (see :root for the light ladder + rationale). */\n --press-shadow-sm: inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --press-shadow: inset 0 2px 4px oklch(var(--shadow-ink) / 0.3);\n --press-shadow-strong: inset 0 2px 4px oklch(var(--shadow-ink) / 0.3), inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --press-shadow-deep: inset 0 2px 4px oklch(var(--shadow-ink) / 0.35), inset 0 1px 2px oklch(var(--shadow-ink) / 0.25);\n --active-shadow:\n 0 2px 6px oklch(var(--shadow-ink) / 0.4), inset 0 1px 2px oklch(var(--shadow-highlight) / 0.15),\n inset 0 0 8px oklch(var(--shadow-highlight) / 0.1);\n /* Selected-control fill — dark twin. Deliberately UNCHANGED in behaviour from the `bg-white/20` it\n replaces: chroma cap 0 makes min() resolve to 0, so this is a pure white lift and the hue arrives\n by show-through from the surface below, which is exactly why dark already read correctly. Light\n has to source its hue directly (see :root); the two modes reach the same result by different\n routes because the near-white gamut only permits one of them. */\n --active-l: 100%;\n --active-c-max: 0;\n --active-a: 0.2;\n /* ── Mode knobs — dark twins (see :root for what each dial does). */\n --glass-sheet-a: 0.05;\n --glass-sheet-a1: 0.03;\n --glass-frost-boost: 0.06;\n --glass-border-a: 0.15;\n --glass-wash-l: 58%;\n --glass-accent-l: 0.68;\n --glass-accent-c: 0.14;\n --glass-glow-lc: 0.7 0.18;\n --glass-glow-a: 0.5;\n --harmonic-l: 0.68;\n --mono-l-1: 0.82;\n --mono-l-2: 0.68;\n --mono-l-3: 0.5;\n --gradient-l: 0.68;\n --gradient-c: 0.14;\n --gradient-contrast-l: 0.9;\n --gradient-contrast-c: 0.09;\n --glass-crystal-l: 96; /* shared with light: the ceiling that forces 96 is hue-driven, not mode-driven */\n --glass-crystal-bg-a: 0.1;\n /* Gloss ink lightness — dark twin. Over an L-28 floor there is 72 L of headroom, so 66 lifts +15.2\n and still holds 59% of the tonal ink's chroma: a specular that is genuinely tinted. Light cannot\n have both (see :root). */\n --glass-gloss-l: 66;\n /* Gloss chroma cap — the L-66 twin. Far more headroom this far from white: 0.1094 is aurora (h205),\n the tightest across all 23 preset scopes — the FRESCOES count here too, which is why this is not\n turquoise's 0.1106. Dark gloss keeps real colour; it was still clipping before (turquoise\n 0,193,186 and carnelian 255,0,0 both had a pinned channel). */\n --glass-gloss-c-max: 0.109;\n /* L lifted from 22% (less extreme light↔dark switch); the opaque chroma scale (0.9) keeps warm hues\n like amber in-gamut instead of hard-clipping to rust. */\n --glass-solid-l: 18%;\n /* The retired sheet's compensation, dark twin (see :root). Much the larger move of the two modes: a\n near-white sheet over a DARK floor lifts it ~12 codes where over the light floor it lifts ~2–5. Same\n two-knob fit, worst OKLab dE 0.0045 across the 14 presets. */\n --glass-opaque-l: 36.4;\n /* 1.05, up from 0.85. The dark floor was never cap-limited — every preset sat at 31–79% of its own\n ceiling, so the MULTIPLIER was the throttle and dark opaque cards lost their tint. 1.05 is the most\n the tightest hue (turquoise) can take without clipping; it lifts every preset +24% at Lc 80.0. */\n --glass-opaque-c-scale: 1.05;\n /* Opaque floor chroma cap, dark twin (see :root). A GUARD, not a throttle — the distinction matters\n and this value used to get it wrong. It was 0.06, the tightest ceiling across the preset scopes,\n which was correct while every jewel shared --glass-tint-c: 0.07 and the floor multiplied it blind.\n Once each preset tuned its own chroma to its own hue, that shared floor-of-the-tightest started\n costing the hues WITH headroom: at 0.06 amethyst lost 30% of its ask, tourmaline 33%, rose 26% —\n against own ceilings of 0.189, 0.152 and 0.142. Measured, every preset's dark ask now sits at\n 0.31–0.79× of its own ceiling, so nothing clips with no cap at all. 0.095 clears the largest ask\n (tourmaline 0.0901) and therefore throttles nobody, while still bounding a future preset that\n pushes chroma far past today's values. check-gamut scores each preset against its own ceiling. */\n /* Guard, not a throttle: the largest dark ask is tourmaline at 0.111, so this binds nobody. */\n --glass-opaque-c-max: 0.12;\n /* Dark keeps the solidify floor IDENTICAL to the opaque floor — the split exists to buy LIGHT-mode\n text contrast, and dark already clears the body floor on every preset with margin. Pinning to the\n opaque values (rather than leaving :root's L92 / ×0.65 to inherit) is what makes this a light-only\n change: a lighter backing under dark glass would wash the whole mode out. */\n --glass-solidify-l: var(--glass-opaque-l);\n --glass-solidify-c-max: var(--glass-opaque-c-max);\n /* ── Chakra dark twins (see :root for what each dial does + how the alphas were solved).\n The facet alphas invert in character: over an L28 body there is 72 L of headroom UP and only 28\n DOWN, so the highlights need far less alpha and the inks far more than their light-mode twins to\n land the same 18 L span. The targets also skew brighter here (+10/−8 rather than +6/−12) — a dark\n gem reads as cut by catching light, where a light one reads by casting shade. */\n --glass-chakra-l: 28;\n --glass-chakra-a: 0.58;\n --glass-chakra-c-max: 0.046; /* ≤ 0.0471, the L-28 ceiling floor (turquoise h190) */\n --glass-chakra-facet-t-a: 0.139; /* top — highlight, +10.0 L */\n --glass-chakra-facet-l-a: 0.056; /* left — highlight, +4.0 L */\n --glass-chakra-facet-r-a: 0.143; /* right — ink, −4.0 L */\n --glass-chakra-facet-b-a: 0.286; /* bottom — ink, −8.0 L */\n /* ── Glass shadows — dark twins (deeper drops, brighter bevels). */\n --glass-shadow:\n 0 4px 16px oklch(var(--shadow-ink) / 0.3), 0 1px 4px oklch(var(--shadow-ink) / 0.15), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.12) inset,\n 0 0 12px oklch(var(--shadow-highlight) / 0.15) inset;\n --glass-shadow-lg:\n 0 8px 32px oklch(var(--shadow-ink) / 0.4), 0 2px 8px oklch(var(--shadow-ink) / 0.2), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.15) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.18) inset;\n --glass-shadow-sm:\n 0 2px 8px oklch(var(--shadow-ink) / 0.25), 0 1px 2px oklch(var(--shadow-ink) / 0.12), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.1) inset,\n 0 0 8px oklch(var(--shadow-highlight) / 0.12) inset;\n --glass-frosted-border: oklch(100% 0 0 / 0.25);\n --glass-frosted-shadow:\n 0 8px 32px oklch(var(--shadow-ink) / 0.5), 0 2px 8px oklch(var(--shadow-ink) / 0.3), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.2) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.22) inset;\n --glass-crystal-shadow: var(--glass-shadow), 0 1px 1px oklch(var(--shadow-highlight) / 0.2) inset;\n --glass-crystal-shadow-hover: var(--glass-shadow-lg), 0 1px 1px oklch(var(--shadow-highlight) / 0.25) inset;\n --foreground: oklch(\n 0.95 0.03 var(--glass-fg-h)\n ); /* CSS baseline (tinted, ~body target) so first paint has no gray→tint flash; AutoForeground refines to the exact APCA solve after */\n /* Size-tiered foregrounds (ARC Bronze Simple Mode bands) — static fallback; AutoForeground re-bands\n these against the live glass surface at runtime. text-foreground-soft / -strong utilities. */\n --foreground-soft: oklch(0.83 0.065 var(--glass-fg-h)); /* large/heading — refined at runtime */\n --foreground-strong: oklch(0.97 0.015 var(--glass-fg-h)); /* fine/small — refined at runtime */\n --foreground-ui: oklch(0.87 0.057 var(--glass-fg-h)); /* icons — AutoForeground re-bands (+ optional hue) */\n /* Static OPAQUE + CRYSTAL tier baselines, dark twins — the standard dark opaque floor (L32) and\n crystal surface keep the page's light-text direction. Presets whose dark floors flip LIGHT (moonstone\n cream) override the -opaque set in their own block; AutoForeground refines when mounted. */\n --foreground-opaque: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-opaque: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-opaque: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-opaque: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-opaque: oklch(0.89 0.04 var(--glass-fg-h));\n --foreground-crystal: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-crystal: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-crystal: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-crystal: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-crystal: oklch(0.89 0.04 var(--glass-fg-h));\n /* Chakra tier baselines, dark twins — the L-28 table keeps the page's light-text direction. */\n --foreground-chakra: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-chakra: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-chakra: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-chakra: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-chakra: oklch(0.89 0.04 var(--glass-fg-h));\n --card: transparent;\n --card-foreground: var(--foreground);\n --popover: transparent;\n --popover-foreground: var(--foreground);\n\n /* Primary colors — dark mode */\n --primary-l: 0.78; /* primary ink lightness in dark mode (light pill) — color composed in the engine */\n --primary-c-max: 0.107; /* ≤ 0.1077, the L-78 ceiling floor (h268 lapis) */\n /* Near-black text on the primary ink — Lc 62.9 at the worst hue, sistine (measured). It was 60.9\n before --primary-c-max: capping the chroma the browser used to gamut-map moved the worst case back\n above the 62 this comment had been claiming. */\n --primary-foreground: oklch(0.15 0 0);\n\n /* Secondary colors — dark mode */\n /* Flat-neutral surfaces — dark twins (see :root for why these are knobs now). Lightness unchanged;\n the caps are the dark ceilings, which are tightest at the DARKEST step, not the lightest. */\n --secondary-l: 0.25;\n --secondary-c-max: 0.041; /* ≤ 0.0417, the L-25 ceiling floor — h205 (aurora) is now tighter than h190 */\n --secondary-foreground: var(--foreground);\n\n /* Muted colors — dark mode */\n --muted-l: 0.18;\n --muted-c-max: 0.03; /* ≤ 0.0307, the L-18 ceiling floor (turquoise h190) */\n --muted-foreground: oklch(0.9 0.031 var(--glass-fg-h)); /* Level 1 — one ramp step toward the chosen color */\n\n /* Accent colors — dark mode */\n /* L34, up from 0.25 — the same fix as light, mirrored. At L25 the ceiling was 0.0417 (spread 29), so the\n fill read as near-black; L34 allows 0.0565 (spread 42) and moves it BRIGHTER than the surface, which is\n the direction a highlight goes on a dark page. Dark has more room than light here (its 50% composite\n sits at 77.5 Lc rather than 76.0), because the fill and the L36.4 card floor are close together. */\n --accent-bg-l: 0.34;\n --accent-bg-c-max: 0.056; /* ≤ 0.0565, the L-34 ceiling floor (aurora h205) */\n --accent-foreground: var(--foreground);\n\n /* Destructive colors - WCAG AA compliant (4.5:1 contrast) */\n --destructive: oklch(0.65 0.22 25); /* Light red - 4.5:1 with dark background */\n --destructive-foreground: oklch(1 0 0); /* White text on red — APCA Lc ~67 (near-black was Lc 42) */\n\n /* Border / input lightness + chroma-cap — dark twins (see :root; color composed in engine.css).\n L is UNCHANGED here: in dark mode the edge sits at 0.30/0.25 on a dark page, so lowering it would\n cost contrast rather than buy color. Only the cap moves, up to the tightest dark ceiling — 0.0422\n at L 0.25 (turquoise h190), which --input hits first, so one cap serves both. */\n --border-l: 0.3;\n --input-l: 0.25;\n --border-c-max: 0.041; /* ≤ 0.0422, the L-0.25 ceiling floor (turquoise h190) */\n --ring: oklch(0.6 0.2 250); /* Focus ring */\n\n /* Chart colors - WCAG AA compliant */\n --chart-1: oklch(0.65 0.2 250); /* Light blue */\n --chart-2: oklch(0.7 0.15 150); /* Light green */\n --chart-3: oklch(0.75 0.2 50); /* Light yellow */\n --chart-4: oklch(0.7 0.2 300); /* Light purple */\n --chart-5: oklch(0.68 0.2 20); /* Light orange */\n\n /* Sidebar colors — dark mode */\n --sidebar-l: 0.18;\n --sidebar-c-max: 0.03; /* ≤ 0.0307, the L-18 ceiling floor (turquoise h190) */\n --sidebar-foreground: var(--foreground);\n --sidebar-primary: oklch(0.56 0.16 250); /* Match --primary; white text clears APCA */\n --sidebar-primary-foreground: oklch(1 0 0); /* White on the sidebar's brand fill */\n --sidebar-accent: oklch(1 0 0 / 0.1); /* Subtle glass overlay (≈ foreground/10), not a solid block */\n --sidebar-accent-foreground: var(--foreground);\n --sidebar-border: var(--border); /* Same divider role, same value it already carried — follow the role token */\n --sidebar-ring: oklch(0.6 0.2 250); /* Light blue ring - 3:1 contrast */\n}\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in oklch, var(--foreground) 22%, transparent) transparent;\n }\n body {\n /* Text uses the band-picked --foreground (set on by AutoForeground on theme/tint change;\n globals carries a static fallback so there's no flash). */\n color: var(--foreground);\n background-color: transparent;\n }\n /* Thin, glass-friendly scrollbars (replaces the chunky native bar) */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: color-mix(in oklch, var(--foreground) 22%, transparent);\n border-radius: 9999px;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: color-mix(in oklch, var(--foreground) 38%, transparent);\n }\n}\n\n/* ── Below the floor ──────────────────────────────────────────────────────────\n The supported floor is Safari 17.5 / Chrome 120 / Firefox 128 / Edge 120, declared as the browserslist\n in SISTINE's package.json. It is set by what this theme actually uses, not by preference — the binding\n constraints are text-wrap:balance (Safari 17.5), bare-`&` CSS nesting and unprefixed mask-composite\n (Chrome 120), and @property (Firefox 128). Tailwind v4's own baseline sits just under it. That\n browserslist does not ship with this file: a consuming app should declare its own, at or above the\n floor, or its toolchain targets whatever its defaults are.\n\n Every colour in this theme is oklch(), fed through custom properties. On an engine without oklch the\n custom property still PARSES (custom props accept anything) and only fails when substituted — so\n `background-color: var(--color-background)` resolves to an invalid value and computes to `unset`,\n i.e. transparent, rather than to a fallback. Surfaces disappear; text usually survives on the UA\n default. This is not a polyfill and does not try to reproduce the palette — it guarantees a legible\n page instead of transparent panels on stacked backgrounds.\n\n UNVERIFIED IN A REAL ENGINE: no browser in the test matrix lacks oklch, so this branch has only been\n confirmed to compile, not to render. Treat it as a floor, not a tested path. */\n@supports not (color: oklch(0% 0 0)) {\n :root {\n color-scheme: light;\n background-color: #f7f7fb;\n color: #17161c;\n }\n body {\n background-color: #f7f7fb;\n color: #17161c;\n }\n .glass,\n [data-surface],\n [data-material] {\n background-color: #ffffffe6;\n border: 1px solid #17161c26;\n backdrop-filter: none;\n }\n}\n/* ── Glass engine ─────────────────────────────────────────────────────────────\n THREE grouped blocks, split by what must re-resolve where (the var-composition-resolves-at-declaration\n gotcha — see scripts/check-theme.mjs):\n\n A. TINT ANCHORS (:root, [data-glass-tint], …) — the tint knobs plus everything mode-INVARIANT that\n composes them, declared on :root AND every tint scope so a scoped data-glass-tint re-resolves the\n whole subgraph. Deliberately NOT on `.dark`: re-declaring --glass-tint-h / --glass-fg-h at a bare\n `.dark` wrapper would reset a page tint inherited from above.\n\n B. MODE-DERIVED SURFACES (:root, .dark, [data-glass-tint], …) — every tint-composing token whose\n light/dark difference is a handful of numbers. Each is declared ONCE, composing the mode knobs\n (--glass-sheet-a, --glass-wash-l, … — see :root/.dark) which are LEAVES that inherit into tint\n scopes. `.dark` IS in this selector so a dark wrapper inside a light page re-composes its surfaces\n from the wrapper's own dark knobs (same-element cascade) — \"toggle .dark anywhere\" keeps working.\n\n C. GLOSS (:root, .dark, [data-glass-tint], [data-gloss]) — the crystal gloss and the stacks that bake\n it, which also have to recompose wherever a `data-gloss` flavor is set. See block C itself.\n\n Foreground tokens stay on bare :root/.dark (AutoForeground owns them). Preset blocks below override\n --glass-tint-h/c/a by source order. Shadow composites are mode-TWINNED on bare :root/.dark (their\n light↔dark ratios are irregular — knobbing them would change values). */\n:root,\n[data-glass-tint] {\n /* Deliberately restrained glassmorphism — enough to read as glass, never enough to fog what is behind it. */\n /* Glass tint — recolor ALL glass by changing these; presets set them via [data-glass-tint].\n -h = hue, -c = chroma (gradient), -a = wash alpha. The gradient alone is too sheer\n (~6–12% opacity) to show a hue, so -a paints a solid colored floor (--glass-tint-wash)\n that reads over any backdrop. Neutral keeps -a:0, so the default glass is unchanged. */\n --glass-tint-h: 250;\n --glass-tint-c: 0.018;\n --glass-tint-a: 0;\n /* The same chroma, capped for NEAR-WHITE surfaces. The sheet stops, the borders and the crystal\n floor sit at L 95–100%, where sRGB's chroma ceiling collapses AND becomes strongly hue-dependent:\n at L=97% it is ~0.071 for peridot (h128) but ~0.014 for lapis (h268). Feeding those the raw\n --glass-tint-c means each preset lands on whatever fraction of its declared value its hue happens\n to permit — lapis was delivering 9% of its 0.15 where peridot delivered 98% of its 0.07 — and the\n shortfall is then resolved by the browser's gamut mapping rather than by us. That is the same\n class of bug the 3% margin in lib/oklch-utils guards against on the computed path (\"foregrounds\n went grey in Safari while staying tinted in Chrome\"); this is its CSS-side counterpart.\n Capping the BASE (rather than lowering any preset) keeps chroma where it has headroom — a jewel's\n depth lives at mid lightness, in --primary and --glass-tint-wash, which are untouched by this.\n Guarded by scripts/check-gamut.mjs.\n 0.025 was still too generous, because the sheet MULTIPLIES this cap: its stops are\n calc(--glass-tint-c-hi * 0.3…1.3), so the top stop asked 0.0325 against an L-95 ceiling of 0.0229\n at the tightest hue — the cap itself clipped. 0.017 is that ceiling divided by the 1.3 multiplier,\n so the sheet now lands inside the gamut at every preset hue instead of at whatever the browser\n chops it to. Cap-then-multiply is the order that matters here; see check-gamut's SCALE/CAP/BOTH\n shapes, which model each form separately for exactly this reason. */\n --glass-tint-c-hi: min(var(--glass-tint-c), 0.017);\n /* Foreground hue — drives every FOREGROUND-keyed token (body/heading text via AutoForeground,\n --muted-foreground, --glass-accent, --glass-glow, --gradient*), kept SEPARATE from the surface\n hue so tuning text never shifts the glass/fresco surface. Defaults to the tint hue (jewels: text\n == surface); frescoes override it to anchor text off their multi-hue surface (Muse night: surface\n 222, text 310). AutoForeground reads this, falling back to --glass-tint-h for older consumers. */\n --glass-fg-h: var(--glass-tint-h);\n /* ── Harmonic color system ────────────────────────────────────────────────────────────────────\n Color-wheel relationships derived from the CONTENT hue (--glass-fg-h) — the same hue --glass-accent\n and --gradient track — so they rotate with the theme/tint automatically (a generalization of the\n --gradient's +63.53° offset). Hues are mode-invariant ANGLES; CSS Color 4 wraps them past 360°, so use\n anywhere as oklch(L C var(--hue-*)); choosing your own L/C also keeps them in-gamut. The ready --color-*\n tokens sit at the --glass-accent envelope (0.6/0.15; .dark lifts them). NOTE: a fixed-chroma harmonic can\n land at the sRGB edge at some hues and get browser-gamut-mapped — prefer the hue tokens + a conservative\n chroma when that matters. */\n /* Harmonic anchor hue — the wheel origin the harmonies rotate from. Normally the content hue, but the two\n \"hue-less\" themes (selenite + moonstone) anchor at 0° instead — each preset block in presets.css declares\n it, so a CSS-only consumer gets the anchor too (the tint switcher also inlines it, for a custom tint\n dragged to chroma 0) — and their accents come out a colorful red-based harmony, not a warp of a\n near-grey hue. */\n --harmony-h: var(--glass-fg-h);\n --hue-base: var(--harmony-h);\n --hue-complement: calc(var(--harmony-h) + 180);\n --hue-analogous-1: calc(var(--harmony-h) - 30);\n --hue-analogous-2: calc(var(--harmony-h) + 30);\n --hue-split-1: calc(var(--harmony-h) + 150);\n --hue-split-2: calc(var(--harmony-h) + 210);\n --hue-triad-1: calc(var(--harmony-h) + 120);\n --hue-triad-2: calc(var(--harmony-h) + 240);\n --hue-tetrad-1: calc(var(--harmony-h) + 60); /* rectangle: base, +60, +180, +240 */\n --hue-tetrad-2: calc(var(--harmony-h) + 180);\n --hue-tetrad-3: calc(var(--harmony-h) + 240);\n --hue-square-1: calc(var(--harmony-h) + 90); /* square: base, +90, +180, +270 */\n --hue-square-2: calc(var(--harmony-h) + 180);\n --hue-square-3: calc(var(--harmony-h) + 270);\n /* Harmonic chroma — a fixed vivid accent chroma so the palette stays colorful in EVERY theme (selenite +\n moonstone included; they get their color from the hue-0 anchor above, not from chroma). --mono-chroma is\n quieter (0.6×) for the subtle mono ramp. */\n --color-chroma: 0.15;\n --mono-chroma: calc(var(--color-chroma) * 0.6);\n /* (--color-* / --mono-* ready colors live in the mode-derived surfaces block below — their L is the\n mode envelope; the hue angles above are the mode-invariant part.) */\n /* (The solidify floor — the --glass-opacity \"component opacity\" dial — is composed INSIDE each sheer\n glass utility, at the element. Composing it here would bake :root's --glass-opacity into the value\n and silently ignore a per-element glass={{opacity}} — the var-composition gotcha again.) */\n /* Blur values — moderate by design; per-variant sizes (mode-invariant). */\n --blur: 2px;\n --blur-sm: 1px;\n --blur-lg: 8px;\n --blur-xl: 12px;\n --blur-frosted: 25px;\n /* Chakra reads as a THICK stone, so it diffuses more than crystal's near-clear 2px even though its\n body is more solid — the two axes are independent (how much light passes vs how scattered it is). */\n --blur-chakra: 6px;\n /* Crystal edge — identical in both modes, so it lives here with the anchors (the crystal floor +\n shadows are mode-varying: --glass-crystal-bg in the surfaces block below, shadows on :root/.dark). */\n --glass-crystal-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / 0.3);\n /* Crystal gloss KNOBS — leaves only. The gloss itself (ink, the three gradients) and every stack that\n bakes it are composed in block C below, which also recomposes at each [data-gloss] scope.\n --glass-gloss-l / --glass-gloss-tint live in tokens.css (66 / 4.25) — do NOT re-declare here:\n engine.css imports AFTER tokens.css, so a duplicate at equal :root specificity silently wins and\n pure-CSS consumers get a stale gloss. */\n --glass-gloss-hue-span: 40; /* hue: degrees the iridescent sweep shifts up & down from the tint hue */\n /* hue: chroma of the iridescent stops, at the flavor's own L 74. Was 0.16, chosen believing an\n over-ceiling request is gamut-MAPPED down to the ceiling (\"holds 81% of that chroma\"). WebKit clips\n per channel instead (see --glass-gloss-c-max in tokens.css), so 0.16 pinned a channel on the\n tightest hues rather than delivering 81% of itself. 0.12 sits just under 0.1226 — aurora (h205) at\n L 74, the tightest across all 23 preset scopes (frescoes included) — so every hue in the sweep now\n renders what it asks for. */\n --glass-gloss-c: 0.12;\n --glass-gloss-hue-dir: 1; /* gloss direction (±1) — flips the TONAL streak diagonal (calc angle) + the HUE sweep order (span × dir); toggled in the crystal demo */\n --blur-crystal: 2px;\n}\n\n/* B. MODE-DERIVED SURFACES — see the engine header above. Each tint-composing token is declared ONCE,\n parameterized by the mode knobs on bare :root/.dark; `.dark` in this selector keeps dark wrappers\n re-composing with their own knobs. */\n:root,\n.dark,\n[data-glass-tint] {\n /* Solid colored floor under the sheer gradient (see --glass-tint-a) — mode via --glass-wash-l/-c-mult. */\n --glass-tint-wash: oklch(var(--glass-wash-l) calc(var(--glass-tint-c) * var(--glass-wash-c-mult)) var(--glass-tint-h) / var(--glass-tint-a));\n /* Solid accent that tracks the FOREGROUND hue at a fixed vivid chroma — used by active controls\n (switch \"on\", slider range). The --accent-h/-c knob overrides apply in BOTH modes (the old dark\n twin dropped them — that was a bug). */\n --glass-accent: oklch(var(--glass-accent-l) var(--accent-c, var(--glass-accent-c)) var(--accent-h, var(--glass-fg-h)));\n /* Stained-glass — grayscale collapses the backdrop to pure LUMINANCE (akin to alpha: only lighter/\n darker survives), and the glass's OWN tint stack supplies the color — OKLCH-true to the theme, no\n hue-rotate approximation. --glass-stain-amount (0..1) sets how completely the backdrop's color is\n stripped; --glass-stain-invert (0..1, default 1) flips the surviving luminance so bright backdrop\n patches stop fighting dark tinted glass (set 0 to disable). The glass-stained utility and the switcher's Stained toggle both reference THIS token. */\n --glass-stain: grayscale(var(--glass-stain-amount, 1)) invert(var(--glass-stain-invert, 0));\n /* Colored glow for the `glow` prop — follows the foreground hue; override to recolor the halo. */\n --glass-glow: oklch(var(--glass-glow-lc) var(--glass-fg-h) / var(--glass-glow-a));\n /* Selected-control FILL (active tab, pressed toggle) — the surface twin of --active-shadow. Composed\n here, not on :root, because it reads the tint: a scoped data-glass-tint must re-resolve it, and it\n is why the chip tracks whichever theme the subtree carries. Mode knobs in tokens.css carry the two\n routes to the same look (light tints the fill, dark lets the surface show through a white lift). */\n --active-bg: oklch(var(--active-l) min(var(--glass-tint-c), var(--active-c-max)) var(--glass-tint-h) / var(--active-a));\n /* Solid PRIMARY ink (default Button, link text) — themed at the tint's OWN chroma (×1.2): selenite\n stays grey, moonstone whispers warm, jewels read clearly tinted. Lightness is the --primary-l mode knob\n (0.42 light / 0.78 dark). Chroma is min(tint × 1.2, --primary-c-max): the ×1.2 is intent — primary\n should read more saturated than the base tint — but uncapped it asked past the sRGB edge at five\n presets in light and two in dark, where the browser, not the theme, picked the delivered colour.\n Measured after capping: --primary-foreground holds Lc 88.1 light / 62.9 dark at the worst hue. */\n --primary: oklch(var(--primary-l) min(calc(var(--glass-tint-c) * 1.2), var(--primary-c-max)) var(--glass-fg-h));\n /* Ready accent-tier colors (hue angles from the anchors block; L is the mode envelope --harmonic-l). */\n --color-complement: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-complement));\n --color-analogous-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-analogous-1));\n --color-analogous-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-analogous-2));\n --color-split-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-split-1));\n --color-split-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-split-2));\n --color-triad-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-triad-1));\n --color-triad-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-triad-2));\n /* Monochromatic — the harmony anchor hue at stepped lightness (a subtle, low-chroma ramp). */\n --mono-1: oklch(var(--mono-l-1) var(--mono-chroma) var(--harmony-h));\n --mono-2: oklch(var(--mono-l-2) var(--mono-chroma) var(--harmony-h));\n --mono-3: oklch(var(--mono-l-3) var(--mono-chroma) var(--harmony-h));\n /* Opaque glass — a fully solid, faintly tinted floor (no see-through) for the \"opaque\" style.\n --glass-opaque-l is the body-lightness knob (:root/.dark; the header L slider drives it; the floor\n AND AutoForeground's --foreground-opaque banding both read it, so they can't drift). */\n /* Chroma is CAPPED now (--glass-opaque-c-max, tokens.css). This floor sat at L 90.9 in light (it is\n L88 now, for the same reason) — near-white territory where the sRGB ceiling collapses — yet it was\n the one near-white surface\n multiplying RAW --glass-tint-c (×1.26) with no cap, while the sheet had --glass-tint-c-hi, chakra\n had --glass-chakra-c-max and the gloss had --glass-gloss-c-max. That made it the binding surface\n for 7 of the 12 jewels: it clipped first, and every preset's chroma budget was set by it. Capping\n it is what let the tints below be normalised to their hue's real capacity rather than to this\n floor's. Multiply-THEN-cap, matching --glass-gloss-ink. */\n --glass-opaque-bg: oklch(\n calc(var(--glass-opaque-l) * 1%) min(calc(var(--glass-tint-c) * var(--glass-opaque-c-scale)), var(--glass-opaque-c-max, 0.3)) var(--glass-tint-h)\n );\n /* The same floor at the SOLIDIFY lightness/cap — the backing `glass` paints under every sheer\n material (see --glass-solidify in utilities.css). Split from --glass-opaque-bg because an opaque\n CARD and the backing under sheer glass are different surfaces with different jobs; tokens.css has\n the reasoning. Same scale, so a preset's tint still drives it; only the lightness and cap differ. */\n --glass-solidify-bg: oklch(\n calc(var(--glass-solidify-l, var(--glass-opaque-l)) * 1%)\n min(calc(var(--glass-tint-c) * var(--glass-opaque-c-scale)), var(--glass-solidify-c-max, var(--glass-opaque-c-max, 0.3))) var(--glass-tint-h)\n );\n /* Sheer glass sheet — hue/chroma from the tint vars, chroma scaled per stop so the highlights stay\n glassy and the body carries the color. Stop alphas ride the mode base --glass-sheet-a (+ fixed\n offsets; the top stop's offset is its own knob --glass-sheet-a1 — dark tapers it to 0.03);\n frosted = the same sheet lifted by --glass-frost-boost. */\n --glass-bg: linear-gradient(\n 135deg,\n oklch(100% calc(var(--glass-tint-c-hi) * 0.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-sheet-a1))) 0%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.01)) 25%,\n oklch(97% calc(var(--glass-tint-c-hi) * 1) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.02)) 50%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-sheet-a)) 75%,\n oklch(95% calc(var(--glass-tint-c-hi) * 1.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.01)) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg,\n oklch(100% calc(var(--glass-tint-c-hi) * 0.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.04)) 0%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.01)) 25%,\n oklch(97% calc(var(--glass-tint-c-hi) * 1) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.02)) 50%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost))) 75%,\n oklch(95% calc(var(--glass-tint-c-hi) * 1.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.01)) 100%\n );\n /* Very subtle white border — the lit edge of a cut sheet (alpha is the mode dial) */\n --glass-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-border-a));\n /* Opaque CHROME edge — the shadcn --border/--input role tokens (dividers, ghost/input outlines, any\n bare `border-border`). Previously flat neutral (oklch(0.88 0 0) / oklch(0.3 0 0)), which made them\n the ONLY chrome in the theme that did not move with the tint: a ghost Button kept a grey edge under\n lapis while its `outline` sibling (border-foreground/20) tracked the theme. Lightness moves only in\n LIGHT mode (0.88 → --border-l 0.85) to buy chroma headroom; see the knob comment in tokens.css.\n Chroma is min(tint chroma, --border-c-max) — a CAP, deliberately not --glass-tint-c-hi and not a\n multiplier. --glass-tint-c-hi (0.025) is the NEAR-WHITE cap, sized for the L 95–100% sheet stops\n and crystal floor; borrowing it here spent only 66% of the ceiling on amethyst and 17% on peridot,\n which is why the edge still read grey. A flat multiplier fails the other way: lapis declares\n c 0.15, so any factor small enough to keep it in gamut pushes the 0.07 jewels below where they\n started. min() gives every preset its full declared chroma up to the ceiling floor and clamps only\n the ones that overreach — and leaves c 0 at exactly 0, so selenite stays the neutral theme it is.\n Declaring them HERE (rather than on :root) also puts them under scripts/check-gamut.mjs, which\n scrapes engine.css for both the calc() and min() chroma shapes — the suite went 20 → 24 surface/mode\n pairs when these landed, so a later retune of the caps is measured, not asserted.\n SURFACE hue (--glass-tint-h), not --glass-fg-h: an edge belongs to the surface it bounds, matching\n --glass-border / --glass-crystal-border. In a fresco that splits the two (Muse night: surface 222,\n text 310) the border follows the panel, not the type. */\n --border: oklch(var(--border-l) min(var(--glass-tint-c), var(--border-c-max)) var(--glass-tint-h));\n --input: oklch(var(--input-l) min(var(--glass-tint-c), var(--border-c-max)) var(--glass-tint-h));\n /* The remaining shadcn SURFACE tokens — the last chroma-0 values in the theme. Same shape as --border\n for the same reason: a toggle's on-state, a hover wash and a sidebar panel all take their fill from\n these, so while they were flat neutrals those surfaces read near-white in light and near-black in\n dark in every preset. Each carries its OWN cap because the sRGB ceiling is a function of lightness\n and these sit from L 18 to L 98 — a shared cap would have to be the smallest of them (0.009, at the\n near-white sidebar) and would tint nothing. See the knobs in tokens.css. */\n --accent: oklch(var(--accent-bg-l) min(var(--glass-tint-c), var(--accent-bg-c-max)) var(--glass-tint-h));\n /* The hover wash as an IMAGE layer (glass-hover-accent). Translucent so the material still reads\n through it — a hover that paints a flat fill stops looking like glass. */\n --glass-hover-wash: color-mix(in oklch, var(--accent) 70%, transparent);\n --secondary: oklch(var(--secondary-l) min(var(--glass-tint-c), var(--secondary-c-max)) var(--glass-tint-h));\n --muted: oklch(var(--muted-l) min(var(--glass-tint-c), var(--muted-c-max)) var(--glass-tint-h));\n --sidebar: oklch(var(--sidebar-l) min(var(--glass-tint-c), var(--sidebar-c-max)) var(--glass-tint-h));\n /* Theme-aware brand gradient — follows --glass-fg-h, from the hue 3 ramp-steps out\n (8/side → step 360/17 ≈ 63.53°) to the base hue, at the mode envelope --gradient-l/-c + 0.5 alpha.\n (Axis/direction variants are computed in JS from lib/oklch-utils.ts — see the /components demo.) */\n --gradient: linear-gradient(\n 135deg,\n oklch(var(--gradient-l) var(--gradient-c) calc(var(--glass-fg-h) + 63.53) / 0.5) 0%,\n oklch(var(--gradient-l) var(--gradient-c) var(--glass-fg-h) / 0.5) 100%\n );\n /* Full-opacity twin of --gradient for gradient TEXT (background-clip: text) — no wash alpha so glyphs stay crisp. */\n --gradient-text: linear-gradient(\n 135deg,\n oklch(var(--gradient-l) var(--gradient-c) calc(var(--glass-fg-h) + 63.53)) 0%,\n oklch(var(--gradient-l) var(--gradient-c) var(--glass-fg-h)) 100%\n );\n /* High-contrast gradient text for printing ON a tinted theme surface: DARK tint-hue gradient in light\n mode, LIGHT one in .dark (--gradient-contrast-l/-c), so legibility comes from lightness. */\n --gradient-text-contrast: linear-gradient(\n 135deg,\n oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) calc(var(--glass-fg-h) + 63.53)) 0%,\n oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) var(--glass-fg-h)) 100%\n );\n /* (The crystal floor, --glass-crystal-bg, is in block C: the hue gloss flavor repaints it.) */\n /* ── Chakra — the step-cut gem surface ────────────────────────────────────────────────────────\n BODY: the stone itself. The one tint-composing token here, so it is the only one with a gamut\n budget — min() against --glass-chakra-c-max (not a multiplier) for the same reason --border uses\n one: lapis declares c 0.15 and would be gamut-mapped, while any multiplier small enough to hold it\n drags the 0.07 jewels below where they should sit. Caps are measured per mode against the tightest\n ceiling across all 17 presets, so every preset delivers 100% of its declared chroma. */\n --glass-chakra-bg: oklch(\n calc(var(--glass-chakra-l) * 1%) min(var(--glass-tint-c), var(--glass-chakra-c-max)) var(--glass-tint-h) /\n var(--glass-chakra-a)\n );\n /* (The chakra SURFACE STACK, --glass-chakra-stack-bg, is in block C: it bakes the gloss.) */\n /* THE CUT — four sides, two steps, as INSET SHADOWS rather than inset background layers.\n A background layer is always a RECTANGLE: background-size shrinks it but gives it no radius of its\n own, so an inset gradient paints square corners over a rounded surface — a card with hard-cornered\n rectangles stacked on it. box-shadow has no such problem; an inset shadow is the border box's own\n rounded rect, offset, so every band follows --radius at whatever value the element carries, for\n free and at any size.\n Each side is a pair. The step-1 band (offset --glass-chakra-step) paints OVER the step-2 band\n (offset twice that), so the visible result is two concentric bands per side: 0→N shows step 1\n composited on step 2, N→2N shows step 2 alone. That is the step cut, and because step 2 is\n literally the same colour at half alpha it cannot drift out of sync with step 1.\n Order matters — box-shadow paints first-listed on top, so every step-1 band precedes its step-2\n twin. Corners: adjacent bands overlap and composite rather than mitering to a hard 45° line, which\n is the trade for following the radius. On a gem that reads as the corner facet catching both\n neighbours, and it is what the existing --glass-shadow bevel already does at top and bottom. */\n --glass-chakra-facets:\n inset 0 var(--glass-chakra-step) 0 oklch(var(--shadow-highlight) / var(--glass-chakra-facet-t-a)),\n inset var(--glass-chakra-step) 0 0 oklch(var(--shadow-highlight) / var(--glass-chakra-facet-l-a)),\n inset calc(var(--glass-chakra-step) * -1) 0 0 oklch(var(--shadow-ink) / var(--glass-chakra-facet-r-a)),\n inset 0 calc(var(--glass-chakra-step) * -1) 0 oklch(var(--shadow-ink) / var(--glass-chakra-facet-b-a)),\n inset 0 calc(var(--glass-chakra-step) * 2) 0 oklch(var(--shadow-highlight) / calc(var(--glass-chakra-facet-t-a) * 0.5)),\n inset calc(var(--glass-chakra-step) * 2) 0 0 oklch(var(--shadow-highlight) / calc(var(--glass-chakra-facet-l-a) * 0.5)),\n inset calc(var(--glass-chakra-step) * -2) 0 0 oklch(var(--shadow-ink) / calc(var(--glass-chakra-facet-r-a) * 0.5)),\n inset 0 calc(var(--glass-chakra-step) * -2) 0 oklch(var(--shadow-ink) / calc(var(--glass-chakra-facet-b-a) * 0.5));\n /* Edge + depth: the standard drop plus the cut. --glass-shadow's own top/bottom bevel is left out —\n the facet bands ARE the bevel, and doubling them would flatten the step read. */\n --glass-chakra-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-border-a));\n --glass-chakra-shadow: var(--glass-shadow), var(--glass-chakra-facets);\n --glass-chakra-shadow-hover: var(--glass-shadow-lg), var(--glass-chakra-facets);\n /* Precomposed background STACKS — the background-image compositions, single-sourced. The surface\n utilities consume them via --srf-* fallbacks (sheer utilities append the element-composed solidify\n layer underneath), and the [data-glass] style remaps below swap them wholesale. Live here so they\n re-resolve per tint scope and per mode wrapper. The crystal stack bakes the gloss into the\n background (the site-wide re-skin can't use ::before/::after — those need position:relative, which\n would break sticky/fixed surfaces). */\n --glass-wash-layer: linear-gradient(var(--glass-tint-wash), var(--glass-tint-wash));\n --glass-stack-bg: var(--glass-bg), var(--glass-wash-layer);\n /* Frosted rides the wash in the IMAGE stack (sheet + wash), leaving the background-color channel\n free for the veil floor. */\n --frosted-stack-bg: var(--glass-frosted-bg), var(--glass-wash-layer);\n /* (--crystal-stack-bg is in block C: it bakes the gloss.) */\n}\n\n/* C. GLOSS — the crystal highlight and everything that bakes it: the ink, the three gradients, the\n crystal floor, and the crystal + chakra stacks. Declared on every scope that can CHANGE it: :root and\n .dark (the mode knobs), [data-glass-tint] (the tint), and [data-gloss] (the flavor). The last is what\n lets `data-gloss` work below : these tokens are composed values, which bake where they are\n declared, so a [data-gloss] section that only re-pinned an input (as the flavor blocks used to) had\n its children inherit 's gradients and stacks pre-composed, and painted the page's gloss.\n THREE FLAVORS, chosen by two plain switches the [data-gloss] blocks in materials.css set to `1` or\n `initial` (unset = tonal):\n white → flat white specular (the classic glass shine) --glass-gloss-is-white: 1\n tonal → a tonally-close single-hue tint of the theme (--glass-gloss-l / --glass-gloss-tint)\n hue → iridescent: the highlight sweeps hues shifted ± --glass-gloss-hue-span around the tint\n hue, and repaints the crystal floor in the sweep --glass-gloss-is-hue: 1\n A switch selects nothing by itself: each flavor's variant (block C′ below) multiplies an alpha by it,\n so with the switch `initial` the variant is invalid at computed-value time and the var() fallback here,\n the tonal shape, applies. The switches are leaves, so they inherit, and a nested [data-gloss] sets both\n to pick its own flavor; a nested [data-glass-tint] recomposes from the inherited switches and keeps the\n page's flavor (it used to reset it to tonal). Each layer applies its own alpha. */\n:root,\n.dark,\n[data-glass-tint],\n[data-gloss] {\n /* The tonal ink as its own token (not inline in the fallback below) so check-gamut's scraper, which\n reads `--x: ` declarations, still sweeps it. */\n --glass-gloss-ink-tonal: calc(var(--glass-gloss-l, 94) * 1%)\n min(calc(var(--glass-tint-c) * var(--glass-gloss-tint, 2)), var(--glass-gloss-c-max, 0.3)) var(--glass-tint-h);\n --glass-gloss-ink: var(--glass-gloss-ink-white, var(--glass-gloss-ink-tonal));\n --glass-gloss-top: var(--glass-gloss-top-hue, linear-gradient(to bottom, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.4), transparent 30%));\n --glass-gloss-streak: var(\n --glass-gloss-streak-hue,\n linear-gradient(calc(135deg * var(--glass-gloss-hue-dir, 1)), oklch(var(--glass-gloss-ink, 100% 0 0) / 0.15), transparent 50%)\n );\n --glass-gloss-glow: var(\n --glass-gloss-glow-hue,\n radial-gradient(circle at 50% 120%, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.2), transparent 70%)\n );\n /* Crystal floor — clear glass; the alpha is the mode dial (--glass-crystal-bg-a).\n Crystal body. The lightness is a KNOB, not 100%, and that is the whole point: at L100 sRGB holds\n ZERO chroma, so this surface asked for 0.015 and delivered exactly none — crystal rendered pure\n white under every preset, in both modes (measured: #fffdff amethyst, #fffef4 moonstone,\n #fbffff lapis; RGB spread 2–11). check-gamut even reported it as \"no attainable chroma at all\"\n and skipped the pair rather than failing it.\n --glass-crystal-l is 96 because that is the lowest lightness at which the TIGHTEST of the 21 hues\n this theme uses (h268, lapis) can still carry the 0.015 this asks for: ceiling 0.0182, 1.22x\n headroom. L96.5 is the exact crossover at 1.06x, too thin given maxChromaFor already sits 3% inside\n the boundary to stop Safari clamping toward grey. The cost is small — at the 0.3 light-mode alpha\n the composite moves ~1.2 L, and 0.4 L in dark. */\n --glass-crystal-bg: var(\n --glass-crystal-bg-hue,\n oklch(calc(var(--glass-crystal-l) * 1%) calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / var(--glass-crystal-bg-a))\n );\n /* SURFACE STACKS: crystal and chakra bake the same gloss triple, so `data-gloss` white / tonal / hue\n reskin both, over a fresco slot and the tint wash. Nothing in the chakra stack is inset, so nothing\n has a corner of its own — the element's border-radius clips the whole stack (see the facet bands in\n block B). Baked into the background, not ::before/::after: the crystal PAGE style re-skins every\n surface, and pseudo layers would need position:relative, which breaks sticky/fixed surfaces. */\n --crystal-stack-bg:\n var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer);\n --glass-chakra-stack-bg:\n var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer);\n}\n\n/* C′. The FLAVOR VARIANTS block C falls back from, each valid only while its switch is `1`. Kept in a\n sibling block (same scopes) so scripts/check-gamut.mjs can model the hue variants at the hue flavor's\n own L74 (the [data-gloss=\"hue\"] block pins --glass-gloss-l there) without misreading block C's tonal\n ink at it.\n hue: each highlight sweeps (tint hue − span) → tint hue → (+ span), interpolated IN OKLCH so the arc\n passes through the base hue. Top leans to the low end, glow to the high end, streak spans both. The\n floor is replaced by a hue-carrying one at the gloss lightness (so Light deepens it): the default\n crystal floor sits near white, where chroma collapses, so the sweep would read over a white sheet. */\n:root,\n.dark,\n[data-glass-tint],\n[data-gloss] {\n --glass-gloss-ink-white: calc(var(--glass-gloss-is-white) * 100%) 0 0;\n --glass-gloss-top-hue: linear-gradient(\n to bottom in oklch,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.4 * var(--glass-gloss-is-hue))\n ),\n transparent 35%\n );\n --glass-gloss-streak-hue: linear-gradient(\n 115deg in oklch,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.32 * var(--glass-gloss-is-hue))\n )\n 0%,\n oklch(calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) var(--glass-tint-h) / calc(0.12 * var(--glass-gloss-is-hue))) 45%,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.32 * var(--glass-gloss-is-hue))\n )\n 85%,\n transparent\n );\n --glass-gloss-glow-hue: radial-gradient(\n circle at 50% 120%,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.25 * var(--glass-gloss-is-hue))\n ),\n transparent 70%\n );\n --glass-crystal-bg-hue: oklch(\n calc(var(--glass-gloss-l, 94) * 1%) calc(var(--glass-tint-c) * 1.2) var(--glass-tint-h) /\n calc(0.4 * var(--glass-gloss-is-hue))\n );\n}\n/* ── Glass surface utilities (@utility — tree-shaken per consumer usage) ──────\n The material system: ONE structural `glass` utility + orthogonal axis classes (border, veil, sm/lg,\n gradient, glow, sheen). Materials live in materials.css as [data-material] token sets; page styles\n in styles.css remap the same --srf-* tokens. No recipe utilities, no !important. */\n\n/* The glow itself — a punchy two-layer colored halo, centralized so its strength is tunable in ONE\n place. `glass-glow` is the resting glow; `glass-glow-lg` (used on hover) intensifies it. Both set\n --glow-layer, which every glass box-shadow folds in, so the glow shows under EVERY glass style.\n Full mechanism + tuning + troubleshooting: docs/glow.md */\n@utility glass-glow {\n --glow-layer: 0 0 8px 1px var(--glass-glow), 0 0 18px 3px var(--glass-glow);\n}\n@utility glass-glow-lg {\n --glow-layer: 0 0 12px 2px var(--glass-glow), 0 0 28px 5px var(--glass-glow);\n}\n\n/* ── THE MATERIAL SYSTEM (Sistine 2.0) ────────────────────────────────────────\n ONE structural utility (`glass`) + orthogonal axis classes. The material (a token set) comes from\n [data-material] (materials.css) or the inherited [data-glass] page remap (styles.css); with\n neither, the --srf-* fallbacks render the default glass material. Axis utilities only SET custom\n props (or properties `glass` never touches), so Tailwind's emission order can never flip a result.\n (The legacy recipe utilities below coexist during the migration and die with it.) */\n\n/* Structural glass — background/filter/shadow plumbing only; borderless (add glass-border).\n Channels, outermost first:\n background-image = [--srf-hover state slot] + [--acc-gradient accent slot] + [--srf-bg-image material stack] + [solidify]\n background-color = --veil-floor (element veil) > --srf-bg-color (material floor) > transparent\n backdrop-filter = --srf-filter (material) > blur(--srf-blur > --blur) saturate(180%)\n box-shadow = --srf-shadow (material) > --srf-elev (size axis) > --glass-shadow, + glow\n The leading gradient slot is a no-op layer so `glass-gradient` composes over ANY material (same\n pattern as the fresco slot inside --crystal-stack-bg). Both no-op slots resolve to `none` rather\n than linear-gradient(transparent, transparent): `none` is a valid , holds the layer\n position identically, and paints nothing — where a transparent GRADIENT still goes through the\n gradient rasteriser (and its dither) to deliver zero pixels.\n The solidify floor (--glass-opacity dial) is composed HERE, at the element — never in a token\n context (the var-composition-resolves-at-declaration gotcha; see scripts/check-theme.mjs). The\n --srf-solidify slot lets a material DROP that layer when it is provably a no-op (the opaque material,\n and the opaque PAGE style, which sets it on its own scope so it reaches `@apply glass` surfaces that\n carry no `.glass` class); materials that want the normal floor pin the slot back to `initial`\n (guaranteed-invalid → the var() fallback composes --glass-solidify at the element). --veil-solidify\n leads the chain: glass-veil sets it (non-inheriting), because a VEILED surface paints the neutral\n veil floor in its background-colour channel, so the layer is real there whatever the slot says. */\n@utility glass {\n /* Fallback 0.7, not 0: every glass surface now sits on a 70% solidify floor unless a consumer dials\n it back. Measured before changing it, because AutoForeground does NOT read --glass-opacity — it\n bands text against the sheer floor, so a more solid surface than it modelled could have cost\n contrast. Body text goes 91.5 -> 86.5 Lc in light and 88.0 -> 85.8 in dark, both far above the\n 75 body floor (it still clears at a full 1.0). Set --glass-opacity: 0 for the old sheer default. */\n /* --glass-solidify-BG, not --glass-opaque-bg: the backing under sheer glass is its own surface now\n (engine.css / tokens.css). In dark the two resolve identically, so this is a light-mode change. */\n --glass-solidify-fill: color-mix(in oklch, var(--glass-solidify-bg, var(--glass-opaque-bg)) calc(var(--glass-opacity, 0.7) * 100%), transparent);\n --glass-solidify: linear-gradient(var(--glass-solidify-fill), var(--glass-solidify-fill));\n /* --srf-hover leads the stack because a hover/selection wash has to sit ON the material, not under it.\n `hover:bg-accent` sets background-COLOR, which is the bottom of the box — beneath all 14 image layers\n the glass stack paints, of which the solidify floor alone is 70% opaque. At most ~30% of the accent\n survived to the eye, so the hover read as the material's own colour (white in light mode) no matter\n what --accent was set to. No amount of tuning the token could fix that; it is a layer-order problem.\n Same no-op `none` slot pattern as --acc-gradient below it. */\n background-image:\n var(--srf-hover, none), var(--acc-gradient, none), var(--srf-bg-image, var(--glass-stack-bg)),\n var(--veil-solidify, var(--srf-solidify, var(--glass-solidify)));\n background-color: var(--veil-floor, var(--srf-bg-color, transparent));\n /* Author ONLY the standard backdrop-filter (no -webkit- twin: Lightning dedupes the pair keeping\n the PREFIXED one, which Chromium never aliased — the [no-webkit-twin] invariant guards this).\n The max() composes the diffuse FLOOR for adaptive surfaces. */\n backdrop-filter: var(--srf-filter, blur(max(var(--srf-blur, var(--blur)), var(--dif, 0px))) saturate(180%)) var(--srf-stain,);\n box-shadow: var(--srf-shadow, var(--srf-elev, var(--glass-shadow))), var(--glow-layer, 0 0 0 0 transparent);\n}\n\n/* Border axis — the material picks the color unless the element asks for ink (--brd-ink, the\n borderColor axis below); width resolves material pin (--srf-border-w, which itself reads the --brd-w\n weight slot) → weight slot directly (adaptive surfaces) → 1px hairline. */\n@utility glass-border {\n border: var(--srf-border-w, var(--brd-w, 1px)) solid var(--brd-ink, var(--srf-border-color, var(--glass-border)));\n}\n/* Border WEIGHTS — token setters for the --brd-w slot (order-safe; same-element composition, like\n glass-sm's --srf-blur). Hairline is the unmarked default (1px; 0.5px under frosted); rim = 2px, frame = 4px. */\n@utility glass-border-rim {\n --brd-w: 2px;\n}\n@utility glass-border-frame {\n --brd-w: 4px;\n}\n/* Border COLOUR — ink. The material edge is a near-white rim at the material's alpha: it reads as cut\n glass on a dark page and as nothing at all on most light themes, at hairline, rim or frame weight\n alike, so a weight axis whose colour disappears went unused. Ink is the element's own text colour\n (--foreground, after any per-material text remap on this element), mixed toward transparent by\n --glass-border-ink-a: a visible line anywhere text is legible. Element-composed and registered\n non-inheriting (registrations.css), so a child border keeps its own material colour. */\n@utility glass-border-ink {\n --brd-ink: color-mix(in oklch, var(--foreground) calc(var(--glass-border-ink-a, 0.5) * 100%), transparent);\n}\n\n/* Diffuse axis — a READABILITY BLUR FLOOR (not a blur value): every translucent material computes\n blur(max(, --dif)), so crystal stays crystal until a text-dense surface opts in.\n Token-setter (order-safe); OPAQUE never reads the slot (--srf-filter: none), so the short-circuit\n is inherited — no GPU spend on solid floors. Tune the floor via --glass-diffuse. */\n@utility glass-diffuse {\n --dif: var(--glass-diffuse, 12px);\n}\n\n/* Veil axis — the legibility floor for read-through overlays (menus, tooltips, toasts).\n Composed AT THE ELEMENT so a scoped --glass-solid-a resolves here (check-theme invariant 2b).\n The veil rides the background-COLOR channel, under the material's image stack, and — being\n element-declared — beats any inherited --srf-bg-color remap. */\n/* Stained axis — REAL stained-glass optics: grayscale collapses the backdrop to pure luminance\n (lighter/darker only, akin to alpha), so the glass's OWN tint supplies every drop of color and\n whatever sits behind renders as tonal shades of the theme. One color-matrix op — free next to the\n blur. --glass-stain-amount (0..1) dials how completely the backdrop's color is stripped. */\n@utility glass-stained {\n --srf-stain: var(--glass-stain);\n}\n\n@utility glass-veil {\n --veil-floor: oklch(var(--glass-solid-l) 0 0 / var(--glass-solid-a));\n /* The veil keeps the solidify layer even where the material or page dropped it (--srf-solidify: none):\n those drops are only no-ops over the opaque floor, and a veiled surface paints the veil instead. */\n --veil-solidify: var(--glass-solidify);\n}\n\n/* THE PAGE BACKDROP LAYER — the fixed, full-viewport ground every sheer material blurs. Put this on the\n `fixed inset-0 -z-10` container each background component renders.\n It exists to force a compositing layer. `backdrop-filter` re-samples everything painted beneath it,\n and this layer sits at z-index -10 under the entire document; unpromoted, the compositor has to\n re-rasterise it against the scroll position every frame, which is where the scroll artifact on the\n sheer materials comes from. `opaque` is the ONE material that sets --srf-filter: none — and the one\n material that never showed the artifact, which is the correlation that points here.\n Measured on the deployed page, this element resolved will-change:auto / transform:none /\n contain:none — nothing was promoting it. Promoting is safe specifically because it has ZERO\n position:fixed descendants and no background-attachment:fixed; a transform/will-change would\n otherwise become their containing block and reparent them. Re-check that before adding children.\n will-change (rather than a translateZ hack) because the hint is honest here: this layer is on screen\n and being sampled for the whole session, so there is nothing to hint OFF later. */\n@utility glass-backdrop-layer {\n will-change: transform;\n}\n\n/* Size axes — blur + elevation tiers. These feed the GLASS material only: frosted/crystal/opaque\n (and page remaps) set --srf-filter/--srf-shadow wholesale, which flattens the tiers — matching\n the old system, where the [data-glass] styles flattened glass-surface-sm/-lg. */\n@utility glass-sm {\n --srf-blur: var(--blur-sm);\n --srf-elev: var(--glass-shadow-sm);\n}\n@utility glass-lg {\n --srf-blur: var(--blur-lg);\n --srf-elev: var(--glass-shadow-lg);\n}\n@utility glass-xl {\n --srf-blur: var(--blur-xl);\n --srf-elev: var(--glass-shadow-lg);\n}\n\n/* ── Gradient ACCENT — a brand wash ON a material, not a material. Fills the no-op --acc-gradient\n slot at the top of every `glass` background, so it composes over glass, frosted, crystal, and\n opaque alike. A token-setter (no property `glass` sets), so it's immune to utility emission order. */\n@utility glass-gradient {\n --acc-gradient: var(--gradient);\n}\n\n/* ── Hover/selection WASH — the interactive twin of glass-gradient. Fills the --srf-hover slot at the\n very top of the background stack, which is the only place a state colour is actually visible on a\n glass surface: `hover:bg-accent` writes background-COLOR, and every one of the material's image\n layers paints over it (solidify alone is 70% opaque), so the accent arrived at ~30% strength blended\n into the material and read as plain white on a light theme.\n The wash is deliberately translucent rather than a flat fill — glass that turns solid on hover stops\n looking like glass. 70% of --accent over the material keeps the wash, gloss and blur reading through\n it while still shifting the surface unmistakably toward the theme colour.\n Pair with `hover:bg-accent` rather than replacing it: the background-color still does the work on the\n NON-glass variants (outline/default paint no material, so nothing covers it there). */\n@utility glass-hover-accent {\n &:hover {\n --srf-hover: linear-gradient(var(--glass-hover-wash), var(--glass-hover-wash));\n }\n}\n\n/* Sheen — the OPT-IN hover shimmer (the gloss slides across on hover). Self-contained (highlight + diagonal\n + glow + the 200%/hover shift), so it works on any glass surface AND, layered on glass-crystal (which\n already has the static gloss), its ::before/::after override crystal's to add the motion. Pseudo-element\n based → position:relative; avoid on sticky/fixed surfaces. */\n@utility glass-sheen {\n position: relative;\n\n &::before,\n &::after {\n content: \"\";\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n }\n\n &::before {\n background: var(--glass-gloss-top), var(--glass-gloss-streak);\n background-size: 200% 200%;\n background-position: 0% 0%;\n opacity: 0.7;\n transition: background-position 0.3s ease;\n }\n\n &::after {\n background: var(--glass-gloss-glow);\n opacity: 0.8;\n }\n\n &:hover::before {\n background-position: 100% 100%;\n }\n}\n\n/* Namespaced: a bare `shimmer` is a global name, and a consumer's own @keyframes shimmer (or another\n library's) replaced this one for everyone, whichever stylesheet loaded last. Animates `translate`, not\n `transform`: Tailwind v4's `-translate-x-full` (the skeleton's resting position) writes the `translate`\n property, which COMPOSES with an animated transform rather than being replaced by it, so the sweep ran\n from -200% to 0% and parked the highlight on top of the skeleton. Animating the same property replaces\n the resting value while running, and with the animation off it falls back to that resting value. */\n@keyframes sistine-shimmer {\n from {\n translate: -100% 0;\n }\n to {\n translate: 100% 0;\n }\n}\n\n/* ── Reduced motion ───────────────────────────────────────────────────────────\n CONTRIBUTING says \"Respect prefers-reduced-motion\"; the pattern scenes did and nothing else did.\n This closes that, but NOT with the blunt global `*, ::before, ::after { animation: none !important }`\n reset — that would also flatten the pattern scenes' own carefully scoped rules and every enter/exit\n transition in the library.\n\n The treatment differs by what the animation actually IS, because the preference is about MOTION, not\n about animation in general:\n - shimmer → translation across a surface. Real motion, purely decorative: stopped. The skeleton\n still reads as a skeleton from its base fill.\n - spin → rotation. Real motion, but it is the ONLY signal that a loader is alive, so freezing\n it reads as \"hung\" rather than \"calm\". Slowed hard instead of stopped.\n - pulse → opacity only. Nothing moves, so there is nothing to trigger a vestibular response.\n Left alone deliberately; it also carries meaning (the input-otp caret is a blink).\n Unlayered on purpose: Tailwind's generated utilities sit inside @layer utilities, and an unlayered\n rule outranks any layered one regardless of order, so this needs neither !important nor a specificity\n hack. */\n@media (prefers-reduced-motion: reduce) {\n .animate-\\[sistine-shimmer_2s_infinite\\],\n .before\\:animate-\\[sistine-shimmer_2s_infinite\\]::before {\n animation: none;\n }\n\n .animate-spin {\n animation-duration: 3s;\n }\n}\n/* ── Materials — [data-material] token sets + gloss flavors ───────────────────\n Four materials, one attribute. Each block declares the FULL --srf-* pin set ON the element, so an\n explicit material always beats an inherited [data-glass] page remap (element-declared custom props\n beat inherited ones). One material per element is structural: attributes can't stack, so there is\n no !important anywhere in the system.\n NOTE these are inherited custom properties — a data-material element also acts as a SCOPED default\n for adaptive `glass` descendants (the mechanism DialogContent's old data-glass hack hand-rolled).\n Pin a child back with its own data-material.\n Import order: after tokens.css — [data-gloss=\"hue\"] re-pins --glass-gloss-l and the crystal shadows,\n which tokens.css declares on :root/.dark at equal specificity, so source order decides on . */\n\n[data-material=\"glass\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--glass-stack-bg);\n --srf-bg-color: transparent;\n /* Composed on the element, so the glass-sm/glass-lg axes still resolve here (same-element\n setter/reader is safe — the composition gotcha only bites pre-composed inherited values). */\n --srf-filter: blur(max(var(--srf-blur, var(--blur)), var(--dif, 0px))) saturate(180%);\n --srf-border-color: var(--glass-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--srf-elev, var(--glass-shadow));\n}\n\n/* Frosted — the tint wash rides the IMAGE stack (--frosted-stack-bg = sheet + wash), leaving the\n background-color channel free for the veil floor. */\n[data-material=\"frosted\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--frosted-stack-bg);\n --srf-bg-color: transparent;\n --srf-filter: blur(max(var(--blur-frosted), var(--dif, 0px))) saturate(200%);\n --srf-border-color: var(--glass-frosted-border);\n --srf-border-w: var(--brd-w, 0.5px);\n --srf-shadow: var(--glass-frosted-shadow);\n}\n\n/* Crystal — gloss BAKED into the background stack (sticky/fixed-safe; no pseudo-elements, no\n position:relative). --crystal-stack-bg carries the --glass-crystal-fresco pass-through. The hover\n sparkle is a plain token swap; box-shadow transitions on the real property. No saturate(),\n matching the old variant. */\n[data-material=\"crystal\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--crystal-stack-bg);\n --srf-bg-color: var(--glass-crystal-bg);\n --srf-filter: blur(max(var(--blur-crystal), var(--dif, 0px)));\n --srf-border-color: var(--glass-crystal-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--glass-crystal-shadow);\n transition: box-shadow 0.3s ease;\n}\n[data-material=\"crystal\"]:hover {\n --srf-shadow: var(--glass-crystal-shadow-hover);\n}\n/* Crystal text tiers — the baked gloss locally LIGHTENS the title zone (worst in dark mode), so text\n inside crystal remaps to the -crystal set AutoForeground bands against the glossed crystal surface.\n VEILED crystal is excluded: the veil floor is exactly what the normal tiers are banded for. */\n[data-material=\"crystal\"]:not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-crystal);\n --foreground-soft: var(--foreground-soft-crystal);\n --foreground-strong: var(--foreground-strong-crystal);\n --foreground-ui: var(--foreground-ui-crystal);\n --muted-foreground: var(--muted-foreground-crystal);\n}\n\n/* Chakra — the step-cut gem: a translucent tinted body with a two-step facet frame on all four edges,\n sitting between crystal (near-clear) and opaque (solid). Like crystal it bakes everything into the\n background stack and the shadow — no pseudo-elements, so it is safe on sticky/fixed surfaces, and it\n carries the same gloss triple, so `data-gloss` white / tonal / hue reskin it too.\n The cut lives entirely in --srf-shadow (see --glass-chakra-facets in engine.css). Nothing here is an\n inset BACKGROUND layer, deliberately: background layers are rectangles with no radius of their own,\n so an inset gradient stacks square corners on a rounded surface. Inset shadows follow --radius.\n Because the stack is plain full-bleed layers, the sheer-glass axes work untouched — glass-diffuse\n raises --dif into the blur below, and the --glass-opacity solidify floor composes under the stack\n exactly as it does for crystal. */\n[data-material=\"chakra\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--glass-chakra-stack-bg);\n --srf-bg-color: var(--glass-chakra-bg);\n --srf-filter: blur(max(var(--blur-chakra), var(--dif, 0px)));\n --srf-border-color: var(--glass-chakra-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--glass-chakra-shadow);\n transition: box-shadow 0.3s ease;\n}\n[data-material=\"chakra\"]:hover {\n --srf-shadow: var(--glass-chakra-shadow-hover);\n}\n/* Chakra text tiers — content sits on the body, whose lightness (--glass-chakra-l) is its own dial and\n can sit well away from the page's. Same reasoning as opaque/crystal; veiled chakra is excluded\n because the veil floor is exactly what the NORMAL tiers are banded for. */\n[data-material=\"chakra\"]:not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-chakra);\n --foreground-soft: var(--foreground-soft-chakra);\n --foreground-strong: var(--foreground-strong-chakra);\n --foreground-ui: var(--foreground-ui-chakra);\n --muted-foreground: var(--muted-foreground-chakra);\n}\n\n/* Opaque — solid tinted floor, nothing to blur through. --glass-opaque-outline stays settable on any\n ancestor: it's composed HERE, on the element, so it resolves where it's read. The floor can be\n LIGHT while the page is dark, so the foreground tiers remap to the -opaque set AutoForeground\n bands against that floor (inherits into the card's content). */\n[data-material=\"opaque\"] {\n /* DROP the solidify layer: it is `--glass-opaque-bg` at --glass-opacity alpha painted directly over a\n background-COLOR of the same --glass-opaque-bg at alpha 1 — i.e. C over C, which composites to C for\n ANY alpha. Measured across tints and both modes it moves every channel by exactly 0. So the layer was\n one full gradient rasterisation (and one dither pass) for provably zero pixels, on the one material\n with no backdrop-filter behind it to mask that noise. Restored when the surface is VEILED (glass-veil\n sets --veil-solidify, which `glass` reads first) — there the background-color is the neutral veil\n floor, not --glass-opaque-bg, so the layer is real (dropping it there would shift up to 31/255 in\n light and 42/255 in dark). */\n --srf-solidify: none;\n /* NO sheet either — opaque now paints a FLAT background-color and nothing else. --glass-bg is a 5-stop\n gradient whose total range, composited over this floor, is 2–4 sRGB codes across an entire card in\n light and 5–7 in dark: one code per ~100–200px, far too shallow to draw cleanly in 8-bit, so the\n rasteriser dithers rather than bands. On every other material a real backdrop-filter supplies blurred\n page content that masks that noise; opaque sets --srf-filter: none, so there is nothing behind it and\n the dither is the only variation on the card — read as grain, worst over text.\n The sheet's GRADIENT was therefore invisible, but its uniform LIFT was not (+1.7 L light, +4.8 L dark),\n so it is folded into --glass-opaque-l rather than dropped: see tokens.css. AutoForeground reads that\n same token, so the -opaque text tiers re-band against the new floor automatically. */\n --srf-bg-image: none;\n --srf-bg-color: var(--glass-opaque-bg);\n --srf-filter: none;\n --srf-border-color: var(--glass-opaque-outline, var(--glass-border));\n /* Outline WEIGHT: an element rim/frame class (--brd-w) beats the page-level outline weight (the\n switcher's --glass-opaque-outline-w on ), which beats the 1px hairline. */\n --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px));\n --srf-shadow: var(--glass-shadow);\n /* Veiled overlays INSIDE an opaque element compute a fully solid floor — same guarantee the opaque\n PAGE style makes (menus/toasts must stay legible on a solid ground). */\n --glass-solid-a: 1;\n --foreground: var(--foreground-opaque);\n --foreground-soft: var(--foreground-soft-opaque);\n --foreground-strong: var(--foreground-strong-opaque);\n --foreground-ui: var(--foreground-ui-opaque);\n --muted-foreground: var(--muted-foreground-opaque);\n}\n\n/* ── Gloss flavors ────────────────────────────────────────────────────────────\n data-gloss on or ANY element swaps the crystal (and chakra) gloss for that subtree. Default\n (unset) = tonal. These blocks set LEAVES only — the two flavor switches, plus the hue flavor's own\n lightness and shadows — and engine.css block C recomposes the gloss on every [data-gloss] element from\n them. That split is what makes a scoped `
    ` work: a composed value bakes\n where it is declared, so a block here that declared one would hand its children 's gloss.\n Each block sets BOTH switches, so a nested flavor overrides an ancestor's instead of combining with it.\n They compose with [data-material=\"crystal\"] AND the [data-glass=\"crystal\"] page style, in both modes.\n Known edge: hue's pinned --glass-gloss-l and bevel-less crystal shadows are mode-twinned leaves that a\n white/tonal section nested INSIDE a hue scope inherits (no mode-aware reset exists for them here). */\n[data-gloss=\"white\"] {\n --glass-gloss-is-white: 1; /* the ink becomes the flat white specular, 100% 0 0 */\n --glass-gloss-is-hue: initial;\n}\n[data-gloss=\"tonal\"] {\n /* Explicit tonal (also the default) — both switches off, so it can override a white/hue ancestor. */\n --glass-gloss-is-white: initial;\n --glass-gloss-is-hue: initial;\n}\n[data-gloss=\"hue\"] {\n --glass-gloss-is-white: initial;\n --glass-gloss-is-hue: 1;\n /* Hue OWNS its lightness rather than sharing the tonal twin. The two flavors want opposite things\n from the same number: tonal wants the brightest value the surface allows (97 in light) so it reads\n as a specular, while hue needs chroma headroom — its stops ask --glass-gloss-c 0.16, and the sRGB\n ceiling at L 97 is 0.0136, so the iridescent sweep would deliver 8% of its colour and wash out to\n near-white exactly where it is supposed to be most vivid.\n 74 is mode-invariant on purpose: it holds 81% of that chroma, and it lands on the right side of\n both surfaces anyway (+18.4 L over the dark floor, −9.0 over the light one — for an iridescent\n sweep, being SEEN matters more than which direction it moves). The demo's flavor switcher already\n seeded 74 at runtime for this reason; this makes a no-JS consumer get it too.\n \"Holds 81% of that chroma\" assumed the browser gamut-MAPS an over-ceiling request down to the\n ceiling. Measured in WebKit it CLIPS instead (see --glass-gloss-c-max in tokens.css), so 0.16 did\n not deliver 81% of itself — it drove the tightest hues to a pinned channel. 0.12 sits just under\n 0.1240, the L-74 ceiling for turquoise (h190) and the tightest of the presets, so the sweep now\n renders every hue it asks for. scripts/check-gamut.mjs reads this 74 to score the variants. */\n --glass-gloss-l: 74;\n /* Kill the white bevel: the crystal shadow carries a white inset highlight, which would sit on top of\n the swept hue. (The white floor is replaced in engine.css block C′.) */\n --glass-crystal-shadow: var(--glass-shadow);\n --glass-crystal-shadow-hover: var(--glass-shadow-lg);\n}\n/* ── Site-wide style remaps + vendor cascade fixes ──────────────────────────── */\n\n/* Keep react-day-picker from painting over the glass background */\n@layer utilities {\n .glass[class*=\"rdp\"],\n .glass .rdp,\n .glass .rdp-month {\n background-color: transparent !important;\n }\n}\n\n/* ── Site-wide glass style: token remaps ─────────────────────────────────────\n data-glass on (frosted | crystal | opaque) re-skins every ADAPTIVE surface (`glass` with no\n data-material) by remapping the --srf-* tokens it reads. An element-level [data-material] pins its\n own tokens and beats these by cascade physics — reinforced by the :where() wrapper, which drops\n every remap (incl. its cross-product legs) to specificity (0,0,0), so a co-located data-material on\n a tint-scope element always wins. The cross-product legs keep the swapped stacks re-resolving inside\n scoped tints, status toasts included (a bare [data-glass] rule would bake 's tint into\n descendants' surfaces). Veiled surfaces (glass-veil) keep their element-composed floor.\n The ELEMENT-level rules further down (text tiers, the opaque edge) need to know which elements are\n surfaces. `.glass` says so for the utility class; an element that wears the material through\n `@apply glass` has no class, so it opts in with a `data-surface` attribute (`data-surface=\"veil\"`\n when it also applies glass-veil). Everything ABOVE is inherited tokens and needs no marker. */\n:where([data-glass=\"frosted\"], [data-glass=\"frosted\"] [data-glass-tint], [data-glass=\"frosted\"][data-glass-tint]) {\n --srf-bg-image: var(--frosted-stack-bg);\n /* NO --srf-bg-color wash here: the wash already rides the IMAGE stack (--frosted-stack-bg), same\n as the frosted material — a color-channel copy double-washed tinted presets and stole the veil\n floor's channel. */\n --srf-filter: blur(max(var(--blur-frosted), var(--dif, 0px))) saturate(200%);\n --srf-border-color: var(--glass-frosted-border);\n --srf-border-w: var(--brd-w, 0.5px);\n --srf-shadow: var(--glass-frosted-shadow);\n}\n:where([data-glass=\"crystal\"], [data-glass=\"crystal\"] [data-glass-tint], [data-glass=\"crystal\"][data-glass-tint]) {\n /* Gloss rides inside --crystal-stack-bg (baked — sticky/fixed-safe). Shimmer stays the opt-in\n glass-sheen. No saturate(), matching the material. */\n --srf-bg-image: var(--crystal-stack-bg);\n --srf-bg-color: var(--glass-crystal-bg);\n --srf-filter: blur(max(var(--blur-crystal), var(--dif, 0px)));\n --srf-border-color: var(--glass-crystal-border);\n --srf-shadow: var(--glass-crystal-shadow);\n}\n/* Crystal page: ADAPTIVE glass (no explicit material, not veiled) gets the -crystal text tiers, same\n as the material remap in materials.css — the baked gloss lightens title zones there too. */\n[data-glass=\"crystal\"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-crystal);\n --foreground-soft: var(--foreground-soft-crystal);\n --foreground-strong: var(--foreground-strong-crystal);\n --foreground-ui: var(--foreground-ui-crystal);\n --muted-foreground: var(--muted-foreground-crystal);\n}\n/* Chakra page — every adaptive surface becomes a step-cut gem. The cut rides --srf-shadow, so it\n follows each surface's own border-radius with nothing to keep in sync. */\n:where([data-glass=\"chakra\"], [data-glass=\"chakra\"] [data-glass-tint], [data-glass=\"chakra\"][data-glass-tint]) {\n --srf-bg-image: var(--glass-chakra-stack-bg);\n --srf-bg-color: var(--glass-chakra-bg);\n --srf-filter: blur(max(var(--blur-chakra), var(--dif, 0px)));\n --srf-border-color: var(--glass-chakra-border);\n --srf-shadow: var(--glass-chakra-shadow);\n}\n/* Chakra page: adaptive glass (no explicit material, not veiled) takes the -chakra text tiers, same\n as the material remap in materials.css. */\n[data-glass=\"chakra\"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-chakra);\n --foreground-soft: var(--foreground-soft-chakra);\n --foreground-strong: var(--foreground-strong-chakra);\n --foreground-ui: var(--foreground-ui-chakra);\n --muted-foreground: var(--muted-foreground-chakra);\n}\n/* Opaque — solid tinted panels for adaptive surfaces. --glass-solid-a: 1 makes any veiled overlay\n (glass-veil) compute a FULLY solid floor here too, so menus/toasts stay legible on an opaque page. */\n:where([data-glass=\"opaque\"], [data-glass=\"opaque\"] [data-glass-tint], [data-glass=\"opaque\"][data-glass-tint]) {\n /* Flat floor, no sheet — same reasoning as [data-material=\"opaque\"] in materials.css: with\n --srf-filter: none there is no blurred backdrop to mask the dither of a 2–7 code gradient, and the\n sheet's uniform lift is folded into --glass-opaque-l instead. */\n --srf-bg-image: none;\n --srf-bg-color: var(--glass-opaque-bg);\n --srf-filter: none;\n --srf-shadow: var(--glass-shadow);\n /* Same provable no-op as [data-material=\"opaque\"]: --glass-opaque-bg sits in the background-COLOR\n channel, so the solidify layer would paint that colour over itself. A TOKEN here (it used to be an\n element rule keyed on `.glass`), so it reaches an `@apply glass` surface too, which kept the light\n solidify backing at 70% over the opaque floor and read nearly white. An explicit material pins the\n slot back (`initial`), and a veiled surface restores the layer with --veil-solidify. */\n --srf-solidify: none;\n}\n/* --glass-solid-a CANNOT ride in the :where() block above. :where() is specificity ZERO by design (so a\n page style never beats an explicit material), and every other declaration up there survives that\n because nothing else declares them — they are material SLOTS with no competing value. This one is\n different: tokens.css declares --glass-solid-a on :root, which is (0,1,0), so the :where() version\n lost every time and the opaque page never actually got its solid floor. Measured: --glass-solid-a\n resolved to 0.65 on under [data-glass=\"opaque\"], identical to every other page style.\n The visible symptom is any VEILED surface on an opaque page staying translucent while the solid\n panels around it go opaque — menus, toasts and popovers, which is exactly what the block above says\n this is for. A bare attribute selector is (0,1,0), which ties :root and wins on source order because\n globals.css imports styles.css after tokens.css. It is a plain literal, so it simply inherits into\n the tint / toast scopes and needs none of their re-resolution. */\n[data-glass=\"opaque\"] {\n --glass-solid-a: 1;\n}\n/* Opaque page: element-level touches for ADAPTIVE surfaces only (a `.glass` or `[data-surface]` element\n with no explicit material — an explicit frosted/crystal card keeps its translucency AND its normal\n foreground). Declared ON the element because (a) --glass-opaque-outline is settable on any ancestor\n and must resolve where it's read, and (b) the foreground remap must not leak into explicit materials.\n The uniform EDGE applies to every adaptive panel, veiled or not — a veiled card under the opaque page\n still reads as a solid panel and needs the same outline. */\n[data-glass=\"opaque\"] :where(.glass, [data-surface]):not([data-material]) {\n --srf-border-color: var(--glass-opaque-outline, var(--glass-border));\n --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px));\n /* Paint the border HERE, not just the tokens: borderless roles (Card default) never render\n glass-border, so without this only bordered roles showed the outline — \"some cards get it,\n some don't\". Under the opaque page every solid panel takes the uniform edge; bordered elements\n resolve the same vars, so nothing double-draws. */\n border: var(--srf-border-w) solid var(--brd-ink, var(--srf-border-color));\n}\n/* The FOREGROUND remap is :not(.glass-veil) — the same carve-out crystal and chakra make, and the same\n one --veil-solidify makes for the solidify layer on this page style. A veiled card does not paint\n --glass-opaque-bg: glass-veil moves the background-COLOR channel to the neutral --veil-floor, so its\n text sits on the veil surface the BASE tokens are banded for, not on the opaque floor. Remapping it\n here handed those cards ink solved for a surface they do not have — visible as a hue jump, since the\n opaque floor is the one surface with nothing composited over it and so sits at the raw --glass-tint-h\n while every sheer surface lands a few degrees off it (measured on the /colors panel: 22.0 vs 14.6).\n Splitting the rule is what keeps the edge treatment while dropping the foreground one. */\n[data-glass=\"opaque\"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-opaque);\n --foreground-soft: var(--foreground-soft-opaque);\n --foreground-strong: var(--foreground-strong-opaque);\n --foreground-ui: var(--foreground-ui-opaque);\n --muted-foreground: var(--muted-foreground-opaque);\n}\n/* ── Glass tint presets ─────────────────────────────────────────────────────\n Set data-glass-tint on any element (commonly ) to recolor the glass in that subtree.\n Most presets just move the tint hue/chroma; \"sistine\" is a bespoke multi-hue fresco (gilded\n gold → terracotta flesh tones → azure sky), echoing the Sistine Chapel ceiling.\n Wash + all tint-derived tokens re-resolve per-scope via the :root,[data-glass-tint] split above. */\n/* Selenite — the neutral stone: no chroma and no wash, so the glass is the plain clear sheet. The tint\n values equal the engine default's hue with the colour switched off; the block exists so a CSS-only\n consumer (no tint switcher) gets selenite's colour-wheel anchor, which a hue-less theme needs. */\n[data-glass-tint=\"selenite\"] {\n --glass-tint-h: 250;\n --glass-tint-c: 0;\n --glass-tint-a: 0;\n --harmony-h: 0;\n}\n[data-glass-tint=\"sapphire\"] {\n --glass-tint-h: 255;\n --glass-tint-c: 0.075;\n --glass-tint-a: 0.19;\n --glass-wash-l: 63%;\n --glass-opaque-c-max: 0.056;\n}\n[data-glass-tint=\"aventurine\"] {\n --glass-tint-h: 158;\n --glass-tint-c: 0.076;\n --glass-tint-a: 0.54;\n --glass-wash-l: 88%;\n --glass-opaque-c-max: 0.195;\n}\n[data-glass-tint=\"amethyst\"] {\n --glass-tint-h: 300;\n --glass-tint-c: 0.101;\n --glass-tint-a: 0.15;\n --glass-wash-l: 55%;\n --glass-opaque-c-max: 0.065;\n}\n[data-glass-tint=\"rose\"] {\n --glass-tint-h: 8;\n --glass-tint-c: 0.095;\n --glass-tint-a: 0.17;\n --glass-wash-l: 64%;\n --glass-opaque-c-max: 0.062;\n}\n[data-glass-tint=\"amber\"] {\n --glass-tint-h: 75;\n --glass-tint-c: 0.062;\n --glass-tint-a: 0.38;\n --glass-wash-l: 81%;\n --glass-opaque-c-max: 0.096;\n}\n[data-glass-tint=\"carnelian\"] {\n --glass-tint-h: 38;\n --glass-tint-c: 0.081;\n --glass-tint-a: 0.19;\n --glass-wash-l: 67%;\n --glass-opaque-c-max: 0.062;\n}\n[data-glass-tint=\"peridot\"] {\n --glass-tint-h: 128;\n --glass-tint-c: 0.083;\n --glass-tint-a: 0.54;\n --glass-wash-l: 92%;\n --glass-opaque-c-max: 0.218;\n}\n[data-glass-tint=\"turquoise\"] {\n --glass-tint-h: 190;\n --glass-tint-c: 0.057;\n --glass-tint-a: 0.54;\n --glass-wash-l: 90%;\n --glass-opaque-c-max: 0.144;\n}\n[data-glass-tint=\"aquamarine\"] {\n --glass-tint-h: 215;\n --glass-tint-c: 0.053;\n --glass-tint-a: 0.5;\n --glass-wash-l: 82%;\n --glass-opaque-c-max: 0.091;\n}\n[data-glass-tint=\"tourmaline\"] {\n --glass-tint-h: 342;\n --glass-tint-c: 0.106;\n --glass-tint-a: 0.18;\n --glass-wash-l: 67%;\n --glass-opaque-c-max: 0.079;\n}\n[data-glass-tint=\"lapis\"] {\n /* Lapis lazuli IS the deep saturated ultramarine, and at plain jewel chroma it reads as just another\n blue next to sapphire. It used to buy that depth by declaring c 0.15 — far past the sRGB ceiling —\n and letting the browser clip. That DID deliver a deep blue, but by accident rather than by design:\n clipping chops channels, which drops LIGHTNESS, and the saturation came from landing darker.\n Measured, oklch(58% 0.375 268) renders L48.1 C0.2941 h271 — a colour nobody wrote down.\n The wash lightness below asks for that colour on purpose instead. h268's chroma ceiling PEAKS at\n L46 (0.2979) and collapses to 0.1293 by L74, so the shared 72/58 wash was sitting on the downslope;\n at L46 the same depth is available inside the gamut. oklch(45% 0.291 268) renders L44.9 C0.2909\n h267.9 — the same deep ultramarine, hue exact, nothing pinned.\n So lapis is no longer an exception to anything: it is the clearest case of the rule the whole\n palette now follows — put the wash where the hue can actually hold chroma, then ask for it. */\n --glass-tint-h: 268;\n --glass-tint-c: 0.085;\n --glass-tint-a: 0.13;\n --glass-wash-l: 46%;\n --glass-opaque-c-max: 0.055;\n}\n[data-glass-tint=\"goldstone\"] {\n --glass-tint-h: 22;\n --glass-tint-c: 0.094;\n --glass-tint-a: 0.17;\n --glass-wash-l: 63%;\n --glass-opaque-c-max: 0.06;\n}\n/* ── Per-mode wash ALPHA ──────────────────────────────────────────────────────\n Alpha slides the surface along a straight line from the material floor toward the wash colour, so\n how much a preset can take is set by how far its wash sits from THAT MODE's floor (L36.4 dark /\n L88 light). A wash near the floor barely moves the surface and tolerates a lot; a wash far from it\n crosses the mid-tone band where no text polarity reaches the body floor, and tolerates little.\n Because every wash now sits at its hue's chroma PEAK, dark-peaking hues (lapis L46, amethyst L55)\n sit near the DARK floor and take high alpha there, while light-peaking hues (peridot L92, turquoise\n L90) sit near the LIGHT floor and take high alpha there. The two columns are near mirrors.\n Each value is HALF that mode's measured headroom, so every preset keeps body text >= 75 Lc in both\n modes with room to spare. A single shared alpha cannot serve both ends: at 0.55 everywhere, five\n presets lose dark-mode text and lapis/amethyst lose light-mode text.\n The switcher no longer inlines --glass-tint-a for a named preset (see applyTint), so THESE are the\n values that render — nothing shadows them. */\n.dark[data-glass-tint=\"rose\"] {\n --glass-tint-a: 0.49;\n}\n.dark[data-glass-tint=\"goldstone\"] {\n --glass-tint-a: 0.5;\n}\n.dark[data-glass-tint=\"carnelian\"] {\n --glass-tint-a: 0.45;\n}\n.dark[data-glass-tint=\"amber\"] {\n --glass-tint-a: 0.34;\n}\n.dark[data-glass-tint=\"peridot\"] {\n --glass-tint-a: 0.28;\n}\n.dark[data-glass-tint=\"aventurine\"] {\n --glass-tint-a: 0.29;\n}\n.dark[data-glass-tint=\"turquoise\"] {\n --glass-tint-a: 0.28;\n}\n.dark[data-glass-tint=\"aquamarine\"] {\n --glass-tint-a: 0.31;\n}\n.dark[data-glass-tint=\"sapphire\"] {\n --glass-tint-a: 0.48;\n}\n.dark[data-glass-tint=\"lapis\"] {\n --glass-tint-a: 0.54;\n}\n.dark[data-glass-tint=\"amethyst\"] {\n --glass-tint-a: 0.54;\n}\n.dark[data-glass-tint=\"tourmaline\"] {\n --glass-tint-a: 0.46;\n}\n[data-glass-tint=\"moonstone\"] {\n /* Moonstone — a warm, very-low-chroma off-white glass (reads as cream, not yellow). A normal translucent\n tint: texture-preserving and light-text in both modes, just a desaturated warm hue. Anchors its harmony\n wheel at 0° (like selenite) so accents come out a colorful set, not a warp of the near-neutral hue. */\n --glass-tint-h: 75;\n --glass-tint-c: 0.047;\n --glass-tint-a: 0.18;\n --harmony-h: 0;\n /* Gloss chroma, moonstone's own. The global --glass-gloss-tint is 4.25 — sized for the JEWELS, where a\n 0.07–0.1 tint wants a vivid gem highlight. Moonstone declares 0.047 precisely so it \"reads as cream,\n not yellow\" (above), and 4.25× asks for 0.1998.\n --glass-gloss-c-max (tokens.css) is what stops that becoming a clipped primary, and in LIGHT it does\n the whole job: 0.013 caps both 0.1998 and 0.047 to the same value, so this override changes nothing\n there. It earns its place in DARK, where the cap is a roomy 0.11 — enough that the multiplier would\n still put moonstone's highlight at 2.3× its declared chroma, i.e. amber on a stone that is supposed\n to be pale. 1× holds the specular at the tint's own chroma and lets its LIGHTNESS do the work. */\n --glass-gloss-tint: 1;\n /* Moonstone daytime body lightness. Fitted for the retired opaque sheet (tokens.css) on moonstone's OWN\n floor rather than the global pair: this cream is already near-white, where a near-white sheet moved it\n far less than it moved the standard L90 floor. dE 0.0002 — indistinguishable from the old surface. */\n --glass-opaque-l: 94.5;\n /* NO pinned --glass-opaque-bg: the floor must stay DERIVED from --glass-opaque-l (engine), because the\n lightness slider / stored per-mode L land as an INLINE --glass-opaque-l that shadows this block — a\n literal pin would freeze the paint while AutoForeground keeps banding text against the token (light\n text on cream). Was c-scale 1 — the value that made the derived color exactly the original cream when\n the sheet still painted over it; 0.92 is that same cream now that the sheet is gone (it desaturated as\n well as lightened, so both knobs carry part of it). */\n --glass-opaque-c-scale: 0.92;\n}\n.dark[data-glass-tint=\"moonstone\"] {\n /* Moonstone night. The wash sits a few points above the standard 58% (warmth without glare; h75\n carries more real luminance per OKLCH point than the cool tints, so pale-warm washes read brighter\n than their numbers). AutoForeground reads BOTH wash knobs live (+ the [moonstone-sync] mirrored\n fallback), so page text and per-card dark-cream text re-band automatically.\n\n The FLOOR is what you actually see: at the default opacity every surface — cards, inputs, the nav\n pill — paints --glass-opaque-bg, and the wash only tints what shows through. It used to derive\n oklch(0.8 0.047 75), which is a sand/tobacco tan, so moonstone night read as brown rather than as\n the pale silvery stone it is named for. Lightness alone does not fix that: h75 is warm, so every\n lightness along it lands somewhere in the tan family. Cutting the CHROMA is what moves it off that\n family. Light mode is untouched (it keeps c-scale 1 at L94, where the same hue reads as cream).\n\n L52, down from a cream L84.9 — moonstone night is a DARK stone now, and that is forced rather than\n preferred. This floor feeds two surfaces at once: opaque cards paint it directly, and it composites\n at --glass-opacity over the dark veil to become the floor sheer glass sits on. Both have to clear\n the body floor, and for h75 nothing in L55–L84 reaches it in EITHER text polarity — that band is the\n mid-tone dead zone. A cream L84.9 put the opaque card just above it (Lc 77.1) but dragged the sheer\n surface to L65, dead centre, where body text topped out at 65.4 Lc and the preset had to be pinned\n as a known exception. Going lighter cannot fix it: the veil is 30% of the composite, which caps the\n solidify floor at L75.6 no matter how light this value gets, so the sheer surface can never climb\n out the top. Only the bottom exit is reachable. L52 is the PALEST value where both surfaces clear:\n sheer body text lands 80.0 Lc (was 65.4) and the opaque card 82.9 (was 77.1), and it also restores\n --foreground-strong, which could not reach its 90 target on the cream floor either. Still stone —\n oklch(0.52 0.021 75) is #70675c, a warm grey — and still well clear of the jewels' L36.4. */\n --glass-wash-l: 64%;\n --glass-wash-c-mult: 2;\n /* Fitted for the retired opaque sheet, like the daytime floor above and likewise not the global dark\n pair: moonstone night is a LIGHT floor, so the near-white sheet barely moved it. dE 0.0002. */\n --glass-opaque-l: 52;\n --glass-opaque-c-scale: 0.44;\n /* Static -opaque tier baselines. These flipped polarity with the floor: on the old cream L84.9 they were\n dark warm literals, and on an L52 stone they have to be light or SSR's first paint is dark-on-dark.\n Values are what AutoForeground itself solves for this surface, so the mounted refine is a no-op. */\n --foreground-opaque: oklch(1 0 75);\n --foreground-soft-opaque: oklch(0.9 0.05 75);\n --foreground-strong-opaque: oklch(1 0 75);\n --foreground-ui-opaque: oklch(0.91 0.05 75);\n --muted-foreground-opaque: oklch(0.97 0.02 75);\n /* Chakra needs the SAME treatment opaque gets above, and did not have it: chakra was added after this\n block was written, so it kept the global dark body (L28) and rendered oklch(0.28 0.046 75) = #36250c,\n a dark brown — the one surface where moonstone still read as tobacco rather than stone. Matching\n opaque's pair — now L51, chroma x0.45 → 0.021, landing on #6d6559, the same stone as the opaque\n floor one point above it.\n The hue stays 75 deliberately. Cutting CHROMA is what moves this off the tan family; lightness alone\n does not, and a cool hue would contradict the warm cream light mode keeps at L94. */\n --glass-chakra-l: 51;\n --glass-chakra-c-max: 0.021;\n /* Same reasoning as the -opaque baselines above, and flipped with them: an L51 body needs LIGHT text. */\n --foreground-chakra: oklch(1 0 75);\n --foreground-soft-chakra: oklch(0.9 0.05 75);\n --foreground-strong-chakra: oklch(1 0 75);\n --foreground-ui-chakra: oklch(0.91 0.05 75);\n --muted-foreground-chakra: oklch(0.96 0.03 75);\n}\n\n/* Semantic surfaces (Alert info/success/warning/destructive; destructive Button/Badge; status toasts,\n which the Toaster stamps from sonner's data-type — see TOAST_TINT in components/ui/sonner.tsx) render\n data-glass-tint=\"\" — one block per status, joining the grouped engine re-resolution above so\n --glass-bg / wash re-resolve at the alert/toast. */\n[data-glass-tint=\"info\"] {\n --glass-tint-h: 255;\n --glass-tint-c: 0.075;\n --glass-tint-a: 0.18;\n --glass-wash-l: 63%;\n}\n[data-glass-tint=\"success\"] {\n --glass-tint-h: 150;\n --glass-tint-c: 0.088;\n --glass-tint-a: 0.18;\n --glass-wash-l: 87%;\n}\n[data-glass-tint=\"warning\"] {\n --glass-tint-h: 85;\n --glass-tint-c: 0.063;\n --glass-tint-a: 0.18;\n --glass-wash-l: 84%;\n}\n[data-glass-tint=\"destructive\"] {\n --glass-tint-h: 25;\n --glass-tint-c: 0.094;\n --glass-tint-a: 0.18;\n --glass-wash-l: 63%;\n}\n/* FRESCOES (muse / sistine / aurora / gloaming): each block's --glass-tint-h/c/a MUST mirror its\n GlassTintSwitcher preset (components/glass-tint-switcher.tsx). The switcher INLINES those onto ,\n shadowing these blocks on any page with the switcher mounted — so these are the static-consumer fallback.\n Keep them identical, or the demo and a static page render different surfaces. (--glass-bg gradients and\n --glass-fg-h are CSS-only — no preset counterpart.) */\n\n/* Muse — the cityscape signature, mode-aware: cool daytime blues in light, the neon night sweep in\n dark, so it tracks the day/night background. Pick the look with the light/dark/system toggle. */\n/* Muse is now ONE palette in both modes: the neon city — cyan → violet → magenta → pink → amber. The\n old day sweep (a pale blue → cyan → teal cityscape) is gone; its hues live on in Aurora, which took\n them over.\n\n What differs between the modes is ALPHA, not colour. A day surface sits on a light page and needs\n more of the gradient to register; night sits on a dark one and needs less before the neon blows out.\n Lightness is shared because these stops are already bright (L 78–85), so they read on either page.\n\n Stop 2's chroma is 0.095, not the 0.17 night shipped with: h290 at L80 has an sRGB ceiling of 0.1052,\n so 0.17 was 162% over and has been browser-gamut-mapped this whole time — which also means it\n rendered differently in Safari (per-channel clamp toward grey) than in Chrome (hue-preserving map).\n 0.095 is 90% of its own ceiling, the same rule the rest of the stops follow. */\n[data-glass-tint=\"muse\"] {\n --glass-tint-h: 230;\n --glass-fg-h: 290; /* text/accent anchor: violet, matching night so the two modes share one identity */\n --glass-tint-c: 0.056;\n --glass-tint-a: 0.16;\n --glass-wash-l: 76%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.32) 0%,\n oklch(80% 0.095 290 / 0.3) 22%,\n oklch(78% 0.2 326 / 0.28) 45%,\n oklch(79% 0.12 350 / 0.28) 70%,\n oklch(84% 0.12 74 / 0.3) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.46) 0%,\n oklch(80% 0.095 290 / 0.44) 22%,\n oklch(78% 0.2 326 / 0.42) 45%,\n oklch(79% 0.12 350 / 0.42) 70%,\n oklch(84% 0.12 74 / 0.44) 100%\n );\n /* Light-mode body: the night anchor's hue (288) at a day-legible lightness. Chroma is 90% of the\n ceiling at L91 h288 (0.0446), because a light surface has very little chroma headroom. */\n --glass-opaque-bg: oklch(91% 0.04 288);\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"muse\"] {\n /* Night: the same neon palette as day, at night's lower alphas (a dark page needs less of the\n gradient before the neon blows out). Only alpha and the solid body differ from the block above. */\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.24) 0%,\n oklch(80% 0.095 290 / 0.22) 22%,\n oklch(78% 0.2 326 / 0.2) 45%,\n oklch(79% 0.12 350 / 0.2) 70%,\n oklch(84% 0.12 74 / 0.22) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.36) 0%,\n oklch(80% 0.095 290 / 0.34) 22%,\n oklch(78% 0.2 326 / 0.32) 45%,\n oklch(79% 0.12 350 / 0.32) 70%,\n oklch(84% 0.12 74 / 0.34) 100%\n );\n --glass-opaque-bg: oklch(34% 0.12 288);\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Sistine — the signature: a painterly fresco of four jewels (amber → rose → sapphire → aventurine,\n in that order), interpolated `in oklch` so the warm→cool transitions stay vivid instead of\n averaging to grey. Border + crystal + accent pick up the rose base via the tint vars. */\n[data-glass-tint=\"sistine\"] {\n --glass-tint-h: 75; /* surface: mirrors the Sistine switcher preset */\n --glass-fg-h: 18; /* text/accent anchor: crimson (hand-selected) — hue 18 reads as deep red, not the rose at h8 */\n --glass-tint-c: 0.062;\n --glass-tint-a: 0.16;\n --glass-wash-l: 81%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(90% 0.11 75 / 0.34) 0%,\n oklch(86% 0.11 8 / 0.3) 33%,\n oklch(86% 0.11 255 / 0.3) 66%,\n oklch(87% 0.11 158 / 0.32) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(90% 0.11 75 / 0.46) 0%,\n oklch(86% 0.11 8 / 0.42) 33%,\n oklch(86% 0.11 255 / 0.42) 66%,\n oklch(87% 0.11 158 / 0.44) 100%\n );\n /* Crystal composes its own background (not --glass-bg), so hand it the fresco explicitly. */\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"sistine\"] {\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.11 75 / 0.22) 0%,\n oklch(82% 0.11 8 / 0.2) 33%,\n oklch(82% 0.11 255 / 0.2) 66%,\n oklch(83% 0.11 158 / 0.22) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.11 75 / 0.32) 0%,\n oklch(82% 0.11 8 / 0.3) 33%,\n oklch(82% 0.11 255 / 0.3) 66%,\n oklch(83% 0.11 158 / 0.32) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Aurora — northern lights: spring-green → teal → violet, interpolated `in oklch` so the cool sweep\n stays vivid. Border + crystal + accent pick up the teal base via the tint vars.\n DAY was authored at L 90/89/84 — within ~5 L of the L-95 page it composites onto — so after the\n alpha only 2.2 L of separation survived and the bands read as haze rather than bands. Night works\n because its stops sit 60+ L above an L-20 page and land 13.6 L clear of it. The fix is that\n separation, not more colour: day already delivered MORE chroma than night (spread 26/29/19 against\n 14/15/13) and still looked washed out, because colour without lightness structure reads as fog.\n Stops now sit at L 76.3/74.7/71.8, which also buys chroma headroom, so each takes ~90% of its OWN\n hue's sRGB ceiling rather than one shared number — h292 violet was clipped to 65% at L 84 and is\n fully in gamut here. Composited: spread 48/40/38 (was 26/29/19), separation 8.0 L (was 2.2).\n How far this could go is bound by TEXT, not by taste. AutoForeground models a fresco surface from\n the tint wash (--glass-tint-c/-a), never from this gradient literal, so it does not re-band when\n these stops move and the contrast has to hold statically. --foreground-soft (the L-40 heading tier)\n is the weakest pair on a light surface and sets the floor: at a composite of L 84 it measures 59.5\n and fails, so the stops are placed to land no darker than ~L 86. Worst measured here is 63.5.\n Frosted gets its OWN alphas rather than the old ~1.37x boost over --glass-bg: with stops this much\n darker, that multiplier drove the violet composite to L 83.4 and soft to 58.4. These land all three\n near L 86.4 instead. The authored shape is unchanged — green, then teal, then violet darkest. */\n/* Aurora now sweeps Muse Day's hues — blue 235 → cyan 205 → teal-green 155 — instead of the old\n green → cyan → violet. LIGHTNESS is untouched, because it is not a style choice here: the day stops\n sit at L 71.8–76.3 to satisfy the --foreground-soft ≥60 Lc bind described above, and Muse Day's own\n L 91–94 is precisely the band these were moved away from when day read as haze.\n\n CHROMA had to be re-derived, it could not travel with the hues. Each stop is 90% of that hue's OWN\n sRGB ceiling at its lightness, which is the rule the previous stops already followed (all three\n measured exactly 90%). A flat number cannot survive a hue change: the old 0.175 is 90% of green's\n ceiling at L76.3 but 127% of blue's, so carrying it over would have clipped stop 1 outright.\n\n Night is re-derived the same way, which also fixes a pre-existing clip: its violet stop asked 0.13\n against a 0.1068 ceiling at h292/L80 — 122% — so that stop has been gamut-mapped by the browser all\n along. */\n[data-glass-tint=\"aurora\"] {\n --glass-tint-h: 205; /* the sweep's midpoint, was 178 */\n --glass-tint-c: 0.054;\n --glass-tint-a: 0.16;\n --glass-wash-l: 86%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(76.3% 0.124 235 / 0.4) 0%,\n oklch(74.7% 0.111 205 / 0.38) 50%,\n oklch(71.8% 0.155 155 / 0.38) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(76.3% 0.124 235 / 0.46) 0%,\n oklch(74.7% 0.111 205 / 0.43) 50%,\n oklch(71.8% 0.155 155 / 0.37) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"aurora\"] {\n --glass-bg: linear-gradient(135deg in oklch, oklch(86% 0.071 235 / 0.22) 0%, oklch(85% 0.127 205 / 0.2) 50%, oklch(80% 0.173 155 / 0.22) 100%);\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.071 235 / 0.32) 0%,\n oklch(85% 0.127 205 / 0.3) 50%,\n oklch(80% 0.173 155 / 0.32) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Gloaming — dusk: gold → rose → indigo, interpolated `in oklch` so the warm→cool fade keeps its\n glow. Night keeps the warm glow; day uses a dusk-rose anchor (the warm hue reads muddy as light-mode text). */\n[data-glass-tint=\"gloaming\"] {\n --glass-tint-h: 32; /* surface: mirrors the Gloaming switcher preset; text shares it (--glass-fg-h defaults to it) */\n --glass-tint-c: 0.089;\n --glass-tint-a: 0.16;\n --glass-wash-l: 64%;\n --glass-bg: linear-gradient(135deg in oklch, oklch(89% 0.12 62 / 0.32) 0%, oklch(84% 0.14 350 / 0.3) 50%, oklch(78% 0.13 278 / 0.3) 100%);\n --glass-frosted-bg: linear-gradient(135deg in oklch, oklch(89% 0.12 62 / 0.44) 0%, oklch(84% 0.14 350 / 0.42) 50%, oklch(78% 0.13 278 / 0.42) 100%);\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"gloaming\"] {\n --glass-tint-h: 32; /* night keeps the warm glow; day uses the dusk-rose above */\n --glass-bg: linear-gradient(135deg in oklch, oklch(86% 0.12 62 / 0.22) 0%, oklch(80% 0.14 350 / 0.2) 50%, oklch(74% 0.13 278 / 0.22) 100%);\n --glass-frosted-bg: linear-gradient(135deg in oklch, oklch(86% 0.12 62 / 0.32) 0%, oklch(80% 0.14 350 / 0.3) 50%, oklch(74% 0.13 278 / 0.32) 100%);\n --glass-crystal-fresco: var(--glass-bg);\n}\n", "type": "registry:style", "target": "app/globals.css" } diff --git a/public/r/toggle-group.json b/public/r/toggle-group.json index 64958278..e1adcdad 100644 --- a/public/r/toggle-group.json +++ b/public/r/toggle-group.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/toggle.json b/public/r/toggle.json index fc9dc606..7e1e15fe 100644 --- a/public/r/toggle.json +++ b/public/r/toggle.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, diff --git a/public/r/tooltip.json b/public/r/tooltip.json index 496af1c5..5630d278 100644 --- a/public/r/tooltip.json +++ b/public/r/tooltip.json @@ -21,7 +21,7 @@ }, { "path": "lib/material.ts", - "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", + "content": "import type * as React from \"react\";\n\n/**\n * The Sistine material system — the single source of the material → markup mapping.\n *\n * FIVE materials (glass, frosted, crystal, chakra, opaque), each a `[data-material]` token set in\n * app/theme/materials.css, composed with orthogonal AXES (border + its colour, veil, size, gradient, glow, sheen)\n * over the one structural `glass` utility. Components call `glassMaterial()` instead of hardcoding\n * recipe class strings (check-theme bans the legacy `glass-bg`/`glass-surface`/… classes).\n *\n * Semantics:\n * - `material` UNDEFINED = adaptive: no attribute is rendered; the surface follows the page's\n * [data-glass] style (the default everywhere).\n * - An explicit material PINS the element under any page style, and — because the token set is\n * inherited custom properties — also acts as a scoped default for adaptive glass descendants\n * (a dialog set to frosted re-skins the controls inside it; pin a child back with its own material).\n * - `\"none\"` = not a glass surface: the helper returns nothing and the component supplies its plain\n * (shadcn) classes.\n */\nexport type Material = \"glass\" | \"frosted\" | \"crystal\" | \"chakra\" | \"opaque\" | \"none\";\n\nexport interface MaterialProps {\n /** undefined = adaptive (follows the page style); \"none\" = the component's plain fallback. */\n material?: Material;\n /** Material border. Three weights: `true`/\"hairline\" = the material's edge (1px; 0.5px under\n * frosted), \"rim\" = 2px, \"frame\" = 4px. */\n border?: boolean | \"hairline\" | \"rim\" | \"frame\";\n /** The border's COLOUR. \"glass\" (the default) = the material's own edge, a near-white rim at the\n * material's alpha, which reads as glass but disappears on light themes at any weight. \"ink\" = the\n * page's text colour (--foreground, at --glass-border-ink-a), a visible line wherever text is legible.\n * Pairs with `border`; under the opaque page style it also colours the uniform panel edge. */\n borderColor?: \"glass\" | \"ink\";\n /** Element-composed legibility floor for read-through overlays (menus, tooltips, toasts). */\n veil?: boolean;\n /** Blur/elevation tier — INTERNAL: set by a component's ROLE (glass-sm/-lg), not a public prop\n * (\"size\" universally means a component's dimensions; the material tier must never shadow that). */\n size?: \"sm\" | \"lg\" | \"xl\";\n /** Readability blur FLOOR (>= --glass-diffuse, default 12px) for text-dense translucent surfaces;\n * inert under opaque. \"stained\" = the dyed mode: same floor, plus true stained-glass optics — the\n * backdrop renders as TONAL SHADES of the theme hue (grayscale luminance collapse — the tint supplies the color). */\n diffuse?: boolean | \"stained\";\n /** Brand-gradient accent layered over the material. */\n gradient?: boolean;\n /** Resting glow, or the intensified \"lg\" halo. */\n glow?: boolean | \"lg\";\n /** Opt-in hover shimmer (pseudo-element based — avoid on sticky/fixed surfaces). */\n sheen?: boolean;\n}\n\n/** The PUBLIC per-instance axis props a component spreads onto its type. Excludes `size` (the material\n * blur tier is a role-internal detail — a component's own `size` prop owns that name). */\nexport type MaterialAxisProps = Omit;\n\n/** The `rest` side of splitAxisProps. Distributes over union props (DayPicker's mode union, Radix\n * ToggleGroup's single/multiple) so a discriminated union survives the split instead of collapsing\n * under a bare Omit. */\nexport type SplitAxisRest

    = P extends unknown ? Omit : never;\n\n/** Split the material axis props off a component's props in ONE place — components never enumerate\n * axis keys, so a NEW axis is zero-touch: extend MaterialProps + this destructure, done. `size` is\n * role-internal (not in MaterialAxisProps) and stays in `rest` for the component's own cva. */\nexport function splitAxisProps

    (\n props: P,\n): [\n MaterialAxisProps,\n SplitAxisRest

    ,\n] {\n const { material, border, borderColor, veil, diffuse, gradient, glow, sheen, ...rest } = props;\n return [\n {\n material,\n border,\n borderColor,\n veil,\n diffuse,\n gradient,\n glow,\n sheen,\n },\n /* The conditional (distributive) rest type can't be proven for an unresolved P — safe by\n * construction: rest is exactly P minus the axis keys. */\n rest as SplitAxisRest

    ,\n ];\n}\n\nexport interface MaterialAttrs {\n /** Spread-ready: undefined for adaptive and \"none\". */\n \"data-material\"?: Exclude;\n /** \"\" when material === \"none\" (caller supplies its plain classes). */\n className: string;\n}\n\nexport function glassMaterial(props: MaterialProps = {}): MaterialAttrs {\n const { material, border, borderColor, veil, size, diffuse, gradient, glow, sheen } = props;\n if (material === \"none\") {\n return {\n className: \"\",\n };\n }\n return {\n \"data-material\": material,\n className: [\n \"glass\",\n border && \"glass-border\",\n border === \"rim\" && \"glass-border-rim\",\n border === \"frame\" && \"glass-border-frame\",\n borderColor === \"ink\" && \"glass-border-ink\",\n veil && \"glass-veil\",\n size === \"sm\" && \"glass-sm\",\n size === \"lg\" && \"glass-lg\",\n size === \"xl\" && \"glass-xl\",\n diffuse && \"glass-diffuse\",\n diffuse === \"stained\" && \"glass-stained\",\n gradient && \"glass-gradient\",\n glow === \"lg\" ? \"glass-glow-lg\" : glow ? \"glass-glow\" : false,\n sheen && \"glass-sheen\",\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\n/**\n * The component-facing resolver: merge a component's ROLE (its default axes for a given semantic\n * variant) with explicit per-instance material/axis props, and return the surface markup — or null\n * when the element is NOT a glass surface (role === null, or material === \"none\").\n *\n * Explicit props override the role per-axis; unspecified axes fall back to the role.\n */\nexport function materialSurface(role: MaterialProps | null, props: MaterialProps): MaterialAttrs | null {\n if (props.material === \"none\" || role === null) {\n return null;\n }\n return glassMaterial({\n material: props.material,\n border: props.border ?? role.border,\n borderColor: props.borderColor ?? role.borderColor,\n veil: props.veil ?? role.veil,\n size: props.size ?? role.size,\n diffuse: props.diffuse ?? role.diffuse,\n gradient: props.gradient ?? role.gradient,\n glow: props.glow ?? role.glow,\n sheen: props.sheen ?? role.sheen,\n });\n}\n\n/**\n * Typed per-element knobs → CSS custom properties ONLY (the cascade-native successor to the old\n * `glass={{…}}` inline-style prop). Everything routes through the token system, so nothing here can\n * fight a material, a page style, or the theme.\n *\n * The tint* keys recolor engine-composed tokens, which re-resolve at grouped declarations — pair\n * them with a `data-glass-tint` attribute on the same element so the engine re-composes there.\n */\nexport interface GlassVars {\n /** → --glass-tint-h (surface hue) */\n tintH?: number;\n /** → --glass-tint-c (surface chroma — the colorfulness master) */\n tintC?: number;\n /** → --glass-tint-a (tint film alpha) */\n tintA?: number;\n /** → --glass-opacity (0..1 solidify dial; element-composed in `glass`) */\n opacity?: number;\n /** → --srf-blur (number → px; glass material only) */\n blur?: number | string;\n /** → --glass-solid-a (veil floor alpha; element-composed in `glass-veil`) */\n solidA?: number;\n}\n\nexport function glassVars(v: GlassVars = {}): React.CSSProperties {\n const s: Record = {};\n if (v.tintH !== undefined) {\n s[\"--glass-tint-h\"] = String(v.tintH);\n }\n if (v.tintC !== undefined) {\n s[\"--glass-tint-c\"] = String(v.tintC);\n }\n if (v.tintA !== undefined) {\n s[\"--glass-tint-a\"] = String(v.tintA);\n }\n if (v.opacity !== undefined) {\n s[\"--glass-opacity\"] = String(v.opacity);\n }\n if (v.blur !== undefined) {\n s[\"--srf-blur\"] = typeof v.blur === \"number\" ? `${v.blur}px` : v.blur;\n }\n if (v.solidA !== undefined) {\n s[\"--glass-solid-a\"] = String(v.solidA);\n }\n return s as React.CSSProperties;\n}\n", "type": "registry:lib", "target": "lib/material.ts" }, @@ -33,7 +33,7 @@ }, { "path": "lib/snap-popper.ts", - "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Keeps text crisp on Radix popper surfaces (dropdown-menu, popover, tooltip, select, hover-card,\n * context-menu).\n *\n * THE DEFECT: Radix positions those surfaces by writing an inline `transform: translate(x, y)` onto a\n * wrapper element. Floating UI does not round x/y, so they land on fractions whenever the trigger does\n * — under browser zoom, fractional DPR, or any flex/grid trigger whose box isn't on a whole pixel. On\n * its own that would be harmless, but sistine's popper content carries `backdrop-filter` (it's glass),\n * which promotes it to its own compositing layer. A promoted layer at a fractional offset has its\n * glyphs rasterized off the device pixel grid and the text renders visibly soft.\n *\n * This is the same defect the dialogs had. There the fix was to stop using a transform to center; a\n * popper can't do that, because its position IS the transform. So instead: round the translate to whole\n * pixels. Sub-pixel placement is imperceptible; blurred text is not.\n *\n * Radix rewrites that transform on scroll, resize and collision flips, so a MutationObserver re-snaps\n * after each write. The observer is re-entrant — our own write retriggers it — so the write is skipped\n * when the value is already integral, which terminates the loop on the first pass.\n */\n/**\n * The whole snap, as a pure string transform: given the wrapper's SPECIFIED inline `transform`, return\n * what it should be rewritten to, or `null` to leave it alone.\n *\n * Returning null is load-bearing in two cases and both are re-entrancy guards, since our own write\n * retriggers the MutationObserver that called us:\n * - the value is already integral, so there is nothing to do and the loop terminates on pass one;\n * - the value isn't a translate we recognise (Radix hasn't positioned yet), so we must not invent one.\n *\n * Split out of the hook so it can be checked without a DOM — see scripts/check-units.mjs.\n */\nexport function snapTransform(specified: string): string | null {\n const parts = /^translate(?:3d)?\\(\\s*(-?[\\d.]+)px\\s*,\\s*(-?[\\d.]+)px/.exec(specified);\n if (!parts) return null;\n const next = `translate(${Math.round(Number(parts[1]))}px, ${Math.round(Number(parts[2]))}px)`;\n return specified === next ? null : next;\n}\n\nexport function useSnappedPopper(): React.RefCallback {\n const cleanupRef = React.useRef<(() => void) | null>(null);\n\n return React.useCallback((node: T | null) => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n if (!node) return;\n\n const wrapper = node.closest(\"[data-radix-popper-content-wrapper]\");\n if (!wrapper) return; // not popper-positioned (e.g. an inline/anchored render) — nothing to snap\n\n const snap = () => {\n // Read and write the SPECIFIED value, never the computed one. Under browser `zoom` the computed\n // transform is in a scaled space, so rounding it and writing the result back as authored px\n // never converges — snapTransform's equality guard would never hold and the observer would\n // re-enter forever.\n const next = snapTransform(wrapper.style.transform);\n if (next !== null) wrapper.style.transform = next;\n };\n\n snap();\n const observer = new MutationObserver(snap);\n observer.observe(wrapper, {\n attributes: true,\n attributeFilter: [\n \"style\",\n ],\n });\n cleanupRef.current = () => observer.disconnect();\n }, []);\n}\n\n/**\n * Merge the forwarded ref with the snapping ref, MEMOIZED — use this, not `composeRefs`, in JSX.\n *\n * A ref callback written inline gets a fresh identity on every render, and React responds by detaching\n * the old one (calling it with `null`) and attaching the new one. For a plain ref that is merely\n * wasteful; here the detach runs `useSnappedPopper`'s cleanup, so the MutationObserver watching the\n * popper wrapper is torn down and rebuilt on every single render of the surface.\n *\n * Same shape as Radix's own `useComposedRefs` — `useCallback` over the composed callback, with the\n * refs themselves as the dependency array, so the identity holds as long as the individual refs do.\n * Kept local rather than importing @radix-ui/react-compose-refs: these components ship through the\n * registry, and a direct dependency would have to be declared on all six of them.\n */\nexport function useComposedRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n /* The spread refs ARE the dependency list — the identity holds while each individual ref does. */\n return React.useCallback(composeRefs(...refs), refs);\n}\n\n/** Merge the forwarded ref with the snapping ref — both need the same node. Prefer the memoized\n * {@link useComposedRefs} anywhere the result is handed to JSX. */\nexport function composeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n return (node: T | null) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.MutableRefObject).current = node;\n }\n };\n}\n", + "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Keeps text crisp on Radix popper surfaces (dropdown-menu, popover, tooltip, select, hover-card,\n * context-menu).\n *\n * THE DEFECT: on a high-density display the text in a popper rendered visibly soft. Radix positions the\n * surface with an inline `transform: translate(x, y)` on a wrapper element, written from Floating UI's\n * `floatingStyles`, and at a device pixel ratio of 1.5 or more Floating UI adds `will-change: transform`\n * to the same style (`@floating-ui/react-dom`, `floatingStyles`). `will-change` promotes the wrapper to\n * its own compositing layer, and WebKit drops subpixel (LCD) antialiasing for text on a composited layer;\n * sistine's popper content is glass, so a blurred backdrop sits right behind that text. Resetting\n * `will-change` to `auto` is the fix. The cost is a repaint on reposition instead of a layer move, for a\n * surface that is open for seconds.\n *\n * The ROUNDING below predates that diagnosis. Its premise was that unrounded offsets put glyphs off the\n * pixel grid, but Floating UI already rounds x/y to the device pixel grid (`roundByDPR`) before writing\n * them. It is kept for browser zoom, where the device grid and the CSS-pixel grid diverge, which has not\n * been verified either way; it is cheap and harmless, and removing it is a separate decision.\n *\n * Radix rewrites the style on scroll, resize and collision flips, so a MutationObserver re-applies both\n * after each write. The observer is re-entrant (our own write retriggers it), so each write is skipped\n * when the value is already what we want, which terminates the loop on the second pass at the latest.\n */\n/**\n * The whole snap, as a pure string transform: given the wrapper's SPECIFIED inline `transform`, return\n * what it should be rewritten to, or `null` to leave it alone.\n *\n * Returning null is load-bearing in two cases and both are re-entrancy guards, since our own write\n * retriggers the MutationObserver that called us:\n * - the value is already integral, so there is nothing to do and the loop terminates on pass one;\n * - the value isn't a translate we recognise (Radix hasn't positioned yet), so we must not invent one.\n *\n * Split out of the hook so it can be checked without a DOM — see tests/snap-popper.test.tsx.\n */\nexport function snapTransform(specified: string): string | null {\n const parts = /^translate(?:3d)?\\(\\s*(-?[\\d.]+)px\\s*,\\s*(-?[\\d.]+)px/.exec(specified);\n if (!parts) return null;\n const next = `translate(${Math.round(Number(parts[1]))}px, ${Math.round(Number(parts[2]))}px)`;\n return specified === next ? null : next;\n}\n\nexport function useSnappedPopper(): React.RefCallback {\n const cleanupRef = React.useRef<(() => void) | null>(null);\n\n return React.useCallback((node: T | null) => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n if (!node) return;\n\n const wrapper = node.closest(\"[data-radix-popper-content-wrapper]\");\n if (!wrapper) return; // not popper-positioned (e.g. an inline/anchored render) — nothing to snap\n\n const snap = () => {\n // Read and write the SPECIFIED value, never the computed one. Under browser `zoom` the computed\n // transform is in a scaled space, so rounding it and writing the result back as authored px\n // never converges — snapTransform's equality guard would never hold and the observer would\n // re-enter forever.\n const next = snapTransform(wrapper.style.transform);\n if (next !== null) wrapper.style.transform = next;\n // Floating UI's will-change (DPR >= 1.5) is what costs the text its antialiasing. Only written when\n // something else is set, so the pass our own write triggers sees `auto` and stops.\n const willChange = wrapper.style.willChange;\n if (willChange !== \"\" && willChange !== \"auto\") wrapper.style.willChange = \"auto\";\n };\n\n snap();\n const observer = new MutationObserver(snap);\n observer.observe(wrapper, {\n attributes: true,\n attributeFilter: [\n \"style\",\n ],\n });\n cleanupRef.current = () => observer.disconnect();\n }, []);\n}\n\n/**\n * Merge the forwarded ref with the snapping ref, MEMOIZED — use this, not `composeRefs`, in JSX.\n *\n * A ref callback written inline gets a fresh identity on every render, and React responds by detaching\n * the old one (calling it with `null`) and attaching the new one. For a plain ref that is merely\n * wasteful; here the detach runs `useSnappedPopper`'s cleanup, so the MutationObserver watching the\n * popper wrapper is torn down and rebuilt on every single render of the surface.\n *\n * Same shape as Radix's own `useComposedRefs` — `useCallback` over the composed callback, with the\n * refs themselves as the dependency array, so the identity holds as long as the individual refs do.\n * Kept local rather than importing @radix-ui/react-compose-refs: these components ship through the\n * registry, and a direct dependency would have to be declared on all six of them.\n */\nexport function useComposedRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n /* The spread refs ARE the dependency list — the identity holds while each individual ref does. */\n return React.useCallback(composeRefs(...refs), refs);\n}\n\n/** Merge the forwarded ref with the snapping ref — both need the same node. Prefer the memoized\n * {@link useComposedRefs} anywhere the result is handed to JSX. */\nexport function composeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {\n return (node: T | null) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.MutableRefObject).current = node;\n }\n };\n}\n", "type": "registry:lib", "target": "lib/snap-popper.ts" }, diff --git a/registry.json b/registry.json index ac2e5a19..07b66869 100644 --- a/registry.json +++ b/registry.json @@ -1688,7 +1688,7 @@ "name": "empty-state", "type": "registry:component", "title": "Empty State", - "description": "An empty state component, with the glass material system.", + "description": "An empty state component, with the glass material system. Legacy: prefer `empty`, which follows shadcn's anatomy (Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent).", "registryDependencies": [ "@sistine/theme" ], @@ -1724,7 +1724,7 @@ "name": "menu-bar", "type": "registry:component", "title": "Menu Bar", - "description": "A menu bar component, with the glass material system.", + "description": "A row of glass buttons for app-style top bars. Legacy, and not a menu system: for File / Edit / View menus with keyboard navigation, use `menubar`.", "registryDependencies": [ "@sistine/button", "@sistine/theme" @@ -1908,6 +1908,274 @@ } ] }, + { + "name": "kbd", + "type": "registry:component", + "title": "Kbd", + "description": "A keyboard key cap for shortcuts and key combinations, drawn in the theme's text colour with the material's hairline edge.", + "dependencies": [ + "class-variance-authority", + "clsx", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "components/ui/kbd.tsx", + "type": "registry:component", + "target": "components/ui/kbd.tsx" + } + ] + }, + { + "name": "empty", + "type": "registry:component", + "title": "Empty", + "description": "An empty state for a list, table or panel with nothing in it yet: media, title, description and actions, as plain layout or on its own glass surface.", + "dependencies": [ + "class-variance-authority", + "clsx", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "components/ui/empty.tsx", + "type": "registry:component", + "target": "components/ui/empty.tsx" + } + ] + }, + { + "name": "progress", + "type": "registry:component", + "title": "Progress", + "description": "A progress bar on the theme's accent, in the same track and fill as Slider.", + "dependencies": [ + "class-variance-authority", + "clsx", + "radix-ui", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "components/ui/progress.tsx", + "type": "registry:component", + "target": "components/ui/progress.tsx" + } + ] + }, + { + "name": "item", + "type": "registry:component", + "title": "Item", + "description": "A versatile row for lists and settings: media, title, description and actions, plain or on bordered or borderless glass.", + "dependencies": [ + "class-variance-authority", + "clsx", + "radix-ui", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/separator", + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "components/ui/item.tsx", + "type": "registry:component", + "target": "components/ui/item.tsx" + } + ] + }, + { + "name": "field", + "type": "registry:component", + "title": "Field", + "description": "Form field layout (label, control, description, error) in vertical, horizontal or responsive orientation, with fieldsets, legends and separators.", + "dependencies": [ + "class-variance-authority", + "clsx", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/label", + "@sistine/separator", + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "components/ui/field.tsx", + "type": "registry:component", + "target": "components/ui/field.tsx" + } + ] + }, + { + "name": "menubar", + "type": "registry:component", + "title": "Menubar", + "description": "A desktop-style menu bar on Radix Menubar: a bordered glass bar whose menus and sub menus are veiled glass overlays.", + "dependencies": [ + "@phosphor-icons/react", + "class-variance-authority", + "clsx", + "radix-ui", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "lib/snap-popper.ts", + "type": "registry:lib", + "target": "lib/snap-popper.ts" + }, + { + "path": "components/ui/menubar.tsx", + "type": "registry:component", + "target": "components/ui/menubar.tsx" + } + ] + }, + { + "name": "combobox", + "type": "registry:component", + "title": "Combobox", + "description": "An input with a filtered list of options, single or multiple with chips, on Base UI's Combobox with a veiled glass popup.", + "dependencies": [ + "@base-ui/react@^1.9.0", + "@phosphor-icons/react", + "class-variance-authority", + "clsx", + "tailwind-merge" + ], + "registryDependencies": [ + "@sistine/button", + "@sistine/input-group", + "@sistine/theme" + ], + "files": [ + { + "path": "lib/utils.ts", + "type": "registry:lib", + "target": "lib/utils.ts" + }, + { + "path": "lib/material.ts", + "type": "registry:lib", + "target": "lib/material.ts" + }, + { + "path": "lib/hover-effects.ts", + "type": "registry:lib", + "target": "lib/hover-effects.ts" + }, + { + "path": "components/ui/combobox.tsx", + "type": "registry:component", + "target": "components/ui/combobox.tsx" + } + ] + }, { "name": "dashboard", "type": "registry:block", diff --git a/registry/theme/globals.css b/registry/theme/globals.css index 96ac735e..7cc3488b 100644 --- a/registry/theme/globals.css +++ b/registry/theme/globals.css @@ -6,8 +6,9 @@ engine default by SOURCE ORDER at equal specificity), - inside tokens.css, .dark comes after :root (same-element ties resolve by source order). Consumers install a FLATTENED single-file build of this graph (registry/theme/globals.css, generated - by scripts/build-theme.mjs) — run `pnpm registry:check` after editing any partial and commit - registry/theme + public/r. + by scripts/build-theme.mjs) — run `bun run registry:check` after editing any partial and commit + registry/theme + public/r. Site-only CSS (the Geist font mapping) lives in app/site.css, which imports + this file, so nothing the documentation site defines leaks into the shipped theme. */ @import "tailwindcss"; @import "tw-animate-css"; @@ -20,14 +21,35 @@ @custom-variant dark (&:where(.dark, .dark *)); /* Composable, NON-inheriting glow layer: the `glow` effect/prop sets it, and every glass box-shadow - folds in `var(--glow-layer, …)` so the glow rides INSIDE each style's shadow — surviving the - per-style `!important` box-shadow overrides instead of being replaced. inherits:false keeps a - glowing surface from lighting up its glass children. */ + folds in `var(--glow-layer, …)` so the glow rides INSIDE each material's and page style's shadow + (they set --srf-shadow; the glow is appended after it) instead of being replaced by it. inherits:false + keeps a glowing surface from lighting up its glass children. */ @property --glow-layer { syntax: "*"; inherits: false; } +/* The veil floor, NON-inheriting for the same reason as the glow: `glass-veil` sets it on the element + and `glass` paints it as that element's background-color. Inherited, a plain glass surface nested in + a veiled menu, popover, tooltip, select or card painted its PARENT's veil as its own floor (under the + Opaque page style: flat white in light, black in dark). A child without a veil of its own now falls + back to its material's --srf-bg-color, as it would anywhere else. */ +@property --veil-floor { + syntax: "*"; + inherits: false; +} +/* Its twin for the solidify layer (see @utility glass-veil): a veiled parent must not restore the layer + on a child that dropped it. */ +@property --veil-solidify { + syntax: "*"; + inherits: false; +} +/* The ink border colour (glass-border-ink): an inked panel must not ink the borders of the glass inside it. */ +@property --brd-ink { + syntax: "*"; + inherits: false; +} + /* Gradient-text tokens are consumed ONLY from JS inline styles (components/gradient-text.tsx) — no CSS rule references them, so Lightning CSS's optimizer would silently drop them (the same failure mode as the harmonic tokens below). Registered here to pin them. DO NOT REMOVE. */ @@ -103,8 +125,6 @@ --color-foreground-soft: var(--foreground-soft); --color-foreground-strong: var(--foreground-strong); --color-foreground-ui: var(--foreground-ui); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -194,6 +214,10 @@ --glass-sheet-a1: 0.04; /* the sheet's TOP-stop offset (dark tapers it to 0.03) */ --glass-frost-boost: 0.1; /* frosted = the sheet lifted by this much */ --glass-border-a: 0.16; /* glass edge alpha — a white rim needs more presence on light backdrops */ + /* Ink border alpha (borderColor="ink" / glass-border-ink): how much of --foreground the line takes. + Mode-invariant because --foreground already flips polarity per mode; 0.5 keeps the line clearly + softer than text while clearing the APCA non-text floor on every preset (check-contrast [ink]). */ + --glass-border-ink-a: 0.5; /* Crystal gloss defaults (the demo's Light/Tint sliders override inline). --glass-gloss-tint is mode-shared; --glass-gloss-l is NOT — see its twin below. AutoForeground's crystal tiers mirror --glass-gloss-l (97 light / 66 dark) as their snapshot-path fallback — keep all three in step. */ @@ -446,9 +470,10 @@ it drops to 76.0 Lc, under the floor+margin, while L93 holds 76.9. */ --accent-bg-l: 0.93; --accent-bg-c-max: 0.032; /* ≤ 0.0323, the L-93 ceiling floor (lapis h268) */ - /* Stays --foreground: this token is text on a SURFACE-lightness background, and it has two callers with - that shape — the accent fill here, and the bg-foreground/10 wash the menus use (dropdown, context, - navigation, command, menubar, select). They only share a text colour while --accent stays near the + /* Stays --foreground: this token is text on a SURFACE-lightness background. Its callers all have that + shape — the ghost button's hover, toggles, and the highlighted row of every menu (dropdown, context, + navigation, command, menu-bar, select), which paint bg-accent so a consumer's --accent reaches them + (the shadcn contract). Text and fill only stay legible together while --accent sits near the surface, which is precisely why the fill was moved by chroma rather than by weight. */ --accent-foreground: var(--foreground); @@ -721,10 +746,12 @@ } /* ── Below the floor ────────────────────────────────────────────────────────── - package.json's browserslist is the supported floor (Safari 17.5 / Chrome 120 / Firefox 128 / Edge - 120). It is set by what this theme actually uses, not by preference — the binding constraints are - text-wrap:balance (Safari 17.5), bare-`&` CSS nesting and unprefixed mask-composite (Chrome 120), - and @property (Firefox 128). Tailwind v4's own baseline sits just under it. + The supported floor is Safari 17.5 / Chrome 120 / Firefox 128 / Edge 120, declared as the browserslist + in SISTINE's package.json. It is set by what this theme actually uses, not by preference — the binding + constraints are text-wrap:balance (Safari 17.5), bare-`&` CSS nesting and unprefixed mask-composite + (Chrome 120), and @property (Firefox 128). Tailwind v4's own baseline sits just under it. That + browserslist does not ship with this file: a consuming app should declare its own, at or above the + floor, or its toolchain targets whatever its defaults are. Every colour in this theme is oklch(), fed through custom properties. On an engine without oklch the custom property still PARSES (custom props accept anything) and only fails when substituted — so @@ -746,6 +773,7 @@ color: #17161c; } .glass, + [data-surface], [data-material] { background-color: #ffffffe6; border: 1px solid #17161c26; @@ -753,7 +781,7 @@ } } /* ── Glass engine ───────────────────────────────────────────────────────────── - TWO grouped blocks, split by what must re-resolve where (the var-composition-resolves-at-declaration + THREE grouped blocks, split by what must re-resolve where (the var-composition-resolves-at-declaration gotcha — see scripts/check-theme.mjs): A. TINT ANCHORS (:root, [data-glass-tint], …) — the tint knobs plus everything mode-INVARIANT that @@ -767,12 +795,14 @@ scopes. `.dark` IS in this selector so a dark wrapper inside a light page re-composes its surfaces from the wrapper's own dark knobs (same-element cascade) — "toggle .dark anywhere" keeps working. + C. GLOSS (:root, .dark, [data-glass-tint], [data-gloss]) — the crystal gloss and the stacks that bake + it, which also have to recompose wherever a `data-gloss` flavor is set. See block C itself. + Foreground tokens stay on bare :root/.dark (AutoForeground owns them). Preset blocks below override --glass-tint-h/c/a by source order. Shadow composites are mode-TWINNED on bare :root/.dark (their light↔dark ratios are irregular — knobbing them would change values). */ :root, -[data-glass-tint], -[data-sonner-toast][data-type] { +[data-glass-tint] { /* Deliberately restrained glassmorphism — enough to read as glass, never enough to fog what is behind it. */ /* Glass tint — recolor ALL glass by changing these; presets set them via [data-glass-tint]. -h = hue, -c = chroma (gradient), -a = wash alpha. The gradient alone is too sheer @@ -814,8 +844,10 @@ land at the sRGB edge at some hues and get browser-gamut-mapped — prefer the hue tokens + a conservative chroma when that matters. */ /* Harmonic anchor hue — the wheel origin the harmonies rotate from. Normally the content hue, but the two - "hue-less" themes (selenite + moonstone) anchor at 0° instead — set by the tint switcher (selenite) + moonstone's - block below — so their accents come out a colorful red-based harmony, not a warp of a near-grey hue. */ + "hue-less" themes (selenite + moonstone) anchor at 0° instead — each preset block in presets.css declares + it, so a CSS-only consumer gets the anchor too (the tint switcher also inlines it, for a custom tint + dragged to chroma 0) — and their accents come out a colorful red-based harmony, not a warp of a + near-grey hue. */ --harmony-h: var(--glass-fg-h); --hue-base: var(--harmony-h); --hue-complement: calc(var(--harmony-h) + 180); @@ -853,17 +885,9 @@ /* Crystal edge — identical in both modes, so it lives here with the anchors (the crystal floor + shadows are mode-varying: --glass-crystal-bg in the surfaces block below, shadows on :root/.dark). */ --glass-crystal-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / 0.3); - /* Crystal gloss — the 3-part light effect (top highlight + diagonal streak + bottom glow) baked into - --crystal-stack-bg (the crystal material + the [data-glass="crystal"] page style) and layered by - glass-sheen. Each is a full gradient, so it composes in a background-image list AND in pseudo layers. - THREE FLAVORS, selected by data-gloss on any ancestor (default, unset = tonal — see the [data-gloss] - blocks further down): - white → flat white specular (the classic glass shine) - tonal → a tonally-close single-hue tint of the theme (--glass-gloss-l / --glass-gloss-tint) - hue → iridescent: the highlight sweeps hues shifted ± --glass-gloss-hue-span around the tint hue - White/tonal share the gradient SHAPES below (only --glass-gloss-ink, the L C H triple, differs); hue - redefines the three gradients to interpolate across hue. Each layer applies its own alpha. */ - /* --glass-gloss-l / --glass-gloss-tint live in tokens.css (66 / 4.25) — do NOT re-declare here: + /* Crystal gloss KNOBS — leaves only. The gloss itself (ink, the three gradients) and every stack that + bakes it are composed in block C below, which also recomposes at each [data-gloss] scope. + --glass-gloss-l / --glass-gloss-tint live in tokens.css (66 / 4.25) — do NOT re-declare here: engine.css imports AFTER tokens.css, so a duplicate at equal :root specificity silently wins and pure-CSS consumers get a stale gloss. */ --glass-gloss-hue-span: 40; /* hue: degrees the iridescent sweep shifts up & down from the tint hue */ @@ -875,15 +899,6 @@ renders what it asks for. */ --glass-gloss-c: 0.12; --glass-gloss-hue-dir: 1; /* gloss direction (±1) — flips the TONAL streak diagonal (calc angle) + the HUE sweep order (span × dir); toggled in the crystal demo */ - --glass-gloss-ink: calc(var(--glass-gloss-l, 94) * 1%) min(calc(var(--glass-tint-c) * var(--glass-gloss-tint, 2)), var(--glass-gloss-c-max, 0.3)) - var(--glass-tint-h); - --glass-gloss-top: linear-gradient(to bottom, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.4), transparent 30%); - --glass-gloss-streak: linear-gradient( - calc(135deg * var(--glass-gloss-hue-dir, 1)), - oklch(var(--glass-gloss-ink, 100% 0 0) / 0.15), - transparent 50% - ); - --glass-gloss-glow: radial-gradient(circle at 50% 120%, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.2), transparent 70%); --blur-crystal: 2px; } @@ -892,8 +907,7 @@ re-composing with their own knobs. */ :root, .dark, -[data-glass-tint], -[data-sonner-toast][data-type] { +[data-glass-tint] { /* Solid colored floor under the sheer gradient (see --glass-tint-a) — mode via --glass-wash-l/-c-mult. */ --glass-tint-wash: oklch(var(--glass-wash-l) calc(var(--glass-tint-c) * var(--glass-wash-c-mult)) var(--glass-tint-h) / var(--glass-tint-a)); /* Solid accent that tracks the FOREGROUND hue at a fixed vivid chroma — used by active controls @@ -1030,18 +1044,7 @@ oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) calc(var(--glass-fg-h) + 63.53)) 0%, oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) var(--glass-fg-h)) 100% ); - /* Crystal floor — clear glass; the alpha is the mode dial (--glass-crystal-bg-a). */ - /* Crystal body. The lightness is a KNOB, not 100%, and that is the whole point: at L100 sRGB holds - ZERO chroma, so this surface asked for 0.015 and delivered exactly none — crystal rendered pure - white under every preset, in both modes (measured: #fffdff amethyst, #fffef4 moonstone, - #fbffff lapis; RGB spread 2–11). check-gamut even reported it as "no attainable chroma at all" - and skipped the pair rather than failing it. - --glass-crystal-l is 96 because that is the lowest lightness at which the TIGHTEST of the 21 hues - this theme uses (h268, lapis) can still carry the 0.015 this asks for: ceiling 0.0182, 1.22x - headroom. L96.5 is the exact crossover at 1.06x, too thin given maxChromaFor already sits 3% inside - the boundary to stop Safari clamping toward grey. The cost is small — at the 0.3 light-mode alpha - the composite moves ~1.2 L, and 0.4 L in dark. */ - --glass-crystal-bg: oklch(calc(var(--glass-crystal-l) * 1%) calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / var(--glass-crystal-bg-a)); + /* (The crystal floor, --glass-crystal-bg, is in block C: the hue gloss flavor repaints it.) */ /* ── Chakra — the step-cut gem surface ──────────────────────────────────────────────────────── BODY: the stone itself. The one tint-composing token here, so it is the only one with a gamut budget — min() against --glass-chakra-c-max (not a multiplier) for the same reason --border uses @@ -1052,12 +1055,7 @@ calc(var(--glass-chakra-l) * 1%) min(var(--glass-tint-c), var(--glass-chakra-c-max)) var(--glass-tint-h) / var(--glass-chakra-a) ); - /* SURFACE STACK: the same gloss triple crystal bakes, so `data-gloss` white / tonal / hue reskin - chakra exactly as they reskin crystal (those blocks remap --glass-gloss-*, which these read), over - a fresco slot and the tint wash. Nothing here is inset, so nothing here has a corner of its own — - the element's border-radius clips the whole stack, which is the point (see the facet bands below). */ - --glass-chakra-stack-bg: - var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer); + /* (The chakra SURFACE STACK, --glass-chakra-stack-bg, is in block C: it bakes the gloss.) */ /* THE CUT — four sides, two steps, as INSET SHADOWS rather than inset background layers. A background layer is always a RECTANGLE: background-size shrinks it but gives it no radius of its own, so an inset gradient paints square corners over a rounded surface — a card with hard-cornered @@ -1097,8 +1095,122 @@ /* Frosted rides the wash in the IMAGE stack (sheet + wash), leaving the background-color channel free for the veil floor. */ --frosted-stack-bg: var(--glass-frosted-bg), var(--glass-wash-layer); + /* (--crystal-stack-bg is in block C: it bakes the gloss.) */ +} + +/* C. GLOSS — the crystal highlight and everything that bakes it: the ink, the three gradients, the + crystal floor, and the crystal + chakra stacks. Declared on every scope that can CHANGE it: :root and + .dark (the mode knobs), [data-glass-tint] (the tint), and [data-gloss] (the flavor). The last is what + lets `data-gloss` work below : these tokens are composed values, which bake where they are + declared, so a [data-gloss] section that only re-pinned an input (as the flavor blocks used to) had + its children inherit 's gradients and stacks pre-composed, and painted the page's gloss. + THREE FLAVORS, chosen by two plain switches the [data-gloss] blocks in materials.css set to `1` or + `initial` (unset = tonal): + white → flat white specular (the classic glass shine) --glass-gloss-is-white: 1 + tonal → a tonally-close single-hue tint of the theme (--glass-gloss-l / --glass-gloss-tint) + hue → iridescent: the highlight sweeps hues shifted ± --glass-gloss-hue-span around the tint + hue, and repaints the crystal floor in the sweep --glass-gloss-is-hue: 1 + A switch selects nothing by itself: each flavor's variant (block C′ below) multiplies an alpha by it, + so with the switch `initial` the variant is invalid at computed-value time and the var() fallback here, + the tonal shape, applies. The switches are leaves, so they inherit, and a nested [data-gloss] sets both + to pick its own flavor; a nested [data-glass-tint] recomposes from the inherited switches and keeps the + page's flavor (it used to reset it to tonal). Each layer applies its own alpha. */ +:root, +.dark, +[data-glass-tint], +[data-gloss] { + /* The tonal ink as its own token (not inline in the fallback below) so check-gamut's scraper, which + reads `--x: ` declarations, still sweeps it. */ + --glass-gloss-ink-tonal: calc(var(--glass-gloss-l, 94) * 1%) + min(calc(var(--glass-tint-c) * var(--glass-gloss-tint, 2)), var(--glass-gloss-c-max, 0.3)) var(--glass-tint-h); + --glass-gloss-ink: var(--glass-gloss-ink-white, var(--glass-gloss-ink-tonal)); + --glass-gloss-top: var(--glass-gloss-top-hue, linear-gradient(to bottom, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.4), transparent 30%)); + --glass-gloss-streak: var( + --glass-gloss-streak-hue, + linear-gradient(calc(135deg * var(--glass-gloss-hue-dir, 1)), oklch(var(--glass-gloss-ink, 100% 0 0) / 0.15), transparent 50%) + ); + --glass-gloss-glow: var( + --glass-gloss-glow-hue, + radial-gradient(circle at 50% 120%, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.2), transparent 70%) + ); + /* Crystal floor — clear glass; the alpha is the mode dial (--glass-crystal-bg-a). + Crystal body. The lightness is a KNOB, not 100%, and that is the whole point: at L100 sRGB holds + ZERO chroma, so this surface asked for 0.015 and delivered exactly none — crystal rendered pure + white under every preset, in both modes (measured: #fffdff amethyst, #fffef4 moonstone, + #fbffff lapis; RGB spread 2–11). check-gamut even reported it as "no attainable chroma at all" + and skipped the pair rather than failing it. + --glass-crystal-l is 96 because that is the lowest lightness at which the TIGHTEST of the 21 hues + this theme uses (h268, lapis) can still carry the 0.015 this asks for: ceiling 0.0182, 1.22x + headroom. L96.5 is the exact crossover at 1.06x, too thin given maxChromaFor already sits 3% inside + the boundary to stop Safari clamping toward grey. The cost is small — at the 0.3 light-mode alpha + the composite moves ~1.2 L, and 0.4 L in dark. */ + --glass-crystal-bg: var( + --glass-crystal-bg-hue, + oklch(calc(var(--glass-crystal-l) * 1%) calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / var(--glass-crystal-bg-a)) + ); + /* SURFACE STACKS: crystal and chakra bake the same gloss triple, so `data-gloss` white / tonal / hue + reskin both, over a fresco slot and the tint wash. Nothing in the chakra stack is inset, so nothing + has a corner of its own — the element's border-radius clips the whole stack (see the facet bands in + block B). Baked into the background, not ::before/::after: the crystal PAGE style re-skins every + surface, and pseudo layers would need position:relative, which breaks sticky/fixed surfaces. */ --crystal-stack-bg: var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer); + --glass-chakra-stack-bg: + var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer); +} + +/* C′. The FLAVOR VARIANTS block C falls back from, each valid only while its switch is `1`. Kept in a + sibling block (same scopes) so scripts/check-gamut.mjs can model the hue variants at the hue flavor's + own L74 (the [data-gloss="hue"] block pins --glass-gloss-l there) without misreading block C's tonal + ink at it. + hue: each highlight sweeps (tint hue − span) → tint hue → (+ span), interpolated IN OKLCH so the arc + passes through the base hue. Top leans to the low end, glow to the high end, streak spans both. The + floor is replaced by a hue-carrying one at the gloss lightness (so Light deepens it): the default + crystal floor sits near white, where chroma collapses, so the sweep would read over a white sheet. */ +:root, +.dark, +[data-glass-tint], +[data-gloss] { + --glass-gloss-ink-white: calc(var(--glass-gloss-is-white) * 100%) 0 0; + --glass-gloss-top-hue: linear-gradient( + to bottom in oklch, + oklch( + calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) + calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / + calc(0.4 * var(--glass-gloss-is-hue)) + ), + transparent 35% + ); + --glass-gloss-streak-hue: linear-gradient( + 115deg in oklch, + oklch( + calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) + calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / + calc(0.32 * var(--glass-gloss-is-hue)) + ) + 0%, + oklch(calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) var(--glass-tint-h) / calc(0.12 * var(--glass-gloss-is-hue))) 45%, + oklch( + calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) + calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / + calc(0.32 * var(--glass-gloss-is-hue)) + ) + 85%, + transparent + ); + --glass-gloss-glow-hue: radial-gradient( + circle at 50% 120%, + oklch( + calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) + calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / + calc(0.25 * var(--glass-gloss-is-hue)) + ), + transparent 70% + ); + --glass-crystal-bg-hue: oklch( + calc(var(--glass-gloss-l, 94) * 1%) calc(var(--glass-tint-c) * 1.2) var(--glass-tint-h) / + calc(0.4 * var(--glass-gloss-is-hue)) + ); } /* ── Glass surface utilities (@utility — tree-shaken per consumer usage) ────── The material system: ONE structural `glass` utility + orthogonal axis classes (border, veil, sm/lg, @@ -1136,9 +1248,12 @@ gradient rasteriser (and its dither) to deliver zero pixels. The solidify floor (--glass-opacity dial) is composed HERE, at the element — never in a token context (the var-composition-resolves-at-declaration gotcha; see scripts/check-theme.mjs). The - --srf-solidify slot lets a material DROP that layer when it is provably a no-op; materials that - want the normal floor pin the slot back to `initial` (guaranteed-invalid → the var() fallback - below composes --glass-solidify at the element, exactly as before). */ + --srf-solidify slot lets a material DROP that layer when it is provably a no-op (the opaque material, + and the opaque PAGE style, which sets it on its own scope so it reaches `@apply glass` surfaces that + carry no `.glass` class); materials that want the normal floor pin the slot back to `initial` + (guaranteed-invalid → the var() fallback composes --glass-solidify at the element). --veil-solidify + leads the chain: glass-veil sets it (non-inheriting), because a VEILED surface paints the neutral + veil floor in its background-colour channel, so the layer is real there whatever the slot says. */ @utility glass { /* Fallback 0.7, not 0: every glass surface now sits on a 70% solidify floor unless a consumer dials it back. Measured before changing it, because AutoForeground does NOT read --glass-opacity — it @@ -1156,7 +1271,8 @@ what --accent was set to. No amount of tuning the token could fix that; it is a layer-order problem. Same no-op `none` slot pattern as --acc-gradient below it. */ background-image: - var(--srf-hover, none), var(--acc-gradient, none), var(--srf-bg-image, var(--glass-stack-bg)), var(--srf-solidify, var(--glass-solidify)); + var(--srf-hover, none), var(--acc-gradient, none), var(--srf-bg-image, var(--glass-stack-bg)), + var(--veil-solidify, var(--srf-solidify, var(--glass-solidify))); background-color: var(--veil-floor, var(--srf-bg-color, transparent)); /* Author ONLY the standard backdrop-filter (no -webkit- twin: Lightning dedupes the pair keeping the PREFIXED one, which Chromium never aliased — the [no-webkit-twin] invariant guards this). @@ -1165,10 +1281,11 @@ box-shadow: var(--srf-shadow, var(--srf-elev, var(--glass-shadow))), var(--glow-layer, 0 0 0 0 transparent); } -/* Border axis — the material picks the color; width resolves material pin (--srf-border-w, which - itself reads the --brd-w weight slot) → weight slot directly (adaptive surfaces) → 1px hairline. */ +/* Border axis — the material picks the color unless the element asks for ink (--brd-ink, the + borderColor axis below); width resolves material pin (--srf-border-w, which itself reads the --brd-w + weight slot) → weight slot directly (adaptive surfaces) → 1px hairline. */ @utility glass-border { - border: var(--srf-border-w, var(--brd-w, 1px)) solid var(--srf-border-color, var(--glass-border)); + border: var(--srf-border-w, var(--brd-w, 1px)) solid var(--brd-ink, var(--srf-border-color, var(--glass-border))); } /* Border WEIGHTS — token setters for the --brd-w slot (order-safe; same-element composition, like glass-sm's --srf-blur). Hairline is the unmarked default (1px; 0.5px under frosted); rim = 2px, frame = 4px. */ @@ -1178,6 +1295,15 @@ @utility glass-border-frame { --brd-w: 4px; } +/* Border COLOUR — ink. The material edge is a near-white rim at the material's alpha: it reads as cut + glass on a dark page and as nothing at all on most light themes, at hairline, rim or frame weight + alike, so a weight axis whose colour disappears went unused. Ink is the element's own text colour + (--foreground, after any per-material text remap on this element), mixed toward transparent by + --glass-border-ink-a: a visible line anywhere text is legible. Element-composed and registered + non-inheriting (registrations.css), so a child border keeps its own material colour. */ +@utility glass-border-ink { + --brd-ink: color-mix(in oklch, var(--foreground) calc(var(--glass-border-ink-a, 0.5) * 100%), transparent); +} /* Diffuse axis — a READABILITY BLUR FLOOR (not a blur value): every translucent material computes blur(max(, --dif)), so crystal stays crystal until a text-dense surface opts in. @@ -1201,6 +1327,9 @@ @utility glass-veil { --veil-floor: oklch(var(--glass-solid-l) 0 0 / var(--glass-solid-a)); + /* The veil keeps the solidify layer even where the material or page dropped it (--srf-solidify: none): + those drops are only no-ops over the opaque floor, and a veiled surface paints the veil instead. */ + --veil-solidify: var(--glass-solidify); } /* THE PAGE BACKDROP LAYER — the fixed, full-viewport ground every sheer material blurs. Put this on the @@ -1293,12 +1422,18 @@ } } -@keyframes shimmer { - 0% { - transform: translateX(-100%); +/* Namespaced: a bare `shimmer` is a global name, and a consumer's own @keyframes shimmer (or another + library's) replaced this one for everyone, whichever stylesheet loaded last. Animates `translate`, not + `transform`: Tailwind v4's `-translate-x-full` (the skeleton's resting position) writes the `translate` + property, which COMPOSES with an animated transform rather than being replaced by it, so the sweep ran + from -200% to 0% and parked the highlight on top of the skeleton. Animating the same property replaces + the resting value while running, and with the animation off it falls back to that resting value. */ +@keyframes sistine-shimmer { + from { + translate: -100% 0; } - 100% { - transform: translateX(100%); + to { + translate: 100% 0; } } @@ -1320,8 +1455,8 @@ rule outranks any layered one regardless of order, so this needs neither !important nor a specificity hack. */ @media (prefers-reduced-motion: reduce) { - .animate-\[shimmer_2s_infinite\], - .before\:animate-\[shimmer_2s_infinite\]::before { + .animate-\[sistine-shimmer_2s_infinite\], + .before\:animate-\[sistine-shimmer_2s_infinite\]::before { animation: none; } @@ -1337,8 +1472,8 @@ NOTE these are inherited custom properties — a data-material element also acts as a SCOPED default for adaptive `glass` descendants (the mechanism DialogContent's old data-glass hack hand-rolled). Pin a child back with its own data-material. - Import order: after engine.css (the [data-gloss] blocks must beat the engine's gloss defaults by - source order at equal specificity on ). */ + Import order: after tokens.css — [data-gloss="hue"] re-pins --glass-gloss-l and the crystal shadows, + which tokens.css declares on :root/.dark at equal specificity, so source order decides on . */ [data-material="glass"] { /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify @@ -1393,7 +1528,7 @@ /* Crystal text tiers — the baked gloss locally LIGHTENS the title zone (worst in dark mode), so text inside crystal remaps to the -crystal set AutoForeground bands against the glossed crystal surface. VEILED crystal is excluded: the veil floor is exactly what the normal tiers are banded for. */ -[data-material="crystal"]:not(.glass-veil) { +[data-material="crystal"]:not(.glass-veil, [data-surface="veil"]) { --foreground: var(--foreground-crystal); --foreground-soft: var(--foreground-soft-crystal); --foreground-strong: var(--foreground-strong-crystal); @@ -1430,7 +1565,7 @@ /* Chakra text tiers — content sits on the body, whose lightness (--glass-chakra-l) is its own dial and can sit well away from the page's. Same reasoning as opaque/crystal; veiled chakra is excluded because the veil floor is exactly what the NORMAL tiers are banded for. */ -[data-material="chakra"]:not(.glass-veil) { +[data-material="chakra"]:not(.glass-veil, [data-surface="veil"]) { --foreground: var(--foreground-chakra); --foreground-soft: var(--foreground-soft-chakra); --foreground-strong: var(--foreground-strong-chakra); @@ -1447,9 +1582,10 @@ background-COLOR of the same --glass-opaque-bg at alpha 1 — i.e. C over C, which composites to C for ANY alpha. Measured across tints and both modes it moves every channel by exactly 0. So the layer was one full gradient rasterisation (and one dither pass) for provably zero pixels, on the one material - with no backdrop-filter behind it to mask that noise. Restored below when the surface is VEILED — - there the background-color is the neutral veil floor, not --glass-opaque-bg, so the layer is real - (dropping it there would shift up to 31/255 in light and 42/255 in dark). */ + with no backdrop-filter behind it to mask that noise. Restored when the surface is VEILED (glass-veil + sets --veil-solidify, which `glass` reads first) — there the background-color is the neutral veil + floor, not --glass-opaque-bg, so the layer is real (dropping it there would shift up to 31/255 in + light and 42/255 in dark). */ --srf-solidify: none; /* NO sheet either — opaque now paints a FLAT background-color and nothing else. --glass-bg is a 5-stop gradient whose total range, composited over this floor, is 2–4 sRGB codes across an entire card in @@ -1478,26 +1614,28 @@ --muted-foreground: var(--muted-foreground-opaque); } -/* Veiled opaque restores the solidify floor: glass-veil moves the background-COLOR channel to the - neutral --veil-floor, so the layer is no longer painting --glass-opaque-bg over itself and carries - the surface's entire tint. Mirrors crystal's `:not(.glass-veil)` carve-out. */ -[data-material="opaque"].glass-veil { - --srf-solidify: initial; -} - /* ── Gloss flavors ──────────────────────────────────────────────────────────── - data-gloss on (or any ancestor) swaps the crystal gloss. Default (unset) = tonal. Token - remaps only — they compose with [data-material="crystal"] AND the [data-glass="crystal"] page - style, in both modes. */ + data-gloss on or ANY element swaps the crystal (and chakra) gloss for that subtree. Default + (unset) = tonal. These blocks set LEAVES only — the two flavor switches, plus the hue flavor's own + lightness and shadows — and engine.css block C recomposes the gloss on every [data-gloss] element from + them. That split is what makes a scoped `

    ` work: a composed value bakes + where it is declared, so a block here that declared one would hand its children 's gloss. + Each block sets BOTH switches, so a nested flavor overrides an ancestor's instead of combining with it. + They compose with [data-material="crystal"] AND the [data-glass="crystal"] page style, in both modes. + Known edge: hue's pinned --glass-gloss-l and bevel-less crystal shadows are mode-twinned leaves that a + white/tonal section nested INSIDE a hue scope inherits (no mode-aware reset exists for them here). */ [data-gloss="white"] { - --glass-gloss-ink: var(--shadow-highlight); /* 100% 0 0 — the classic flat white specular */ + --glass-gloss-is-white: 1; /* the ink becomes the flat white specular, 100% 0 0 */ + --glass-gloss-is-hue: initial; } [data-gloss="tonal"] { - /* Explicit tonal (also the default) — re-asserted so it can override a white/hue set on an ancestor. */ - --glass-gloss-ink: calc(var(--glass-gloss-l, 94) * 1%) min(calc(var(--glass-tint-c) * var(--glass-gloss-tint, 2)), var(--glass-gloss-c-max, 0.3)) - var(--glass-tint-h); + /* Explicit tonal (also the default) — both switches off, so it can override a white/hue ancestor. */ + --glass-gloss-is-white: initial; + --glass-gloss-is-hue: initial; } [data-gloss="hue"] { + --glass-gloss-is-white: initial; + --glass-gloss-is-hue: 1; /* Hue OWNS its lightness rather than sharing the tonal twin. The two flavors want opposite things from the same number: tonal wants the brightest value the surface allows (97 in light) so it reads as a specular, while hue needs chroma headroom — its stops ask --glass-gloss-c 0.16, and the sRGB @@ -1511,49 +1649,10 @@ ceiling. Measured in WebKit it CLIPS instead (see --glass-gloss-c-max in tokens.css), so 0.16 did not deliver 81% of itself — it drove the tightest hues to a pinned channel. 0.12 sits just under 0.1240, the L-74 ceiling for turquoise (h190) and the tightest of the presets, so the sweep now - renders every hue it asks for. */ + renders every hue it asks for. scripts/check-gamut.mjs reads this 74 to score the variants. */ --glass-gloss-l: 74; - /* Iridescent — each highlight sweeps (tint hue − span) → tint hue → (+ span), interpolated IN OKLCH so the - arc passes through the base hue. Top leans to the low end, glow to the high end, streak spans both. */ - --glass-gloss-top: linear-gradient( - to bottom in oklch, - oklch( - calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) - calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / - 0.4 - ), - transparent 35% - ); - --glass-gloss-streak: linear-gradient( - 115deg in oklch, - oklch( - calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) - calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / - 0.32 - ) - 0%, - oklch(calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) var(--glass-tint-h) / 0.12) 45%, - oklch( - calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) - calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / - 0.32 - ) - 85%, - transparent - ); - --glass-gloss-glow: radial-gradient( - circle at 50% 120%, - oklch( - calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) - calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) / - 0.25 - ), - transparent 70% - ); - /* Kill the white: the default crystal floor is oklch(100% …) = PURE white (chroma collapses at L100), and - the crystal shadow carries a white inset bevel. In the iridescent flavor, replace the floor with a - hue-carrying one at the gloss lightness (so Light deepens it) and drop the bevel — leaving only swept hue. */ - --glass-crystal-bg: oklch(calc(var(--glass-gloss-l, 94) * 1%) calc(var(--glass-tint-c) * 1.2) var(--glass-tint-h) / 0.4); + /* Kill the white bevel: the crystal shadow carries a white inset highlight, which would sit on top of + the swept hue. (The white floor is replaced in engine.css block C′.) */ --glass-crystal-shadow: var(--glass-shadow); --glass-crystal-shadow-hover: var(--glass-shadow-lg); } @@ -1573,15 +1672,14 @@ data-material) by remapping the --srf-* tokens it reads. An element-level [data-material] pins its own tokens and beats these by cascade physics — reinforced by the :where() wrapper, which drops every remap (incl. its cross-product legs) to specificity (0,0,0), so a co-located data-material on - a toast or tint-scope element always wins. The cross-product legs keep the swapped stacks - re-resolving inside scoped tints and status toasts (a bare [data-glass] rule would bake 's - tint into descendants' surfaces). Veiled surfaces (glass-veil) keep their element-composed floor. */ -:where( - [data-glass="frosted"], - [data-glass="frosted"] [data-glass-tint], - [data-glass="frosted"][data-glass-tint], - [data-glass="frosted"] [data-sonner-toast][data-type] -) { + a tint-scope element always wins. The cross-product legs keep the swapped stacks re-resolving inside + scoped tints, status toasts included (a bare [data-glass] rule would bake 's tint into + descendants' surfaces). Veiled surfaces (glass-veil) keep their element-composed floor. + The ELEMENT-level rules further down (text tiers, the opaque edge) need to know which elements are + surfaces. `.glass` says so for the utility class; an element that wears the material through + `@apply glass` has no class, so it opts in with a `data-surface` attribute (`data-surface="veil"` + when it also applies glass-veil). Everything ABOVE is inherited tokens and needs no marker. */ +:where([data-glass="frosted"], [data-glass="frosted"] [data-glass-tint], [data-glass="frosted"][data-glass-tint]) { --srf-bg-image: var(--frosted-stack-bg); /* NO --srf-bg-color wash here: the wash already rides the IMAGE stack (--frosted-stack-bg), same as the frosted material — a color-channel copy double-washed tinted presets and stole the veil @@ -1591,12 +1689,7 @@ --srf-border-w: var(--brd-w, 0.5px); --srf-shadow: var(--glass-frosted-shadow); } -:where( - [data-glass="crystal"], - [data-glass="crystal"] [data-glass-tint], - [data-glass="crystal"][data-glass-tint], - [data-glass="crystal"] [data-sonner-toast][data-type] -) { +:where([data-glass="crystal"], [data-glass="crystal"] [data-glass-tint], [data-glass="crystal"][data-glass-tint]) { /* Gloss rides inside --crystal-stack-bg (baked — sticky/fixed-safe). Shimmer stays the opt-in glass-sheen. No saturate(), matching the material. */ --srf-bg-image: var(--crystal-stack-bg); @@ -1607,7 +1700,7 @@ } /* Crystal page: ADAPTIVE glass (no explicit material, not veiled) gets the -crystal text tiers, same as the material remap in materials.css — the baked gloss lightens title zones there too. */ -[data-glass="crystal"] :where(.glass):not([data-material]):not(.glass-veil) { +[data-glass="crystal"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface="veil"]) { --foreground: var(--foreground-crystal); --foreground-soft: var(--foreground-soft-crystal); --foreground-strong: var(--foreground-strong-crystal); @@ -1616,12 +1709,7 @@ } /* Chakra page — every adaptive surface becomes a step-cut gem. The cut rides --srf-shadow, so it follows each surface's own border-radius with nothing to keep in sync. */ -:where( - [data-glass="chakra"], - [data-glass="chakra"] [data-glass-tint], - [data-glass="chakra"][data-glass-tint], - [data-glass="chakra"] [data-sonner-toast][data-type] -) { +:where([data-glass="chakra"], [data-glass="chakra"] [data-glass-tint], [data-glass="chakra"][data-glass-tint]) { --srf-bg-image: var(--glass-chakra-stack-bg); --srf-bg-color: var(--glass-chakra-bg); --srf-filter: blur(max(var(--blur-chakra), var(--dif, 0px))); @@ -1630,7 +1718,7 @@ } /* Chakra page: adaptive glass (no explicit material, not veiled) takes the -chakra text tiers, same as the material remap in materials.css. */ -[data-glass="chakra"] :where(.glass):not([data-material]):not(.glass-veil) { +[data-glass="chakra"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface="veil"]) { --foreground: var(--foreground-chakra); --foreground-soft: var(--foreground-soft-chakra); --foreground-strong: var(--foreground-strong-chakra); @@ -1639,12 +1727,7 @@ } /* Opaque — solid tinted panels for adaptive surfaces. --glass-solid-a: 1 makes any veiled overlay (glass-veil) compute a FULLY solid floor here too, so menus/toasts stay legible on an opaque page. */ -:where( - [data-glass="opaque"], - [data-glass="opaque"] [data-glass-tint], - [data-glass="opaque"][data-glass-tint], - [data-glass="opaque"] [data-sonner-toast][data-type] -) { +:where([data-glass="opaque"], [data-glass="opaque"] [data-glass-tint], [data-glass="opaque"][data-glass-tint]) { /* Flat floor, no sheet — same reasoning as [data-material="opaque"] in materials.css: with --srf-filter: none there is no blurred backdrop to mask the dither of a 2–7 code gradient, and the sheet's uniform lift is folded into --glass-opaque-l instead. */ @@ -1652,6 +1735,12 @@ --srf-bg-color: var(--glass-opaque-bg); --srf-filter: none; --srf-shadow: var(--glass-shadow); + /* Same provable no-op as [data-material="opaque"]: --glass-opaque-bg sits in the background-COLOR + channel, so the solidify layer would paint that colour over itself. A TOKEN here (it used to be an + element rule keyed on `.glass`), so it reaches an `@apply glass` surface too, which kept the light + solidify backing at 70% over the opaque floor and read nearly white. An explicit material pins the + slot back (`initial`), and a veiled surface restores the layer with --veil-solidify. */ + --srf-solidify: none; } /* --glass-solid-a CANNOT ride in the :where() block above. :where() is specificity ZERO by design (so a page style never beats an explicit material), and every other declaration up there survives that @@ -1667,37 +1756,30 @@ [data-glass="opaque"] { --glass-solid-a: 1; } -/* Opaque page: element-level touches for ADAPTIVE surfaces only (`.glass` with no explicit material — - an explicit frosted/crystal card keeps its translucency AND its normal foreground). Declared ON the - element because (a) --glass-opaque-outline is settable on any ancestor and must resolve where it's - read, and (b) the foreground remap must not leak into explicit materials. */ -[data-glass="opaque"] :where(.glass):not([data-material]):not(.glass-veil) { - /* Same provable no-op as [data-material="opaque"]: this page remap puts --glass-opaque-bg in the - background-COLOR channel, so the solidify layer paints that colour over itself. Element-level and - :not([data-material]) so it cannot leak into an explicit material, :not(.glass-veil) because a - veiled floor is neutral and the layer is real there. */ - --srf-solidify: none; -} -/* The uniform EDGE applies to every adaptive panel, veiled or not — a veiled card under the opaque page +/* Opaque page: element-level touches for ADAPTIVE surfaces only (a `.glass` or `[data-surface]` element + with no explicit material — an explicit frosted/crystal card keeps its translucency AND its normal + foreground). Declared ON the element because (a) --glass-opaque-outline is settable on any ancestor + and must resolve where it's read, and (b) the foreground remap must not leak into explicit materials. + The uniform EDGE applies to every adaptive panel, veiled or not — a veiled card under the opaque page still reads as a solid panel and needs the same outline. */ -[data-glass="opaque"] :where(.glass):not([data-material]) { +[data-glass="opaque"] :where(.glass, [data-surface]):not([data-material]) { --srf-border-color: var(--glass-opaque-outline, var(--glass-border)); --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px)); /* Paint the border HERE, not just the tokens: borderless roles (Card default) never render glass-border, so without this only bordered roles showed the outline — "some cards get it, some don't". Under the opaque page every solid panel takes the uniform edge; bordered elements resolve the same vars, so nothing double-draws. */ - border: var(--srf-border-w) solid var(--srf-border-color); + border: var(--srf-border-w) solid var(--brd-ink, var(--srf-border-color)); } /* The FOREGROUND remap is :not(.glass-veil) — the same carve-out crystal and chakra make, and the same - one the --srf-solidify rule above already makes for this page style. A veiled card does not paint + one --veil-solidify makes for the solidify layer on this page style. A veiled card does not paint --glass-opaque-bg: glass-veil moves the background-COLOR channel to the neutral --veil-floor, so its text sits on the veil surface the BASE tokens are banded for, not on the opaque floor. Remapping it here handed those cards ink solved for a surface they do not have — visible as a hue jump, since the opaque floor is the one surface with nothing composited over it and so sits at the raw --glass-tint-h while every sheer surface lands a few degrees off it (measured on the /colors panel: 22.0 vs 14.6). Splitting the rule is what keeps the edge treatment while dropping the foreground one. */ -[data-glass="opaque"] :where(.glass):not([data-material]):not(.glass-veil) { +[data-glass="opaque"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface="veil"]) { --foreground: var(--foreground-opaque); --foreground-soft: var(--foreground-soft-opaque); --foreground-strong: var(--foreground-strong-opaque); @@ -1709,6 +1791,15 @@ Most presets just move the tint hue/chroma; "sistine" is a bespoke multi-hue fresco (gilded gold → terracotta flesh tones → azure sky), echoing the Sistine Chapel ceiling. Wash + all tint-derived tokens re-resolve per-scope via the :root,[data-glass-tint] split above. */ +/* Selenite — the neutral stone: no chroma and no wash, so the glass is the plain clear sheet. The tint + values equal the engine default's hue with the colour switched off; the block exists so a CSS-only + consumer (no tint switcher) gets selenite's colour-wheel anchor, which a hue-less theme needs. */ +[data-glass-tint="selenite"] { + --glass-tint-h: 250; + --glass-tint-c: 0; + --glass-tint-a: 0; + --harmony-h: 0; +} [data-glass-tint="sapphire"] { --glass-tint-h: 255; --glass-tint-c: 0.075; @@ -1938,33 +2029,29 @@ --muted-foreground-chakra: oklch(0.96 0.03 75); } -/* Semantic surfaces (Alert info/success/warning/destructive; destructive Button/Badge) render - data-glass-tint="", and sonner toasts hit the SAME hues via their data-type attribute - (destructive pairs with sonner's "error") — one block per status, joining the grouped engine - re-resolution above so --glass-bg / wash re-resolve at the alert/toast. */ -[data-glass-tint="info"], -[data-sonner-toast][data-type="info"] { +/* Semantic surfaces (Alert info/success/warning/destructive; destructive Button/Badge; status toasts, + which the Toaster stamps from sonner's data-type — see TOAST_TINT in components/ui/sonner.tsx) render + data-glass-tint="" — one block per status, joining the grouped engine re-resolution above so + --glass-bg / wash re-resolve at the alert/toast. */ +[data-glass-tint="info"] { --glass-tint-h: 255; --glass-tint-c: 0.075; --glass-tint-a: 0.18; --glass-wash-l: 63%; } -[data-glass-tint="success"], -[data-sonner-toast][data-type="success"] { +[data-glass-tint="success"] { --glass-tint-h: 150; --glass-tint-c: 0.088; --glass-tint-a: 0.18; --glass-wash-l: 87%; } -[data-glass-tint="warning"], -[data-sonner-toast][data-type="warning"] { +[data-glass-tint="warning"] { --glass-tint-h: 85; --glass-tint-c: 0.063; --glass-tint-a: 0.18; --glass-wash-l: 84%; } -[data-glass-tint="destructive"], -[data-sonner-toast][data-type="error"] { +[data-glass-tint="destructive"] { --glass-tint-h: 25; --glass-tint-c: 0.094; --glass-tint-a: 0.18; @@ -2162,47 +2249,3 @@ --glass-frosted-bg: linear-gradient(135deg in oklch, oklch(86% 0.12 62 / 0.32) 0%, oklch(80% 0.14 350 / 0.3) 50%, oklch(74% 0.13 278 / 0.32) 100%); --glass-crystal-fresco: var(--glass-bg); } -/* Sonner Toast Notifications - Glass Effect Overrides */ -[data-sonner-toast] { - /* Glass gradient layered over a mostly-opaque surface so toast text stays legible. - Lower the popover % to make toasts more see-through, raise it to make them more solid. */ - background: var(--glass-bg), color-mix(in oklch, var(--popover) 78%, transparent) !important; - backdrop-filter: blur(var(--blur)) saturate(180%) !important; - border: 1px solid var(--glass-border) !important; - box-shadow: var(--glass-shadow-lg), var(--glow-layer, 0 0 0 0 transparent) !important; - color: var(--foreground) !important; -} - -/* Status toast borders — derived from the toast's own status tint (the merged status blocks above set - --glass-tint-h at the toast) instead of four hardcoded tailwind-500 literals; within ~0.1 L of the - originals on a 1px 30%-alpha border. NOT bare [data-type]: default/loading toasts keep the plain - glass border. */ -[data-sonner-toast][data-type="success"], -[data-sonner-toast][data-type="error"], -[data-sonner-toast][data-type="warning"], -[data-sonner-toast][data-type="info"] { - border-color: oklch(0.7 0.18 var(--glass-tint-h) / 0.3) !important; -} - -/* Toast description text */ -[data-sonner-toast] [data-description] { - color: var(--muted-foreground) !important; -} - -/* Toast action / cancel / close buttons — one glass recipe (the hover fade now covers all three; - previously only the action button transitioned). Cancel dims to muted below. */ -[data-sonner-toast] :is([data-button], [data-cancel], [data-close-button]) { - background: var(--glass-bg) !important; - backdrop-filter: blur(var(--blur-sm)) !important; - border: 1px solid var(--glass-border) !important; - color: var(--foreground) !important; - transition: opacity 0.2s ease !important; -} - -[data-sonner-toast] [data-cancel] { - color: var(--muted-foreground) !important; -} - -[data-sonner-toast] :is([data-button], [data-cancel], [data-close-button]):hover { - opacity: 0.9 !important; -} diff --git a/scripts/check-contrast.mjs b/scripts/check-contrast.mjs index f3dcc5a7..3ea62977 100644 --- a/scripts/check-contrast.mjs +++ b/scripts/check-contrast.mjs @@ -21,6 +21,9 @@ * 1. [body] the body tier reaches >= BODY_FLOOR Lc on the normal (wash + solidify) surface. * 2. [margin] it does so with at least MARGIN Lc of slack, so a preset cannot sit exactly on the line * where a rounding change tips it under. + * 3. [ink] the ink border (borderColor="ink": --foreground at --glass-border-ink-a over the surface) + * clears INK_FLOOR Lc, APCA's floor for non-text elements a user must see, on both the sheer + * surface and the opaque card. It exists because the material's own edge does not. * * It models the surface the way components/auto-foreground.tsx does — glassSolidSurface() with the * solidify floor, then readableForeground() for the body band — so a change to that model shows up here @@ -34,6 +37,7 @@ import { fileURLToPath } from "node:url"; import { apcaContrast, boostBand, + compositeSurface, foregroundRamp, glassSolidSurface, LC_AIM_KNOWN, @@ -56,6 +60,9 @@ const read = (p) => readFileSync(join(ROOT, p), "utf8"); const BODY_FLOOR = READABLE_USAGE.body.floor; // 75 — read from the source of truth, never hardcoded const MARGIN = 1.5; // Lc of slack required on top of the floor +/* APCA's level for non-text elements that must be perceived (outlines, focus rings, dividers that carry + meaning). Well under any text floor, deliberately: a border is a shape, not glyphs. */ +const INK_FLOOR = 30; /** * Known, DESIGN-level exceptions: pinned at their measured value so they cannot silently get worse, and @@ -116,6 +123,7 @@ const vars = modeVars(); const all = presets(); const failures = []; const report = []; +const inkReport = []; for (const p of all) { for (const mode of ["light", "dark"]) { @@ -123,7 +131,7 @@ for (const p of all) { const h = resolve(p, mode, vars, "glass-tint-h"); const c = resolve(p, mode, vars, "glass-tint-c"); const a = resolve(p, mode, vars, "glass-tint-a"); - if (h === undefined || c === undefined || a === undefined) continue; // selenite has no block + if (h === undefined || c === undefined || a === undefined) continue; // an incomplete scrape, not a preset const washL = resolve(p, mode, vars, "glass-wash-l", dark ? 58 : 72); const washCMult = resolve(p, mode, vars, "glass-wash-c-mult", 2.5); @@ -189,6 +197,24 @@ for (const p of all) { ); } + /* The ink border: the same --foreground the surface's text uses, mixed toward transparent and painted + over that surface (a border sits on its element's own background). color-mix(…, transparent) in + oklch keeps the colour and scales its alpha, which the browser then blends in sRGB, as this does. */ + const inkA = vars[mode].get("glass-border-ink-a") ?? vars.light.get("glass-border-ink-a") ?? 0.5; + for (const [where, ink, ground] of [ + ["sheer", fg, surface], + ["opaque", opaqueInk, opaqueSurface], + ]) { + const line = compositeSurface(ground, [{ ...ink, a: inkA }]); + const inkLc = Math.abs(apcaContrast(line, ground)); + if (inkLc < INK_FLOOR) { + failures.push( + `[ink] ${p.name} (${mode}, ${where}) — the ink border reaches only ${inkLc.toFixed(1)} Lc against its surface, under the ${INK_FLOOR} non-text floor. Raise --glass-border-ink-a (${inkA}).`, + ); + } + inkReport.push({ name: p.name, mode, where, lc: inkLc }); + } + const known = KNOWN.get(`${p.name}|${mode}`); if (known) { /* Pinned: must not get WORSE than the value recorded when it was accepted. If it improves past the @@ -225,7 +251,7 @@ if (process.argv.includes("--report")) { } /* Sanity floor: if the scrapers matched nothing the check is worthless. 12 jewels + moonstone + - 4 frescoes + 4 status = 21 named blocks, x2 modes, minus selenite which has no block. */ + 4 frescoes + 4 status + selenite = 22 named blocks, x2 modes. */ if (report.length < 30) { console.error(`✗ contrast guard: scored only ${report.length} preset/mode pairs — the CSS shape changed, fix the parser`); process.exit(1); @@ -238,8 +264,10 @@ if (failures.length) { would make the summary contradict itself ("clears the floor everywhere … tightest 65.4"). */ const scored = report.filter((r) => !KNOWN.has(`${r.name}|${r.mode}`)); const worst = scored.reduce((a, b) => (b.lc < a.lc ? b : a)); +const inkWorst = inkReport.reduce((a, b) => (b.lc < a.lc ? b : a)); console.log( `✓ preset contrast — ${scored.length} preset/mode pairs clear the ${BODY_FLOOR} Lc body floor ` + `(tightest ${worst.name} ${worst.mode} at ${worst.lc.toFixed(1)})` + - (KNOWN.size ? `; ${KNOWN.size} pinned exception${KNOWN.size > 1 ? "s" : ""}: ${[...KNOWN.keys()].join(", ")}` : ""), + (KNOWN.size ? `; ${KNOWN.size} pinned exception${KNOWN.size > 1 ? "s" : ""}: ${[...KNOWN.keys()].join(", ")}` : "") + + `; ink border ≥ ${INK_FLOOR} Lc on ${inkReport.length} surfaces (tightest ${inkWorst.name} ${inkWorst.mode} ${inkWorst.where} at ${inkWorst.lc.toFixed(1)})`, ); diff --git a/scripts/check-gamut.mjs b/scripts/check-gamut.mjs index 01b4b9fd..37e09e87 100644 --- a/scripts/check-gamut.mjs +++ b/scripts/check-gamut.mjs @@ -132,6 +132,23 @@ const blocks = (source) => { return out; }; +/* The hue gloss flavor's variants (engine.css block C′) are gated on --glass-gloss-is-hue and only ever + render inside a [data-gloss="hue"] scope, which pins its own --glass-gloss-l. They used to live IN that + block, where the block-local lookup found it; now they are composed in the engine (so a scoped + data-gloss recomposes them) and the lightness has to be seeded from the flavor block explicitly, or the + sweep would score them at the tonal flavor's L97/L66 — a surface that never renders. */ +const HUE_GLOSS_L = (() => { + const m = css.materials.match(/\[data-gloss="hue"\]\s*\{[\s\S]*?--glass-gloss-l:\s*([\d.]+)/); + return m ? +m[1] : undefined; +})(); +/** Numeric custom props a block declares (or, for the hue variants, renders under). */ +function localVars(body) { + const local = new Map(); + if (body.includes("--glass-gloss-is-hue") && HUE_GLOSS_L !== undefined) local.set("glass-gloss-l", HUE_GLOSS_L); + for (const [, k, v] of body.matchAll(/--([a-z0-9-]+):\s*([\d.]+)%?\s*;/g)) local.set(k, +v); + return local; +} + /* ── Surfaces: every declaration composing tint chroma, scraped so the table cannot drift ────── */ function collectSurfaces(vars) { const surfaces = []; @@ -157,8 +174,7 @@ function collectSurfaces(vars) { for (const [file, src] of Object.entries({ engine: css.engine, materials: css.materials })) { for (const body of blocks(src)) { /* Numeric custom props declared right here, which shadow the tokens.css values below. */ - const local = new Map(); - for (const [, k, v] of body.matchAll(/--([a-z0-9-]+):\s*([\d.]+)%?\s*;/g)) local.set(k, +v); + const local = localVars(body); for (const m of body.matchAll(re)) { const g = m.groups; /* mk/mkdef = the BOTH shape's multiplier; k/kdef = the SCALE shape's. A pure CAP has neither and @@ -260,8 +276,7 @@ function collectFixed(vars) { }; for (const [file, src] of Object.entries({ engine: css.engine, materials: css.materials })) { for (const body of blocks(src)) { - const local = new Map(); - for (const [, k, v] of body.matchAll(/--([a-z0-9-]+):\s*([\d.]+)%?\s*;/g)) local.set(k, +v); + const local = localVars(body); for (const args of oklchArgs(body)) { const parts = topSplit(args); if (parts.length < 3) continue; diff --git a/scripts/check-patterns.mjs b/scripts/check-patterns.mjs index afec2a4d..7cdbc7f8 100644 --- a/scripts/check-patterns.mjs +++ b/scripts/check-patterns.mjs @@ -10,6 +10,16 @@ * Why: a style added to the union but omitted from the array silently vanishes from the switcher's * cycle, and TypeScript won't catch it (a subset still satisfies `PatternStyle[]`). This test does. * + * And how a scene STOPS. Reduced motion and Speed 0 both remove a scene's animations through one helper, + * `stopRules()`, which emits the `@media (prefers-reduced-motion)` block and the `[data-pat-static]` one + * from the same selector list: + * - [stop-paused] nothing pauses instead (`animation-play-state` / the old `--pat-play` knob): a paused + * animation leaves the element animating, and WebKit kept re-rendering six full-viewport blurred, + * masked layers at Speed 0; + * - [stop-rules] every ANIMATED_PATTERNS scene's stylesheet calls `stopRules(`, and so does any scene + * stylesheet that declares an animation at all, so a new scene cannot honour one condition and not + * the other (the original bug). + * * Run: node scripts/check-patterns.mjs (wired into `bun run test`) */ import { readFileSync } from "node:fs"; @@ -50,10 +60,34 @@ if (densType.length && densArray.length && !sameSet(densType, densArray)) { fail.push(`[density-sync] PatternDensity ≠ PATTERN_DENSITIES ([${only(densType, densArray)}] / [${only(densArray, densType)}])`); } +/* Comments explain the old mechanism by name, so they are stripped before looking for it. */ +const code = src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/[^\n]*/g, "$1"); +for (const banned of ["animation-play-state", "--pat-play"]) { + if (code.includes(banned)) { + fail.push(`[stop-paused] pattern-background.tsx uses ${banned}: a paused scene keeps animating — stop it with stopRules() (animation: none) instead.`); + } +} + +const sceneStyles = [...src.matchAll(/const ([A-Z_]+_STYLES) = `([\s\S]*?)`;/g)].map((m) => ({ name: m[1], body: m[2] })); +const callsStopRules = (body) => body.includes("${stopRules("); +for (const a of animated) { + const sheet = sceneStyles.find((st) => st.body.includes(`[data-pattern="${a}"]`)); + if (!sheet) fail.push(`[stop-rules] no *_STYLES stylesheet styles [data-pattern="${a}"] — cannot check how it stops.`); + else if (!callsStopRules(sheet.body)) { + fail.push(`[stop-rules] ${sheet.name} (${a}) never calls stopRules(): it keeps animating under reduced motion and/or Speed 0.`); + } +} +for (const st of sceneStyles) { + const animates = [...st.body.matchAll(/animation(?:-name)?:\s*([^;]+);/g)].some((m) => m[1].trim() !== "none"); + if (animates && !callsStopRules(st.body)) { + fail.push(`[stop-rules] ${st.name} declares an animation but never calls stopRules() — list its animated selectors there.`); + } +} + if (fail.length > 0) { console.error(`✗ pattern registry drift:\n${fail.map((f) => ` ${f}`).join("\n")}`); process.exit(1); } console.log( - `✓ pattern lists in sync — ${array.length} styles (${animated.length} animated), ${densArray.length} densities; union == array`, + `✓ pattern lists in sync — ${array.length} styles (${animated.length} animated, each stopped by stopRules), ${densArray.length} densities; union == array`, ); diff --git a/scripts/check-theme.mjs b/scripts/check-theme.mjs index 6bdb0630..50aba4e4 100644 --- a/scripts/check-theme.mjs +++ b/scripts/check-theme.mjs @@ -17,8 +17,13 @@ * 6a.[materials] each [data-material="…"] block pins the full --srf-* set (+ opaque fg / crystal hover). * 6b.[recipes-dead] no retired recipe utility (glass-bg/-surface/-solid/…) is defined or class-used. * 6c.[material-union] lib/material.ts's Material type == the four CSS materials + "none". - * 3. [preset] Every GlassTintSwitcher preset (except selenite) has a [data-glass-tint="x"] block. - * 4. [status] Every status a component renders via data-glass-tint has a [data-glass-tint] block. + * 6g.[double-slash] no class with two "/" modifiers (Tailwind drops it silently). + * 6h.[surface-marker] element rules keyed on .glass / .glass-veil also accept [data-surface] / ="veil". + * 6i.[solidify-owner] only the opaque material/page drop the solidify layer; the veil restores it. + * 6j.[gloss-scope] the gloss is composed on [data-gloss] scopes too; flavor blocks set leaves only. + * 3. [preset] Every GlassTintSwitcher preset has a [data-glass-tint="x"] block. + * 4. [status] Every status a component renders via data-glass-tint (or the Toaster's TOAST_TINT) has a block. + * 4b.[vendor-free] No third-party component selectors (data-sonner) in the theme. * 5. [fresco] Every fresco preset (sets --glass-crystal-fresco) has a FRESCO_HUES entry. * 6. [variants] Every glass component (has a crystal: variant) also has surface: + solid: variants. * 7. [sync] public/r/theme.json embeds the CURRENT flattened theme (registry not stale). @@ -151,11 +156,10 @@ for (const r of rules) { } } -// 3. [preset] every switcher preset (except selenite) has a [data-glass-tint="x"] block +// 3. [preset] every switcher preset has a [data-glass-tint="x"] block. Selenite included: without one, a +// CSS-only consumer had no colour-wheel anchor on it (--harmony-h: 0 lived only in the switcher's JS). const switcher = readFileSync(join(root, "components/glass-tint-switcher.tsx"), "utf8"); -const presets = [...new Set([...switcher.matchAll(/value:\s*"([a-z]+)"/g)].map((m) => m[1]))].filter( - (v) => v !== "selenite" && v !== "custom", -); +const presets = [...new Set([...switcher.matchAll(/value:\s*"([a-z]+)"/g)].map((m) => m[1]))].filter((v) => v !== "custom"); for (const v of presets) { if (!css.includes(`[data-glass-tint="${v}"]`)) { fail(`[preset] switcher preset "${v}" has no [data-glass-tint="${v}"] block in globals.css.`); @@ -171,12 +175,25 @@ for (const rel of componentFiles) { for (const lit of m[1].matchAll(/"([a-z-]+)"/g)) statuses.add(lit[1]); } } +/* The Toaster stamps its tints from a map rather than a JSX attribute (sonner renders the toasts), so its + values are read out of TOAST_TINT. */ +{ + const sonner = readFileSync(join(root, "components/ui/sonner.tsx"), "utf8"); + const map = sonner.match(/export const TOAST_TINT[^=]*=\s*\{([\s\S]*?)\};/); + if (!map) fail("[status] components/ui/sonner.tsx has no TOAST_TINT map — if the stamping moved, update this rule rather than dropping it."); + else for (const lit of map[1].matchAll(/:\s*"([a-z-]+)"/g)) statuses.add(lit[1]); +} for (const s of statuses) { if (!css.includes(`[data-glass-tint="${s}"]`)) { fail(`[status] a component renders data-glass-tint="${s}" but globals.css has no [data-glass-tint="${s}"] block.`); } } +// 4b. [vendor-free] the theme carries no third-party component's selectors. A consumer who never renders +// a toast installed every [data-sonner-toast] rule anyway, and sonner's own unlayered stylesheet could +// only be fought with !important. Vendor styling belongs to the component that renders the vendor. +if (/data-sonner/.test(css)) fail("[vendor-free] the flattened theme references data-sonner — style toasts in components/ui/sonner.tsx, not the theme."); + // 5. [fresco] every fresco preset (sets --glass-crystal-fresco) has a FRESCO_HUES entry, so its // canvas/gradient background matches it instead of collapsing to one hue. const canvasUtils = readFileSync(join(root, "lib/canvas-background-utils.ts"), "utf8"); @@ -349,6 +366,126 @@ for (const dir of ["components", "app"]) { } } +// 6g. [double-slash] no class with a second `/` modifier (`bg-foreground/10/50`). Tailwind v4 cannot parse +// it and DROPS the class without a warning, so the state it styles (navigation-menu's hover, focus +// and open backgrounds, for a whole release) just never renders. It is what a mechanical find-replace +// of `bg-accent` → `bg-foreground/10` leaves behind wherever `bg-accent/50` stood. +{ + /* Per whitespace/quote-delimited token, so variant prefixes (`data-[active]:`) ride along; a URL in a + code sample is the one other thing shaped `a-b/1/2`, hence the `://` exclusion. */ + const DOUBLE_SLASH = /[a-z]-[\w-]*\/\d+\/\d+$/; + const doubleSlash = (region) => region.split(/[\s"'`]+/).find((t) => DOUBLE_SLASH.test(t) && !t.includes("://")); + for (const dir of ["components", "app"]) { + for (const rel of readdirSync(join(root, dir), { recursive: true })) { + if (typeof rel !== "string" || !/\.(tsx|ts)$/.test(rel)) continue; + const src = readFileSync(join(root, dir, rel), "utf8") + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/(^|[^:])\/\/[^\n]*/g, "$1"); + const hit = classRegions(src).map(doubleSlash).find(Boolean); + if (hit) fail(`[double-slash] ${dir}/${rel} uses "${hit}" — Tailwind drops a class with two "/" modifiers silently. Keep one opacity (e.g. bg-accent/50).`); + } + } +} + +// 6h. [surface-marker] an ELEMENT-level rule that picks out surfaces by the `.glass` class must also +// accept the documented `[data-surface]` marker, and every `.glass-veil` carve-out must also accept +// `[data-surface="veil"]`. An element that wears the material through `@apply glass` has no class, +// so a rule keyed on the class alone silently skips it (the opaque page left such surfaces with the +// sheer backing and the wrong text tier). Vendor fixes that target a class combination by design +// (react-day-picker's `.glass .rdp`) are not surface pickers and are exempt. +/** Split a selector list on its TOP-LEVEL commas only — `selectors()` also splits inside :not()/:where(). */ +function splitTop(sel) { + const out = []; + let depth = 0; + let start = 0; + for (let i = 0; i < sel.length; i++) { + if (sel[i] === "(") depth++; + else if (sel[i] === ")") depth--; + else if (sel[i] === "," && depth === 0) { + out.push(sel.slice(start, i).trim()); + start = i + 1; + } + } + out.push(sel.slice(start).trim()); + return out.filter(Boolean); +} +{ + const VENDOR = /\brdp\b/; + for (const r of rules) { + for (const sel of splitTop(r.selector)) { + if (VENDOR.test(sel) || sel.startsWith("@")) continue; + if (/:where\(\.glass\)|(^|[\s>+~])\.glass(?![-\w])/.test(sel) && !sel.includes("[data-surface]")) { + fail(`[surface-marker] "${sel}" picks surfaces by the .glass class only — use :where(.glass, [data-surface]) so @apply glass surfaces match too.`); + } + if (/\.glass-veil\b/.test(sel) && !/\.glass-veil,\s*\[data-surface="veil"\]/.test(sel)) { + fail(`[surface-marker] "${sel}" carves out .glass-veil only — add [data-surface="veil"] (e.g. :not(.glass-veil, [data-surface="veil"])).`); + } + } + } +} + +// 6i. [solidify-owner] who may drop the solidify layer. `--srf-solidify: none` is a provable no-op only +// over the opaque floor, so it may appear ONLY in the opaque material and the opaque PAGE scope (a +// token, so `@apply glass` surfaces inherit it — never an element rule keyed on `.glass`). The veil +// restores the layer through --veil-solidify, which `glass` must read first and which must not +// inherit (a veiled parent would otherwise restore it on a child that dropped it). +{ + for (const r of rules) { + for (const d of decls(r.body)) { + if (d.name !== "--srf-solidify" || d.value !== "none") continue; + const sels = splitTop(r.selector); + const opaqueMaterial = sels.length === 1 && sels[0] === '[data-material="opaque"]'; + const whereArgs = sels.length === 1 && /^:where\(([\s\S]*)\)$/.exec(sels[0])?.[1]; + const opaquePage = !!whereArgs && splitTop(whereArgs).every((x) => x.startsWith('[data-glass="opaque"]')); + if (!opaqueMaterial && !opaquePage) { + fail(`[solidify-owner] "${r.selector.replace(/\s+/g, " ")}" drops the solidify layer — only [data-material="opaque"] and the opaque page's :where() token block may.`); + } + } + } + const glassUtil = rules.find((r) => r.selector === "@utility glass"); + if (!glassUtil || !/var\(--veil-solidify,\s*var\(--srf-solidify,\s*var\(--glass-solidify\)\)\)/.test(glassUtil.body)) { + fail(`[solidify-owner] @utility glass must paint var(--veil-solidify, var(--srf-solidify, var(--glass-solidify))) — the veil restores the layer first.`); + } + const veilUtil = rules.find((r) => r.selector === "@utility glass-veil"); + if (!veilUtil || !/--veil-solidify:\s*var\(--glass-solidify\)/.test(veilUtil.body)) { + fail(`[solidify-owner] @utility glass-veil must set --veil-solidify: var(--glass-solidify).`); + } + for (const prop of ["--veil-floor", "--veil-solidify"]) { + const reg = rules.find((r) => r.selector === `@property ${prop}`); + if (!reg || !/inherits:\s*false/.test(reg.body)) fail(`[solidify-owner] ${prop} must be registered with @property … inherits: false.`); + } +} + +// 6j. [gloss-scope] `data-gloss` works below only if every token that COMPOSES the gloss is +// declared on a rule that includes [data-gloss] (with :root/.dark/[data-glass-tint]), so it +// recomposes in each flavor scope; and the [data-gloss="…"] blocks set LEAVES only, both switches +// each, so a nested flavor overrides its ancestor's. The old blocks re-pinned the ink alone and the +// pre-composed gradients and stacks rode in from : every scoped flavor painted the page's. +{ + const GLOSS = /^--(glass-gloss-(ink|top|streak|glow)(-[a-z]+)?|glass-crystal-bg(-hue)?|crystal-stack-bg|glass-chakra-stack-bg)$/; + const SCOPE = [":root", ".dark", "[data-glass-tint]", "[data-gloss]"]; + for (const r of rules) { + const sels = splitTop(r.selector); + for (const d of decls(r.body)) { + if (!GLOSS.test(d.name)) continue; + if (!SCOPE.every((x) => sels.includes(x))) { + fail(`[gloss-scope] ${d.name} is declared on "${r.selector.replace(/\s+/g, " ")}" — compose it in engine.css block C (${SCOPE.join(", ")}) so a scoped data-gloss recomposes it.`); + } + } + } + for (const flavor of ["white", "tonal", "hue"]) { + const block = rules.find((r) => r.selector === `[data-gloss="${flavor}"]`); + if (!block) { + fail(`[gloss-scope] no [data-gloss="${flavor}"] block.`); + continue; + } + const names = new Set(decls(block.body).map((d) => d.name)); + for (const sw of ["--glass-gloss-is-white", "--glass-gloss-is-hue"]) { + if (!names.has(sw)) fail(`[gloss-scope] [data-gloss="${flavor}"] does not set ${sw} — a nested flavor would combine with its ancestor's instead of replacing it.`); + } + } +} + // 6c. [material-union] lib/material.ts's Material union == the four CSS materials (+ "none"). const matSrc = readFileSync(join(root, "lib/material.ts"), "utf8"); const union = [...(matSrc.match(/type Material =([^;]+);/)?.[1] ?? "").matchAll(/"([a-z]+)"/g)].map((x) => x[1]); @@ -434,7 +571,7 @@ const presetTint = new Map( ); for (const [value, tint] of presetTint) { const blocks = rules.filter((r) => r.selector.includes(`[data-glass-tint="${value}"]`)); - if (blocks.length === 0) continue; // selenite has no block — invariant 3 already flags missing blocks + if (blocks.length === 0) continue; // invariant 3 already flags a missing block for (const b of blocks) { /* --glass-tint-a is MODE-SPLIT now (a wash near that mode's floor tolerates far more alpha), and the switcher no longer inlines it for a named preset — CSS is the only source, so there is nothing to diff --git a/stories/Combobox.stories.tsx b/stories/Combobox.stories.tsx new file mode 100644 index 00000000..40195d73 --- /dev/null +++ b/stories/Combobox.stories.tsx @@ -0,0 +1,95 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + Combobox, + ComboboxChip, + ComboboxChips, + ComboboxChipsInput, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxValue, + useComboboxAnchor, +} from "@/components/ui/combobox"; + +const meta = { + title: "Sistine/Combobox", + component: Combobox, + parameters: { + layout: "centered", + }, + tags: [ + "autodocs", + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const frameworks = [ + "Next.js", + "Remix", + "Astro", + "SvelteKit", + "Nuxt", + "Vite", +]; + +export const Single: Story = { + render: () => ( + + + + No framework found. + + {(item: string) => ( + + {item} + + )} + + + + ), +}; + +function MultipleDemo() { + const anchor = useComboboxAnchor(); + return ( + + + + {(values: string[]) => + values.map((value) => ( + + {value} + + )) + } + + + + + No framework found. + + {(item: string) => ( + + {item} + + )} + + + + ); +} + +export const Multiple: Story = { + render: () => , +}; diff --git a/stories/Dialog.stories.tsx b/stories/Dialog.stories.tsx index 29d5cff4..5612090d 100644 --- a/stories/Dialog.stories.tsx +++ b/stories/Dialog.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -130,3 +130,39 @@ export const WithForm: Story = { ), }; + +/* DialogBody: only the body scrolls. The header and footer stay where they are, and the footer's glass + buttons sit outside the scroll box, so their glow is never sliced at its edge. */ +export const ScrollingBody: Story = { + render: () => ( + + + + + + + Terms of service + Scroll to the end, then accept. + + + {Array.from( + { + length: 24, + }, + (_, i) => ( +

    + {i + 1}. These terms govern your use of the service. A long, unbroken token like + https://example.com/a/very/long/path/that/would/otherwise/widen/the/dialog/{i} wraps here, because the body lets its children shrink + below their content width. +

    + ), + )} +
    + + + + +
    +
    + ), +}; diff --git a/stories/Empty.stories.tsx b/stories/Empty.stories.tsx new file mode 100644 index 00000000..2cc1ecd4 --- /dev/null +++ b/stories/Empty.stories.tsx @@ -0,0 +1,53 @@ +import { FolderOpenIcon, MagnifyingGlassIcon } from "@phosphor-icons/react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"; + +const meta = { + title: "Sistine/Empty", + component: Empty, + parameters: { + layout: "centered", + }, + tags: [ + "autodocs", + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/* Plain by default: the usual home of an empty state is a surface that is already glass. */ +export const InsideACard: Story = { + render: () => ( + + + + + + + No projects yet + Create your first project to get started. + + + + + + + ), +}; + +export const OnItsOwnSurface: Story = { + render: () => ( + + + + + + No results + Nothing matches “glass”. Try a different search. + + + ), +}; diff --git a/stories/Field.stories.tsx b/stories/Field.stories.tsx new file mode 100644 index 00000000..f16b1228 --- /dev/null +++ b/stories/Field.stories.tsx @@ -0,0 +1,94 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Field, + FieldContent, + FieldDescription, + FieldError, + FieldGroup, + FieldLabel, + FieldLegend, + FieldSeparator, + FieldSet, + FieldTitle, +} from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; + +const meta = { + title: "Sistine/Field", + component: Field, + parameters: { + layout: "centered", + }, + tags: [ + "autodocs", + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Form: Story = { + render: () => ( +
    +
    + Profile + How you appear to others. + + + Name + + Your full name, as you would like it shown. + + + Email + + + + or + + + Show my profile publicly + + +
    +
    + ), +}; + +/* A label wrapping a whole Field is a choice card; it lights up while the control inside is checked. */ +export const ChoiceCards: Story = { + render: () => ( + + {[ + [ + "starter", + "Starter", + "For one person.", + ], + [ + "pro", + "Pro", + "For small teams.", + ], + ].map(([value, title, description]) => ( + + + + {title} + {description} + + + + + ))} + + ), +}; diff --git a/stories/Item.stories.tsx b/stories/Item.stories.tsx new file mode 100644 index 00000000..d125dee7 --- /dev/null +++ b/stories/Item.stories.tsx @@ -0,0 +1,75 @@ +import { BellIcon, ShieldCheckIcon } from "@phosphor-icons/react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { Button } from "@/components/ui/button"; +import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemSeparator, ItemTitle } from "@/components/ui/item"; +import { Switch } from "@/components/ui/switch"; + +const meta = { + title: "Sistine/Item", + component: Item, + parameters: { + layout: "centered", + }, + tags: [ + "autodocs", + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Variants: Story = { + render: () => ( + + {( + [ + "default", + "outline", + "muted", + ] as const + ).map((variant) => ( + + + + + + {variant} + Mentions, replies and invitations. + + + + + + ))} + + ), +}; + +export const AsLink: Story = { + render: () => ( + + +
    + + + + + Security + Passkeys, sessions and recovery codes. + + + + + + + Weekly digest + + + + + + + ), +}; diff --git a/stories/Kbd.stories.tsx b/stories/Kbd.stories.tsx new file mode 100644 index 00000000..51ac370c --- /dev/null +++ b/stories/Kbd.stories.tsx @@ -0,0 +1,38 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Kbd, KbdGroup } from "@/components/ui/kbd"; + +const meta = { + title: "Sistine/Kbd", + component: Kbd, + parameters: { + layout: "centered", + }, + tags: [ + "autodocs", + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => Esc, +}; + +export const Combination: Story = { + render: () => ( + + ⌘ + ⇧ + P + + ), +}; + +export const InText: Story = { + render: () => ( +

    + Press ⌘ + K to search, Esc to close. +

    + ), +}; diff --git a/stories/MenuBar.stories.tsx b/stories/MenuBarLegacy.stories.tsx similarity index 97% rename from stories/MenuBar.stories.tsx rename to stories/MenuBarLegacy.stories.tsx index 03289810..b1bb4328 100644 --- a/stories/MenuBar.stories.tsx +++ b/stories/MenuBarLegacy.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { MenuBar, MenuBarItem } from "@/components/ui/menu-bar"; const meta = { - title: "Sistine/MenuBar", + title: "Sistine/MenuBarLegacy", component: MenuBar, parameters: { layout: "centered", diff --git a/stories/Menubar.stories.tsx b/stories/Menubar.stories.tsx new file mode 100644 index 00000000..97b06aec --- /dev/null +++ b/stories/Menubar.stories.tsx @@ -0,0 +1,89 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + Menubar, + MenubarCheckboxItem, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarRadioGroup, + MenubarRadioItem, + MenubarSeparator, + MenubarShortcut, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +} from "@/components/ui/menubar"; + +const meta = { + title: "Sistine/Menubar", + component: Menubar, + parameters: { + layout: "centered", + }, + tags: [ + "autodocs", + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + + File + + + New tab ⌘T + + + New window ⌘N + + New incognito window + + + Share + + Email link + Messages + + + + Close window + + + + View + + Show bookmarks bar + Show full URLs + + + Compact + Comfortable + + + + + ), +}; + +export const Frosted: Story = { + render: () => ( + + + Edit + + + Undo ⌘Z + + + Redo ⇧⌘Z + + + + + ), +}; diff --git a/stories/Progress.stories.tsx b/stories/Progress.stories.tsx new file mode 100644 index 00000000..8e93ca64 --- /dev/null +++ b/stories/Progress.stories.tsx @@ -0,0 +1,38 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Progress } from "@/components/ui/progress"; + +const meta = { + title: "Sistine/Progress", + component: Progress, + parameters: { + layout: "centered", + }, + tags: [ + "autodocs", + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + value: 66, + }, + render: (args) => , +}; + +export const Steps: Story = { + render: () => ( +
    + {[ + 0, + 25, + 50, + 100, + ].map((v) => ( + + ))} +
    + ), +}; diff --git a/stories/Table.stories.tsx b/stories/Table.stories.tsx index 61017416..61dd6d77 100644 --- a/stories/Table.stories.tsx +++ b/stories/Table.stories.tsx @@ -176,3 +176,35 @@ export const Simple: Story = {
    ), }; + +/* The container is the scroller: `containerClassName` gives it a height and `overflow-y-auto`, and the + sticky header pins to it while the rows scroll underneath. The header takes the opaque floor so the + rows passing beneath it do not read through its translucent default fill. */ +export const StickyHeader: Story = { + render: () => ( + + + + Invoice + Status + Method + Amount + + + + {[ + ...invoices, + ...invoices, + ...invoices, + ].map((invoice, i) => ( + + {invoice.invoice} + {invoice.paymentStatus} + {invoice.paymentMethod} + {invoice.totalAmount} + + ))} + +
    + ), +}; diff --git a/tests/auto-foreground-lapis.test.tsx b/tests/auto-foreground-lapis.test.tsx new file mode 100644 index 00000000..1cd71098 --- /dev/null +++ b/tests/auto-foreground-lapis.test.tsx @@ -0,0 +1,162 @@ +/** + * Lapis, light mode: body text must stay ink-blue, not #000002. + * + * Reported by the first consumer: on the dark-wash presets, plain body text solved to near-black + * (#000002) in light mode, i.e. the hue was spent for a sliver of contrast. Lapis is the hardest case: + * its wash sits at L46, the deepest in the palette, so its sheer surface is the darkest light-mode + * surface the theme ships and the ramp's tinted steps have the least room above the body floor. + * + * What decides it is pickTonalInBand's legibility guard: when the best TONAL step misses the 75 Lc body + * floor it hands back the raw ramp's extreme, and on a tinted ramp that extreme renders black. These + * mount AutoForeground on lapis's own tokens (presets.css) and measure the colour it actually emits + * against the surface check-contrast models, so they test the shipped token, not a parallel solve. + * + * Reproducing it first showed the shipped backing (solidify L92, at the default opacity and at the + * consumer's 0.8) was never affected: L20 renders #09014e and clears the floor with room to spare. The + * report came from a darker backing, and there the guard's trade was unbounded. The consumer also carried + * a second hunk that dropped ramp steps by measured luminance (lapis L20 measures Ys 0.0042); it is NOT + * ported, because it removes exactly the step that keeps lapis ink-blue, at any backing. + */ +import { render, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { AutoForeground } from "@/components/auto-foreground"; +import { + apcaContrast, + glassSolidSurface, + type OklchColor, + oklchToSrgb, + parseOklch, + READABLE_USAGE, + TONAL_HUE_SLACK_LC, + TONAL_MIN_L, +} from "@/lib/oklch-utils"; + +/* presets.css, [data-glass-tint="lapis"] (light). */ +const LAPIS = { + h: 268, + c: 0.085, + a: 0.13, + washL: 46, + opaqueCMax: 0.055, +}; +/* tokens.css :root: the solid floor alpha, the solidify floor's chroma scale and its derived cap. */ +const SOLID_A = 0.65; +const OPAQUE_C_SCALE = 0.85; +const SOLIDIFY_C_MAX = LAPIS.opaqueCMax * 0.65; +/* check-contrast's [margin]: the floor plus 1.5 Lc of slack. */ +const BODY_WITH_MARGIN = READABLE_USAGE.body.floor + 1.5; + +const root = () => document.documentElement; +const read = (name: string) => root().style.getPropertyValue(name).trim(); + +/** The surface body text sits on, composed the way check-contrast and AutoForeground compose it. */ +function lapisSurface(solidifyL: number, opacity: number): OklchColor { + return glassSolidSurface( + false, + { + h: LAPIS.h, + c: LAPIS.c, + a: LAPIS.a, + }, + SOLID_A, + LAPIS.washL, + 2.5, + { + l: solidifyL, + c: Math.min(LAPIS.c * OPAQUE_C_SCALE, SOLIDIFY_C_MAX), + a: opacity, + }, + ); +} + +async function mountLapis(opts: { solidifyL?: number; opacity?: number } = {}) { + const s = root().style; + s.setProperty("--glass-tint-h", String(LAPIS.h)); + s.setProperty("--glass-tint-c", String(LAPIS.c)); + s.setProperty("--glass-tint-a", String(LAPIS.a)); + s.setProperty("--glass-wash-l", `${LAPIS.washL}%`); + s.setProperty("--glass-opaque-c-max", String(LAPIS.opaqueCMax)); + if (opts.solidifyL !== undefined) s.setProperty("--glass-solidify-l", String(opts.solidifyL)); + if (opts.opacity !== undefined) s.setProperty("--glass-opacity", String(opts.opacity)); + render(); + await waitFor(() => expect(read("--foreground")).not.toBe("")); + const body = parseOklch(read("--foreground")); + if (!body) throw new Error(`--foreground is not oklch: ${read("--foreground")}`); + return body; +} + +function expectInkBlue(body: OklchColor, surface: OklchColor) { + const lc = Math.abs(apcaContrast(body, surface)); + const rgb = oklchToSrgb(body.l, body.c, body.h); + const label = `--foreground L${body.l.toFixed(1)} C${body.c.toFixed(3)} on surface L${surface.l.toFixed(1)} (${lc.toFixed(1)} Lc)`; + expect(body.l, `${label}: below the tonal clip, i.e. the raw ramp's black end`).toBeGreaterThanOrEqual(TONAL_MIN_L); + /* The honest black test is the rendered channels, not the requested chroma: at L≈0 any chroma renders + #000000, and lapis L20 renders #09014e, whose brightest channel (blue) is ~0.31. */ + expect(Math.max(...rgb), `${label}: renders black (sRGB ${rgb.map((v) => v.toFixed(3))})`).toBeGreaterThanOrEqual(0.1); + expect(lc, `${label}: under the body floor plus margin`).toBeGreaterThanOrEqual(BODY_WITH_MARGIN); +} + +beforeEach(() => { + localStorage.clear(); + root().className = ""; + root().removeAttribute("style"); +}); + +afterEach(() => { + localStorage.clear(); + root().className = ""; + root().removeAttribute("style"); +}); + +describe("AutoForeground on lapis, light mode", () => { + it("keeps body text ink-blue on the shipped backing (solidify L92)", async () => { + const body = await mountLapis(); + expectInkBlue(body, lapisSurface(92, 0.7)); + }); + + it("keeps body text ink-blue at a higher --glass-opacity", async () => { + const body = await mountLapis({ + opacity: 0.8, + }); + expectInkBlue(body, lapisSurface(92, 0.8)); + }); + + /* The older L88 backing, which the consumer's measurement was assumed to match. It does not reproduce + the report under today's compositing: the surface lands at L84.9 and the L20 step clears the floor + (75.2 Lc), so the guard never fires. Pinned so a model change that tips it under is caught here. */ + it("keeps body text ink-blue on the older L88 backing", async () => { + const surface = lapisSurface(88, 0.7); + const body = await mountLapis({ + solidifyL: 88, + }); + expect(body.l).toBeGreaterThanOrEqual(TONAL_MIN_L); + expect(Math.abs(apcaContrast(body, surface))).toBeGreaterThanOrEqual(READABLE_USAGE.body.floor); + }); + + /* The REPORTED shape, reproduced: the tinted L20 step misses the floor (73.0 Lc) and the black end + misses it too (74.8), so the guard spent the whole hue on 1.8 Lc that still was not legible. Bounded + now: a tonal pick within TONAL_HUE_SLACK_LC of the floor beats an extreme that misses it as well. */ + it("keeps the hue when black would still miss the floor (L86 backing)", async () => { + const surface = lapisSurface(86, 0.7); + const body = await mountLapis({ + solidifyL: 86, + }); + const lc = Math.abs(apcaContrast(body, surface)); + const rgb = oklchToSrgb(body.l, body.c, body.h); + expect(body.l, `L${body.l.toFixed(1)} at ${lc.toFixed(1)} Lc on L${surface.l.toFixed(1)}`).toBeGreaterThanOrEqual(TONAL_MIN_L); + expect(Math.max(...rgb)).toBeGreaterThanOrEqual(0.1); + expect(lc).toBeGreaterThanOrEqual(READABLE_USAGE.body.floor - TONAL_HUE_SLACK_LC); + }); + + /* The other side of the bound. At the consumer's reported surface (L84.2, an 86.8 backing) black DOES + clear the floor (75.7 Lc) and L20 does not (73.9), and legibility outranks hue: that is the line + check-contrast signs the presets off against, so the guard must still take the extreme here. */ + it("still prefers a fallback that clears the floor (the reported L84.2 surface)", async () => { + const surface = lapisSurface(86.8, 0.7); + const body = await mountLapis({ + solidifyL: 86.8, + }); + expect(body.l).toBeLessThan(TONAL_MIN_L); + expect(Math.abs(apcaContrast(body, surface))).toBeGreaterThanOrEqual(READABLE_USAGE.body.floor); + }); +}); diff --git a/tests/chart-container.test.tsx b/tests/chart-container.test.tsx new file mode 100644 index 00000000..0fbc00d4 --- /dev/null +++ b/tests/chart-container.test.tsx @@ -0,0 +1,152 @@ +/** + * ChartContainer: one size contract, one observer, nothing at 0×0. + * + * The container used to hand recharts a percent-sized ResponsiveContainer with `minHeight={150}` inside + * an `aspect-video` box. Recharts observes that div itself, the floor grew the box it was measuring, and + * WebKit reported "ResizeObserver loop completed with undelivered notifications" every frame; a chart in + * a hidden tab measured 0×0 and recharts warned "width(0) and height(0) of chart should be greater than + * 0" on every pass. Now ONE observer measures a plot box the chart never sits in (it is absolute), and + * the chart gets the measured size as numbers, which recharts renders without a second observer. + * + * jsdom has no layout, so the ResizeObserver is stubbed and driven by hand: each test decides what the + * plot box measures. + */ +import { act, render } from "@testing-library/react"; +import { Bar, BarChart } from "recharts"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { type ChartConfig, ChartContainer } from "@/components/ui/chart"; + +const config = { + value: { + label: "Value", + color: "var(--chart-1)", + }, +} satisfies ChartConfig; + +const data = [ + { + month: "Jan", + value: 40, + }, + { + month: "Feb", + value: 90, + }, +]; + +class StubObserver { + static instances: StubObserver[] = []; + targets: Element[] = []; + constructor(private readonly callback: ResizeObserverCallback) { + StubObserver.instances.push(this); + } + observe(target: Element) { + this.targets.push(target); + } + unobserve() {} + disconnect() { + this.targets = []; + } + /** Report a size for every observed target, as the browser would after layout. */ + resize(width: number, height: number) { + const entries = this.targets.map( + (target) => + ({ + target, + contentRect: { + width, + height, + }, + }) as unknown as ResizeObserverEntry, + ); + act(() => this.callback(entries, this as unknown as ResizeObserver)); + } +} + +const original = globalThis.ResizeObserver; + +beforeEach(() => { + StubObserver.instances = []; + globalThis.ResizeObserver = StubObserver as unknown as typeof ResizeObserver; +}); + +afterEach(() => { + globalThis.ResizeObserver = original; + vi.restoreAllMocks(); +}); + +function renderChart() { + return render( + + + + + , + ); +} + +const svg = (container: HTMLElement) => container.querySelector("svg.recharts-surface"); + +describe("ChartContainer sizing", () => { + it("renders no chart before the plot box has a size", () => { + const { container } = renderChart(); + expect(container.querySelector('[data-slot="chart-plot"]')).not.toBeNull(); + expect(svg(container)).toBeNull(); + }); + + it("renders nothing at 0×0 (a chart in a hidden tab)", () => { + const { container } = renderChart(); + StubObserver.instances[0].resize(0, 0); + expect(svg(container)).toBeNull(); + }); + + it("renders the chart at the measured size", () => { + const { container } = renderChart(); + StubObserver.instances[0].resize(400, 225); + const surface = svg(container); + expect(surface).not.toBeNull(); + expect(surface?.getAttribute("width")).toBe("400"); + expect(surface?.getAttribute("height")).toBe("225"); + }); + + it("keeps the last size when the box collapses to 0×0", () => { + const { container } = renderChart(); + StubObserver.instances[0].resize(400, 225); + StubObserver.instances[0].resize(0, 0); + expect(svg(container)?.getAttribute("width")).toBe("400"); + }); + + it("observes the plot box with exactly one observer", () => { + const { container } = renderChart(); + StubObserver.instances[0].resize(400, 225); + expect(StubObserver.instances).toHaveLength(1); + expect(StubObserver.instances[0].targets).toEqual([ + container.querySelector('[data-slot="chart-plot"]'), + ]); + /* Recharts' percent-size detector would render this wrapper and observe it; the fixed path does not. */ + expect(container.querySelector(".recharts-responsive-container")).toBeNull(); + }); + + it("never logs recharts' zero-size warning", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const { container } = renderChart(); + StubObserver.instances[0].resize(0, 0); + StubObserver.instances[0].resize(400, 225); + expect(svg(container)).not.toBeNull(); + const zeroSize = warn.mock.calls.filter(([msg]) => String(msg).includes("should be greater than 0")); + expect(zeroSize).toEqual([]); + }); + + it("keeps the caller's height and the 150px floor on the container", () => { + const { container } = renderChart(); + const root = container.querySelector("[data-chart]"); + expect(root).toHaveClass("h-[300px]", "min-h-[150px]", "aspect-video"); + }); + + it("falls back to recharts' own percent container without ResizeObserver", () => { + // @ts-expect-error — simulating an engine without the API + delete globalThis.ResizeObserver; + const { container } = renderChart(); + expect(container.querySelector(".recharts-responsive-container")).not.toBeNull(); + }); +}); diff --git a/tests/combobox.test.tsx b/tests/combobox.test.tsx new file mode 100644 index 00000000..3c8e9230 --- /dev/null +++ b/tests/combobox.test.tsx @@ -0,0 +1,60 @@ +/** + * Combobox: an enabled option is clickable. + * + * The bug this pins came from cmdk (tests/command.test.tsx): a primitive that writes + * `data-disabled="false"` on every enabled item turns Tailwind's bare `data-disabled:` variant, a + * presence selector, into `pointer-events: none` on every row. Base UI omits the attribute when an item + * is enabled, which is what makes the bare variant safe in combobox.tsx; if that ever changes, these fail + * before a user finds a picker that cannot be clicked. + */ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxList } from "@/components/ui/combobox"; + +const fruits = [ + "Apple", + "Banana", + "Cherry", +]; + +function renderOpen(onValueChange?: (v: unknown) => void) { + render( + + + + + {(item: string) => ( + + {item} + + )} + + + , + ); +} + +const option = (name: string) => screen.getByText(name).closest('[data-slot="combobox-item"]') as HTMLElement; + +describe("Combobox", () => { + it("puts no data-disabled attribute on an enabled item", async () => { + renderOpen(); + await waitFor(() => expect(screen.getByText("Apple")).toBeInTheDocument()); + expect(option("Apple").hasAttribute("data-disabled")).toBe(false); + expect(option("Cherry").hasAttribute("data-disabled")).toBe(true); + }); + + it("selects an enabled item on click", async () => { + const changes: unknown[] = []; + renderOpen((v) => changes.push(v)); + await waitFor(() => expect(screen.getByText("Banana")).toBeInTheDocument()); + fireEvent.click(option("Banana")); + await waitFor(() => expect(changes).toContain("Banana")); + }); + + it("draws the popup as bordered, veiled glass", async () => { + renderOpen(); + await waitFor(() => expect(document.querySelector('[data-slot="combobox-content"]')).not.toBeNull()); + expect(document.querySelector('[data-slot="combobox-content"]')).toHaveClass("glass", "glass-border", "glass-veil", "rounded-xl"); + }); +}); diff --git a/tests/command.test.tsx b/tests/command.test.tsx new file mode 100644 index 00000000..e336f526 --- /dev/null +++ b/tests/command.test.tsx @@ -0,0 +1,75 @@ +/** + * The picker entries must be CLICKABLE, not keyboard-only. + * + * Reported 2026-08-31: selecting a skill or Library document — at session setup + * and in the Context sheet — required pressing Enter. Clicking an entry did + * nothing. + * + * The cause was a stylesheet bug wearing a behaviour bug's clothes: + * + * 1. cmdk renders `data-disabled={!!disabled}` on EVERY item, enabled or not. + * 2. React stringifies booleans on `data-*` attributes, so an enabled item + * ships the *present* attribute `data-disabled="false"`. + * 3. Tailwind compiles the bare `data-[disabled]:` variant to the attribute + * PRESENCE selector `[data-disabled]`, which matches `"false"` exactly as + * well as `"true"`. + * + * So `[data-disabled]{pointer-events:none}` applied to every entry in every + * command list. Enter kept working — cmdk dispatches its own `cmdk-item-select` + * event straight at the node, which never consults pointer-events — and that is + * what disguised a CSS defect as a mouse-only quirk. + * + * The Radix menus next door use the same bare variant safely, because Radix + * omits the attribute when an item is enabled. The variant is only a trap for + * primitives that always emit it, so the guard is scoped to this file rather + * than banned repo-wide. + */ +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Command as CommandPrimitive } from "cmdk"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; + +const SOURCE = resolve(dirname(fileURLToPath(import.meta.url)), "../components/ui/command.tsx"); + +describe("CommandItem is clickable", () => { + // The precondition the guard below rests on. If cmdk ever starts omitting + // the attribute for enabled items this test fails, and the guard can go. + it('cmdk puts data-disabled="false" on an ENABLED item', () => { + const html = renderToStaticMarkup( + + + Enabled + + , + ); + + expect(html).toContain('data-disabled="false"'); + }); + + // The guard. `data-[disabled]:` matches that "false" and kills the click. + it("styles disabled state with data-[disabled=true], never the bare variant", () => { + const source = readFileSync(SOURCE, "utf8"); + const bare = source.match(/data-\[disabled\]:[a-z0-9-]+/g) ?? []; + + expect( + bare, + `bare data-[disabled]: variants compile to the presence selector [data-disabled], which +matches the data-disabled="false" cmdk puts on every ENABLED item — so these would apply to +entries that are not disabled. With pointer-events-none among them, no picker entry can be +clicked. Use data-[disabled=true]: instead. Found: ${bare.join(", ")}`, + ).toEqual([]); + }); + + // Belt and braces: whatever the spelling, the click must not be disabled for + // an enabled row. This is the property the user actually cares about. + it("does not disable pointer events except on a genuinely disabled item", () => { + const source = readFileSync(SOURCE, "utf8"); + for (const variant of source.match(/\S*:pointer-events-none/g) ?? []) { + // `[&_svg]:` is fine and deliberate — it lets a click on an icon fall + // through to the row rather than being swallowed by the icon. + expect(variant, `${variant} would block clicks on enabled rows`).toMatch(/^(\[&_svg\]|data-\[disabled=true\]):/); + } + }); +}); diff --git a/tests/dialog.test.tsx b/tests/dialog.test.tsx index 7f878275..8c6b14d8 100644 --- a/tests/dialog.test.tsx +++ b/tests/dialog.test.tsx @@ -8,7 +8,7 @@ */ import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; -import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; +import { Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; function open(props: React.ComponentProps = {}) { render( @@ -83,3 +83,88 @@ describe("DialogContent: bodyClassName", () => { expect(wrapper).not.toHaveClass("custom-surface"); }); }); + +/** + * DialogBody: only the body scrolls. + * + * The layout wrapper used to be the one scroll box for everything, header and footer included, and + * `overflow-y: auto` clips on all four sides: a glass button's glow in the footer was sliced at the edge + * it touched, and the footer scrolled away with the text. With a DialogBody child the wrapper turns into + * a non-clipping flex column and the body alone scrolls. jsdom does not evaluate `:has()` for layout, so + * these pin the classes that do it and the structure they depend on. + */ +function openWithBody() { + render( + + + + Title + + +

    Body

    +
    + + + +
    +
    , + ); + return { + header: screen.getByTestId("header"), + body: screen.getByTestId("body"), + footer: screen.getByTestId("footer"), + }; +} + +describe("DialogBody", () => { + it("keeps the footer and header outside the scrolling body", () => { + const { header, body, footer } = openWithBody(); + expect(body).toHaveAttribute("data-slot", "dialog-body"); + expect(body).toHaveClass("overflow-y-auto", "min-h-0", "flex-1"); + expect(body.contains(footer)).toBe(false); + expect(body.contains(header)).toBe(false); + /* Siblings in the layout wrapper, so they sit around the scroller rather than in it. */ + expect(footer.parentElement).toBe(body.parentElement); + expect(header.parentElement).toBe(body.parentElement); + }); + + it("turns the layout wrapper into a non-clipping column only when a body is present", () => { + const { body } = openWithBody(); + const layout = body.parentElement; + expect(layout).toHaveAttribute("data-slot", "dialog-layout"); + expect(layout).toHaveClass( + "has-[>[data-slot=dialog-body]]:flex", + "has-[>[data-slot=dialog-body]]:flex-col", + "has-[>[data-slot=dialog-body]]:overflow-visible", + ); + }); + + it("keeps the chrome from shrinking when the body is tall", () => { + const { header, footer } = openWithBody(); + expect(header).toHaveClass("shrink-0"); + expect(footer).toHaveClass("shrink-0"); + }); + + it("runs the scroll box to the dialog's edges, with room for a glow", () => { + const { body } = openWithBody(); + expect(body).toHaveClass("-mx-6", "px-6", "-my-2", "py-2"); + }); + + it("lets its children shrink below their content width", () => { + const { body } = openWithBody(); + expect(body).toHaveClass("[&>*]:min-w-0"); + expect(body.parentElement).toHaveClass("min-w-0", "[&>*]:min-w-0"); + }); + + it("merges a className rather than replacing the defaults", () => { + const { body } = openWithBody(); + expect(body).toHaveClass("custom-body", "overflow-y-auto", "grid", "gap-4"); + }); + + it("leaves a dialog without a body scrolling as one box, as before", () => { + const wrapper = open(); + expect(wrapper).toHaveAttribute("data-slot", "dialog-layout"); + expect(wrapper).toHaveClass("grid", "overflow-y-auto", "min-h-0"); + expect(wrapper.querySelector('[data-slot="dialog-body"]')).toBeNull(); + }); +}); diff --git a/tests/field.test.tsx b/tests/field.test.tsx new file mode 100644 index 00000000..8787e9e5 --- /dev/null +++ b/tests/field.test.tsx @@ -0,0 +1,83 @@ +/** + * Field: the fixes over shadcn's version. + * + * - A label wrapping a Field (a choice card) lights up from the control's `data-state="checked"`; + * shadcn's `has-data-checked:` looked for a `data-checked` attribute no Radix control renders. + * - A horizontal Field balances its description through `data-orientation`, which the Field sets; + * shadcn's `group-has-data-horizontal/field:` asked for an attribute that does not exist. + * - FieldSeparator's label sits between two rules instead of on a `bg-background` patch, which on glass + * painted an opaque page-coloured block. + * - FieldError de-duplicates messages and renders one as text, several as a list. + */ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Field, FieldDescription, FieldError, FieldLabel, FieldSeparator } from "@/components/ui/field"; + +describe("Field", () => { + it("styles a choice card from the Radix checked state", () => { + render(Plan); + const cls = screen.getByTestId("label").className; + expect(cls).toContain("has-data-[state=checked]:bg-primary/5"); + expect(cls).not.toMatch(/has-data-checked:/); + }); + + it("keys the horizontal description on data-orientation", () => { + render( + + Hint + , + ); + expect(screen.getByTestId("field")).toHaveAttribute("data-orientation", "horizontal"); + expect(screen.getByTestId("desc").className).toContain("group-data-[orientation=horizontal]/field:text-balance"); + }); + + it("separates with two rules and no background knock-out", () => { + const { container } = render(or); + expect(container.querySelectorAll('[data-slot="separator"]')).toHaveLength(2); + expect(container.innerHTML).not.toContain("bg-background"); + }); + + it("renders a single separator without a label", () => { + const { container } = render(); + expect(container.querySelectorAll('[data-slot="separator"]')).toHaveLength(1); + }); + + it("de-duplicates errors and renders one as text", () => { + render( + , + ); + const alert = screen.getByRole("alert"); + expect(alert).toHaveTextContent("Required"); + expect(alert.querySelector("ul")).toBeNull(); + }); + + it("lists several distinct errors", () => { + render( + , + ); + expect(screen.getByRole("alert").querySelectorAll("li")).toHaveLength(2); + }); + + it("renders nothing without errors or children", () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/tests/item.test.tsx b/tests/item.test.tsx new file mode 100644 index 00000000..9ae74b27 --- /dev/null +++ b/tests/item.test.tsx @@ -0,0 +1,68 @@ +/** + * Item: each variant's surface role, and Empty/Kbd's surfaces (the small adopted items). + */ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Empty, EmptyMedia } from "@/components/ui/empty"; +import { Item } from "@/components/ui/item"; +import { Kbd } from "@/components/ui/kbd"; + +describe("Item", () => { + it("is a plain row by default", () => { + render(Row); + expect(screen.getByTestId("i")).not.toHaveClass("glass"); + }); + + it("is bordered glass as outline and borderless glass as muted", () => { + render( + <> + + A + + + B + + , + ); + expect(screen.getByTestId("outline")).toHaveClass("glass", "glass-border"); + expect(screen.getByTestId("muted")).toHaveClass("glass"); + expect(screen.getByTestId("muted")).not.toHaveClass("glass-border"); + }); + + it("renders its child element with asChild", () => { + render( + + Security settings + , + ); + const link = screen.getByText("Security settings"); + expect(link.tagName).toBe("A"); + expect(link).toHaveAttribute("data-slot", "item"); + expect(link).toHaveClass("glass"); + }); +}); + +describe("Empty and Kbd", () => { + it("keeps Empty plain unless asked for glass", () => { + render( + <> + + + , + ); + expect(screen.getByTestId("plain")).not.toHaveClass("glass"); + expect(screen.getByTestId("glass")).toHaveClass("glass"); + }); + + it("draws the icon media as a bordered small-tier chip", () => { + render(); + expect(screen.getByTestId("m")).toHaveClass("glass", "glass-border", "glass-sm"); + }); + + it("draws a key cap in theme tokens, not a glass surface", () => { + render(K); + const cap = screen.getByText("K"); + expect(cap).toHaveClass("bg-foreground/10", "border-[var(--glass-border)]"); + expect(cap).not.toHaveClass("glass"); + }); +}); diff --git a/tests/material.test.tsx b/tests/material.test.tsx new file mode 100644 index 00000000..b6a7cabd --- /dev/null +++ b/tests/material.test.tsx @@ -0,0 +1,128 @@ +/** + * The material helpers: class output, role inheritance, and that axis props never reach the DOM. + * + * Every component resolves its surface through `materialSurface(role, axes)` and strips the axes with + * `splitAxisProps`. An axis that glassMaterial ignores silently does nothing; one that splitAxisProps + * forgets lands on the element as an unknown attribute (React warns, and `borderColor` would serialize as + * `bordercolor="ink"`). The ink border colour is the newest axis, so it is the one pinned here end to end. + */ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Card } from "@/components/ui/card"; +import { glassMaterial, materialSurface, splitAxisProps } from "@/lib/material"; + +describe("glassMaterial", () => { + it("emits the structural class plus one class per axis", () => { + const { className } = glassMaterial({ + border: "rim", + borderColor: "ink", + veil: true, + size: "lg", + }); + expect(className.split(" ")).toEqual([ + "glass", + "glass-border", + "glass-border-rim", + "glass-border-ink", + "glass-veil", + "glass-lg", + ]); + }); + + it("keeps the material's own edge colour by default", () => { + expect( + glassMaterial({ + border: true, + }).className, + ).not.toContain("glass-border-ink"); + expect( + glassMaterial({ + border: true, + borderColor: "glass", + }).className, + ).not.toContain("glass-border-ink"); + }); + + it("returns nothing for material none", () => { + expect( + glassMaterial({ + material: "none", + border: true, + borderColor: "ink", + }), + ).toEqual({ + className: "", + }); + }); +}); + +describe("materialSurface", () => { + it("lets a role default the border colour", () => { + const m = materialSurface( + { + border: true, + borderColor: "ink", + }, + {}, + ); + expect(m?.className).toContain("glass-border-ink"); + }); + + it("lets an explicit prop override the role's border colour", () => { + const m = materialSurface( + { + border: true, + borderColor: "ink", + }, + { + borderColor: "glass", + }, + ); + expect(m?.className).not.toContain("glass-border-ink"); + }); + + it("inherits every axis the instance leaves unset", () => { + const m = materialSurface( + { + border: "frame", + veil: true, + }, + { + borderColor: "ink", + }, + ); + expect(m?.className.split(" ")).toEqual([ + "glass", + "glass-border", + "glass-border-frame", + "glass-border-ink", + "glass-veil", + ]); + }); +}); + +describe("splitAxisProps", () => { + it("takes borderColor with the other axes", () => { + const [axes, rest] = splitAxisProps({ + borderColor: "ink" as const, + border: true, + id: "x", + }); + expect(axes.borderColor).toBe("ink"); + expect(rest).toEqual({ + id: "x", + }); + }); + + it("does not leak borderColor onto the DOM through a component", () => { + const { container } = render( + + Body + , + ); + const card = container.querySelector('[data-slot="card"]') as HTMLElement; + expect(card).toHaveClass("glass-border", "glass-border-ink"); + expect(card.getAttribute("bordercolor")).toBeNull(); + expect(card.getAttribute("borderColor")).toBeNull(); + }); +}); diff --git a/tests/menu-sub-portal.test.tsx b/tests/menu-sub-portal.test.tsx new file mode 100644 index 00000000..5d8fb670 --- /dev/null +++ b/tests/menu-sub-portal.test.tsx @@ -0,0 +1,96 @@ +/** + * Sub menus render in a portal, like the menu that opens them. + * + * A glass menu draws a `backdrop-filter`, and an element with a backdrop filter is the containing block + * of every fixed-position descendant. Radix positions a sub menu with `position: fixed`, so a SubContent + * rendered INSIDE the menu was placed against the menu rather than the viewport, and clipped by the + * menu's `overflow-y-auto`: the sub menu opened as a sliver inside its parent. Portaling it, as Content + * already is, takes it out of that box. The assertion is structural (no menu-content ancestor) because + * jsdom computes no layout to clip against. + */ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +describe("DropdownMenuSubContent", () => { + it("is portaled out of the menu content", () => { + render( + + Open + + Item + + More + + Nested + + + + , + ); + + const sub = document.querySelector('[data-slot="dropdown-menu-sub-content"]'); + expect(sub).not.toBeNull(); + expect(screen.getByText("Nested")).toBeInTheDocument(); + expect(sub?.closest('[data-slot="dropdown-menu-content"]')).toBeNull(); + }); + + it("still carries the menu's glass surface", () => { + render( + + Open + + + More + + Nested + + + + , + ); + expect(document.querySelector('[data-slot="dropdown-menu-sub-content"]')).toHaveClass("glass", "glass-border", "glass-veil"); + }); +}); + +describe("ContextMenuSubContent", () => { + it("is portaled out of the menu content", () => { + render( + + Target + + Item + + More + + Nested + + + + , + ); + fireEvent.contextMenu(screen.getByText("Target")); + + const sub = document.querySelector('[data-slot="context-menu-sub-content"]'); + expect(sub).not.toBeNull(); + expect(screen.getByText("Nested")).toBeInTheDocument(); + expect(sub?.closest('[data-slot="context-menu-content"]')).toBeNull(); + }); +}); diff --git a/tests/menubar.test.tsx b/tests/menubar.test.tsx new file mode 100644 index 00000000..9757db43 --- /dev/null +++ b/tests/menubar.test.tsx @@ -0,0 +1,84 @@ +/** + * Menubar: material-aware, and its menus render in portals, sub menus included. + * + * Same reason as dropdown-menu and context-menu (tests/menu-sub-portal.test.tsx): a glass menu's + * backdrop-filter makes it the containing block of every fixed-position descendant, so a sub menu left + * inside it is positioned against the menu and clipped by it. + */ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { + Menubar, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +} from "@/components/ui/menubar"; + +function openMenubar(props: { variant?: "default" | "glass" } = {}) { + render( + + + File + + New tab + + Share + + Email link + + + + + , + ); + return { + bar: document.querySelector('[data-slot="menubar"]') as HTMLElement, + content: document.querySelector('[data-slot="menubar-content"]') as HTMLElement, + sub: document.querySelector('[data-slot="menubar-sub-content"]') as HTMLElement, + }; +} + +describe("Menubar", () => { + it("portals the sub menu out of the menu", () => { + const { content, sub } = openMenubar(); + expect(content).not.toBeNull(); + expect(sub).not.toBeNull(); + expect(screen.getByText("Email link")).toBeInTheDocument(); + expect(sub.closest('[data-slot="menubar-content"]')).toBeNull(); + }); + + it("portals the menu out of the bar", () => { + const { bar, content } = openMenubar(); + expect(bar.contains(content)).toBe(false); + }); + + it("draws the bar as bordered glass and both menus as bordered, veiled glass", () => { + const { bar, content, sub } = openMenubar(); + expect(bar).toHaveClass("glass", "glass-border"); + expect(bar).not.toHaveClass("glass-veil"); + for (const menu of [ + content, + sub, + ]) + expect(menu).toHaveClass("glass", "glass-border", "glass-veil"); + }); + + it("highlights rows with the accent token", () => { + openMenubar(); + const item = screen.getByText("New tab"); + expect(item.className).toContain("focus:bg-accent"); + }); + + it("drops the surface for variant=default", () => { + const { bar, content } = openMenubar({ + variant: "default", + }); + expect(bar).not.toHaveClass("glass"); + expect(content).not.toHaveClass("glass"); + expect(content).toHaveClass("bg-popover"); + }); +}); diff --git a/tests/pattern-background.test.tsx b/tests/pattern-background.test.tsx new file mode 100644 index 00000000..83c071b9 --- /dev/null +++ b/tests/pattern-background.test.tsx @@ -0,0 +1,110 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { ANIMATED_PATTERNS, PATTERN_STYLES, PatternBackground, stopRules } from "@/components/pattern-background"; + +/** + * A scene stops for two reasons — `prefers-reduced-motion` and Speed 0 — and the + * bug these tests pin is the two DRIFTING APART: every scene honoured reduced + * motion while Speed 0 only set `animation-play-state: paused`, which leaves the + * element in an animating state and keeps WebKit re-rendering its layer + * (`.au-curtain` carries a `filter: blur()` and a two-layer `mask-composite`, six + * of them at full-viewport size). See AMXSF-562. + * + * `stopRules` is the single source both conditions are emitted from, so these + * assert the property that makes drift impossible rather than the CSS text. + */ +describe("stopRules", () => { + it("emits BOTH stop conditions for the same selectors", () => { + const css = stopRules(".au-curtain", ".au-ripples"); + + expect(css).toContain("@media (prefers-reduced-motion: reduce)"); + expect(css).toContain("[data-pat-static] .au-curtain"); + expect(css).toContain("[data-pat-static] .au-ripples"); + }); + + it("REMOVES the animation rather than pausing it, in both conditions", () => { + const css = stopRules(".au-curtain"); + + // Two rule bodies, one per condition — and neither may park the animation. + expect(css.match(/animation:\s*none/g)).toHaveLength(2); + expect(css).not.toContain("animation-play-state"); + expect(css).not.toContain("paused"); + }); + + it("releases will-change in both conditions", () => { + // `will-change` on a stopped animation is a promoted layer doing no work. + // The old declaration resolved to `will-change: auto, auto` at Speed 0 — + // invalid CSS, so it was dropped and the release never happened. + const css = stopRules(".du-grain-tile", ".du-veil"); + + expect(css.match(/will-change:\s*auto/g)).toHaveLength(2); + expect(css).not.toContain("auto, auto"); + }); + + it("scopes every selector under the static attribute, not just the first", () => { + // A `.a, .b` list prefixed once would leave `.b` unscoped and still animating. + const css = stopRules(".pac-runner", ".pac-man", ".pac-ghost"); + const staticRule = css.split("\n").at(-1) ?? ""; + + for (const sel of [ + ".pac-runner", + ".pac-man", + ".pac-ghost", + ]) { + expect(staticRule).toContain(`[data-pat-static] ${sel}`); + } + }); + + it("holds for a single selector", () => { + const css = stopRules(".sw-grid-rungs"); + + expect(css).toContain("@media (prefers-reduced-motion: reduce) { .sw-grid-rungs {"); + expect(css).toContain("[data-pat-static] .sw-grid-rungs {"); + }); +}); + +/** + * The other half of the same bug: `stopRules` can emit a perfect + * `[data-pat-static]` rule and it still does nothing unless the attribute actually + * lands on the scene root. Rendered with `renderToStaticMarkup` — no DOM needed, + * and the first render is where the attribute is decided, so nothing is lost by + * not having effects. + */ +describe("PatternBackground data-pat-static", () => { + const markup = (style: (typeof PATTERN_STYLES)[number], speed: number) => renderToStaticMarkup(); + + // Match the rendered ATTRIBUTE, not the string. Each scene inlines its own + // stylesheet, which always contains the `[data-pat-static]` SELECTOR — so a bare + // `toContain("data-pat-static")` passes even when the attribute is absent. React + // emits `=""` for the empty-string prop, and the selector never has an `=`. + const ATTR = 'data-pat-static=""'; + + it("marks every animated scene static at speed 0", () => { + for (const style of ANIMATED_PATTERNS) { + expect(markup(style, 0), `${style} at speed 0`).toContain(ATTR); + } + }); + + it("leaves the attribute OFF while the scene is running", () => { + for (const style of ANIMATED_PATTERNS) { + expect(markup(style, 8), `${style} at speed 8`).not.toContain(ATTR); + } + }); + + it("still renders the scene when static — stopped is not hidden", () => { + // `animation: none` must stop the motion without removing the artwork. + const stopped = markup("aurora", 0); + expect(stopped).toContain("au-curtain"); + expect(stopped).toContain('data-pattern="aurora"'); + }); +}); + +describe("ANIMATED_PATTERNS", () => { + it("only names real pattern styles", () => { + // The set gates the speed control; a stale name there means a scene whose + // Speed 0 silently does nothing. + for (const style of ANIMATED_PATTERNS) { + expect(PATTERN_STYLES).toContain(style); + } + }); +}); diff --git a/tests/progress.test.tsx b/tests/progress.test.tsx new file mode 100644 index 00000000..8374d232 --- /dev/null +++ b/tests/progress.test.tsx @@ -0,0 +1,37 @@ +/** + * Progress: Slider's track and fill, and the indicator offset that draws the value. + */ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Progress } from "@/components/ui/progress"; + +const parts = (value?: number) => { + const { container } = render(); + return { + root: container.querySelector('[data-slot="progress"]') as HTMLElement, + indicator: container.querySelector('[data-slot="progress-indicator"]') as HTMLElement, + }; +}; + +describe("Progress", () => { + it("offsets the indicator by the remaining fraction", () => { + expect(parts(66).indicator.style.transform).toBe("translateX(-34%)"); + expect(parts(100).indicator.style.transform).toBe("translateX(-0%)"); + }); + + it("treats a missing value as empty", () => { + expect(parts(undefined).indicator.style.transform).toBe("translateX(-100%)"); + }); + + it("uses the slider's track and the glass accent fill", () => { + const { root, indicator } = parts(50); + expect(root).toHaveClass("bg-foreground/15", "rounded-full"); + expect(indicator).toHaveClass("bg-[var(--glass-accent)]"); + }); + + it("reports the value to assistive tech", () => { + const { root } = parts(40); + expect(root).toHaveAttribute("role", "progressbar"); + expect(root).toHaveAttribute("aria-valuenow", "40"); + }); +}); diff --git a/tests/snap-popper.test.tsx b/tests/snap-popper.test.tsx index aea2c8e2..43c6aad5 100644 --- a/tests/snap-popper.test.tsx +++ b/tests/snap-popper.test.tsx @@ -1,7 +1,7 @@ /** * TST-1-2 (the half that needs a DOM) — ref composition and the MutationObserver wiring. * - * The pure rounding is covered without a DOM in scripts/check-units.mjs. What needs jsdom is the + * The pure rounding is covered without a DOM at the bottom of this file. What needs jsdom is the * behaviour AROUND it: that the hook finds the Radix wrapper, re-snaps when Radix rewrites the * transform, does not loop on its own write, and that composeRefs still hands the node to the * forwarded ref (drop that and every consumer's ref silently becomes null). @@ -93,6 +93,75 @@ describe("useSnappedPopper", () => { expect(getByTestId("bare").style.transform).toBe("translate(1.5px, 1.5px)"); }); + /* The real cause of soft popper text: at DPR >= 1.5 Floating UI writes `will-change: transform` next + to the transform, which composites the wrapper, and WebKit drops subpixel antialiasing on a + composited layer. */ + it("resets Floating UI's will-change on mount", () => { + function Promoted() { + const snapRef = useSnappedPopper(); + return ( +
    +
    +
    + ); + } + const { getByTestId } = render(); + expect(getByTestId("wrapper").style.willChange).toBe("auto"); + }); + + it("resets will-change again after Radix rewrites the style", async () => { + let wrapper: HTMLElement | undefined; + render( (wrapper = el)} />); + const el = wrapper as HTMLElement; + + el.style.willChange = "transform"; + el.style.transform = "translate(20.5px, 20.5px)"; + await flush(); + + expect(el.style.willChange).toBe("auto"); + expect(el.style.transform).toBe("translate(21px, 21px)"); + }); + + it("does not loop on its own will-change write", async () => { + let wrapper: HTMLElement | undefined; + render( (wrapper = el)} />); + const el = wrapper as HTMLElement; + /* Count style writes after the first reset: if the re-entrancy guard regressed, every pass would + write again and the records would keep coming. */ + let writes = 0; + const counter = new MutationObserver((records) => { + writes += records.length; + }); + counter.observe(el, { + attributes: true, + attributeFilter: [ + "style", + ], + }); + el.style.willChange = "transform"; + await flush(); + await flush(); + await flush(); + counter.disconnect(); + + expect(el.style.willChange).toBe("auto"); + /* Ours plus the test's own: the write that set "transform" and the one reset to "auto". */ + expect(writes).toBe(2); + }); + + it("leaves will-change unset when Floating UI did not set it (DPR < 1.5)", () => { + let wrapper: HTMLElement | undefined; + render( (wrapper = el)} />); + expect(wrapper?.style.willChange).toBe(""); + }); + it("disconnects its observer on unmount", async () => { let wrapper: HTMLElement | undefined; const view = render( (wrapper = el)} />); diff --git a/tests/sonner.test.tsx b/tests/sonner.test.tsx new file mode 100644 index 00000000..f5bbdb94 --- /dev/null +++ b/tests/sonner.test.tsx @@ -0,0 +1,111 @@ +/** + * Toaster: toasts are glass surfaces from the material system, and status toasts take their status tint. + * + * The theme used to style toasts itself, with `[data-sonner-toast]` selectors woven through the engine, + * the presets and every page style, plus !important overrides against sonner's own unlayered stylesheet + * (which beat every Tailwind utility, so the `glass` classes the Toaster passed were dead). Now the + * Toaster is `unstyled`, the surface is `glassMaterial()`, and an observer stamps `data-glass-tint` from + * sonner's `data-type` so the theme's ordinary status blocks tint the toast. These pin that contract. + */ +import { act, render, waitFor } from "@testing-library/react"; +import { toast } from "sonner"; +import { afterEach, beforeAll, describe, expect, it } from "vitest"; +import { TOAST_TINT, Toaster } from "@/components/ui/sonner"; + +/* sonner resolves theme="system" through matchMedia, which jsdom does not implement. */ +beforeAll(() => { + if (!window.matchMedia) { + window.matchMedia = ((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + })) as unknown as typeof window.matchMedia; + } +}); + +const toastByTitle = (title: string) => + [ + ...document.querySelectorAll("[data-sonner-toast]"), + ].find((t) => t.querySelector("[data-title]")?.textContent === title); + +async function show(fire: () => void, title: string) { + act(fire); + await waitFor(() => expect(toastByTitle(title)).toBeDefined()); + return toastByTitle(title) as HTMLElement; +} + +afterEach(() => { + act(() => { + toast.dismiss(); + }); +}); + +describe("Toaster", () => { + it("renders unstyled toasts on the glass material", async () => { + render(); + const el = await show(() => toast("Plain"), "Plain"); + expect(el).toHaveAttribute("data-styled", "false"); + expect(el).toHaveClass("glass", "glass-border", "glass-veil", "glass-lg", "rounded-xl"); + }); + + it.each(Object.entries(TOAST_TINT))("stamps a %s toast with data-glass-tint=%s", async (type, tint) => { + render(); + const fire = { + success: () => toast.success(`T-${type}`), + error: () => toast.error(`T-${type}`), + warning: () => toast.warning(`T-${type}`), + info: () => toast.info(`T-${type}`), + }[type]; + if (!fire) throw new Error(`no trigger for ${type}`); + const el = await show(fire, `T-${type}`); + await waitFor(() => expect(el).toHaveAttribute("data-glass-tint", tint)); + }); + + it("leaves a default toast on the page tint", async () => { + render(); + const el = await show(() => toast("Default"), "Default"); + expect(el).not.toHaveAttribute("data-glass-tint"); + }); + + it("re-stamps a promise toast when it settles", async () => { + render(); + let resolve: (v: unknown) => void = () => {}; + const promise = new Promise((r) => { + resolve = r; + }); + act(() => { + toast.promise(promise, { + loading: "Saving", + success: "Saved", + error: "Failed", + }); + }); + await waitFor(() => expect(toastByTitle("Saving")).toBeDefined()); + expect(toastByTitle("Saving")).not.toHaveAttribute("data-glass-tint"); + await act(async () => { + resolve(undefined); + await promise; + }); + await waitFor(() => expect(toastByTitle("Saved")).toHaveAttribute("data-glass-tint", "success")); + }); + + it("merges a consumer's classNames key by key", async () => { + render( + , + ); + const el = await show(() => toast("Merged"), "Merged"); + expect(el.querySelector("[data-title]")).toHaveClass("custom-title", "font-medium"); + expect(el).toHaveClass("glass"); + }); +}); diff --git a/tests/table.test.tsx b/tests/table.test.tsx new file mode 100644 index 00000000..6d62647a --- /dev/null +++ b/tests/table.test.tsx @@ -0,0 +1,90 @@ +/** + * Table: a sticky header has to be able to stick. + * + * `position: sticky` binds to the NEAREST scroll container, and `overflow: hidden` makes one. The glass + * variant used to put `rounded-lg overflow-hidden` on the itself (and on its container), so a + * `sticky top-0` header bound to the table's own box, which never scrolls: it rode along with the rows + * while an outer element scrolled around it. The corners are now clipped by `overflow-clip` on the + * container (clip is NOT a scroll container), and `containerClassName` lets a consumer make that + * container the scroller on purpose. + */ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; + +function renderTable(props: React.ComponentProps = {}) { + const { container } = render( +
    + + + Name + + + + + Row + + +
    , + ); + const wrapper = container.querySelector('[data-slot="table-container"]') as HTMLElement; + const table = container.querySelector('[data-slot="table"]') as HTMLElement; + return { + wrapper, + table, + }; +} + +describe("Table: sticky headers", () => { + it("never makes the a scroll container", () => { + for (const variant of [ + "glass", + "default", + ] as const) { + const { table } = renderTable({ + variant, + }); + expect(table.className).not.toMatch(/\boverflow-(hidden|auto|scroll)\b/); + } + }); + + it("clips the glass corners on the container without overflow-hidden", () => { + const { wrapper } = renderTable(); + expect(wrapper).toHaveClass("rounded-lg", "overflow-clip"); + expect(wrapper).not.toHaveClass("overflow-hidden"); + }); + + it("keeps the glass surface rounded on the table", () => { + const { table } = renderTable(); + expect(table).toHaveClass("glass", "rounded-lg"); + }); + + it("merges containerClassName onto the container, not the table", () => { + const { wrapper, table } = renderTable({ + containerClassName: "max-h-96 overflow-y-auto", + }); + expect(wrapper).toHaveClass("rounded-lg", "overflow-clip", "max-h-96", "overflow-y-auto"); + expect(table).not.toHaveClass("max-h-96"); + expect(table).not.toHaveClass("overflow-y-auto"); + }); + + it("does not forward containerClassName to the DOM as an attribute", () => { + const { table } = renderTable({ + containerClassName: "max-h-96", + }); + expect(table.getAttribute("containerclassname")).toBeNull(); + expect(table.getAttribute("containerClassName")).toBeNull(); + }); + + it("leaves the default variant's container unstyled unless asked", () => { + const { wrapper } = renderTable({ + variant: "default", + }); + expect(wrapper.className).toBe(""); + const asked = renderTable({ + variant: "default", + containerClassName: "max-h-96 overflow-y-auto", + }); + expect(asked.wrapper).toHaveClass("max-h-96", "overflow-y-auto"); + }); +}); From 4c645abb752148488eea46d210f025d0c4eab1b2 Mon Sep 17 00:00:00 2001 From: WeekendsuperHero <4048475+WeekendSuperhero@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:25:40 -0700 Subject: [PATCH 2/7] fix light mode --- CHANGELOG.md | 1 + app/theme/tokens.css | 19 +++++++++++++++++-- public/r/theme.json | 2 +- registry/theme/globals.css | 19 +++++++++++++++++-- scripts/check-contrast.mjs | 6 +++++- scripts/check-gamut.mjs | 14 ++++++++++++-- 6 files changed, 53 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7d88f23..99600b80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Horizon scenes**: added three pure-CSS animated pattern backgrounds on the shared 42% horizon, all drawn from photographic references rather than illustration: `clouds` (a deep-blue altitude sky over a parallax cloud deck, with a placeable moon), `dune` (golden-hour ridges receding into haze, blowing sand, a placeable moon), and `aurora` (borealis curtains mirrored in rippling water below the waterline). None of the three draws a horizon *line*: the sky dissolves into the ground across a mottled haze band, and distant layers wash toward the sky's own value. All are light/dark aware, take a half-strength theme cast in the mid-sky, move on compositor-only transforms, and respect reduced motion. `clouds` and `dune` swap the disc per mode: a **sun** by day (a hot core with a wide bloom, sitting low in the warm band for `dune`) and a prominent moon by night, from one shared rule, and `dune` adds a **blowing-sand control** (`breeze` / `storm` / `still`) driving wind veils that stream across the ridges. ### Fixed +- **Dark opaque cards were throttled by the light-mode caps**: each jewel pins `--glass-opaque-c-max` to its hue's sRGB ceiling at the LIGHT floor (L88), in an unscoped preset block that ties `.dark`'s 0.12 guard on specificity and won on source order. So in dark the seven blue/purple/red jewels rendered their opaque floor (and the dark solidify backing, which shares it) at 23–53% of the chroma their hue holds at L36.4: amethyst C 0.065 instead of its 0.106 ask, lapis 0.055 instead of 0.089. The dark guard now has its own `.dark, .dark[data-glass-tint], .dark [data-glass-tint]` rule that outranks the light pins, so dark floors render the documented `tint × 1.05` (+40–60% chroma on sapphire, amethyst, rose, carnelian, tourmaline, lapis and goldstone; the other presets were already uncapped). Opaque body text holds ~81 Lc either way. `check-gamut` and `check-contrast` now model those pins as light-only instead of scoring the leak. - **Component one-liners from the first consumer's patch list**: `CommandItem` dropped its bare `data-[disabled]:` variants, which matched the `data-disabled="false"` cmdk puts on every enabled item and made no picker entry clickable by mouse (items are now `cursor-pointer`; pinned by `tests/command.test.tsx`). `DropdownMenuSubContent` and `ContextMenuSubContent` are portaled like their parent content, so a glass menu's `backdrop-filter` no longer clips its sub menu. `navigation-menu`'s `bg-foreground/10/50` classes, which Tailwind drops silently, are `bg-accent/50`, and `check-theme` gains a `[double-slash]` guard. Every menu row (dropdown, context, select, command, navigation, menu-bar) now highlights in `bg-accent`, so a consumer's `--accent` reaches it. The `ghost` button is borderless (`outline` is the bordered one, as in shadcn). `Input` and `Textarea` no longer scale up on focus; the ring marks it. `Switch` gains a hover state. `auto-foreground` exports `FG_STORAGE_KEY`, `RAMP_KEY` and `FG_EVENT`. - **Theme one-liners from the same list**: the theme no longer maps `--font-sans` / `--font-mono` to `--font-geist-*`, which only this site defines, so a consumer without Geist gets Tailwind's default stacks instead of nothing; the mapping moved to the site-only `app/site.css`, and the site's next/font variables moved to ``, where `:root` resolves them (the site itself had been rendering the system stack). `--veil-floor` is registered `inherits: false`, so a glass surface nested in a veiled one no longer paints its parent's veil. `@keyframes shimmer` is namespaced `sistine-shimmer` and animates `translate`, so the skeleton's highlight sweeps -100% → 100% instead of composing with `-translate-x-full` and parking on top of it. Selenite gets a `[data-glass-tint="selenite"]` block carrying its `--harmony-h: 0` anchor, so a CSS-only consumer has one. Comments: `bun run` instead of `pnpm`, and the browserslist floor is documented as Sistine's own, which consumers should declare for themselves. - **Component behaviour**: `Table`'s glass variant no longer puts `overflow-hidden` on the `
    ` (a scroll container, so a sticky header bound to it and rode along with the rows); the container clips the corners with `overflow-clip` instead, and a new `containerClassName` makes it the scroller (`max-h-96 overflow-y-auto` + a `sticky top-0` header). `ChartContainer` measures its plot box with one ResizeObserver and hands recharts a fixed size from an absolute layer, so the `minHeight={150}` feedback loop ("ResizeObserver loop completed with undelivered notifications") and the zero-size warnings for charts in hidden tabs are gone; the 150px floor moved to the container as `min-h-[150px]`. A new `DialogBody` makes only the body scroll, with the header and footer fixed around it, so a footer button's glow is no longer sliced by the scroll box; dialogs without it behave as before, and every dialog's children can now shrink below their content width. The popper snap also resets the `will-change: transform` Floating UI writes at DPR ≥ 1.5, the actual cause of soft popper text in WebKit (it composites the wrapper and drops subpixel antialiasing). diff --git a/app/theme/tokens.css b/app/theme/tokens.css index c6891a72..5266acd5 100644 --- a/app/theme/tokens.css +++ b/app/theme/tokens.css @@ -487,8 +487,8 @@ 0.31–0.79× of its own ceiling, so nothing clips with no cap at all. 0.095 clears the largest ask (tourmaline 0.0901) and therefore throttles nobody, while still bounding a future preset that pushes chroma far past today's values. check-gamut scores each preset against its own ceiling. */ - /* Guard, not a throttle: the largest dark ask is tourmaline at 0.111, so this binds nobody. */ - --glass-opaque-c-max: 0.12; + /* Guard, not a throttle: the largest dark ask is tourmaline at 0.111, so this binds nobody. Declared + in its own rule right after this block (see there for why). */ /* Dark keeps the solidify floor IDENTICAL to the opaque floor — the split exists to buy LIGHT-mode text contrast, and dark already clears the body floor on every preset with margin. Pinning to the opaque values (rather than leaving :root's L92 / ×0.65 to inherit) is what makes this a light-only @@ -616,6 +616,21 @@ --sidebar-ring: oklch(0.6 0.2 250); /* Light blue ring - 3:1 contrast */ } +/* The dark opaque-floor chroma guard, in its own rule so it can OUTRANK the presets. Each jewel pins + --glass-opaque-c-max to its hue's sRGB ceiling at the LIGHT floor (L88, presets.css), in an unscoped + block. On that block and `.dark` tie at (0,1,0) and presets.css loads later, so the light caps + used to win in dark too: amethyst's dark floor rendered C 0.065 against an ask of 0.106 and a ceiling + of 0.189, lapis 0.055 against 0.236, and every blue/purple/red jewel's dark opaque card came out a dull, + greyed version of its tint (23–53% of the chroma its hue can hold at L36.4). The light caps describe + the light floor only; at L36.4 every preset's ask sits well inside its own ceiling, so this guard is + the only cap dark needs. (0,2,0) beats a preset block; `.dark [data-glass-tint]` covers a scoped tint + inside a dark page, and a preset's own `.dark[data-glass-tint="x"]` block still wins by source order. */ +.dark, +.dark[data-glass-tint], +.dark [data-glass-tint] { + --glass-opaque-c-max: 0.12; +} + @layer base { * { @apply border-border outline-ring/50; diff --git a/public/r/theme.json b/public/r/theme.json index 3c86ecca..1a0de590 100644 --- a/public/r/theme.json +++ b/public/r/theme.json @@ -10,7 +10,7 @@ "files": [ { "path": "registry/theme/globals.css", - "content": "/*\n Sistine theme — aggregator. The theme is authored as focused partials under app/theme/; Tailwind v4\n bundles these relative imports itself (no postcss-import needed). ORDER MATTERS:\n - package imports come first (CSS requires @import before other rules),\n - engine.css must precede presets.css / frescoes.css (a preset's --glass-tint-h overrides the\n engine default by SOURCE ORDER at equal specificity),\n - inside tokens.css, .dark comes after :root (same-element ties resolve by source order).\n Consumers install a FLATTENED single-file build of this graph (registry/theme/globals.css, generated\n by scripts/build-theme.mjs) — run `bun run registry:check` after editing any partial and commit\n registry/theme + public/r. Site-only CSS (the Geist font mapping) lives in app/site.css, which imports\n this file, so nothing the documentation site defines leaks into the shipped theme.\n*/\n@import \"tailwindcss\";\n@import \"tw-animate-css\";\n/* ── Registrations — @custom-variant + @property pins for JS-only tokens ──────\n Loaded first so the dark variant and typed properties exist before anything composes them. */\n\n/* Class-based dark mode, v4-standard form: matches the `.dark` element ITSELF and its descendants, so\n consumers can toggle `.dark` on any wrapper (not just / next-themes), at zero specificity\n (`:where`) so `dark:` overrides by cascade order like every other variant. */\n@custom-variant dark (&:where(.dark, .dark *));\n\n/* Composable, NON-inheriting glow layer: the `glow` effect/prop sets it, and every glass box-shadow\n folds in `var(--glow-layer, …)` so the glow rides INSIDE each material's and page style's shadow\n (they set --srf-shadow; the glow is appended after it) instead of being replaced by it. inherits:false\n keeps a glowing surface from lighting up its glass children. */\n@property --glow-layer {\n syntax: \"*\";\n inherits: false;\n}\n\n/* The veil floor, NON-inheriting for the same reason as the glow: `glass-veil` sets it on the element\n and `glass` paints it as that element's background-color. Inherited, a plain glass surface nested in\n a veiled menu, popover, tooltip, select or card painted its PARENT's veil as its own floor (under the\n Opaque page style: flat white in light, black in dark). A child without a veil of its own now falls\n back to its material's --srf-bg-color, as it would anywhere else. */\n@property --veil-floor {\n syntax: \"*\";\n inherits: false;\n}\n/* Its twin for the solidify layer (see @utility glass-veil): a veiled parent must not restore the layer\n on a child that dropped it. */\n@property --veil-solidify {\n syntax: \"*\";\n inherits: false;\n}\n/* The ink border colour (glass-border-ink): an inked panel must not ink the borders of the glass inside it. */\n@property --brd-ink {\n syntax: \"*\";\n inherits: false;\n}\n\n/* Gradient-text tokens are consumed ONLY from JS inline styles (components/gradient-text.tsx) — no CSS\n rule references them, so Lightning CSS's optimizer would silently drop them (the same failure mode as\n the harmonic tokens below). Registered here to pin them. DO NOT REMOVE. */\n@property --gradient-text {\n syntax: \"*\";\n inherits: true;\n}\n@property --gradient-text-contrast {\n syntax: \"*\";\n inherits: true;\n}\n/* ── Harmonic color registrations — compacted one-liners (biome's CSS formatter is excluded for this\n file; see biome.json). WHY these exist: the comment below. */\n\n/* ── Harmonic token registration ──────────────────────────────────────────────────────────────\n The harmonic tokens (--harmony-h, --hue-*, --color-*, --mono-*, defined in engine.css) are\n consumed ONLY from JS / inline styles (HarmonicSwatchesDemo, AutoForeground) — never referenced by\n a CSS rule. Tailwind v4's Lightning CSS optimizer constant-folds + dead-code-eliminates any custom\n property no CSS rule uses, which silently dropped this ENTIRE subgraph from the COMPILED stylesheet\n (so every `oklch(L C var(--hue-*))` / `var(--color-*)` resolved to nothing at runtime). Registering\n them via @property marks them as first-class typed API the optimizer must preserve. DO NOT REMOVE. */\n@property --harmony-h { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --color-chroma { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --mono-chroma { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-base { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-complement { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-analogous-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-analogous-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-split-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-split-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-triad-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-triad-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-3 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-3 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --color-complement { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-analogous-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-analogous-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-split-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-split-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-triad-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-triad-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-3 { syntax: \"\"; inherits: true; initial-value: transparent; }\n/* ── Design tokens — @theme mapping, base shadcn colors, mode knobs + shadow twins, base layer ─────\n :root carries light values + the LEAF mode knobs the engine composes; .dark carries the dark twins.\n .dark must come after :root (same-element ties resolve by source order on html.dark). */\n\n@theme inline {\n /* Easing. Tailwind's built-ins are deliberately gentle; these are the stronger variants UI motion\n wants, exposed as `ease-out` / `ease-in-out` / `ease-drawer` utilities.\n out — anything ENTERING or EXITING. Starts fast, so the first frame (the one the user is\n actually watching) already moves.\n in-out — things MOVING on screen that neither enter nor leave: carousels, sliders.\n drawer — a slower settle for panels that slide in from an edge, so a large surface arrives with\n weight instead of snapping.\n There is deliberately no `ease-in` token: it delays the first frame, which is the one moment the\n interface most needs to feel responsive. */\n --ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);\n --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);\n /* Anything with a bare `transition` and no `ease-*` inherits this. Tailwind ships\n cubic-bezier(0.4, 0, 0.2, 1) — symmetric, so it eases IN as well, and the first frame barely\n moves. Same curve as --ease-out, written literally because @theme inline doesn't emit the vars\n it inlines. */\n --default-transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1);\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-foreground-soft: var(--foreground-soft);\n --color-foreground-strong: var(--foreground-strong);\n --color-foreground-ui: var(--foreground-ui);\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n --radius: 1.25rem;\n /* Shadow ink — the base black/white that every shadow, pressed-inset, and bevel highlight is built\n from. Mode-invariant (only the ALPHA changes per surface/mode), so apply alpha at the call site:\n oklch(var(--shadow-ink) / ). One knob to retint every shadow at once (e.g. a cooler, less-flat\n ink) instead of hunting down dozens of oklch(0% 0 0) / oklch(100% 0 0) literals. */\n --shadow-ink: 0% 0 0; /* drop + pressed-inset shadow color */\n --shadow-highlight: 100% 0 0; /* inner bevel / top highlight */\n /* Active/pressed shadow SCALE, built from the ink above and mode-aware (deeper in dark; overridden in\n .dark), so components reference these with NO dark: twin. Depth ladder — all inset \"push-in\" presses\n except --active-shadow, which LIFTS the selected tab/toggle. --press-shadow-strong shares the\n standard press's 0.2 primary and stacks a contact layer (was 0.15; raised so the two-layer presses\n build ON the single-layer one instead of undercutting it):\n --press-shadow-sm — subtle press (ghost, outline)\n --press-shadow — standard press (default, gradient, opaque)\n --press-shadow-strong — firm two-layer press (glass, destructive, secondary, frosted)\n --press-shadow-deep — deepest press (crystal)\n --active-shadow — selected-control LIFT (tabs, toggle) */\n --press-shadow-sm: inset 0 1px 2px oklch(var(--shadow-ink) / 0.1);\n --press-shadow: inset 0 2px 4px oklch(var(--shadow-ink) / 0.2);\n --press-shadow-strong: inset 0 2px 4px oklch(var(--shadow-ink) / 0.2), inset 0 1px 2px oklch(var(--shadow-ink) / 0.15);\n --press-shadow-deep: inset 0 2px 4px oklch(var(--shadow-ink) / 0.25), inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --active-shadow: 0 1px 3px oklch(var(--shadow-ink) / 0.1), inset 0 1px 1px oklch(var(--shadow-highlight) / 0.5);\n /* Selected-control FILL — the twin of --active-shadow, which had the lift but no surface, so tabs and\n toggle-group each hardcoded `bg-white/80 dark:bg-white/20`. The colour is composed in engine.css;\n these are its knobs.\n The two modes source their hue by DIFFERENT routes, and that asymmetry is the whole design.\n DARK is structurally theme-driven: the fill is chroma 0 at alpha 0.2, so 100% of the colour is the\n tinted surface showing through — the chip literally IS the theme surface, lifted. Nothing can make\n it drift off-theme.\n LIGHT cannot use that route. Show-through only carries hue if the surface has hue to give, and a\n light glass surface measures around chroma 0.018 (the sheet is itself capped by --glass-tint-c-hi\n at 0.025 near white). So at alpha 0.9 the fill supplies ~94% of the chip's colour and has to carry\n the tint ITSELF.\n Which makes the fill's own lightness the binding constraint, because the near-white ceiling\n collapses hue-dependently — 0.0323 at L 93, 0.061 at L 87, and only 0.0091 by L 98. At the old\n L 93 the cap was 0.031 and the chip delivered an RGB spread of 20: nominally theme-tinted, visibly\n neutral, which is why day did not read as theme-driven while night did. L 87 roughly doubles the\n available chroma and the delivered spread (37), at the cost of sitting 3.6 L BELOW the tab list\n rather than 1.8 above it — the lift is carried by --active-shadow, so the chip still reads\n selected, now as a tinted fill rather than a white one.\n Capped with min() like --border and the chakra body, so selenite (chroma 0) stays exactly neutral\n and no preset can push the fill out of gamut. */\n --active-l: 87%;\n --active-c-max: 0.055; /* ≤ 0.061, the L-87 ceiling floor (lapis h268) */\n --active-a: 0.9;\n /* ── Mode knobs ──────────────────────────────────────────────────────────\n Single-number dials the glass engine's mode-derived block composes (light values here, dark twins in\n .dark). LEAVES ONLY — none composes a tint var (check-theme invariant 1), so they may live on the bare\n blocks and inherit into tint scopes, where the engine re-resolves the derived tokens. */\n --glass-sheet-a: 0.11; /* sheer glass sheet base alpha (per-stop offsets ride on top) */\n --glass-sheet-a1: 0.04; /* the sheet's TOP-stop offset (dark tapers it to 0.03) */\n --glass-frost-boost: 0.1; /* frosted = the sheet lifted by this much */\n --glass-border-a: 0.16; /* glass edge alpha — a white rim needs more presence on light backdrops */\n /* Ink border alpha (borderColor=\"ink\" / glass-border-ink): how much of --foreground the line takes.\n Mode-invariant because --foreground already flips polarity per mode; 0.5 keeps the line clearly\n softer than text while clearing the APCA non-text floor on every preset (check-contrast [ink]). */\n --glass-border-ink-a: 0.5;\n /* Crystal gloss defaults (the demo's Light/Tint sliders override inline). --glass-gloss-tint is\n mode-shared; --glass-gloss-l is NOT — see its twin below. AutoForeground's crystal tiers mirror\n --glass-gloss-l (97 light / 66 dark) as their snapshot-path fallback — keep all three in step. */\n --glass-diffuse: 12px; /* readability blur FLOOR the glass-diffuse axis raises surfaces to (opaque ignores it) */\n /* Gloss ink lightness (\"Light\" in the demo) — a MODE TWIN, because a gloss is defined relative to\n the surface under it and those surfaces are 68 L apart. The crystal floor computes to L 96.5 in\n light and L 28 in dark, so a single shared value cannot be a highlight in both: at 66 the top stop\n lifts dark by +15.2 L (a specular) and darkens light by −12.2 L (a smudge — the same \"grey\n speculars read as grime\" failure the shadow bevel comment warns about).\n 97 is the light twin: composited it lands +0.2 L over the surface, which is all the headroom that\n exists above L 96.5. The cost is a chroma ceiling, and it is strongly HUE-dependent rather than\n uniform — at L 97 it ranges from 0.0686 for peridot (h128) down to 0.0136 for lapis (h268).\n CORRECTION, measured in WebKit: an over-ceiling oklch() is NOT gamut-mapped down to that ceiling.\n It is CLIPPED per channel, which lands FURTHER from the request, not nearer:\n oklch(97% 0.0686 128) -> 232,255,205 (the ceiling — what the model predicts)\n oklch(97% 0.2975 128) -> 185,255,0 (what actually shipped: pure chartreuse)\n So the Tint multiplier did not \"do real work up to each hue's own ceiling\"; above the ceiling it\n drove the ink to a fully saturated primary. Every preset was affected — turquoise and aquamarine\n both clipped to 0,255,255, i.e. the two were indistinguishable — and moonstone reached 255,222,33,\n the egg-yolk cast that made light mode read yellow. --glass-gloss-c-max below is the fix, sized the\n way --glass-chakra-c-max is: the TIGHTEST ceiling across every preset hue, so no preset clips and\n all of them deliver 100% of what they ask for. */\n --glass-gloss-l: 97;\n /* Gloss chroma cap — the L-97 twin. 0.0136 is lapis (h268), the tightest of the 17 presets; 0.013\n sits just under it. Light gloss is therefore near-achromatic by necessity, not by choice: sRGB\n simply holds no more colour that close to white, and asking for more clips rather than tints. */\n --glass-gloss-c-max: 0.013;\n /* Gloss chroma multiplier over the tint (\"Tint\"). Calibrated for the DEFAULT tint (0.018 × 4.25 =\n 0.077, comfortably in gamut); a preset multiplies up from a much higher base, so the product is\n clamped to 0.3 where --glass-gloss-ink composes it — sRGB holds ~0.32 chroma at its very best hue\n and far less at L=66, so beyond that the browser, not the theme, is picking the color. Jewels\n (0.07 × 4.25 = 0.298) sit under the clamp and are unaffected; only the status tints hit it. */\n --glass-gloss-tint: 4.25;\n --glass-wash-l: 72%; /* tint wash lightness — deeper in dark so the tint reads over dark backdrops */\n --glass-wash-c-mult: 2.5; /* tint wash chroma multiplier (moonstone night quiets it to 2) */\n --glass-accent-l: 0.6; /* accent envelope — lifted in dark so the hue pops */\n --glass-accent-c: 0.15;\n --glass-glow-lc: 0.62 0.2; /* glow L+C pair */\n --glass-glow-a: 0.45;\n --harmonic-l: 0.6; /* --color-* accent-tier lightness */\n --mono-l-1: 0.75; /* mono ramp steps (irregular light↔dark lifts — three dials, not one) */\n --mono-l-2: 0.6;\n --mono-l-3: 0.45;\n --gradient-l: 0.6; /* brand-gradient envelope (matches --glass-accent-l/-c) */\n --gradient-c: 0.15;\n --gradient-contrast-l: 0.32; /* contrast gradient text: dark-on-light here, light-on-dark in .dark */\n --gradient-contrast-c: 0.14;\n --glass-crystal-l: 96; /* crystal body lightness — see --glass-crystal-bg; 100 carries zero chroma */\n --glass-crystal-bg-a: 0.3; /* crystal floor alpha (near-clear in dark) */\n /* glass-solid floor — the \"how solid\" dial (0 = sheer, 1 = fully solid; shared by light/dark) + the\n mode floor lightness; composed in @utility glass-veil so a scoped --glass-solid-a resolves there. */\n --glass-solid-a: 0.65;\n --glass-solid-l: 99%;\n /* Opaque tint BODY lightness (plain number → % in the engine). The header L slider drives this; the\n opaque floor color AND AutoForeground's --foreground-opaque banding both read it, so they can't\n drift. LOWER = deeper tint.\n L AND the c-scale below together absorb the retired opaque SHEET (see materials.css). --glass-bg was a\n near-white gradient at ~0.11 alpha, so it did two things to this floor: lightened it AND desaturated\n it. Compensating with lightness alone reproduced only half of that — it left the floor too saturated,\n and pushing L further toward white also SHRINKS the sRGB chroma ceiling, which blew the opaque body\n past check-gamut's 4.6× budget (lapis hit 5.46×). Moving both knobs reproduces both effects and stays\n inside the budget at 4.47×. Fitted across the 14 presets: worst OKLab dE 0.0021, mean 0.0009 — an\n order of magnitude under a just-noticeable difference, so the surface is unchanged to the eye.\n Moonstone pins its own pair (presets.css); its floor is near-white, where the sheet did far less. */\n /* L88, not 90.9. Near-white is where the sRGB chroma ceiling collapses, so an opaque card at 90.9 had\n two problems at once: it read as washed-toward-white, and it could not hold enough chroma to carry\n its own tint (lapis and sapphire were at 97–99% of a ceiling worth only 0.043). Dropping 2.9 L\n roughly doubles every hue's ceiling while keeping body text at Lc 80.0 — the floor is still firmly\n on the light side, so text polarity never changes. */\n --glass-opaque-l: 88;\n /* 0.85, down from 1.26. The old value only looked large: it was immediately clipped to the shared\n 0.042 cap, so every preset rendered the SAME chroma regardless of its tint. Presets now pin their\n own --glass-opaque-c-max (their hue's real ceiling at L88), and 0.85 is the multiplier that lets\n each one reach it. Net effect vs the old flat 0.042: +7% to +88% depending on hue. */\n --glass-opaque-c-scale: 0.85;\n /* Opaque floor chroma cap — the near-white sibling of --glass-tint-c-hi / --glass-chakra-c-max /\n --glass-gloss-c-max, and the last surface that lacked one. Without a cap the floor multiplied raw\n tint chroma this close to white and clipped for most of the palette.\n FALLBACK ONLY: the 12 jewels each pin their own above (their hue's real ceiling at L88); this\n covers the 9 scopes that don't — the 4 frescoes, the 4 status tints and moonstone. Sizing the\n SHARED cap to the tightest hue is the mistake that came before: one number low enough for amber\n (h75, ceiling 0.036 at L88) left peridot at 18% of its own much larger ceiling. So this is\n deliberately not the tightest — per-hue pins are, and scripts/check-gamut.mjs sweeps every scope\n against its real ceiling rather than trusting this number. */\n --glass-opaque-c-max: 0.055;\n /* ── Solidify floor — the opaque backing UNDER sheer glass ───────────────────\n `glass` paints --glass-solidify (this colour at --glass-opacity) as the bottom background-image\n layer of every SHEER material, so it is ~70% of what page/crystal/chakra text actually sits on.\n It used to be --glass-opaque-bg itself, which conflated two different jobs: the colour an OPAQUE\n CARD shows, and the backing that sets sheer text contrast. Opaque cards explicitly drop this layer\n (materials.css, --srf-solidify: none — it would be C over C), so the two were never actually the\n same surface; they only shared a token.\n Splitting them is what lets the sheer floor rise without repainting opaque cards. It is the right\n lever for text contrast because it is the only one with real WEIGHT in the composite (0.7) that is\n not part of a preset's declared identity: --glass-wash-l is the theme COLOUR (moving it changes\n what lapis IS), and --glass-tint-a is already per-mode and desaturates. Measured across all 17\n scopes, L92 is what puts every preset's body text over the 75 floor with the 1.5 Lc margin, with\n no preset's tint tokens touched. DARK needs none of this (it clears everywhere already), so it\n pins both back to the opaque floor below and nothing changes there. */\n --glass-solidify-l: 92;\n /* The floor's chroma ceiling collapses as it lightens, so the L88 caps above would clip here. Rather\n than 17 new per-preset numbers, derive from the ones each preset already declares: across the\n palette the L92/L88 ceiling ratio runs 0.60–0.90, and 0.65 lands every scope INSIDE its real\n ceiling (worst 0.99×, verified by scripts/check-gamut.mjs). Hues that peak light — peridot,\n turquoise, aventurine, amber, aquamarine — are not bound by it at all and keep 100% of their\n surface chroma; the dark-peaking arc keeps 80–86%. */\n --glass-solidify-c-max: calc(var(--glass-opaque-c-max, 0.055) * 0.65);\n /* ── Chakra — the step-cut material (between opaque and crystal) ─────────────\n Every other material is lit from ONE direction: --glass-shadow carries a bright top bevel and a\n dark bottom shade, so surfaces read as a lit dome. Chakra lights all FOUR edges as mitered facets\n (a conic-gradient with hard stops on the diagonals, composed in engine.css), so it reads as a\n step-cut gem instead.\n The facets are pure LUMINANCE overlays — --shadow-highlight / --shadow-ink at these alphas, chroma\n 0 — not tinted colors. Two reasons, and they are the same reason: (1) the theme hue already comes\n through from the tinted body floor beneath, and a translucent white/black over it shifts lightness\n while preserving hue; (2) a bright facet sits near L96 in light mode, where the sRGB chroma ceiling\n across the 17 presets collapses to 0.0182 — a tinted highlight there would be gamut-mapped by the\n browser at nearly every preset. This is the theme's existing rule for the top bevel (\"a specular,\n not a shadow; grey speculars read as grime\"), applied to all four sides.\n The alphas are irregular mode TWINS, not one knob: the same alpha buys a very different lightness\n step over an L88 body than over an L28 one. They are solved so BOTH modes span 18 L end to end —\n light 94.0 > 90.5 > 88.0 > 83.0 > 76.0, dark 38.0 > 32.0 > 28.0 > 24.0 > 20.0 (top, left, table,\n right, bottom). Light comes from the top-left, matching the 135° axis of --glass-bg. */\n --glass-chakra-l: 88; /* body/table lightness */\n /* Body alpha — the \"between crystal and opaque\" dial, and now genuinely that across the WHOLE\n surface: crystal's floor sits at 0.3, opaque at 1. Nothing is painted over the middle any more\n (the cut moved into box-shadow), so this alone decides how much backdrop shows, and glass-diffuse\n / --glass-opacity modulate it the same way they do on crystal. */\n --glass-chakra-a: 0.62;\n --glass-chakra-c-max: 0.055; /* body chroma cap — ≤ 0.0564, the L-88 ceiling floor (lapis h268) */\n --glass-chakra-step: 3px; /* width of ONE bevel step (the frame is two) */\n --glass-chakra-facet-t-a: 0.5; /* top — highlight, +6.0 L */\n --glass-chakra-facet-l-a: 0.208; /* left — highlight, +2.5 L */\n --glass-chakra-facet-r-a: 0.057; /* right — ink, −5.0 L */\n --glass-chakra-facet-b-a: 0.136; /* bottom — ink, −12.0 L */\n /* ── Glass shadows — mode TWINS (not knobs: the light↔dark alpha ratios AND layer structures are\n irregular). They compose only --shadow-ink/-highlight, never tint vars, so bare placement is legal.\n LIGHT twins carry extra definition layers dark mode gets for free (a light surface on a dark page\n separates itself): a tight CONTACT drop + 1px dark outer HAIRLINE define the edge on same-lightness\n backdrops, and a directional BEVEL (bright top inner edge, ink bottom inner edge) makes pills read\n convex instead of printed. The last layer flips family per mode: dark's white inner GLOW (luminous\n glass) becomes an ink VIGNETTE in light — a white wash on a light surface brightens without\n defining, while a faint vignette reads as glass thickness. (Frosted keeps its white glow in both\n modes: milkiness IS that material.) The top bevel stays WHITE in both — it's a specular, not a\n shadow; grey speculars read as grime. Layer order: ambient drop, contact drop, hairline, top\n bevel, bottom shade, inner glow/vignette. */\n --glass-shadow:\n 0 4px 16px oklch(var(--shadow-ink) / 0.13), 0 1px 3px oklch(var(--shadow-ink) / 0.11), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.4) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.08) inset,\n 0 0 12px oklch(var(--shadow-ink) / 0.05) inset;\n --glass-shadow-lg:\n 0 8px 32px oklch(var(--shadow-ink) / 0.16), 0 2px 6px oklch(var(--shadow-ink) / 0.12), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.45) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.09) inset,\n 0 0 20px oklch(var(--shadow-ink) / 0.06) inset;\n --glass-shadow-sm:\n 0 2px 8px oklch(var(--shadow-ink) / 0.1), 0 1px 2px oklch(var(--shadow-ink) / 0.09), 0 0 0 1px oklch(var(--shadow-ink) / 0.04),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.35) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.07) inset,\n 0 0 8px oklch(var(--shadow-ink) / 0.04) inset;\n --glass-frosted-border: oklch(100% 0 0 / 0.3);\n --glass-frosted-shadow:\n 0 8px 32px oklch(var(--shadow-ink) / 0.17), 0 2px 6px oklch(var(--shadow-ink) / 0.13), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.5) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.09) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.18) inset;\n --glass-crystal-shadow: var(--glass-shadow), 0 1px 1px oklch(var(--shadow-highlight) / 0.4) inset;\n --glass-crystal-shadow-hover: var(--glass-shadow-lg), 0 1px 1px oklch(var(--shadow-highlight) / 0.45) inset;\n /* Base colors - Light mode with dark text */\n --background: transparent;\n --foreground: oklch(\n 0.2 0.035 var(--glass-fg-h)\n ); /* CSS baseline (tinted, ~body target) so first paint has no gray→tint flash; AutoForeground refines to the exact APCA solve after */\n /* Size-tiered foregrounds (ARC Bronze Simple Mode bands) — static fallback; AutoForeground re-bands\n these against the live glass surface at runtime. text-foreground-soft / -strong utilities. */\n --foreground-soft: oklch(\n 0.45 0.077 var(--glass-fg-h)\n ); /* large/heading — baseline matched to AutoForeground's median pick; chroma is the LOWEST any preset hue solves to, so no hue clips before JS refines */\n --foreground-strong: oklch(\n 0.2 0.034 var(--glass-fg-h)\n ); /* fine/small — the ramp's tonal clip put this at L20, not L12; the old value flashed dark-then-tinted on every load */\n --foreground-ui: oklch(0.42 0.071 var(--glass-fg-h)); /* icons — AutoForeground re-bands (+ optional hue) */\n /* Static baselines for the OPAQUE + CRYSTAL tier sets (the [data-material] remaps read these) —\n WITHOUT them, a no-JS consumer (or SSR first paint) would substitute an unset var and INHERIT the\n page foreground onto those floors. Light mode: the opaque floor (L90) and crystal surface share\n the page's direction, so the normal literals apply; AutoForeground refines when mounted. */\n --foreground-opaque: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-opaque: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-opaque: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-opaque: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-opaque: oklch(0.14 0.04 var(--glass-fg-h));\n --foreground-crystal: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-crystal: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-crystal: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-crystal: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-crystal: oklch(0.14 0.04 var(--glass-fg-h));\n /* Chakra tier baselines — text sits on the TABLE (the flat centre, L 88), not on the facets, so in\n light mode the direction matches the page and the normal literals apply. AutoForeground re-bands\n against the composited table when mounted. */\n --foreground-chakra: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-chakra: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-chakra: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-chakra: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-chakra: oklch(0.14 0.04 var(--glass-fg-h));\n --card: transparent;\n --card-foreground: var(--foreground);\n --popover: transparent;\n --popover-foreground: var(--foreground);\n\n /* Primary colors - Light mode */\n --primary-l: 0.42; /* primary ink lightness (dark pill on light pages) — the color itself is composed in the engine, following the theme hue */\n /* Ceiling on the primary ink's chroma. The ×1.2 in the engine is intent — primary should read MORE\n saturated than the base tint — but uncapped it asked past the sRGB edge for five presets in light\n (turquoise, aquamarine, success, warning, aurora) and two in dark (lapis, info), where the browser\n gamut-maps and the delivered colour stops tracking the authored one.\n Measured against --glass-fg-h, not --glass-tint-h: --primary follows the FOREGROUND hue, and the\n Muse/Gloaming frescoes decouple the two (fg 290 / 18), so those hues are in the sweep — aurora only\n shows up as out-of-gamut once you check the fg hue. */\n --primary-c-max: 0.069; /* ≤ 0.0697, the L-42 ceiling floor — h205 (aurora) is now tighter than h190 */\n --primary-foreground: oklch(0.98 0 0); /* White text on the primary ink — Lc 88.1 at the worst hue, turquoise (measured) */\n\n /* Secondary colors - Light mode */\n /* ── Flat-neutral shadcn SURFACES, now theme-tinted ──────────────────────────\n --accent, --secondary, --muted and --sidebar were the last chroma-0 tokens in the theme, which is\n why a toggle's on-state read as near-white in light and near-black in dark no matter which preset\n was active: the fill simply had no hue to give. Same treatment as --border — lightness stays put\n (pure recolor), the colour moves to engine.css so it re-resolves per tint scope, and chroma is\n min(the preset's own, a cap) so a preset that declares less keeps its restraint and selenite stays\n exactly neutral.\n Each cap is measured against the tightest sRGB ceiling across all 17 presets AT THAT LIGHTNESS, and\n they differ by a factor of eight because the ceiling collapses toward white: 0.0714 at L 85, but\n only 0.0091 at L 98. One shared cap would have to be the smallest, tinting nothing. */\n --secondary-l: 0.96;\n --secondary-c-max: 0.018; /* ≤ 0.0182, the L-96 ceiling floor (lapis h268) */\n --secondary-foreground: var(--foreground);\n\n /* Muted colors - Light mode */\n --muted-l: 0.85;\n --muted-c-max: 0.071; /* ≤ 0.0714, the L-85 ceiling floor (lapis h268) */\n --muted-foreground: oklch(\n 0.35 0.06 var(--glass-fg-h)\n ); /* Level 1 — L35, not L14: the widest gap of the set, and the one doing most of the first-paint flash */\n\n /* Accent colors - Light mode. NOTE the knob prefix: --accent-c / --accent-h are already taken as the\n user-facing overrides for --glass-accent (the vivid switch/slider fill), a different role. */\n /* L93, down from 0.96, and the cap raised with it. The hover fill was reading as plain white: at L96 the\n sRGB gamut allows a chroma of only 0.0182, an RGB spread of 19, so \"tinted\" was arithmetically\n invisible. Chroma near the extremes is not a free parameter — it is whatever the gamut leaves — so the\n only way to a visible tint is to step off the extreme. Three points of lightness buys 1.7x the spread\n (19 -> 32) and moves the fill DARKER than the surface, the direction a highlight should go on a light\n page. It cannot go much further: --accent is also painted at 50% (bg-accent/50 on calendar's \"today\",\n button's active state, ToggleGroup hover), and that composite over an L88 card is what binds — at L92\n it drops to 76.0 Lc, under the floor+margin, while L93 holds 76.9. */\n --accent-bg-l: 0.93;\n --accent-bg-c-max: 0.032; /* ≤ 0.0323, the L-93 ceiling floor (lapis h268) */\n /* Stays --foreground: this token is text on a SURFACE-lightness background. Its callers all have that\n shape — the ghost button's hover, toggles, and the highlighted row of every menu (dropdown, context,\n navigation, command, menu-bar, select), which paint bg-accent so a consumer's --accent reaches them\n (the shadcn contract). Text and fill only stay legible together while --accent sits near the\n surface, which is precisely why the fill was moved by chroma rather than by weight. */\n --accent-foreground: var(--foreground);\n\n /* Destructive colors - WCAG AA compliant (4.5:1 contrast) */\n --destructive: oklch(0.55 0.22 25); /* Red - 4.5:1 with white */\n --destructive-foreground: oklch(0.98 0 0); /* White text on red */\n\n /* Border / input LIGHTNESS + chroma-CAP knobs — the COLOR is composed in engine.css so the edge\n follows the theme tint like every other surface token (leaves, so they inherit into tint scopes\n where the engine re-resolves).\n L 0.85, not the 0.88 the flat neutral carried: sRGB's chroma ceiling collapses as lightness climbs,\n and at 0.88 the binding ceiling across all 17 presets is 0.0564 — the most colorful in-gamut edge\n possible there still reads as grey (mean RGB spread 51). Dropping 3 points of L raises the ceiling\n to 0.0714 and nearly doubles delivered colorfulness (mean spread 65) while moving the grey\n reference only #d7d7d7 → #cecece, which is below the threshold of a perceived weight change.\n --border-c-max is the CAP, not the value: chroma is min(tint chroma, cap), so a preset that\n declares less than the cap keeps its own restraint — selenite (c 0) stays exactly neutral, which is\n the whole point of that theme — while lapis (c 0.15) clamps instead of being gamut-mapped by the\n browser. A flat multiplier cannot do both: any factor small enough to keep lapis in gamut drags the\n 0.07 jewels below where they are today. */\n --border-l: 0.85;\n --input-l: 0.85;\n --border-c-max: 0.07; /* ≤ 0.0714, the L-0.85 ceiling floor (lapis h268) */\n --ring: oklch(0.5 0.2 250); /* Focus ring */\n\n /* Chart colors - WCAG AA compliant */\n --chart-1: oklch(0.6 0.2 250); /* Blue */\n --chart-2: oklch(0.55 0.15 150); /* Green */\n --chart-3: oklch(0.5 0.2 50); /* Yellow */\n --chart-4: oklch(0.65 0.2 300); /* Purple */\n --chart-5: oklch(0.6 0.2 20); /* Orange */\n\n /* Sidebar colors - Light mode */\n --sidebar-l: 0.98;\n --sidebar-c-max: 0.009; /* ≤ 0.0091, the L-98 ceiling floor — near white holds almost no hue */\n --sidebar-foreground: var(--foreground);\n --sidebar-primary: oklch(0.5 0.2 250); /* Blue */\n --sidebar-primary-foreground: oklch(0.98 0 0); /* White text on primary */\n --sidebar-accent: oklch(0.15 0 0 / 0.1); /* Subtle glass overlay (≈ foreground/10), not a solid block */\n --sidebar-accent-foreground: var(--foreground);\n --sidebar-border: var(--border); /* Same divider role, same value it already carried — follow the role token */\n --sidebar-ring: oklch(0.5 0.2 250); /* Blue ring - 3:1 contrast */\n}\n\n.dark {\n /* Base colors — light text on a dark ground */\n --background: transparent;\n /* Pressed/active shadow SCALE — dark overrides: primaries deepen +0.1, contact layers +0.05, so the\n push-in still reads on dark surfaces (see :root for the light ladder + rationale). */\n --press-shadow-sm: inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --press-shadow: inset 0 2px 4px oklch(var(--shadow-ink) / 0.3);\n --press-shadow-strong: inset 0 2px 4px oklch(var(--shadow-ink) / 0.3), inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --press-shadow-deep: inset 0 2px 4px oklch(var(--shadow-ink) / 0.35), inset 0 1px 2px oklch(var(--shadow-ink) / 0.25);\n --active-shadow:\n 0 2px 6px oklch(var(--shadow-ink) / 0.4), inset 0 1px 2px oklch(var(--shadow-highlight) / 0.15),\n inset 0 0 8px oklch(var(--shadow-highlight) / 0.1);\n /* Selected-control fill — dark twin. Deliberately UNCHANGED in behaviour from the `bg-white/20` it\n replaces: chroma cap 0 makes min() resolve to 0, so this is a pure white lift and the hue arrives\n by show-through from the surface below, which is exactly why dark already read correctly. Light\n has to source its hue directly (see :root); the two modes reach the same result by different\n routes because the near-white gamut only permits one of them. */\n --active-l: 100%;\n --active-c-max: 0;\n --active-a: 0.2;\n /* ── Mode knobs — dark twins (see :root for what each dial does). */\n --glass-sheet-a: 0.05;\n --glass-sheet-a1: 0.03;\n --glass-frost-boost: 0.06;\n --glass-border-a: 0.15;\n --glass-wash-l: 58%;\n --glass-accent-l: 0.68;\n --glass-accent-c: 0.14;\n --glass-glow-lc: 0.7 0.18;\n --glass-glow-a: 0.5;\n --harmonic-l: 0.68;\n --mono-l-1: 0.82;\n --mono-l-2: 0.68;\n --mono-l-3: 0.5;\n --gradient-l: 0.68;\n --gradient-c: 0.14;\n --gradient-contrast-l: 0.9;\n --gradient-contrast-c: 0.09;\n --glass-crystal-l: 96; /* shared with light: the ceiling that forces 96 is hue-driven, not mode-driven */\n --glass-crystal-bg-a: 0.1;\n /* Gloss ink lightness — dark twin. Over an L-28 floor there is 72 L of headroom, so 66 lifts +15.2\n and still holds 59% of the tonal ink's chroma: a specular that is genuinely tinted. Light cannot\n have both (see :root). */\n --glass-gloss-l: 66;\n /* Gloss chroma cap — the L-66 twin. Far more headroom this far from white: 0.1094 is aurora (h205),\n the tightest across all 23 preset scopes — the FRESCOES count here too, which is why this is not\n turquoise's 0.1106. Dark gloss keeps real colour; it was still clipping before (turquoise\n 0,193,186 and carnelian 255,0,0 both had a pinned channel). */\n --glass-gloss-c-max: 0.109;\n /* L lifted from 22% (less extreme light↔dark switch); the opaque chroma scale (0.9) keeps warm hues\n like amber in-gamut instead of hard-clipping to rust. */\n --glass-solid-l: 18%;\n /* The retired sheet's compensation, dark twin (see :root). Much the larger move of the two modes: a\n near-white sheet over a DARK floor lifts it ~12 codes where over the light floor it lifts ~2–5. Same\n two-knob fit, worst OKLab dE 0.0045 across the 14 presets. */\n --glass-opaque-l: 36.4;\n /* 1.05, up from 0.85. The dark floor was never cap-limited — every preset sat at 31–79% of its own\n ceiling, so the MULTIPLIER was the throttle and dark opaque cards lost their tint. 1.05 is the most\n the tightest hue (turquoise) can take without clipping; it lifts every preset +24% at Lc 80.0. */\n --glass-opaque-c-scale: 1.05;\n /* Opaque floor chroma cap, dark twin (see :root). A GUARD, not a throttle — the distinction matters\n and this value used to get it wrong. It was 0.06, the tightest ceiling across the preset scopes,\n which was correct while every jewel shared --glass-tint-c: 0.07 and the floor multiplied it blind.\n Once each preset tuned its own chroma to its own hue, that shared floor-of-the-tightest started\n costing the hues WITH headroom: at 0.06 amethyst lost 30% of its ask, tourmaline 33%, rose 26% —\n against own ceilings of 0.189, 0.152 and 0.142. Measured, every preset's dark ask now sits at\n 0.31–0.79× of its own ceiling, so nothing clips with no cap at all. 0.095 clears the largest ask\n (tourmaline 0.0901) and therefore throttles nobody, while still bounding a future preset that\n pushes chroma far past today's values. check-gamut scores each preset against its own ceiling. */\n /* Guard, not a throttle: the largest dark ask is tourmaline at 0.111, so this binds nobody. */\n --glass-opaque-c-max: 0.12;\n /* Dark keeps the solidify floor IDENTICAL to the opaque floor — the split exists to buy LIGHT-mode\n text contrast, and dark already clears the body floor on every preset with margin. Pinning to the\n opaque values (rather than leaving :root's L92 / ×0.65 to inherit) is what makes this a light-only\n change: a lighter backing under dark glass would wash the whole mode out. */\n --glass-solidify-l: var(--glass-opaque-l);\n --glass-solidify-c-max: var(--glass-opaque-c-max);\n /* ── Chakra dark twins (see :root for what each dial does + how the alphas were solved).\n The facet alphas invert in character: over an L28 body there is 72 L of headroom UP and only 28\n DOWN, so the highlights need far less alpha and the inks far more than their light-mode twins to\n land the same 18 L span. The targets also skew brighter here (+10/−8 rather than +6/−12) — a dark\n gem reads as cut by catching light, where a light one reads by casting shade. */\n --glass-chakra-l: 28;\n --glass-chakra-a: 0.58;\n --glass-chakra-c-max: 0.046; /* ≤ 0.0471, the L-28 ceiling floor (turquoise h190) */\n --glass-chakra-facet-t-a: 0.139; /* top — highlight, +10.0 L */\n --glass-chakra-facet-l-a: 0.056; /* left — highlight, +4.0 L */\n --glass-chakra-facet-r-a: 0.143; /* right — ink, −4.0 L */\n --glass-chakra-facet-b-a: 0.286; /* bottom — ink, −8.0 L */\n /* ── Glass shadows — dark twins (deeper drops, brighter bevels). */\n --glass-shadow:\n 0 4px 16px oklch(var(--shadow-ink) / 0.3), 0 1px 4px oklch(var(--shadow-ink) / 0.15), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.12) inset,\n 0 0 12px oklch(var(--shadow-highlight) / 0.15) inset;\n --glass-shadow-lg:\n 0 8px 32px oklch(var(--shadow-ink) / 0.4), 0 2px 8px oklch(var(--shadow-ink) / 0.2), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.15) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.18) inset;\n --glass-shadow-sm:\n 0 2px 8px oklch(var(--shadow-ink) / 0.25), 0 1px 2px oklch(var(--shadow-ink) / 0.12), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.1) inset,\n 0 0 8px oklch(var(--shadow-highlight) / 0.12) inset;\n --glass-frosted-border: oklch(100% 0 0 / 0.25);\n --glass-frosted-shadow:\n 0 8px 32px oklch(var(--shadow-ink) / 0.5), 0 2px 8px oklch(var(--shadow-ink) / 0.3), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.2) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.22) inset;\n --glass-crystal-shadow: var(--glass-shadow), 0 1px 1px oklch(var(--shadow-highlight) / 0.2) inset;\n --glass-crystal-shadow-hover: var(--glass-shadow-lg), 0 1px 1px oklch(var(--shadow-highlight) / 0.25) inset;\n --foreground: oklch(\n 0.95 0.03 var(--glass-fg-h)\n ); /* CSS baseline (tinted, ~body target) so first paint has no gray→tint flash; AutoForeground refines to the exact APCA solve after */\n /* Size-tiered foregrounds (ARC Bronze Simple Mode bands) — static fallback; AutoForeground re-bands\n these against the live glass surface at runtime. text-foreground-soft / -strong utilities. */\n --foreground-soft: oklch(0.83 0.065 var(--glass-fg-h)); /* large/heading — refined at runtime */\n --foreground-strong: oklch(0.97 0.015 var(--glass-fg-h)); /* fine/small — refined at runtime */\n --foreground-ui: oklch(0.87 0.057 var(--glass-fg-h)); /* icons — AutoForeground re-bands (+ optional hue) */\n /* Static OPAQUE + CRYSTAL tier baselines, dark twins — the standard dark opaque floor (L32) and\n crystal surface keep the page's light-text direction. Presets whose dark floors flip LIGHT (moonstone\n cream) override the -opaque set in their own block; AutoForeground refines when mounted. */\n --foreground-opaque: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-opaque: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-opaque: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-opaque: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-opaque: oklch(0.89 0.04 var(--glass-fg-h));\n --foreground-crystal: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-crystal: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-crystal: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-crystal: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-crystal: oklch(0.89 0.04 var(--glass-fg-h));\n /* Chakra tier baselines, dark twins — the L-28 table keeps the page's light-text direction. */\n --foreground-chakra: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-chakra: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-chakra: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-chakra: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-chakra: oklch(0.89 0.04 var(--glass-fg-h));\n --card: transparent;\n --card-foreground: var(--foreground);\n --popover: transparent;\n --popover-foreground: var(--foreground);\n\n /* Primary colors — dark mode */\n --primary-l: 0.78; /* primary ink lightness in dark mode (light pill) — color composed in the engine */\n --primary-c-max: 0.107; /* ≤ 0.1077, the L-78 ceiling floor (h268 lapis) */\n /* Near-black text on the primary ink — Lc 62.9 at the worst hue, sistine (measured). It was 60.9\n before --primary-c-max: capping the chroma the browser used to gamut-map moved the worst case back\n above the 62 this comment had been claiming. */\n --primary-foreground: oklch(0.15 0 0);\n\n /* Secondary colors — dark mode */\n /* Flat-neutral surfaces — dark twins (see :root for why these are knobs now). Lightness unchanged;\n the caps are the dark ceilings, which are tightest at the DARKEST step, not the lightest. */\n --secondary-l: 0.25;\n --secondary-c-max: 0.041; /* ≤ 0.0417, the L-25 ceiling floor — h205 (aurora) is now tighter than h190 */\n --secondary-foreground: var(--foreground);\n\n /* Muted colors — dark mode */\n --muted-l: 0.18;\n --muted-c-max: 0.03; /* ≤ 0.0307, the L-18 ceiling floor (turquoise h190) */\n --muted-foreground: oklch(0.9 0.031 var(--glass-fg-h)); /* Level 1 — one ramp step toward the chosen color */\n\n /* Accent colors — dark mode */\n /* L34, up from 0.25 — the same fix as light, mirrored. At L25 the ceiling was 0.0417 (spread 29), so the\n fill read as near-black; L34 allows 0.0565 (spread 42) and moves it BRIGHTER than the surface, which is\n the direction a highlight goes on a dark page. Dark has more room than light here (its 50% composite\n sits at 77.5 Lc rather than 76.0), because the fill and the L36.4 card floor are close together. */\n --accent-bg-l: 0.34;\n --accent-bg-c-max: 0.056; /* ≤ 0.0565, the L-34 ceiling floor (aurora h205) */\n --accent-foreground: var(--foreground);\n\n /* Destructive colors - WCAG AA compliant (4.5:1 contrast) */\n --destructive: oklch(0.65 0.22 25); /* Light red - 4.5:1 with dark background */\n --destructive-foreground: oklch(1 0 0); /* White text on red — APCA Lc ~67 (near-black was Lc 42) */\n\n /* Border / input lightness + chroma-cap — dark twins (see :root; color composed in engine.css).\n L is UNCHANGED here: in dark mode the edge sits at 0.30/0.25 on a dark page, so lowering it would\n cost contrast rather than buy color. Only the cap moves, up to the tightest dark ceiling — 0.0422\n at L 0.25 (turquoise h190), which --input hits first, so one cap serves both. */\n --border-l: 0.3;\n --input-l: 0.25;\n --border-c-max: 0.041; /* ≤ 0.0422, the L-0.25 ceiling floor (turquoise h190) */\n --ring: oklch(0.6 0.2 250); /* Focus ring */\n\n /* Chart colors - WCAG AA compliant */\n --chart-1: oklch(0.65 0.2 250); /* Light blue */\n --chart-2: oklch(0.7 0.15 150); /* Light green */\n --chart-3: oklch(0.75 0.2 50); /* Light yellow */\n --chart-4: oklch(0.7 0.2 300); /* Light purple */\n --chart-5: oklch(0.68 0.2 20); /* Light orange */\n\n /* Sidebar colors — dark mode */\n --sidebar-l: 0.18;\n --sidebar-c-max: 0.03; /* ≤ 0.0307, the L-18 ceiling floor (turquoise h190) */\n --sidebar-foreground: var(--foreground);\n --sidebar-primary: oklch(0.56 0.16 250); /* Match --primary; white text clears APCA */\n --sidebar-primary-foreground: oklch(1 0 0); /* White on the sidebar's brand fill */\n --sidebar-accent: oklch(1 0 0 / 0.1); /* Subtle glass overlay (≈ foreground/10), not a solid block */\n --sidebar-accent-foreground: var(--foreground);\n --sidebar-border: var(--border); /* Same divider role, same value it already carried — follow the role token */\n --sidebar-ring: oklch(0.6 0.2 250); /* Light blue ring - 3:1 contrast */\n}\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in oklch, var(--foreground) 22%, transparent) transparent;\n }\n body {\n /* Text uses the band-picked --foreground (set on by AutoForeground on theme/tint change;\n globals carries a static fallback so there's no flash). */\n color: var(--foreground);\n background-color: transparent;\n }\n /* Thin, glass-friendly scrollbars (replaces the chunky native bar) */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: color-mix(in oklch, var(--foreground) 22%, transparent);\n border-radius: 9999px;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: color-mix(in oklch, var(--foreground) 38%, transparent);\n }\n}\n\n/* ── Below the floor ──────────────────────────────────────────────────────────\n The supported floor is Safari 17.5 / Chrome 120 / Firefox 128 / Edge 120, declared as the browserslist\n in SISTINE's package.json. It is set by what this theme actually uses, not by preference — the binding\n constraints are text-wrap:balance (Safari 17.5), bare-`&` CSS nesting and unprefixed mask-composite\n (Chrome 120), and @property (Firefox 128). Tailwind v4's own baseline sits just under it. That\n browserslist does not ship with this file: a consuming app should declare its own, at or above the\n floor, or its toolchain targets whatever its defaults are.\n\n Every colour in this theme is oklch(), fed through custom properties. On an engine without oklch the\n custom property still PARSES (custom props accept anything) and only fails when substituted — so\n `background-color: var(--color-background)` resolves to an invalid value and computes to `unset`,\n i.e. transparent, rather than to a fallback. Surfaces disappear; text usually survives on the UA\n default. This is not a polyfill and does not try to reproduce the palette — it guarantees a legible\n page instead of transparent panels on stacked backgrounds.\n\n UNVERIFIED IN A REAL ENGINE: no browser in the test matrix lacks oklch, so this branch has only been\n confirmed to compile, not to render. Treat it as a floor, not a tested path. */\n@supports not (color: oklch(0% 0 0)) {\n :root {\n color-scheme: light;\n background-color: #f7f7fb;\n color: #17161c;\n }\n body {\n background-color: #f7f7fb;\n color: #17161c;\n }\n .glass,\n [data-surface],\n [data-material] {\n background-color: #ffffffe6;\n border: 1px solid #17161c26;\n backdrop-filter: none;\n }\n}\n/* ── Glass engine ─────────────────────────────────────────────────────────────\n THREE grouped blocks, split by what must re-resolve where (the var-composition-resolves-at-declaration\n gotcha — see scripts/check-theme.mjs):\n\n A. TINT ANCHORS (:root, [data-glass-tint], …) — the tint knobs plus everything mode-INVARIANT that\n composes them, declared on :root AND every tint scope so a scoped data-glass-tint re-resolves the\n whole subgraph. Deliberately NOT on `.dark`: re-declaring --glass-tint-h / --glass-fg-h at a bare\n `.dark` wrapper would reset a page tint inherited from above.\n\n B. MODE-DERIVED SURFACES (:root, .dark, [data-glass-tint], …) — every tint-composing token whose\n light/dark difference is a handful of numbers. Each is declared ONCE, composing the mode knobs\n (--glass-sheet-a, --glass-wash-l, … — see :root/.dark) which are LEAVES that inherit into tint\n scopes. `.dark` IS in this selector so a dark wrapper inside a light page re-composes its surfaces\n from the wrapper's own dark knobs (same-element cascade) — \"toggle .dark anywhere\" keeps working.\n\n C. GLOSS (:root, .dark, [data-glass-tint], [data-gloss]) — the crystal gloss and the stacks that bake\n it, which also have to recompose wherever a `data-gloss` flavor is set. See block C itself.\n\n Foreground tokens stay on bare :root/.dark (AutoForeground owns them). Preset blocks below override\n --glass-tint-h/c/a by source order. Shadow composites are mode-TWINNED on bare :root/.dark (their\n light↔dark ratios are irregular — knobbing them would change values). */\n:root,\n[data-glass-tint] {\n /* Deliberately restrained glassmorphism — enough to read as glass, never enough to fog what is behind it. */\n /* Glass tint — recolor ALL glass by changing these; presets set them via [data-glass-tint].\n -h = hue, -c = chroma (gradient), -a = wash alpha. The gradient alone is too sheer\n (~6–12% opacity) to show a hue, so -a paints a solid colored floor (--glass-tint-wash)\n that reads over any backdrop. Neutral keeps -a:0, so the default glass is unchanged. */\n --glass-tint-h: 250;\n --glass-tint-c: 0.018;\n --glass-tint-a: 0;\n /* The same chroma, capped for NEAR-WHITE surfaces. The sheet stops, the borders and the crystal\n floor sit at L 95–100%, where sRGB's chroma ceiling collapses AND becomes strongly hue-dependent:\n at L=97% it is ~0.071 for peridot (h128) but ~0.014 for lapis (h268). Feeding those the raw\n --glass-tint-c means each preset lands on whatever fraction of its declared value its hue happens\n to permit — lapis was delivering 9% of its 0.15 where peridot delivered 98% of its 0.07 — and the\n shortfall is then resolved by the browser's gamut mapping rather than by us. That is the same\n class of bug the 3% margin in lib/oklch-utils guards against on the computed path (\"foregrounds\n went grey in Safari while staying tinted in Chrome\"); this is its CSS-side counterpart.\n Capping the BASE (rather than lowering any preset) keeps chroma where it has headroom — a jewel's\n depth lives at mid lightness, in --primary and --glass-tint-wash, which are untouched by this.\n Guarded by scripts/check-gamut.mjs.\n 0.025 was still too generous, because the sheet MULTIPLIES this cap: its stops are\n calc(--glass-tint-c-hi * 0.3…1.3), so the top stop asked 0.0325 against an L-95 ceiling of 0.0229\n at the tightest hue — the cap itself clipped. 0.017 is that ceiling divided by the 1.3 multiplier,\n so the sheet now lands inside the gamut at every preset hue instead of at whatever the browser\n chops it to. Cap-then-multiply is the order that matters here; see check-gamut's SCALE/CAP/BOTH\n shapes, which model each form separately for exactly this reason. */\n --glass-tint-c-hi: min(var(--glass-tint-c), 0.017);\n /* Foreground hue — drives every FOREGROUND-keyed token (body/heading text via AutoForeground,\n --muted-foreground, --glass-accent, --glass-glow, --gradient*), kept SEPARATE from the surface\n hue so tuning text never shifts the glass/fresco surface. Defaults to the tint hue (jewels: text\n == surface); frescoes override it to anchor text off their multi-hue surface (Muse night: surface\n 222, text 310). AutoForeground reads this, falling back to --glass-tint-h for older consumers. */\n --glass-fg-h: var(--glass-tint-h);\n /* ── Harmonic color system ────────────────────────────────────────────────────────────────────\n Color-wheel relationships derived from the CONTENT hue (--glass-fg-h) — the same hue --glass-accent\n and --gradient track — so they rotate with the theme/tint automatically (a generalization of the\n --gradient's +63.53° offset). Hues are mode-invariant ANGLES; CSS Color 4 wraps them past 360°, so use\n anywhere as oklch(L C var(--hue-*)); choosing your own L/C also keeps them in-gamut. The ready --color-*\n tokens sit at the --glass-accent envelope (0.6/0.15; .dark lifts them). NOTE: a fixed-chroma harmonic can\n land at the sRGB edge at some hues and get browser-gamut-mapped — prefer the hue tokens + a conservative\n chroma when that matters. */\n /* Harmonic anchor hue — the wheel origin the harmonies rotate from. Normally the content hue, but the two\n \"hue-less\" themes (selenite + moonstone) anchor at 0° instead — each preset block in presets.css declares\n it, so a CSS-only consumer gets the anchor too (the tint switcher also inlines it, for a custom tint\n dragged to chroma 0) — and their accents come out a colorful red-based harmony, not a warp of a\n near-grey hue. */\n --harmony-h: var(--glass-fg-h);\n --hue-base: var(--harmony-h);\n --hue-complement: calc(var(--harmony-h) + 180);\n --hue-analogous-1: calc(var(--harmony-h) - 30);\n --hue-analogous-2: calc(var(--harmony-h) + 30);\n --hue-split-1: calc(var(--harmony-h) + 150);\n --hue-split-2: calc(var(--harmony-h) + 210);\n --hue-triad-1: calc(var(--harmony-h) + 120);\n --hue-triad-2: calc(var(--harmony-h) + 240);\n --hue-tetrad-1: calc(var(--harmony-h) + 60); /* rectangle: base, +60, +180, +240 */\n --hue-tetrad-2: calc(var(--harmony-h) + 180);\n --hue-tetrad-3: calc(var(--harmony-h) + 240);\n --hue-square-1: calc(var(--harmony-h) + 90); /* square: base, +90, +180, +270 */\n --hue-square-2: calc(var(--harmony-h) + 180);\n --hue-square-3: calc(var(--harmony-h) + 270);\n /* Harmonic chroma — a fixed vivid accent chroma so the palette stays colorful in EVERY theme (selenite +\n moonstone included; they get their color from the hue-0 anchor above, not from chroma). --mono-chroma is\n quieter (0.6×) for the subtle mono ramp. */\n --color-chroma: 0.15;\n --mono-chroma: calc(var(--color-chroma) * 0.6);\n /* (--color-* / --mono-* ready colors live in the mode-derived surfaces block below — their L is the\n mode envelope; the hue angles above are the mode-invariant part.) */\n /* (The solidify floor — the --glass-opacity \"component opacity\" dial — is composed INSIDE each sheer\n glass utility, at the element. Composing it here would bake :root's --glass-opacity into the value\n and silently ignore a per-element glass={{opacity}} — the var-composition gotcha again.) */\n /* Blur values — moderate by design; per-variant sizes (mode-invariant). */\n --blur: 2px;\n --blur-sm: 1px;\n --blur-lg: 8px;\n --blur-xl: 12px;\n --blur-frosted: 25px;\n /* Chakra reads as a THICK stone, so it diffuses more than crystal's near-clear 2px even though its\n body is more solid — the two axes are independent (how much light passes vs how scattered it is). */\n --blur-chakra: 6px;\n /* Crystal edge — identical in both modes, so it lives here with the anchors (the crystal floor +\n shadows are mode-varying: --glass-crystal-bg in the surfaces block below, shadows on :root/.dark). */\n --glass-crystal-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / 0.3);\n /* Crystal gloss KNOBS — leaves only. The gloss itself (ink, the three gradients) and every stack that\n bakes it are composed in block C below, which also recomposes at each [data-gloss] scope.\n --glass-gloss-l / --glass-gloss-tint live in tokens.css (66 / 4.25) — do NOT re-declare here:\n engine.css imports AFTER tokens.css, so a duplicate at equal :root specificity silently wins and\n pure-CSS consumers get a stale gloss. */\n --glass-gloss-hue-span: 40; /* hue: degrees the iridescent sweep shifts up & down from the tint hue */\n /* hue: chroma of the iridescent stops, at the flavor's own L 74. Was 0.16, chosen believing an\n over-ceiling request is gamut-MAPPED down to the ceiling (\"holds 81% of that chroma\"). WebKit clips\n per channel instead (see --glass-gloss-c-max in tokens.css), so 0.16 pinned a channel on the\n tightest hues rather than delivering 81% of itself. 0.12 sits just under 0.1226 — aurora (h205) at\n L 74, the tightest across all 23 preset scopes (frescoes included) — so every hue in the sweep now\n renders what it asks for. */\n --glass-gloss-c: 0.12;\n --glass-gloss-hue-dir: 1; /* gloss direction (±1) — flips the TONAL streak diagonal (calc angle) + the HUE sweep order (span × dir); toggled in the crystal demo */\n --blur-crystal: 2px;\n}\n\n/* B. MODE-DERIVED SURFACES — see the engine header above. Each tint-composing token is declared ONCE,\n parameterized by the mode knobs on bare :root/.dark; `.dark` in this selector keeps dark wrappers\n re-composing with their own knobs. */\n:root,\n.dark,\n[data-glass-tint] {\n /* Solid colored floor under the sheer gradient (see --glass-tint-a) — mode via --glass-wash-l/-c-mult. */\n --glass-tint-wash: oklch(var(--glass-wash-l) calc(var(--glass-tint-c) * var(--glass-wash-c-mult)) var(--glass-tint-h) / var(--glass-tint-a));\n /* Solid accent that tracks the FOREGROUND hue at a fixed vivid chroma — used by active controls\n (switch \"on\", slider range). The --accent-h/-c knob overrides apply in BOTH modes (the old dark\n twin dropped them — that was a bug). */\n --glass-accent: oklch(var(--glass-accent-l) var(--accent-c, var(--glass-accent-c)) var(--accent-h, var(--glass-fg-h)));\n /* Stained-glass — grayscale collapses the backdrop to pure LUMINANCE (akin to alpha: only lighter/\n darker survives), and the glass's OWN tint stack supplies the color — OKLCH-true to the theme, no\n hue-rotate approximation. --glass-stain-amount (0..1) sets how completely the backdrop's color is\n stripped; --glass-stain-invert (0..1, default 1) flips the surviving luminance so bright backdrop\n patches stop fighting dark tinted glass (set 0 to disable). The glass-stained utility and the switcher's Stained toggle both reference THIS token. */\n --glass-stain: grayscale(var(--glass-stain-amount, 1)) invert(var(--glass-stain-invert, 0));\n /* Colored glow for the `glow` prop — follows the foreground hue; override to recolor the halo. */\n --glass-glow: oklch(var(--glass-glow-lc) var(--glass-fg-h) / var(--glass-glow-a));\n /* Selected-control FILL (active tab, pressed toggle) — the surface twin of --active-shadow. Composed\n here, not on :root, because it reads the tint: a scoped data-glass-tint must re-resolve it, and it\n is why the chip tracks whichever theme the subtree carries. Mode knobs in tokens.css carry the two\n routes to the same look (light tints the fill, dark lets the surface show through a white lift). */\n --active-bg: oklch(var(--active-l) min(var(--glass-tint-c), var(--active-c-max)) var(--glass-tint-h) / var(--active-a));\n /* Solid PRIMARY ink (default Button, link text) — themed at the tint's OWN chroma (×1.2): selenite\n stays grey, moonstone whispers warm, jewels read clearly tinted. Lightness is the --primary-l mode knob\n (0.42 light / 0.78 dark). Chroma is min(tint × 1.2, --primary-c-max): the ×1.2 is intent — primary\n should read more saturated than the base tint — but uncapped it asked past the sRGB edge at five\n presets in light and two in dark, where the browser, not the theme, picked the delivered colour.\n Measured after capping: --primary-foreground holds Lc 88.1 light / 62.9 dark at the worst hue. */\n --primary: oklch(var(--primary-l) min(calc(var(--glass-tint-c) * 1.2), var(--primary-c-max)) var(--glass-fg-h));\n /* Ready accent-tier colors (hue angles from the anchors block; L is the mode envelope --harmonic-l). */\n --color-complement: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-complement));\n --color-analogous-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-analogous-1));\n --color-analogous-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-analogous-2));\n --color-split-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-split-1));\n --color-split-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-split-2));\n --color-triad-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-triad-1));\n --color-triad-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-triad-2));\n /* Monochromatic — the harmony anchor hue at stepped lightness (a subtle, low-chroma ramp). */\n --mono-1: oklch(var(--mono-l-1) var(--mono-chroma) var(--harmony-h));\n --mono-2: oklch(var(--mono-l-2) var(--mono-chroma) var(--harmony-h));\n --mono-3: oklch(var(--mono-l-3) var(--mono-chroma) var(--harmony-h));\n /* Opaque glass — a fully solid, faintly tinted floor (no see-through) for the \"opaque\" style.\n --glass-opaque-l is the body-lightness knob (:root/.dark; the header L slider drives it; the floor\n AND AutoForeground's --foreground-opaque banding both read it, so they can't drift). */\n /* Chroma is CAPPED now (--glass-opaque-c-max, tokens.css). This floor sat at L 90.9 in light (it is\n L88 now, for the same reason) — near-white territory where the sRGB ceiling collapses — yet it was\n the one near-white surface\n multiplying RAW --glass-tint-c (×1.26) with no cap, while the sheet had --glass-tint-c-hi, chakra\n had --glass-chakra-c-max and the gloss had --glass-gloss-c-max. That made it the binding surface\n for 7 of the 12 jewels: it clipped first, and every preset's chroma budget was set by it. Capping\n it is what let the tints below be normalised to their hue's real capacity rather than to this\n floor's. Multiply-THEN-cap, matching --glass-gloss-ink. */\n --glass-opaque-bg: oklch(\n calc(var(--glass-opaque-l) * 1%) min(calc(var(--glass-tint-c) * var(--glass-opaque-c-scale)), var(--glass-opaque-c-max, 0.3)) var(--glass-tint-h)\n );\n /* The same floor at the SOLIDIFY lightness/cap — the backing `glass` paints under every sheer\n material (see --glass-solidify in utilities.css). Split from --glass-opaque-bg because an opaque\n CARD and the backing under sheer glass are different surfaces with different jobs; tokens.css has\n the reasoning. Same scale, so a preset's tint still drives it; only the lightness and cap differ. */\n --glass-solidify-bg: oklch(\n calc(var(--glass-solidify-l, var(--glass-opaque-l)) * 1%)\n min(calc(var(--glass-tint-c) * var(--glass-opaque-c-scale)), var(--glass-solidify-c-max, var(--glass-opaque-c-max, 0.3))) var(--glass-tint-h)\n );\n /* Sheer glass sheet — hue/chroma from the tint vars, chroma scaled per stop so the highlights stay\n glassy and the body carries the color. Stop alphas ride the mode base --glass-sheet-a (+ fixed\n offsets; the top stop's offset is its own knob --glass-sheet-a1 — dark tapers it to 0.03);\n frosted = the same sheet lifted by --glass-frost-boost. */\n --glass-bg: linear-gradient(\n 135deg,\n oklch(100% calc(var(--glass-tint-c-hi) * 0.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-sheet-a1))) 0%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.01)) 25%,\n oklch(97% calc(var(--glass-tint-c-hi) * 1) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.02)) 50%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-sheet-a)) 75%,\n oklch(95% calc(var(--glass-tint-c-hi) * 1.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.01)) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg,\n oklch(100% calc(var(--glass-tint-c-hi) * 0.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.04)) 0%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.01)) 25%,\n oklch(97% calc(var(--glass-tint-c-hi) * 1) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.02)) 50%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost))) 75%,\n oklch(95% calc(var(--glass-tint-c-hi) * 1.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.01)) 100%\n );\n /* Very subtle white border — the lit edge of a cut sheet (alpha is the mode dial) */\n --glass-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-border-a));\n /* Opaque CHROME edge — the shadcn --border/--input role tokens (dividers, ghost/input outlines, any\n bare `border-border`). Previously flat neutral (oklch(0.88 0 0) / oklch(0.3 0 0)), which made them\n the ONLY chrome in the theme that did not move with the tint: a ghost Button kept a grey edge under\n lapis while its `outline` sibling (border-foreground/20) tracked the theme. Lightness moves only in\n LIGHT mode (0.88 → --border-l 0.85) to buy chroma headroom; see the knob comment in tokens.css.\n Chroma is min(tint chroma, --border-c-max) — a CAP, deliberately not --glass-tint-c-hi and not a\n multiplier. --glass-tint-c-hi (0.025) is the NEAR-WHITE cap, sized for the L 95–100% sheet stops\n and crystal floor; borrowing it here spent only 66% of the ceiling on amethyst and 17% on peridot,\n which is why the edge still read grey. A flat multiplier fails the other way: lapis declares\n c 0.15, so any factor small enough to keep it in gamut pushes the 0.07 jewels below where they\n started. min() gives every preset its full declared chroma up to the ceiling floor and clamps only\n the ones that overreach — and leaves c 0 at exactly 0, so selenite stays the neutral theme it is.\n Declaring them HERE (rather than on :root) also puts them under scripts/check-gamut.mjs, which\n scrapes engine.css for both the calc() and min() chroma shapes — the suite went 20 → 24 surface/mode\n pairs when these landed, so a later retune of the caps is measured, not asserted.\n SURFACE hue (--glass-tint-h), not --glass-fg-h: an edge belongs to the surface it bounds, matching\n --glass-border / --glass-crystal-border. In a fresco that splits the two (Muse night: surface 222,\n text 310) the border follows the panel, not the type. */\n --border: oklch(var(--border-l) min(var(--glass-tint-c), var(--border-c-max)) var(--glass-tint-h));\n --input: oklch(var(--input-l) min(var(--glass-tint-c), var(--border-c-max)) var(--glass-tint-h));\n /* The remaining shadcn SURFACE tokens — the last chroma-0 values in the theme. Same shape as --border\n for the same reason: a toggle's on-state, a hover wash and a sidebar panel all take their fill from\n these, so while they were flat neutrals those surfaces read near-white in light and near-black in\n dark in every preset. Each carries its OWN cap because the sRGB ceiling is a function of lightness\n and these sit from L 18 to L 98 — a shared cap would have to be the smallest of them (0.009, at the\n near-white sidebar) and would tint nothing. See the knobs in tokens.css. */\n --accent: oklch(var(--accent-bg-l) min(var(--glass-tint-c), var(--accent-bg-c-max)) var(--glass-tint-h));\n /* The hover wash as an IMAGE layer (glass-hover-accent). Translucent so the material still reads\n through it — a hover that paints a flat fill stops looking like glass. */\n --glass-hover-wash: color-mix(in oklch, var(--accent) 70%, transparent);\n --secondary: oklch(var(--secondary-l) min(var(--glass-tint-c), var(--secondary-c-max)) var(--glass-tint-h));\n --muted: oklch(var(--muted-l) min(var(--glass-tint-c), var(--muted-c-max)) var(--glass-tint-h));\n --sidebar: oklch(var(--sidebar-l) min(var(--glass-tint-c), var(--sidebar-c-max)) var(--glass-tint-h));\n /* Theme-aware brand gradient — follows --glass-fg-h, from the hue 3 ramp-steps out\n (8/side → step 360/17 ≈ 63.53°) to the base hue, at the mode envelope --gradient-l/-c + 0.5 alpha.\n (Axis/direction variants are computed in JS from lib/oklch-utils.ts — see the /components demo.) */\n --gradient: linear-gradient(\n 135deg,\n oklch(var(--gradient-l) var(--gradient-c) calc(var(--glass-fg-h) + 63.53) / 0.5) 0%,\n oklch(var(--gradient-l) var(--gradient-c) var(--glass-fg-h) / 0.5) 100%\n );\n /* Full-opacity twin of --gradient for gradient TEXT (background-clip: text) — no wash alpha so glyphs stay crisp. */\n --gradient-text: linear-gradient(\n 135deg,\n oklch(var(--gradient-l) var(--gradient-c) calc(var(--glass-fg-h) + 63.53)) 0%,\n oklch(var(--gradient-l) var(--gradient-c) var(--glass-fg-h)) 100%\n );\n /* High-contrast gradient text for printing ON a tinted theme surface: DARK tint-hue gradient in light\n mode, LIGHT one in .dark (--gradient-contrast-l/-c), so legibility comes from lightness. */\n --gradient-text-contrast: linear-gradient(\n 135deg,\n oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) calc(var(--glass-fg-h) + 63.53)) 0%,\n oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) var(--glass-fg-h)) 100%\n );\n /* (The crystal floor, --glass-crystal-bg, is in block C: the hue gloss flavor repaints it.) */\n /* ── Chakra — the step-cut gem surface ────────────────────────────────────────────────────────\n BODY: the stone itself. The one tint-composing token here, so it is the only one with a gamut\n budget — min() against --glass-chakra-c-max (not a multiplier) for the same reason --border uses\n one: lapis declares c 0.15 and would be gamut-mapped, while any multiplier small enough to hold it\n drags the 0.07 jewels below where they should sit. Caps are measured per mode against the tightest\n ceiling across all 17 presets, so every preset delivers 100% of its declared chroma. */\n --glass-chakra-bg: oklch(\n calc(var(--glass-chakra-l) * 1%) min(var(--glass-tint-c), var(--glass-chakra-c-max)) var(--glass-tint-h) /\n var(--glass-chakra-a)\n );\n /* (The chakra SURFACE STACK, --glass-chakra-stack-bg, is in block C: it bakes the gloss.) */\n /* THE CUT — four sides, two steps, as INSET SHADOWS rather than inset background layers.\n A background layer is always a RECTANGLE: background-size shrinks it but gives it no radius of its\n own, so an inset gradient paints square corners over a rounded surface — a card with hard-cornered\n rectangles stacked on it. box-shadow has no such problem; an inset shadow is the border box's own\n rounded rect, offset, so every band follows --radius at whatever value the element carries, for\n free and at any size.\n Each side is a pair. The step-1 band (offset --glass-chakra-step) paints OVER the step-2 band\n (offset twice that), so the visible result is two concentric bands per side: 0→N shows step 1\n composited on step 2, N→2N shows step 2 alone. That is the step cut, and because step 2 is\n literally the same colour at half alpha it cannot drift out of sync with step 1.\n Order matters — box-shadow paints first-listed on top, so every step-1 band precedes its step-2\n twin. Corners: adjacent bands overlap and composite rather than mitering to a hard 45° line, which\n is the trade for following the radius. On a gem that reads as the corner facet catching both\n neighbours, and it is what the existing --glass-shadow bevel already does at top and bottom. */\n --glass-chakra-facets:\n inset 0 var(--glass-chakra-step) 0 oklch(var(--shadow-highlight) / var(--glass-chakra-facet-t-a)),\n inset var(--glass-chakra-step) 0 0 oklch(var(--shadow-highlight) / var(--glass-chakra-facet-l-a)),\n inset calc(var(--glass-chakra-step) * -1) 0 0 oklch(var(--shadow-ink) / var(--glass-chakra-facet-r-a)),\n inset 0 calc(var(--glass-chakra-step) * -1) 0 oklch(var(--shadow-ink) / var(--glass-chakra-facet-b-a)),\n inset 0 calc(var(--glass-chakra-step) * 2) 0 oklch(var(--shadow-highlight) / calc(var(--glass-chakra-facet-t-a) * 0.5)),\n inset calc(var(--glass-chakra-step) * 2) 0 0 oklch(var(--shadow-highlight) / calc(var(--glass-chakra-facet-l-a) * 0.5)),\n inset calc(var(--glass-chakra-step) * -2) 0 0 oklch(var(--shadow-ink) / calc(var(--glass-chakra-facet-r-a) * 0.5)),\n inset 0 calc(var(--glass-chakra-step) * -2) 0 oklch(var(--shadow-ink) / calc(var(--glass-chakra-facet-b-a) * 0.5));\n /* Edge + depth: the standard drop plus the cut. --glass-shadow's own top/bottom bevel is left out —\n the facet bands ARE the bevel, and doubling them would flatten the step read. */\n --glass-chakra-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-border-a));\n --glass-chakra-shadow: var(--glass-shadow), var(--glass-chakra-facets);\n --glass-chakra-shadow-hover: var(--glass-shadow-lg), var(--glass-chakra-facets);\n /* Precomposed background STACKS — the background-image compositions, single-sourced. The surface\n utilities consume them via --srf-* fallbacks (sheer utilities append the element-composed solidify\n layer underneath), and the [data-glass] style remaps below swap them wholesale. Live here so they\n re-resolve per tint scope and per mode wrapper. The crystal stack bakes the gloss into the\n background (the site-wide re-skin can't use ::before/::after — those need position:relative, which\n would break sticky/fixed surfaces). */\n --glass-wash-layer: linear-gradient(var(--glass-tint-wash), var(--glass-tint-wash));\n --glass-stack-bg: var(--glass-bg), var(--glass-wash-layer);\n /* Frosted rides the wash in the IMAGE stack (sheet + wash), leaving the background-color channel\n free for the veil floor. */\n --frosted-stack-bg: var(--glass-frosted-bg), var(--glass-wash-layer);\n /* (--crystal-stack-bg is in block C: it bakes the gloss.) */\n}\n\n/* C. GLOSS — the crystal highlight and everything that bakes it: the ink, the three gradients, the\n crystal floor, and the crystal + chakra stacks. Declared on every scope that can CHANGE it: :root and\n .dark (the mode knobs), [data-glass-tint] (the tint), and [data-gloss] (the flavor). The last is what\n lets `data-gloss` work below : these tokens are composed values, which bake where they are\n declared, so a [data-gloss] section that only re-pinned an input (as the flavor blocks used to) had\n its children inherit 's gradients and stacks pre-composed, and painted the page's gloss.\n THREE FLAVORS, chosen by two plain switches the [data-gloss] blocks in materials.css set to `1` or\n `initial` (unset = tonal):\n white → flat white specular (the classic glass shine) --glass-gloss-is-white: 1\n tonal → a tonally-close single-hue tint of the theme (--glass-gloss-l / --glass-gloss-tint)\n hue → iridescent: the highlight sweeps hues shifted ± --glass-gloss-hue-span around the tint\n hue, and repaints the crystal floor in the sweep --glass-gloss-is-hue: 1\n A switch selects nothing by itself: each flavor's variant (block C′ below) multiplies an alpha by it,\n so with the switch `initial` the variant is invalid at computed-value time and the var() fallback here,\n the tonal shape, applies. The switches are leaves, so they inherit, and a nested [data-gloss] sets both\n to pick its own flavor; a nested [data-glass-tint] recomposes from the inherited switches and keeps the\n page's flavor (it used to reset it to tonal). Each layer applies its own alpha. */\n:root,\n.dark,\n[data-glass-tint],\n[data-gloss] {\n /* The tonal ink as its own token (not inline in the fallback below) so check-gamut's scraper, which\n reads `--x: ` declarations, still sweeps it. */\n --glass-gloss-ink-tonal: calc(var(--glass-gloss-l, 94) * 1%)\n min(calc(var(--glass-tint-c) * var(--glass-gloss-tint, 2)), var(--glass-gloss-c-max, 0.3)) var(--glass-tint-h);\n --glass-gloss-ink: var(--glass-gloss-ink-white, var(--glass-gloss-ink-tonal));\n --glass-gloss-top: var(--glass-gloss-top-hue, linear-gradient(to bottom, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.4), transparent 30%));\n --glass-gloss-streak: var(\n --glass-gloss-streak-hue,\n linear-gradient(calc(135deg * var(--glass-gloss-hue-dir, 1)), oklch(var(--glass-gloss-ink, 100% 0 0) / 0.15), transparent 50%)\n );\n --glass-gloss-glow: var(\n --glass-gloss-glow-hue,\n radial-gradient(circle at 50% 120%, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.2), transparent 70%)\n );\n /* Crystal floor — clear glass; the alpha is the mode dial (--glass-crystal-bg-a).\n Crystal body. The lightness is a KNOB, not 100%, and that is the whole point: at L100 sRGB holds\n ZERO chroma, so this surface asked for 0.015 and delivered exactly none — crystal rendered pure\n white under every preset, in both modes (measured: #fffdff amethyst, #fffef4 moonstone,\n #fbffff lapis; RGB spread 2–11). check-gamut even reported it as \"no attainable chroma at all\"\n and skipped the pair rather than failing it.\n --glass-crystal-l is 96 because that is the lowest lightness at which the TIGHTEST of the 21 hues\n this theme uses (h268, lapis) can still carry the 0.015 this asks for: ceiling 0.0182, 1.22x\n headroom. L96.5 is the exact crossover at 1.06x, too thin given maxChromaFor already sits 3% inside\n the boundary to stop Safari clamping toward grey. The cost is small — at the 0.3 light-mode alpha\n the composite moves ~1.2 L, and 0.4 L in dark. */\n --glass-crystal-bg: var(\n --glass-crystal-bg-hue,\n oklch(calc(var(--glass-crystal-l) * 1%) calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / var(--glass-crystal-bg-a))\n );\n /* SURFACE STACKS: crystal and chakra bake the same gloss triple, so `data-gloss` white / tonal / hue\n reskin both, over a fresco slot and the tint wash. Nothing in the chakra stack is inset, so nothing\n has a corner of its own — the element's border-radius clips the whole stack (see the facet bands in\n block B). Baked into the background, not ::before/::after: the crystal PAGE style re-skins every\n surface, and pseudo layers would need position:relative, which breaks sticky/fixed surfaces. */\n --crystal-stack-bg:\n var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer);\n --glass-chakra-stack-bg:\n var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer);\n}\n\n/* C′. The FLAVOR VARIANTS block C falls back from, each valid only while its switch is `1`. Kept in a\n sibling block (same scopes) so scripts/check-gamut.mjs can model the hue variants at the hue flavor's\n own L74 (the [data-gloss=\"hue\"] block pins --glass-gloss-l there) without misreading block C's tonal\n ink at it.\n hue: each highlight sweeps (tint hue − span) → tint hue → (+ span), interpolated IN OKLCH so the arc\n passes through the base hue. Top leans to the low end, glow to the high end, streak spans both. The\n floor is replaced by a hue-carrying one at the gloss lightness (so Light deepens it): the default\n crystal floor sits near white, where chroma collapses, so the sweep would read over a white sheet. */\n:root,\n.dark,\n[data-glass-tint],\n[data-gloss] {\n --glass-gloss-ink-white: calc(var(--glass-gloss-is-white) * 100%) 0 0;\n --glass-gloss-top-hue: linear-gradient(\n to bottom in oklch,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.4 * var(--glass-gloss-is-hue))\n ),\n transparent 35%\n );\n --glass-gloss-streak-hue: linear-gradient(\n 115deg in oklch,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.32 * var(--glass-gloss-is-hue))\n )\n 0%,\n oklch(calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) var(--glass-tint-h) / calc(0.12 * var(--glass-gloss-is-hue))) 45%,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.32 * var(--glass-gloss-is-hue))\n )\n 85%,\n transparent\n );\n --glass-gloss-glow-hue: radial-gradient(\n circle at 50% 120%,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.25 * var(--glass-gloss-is-hue))\n ),\n transparent 70%\n );\n --glass-crystal-bg-hue: oklch(\n calc(var(--glass-gloss-l, 94) * 1%) calc(var(--glass-tint-c) * 1.2) var(--glass-tint-h) /\n calc(0.4 * var(--glass-gloss-is-hue))\n );\n}\n/* ── Glass surface utilities (@utility — tree-shaken per consumer usage) ──────\n The material system: ONE structural `glass` utility + orthogonal axis classes (border, veil, sm/lg,\n gradient, glow, sheen). Materials live in materials.css as [data-material] token sets; page styles\n in styles.css remap the same --srf-* tokens. No recipe utilities, no !important. */\n\n/* The glow itself — a punchy two-layer colored halo, centralized so its strength is tunable in ONE\n place. `glass-glow` is the resting glow; `glass-glow-lg` (used on hover) intensifies it. Both set\n --glow-layer, which every glass box-shadow folds in, so the glow shows under EVERY glass style.\n Full mechanism + tuning + troubleshooting: docs/glow.md */\n@utility glass-glow {\n --glow-layer: 0 0 8px 1px var(--glass-glow), 0 0 18px 3px var(--glass-glow);\n}\n@utility glass-glow-lg {\n --glow-layer: 0 0 12px 2px var(--glass-glow), 0 0 28px 5px var(--glass-glow);\n}\n\n/* ── THE MATERIAL SYSTEM (Sistine 2.0) ────────────────────────────────────────\n ONE structural utility (`glass`) + orthogonal axis classes. The material (a token set) comes from\n [data-material] (materials.css) or the inherited [data-glass] page remap (styles.css); with\n neither, the --srf-* fallbacks render the default glass material. Axis utilities only SET custom\n props (or properties `glass` never touches), so Tailwind's emission order can never flip a result.\n (The legacy recipe utilities below coexist during the migration and die with it.) */\n\n/* Structural glass — background/filter/shadow plumbing only; borderless (add glass-border).\n Channels, outermost first:\n background-image = [--srf-hover state slot] + [--acc-gradient accent slot] + [--srf-bg-image material stack] + [solidify]\n background-color = --veil-floor (element veil) > --srf-bg-color (material floor) > transparent\n backdrop-filter = --srf-filter (material) > blur(--srf-blur > --blur) saturate(180%)\n box-shadow = --srf-shadow (material) > --srf-elev (size axis) > --glass-shadow, + glow\n The leading gradient slot is a no-op layer so `glass-gradient` composes over ANY material (same\n pattern as the fresco slot inside --crystal-stack-bg). Both no-op slots resolve to `none` rather\n than linear-gradient(transparent, transparent): `none` is a valid , holds the layer\n position identically, and paints nothing — where a transparent GRADIENT still goes through the\n gradient rasteriser (and its dither) to deliver zero pixels.\n The solidify floor (--glass-opacity dial) is composed HERE, at the element — never in a token\n context (the var-composition-resolves-at-declaration gotcha; see scripts/check-theme.mjs). The\n --srf-solidify slot lets a material DROP that layer when it is provably a no-op (the opaque material,\n and the opaque PAGE style, which sets it on its own scope so it reaches `@apply glass` surfaces that\n carry no `.glass` class); materials that want the normal floor pin the slot back to `initial`\n (guaranteed-invalid → the var() fallback composes --glass-solidify at the element). --veil-solidify\n leads the chain: glass-veil sets it (non-inheriting), because a VEILED surface paints the neutral\n veil floor in its background-colour channel, so the layer is real there whatever the slot says. */\n@utility glass {\n /* Fallback 0.7, not 0: every glass surface now sits on a 70% solidify floor unless a consumer dials\n it back. Measured before changing it, because AutoForeground does NOT read --glass-opacity — it\n bands text against the sheer floor, so a more solid surface than it modelled could have cost\n contrast. Body text goes 91.5 -> 86.5 Lc in light and 88.0 -> 85.8 in dark, both far above the\n 75 body floor (it still clears at a full 1.0). Set --glass-opacity: 0 for the old sheer default. */\n /* --glass-solidify-BG, not --glass-opaque-bg: the backing under sheer glass is its own surface now\n (engine.css / tokens.css). In dark the two resolve identically, so this is a light-mode change. */\n --glass-solidify-fill: color-mix(in oklch, var(--glass-solidify-bg, var(--glass-opaque-bg)) calc(var(--glass-opacity, 0.7) * 100%), transparent);\n --glass-solidify: linear-gradient(var(--glass-solidify-fill), var(--glass-solidify-fill));\n /* --srf-hover leads the stack because a hover/selection wash has to sit ON the material, not under it.\n `hover:bg-accent` sets background-COLOR, which is the bottom of the box — beneath all 14 image layers\n the glass stack paints, of which the solidify floor alone is 70% opaque. At most ~30% of the accent\n survived to the eye, so the hover read as the material's own colour (white in light mode) no matter\n what --accent was set to. No amount of tuning the token could fix that; it is a layer-order problem.\n Same no-op `none` slot pattern as --acc-gradient below it. */\n background-image:\n var(--srf-hover, none), var(--acc-gradient, none), var(--srf-bg-image, var(--glass-stack-bg)),\n var(--veil-solidify, var(--srf-solidify, var(--glass-solidify)));\n background-color: var(--veil-floor, var(--srf-bg-color, transparent));\n /* Author ONLY the standard backdrop-filter (no -webkit- twin: Lightning dedupes the pair keeping\n the PREFIXED one, which Chromium never aliased — the [no-webkit-twin] invariant guards this).\n The max() composes the diffuse FLOOR for adaptive surfaces. */\n backdrop-filter: var(--srf-filter, blur(max(var(--srf-blur, var(--blur)), var(--dif, 0px))) saturate(180%)) var(--srf-stain,);\n box-shadow: var(--srf-shadow, var(--srf-elev, var(--glass-shadow))), var(--glow-layer, 0 0 0 0 transparent);\n}\n\n/* Border axis — the material picks the color unless the element asks for ink (--brd-ink, the\n borderColor axis below); width resolves material pin (--srf-border-w, which itself reads the --brd-w\n weight slot) → weight slot directly (adaptive surfaces) → 1px hairline. */\n@utility glass-border {\n border: var(--srf-border-w, var(--brd-w, 1px)) solid var(--brd-ink, var(--srf-border-color, var(--glass-border)));\n}\n/* Border WEIGHTS — token setters for the --brd-w slot (order-safe; same-element composition, like\n glass-sm's --srf-blur). Hairline is the unmarked default (1px; 0.5px under frosted); rim = 2px, frame = 4px. */\n@utility glass-border-rim {\n --brd-w: 2px;\n}\n@utility glass-border-frame {\n --brd-w: 4px;\n}\n/* Border COLOUR — ink. The material edge is a near-white rim at the material's alpha: it reads as cut\n glass on a dark page and as nothing at all on most light themes, at hairline, rim or frame weight\n alike, so a weight axis whose colour disappears went unused. Ink is the element's own text colour\n (--foreground, after any per-material text remap on this element), mixed toward transparent by\n --glass-border-ink-a: a visible line anywhere text is legible. Element-composed and registered\n non-inheriting (registrations.css), so a child border keeps its own material colour. */\n@utility glass-border-ink {\n --brd-ink: color-mix(in oklch, var(--foreground) calc(var(--glass-border-ink-a, 0.5) * 100%), transparent);\n}\n\n/* Diffuse axis — a READABILITY BLUR FLOOR (not a blur value): every translucent material computes\n blur(max(, --dif)), so crystal stays crystal until a text-dense surface opts in.\n Token-setter (order-safe); OPAQUE never reads the slot (--srf-filter: none), so the short-circuit\n is inherited — no GPU spend on solid floors. Tune the floor via --glass-diffuse. */\n@utility glass-diffuse {\n --dif: var(--glass-diffuse, 12px);\n}\n\n/* Veil axis — the legibility floor for read-through overlays (menus, tooltips, toasts).\n Composed AT THE ELEMENT so a scoped --glass-solid-a resolves here (check-theme invariant 2b).\n The veil rides the background-COLOR channel, under the material's image stack, and — being\n element-declared — beats any inherited --srf-bg-color remap. */\n/* Stained axis — REAL stained-glass optics: grayscale collapses the backdrop to pure luminance\n (lighter/darker only, akin to alpha), so the glass's OWN tint supplies every drop of color and\n whatever sits behind renders as tonal shades of the theme. One color-matrix op — free next to the\n blur. --glass-stain-amount (0..1) dials how completely the backdrop's color is stripped. */\n@utility glass-stained {\n --srf-stain: var(--glass-stain);\n}\n\n@utility glass-veil {\n --veil-floor: oklch(var(--glass-solid-l) 0 0 / var(--glass-solid-a));\n /* The veil keeps the solidify layer even where the material or page dropped it (--srf-solidify: none):\n those drops are only no-ops over the opaque floor, and a veiled surface paints the veil instead. */\n --veil-solidify: var(--glass-solidify);\n}\n\n/* THE PAGE BACKDROP LAYER — the fixed, full-viewport ground every sheer material blurs. Put this on the\n `fixed inset-0 -z-10` container each background component renders.\n It exists to force a compositing layer. `backdrop-filter` re-samples everything painted beneath it,\n and this layer sits at z-index -10 under the entire document; unpromoted, the compositor has to\n re-rasterise it against the scroll position every frame, which is where the scroll artifact on the\n sheer materials comes from. `opaque` is the ONE material that sets --srf-filter: none — and the one\n material that never showed the artifact, which is the correlation that points here.\n Measured on the deployed page, this element resolved will-change:auto / transform:none /\n contain:none — nothing was promoting it. Promoting is safe specifically because it has ZERO\n position:fixed descendants and no background-attachment:fixed; a transform/will-change would\n otherwise become their containing block and reparent them. Re-check that before adding children.\n will-change (rather than a translateZ hack) because the hint is honest here: this layer is on screen\n and being sampled for the whole session, so there is nothing to hint OFF later. */\n@utility glass-backdrop-layer {\n will-change: transform;\n}\n\n/* Size axes — blur + elevation tiers. These feed the GLASS material only: frosted/crystal/opaque\n (and page remaps) set --srf-filter/--srf-shadow wholesale, which flattens the tiers — matching\n the old system, where the [data-glass] styles flattened glass-surface-sm/-lg. */\n@utility glass-sm {\n --srf-blur: var(--blur-sm);\n --srf-elev: var(--glass-shadow-sm);\n}\n@utility glass-lg {\n --srf-blur: var(--blur-lg);\n --srf-elev: var(--glass-shadow-lg);\n}\n@utility glass-xl {\n --srf-blur: var(--blur-xl);\n --srf-elev: var(--glass-shadow-lg);\n}\n\n/* ── Gradient ACCENT — a brand wash ON a material, not a material. Fills the no-op --acc-gradient\n slot at the top of every `glass` background, so it composes over glass, frosted, crystal, and\n opaque alike. A token-setter (no property `glass` sets), so it's immune to utility emission order. */\n@utility glass-gradient {\n --acc-gradient: var(--gradient);\n}\n\n/* ── Hover/selection WASH — the interactive twin of glass-gradient. Fills the --srf-hover slot at the\n very top of the background stack, which is the only place a state colour is actually visible on a\n glass surface: `hover:bg-accent` writes background-COLOR, and every one of the material's image\n layers paints over it (solidify alone is 70% opaque), so the accent arrived at ~30% strength blended\n into the material and read as plain white on a light theme.\n The wash is deliberately translucent rather than a flat fill — glass that turns solid on hover stops\n looking like glass. 70% of --accent over the material keeps the wash, gloss and blur reading through\n it while still shifting the surface unmistakably toward the theme colour.\n Pair with `hover:bg-accent` rather than replacing it: the background-color still does the work on the\n NON-glass variants (outline/default paint no material, so nothing covers it there). */\n@utility glass-hover-accent {\n &:hover {\n --srf-hover: linear-gradient(var(--glass-hover-wash), var(--glass-hover-wash));\n }\n}\n\n/* Sheen — the OPT-IN hover shimmer (the gloss slides across on hover). Self-contained (highlight + diagonal\n + glow + the 200%/hover shift), so it works on any glass surface AND, layered on glass-crystal (which\n already has the static gloss), its ::before/::after override crystal's to add the motion. Pseudo-element\n based → position:relative; avoid on sticky/fixed surfaces. */\n@utility glass-sheen {\n position: relative;\n\n &::before,\n &::after {\n content: \"\";\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n }\n\n &::before {\n background: var(--glass-gloss-top), var(--glass-gloss-streak);\n background-size: 200% 200%;\n background-position: 0% 0%;\n opacity: 0.7;\n transition: background-position 0.3s ease;\n }\n\n &::after {\n background: var(--glass-gloss-glow);\n opacity: 0.8;\n }\n\n &:hover::before {\n background-position: 100% 100%;\n }\n}\n\n/* Namespaced: a bare `shimmer` is a global name, and a consumer's own @keyframes shimmer (or another\n library's) replaced this one for everyone, whichever stylesheet loaded last. Animates `translate`, not\n `transform`: Tailwind v4's `-translate-x-full` (the skeleton's resting position) writes the `translate`\n property, which COMPOSES with an animated transform rather than being replaced by it, so the sweep ran\n from -200% to 0% and parked the highlight on top of the skeleton. Animating the same property replaces\n the resting value while running, and with the animation off it falls back to that resting value. */\n@keyframes sistine-shimmer {\n from {\n translate: -100% 0;\n }\n to {\n translate: 100% 0;\n }\n}\n\n/* ── Reduced motion ───────────────────────────────────────────────────────────\n CONTRIBUTING says \"Respect prefers-reduced-motion\"; the pattern scenes did and nothing else did.\n This closes that, but NOT with the blunt global `*, ::before, ::after { animation: none !important }`\n reset — that would also flatten the pattern scenes' own carefully scoped rules and every enter/exit\n transition in the library.\n\n The treatment differs by what the animation actually IS, because the preference is about MOTION, not\n about animation in general:\n - shimmer → translation across a surface. Real motion, purely decorative: stopped. The skeleton\n still reads as a skeleton from its base fill.\n - spin → rotation. Real motion, but it is the ONLY signal that a loader is alive, so freezing\n it reads as \"hung\" rather than \"calm\". Slowed hard instead of stopped.\n - pulse → opacity only. Nothing moves, so there is nothing to trigger a vestibular response.\n Left alone deliberately; it also carries meaning (the input-otp caret is a blink).\n Unlayered on purpose: Tailwind's generated utilities sit inside @layer utilities, and an unlayered\n rule outranks any layered one regardless of order, so this needs neither !important nor a specificity\n hack. */\n@media (prefers-reduced-motion: reduce) {\n .animate-\\[sistine-shimmer_2s_infinite\\],\n .before\\:animate-\\[sistine-shimmer_2s_infinite\\]::before {\n animation: none;\n }\n\n .animate-spin {\n animation-duration: 3s;\n }\n}\n/* ── Materials — [data-material] token sets + gloss flavors ───────────────────\n Four materials, one attribute. Each block declares the FULL --srf-* pin set ON the element, so an\n explicit material always beats an inherited [data-glass] page remap (element-declared custom props\n beat inherited ones). One material per element is structural: attributes can't stack, so there is\n no !important anywhere in the system.\n NOTE these are inherited custom properties — a data-material element also acts as a SCOPED default\n for adaptive `glass` descendants (the mechanism DialogContent's old data-glass hack hand-rolled).\n Pin a child back with its own data-material.\n Import order: after tokens.css — [data-gloss=\"hue\"] re-pins --glass-gloss-l and the crystal shadows,\n which tokens.css declares on :root/.dark at equal specificity, so source order decides on . */\n\n[data-material=\"glass\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--glass-stack-bg);\n --srf-bg-color: transparent;\n /* Composed on the element, so the glass-sm/glass-lg axes still resolve here (same-element\n setter/reader is safe — the composition gotcha only bites pre-composed inherited values). */\n --srf-filter: blur(max(var(--srf-blur, var(--blur)), var(--dif, 0px))) saturate(180%);\n --srf-border-color: var(--glass-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--srf-elev, var(--glass-shadow));\n}\n\n/* Frosted — the tint wash rides the IMAGE stack (--frosted-stack-bg = sheet + wash), leaving the\n background-color channel free for the veil floor. */\n[data-material=\"frosted\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--frosted-stack-bg);\n --srf-bg-color: transparent;\n --srf-filter: blur(max(var(--blur-frosted), var(--dif, 0px))) saturate(200%);\n --srf-border-color: var(--glass-frosted-border);\n --srf-border-w: var(--brd-w, 0.5px);\n --srf-shadow: var(--glass-frosted-shadow);\n}\n\n/* Crystal — gloss BAKED into the background stack (sticky/fixed-safe; no pseudo-elements, no\n position:relative). --crystal-stack-bg carries the --glass-crystal-fresco pass-through. The hover\n sparkle is a plain token swap; box-shadow transitions on the real property. No saturate(),\n matching the old variant. */\n[data-material=\"crystal\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--crystal-stack-bg);\n --srf-bg-color: var(--glass-crystal-bg);\n --srf-filter: blur(max(var(--blur-crystal), var(--dif, 0px)));\n --srf-border-color: var(--glass-crystal-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--glass-crystal-shadow);\n transition: box-shadow 0.3s ease;\n}\n[data-material=\"crystal\"]:hover {\n --srf-shadow: var(--glass-crystal-shadow-hover);\n}\n/* Crystal text tiers — the baked gloss locally LIGHTENS the title zone (worst in dark mode), so text\n inside crystal remaps to the -crystal set AutoForeground bands against the glossed crystal surface.\n VEILED crystal is excluded: the veil floor is exactly what the normal tiers are banded for. */\n[data-material=\"crystal\"]:not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-crystal);\n --foreground-soft: var(--foreground-soft-crystal);\n --foreground-strong: var(--foreground-strong-crystal);\n --foreground-ui: var(--foreground-ui-crystal);\n --muted-foreground: var(--muted-foreground-crystal);\n}\n\n/* Chakra — the step-cut gem: a translucent tinted body with a two-step facet frame on all four edges,\n sitting between crystal (near-clear) and opaque (solid). Like crystal it bakes everything into the\n background stack and the shadow — no pseudo-elements, so it is safe on sticky/fixed surfaces, and it\n carries the same gloss triple, so `data-gloss` white / tonal / hue reskin it too.\n The cut lives entirely in --srf-shadow (see --glass-chakra-facets in engine.css). Nothing here is an\n inset BACKGROUND layer, deliberately: background layers are rectangles with no radius of their own,\n so an inset gradient stacks square corners on a rounded surface. Inset shadows follow --radius.\n Because the stack is plain full-bleed layers, the sheer-glass axes work untouched — glass-diffuse\n raises --dif into the blur below, and the --glass-opacity solidify floor composes under the stack\n exactly as it does for crystal. */\n[data-material=\"chakra\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--glass-chakra-stack-bg);\n --srf-bg-color: var(--glass-chakra-bg);\n --srf-filter: blur(max(var(--blur-chakra), var(--dif, 0px)));\n --srf-border-color: var(--glass-chakra-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--glass-chakra-shadow);\n transition: box-shadow 0.3s ease;\n}\n[data-material=\"chakra\"]:hover {\n --srf-shadow: var(--glass-chakra-shadow-hover);\n}\n/* Chakra text tiers — content sits on the body, whose lightness (--glass-chakra-l) is its own dial and\n can sit well away from the page's. Same reasoning as opaque/crystal; veiled chakra is excluded\n because the veil floor is exactly what the NORMAL tiers are banded for. */\n[data-material=\"chakra\"]:not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-chakra);\n --foreground-soft: var(--foreground-soft-chakra);\n --foreground-strong: var(--foreground-strong-chakra);\n --foreground-ui: var(--foreground-ui-chakra);\n --muted-foreground: var(--muted-foreground-chakra);\n}\n\n/* Opaque — solid tinted floor, nothing to blur through. --glass-opaque-outline stays settable on any\n ancestor: it's composed HERE, on the element, so it resolves where it's read. The floor can be\n LIGHT while the page is dark, so the foreground tiers remap to the -opaque set AutoForeground\n bands against that floor (inherits into the card's content). */\n[data-material=\"opaque\"] {\n /* DROP the solidify layer: it is `--glass-opaque-bg` at --glass-opacity alpha painted directly over a\n background-COLOR of the same --glass-opaque-bg at alpha 1 — i.e. C over C, which composites to C for\n ANY alpha. Measured across tints and both modes it moves every channel by exactly 0. So the layer was\n one full gradient rasterisation (and one dither pass) for provably zero pixels, on the one material\n with no backdrop-filter behind it to mask that noise. Restored when the surface is VEILED (glass-veil\n sets --veil-solidify, which `glass` reads first) — there the background-color is the neutral veil\n floor, not --glass-opaque-bg, so the layer is real (dropping it there would shift up to 31/255 in\n light and 42/255 in dark). */\n --srf-solidify: none;\n /* NO sheet either — opaque now paints a FLAT background-color and nothing else. --glass-bg is a 5-stop\n gradient whose total range, composited over this floor, is 2–4 sRGB codes across an entire card in\n light and 5–7 in dark: one code per ~100–200px, far too shallow to draw cleanly in 8-bit, so the\n rasteriser dithers rather than bands. On every other material a real backdrop-filter supplies blurred\n page content that masks that noise; opaque sets --srf-filter: none, so there is nothing behind it and\n the dither is the only variation on the card — read as grain, worst over text.\n The sheet's GRADIENT was therefore invisible, but its uniform LIFT was not (+1.7 L light, +4.8 L dark),\n so it is folded into --glass-opaque-l rather than dropped: see tokens.css. AutoForeground reads that\n same token, so the -opaque text tiers re-band against the new floor automatically. */\n --srf-bg-image: none;\n --srf-bg-color: var(--glass-opaque-bg);\n --srf-filter: none;\n --srf-border-color: var(--glass-opaque-outline, var(--glass-border));\n /* Outline WEIGHT: an element rim/frame class (--brd-w) beats the page-level outline weight (the\n switcher's --glass-opaque-outline-w on ), which beats the 1px hairline. */\n --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px));\n --srf-shadow: var(--glass-shadow);\n /* Veiled overlays INSIDE an opaque element compute a fully solid floor — same guarantee the opaque\n PAGE style makes (menus/toasts must stay legible on a solid ground). */\n --glass-solid-a: 1;\n --foreground: var(--foreground-opaque);\n --foreground-soft: var(--foreground-soft-opaque);\n --foreground-strong: var(--foreground-strong-opaque);\n --foreground-ui: var(--foreground-ui-opaque);\n --muted-foreground: var(--muted-foreground-opaque);\n}\n\n/* ── Gloss flavors ────────────────────────────────────────────────────────────\n data-gloss on or ANY element swaps the crystal (and chakra) gloss for that subtree. Default\n (unset) = tonal. These blocks set LEAVES only — the two flavor switches, plus the hue flavor's own\n lightness and shadows — and engine.css block C recomposes the gloss on every [data-gloss] element from\n them. That split is what makes a scoped `
    ` work: a composed value bakes\n where it is declared, so a block here that declared one would hand its children 's gloss.\n Each block sets BOTH switches, so a nested flavor overrides an ancestor's instead of combining with it.\n They compose with [data-material=\"crystal\"] AND the [data-glass=\"crystal\"] page style, in both modes.\n Known edge: hue's pinned --glass-gloss-l and bevel-less crystal shadows are mode-twinned leaves that a\n white/tonal section nested INSIDE a hue scope inherits (no mode-aware reset exists for them here). */\n[data-gloss=\"white\"] {\n --glass-gloss-is-white: 1; /* the ink becomes the flat white specular, 100% 0 0 */\n --glass-gloss-is-hue: initial;\n}\n[data-gloss=\"tonal\"] {\n /* Explicit tonal (also the default) — both switches off, so it can override a white/hue ancestor. */\n --glass-gloss-is-white: initial;\n --glass-gloss-is-hue: initial;\n}\n[data-gloss=\"hue\"] {\n --glass-gloss-is-white: initial;\n --glass-gloss-is-hue: 1;\n /* Hue OWNS its lightness rather than sharing the tonal twin. The two flavors want opposite things\n from the same number: tonal wants the brightest value the surface allows (97 in light) so it reads\n as a specular, while hue needs chroma headroom — its stops ask --glass-gloss-c 0.16, and the sRGB\n ceiling at L 97 is 0.0136, so the iridescent sweep would deliver 8% of its colour and wash out to\n near-white exactly where it is supposed to be most vivid.\n 74 is mode-invariant on purpose: it holds 81% of that chroma, and it lands on the right side of\n both surfaces anyway (+18.4 L over the dark floor, −9.0 over the light one — for an iridescent\n sweep, being SEEN matters more than which direction it moves). The demo's flavor switcher already\n seeded 74 at runtime for this reason; this makes a no-JS consumer get it too.\n \"Holds 81% of that chroma\" assumed the browser gamut-MAPS an over-ceiling request down to the\n ceiling. Measured in WebKit it CLIPS instead (see --glass-gloss-c-max in tokens.css), so 0.16 did\n not deliver 81% of itself — it drove the tightest hues to a pinned channel. 0.12 sits just under\n 0.1240, the L-74 ceiling for turquoise (h190) and the tightest of the presets, so the sweep now\n renders every hue it asks for. scripts/check-gamut.mjs reads this 74 to score the variants. */\n --glass-gloss-l: 74;\n /* Kill the white bevel: the crystal shadow carries a white inset highlight, which would sit on top of\n the swept hue. (The white floor is replaced in engine.css block C′.) */\n --glass-crystal-shadow: var(--glass-shadow);\n --glass-crystal-shadow-hover: var(--glass-shadow-lg);\n}\n/* ── Site-wide style remaps + vendor cascade fixes ──────────────────────────── */\n\n/* Keep react-day-picker from painting over the glass background */\n@layer utilities {\n .glass[class*=\"rdp\"],\n .glass .rdp,\n .glass .rdp-month {\n background-color: transparent !important;\n }\n}\n\n/* ── Site-wide glass style: token remaps ─────────────────────────────────────\n data-glass on (frosted | crystal | opaque) re-skins every ADAPTIVE surface (`glass` with no\n data-material) by remapping the --srf-* tokens it reads. An element-level [data-material] pins its\n own tokens and beats these by cascade physics — reinforced by the :where() wrapper, which drops\n every remap (incl. its cross-product legs) to specificity (0,0,0), so a co-located data-material on\n a tint-scope element always wins. The cross-product legs keep the swapped stacks re-resolving inside\n scoped tints, status toasts included (a bare [data-glass] rule would bake 's tint into\n descendants' surfaces). Veiled surfaces (glass-veil) keep their element-composed floor.\n The ELEMENT-level rules further down (text tiers, the opaque edge) need to know which elements are\n surfaces. `.glass` says so for the utility class; an element that wears the material through\n `@apply glass` has no class, so it opts in with a `data-surface` attribute (`data-surface=\"veil\"`\n when it also applies glass-veil). Everything ABOVE is inherited tokens and needs no marker. */\n:where([data-glass=\"frosted\"], [data-glass=\"frosted\"] [data-glass-tint], [data-glass=\"frosted\"][data-glass-tint]) {\n --srf-bg-image: var(--frosted-stack-bg);\n /* NO --srf-bg-color wash here: the wash already rides the IMAGE stack (--frosted-stack-bg), same\n as the frosted material — a color-channel copy double-washed tinted presets and stole the veil\n floor's channel. */\n --srf-filter: blur(max(var(--blur-frosted), var(--dif, 0px))) saturate(200%);\n --srf-border-color: var(--glass-frosted-border);\n --srf-border-w: var(--brd-w, 0.5px);\n --srf-shadow: var(--glass-frosted-shadow);\n}\n:where([data-glass=\"crystal\"], [data-glass=\"crystal\"] [data-glass-tint], [data-glass=\"crystal\"][data-glass-tint]) {\n /* Gloss rides inside --crystal-stack-bg (baked — sticky/fixed-safe). Shimmer stays the opt-in\n glass-sheen. No saturate(), matching the material. */\n --srf-bg-image: var(--crystal-stack-bg);\n --srf-bg-color: var(--glass-crystal-bg);\n --srf-filter: blur(max(var(--blur-crystal), var(--dif, 0px)));\n --srf-border-color: var(--glass-crystal-border);\n --srf-shadow: var(--glass-crystal-shadow);\n}\n/* Crystal page: ADAPTIVE glass (no explicit material, not veiled) gets the -crystal text tiers, same\n as the material remap in materials.css — the baked gloss lightens title zones there too. */\n[data-glass=\"crystal\"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-crystal);\n --foreground-soft: var(--foreground-soft-crystal);\n --foreground-strong: var(--foreground-strong-crystal);\n --foreground-ui: var(--foreground-ui-crystal);\n --muted-foreground: var(--muted-foreground-crystal);\n}\n/* Chakra page — every adaptive surface becomes a step-cut gem. The cut rides --srf-shadow, so it\n follows each surface's own border-radius with nothing to keep in sync. */\n:where([data-glass=\"chakra\"], [data-glass=\"chakra\"] [data-glass-tint], [data-glass=\"chakra\"][data-glass-tint]) {\n --srf-bg-image: var(--glass-chakra-stack-bg);\n --srf-bg-color: var(--glass-chakra-bg);\n --srf-filter: blur(max(var(--blur-chakra), var(--dif, 0px)));\n --srf-border-color: var(--glass-chakra-border);\n --srf-shadow: var(--glass-chakra-shadow);\n}\n/* Chakra page: adaptive glass (no explicit material, not veiled) takes the -chakra text tiers, same\n as the material remap in materials.css. */\n[data-glass=\"chakra\"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-chakra);\n --foreground-soft: var(--foreground-soft-chakra);\n --foreground-strong: var(--foreground-strong-chakra);\n --foreground-ui: var(--foreground-ui-chakra);\n --muted-foreground: var(--muted-foreground-chakra);\n}\n/* Opaque — solid tinted panels for adaptive surfaces. --glass-solid-a: 1 makes any veiled overlay\n (glass-veil) compute a FULLY solid floor here too, so menus/toasts stay legible on an opaque page. */\n:where([data-glass=\"opaque\"], [data-glass=\"opaque\"] [data-glass-tint], [data-glass=\"opaque\"][data-glass-tint]) {\n /* Flat floor, no sheet — same reasoning as [data-material=\"opaque\"] in materials.css: with\n --srf-filter: none there is no blurred backdrop to mask the dither of a 2–7 code gradient, and the\n sheet's uniform lift is folded into --glass-opaque-l instead. */\n --srf-bg-image: none;\n --srf-bg-color: var(--glass-opaque-bg);\n --srf-filter: none;\n --srf-shadow: var(--glass-shadow);\n /* Same provable no-op as [data-material=\"opaque\"]: --glass-opaque-bg sits in the background-COLOR\n channel, so the solidify layer would paint that colour over itself. A TOKEN here (it used to be an\n element rule keyed on `.glass`), so it reaches an `@apply glass` surface too, which kept the light\n solidify backing at 70% over the opaque floor and read nearly white. An explicit material pins the\n slot back (`initial`), and a veiled surface restores the layer with --veil-solidify. */\n --srf-solidify: none;\n}\n/* --glass-solid-a CANNOT ride in the :where() block above. :where() is specificity ZERO by design (so a\n page style never beats an explicit material), and every other declaration up there survives that\n because nothing else declares them — they are material SLOTS with no competing value. This one is\n different: tokens.css declares --glass-solid-a on :root, which is (0,1,0), so the :where() version\n lost every time and the opaque page never actually got its solid floor. Measured: --glass-solid-a\n resolved to 0.65 on under [data-glass=\"opaque\"], identical to every other page style.\n The visible symptom is any VEILED surface on an opaque page staying translucent while the solid\n panels around it go opaque — menus, toasts and popovers, which is exactly what the block above says\n this is for. A bare attribute selector is (0,1,0), which ties :root and wins on source order because\n globals.css imports styles.css after tokens.css. It is a plain literal, so it simply inherits into\n the tint / toast scopes and needs none of their re-resolution. */\n[data-glass=\"opaque\"] {\n --glass-solid-a: 1;\n}\n/* Opaque page: element-level touches for ADAPTIVE surfaces only (a `.glass` or `[data-surface]` element\n with no explicit material — an explicit frosted/crystal card keeps its translucency AND its normal\n foreground). Declared ON the element because (a) --glass-opaque-outline is settable on any ancestor\n and must resolve where it's read, and (b) the foreground remap must not leak into explicit materials.\n The uniform EDGE applies to every adaptive panel, veiled or not — a veiled card under the opaque page\n still reads as a solid panel and needs the same outline. */\n[data-glass=\"opaque\"] :where(.glass, [data-surface]):not([data-material]) {\n --srf-border-color: var(--glass-opaque-outline, var(--glass-border));\n --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px));\n /* Paint the border HERE, not just the tokens: borderless roles (Card default) never render\n glass-border, so without this only bordered roles showed the outline — \"some cards get it,\n some don't\". Under the opaque page every solid panel takes the uniform edge; bordered elements\n resolve the same vars, so nothing double-draws. */\n border: var(--srf-border-w) solid var(--brd-ink, var(--srf-border-color));\n}\n/* The FOREGROUND remap is :not(.glass-veil) — the same carve-out crystal and chakra make, and the same\n one --veil-solidify makes for the solidify layer on this page style. A veiled card does not paint\n --glass-opaque-bg: glass-veil moves the background-COLOR channel to the neutral --veil-floor, so its\n text sits on the veil surface the BASE tokens are banded for, not on the opaque floor. Remapping it\n here handed those cards ink solved for a surface they do not have — visible as a hue jump, since the\n opaque floor is the one surface with nothing composited over it and so sits at the raw --glass-tint-h\n while every sheer surface lands a few degrees off it (measured on the /colors panel: 22.0 vs 14.6).\n Splitting the rule is what keeps the edge treatment while dropping the foreground one. */\n[data-glass=\"opaque\"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-opaque);\n --foreground-soft: var(--foreground-soft-opaque);\n --foreground-strong: var(--foreground-strong-opaque);\n --foreground-ui: var(--foreground-ui-opaque);\n --muted-foreground: var(--muted-foreground-opaque);\n}\n/* ── Glass tint presets ─────────────────────────────────────────────────────\n Set data-glass-tint on any element (commonly ) to recolor the glass in that subtree.\n Most presets just move the tint hue/chroma; \"sistine\" is a bespoke multi-hue fresco (gilded\n gold → terracotta flesh tones → azure sky), echoing the Sistine Chapel ceiling.\n Wash + all tint-derived tokens re-resolve per-scope via the :root,[data-glass-tint] split above. */\n/* Selenite — the neutral stone: no chroma and no wash, so the glass is the plain clear sheet. The tint\n values equal the engine default's hue with the colour switched off; the block exists so a CSS-only\n consumer (no tint switcher) gets selenite's colour-wheel anchor, which a hue-less theme needs. */\n[data-glass-tint=\"selenite\"] {\n --glass-tint-h: 250;\n --glass-tint-c: 0;\n --glass-tint-a: 0;\n --harmony-h: 0;\n}\n[data-glass-tint=\"sapphire\"] {\n --glass-tint-h: 255;\n --glass-tint-c: 0.075;\n --glass-tint-a: 0.19;\n --glass-wash-l: 63%;\n --glass-opaque-c-max: 0.056;\n}\n[data-glass-tint=\"aventurine\"] {\n --glass-tint-h: 158;\n --glass-tint-c: 0.076;\n --glass-tint-a: 0.54;\n --glass-wash-l: 88%;\n --glass-opaque-c-max: 0.195;\n}\n[data-glass-tint=\"amethyst\"] {\n --glass-tint-h: 300;\n --glass-tint-c: 0.101;\n --glass-tint-a: 0.15;\n --glass-wash-l: 55%;\n --glass-opaque-c-max: 0.065;\n}\n[data-glass-tint=\"rose\"] {\n --glass-tint-h: 8;\n --glass-tint-c: 0.095;\n --glass-tint-a: 0.17;\n --glass-wash-l: 64%;\n --glass-opaque-c-max: 0.062;\n}\n[data-glass-tint=\"amber\"] {\n --glass-tint-h: 75;\n --glass-tint-c: 0.062;\n --glass-tint-a: 0.38;\n --glass-wash-l: 81%;\n --glass-opaque-c-max: 0.096;\n}\n[data-glass-tint=\"carnelian\"] {\n --glass-tint-h: 38;\n --glass-tint-c: 0.081;\n --glass-tint-a: 0.19;\n --glass-wash-l: 67%;\n --glass-opaque-c-max: 0.062;\n}\n[data-glass-tint=\"peridot\"] {\n --glass-tint-h: 128;\n --glass-tint-c: 0.083;\n --glass-tint-a: 0.54;\n --glass-wash-l: 92%;\n --glass-opaque-c-max: 0.218;\n}\n[data-glass-tint=\"turquoise\"] {\n --glass-tint-h: 190;\n --glass-tint-c: 0.057;\n --glass-tint-a: 0.54;\n --glass-wash-l: 90%;\n --glass-opaque-c-max: 0.144;\n}\n[data-glass-tint=\"aquamarine\"] {\n --glass-tint-h: 215;\n --glass-tint-c: 0.053;\n --glass-tint-a: 0.5;\n --glass-wash-l: 82%;\n --glass-opaque-c-max: 0.091;\n}\n[data-glass-tint=\"tourmaline\"] {\n --glass-tint-h: 342;\n --glass-tint-c: 0.106;\n --glass-tint-a: 0.18;\n --glass-wash-l: 67%;\n --glass-opaque-c-max: 0.079;\n}\n[data-glass-tint=\"lapis\"] {\n /* Lapis lazuli IS the deep saturated ultramarine, and at plain jewel chroma it reads as just another\n blue next to sapphire. It used to buy that depth by declaring c 0.15 — far past the sRGB ceiling —\n and letting the browser clip. That DID deliver a deep blue, but by accident rather than by design:\n clipping chops channels, which drops LIGHTNESS, and the saturation came from landing darker.\n Measured, oklch(58% 0.375 268) renders L48.1 C0.2941 h271 — a colour nobody wrote down.\n The wash lightness below asks for that colour on purpose instead. h268's chroma ceiling PEAKS at\n L46 (0.2979) and collapses to 0.1293 by L74, so the shared 72/58 wash was sitting on the downslope;\n at L46 the same depth is available inside the gamut. oklch(45% 0.291 268) renders L44.9 C0.2909\n h267.9 — the same deep ultramarine, hue exact, nothing pinned.\n So lapis is no longer an exception to anything: it is the clearest case of the rule the whole\n palette now follows — put the wash where the hue can actually hold chroma, then ask for it. */\n --glass-tint-h: 268;\n --glass-tint-c: 0.085;\n --glass-tint-a: 0.13;\n --glass-wash-l: 46%;\n --glass-opaque-c-max: 0.055;\n}\n[data-glass-tint=\"goldstone\"] {\n --glass-tint-h: 22;\n --glass-tint-c: 0.094;\n --glass-tint-a: 0.17;\n --glass-wash-l: 63%;\n --glass-opaque-c-max: 0.06;\n}\n/* ── Per-mode wash ALPHA ──────────────────────────────────────────────────────\n Alpha slides the surface along a straight line from the material floor toward the wash colour, so\n how much a preset can take is set by how far its wash sits from THAT MODE's floor (L36.4 dark /\n L88 light). A wash near the floor barely moves the surface and tolerates a lot; a wash far from it\n crosses the mid-tone band where no text polarity reaches the body floor, and tolerates little.\n Because every wash now sits at its hue's chroma PEAK, dark-peaking hues (lapis L46, amethyst L55)\n sit near the DARK floor and take high alpha there, while light-peaking hues (peridot L92, turquoise\n L90) sit near the LIGHT floor and take high alpha there. The two columns are near mirrors.\n Each value is HALF that mode's measured headroom, so every preset keeps body text >= 75 Lc in both\n modes with room to spare. A single shared alpha cannot serve both ends: at 0.55 everywhere, five\n presets lose dark-mode text and lapis/amethyst lose light-mode text.\n The switcher no longer inlines --glass-tint-a for a named preset (see applyTint), so THESE are the\n values that render — nothing shadows them. */\n.dark[data-glass-tint=\"rose\"] {\n --glass-tint-a: 0.49;\n}\n.dark[data-glass-tint=\"goldstone\"] {\n --glass-tint-a: 0.5;\n}\n.dark[data-glass-tint=\"carnelian\"] {\n --glass-tint-a: 0.45;\n}\n.dark[data-glass-tint=\"amber\"] {\n --glass-tint-a: 0.34;\n}\n.dark[data-glass-tint=\"peridot\"] {\n --glass-tint-a: 0.28;\n}\n.dark[data-glass-tint=\"aventurine\"] {\n --glass-tint-a: 0.29;\n}\n.dark[data-glass-tint=\"turquoise\"] {\n --glass-tint-a: 0.28;\n}\n.dark[data-glass-tint=\"aquamarine\"] {\n --glass-tint-a: 0.31;\n}\n.dark[data-glass-tint=\"sapphire\"] {\n --glass-tint-a: 0.48;\n}\n.dark[data-glass-tint=\"lapis\"] {\n --glass-tint-a: 0.54;\n}\n.dark[data-glass-tint=\"amethyst\"] {\n --glass-tint-a: 0.54;\n}\n.dark[data-glass-tint=\"tourmaline\"] {\n --glass-tint-a: 0.46;\n}\n[data-glass-tint=\"moonstone\"] {\n /* Moonstone — a warm, very-low-chroma off-white glass (reads as cream, not yellow). A normal translucent\n tint: texture-preserving and light-text in both modes, just a desaturated warm hue. Anchors its harmony\n wheel at 0° (like selenite) so accents come out a colorful set, not a warp of the near-neutral hue. */\n --glass-tint-h: 75;\n --glass-tint-c: 0.047;\n --glass-tint-a: 0.18;\n --harmony-h: 0;\n /* Gloss chroma, moonstone's own. The global --glass-gloss-tint is 4.25 — sized for the JEWELS, where a\n 0.07–0.1 tint wants a vivid gem highlight. Moonstone declares 0.047 precisely so it \"reads as cream,\n not yellow\" (above), and 4.25× asks for 0.1998.\n --glass-gloss-c-max (tokens.css) is what stops that becoming a clipped primary, and in LIGHT it does\n the whole job: 0.013 caps both 0.1998 and 0.047 to the same value, so this override changes nothing\n there. It earns its place in DARK, where the cap is a roomy 0.11 — enough that the multiplier would\n still put moonstone's highlight at 2.3× its declared chroma, i.e. amber on a stone that is supposed\n to be pale. 1× holds the specular at the tint's own chroma and lets its LIGHTNESS do the work. */\n --glass-gloss-tint: 1;\n /* Moonstone daytime body lightness. Fitted for the retired opaque sheet (tokens.css) on moonstone's OWN\n floor rather than the global pair: this cream is already near-white, where a near-white sheet moved it\n far less than it moved the standard L90 floor. dE 0.0002 — indistinguishable from the old surface. */\n --glass-opaque-l: 94.5;\n /* NO pinned --glass-opaque-bg: the floor must stay DERIVED from --glass-opaque-l (engine), because the\n lightness slider / stored per-mode L land as an INLINE --glass-opaque-l that shadows this block — a\n literal pin would freeze the paint while AutoForeground keeps banding text against the token (light\n text on cream). Was c-scale 1 — the value that made the derived color exactly the original cream when\n the sheet still painted over it; 0.92 is that same cream now that the sheet is gone (it desaturated as\n well as lightened, so both knobs carry part of it). */\n --glass-opaque-c-scale: 0.92;\n}\n.dark[data-glass-tint=\"moonstone\"] {\n /* Moonstone night. The wash sits a few points above the standard 58% (warmth without glare; h75\n carries more real luminance per OKLCH point than the cool tints, so pale-warm washes read brighter\n than their numbers). AutoForeground reads BOTH wash knobs live (+ the [moonstone-sync] mirrored\n fallback), so page text and per-card dark-cream text re-band automatically.\n\n The FLOOR is what you actually see: at the default opacity every surface — cards, inputs, the nav\n pill — paints --glass-opaque-bg, and the wash only tints what shows through. It used to derive\n oklch(0.8 0.047 75), which is a sand/tobacco tan, so moonstone night read as brown rather than as\n the pale silvery stone it is named for. Lightness alone does not fix that: h75 is warm, so every\n lightness along it lands somewhere in the tan family. Cutting the CHROMA is what moves it off that\n family. Light mode is untouched (it keeps c-scale 1 at L94, where the same hue reads as cream).\n\n L52, down from a cream L84.9 — moonstone night is a DARK stone now, and that is forced rather than\n preferred. This floor feeds two surfaces at once: opaque cards paint it directly, and it composites\n at --glass-opacity over the dark veil to become the floor sheer glass sits on. Both have to clear\n the body floor, and for h75 nothing in L55–L84 reaches it in EITHER text polarity — that band is the\n mid-tone dead zone. A cream L84.9 put the opaque card just above it (Lc 77.1) but dragged the sheer\n surface to L65, dead centre, where body text topped out at 65.4 Lc and the preset had to be pinned\n as a known exception. Going lighter cannot fix it: the veil is 30% of the composite, which caps the\n solidify floor at L75.6 no matter how light this value gets, so the sheer surface can never climb\n out the top. Only the bottom exit is reachable. L52 is the PALEST value where both surfaces clear:\n sheer body text lands 80.0 Lc (was 65.4) and the opaque card 82.9 (was 77.1), and it also restores\n --foreground-strong, which could not reach its 90 target on the cream floor either. Still stone —\n oklch(0.52 0.021 75) is #70675c, a warm grey — and still well clear of the jewels' L36.4. */\n --glass-wash-l: 64%;\n --glass-wash-c-mult: 2;\n /* Fitted for the retired opaque sheet, like the daytime floor above and likewise not the global dark\n pair: moonstone night is a LIGHT floor, so the near-white sheet barely moved it. dE 0.0002. */\n --glass-opaque-l: 52;\n --glass-opaque-c-scale: 0.44;\n /* Static -opaque tier baselines. These flipped polarity with the floor: on the old cream L84.9 they were\n dark warm literals, and on an L52 stone they have to be light or SSR's first paint is dark-on-dark.\n Values are what AutoForeground itself solves for this surface, so the mounted refine is a no-op. */\n --foreground-opaque: oklch(1 0 75);\n --foreground-soft-opaque: oklch(0.9 0.05 75);\n --foreground-strong-opaque: oklch(1 0 75);\n --foreground-ui-opaque: oklch(0.91 0.05 75);\n --muted-foreground-opaque: oklch(0.97 0.02 75);\n /* Chakra needs the SAME treatment opaque gets above, and did not have it: chakra was added after this\n block was written, so it kept the global dark body (L28) and rendered oklch(0.28 0.046 75) = #36250c,\n a dark brown — the one surface where moonstone still read as tobacco rather than stone. Matching\n opaque's pair — now L51, chroma x0.45 → 0.021, landing on #6d6559, the same stone as the opaque\n floor one point above it.\n The hue stays 75 deliberately. Cutting CHROMA is what moves this off the tan family; lightness alone\n does not, and a cool hue would contradict the warm cream light mode keeps at L94. */\n --glass-chakra-l: 51;\n --glass-chakra-c-max: 0.021;\n /* Same reasoning as the -opaque baselines above, and flipped with them: an L51 body needs LIGHT text. */\n --foreground-chakra: oklch(1 0 75);\n --foreground-soft-chakra: oklch(0.9 0.05 75);\n --foreground-strong-chakra: oklch(1 0 75);\n --foreground-ui-chakra: oklch(0.91 0.05 75);\n --muted-foreground-chakra: oklch(0.96 0.03 75);\n}\n\n/* Semantic surfaces (Alert info/success/warning/destructive; destructive Button/Badge; status toasts,\n which the Toaster stamps from sonner's data-type — see TOAST_TINT in components/ui/sonner.tsx) render\n data-glass-tint=\"\" — one block per status, joining the grouped engine re-resolution above so\n --glass-bg / wash re-resolve at the alert/toast. */\n[data-glass-tint=\"info\"] {\n --glass-tint-h: 255;\n --glass-tint-c: 0.075;\n --glass-tint-a: 0.18;\n --glass-wash-l: 63%;\n}\n[data-glass-tint=\"success\"] {\n --glass-tint-h: 150;\n --glass-tint-c: 0.088;\n --glass-tint-a: 0.18;\n --glass-wash-l: 87%;\n}\n[data-glass-tint=\"warning\"] {\n --glass-tint-h: 85;\n --glass-tint-c: 0.063;\n --glass-tint-a: 0.18;\n --glass-wash-l: 84%;\n}\n[data-glass-tint=\"destructive\"] {\n --glass-tint-h: 25;\n --glass-tint-c: 0.094;\n --glass-tint-a: 0.18;\n --glass-wash-l: 63%;\n}\n/* FRESCOES (muse / sistine / aurora / gloaming): each block's --glass-tint-h/c/a MUST mirror its\n GlassTintSwitcher preset (components/glass-tint-switcher.tsx). The switcher INLINES those onto ,\n shadowing these blocks on any page with the switcher mounted — so these are the static-consumer fallback.\n Keep them identical, or the demo and a static page render different surfaces. (--glass-bg gradients and\n --glass-fg-h are CSS-only — no preset counterpart.) */\n\n/* Muse — the cityscape signature, mode-aware: cool daytime blues in light, the neon night sweep in\n dark, so it tracks the day/night background. Pick the look with the light/dark/system toggle. */\n/* Muse is now ONE palette in both modes: the neon city — cyan → violet → magenta → pink → amber. The\n old day sweep (a pale blue → cyan → teal cityscape) is gone; its hues live on in Aurora, which took\n them over.\n\n What differs between the modes is ALPHA, not colour. A day surface sits on a light page and needs\n more of the gradient to register; night sits on a dark one and needs less before the neon blows out.\n Lightness is shared because these stops are already bright (L 78–85), so they read on either page.\n\n Stop 2's chroma is 0.095, not the 0.17 night shipped with: h290 at L80 has an sRGB ceiling of 0.1052,\n so 0.17 was 162% over and has been browser-gamut-mapped this whole time — which also means it\n rendered differently in Safari (per-channel clamp toward grey) than in Chrome (hue-preserving map).\n 0.095 is 90% of its own ceiling, the same rule the rest of the stops follow. */\n[data-glass-tint=\"muse\"] {\n --glass-tint-h: 230;\n --glass-fg-h: 290; /* text/accent anchor: violet, matching night so the two modes share one identity */\n --glass-tint-c: 0.056;\n --glass-tint-a: 0.16;\n --glass-wash-l: 76%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.32) 0%,\n oklch(80% 0.095 290 / 0.3) 22%,\n oklch(78% 0.2 326 / 0.28) 45%,\n oklch(79% 0.12 350 / 0.28) 70%,\n oklch(84% 0.12 74 / 0.3) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.46) 0%,\n oklch(80% 0.095 290 / 0.44) 22%,\n oklch(78% 0.2 326 / 0.42) 45%,\n oklch(79% 0.12 350 / 0.42) 70%,\n oklch(84% 0.12 74 / 0.44) 100%\n );\n /* Light-mode body: the night anchor's hue (288) at a day-legible lightness. Chroma is 90% of the\n ceiling at L91 h288 (0.0446), because a light surface has very little chroma headroom. */\n --glass-opaque-bg: oklch(91% 0.04 288);\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"muse\"] {\n /* Night: the same neon palette as day, at night's lower alphas (a dark page needs less of the\n gradient before the neon blows out). Only alpha and the solid body differ from the block above. */\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.24) 0%,\n oklch(80% 0.095 290 / 0.22) 22%,\n oklch(78% 0.2 326 / 0.2) 45%,\n oklch(79% 0.12 350 / 0.2) 70%,\n oklch(84% 0.12 74 / 0.22) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.36) 0%,\n oklch(80% 0.095 290 / 0.34) 22%,\n oklch(78% 0.2 326 / 0.32) 45%,\n oklch(79% 0.12 350 / 0.32) 70%,\n oklch(84% 0.12 74 / 0.34) 100%\n );\n --glass-opaque-bg: oklch(34% 0.12 288);\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Sistine — the signature: a painterly fresco of four jewels (amber → rose → sapphire → aventurine,\n in that order), interpolated `in oklch` so the warm→cool transitions stay vivid instead of\n averaging to grey. Border + crystal + accent pick up the rose base via the tint vars. */\n[data-glass-tint=\"sistine\"] {\n --glass-tint-h: 75; /* surface: mirrors the Sistine switcher preset */\n --glass-fg-h: 18; /* text/accent anchor: crimson (hand-selected) — hue 18 reads as deep red, not the rose at h8 */\n --glass-tint-c: 0.062;\n --glass-tint-a: 0.16;\n --glass-wash-l: 81%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(90% 0.11 75 / 0.34) 0%,\n oklch(86% 0.11 8 / 0.3) 33%,\n oklch(86% 0.11 255 / 0.3) 66%,\n oklch(87% 0.11 158 / 0.32) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(90% 0.11 75 / 0.46) 0%,\n oklch(86% 0.11 8 / 0.42) 33%,\n oklch(86% 0.11 255 / 0.42) 66%,\n oklch(87% 0.11 158 / 0.44) 100%\n );\n /* Crystal composes its own background (not --glass-bg), so hand it the fresco explicitly. */\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"sistine\"] {\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.11 75 / 0.22) 0%,\n oklch(82% 0.11 8 / 0.2) 33%,\n oklch(82% 0.11 255 / 0.2) 66%,\n oklch(83% 0.11 158 / 0.22) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.11 75 / 0.32) 0%,\n oklch(82% 0.11 8 / 0.3) 33%,\n oklch(82% 0.11 255 / 0.3) 66%,\n oklch(83% 0.11 158 / 0.32) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Aurora — northern lights: spring-green → teal → violet, interpolated `in oklch` so the cool sweep\n stays vivid. Border + crystal + accent pick up the teal base via the tint vars.\n DAY was authored at L 90/89/84 — within ~5 L of the L-95 page it composites onto — so after the\n alpha only 2.2 L of separation survived and the bands read as haze rather than bands. Night works\n because its stops sit 60+ L above an L-20 page and land 13.6 L clear of it. The fix is that\n separation, not more colour: day already delivered MORE chroma than night (spread 26/29/19 against\n 14/15/13) and still looked washed out, because colour without lightness structure reads as fog.\n Stops now sit at L 76.3/74.7/71.8, which also buys chroma headroom, so each takes ~90% of its OWN\n hue's sRGB ceiling rather than one shared number — h292 violet was clipped to 65% at L 84 and is\n fully in gamut here. Composited: spread 48/40/38 (was 26/29/19), separation 8.0 L (was 2.2).\n How far this could go is bound by TEXT, not by taste. AutoForeground models a fresco surface from\n the tint wash (--glass-tint-c/-a), never from this gradient literal, so it does not re-band when\n these stops move and the contrast has to hold statically. --foreground-soft (the L-40 heading tier)\n is the weakest pair on a light surface and sets the floor: at a composite of L 84 it measures 59.5\n and fails, so the stops are placed to land no darker than ~L 86. Worst measured here is 63.5.\n Frosted gets its OWN alphas rather than the old ~1.37x boost over --glass-bg: with stops this much\n darker, that multiplier drove the violet composite to L 83.4 and soft to 58.4. These land all three\n near L 86.4 instead. The authored shape is unchanged — green, then teal, then violet darkest. */\n/* Aurora now sweeps Muse Day's hues — blue 235 → cyan 205 → teal-green 155 — instead of the old\n green → cyan → violet. LIGHTNESS is untouched, because it is not a style choice here: the day stops\n sit at L 71.8–76.3 to satisfy the --foreground-soft ≥60 Lc bind described above, and Muse Day's own\n L 91–94 is precisely the band these were moved away from when day read as haze.\n\n CHROMA had to be re-derived, it could not travel with the hues. Each stop is 90% of that hue's OWN\n sRGB ceiling at its lightness, which is the rule the previous stops already followed (all three\n measured exactly 90%). A flat number cannot survive a hue change: the old 0.175 is 90% of green's\n ceiling at L76.3 but 127% of blue's, so carrying it over would have clipped stop 1 outright.\n\n Night is re-derived the same way, which also fixes a pre-existing clip: its violet stop asked 0.13\n against a 0.1068 ceiling at h292/L80 — 122% — so that stop has been gamut-mapped by the browser all\n along. */\n[data-glass-tint=\"aurora\"] {\n --glass-tint-h: 205; /* the sweep's midpoint, was 178 */\n --glass-tint-c: 0.054;\n --glass-tint-a: 0.16;\n --glass-wash-l: 86%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(76.3% 0.124 235 / 0.4) 0%,\n oklch(74.7% 0.111 205 / 0.38) 50%,\n oklch(71.8% 0.155 155 / 0.38) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(76.3% 0.124 235 / 0.46) 0%,\n oklch(74.7% 0.111 205 / 0.43) 50%,\n oklch(71.8% 0.155 155 / 0.37) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"aurora\"] {\n --glass-bg: linear-gradient(135deg in oklch, oklch(86% 0.071 235 / 0.22) 0%, oklch(85% 0.127 205 / 0.2) 50%, oklch(80% 0.173 155 / 0.22) 100%);\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.071 235 / 0.32) 0%,\n oklch(85% 0.127 205 / 0.3) 50%,\n oklch(80% 0.173 155 / 0.32) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Gloaming — dusk: gold → rose → indigo, interpolated `in oklch` so the warm→cool fade keeps its\n glow. Night keeps the warm glow; day uses a dusk-rose anchor (the warm hue reads muddy as light-mode text). */\n[data-glass-tint=\"gloaming\"] {\n --glass-tint-h: 32; /* surface: mirrors the Gloaming switcher preset; text shares it (--glass-fg-h defaults to it) */\n --glass-tint-c: 0.089;\n --glass-tint-a: 0.16;\n --glass-wash-l: 64%;\n --glass-bg: linear-gradient(135deg in oklch, oklch(89% 0.12 62 / 0.32) 0%, oklch(84% 0.14 350 / 0.3) 50%, oklch(78% 0.13 278 / 0.3) 100%);\n --glass-frosted-bg: linear-gradient(135deg in oklch, oklch(89% 0.12 62 / 0.44) 0%, oklch(84% 0.14 350 / 0.42) 50%, oklch(78% 0.13 278 / 0.42) 100%);\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"gloaming\"] {\n --glass-tint-h: 32; /* night keeps the warm glow; day uses the dusk-rose above */\n --glass-bg: linear-gradient(135deg in oklch, oklch(86% 0.12 62 / 0.22) 0%, oklch(80% 0.14 350 / 0.2) 50%, oklch(74% 0.13 278 / 0.22) 100%);\n --glass-frosted-bg: linear-gradient(135deg in oklch, oklch(86% 0.12 62 / 0.32) 0%, oklch(80% 0.14 350 / 0.3) 50%, oklch(74% 0.13 278 / 0.32) 100%);\n --glass-crystal-fresco: var(--glass-bg);\n}\n", + "content": "/*\n Sistine theme — aggregator. The theme is authored as focused partials under app/theme/; Tailwind v4\n bundles these relative imports itself (no postcss-import needed). ORDER MATTERS:\n - package imports come first (CSS requires @import before other rules),\n - engine.css must precede presets.css / frescoes.css (a preset's --glass-tint-h overrides the\n engine default by SOURCE ORDER at equal specificity),\n - inside tokens.css, .dark comes after :root (same-element ties resolve by source order).\n Consumers install a FLATTENED single-file build of this graph (registry/theme/globals.css, generated\n by scripts/build-theme.mjs) — run `bun run registry:check` after editing any partial and commit\n registry/theme + public/r. Site-only CSS (the Geist font mapping) lives in app/site.css, which imports\n this file, so nothing the documentation site defines leaks into the shipped theme.\n*/\n@import \"tailwindcss\";\n@import \"tw-animate-css\";\n/* ── Registrations — @custom-variant + @property pins for JS-only tokens ──────\n Loaded first so the dark variant and typed properties exist before anything composes them. */\n\n/* Class-based dark mode, v4-standard form: matches the `.dark` element ITSELF and its descendants, so\n consumers can toggle `.dark` on any wrapper (not just / next-themes), at zero specificity\n (`:where`) so `dark:` overrides by cascade order like every other variant. */\n@custom-variant dark (&:where(.dark, .dark *));\n\n/* Composable, NON-inheriting glow layer: the `glow` effect/prop sets it, and every glass box-shadow\n folds in `var(--glow-layer, …)` so the glow rides INSIDE each material's and page style's shadow\n (they set --srf-shadow; the glow is appended after it) instead of being replaced by it. inherits:false\n keeps a glowing surface from lighting up its glass children. */\n@property --glow-layer {\n syntax: \"*\";\n inherits: false;\n}\n\n/* The veil floor, NON-inheriting for the same reason as the glow: `glass-veil` sets it on the element\n and `glass` paints it as that element's background-color. Inherited, a plain glass surface nested in\n a veiled menu, popover, tooltip, select or card painted its PARENT's veil as its own floor (under the\n Opaque page style: flat white in light, black in dark). A child without a veil of its own now falls\n back to its material's --srf-bg-color, as it would anywhere else. */\n@property --veil-floor {\n syntax: \"*\";\n inherits: false;\n}\n/* Its twin for the solidify layer (see @utility glass-veil): a veiled parent must not restore the layer\n on a child that dropped it. */\n@property --veil-solidify {\n syntax: \"*\";\n inherits: false;\n}\n/* The ink border colour (glass-border-ink): an inked panel must not ink the borders of the glass inside it. */\n@property --brd-ink {\n syntax: \"*\";\n inherits: false;\n}\n\n/* Gradient-text tokens are consumed ONLY from JS inline styles (components/gradient-text.tsx) — no CSS\n rule references them, so Lightning CSS's optimizer would silently drop them (the same failure mode as\n the harmonic tokens below). Registered here to pin them. DO NOT REMOVE. */\n@property --gradient-text {\n syntax: \"*\";\n inherits: true;\n}\n@property --gradient-text-contrast {\n syntax: \"*\";\n inherits: true;\n}\n/* ── Harmonic color registrations — compacted one-liners (biome's CSS formatter is excluded for this\n file; see biome.json). WHY these exist: the comment below. */\n\n/* ── Harmonic token registration ──────────────────────────────────────────────────────────────\n The harmonic tokens (--harmony-h, --hue-*, --color-*, --mono-*, defined in engine.css) are\n consumed ONLY from JS / inline styles (HarmonicSwatchesDemo, AutoForeground) — never referenced by\n a CSS rule. Tailwind v4's Lightning CSS optimizer constant-folds + dead-code-eliminates any custom\n property no CSS rule uses, which silently dropped this ENTIRE subgraph from the COMPILED stylesheet\n (so every `oklch(L C var(--hue-*))` / `var(--color-*)` resolved to nothing at runtime). Registering\n them via @property marks them as first-class typed API the optimizer must preserve. DO NOT REMOVE. */\n@property --harmony-h { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --color-chroma { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --mono-chroma { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-base { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-complement { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-analogous-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-analogous-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-split-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-split-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-triad-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-triad-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-tetrad-3 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-1 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-2 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --hue-square-3 { syntax: \"\"; inherits: true; initial-value: 0; }\n@property --color-complement { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-analogous-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-analogous-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-split-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-split-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-triad-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --color-triad-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-1 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-2 { syntax: \"\"; inherits: true; initial-value: transparent; }\n@property --mono-3 { syntax: \"\"; inherits: true; initial-value: transparent; }\n/* ── Design tokens — @theme mapping, base shadcn colors, mode knobs + shadow twins, base layer ─────\n :root carries light values + the LEAF mode knobs the engine composes; .dark carries the dark twins.\n .dark must come after :root (same-element ties resolve by source order on html.dark). */\n\n@theme inline {\n /* Easing. Tailwind's built-ins are deliberately gentle; these are the stronger variants UI motion\n wants, exposed as `ease-out` / `ease-in-out` / `ease-drawer` utilities.\n out — anything ENTERING or EXITING. Starts fast, so the first frame (the one the user is\n actually watching) already moves.\n in-out — things MOVING on screen that neither enter nor leave: carousels, sliders.\n drawer — a slower settle for panels that slide in from an edge, so a large surface arrives with\n weight instead of snapping.\n There is deliberately no `ease-in` token: it delays the first frame, which is the one moment the\n interface most needs to feel responsive. */\n --ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);\n --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);\n /* Anything with a bare `transition` and no `ease-*` inherits this. Tailwind ships\n cubic-bezier(0.4, 0, 0.2, 1) — symmetric, so it eases IN as well, and the first frame barely\n moves. Same curve as --ease-out, written literally because @theme inline doesn't emit the vars\n it inlines. */\n --default-transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1);\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-foreground-soft: var(--foreground-soft);\n --color-foreground-strong: var(--foreground-strong);\n --color-foreground-ui: var(--foreground-ui);\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n --radius: 1.25rem;\n /* Shadow ink — the base black/white that every shadow, pressed-inset, and bevel highlight is built\n from. Mode-invariant (only the ALPHA changes per surface/mode), so apply alpha at the call site:\n oklch(var(--shadow-ink) / ). One knob to retint every shadow at once (e.g. a cooler, less-flat\n ink) instead of hunting down dozens of oklch(0% 0 0) / oklch(100% 0 0) literals. */\n --shadow-ink: 0% 0 0; /* drop + pressed-inset shadow color */\n --shadow-highlight: 100% 0 0; /* inner bevel / top highlight */\n /* Active/pressed shadow SCALE, built from the ink above and mode-aware (deeper in dark; overridden in\n .dark), so components reference these with NO dark: twin. Depth ladder — all inset \"push-in\" presses\n except --active-shadow, which LIFTS the selected tab/toggle. --press-shadow-strong shares the\n standard press's 0.2 primary and stacks a contact layer (was 0.15; raised so the two-layer presses\n build ON the single-layer one instead of undercutting it):\n --press-shadow-sm — subtle press (ghost, outline)\n --press-shadow — standard press (default, gradient, opaque)\n --press-shadow-strong — firm two-layer press (glass, destructive, secondary, frosted)\n --press-shadow-deep — deepest press (crystal)\n --active-shadow — selected-control LIFT (tabs, toggle) */\n --press-shadow-sm: inset 0 1px 2px oklch(var(--shadow-ink) / 0.1);\n --press-shadow: inset 0 2px 4px oklch(var(--shadow-ink) / 0.2);\n --press-shadow-strong: inset 0 2px 4px oklch(var(--shadow-ink) / 0.2), inset 0 1px 2px oklch(var(--shadow-ink) / 0.15);\n --press-shadow-deep: inset 0 2px 4px oklch(var(--shadow-ink) / 0.25), inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --active-shadow: 0 1px 3px oklch(var(--shadow-ink) / 0.1), inset 0 1px 1px oklch(var(--shadow-highlight) / 0.5);\n /* Selected-control FILL — the twin of --active-shadow, which had the lift but no surface, so tabs and\n toggle-group each hardcoded `bg-white/80 dark:bg-white/20`. The colour is composed in engine.css;\n these are its knobs.\n The two modes source their hue by DIFFERENT routes, and that asymmetry is the whole design.\n DARK is structurally theme-driven: the fill is chroma 0 at alpha 0.2, so 100% of the colour is the\n tinted surface showing through — the chip literally IS the theme surface, lifted. Nothing can make\n it drift off-theme.\n LIGHT cannot use that route. Show-through only carries hue if the surface has hue to give, and a\n light glass surface measures around chroma 0.018 (the sheet is itself capped by --glass-tint-c-hi\n at 0.025 near white). So at alpha 0.9 the fill supplies ~94% of the chip's colour and has to carry\n the tint ITSELF.\n Which makes the fill's own lightness the binding constraint, because the near-white ceiling\n collapses hue-dependently — 0.0323 at L 93, 0.061 at L 87, and only 0.0091 by L 98. At the old\n L 93 the cap was 0.031 and the chip delivered an RGB spread of 20: nominally theme-tinted, visibly\n neutral, which is why day did not read as theme-driven while night did. L 87 roughly doubles the\n available chroma and the delivered spread (37), at the cost of sitting 3.6 L BELOW the tab list\n rather than 1.8 above it — the lift is carried by --active-shadow, so the chip still reads\n selected, now as a tinted fill rather than a white one.\n Capped with min() like --border and the chakra body, so selenite (chroma 0) stays exactly neutral\n and no preset can push the fill out of gamut. */\n --active-l: 87%;\n --active-c-max: 0.055; /* ≤ 0.061, the L-87 ceiling floor (lapis h268) */\n --active-a: 0.9;\n /* ── Mode knobs ──────────────────────────────────────────────────────────\n Single-number dials the glass engine's mode-derived block composes (light values here, dark twins in\n .dark). LEAVES ONLY — none composes a tint var (check-theme invariant 1), so they may live on the bare\n blocks and inherit into tint scopes, where the engine re-resolves the derived tokens. */\n --glass-sheet-a: 0.11; /* sheer glass sheet base alpha (per-stop offsets ride on top) */\n --glass-sheet-a1: 0.04; /* the sheet's TOP-stop offset (dark tapers it to 0.03) */\n --glass-frost-boost: 0.1; /* frosted = the sheet lifted by this much */\n --glass-border-a: 0.16; /* glass edge alpha — a white rim needs more presence on light backdrops */\n /* Ink border alpha (borderColor=\"ink\" / glass-border-ink): how much of --foreground the line takes.\n Mode-invariant because --foreground already flips polarity per mode; 0.5 keeps the line clearly\n softer than text while clearing the APCA non-text floor on every preset (check-contrast [ink]). */\n --glass-border-ink-a: 0.5;\n /* Crystal gloss defaults (the demo's Light/Tint sliders override inline). --glass-gloss-tint is\n mode-shared; --glass-gloss-l is NOT — see its twin below. AutoForeground's crystal tiers mirror\n --glass-gloss-l (97 light / 66 dark) as their snapshot-path fallback — keep all three in step. */\n --glass-diffuse: 12px; /* readability blur FLOOR the glass-diffuse axis raises surfaces to (opaque ignores it) */\n /* Gloss ink lightness (\"Light\" in the demo) — a MODE TWIN, because a gloss is defined relative to\n the surface under it and those surfaces are 68 L apart. The crystal floor computes to L 96.5 in\n light and L 28 in dark, so a single shared value cannot be a highlight in both: at 66 the top stop\n lifts dark by +15.2 L (a specular) and darkens light by −12.2 L (a smudge — the same \"grey\n speculars read as grime\" failure the shadow bevel comment warns about).\n 97 is the light twin: composited it lands +0.2 L over the surface, which is all the headroom that\n exists above L 96.5. The cost is a chroma ceiling, and it is strongly HUE-dependent rather than\n uniform — at L 97 it ranges from 0.0686 for peridot (h128) down to 0.0136 for lapis (h268).\n CORRECTION, measured in WebKit: an over-ceiling oklch() is NOT gamut-mapped down to that ceiling.\n It is CLIPPED per channel, which lands FURTHER from the request, not nearer:\n oklch(97% 0.0686 128) -> 232,255,205 (the ceiling — what the model predicts)\n oklch(97% 0.2975 128) -> 185,255,0 (what actually shipped: pure chartreuse)\n So the Tint multiplier did not \"do real work up to each hue's own ceiling\"; above the ceiling it\n drove the ink to a fully saturated primary. Every preset was affected — turquoise and aquamarine\n both clipped to 0,255,255, i.e. the two were indistinguishable — and moonstone reached 255,222,33,\n the egg-yolk cast that made light mode read yellow. --glass-gloss-c-max below is the fix, sized the\n way --glass-chakra-c-max is: the TIGHTEST ceiling across every preset hue, so no preset clips and\n all of them deliver 100% of what they ask for. */\n --glass-gloss-l: 97;\n /* Gloss chroma cap — the L-97 twin. 0.0136 is lapis (h268), the tightest of the 17 presets; 0.013\n sits just under it. Light gloss is therefore near-achromatic by necessity, not by choice: sRGB\n simply holds no more colour that close to white, and asking for more clips rather than tints. */\n --glass-gloss-c-max: 0.013;\n /* Gloss chroma multiplier over the tint (\"Tint\"). Calibrated for the DEFAULT tint (0.018 × 4.25 =\n 0.077, comfortably in gamut); a preset multiplies up from a much higher base, so the product is\n clamped to 0.3 where --glass-gloss-ink composes it — sRGB holds ~0.32 chroma at its very best hue\n and far less at L=66, so beyond that the browser, not the theme, is picking the color. Jewels\n (0.07 × 4.25 = 0.298) sit under the clamp and are unaffected; only the status tints hit it. */\n --glass-gloss-tint: 4.25;\n --glass-wash-l: 72%; /* tint wash lightness — deeper in dark so the tint reads over dark backdrops */\n --glass-wash-c-mult: 2.5; /* tint wash chroma multiplier (moonstone night quiets it to 2) */\n --glass-accent-l: 0.6; /* accent envelope — lifted in dark so the hue pops */\n --glass-accent-c: 0.15;\n --glass-glow-lc: 0.62 0.2; /* glow L+C pair */\n --glass-glow-a: 0.45;\n --harmonic-l: 0.6; /* --color-* accent-tier lightness */\n --mono-l-1: 0.75; /* mono ramp steps (irregular light↔dark lifts — three dials, not one) */\n --mono-l-2: 0.6;\n --mono-l-3: 0.45;\n --gradient-l: 0.6; /* brand-gradient envelope (matches --glass-accent-l/-c) */\n --gradient-c: 0.15;\n --gradient-contrast-l: 0.32; /* contrast gradient text: dark-on-light here, light-on-dark in .dark */\n --gradient-contrast-c: 0.14;\n --glass-crystal-l: 96; /* crystal body lightness — see --glass-crystal-bg; 100 carries zero chroma */\n --glass-crystal-bg-a: 0.3; /* crystal floor alpha (near-clear in dark) */\n /* glass-solid floor — the \"how solid\" dial (0 = sheer, 1 = fully solid; shared by light/dark) + the\n mode floor lightness; composed in @utility glass-veil so a scoped --glass-solid-a resolves there. */\n --glass-solid-a: 0.65;\n --glass-solid-l: 99%;\n /* Opaque tint BODY lightness (plain number → % in the engine). The header L slider drives this; the\n opaque floor color AND AutoForeground's --foreground-opaque banding both read it, so they can't\n drift. LOWER = deeper tint.\n L AND the c-scale below together absorb the retired opaque SHEET (see materials.css). --glass-bg was a\n near-white gradient at ~0.11 alpha, so it did two things to this floor: lightened it AND desaturated\n it. Compensating with lightness alone reproduced only half of that — it left the floor too saturated,\n and pushing L further toward white also SHRINKS the sRGB chroma ceiling, which blew the opaque body\n past check-gamut's 4.6× budget (lapis hit 5.46×). Moving both knobs reproduces both effects and stays\n inside the budget at 4.47×. Fitted across the 14 presets: worst OKLab dE 0.0021, mean 0.0009 — an\n order of magnitude under a just-noticeable difference, so the surface is unchanged to the eye.\n Moonstone pins its own pair (presets.css); its floor is near-white, where the sheet did far less. */\n /* L88, not 90.9. Near-white is where the sRGB chroma ceiling collapses, so an opaque card at 90.9 had\n two problems at once: it read as washed-toward-white, and it could not hold enough chroma to carry\n its own tint (lapis and sapphire were at 97–99% of a ceiling worth only 0.043). Dropping 2.9 L\n roughly doubles every hue's ceiling while keeping body text at Lc 80.0 — the floor is still firmly\n on the light side, so text polarity never changes. */\n --glass-opaque-l: 88;\n /* 0.85, down from 1.26. The old value only looked large: it was immediately clipped to the shared\n 0.042 cap, so every preset rendered the SAME chroma regardless of its tint. Presets now pin their\n own --glass-opaque-c-max (their hue's real ceiling at L88), and 0.85 is the multiplier that lets\n each one reach it. Net effect vs the old flat 0.042: +7% to +88% depending on hue. */\n --glass-opaque-c-scale: 0.85;\n /* Opaque floor chroma cap — the near-white sibling of --glass-tint-c-hi / --glass-chakra-c-max /\n --glass-gloss-c-max, and the last surface that lacked one. Without a cap the floor multiplied raw\n tint chroma this close to white and clipped for most of the palette.\n FALLBACK ONLY: the 12 jewels each pin their own above (their hue's real ceiling at L88); this\n covers the 9 scopes that don't — the 4 frescoes, the 4 status tints and moonstone. Sizing the\n SHARED cap to the tightest hue is the mistake that came before: one number low enough for amber\n (h75, ceiling 0.036 at L88) left peridot at 18% of its own much larger ceiling. So this is\n deliberately not the tightest — per-hue pins are, and scripts/check-gamut.mjs sweeps every scope\n against its real ceiling rather than trusting this number. */\n --glass-opaque-c-max: 0.055;\n /* ── Solidify floor — the opaque backing UNDER sheer glass ───────────────────\n `glass` paints --glass-solidify (this colour at --glass-opacity) as the bottom background-image\n layer of every SHEER material, so it is ~70% of what page/crystal/chakra text actually sits on.\n It used to be --glass-opaque-bg itself, which conflated two different jobs: the colour an OPAQUE\n CARD shows, and the backing that sets sheer text contrast. Opaque cards explicitly drop this layer\n (materials.css, --srf-solidify: none — it would be C over C), so the two were never actually the\n same surface; they only shared a token.\n Splitting them is what lets the sheer floor rise without repainting opaque cards. It is the right\n lever for text contrast because it is the only one with real WEIGHT in the composite (0.7) that is\n not part of a preset's declared identity: --glass-wash-l is the theme COLOUR (moving it changes\n what lapis IS), and --glass-tint-a is already per-mode and desaturates. Measured across all 17\n scopes, L92 is what puts every preset's body text over the 75 floor with the 1.5 Lc margin, with\n no preset's tint tokens touched. DARK needs none of this (it clears everywhere already), so it\n pins both back to the opaque floor below and nothing changes there. */\n --glass-solidify-l: 92;\n /* The floor's chroma ceiling collapses as it lightens, so the L88 caps above would clip here. Rather\n than 17 new per-preset numbers, derive from the ones each preset already declares: across the\n palette the L92/L88 ceiling ratio runs 0.60–0.90, and 0.65 lands every scope INSIDE its real\n ceiling (worst 0.99×, verified by scripts/check-gamut.mjs). Hues that peak light — peridot,\n turquoise, aventurine, amber, aquamarine — are not bound by it at all and keep 100% of their\n surface chroma; the dark-peaking arc keeps 80–86%. */\n --glass-solidify-c-max: calc(var(--glass-opaque-c-max, 0.055) * 0.65);\n /* ── Chakra — the step-cut material (between opaque and crystal) ─────────────\n Every other material is lit from ONE direction: --glass-shadow carries a bright top bevel and a\n dark bottom shade, so surfaces read as a lit dome. Chakra lights all FOUR edges as mitered facets\n (a conic-gradient with hard stops on the diagonals, composed in engine.css), so it reads as a\n step-cut gem instead.\n The facets are pure LUMINANCE overlays — --shadow-highlight / --shadow-ink at these alphas, chroma\n 0 — not tinted colors. Two reasons, and they are the same reason: (1) the theme hue already comes\n through from the tinted body floor beneath, and a translucent white/black over it shifts lightness\n while preserving hue; (2) a bright facet sits near L96 in light mode, where the sRGB chroma ceiling\n across the 17 presets collapses to 0.0182 — a tinted highlight there would be gamut-mapped by the\n browser at nearly every preset. This is the theme's existing rule for the top bevel (\"a specular,\n not a shadow; grey speculars read as grime\"), applied to all four sides.\n The alphas are irregular mode TWINS, not one knob: the same alpha buys a very different lightness\n step over an L88 body than over an L28 one. They are solved so BOTH modes span 18 L end to end —\n light 94.0 > 90.5 > 88.0 > 83.0 > 76.0, dark 38.0 > 32.0 > 28.0 > 24.0 > 20.0 (top, left, table,\n right, bottom). Light comes from the top-left, matching the 135° axis of --glass-bg. */\n --glass-chakra-l: 88; /* body/table lightness */\n /* Body alpha — the \"between crystal and opaque\" dial, and now genuinely that across the WHOLE\n surface: crystal's floor sits at 0.3, opaque at 1. Nothing is painted over the middle any more\n (the cut moved into box-shadow), so this alone decides how much backdrop shows, and glass-diffuse\n / --glass-opacity modulate it the same way they do on crystal. */\n --glass-chakra-a: 0.62;\n --glass-chakra-c-max: 0.055; /* body chroma cap — ≤ 0.0564, the L-88 ceiling floor (lapis h268) */\n --glass-chakra-step: 3px; /* width of ONE bevel step (the frame is two) */\n --glass-chakra-facet-t-a: 0.5; /* top — highlight, +6.0 L */\n --glass-chakra-facet-l-a: 0.208; /* left — highlight, +2.5 L */\n --glass-chakra-facet-r-a: 0.057; /* right — ink, −5.0 L */\n --glass-chakra-facet-b-a: 0.136; /* bottom — ink, −12.0 L */\n /* ── Glass shadows — mode TWINS (not knobs: the light↔dark alpha ratios AND layer structures are\n irregular). They compose only --shadow-ink/-highlight, never tint vars, so bare placement is legal.\n LIGHT twins carry extra definition layers dark mode gets for free (a light surface on a dark page\n separates itself): a tight CONTACT drop + 1px dark outer HAIRLINE define the edge on same-lightness\n backdrops, and a directional BEVEL (bright top inner edge, ink bottom inner edge) makes pills read\n convex instead of printed. The last layer flips family per mode: dark's white inner GLOW (luminous\n glass) becomes an ink VIGNETTE in light — a white wash on a light surface brightens without\n defining, while a faint vignette reads as glass thickness. (Frosted keeps its white glow in both\n modes: milkiness IS that material.) The top bevel stays WHITE in both — it's a specular, not a\n shadow; grey speculars read as grime. Layer order: ambient drop, contact drop, hairline, top\n bevel, bottom shade, inner glow/vignette. */\n --glass-shadow:\n 0 4px 16px oklch(var(--shadow-ink) / 0.13), 0 1px 3px oklch(var(--shadow-ink) / 0.11), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.4) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.08) inset,\n 0 0 12px oklch(var(--shadow-ink) / 0.05) inset;\n --glass-shadow-lg:\n 0 8px 32px oklch(var(--shadow-ink) / 0.16), 0 2px 6px oklch(var(--shadow-ink) / 0.12), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.45) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.09) inset,\n 0 0 20px oklch(var(--shadow-ink) / 0.06) inset;\n --glass-shadow-sm:\n 0 2px 8px oklch(var(--shadow-ink) / 0.1), 0 1px 2px oklch(var(--shadow-ink) / 0.09), 0 0 0 1px oklch(var(--shadow-ink) / 0.04),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.35) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.07) inset,\n 0 0 8px oklch(var(--shadow-ink) / 0.04) inset;\n --glass-frosted-border: oklch(100% 0 0 / 0.3);\n --glass-frosted-shadow:\n 0 8px 32px oklch(var(--shadow-ink) / 0.17), 0 2px 6px oklch(var(--shadow-ink) / 0.13), 0 0 0 1px oklch(var(--shadow-ink) / 0.05),\n 0 1px 0 oklch(var(--shadow-highlight) / 0.5) inset, 0 -1px 0 oklch(var(--shadow-ink) / 0.09) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.18) inset;\n --glass-crystal-shadow: var(--glass-shadow), 0 1px 1px oklch(var(--shadow-highlight) / 0.4) inset;\n --glass-crystal-shadow-hover: var(--glass-shadow-lg), 0 1px 1px oklch(var(--shadow-highlight) / 0.45) inset;\n /* Base colors - Light mode with dark text */\n --background: transparent;\n --foreground: oklch(\n 0.2 0.035 var(--glass-fg-h)\n ); /* CSS baseline (tinted, ~body target) so first paint has no gray→tint flash; AutoForeground refines to the exact APCA solve after */\n /* Size-tiered foregrounds (ARC Bronze Simple Mode bands) — static fallback; AutoForeground re-bands\n these against the live glass surface at runtime. text-foreground-soft / -strong utilities. */\n --foreground-soft: oklch(\n 0.45 0.077 var(--glass-fg-h)\n ); /* large/heading — baseline matched to AutoForeground's median pick; chroma is the LOWEST any preset hue solves to, so no hue clips before JS refines */\n --foreground-strong: oklch(\n 0.2 0.034 var(--glass-fg-h)\n ); /* fine/small — the ramp's tonal clip put this at L20, not L12; the old value flashed dark-then-tinted on every load */\n --foreground-ui: oklch(0.42 0.071 var(--glass-fg-h)); /* icons — AutoForeground re-bands (+ optional hue) */\n /* Static baselines for the OPAQUE + CRYSTAL tier sets (the [data-material] remaps read these) —\n WITHOUT them, a no-JS consumer (or SSR first paint) would substitute an unset var and INHERIT the\n page foreground onto those floors. Light mode: the opaque floor (L90) and crystal surface share\n the page's direction, so the normal literals apply; AutoForeground refines when mounted. */\n --foreground-opaque: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-opaque: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-opaque: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-opaque: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-opaque: oklch(0.14 0.04 var(--glass-fg-h));\n --foreground-crystal: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-crystal: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-crystal: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-crystal: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-crystal: oklch(0.14 0.04 var(--glass-fg-h));\n /* Chakra tier baselines — text sits on the TABLE (the flat centre, L 88), not on the facets, so in\n light mode the direction matches the page and the normal literals apply. AutoForeground re-bands\n against the composited table when mounted. */\n --foreground-chakra: oklch(0.22 0.04 var(--glass-fg-h));\n --foreground-soft-chakra: oklch(0.4 0.04 var(--glass-fg-h));\n --foreground-strong-chakra: oklch(0.12 0.04 var(--glass-fg-h));\n --foreground-ui-chakra: oklch(0.46 0.05 var(--glass-fg-h));\n --muted-foreground-chakra: oklch(0.14 0.04 var(--glass-fg-h));\n --card: transparent;\n --card-foreground: var(--foreground);\n --popover: transparent;\n --popover-foreground: var(--foreground);\n\n /* Primary colors - Light mode */\n --primary-l: 0.42; /* primary ink lightness (dark pill on light pages) — the color itself is composed in the engine, following the theme hue */\n /* Ceiling on the primary ink's chroma. The ×1.2 in the engine is intent — primary should read MORE\n saturated than the base tint — but uncapped it asked past the sRGB edge for five presets in light\n (turquoise, aquamarine, success, warning, aurora) and two in dark (lapis, info), where the browser\n gamut-maps and the delivered colour stops tracking the authored one.\n Measured against --glass-fg-h, not --glass-tint-h: --primary follows the FOREGROUND hue, and the\n Muse/Gloaming frescoes decouple the two (fg 290 / 18), so those hues are in the sweep — aurora only\n shows up as out-of-gamut once you check the fg hue. */\n --primary-c-max: 0.069; /* ≤ 0.0697, the L-42 ceiling floor — h205 (aurora) is now tighter than h190 */\n --primary-foreground: oklch(0.98 0 0); /* White text on the primary ink — Lc 88.1 at the worst hue, turquoise (measured) */\n\n /* Secondary colors - Light mode */\n /* ── Flat-neutral shadcn SURFACES, now theme-tinted ──────────────────────────\n --accent, --secondary, --muted and --sidebar were the last chroma-0 tokens in the theme, which is\n why a toggle's on-state read as near-white in light and near-black in dark no matter which preset\n was active: the fill simply had no hue to give. Same treatment as --border — lightness stays put\n (pure recolor), the colour moves to engine.css so it re-resolves per tint scope, and chroma is\n min(the preset's own, a cap) so a preset that declares less keeps its restraint and selenite stays\n exactly neutral.\n Each cap is measured against the tightest sRGB ceiling across all 17 presets AT THAT LIGHTNESS, and\n they differ by a factor of eight because the ceiling collapses toward white: 0.0714 at L 85, but\n only 0.0091 at L 98. One shared cap would have to be the smallest, tinting nothing. */\n --secondary-l: 0.96;\n --secondary-c-max: 0.018; /* ≤ 0.0182, the L-96 ceiling floor (lapis h268) */\n --secondary-foreground: var(--foreground);\n\n /* Muted colors - Light mode */\n --muted-l: 0.85;\n --muted-c-max: 0.071; /* ≤ 0.0714, the L-85 ceiling floor (lapis h268) */\n --muted-foreground: oklch(\n 0.35 0.06 var(--glass-fg-h)\n ); /* Level 1 — L35, not L14: the widest gap of the set, and the one doing most of the first-paint flash */\n\n /* Accent colors - Light mode. NOTE the knob prefix: --accent-c / --accent-h are already taken as the\n user-facing overrides for --glass-accent (the vivid switch/slider fill), a different role. */\n /* L93, down from 0.96, and the cap raised with it. The hover fill was reading as plain white: at L96 the\n sRGB gamut allows a chroma of only 0.0182, an RGB spread of 19, so \"tinted\" was arithmetically\n invisible. Chroma near the extremes is not a free parameter — it is whatever the gamut leaves — so the\n only way to a visible tint is to step off the extreme. Three points of lightness buys 1.7x the spread\n (19 -> 32) and moves the fill DARKER than the surface, the direction a highlight should go on a light\n page. It cannot go much further: --accent is also painted at 50% (bg-accent/50 on calendar's \"today\",\n button's active state, ToggleGroup hover), and that composite over an L88 card is what binds — at L92\n it drops to 76.0 Lc, under the floor+margin, while L93 holds 76.9. */\n --accent-bg-l: 0.93;\n --accent-bg-c-max: 0.032; /* ≤ 0.0323, the L-93 ceiling floor (lapis h268) */\n /* Stays --foreground: this token is text on a SURFACE-lightness background. Its callers all have that\n shape — the ghost button's hover, toggles, and the highlighted row of every menu (dropdown, context,\n navigation, command, menu-bar, select), which paint bg-accent so a consumer's --accent reaches them\n (the shadcn contract). Text and fill only stay legible together while --accent sits near the\n surface, which is precisely why the fill was moved by chroma rather than by weight. */\n --accent-foreground: var(--foreground);\n\n /* Destructive colors - WCAG AA compliant (4.5:1 contrast) */\n --destructive: oklch(0.55 0.22 25); /* Red - 4.5:1 with white */\n --destructive-foreground: oklch(0.98 0 0); /* White text on red */\n\n /* Border / input LIGHTNESS + chroma-CAP knobs — the COLOR is composed in engine.css so the edge\n follows the theme tint like every other surface token (leaves, so they inherit into tint scopes\n where the engine re-resolves).\n L 0.85, not the 0.88 the flat neutral carried: sRGB's chroma ceiling collapses as lightness climbs,\n and at 0.88 the binding ceiling across all 17 presets is 0.0564 — the most colorful in-gamut edge\n possible there still reads as grey (mean RGB spread 51). Dropping 3 points of L raises the ceiling\n to 0.0714 and nearly doubles delivered colorfulness (mean spread 65) while moving the grey\n reference only #d7d7d7 → #cecece, which is below the threshold of a perceived weight change.\n --border-c-max is the CAP, not the value: chroma is min(tint chroma, cap), so a preset that\n declares less than the cap keeps its own restraint — selenite (c 0) stays exactly neutral, which is\n the whole point of that theme — while lapis (c 0.15) clamps instead of being gamut-mapped by the\n browser. A flat multiplier cannot do both: any factor small enough to keep lapis in gamut drags the\n 0.07 jewels below where they are today. */\n --border-l: 0.85;\n --input-l: 0.85;\n --border-c-max: 0.07; /* ≤ 0.0714, the L-0.85 ceiling floor (lapis h268) */\n --ring: oklch(0.5 0.2 250); /* Focus ring */\n\n /* Chart colors - WCAG AA compliant */\n --chart-1: oklch(0.6 0.2 250); /* Blue */\n --chart-2: oklch(0.55 0.15 150); /* Green */\n --chart-3: oklch(0.5 0.2 50); /* Yellow */\n --chart-4: oklch(0.65 0.2 300); /* Purple */\n --chart-5: oklch(0.6 0.2 20); /* Orange */\n\n /* Sidebar colors - Light mode */\n --sidebar-l: 0.98;\n --sidebar-c-max: 0.009; /* ≤ 0.0091, the L-98 ceiling floor — near white holds almost no hue */\n --sidebar-foreground: var(--foreground);\n --sidebar-primary: oklch(0.5 0.2 250); /* Blue */\n --sidebar-primary-foreground: oklch(0.98 0 0); /* White text on primary */\n --sidebar-accent: oklch(0.15 0 0 / 0.1); /* Subtle glass overlay (≈ foreground/10), not a solid block */\n --sidebar-accent-foreground: var(--foreground);\n --sidebar-border: var(--border); /* Same divider role, same value it already carried — follow the role token */\n --sidebar-ring: oklch(0.5 0.2 250); /* Blue ring - 3:1 contrast */\n}\n\n.dark {\n /* Base colors — light text on a dark ground */\n --background: transparent;\n /* Pressed/active shadow SCALE — dark overrides: primaries deepen +0.1, contact layers +0.05, so the\n push-in still reads on dark surfaces (see :root for the light ladder + rationale). */\n --press-shadow-sm: inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --press-shadow: inset 0 2px 4px oklch(var(--shadow-ink) / 0.3);\n --press-shadow-strong: inset 0 2px 4px oklch(var(--shadow-ink) / 0.3), inset 0 1px 2px oklch(var(--shadow-ink) / 0.2);\n --press-shadow-deep: inset 0 2px 4px oklch(var(--shadow-ink) / 0.35), inset 0 1px 2px oklch(var(--shadow-ink) / 0.25);\n --active-shadow:\n 0 2px 6px oklch(var(--shadow-ink) / 0.4), inset 0 1px 2px oklch(var(--shadow-highlight) / 0.15),\n inset 0 0 8px oklch(var(--shadow-highlight) / 0.1);\n /* Selected-control fill — dark twin. Deliberately UNCHANGED in behaviour from the `bg-white/20` it\n replaces: chroma cap 0 makes min() resolve to 0, so this is a pure white lift and the hue arrives\n by show-through from the surface below, which is exactly why dark already read correctly. Light\n has to source its hue directly (see :root); the two modes reach the same result by different\n routes because the near-white gamut only permits one of them. */\n --active-l: 100%;\n --active-c-max: 0;\n --active-a: 0.2;\n /* ── Mode knobs — dark twins (see :root for what each dial does). */\n --glass-sheet-a: 0.05;\n --glass-sheet-a1: 0.03;\n --glass-frost-boost: 0.06;\n --glass-border-a: 0.15;\n --glass-wash-l: 58%;\n --glass-accent-l: 0.68;\n --glass-accent-c: 0.14;\n --glass-glow-lc: 0.7 0.18;\n --glass-glow-a: 0.5;\n --harmonic-l: 0.68;\n --mono-l-1: 0.82;\n --mono-l-2: 0.68;\n --mono-l-3: 0.5;\n --gradient-l: 0.68;\n --gradient-c: 0.14;\n --gradient-contrast-l: 0.9;\n --gradient-contrast-c: 0.09;\n --glass-crystal-l: 96; /* shared with light: the ceiling that forces 96 is hue-driven, not mode-driven */\n --glass-crystal-bg-a: 0.1;\n /* Gloss ink lightness — dark twin. Over an L-28 floor there is 72 L of headroom, so 66 lifts +15.2\n and still holds 59% of the tonal ink's chroma: a specular that is genuinely tinted. Light cannot\n have both (see :root). */\n --glass-gloss-l: 66;\n /* Gloss chroma cap — the L-66 twin. Far more headroom this far from white: 0.1094 is aurora (h205),\n the tightest across all 23 preset scopes — the FRESCOES count here too, which is why this is not\n turquoise's 0.1106. Dark gloss keeps real colour; it was still clipping before (turquoise\n 0,193,186 and carnelian 255,0,0 both had a pinned channel). */\n --glass-gloss-c-max: 0.109;\n /* L lifted from 22% (less extreme light↔dark switch); the opaque chroma scale (0.9) keeps warm hues\n like amber in-gamut instead of hard-clipping to rust. */\n --glass-solid-l: 18%;\n /* The retired sheet's compensation, dark twin (see :root). Much the larger move of the two modes: a\n near-white sheet over a DARK floor lifts it ~12 codes where over the light floor it lifts ~2–5. Same\n two-knob fit, worst OKLab dE 0.0045 across the 14 presets. */\n --glass-opaque-l: 36.4;\n /* 1.05, up from 0.85. The dark floor was never cap-limited — every preset sat at 31–79% of its own\n ceiling, so the MULTIPLIER was the throttle and dark opaque cards lost their tint. 1.05 is the most\n the tightest hue (turquoise) can take without clipping; it lifts every preset +24% at Lc 80.0. */\n --glass-opaque-c-scale: 1.05;\n /* Opaque floor chroma cap, dark twin (see :root). A GUARD, not a throttle — the distinction matters\n and this value used to get it wrong. It was 0.06, the tightest ceiling across the preset scopes,\n which was correct while every jewel shared --glass-tint-c: 0.07 and the floor multiplied it blind.\n Once each preset tuned its own chroma to its own hue, that shared floor-of-the-tightest started\n costing the hues WITH headroom: at 0.06 amethyst lost 30% of its ask, tourmaline 33%, rose 26% —\n against own ceilings of 0.189, 0.152 and 0.142. Measured, every preset's dark ask now sits at\n 0.31–0.79× of its own ceiling, so nothing clips with no cap at all. 0.095 clears the largest ask\n (tourmaline 0.0901) and therefore throttles nobody, while still bounding a future preset that\n pushes chroma far past today's values. check-gamut scores each preset against its own ceiling. */\n /* Guard, not a throttle: the largest dark ask is tourmaline at 0.111, so this binds nobody. Declared\n in its own rule right after this block (see there for why). */\n /* Dark keeps the solidify floor IDENTICAL to the opaque floor — the split exists to buy LIGHT-mode\n text contrast, and dark already clears the body floor on every preset with margin. Pinning to the\n opaque values (rather than leaving :root's L92 / ×0.65 to inherit) is what makes this a light-only\n change: a lighter backing under dark glass would wash the whole mode out. */\n --glass-solidify-l: var(--glass-opaque-l);\n --glass-solidify-c-max: var(--glass-opaque-c-max);\n /* ── Chakra dark twins (see :root for what each dial does + how the alphas were solved).\n The facet alphas invert in character: over an L28 body there is 72 L of headroom UP and only 28\n DOWN, so the highlights need far less alpha and the inks far more than their light-mode twins to\n land the same 18 L span. The targets also skew brighter here (+10/−8 rather than +6/−12) — a dark\n gem reads as cut by catching light, where a light one reads by casting shade. */\n --glass-chakra-l: 28;\n --glass-chakra-a: 0.58;\n --glass-chakra-c-max: 0.046; /* ≤ 0.0471, the L-28 ceiling floor (turquoise h190) */\n --glass-chakra-facet-t-a: 0.139; /* top — highlight, +10.0 L */\n --glass-chakra-facet-l-a: 0.056; /* left — highlight, +4.0 L */\n --glass-chakra-facet-r-a: 0.143; /* right — ink, −4.0 L */\n --glass-chakra-facet-b-a: 0.286; /* bottom — ink, −8.0 L */\n /* ── Glass shadows — dark twins (deeper drops, brighter bevels). */\n --glass-shadow:\n 0 4px 16px oklch(var(--shadow-ink) / 0.3), 0 1px 4px oklch(var(--shadow-ink) / 0.15), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.12) inset,\n 0 0 12px oklch(var(--shadow-highlight) / 0.15) inset;\n --glass-shadow-lg:\n 0 8px 32px oklch(var(--shadow-ink) / 0.4), 0 2px 8px oklch(var(--shadow-ink) / 0.2), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.15) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.18) inset;\n --glass-shadow-sm:\n 0 2px 8px oklch(var(--shadow-ink) / 0.25), 0 1px 2px oklch(var(--shadow-ink) / 0.12), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.1) inset,\n 0 0 8px oklch(var(--shadow-highlight) / 0.12) inset;\n --glass-frosted-border: oklch(100% 0 0 / 0.25);\n --glass-frosted-shadow:\n 0 8px 32px oklch(var(--shadow-ink) / 0.5), 0 2px 8px oklch(var(--shadow-ink) / 0.3), 0 0 0 0.5px oklch(var(--shadow-highlight) / 0.2) inset,\n 0 0 20px oklch(var(--shadow-highlight) / 0.22) inset;\n --glass-crystal-shadow: var(--glass-shadow), 0 1px 1px oklch(var(--shadow-highlight) / 0.2) inset;\n --glass-crystal-shadow-hover: var(--glass-shadow-lg), 0 1px 1px oklch(var(--shadow-highlight) / 0.25) inset;\n --foreground: oklch(\n 0.95 0.03 var(--glass-fg-h)\n ); /* CSS baseline (tinted, ~body target) so first paint has no gray→tint flash; AutoForeground refines to the exact APCA solve after */\n /* Size-tiered foregrounds (ARC Bronze Simple Mode bands) — static fallback; AutoForeground re-bands\n these against the live glass surface at runtime. text-foreground-soft / -strong utilities. */\n --foreground-soft: oklch(0.83 0.065 var(--glass-fg-h)); /* large/heading — refined at runtime */\n --foreground-strong: oklch(0.97 0.015 var(--glass-fg-h)); /* fine/small — refined at runtime */\n --foreground-ui: oklch(0.87 0.057 var(--glass-fg-h)); /* icons — AutoForeground re-bands (+ optional hue) */\n /* Static OPAQUE + CRYSTAL tier baselines, dark twins — the standard dark opaque floor (L32) and\n crystal surface keep the page's light-text direction. Presets whose dark floors flip LIGHT (moonstone\n cream) override the -opaque set in their own block; AutoForeground refines when mounted. */\n --foreground-opaque: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-opaque: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-opaque: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-opaque: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-opaque: oklch(0.89 0.04 var(--glass-fg-h));\n --foreground-crystal: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-crystal: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-crystal: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-crystal: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-crystal: oklch(0.89 0.04 var(--glass-fg-h));\n /* Chakra tier baselines, dark twins — the L-28 table keeps the page's light-text direction. */\n --foreground-chakra: oklch(0.92 0.04 var(--glass-fg-h));\n --foreground-soft-chakra: oklch(0.78 0.04 var(--glass-fg-h));\n --foreground-strong-chakra: oklch(0.96 0.035 var(--glass-fg-h));\n --foreground-ui-chakra: oklch(0.75 0.05 var(--glass-fg-h));\n --muted-foreground-chakra: oklch(0.89 0.04 var(--glass-fg-h));\n --card: transparent;\n --card-foreground: var(--foreground);\n --popover: transparent;\n --popover-foreground: var(--foreground);\n\n /* Primary colors — dark mode */\n --primary-l: 0.78; /* primary ink lightness in dark mode (light pill) — color composed in the engine */\n --primary-c-max: 0.107; /* ≤ 0.1077, the L-78 ceiling floor (h268 lapis) */\n /* Near-black text on the primary ink — Lc 62.9 at the worst hue, sistine (measured). It was 60.9\n before --primary-c-max: capping the chroma the browser used to gamut-map moved the worst case back\n above the 62 this comment had been claiming. */\n --primary-foreground: oklch(0.15 0 0);\n\n /* Secondary colors — dark mode */\n /* Flat-neutral surfaces — dark twins (see :root for why these are knobs now). Lightness unchanged;\n the caps are the dark ceilings, which are tightest at the DARKEST step, not the lightest. */\n --secondary-l: 0.25;\n --secondary-c-max: 0.041; /* ≤ 0.0417, the L-25 ceiling floor — h205 (aurora) is now tighter than h190 */\n --secondary-foreground: var(--foreground);\n\n /* Muted colors — dark mode */\n --muted-l: 0.18;\n --muted-c-max: 0.03; /* ≤ 0.0307, the L-18 ceiling floor (turquoise h190) */\n --muted-foreground: oklch(0.9 0.031 var(--glass-fg-h)); /* Level 1 — one ramp step toward the chosen color */\n\n /* Accent colors — dark mode */\n /* L34, up from 0.25 — the same fix as light, mirrored. At L25 the ceiling was 0.0417 (spread 29), so the\n fill read as near-black; L34 allows 0.0565 (spread 42) and moves it BRIGHTER than the surface, which is\n the direction a highlight goes on a dark page. Dark has more room than light here (its 50% composite\n sits at 77.5 Lc rather than 76.0), because the fill and the L36.4 card floor are close together. */\n --accent-bg-l: 0.34;\n --accent-bg-c-max: 0.056; /* ≤ 0.0565, the L-34 ceiling floor (aurora h205) */\n --accent-foreground: var(--foreground);\n\n /* Destructive colors - WCAG AA compliant (4.5:1 contrast) */\n --destructive: oklch(0.65 0.22 25); /* Light red - 4.5:1 with dark background */\n --destructive-foreground: oklch(1 0 0); /* White text on red — APCA Lc ~67 (near-black was Lc 42) */\n\n /* Border / input lightness + chroma-cap — dark twins (see :root; color composed in engine.css).\n L is UNCHANGED here: in dark mode the edge sits at 0.30/0.25 on a dark page, so lowering it would\n cost contrast rather than buy color. Only the cap moves, up to the tightest dark ceiling — 0.0422\n at L 0.25 (turquoise h190), which --input hits first, so one cap serves both. */\n --border-l: 0.3;\n --input-l: 0.25;\n --border-c-max: 0.041; /* ≤ 0.0422, the L-0.25 ceiling floor (turquoise h190) */\n --ring: oklch(0.6 0.2 250); /* Focus ring */\n\n /* Chart colors - WCAG AA compliant */\n --chart-1: oklch(0.65 0.2 250); /* Light blue */\n --chart-2: oklch(0.7 0.15 150); /* Light green */\n --chart-3: oklch(0.75 0.2 50); /* Light yellow */\n --chart-4: oklch(0.7 0.2 300); /* Light purple */\n --chart-5: oklch(0.68 0.2 20); /* Light orange */\n\n /* Sidebar colors — dark mode */\n --sidebar-l: 0.18;\n --sidebar-c-max: 0.03; /* ≤ 0.0307, the L-18 ceiling floor (turquoise h190) */\n --sidebar-foreground: var(--foreground);\n --sidebar-primary: oklch(0.56 0.16 250); /* Match --primary; white text clears APCA */\n --sidebar-primary-foreground: oklch(1 0 0); /* White on the sidebar's brand fill */\n --sidebar-accent: oklch(1 0 0 / 0.1); /* Subtle glass overlay (≈ foreground/10), not a solid block */\n --sidebar-accent-foreground: var(--foreground);\n --sidebar-border: var(--border); /* Same divider role, same value it already carried — follow the role token */\n --sidebar-ring: oklch(0.6 0.2 250); /* Light blue ring - 3:1 contrast */\n}\n\n/* The dark opaque-floor chroma guard, in its own rule so it can OUTRANK the presets. Each jewel pins\n --glass-opaque-c-max to its hue's sRGB ceiling at the LIGHT floor (L88, presets.css), in an unscoped\n block. On that block and `.dark` tie at (0,1,0) and presets.css loads later, so the light caps\n used to win in dark too: amethyst's dark floor rendered C 0.065 against an ask of 0.106 and a ceiling\n of 0.189, lapis 0.055 against 0.236, and every blue/purple/red jewel's dark opaque card came out a dull,\n greyed version of its tint (23–53% of the chroma its hue can hold at L36.4). The light caps describe\n the light floor only; at L36.4 every preset's ask sits well inside its own ceiling, so this guard is\n the only cap dark needs. (0,2,0) beats a preset block; `.dark [data-glass-tint]` covers a scoped tint\n inside a dark page, and a preset's own `.dark[data-glass-tint=\"x\"]` block still wins by source order. */\n.dark,\n.dark[data-glass-tint],\n.dark [data-glass-tint] {\n --glass-opaque-c-max: 0.12;\n}\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in oklch, var(--foreground) 22%, transparent) transparent;\n }\n body {\n /* Text uses the band-picked --foreground (set on by AutoForeground on theme/tint change;\n globals carries a static fallback so there's no flash). */\n color: var(--foreground);\n background-color: transparent;\n }\n /* Thin, glass-friendly scrollbars (replaces the chunky native bar) */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: color-mix(in oklch, var(--foreground) 22%, transparent);\n border-radius: 9999px;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: color-mix(in oklch, var(--foreground) 38%, transparent);\n }\n}\n\n/* ── Below the floor ──────────────────────────────────────────────────────────\n The supported floor is Safari 17.5 / Chrome 120 / Firefox 128 / Edge 120, declared as the browserslist\n in SISTINE's package.json. It is set by what this theme actually uses, not by preference — the binding\n constraints are text-wrap:balance (Safari 17.5), bare-`&` CSS nesting and unprefixed mask-composite\n (Chrome 120), and @property (Firefox 128). Tailwind v4's own baseline sits just under it. That\n browserslist does not ship with this file: a consuming app should declare its own, at or above the\n floor, or its toolchain targets whatever its defaults are.\n\n Every colour in this theme is oklch(), fed through custom properties. On an engine without oklch the\n custom property still PARSES (custom props accept anything) and only fails when substituted — so\n `background-color: var(--color-background)` resolves to an invalid value and computes to `unset`,\n i.e. transparent, rather than to a fallback. Surfaces disappear; text usually survives on the UA\n default. This is not a polyfill and does not try to reproduce the palette — it guarantees a legible\n page instead of transparent panels on stacked backgrounds.\n\n UNVERIFIED IN A REAL ENGINE: no browser in the test matrix lacks oklch, so this branch has only been\n confirmed to compile, not to render. Treat it as a floor, not a tested path. */\n@supports not (color: oklch(0% 0 0)) {\n :root {\n color-scheme: light;\n background-color: #f7f7fb;\n color: #17161c;\n }\n body {\n background-color: #f7f7fb;\n color: #17161c;\n }\n .glass,\n [data-surface],\n [data-material] {\n background-color: #ffffffe6;\n border: 1px solid #17161c26;\n backdrop-filter: none;\n }\n}\n/* ── Glass engine ─────────────────────────────────────────────────────────────\n THREE grouped blocks, split by what must re-resolve where (the var-composition-resolves-at-declaration\n gotcha — see scripts/check-theme.mjs):\n\n A. TINT ANCHORS (:root, [data-glass-tint], …) — the tint knobs plus everything mode-INVARIANT that\n composes them, declared on :root AND every tint scope so a scoped data-glass-tint re-resolves the\n whole subgraph. Deliberately NOT on `.dark`: re-declaring --glass-tint-h / --glass-fg-h at a bare\n `.dark` wrapper would reset a page tint inherited from above.\n\n B. MODE-DERIVED SURFACES (:root, .dark, [data-glass-tint], …) — every tint-composing token whose\n light/dark difference is a handful of numbers. Each is declared ONCE, composing the mode knobs\n (--glass-sheet-a, --glass-wash-l, … — see :root/.dark) which are LEAVES that inherit into tint\n scopes. `.dark` IS in this selector so a dark wrapper inside a light page re-composes its surfaces\n from the wrapper's own dark knobs (same-element cascade) — \"toggle .dark anywhere\" keeps working.\n\n C. GLOSS (:root, .dark, [data-glass-tint], [data-gloss]) — the crystal gloss and the stacks that bake\n it, which also have to recompose wherever a `data-gloss` flavor is set. See block C itself.\n\n Foreground tokens stay on bare :root/.dark (AutoForeground owns them). Preset blocks below override\n --glass-tint-h/c/a by source order. Shadow composites are mode-TWINNED on bare :root/.dark (their\n light↔dark ratios are irregular — knobbing them would change values). */\n:root,\n[data-glass-tint] {\n /* Deliberately restrained glassmorphism — enough to read as glass, never enough to fog what is behind it. */\n /* Glass tint — recolor ALL glass by changing these; presets set them via [data-glass-tint].\n -h = hue, -c = chroma (gradient), -a = wash alpha. The gradient alone is too sheer\n (~6–12% opacity) to show a hue, so -a paints a solid colored floor (--glass-tint-wash)\n that reads over any backdrop. Neutral keeps -a:0, so the default glass is unchanged. */\n --glass-tint-h: 250;\n --glass-tint-c: 0.018;\n --glass-tint-a: 0;\n /* The same chroma, capped for NEAR-WHITE surfaces. The sheet stops, the borders and the crystal\n floor sit at L 95–100%, where sRGB's chroma ceiling collapses AND becomes strongly hue-dependent:\n at L=97% it is ~0.071 for peridot (h128) but ~0.014 for lapis (h268). Feeding those the raw\n --glass-tint-c means each preset lands on whatever fraction of its declared value its hue happens\n to permit — lapis was delivering 9% of its 0.15 where peridot delivered 98% of its 0.07 — and the\n shortfall is then resolved by the browser's gamut mapping rather than by us. That is the same\n class of bug the 3% margin in lib/oklch-utils guards against on the computed path (\"foregrounds\n went grey in Safari while staying tinted in Chrome\"); this is its CSS-side counterpart.\n Capping the BASE (rather than lowering any preset) keeps chroma where it has headroom — a jewel's\n depth lives at mid lightness, in --primary and --glass-tint-wash, which are untouched by this.\n Guarded by scripts/check-gamut.mjs.\n 0.025 was still too generous, because the sheet MULTIPLIES this cap: its stops are\n calc(--glass-tint-c-hi * 0.3…1.3), so the top stop asked 0.0325 against an L-95 ceiling of 0.0229\n at the tightest hue — the cap itself clipped. 0.017 is that ceiling divided by the 1.3 multiplier,\n so the sheet now lands inside the gamut at every preset hue instead of at whatever the browser\n chops it to. Cap-then-multiply is the order that matters here; see check-gamut's SCALE/CAP/BOTH\n shapes, which model each form separately for exactly this reason. */\n --glass-tint-c-hi: min(var(--glass-tint-c), 0.017);\n /* Foreground hue — drives every FOREGROUND-keyed token (body/heading text via AutoForeground,\n --muted-foreground, --glass-accent, --glass-glow, --gradient*), kept SEPARATE from the surface\n hue so tuning text never shifts the glass/fresco surface. Defaults to the tint hue (jewels: text\n == surface); frescoes override it to anchor text off their multi-hue surface (Muse night: surface\n 222, text 310). AutoForeground reads this, falling back to --glass-tint-h for older consumers. */\n --glass-fg-h: var(--glass-tint-h);\n /* ── Harmonic color system ────────────────────────────────────────────────────────────────────\n Color-wheel relationships derived from the CONTENT hue (--glass-fg-h) — the same hue --glass-accent\n and --gradient track — so they rotate with the theme/tint automatically (a generalization of the\n --gradient's +63.53° offset). Hues are mode-invariant ANGLES; CSS Color 4 wraps them past 360°, so use\n anywhere as oklch(L C var(--hue-*)); choosing your own L/C also keeps them in-gamut. The ready --color-*\n tokens sit at the --glass-accent envelope (0.6/0.15; .dark lifts them). NOTE: a fixed-chroma harmonic can\n land at the sRGB edge at some hues and get browser-gamut-mapped — prefer the hue tokens + a conservative\n chroma when that matters. */\n /* Harmonic anchor hue — the wheel origin the harmonies rotate from. Normally the content hue, but the two\n \"hue-less\" themes (selenite + moonstone) anchor at 0° instead — each preset block in presets.css declares\n it, so a CSS-only consumer gets the anchor too (the tint switcher also inlines it, for a custom tint\n dragged to chroma 0) — and their accents come out a colorful red-based harmony, not a warp of a\n near-grey hue. */\n --harmony-h: var(--glass-fg-h);\n --hue-base: var(--harmony-h);\n --hue-complement: calc(var(--harmony-h) + 180);\n --hue-analogous-1: calc(var(--harmony-h) - 30);\n --hue-analogous-2: calc(var(--harmony-h) + 30);\n --hue-split-1: calc(var(--harmony-h) + 150);\n --hue-split-2: calc(var(--harmony-h) + 210);\n --hue-triad-1: calc(var(--harmony-h) + 120);\n --hue-triad-2: calc(var(--harmony-h) + 240);\n --hue-tetrad-1: calc(var(--harmony-h) + 60); /* rectangle: base, +60, +180, +240 */\n --hue-tetrad-2: calc(var(--harmony-h) + 180);\n --hue-tetrad-3: calc(var(--harmony-h) + 240);\n --hue-square-1: calc(var(--harmony-h) + 90); /* square: base, +90, +180, +270 */\n --hue-square-2: calc(var(--harmony-h) + 180);\n --hue-square-3: calc(var(--harmony-h) + 270);\n /* Harmonic chroma — a fixed vivid accent chroma so the palette stays colorful in EVERY theme (selenite +\n moonstone included; they get their color from the hue-0 anchor above, not from chroma). --mono-chroma is\n quieter (0.6×) for the subtle mono ramp. */\n --color-chroma: 0.15;\n --mono-chroma: calc(var(--color-chroma) * 0.6);\n /* (--color-* / --mono-* ready colors live in the mode-derived surfaces block below — their L is the\n mode envelope; the hue angles above are the mode-invariant part.) */\n /* (The solidify floor — the --glass-opacity \"component opacity\" dial — is composed INSIDE each sheer\n glass utility, at the element. Composing it here would bake :root's --glass-opacity into the value\n and silently ignore a per-element glass={{opacity}} — the var-composition gotcha again.) */\n /* Blur values — moderate by design; per-variant sizes (mode-invariant). */\n --blur: 2px;\n --blur-sm: 1px;\n --blur-lg: 8px;\n --blur-xl: 12px;\n --blur-frosted: 25px;\n /* Chakra reads as a THICK stone, so it diffuses more than crystal's near-clear 2px even though its\n body is more solid — the two axes are independent (how much light passes vs how scattered it is). */\n --blur-chakra: 6px;\n /* Crystal edge — identical in both modes, so it lives here with the anchors (the crystal floor +\n shadows are mode-varying: --glass-crystal-bg in the surfaces block below, shadows on :root/.dark). */\n --glass-crystal-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / 0.3);\n /* Crystal gloss KNOBS — leaves only. The gloss itself (ink, the three gradients) and every stack that\n bakes it are composed in block C below, which also recomposes at each [data-gloss] scope.\n --glass-gloss-l / --glass-gloss-tint live in tokens.css (66 / 4.25) — do NOT re-declare here:\n engine.css imports AFTER tokens.css, so a duplicate at equal :root specificity silently wins and\n pure-CSS consumers get a stale gloss. */\n --glass-gloss-hue-span: 40; /* hue: degrees the iridescent sweep shifts up & down from the tint hue */\n /* hue: chroma of the iridescent stops, at the flavor's own L 74. Was 0.16, chosen believing an\n over-ceiling request is gamut-MAPPED down to the ceiling (\"holds 81% of that chroma\"). WebKit clips\n per channel instead (see --glass-gloss-c-max in tokens.css), so 0.16 pinned a channel on the\n tightest hues rather than delivering 81% of itself. 0.12 sits just under 0.1226 — aurora (h205) at\n L 74, the tightest across all 23 preset scopes (frescoes included) — so every hue in the sweep now\n renders what it asks for. */\n --glass-gloss-c: 0.12;\n --glass-gloss-hue-dir: 1; /* gloss direction (±1) — flips the TONAL streak diagonal (calc angle) + the HUE sweep order (span × dir); toggled in the crystal demo */\n --blur-crystal: 2px;\n}\n\n/* B. MODE-DERIVED SURFACES — see the engine header above. Each tint-composing token is declared ONCE,\n parameterized by the mode knobs on bare :root/.dark; `.dark` in this selector keeps dark wrappers\n re-composing with their own knobs. */\n:root,\n.dark,\n[data-glass-tint] {\n /* Solid colored floor under the sheer gradient (see --glass-tint-a) — mode via --glass-wash-l/-c-mult. */\n --glass-tint-wash: oklch(var(--glass-wash-l) calc(var(--glass-tint-c) * var(--glass-wash-c-mult)) var(--glass-tint-h) / var(--glass-tint-a));\n /* Solid accent that tracks the FOREGROUND hue at a fixed vivid chroma — used by active controls\n (switch \"on\", slider range). The --accent-h/-c knob overrides apply in BOTH modes (the old dark\n twin dropped them — that was a bug). */\n --glass-accent: oklch(var(--glass-accent-l) var(--accent-c, var(--glass-accent-c)) var(--accent-h, var(--glass-fg-h)));\n /* Stained-glass — grayscale collapses the backdrop to pure LUMINANCE (akin to alpha: only lighter/\n darker survives), and the glass's OWN tint stack supplies the color — OKLCH-true to the theme, no\n hue-rotate approximation. --glass-stain-amount (0..1) sets how completely the backdrop's color is\n stripped; --glass-stain-invert (0..1, default 1) flips the surviving luminance so bright backdrop\n patches stop fighting dark tinted glass (set 0 to disable). The glass-stained utility and the switcher's Stained toggle both reference THIS token. */\n --glass-stain: grayscale(var(--glass-stain-amount, 1)) invert(var(--glass-stain-invert, 0));\n /* Colored glow for the `glow` prop — follows the foreground hue; override to recolor the halo. */\n --glass-glow: oklch(var(--glass-glow-lc) var(--glass-fg-h) / var(--glass-glow-a));\n /* Selected-control FILL (active tab, pressed toggle) — the surface twin of --active-shadow. Composed\n here, not on :root, because it reads the tint: a scoped data-glass-tint must re-resolve it, and it\n is why the chip tracks whichever theme the subtree carries. Mode knobs in tokens.css carry the two\n routes to the same look (light tints the fill, dark lets the surface show through a white lift). */\n --active-bg: oklch(var(--active-l) min(var(--glass-tint-c), var(--active-c-max)) var(--glass-tint-h) / var(--active-a));\n /* Solid PRIMARY ink (default Button, link text) — themed at the tint's OWN chroma (×1.2): selenite\n stays grey, moonstone whispers warm, jewels read clearly tinted. Lightness is the --primary-l mode knob\n (0.42 light / 0.78 dark). Chroma is min(tint × 1.2, --primary-c-max): the ×1.2 is intent — primary\n should read more saturated than the base tint — but uncapped it asked past the sRGB edge at five\n presets in light and two in dark, where the browser, not the theme, picked the delivered colour.\n Measured after capping: --primary-foreground holds Lc 88.1 light / 62.9 dark at the worst hue. */\n --primary: oklch(var(--primary-l) min(calc(var(--glass-tint-c) * 1.2), var(--primary-c-max)) var(--glass-fg-h));\n /* Ready accent-tier colors (hue angles from the anchors block; L is the mode envelope --harmonic-l). */\n --color-complement: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-complement));\n --color-analogous-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-analogous-1));\n --color-analogous-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-analogous-2));\n --color-split-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-split-1));\n --color-split-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-split-2));\n --color-triad-1: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-triad-1));\n --color-triad-2: oklch(var(--harmonic-l) var(--color-chroma) var(--hue-triad-2));\n /* Monochromatic — the harmony anchor hue at stepped lightness (a subtle, low-chroma ramp). */\n --mono-1: oklch(var(--mono-l-1) var(--mono-chroma) var(--harmony-h));\n --mono-2: oklch(var(--mono-l-2) var(--mono-chroma) var(--harmony-h));\n --mono-3: oklch(var(--mono-l-3) var(--mono-chroma) var(--harmony-h));\n /* Opaque glass — a fully solid, faintly tinted floor (no see-through) for the \"opaque\" style.\n --glass-opaque-l is the body-lightness knob (:root/.dark; the header L slider drives it; the floor\n AND AutoForeground's --foreground-opaque banding both read it, so they can't drift). */\n /* Chroma is CAPPED now (--glass-opaque-c-max, tokens.css). This floor sat at L 90.9 in light (it is\n L88 now, for the same reason) — near-white territory where the sRGB ceiling collapses — yet it was\n the one near-white surface\n multiplying RAW --glass-tint-c (×1.26) with no cap, while the sheet had --glass-tint-c-hi, chakra\n had --glass-chakra-c-max and the gloss had --glass-gloss-c-max. That made it the binding surface\n for 7 of the 12 jewels: it clipped first, and every preset's chroma budget was set by it. Capping\n it is what let the tints below be normalised to their hue's real capacity rather than to this\n floor's. Multiply-THEN-cap, matching --glass-gloss-ink. */\n --glass-opaque-bg: oklch(\n calc(var(--glass-opaque-l) * 1%) min(calc(var(--glass-tint-c) * var(--glass-opaque-c-scale)), var(--glass-opaque-c-max, 0.3)) var(--glass-tint-h)\n );\n /* The same floor at the SOLIDIFY lightness/cap — the backing `glass` paints under every sheer\n material (see --glass-solidify in utilities.css). Split from --glass-opaque-bg because an opaque\n CARD and the backing under sheer glass are different surfaces with different jobs; tokens.css has\n the reasoning. Same scale, so a preset's tint still drives it; only the lightness and cap differ. */\n --glass-solidify-bg: oklch(\n calc(var(--glass-solidify-l, var(--glass-opaque-l)) * 1%)\n min(calc(var(--glass-tint-c) * var(--glass-opaque-c-scale)), var(--glass-solidify-c-max, var(--glass-opaque-c-max, 0.3))) var(--glass-tint-h)\n );\n /* Sheer glass sheet — hue/chroma from the tint vars, chroma scaled per stop so the highlights stay\n glassy and the body carries the color. Stop alphas ride the mode base --glass-sheet-a (+ fixed\n offsets; the top stop's offset is its own knob --glass-sheet-a1 — dark tapers it to 0.03);\n frosted = the same sheet lifted by --glass-frost-boost. */\n --glass-bg: linear-gradient(\n 135deg,\n oklch(100% calc(var(--glass-tint-c-hi) * 0.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-sheet-a1))) 0%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.01)) 25%,\n oklch(97% calc(var(--glass-tint-c-hi) * 1) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.02)) 50%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-sheet-a)) 75%,\n oklch(95% calc(var(--glass-tint-c-hi) * 1.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + 0.01)) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg,\n oklch(100% calc(var(--glass-tint-c-hi) * 0.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.04)) 0%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.01)) 25%,\n oklch(97% calc(var(--glass-tint-c-hi) * 1) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.02)) 50%,\n oklch(99% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost))) 75%,\n oklch(95% calc(var(--glass-tint-c-hi) * 1.3) var(--glass-tint-h) / calc(var(--glass-sheet-a) + var(--glass-frost-boost) + 0.01)) 100%\n );\n /* Very subtle white border — the lit edge of a cut sheet (alpha is the mode dial) */\n --glass-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-border-a));\n /* Opaque CHROME edge — the shadcn --border/--input role tokens (dividers, ghost/input outlines, any\n bare `border-border`). Previously flat neutral (oklch(0.88 0 0) / oklch(0.3 0 0)), which made them\n the ONLY chrome in the theme that did not move with the tint: a ghost Button kept a grey edge under\n lapis while its `outline` sibling (border-foreground/20) tracked the theme. Lightness moves only in\n LIGHT mode (0.88 → --border-l 0.85) to buy chroma headroom; see the knob comment in tokens.css.\n Chroma is min(tint chroma, --border-c-max) — a CAP, deliberately not --glass-tint-c-hi and not a\n multiplier. --glass-tint-c-hi (0.025) is the NEAR-WHITE cap, sized for the L 95–100% sheet stops\n and crystal floor; borrowing it here spent only 66% of the ceiling on amethyst and 17% on peridot,\n which is why the edge still read grey. A flat multiplier fails the other way: lapis declares\n c 0.15, so any factor small enough to keep it in gamut pushes the 0.07 jewels below where they\n started. min() gives every preset its full declared chroma up to the ceiling floor and clamps only\n the ones that overreach — and leaves c 0 at exactly 0, so selenite stays the neutral theme it is.\n Declaring them HERE (rather than on :root) also puts them under scripts/check-gamut.mjs, which\n scrapes engine.css for both the calc() and min() chroma shapes — the suite went 20 → 24 surface/mode\n pairs when these landed, so a later retune of the caps is measured, not asserted.\n SURFACE hue (--glass-tint-h), not --glass-fg-h: an edge belongs to the surface it bounds, matching\n --glass-border / --glass-crystal-border. In a fresco that splits the two (Muse night: surface 222,\n text 310) the border follows the panel, not the type. */\n --border: oklch(var(--border-l) min(var(--glass-tint-c), var(--border-c-max)) var(--glass-tint-h));\n --input: oklch(var(--input-l) min(var(--glass-tint-c), var(--border-c-max)) var(--glass-tint-h));\n /* The remaining shadcn SURFACE tokens — the last chroma-0 values in the theme. Same shape as --border\n for the same reason: a toggle's on-state, a hover wash and a sidebar panel all take their fill from\n these, so while they were flat neutrals those surfaces read near-white in light and near-black in\n dark in every preset. Each carries its OWN cap because the sRGB ceiling is a function of lightness\n and these sit from L 18 to L 98 — a shared cap would have to be the smallest of them (0.009, at the\n near-white sidebar) and would tint nothing. See the knobs in tokens.css. */\n --accent: oklch(var(--accent-bg-l) min(var(--glass-tint-c), var(--accent-bg-c-max)) var(--glass-tint-h));\n /* The hover wash as an IMAGE layer (glass-hover-accent). Translucent so the material still reads\n through it — a hover that paints a flat fill stops looking like glass. */\n --glass-hover-wash: color-mix(in oklch, var(--accent) 70%, transparent);\n --secondary: oklch(var(--secondary-l) min(var(--glass-tint-c), var(--secondary-c-max)) var(--glass-tint-h));\n --muted: oklch(var(--muted-l) min(var(--glass-tint-c), var(--muted-c-max)) var(--glass-tint-h));\n --sidebar: oklch(var(--sidebar-l) min(var(--glass-tint-c), var(--sidebar-c-max)) var(--glass-tint-h));\n /* Theme-aware brand gradient — follows --glass-fg-h, from the hue 3 ramp-steps out\n (8/side → step 360/17 ≈ 63.53°) to the base hue, at the mode envelope --gradient-l/-c + 0.5 alpha.\n (Axis/direction variants are computed in JS from lib/oklch-utils.ts — see the /components demo.) */\n --gradient: linear-gradient(\n 135deg,\n oklch(var(--gradient-l) var(--gradient-c) calc(var(--glass-fg-h) + 63.53) / 0.5) 0%,\n oklch(var(--gradient-l) var(--gradient-c) var(--glass-fg-h) / 0.5) 100%\n );\n /* Full-opacity twin of --gradient for gradient TEXT (background-clip: text) — no wash alpha so glyphs stay crisp. */\n --gradient-text: linear-gradient(\n 135deg,\n oklch(var(--gradient-l) var(--gradient-c) calc(var(--glass-fg-h) + 63.53)) 0%,\n oklch(var(--gradient-l) var(--gradient-c) var(--glass-fg-h)) 100%\n );\n /* High-contrast gradient text for printing ON a tinted theme surface: DARK tint-hue gradient in light\n mode, LIGHT one in .dark (--gradient-contrast-l/-c), so legibility comes from lightness. */\n --gradient-text-contrast: linear-gradient(\n 135deg,\n oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) calc(var(--glass-fg-h) + 63.53)) 0%,\n oklch(var(--gradient-contrast-l) var(--gradient-contrast-c) var(--glass-fg-h)) 100%\n );\n /* (The crystal floor, --glass-crystal-bg, is in block C: the hue gloss flavor repaints it.) */\n /* ── Chakra — the step-cut gem surface ────────────────────────────────────────────────────────\n BODY: the stone itself. The one tint-composing token here, so it is the only one with a gamut\n budget — min() against --glass-chakra-c-max (not a multiplier) for the same reason --border uses\n one: lapis declares c 0.15 and would be gamut-mapped, while any multiplier small enough to hold it\n drags the 0.07 jewels below where they should sit. Caps are measured per mode against the tightest\n ceiling across all 17 presets, so every preset delivers 100% of its declared chroma. */\n --glass-chakra-bg: oklch(\n calc(var(--glass-chakra-l) * 1%) min(var(--glass-tint-c), var(--glass-chakra-c-max)) var(--glass-tint-h) /\n var(--glass-chakra-a)\n );\n /* (The chakra SURFACE STACK, --glass-chakra-stack-bg, is in block C: it bakes the gloss.) */\n /* THE CUT — four sides, two steps, as INSET SHADOWS rather than inset background layers.\n A background layer is always a RECTANGLE: background-size shrinks it but gives it no radius of its\n own, so an inset gradient paints square corners over a rounded surface — a card with hard-cornered\n rectangles stacked on it. box-shadow has no such problem; an inset shadow is the border box's own\n rounded rect, offset, so every band follows --radius at whatever value the element carries, for\n free and at any size.\n Each side is a pair. The step-1 band (offset --glass-chakra-step) paints OVER the step-2 band\n (offset twice that), so the visible result is two concentric bands per side: 0→N shows step 1\n composited on step 2, N→2N shows step 2 alone. That is the step cut, and because step 2 is\n literally the same colour at half alpha it cannot drift out of sync with step 1.\n Order matters — box-shadow paints first-listed on top, so every step-1 band precedes its step-2\n twin. Corners: adjacent bands overlap and composite rather than mitering to a hard 45° line, which\n is the trade for following the radius. On a gem that reads as the corner facet catching both\n neighbours, and it is what the existing --glass-shadow bevel already does at top and bottom. */\n --glass-chakra-facets:\n inset 0 var(--glass-chakra-step) 0 oklch(var(--shadow-highlight) / var(--glass-chakra-facet-t-a)),\n inset var(--glass-chakra-step) 0 0 oklch(var(--shadow-highlight) / var(--glass-chakra-facet-l-a)),\n inset calc(var(--glass-chakra-step) * -1) 0 0 oklch(var(--shadow-ink) / var(--glass-chakra-facet-r-a)),\n inset 0 calc(var(--glass-chakra-step) * -1) 0 oklch(var(--shadow-ink) / var(--glass-chakra-facet-b-a)),\n inset 0 calc(var(--glass-chakra-step) * 2) 0 oklch(var(--shadow-highlight) / calc(var(--glass-chakra-facet-t-a) * 0.5)),\n inset calc(var(--glass-chakra-step) * 2) 0 0 oklch(var(--shadow-highlight) / calc(var(--glass-chakra-facet-l-a) * 0.5)),\n inset calc(var(--glass-chakra-step) * -2) 0 0 oklch(var(--shadow-ink) / calc(var(--glass-chakra-facet-r-a) * 0.5)),\n inset 0 calc(var(--glass-chakra-step) * -2) 0 oklch(var(--shadow-ink) / calc(var(--glass-chakra-facet-b-a) * 0.5));\n /* Edge + depth: the standard drop plus the cut. --glass-shadow's own top/bottom bevel is left out —\n the facet bands ARE the bevel, and doubling them would flatten the step read. */\n --glass-chakra-border: oklch(100% calc(var(--glass-tint-c-hi) * 0.5) var(--glass-tint-h) / var(--glass-border-a));\n --glass-chakra-shadow: var(--glass-shadow), var(--glass-chakra-facets);\n --glass-chakra-shadow-hover: var(--glass-shadow-lg), var(--glass-chakra-facets);\n /* Precomposed background STACKS — the background-image compositions, single-sourced. The surface\n utilities consume them via --srf-* fallbacks (sheer utilities append the element-composed solidify\n layer underneath), and the [data-glass] style remaps below swap them wholesale. Live here so they\n re-resolve per tint scope and per mode wrapper. The crystal stack bakes the gloss into the\n background (the site-wide re-skin can't use ::before/::after — those need position:relative, which\n would break sticky/fixed surfaces). */\n --glass-wash-layer: linear-gradient(var(--glass-tint-wash), var(--glass-tint-wash));\n --glass-stack-bg: var(--glass-bg), var(--glass-wash-layer);\n /* Frosted rides the wash in the IMAGE stack (sheet + wash), leaving the background-color channel\n free for the veil floor. */\n --frosted-stack-bg: var(--glass-frosted-bg), var(--glass-wash-layer);\n /* (--crystal-stack-bg is in block C: it bakes the gloss.) */\n}\n\n/* C. GLOSS — the crystal highlight and everything that bakes it: the ink, the three gradients, the\n crystal floor, and the crystal + chakra stacks. Declared on every scope that can CHANGE it: :root and\n .dark (the mode knobs), [data-glass-tint] (the tint), and [data-gloss] (the flavor). The last is what\n lets `data-gloss` work below : these tokens are composed values, which bake where they are\n declared, so a [data-gloss] section that only re-pinned an input (as the flavor blocks used to) had\n its children inherit 's gradients and stacks pre-composed, and painted the page's gloss.\n THREE FLAVORS, chosen by two plain switches the [data-gloss] blocks in materials.css set to `1` or\n `initial` (unset = tonal):\n white → flat white specular (the classic glass shine) --glass-gloss-is-white: 1\n tonal → a tonally-close single-hue tint of the theme (--glass-gloss-l / --glass-gloss-tint)\n hue → iridescent: the highlight sweeps hues shifted ± --glass-gloss-hue-span around the tint\n hue, and repaints the crystal floor in the sweep --glass-gloss-is-hue: 1\n A switch selects nothing by itself: each flavor's variant (block C′ below) multiplies an alpha by it,\n so with the switch `initial` the variant is invalid at computed-value time and the var() fallback here,\n the tonal shape, applies. The switches are leaves, so they inherit, and a nested [data-gloss] sets both\n to pick its own flavor; a nested [data-glass-tint] recomposes from the inherited switches and keeps the\n page's flavor (it used to reset it to tonal). Each layer applies its own alpha. */\n:root,\n.dark,\n[data-glass-tint],\n[data-gloss] {\n /* The tonal ink as its own token (not inline in the fallback below) so check-gamut's scraper, which\n reads `--x: ` declarations, still sweeps it. */\n --glass-gloss-ink-tonal: calc(var(--glass-gloss-l, 94) * 1%)\n min(calc(var(--glass-tint-c) * var(--glass-gloss-tint, 2)), var(--glass-gloss-c-max, 0.3)) var(--glass-tint-h);\n --glass-gloss-ink: var(--glass-gloss-ink-white, var(--glass-gloss-ink-tonal));\n --glass-gloss-top: var(--glass-gloss-top-hue, linear-gradient(to bottom, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.4), transparent 30%));\n --glass-gloss-streak: var(\n --glass-gloss-streak-hue,\n linear-gradient(calc(135deg * var(--glass-gloss-hue-dir, 1)), oklch(var(--glass-gloss-ink, 100% 0 0) / 0.15), transparent 50%)\n );\n --glass-gloss-glow: var(\n --glass-gloss-glow-hue,\n radial-gradient(circle at 50% 120%, oklch(var(--glass-gloss-ink, 100% 0 0) / 0.2), transparent 70%)\n );\n /* Crystal floor — clear glass; the alpha is the mode dial (--glass-crystal-bg-a).\n Crystal body. The lightness is a KNOB, not 100%, and that is the whole point: at L100 sRGB holds\n ZERO chroma, so this surface asked for 0.015 and delivered exactly none — crystal rendered pure\n white under every preset, in both modes (measured: #fffdff amethyst, #fffef4 moonstone,\n #fbffff lapis; RGB spread 2–11). check-gamut even reported it as \"no attainable chroma at all\"\n and skipped the pair rather than failing it.\n --glass-crystal-l is 96 because that is the lowest lightness at which the TIGHTEST of the 21 hues\n this theme uses (h268, lapis) can still carry the 0.015 this asks for: ceiling 0.0182, 1.22x\n headroom. L96.5 is the exact crossover at 1.06x, too thin given maxChromaFor already sits 3% inside\n the boundary to stop Safari clamping toward grey. The cost is small — at the 0.3 light-mode alpha\n the composite moves ~1.2 L, and 0.4 L in dark. */\n --glass-crystal-bg: var(\n --glass-crystal-bg-hue,\n oklch(calc(var(--glass-crystal-l) * 1%) calc(var(--glass-tint-c-hi) * 0.6) var(--glass-tint-h) / var(--glass-crystal-bg-a))\n );\n /* SURFACE STACKS: crystal and chakra bake the same gloss triple, so `data-gloss` white / tonal / hue\n reskin both, over a fresco slot and the tint wash. Nothing in the chakra stack is inset, so nothing\n has a corner of its own — the element's border-radius clips the whole stack (see the facet bands in\n block B). Baked into the background, not ::before/::after: the crystal PAGE style re-skins every\n surface, and pseudo layers would need position:relative, which breaks sticky/fixed surfaces. */\n --crystal-stack-bg:\n var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer);\n --glass-chakra-stack-bg:\n var(--glass-gloss-top), var(--glass-gloss-glow), var(--glass-gloss-streak), var(--glass-crystal-fresco, none), var(--glass-wash-layer);\n}\n\n/* C′. The FLAVOR VARIANTS block C falls back from, each valid only while its switch is `1`. Kept in a\n sibling block (same scopes) so scripts/check-gamut.mjs can model the hue variants at the hue flavor's\n own L74 (the [data-gloss=\"hue\"] block pins --glass-gloss-l there) without misreading block C's tonal\n ink at it.\n hue: each highlight sweeps (tint hue − span) → tint hue → (+ span), interpolated IN OKLCH so the arc\n passes through the base hue. Top leans to the low end, glow to the high end, streak spans both. The\n floor is replaced by a hue-carrying one at the gloss lightness (so Light deepens it): the default\n crystal floor sits near white, where chroma collapses, so the sweep would read over a white sheet. */\n:root,\n.dark,\n[data-glass-tint],\n[data-gloss] {\n --glass-gloss-ink-white: calc(var(--glass-gloss-is-white) * 100%) 0 0;\n --glass-gloss-top-hue: linear-gradient(\n to bottom in oklch,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.4 * var(--glass-gloss-is-hue))\n ),\n transparent 35%\n );\n --glass-gloss-streak-hue: linear-gradient(\n 115deg in oklch,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) - var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.32 * var(--glass-gloss-is-hue))\n )\n 0%,\n oklch(calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12) var(--glass-tint-h) / calc(0.12 * var(--glass-gloss-is-hue))) 45%,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.32 * var(--glass-gloss-is-hue))\n )\n 85%,\n transparent\n );\n --glass-gloss-glow-hue: radial-gradient(\n circle at 50% 120%,\n oklch(\n calc(var(--glass-gloss-l, 94) * 1%) var(--glass-gloss-c, 0.12)\n calc(var(--glass-tint-h) + var(--glass-gloss-hue-span, 40) * var(--glass-gloss-hue-dir, 1)) /\n calc(0.25 * var(--glass-gloss-is-hue))\n ),\n transparent 70%\n );\n --glass-crystal-bg-hue: oklch(\n calc(var(--glass-gloss-l, 94) * 1%) calc(var(--glass-tint-c) * 1.2) var(--glass-tint-h) /\n calc(0.4 * var(--glass-gloss-is-hue))\n );\n}\n/* ── Glass surface utilities (@utility — tree-shaken per consumer usage) ──────\n The material system: ONE structural `glass` utility + orthogonal axis classes (border, veil, sm/lg,\n gradient, glow, sheen). Materials live in materials.css as [data-material] token sets; page styles\n in styles.css remap the same --srf-* tokens. No recipe utilities, no !important. */\n\n/* The glow itself — a punchy two-layer colored halo, centralized so its strength is tunable in ONE\n place. `glass-glow` is the resting glow; `glass-glow-lg` (used on hover) intensifies it. Both set\n --glow-layer, which every glass box-shadow folds in, so the glow shows under EVERY glass style.\n Full mechanism + tuning + troubleshooting: docs/glow.md */\n@utility glass-glow {\n --glow-layer: 0 0 8px 1px var(--glass-glow), 0 0 18px 3px var(--glass-glow);\n}\n@utility glass-glow-lg {\n --glow-layer: 0 0 12px 2px var(--glass-glow), 0 0 28px 5px var(--glass-glow);\n}\n\n/* ── THE MATERIAL SYSTEM (Sistine 2.0) ────────────────────────────────────────\n ONE structural utility (`glass`) + orthogonal axis classes. The material (a token set) comes from\n [data-material] (materials.css) or the inherited [data-glass] page remap (styles.css); with\n neither, the --srf-* fallbacks render the default glass material. Axis utilities only SET custom\n props (or properties `glass` never touches), so Tailwind's emission order can never flip a result.\n (The legacy recipe utilities below coexist during the migration and die with it.) */\n\n/* Structural glass — background/filter/shadow plumbing only; borderless (add glass-border).\n Channels, outermost first:\n background-image = [--srf-hover state slot] + [--acc-gradient accent slot] + [--srf-bg-image material stack] + [solidify]\n background-color = --veil-floor (element veil) > --srf-bg-color (material floor) > transparent\n backdrop-filter = --srf-filter (material) > blur(--srf-blur > --blur) saturate(180%)\n box-shadow = --srf-shadow (material) > --srf-elev (size axis) > --glass-shadow, + glow\n The leading gradient slot is a no-op layer so `glass-gradient` composes over ANY material (same\n pattern as the fresco slot inside --crystal-stack-bg). Both no-op slots resolve to `none` rather\n than linear-gradient(transparent, transparent): `none` is a valid , holds the layer\n position identically, and paints nothing — where a transparent GRADIENT still goes through the\n gradient rasteriser (and its dither) to deliver zero pixels.\n The solidify floor (--glass-opacity dial) is composed HERE, at the element — never in a token\n context (the var-composition-resolves-at-declaration gotcha; see scripts/check-theme.mjs). The\n --srf-solidify slot lets a material DROP that layer when it is provably a no-op (the opaque material,\n and the opaque PAGE style, which sets it on its own scope so it reaches `@apply glass` surfaces that\n carry no `.glass` class); materials that want the normal floor pin the slot back to `initial`\n (guaranteed-invalid → the var() fallback composes --glass-solidify at the element). --veil-solidify\n leads the chain: glass-veil sets it (non-inheriting), because a VEILED surface paints the neutral\n veil floor in its background-colour channel, so the layer is real there whatever the slot says. */\n@utility glass {\n /* Fallback 0.7, not 0: every glass surface now sits on a 70% solidify floor unless a consumer dials\n it back. Measured before changing it, because AutoForeground does NOT read --glass-opacity — it\n bands text against the sheer floor, so a more solid surface than it modelled could have cost\n contrast. Body text goes 91.5 -> 86.5 Lc in light and 88.0 -> 85.8 in dark, both far above the\n 75 body floor (it still clears at a full 1.0). Set --glass-opacity: 0 for the old sheer default. */\n /* --glass-solidify-BG, not --glass-opaque-bg: the backing under sheer glass is its own surface now\n (engine.css / tokens.css). In dark the two resolve identically, so this is a light-mode change. */\n --glass-solidify-fill: color-mix(in oklch, var(--glass-solidify-bg, var(--glass-opaque-bg)) calc(var(--glass-opacity, 0.7) * 100%), transparent);\n --glass-solidify: linear-gradient(var(--glass-solidify-fill), var(--glass-solidify-fill));\n /* --srf-hover leads the stack because a hover/selection wash has to sit ON the material, not under it.\n `hover:bg-accent` sets background-COLOR, which is the bottom of the box — beneath all 14 image layers\n the glass stack paints, of which the solidify floor alone is 70% opaque. At most ~30% of the accent\n survived to the eye, so the hover read as the material's own colour (white in light mode) no matter\n what --accent was set to. No amount of tuning the token could fix that; it is a layer-order problem.\n Same no-op `none` slot pattern as --acc-gradient below it. */\n background-image:\n var(--srf-hover, none), var(--acc-gradient, none), var(--srf-bg-image, var(--glass-stack-bg)),\n var(--veil-solidify, var(--srf-solidify, var(--glass-solidify)));\n background-color: var(--veil-floor, var(--srf-bg-color, transparent));\n /* Author ONLY the standard backdrop-filter (no -webkit- twin: Lightning dedupes the pair keeping\n the PREFIXED one, which Chromium never aliased — the [no-webkit-twin] invariant guards this).\n The max() composes the diffuse FLOOR for adaptive surfaces. */\n backdrop-filter: var(--srf-filter, blur(max(var(--srf-blur, var(--blur)), var(--dif, 0px))) saturate(180%)) var(--srf-stain,);\n box-shadow: var(--srf-shadow, var(--srf-elev, var(--glass-shadow))), var(--glow-layer, 0 0 0 0 transparent);\n}\n\n/* Border axis — the material picks the color unless the element asks for ink (--brd-ink, the\n borderColor axis below); width resolves material pin (--srf-border-w, which itself reads the --brd-w\n weight slot) → weight slot directly (adaptive surfaces) → 1px hairline. */\n@utility glass-border {\n border: var(--srf-border-w, var(--brd-w, 1px)) solid var(--brd-ink, var(--srf-border-color, var(--glass-border)));\n}\n/* Border WEIGHTS — token setters for the --brd-w slot (order-safe; same-element composition, like\n glass-sm's --srf-blur). Hairline is the unmarked default (1px; 0.5px under frosted); rim = 2px, frame = 4px. */\n@utility glass-border-rim {\n --brd-w: 2px;\n}\n@utility glass-border-frame {\n --brd-w: 4px;\n}\n/* Border COLOUR — ink. The material edge is a near-white rim at the material's alpha: it reads as cut\n glass on a dark page and as nothing at all on most light themes, at hairline, rim or frame weight\n alike, so a weight axis whose colour disappears went unused. Ink is the element's own text colour\n (--foreground, after any per-material text remap on this element), mixed toward transparent by\n --glass-border-ink-a: a visible line anywhere text is legible. Element-composed and registered\n non-inheriting (registrations.css), so a child border keeps its own material colour. */\n@utility glass-border-ink {\n --brd-ink: color-mix(in oklch, var(--foreground) calc(var(--glass-border-ink-a, 0.5) * 100%), transparent);\n}\n\n/* Diffuse axis — a READABILITY BLUR FLOOR (not a blur value): every translucent material computes\n blur(max(, --dif)), so crystal stays crystal until a text-dense surface opts in.\n Token-setter (order-safe); OPAQUE never reads the slot (--srf-filter: none), so the short-circuit\n is inherited — no GPU spend on solid floors. Tune the floor via --glass-diffuse. */\n@utility glass-diffuse {\n --dif: var(--glass-diffuse, 12px);\n}\n\n/* Veil axis — the legibility floor for read-through overlays (menus, tooltips, toasts).\n Composed AT THE ELEMENT so a scoped --glass-solid-a resolves here (check-theme invariant 2b).\n The veil rides the background-COLOR channel, under the material's image stack, and — being\n element-declared — beats any inherited --srf-bg-color remap. */\n/* Stained axis — REAL stained-glass optics: grayscale collapses the backdrop to pure luminance\n (lighter/darker only, akin to alpha), so the glass's OWN tint supplies every drop of color and\n whatever sits behind renders as tonal shades of the theme. One color-matrix op — free next to the\n blur. --glass-stain-amount (0..1) dials how completely the backdrop's color is stripped. */\n@utility glass-stained {\n --srf-stain: var(--glass-stain);\n}\n\n@utility glass-veil {\n --veil-floor: oklch(var(--glass-solid-l) 0 0 / var(--glass-solid-a));\n /* The veil keeps the solidify layer even where the material or page dropped it (--srf-solidify: none):\n those drops are only no-ops over the opaque floor, and a veiled surface paints the veil instead. */\n --veil-solidify: var(--glass-solidify);\n}\n\n/* THE PAGE BACKDROP LAYER — the fixed, full-viewport ground every sheer material blurs. Put this on the\n `fixed inset-0 -z-10` container each background component renders.\n It exists to force a compositing layer. `backdrop-filter` re-samples everything painted beneath it,\n and this layer sits at z-index -10 under the entire document; unpromoted, the compositor has to\n re-rasterise it against the scroll position every frame, which is where the scroll artifact on the\n sheer materials comes from. `opaque` is the ONE material that sets --srf-filter: none — and the one\n material that never showed the artifact, which is the correlation that points here.\n Measured on the deployed page, this element resolved will-change:auto / transform:none /\n contain:none — nothing was promoting it. Promoting is safe specifically because it has ZERO\n position:fixed descendants and no background-attachment:fixed; a transform/will-change would\n otherwise become their containing block and reparent them. Re-check that before adding children.\n will-change (rather than a translateZ hack) because the hint is honest here: this layer is on screen\n and being sampled for the whole session, so there is nothing to hint OFF later. */\n@utility glass-backdrop-layer {\n will-change: transform;\n}\n\n/* Size axes — blur + elevation tiers. These feed the GLASS material only: frosted/crystal/opaque\n (and page remaps) set --srf-filter/--srf-shadow wholesale, which flattens the tiers — matching\n the old system, where the [data-glass] styles flattened glass-surface-sm/-lg. */\n@utility glass-sm {\n --srf-blur: var(--blur-sm);\n --srf-elev: var(--glass-shadow-sm);\n}\n@utility glass-lg {\n --srf-blur: var(--blur-lg);\n --srf-elev: var(--glass-shadow-lg);\n}\n@utility glass-xl {\n --srf-blur: var(--blur-xl);\n --srf-elev: var(--glass-shadow-lg);\n}\n\n/* ── Gradient ACCENT — a brand wash ON a material, not a material. Fills the no-op --acc-gradient\n slot at the top of every `glass` background, so it composes over glass, frosted, crystal, and\n opaque alike. A token-setter (no property `glass` sets), so it's immune to utility emission order. */\n@utility glass-gradient {\n --acc-gradient: var(--gradient);\n}\n\n/* ── Hover/selection WASH — the interactive twin of glass-gradient. Fills the --srf-hover slot at the\n very top of the background stack, which is the only place a state colour is actually visible on a\n glass surface: `hover:bg-accent` writes background-COLOR, and every one of the material's image\n layers paints over it (solidify alone is 70% opaque), so the accent arrived at ~30% strength blended\n into the material and read as plain white on a light theme.\n The wash is deliberately translucent rather than a flat fill — glass that turns solid on hover stops\n looking like glass. 70% of --accent over the material keeps the wash, gloss and blur reading through\n it while still shifting the surface unmistakably toward the theme colour.\n Pair with `hover:bg-accent` rather than replacing it: the background-color still does the work on the\n NON-glass variants (outline/default paint no material, so nothing covers it there). */\n@utility glass-hover-accent {\n &:hover {\n --srf-hover: linear-gradient(var(--glass-hover-wash), var(--glass-hover-wash));\n }\n}\n\n/* Sheen — the OPT-IN hover shimmer (the gloss slides across on hover). Self-contained (highlight + diagonal\n + glow + the 200%/hover shift), so it works on any glass surface AND, layered on glass-crystal (which\n already has the static gloss), its ::before/::after override crystal's to add the motion. Pseudo-element\n based → position:relative; avoid on sticky/fixed surfaces. */\n@utility glass-sheen {\n position: relative;\n\n &::before,\n &::after {\n content: \"\";\n position: absolute;\n inset: 0;\n border-radius: inherit;\n pointer-events: none;\n }\n\n &::before {\n background: var(--glass-gloss-top), var(--glass-gloss-streak);\n background-size: 200% 200%;\n background-position: 0% 0%;\n opacity: 0.7;\n transition: background-position 0.3s ease;\n }\n\n &::after {\n background: var(--glass-gloss-glow);\n opacity: 0.8;\n }\n\n &:hover::before {\n background-position: 100% 100%;\n }\n}\n\n/* Namespaced: a bare `shimmer` is a global name, and a consumer's own @keyframes shimmer (or another\n library's) replaced this one for everyone, whichever stylesheet loaded last. Animates `translate`, not\n `transform`: Tailwind v4's `-translate-x-full` (the skeleton's resting position) writes the `translate`\n property, which COMPOSES with an animated transform rather than being replaced by it, so the sweep ran\n from -200% to 0% and parked the highlight on top of the skeleton. Animating the same property replaces\n the resting value while running, and with the animation off it falls back to that resting value. */\n@keyframes sistine-shimmer {\n from {\n translate: -100% 0;\n }\n to {\n translate: 100% 0;\n }\n}\n\n/* ── Reduced motion ───────────────────────────────────────────────────────────\n CONTRIBUTING says \"Respect prefers-reduced-motion\"; the pattern scenes did and nothing else did.\n This closes that, but NOT with the blunt global `*, ::before, ::after { animation: none !important }`\n reset — that would also flatten the pattern scenes' own carefully scoped rules and every enter/exit\n transition in the library.\n\n The treatment differs by what the animation actually IS, because the preference is about MOTION, not\n about animation in general:\n - shimmer → translation across a surface. Real motion, purely decorative: stopped. The skeleton\n still reads as a skeleton from its base fill.\n - spin → rotation. Real motion, but it is the ONLY signal that a loader is alive, so freezing\n it reads as \"hung\" rather than \"calm\". Slowed hard instead of stopped.\n - pulse → opacity only. Nothing moves, so there is nothing to trigger a vestibular response.\n Left alone deliberately; it also carries meaning (the input-otp caret is a blink).\n Unlayered on purpose: Tailwind's generated utilities sit inside @layer utilities, and an unlayered\n rule outranks any layered one regardless of order, so this needs neither !important nor a specificity\n hack. */\n@media (prefers-reduced-motion: reduce) {\n .animate-\\[sistine-shimmer_2s_infinite\\],\n .before\\:animate-\\[sistine-shimmer_2s_infinite\\]::before {\n animation: none;\n }\n\n .animate-spin {\n animation-duration: 3s;\n }\n}\n/* ── Materials — [data-material] token sets + gloss flavors ───────────────────\n Four materials, one attribute. Each block declares the FULL --srf-* pin set ON the element, so an\n explicit material always beats an inherited [data-glass] page remap (element-declared custom props\n beat inherited ones). One material per element is structural: attributes can't stack, so there is\n no !important anywhere in the system.\n NOTE these are inherited custom properties — a data-material element also acts as a SCOPED default\n for adaptive `glass` descendants (the mechanism DialogContent's old data-glass hack hand-rolled).\n Pin a child back with its own data-material.\n Import order: after tokens.css — [data-gloss=\"hue\"] re-pins --glass-gloss-l and the crystal shadows,\n which tokens.css declares on :root/.dark at equal specificity, so source order decides on . */\n\n[data-material=\"glass\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--glass-stack-bg);\n --srf-bg-color: transparent;\n /* Composed on the element, so the glass-sm/glass-lg axes still resolve here (same-element\n setter/reader is safe — the composition gotcha only bites pre-composed inherited values). */\n --srf-filter: blur(max(var(--srf-blur, var(--blur)), var(--dif, 0px))) saturate(180%);\n --srf-border-color: var(--glass-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--srf-elev, var(--glass-shadow));\n}\n\n/* Frosted — the tint wash rides the IMAGE stack (--frosted-stack-bg = sheet + wash), leaving the\n background-color channel free for the veil floor. */\n[data-material=\"frosted\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--frosted-stack-bg);\n --srf-bg-color: transparent;\n --srf-filter: blur(max(var(--blur-frosted), var(--dif, 0px))) saturate(200%);\n --srf-border-color: var(--glass-frosted-border);\n --srf-border-w: var(--brd-w, 0.5px);\n --srf-shadow: var(--glass-frosted-shadow);\n}\n\n/* Crystal — gloss BAKED into the background stack (sticky/fixed-safe; no pseudo-elements, no\n position:relative). --crystal-stack-bg carries the --glass-crystal-fresco pass-through. The hover\n sparkle is a plain token swap; box-shadow transitions on the real property. No saturate(),\n matching the old variant. */\n[data-material=\"crystal\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--crystal-stack-bg);\n --srf-bg-color: var(--glass-crystal-bg);\n --srf-filter: blur(max(var(--blur-crystal), var(--dif, 0px)));\n --srf-border-color: var(--glass-crystal-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--glass-crystal-shadow);\n transition: box-shadow 0.3s ease;\n}\n[data-material=\"crystal\"]:hover {\n --srf-shadow: var(--glass-crystal-shadow-hover);\n}\n/* Crystal text tiers — the baked gloss locally LIGHTENS the title zone (worst in dark mode), so text\n inside crystal remaps to the -crystal set AutoForeground bands against the glossed crystal surface.\n VEILED crystal is excluded: the veil floor is exactly what the normal tiers are banded for. */\n[data-material=\"crystal\"]:not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-crystal);\n --foreground-soft: var(--foreground-soft-crystal);\n --foreground-strong: var(--foreground-strong-crystal);\n --foreground-ui: var(--foreground-ui-crystal);\n --muted-foreground: var(--muted-foreground-crystal);\n}\n\n/* Chakra — the step-cut gem: a translucent tinted body with a two-step facet frame on all four edges,\n sitting between crystal (near-clear) and opaque (solid). Like crystal it bakes everything into the\n background stack and the shadow — no pseudo-elements, so it is safe on sticky/fixed surfaces, and it\n carries the same gloss triple, so `data-gloss` white / tonal / hue reskin it too.\n The cut lives entirely in --srf-shadow (see --glass-chakra-facets in engine.css). Nothing here is an\n inset BACKGROUND layer, deliberately: background layers are rectangles with no radius of their own,\n so an inset gradient stacks square corners on a rounded surface. Inset shadows follow --radius.\n Because the stack is plain full-bleed layers, the sheer-glass axes work untouched — glass-diffuse\n raises --dif into the blur below, and the --glass-opacity solidify floor composes under the stack\n exactly as it does for crystal. */\n[data-material=\"chakra\"] {\n /* Solidify slot: `initial` = guaranteed-invalid, so `glass` falls back to composing --glass-solidify\n AT THE ELEMENT (never inherited pre-composed — the var-composition gotcha). Pinned so an opaque\n PAGE remap can never leak its dropped layer into an explicit material. */\n --srf-solidify: initial;\n --srf-bg-image: var(--glass-chakra-stack-bg);\n --srf-bg-color: var(--glass-chakra-bg);\n --srf-filter: blur(max(var(--blur-chakra), var(--dif, 0px)));\n --srf-border-color: var(--glass-chakra-border);\n --srf-border-w: var(--brd-w, 1px);\n --srf-shadow: var(--glass-chakra-shadow);\n transition: box-shadow 0.3s ease;\n}\n[data-material=\"chakra\"]:hover {\n --srf-shadow: var(--glass-chakra-shadow-hover);\n}\n/* Chakra text tiers — content sits on the body, whose lightness (--glass-chakra-l) is its own dial and\n can sit well away from the page's. Same reasoning as opaque/crystal; veiled chakra is excluded\n because the veil floor is exactly what the NORMAL tiers are banded for. */\n[data-material=\"chakra\"]:not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-chakra);\n --foreground-soft: var(--foreground-soft-chakra);\n --foreground-strong: var(--foreground-strong-chakra);\n --foreground-ui: var(--foreground-ui-chakra);\n --muted-foreground: var(--muted-foreground-chakra);\n}\n\n/* Opaque — solid tinted floor, nothing to blur through. --glass-opaque-outline stays settable on any\n ancestor: it's composed HERE, on the element, so it resolves where it's read. The floor can be\n LIGHT while the page is dark, so the foreground tiers remap to the -opaque set AutoForeground\n bands against that floor (inherits into the card's content). */\n[data-material=\"opaque\"] {\n /* DROP the solidify layer: it is `--glass-opaque-bg` at --glass-opacity alpha painted directly over a\n background-COLOR of the same --glass-opaque-bg at alpha 1 — i.e. C over C, which composites to C for\n ANY alpha. Measured across tints and both modes it moves every channel by exactly 0. So the layer was\n one full gradient rasterisation (and one dither pass) for provably zero pixels, on the one material\n with no backdrop-filter behind it to mask that noise. Restored when the surface is VEILED (glass-veil\n sets --veil-solidify, which `glass` reads first) — there the background-color is the neutral veil\n floor, not --glass-opaque-bg, so the layer is real (dropping it there would shift up to 31/255 in\n light and 42/255 in dark). */\n --srf-solidify: none;\n /* NO sheet either — opaque now paints a FLAT background-color and nothing else. --glass-bg is a 5-stop\n gradient whose total range, composited over this floor, is 2–4 sRGB codes across an entire card in\n light and 5–7 in dark: one code per ~100–200px, far too shallow to draw cleanly in 8-bit, so the\n rasteriser dithers rather than bands. On every other material a real backdrop-filter supplies blurred\n page content that masks that noise; opaque sets --srf-filter: none, so there is nothing behind it and\n the dither is the only variation on the card — read as grain, worst over text.\n The sheet's GRADIENT was therefore invisible, but its uniform LIFT was not (+1.7 L light, +4.8 L dark),\n so it is folded into --glass-opaque-l rather than dropped: see tokens.css. AutoForeground reads that\n same token, so the -opaque text tiers re-band against the new floor automatically. */\n --srf-bg-image: none;\n --srf-bg-color: var(--glass-opaque-bg);\n --srf-filter: none;\n --srf-border-color: var(--glass-opaque-outline, var(--glass-border));\n /* Outline WEIGHT: an element rim/frame class (--brd-w) beats the page-level outline weight (the\n switcher's --glass-opaque-outline-w on ), which beats the 1px hairline. */\n --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px));\n --srf-shadow: var(--glass-shadow);\n /* Veiled overlays INSIDE an opaque element compute a fully solid floor — same guarantee the opaque\n PAGE style makes (menus/toasts must stay legible on a solid ground). */\n --glass-solid-a: 1;\n --foreground: var(--foreground-opaque);\n --foreground-soft: var(--foreground-soft-opaque);\n --foreground-strong: var(--foreground-strong-opaque);\n --foreground-ui: var(--foreground-ui-opaque);\n --muted-foreground: var(--muted-foreground-opaque);\n}\n\n/* ── Gloss flavors ────────────────────────────────────────────────────────────\n data-gloss on or ANY element swaps the crystal (and chakra) gloss for that subtree. Default\n (unset) = tonal. These blocks set LEAVES only — the two flavor switches, plus the hue flavor's own\n lightness and shadows — and engine.css block C recomposes the gloss on every [data-gloss] element from\n them. That split is what makes a scoped `
    ` work: a composed value bakes\n where it is declared, so a block here that declared one would hand its children 's gloss.\n Each block sets BOTH switches, so a nested flavor overrides an ancestor's instead of combining with it.\n They compose with [data-material=\"crystal\"] AND the [data-glass=\"crystal\"] page style, in both modes.\n Known edge: hue's pinned --glass-gloss-l and bevel-less crystal shadows are mode-twinned leaves that a\n white/tonal section nested INSIDE a hue scope inherits (no mode-aware reset exists for them here). */\n[data-gloss=\"white\"] {\n --glass-gloss-is-white: 1; /* the ink becomes the flat white specular, 100% 0 0 */\n --glass-gloss-is-hue: initial;\n}\n[data-gloss=\"tonal\"] {\n /* Explicit tonal (also the default) — both switches off, so it can override a white/hue ancestor. */\n --glass-gloss-is-white: initial;\n --glass-gloss-is-hue: initial;\n}\n[data-gloss=\"hue\"] {\n --glass-gloss-is-white: initial;\n --glass-gloss-is-hue: 1;\n /* Hue OWNS its lightness rather than sharing the tonal twin. The two flavors want opposite things\n from the same number: tonal wants the brightest value the surface allows (97 in light) so it reads\n as a specular, while hue needs chroma headroom — its stops ask --glass-gloss-c 0.16, and the sRGB\n ceiling at L 97 is 0.0136, so the iridescent sweep would deliver 8% of its colour and wash out to\n near-white exactly where it is supposed to be most vivid.\n 74 is mode-invariant on purpose: it holds 81% of that chroma, and it lands on the right side of\n both surfaces anyway (+18.4 L over the dark floor, −9.0 over the light one — for an iridescent\n sweep, being SEEN matters more than which direction it moves). The demo's flavor switcher already\n seeded 74 at runtime for this reason; this makes a no-JS consumer get it too.\n \"Holds 81% of that chroma\" assumed the browser gamut-MAPS an over-ceiling request down to the\n ceiling. Measured in WebKit it CLIPS instead (see --glass-gloss-c-max in tokens.css), so 0.16 did\n not deliver 81% of itself — it drove the tightest hues to a pinned channel. 0.12 sits just under\n 0.1240, the L-74 ceiling for turquoise (h190) and the tightest of the presets, so the sweep now\n renders every hue it asks for. scripts/check-gamut.mjs reads this 74 to score the variants. */\n --glass-gloss-l: 74;\n /* Kill the white bevel: the crystal shadow carries a white inset highlight, which would sit on top of\n the swept hue. (The white floor is replaced in engine.css block C′.) */\n --glass-crystal-shadow: var(--glass-shadow);\n --glass-crystal-shadow-hover: var(--glass-shadow-lg);\n}\n/* ── Site-wide style remaps + vendor cascade fixes ──────────────────────────── */\n\n/* Keep react-day-picker from painting over the glass background */\n@layer utilities {\n .glass[class*=\"rdp\"],\n .glass .rdp,\n .glass .rdp-month {\n background-color: transparent !important;\n }\n}\n\n/* ── Site-wide glass style: token remaps ─────────────────────────────────────\n data-glass on (frosted | crystal | opaque) re-skins every ADAPTIVE surface (`glass` with no\n data-material) by remapping the --srf-* tokens it reads. An element-level [data-material] pins its\n own tokens and beats these by cascade physics — reinforced by the :where() wrapper, which drops\n every remap (incl. its cross-product legs) to specificity (0,0,0), so a co-located data-material on\n a tint-scope element always wins. The cross-product legs keep the swapped stacks re-resolving inside\n scoped tints, status toasts included (a bare [data-glass] rule would bake 's tint into\n descendants' surfaces). Veiled surfaces (glass-veil) keep their element-composed floor.\n The ELEMENT-level rules further down (text tiers, the opaque edge) need to know which elements are\n surfaces. `.glass` says so for the utility class; an element that wears the material through\n `@apply glass` has no class, so it opts in with a `data-surface` attribute (`data-surface=\"veil\"`\n when it also applies glass-veil). Everything ABOVE is inherited tokens and needs no marker. */\n:where([data-glass=\"frosted\"], [data-glass=\"frosted\"] [data-glass-tint], [data-glass=\"frosted\"][data-glass-tint]) {\n --srf-bg-image: var(--frosted-stack-bg);\n /* NO --srf-bg-color wash here: the wash already rides the IMAGE stack (--frosted-stack-bg), same\n as the frosted material — a color-channel copy double-washed tinted presets and stole the veil\n floor's channel. */\n --srf-filter: blur(max(var(--blur-frosted), var(--dif, 0px))) saturate(200%);\n --srf-border-color: var(--glass-frosted-border);\n --srf-border-w: var(--brd-w, 0.5px);\n --srf-shadow: var(--glass-frosted-shadow);\n}\n:where([data-glass=\"crystal\"], [data-glass=\"crystal\"] [data-glass-tint], [data-glass=\"crystal\"][data-glass-tint]) {\n /* Gloss rides inside --crystal-stack-bg (baked — sticky/fixed-safe). Shimmer stays the opt-in\n glass-sheen. No saturate(), matching the material. */\n --srf-bg-image: var(--crystal-stack-bg);\n --srf-bg-color: var(--glass-crystal-bg);\n --srf-filter: blur(max(var(--blur-crystal), var(--dif, 0px)));\n --srf-border-color: var(--glass-crystal-border);\n --srf-shadow: var(--glass-crystal-shadow);\n}\n/* Crystal page: ADAPTIVE glass (no explicit material, not veiled) gets the -crystal text tiers, same\n as the material remap in materials.css — the baked gloss lightens title zones there too. */\n[data-glass=\"crystal\"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-crystal);\n --foreground-soft: var(--foreground-soft-crystal);\n --foreground-strong: var(--foreground-strong-crystal);\n --foreground-ui: var(--foreground-ui-crystal);\n --muted-foreground: var(--muted-foreground-crystal);\n}\n/* Chakra page — every adaptive surface becomes a step-cut gem. The cut rides --srf-shadow, so it\n follows each surface's own border-radius with nothing to keep in sync. */\n:where([data-glass=\"chakra\"], [data-glass=\"chakra\"] [data-glass-tint], [data-glass=\"chakra\"][data-glass-tint]) {\n --srf-bg-image: var(--glass-chakra-stack-bg);\n --srf-bg-color: var(--glass-chakra-bg);\n --srf-filter: blur(max(var(--blur-chakra), var(--dif, 0px)));\n --srf-border-color: var(--glass-chakra-border);\n --srf-shadow: var(--glass-chakra-shadow);\n}\n/* Chakra page: adaptive glass (no explicit material, not veiled) takes the -chakra text tiers, same\n as the material remap in materials.css. */\n[data-glass=\"chakra\"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-chakra);\n --foreground-soft: var(--foreground-soft-chakra);\n --foreground-strong: var(--foreground-strong-chakra);\n --foreground-ui: var(--foreground-ui-chakra);\n --muted-foreground: var(--muted-foreground-chakra);\n}\n/* Opaque — solid tinted panels for adaptive surfaces. --glass-solid-a: 1 makes any veiled overlay\n (glass-veil) compute a FULLY solid floor here too, so menus/toasts stay legible on an opaque page. */\n:where([data-glass=\"opaque\"], [data-glass=\"opaque\"] [data-glass-tint], [data-glass=\"opaque\"][data-glass-tint]) {\n /* Flat floor, no sheet — same reasoning as [data-material=\"opaque\"] in materials.css: with\n --srf-filter: none there is no blurred backdrop to mask the dither of a 2–7 code gradient, and the\n sheet's uniform lift is folded into --glass-opaque-l instead. */\n --srf-bg-image: none;\n --srf-bg-color: var(--glass-opaque-bg);\n --srf-filter: none;\n --srf-shadow: var(--glass-shadow);\n /* Same provable no-op as [data-material=\"opaque\"]: --glass-opaque-bg sits in the background-COLOR\n channel, so the solidify layer would paint that colour over itself. A TOKEN here (it used to be an\n element rule keyed on `.glass`), so it reaches an `@apply glass` surface too, which kept the light\n solidify backing at 70% over the opaque floor and read nearly white. An explicit material pins the\n slot back (`initial`), and a veiled surface restores the layer with --veil-solidify. */\n --srf-solidify: none;\n}\n/* --glass-solid-a CANNOT ride in the :where() block above. :where() is specificity ZERO by design (so a\n page style never beats an explicit material), and every other declaration up there survives that\n because nothing else declares them — they are material SLOTS with no competing value. This one is\n different: tokens.css declares --glass-solid-a on :root, which is (0,1,0), so the :where() version\n lost every time and the opaque page never actually got its solid floor. Measured: --glass-solid-a\n resolved to 0.65 on under [data-glass=\"opaque\"], identical to every other page style.\n The visible symptom is any VEILED surface on an opaque page staying translucent while the solid\n panels around it go opaque — menus, toasts and popovers, which is exactly what the block above says\n this is for. A bare attribute selector is (0,1,0), which ties :root and wins on source order because\n globals.css imports styles.css after tokens.css. It is a plain literal, so it simply inherits into\n the tint / toast scopes and needs none of their re-resolution. */\n[data-glass=\"opaque\"] {\n --glass-solid-a: 1;\n}\n/* Opaque page: element-level touches for ADAPTIVE surfaces only (a `.glass` or `[data-surface]` element\n with no explicit material — an explicit frosted/crystal card keeps its translucency AND its normal\n foreground). Declared ON the element because (a) --glass-opaque-outline is settable on any ancestor\n and must resolve where it's read, and (b) the foreground remap must not leak into explicit materials.\n The uniform EDGE applies to every adaptive panel, veiled or not — a veiled card under the opaque page\n still reads as a solid panel and needs the same outline. */\n[data-glass=\"opaque\"] :where(.glass, [data-surface]):not([data-material]) {\n --srf-border-color: var(--glass-opaque-outline, var(--glass-border));\n --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px));\n /* Paint the border HERE, not just the tokens: borderless roles (Card default) never render\n glass-border, so without this only bordered roles showed the outline — \"some cards get it,\n some don't\". Under the opaque page every solid panel takes the uniform edge; bordered elements\n resolve the same vars, so nothing double-draws. */\n border: var(--srf-border-w) solid var(--brd-ink, var(--srf-border-color));\n}\n/* The FOREGROUND remap is :not(.glass-veil) — the same carve-out crystal and chakra make, and the same\n one --veil-solidify makes for the solidify layer on this page style. A veiled card does not paint\n --glass-opaque-bg: glass-veil moves the background-COLOR channel to the neutral --veil-floor, so its\n text sits on the veil surface the BASE tokens are banded for, not on the opaque floor. Remapping it\n here handed those cards ink solved for a surface they do not have — visible as a hue jump, since the\n opaque floor is the one surface with nothing composited over it and so sits at the raw --glass-tint-h\n while every sheer surface lands a few degrees off it (measured on the /colors panel: 22.0 vs 14.6).\n Splitting the rule is what keeps the edge treatment while dropping the foreground one. */\n[data-glass=\"opaque\"] :where(.glass, [data-surface]):not([data-material]):not(.glass-veil, [data-surface=\"veil\"]) {\n --foreground: var(--foreground-opaque);\n --foreground-soft: var(--foreground-soft-opaque);\n --foreground-strong: var(--foreground-strong-opaque);\n --foreground-ui: var(--foreground-ui-opaque);\n --muted-foreground: var(--muted-foreground-opaque);\n}\n/* ── Glass tint presets ─────────────────────────────────────────────────────\n Set data-glass-tint on any element (commonly ) to recolor the glass in that subtree.\n Most presets just move the tint hue/chroma; \"sistine\" is a bespoke multi-hue fresco (gilded\n gold → terracotta flesh tones → azure sky), echoing the Sistine Chapel ceiling.\n Wash + all tint-derived tokens re-resolve per-scope via the :root,[data-glass-tint] split above. */\n/* Selenite — the neutral stone: no chroma and no wash, so the glass is the plain clear sheet. The tint\n values equal the engine default's hue with the colour switched off; the block exists so a CSS-only\n consumer (no tint switcher) gets selenite's colour-wheel anchor, which a hue-less theme needs. */\n[data-glass-tint=\"selenite\"] {\n --glass-tint-h: 250;\n --glass-tint-c: 0;\n --glass-tint-a: 0;\n --harmony-h: 0;\n}\n[data-glass-tint=\"sapphire\"] {\n --glass-tint-h: 255;\n --glass-tint-c: 0.075;\n --glass-tint-a: 0.19;\n --glass-wash-l: 63%;\n --glass-opaque-c-max: 0.056;\n}\n[data-glass-tint=\"aventurine\"] {\n --glass-tint-h: 158;\n --glass-tint-c: 0.076;\n --glass-tint-a: 0.54;\n --glass-wash-l: 88%;\n --glass-opaque-c-max: 0.195;\n}\n[data-glass-tint=\"amethyst\"] {\n --glass-tint-h: 300;\n --glass-tint-c: 0.101;\n --glass-tint-a: 0.15;\n --glass-wash-l: 55%;\n --glass-opaque-c-max: 0.065;\n}\n[data-glass-tint=\"rose\"] {\n --glass-tint-h: 8;\n --glass-tint-c: 0.095;\n --glass-tint-a: 0.17;\n --glass-wash-l: 64%;\n --glass-opaque-c-max: 0.062;\n}\n[data-glass-tint=\"amber\"] {\n --glass-tint-h: 75;\n --glass-tint-c: 0.062;\n --glass-tint-a: 0.38;\n --glass-wash-l: 81%;\n --glass-opaque-c-max: 0.096;\n}\n[data-glass-tint=\"carnelian\"] {\n --glass-tint-h: 38;\n --glass-tint-c: 0.081;\n --glass-tint-a: 0.19;\n --glass-wash-l: 67%;\n --glass-opaque-c-max: 0.062;\n}\n[data-glass-tint=\"peridot\"] {\n --glass-tint-h: 128;\n --glass-tint-c: 0.083;\n --glass-tint-a: 0.54;\n --glass-wash-l: 92%;\n --glass-opaque-c-max: 0.218;\n}\n[data-glass-tint=\"turquoise\"] {\n --glass-tint-h: 190;\n --glass-tint-c: 0.057;\n --glass-tint-a: 0.54;\n --glass-wash-l: 90%;\n --glass-opaque-c-max: 0.144;\n}\n[data-glass-tint=\"aquamarine\"] {\n --glass-tint-h: 215;\n --glass-tint-c: 0.053;\n --glass-tint-a: 0.5;\n --glass-wash-l: 82%;\n --glass-opaque-c-max: 0.091;\n}\n[data-glass-tint=\"tourmaline\"] {\n --glass-tint-h: 342;\n --glass-tint-c: 0.106;\n --glass-tint-a: 0.18;\n --glass-wash-l: 67%;\n --glass-opaque-c-max: 0.079;\n}\n[data-glass-tint=\"lapis\"] {\n /* Lapis lazuli IS the deep saturated ultramarine, and at plain jewel chroma it reads as just another\n blue next to sapphire. It used to buy that depth by declaring c 0.15 — far past the sRGB ceiling —\n and letting the browser clip. That DID deliver a deep blue, but by accident rather than by design:\n clipping chops channels, which drops LIGHTNESS, and the saturation came from landing darker.\n Measured, oklch(58% 0.375 268) renders L48.1 C0.2941 h271 — a colour nobody wrote down.\n The wash lightness below asks for that colour on purpose instead. h268's chroma ceiling PEAKS at\n L46 (0.2979) and collapses to 0.1293 by L74, so the shared 72/58 wash was sitting on the downslope;\n at L46 the same depth is available inside the gamut. oklch(45% 0.291 268) renders L44.9 C0.2909\n h267.9 — the same deep ultramarine, hue exact, nothing pinned.\n So lapis is no longer an exception to anything: it is the clearest case of the rule the whole\n palette now follows — put the wash where the hue can actually hold chroma, then ask for it. */\n --glass-tint-h: 268;\n --glass-tint-c: 0.085;\n --glass-tint-a: 0.13;\n --glass-wash-l: 46%;\n --glass-opaque-c-max: 0.055;\n}\n[data-glass-tint=\"goldstone\"] {\n --glass-tint-h: 22;\n --glass-tint-c: 0.094;\n --glass-tint-a: 0.17;\n --glass-wash-l: 63%;\n --glass-opaque-c-max: 0.06;\n}\n/* ── Per-mode wash ALPHA ──────────────────────────────────────────────────────\n Alpha slides the surface along a straight line from the material floor toward the wash colour, so\n how much a preset can take is set by how far its wash sits from THAT MODE's floor (L36.4 dark /\n L88 light). A wash near the floor barely moves the surface and tolerates a lot; a wash far from it\n crosses the mid-tone band where no text polarity reaches the body floor, and tolerates little.\n Because every wash now sits at its hue's chroma PEAK, dark-peaking hues (lapis L46, amethyst L55)\n sit near the DARK floor and take high alpha there, while light-peaking hues (peridot L92, turquoise\n L90) sit near the LIGHT floor and take high alpha there. The two columns are near mirrors.\n Each value is HALF that mode's measured headroom, so every preset keeps body text >= 75 Lc in both\n modes with room to spare. A single shared alpha cannot serve both ends: at 0.55 everywhere, five\n presets lose dark-mode text and lapis/amethyst lose light-mode text.\n The switcher no longer inlines --glass-tint-a for a named preset (see applyTint), so THESE are the\n values that render — nothing shadows them. */\n.dark[data-glass-tint=\"rose\"] {\n --glass-tint-a: 0.49;\n}\n.dark[data-glass-tint=\"goldstone\"] {\n --glass-tint-a: 0.5;\n}\n.dark[data-glass-tint=\"carnelian\"] {\n --glass-tint-a: 0.45;\n}\n.dark[data-glass-tint=\"amber\"] {\n --glass-tint-a: 0.34;\n}\n.dark[data-glass-tint=\"peridot\"] {\n --glass-tint-a: 0.28;\n}\n.dark[data-glass-tint=\"aventurine\"] {\n --glass-tint-a: 0.29;\n}\n.dark[data-glass-tint=\"turquoise\"] {\n --glass-tint-a: 0.28;\n}\n.dark[data-glass-tint=\"aquamarine\"] {\n --glass-tint-a: 0.31;\n}\n.dark[data-glass-tint=\"sapphire\"] {\n --glass-tint-a: 0.48;\n}\n.dark[data-glass-tint=\"lapis\"] {\n --glass-tint-a: 0.54;\n}\n.dark[data-glass-tint=\"amethyst\"] {\n --glass-tint-a: 0.54;\n}\n.dark[data-glass-tint=\"tourmaline\"] {\n --glass-tint-a: 0.46;\n}\n[data-glass-tint=\"moonstone\"] {\n /* Moonstone — a warm, very-low-chroma off-white glass (reads as cream, not yellow). A normal translucent\n tint: texture-preserving and light-text in both modes, just a desaturated warm hue. Anchors its harmony\n wheel at 0° (like selenite) so accents come out a colorful set, not a warp of the near-neutral hue. */\n --glass-tint-h: 75;\n --glass-tint-c: 0.047;\n --glass-tint-a: 0.18;\n --harmony-h: 0;\n /* Gloss chroma, moonstone's own. The global --glass-gloss-tint is 4.25 — sized for the JEWELS, where a\n 0.07–0.1 tint wants a vivid gem highlight. Moonstone declares 0.047 precisely so it \"reads as cream,\n not yellow\" (above), and 4.25× asks for 0.1998.\n --glass-gloss-c-max (tokens.css) is what stops that becoming a clipped primary, and in LIGHT it does\n the whole job: 0.013 caps both 0.1998 and 0.047 to the same value, so this override changes nothing\n there. It earns its place in DARK, where the cap is a roomy 0.11 — enough that the multiplier would\n still put moonstone's highlight at 2.3× its declared chroma, i.e. amber on a stone that is supposed\n to be pale. 1× holds the specular at the tint's own chroma and lets its LIGHTNESS do the work. */\n --glass-gloss-tint: 1;\n /* Moonstone daytime body lightness. Fitted for the retired opaque sheet (tokens.css) on moonstone's OWN\n floor rather than the global pair: this cream is already near-white, where a near-white sheet moved it\n far less than it moved the standard L90 floor. dE 0.0002 — indistinguishable from the old surface. */\n --glass-opaque-l: 94.5;\n /* NO pinned --glass-opaque-bg: the floor must stay DERIVED from --glass-opaque-l (engine), because the\n lightness slider / stored per-mode L land as an INLINE --glass-opaque-l that shadows this block — a\n literal pin would freeze the paint while AutoForeground keeps banding text against the token (light\n text on cream). Was c-scale 1 — the value that made the derived color exactly the original cream when\n the sheet still painted over it; 0.92 is that same cream now that the sheet is gone (it desaturated as\n well as lightened, so both knobs carry part of it). */\n --glass-opaque-c-scale: 0.92;\n}\n.dark[data-glass-tint=\"moonstone\"] {\n /* Moonstone night. The wash sits a few points above the standard 58% (warmth without glare; h75\n carries more real luminance per OKLCH point than the cool tints, so pale-warm washes read brighter\n than their numbers). AutoForeground reads BOTH wash knobs live (+ the [moonstone-sync] mirrored\n fallback), so page text and per-card dark-cream text re-band automatically.\n\n The FLOOR is what you actually see: at the default opacity every surface — cards, inputs, the nav\n pill — paints --glass-opaque-bg, and the wash only tints what shows through. It used to derive\n oklch(0.8 0.047 75), which is a sand/tobacco tan, so moonstone night read as brown rather than as\n the pale silvery stone it is named for. Lightness alone does not fix that: h75 is warm, so every\n lightness along it lands somewhere in the tan family. Cutting the CHROMA is what moves it off that\n family. Light mode is untouched (it keeps c-scale 1 at L94, where the same hue reads as cream).\n\n L52, down from a cream L84.9 — moonstone night is a DARK stone now, and that is forced rather than\n preferred. This floor feeds two surfaces at once: opaque cards paint it directly, and it composites\n at --glass-opacity over the dark veil to become the floor sheer glass sits on. Both have to clear\n the body floor, and for h75 nothing in L55–L84 reaches it in EITHER text polarity — that band is the\n mid-tone dead zone. A cream L84.9 put the opaque card just above it (Lc 77.1) but dragged the sheer\n surface to L65, dead centre, where body text topped out at 65.4 Lc and the preset had to be pinned\n as a known exception. Going lighter cannot fix it: the veil is 30% of the composite, which caps the\n solidify floor at L75.6 no matter how light this value gets, so the sheer surface can never climb\n out the top. Only the bottom exit is reachable. L52 is the PALEST value where both surfaces clear:\n sheer body text lands 80.0 Lc (was 65.4) and the opaque card 82.9 (was 77.1), and it also restores\n --foreground-strong, which could not reach its 90 target on the cream floor either. Still stone —\n oklch(0.52 0.021 75) is #70675c, a warm grey — and still well clear of the jewels' L36.4. */\n --glass-wash-l: 64%;\n --glass-wash-c-mult: 2;\n /* Fitted for the retired opaque sheet, like the daytime floor above and likewise not the global dark\n pair: moonstone night is a LIGHT floor, so the near-white sheet barely moved it. dE 0.0002. */\n --glass-opaque-l: 52;\n --glass-opaque-c-scale: 0.44;\n /* Static -opaque tier baselines. These flipped polarity with the floor: on the old cream L84.9 they were\n dark warm literals, and on an L52 stone they have to be light or SSR's first paint is dark-on-dark.\n Values are what AutoForeground itself solves for this surface, so the mounted refine is a no-op. */\n --foreground-opaque: oklch(1 0 75);\n --foreground-soft-opaque: oklch(0.9 0.05 75);\n --foreground-strong-opaque: oklch(1 0 75);\n --foreground-ui-opaque: oklch(0.91 0.05 75);\n --muted-foreground-opaque: oklch(0.97 0.02 75);\n /* Chakra needs the SAME treatment opaque gets above, and did not have it: chakra was added after this\n block was written, so it kept the global dark body (L28) and rendered oklch(0.28 0.046 75) = #36250c,\n a dark brown — the one surface where moonstone still read as tobacco rather than stone. Matching\n opaque's pair — now L51, chroma x0.45 → 0.021, landing on #6d6559, the same stone as the opaque\n floor one point above it.\n The hue stays 75 deliberately. Cutting CHROMA is what moves this off the tan family; lightness alone\n does not, and a cool hue would contradict the warm cream light mode keeps at L94. */\n --glass-chakra-l: 51;\n --glass-chakra-c-max: 0.021;\n /* Same reasoning as the -opaque baselines above, and flipped with them: an L51 body needs LIGHT text. */\n --foreground-chakra: oklch(1 0 75);\n --foreground-soft-chakra: oklch(0.9 0.05 75);\n --foreground-strong-chakra: oklch(1 0 75);\n --foreground-ui-chakra: oklch(0.91 0.05 75);\n --muted-foreground-chakra: oklch(0.96 0.03 75);\n}\n\n/* Semantic surfaces (Alert info/success/warning/destructive; destructive Button/Badge; status toasts,\n which the Toaster stamps from sonner's data-type — see TOAST_TINT in components/ui/sonner.tsx) render\n data-glass-tint=\"\" — one block per status, joining the grouped engine re-resolution above so\n --glass-bg / wash re-resolve at the alert/toast. */\n[data-glass-tint=\"info\"] {\n --glass-tint-h: 255;\n --glass-tint-c: 0.075;\n --glass-tint-a: 0.18;\n --glass-wash-l: 63%;\n}\n[data-glass-tint=\"success\"] {\n --glass-tint-h: 150;\n --glass-tint-c: 0.088;\n --glass-tint-a: 0.18;\n --glass-wash-l: 87%;\n}\n[data-glass-tint=\"warning\"] {\n --glass-tint-h: 85;\n --glass-tint-c: 0.063;\n --glass-tint-a: 0.18;\n --glass-wash-l: 84%;\n}\n[data-glass-tint=\"destructive\"] {\n --glass-tint-h: 25;\n --glass-tint-c: 0.094;\n --glass-tint-a: 0.18;\n --glass-wash-l: 63%;\n}\n/* FRESCOES (muse / sistine / aurora / gloaming): each block's --glass-tint-h/c/a MUST mirror its\n GlassTintSwitcher preset (components/glass-tint-switcher.tsx). The switcher INLINES those onto ,\n shadowing these blocks on any page with the switcher mounted — so these are the static-consumer fallback.\n Keep them identical, or the demo and a static page render different surfaces. (--glass-bg gradients and\n --glass-fg-h are CSS-only — no preset counterpart.) */\n\n/* Muse — the cityscape signature, mode-aware: cool daytime blues in light, the neon night sweep in\n dark, so it tracks the day/night background. Pick the look with the light/dark/system toggle. */\n/* Muse is now ONE palette in both modes: the neon city — cyan → violet → magenta → pink → amber. The\n old day sweep (a pale blue → cyan → teal cityscape) is gone; its hues live on in Aurora, which took\n them over.\n\n What differs between the modes is ALPHA, not colour. A day surface sits on a light page and needs\n more of the gradient to register; night sits on a dark one and needs less before the neon blows out.\n Lightness is shared because these stops are already bright (L 78–85), so they read on either page.\n\n Stop 2's chroma is 0.095, not the 0.17 night shipped with: h290 at L80 has an sRGB ceiling of 0.1052,\n so 0.17 was 162% over and has been browser-gamut-mapped this whole time — which also means it\n rendered differently in Safari (per-channel clamp toward grey) than in Chrome (hue-preserving map).\n 0.095 is 90% of its own ceiling, the same rule the rest of the stops follow. */\n[data-glass-tint=\"muse\"] {\n --glass-tint-h: 230;\n --glass-fg-h: 290; /* text/accent anchor: violet, matching night so the two modes share one identity */\n --glass-tint-c: 0.056;\n --glass-tint-a: 0.16;\n --glass-wash-l: 76%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.32) 0%,\n oklch(80% 0.095 290 / 0.3) 22%,\n oklch(78% 0.2 326 / 0.28) 45%,\n oklch(79% 0.12 350 / 0.28) 70%,\n oklch(84% 0.12 74 / 0.3) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.46) 0%,\n oklch(80% 0.095 290 / 0.44) 22%,\n oklch(78% 0.2 326 / 0.42) 45%,\n oklch(79% 0.12 350 / 0.42) 70%,\n oklch(84% 0.12 74 / 0.44) 100%\n );\n /* Light-mode body: the night anchor's hue (288) at a day-legible lightness. Chroma is 90% of the\n ceiling at L91 h288 (0.0446), because a light surface has very little chroma headroom. */\n --glass-opaque-bg: oklch(91% 0.04 288);\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"muse\"] {\n /* Night: the same neon palette as day, at night's lower alphas (a dark page needs less of the\n gradient before the neon blows out). Only alpha and the solid body differ from the block above. */\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.24) 0%,\n oklch(80% 0.095 290 / 0.22) 22%,\n oklch(78% 0.2 326 / 0.2) 45%,\n oklch(79% 0.12 350 / 0.2) 70%,\n oklch(84% 0.12 74 / 0.22) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(85% 0.1 222 / 0.36) 0%,\n oklch(80% 0.095 290 / 0.34) 22%,\n oklch(78% 0.2 326 / 0.32) 45%,\n oklch(79% 0.12 350 / 0.32) 70%,\n oklch(84% 0.12 74 / 0.34) 100%\n );\n --glass-opaque-bg: oklch(34% 0.12 288);\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Sistine — the signature: a painterly fresco of four jewels (amber → rose → sapphire → aventurine,\n in that order), interpolated `in oklch` so the warm→cool transitions stay vivid instead of\n averaging to grey. Border + crystal + accent pick up the rose base via the tint vars. */\n[data-glass-tint=\"sistine\"] {\n --glass-tint-h: 75; /* surface: mirrors the Sistine switcher preset */\n --glass-fg-h: 18; /* text/accent anchor: crimson (hand-selected) — hue 18 reads as deep red, not the rose at h8 */\n --glass-tint-c: 0.062;\n --glass-tint-a: 0.16;\n --glass-wash-l: 81%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(90% 0.11 75 / 0.34) 0%,\n oklch(86% 0.11 8 / 0.3) 33%,\n oklch(86% 0.11 255 / 0.3) 66%,\n oklch(87% 0.11 158 / 0.32) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(90% 0.11 75 / 0.46) 0%,\n oklch(86% 0.11 8 / 0.42) 33%,\n oklch(86% 0.11 255 / 0.42) 66%,\n oklch(87% 0.11 158 / 0.44) 100%\n );\n /* Crystal composes its own background (not --glass-bg), so hand it the fresco explicitly. */\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"sistine\"] {\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.11 75 / 0.22) 0%,\n oklch(82% 0.11 8 / 0.2) 33%,\n oklch(82% 0.11 255 / 0.2) 66%,\n oklch(83% 0.11 158 / 0.22) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.11 75 / 0.32) 0%,\n oklch(82% 0.11 8 / 0.3) 33%,\n oklch(82% 0.11 255 / 0.3) 66%,\n oklch(83% 0.11 158 / 0.32) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Aurora — northern lights: spring-green → teal → violet, interpolated `in oklch` so the cool sweep\n stays vivid. Border + crystal + accent pick up the teal base via the tint vars.\n DAY was authored at L 90/89/84 — within ~5 L of the L-95 page it composites onto — so after the\n alpha only 2.2 L of separation survived and the bands read as haze rather than bands. Night works\n because its stops sit 60+ L above an L-20 page and land 13.6 L clear of it. The fix is that\n separation, not more colour: day already delivered MORE chroma than night (spread 26/29/19 against\n 14/15/13) and still looked washed out, because colour without lightness structure reads as fog.\n Stops now sit at L 76.3/74.7/71.8, which also buys chroma headroom, so each takes ~90% of its OWN\n hue's sRGB ceiling rather than one shared number — h292 violet was clipped to 65% at L 84 and is\n fully in gamut here. Composited: spread 48/40/38 (was 26/29/19), separation 8.0 L (was 2.2).\n How far this could go is bound by TEXT, not by taste. AutoForeground models a fresco surface from\n the tint wash (--glass-tint-c/-a), never from this gradient literal, so it does not re-band when\n these stops move and the contrast has to hold statically. --foreground-soft (the L-40 heading tier)\n is the weakest pair on a light surface and sets the floor: at a composite of L 84 it measures 59.5\n and fails, so the stops are placed to land no darker than ~L 86. Worst measured here is 63.5.\n Frosted gets its OWN alphas rather than the old ~1.37x boost over --glass-bg: with stops this much\n darker, that multiplier drove the violet composite to L 83.4 and soft to 58.4. These land all three\n near L 86.4 instead. The authored shape is unchanged — green, then teal, then violet darkest. */\n/* Aurora now sweeps Muse Day's hues — blue 235 → cyan 205 → teal-green 155 — instead of the old\n green → cyan → violet. LIGHTNESS is untouched, because it is not a style choice here: the day stops\n sit at L 71.8–76.3 to satisfy the --foreground-soft ≥60 Lc bind described above, and Muse Day's own\n L 91–94 is precisely the band these were moved away from when day read as haze.\n\n CHROMA had to be re-derived, it could not travel with the hues. Each stop is 90% of that hue's OWN\n sRGB ceiling at its lightness, which is the rule the previous stops already followed (all three\n measured exactly 90%). A flat number cannot survive a hue change: the old 0.175 is 90% of green's\n ceiling at L76.3 but 127% of blue's, so carrying it over would have clipped stop 1 outright.\n\n Night is re-derived the same way, which also fixes a pre-existing clip: its violet stop asked 0.13\n against a 0.1068 ceiling at h292/L80 — 122% — so that stop has been gamut-mapped by the browser all\n along. */\n[data-glass-tint=\"aurora\"] {\n --glass-tint-h: 205; /* the sweep's midpoint, was 178 */\n --glass-tint-c: 0.054;\n --glass-tint-a: 0.16;\n --glass-wash-l: 86%;\n --glass-bg: linear-gradient(\n 135deg in oklch,\n oklch(76.3% 0.124 235 / 0.4) 0%,\n oklch(74.7% 0.111 205 / 0.38) 50%,\n oklch(71.8% 0.155 155 / 0.38) 100%\n );\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(76.3% 0.124 235 / 0.46) 0%,\n oklch(74.7% 0.111 205 / 0.43) 50%,\n oklch(71.8% 0.155 155 / 0.37) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"aurora\"] {\n --glass-bg: linear-gradient(135deg in oklch, oklch(86% 0.071 235 / 0.22) 0%, oklch(85% 0.127 205 / 0.2) 50%, oklch(80% 0.173 155 / 0.22) 100%);\n --glass-frosted-bg: linear-gradient(\n 135deg in oklch,\n oklch(86% 0.071 235 / 0.32) 0%,\n oklch(85% 0.127 205 / 0.3) 50%,\n oklch(80% 0.173 155 / 0.32) 100%\n );\n --glass-crystal-fresco: var(--glass-bg);\n}\n\n/* Gloaming — dusk: gold → rose → indigo, interpolated `in oklch` so the warm→cool fade keeps its\n glow. Night keeps the warm glow; day uses a dusk-rose anchor (the warm hue reads muddy as light-mode text). */\n[data-glass-tint=\"gloaming\"] {\n --glass-tint-h: 32; /* surface: mirrors the Gloaming switcher preset; text shares it (--glass-fg-h defaults to it) */\n --glass-tint-c: 0.089;\n --glass-tint-a: 0.16;\n --glass-wash-l: 64%;\n --glass-bg: linear-gradient(135deg in oklch, oklch(89% 0.12 62 / 0.32) 0%, oklch(84% 0.14 350 / 0.3) 50%, oklch(78% 0.13 278 / 0.3) 100%);\n --glass-frosted-bg: linear-gradient(135deg in oklch, oklch(89% 0.12 62 / 0.44) 0%, oklch(84% 0.14 350 / 0.42) 50%, oklch(78% 0.13 278 / 0.42) 100%);\n --glass-crystal-fresco: var(--glass-bg);\n}\n.dark[data-glass-tint=\"gloaming\"] {\n --glass-tint-h: 32; /* night keeps the warm glow; day uses the dusk-rose above */\n --glass-bg: linear-gradient(135deg in oklch, oklch(86% 0.12 62 / 0.22) 0%, oklch(80% 0.14 350 / 0.2) 50%, oklch(74% 0.13 278 / 0.22) 100%);\n --glass-frosted-bg: linear-gradient(135deg in oklch, oklch(86% 0.12 62 / 0.32) 0%, oklch(80% 0.14 350 / 0.3) 50%, oklch(74% 0.13 278 / 0.32) 100%);\n --glass-crystal-fresco: var(--glass-bg);\n}\n", "type": "registry:style", "target": "app/globals.css" } diff --git a/registry/theme/globals.css b/registry/theme/globals.css index 7cc3488b..7d4846b1 100644 --- a/registry/theme/globals.css +++ b/registry/theme/globals.css @@ -587,8 +587,8 @@ 0.31–0.79× of its own ceiling, so nothing clips with no cap at all. 0.095 clears the largest ask (tourmaline 0.0901) and therefore throttles nobody, while still bounding a future preset that pushes chroma far past today's values. check-gamut scores each preset against its own ceiling. */ - /* Guard, not a throttle: the largest dark ask is tourmaline at 0.111, so this binds nobody. */ - --glass-opaque-c-max: 0.12; + /* Guard, not a throttle: the largest dark ask is tourmaline at 0.111, so this binds nobody. Declared + in its own rule right after this block (see there for why). */ /* Dark keeps the solidify floor IDENTICAL to the opaque floor — the split exists to buy LIGHT-mode text contrast, and dark already clears the body floor on every preset with margin. Pinning to the opaque values (rather than leaving :root's L92 / ×0.65 to inherit) is what makes this a light-only @@ -716,6 +716,21 @@ --sidebar-ring: oklch(0.6 0.2 250); /* Light blue ring - 3:1 contrast */ } +/* The dark opaque-floor chroma guard, in its own rule so it can OUTRANK the presets. Each jewel pins + --glass-opaque-c-max to its hue's sRGB ceiling at the LIGHT floor (L88, presets.css), in an unscoped + block. On that block and `.dark` tie at (0,1,0) and presets.css loads later, so the light caps + used to win in dark too: amethyst's dark floor rendered C 0.065 against an ask of 0.106 and a ceiling + of 0.189, lapis 0.055 against 0.236, and every blue/purple/red jewel's dark opaque card came out a dull, + greyed version of its tint (23–53% of the chroma its hue can hold at L36.4). The light caps describe + the light floor only; at L36.4 every preset's ask sits well inside its own ceiling, so this guard is + the only cap dark needs. (0,2,0) beats a preset block; `.dark [data-glass-tint]` covers a scoped tint + inside a dark page, and a preset's own `.dark[data-glass-tint="x"]` block still wins by source order. */ +.dark, +.dark[data-glass-tint], +.dark [data-glass-tint] { + --glass-opaque-c-max: 0.12; +} + @layer base { * { @apply border-border outline-ring/50; diff --git a/scripts/check-contrast.mjs b/scripts/check-contrast.mjs index 3ea62977..5977ad7f 100644 --- a/scripts/check-contrast.mjs +++ b/scripts/check-contrast.mjs @@ -137,7 +137,11 @@ for (const p of all) { const washCMult = resolve(p, mode, vars, "glass-wash-c-mult", 2.5); const opaqueL = resolve(p, mode, vars, "glass-opaque-l", dark ? 36.4 : 88); const cScale = resolve(p, mode, vars, "glass-opaque-c-scale", dark ? 1.05 : 0.85); - const cMax = resolve(p, mode, vars, "glass-opaque-c-max", dark ? 0.12 : 0.055); + /* A preset's --glass-opaque-c-max is its ceiling at the LIGHT floor; in dark, tokens.css's + `.dark[data-glass-tint]` guard outranks it, so only a preset's own dark block can pin it there. */ + const cMax = dark + ? (p.dark.get("glass-opaque-c-max") ?? vars.dark.get("glass-opaque-c-max") ?? 0.12) + : resolve(p, mode, vars, "glass-opaque-c-max", 0.055); const solidA = vars[mode].get("glass-solid-a") ?? 0.65; /* The backing under sheer glass is --glass-solidify-*, its own surface since the split (tokens.css) diff --git a/scripts/check-gamut.mjs b/scripts/check-gamut.mjs index 37e09e87..3a563def 100644 --- a/scripts/check-gamut.mjs +++ b/scripts/check-gamut.mjs @@ -83,7 +83,8 @@ function collectPresets() { that no preset actually renders. Captured here and applied per row in the evaluator. */ const vars = new Map(prev.vars ?? []); for (const [, k, v] of body.matchAll(/--([a-z0-9-]+):\s*([\d.]+)%?\s*;/g)) vars.set(k, +v); - out.set(key, { name, dark: !!darkSel, h: h ? +h[1] : prev.h, c: c ? +c[1] : prev.c, vars }); + /* ownVars: what THIS block declares, before a dark-only block is merged with its light base below. */ + out.set(key, { name, dark: !!darkSel, h: h ? +h[1] : prev.h, c: c ? +c[1] : prev.c, vars, ownVars: new Map(vars) }); } } /* A dark-only block (moonstone) inherits the hue/chroma its light block declared. */ @@ -354,6 +355,11 @@ if (!capMatch) { } const TINT_C_HI_CAP = +capMatch[1]; +/* Preset pins that hold for the light floor only (see the evaluator). */ +const LIGHT_ONLY_PINS = new Set([ + "glass-opaque-c-max", +]); + for (const s of surfaces) { const rows = presets .filter((p) => (s.mode === "dark" ? true : !p.dark)) @@ -369,7 +375,11 @@ for (const s of surfaces) { const pinned = s.lname ? p.vars?.get(s.lname) : undefined; const surfaceL = pinned === undefined ? s.l : pinned <= 1 ? pinned * 100 : pinned; const pinnedMult = s.kname ? p.vars?.get(s.kname) : undefined; - const pinnedCap = s.capName ? p.vars?.get(s.capName) : undefined; + /* The jewels' --glass-opaque-c-max pins are their ceilings at the LIGHT floor, and tokens.css's + `.dark[data-glass-tint]` guard outranks them in dark. So in dark only a preset's own dark block + can pin that cap; scoring the light pin there models the leak the guard closed. */ + const lightOnly = s.mode === "dark" && LIGHT_ONLY_PINS.has(s.capName); + const pinnedCap = s.capName ? (lightOnly ? (p.dark ? p.ownVars?.get(s.capName) : undefined) : p.vars?.get(s.capName)) : undefined; const base = s.capped ? Math.min(p.c, TINT_C_HI_CAP) : p.c; let want = base * (pinnedMult ?? s.mult); const cap = pinnedCap ?? s.hardCap; From 748a6ad131335aa4394593268798f8fcfe22acb2 Mon Sep 17 00:00:00 2001 From: WeekendsuperHero <4048475+WeekendSuperhero@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:31:09 -0700 Subject: [PATCH 3/7] new scripts and no utils --- .bun-version | 2 +- .gitignore | 3 + CHANGELOG.md | 17 + app/backdrop/page.tsx | 20 + app/docs/getting-started/page.tsx | 4 +- app/docs/theming/page.tsx | 18 +- app/theme/engine.css | 20 +- app/theme/frescoes.css | 98 +- app/theme/materials.css | 23 +- app/theme/presets.css | 53 +- app/theme/styles.css | 30 +- app/theme/tokens.css | 61 +- app/theme/utilities.css | 10 +- bun.lock | 72 +- bunfig.toml | 5 + components/auto-foreground.tsx | 44 +- components/backdrop-scene.tsx | 46 + components/background-provider.tsx | 4 +- components/glass-tint-switcher.tsx | 36 +- components/pattern-background.tsx | 2118 +++++++++++++++-- docs/color-tokens.md | 2 +- lib/oklch-utils.ts | 71 + package.json | 7 +- public/r/auto-foreground.json | 2 +- public/r/background-controller.json | 6 +- public/r/oklch-utils.json | 2 +- public/r/registry.json | 2 +- public/r/theme.json | 2 +- registry.json | 2 +- registry/theme/globals.css | 299 ++- scripts/bun-test-guard.ts | 9 + scripts/check-contrast.mjs | 45 +- scripts/check-gamut.mjs | 28 +- scripts/check-theme.mjs | 43 +- scripts/export-backgrounds.mjs | 534 +++++ tests/auto-foreground-hue-match.test.tsx | 39 +- tests/auto-foreground-surface-parity.test.tsx | 62 +- tests/fresco-text.test.tsx | 176 ++ tests/pattern-background.test.tsx | 175 ++ 39 files changed, 3574 insertions(+), 616 deletions(-) create mode 100644 app/backdrop/page.tsx create mode 100644 bunfig.toml create mode 100644 components/backdrop-scene.tsx create mode 100644 scripts/bun-test-guard.ts create mode 100644 scripts/export-backgrounds.mjs create mode 100644 tests/fresco-text.test.tsx diff --git a/.bun-version b/.bun-version index 085c0f26..428b770e 100644 --- a/.bun-version +++ b/.bun-version @@ -1 +1 @@ -1.3.14 +1.4.3 diff --git a/.gitignore b/.gitignore index 902c3e90..db0179d6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ /.next/ /out/ +# scripts/export-backgrounds.mjs output +/exports/ + # production /build diff --git a/CHANGELOG.md b/CHANGELOG.md index 99600b80..21544fb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +- **Tooling**: Vitest 4 → 5 and framer-motion 13 → 14. Neither needed code changes: the suite has no `vi.mock`, snapshots or un-awaited async assertions, and its spies are created per test, so Vitest 5's breaking changes (mocks cleared before each test, unawaited assertions failing, and the rest) do not touch it; framer-motion is not imported anywhere. `bun test` now stops with the right commands (`bun run test`, `bun run test:dom`) through a `bunfig.toml` preload: it is Bun's own runner, which ignores `vitest.config.mts` (no jsdom, no setup file), so it reported ~180 false failures such as `localStorage is not defined`. +- **Aurora by day**: the day palette is now BLUE HOUR so the lights show by day. Under the old pale golden-hour sky (L 0.76–0.85 below the zenith, plus a white cirrus veil and a horizon haze) the curtains could only sit below the sky's lightness and read as muddy bands, and the whole sky read washed out; now the upper sky is a saturated twilight blue (zenith L 0.42) and the curtains sit above it in lightness and glow, as they do at night. By day their bright lip lifts a quarter of the way up (the reflection's drops to match), so the warm horizon band and the sun's glare show beneath them, where real aurora fades into a low sun. Day rays are also crisp: a new `--au-crisp` (day 0.3, night 1) scales both the curtain blur and each ray's feathered ramp in and out of the tile (a `rays()` helper builds the three tiles), so the edges are clean and deep blue shows between the rays where the soft ramps used to merge into one pastel haze. The wide violet-pink veil layer steps back to 0.3 by day (0.75 at night), and the horizon glow strip, the cirrus and most of the waterline haze are off by day. The sun is the same banded outrun disc as every other scene: it had been made solid because its band gaps showed the curtains through the disc, and a sky-gradient patch masked to the disc (`.au-sunsky`) now sits behind it, so the gaps show sky. The day reflection is a set of straight STREAKS: with crisp rays, a plain mirror of the curtains read as identical bars ruled down the water (and a wavering, glint-broken version that briefly replaced it read as lights carried along on a belt as they swayed), so by day an inline-SVG reflection (`AuroraStreaks`) stands in for the gradient mirror. Each ray's streak leans as the mirrored ray does with a slight tilt of its own, widens toward the viewer as a reflection does in perspective, is brightest at the waterline and fades with distance, and carries a lighter core down its middle, the shine of calm water by day; the day water itself is lighter, mirroring the sky. The streaks are generated from the same ray tiles as the sky (`AU_TILES`), so each starts under its own ray, and sit in the same `.au-curtain` boxes, so they lean, sway, fade and blur in step with it. Night keeps its soft gradient mirror. +- **Muse is one fresco for both modes**: Aurora took over Muse's old day sweep, so Muse no longer keeps a night twin. Its `.dark` block is gone and one block defines the neon sheet on both pages; the light/dark toggle changes the page under the sheet, not the sheet. A single alpha has to hold on both, and the dark opaque card is the bind (the sheet's bright stops lift its L33 floor toward light text): at the old day alphas that text needed pure white, so the sheet sits at 3/4 of the way from night's alphas to day's (0.26–0.30, frosted 0.38–0.42) and the wash makes room (`--glass-wash-l` 76% → 66%, `--glass-tint-a` 0.16 → 0.1, switcher preset too), which leaves a light surface where it was and a dark one 3.5 L darker. Muse's pinned `--glass-opaque-bg` (h288 violet at L91 / L34) is dropped too, so its opaque floor derives like every jewel's: the Lightness slider now moves a Muse card, AutoForeground models the floor it actually paints, and an opaque Muse card takes the blue tint its other materials already had rather than a fixed violet. Body text: 86.6 Lc light, 92.4 dark. +- **Outline is a page-wide axis on every material**: the switcher's outline toggle used to reach opaque surfaces only (`--glass-opaque-outline`), so on any other material it did nothing. `data-glass-outline` on the root now draws `var(--glass-outline)` (default the accent) at `var(--glass-outline-w)` round every `.glass` and `[data-surface]` element, whatever the material (glass, frosted, crystal, chakra and opaque, bordered or not). The glass utility now resolves one edge (`--srf-edge` / `--srf-edge-w`) in priority order: `borderColor="ink"`, then the outline, then the material's own rim. The old `--glass-opaque-outline(-w)` names are still read as fallbacks, and the switcher keeps its storage keys; its toggle is now labelled "Outline". `check-theme` guards the wiring (`[outline-all]`). +- **Opaque carries the fresco**: the four frescoes (muse, sistine, aurora, gloaming) paint their multi-hue sheet over crystal and chakra; opaque (the material and the opaque page style) now layers the same `--glass-crystal-fresco` slot between its tint wash and floor, so a fresco page reads as one sheet whichever material a surface uses. It had been left out only because nothing modelled text contrast under the sheet. That gap affected every material (see Fixed). +- **Opaque is the glass stack at full opacity**: every other material ends its background stack in the tint wash (the solid film of the preset's colour); opaque dropped it, so its floor had to carry the tint alone and an opaque card read as a darker, greyer cousin of the glass beside it, most visibly as the dull chip in the Button materials row. Opaque (the material and the opaque page style) now paints its floor under the same `--glass-wash-layer`. The floor moves to where floor plus wash lands beside each preset's glass: L93 in light (from 88, its cap scaled by a new `--glass-opaque-cap-scale: 0.55` because the per-preset caps are L88 ceilings) and L33.4 in dark (from 36.4, with the dark chroma scale 0.98 so turquoise stays in gamut there). Measured on lapis, opaque now samples `rgb(196,207,243)` beside glass `rgb(193,206,244)` in light, and `rgb(47,66,159)` beside `rgb(61,82,184)` in dark (it was `rgb(50,61,91)`). The sheer backing keeps its own lightness in both modes (92 / 36.4; moonstone night 52), so the tint switcher's Lightness slider now moves the opaque card in dark as it already did in light, and dark glass gives up 7% of its backing chroma to the shared scale. Text stays above the 76.5 Lc margin on every opaque card (tightest destructive light 76.6, moonstone dark 76.6) and light opaque body text now reaches its full 84 aim; `--glass-border-ink-a` rises to 0.55 to keep the ink border over its non-text floor on the lighter dark cards. AutoForeground, `check-contrast` and the surface-parity and hue-match tests model the composite; `check-gamut` learns the scaled-cap shape and scores dark surfaces by a preset's own dark block. +- **Chase**: Pac-Man and the ghosts are 30% larger (1.3 pellet cells, capped at 44px), and the ghosts have painted eyes, white with pupils looking where they are going; their eyes used to be holes in the sprite, so a pellet behind one read as a stray pupil. STILL (Speed 0 or reduced motion) the maze is a frozen moment of the game instead of an empty board: each lane's runners hold a designed pose as their base transform (which the running animation overrides), Pac mid-row with his mouth open and an eaten trail behind him, his ghost ahead, the caught lanes' ghosts in fright blue just out of reach. With the animations removed they had sat at their keyframe origins, every Pac off-screen to the left and every ghost stacked in a column at the edge. +- **Clouds, refined**: by day the deck reads as cloud rather than fog. The horizon haze over it is thinner (0.75 → 0.4 alpha), the dome tops feather over 18% of their radius instead of a third (so the puffs keep their silhouettes against one another), and the shaded bellies under them sit 8–9 L deeper. The deck stays the authored white and blue-grey, with no theme cast (tinted snow reads dirty). At night the sky takes the same tint cast as the day sky (`--sw-cast`); it ignored the tint entirely, the one horizon scene that looked the same in every theme after dark. - **Theme design fixes for consumers (BREAKING for custom toasters)**: *`@apply glass` surfaces* — the opaque page style now drops the solidify layer through a token on its own scope (`--srf-solidify: none`), so it reaches surfaces with no `.glass` class, and `glass-veil` restores the layer with a non-inheriting `--veil-solidify`; element-level page-style rules also accept a documented `data-surface` marker (`data-surface="veil"` when veiled). New invariants `[surface-marker]` and `[solidify-owner]`. *Toasts* — every `[data-sonner-toast]` rule left the theme (engine blocks, status presets, page styles and `sonner.css`); `Toaster` is now `unstyled`, draws each toast with `glassMaterial({ border, veil, size: "lg" })` and sonner's layout in classes, and stamps `data-glass-tint` from sonner's `data-type` (`error` → `destructive`), so status toasts use the theme's ordinary status blocks. Visible changes: rounder corners, status title colours apply (sonner's unlayered `color: inherit` used to beat them), status edges in their own hue, and default toasts follow the page tint. A consumer with its own Toaster must adopt the new `sonner.tsx`. New invariant `[vendor-free]`; `[status]` reads `TOAST_TINT`. *Scoped gloss* — `data-gloss` now works on any element, not just ``: the gloss and the stacks that bake it are composed in a new engine block C on `:root, .dark, [data-glass-tint], [data-gloss]`, flavors are two leaf switches (`--glass-gloss-is-white` / `--glass-gloss-is-hue`), and a nested `data-glass-tint` keeps the inherited flavor instead of resetting it to tonal. New invariant `[gloss-scope]`; `check-gamut` seeds the hue flavor's L74 for its variants. - **Chakra renames (BREAKING)**: renamed the tint presets Emerald → Aventurine, Bone → Moonstone, and Neutral → Selenite to align the jewel palette with the chakra-stone set. The demo switcher migrates stored legacy names automatically, but consumers targeting `[data-glass-tint="emerald"]` / `[data-glass-tint="bone"]` must update to the new names. - **Material system (BREAKING)**: redesigned the glass API around four **materials** (`glass`, `frosted`, `crystal`, `opaque`) selected with a `material` prop (undefined = adaptive glass, the default; `material="none"` = plain, non-glass), composed with orthogonal **axes**, `border`, `veil` (the legibility floor for menus/overlays), `gradient`, `glow`, `sheen`, while `size` stays the component's own dimension. On raw elements the surface is the single structural `glass` utility plus the `glass-border` / `glass-veil` / `glass-sm` / `glass-lg` / `glass-gradient` / `glass-glow` / `glass-glow-lg` / `glass-sheen` axis classes, with the material set via a `data-material` attribute. Components now live in **one tree** (`components/ui/*`) and resolve their surface through `materialSurface` / `glassMaterial` in `lib/material.ts` instead of hardcoded recipe-class strings. Renamed the crystal gloss attribute `data-crystal` → `data-gloss`. The page-style `data-glass` and tint `data-glass-tint` attributes are unchanged. @@ -16,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Legacy glass API (BREAKING)**: removed the `variant="glass|frosted|crystal|opaque|surface|solid"` surface tiers, the recipe utility classes (`glass-bg`, `glass-surface`, `glass-solid`, `glass-opaque`, `glass-frosted`, `glass-crystal`), the per-component `glass={{ color, blur, transparency, outline }}` prop, replaced by the `glassVars({ tintH, tintC, tintA, opacity, blur, solidA })` helper that emits CSS custom properties, and the `components/ui/glass/*` wrapper tree. ### Added +- **Background export (`bun run export:backgrounds`)**: writes every pattern scene as a still image (wallpapers): light and dark, every sun / moon placement on the horizon scenes, at named sizes (`desktop` 3840×2160 by default, `laptop`, `ultrawide`, `tablet`, `phone`) or any `WxH@scale`, in PNG, WebP or JPEG, for any tint presets and an optional pinned accent. `--format mp4` or `webm` writes seamless video loops of the animated scenes instead (H.264 / VP9 through ffmpeg). Time is stepped, not recorded: every CSS animation is paused and set to each frame's time, so no frame drops and the timing is exact however slow the capture. Every loop is seamless, with no crossfade in the usual case: when a scene's animations share a common cycle of at most `--max-loop` seconds (default 60) the video is exactly that cycle (synthwave and moonrise: 8s at speed 8); otherwise it is RETIMED, a whole-frame length into which every animation fits a whole number of its cycles once its speed is nudged, chosen so the largest nudge is smallest (at the defaults dune ×1.09, chase ×1.14, clouds and aurora ×1.33, forest's slowest fog ×1.6), and the factors are printed. Only past `--max-stretch` (default 1.75×) does it fall back to `--duration` seconds whose last `--fade` seconds are crossfaded into the start, which double-exposes whatever moved meanwhile. `--speed`, `--fps` and the size apply as for stills (video defaults to 1920×1080). Chrome runs with `--run-all-compositor-stages-before-draw`: raster is otherwise asynchronous, and a layer seeked into view from off-screen (a Pac sprite) was captured blank, before its tiles were painted. The scenes are live CSS and inline SVG that no in-page rasterizer paints faithfully, so `scripts/export-backgrounds.mjs` drives a real headless Chrome over the DevTools protocol, with no new dependencies (it finds Chrome, Chromium, Edge or Playwright's cached Chromium, or takes `--chrome`). It seeds theme, tint and accent into a throwaway profile, checks that each frame actually rendered them, and writes to the git-ignored `exports/backgrounds//`. The default run is 46 images in about 70 seconds against `bun run dev`. It photographs a new `/backdrop` route: one scene, full-viewport, picked by the query (`?scene=forest&disc=left&density=…&sand=…&speed=…`), in the visitor's own theme and tint, so the URL also works as a live wallpaper. The forest scene's SVG ids are now scoped per instance (`useId`), because `url(#…)` resolves to the first match in the document, so a second forest on a page painted with the first one's gradients. +- **`forest` pattern scene**: a coastal redwood forest seen from inside it, standing on a hill, at sunrise by day and under the moon by night. Dense here is about where you stand, not how many trees there are: the near ridge holds a few giants whose crowns climb out of the top of the frame on massive, buttressed red trunks, and each of the four ridges behind holds more, smaller trees with fewer tiers, receding by aerial perspective into marine-layer fog that pools under every ridge (no horizon line). The three nearest ridges leave a CLEARING in their trees and are shifted by the sun / moon placement so it always sits under the disc: you look out at the sun or moon through a gap in the giants, with the hill dipping into it, while the ridges behind the giants stand densest right around the disc (their clearings are narrow, so their crowns crowd in and overlap its edges). The trees take dune's cut, faceted look: every crown is a stack of tiers, each a triangle with a FLAT underside and upper edges that SCOOP in, so the green sweeps out to its tips like a drooping bough, each overlapping the one below by more than half its height so a DEEP CUT runs almost to the trunk between tiers; the outline stays straight-sided, columnar through the lower crown and then tapering to a spire or, on a quarter of the trees (old growth), to a flat top. Trees vary in how deep their branches scoop, which side reaches further and how strongly their tiers alternate long and short, and each is TURNED: the line between its lit and shadow sides runs off the trunk axis, so it reads as a cone angled toward or away from the viewer rather than a symmetric cut-out. (On the way there: an underside rising to a point at the trunk stacked every tier into a column of chevrons, a convex domed outline read as puffy, and a multi-spike old-growth top read as a tree with two heads that repeated across the screen.) Every tier splits at the trunk into two facets, shadow and lit, and the lit side follows the sun / moon placement control the way dune's faces follow `--du-light`; for depth each tier casts a translucent shadow band onto the tier below (a light and a mid green on the lit side, two deeper ones in shadow), clipped to the crowns so it never hangs off a deep-cut tip into the sky, kept to the width of the tier it shades with its ends tapered as under a cone, and skipped where a narrow spire tier sits on a broad one (there it floated mid-crown as a dark bar). Trunks are rounded, with a curved buttress flare and a darker strip down each edge (a four-band cylinder with their lit and shadow halves), and striped with deep red-brown vertical FURROWS, dune's ridge striation, that splay into the buttress; the ground is angular and stays BROWN at every distance (a stylistic choice: the trees fade into the haze, but each floor shades from its duff brown to a deeper brown, the trunks fade toward it rather than into fog, and the drifting fog that crosses the floor is thinned so it doesn't wash it out), with dune's sand detail in forest form: a lit rim along the near crests and, on the near floor, a litter of wood streaks and duff specks in browns only (no needles: short dashes at every slant read as green pine needles on the floor), drawn as two SVG patterns of unrelated cell sizes (as dune runs two striation passes at different periods) so neither reads as rows. The disc sits behind the trees, by day crepuscular rays break through the fog in the clearing, and motion is three fog strips drifting on compositor-only loops, stopped by `stopRules` like every scene. The day palette stays mid-tone (nearest shadow facet L 0.58) because light-mode hero copy sits on the giants' crowns. Unlike the other scenes, the trees are INLINE SVG rather than CSS masks: one `` per ridge, its tile drawn once and placed side by side with ``, each part filled by a gradient whose stop colours read the palette variables. As 63 full-viewport masked layers, a light/dark flip re-composited every mask and took ~1.2s in Safari against dune's ~0.2s; as SVG it only refills paths (~65ms against dune's ~45ms in headless Chromium). Each ridge deals its trees into layers in turn, painted back to front (three on the dense ridges, where a crown reaches the tree after its neighbour), since each part is one path with no stacking order inside it and overlapping trees in one layer merged; on ridges without a clearing the tree count is a multiple of the layers, so neighbours across a tile repeat land in different layers too. The geometry comes from a seeded generator, so the forest is identical on server and client, and whole-cycle ridges with trees simply overlapping into the next tile make every ridge repeat seamlessly (about 146 KB of markup while the scene is active, roughly 31 KB gzipped). Tests pin determinism, seamless tiling, that every palette colour a ridge paints with is defined in both modes, that the clearing ridges are anchored to the disc, and that the facets facing the disc are the lit ones. Also: the shared moon and sun now round their size to whole pixels (behind `@supports`, since `round()` reached Chrome only in 125), because WebKit clipped a flat sliver off a filtered disc at fractional widths. - **Seven shadcn items, material-aware**: `kbd` (theme tokens and the glass hairline instead of a surface, no tooltip inversion), `empty` (shadcn's anatomy; plain by default, `variant="glass"` for its own surface, a bordered chip for `EmptyMedia variant="icon"`; supersedes the legacy `empty-state`), `progress` (Slider's track and `--glass-accent` fill), `item` (a surface role per variant: plain `default`, bordered-glass `outline`, borderless-glass `muted`), `field` (plain layout, with shadcn's dead `has-data-checked:` / `group-has-data-horizontal/field:` variants fixed and the separator drawn as two rules instead of a `bg-background` knock-out), `menubar` (Radix Menubar; a bordered glass bar whose menus and sub menus are portaled, veiled glass with the popper snap and `bg-accent` rows; the legacy `menu-bar` story is now `Sistine/MenuBarLegacy`) and `combobox` (Base UI, as shadcn now uses, via the new `@base-ui/react@^1.9.0` dependency; veiled glass popup, a bordered small-tier chips field, and shadcn's never-matching `data-open:data-closed:animate-out` fixed). Each ships as a registry item with a docs preview, a code example, a story and tests (`menubar`, `combobox`, `field`, `progress`, `item`). `form` and `split-pane` were not adopted: Field with a form library's Controller covers the first, `resizable` the second. - **`borderColor="ink"` axis**: the material edge is a near-white rim at the material's alpha, which reads as glass on a dark page and as nothing on most light themes at any weight, so `border="rim" | "frame"` had no visible use there. `borderColor="ink"` (class `glass-border-ink`) draws the border in the element's own text colour, `--foreground` at `--glass-border-ink-a` (0.5), through `MaterialProps`, `splitAxisProps`, `glassMaterial` and role defaults in `materialSurface`; it also colours the opaque page's uniform panel edge. `--brd-ink` is registered non-inheriting, so an inked panel does not ink the borders inside it. `check-contrast` gains an `[ink]` check that the line clears the APCA non-text floor (30 Lc) on the sheer and opaque surface of every preset in both modes (tightest 31.6); `tests/material.test.tsx` pins the class output, role inheritance and that the prop never reaches the DOM. - **ARC Bronze contrast**: introduced APCA contrast banding targeting for size-tiered text foregrounds and icons. @@ -32,6 +42,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Horizon scenes**: added three pure-CSS animated pattern backgrounds on the shared 42% horizon, all drawn from photographic references rather than illustration: `clouds` (a deep-blue altitude sky over a parallax cloud deck, with a placeable moon), `dune` (golden-hour ridges receding into haze, blowing sand, a placeable moon), and `aurora` (borealis curtains mirrored in rippling water below the waterline). None of the three draws a horizon *line*: the sky dissolves into the ground across a mottled haze band, and distant layers wash toward the sky's own value. All are light/dark aware, take a half-strength theme cast in the mid-sky, move on compositor-only transforms, and respect reduced motion. `clouds` and `dune` swap the disc per mode: a **sun** by day (a hot core with a wide bloom, sitting low in the warm band for `dune`) and a prominent moon by night, from one shared rule, and `dune` adds a **blowing-sand control** (`breeze` / `storm` / `still`) driving wind veils that stream across the ridges. ### Fixed +- **Aurora's horizon looked drawn on at night**: a cyan rule ran along the waterline, through the dark gaps between the curtains as much as under them. It was several things meeting on the line. Two full-width glow strips (above the line, and its reflection below) and the haze band all peaked exactly on it, the same at every x; both strips are gone, and the haze is flat-topped and thinner. The sky curtains ended hard 1vh past the line and showed through the water sheet as it faded in (a bright strip with a step under it), and the reflection's own blur ate its first few px (a dark one); both sets of curtains now run 3vh past the line, the sky ones clipped exactly on it and the mirror ones by the water box, so each is at full strength right to the line, and their skew pivots mirror each other through it. The water sheet's edge, meant to sit on the line, sat a quarter-percent above it (8% of its height rather than 4vh), and the ripples started at full strength on it (a 1px highlight on the line, a dark stripe 14px under it); the sheet is pinned in vh and the ripples fade in over 6vh below the line, on a still wrapper so the fade does not drift with them. Across the line, measured: night 113 → 111, day 170 → 169, no step, ridge or dip. One seam was left that an averaged profile cannot show: the reflection is blurred more than the sky (×1.5 against ×1.15), so where they meet every column changed profile, bright ones stepping down and the gaps between them stepping up, which averages out but drew a hard edge along the horizon on the static night scene (6.5 levels per column across one px in Safari, against 0.3–0.5 between other rows). At night the reflection now takes the sky's blur (`--au-mirror-blur-m`), and the step at the line is the same as between any two rows in WebKit (0.4) and Chromium (0.7). By day the softer mirror is unchanged: its reflection starts below the line, where it never meets the sky. +- **Chase: eaten pellets grew back**: Pac's wake, which erases the pellets he eats, was a 34vw gradient that rode behind him, so the dots reappeared right behind him and the next pass ate them again. It is now a strip from his mouth back past the left edge, on its own runner in step with his, that fades only once he is off-screen, so the row stays eaten for the whole pass and refills between passes. Every wake now sits in a layer below every sprite, so a Pac or ghost crossing another lane is no longer painted over by that lane's wake. +- **Synthwave and moonrise floors hitched once per loop**: the floor scrolls one grid cell per loop, `4vmin`, which is fractional on most windows (28.8px at 720px tall), while the gradient tile is painted at whole pixels, so each loop ended a fraction of a pixel off where it began and the floor jumped, magnified by its tilt. The cell is now rounded to a whole pixel (`round(4vmin, 1px)` where supported), so the scroll lands exactly on the tile. Found by the video export, which measures every frame step: the wrap step was larger than any other step in the loop, and is now in line with them. +- **Moving the sun / moon tore it in Safari**: changing the left / center / right placement on any horizon scene left the old disc's top half painted at the old spot and drew only the lower half at the new one. The disc carries a drop-shadow filter and a mask, and WebKit repainted only part of its old and new areas when its `left` changed. The shared sun and moon now get their own compositing layer (`will-change: transform`), so a move just repositions the layer. +- **Scoped tints lost their night overrides**: preset and fresco dark blocks were written as compound selectors (`.dark[data-glass-tint="lapis"]`), which only match when `.dark` and the tint sit on the same element, i.e. a page-wide tint. A preset scoped inside a dark page (`
    `) never matched them and kept its light-mode values for the dark-only tweaks (lapis's tint alpha, moonstone's lifted night floor, each fresco's night sheet). All 17 dark blocks (13 presets, 4 frescoes) now also list the descendant form (`.dark [data-glass-tint="lapis"]`), and `check-theme` fails any night block without its twin (`[night-scope]`). +- **A fresco page's sheet leaked into scoped single-hue tints**: `--glass-crystal-fresco` was declared only by the fresco blocks and inherits, so a `
    ` on a muse page painted muse's sweep over its crystal, chakra (and now opaque) surfaces. Engine block A resets the slot to `none` at every tint scope; the fresco blocks, later in source order, set it back. +- **Text under a fresco was banded against the plain surface**: AutoForeground solved text against the surface without the fresco sheet, so wherever a stop pulled the surface toward the text's own polarity body text dipped to 66–74 Lc. It now reads `--glass-crystal-fresco`, parses its stops (`parseGradientStops`) and solves each tier against the stop composite that leaves the least contrast for that polarity (`limitingSurface`, both new in `lib/oklch-utils.ts`). `check-contrast` does the same: the 75 Lc body floor is scored at the hardest stop and the 1.5 Lc margin against the plain surface. Gloaming's tint alpha drops 0.16 → 0.12 (preset and switcher) so its light opaque card clears the floor under the violet stop (74.6 before). `check-gamut` also strips comments before matching preset selectors, so a selector quoted in a comment can no longer capture another preset's dark block. New `tests/fresco-text.test.tsx`. - **Dark opaque cards were throttled by the light-mode caps**: each jewel pins `--glass-opaque-c-max` to its hue's sRGB ceiling at the LIGHT floor (L88), in an unscoped preset block that ties `.dark`'s 0.12 guard on specificity and won on source order. So in dark the seven blue/purple/red jewels rendered their opaque floor (and the dark solidify backing, which shares it) at 23–53% of the chroma their hue holds at L36.4: amethyst C 0.065 instead of its 0.106 ask, lapis 0.055 instead of 0.089. The dark guard now has its own `.dark, .dark[data-glass-tint], .dark [data-glass-tint]` rule that outranks the light pins, so dark floors render the documented `tint × 1.05` (+40–60% chroma on sapphire, amethyst, rose, carnelian, tourmaline, lapis and goldstone; the other presets were already uncapped). Opaque body text holds ~81 Lc either way. `check-gamut` and `check-contrast` now model those pins as light-only instead of scoring the leak. - **Component one-liners from the first consumer's patch list**: `CommandItem` dropped its bare `data-[disabled]:` variants, which matched the `data-disabled="false"` cmdk puts on every enabled item and made no picker entry clickable by mouse (items are now `cursor-pointer`; pinned by `tests/command.test.tsx`). `DropdownMenuSubContent` and `ContextMenuSubContent` are portaled like their parent content, so a glass menu's `backdrop-filter` no longer clips its sub menu. `navigation-menu`'s `bg-foreground/10/50` classes, which Tailwind drops silently, are `bg-accent/50`, and `check-theme` gains a `[double-slash]` guard. Every menu row (dropdown, context, select, command, navigation, menu-bar) now highlights in `bg-accent`, so a consumer's `--accent` reaches it. The `ghost` button is borderless (`outline` is the bordered one, as in shadcn). `Input` and `Textarea` no longer scale up on focus; the ring marks it. `Switch` gains a hover state. `auto-foreground` exports `FG_STORAGE_KEY`, `RAMP_KEY` and `FG_EVENT`. - **Theme one-liners from the same list**: the theme no longer maps `--font-sans` / `--font-mono` to `--font-geist-*`, which only this site defines, so a consumer without Geist gets Tailwind's default stacks instead of nothing; the mapping moved to the site-only `app/site.css`, and the site's next/font variables moved to ``, where `:root` resolves them (the site itself had been rendering the system stack). `--veil-floor` is registered `inherits: false`, so a glass surface nested in a veiled one no longer paints its parent's veil. `@keyframes shimmer` is namespaced `sistine-shimmer` and animates `translate`, so the skeleton's highlight sweeps -100% → 100% instead of composing with `-translate-x-full` and parking on top of it. Selenite gets a `[data-glass-tint="selenite"]` block carrying its `--harmony-h: 0` anchor, so a CSS-only consumer has one. Comments: `bun run` instead of `pnpm`, and the browserslist floor is documented as Sistine's own, which consumers should declare for themselves. diff --git a/app/backdrop/page.tsx b/app/backdrop/page.tsx new file mode 100644 index 00000000..360dc0e4 --- /dev/null +++ b/app/backdrop/page.tsx @@ -0,0 +1,20 @@ +import { Suspense } from "react"; +import { BackdropScene } from "@/components/backdrop-scene"; + +export const metadata = { + title: "Backdrop · Sistine", + description: "A single Sistine pattern scene, full-screen: for wallpapers and for scripts/export-backgrounds.mjs.", + robots: { + index: false, + }, +}; + +/* The scene reads the query, which a statically exported page only knows in the browser, so it renders + client-side under this boundary (the build fails on useSearchParams without one). */ +export default function BackdropPage() { + return ( + + + + ); +} diff --git a/app/docs/getting-started/page.tsx b/app/docs/getting-started/page.tsx index cb6cb849..df70aafa 100644 --- a/app/docs/getting-started/page.tsx +++ b/app/docs/getting-started/page.tsx @@ -225,13 +225,13 @@ export function Example() { :root { --glass-sheet-a: 0.11; /* glass sheet alpha (how much body) */ --glass-border-a: 0.16; /* edge alpha */ - --glass-opaque-l: 88; /* opaque floor lightness */ + --glass-opaque-l: 93; /* opaque card floor lightness (under the tint wash) */ } .dark { --glass-sheet-a: 0.05; --glass-border-a: 0.15; - --glass-opaque-l: 36.4; + --glass-opaque-l: 33.4; }`} /> diff --git a/app/docs/theming/page.tsx b/app/docs/theming/page.tsx index 2106b180..063503c2 100644 --- a/app/docs/theming/page.tsx +++ b/app/docs/theming/page.tsx @@ -99,7 +99,7 @@ export default function ThemingPage() { "opaque", 'material="opaque"', 'data-material="opaque"', - "fully solid, no see-through", + "fully solid, no see-through: the same tint (and a fresco's sweep) as the glass beside it, at full opacity", ], ].map(([m, prop, attr, look]) => (
    @@ -752,8 +752,9 @@ const onAccent = useReadableForeground(accent, "large"); harmonic accents together. 0 = neutral.
  • - --glass-opaque-l: tint body lightness (OKLCH L); lower = a deeper tint. Mode-aware default (88 light / 36.4 - dark), and the floor AutoForeground bands opaque-card text against. + --glass-opaque-l: the opaque card's floor lightness (OKLCH L); lower = a deeper tint. The card is this floor + under the same tint wash every material carries, so it reads as the glass stack at full opacity. Mode-aware default (93 light / 33.4 + dark), and the surface AutoForeground bands opaque-card text against.
  • --glass-opacity: solidity floor for any glass element: 0 = the material's natural sheer glass,{" "} @@ -769,10 +770,13 @@ const onAccent = useReadableForeground(accent, "large"); --glass-stain-amount: how completely stained strips the backdrop's color (0–1; default 1).
  • - --glass-opaque-outline: optional accent outline for opaque surfaces. Unset → the plain glass border; set it - (e.g. var(--glass-accent)) on any ancestor to give every opaque component a colored edge. --glass-opaque-outline-w{" "} - sets its weight (the switcher's hairline / rim / frame = 1 / 2 / 4px); an element-level border="rim" /{" "} - "frame" still wins. + data-glass-outline + --glass-outline: an optional outline on every glass surface, whatever its + material. Put data-glass-outline on <html> (or any ancestor) and every surface below it, borderless ones + included, gets an edge in the theme accent; --glass-outline recolours it (on its own, without the attribute, it recolours + only the edges surfaces already draw). --glass-outline-w sets the weight of every glass edge (the switcher's hairline / + rim / frame = each material's own hairline / 2 / 4px). An element-level border="rim" /{" "} + "frame" and borderColor="ink" still win. The old opaque-only names,{" "} + --glass-opaque-outline / -w, are still read.
  • --glass-tint-a: the tint film alpha (per preset). diff --git a/app/theme/engine.css b/app/theme/engine.css index c6afcee7..287a362b 100644 --- a/app/theme/engine.css +++ b/app/theme/engine.css @@ -50,8 +50,8 @@ /* Foreground hue — drives every FOREGROUND-keyed token (body/heading text via AutoForeground, --muted-foreground, --glass-accent, --glass-glow, --gradient*), kept SEPARATE from the surface hue so tuning text never shifts the glass/fresco surface. Defaults to the tint hue (jewels: text - == surface); frescoes override it to anchor text off their multi-hue surface (Muse night: surface - 222, text 310). AutoForeground reads this, falling back to --glass-tint-h for older consumers. */ + == surface); frescoes override it to anchor text off their multi-hue surface (Muse: surface + 230, text 290). AutoForeground reads this, falling back to --glass-tint-h for older consumers. */ --glass-fg-h: var(--glass-tint-h); /* ── Harmonic color system ──────────────────────────────────────────────────────────────────── Color-wheel relationships derived from the CONTENT hue (--glass-fg-h) — the same hue --glass-accent @@ -117,6 +117,11 @@ renders what it asks for. */ --glass-gloss-c: 0.12; --glass-gloss-hue-dir: 1; /* gloss direction (±1) — flips the TONAL streak diagonal (calc angle) + the HUE sweep order (span × dir); toggled in the crystal demo */ + /* The fresco slot, reset at every tint scope. The four fresco presets (frescoes.css, later in source + order) point it at their multi-hue sheet; every other preset leaves it at `none`. Without the reset a + single-hue preset scoped inside a fresco page (
    on a muse page) + inherited the page's sheet, and its crystal, chakra and opaque surfaces painted muse's sweep. */ + --glass-crystal-fresco: none; --blur-crystal: 2px; } @@ -175,8 +180,13 @@ for 7 of the 12 jewels: it clipped first, and every preset's chroma budget was set by it. Capping it is what let the tints below be normalised to their hue's real capacity rather than to this floor's. Multiply-THEN-cap, matching --glass-gloss-ink. */ + /* The cap is scaled per mode (--glass-opaque-cap-scale): the per-preset caps are L88 ceilings, and the + light floor now sits at L93, where they would overshoot. Composed here, not on :root, so a scoped + preset's own cap is the one scaled. */ --glass-opaque-bg: oklch( - calc(var(--glass-opaque-l) * 1%) min(calc(var(--glass-tint-c) * var(--glass-opaque-c-scale)), var(--glass-opaque-c-max, 0.3)) var(--glass-tint-h) + calc(var(--glass-opaque-l) * 1%) + min(calc(var(--glass-tint-c) * var(--glass-opaque-c-scale)), calc(var(--glass-opaque-c-max, 0.3) * var(--glass-opaque-cap-scale, 1))) + var(--glass-tint-h) ); /* The same floor at the SOLIDIFY lightness/cap — the backing `glass` paints under every sheer material (see --glass-solidify in utilities.css). Split from --glass-opaque-bg because an opaque @@ -224,8 +234,8 @@ scrapes engine.css for both the calc() and min() chroma shapes — the suite went 20 → 24 surface/mode pairs when these landed, so a later retune of the caps is measured, not asserted. SURFACE hue (--glass-tint-h), not --glass-fg-h: an edge belongs to the surface it bounds, matching - --glass-border / --glass-crystal-border. In a fresco that splits the two (Muse night: surface 222, - text 310) the border follows the panel, not the type. */ + --glass-border / --glass-crystal-border. In a fresco that splits the two (Muse: surface 230, + text 290) the border follows the panel, not the type. */ --border: oklch(var(--border-l) min(var(--glass-tint-c), var(--border-c-max)) var(--glass-tint-h)); --input: oklch(var(--input-l) min(var(--glass-tint-c), var(--border-c-max)) var(--glass-tint-h)); /* The remaining shadcn SURFACE tokens — the last chroma-0 values in the theme. Same shape as --border diff --git a/app/theme/frescoes.css b/app/theme/frescoes.css index d5c7a8a2..83eebb16 100644 --- a/app/theme/frescoes.css +++ b/app/theme/frescoes.css @@ -4,67 +4,47 @@ Keep them identical, or the demo and a static page render different surfaces. (--glass-bg gradients and --glass-fg-h are CSS-only — no preset counterpart.) */ -/* Muse — the cityscape signature, mode-aware: cool daytime blues in light, the neon night sweep in - dark, so it tracks the day/night background. Pick the look with the light/dark/system toggle. */ -/* Muse is now ONE palette in both modes: the neon city — cyan → violet → magenta → pink → amber. The - old day sweep (a pale blue → cyan → teal cityscape) is gone; its hues live on in Aurora, which took - them over. +/* Muse — the neon city: cyan → violet → magenta → pink → amber, ONE definition for both modes. Its old + day sweep (a pale blue → cyan → teal cityscape) lives on in Aurora, so Muse no longer needs a night + twin: no .dark block, and the light/dark toggle changes the page under the sheet, not the sheet. - What differs between the modes is ALPHA, not colour. A day surface sits on a light page and needs - more of the gradient to register; night sits on a dark one and needs less before the neon blows out. - Lightness is shared because these stops are already bright (L 78–85), so they read on either page. + Lightness is shared because the stops are already bright (L 78–85), so they read on either page. + Stop 2's chroma is 0.095 because h290 at L80 has an sRGB ceiling of 0.1052; the 0.17 it once shipped + with was browser-gamut-mapped (and rendered differently in Safari than in Chrome). 0.095 is 90% of + its own ceiling, the rule every stop follows. - Stop 2's chroma is 0.095, not the 0.17 night shipped with: h290 at L80 has an sRGB ceiling of 0.1052, - so 0.17 was 162% over and has been browser-gamut-mapped this whole time — which also means it - rendered differently in Safari (per-channel clamp toward grey) than in Chrome (hue-preserving map). - 0.095 is 90% of its own ceiling, the same rule the rest of the stops follow. */ + One alpha has to hold on BOTH pages, and the dark opaque card is the bind: there the sheet's bright + stops lift an L33 floor toward the text, and at the old day alphas (0.28–0.32) light text needed pure + white, past the tonal clip, so the ink lost the theme's hue. The sheet sits at 3/4 of the way from + the old night alphas to the old day ones, and the wash makes the room: L66 at 0.10 (was L76 at 0.16) + lands a light surface where it was (≈ L90) and a dark one 3.5 L darker. Measured: body 86.6 Lc light, + 92.4 dark; the dark opaque ink stays tinted. + + No pinned --glass-opaque-bg: the opaque floor DERIVES from --glass-opaque-l like every jewel, so the + switcher's Lightness slider moves a Muse card and AutoForeground models the floor it actually paints + (the old pins were h288 violet at L91 / L34, against a model of h230 at L93 / L33.4). */ [data-glass-tint="muse"] { --glass-tint-h: 230; - --glass-fg-h: 290; /* text/accent anchor: violet, matching night so the two modes share one identity */ + --glass-fg-h: 290; /* text/accent anchor: violet, the sheet's second stop */ --glass-tint-c: 0.056; - --glass-tint-a: 0.16; - --glass-wash-l: 76%; + --glass-tint-a: 0.1; + --glass-wash-l: 66%; --glass-bg: linear-gradient( 135deg in oklch, - oklch(85% 0.1 222 / 0.32) 0%, - oklch(80% 0.095 290 / 0.3) 22%, - oklch(78% 0.2 326 / 0.28) 45%, - oklch(79% 0.12 350 / 0.28) 70%, - oklch(84% 0.12 74 / 0.3) 100% + oklch(85% 0.1 222 / 0.3) 0%, + oklch(80% 0.095 290 / 0.28) 22%, + oklch(78% 0.2 326 / 0.26) 45%, + oklch(79% 0.12 350 / 0.26) 70%, + oklch(84% 0.12 74 / 0.28) 100% ); --glass-frosted-bg: linear-gradient( 135deg in oklch, - oklch(85% 0.1 222 / 0.46) 0%, - oklch(80% 0.095 290 / 0.44) 22%, - oklch(78% 0.2 326 / 0.42) 45%, - oklch(79% 0.12 350 / 0.42) 70%, - oklch(84% 0.12 74 / 0.44) 100% + oklch(85% 0.1 222 / 0.42) 0%, + oklch(80% 0.095 290 / 0.4) 22%, + oklch(78% 0.2 326 / 0.38) 45%, + oklch(79% 0.12 350 / 0.38) 70%, + oklch(84% 0.12 74 / 0.4) 100% ); - /* Light-mode body: the night anchor's hue (288) at a day-legible lightness. Chroma is 90% of the - ceiling at L91 h288 (0.0446), because a light surface has very little chroma headroom. */ - --glass-opaque-bg: oklch(91% 0.04 288); - --glass-crystal-fresco: var(--glass-bg); -} -.dark[data-glass-tint="muse"] { - /* Night: the same neon palette as day, at night's lower alphas (a dark page needs less of the - gradient before the neon blows out). Only alpha and the solid body differ from the block above. */ - --glass-bg: linear-gradient( - 135deg in oklch, - oklch(85% 0.1 222 / 0.24) 0%, - oklch(80% 0.095 290 / 0.22) 22%, - oklch(78% 0.2 326 / 0.2) 45%, - oklch(79% 0.12 350 / 0.2) 70%, - oklch(84% 0.12 74 / 0.22) 100% - ); - --glass-frosted-bg: linear-gradient( - 135deg in oklch, - oklch(85% 0.1 222 / 0.36) 0%, - oklch(80% 0.095 290 / 0.34) 22%, - oklch(78% 0.2 326 / 0.32) 45%, - oklch(79% 0.12 350 / 0.32) 70%, - oklch(84% 0.12 74 / 0.34) 100% - ); - --glass-opaque-bg: oklch(34% 0.12 288); --glass-crystal-fresco: var(--glass-bg); } @@ -94,7 +74,8 @@ /* Crystal composes its own background (not --glass-bg), so hand it the fresco explicitly. */ --glass-crystal-fresco: var(--glass-bg); } -.dark[data-glass-tint="sistine"] { +.dark[data-glass-tint="sistine"], +.dark [data-glass-tint="sistine"] { --glass-bg: linear-gradient( 135deg in oklch, oklch(86% 0.11 75 / 0.22) 0%, @@ -130,9 +111,9 @@ Frosted gets its OWN alphas rather than the old ~1.37x boost over --glass-bg: with stops this much darker, that multiplier drove the violet composite to L 83.4 and soft to 58.4. These land all three near L 86.4 instead. The authored shape is unchanged — green, then teal, then violet darkest. */ -/* Aurora now sweeps Muse Day's hues — blue 235 → cyan 205 → teal-green 155 — instead of the old +/* Aurora now sweeps the retired Muse day sweep's hues — blue 235 → cyan 205 → teal-green 155 — instead of the old green → cyan → violet. LIGHTNESS is untouched, because it is not a style choice here: the day stops - sit at L 71.8–76.3 to satisfy the --foreground-soft ≥60 Lc bind described above, and Muse Day's own + sit at L 71.8–76.3 to satisfy the --foreground-soft ≥60 Lc bind described above, and that sweep's own L 91–94 is precisely the band these were moved away from when day read as haze. CHROMA had to be re-derived, it could not travel with the hues. Each stop is 90% of that hue's OWN @@ -162,7 +143,8 @@ ); --glass-crystal-fresco: var(--glass-bg); } -.dark[data-glass-tint="aurora"] { +.dark[data-glass-tint="aurora"], +.dark [data-glass-tint="aurora"] { --glass-bg: linear-gradient(135deg in oklch, oklch(86% 0.071 235 / 0.22) 0%, oklch(85% 0.127 205 / 0.2) 50%, oklch(80% 0.173 155 / 0.22) 100%); --glass-frosted-bg: linear-gradient( 135deg in oklch, @@ -178,13 +160,17 @@ [data-glass-tint="gloaming"] { --glass-tint-h: 32; /* surface: mirrors the Gloaming switcher preset; text shares it (--glass-fg-h defaults to it) */ --glass-tint-c: 0.089; - --glass-tint-a: 0.16; + /* 0.12, down from 0.16: with text solved against the gradient's hardest stop (the L78 violet, over a + dusk-rose wash that already sits dark), the light opaque card could not reach the 75 Lc body floor + even in pure black (74.6). 0.14 reached it only in black; 0.12 keeps the ink tinted. */ + --glass-tint-a: 0.12; --glass-wash-l: 64%; --glass-bg: linear-gradient(135deg in oklch, oklch(89% 0.12 62 / 0.32) 0%, oklch(84% 0.14 350 / 0.3) 50%, oklch(78% 0.13 278 / 0.3) 100%); --glass-frosted-bg: linear-gradient(135deg in oklch, oklch(89% 0.12 62 / 0.44) 0%, oklch(84% 0.14 350 / 0.42) 50%, oklch(78% 0.13 278 / 0.42) 100%); --glass-crystal-fresco: var(--glass-bg); } -.dark[data-glass-tint="gloaming"] { +.dark[data-glass-tint="gloaming"], +.dark [data-glass-tint="gloaming"] { --glass-tint-h: 32; /* night keeps the warm glow; day uses the dusk-rose above */ --glass-bg: linear-gradient(135deg in oklch, oklch(86% 0.12 62 / 0.22) 0%, oklch(80% 0.14 350 / 0.2) 50%, oklch(74% 0.13 278 / 0.22) 100%); --glass-frosted-bg: linear-gradient(135deg in oklch, oklch(86% 0.12 62 / 0.32) 0%, oklch(80% 0.14 350 / 0.3) 50%, oklch(74% 0.13 278 / 0.32) 100%); diff --git a/app/theme/materials.css b/app/theme/materials.css index 85701b6a..59411ebf 100644 --- a/app/theme/materials.css +++ b/app/theme/materials.css @@ -121,7 +121,14 @@ floor, not --glass-opaque-bg, so the layer is real (dropping it there would shift up to 31/255 in light and 42/255 in dark). */ --srf-solidify: none; - /* NO sheet either — opaque now paints a FLAT background-color and nothing else. --glass-bg is a 5-stop + /* The WASH, and only the wash. Every other material's stack ends in --glass-wash-layer, the solid film + of the preset's colour that carries the tint over any backdrop; opaque used to drop it with the + sheet, so its floor had to carry the tint alone and the opaque card read as a darker, greyer cousin + of the glass beside it (in dark mode most of all: amethyst glass L44 C0.18, opaque L36 C0.11). With + the wash, opaque is the glass stack at full opacity: within 0.7 L of its glass siblings in light and + 2–4 L above them in dark, where no darker page shows through. The wash is one flat colour (both + stops equal), so it rasterises without the dither the sheet's gradient caused (below). + NO sheet — --glass-bg is a 5-stop gradient whose total range, composited over this floor, is 2–4 sRGB codes across an entire card in light and 5–7 in dark: one code per ~100–200px, far too shallow to draw cleanly in 8-bit, so the rasteriser dithers rather than bands. On every other material a real backdrop-filter supplies blurred @@ -130,13 +137,17 @@ The sheet's GRADIENT was therefore invisible, but its uniform LIFT was not (+1.7 L light, +4.8 L dark), so it is folded into --glass-opaque-l rather than dropped: see tokens.css. AutoForeground reads that same token, so the -opaque text tiers re-band against the new floor automatically. */ - --srf-bg-image: none; + /* The fresco slot leads, as in the crystal and chakra stacks: unset (`none`) on a single-hue preset, the + fresco's own multi-hue gradient on muse / sistine / aurora / gloaming, which opaque used to lose + entirely and render as one flat anchor hue. Its stops have a real range, unlike the sheet's 2–7 + codes, so it rasterises without the dither the sheet was dropped for. */ + --srf-bg-image: var(--glass-crystal-fresco, none), var(--glass-wash-layer); --srf-bg-color: var(--glass-opaque-bg); --srf-filter: none; - --srf-border-color: var(--glass-opaque-outline, var(--glass-border)); - /* Outline WEIGHT: an element rim/frame class (--brd-w) beats the page-level outline weight (the - switcher's --glass-opaque-outline-w on ), which beats the 1px hairline. */ - --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px)); + /* The material's own edge. A page outline (--glass-outline / -w) overrides it in `glass`'s --srf-edge, + the same as for every other material. */ + --srf-border-color: var(--glass-border); + --srf-border-w: var(--brd-w, 1px); --srf-shadow: var(--glass-shadow); /* Veiled overlays INSIDE an opaque element compute a fully solid floor — same guarantee the opaque PAGE style makes (menus/toasts must stay legible on a solid ground). */ diff --git a/app/theme/presets.css b/app/theme/presets.css index 94abda29..40ad9a7a 100644 --- a/app/theme/presets.css +++ b/app/theme/presets.css @@ -119,41 +119,57 @@ modes with room to spare. A single shared alpha cannot serve both ends: at 0.55 everywhere, five presets lose dark-mode text and lapis/amethyst lose light-mode text. The switcher no longer inlines --glass-tint-a for a named preset (see applyTint), so THESE are the - values that render — nothing shadows them. */ -.dark[data-glass-tint="rose"] { + values that render — nothing shadows them. + Every night block carries two selectors: `.dark[data-glass-tint="x"]` for a preset on the same element + as `.dark` (normally ), and `.dark [data-glass-tint="x"]` for a preset scoped to an element INSIDE + a dark page (`
    `), which the compound form never matched, so a scoped + preset in dark mode used to render its DAY values. check-theme's [night-scope] keeps them paired. */ +.dark[data-glass-tint="rose"], +.dark [data-glass-tint="rose"] { --glass-tint-a: 0.49; } -.dark[data-glass-tint="goldstone"] { +.dark[data-glass-tint="goldstone"], +.dark [data-glass-tint="goldstone"] { --glass-tint-a: 0.5; } -.dark[data-glass-tint="carnelian"] { +.dark[data-glass-tint="carnelian"], +.dark [data-glass-tint="carnelian"] { --glass-tint-a: 0.45; } -.dark[data-glass-tint="amber"] { +.dark[data-glass-tint="amber"], +.dark [data-glass-tint="amber"] { --glass-tint-a: 0.34; } -.dark[data-glass-tint="peridot"] { +.dark[data-glass-tint="peridot"], +.dark [data-glass-tint="peridot"] { --glass-tint-a: 0.28; } -.dark[data-glass-tint="aventurine"] { +.dark[data-glass-tint="aventurine"], +.dark [data-glass-tint="aventurine"] { --glass-tint-a: 0.29; } -.dark[data-glass-tint="turquoise"] { +.dark[data-glass-tint="turquoise"], +.dark [data-glass-tint="turquoise"] { --glass-tint-a: 0.28; } -.dark[data-glass-tint="aquamarine"] { +.dark[data-glass-tint="aquamarine"], +.dark [data-glass-tint="aquamarine"] { --glass-tint-a: 0.31; } -.dark[data-glass-tint="sapphire"] { +.dark[data-glass-tint="sapphire"], +.dark [data-glass-tint="sapphire"] { --glass-tint-a: 0.48; } -.dark[data-glass-tint="lapis"] { +.dark[data-glass-tint="lapis"], +.dark [data-glass-tint="lapis"] { --glass-tint-a: 0.54; } -.dark[data-glass-tint="amethyst"] { +.dark[data-glass-tint="amethyst"], +.dark [data-glass-tint="amethyst"] { --glass-tint-a: 0.54; } -.dark[data-glass-tint="tourmaline"] { +.dark[data-glass-tint="tourmaline"], +.dark [data-glass-tint="tourmaline"] { --glass-tint-a: 0.46; } [data-glass-tint="moonstone"] { @@ -185,7 +201,8 @@ well as lightened, so both knobs carry part of it). */ --glass-opaque-c-scale: 0.92; } -.dark[data-glass-tint="moonstone"] { +.dark[data-glass-tint="moonstone"], +.dark [data-glass-tint="moonstone"] { /* Moonstone night. The wash sits a few points above the standard 58% (warmth without glare; h75 carries more real luminance per OKLCH point than the cool tints, so pale-warm washes read brighter than their numbers). AutoForeground reads BOTH wash knobs live (+ the [moonstone-sync] mirrored @@ -213,8 +230,12 @@ --glass-wash-l: 64%; --glass-wash-c-mult: 2; /* Fitted for the retired opaque sheet, like the daytime floor above and likewise not the global dark - pair: moonstone night is a LIGHT floor, so the near-white sheet barely moved it. dE 0.0002. */ - --glass-opaque-l: 52; + pair: moonstone night is a LIGHT floor, so the near-white sheet barely moved it. dE 0.0002. + The backing under sheer glass stays at that L52; the opaque card sits three points lower (49) for the + same reason every dark opaque card does (tokens.css): it carries the wash now, with no page showing + through it. */ + --glass-solidify-l: 52; + --glass-opaque-l: 49; --glass-opaque-c-scale: 0.44; /* Static -opaque tier baselines. These flipped polarity with the floor: on the old cream L84.9 they were dark warm literals, and on an L52 stone they have to be light or SSR's first paint is dark-on-dark. diff --git a/app/theme/styles.css b/app/theme/styles.css index 95011de6..1e30ea4f 100644 --- a/app/theme/styles.css +++ b/app/theme/styles.css @@ -70,10 +70,10 @@ /* Opaque — solid tinted panels for adaptive surfaces. --glass-solid-a: 1 makes any veiled overlay (glass-veil) compute a FULLY solid floor here too, so menus/toasts stay legible on an opaque page. */ :where([data-glass="opaque"], [data-glass="opaque"] [data-glass-tint], [data-glass="opaque"][data-glass-tint]) { - /* Flat floor, no sheet — same reasoning as [data-material="opaque"] in materials.css: with - --srf-filter: none there is no blurred backdrop to mask the dither of a 2–7 code gradient, and the - sheet's uniform lift is folded into --glass-opaque-l instead. */ - --srf-bg-image: none; + /* Floor + the tint wash, no sheet — same as [data-material="opaque"] in materials.css: the wash is what + makes opaque the glass stack at full opacity rather than a greyer cousin of it, and the sheet stays + out because with --srf-filter: none nothing masks the dither of its 2–7 code gradient. */ + --srf-bg-image: var(--glass-crystal-fresco, none), var(--glass-wash-layer); --srf-bg-color: var(--glass-opaque-bg); --srf-filter: none; --srf-shadow: var(--glass-shadow); @@ -100,18 +100,30 @@ } /* Opaque page: element-level touches for ADAPTIVE surfaces only (a `.glass` or `[data-surface]` element with no explicit material — an explicit frosted/crystal card keeps its translucency AND its normal - foreground). Declared ON the element because (a) --glass-opaque-outline is settable on any ancestor - and must resolve where it's read, and (b) the foreground remap must not leak into explicit materials. + foreground). Declared ON the element because (a) the page outline is settable on any ancestor and + must resolve where it's read, and (b) the foreground remap must not leak into explicit materials. The uniform EDGE applies to every adaptive panel, veiled or not — a veiled card under the opaque page still reads as a solid panel and needs the same outline. */ [data-glass="opaque"] :where(.glass, [data-surface]):not([data-material]) { - --srf-border-color: var(--glass-opaque-outline, var(--glass-border)); - --srf-border-w: var(--brd-w, var(--glass-opaque-outline-w, 1px)); + --srf-border-color: var(--glass-border); + --srf-border-w: var(--brd-w, 1px); /* Paint the border HERE, not just the tokens: borderless roles (Card default) never render glass-border, so without this only bordered roles showed the outline — "some cards get it, some don't". Under the opaque page every solid panel takes the uniform edge; bordered elements resolve the same vars, so nothing double-draws. */ - border: var(--srf-border-w) solid var(--brd-ink, var(--srf-border-color)); + border: var(--srf-edge-w, 1px) solid var(--srf-edge, var(--glass-border)); +} + +/* ── The page OUTLINE — every material ─────────────────────────────────────── + data-glass-outline on (or any ancestor) gives every glass surface below it an edge in the + theme accent: glass, frosted, crystal, chakra and opaque alike, bordered roles and borderless ones, + which is what the opaque page already does for its own panels. --glass-outline recolours it (on its + own, without the attribute, it recolours only the edges surfaces already draw) and --glass-outline-w + sets the weight (1 / 2 / 4px = hairline / rim / frame); an element's own border="rim" or "frame" and + borderColor="ink" still win. :is(), not :where(), so this ties the opaque page's edge rule on + specificity and wins by coming after it. */ +[data-glass-outline] :is(.glass, [data-surface]) { + border: var(--srf-edge-w, 1px) solid var(--brd-ink, var(--glass-outline, var(--glass-opaque-outline, var(--glass-accent)))); } /* The FOREGROUND remap is :not(.glass-veil) — the same carve-out crystal and chakra make, and the same one --veil-solidify makes for the solidify layer on this page style. A veiled card does not paint diff --git a/app/theme/tokens.css b/app/theme/tokens.css index 5266acd5..80b98700 100644 --- a/app/theme/tokens.css +++ b/app/theme/tokens.css @@ -116,8 +116,9 @@ --glass-border-a: 0.16; /* glass edge alpha — a white rim needs more presence on light backdrops */ /* Ink border alpha (borderColor="ink" / glass-border-ink): how much of --foreground the line takes. Mode-invariant because --foreground already flips polarity per mode; 0.5 keeps the line clearly - softer than text while clearing the APCA non-text floor on every preset (check-contrast [ink]). */ - --glass-border-ink-a: 0.5; + softer than text while clearing the APCA non-text floor on every preset (check-contrast [ink]); 0.55 since + opaque cards carry the wash, whose lighter dark surface left goldstone's line at 29.9 Lc. */ + --glass-border-ink-a: 0.55; /* Crystal gloss defaults (the demo's Light/Tint sliders override inline). --glass-gloss-tint is mode-shared; --glass-gloss-l is NOT — see its twin below. AutoForeground's crystal tiers mirror --glass-gloss-l (97 light / 66 dark) as their snapshot-path fallback — keep all three in step. */ @@ -182,12 +183,15 @@ inside the budget at 4.47×. Fitted across the 14 presets: worst OKLab dE 0.0021, mean 0.0009 — an order of magnitude under a just-noticeable difference, so the surface is unchanged to the eye. Moonstone pins its own pair (presets.css); its floor is near-white, where the sheet did far less. */ - /* L88, not 90.9. Near-white is where the sRGB chroma ceiling collapses, so an opaque card at 90.9 had - two problems at once: it read as washed-toward-white, and it could not hold enough chroma to carry - its own tint (lapis and sapphire were at 97–99% of a ceiling worth only 0.043). Dropping 2.9 L - roughly doubles every hue's ceiling while keeping body text at Lc 80.0 — the floor is still firmly - on the light side, so text polarity never changes. */ - --glass-opaque-l: 88; + /* L93. The opaque card is the glass stack at full opacity: this floor plus the same tint WASH every + other material carries (materials.css). It used to be L88 with no wash, which made the floor carry + the tint on its own, and near white that cannot work: the sRGB ceiling collapses, so the jewels sat + at 97% of it and still read pastel, while greens and teals got half the chroma of their glass + siblings. With the wash doing the tinting, the floor's job is the one the backing under sheer glass + does (L92, --glass-solidify-l), and 93 is where floor + wash lands within 0.7 L of each preset's + glass surface (it has no lighter page showing through to make up the last point) while body text + keeps the 76.5 margin on every preset (tightest: destructive 76.6). */ + --glass-opaque-l: 93; /* 0.85, down from 1.26. The old value only looked large: it was immediately clipped to the shared 0.042 cap, so every preset rendered the SAME chroma regardless of its tint. Presets now pin their own --glass-opaque-c-max (their hue's real ceiling at L88), and 0.85 is the multiplier that lets @@ -203,6 +207,12 @@ deliberately not the tightest — per-hue pins are, and scripts/check-gamut.mjs sweeps every scope against its real ceiling rather than trusting this number. */ --glass-opaque-c-max: 0.055; + /* The share of that cap the opaque FLOOR may use. The caps above are each hue's ceiling at L88 (the + solidify backing derives its own from them too), and at the floor's L93 the ceiling is 55–90% of + that depending on hue; 0.55 keeps the tightest (tourmaline) at 0.96 of its ceiling. The floor + needs little chroma now that the wash carries the tint. Dark sets 1: its floor sits mid-dark, where + every preset's ask is far inside its ceiling. */ + --glass-opaque-cap-scale: 0.55; /* ── Solidify floor — the opaque backing UNDER sheer glass ─────────────────── `glass` paints --glass-solidify (this colour at --glass-opacity) as the bottom background-image layer of every SHEER material, so it is ~70% of what page/crystal/chakra text actually sits on. @@ -331,7 +341,7 @@ (turquoise, aquamarine, success, warning, aurora) and two in dark (lapis, info), where the browser gamut-maps and the delivered colour stops tracking the authored one. Measured against --glass-fg-h, not --glass-tint-h: --primary follows the FOREGROUND hue, and the - Muse/Gloaming frescoes decouple the two (fg 290 / 18), so those hues are in the sweep — aurora only + Muse/Sistine frescoes decouple the two (fg 290 / 18), so those hues are in the sweep — aurora only shows up as out-of-gamut once you check the fg hue. */ --primary-c-max: 0.069; /* ≤ 0.0697, the L-42 ceiling floor — h205 (aurora) is now tighter than h190 */ --primary-foreground: oklch(0.98 0 0); /* White text on the primary ink — Lc 88.1 at the worst hue, turquoise (measured) */ @@ -472,12 +482,22 @@ --glass-solid-l: 18%; /* The retired sheet's compensation, dark twin (see :root). Much the larger move of the two modes: a near-white sheet over a DARK floor lifts it ~12 codes where over the light floor it lifts ~2–5. Same - two-knob fit, worst OKLab dE 0.0045 across the 14 presets. */ - --glass-opaque-l: 36.4; - /* 1.05, up from 0.85. The dark floor was never cap-limited — every preset sat at 31–79% of its own - ceiling, so the MULTIPLIER was the throttle and dark opaque cards lost their tint. 1.05 is the most - the tightest hue (turquoise) can take without clipping; it lifts every preset +24% at Lc 80.0. */ - --glass-opaque-c-scale: 1.05; + two-knob fit, worst OKLab dE 0.0045 across the 14 presets. + 33.4 since opaque carries the tint wash (materials.css). The wash lifts the card, and unlike sheer + glass no dark page shows through it, so at the backing's own 36.4 an opaque card sat 3–5 L above its + glass siblings, and on the light-wash presets (aventurine, peridot, turquoise) only pure white text + still reached the body floor. Three points down puts it within ~2 L of the glass beside it and keeps + tinted text above the 76.5 margin everywhere (tightest: moonstone 76.6). The light floor makes the + mirror-image move (L93 over a L92 backing) for the mirror-image reason. */ + --glass-opaque-l: 33.4; + /* 0.98. Was 1.05 (up from 0.85): the dark floor was never cap-limited — every preset sat at 31–79% of + its own ceiling, so the MULTIPLIER was the throttle and dark opaque cards lost their tint. It is + still the most the tightest hue (turquoise, ×0.984) can take without clipping, now at the floor's + L33.4, where the ceilings are lower than at L36.4. The wash carries most of the colour on top of it. + The sheer backing shares this scale (engine.css), so dark glass gives up 7% of its backing chroma + with it, under a wash that dominates it. */ + --glass-opaque-c-scale: 0.98; + --glass-opaque-cap-scale: 1; /* Opaque floor chroma cap, dark twin (see :root). A GUARD, not a throttle — the distinction matters and this value used to get it wrong. It was 0.06, the tightest ceiling across the preset scopes, which was correct while every jewel shared --glass-tint-c: 0.07 and the floor multiplied it blind. @@ -489,11 +509,12 @@ pushes chroma far past today's values. check-gamut scores each preset against its own ceiling. */ /* Guard, not a throttle: the largest dark ask is tourmaline at 0.111, so this binds nobody. Declared in its own rule right after this block (see there for why). */ - /* Dark keeps the solidify floor IDENTICAL to the opaque floor — the split exists to buy LIGHT-mode - text contrast, and dark already clears the body floor on every preset with margin. Pinning to the - opaque values (rather than leaving :root's L92 / ×0.65 to inherit) is what makes this a light-only - change: a lighter backing under dark glass would wash the whole mode out. */ - --glass-solidify-l: var(--glass-opaque-l); + /* The dark backing under sheer glass, at the lightness the opaque floor had before opaque gained the + wash: dark glass is unchanged by that move. It is its own value now, as light's L92 is, so the tint + switcher's Lightness slider (which writes --glass-opaque-l) moves the opaque card in both modes and + the sheer backing in neither. The cap stays the opaque one: dark already clears the body floor on + every preset with margin, and a lighter or greyer backing would wash the whole mode out. */ + --glass-solidify-l: 36.4; --glass-solidify-c-max: var(--glass-opaque-c-max); /* ── Chakra dark twins (see :root for what each dial does + how the alphas were solved). The facet alphas invert in character: over an L28 body there is 72 L of headroom UP and only 28 diff --git a/app/theme/utilities.css b/app/theme/utilities.css index 12d3ae8f..48c7324b 100644 --- a/app/theme/utilities.css +++ b/app/theme/utilities.css @@ -65,13 +65,21 @@ The max() composes the diffuse FLOOR for adaptive surfaces. */ backdrop-filter: var(--srf-filter, blur(max(var(--srf-blur, var(--blur)), var(--dif, 0px))) saturate(180%)) var(--srf-stain,); box-shadow: var(--srf-shadow, var(--srf-elev, var(--glass-shadow))), var(--glow-layer, 0 0 0 0 transparent); + /* THE EDGE, resolved once per surface for everything that draws one (glass-border, the opaque page's + uniform edge, the page outline). Precedence, strongest first: the element's own ink colour or + rim/frame weight, then the PAGE OUTLINE (--glass-outline / --glass-outline-w, set on any ancestor; + --glass-opaque-outline[-w] are the old opaque-only names, still read), then the material's own edge + (--srf-border-color / --srf-border-w), then the plain glass hairline. Element-composed, so an outline + set on a subtree resolves where it is read. */ + --srf-edge: var(--brd-ink, var(--glass-outline, var(--glass-opaque-outline, var(--srf-border-color, var(--glass-border))))); + --srf-edge-w: var(--brd-w, var(--glass-outline-w, var(--glass-opaque-outline-w, var(--srf-border-w, 1px)))); } /* Border axis — the material picks the color unless the element asks for ink (--brd-ink, the borderColor axis below); width resolves material pin (--srf-border-w, which itself reads the --brd-w weight slot) → weight slot directly (adaptive surfaces) → 1px hairline. */ @utility glass-border { - border: var(--srf-border-w, var(--brd-w, 1px)) solid var(--brd-ink, var(--srf-border-color, var(--glass-border))); + border: var(--srf-edge-w, 1px) solid var(--srf-edge, var(--glass-border)); } /* Border WEIGHTS — token setters for the --brd-w slot (order-safe; same-element composition, like glass-sm's --srf-blur). Hairline is the unmarked default (1px; 0.5px under frosted); rim = 2px, frame = 4px. */ diff --git a/bun.lock b/bun.lock index 3f9c3b6f..ff314efa 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.4.0", - "framer-motion": "^13.5.1", + "framer-motion": "^14.4.0", "input-otp": "^1.5.0", "next": "^16.4.2", "next-themes": "^0.4.6", @@ -21,7 +21,7 @@ "react-day-picker": "^10.0.2", "react-dom": "^19.3.1", "react-easy-crop": "^6.2.4", - "react-resizable-panels": "^4.14.6", + "react-resizable-panels": "^4.14.7", "recharts": "^3.10.1", "shadcn": "^4.22.1", "sonner": "^2.0.8", @@ -51,7 +51,7 @@ "storybook": "^10.6.2", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", - "vitest": "^4.1.12", + "vitest": "^5.0.5", }, }, }, @@ -912,17 +912,13 @@ "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - "@vitest/mocker": ["@vitest/mocker@4.1.12", "", { "dependencies": { "@vitest/spy": "4.1.12", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA=="], + "@vitest/mocker": ["@vitest/mocker@5.0.5", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.0.5", "estree-walker": "^3.0.3", "magic-string": "^1.4.3" }, "peerDependencies": { "msw": "^2.4.9 || ^3.0.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.12", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA=="], - - "@vitest/runner": ["@vitest/runner@4.1.12", "", { "dependencies": { "@vitest/utils": "4.1.12", "pathe": "^2.0.3" } }, "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w=="], - - "@vitest/snapshot": ["@vitest/snapshot@4.1.12", "", { "dependencies": { "@vitest/pretty-format": "4.1.12", "@vitest/utils": "4.1.12", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw=="], + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - "@vitest/utils": ["@vitest/utils@4.1.12", "", { "dependencies": { "@vitest/pretty-format": "4.1.12", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA=="], + "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], @@ -1018,7 +1014,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.29", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.30", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-YJs6Pz17RMNv8HoyKREtCY38E3BV1nPkWVmIb+XN1uHC1Vqt3KZCH/uJH9ZKRoiKb/icZ6tvehyee6RZNbFfRA=="], "bidi-js": ["bidi-js@1.1.0", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg=="], @@ -1076,7 +1072,7 @@ "case-sensitive-paths-webpack-plugin": ["case-sensitive-paths-webpack-plugin@2.4.0", "", {}, "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw=="], - "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + "chai": ["chai@6.3.0", "", {}, "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -1266,7 +1262,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.459", "", {}, "sha512-4SC9HHSFjO/liAhifpdOL5hG9b4BP7/5G3y3UPZVO8trlAXr1CaUEEJL0gWK3kOcYcZrOA5rkwG45Gra/lTIgg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.460", "", {}, "sha512-rRaVC7bNUyGsL+GMtmZfv3AfgG6za7gE9v5bT0PIbjEerddA0IQ+tCK05NFAXqCpNlrxBZqbFqquyxHRQONHTA=="], "elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="], @@ -1342,7 +1338,7 @@ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - "fast-uri": ["fast-uri@3.1.8", "", {}, "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg=="], + "fast-uri": ["fast-uri@3.1.9", "", {}, "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ=="], "fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="], @@ -1370,7 +1366,7 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "framer-motion": ["framer-motion@13.5.1", "", { "dependencies": { "motion-dom": "13.5.1", "motion-utils": "13.5.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-HeWOaeHD3kyTSIEMT6+nw7fBn4saIfxxabZyAITbycAl4xr1kzGJKIvGt/OWghSlvIA9qTnZ1zTGelHAe6S8DA=="], + "framer-motion": ["framer-motion@14.4.0", "", { "dependencies": { "motion-dom": "14.4.0", "motion-utils": "14.1.1", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-HtO1fxord2rnXYRyr1laG4uY7+OtgwDc9Szcg1wlMq/LF7BO3/S7a+KEo691U7pSi+i0ueRuRqaD1UaLEmZHuQ=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -1526,7 +1522,7 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.12", "", {}, "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw=="], + "jose": ["jose@6.2.13", "", {}, "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -1600,7 +1596,7 @@ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "magic-string": ["magic-string@1.4.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ=="], "make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="], @@ -1644,9 +1640,9 @@ "minimizer-webpack-plugin": ["minimizer-webpack-plugin@5.13.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "jest-worker": "^27.4.5", "schema-utils": "^4.6.0", "terser": "^5.51.0" }, "peerDependencies": { "@minify-html/node": "*", "@napi-rs/image": "*", "@swc/core": "*", "@swc/css": "*", "@swc/html": "*", "clean-css": "*", "cssnano": "*", "csso": "*", "esbuild": "*", "html-minifier-terser": "*", "imagemin": "*", "lightningcss": "*", "postcss": "*", "sharp": "*", "svgo": "*", "uglify-js": "*", "webpack": "^5.1.0" }, "optionalPeers": ["@minify-html/node", "@napi-rs/image", "@swc/core", "@swc/css", "@swc/html", "clean-css", "cssnano", "csso", "esbuild", "html-minifier-terser", "imagemin", "lightningcss", "postcss", "sharp", "svgo", "uglify-js"] }, "sha512-1mhAfvachWYYOm8tpmany5NLuvP+F2wxZWBSX8DC0uFS2wJpM9q3rWgN9wBooAH9d5V12wX0BjyJe4fMjHMarQ=="], - "motion-dom": ["motion-dom@13.5.1", "", { "dependencies": { "motion-utils": "13.5.0" } }, "sha512-/7CVCD20Mz1KobktorpdgvowV0V41J7UxqE+qWS3GkSJqrRr5ft+2RloOYtEKv+wvBp3JaSFBclhxt4ja8ovHg=="], + "motion-dom": ["motion-dom@14.4.0", "", { "dependencies": { "motion-utils": "14.1.1" } }, "sha512-1q8RyHSdr5COpzrv3JgJ9XRjOO88ZN9CwflpkGgyO/SR/SNRP/dD6EU8SEo092kcKY34IzHC5diJPKrQRMA8lQ=="], - "motion-utils": ["motion-utils@13.5.0", "", {}, "sha512-Hqe/sqjIKliPZg8sVjWAkdySDPJNIeKH+euVkDOG8ri3DUUU7ZjndmHsGqEPz2iHpiyVaV5I22iguCMHU2he+Q=="], + "motion-utils": ["motion-utils@14.1.1", "", {}, "sha512-3VvN5Uvf8B6qG+OfOui6Nht5EWp+vLuU3IqcAj7ItZGKqtmTjCebl5sGZF7/08gEHRnEl1AP/fIwbcEyVrcQGQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1690,7 +1686,7 @@ "objectorarray": ["objectorarray@1.0.5", "", {}, "sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg=="], - "obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="], + "obug": ["obug@3.0.0", "", {}, "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -1746,8 +1742,6 @@ "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], "pbkdf2": ["pbkdf2@3.1.7", "", { "dependencies": { "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", "sha.js": "^2.4.12", "to-buffer": "^1.2.2" } }, "sha512-nS4mvFgVwUrecPTRdpdseM2fwpdfnX0/HibA8DgBRQtihUAzHQYJFU2l+OlEtiXtpqkrMvCPDwyXMUnASDt6Qw=="], @@ -1840,7 +1834,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-resizable-panels": ["react-resizable-panels@4.14.6", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-YqkTuTDV1ncknnR5YN/iI8icpClippc9/10job9zJIppt2r8yUwaVfVxhzFNO+3DIxrnAwhcMMs1FdX5xt9C9A=="], + "react-resizable-panels": ["react-resizable-panels@4.14.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-UP8lccy1yGA8p8c6UvPaMDnqYePv/CWOv3R6rTAJ9f+UmuaeiTFDJlvcp5rFu5GJcQ1ofCmkjbzp0HceJXZRbw=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], @@ -1946,8 +1940,6 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], @@ -1964,8 +1956,6 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], @@ -2020,13 +2010,13 @@ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + "tinybench": ["tinybench@6.2.2", "", {}, "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA=="], "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - "tinyrainbow": ["tinyrainbow@3.2.0", "", {}, "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A=="], + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], "tinyspy": ["tinyspy@4.0.6", "", {}, "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg=="], @@ -2114,7 +2104,7 @@ "vite": ["vite@8.3.4", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.29", "rolldown": "~1.2.12", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ=="], - "vitest": ["vitest@4.1.12", "", { "dependencies": { "@vitest/expect": "4.1.12", "@vitest/mocker": "4.1.12", "@vitest/pretty-format": "4.1.12", "@vitest/runner": "4.1.12", "@vitest/snapshot": "4.1.12", "@vitest/spy": "4.1.12", "@vitest/utils": "4.1.12", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.12", "@vitest/browser-preview": "4.1.12", "@vitest/browser-webdriverio": "^4.1.11", "@vitest/coverage-istanbul": "4.1.12", "@vitest/coverage-v8": "4.1.12", "@vitest/ui": "4.1.12", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA=="], + "vitest": ["vitest@5.0.5", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.5", "chai": "^6.3.0", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.4.3", "obug": "^3.0.0", "picomatch": "^4.0.7", "std-env": "^4.3.0", "tinybench": "^6.2.2", "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", "why-is-node-running": "3.2.1" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.5", "@vitest/browser-preview": "5.0.5", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.5", "@vitest/coverage-v8": "5.0.5", "@vitest/ui": "5.0.5", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg=="], "vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="], @@ -2142,7 +2132,7 @@ "which-typed-array": ["which-typed-array@1.1.24", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ=="], - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "why-is-node-running": ["why-is-node-running@3.2.1", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -2216,14 +2206,12 @@ "@storybook/builder-webpack5/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - "@storybook/builder-webpack5/magic-string": ["magic-string@1.4.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ=="], - "@storybook/builder-webpack5/style-loader": ["style-loader@4.0.0", "", { "peerDependencies": { "webpack": "^5.27.0" } }, "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA=="], - "@storybook/preset-react-webpack/magic-string": ["magic-string@1.4.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ=="], - "@storybook/react-docgen-typescript-plugin/find-cache-dir": ["find-cache-dir@3.3.2", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig=="], + "@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], @@ -2242,11 +2230,9 @@ "@ts-morph/common/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + "@vitest/expect/chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], - "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.12", "", {}, "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg=="], + "@vitest/mocker/@vitest/spy": ["@vitest/spy@5.0.5", "", {}, "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw=="], "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], @@ -2388,10 +2374,6 @@ "vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - "vitest/@vitest/expect": ["@vitest/expect@4.1.12", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.12", "@vitest/utils": "4.1.12", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ=="], - - "vitest/@vitest/spy": ["@vitest/spy@4.1.12", "", {}, "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg=="], - "webpack-hot-middleware/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], @@ -2420,8 +2402,6 @@ "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], - "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - "browserify-sign/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "browserify-sign/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -2470,8 +2450,6 @@ "vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], - "vitest/@vitest/expect/chai": ["chai@6.3.0", "", {}, "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg=="], - "webpack-hot-middleware/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@storybook/react-docgen-typescript-plugin/find-cache-dir/pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 00000000..744df7a2 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,5 @@ +# `bun test` is Bun's built-in runner, not this project's. It ignores vitest.config.mts (no jsdom, no +# setup file, no jest-dom matchers), so it fails most of the DOM tests with errors like +# "localStorage is not defined". The preload below stops it with the right commands instead. +[test] +preload = ["./scripts/bun-test-guard.ts"] diff --git a/components/auto-foreground.tsx b/components/auto-foreground.tsx index d9504b66..112be7e3 100644 --- a/components/auto-foreground.tsx +++ b/components/auto-foreground.tsx @@ -14,6 +14,8 @@ import { harmonicHue, LC_AIM_KNOWN, LC_MARGIN, + limitingSurface, + parseGradientStops, pickTonalInBand, READABLE_USAGE, readableTonal, @@ -207,6 +209,10 @@ export function AutoForeground({ palette: paletteProp, ramp: rampProp }: AutoFor const v = inputs ? inputs[name] : cs ? Number.parseFloat(cs.getPropertyValue(name)) : Number.NaN; return v == null || Number.isNaN(v) ? fb : v; }; + /* A fresco's multi-hue sheet, if one is active (--glass-crystal-fresco is unset on every other preset). + Every material paints it over its surface, so every tier set bands against the hardest stop of it + (see applyTiers). Frescoes always take the DOM path, so the snapshot never has to carry it. */ + const frescoStops = cs ? parseGradientStops(cs.getPropertyValue("--glass-crystal-fresco")) : []; // Foregrounds FOLLOW THE CHOSEN FOREGROUND HUE: the ramp's hue is --glass-fg-h (which defaults to // the glass tint --glass-tint-h, but frescoes set it apart so text anchors off their surface). // Lightness + chroma (vividness) and step count come from the /colors ramp config. Picks are @@ -281,7 +287,7 @@ export function AutoForeground({ palette: paletteProp, ramp: rampProp }: AutoFor Fallbacks mirror tokens.css: the derived cap is --glass-opaque-c-max × 0.65 in light. */ const solidifyFloor = { l: num("--glass-solidify-l", dark ? 36.4 : 92), - c: Math.min(tintC * num("--glass-opaque-c-scale", dark ? 1.05 : 0.85), num("--glass-solidify-c-max", dark ? opaqueCMax : opaqueCMax * 0.65)), + c: Math.min(tintC * num("--glass-opaque-c-scale", dark ? 0.98 : 0.85), num("--glass-solidify-c-max", dark ? opaqueCMax : opaqueCMax * 0.65)), a: glassOpacity, }; /** The solidify floor as a compositable layer — the layer order `glass` paints. */ @@ -395,6 +401,11 @@ export function AutoForeground({ palette: paletteProp, ramp: rampProp }: AutoFor // Lift the band's TARGET toward its ceiling by the uncertainty boost + the baseline aim. Floors and // ceilings stay — the margin aims higher, it never legalizes a harsher pick than the band allowed. const boost = (band: ContrastBand) => boostBand(band, lcBoost, lcAim); + /* What the text is SOLVED against: the surface itself, or, under a fresco, the composite at the + gradient stop that leaves the least contrast (lib/oklch-utils limitingSurface). Text sized for the + plain surface dipped to 66–74 Lc at the stops. The HUE still comes from `surface`: ink follows the + material it sits on, not whichever stop of the sweep happens to be hardest. */ + const bg = limitingSurface(surface, frescoStops); /* Pick from the TONAL ramp, never at the cost of the band's floor — see pickTonalInBand in lib/oklch-utils. Today no band needs the fallback (light body bottoms out at 75.4 Lc on the clipped ramp), but /colors lets a consumer re-base the ramp entirely, so the guard exists so @@ -410,7 +421,7 @@ export function AutoForeground({ palette: paletteProp, ramp: rampProp }: AutoFor const band = boost(rawBand); return formatOklch( adaptive - ? readableTonal(surface, band, { + ? readableTonal(bg, band, { // Opaque cards on the hue-less themes (selenite + moonstone) follow the chosen accent too — same as // the normal surface above — so moonstone/selenite opaque-card text tints toward the accent. // Otherwise THIS surface's own composited hue, not the tint token's (see surfaceH): each @@ -421,7 +432,7 @@ export function AutoForeground({ palette: paletteProp, ramp: rampProp }: AutoFor // minChroma search does not bail to the extreme (see readableTonal). minChroma: huelessAccent ? 0.08 : 0, }) - : pickTonalInBand(ramp, surface, band), + : pickTonalInBand(ramp, bg, band), ); }; root.style.setProperty(`--foreground${suffix}`, tier(READABLE_USAGE.body)); @@ -444,7 +455,7 @@ export function AutoForeground({ palette: paletteProp, ramp: rampProp }: AutoFor : formatOklch( // Clipped like every other solve: a PINNED hue is the one case where collapsing to the // achromatic extreme is most obviously wrong — the whole point of pinning is to see it. - readableTonal(surface, boost(READABLE_USAGE[usage]), { + readableTonal(bg, boost(READABLE_USAGE[usage]), { hue: typeof choice === "string" ? harmonicHue(fgHarmonyH, choice) : choice, chroma: 0.15, }), @@ -469,7 +480,7 @@ export function AutoForeground({ palette: paletteProp, ramp: rampProp }: AutoFor root.style.setProperty( `--foreground-ui${suffix}`, formatOklch( - readableTonal(surface, boost(READABLE_USAGE.ui), { + readableTonal(bg, boost(READABLE_USAGE.ui), { hue: iconH, chroma: iconHue != null ? 0.15 : tintC > 0 ? cfgC : 0, }), @@ -481,7 +492,7 @@ export function AutoForeground({ palette: paletteProp, ramp: rampProp }: AutoFor // show-through margin lifting each band target as the floor gets sheerer. Derived above the ramp // (see normalSurface) because the ramp's hue is read off it. applyTiers(normalSurface, "", false, normalLcBoost); - /* Opaque cards paint --glass-opaque-bg, so band a second set against THAT — derived here from + /* Opaque cards paint --glass-opaque-bg (under the wash), so band a second set against THAT — derived here from --glass-opaque-l / -c-max rather than reused from solidifyFloor. Those two used to be the same colour, and this call read `solidifyFloor` on exactly that basis; they are separate surfaces now (tokens.css), so sharing would band opaque-card text against the sheer backing instead of the @@ -490,12 +501,23 @@ export function AutoForeground({ palette: paletteProp, ramp: rampProp }: AutoFor carrying its own `* 0.9` multiplier (matching neither mode) and no chroma cap at all. `adaptive` so a LIGHT floor (moonstone cream) gets DARK text — the theme ramp only spans the readable half and can't. */ + /* The opaque card is its floor PLUS the tint wash (materials.css: the glass stack at full opacity), + so the tier bands against the composite. The floor's cap is the preset cap times + --glass-opaque-cap-scale (the light floor sits at L93, where the L88 caps would overshoot). */ + const opaqueFloor = { + l: num("--glass-opaque-l", dark ? 33.4 : 93), + c: Math.min(tintC * num("--glass-opaque-c-scale", dark ? 0.98 : 0.85), opaqueCMax * num("--glass-opaque-cap-scale", dark ? 1 : 0.55)), + h: tintH, + }; applyTiers( - { - l: num("--glass-opaque-l", dark ? 36.4 : 88), - c: Math.min(tintC * num("--glass-opaque-c-scale", dark ? 1.05 : 0.85), opaqueCMax), - h: tintH, - }, + compositeSurface(opaqueFloor, [ + { + l: washL, + c: tintC * washCMult, + h: tintH, + a: tintA, + }, + ]), "-opaque", true, 0, // no uncertainty — --glass-opaque-l models this floor exactly diff --git a/components/backdrop-scene.tsx b/components/backdrop-scene.tsx new file mode 100644 index 00000000..55067a8f --- /dev/null +++ b/components/backdrop-scene.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { PATTERN_DISCS, PATTERN_SANDS } from "@/components/background-provider"; +import { PATTERN_DENSITIES, PATTERN_STYLES, PatternBackground } from "@/components/pattern-background"; + +/* The site chrome stays mounted under the scene (the root layout wraps every route), so it is taken out of + view, focus and the accessibility tree, and the page cannot scroll a scrollbar into the frame. The + provider's own background goes too: dots, grid and mesh paint over a transparent --background, so it + showed through them, and the scene should sit on the same page base it has on the site. */ +const BARE = ` +html, body { overflow: hidden; } +body header, body footer, [data-sonner-toaster] { visibility: hidden; } +.glass-backdrop-layer:not([data-backdrop] *) { display: none; } +`; + +function pick(value: string | null, options: readonly T[], fallback: T): T { + return options.includes(value as T) ? (value as T) : fallback; +} + +/** + * One pattern scene, full-viewport, over everything else on the page: what `scripts/export-backgrounds.mjs` + * photographs, and a live-wallpaper URL in its own right. The QUERY picks the scene; theme and tint are the + * visitor's own, restored by next-themes and the tint switcher exactly as on any other page. + * + * /backdrop?scene=forest&disc=left&density=medium&sand=breeze&speed=0 + * + * Unknown or missing values fall back to the site's defaults; `speed` is the loop in seconds (0 = static). + */ +export function BackdropScene() { + const params = useSearchParams(); + const scene = pick(params.get("scene"), PATTERN_STYLES, "moonrise"); + const speed = Number(params.get("speed") ?? 0); + return ( +
    + + 0 ? speed : 0} + disc={pick(params.get("disc"), PATTERN_DISCS, "right")} + sand={pick(params.get("sand"), PATTERN_SANDS, "breeze")} + /> +
    + ); +} diff --git a/components/background-provider.tsx b/components/background-provider.tsx index 1e3b9881..ec70ecd0 100644 --- a/components/background-provider.tsx +++ b/components/background-provider.tsx @@ -104,7 +104,7 @@ export const CANVAS_SPEEDS = [ ] as const; /** Animated-pattern pace: loop/scroll duration in SECONDS (2s fastest … 16s slowest); 0 = static. - * Shared by every animated pattern (synthwave, moonrise, clouds, dune, aurora, chase) via the --pat-dur CSS var. */ + * Shared by every animated pattern (synthwave, moonrise, clouds, dune, aurora, forest, chase) via the --pat-dur CSS var. */ export const PATTERN_SPEEDS = [ 2, 4, @@ -114,7 +114,7 @@ export const PATTERN_SPEEDS = [ 0, ] as const; -/** Sun/moon horizontal placement for the horizon scenes (synthwave / moonrise / clouds / dune) — off-center +/** Sun/moon horizontal placement for the horizon scenes (synthwave / moonrise / clouds / dune / aurora / forest) — off-center * clears the page's centered content. */ export const PATTERN_DISCS = [ "right", diff --git a/components/glass-tint-switcher.tsx b/components/glass-tint-switcher.tsx index ffe09c70..0119e8c4 100644 --- a/components/glass-tint-switcher.tsx +++ b/components/glass-tint-switcher.tsx @@ -37,11 +37,12 @@ function applyDiffuse(floor: number) { } } const OPAQUE_L_KEY = "sistine-glass-opaque-l"; +/* Storage keys keep their opaque-era names so a saved outline survives the move to all materials. */ const OUTLINE_KEY = "sistine-glass-opaque-outline"; const OUTLINE_W_KEY = "sistine-glass-opaque-outline-w"; const ACCENT_KEY = "sistine-accent"; -/** Opaque-outline weight → the --glass-opaque-outline-w page var (hairline = unset → 1px default). +/** Outline weight → the --glass-outline-w page var (hairline = unset → each material's own hairline). * Same ladder as the border axis (glass-border-rim/-frame); an element-level rim/frame beats this. */ const OUTLINE_WEIGHTS = { hairline: null, @@ -122,7 +123,7 @@ const PRESETS = [ label: "Muse", h: 230, c: 0.056, - a: 0.16, + a: 0.1, bespoke: true, swatch: "linear-gradient(135deg in oklch, oklch(85% 0.1 222), oklch(78% 0.2 326), oklch(84% 0.12 74))", }, @@ -140,7 +141,7 @@ const PRESETS = [ label: "Gloaming", h: 32, c: 0.089, - a: 0.16, + a: 0.12, bespoke: true, swatch: "linear-gradient(135deg in oklch, oklch(84% 0.13 62), oklch(78% 0.15 350), oklch(64% 0.14 278))", }, @@ -344,7 +345,7 @@ function emitFg(v: { h: number; c: number; a: number; base: PresetValue; l: numb } /** Per-mode tint-body lightness override ({light?, dark?}), stored as JSON under OPAQUE_L_KEY. Because - * --glass-opaque-l is mode-aware in CSS (88 light / 36.4 dark), L must be tracked separately per mode — a + * --glass-opaque-l is mode-aware in CSS (93 light / 33.4 dark), L must be tracked separately per mode — a * single global value would wash out the other mode. Old plain-number values parse to a non-object and are * ignored, self-healing to the mode default. */ function readLmap(): { @@ -452,11 +453,11 @@ export function GlassTintSwitcher() { if (df > 0) applyDiffuse(df); const storedOutline = localStorage.getItem(OUTLINE_KEY) === "1"; setOutline(storedOutline); - if (storedOutline) document.documentElement.style.setProperty("--glass-opaque-outline", "var(--glass-accent)"); + if (storedOutline) document.documentElement.dataset.glassOutline = ""; const storedOutlineW = localStorage.getItem(OUTLINE_W_KEY) as OutlineWeight | null; if (storedOutlineW && OUTLINE_WEIGHTS[storedOutlineW]) { setOutlineW(storedOutlineW); - document.documentElement.style.setProperty("--glass-opaque-outline-w", OUTLINE_WEIGHTS[storedOutlineW]); + document.documentElement.style.setProperty("--glass-outline-w", OUTLINE_WEIGHTS[storedOutlineW]); } try { const acc = JSON.parse(localStorage.getItem(ACCENT_KEY) ?? "null"); @@ -474,7 +475,7 @@ export function GlassTintSwitcher() { } }, []); - // Tint-body LIGHTNESS is PER-MODE: --glass-opaque-l is mode-aware in CSS (88 light / 36.4 dark), so a single + // Tint-body LIGHTNESS is PER-MODE: --glass-opaque-l is mode-aware in CSS (93 light / 33.4 dark), so a single // global override would clobber the other mode (dark opaque surfaces would render light). (Re-)apply the // current mode's stored L on mount + on every light/dark toggle; with no stored L for a mode, clear the // inline override so the CSS default wins. Fires the fg event so AutoForeground re-bands the opaque floor. @@ -489,7 +490,7 @@ export function GlassTintSwitcher() { } else { root.style.removeProperty("--glass-opaque-l"); const computed = Number.parseFloat(getComputedStyle(root).getPropertyValue("--glass-opaque-l")); - setLightness(Number.isFinite(computed) ? computed : mode === "dark" ? 36.4 : 88); + setLightness(Number.isFinite(computed) ? computed : mode === "dark" ? 33.4 : 93); } window.dispatchEvent(new Event(FG_EVENT)); }; @@ -607,16 +608,17 @@ export function GlassTintSwitcher() { } }; - // Optional accent outline for opaque surfaces — sets --glass-opaque-outline to the theme accent so opaque - // components gain a colored outline (flair) instead of the flat default border. Applies site-wide. - // Outline WEIGHT — page-wide width for the same borders the outline toggle colors (opaque material - // cards + adaptive surfaces on the opaque page style). hairline clears the var (1px default). + // Optional accent OUTLINE on every glass surface, whatever its material: sets data-glass-outline on , + // which draws an edge in the theme accent around glass, frosted, crystal, chakra and opaque surfaces, + // borderless ones included (styles.css). Applies site-wide. + // Outline WEIGHT — page-wide width for every glass edge (the outline's, and the borders surfaces already + // draw). hairline clears the var, so each material keeps its own hairline (0.5px under frosted). const changeOutlineW = (w: OutlineWeight) => { setOutlineW(w); const root = document.documentElement; const px = OUTLINE_WEIGHTS[w]; - if (px) root.style.setProperty("--glass-opaque-outline-w", px); - else root.style.removeProperty("--glass-opaque-outline-w"); + if (px) root.style.setProperty("--glass-outline-w", px); + else root.style.removeProperty("--glass-outline-w"); try { localStorage.setItem(OUTLINE_W_KEY, w); } catch { @@ -627,8 +629,8 @@ export function GlassTintSwitcher() { const changeOutline = (on: boolean) => { setOutline(on); const root = document.documentElement; - if (on) root.style.setProperty("--glass-opaque-outline", "var(--glass-accent)"); - else root.style.removeProperty("--glass-opaque-outline"); + if (on) root.dataset.glassOutline = ""; + else delete root.dataset.glassOutline; try { localStorage.setItem(OUTLINE_KEY, on ? "1" : "0"); } catch { @@ -812,7 +814,7 @@ export function GlassTintSwitcher() { />
    - Opaque outline + Outline + ); +}); AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; const AlertDialogCancel = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & Pick, "variant" | "size"> ->(({ className, variant = "outline", size = "default", ...props }, ref) => ( - -)); + AlertDialogButtonProps +>(({ className, variant = "outline", size = "default", ...props }, ref) => { + const [axes, rest] = splitAxisProps(props); + return ( + + ); +}); AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; export { diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx index 06eb845b..c8bf11c5 100644 --- a/components/ui/alert.tsx +++ b/components/ui/alert.tsx @@ -83,8 +83,10 @@ const Alert = React.forwardRef< }); Alert.displayName = "Alert"; -const AlertTitle = React.forwardRef>(({ className, ...props }, ref) => ( -
    +/* A div, as in shadcn: an
    put a fixed-level heading into the document outline of every page that showed + an alert. Wrap it in a heading of the right level where the alert IS a section heading. */ +const AlertTitle = React.forwardRef>(({ className, ...props }, ref) => ( +
    )); AlertTitle.displayName = "AlertTitle"; diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx index 328aea34..c8a9bcdb 100644 --- a/components/ui/badge.tsx +++ b/components/ui/badge.tsx @@ -8,7 +8,7 @@ import { cn } from "@/lib/utils"; /* Variant classes carry BEHAVIOR only (text, hover, destructive chrome); the SURFACE comes from materialSurface (the material system) — see SURFACE_ROLE below for how semantic variants ride it. */ const badgeVariants = cva( - "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2.5 py-0.5 text-xs font-semibold whitespace-nowrap transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2.5 py-0.5 text-xs font-semibold whitespace-nowrap transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", { variants: { variant: { diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx index 6c5dbcf9..ffaaf2fd 100644 --- a/components/ui/breadcrumb.tsx +++ b/components/ui/breadcrumb.tsx @@ -5,14 +5,11 @@ import * as React from "react"; import { type MaterialAxisProps, materialSurface, splitAxisProps } from "@/lib/material"; import { cn } from "@/lib/utils"; -const Breadcrumb = React.forwardRef< - HTMLElement, - React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ComponentType<{ - className?: string; - }>; - } ->(({ ...props }, ref) =>
  • (({ className, variant = "glass", effect, ...props }, ref) => { const [axes, rest] = splitAxisProps(props); - const m = materialSurface(variant === "default" ? null : ROLE, axes); + const m = materialSurface(variant === "glass" ? ROLE : null, axes); return ( ({ - variant: "glass", - size: "default", spacing: 0, }); +/* Item sizes, on the same steps as the standalone Toggle; `size` used to set data-size and nothing else. */ +const ITEM_SIZE: Record = { + sm: "px-2 py-0.5 text-xs", + default: "px-3 py-1", + lg: "px-4 py-2", +}; + /* Role: bordered adaptive glass. */ const ROLE = { border: true, @@ -28,28 +36,43 @@ const ToggleGroup = React.forwardRef< React.ComponentPropsWithoutRef & MaterialAxisProps & { variant?: ToggleGroupVariant; - size?: "default" | "sm" | "lg"; + size?: ToggleGroupSize; + /** Gap between items, in Tailwind spacing steps (0 = touching). */ spacing?: number; } ->(({ className, variant = "glass", size = "default", spacing = 0, children, ...props }, ref) => { - const [axes, rest] = splitAxisProps(props); +>(({ className, variant, size, spacing = 0, children, ...props }, ref) => { + const [axes, { style, ...rest }] = splitAxisProps(props); /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */ const variants = { default: "", glass: "rounded-md p-1", + outline: "", }; + const resolved = variant ?? "glass"; - const m = materialSurface(variant === "default" ? null : ROLE, axes); + const m = materialSurface(resolved === "glass" ? ROLE : null, axes); return ( , React.ComponentPropsWithoutRef & { variant?: ToggleGroupVariant; - size?: "default" | "sm" | "lg"; + size?: ToggleGroupSize; } >(({ className, variant, size, ...props }, ref) => { const context = React.useContext(ToggleGroupContext); + const itemVariant = context.variant || variant || "glass"; + const itemSize = context.size || size || "default"; return ( ,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nAccordionItem.displayName = \"AccordionItem\";\n\nconst AccordionTrigger = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n variant?: \"default\" | \"glass\";\n /** Glow rides the OPEN state (folded from the glass wrapper). */\n glow?: boolean;\n }\n>(({ className, variant = \"glass\", glow, children, ...props }, ref) => {\n // No glass surface here — the variants are hover tints only, so no resolveMaterial.\n const variants = {\n default: \"hover:bg-accent\",\n glass: \"hover:bg-muted/50\",\n };\n\n return (\n \n svg]:rotate-180\",\n variants[variant],\n glow && \"data-[state=open]:glass-glow\",\n className,\n )}\n {...props}\n >\n {children}\n \n \n \n );\n});\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;\n\nconst AccordionContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, ...props }, ref) => (\n \n
    {children}
    \n \n));\n\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nexport { Accordion, AccordionContent, AccordionItem, AccordionTrigger };\n", + "content": "\"use client\";\n\nimport { CaretDownIcon } from \"@phosphor-icons/react\";\nimport { Accordion as AccordionPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/* The root renders a div, so it stamps its data-slot like every other part. */\nconst Accordion = React.forwardRef, React.ComponentPropsWithoutRef>(\n (props, ref) => ,\n);\nAccordion.displayName = AccordionPrimitive.Root.displayName;\n\nconst AccordionItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nAccordionItem.displayName = \"AccordionItem\";\n\nconst AccordionTrigger = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n variant?: \"default\" | \"glass\";\n /** Glow rides the OPEN state (folded from the glass wrapper). */\n glow?: boolean;\n }\n>(({ className, variant = \"glass\", glow, children, ...props }, ref) => {\n // No glass surface here — the variants are hover tints only, so no resolveMaterial.\n const variants = {\n default: \"hover:bg-accent\",\n glass: \"hover:bg-muted/50\",\n };\n\n return (\n \n svg]:rotate-180\",\n variants[variant],\n glow && \"data-[state=open]:glass-glow\",\n className,\n )}\n {...props}\n >\n {children}\n \n \n \n );\n});\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;\n\nconst AccordionContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, ...props }, ref) => (\n \n
    {children}
    \n \n));\n\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nexport { Accordion, AccordionContent, AccordionItem, AccordionTrigger };\n", "type": "registry:component", "target": "components/ui/accordion.tsx" } diff --git a/public/r/alert-dialog.json b/public/r/alert-dialog.json index f2ecb195..51c44ee7 100644 --- a/public/r/alert-dialog.json +++ b/public/r/alert-dialog.json @@ -34,7 +34,7 @@ }, { "path": "components/ui/alert-dialog.tsx", - "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { AlertDialog as AlertDialogPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { buttonVariants } from \"./button\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface.\n Centered WITHOUT a transform — see the matching note in dialog.tsx: `left-1/2 top-1/2 -translate-1/2`\n puts any odd-pixel-sized dialog on a half pixel and blurs every glyph on the composited glass\n surface, so this centers through layout (`inset-0 m-auto h-fit`) and the slide-* utilities that only\n existed to offset that translate are gone. Fade + zoom end at identity. */\nconst alertDialogContentVariants = cva(\n // Capped like dialog.tsx so over-tall content can't run off the bottom unreachable. This one scrolls\n // the whole surface rather than an inner wrapper — AlertDialogContent hands children straight to\n // Radix, so there is nothing to wrap. Its actions scroll with the content; acceptable, because an\n // alert dialog that tall is already a design smell.\n \"group/alert-dialog-content @container/alert-dialog-content fixed inset-0 z-50 m-auto grid h-fit max-h-[calc(100dvh_-_2rem)] w-full max-w-lg gap-4 overflow-y-auto rounded-lg p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n {\n variants: {\n variant: {\n default: \"bg-background border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\n/* Role: bordered adaptive glass at the elevated blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"lg\",\n};\n\nconst AlertDialog = ({ ...props }: React.ComponentProps) => (\n \n);\n\nconst AlertDialogTrigger = ({ ...props }: React.ComponentProps) => (\n \n);\n\nconst AlertDialogPortal = ({ ...props }: React.ComponentProps) => (\n \n);\n\nconst AlertDialogOverlay = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n VariantProps &\n MaterialAxisProps & {\n /** Elevated backdrop blur (folded from the glass wrapper). */\n animated?: boolean;\n size?: \"default\" | \"sm\";\n }\n>(({ className, variant = \"glass\", animated = true, size = \"default\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n \n );\n});\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (\n \n);\nAlertDialogHeader.displayName = \"AlertDialogHeader\";\n\nconst AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (\n \n);\nAlertDialogFooter.displayName = \"AlertDialogFooter\";\n\nconst AlertDialogTitle = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogDescription = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nAlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;\n\nconst AlertDialogMedia = ({ className, ...props }: React.HTMLAttributes) => (\n \n);\nAlertDialogMedia.displayName = \"AlertDialogMedia\";\n\nconst AlertDialogAction = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & Pick, \"variant\" | \"size\">\n>(({ className, variant = \"glass\", size = \"default\", ...props }, ref) => (\n \n));\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & Pick, \"variant\" | \"size\">\n>(({ className, variant = \"outline\", size = \"default\", ...props }, ref) => (\n \n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogMedia,\n AlertDialogOverlay,\n AlertDialogPortal,\n AlertDialogTitle,\n AlertDialogTrigger,\n};\n", + "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { AlertDialog as AlertDialogPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button, type buttonVariants } from \"./button\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface.\n Centered WITHOUT a transform — see the matching note in dialog.tsx: `left-1/2 top-1/2 -translate-1/2`\n puts any odd-pixel-sized dialog on a half pixel and blurs every glyph on the composited glass\n surface, so this centers through layout (`inset-0 m-auto h-fit`) and the slide-* utilities that only\n existed to offset that translate are gone. Fade + zoom end at identity. */\nconst alertDialogContentVariants = cva(\n // A 1rem gutter each side on narrow screens (`min(100% − 2rem, …)`): at `w-full max-w-lg` it ran edge to\n // edge below 512px. Capped like dialog.tsx so over-tall content can't run off the bottom unreachable. This one scrolls\n // the whole surface rather than an inner wrapper — AlertDialogContent hands children straight to\n // Radix, so there is nothing to wrap. Its actions scroll with the content; acceptable, because an\n // alert dialog that tall is already a design smell.\n \"group/alert-dialog-content @container/alert-dialog-content fixed inset-0 z-50 m-auto grid h-fit max-h-[calc(100dvh_-_2rem)] w-full max-w-[min(calc(100%-2rem),32rem)] gap-4 overflow-y-auto rounded-lg p-6 shadow-lg duration-200 data-[size=sm]:max-w-[min(calc(100%-2rem),20rem)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n {\n variants: {\n variant: {\n default: \"bg-background border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\n/* Role: bordered adaptive glass at the elevated blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"lg\",\n};\n\nconst AlertDialog = ({ ...props }: React.ComponentProps) => (\n \n);\n\nconst AlertDialogTrigger = ({ ...props }: React.ComponentProps) => (\n \n);\n\nconst AlertDialogPortal = ({ ...props }: React.ComponentProps) => (\n \n);\n\nconst AlertDialogOverlay = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n VariantProps &\n MaterialAxisProps & {\n /** Elevated backdrop blur (folded from the glass wrapper). */\n animated?: boolean;\n size?: \"default\" | \"sm\";\n }\n>(({ className, variant = \"glass\", animated = true, size = \"default\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n \n );\n});\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (\n \n);\nAlertDialogHeader.displayName = \"AlertDialogHeader\";\n\nconst AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (\n \n);\nAlertDialogFooter.displayName = \"AlertDialogFooter\";\n\nconst AlertDialogTitle = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogDescription = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nAlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;\n\nconst AlertDialogMedia = ({ className, ...props }: React.HTMLAttributes) => (\n \n);\nAlertDialogMedia.displayName = \"AlertDialogMedia\";\n\n/* Action and Cancel ARE Buttons (asChild onto the Radix primitive), as upstream composes them. They used to\n put buttonVariants on the bare primitive, but in Sistine buttonVariants carries behaviour only: the\n SURFACE comes from materialSurface inside Button. So the default (glass) action rendered with no glass at\n all, a destructive one without its tint, and neither carried data-variant / data-size / data-material\n or accepted the material axes. */\ntype AlertDialogButtonProps = React.ComponentPropsWithoutRef &\n Pick, \"variant\" | \"size\"> &\n MaterialAxisProps;\n\nconst AlertDialogAction = React.forwardRef<\n React.ElementRef,\n AlertDialogButtonProps\n>(({ className, variant = \"glass\", size = \"default\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n return (\n \n );\n});\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n React.ElementRef,\n AlertDialogButtonProps\n>(({ className, variant = \"outline\", size = \"default\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n return (\n \n );\n});\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogMedia,\n AlertDialogOverlay,\n AlertDialogPortal,\n AlertDialogTitle,\n AlertDialogTrigger,\n};\n", "type": "registry:component", "target": "components/ui/alert-dialog.tsx" } diff --git a/public/r/alert.json b/public/r/alert.json index 69cce48d..bd47af79 100644 --- a/public/r/alert.json +++ b/public/r/alert.json @@ -32,7 +32,7 @@ }, { "path": "components/ui/alert.tsx", - "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { type HoverEffect, hoverEffects } from \"@/lib/hover-effects\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only (text, status chrome); the surface comes from materialSurface.\n Status variants ride a BORDERLESS glass surface (their colored border is their own chrome). */\nconst alertVariants = cva(\n \"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7\",\n {\n variants: {\n variant: {\n default: \"bg-background text-foreground\",\n glass: \"text-foreground\",\n info: \"border border-blue-500/60 text-blue-600 shadow-[var(--glass-shadow)] dark:text-blue-400 [&>svg]:text-blue-600 dark:[&>svg]:text-blue-400\",\n success:\n \"border border-green-500/60 text-green-600 shadow-[var(--glass-shadow)] dark:text-green-400 [&>svg]:text-green-600 dark:[&>svg]:text-green-400\",\n warning:\n \"border border-yellow-500/60 text-yellow-600 shadow-[var(--glass-shadow)] dark:text-yellow-400 [&>svg]:text-yellow-600 dark:[&>svg]:text-yellow-400\",\n destructive: \"border border-destructive/60 text-destructive shadow-[var(--glass-shadow)] [&>svg]:text-destructive\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\nconst STATUS_VARIANTS = [\n \"info\",\n \"success\",\n \"warning\",\n \"destructive\",\n] as const;\n\n/* Each semantic variant's surface role: statuses ride a BORDERLESS glass surface (their colored\n border is their own chrome); default renders no glass surface. */\nconst SURFACE_ROLE: Record = {\n default: null,\n glass: {},\n info: {},\n success: {},\n warning: {},\n destructive: {},\n};\n\nconst Alert = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes &\n VariantProps &\n MaterialAxisProps & {\n effect?: HoverEffect;\n }\n>(({ className, variant, effect, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const isStatus = variant !== null && variant !== undefined && (STATUS_VARIANTS as readonly string[]).includes(variant);\n const m = materialSurface(SURFACE_ROLE[variant ?? \"glass\"] ?? null, axes);\n\n return (\n \n );\n});\nAlert.displayName = \"Alert\";\n\nconst AlertTitle = React.forwardRef>(({ className, ...props }, ref) => (\n
    \n));\nAlertTitle.displayName = \"AlertTitle\";\n\nconst AlertDescription = React.forwardRef>(({ className, ...props }, ref) => (\n
    \n));\nAlertDescription.displayName = \"AlertDescription\";\n\nexport { Alert, AlertDescription, AlertTitle };\n", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { type HoverEffect, hoverEffects } from \"@/lib/hover-effects\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only (text, status chrome); the surface comes from materialSurface.\n Status variants ride a BORDERLESS glass surface (their colored border is their own chrome). */\nconst alertVariants = cva(\n \"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7\",\n {\n variants: {\n variant: {\n default: \"bg-background text-foreground\",\n glass: \"text-foreground\",\n info: \"border border-blue-500/60 text-blue-600 shadow-[var(--glass-shadow)] dark:text-blue-400 [&>svg]:text-blue-600 dark:[&>svg]:text-blue-400\",\n success:\n \"border border-green-500/60 text-green-600 shadow-[var(--glass-shadow)] dark:text-green-400 [&>svg]:text-green-600 dark:[&>svg]:text-green-400\",\n warning:\n \"border border-yellow-500/60 text-yellow-600 shadow-[var(--glass-shadow)] dark:text-yellow-400 [&>svg]:text-yellow-600 dark:[&>svg]:text-yellow-400\",\n destructive: \"border border-destructive/60 text-destructive shadow-[var(--glass-shadow)] [&>svg]:text-destructive\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\nconst STATUS_VARIANTS = [\n \"info\",\n \"success\",\n \"warning\",\n \"destructive\",\n] as const;\n\n/* Each semantic variant's surface role: statuses ride a BORDERLESS glass surface (their colored\n border is their own chrome); default renders no glass surface. */\nconst SURFACE_ROLE: Record = {\n default: null,\n glass: {},\n info: {},\n success: {},\n warning: {},\n destructive: {},\n};\n\nconst Alert = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes &\n VariantProps &\n MaterialAxisProps & {\n effect?: HoverEffect;\n }\n>(({ className, variant, effect, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const isStatus = variant !== null && variant !== undefined && (STATUS_VARIANTS as readonly string[]).includes(variant);\n const m = materialSurface(SURFACE_ROLE[variant ?? \"glass\"] ?? null, axes);\n\n return (\n \n );\n});\nAlert.displayName = \"Alert\";\n\n/* A div, as in shadcn: an
    put a fixed-level heading into the document outline of every page that showed\n an alert. Wrap it in a heading of the right level where the alert IS a section heading. */\nconst AlertTitle = React.forwardRef>(({ className, ...props }, ref) => (\n
    \n));\nAlertTitle.displayName = \"AlertTitle\";\n\nconst AlertDescription = React.forwardRef>(({ className, ...props }, ref) => (\n
    \n));\nAlertDescription.displayName = \"AlertDescription\";\n\nexport { Alert, AlertDescription, AlertTitle };\n", "type": "registry:component", "target": "components/ui/alert.tsx" } diff --git a/public/r/badge.json b/public/r/badge.json index b3ebd5ef..be0b132a 100644 --- a/public/r/badge.json +++ b/public/r/badge.json @@ -33,7 +33,7 @@ }, { "path": "components/ui/badge.tsx", - "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\n\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only (text, hover, destructive chrome); the SURFACE comes from\n materialSurface (the material system) — see SURFACE_ROLE below for how semantic variants ride it. */\nconst badgeVariants = cva(\n \"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2.5 py-0.5 text-xs font-semibold whitespace-nowrap transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3\",\n {\n variants: {\n variant: {\n default: \"border-transparent bg-primary text-primary-foreground shadow [a&]:hover:bg-primary/80\",\n glass: \"text-foreground\",\n secondary: \"text-muted-foreground\",\n destructive: \"border border-destructive/50 text-destructive\",\n outline: \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n ghost: \"border-transparent [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n link: \"border-transparent text-primary underline-offset-4 [a&]:hover:underline\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\n/* Each semantic variant's surface role (null = no glass surface — the variant styles itself, e.g.\n default/outline/ghost/link; destructive rides a borderless glass surface, its red border its own). */\nconst SURFACE_ROLE: Record = {\n glass: {\n border: true,\n },\n secondary: {\n border: true,\n },\n destructive: {\n size: \"sm\",\n },\n default: null,\n outline: null,\n ghost: null,\n link: null,\n};\n\nexport interface BadgeProps extends React.HTMLAttributes, VariantProps, MaterialAxisProps {\n asChild?: boolean;\n}\n\nfunction Badge({ className, variant, asChild = false, ...props }: BadgeProps) {\n const Comp = asChild ? SlotPrimitive.Slot : \"span\";\n\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(SURFACE_ROLE[variant ?? \"glass\"] ?? null, axes);\n\n return (\n \n );\n}\n\nexport { Badge, badgeVariants };\n", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport type * as React from \"react\";\n\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only (text, hover, destructive chrome); the SURFACE comes from\n materialSurface (the material system) — see SURFACE_ROLE below for how semantic variants ride it. */\nconst badgeVariants = cva(\n \"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2.5 py-0.5 text-xs font-semibold whitespace-nowrap transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3\",\n {\n variants: {\n variant: {\n default: \"border-transparent bg-primary text-primary-foreground shadow [a&]:hover:bg-primary/80\",\n glass: \"text-foreground\",\n secondary: \"text-muted-foreground\",\n destructive: \"border border-destructive/50 text-destructive\",\n outline: \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n ghost: \"border-transparent [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n link: \"border-transparent text-primary underline-offset-4 [a&]:hover:underline\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\n/* Each semantic variant's surface role (null = no glass surface — the variant styles itself, e.g.\n default/outline/ghost/link; destructive rides a borderless glass surface, its red border its own). */\nconst SURFACE_ROLE: Record = {\n glass: {\n border: true,\n },\n secondary: {\n border: true,\n },\n destructive: {\n size: \"sm\",\n },\n default: null,\n outline: null,\n ghost: null,\n link: null,\n};\n\nexport interface BadgeProps extends React.HTMLAttributes, VariantProps, MaterialAxisProps {\n asChild?: boolean;\n}\n\nfunction Badge({ className, variant, asChild = false, ...props }: BadgeProps) {\n const Comp = asChild ? SlotPrimitive.Slot : \"span\";\n\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(SURFACE_ROLE[variant ?? \"glass\"] ?? null, axes);\n\n return (\n \n );\n}\n\nexport { Badge, badgeVariants };\n", "type": "registry:component", "target": "components/ui/badge.tsx" } diff --git a/public/r/breadcrumb.json b/public/r/breadcrumb.json index bf16d7cd..97278f9b 100644 --- a/public/r/breadcrumb.json +++ b/public/r/breadcrumb.json @@ -34,7 +34,7 @@ }, { "path": "components/ui/breadcrumb.tsx", - "content": "import { ArrowsHorizontalIcon, CaretRightIcon } from \"@phosphor-icons/react\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\nconst Breadcrumb = React.forwardRef<\n HTMLElement,\n React.ComponentPropsWithoutRef<\"nav\"> & {\n separator?: React.ComponentType<{\n className?: string;\n }>;\n }\n>(({ ...props }, ref) =>
    \n\n {totalItems > 1 && (\n <>\n \n \n \n \n\n
    \n {items.map((_, index) => (\n goToSlide(index)}\n aria-label={`Go to slide ${index + 1}`}\n />\n ))}\n
    \n \n )}\n
    \n );\n },\n);\nCarousel.displayName = \"Carousel\";\n\nexport { Carousel };\n", + "content": "\"use client\";\n\nimport { CaretLeftIcon, CaretRightIcon, PauseIcon, PlayIcon } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { type HoverEffect, hoverEffects } from \"@/lib/hover-effects\";\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst carouselVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground border shadow-sm\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: borderless adaptive glass. */\nconst ROLE = {};\n\nexport interface CarouselProps extends React.HTMLAttributes, VariantProps, MaterialAxisProps {\n effect?: HoverEffect;\n autoPlay?: boolean;\n interval?: number;\n}\n\n/* The prefers-reduced-motion query, live. */\nfunction useReducedMotion() {\n return React.useSyncExternalStore(\n (onChange) => {\n const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n query.addEventListener(\"change\", onChange);\n return () => query.removeEventListener(\"change\", onChange);\n },\n () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n () => false,\n );\n}\n\n/* The accessibility contract follows shadcn's carousel (an Embla component with a different API; this one\n stays a children-based slider): the root is a labelled `region` with aria-roledescription \"carousel\"\n and every slide a `group` \"slide\"; ←/→ move between slides while focus is inside; the previous / next\n buttons are named (they were icon-only, so a screen reader announced two unnamed buttons) and the\n current dot is marked. Off-screen slides are `inert`, so Tab cannot land inside a slide you cannot see.\n Autoplay (WCAG 2.2.2): it pauses while the pointer or focus is on the carousel, carries a visible\n pause / play button, and does not start at all under prefers-reduced-motion. */\nconst Carousel = React.forwardRef(\n ({ className, variant = \"glass\", effect, autoPlay = false, interval = 3000, children, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const [currentIndex, setCurrentIndex] = React.useState(0);\n const [paused, setPaused] = React.useState(false);\n const [hovered, setHovered] = React.useState(false);\n const [focused, setFocused] = React.useState(false);\n const reducedMotion = useReducedMotion();\n const items = React.Children.toArray(children);\n const totalItems = items.length;\n const playing = autoPlay && !paused && !hovered && !focused && !reducedMotion && totalItems > 1;\n\n React.useEffect(() => {\n if (!playing) return;\n\n const timer = setInterval(() => {\n setCurrentIndex((prev) => (prev + 1) % totalItems);\n }, interval);\n\n return () => clearInterval(timer);\n }, [\n playing,\n interval,\n totalItems,\n ]);\n\n const goToSlide = (index: number) => {\n setCurrentIndex(index);\n };\n\n const goToPrevious = () => {\n setCurrentIndex((prev) => (prev - 1 + totalItems) % totalItems);\n };\n\n const goToNext = () => {\n setCurrentIndex((prev) => (prev + 1) % totalItems);\n };\n\n const onKeyDownCapture = (event: React.KeyboardEvent) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n goToPrevious();\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault();\n goToNext();\n }\n };\n\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n if (totalItems === 0) return null;\n\n return (\n setHovered(true)}\n onPointerLeave={() => setHovered(false)}\n onFocus={() => setFocused(true)}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setFocused(false);\n }}\n {...rest}\n >\n \n {items.map((item, index) => (\n \n {item}\n
    \n ))}\n \n\n {totalItems > 1 && (\n <>\n \n \n \n \n \n \n\n
    \n {items.map((_, index) => (\n goToSlide(index)}\n aria-label={`Go to slide ${index + 1}`}\n aria-current={index === currentIndex ? \"true\" : undefined}\n />\n ))}\n
    \n\n {autoPlay && !reducedMotion && (\n setPaused((p) => !p)}\n >\n {paused ? : }\n \n )}\n \n )}\n \n );\n },\n);\nCarousel.displayName = \"Carousel\";\n\nexport { Carousel };\n", "type": "registry:component", "target": "components/ui/carousel.tsx" } diff --git a/public/r/chart.json b/public/r/chart.json index e65e66d3..743c9d40 100644 --- a/public/r/chart.json +++ b/public/r/chart.json @@ -33,7 +33,7 @@ }, { "path": "components/ui/chart.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as RechartsPrimitive from \"recharts\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = {\n light: \"\",\n dark: \".dark\",\n} as const;\n\n/* Roles: the container and legend are bordered adaptive glass;\n the tooltip is bordered + veiled. */\nconst SURFACE_ROLE = {\n border: true,\n};\nconst TOOLTIP_ROLE = {\n border: true,\n veil: true,\n};\n\nexport type ChartConfig = {\n [key in string]: {\n label?: React.ReactNode;\n icon?: React.ComponentType;\n } & (\n | {\n color?: string;\n theme?: never;\n }\n | {\n color?: never;\n theme: {\n light?: string;\n dark?: string;\n };\n }\n );\n};\n\ntype ChartContextProps = {\n config: ChartConfig;\n};\n\nconst ChartContext = React.createContext(null);\n\nfunction useChart() {\n const context = React.useContext(ChartContext);\n\n if (!context) {\n throw new Error(\"useChart must be used within a \");\n }\n\n return context;\n}\n\ntype PlotSize = {\n width: number;\n height: number;\n};\n\n/**\n * The plot box's size, measured by ONE ResizeObserver on an element whose size comes from CSS alone.\n *\n * Recharts' percent-sized ResponsiveContainer observes its own div, and the container used to give that\n * div `minHeight={150}` inside an `aspect-video` box: measure, grow to the floor, measure again — a\n * ResizeObserver feedback loop WebKit reports as \"ResizeObserver loop completed with undelivered\n * notifications\" and re-enters every frame. Here the measured box never contains the chart in flow (the\n * chart sits in an absolute layer), so what it measures cannot change what it measures.\n *\n * - `null` until the first non-empty measurement: nothing renders at 0×0, which is what a chart in a\n * hidden (display: none) tab measures, and why recharts warned \"width(0) and height(0) of chart\n * should be greater than 0\" on every pass.\n * - A 0×0 measurement after that is ignored, so a tab that hides keeps its last size rather than\n * unmounting the chart.\n * - `\"unobserved\"` when there is no ResizeObserver: the caller falls back to the percent container.\n */\nfunction usePlotSize(ref: React.RefObject): PlotSize | \"unobserved\" | null {\n const [size, setSize] = React.useState(null);\n\n React.useEffect(() => {\n const el = ref.current;\n if (!el) return;\n if (typeof ResizeObserver === \"undefined\") {\n setSize(\"unobserved\");\n return;\n }\n const observer = new ResizeObserver((entries) => {\n const box = entries[entries.length - 1]?.contentRect;\n if (!box) return;\n /* Floor, not round: a chart half a pixel wider than its layer would overflow it. */\n const width = Math.floor(box.width);\n const height = Math.floor(box.height);\n if (width <= 0 || height <= 0) return;\n setSize((prev) =>\n prev !== null && prev !== \"unobserved\" && prev.width === width && prev.height === height\n ? prev\n : {\n width,\n height,\n },\n );\n });\n observer.observe(el);\n return () => observer.disconnect();\n }, [\n ref,\n ]);\n\n return size;\n}\n\nconst ChartContainer = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n config: ChartConfig;\n children: React.ComponentProps[\"children\"];\n } & MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }\n>(({ id, className, children, config, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const uniqueId = React.useId();\n const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`;\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"\",\n glass: \"rounded-lg p-4\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : SURFACE_ROLE, axes);\n const plotRef = React.useRef(null);\n const plot = usePlotSize(plotRef);\n\n return (\n \n \n // .recharts-cartesian-axis-tick-label > text). The old descendant selector matched nothing after\n // the upgrade, so every tick kept Recharts' own `fill=\"#666\"` — grey in all 17 themes, in both\n // modes, which is what made the axes unreadable. .recharts-cartesian-axis-tick-value sits ON the\n // in both Recharts 2 and 3, so it survives the structure change. (A CSS `fill` beats the\n // fill presentation attribute Recharts sets, so no !important is needed.)\n /* ONE size contract: the caller's height (`h-[300px]`), else the aspect. `min-h-[150px]` is the\n floor the old `minHeight={150}` gave the chart, moved onto this box so it no longer feeds back\n through what the observer measures; a caller's min-h-* still wins. */\n \"flex min-h-[150px] aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick-value]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none\",\n variants[variant],\n className,\n )}\n {...rest}\n >\n \n
    \n {plot === \"unobserved\" ? (\n {children}\n ) : plot ? (\n /* Absolute, so the chart never sizes the box being measured. Numeric width + height take\n recharts' fixed-size path: no size-detector div and no second observer. */\n
    \n \n {children}\n \n
    \n ) : null}\n
    \n \n \n );\n});\nChartContainer.displayName = \"Chart\";\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);\n\n if (!colorConfig.length) {\n return null;\n }\n\n return (\n `\n${prefix} [data-chart=${id}] {\n${colorConfig\n .map(([key, itemConfig]) => {\n const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;\n return color ? ` --color-${key}: ${color};` : null;\n })\n .join(\"\\n\")}\n}\n`,\n )\n .join(\"\\n\"),\n }}\n />\n );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\ntype TooltipProps = React.ComponentProps & {\n active?: boolean;\n payload?: Array<{\n dataKey?: string;\n name?: string;\n type?: string;\n value?: number | string;\n color?: string;\n payload?: {\n fill?: string;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n }>;\n};\n\ntype TooltipPayloadItem = NonNullable[number];\n\nconst ChartTooltipContent = React.forwardRef<\n HTMLDivElement,\n Omit &\n React.ComponentProps<\"div\"> &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n indicator?: \"dot\" | \"line\" | \"dashed\";\n hideLabel?: boolean;\n hideIndicator?: boolean;\n nameKey?: string;\n labelKey?: string;\n label?: React.ReactNode;\n labelClassName?: string;\n color?: string;\n labelFormatter?: (value: React.ReactNode, payload: TooltipPayloadItem[]) => React.ReactNode;\n formatter?: (\n value: TooltipPayloadItem[\"value\"],\n name: TooltipPayloadItem[\"name\"],\n item: TooltipPayloadItem,\n index: number,\n payload: TooltipPayloadItem[\"payload\"],\n ) => React.ReactNode;\n }\n>(\n (\n {\n active,\n payload,\n className,\n indicator = \"dot\",\n variant = \"glass\",\n hideLabel = false,\n hideIndicator = false,\n label,\n labelFormatter,\n labelClassName,\n formatter,\n color,\n nameKey,\n labelKey,\n ...props\n },\n ref,\n ) => {\n /* This component never forwards unknown props to the DOM — only the axes are consumed. */\n const [axes] = splitAxisProps(props);\n const { config } = useChart();\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : TOOLTIP_ROLE, axes);\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null;\n }\n\n const [item] = payload;\n const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n const value = !labelKey && typeof label === \"string\" ? config[label]?.label || label : configItem?.label;\n\n if (labelFormatter) {\n return
    {labelFormatter(value, payload)}
    ;\n }\n\n if (!value) {\n return null;\n }\n\n return
    {value}
    ;\n }, [\n label,\n labelFormatter,\n payload,\n hideLabel,\n labelClassName,\n config,\n labelKey,\n ]);\n\n if (!active || !payload?.length) {\n return null;\n }\n\n const nestLabel = payload.length === 1 && indicator !== \"dot\";\n\n return (\n \n {!nestLabel ? tooltipLabel : null}\n
    \n {payload\n ?.filter((item) => item.type !== \"none\")\n .map((item, index) => {\n const key = `${nameKey || item.name || item.dataKey || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n const indicatorColor =\n color ||\n (\n item.payload as\n | {\n fill?: string;\n }\n | undefined\n )?.fill ||\n item.color;\n\n return (\n svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground\",\n indicator === \"dot\" && \"items-center\",\n )}\n >\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, item.payload)\n ) : (\n <>\n {configItem?.icon ? (\n \n ) : (\n !hideIndicator && (\n \n )\n )}\n
    \n
    \n {nestLabel ? tooltipLabel : null}\n {configItem?.label || item.name}\n
    \n {item.value != null && (\n \n {typeof item.value === \"number\" ? item.value.toLocaleString() : String(item.value)}\n \n )}\n
    \n \n )}\n
    \n );\n })}\n \n \n );\n },\n);\nChartTooltipContent.displayName = \"ChartTooltip\";\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\ntype LegendProps = {\n payload?: Array<{\n value?: string;\n dataKey?: string;\n type?: string;\n color?: string;\n [key: string]: unknown;\n }>;\n verticalAlign?: \"top\" | \"bottom\";\n};\n\nconst ChartLegendContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> &\n LegendProps &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n hideIcon?: boolean;\n nameKey?: string;\n }\n>(({ className, payload, verticalAlign = \"bottom\", variant = \"glass\", hideIcon = false, nameKey, ...props }, ref) => {\n /* This component never forwards unknown props to the DOM — only the axes are consumed. */\n const [axes] = splitAxisProps(props);\n const { config } = useChart();\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"\",\n glass: \"rounded-lg px-2 py-1\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : SURFACE_ROLE, axes);\n\n if (!payload?.length) {\n return null;\n }\n\n return (\n \n {payload\n .filter((item) => item.type !== \"none\")\n .map((item, index) => {\n const key = `${nameKey || item.dataKey || item.value || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n\n return (\n
    svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground\")}>\n {configItem?.icon && !hideIcon ? (\n \n ) : (\n \n )}\n {configItem?.label}\n
    \n );\n })}\n \n );\n});\nChartLegendContent.displayName = \"ChartLegend\";\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {\n if (typeof payload !== \"object\" || payload === null) {\n return undefined;\n }\n\n const payloadPayload = \"payload\" in payload && typeof payload.payload === \"object\" && payload.payload !== null ? payload.payload : undefined;\n\n let configLabelKey: string = key;\n\n if (key in payload && typeof payload[key as keyof typeof payload] === \"string\") {\n configLabelKey = payload[key as keyof typeof payload] as string;\n } else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\") {\n configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;\n }\n\n return configLabelKey in config ? config[configLabelKey] : config[key];\n}\n\nexport { ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent };\n", + "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as RechartsPrimitive from \"recharts\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = {\n light: \"\",\n dark: \".dark\",\n} as const;\n\n/* Roles: the container and legend are bordered adaptive glass;\n the tooltip is bordered + veiled. */\nconst SURFACE_ROLE = {\n border: true,\n};\nconst TOOLTIP_ROLE = {\n border: true,\n veil: true,\n};\n\nexport type ChartConfig = {\n [key in string]: {\n label?: React.ReactNode;\n icon?: React.ComponentType;\n } & (\n | {\n color?: string;\n theme?: never;\n }\n | {\n color?: never;\n theme: {\n light?: string;\n dark?: string;\n };\n }\n );\n};\n\ntype ChartContextProps = {\n config: ChartConfig;\n};\n\nconst ChartContext = React.createContext(null);\n\nfunction useChart() {\n const context = React.useContext(ChartContext);\n\n if (!context) {\n throw new Error(\"useChart must be used within a \");\n }\n\n return context;\n}\n\ntype PlotSize = {\n width: number;\n height: number;\n};\n\n/**\n * The plot box's size, measured by ONE ResizeObserver on an element whose size comes from CSS alone.\n *\n * Recharts' percent-sized ResponsiveContainer observes its own div, and the container used to give that\n * div `minHeight={150}` inside an `aspect-video` box: measure, grow to the floor, measure again — a\n * ResizeObserver feedback loop WebKit reports as \"ResizeObserver loop completed with undelivered\n * notifications\" and re-enters every frame. Here the measured box never contains the chart in flow (the\n * chart sits in an absolute layer), so what it measures cannot change what it measures.\n *\n * - `null` until the first non-empty measurement: nothing renders at 0×0, which is what a chart in a\n * hidden (display: none) tab measures, and why recharts warned \"width(0) and height(0) of chart\n * should be greater than 0\" on every pass.\n * - A 0×0 measurement after that is ignored, so a tab that hides keeps its last size rather than\n * unmounting the chart.\n * - `\"unobserved\"` when there is no ResizeObserver: the caller falls back to the percent container.\n */\nfunction usePlotSize(ref: React.RefObject): PlotSize | \"unobserved\" | null {\n const [size, setSize] = React.useState(null);\n\n React.useEffect(() => {\n const el = ref.current;\n if (!el) return;\n if (typeof ResizeObserver === \"undefined\") {\n setSize(\"unobserved\");\n return;\n }\n const observer = new ResizeObserver((entries) => {\n const box = entries[entries.length - 1]?.contentRect;\n if (!box) return;\n /* Floor, not round: a chart half a pixel wider than its layer would overflow it. */\n const width = Math.floor(box.width);\n const height = Math.floor(box.height);\n if (width <= 0 || height <= 0) return;\n setSize((prev) =>\n prev !== null && prev !== \"unobserved\" && prev.width === width && prev.height === height\n ? prev\n : {\n width,\n height,\n },\n );\n });\n observer.observe(el);\n return () => observer.disconnect();\n }, [\n ref,\n ]);\n\n return size;\n}\n\nconst ChartContainer = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> & {\n config: ChartConfig;\n children: React.ComponentProps[\"children\"];\n } & MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }\n>(({ id, className, children, config, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const uniqueId = React.useId();\n const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`;\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"\",\n glass: \"rounded-lg p-4\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : SURFACE_ROLE, axes);\n const plotRef = React.useRef(null);\n const plot = usePlotSize(plotRef);\n\n return (\n \n \n // .recharts-cartesian-axis-tick-label > text). The old descendant selector matched nothing after\n // the upgrade, so every tick kept Recharts' own `fill=\"#666\"` — grey in all 17 themes, in both\n // modes, which is what made the axes unreadable. .recharts-cartesian-axis-tick-value sits ON the\n // in both Recharts 2 and 3, so it survives the structure change. (A CSS `fill` beats the\n // fill presentation attribute Recharts sets, so no !important is needed.)\n /* ONE size contract: the caller's height (`h-[300px]`), else the aspect. `min-h-[150px]` is the\n floor the old `minHeight={150}` gave the chart, moved onto this box so it no longer feeds back\n through what the observer measures; a caller's min-h-* still wins. */\n \"flex min-h-[150px] aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick-value]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden\",\n variants[variant],\n className,\n )}\n {...rest}\n >\n \n
    \n {plot === \"unobserved\" ? (\n {children}\n ) : plot ? (\n /* Absolute, so the chart never sizes the box being measured. Numeric width + height take\n recharts' fixed-size path: no size-detector div and no second observer. */\n
    \n \n {children}\n \n
    \n ) : null}\n
    \n \n \n );\n});\nChartContainer.displayName = \"Chart\";\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);\n\n if (!colorConfig.length) {\n return null;\n }\n\n return (\n `\n${prefix} [data-chart=${id}] {\n${colorConfig\n .map(([key, itemConfig]) => {\n const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;\n return color ? ` --color-${key}: ${color};` : null;\n })\n .join(\"\\n\")}\n}\n`,\n )\n .join(\"\\n\"),\n }}\n />\n );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\ntype TooltipProps = React.ComponentProps & {\n active?: boolean;\n payload?: Array<{\n dataKey?: string;\n name?: string;\n type?: string;\n value?: number | string;\n color?: string;\n payload?: {\n fill?: string;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n }>;\n};\n\ntype TooltipPayloadItem = NonNullable[number];\n\nconst ChartTooltipContent = React.forwardRef<\n HTMLDivElement,\n Omit &\n React.ComponentProps<\"div\"> &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n indicator?: \"dot\" | \"line\" | \"dashed\";\n hideLabel?: boolean;\n hideIndicator?: boolean;\n nameKey?: string;\n labelKey?: string;\n label?: React.ReactNode;\n labelClassName?: string;\n color?: string;\n labelFormatter?: (value: React.ReactNode, payload: TooltipPayloadItem[]) => React.ReactNode;\n formatter?: (\n value: TooltipPayloadItem[\"value\"],\n name: TooltipPayloadItem[\"name\"],\n item: TooltipPayloadItem,\n index: number,\n payload: TooltipPayloadItem[\"payload\"],\n ) => React.ReactNode;\n }\n>(\n (\n {\n active,\n payload,\n className,\n indicator = \"dot\",\n variant = \"glass\",\n hideLabel = false,\n hideIndicator = false,\n label,\n labelFormatter,\n labelClassName,\n formatter,\n color,\n nameKey,\n labelKey,\n ...props\n },\n ref,\n ) => {\n /* This component never forwards unknown props to the DOM — only the axes are consumed. */\n const [axes] = splitAxisProps(props);\n const { config } = useChart();\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : TOOLTIP_ROLE, axes);\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null;\n }\n\n const [item] = payload;\n const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n const value = !labelKey && typeof label === \"string\" ? config[label]?.label || label : configItem?.label;\n\n if (labelFormatter) {\n return
    {labelFormatter(value, payload)}
    ;\n }\n\n if (!value) {\n return null;\n }\n\n return
    {value}
    ;\n }, [\n label,\n labelFormatter,\n payload,\n hideLabel,\n labelClassName,\n config,\n labelKey,\n ]);\n\n if (!active || !payload?.length) {\n return null;\n }\n\n const nestLabel = payload.length === 1 && indicator !== \"dot\";\n\n return (\n \n {!nestLabel ? tooltipLabel : null}\n
    \n {payload\n ?.filter((item) => item.type !== \"none\")\n .map((item, index) => {\n const key = `${nameKey || item.name || item.dataKey || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n const indicatorColor =\n color ||\n (\n item.payload as\n | {\n fill?: string;\n }\n | undefined\n )?.fill ||\n item.color;\n\n return (\n svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground\",\n indicator === \"dot\" && \"items-center\",\n )}\n >\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, item.payload)\n ) : (\n <>\n {configItem?.icon ? (\n \n ) : (\n !hideIndicator && (\n \n )\n )}\n
    \n
    \n {nestLabel ? tooltipLabel : null}\n {configItem?.label || item.name}\n
    \n {item.value != null && (\n \n {typeof item.value === \"number\" ? item.value.toLocaleString() : String(item.value)}\n \n )}\n
    \n \n )}\n
    \n );\n })}\n \n \n );\n },\n);\nChartTooltipContent.displayName = \"ChartTooltip\";\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\ntype LegendProps = {\n payload?: Array<{\n value?: string;\n dataKey?: string;\n type?: string;\n color?: string;\n [key: string]: unknown;\n }>;\n verticalAlign?: \"top\" | \"bottom\";\n};\n\nconst ChartLegendContent = React.forwardRef<\n HTMLDivElement,\n React.ComponentProps<\"div\"> &\n LegendProps &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n hideIcon?: boolean;\n nameKey?: string;\n }\n>(({ className, payload, verticalAlign = \"bottom\", variant = \"glass\", hideIcon = false, nameKey, ...props }, ref) => {\n /* This component never forwards unknown props to the DOM — only the axes are consumed. */\n const [axes] = splitAxisProps(props);\n const { config } = useChart();\n\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"\",\n glass: \"rounded-lg px-2 py-1\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : SURFACE_ROLE, axes);\n\n if (!payload?.length) {\n return null;\n }\n\n return (\n \n {payload\n .filter((item) => item.type !== \"none\")\n .map((item, index) => {\n const key = `${nameKey || item.dataKey || item.value || \"value\"}`;\n const configItem = getPayloadConfigFromPayload(config, item, key);\n\n return (\n svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground\")}\n >\n {configItem?.icon && !hideIcon ? (\n \n ) : (\n \n )}\n {configItem?.label}\n \n );\n })}\n \n );\n});\nChartLegendContent.displayName = \"ChartLegend\";\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {\n if (typeof payload !== \"object\" || payload === null) {\n return undefined;\n }\n\n const payloadPayload = \"payload\" in payload && typeof payload.payload === \"object\" && payload.payload !== null ? payload.payload : undefined;\n\n let configLabelKey: string = key;\n\n if (key in payload && typeof payload[key as keyof typeof payload] === \"string\") {\n configLabelKey = payload[key as keyof typeof payload] as string;\n } else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\") {\n configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;\n }\n\n return configLabelKey in config ? config[configLabelKey] : config[key];\n}\n\nexport { ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent };\n", "type": "registry:component", "target": "components/ui/chart.tsx" } diff --git a/public/r/checkbox.json b/public/r/checkbox.json index 52c2d54c..a0cf6223 100644 --- a/public/r/checkbox.json +++ b/public/r/checkbox.json @@ -34,7 +34,7 @@ }, { "path": "components/ui/checkbox.tsx", - "content": "\"use client\";\n\nimport { Check } from \"@phosphor-icons/react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Role: bordered adaptive glass. */\nconst ROLE = {\n border: true,\n};\n\nconst Checkbox = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n /* `glow` rides the CHECKED state on toggles — kept out of the materialSurface forward below. */\n const m = materialSurface(variant === \"default\" ? null : ROLE, {\n ...axes,\n glow: undefined,\n });\n\n return (\n \n \n \n \n \n );\n});\nCheckbox.displayName = CheckboxPrimitive.Root.displayName;\n\nexport { Checkbox };\n", + "content": "\"use client\";\n\nimport { Check } from \"@phosphor-icons/react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Role: bordered adaptive glass. */\nconst ROLE = {\n border: true,\n};\n\nconst Checkbox = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n /* `glow` rides the CHECKED state on toggles — kept out of the materialSurface forward below. */\n const m = materialSurface(variant === \"default\" ? null : ROLE, {\n ...axes,\n glow: undefined,\n });\n\n return (\n \n \n \n \n \n );\n});\nCheckbox.displayName = CheckboxPrimitive.Root.displayName;\n\nexport { Checkbox };\n", "type": "registry:component", "target": "components/ui/checkbox.tsx" } diff --git a/public/r/combobox.json b/public/r/combobox.json index 2887acfc..02f46aef 100644 --- a/public/r/combobox.json +++ b/public/r/combobox.json @@ -36,7 +36,7 @@ }, { "path": "components/ui/combobox.tsx", - "content": "\"use client\";\n\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react\";\nimport { CaretDownIcon, CheckIcon, XIcon } from \"@phosphor-icons/react\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\nimport { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from \"./input-group\";\n\n/**\n * An input with a filtered list of options, single or multiple (chips), on Base UI's Combobox: the\n * primitive shadcn's combobox now uses, because Radix has none. The field is Sistine's InputGroup; the\n * popup is bordered, veiled glass like every other menu; the chips field is a bordered small-tier glass\n * surface like Input.\n *\n * Base UI marks state with presence attributes (`data-open`, `data-highlighted`, `data-disabled`) and\n * omits them when off, so the bare `data-disabled:` variant is safe here, unlike cmdk (see command.tsx).\n */\n\n/* Roles: the popup is a read-through overlay; the chips field is an input-sized surface. */\nconst POPUP_ROLE = {\n border: true,\n veil: true,\n};\nconst CHIPS_ROLE = {\n border: true,\n size: \"sm\" as const,\n};\n\nconst Combobox = ComboboxPrimitive.Root;\n\nfunction ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {\n return ;\n}\n\nfunction ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) {\n return (\n \n {children}\n \n \n );\n}\n\nfunction ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {\n return (\n }\n className={cn(className)}\n {...props}\n >\n \n \n );\n}\n\nfunction ComboboxInput({\n className,\n children,\n disabled = false,\n showTrigger = true,\n showClear = false,\n ...props\n}: ComboboxPrimitive.Input.Props & {\n showTrigger?: boolean;\n showClear?: boolean;\n}) {\n return (\n \n } {...props} />\n \n {showTrigger && (\n \n \n \n )}\n {showClear && }\n \n {children}\n \n );\n}\n\nfunction ComboboxContent({\n className,\n side = \"bottom\",\n sideOffset = 6,\n align = \"start\",\n alignOffset = 0,\n anchor,\n variant = \"glass\",\n ...props\n}: ComboboxPrimitive.Popup.Props &\n Pick &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : POPUP_ROLE, axes);\n\n return (\n \n \n \n \n \n );\n}\n\nfunction ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {\n return (\n \n );\n}\n\nfunction ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) {\n return (\n \n {children}\n }>\n \n \n \n );\n}\n\nfunction ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {\n return ;\n}\n\nfunction ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {\n return (\n \n );\n}\n\nfunction ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {\n return ;\n}\n\nfunction ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {\n return (\n \n );\n}\n\nfunction ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) {\n return ;\n}\n\nfunction ComboboxChips({\n className,\n ...props\n}: React.ComponentPropsWithRef & ComboboxPrimitive.Chips.Props & MaterialAxisProps) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(CHIPS_ROLE, axes);\n\n return (\n \n );\n}\n\nfunction ComboboxChip({\n className,\n children,\n showRemove = true,\n ...props\n}: ComboboxPrimitive.Chip.Props & {\n showRemove?: boolean;\n}) {\n return (\n \n {children}\n {showRemove && (\n }\n className=\"-ml-1 opacity-50 hover:opacity-100\"\n data-slot=\"combobox-chip-remove\"\n >\n \n \n )}\n \n );\n}\n\nfunction ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) {\n return (\n \n );\n}\n\nfunction useComboboxAnchor() {\n return React.useRef(null);\n}\n\nexport {\n Combobox,\n ComboboxChip,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxClear,\n ComboboxCollection,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxGroup,\n ComboboxInput,\n ComboboxItem,\n ComboboxLabel,\n ComboboxList,\n ComboboxSeparator,\n ComboboxTrigger,\n ComboboxValue,\n useComboboxAnchor,\n};\n", + "content": "\"use client\";\n\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react\";\nimport { CaretDownIcon, CheckIcon, XIcon } from \"@phosphor-icons/react\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\nimport { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from \"./input-group\";\n\n/**\n * An input with a filtered list of options, single or multiple (chips), on Base UI's Combobox: the\n * primitive shadcn's combobox now uses, because Radix has none. The field is Sistine's InputGroup; the\n * popup is bordered, veiled glass like every other menu; the chips field is a bordered small-tier glass\n * surface like Input.\n *\n * Base UI marks state with presence attributes (`data-open`, `data-highlighted`, `data-disabled`) and\n * omits them when off, so the bare `data-disabled:` variant is safe here, unlike cmdk (see command.tsx).\n */\n\n/* Roles: the popup is a read-through overlay; the chips field is an input-sized surface. */\nconst POPUP_ROLE = {\n border: true,\n veil: true,\n};\nconst CHIPS_ROLE = {\n border: true,\n size: \"sm\" as const,\n};\n\nconst Combobox = ComboboxPrimitive.Root;\n\nfunction ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {\n return ;\n}\n\nfunction ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) {\n return (\n \n {children}\n \n \n );\n}\n\nfunction ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {\n return (\n }\n className={cn(className)}\n {...props}\n >\n \n \n );\n}\n\nfunction ComboboxInput({\n className,\n children,\n disabled = false,\n showTrigger = true,\n showClear = false,\n ...props\n}: ComboboxPrimitive.Input.Props & {\n showTrigger?: boolean;\n showClear?: boolean;\n}) {\n return (\n \n } {...props} />\n \n {showTrigger && (\n \n \n \n )}\n {showClear && }\n \n {children}\n \n );\n}\n\nfunction ComboboxContent({\n className,\n side = \"bottom\",\n sideOffset = 6,\n align = \"start\",\n alignOffset = 0,\n anchor,\n variant = \"glass\",\n ...props\n}: ComboboxPrimitive.Popup.Props &\n Pick &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : POPUP_ROLE, axes);\n\n return (\n \n \n \n \n \n );\n}\n\nfunction ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {\n return (\n \n );\n}\n\nfunction ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) {\n return (\n \n {children}\n }\n >\n \n \n \n );\n}\n\nfunction ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {\n return ;\n}\n\nfunction ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {\n return (\n \n );\n}\n\nfunction ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {\n return ;\n}\n\nfunction ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {\n return (\n \n );\n}\n\nfunction ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) {\n return ;\n}\n\nfunction ComboboxChips({\n className,\n ...props\n}: React.ComponentPropsWithRef & ComboboxPrimitive.Chips.Props & MaterialAxisProps) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(CHIPS_ROLE, axes);\n\n return (\n \n );\n}\n\nfunction ComboboxChip({\n className,\n children,\n showRemove = true,\n ...props\n}: ComboboxPrimitive.Chip.Props & {\n showRemove?: boolean;\n}) {\n return (\n \n {children}\n {showRemove && (\n }\n className=\"-ml-1 opacity-50 hover:opacity-100\"\n data-slot=\"combobox-chip-remove\"\n >\n \n \n )}\n \n );\n}\n\nfunction ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) {\n return (\n \n );\n}\n\nfunction useComboboxAnchor() {\n return React.useRef(null);\n}\n\nexport {\n Combobox,\n ComboboxChip,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxClear,\n ComboboxCollection,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxGroup,\n ComboboxInput,\n ComboboxItem,\n ComboboxLabel,\n ComboboxList,\n ComboboxSeparator,\n ComboboxTrigger,\n ComboboxValue,\n useComboboxAnchor,\n};\n", "type": "registry:component", "target": "components/ui/combobox.tsx" } diff --git a/public/r/command.json b/public/r/command.json index 5b258f5b..fca13ebc 100644 --- a/public/r/command.json +++ b/public/r/command.json @@ -36,7 +36,7 @@ }, { "path": "components/ui/command.tsx", - "content": "\"use client\";\n\nimport { MagnifyingGlassIcon } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport type { Dialog as DialogPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"./dialog\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst commandVariants = cva(\"flex h-full w-full flex-col overflow-hidden rounded-xl\", {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst Command = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n );\n});\nCommand.displayName = CommandPrimitive.displayName;\n\nconst CommandDialog = ({\n title = \"Command Palette\",\n description = \"Search for a command to run...\",\n children,\n className,\n showCloseButton = true,\n ...props\n}: DialogPrimitive.DialogProps & {\n title?: string;\n description?: string;\n className?: string;\n showCloseButton?: boolean;\n}) => {\n return (\n \n {/* Radix a11y: DialogContent requires a title + description. */}\n \n {title}\n {description}\n \n \n \n {children}\n \n \n \n );\n};\n\nconst CommandInput = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n
    \n \n \n
    \n ),\n);\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef, React.ComponentPropsWithoutRef>(\n (props, ref) => ,\n);\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\n/**\n * ⚠️ The disabled styles MUST be `data-[disabled=true]:`, never bare\n * `data-[disabled]:` — see `tests/command.test.tsx`.\n *\n * cmdk renders `data-disabled={!!disabled}` unconditionally, and React\n * stringifies booleans on `data-*` attributes, so an ENABLED item still carries\n * `data-disabled=\"false\"` in the DOM. Tailwind compiles the bare variant to the\n * attribute-PRESENCE selector `[data-disabled]`, which matches `\"false\"` just as\n * happily as `\"true\"` — so `pointer-events: none` landed on every item and no\n * entry in a picker could be clicked. Keyboard selection still worked, which is\n * what made it look like a mouse-only quirk rather than a stylesheet bug: cmdk\n * dispatches its own `cmdk-item-select` event straight at the node on Enter, and\n * that bypasses hit-testing entirely.\n *\n * The Radix menus next door keep the bare variant safely — Radix omits the\n * attribute outright when the item is enabled. cmdk is the one that always\n * emits it.\n */\nconst CommandItem = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => {\n return ;\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut };\n", + "content": "\"use client\";\n\nimport { MagnifyingGlassIcon } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport type { Dialog as DialogPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"./dialog\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst commandVariants = cva(\"flex h-full w-full flex-col overflow-hidden rounded-xl\", {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst Command = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n );\n});\nCommand.displayName = CommandPrimitive.displayName;\n\nconst CommandDialog = ({\n title = \"Command Palette\",\n description = \"Search for a command to run...\",\n children,\n className,\n showCloseButton = true,\n ...props\n}: DialogPrimitive.DialogProps & {\n title?: string;\n description?: string;\n className?: string;\n showCloseButton?: boolean;\n}) => {\n return (\n \n {/* Radix a11y: DialogContent requires a title + description. */}\n \n {title}\n {description}\n \n \n \n {children}\n \n \n \n );\n};\n\nconst CommandInput = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n
    \n \n \n
    \n ),\n);\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef, React.ComponentPropsWithoutRef>(\n (props, ref) => ,\n);\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\n/**\n * ⚠️ The disabled styles MUST be `data-[disabled=true]:`, never bare\n * `data-[disabled]:` — see `tests/command.test.tsx`.\n *\n * cmdk renders `data-disabled={!!disabled}` unconditionally, and React\n * stringifies booleans on `data-*` attributes, so an ENABLED item still carries\n * `data-disabled=\"false\"` in the DOM. Tailwind compiles the bare variant to the\n * attribute-PRESENCE selector `[data-disabled]`, which matches `\"false\"` just as\n * happily as `\"true\"` — so `pointer-events: none` landed on every item and no\n * entry in a picker could be clicked. Keyboard selection still worked, which is\n * what made it look like a mouse-only quirk rather than a stylesheet bug: cmdk\n * dispatches its own `cmdk-item-select` event straight at the node on Enter, and\n * that bypasses hit-testing entirely.\n *\n * The Radix menus next door keep the bare variant safely — Radix omits the\n * attribute outright when the item is enabled. cmdk is the one that always\n * emits it.\n */\nconst CommandItem = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => {\n return ;\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut };\n", "type": "registry:component", "target": "components/ui/command.tsx" } diff --git a/public/r/context-menu.json b/public/r/context-menu.json index 78a3532c..ae83cee3 100644 --- a/public/r/context-menu.json +++ b/public/r/context-menu.json @@ -40,7 +40,7 @@ }, { "path": "components/ui/context-menu.tsx", - "content": "\"use client\";\n\nimport { CaretRightIcon, Check, Circle } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { ContextMenu as ContextMenuPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { useComposedRefs, useSnappedPopper } from \"@/lib/snap-popper\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst contextMenuSubContentVariants = cva(\n \"z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-xl p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\nconst contextMenuContentVariants = cva(\n \"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst ContextMenu = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenu.displayName = ContextMenuPrimitive.Root.displayName;\n\nconst ContextMenuTrigger = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuTrigger.displayName = ContextMenuPrimitive.Trigger.displayName;\n\nconst ContextMenuGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuGroup.displayName = ContextMenuPrimitive.Group.displayName;\n\nconst ContextMenuPortal = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuPortal.displayName = ContextMenuPrimitive.Portal.displayName;\n\nconst ContextMenuSub = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuSub.displayName = ContextMenuPrimitive.Sub.displayName;\n\nconst ContextMenuRadioGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuRadioGroup.displayName = ContextMenuPrimitive.RadioGroup.displayName;\n\nconst ContextMenuSubTrigger = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, children, ...props }, ref) => (\n \n {children}\n \n \n));\nContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;\n\nconst ContextMenuSubContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n /* Portaled like ContextMenuContent. A glass menu's backdrop-filter makes it the containing block of\n every fixed-position descendant, so a sub menu rendered inside it is placed against the menu and\n clipped by its overflow instead of floating beside it. */\n return (\n \n \n \n );\n});\nContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;\n\nconst ContextMenuContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", style, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n );\n});\nContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;\n\nconst ContextMenuItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n }\n>(({ className, inset, variant = \"default\", ...props }, ref) => (\n \n));\nContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;\n\nconst ContextMenuCheckboxItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, checked, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;\n\nconst ContextMenuRadioItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;\n\nconst ContextMenuLabel = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, ...props }, ref) => (\n \n));\nContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;\n\nconst ContextMenuSeparator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;\n\nconst ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {\n return ;\n};\nContextMenuShortcut.displayName = \"ContextMenuShortcut\";\n\nexport {\n ContextMenu,\n ContextMenuCheckboxItem,\n ContextMenuContent,\n ContextMenuGroup,\n ContextMenuItem,\n ContextMenuLabel,\n ContextMenuPortal,\n ContextMenuRadioGroup,\n ContextMenuRadioItem,\n ContextMenuSeparator,\n ContextMenuShortcut,\n ContextMenuSub,\n ContextMenuSubContent,\n ContextMenuSubTrigger,\n ContextMenuTrigger,\n};\n", + "content": "\"use client\";\n\nimport { CaretRightIcon, Check, Circle } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { ContextMenu as ContextMenuPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { useComposedRefs, useSnappedPopper } from \"@/lib/snap-popper\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst contextMenuSubContentVariants = cva(\n \"z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-xl p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\nconst contextMenuContentVariants = cva(\n \"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n {\n variants: {\n variant: {\n default: \"bg-popover text-popover-foreground border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst ContextMenu = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenu.displayName = ContextMenuPrimitive.Root.displayName;\n\nconst ContextMenuTrigger = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuTrigger.displayName = ContextMenuPrimitive.Trigger.displayName;\n\nconst ContextMenuGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuGroup.displayName = ContextMenuPrimitive.Group.displayName;\n\nconst ContextMenuPortal = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuPortal.displayName = ContextMenuPrimitive.Portal.displayName;\n\nconst ContextMenuSub = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuSub.displayName = ContextMenuPrimitive.Sub.displayName;\n\nconst ContextMenuRadioGroup = ({ ...props }: React.ComponentPropsWithoutRef) => (\n \n);\nContextMenuRadioGroup.displayName = ContextMenuPrimitive.RadioGroup.displayName;\n\nconst ContextMenuSubTrigger = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, children, ...props }, ref) => (\n \n {children}\n \n \n));\nContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;\n\nconst ContextMenuSubContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n /* Portaled like ContextMenuContent. A glass menu's backdrop-filter makes it the containing block of\n every fixed-position descendant, so a sub menu rendered inside it is placed against the menu and\n clipped by its overflow instead of floating beside it. */\n return (\n \n \n \n );\n});\nContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;\n\nconst ContextMenuContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", style, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n );\n});\nContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;\n\nconst ContextMenuItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n }\n>(({ className, inset, variant = \"default\", ...props }, ref) => (\n \n));\nContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;\n\nconst ContextMenuCheckboxItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, checked, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;\n\nconst ContextMenuRadioItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, ...props }, ref) => (\n \n \n \n \n \n \n {children}\n \n));\nContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;\n\nconst ContextMenuLabel = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n inset?: boolean;\n }\n>(({ className, inset, ...props }, ref) => (\n \n));\nContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;\n\nconst ContextMenuSeparator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;\n\nconst ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {\n return ;\n};\nContextMenuShortcut.displayName = \"ContextMenuShortcut\";\n\nexport {\n ContextMenu,\n ContextMenuCheckboxItem,\n ContextMenuContent,\n ContextMenuGroup,\n ContextMenuItem,\n ContextMenuLabel,\n ContextMenuPortal,\n ContextMenuRadioGroup,\n ContextMenuRadioItem,\n ContextMenuSeparator,\n ContextMenuShortcut,\n ContextMenuSub,\n ContextMenuSubContent,\n ContextMenuSubTrigger,\n ContextMenuTrigger,\n};\n", "type": "registry:component", "target": "components/ui/context-menu.tsx" } diff --git a/public/r/dialog.json b/public/r/dialog.json index 41351dc8..1ad3ad4b 100644 --- a/public/r/dialog.json +++ b/public/r/dialog.json @@ -35,7 +35,7 @@ }, { "path": "components/ui/dialog.tsx", - "content": "\"use client\";\n\nimport { X } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst dialogContentVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"bg-background border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass at the elevated blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"lg\",\n};\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n VariantProps &\n MaterialAxisProps & {\n /** Elevated backdrop blur (folded from the glass wrapper). */\n animated?: boolean;\n /** Styles the layout wrapper around `children` (`data-slot=\"dialog-layout\"`). `className` targets\n * the OUTER surface, so without this a consumer has no way to change the body's layout — swap the\n * grid for flex, drop the gap, or take the scrolling over entirely.\n * Its `-m-6 p-6` is a matched pair that cancels the dialog's padding so the scroll container\n * stops clipping child shadows; override one and you must override the other. */\n bodyClassName?: string;\n showCloseButton?: boolean;\n }\n>(({ className, variant = \"glass\", children, animated = true, bodyClassName, showCloseButton = true, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n {/* The layout wrapper has two modes.\n WITHOUT a DialogBody child it is the scroll box, as it always was: header, content and footer\n scroll together.\n WITH one (`has-[>[data-slot=dialog-body]]:`) it becomes a non-clipping flex column and only\n the body scrolls: the header and footer are chrome, not content, and inside a scroll box a\n glass button's glow was sliced at whichever edge it touched (overflow-y:auto clips on all\n four sides, see below), and the footer scrolled away with the text.\n min-w-0 + [&>*]:min-w-0: grid and flex items default to min-width:auto, so one unbreakable\n child (a long path with `truncate`) set the track WIDER than the dialog and every line laid\n out to that phantom width, clipped at the border. Letting the items shrink fixes wrap/truncate.\n min-h-0 is load-bearing: a grid item defaults to min-height:auto, which refuses to shrink\n below its content, so without it this never scrolls and the dialog just clips again.\n -m-6 p-6 cancels the dialog's own p-6 so this box's edges land ON the dialog's padding box\n instead of its content box. That matters because `overflow-y: auto` is not one-axis: when\n either axis is non-visible the other computes visible -> auto, so this is a scroll\n container HORIZONTALLY too, and a scroll container clips at its padding box. Sitting at the\n content box it sheared every child's box-shadow flat 24px inside the dialog edge — a hard\n vertical cut down the side of the last footer button and a hard horizontal one under the\n footer row. Cancelling the padding moves that clip out to where the dialog's own\n overflow-hidden already clips, which does the same job and follows the rounded corners.\n The two utilities are a pair: overriding padding alone via bodyClassName re-offsets the\n body by the leftover negative margin. */}\n *]:min-w-0\",\n \"has-[>[data-slot=dialog-body]]:flex has-[>[data-slot=dialog-body]]:flex-col has-[>[data-slot=dialog-body]]:overflow-visible\",\n bodyClassName,\n )}\n >\n {children}\n \n {showCloseButton && (\n \n \n Close\n \n )}\n \n \n );\n});\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (\n \n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/**\n * The scrolling region of a dialog. Optional: put it between DialogHeader and DialogFooter and only it\n * scrolls, with the header and footer fixed around it; leave it out and the whole dialog content scrolls\n * as one, as before.\n * `-mx-6 px-6` runs the scroll box out to the dialog's edges (a scroll container clips at its padding\n * box, so sitting at the content box it sheared child shadows 24px inside the edge), and `-my-2 py-2`\n * gives a glow 8px of room above and below without moving the content.\n */\nconst DialogBody = ({ className, ...props }: React.HTMLAttributes) => (\n *]:min-w-0\", className)}\n {...props}\n />\n);\nDialogBody.displayName = \"DialogBody\";\n\nconst DialogFooter = ({\n className,\n showCloseButton = false,\n children,\n ...props\n}: React.HTMLAttributes & {\n showCloseButton?: boolean;\n}) => (\n \n {children}\n {showCloseButton && (\n \n \n \n )}\n \n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n Dialog,\n DialogBody,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n", + "content": "\"use client\";\n\nimport { X } from \"@phosphor-icons/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst dialogContentVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"bg-background border\",\n glass: \"text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass at the elevated blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"lg\",\n};\n\nconst Dialog = DialogPrimitive.Root;\n\n/* Trigger and Close stamp their data-slot (consumers style and test against it), as the content's own close\n button already did; Root and Portal render no element of their own, so they stay plain aliases. */\nconst DialogTrigger = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>((props, ref) => );\nDialogTrigger.displayName = DialogPrimitive.Trigger.displayName;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = React.forwardRef, React.ComponentPropsWithoutRef>(\n (props, ref) => ,\n);\nDialogClose.displayName = DialogPrimitive.Close.displayName;\n\nconst DialogOverlay = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n VariantProps &\n MaterialAxisProps & {\n /** Elevated backdrop blur (folded from the glass wrapper). */\n animated?: boolean;\n /** Styles the layout wrapper around `children` (`data-slot=\"dialog-layout\"`). `className` targets\n * the OUTER surface, so without this a consumer has no way to change the body's layout — swap the\n * grid for flex, drop the gap, or take the scrolling over entirely.\n * Its `-m-6 p-6` is a matched pair that cancels the dialog's padding so the scroll container\n * stops clipping child shadows; override one and you must override the other. */\n bodyClassName?: string;\n showCloseButton?: boolean;\n }\n>(({ className, variant = \"glass\", children, animated = true, bodyClassName, showCloseButton = true, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n {/* The layout wrapper has two modes.\n WITHOUT a DialogBody child it is the scroll box, as it always was: header, content and footer\n scroll together.\n WITH one (`has-[>[data-slot=dialog-body]]:`) it becomes a non-clipping flex column and only\n the body scrolls: the header and footer are chrome, not content, and inside a scroll box a\n glass button's glow was sliced at whichever edge it touched (overflow-y:auto clips on all\n four sides, see below), and the footer scrolled away with the text.\n min-w-0 + [&>*]:min-w-0: grid and flex items default to min-width:auto, so one unbreakable\n child (a long path with `truncate`) set the track WIDER than the dialog and every line laid\n out to that phantom width, clipped at the border. Letting the items shrink fixes wrap/truncate.\n min-h-0 is load-bearing: a grid item defaults to min-height:auto, which refuses to shrink\n below its content, so without it this never scrolls and the dialog just clips again.\n -m-6 p-6 cancels the dialog's own p-6 so this box's edges land ON the dialog's padding box\n instead of its content box. That matters because `overflow-y: auto` is not one-axis: when\n either axis is non-visible the other computes visible -> auto, so this is a scroll\n container HORIZONTALLY too, and a scroll container clips at its padding box. Sitting at the\n content box it sheared every child's box-shadow flat 24px inside the dialog edge — a hard\n vertical cut down the side of the last footer button and a hard horizontal one under the\n footer row. Cancelling the padding moves that clip out to where the dialog's own\n overflow-hidden already clips, which does the same job and follows the rounded corners.\n The two utilities are a pair: overriding padding alone via bodyClassName re-offsets the\n body by the leftover negative margin. */}\n *]:min-w-0\",\n \"has-[>[data-slot=dialog-body]]:flex has-[>[data-slot=dialog-body]]:flex-col has-[>[data-slot=dialog-body]]:overflow-visible\",\n bodyClassName,\n )}\n >\n {children}\n \n {showCloseButton && (\n \n \n Close\n \n )}\n \n \n );\n});\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (\n \n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/**\n * The scrolling region of a dialog. Optional: put it between DialogHeader and DialogFooter and only it\n * scrolls, with the header and footer fixed around it; leave it out and the whole dialog content scrolls\n * as one, as before.\n * `-mx-6 px-6` runs the scroll box out to the dialog's edges (a scroll container clips at its padding\n * box, so sitting at the content box it sheared child shadows 24px inside the edge), and `-my-2 py-2`\n * gives a glow 8px of room above and below without moving the content.\n */\nconst DialogBody = ({ className, ...props }: React.HTMLAttributes) => (\n *]:min-w-0\", className)}\n {...props}\n />\n);\nDialogBody.displayName = \"DialogBody\";\n\nconst DialogFooter = ({\n className,\n showCloseButton = false,\n children,\n ...props\n}: React.HTMLAttributes & {\n showCloseButton?: boolean;\n}) => (\n \n {children}\n {showCloseButton && (\n \n \n \n )}\n \n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n Dialog,\n DialogBody,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n", "type": "registry:component", "target": "components/ui/dialog.tsx" } diff --git a/public/r/drawer.json b/public/r/drawer.json index 4773f00c..59ae5461 100644 --- a/public/r/drawer.json +++ b/public/r/drawer.json @@ -33,7 +33,7 @@ }, { "path": "components/ui/drawer.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"vaul\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Role: borderless adaptive glass. */\nconst ROLE = {};\n\nconst Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps) => (\n \n);\nDrawer.displayName = \"Drawer\";\n\nconst DrawerTrigger = DrawerPrimitive.Trigger;\n\nconst DrawerPortal = DrawerPrimitive.Portal;\n\nconst DrawerClose = DrawerPrimitive.Close;\n\nconst DrawerOverlay = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;\n\nconst DrawerContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }\n>(({ className, children, variant = \"glass\", ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"bg-background\",\n glass: \"text-foreground\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n \n
    a scroll container, and a\n sticky header binds to its nearest one, so it rode along with the rows. The container clips the\n corners instead (see below). */\n glass: \"rounded-lg\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass. */\nconst ROLE = {\n border: true,\n};\n\nconst Table = React.forwardRef<\n HTMLTableElement,\n React.HTMLAttributes &\n VariantProps &\n MaterialAxisProps & {\n /** When true, rows alternate in brightness and the per-row dividers are removed. */\n striped?: boolean;\n /** Classes for the wrapping container, which is the element that scrolls: pass a height and an\n * overflow (`max-h-96 overflow-y-auto`) and a `sticky top-0` TableHeader pins to it. */\n containerClassName?: string;\n }\n>(({ className, containerClassName, variant = \"glass\", striped = false, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n const stripedClass = striped\n ? \"[&_tbody_tr]:border-0 [&_tbody_tr:nth-child(even)]:bg-black/[0.07] dark:[&_tbody_tr:nth-child(even)]:bg-black/[0.22] [&_tbody_tr:nth-child(even):hover]:bg-muted/50\"\n : \"\";\n\n return (\n /* `overflow-clip`, not `overflow-hidden`: it clips the header and row fills to the rounded corners\n without making the container a scroll container, so it never captures a sticky header on its own.\n A consumer's `overflow-y-auto` (containerClassName) makes it the scroller on purpose; the x axis\n then computes to hidden and keeps the corners clipped. */\n
    \n \n
    \n );\n});\nTable.displayName = \"Table\";\n\nconst TableHeader = React.forwardRef>(({ className, ...props }, ref) => (\n
    \n));\nTableHeader.displayName = \"TableHeader\";\n\nconst TableBody = React.forwardRef>(({ className, ...props }, ref) => (\n \n));\nTableBody.displayName = \"TableBody\";\n\nconst TableFooter = React.forwardRef>(({ className, ...props }, ref) => (\n tr]:last:border-b-0\", className)} {...props} />\n));\nTableFooter.displayName = \"TableFooter\";\n\nconst TableRow = React.forwardRef>(({ className, ...props }, ref) => (\n \n));\nTableRow.displayName = \"TableRow\";\n\nconst TableHead = React.forwardRef>(({ className, ...props }, ref) => (\n [role=checkbox]]:translate-y-[2px]\",\n className,\n )}\n {...props}\n />\n));\nTableHead.displayName = \"TableHead\";\n\nconst TableCell = React.forwardRef>(({ className, ...props }, ref) => (\n [role=checkbox]]:translate-y-[2px]\", className)}\n {...props}\n />\n));\nTableCell.displayName = \"TableCell\";\n\nconst TableCaption = React.forwardRef>(({ className, ...props }, ref) => (\n
    \n));\nTableCaption.displayName = \"TableCaption\";\n\nexport { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };\n", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst tableVariants = cva(\"w-full caption-bottom text-sm\", {\n variants: {\n variant: {\n default: \"\",\n /* Rounded, but NOT overflow-hidden: `overflow: hidden` makes the a scroll container, and a\n sticky header binds to its nearest one, so it rode along with the rows. The container clips the\n corners instead (see below). */\n glass: \"rounded-lg\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass. */\nconst ROLE = {\n border: true,\n};\n\nconst Table = React.forwardRef<\n HTMLTableElement,\n React.HTMLAttributes &\n VariantProps &\n MaterialAxisProps & {\n /** When true, rows alternate in brightness and the per-row dividers are removed. */\n striped?: boolean;\n /** Classes for the wrapping container, which is the element that scrolls: pass a height and an\n * overflow (`max-h-96 overflow-y-auto`) and a `sticky top-0` TableHeader pins to it. */\n containerClassName?: string;\n /** Let a table wider than its container scroll sideways instead of being clipped (shadcn's table\n * always does). Opt-in because it makes the container a scroll container: a `sticky top-0` header\n * then pins to the container rather than the page, so pair it with a max height when the header\n * should stay visible as the rows scroll. */\n scrollX?: boolean;\n }\n>(({ className, containerClassName, variant = \"glass\", striped = false, scrollX = false, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n const stripedClass = striped\n ? \"[&_tbody_tr]:border-0 [&_tbody_tr:nth-child(even)]:bg-black/[0.07] dark:[&_tbody_tr:nth-child(even)]:bg-black/[0.22] [&_tbody_tr:nth-child(even):hover]:bg-muted/50\"\n : \"\";\n\n return (\n /* `overflow-clip`, not `overflow-hidden`: it clips the header and row fills to the rounded corners\n without making the container a scroll container, so it never captures a sticky header on its own.\n A consumer's `overflow-y-auto` (containerClassName) makes it the scroller on purpose; the x axis\n then computes to hidden and keeps the corners clipped. */\n
    \n \n
    \n );\n});\nTable.displayName = \"Table\";\n\nconst TableHeader = React.forwardRef>(({ className, ...props }, ref) => (\n
    \n));\nTableHeader.displayName = \"TableHeader\";\n\nconst TableBody = React.forwardRef>(({ className, ...props }, ref) => (\n \n));\nTableBody.displayName = \"TableBody\";\n\nconst TableFooter = React.forwardRef>(({ className, ...props }, ref) => (\n tr]:last:border-b-0\", className)} {...props} />\n));\nTableFooter.displayName = \"TableFooter\";\n\nconst TableRow = React.forwardRef>(({ className, ...props }, ref) => (\n \n));\nTableRow.displayName = \"TableRow\";\n\nconst TableHead = React.forwardRef>(({ className, ...props }, ref) => (\n [role=checkbox]]:translate-y-[2px]\",\n className,\n )}\n {...props}\n />\n));\nTableHead.displayName = \"TableHead\";\n\nconst TableCell = React.forwardRef>(({ className, ...props }, ref) => (\n [role=checkbox]]:translate-y-[2px]\", className)}\n {...props}\n />\n));\nTableCell.displayName = \"TableCell\";\n\nconst TableCaption = React.forwardRef>(({ className, ...props }, ref) => (\n
    \n));\nTableCaption.displayName = \"TableCaption\";\n\nexport { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };\n", "type": "registry:component", "target": "components/ui/table.tsx" } diff --git a/public/r/tabs.json b/public/r/tabs.json index 9e64d6bd..6e46caab 100644 --- a/public/r/tabs.json +++ b/public/r/tabs.json @@ -33,7 +33,7 @@ }, { "path": "components/ui/tabs.tsx", - "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type HoverEffect, hoverEffects } from \"@/lib/hover-effects\";\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst tabsListVariants = cva(\"inline-flex h-9 items-center justify-center rounded-lg p-1 text-muted-foreground\", {\n variants: {\n variant: {\n default: \"bg-muted\",\n glass: \"\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass. */\nconst ROLE = {\n border: true,\n};\n\nconst Tabs = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, orientation = \"horizontal\", ...props }, ref) => (\n \n ),\n);\nTabs.displayName = TabsPrimitive.Root.displayName;\n\nconst TabsList = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n VariantProps &\n MaterialAxisProps & {\n effect?: HoverEffect;\n }\n>(({ className, variant = \"glass\", effect, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n );\n});\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsContent, TabsList, TabsTrigger };\n", + "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type HoverEffect, hoverEffects } from \"@/lib/hover-effects\";\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. The list follows the Tabs\n root's orientation (a vertical column at fit height, rather than the fixed 36px row every orientation\n used to get), and `line` is shadcn's underline style: no surface, the active trigger marked by a bar. */\nconst tabsListVariants = cva(\n \"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-1 text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none\",\n {\n variants: {\n variant: {\n default: \"bg-muted\",\n glass: \"\",\n line: \"gap-1 bg-transparent\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n },\n);\n\n/* Role: bordered adaptive glass. */\nconst ROLE = {\n border: true,\n};\n\nconst Tabs = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, orientation = \"horizontal\", ...props }, ref) => (\n \n ),\n);\nTabs.displayName = TabsPrimitive.Root.displayName;\n\nconst TabsList = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n VariantProps &\n MaterialAxisProps & {\n effect?: HoverEffect;\n }\n>(({ className, variant = \"glass\", effect, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"glass\" ? ROLE : null, axes);\n\n return (\n \n );\n});\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef, React.ComponentPropsWithoutRef>(\n ({ className, ...props }, ref) => (\n \n ),\n);\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsContent, TabsList, TabsTrigger, tabsListVariants };\n", "type": "registry:component", "target": "components/ui/tabs.tsx" } diff --git a/public/r/textarea.json b/public/r/textarea.json index 00d02205..1607a191 100644 --- a/public/r/textarea.json +++ b/public/r/textarea.json @@ -32,7 +32,7 @@ }, { "path": "components/ui/textarea.tsx", - "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst textareaVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"dark:bg-input/30 border-input bg-transparent shadow-xs\",\n glass: \"\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass at the small blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"sm\",\n};\n\nfunction Textarea({\n className,\n variant = \"glass\",\n icon,\n error,\n ...props\n}: React.ComponentProps<\"textarea\"> &\n VariantProps &\n MaterialAxisProps & {\n /** Leading icon (folded from the glass wrapper). */\n icon?: React.ReactNode;\n /** Destructive border + focus ring (folded from the glass wrapper). */\n error?: boolean;\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n const textarea = (\n \n );\n\n if (icon) {\n return (\n
    \n
    {icon}
    \n {textarea}\n
    \n );\n }\n\n return textarea;\n}\n\nexport { Textarea };\n", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\nconst textareaVariants = cva(\"\", {\n variants: {\n variant: {\n default: \"dark:bg-input/30 border-input bg-transparent shadow-xs\",\n glass: \"\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n },\n});\n\n/* Role: bordered adaptive glass at the small blur tier. */\nconst ROLE: MaterialProps = {\n border: true,\n size: \"sm\",\n};\n\nfunction Textarea({\n className,\n variant = \"glass\",\n icon,\n error,\n ...props\n}: React.ComponentProps<\"textarea\"> &\n VariantProps &\n MaterialAxisProps & {\n /** Leading icon (folded from the glass wrapper). */\n icon?: React.ReactNode;\n /** Destructive border + focus ring (folded from the glass wrapper). */\n error?: boolean;\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n const textarea = (\n \n );\n\n if (icon) {\n return (\n
    \n
    {icon}
    \n {textarea}\n
    \n );\n }\n\n return textarea;\n}\n\nexport { Textarea };\n", "type": "registry:component", "target": "components/ui/textarea.tsx" } diff --git a/public/r/toggle-group.json b/public/r/toggle-group.json index e1adcdad..b9347994 100644 --- a/public/r/toggle-group.json +++ b/public/r/toggle-group.json @@ -33,7 +33,7 @@ }, { "path": "components/ui/toggle-group.tsx", - "content": "\"use client\";\n\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\ntype ToggleGroupVariant = \"default\" | \"glass\";\n\nconst ToggleGroupContext = React.createContext<{\n variant?: ToggleGroupVariant;\n size?: \"default\" | \"sm\" | \"lg\";\n spacing?: number;\n}>({\n variant: \"glass\",\n size: \"default\",\n spacing: 0,\n});\n\n/* Role: bordered adaptive glass. */\nconst ROLE = {\n border: true,\n};\n\nconst ToggleGroup = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n MaterialAxisProps & {\n variant?: ToggleGroupVariant;\n size?: \"default\" | \"sm\" | \"lg\";\n spacing?: number;\n }\n>(({ className, variant = \"glass\", size = \"default\", spacing = 0, children, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"\",\n glass: \"rounded-md p-1\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n {children}\n \n \n );\n});\n\nToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;\n\nconst ToggleGroupItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n variant?: ToggleGroupVariant;\n size?: \"default\" | \"sm\" | \"lg\";\n }\n>(({ className, variant, size, ...props }, ref) => {\n const context = React.useContext(ToggleGroupContext);\n\n return (\n \n );\n});\n\nToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;\n\nexport { ToggleGroup, ToggleGroupItem };\n", + "content": "\"use client\";\n\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\ntype ToggleGroupVariant = \"default\" | \"glass\" | \"outline\";\ntype ToggleGroupSize = \"default\" | \"sm\" | \"lg\";\n\n/* What the GROUP set, undefined where it set nothing: an item's own variant / size applies unless the group\n sets one, as in shadcn. The root used to default both, so a per-item override could never take effect. */\nconst ToggleGroupContext = React.createContext<{\n variant?: ToggleGroupVariant;\n size?: ToggleGroupSize;\n spacing?: number;\n}>({\n spacing: 0,\n});\n\n/* Item sizes, on the same steps as the standalone Toggle; `size` used to set data-size and nothing else. */\nconst ITEM_SIZE: Record = {\n sm: \"px-2 py-0.5 text-xs\",\n default: \"px-3 py-1\",\n lg: \"px-4 py-2\",\n};\n\n/* Role: bordered adaptive glass. */\nconst ROLE = {\n border: true,\n};\n\nconst ToggleGroup = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n MaterialAxisProps & {\n variant?: ToggleGroupVariant;\n size?: ToggleGroupSize;\n /** Gap between items, in Tailwind spacing steps (0 = touching). */\n spacing?: number;\n }\n>(({ className, variant, size, spacing = 0, children, ...props }, ref) => {\n const [axes, { style, ...rest }] = splitAxisProps(props);\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"\",\n glass: \"rounded-md p-1\",\n outline: \"\",\n };\n const resolved = variant ?? \"glass\";\n\n const m = materialSurface(resolved === \"glass\" ? ROLE : null, axes);\n\n return (\n \n \n {children}\n \n \n );\n});\n\nToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;\n\nconst ToggleGroupItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n variant?: ToggleGroupVariant;\n size?: ToggleGroupSize;\n }\n>(({ className, variant, size, ...props }, ref) => {\n const context = React.useContext(ToggleGroupContext);\n const itemVariant = context.variant || variant || \"glass\";\n const itemSize = context.size || size || \"default\";\n\n return (\n \n );\n});\n\nToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;\n\nexport { ToggleGroup, ToggleGroupItem };\n", "type": "registry:component", "target": "components/ui/toggle-group.tsx" } diff --git a/public/r/toggle.json b/public/r/toggle.json index 7e1e15fe..7a5e09bc 100644 --- a/public/r/toggle.json +++ b/public/r/toggle.json @@ -33,7 +33,7 @@ }, { "path": "components/ui/toggle.tsx", - "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Toggle as TogglePrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface.\n The ON state uses the selected-control pair (--active-bg + --active-shadow), the same tokens Tabs and\n ToggleGroupItem use — Toggle is the same role and was the one control still left out. It previously\n reused --accent, the HOVER wash: a token borrowed for a state it does not name. That reads wrong for\n two reasons even now that --accent is tinted. It cannot distinguish hover from selected, since both\n would paint the identical fill; and --accent sits at L 96, where the sRGB ceiling is 0.0182 and the\n most hue any fill can carry is an RGB spread of 13, against 22 for --active-bg at L 93. A selected\n state needs to be seen from across the room; a hover only has to be seen under the cursor. */\nconst toggleVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-[color,background-color,box-shadow] hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 data-[state=on]:bg-[var(--active-bg)] data-[state=on]:text-foreground data-[state=on]:shadow-[var(--active-shadow)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n outline: \"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground\",\n /* glass-hover-accent, not just the base `hover:bg-accent`: this variant paints a material, and a\n background-COLOR sits under every one of its image layers. See utilities.css. */\n glass: \"glass-hover-accent hover:opacity-90\",\n },\n size: {\n default: \"h-10 px-3\",\n sm: \"h-9 px-2.5\",\n lg: \"h-11 px-5\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n size: \"default\",\n },\n },\n);\n\n/* Each semantic variant's surface role (default/outline render no glass surface). */\nconst SURFACE_ROLE: Record = {\n glass: {\n border: true,\n },\n default: null,\n outline: null,\n};\n\nconst Toggle = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", size, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n /* `glow` rides the ON state on toggles — kept out of the materialSurface forward below. */\n const m = materialSurface(SURFACE_ROLE[variant ?? \"glass\"] ?? null, {\n ...axes,\n glow: undefined,\n });\n\n return (\n \n );\n});\n\nToggle.displayName = TogglePrimitive.Root.displayName;\n\nexport { Toggle, toggleVariants };\n", + "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Toggle as TogglePrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, type MaterialProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\n\n/* Variant classes carry BEHAVIOR only; the surface comes from materialSurface.\n The ON state uses the selected-control pair (--active-bg + --active-shadow), the same tokens Tabs and\n ToggleGroupItem use — Toggle is the same role and was the one control still left out. It previously\n reused --accent, the HOVER wash: a token borrowed for a state it does not name. That reads wrong for\n two reasons even now that --accent is tinted. It cannot distinguish hover from selected, since both\n would paint the identical fill; and --accent sits at L 96, where the sRGB ceiling is 0.0182 and the\n most hue any fill can carry is an RGB spread of 13, against 22 for --active-bg at L 93. A selected\n state needs to be seen from across the room; a hover only has to be seen under the cursor. */\nconst toggleVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-[color,background-color,box-shadow] hover:bg-accent hover:text-accent-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 data-[state=on]:bg-[var(--active-bg)] data-[state=on]:text-foreground data-[state=on]:shadow-[var(--active-shadow)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n outline: \"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground\",\n /* glass-hover-accent, not just the base `hover:bg-accent`: this variant paints a material, and a\n background-COLOR sits under every one of its image layers. See utilities.css. */\n glass: \"glass-hover-accent hover:opacity-90\",\n },\n size: {\n default: \"h-10 px-3\",\n sm: \"h-9 px-2.5\",\n lg: \"h-11 px-5\",\n },\n },\n defaultVariants: {\n variant: \"glass\",\n size: \"default\",\n },\n },\n);\n\n/* Each semantic variant's surface role (default/outline render no glass surface). */\nconst SURFACE_ROLE: Record = {\n glass: {\n border: true,\n },\n default: null,\n outline: null,\n};\n\nconst Toggle = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & VariantProps & MaterialAxisProps\n>(({ className, variant = \"glass\", size, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n /* `glow` rides the ON state on toggles — kept out of the materialSurface forward below. */\n const m = materialSurface(SURFACE_ROLE[variant ?? \"glass\"] ?? null, {\n ...axes,\n glow: undefined,\n });\n\n return (\n \n );\n});\n\nToggle.displayName = TogglePrimitive.Root.displayName;\n\nexport { Toggle, toggleVariants };\n", "type": "registry:component", "target": "components/ui/toggle.tsx" } diff --git a/public/r/tooltip.json b/public/r/tooltip.json index 5630d278..187210e7 100644 --- a/public/r/tooltip.json +++ b/public/r/tooltip.json @@ -39,7 +39,7 @@ }, { "path": "components/ui/tooltip.tsx", - "content": "\"use client\";\n\nimport { Tooltip as TooltipPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { useComposedRefs, useSnappedPopper } from \"@/lib/snap-popper\";\nimport { cn } from \"@/lib/utils\";\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst TooltipProvider = ({ delayDuration = 0, ...props }: React.ComponentProps) => (\n \n);\n\nconst Tooltip = ({ ...props }: React.ComponentProps) => ;\n\nconst TooltipTrigger = ({ ...props }: React.ComponentProps) => (\n \n);\n\nconst TooltipContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }\n>(({ className, variant = \"glass\", sideOffset = 4, children, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"bg-primary text-primary-foreground\",\n glass: \"text-foreground\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n {children}\n \n \n \n );\n});\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };\n", + "content": "\"use client\";\n\nimport { Tooltip as TooltipPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { useComposedRefs, useSnappedPopper } from \"@/lib/snap-popper\";\nimport { cn } from \"@/lib/utils\";\n\n/* Role: bordered + veiled adaptive glass. */\nconst ROLE = {\n border: true,\n veil: true,\n};\n\nconst TooltipProvider = ({ delayDuration = 0, ...props }: React.ComponentProps) => (\n \n);\n\nconst Tooltip = ({ ...props }: React.ComponentProps) => ;\n\nconst TooltipTrigger = ({ ...props }: React.ComponentProps) => (\n \n);\n\nconst TooltipContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }\n>(({ className, variant = \"glass\", sideOffset = 4, children, ...props }, ref) => {\n const [axes, rest] = splitAxisProps(props);\n const snapRef = useSnappedPopper();\n const composedRef = useComposedRefs(ref, snapRef);\n /* Variant classes carry BEHAVIOR only; the surface comes from materialSurface. */\n const variants = {\n default: \"bg-primary text-primary-foreground\",\n glass: \"text-foreground\",\n };\n\n const m = materialSurface(variant === \"default\" ? null : ROLE, axes);\n\n return (\n \n \n {children}\n \n \n \n );\n});\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };\n", "type": "registry:component", "target": "components/ui/tooltip.tsx" } diff --git a/registry.json b/registry.json index 33660b8d..d6ddb832 100644 --- a/registry.json +++ b/registry.json @@ -1393,6 +1393,7 @@ "@phosphor-icons/react", "class-variance-authority", "clsx", + "next-themes", "sonner", "tailwind-merge" ], diff --git a/tests/setup.ts b/tests/setup.ts index 0a51939c..bc1cacb0 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -15,3 +15,10 @@ if (!("ResizeObserver" in globalThis)) { disconnect() {} } as unknown as typeof ResizeObserver; } + +/* jsdom has no layout, so no document.elementFromPoint either; input-otp calls it on a timer to detect a + password manager's overlay badge, and the resulting TypeError surfaced as an unhandled error. Nothing is + at any point in a layout-less document. */ +if (typeof document.elementFromPoint !== "function") { + document.elementFromPoint = () => null; +} diff --git a/tests/upstream-parity.test.tsx b/tests/upstream-parity.test.tsx new file mode 100644 index 00000000..1a7908fb --- /dev/null +++ b/tests/upstream-parity.test.tsx @@ -0,0 +1,321 @@ +/** + * Fixes from the review against upstream shadcn (new-york-v4): places where a component lacked what shadcn's + * has, or did something broken that shadcn's does not. Each test pins one behaviour, not a class string, + * wherever behaviour is observable in jsdom. + */ + +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { ThemeProvider } from "next-themes"; +import { toast } from "sonner"; +import { afterEach, beforeAll, describe, expect, it } from "vitest"; +import { Alert, AlertTitle } from "@/components/ui/alert"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogTitle } from "@/components/ui/alert-dialog"; +import { Carousel } from "@/components/ui/carousel"; +import { Dialog, DialogClose, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { Select, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Sheet, SheetContent, SheetFooter, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; +import { Toaster } from "@/components/ui/sonner"; +import { Spinner } from "@/components/ui/spinner"; +import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; +import { Tabs, TabsList, TabsTrigger, tabsListVariants } from "@/components/ui/tabs"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; + +/* sonner and the carousel read matchMedia, which jsdom does not implement. The OS here prefers LIGHT. */ +beforeAll(() => { + if (!window.matchMedia) { + window.matchMedia = ((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + })) as unknown as typeof window.matchMedia; + } +}); + +describe("resizable", () => { + it("draws the horizontal-bar handle only for a VERTICAL group's separator", () => { + // react-resizable-panels v4 reports the LINE's orientation: side-by-side panels get a vertical separator. + const { container } = render( + + a + + b + , + ); + const handle = container.querySelector('[data-slot="resizable-handle"]') as HTMLElement; + expect(handle.getAttribute("aria-orientation")).toBe("vertical"); + expect(handle.className).toContain("aria-[orientation=horizontal]:h-px"); + expect(handle.className).not.toContain("aria-[orientation=vertical]:h-px"); + }); +}); + +describe("input-otp", () => { + it("supports shadcn's composition: slots addressed by index show what is typed", async () => { + render( + + + + + + + + , + ); + await userEvent.type(screen.getByRole("textbox"), "42"); + const slots = document.querySelectorAll('[data-slot="input-otp-slot"]'); + expect(slots).toHaveLength(4); + expect(slots[0].textContent).toBe("4"); + expect(slots[1].textContent).toBe("2"); + }); + + it("merges containerClassName into the surface instead of replacing it", () => { + const { container } = render(); + const box = container.querySelector(".custom-container") as HTMLElement; + expect(box).not.toBeNull(); + expect(box.className).toContain("flex"); + expect(box.className).toContain("items-center"); + }); +}); + +describe("alert-dialog", () => { + it("renders Action and Cancel as real Buttons, so they carry the material surface and data attributes", () => { + render( + + + Delete? + Cancel + Delete + + , + ); + const action = screen.getByText("Delete").closest("button") as HTMLElement; + const cancel = screen.getByText("Cancel").closest("button") as HTMLElement; + expect(action.dataset.slot).toBe("alert-dialog-action"); + expect(action.dataset.variant).toBe("destructive"); + expect(action.dataset.glassTint).toBe("destructive"); + expect(cancel.dataset.slot).toBe("alert-dialog-cancel"); + expect(cancel.dataset.variant).toBe("outline"); + expect(cancel.className).not.toContain("mt-2"); + }); +}); + +describe("dialog and sheet", () => { + it("stamp data-slot on the trigger and on a standalone close", () => { + render( + + Open + + Title + Done + + , + ); + expect(screen.getByText("Open").dataset.slot).toBe("dialog-trigger"); + expect(screen.getByText("Done").dataset.slot).toBe("dialog-close"); + }); + + it("lay the sheet out as a column with the footer pinned to the bottom", () => { + render( + + Open + + Title + Footer + + , + ); + expect(screen.getByText("Open").dataset.slot).toBe("sheet-trigger"); + const content = document.querySelector('[data-slot="sheet-content"]') as HTMLElement; + expect(content.className).toMatch(/\bflex\b/); + expect(content.className).toMatch(/\bflex-col\b/); + expect(screen.getByText("Footer").className).toMatch(/\bmt-auto\b/); + }); +}); + +describe("carousel", () => { + const slides = () => ( + +
    One
    +
    Two
    +
    Three
    +
    + ); + + it("is a labelled region whose controls are named", () => { + render(slides()); + const region = screen.getByRole("region"); + expect(region.getAttribute("aria-roledescription")).toBe("carousel"); + expect( + screen.getByRole("button", { + name: "Previous slide", + }), + ).toBeTruthy(); + expect( + screen.getByRole("button", { + name: "Next slide", + }), + ).toBeTruthy(); + expect(region.querySelectorAll('[aria-roledescription="slide"]')).toHaveLength(3); + }); + + it("moves with the arrow keys and keeps off-screen slides out of the tab order", () => { + render(slides()); + const region = screen.getByRole("region"); + const current = () => region.querySelector('[aria-current="true"]')?.getAttribute("aria-label"); + expect(current()).toBe("Go to slide 1"); + fireEvent.keyDown(region, { + key: "ArrowRight", + }); + expect(current()).toBe("Go to slide 2"); + fireEvent.keyDown(region, { + key: "ArrowLeft", + }); + expect(current()).toBe("Go to slide 1"); + const items = region.querySelectorAll('[data-slot="carousel-item"]'); + expect(items[0].hasAttribute("inert")).toBe(false); + expect(items[1].hasAttribute("inert")).toBe(true); + }); + + it("offers a pause control when it autoplays", () => { + render( + +
    One
    +
    Two
    +
    , + ); + const pause = screen.getByRole("button", { + name: "Pause slideshow", + }); + fireEvent.click(pause); + expect( + screen.getByRole("button", { + name: "Play slideshow", + }), + ).toBeTruthy(); + }); +}); + +describe("sonner", () => { + afterEach(() => { + act(() => { + toast.dismiss(); + }); + }); + + it("follows the app's theme, not the OS", async () => { + // The OS (matchMedia) prefers light; the app is forced dark. + render( + + + , + ); + act(() => { + toast("Saved"); + }); + await waitFor(() => expect(document.querySelector("[data-sonner-toaster]")?.getAttribute("data-sonner-theme")).toBe("dark")); + }); +}); + +describe("tabs", () => { + it("lays a vertical list out as a column and exports its variants", () => { + render( + + + A + B + + , + ); + const list = document.querySelector('[data-slot="tabs-list"]') as HTMLElement; + expect(list.dataset.variant).toBe("line"); + expect(list.className).toContain("group-data-[orientation=vertical]/tabs:flex-col"); + expect(screen.getByText("A").className).toContain("group-data-[orientation=vertical]/tabs:w-full"); + expect(typeof tabsListVariants).toBe("function"); + }); +}); + +describe("toggle-group", () => { + it("turns spacing into a gap, size into item size, and lets an item override what the group leaves unset", () => { + render( + + + A + + , + ); + const root = document.querySelector('[data-slot="toggle-group"]') as HTMLElement; + expect(root.style.getPropertyValue("--gap")).toBe("2"); + const item = screen.getByText("A"); + expect(item.dataset.size).toBe("sm"); + expect(item.dataset.variant).toBe("outline"); + expect(item.className).toContain("text-xs"); + }); +}); + +describe("select", () => { + it("shows the error state and stamps the value's slot", () => { + render( + , + ); + const trigger = document.querySelector('[data-slot="select-trigger"]') as HTMLElement; + expect(trigger.className).toContain("aria-invalid:border-destructive"); + expect(document.querySelector('[data-slot="select-value"]')).not.toBeNull(); + }); +}); + +describe("smaller fixes", () => { + it("lets a wide table scroll sideways when asked, keeping the clipped corners", () => { + const { container } = render( + + + + x + + +
    , + ); + const box = container.querySelector('[data-slot="table-container"]') as HTMLElement; + expect(box.className).toMatch(/\boverflow-x-auto\b/); + expect(box.className).toMatch(/\boverflow-clip\b/); + }); + + it("announces Input's error prop as aria-invalid", () => { + render(); + expect(screen.getByPlaceholderText("email").getAttribute("aria-invalid")).toBe("true"); + }); + + it("renders AlertTitle as a div, not a fixed-level heading", () => { + render( + + Heads up + , + ); + expect(screen.getByText("Heads up").tagName).toBe("DIV"); + expect(screen.queryByRole("heading")).toBeNull(); + }); + + it("sizes the spinner like an icon inside a control", () => { + render(); + expect(screen.getByRole("status").className).toContain("in-data-[slot=button]:size-4"); + }); + + it("uses outline-hidden, never outline-none, so focus survives forced-colors mode", () => { + const dir = join(process.cwd(), "components/ui"); + const offenders = readdirSync(dir).filter((f) => /\boutline-none\b/.test(readFileSync(join(dir, f), "utf8"))); + expect(offenders).toEqual([]); + }); +}); From 372adf2bca86080dca3fa4b37201c84e868fdfd3 Mon Sep 17 00:00:00 2001 From: WeekendsuperHero <4048475+WeekendSuperhero@users.noreply.github.com> Date: Tue, 20 Oct 2026 04:56:33 -0700 Subject: [PATCH 6/7] updated packages --- bun.lock | 44 ++++++++++++++++++++++---------------------- package.json | 14 +++++++------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/bun.lock b/bun.lock index ff314efa..98603113 100644 --- a/bun.lock +++ b/bun.lock @@ -32,12 +32,12 @@ }, "devDependencies": { "@biomejs/biome": "^2.5.15", - "@chromatic-com/storybook": "^5.5.1", - "@storybook/addon-a11y": "^10.6.2", - "@storybook/addon-docs": "^10.6.2", - "@storybook/addon-onboarding": "^10.6.2", - "@storybook/addon-vitest": "^10.6.2", - "@storybook/nextjs": "^10.6.2", + "@chromatic-com/storybook": "^5.6.0", + "@storybook/addon-a11y": "^10.6.3", + "@storybook/addon-docs": "^10.6.3", + "@storybook/addon-onboarding": "^10.6.3", + "@storybook/addon-vitest": "^10.6.3", + "@storybook/nextjs": "^10.6.3", "@tailwindcss/postcss": "^4.3.3", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", @@ -48,7 +48,7 @@ "@vitejs/plugin-react": "^6.1.2", "apca-w3": "^0.1.9", "jsdom": "^30.1.3", - "storybook": "^10.6.2", + "storybook": "^10.6.3", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", "vitest": "^5.0.5", @@ -298,7 +298,7 @@ "@bramus/specificity": ["@bramus/specificity@2.4.3", "", { "dependencies": { "css-tree": "^3.2.1" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw=="], - "@chromatic-com/storybook": ["@chromatic-com/storybook@5.5.1", "", { "dependencies": { "@neoconfetti/react": "^1.0.0", "chromatic": "^18.0.1", "jsonfile": "^6.1.0", "strip-ansi": "^7.1.0" }, "peerDependencies": { "storybook": "^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0 || ^11.0.0-0" } }, "sha512-DOtUEBIJHmqsOWg+Hi1t5m5FTNlyzL0NpZUwQob+ex30nDvL4p+MrYctmvEqH/QT72mf9mLXbz3Uj8aoWD2OYA=="], + "@chromatic-com/storybook": ["@chromatic-com/storybook@5.6.0", "", { "dependencies": { "@neoconfetti/react": "^1.0.0", "chromatic": "^18.12.1", "jsonfile": "^6.1.0", "strip-ansi": "^7.1.0" }, "peerDependencies": { "storybook": "^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0 || ^11.0.0-0" } }, "sha512-Z7YMtr9DVwegQeWcBeQaiK5dLcAvgpX53Qr/eGGYTyAEqTb1meUr5VQNq1wmst0Lj+gT6lQEco0PuFfUuf3jzA=="], "@csstools/color-helpers": ["@csstools/color-helpers@6.1.2", "", {}, "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow=="], @@ -734,31 +734,31 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@storybook/addon-a11y": ["@storybook/addon-a11y@10.6.2", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.6.2" } }, "sha512-zleS+btwrTRcUf3opbZntKsEt6PTqAtxsek08tx5jh5fRvd6wWmtnwW3uaBcypfIKnheTB3KGljJwFX+Mc+Umg=="], + "@storybook/addon-a11y": ["@storybook/addon-a11y@10.6.3", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.6.3" } }, "sha512-nqvjpmu5g+PPtEKwgE0qAi4wfuQ9yngOb/1Gm24pBBZOuRsd3UMEBjKORSuAGrIAz9X3LCFfQi7sCcBFXvFbwg=="], - "@storybook/addon-docs": ["@storybook/addon-docs@10.6.2", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.6.2", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0", "unplugin": "^2.3.5" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.2" }, "optionalPeers": ["@types/react"] }, "sha512-f7Rv/rzX1sk4lUDTeoIRkXWcWvNwLv7vBnfpFGb3f88MliB+M4ZekGKNiKtrmi44uLFt52gzwwPzj2eGDu0DQQ=="], + "@storybook/addon-docs": ["@storybook/addon-docs@10.6.3", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.6.3", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0", "unplugin": "^2.3.5" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.3" }, "optionalPeers": ["@types/react"] }, "sha512-365f712au4w/xvFqEKY6TIvTeGGAY7FKRB8zP4CFa3VSv9oyxHaq/ublFg4YDbCeRagppwl1PZ6k4+PoUor3+A=="], - "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.6.2", "", { "peerDependencies": { "storybook": "^10.6.2" } }, "sha512-3X+PD7MB/wHX8TkTfkkmKtYeVsrxwPZY3T0GkedJ5N8RQfdr5A3QrhTBiAnk1L1K1WdpAjdFxxmYivtvq7s8ig=="], + "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.6.3", "", { "peerDependencies": { "storybook": "^10.6.3" } }, "sha512-C/7AspvhzErXFOR4VHGBW/+G3gej9Lwr8dqH1oIaf0oi2qlhQB9ZBDe6Ds/u+EnW0C74aPlMwXFjSXk02ycfrg=="], - "@storybook/addon-vitest": ["@storybook/addon-vitest@10.6.2", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0 || ^5.0.0", "@vitest/browser-playwright": "^4.0.0 || ^5.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0 || ^5.0.0", "storybook": "^10.6.2", "vitest": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-gyyBzncvUyEgWQR3LinnGuI4UsWIUYLyw1u8i244aTxmY4uxCYjsDmUNkaSr8xQzNz8nh2Ckh3G4bO4SvJ6nJQ=="], + "@storybook/addon-vitest": ["@storybook/addon-vitest@10.6.3", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2" }, "peerDependencies": { "@vitest/browser": "^4.0.0 || ^5.0.0", "@vitest/browser-playwright": "^4.0.0 || ^5.0.0", "storybook": "^10.6.3", "vitest": "^4.0.0 || ^5.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "vitest"] }, "sha512-ANz0SHAi9OTtrZt+YmcQvP7tcUZrTSm8Gq/nI7p9ZRyHAUOTdGoCex2cPyLc3mEqZTCBnaYXR6ZhURlpGsh9dQ=="], - "@storybook/builder-webpack5": ["@storybook/builder-webpack5@10.6.2", "", { "dependencies": { "@storybook/core-webpack": "10.6.2", "case-sensitive-paths-webpack-plugin": "^2.4.0", "cjs-module-lexer": "^1.2.3", "css-loader": "^7.1.2", "es-module-lexer": "^1.5.0", "fork-ts-checker-webpack-plugin": "^9.1.0", "html-webpack-plugin": "^5.5.0", "magic-string": "^1.1.0", "semver": "^7.7.3", "style-loader": "^4.0.0", "terser-webpack-plugin": "^5.3.17", "ts-dedent": "^2.0.0", "webpack": "5", "webpack-dev-middleware": "^6.1.2", "webpack-hot-middleware": "^2.25.1", "webpack-virtual-modules": "^0.6.0" }, "peerDependencies": { "storybook": "^10.6.2", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-rlXEjV1ZH+AKMVvlqUjm+pmimmi0asdGv0kTxNERQPu5Kaer9NmNmyYIUJvBYyeQfAg1KB4OntF3ik71jJl3HA=="], + "@storybook/builder-webpack5": ["@storybook/builder-webpack5@10.6.3", "", { "dependencies": { "@storybook/core-webpack": "10.6.3", "case-sensitive-paths-webpack-plugin": "^2.4.0", "cjs-module-lexer": "^1.2.3", "css-loader": "^7.1.2", "es-module-lexer": "^1.5.0", "fork-ts-checker-webpack-plugin": "^9.1.0", "html-webpack-plugin": "^5.5.0", "magic-string": "^1.1.0", "semver": "^7.7.3", "style-loader": "^4.0.0", "terser-webpack-plugin": "^5.3.17", "ts-dedent": "^2.0.0", "webpack": "5", "webpack-dev-middleware": "^6.1.2", "webpack-hot-middleware": "^2.25.1", "webpack-virtual-modules": "^0.6.0" }, "peerDependencies": { "storybook": "^10.6.3", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-ZqXHwkVWqbXQ0fKTArTh+H0Wxp+9e/vcNVME492JA2dTsoMojiF3xaULpoXfsgF1aaYlQ71VQm4U+vl/pZbgRQ=="], - "@storybook/core-webpack": ["@storybook/core-webpack@10.6.2", "", { "dependencies": { "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.6.2" } }, "sha512-6nEmDApLKSKr6SWWFJUCI4wIAhR91TV7KpoYkegzS04GpJ6gRW0tZjUdGbCJuKt1hepxULtZxSU4pxddAtNoaA=="], + "@storybook/core-webpack": ["@storybook/core-webpack@10.6.3", "", { "dependencies": { "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.6.3" } }, "sha512-RUC/d9vR/0okivlmWSxPnqYDLYoXHSMO74u0mDNI00OD8NN5EFUJXgnoe7pDFb0wdu6EcPgb+quTAIGCWSXXjQ=="], "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], "@storybook/icons": ["@storybook/icons@2.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-FpUB/xizIrV1ILhCIYFo+TtQNN9YEoLjfcnAW0JtOXL+BcM2aisSysI0B54JMWDnljriXySBHgN6ps4s9lb75A=="], - "@storybook/nextjs": ["@storybook/nextjs@10.6.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-runtime": "^7.28.5", "@babel/preset-env": "^7.28.5", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@babel/runtime": "^7.28.4", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@storybook/builder-webpack5": "10.6.2", "@storybook/preset-react-webpack": "10.6.2", "@storybook/react": "10.6.2", "@types/semver": "^7.7.1", "babel-loader": "^9.1.3", "css-loader": "^6.7.3", "loader-utils": "^3.2.1", "node-polyfill-webpack-plugin": "^2.0.1", "postcss": "^8.4.38", "postcss-loader": "^8.1.1", "probe-image-size": "^7.3.0", "react-refresh": "^0.14.0", "resolve-url-loader": "^5.0.0", "sass-loader": "^16.0.5", "semver": "^7.7.3", "style-loader": "^3.3.1", "styled-jsx": "^5.1.6", "tsconfig-paths": "^4.0.0", "tsconfig-paths-webpack-plugin": "^4.0.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "next": "^14.1.0 || ^15.0.0 || ^16.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.2", "typescript": "*", "webpack": "^5.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom", "typescript", "webpack"] }, "sha512-T1qokw4brqv7uZh30GlVyqcVTCriYpNPyfpb/joUlDtU6i6bzmAiIg4ZW3aXe/aDM1RDZhrVOQ562BsVzh1MvA=="], + "@storybook/nextjs": ["@storybook/nextjs@10.6.3", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-runtime": "^7.28.5", "@babel/preset-env": "^7.28.5", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@babel/runtime": "^7.28.4", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@storybook/builder-webpack5": "10.6.3", "@storybook/preset-react-webpack": "10.6.3", "@storybook/react": "10.6.3", "@types/semver": "^7.7.1", "babel-loader": "^9.1.3", "css-loader": "^6.7.3", "loader-utils": "^3.2.1", "node-polyfill-webpack-plugin": "^2.0.1", "postcss": "^8.4.38", "postcss-loader": "^8.1.1", "probe-image-size": "^7.3.0", "react-refresh": "^0.14.0", "resolve-url-loader": "^5.0.0", "sass-loader": "^16.0.5", "semver": "^7.7.3", "style-loader": "^3.3.1", "styled-jsx": "^5.1.6", "tsconfig-paths": "^4.0.0", "tsconfig-paths-webpack-plugin": "^4.0.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "next": "^14.1.0 || ^15.0.0 || ^16.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.3", "typescript": "*", "webpack": "^5.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom", "typescript", "webpack"] }, "sha512-oKmLlj0DVmeUkbozTIrD1kPcYov3yTRWzzxJICZ6MxFclSSw55VN5UPo19c0RZcRLBa95VkwOQcJWZLuTDL46g=="], - "@storybook/preset-react-webpack": ["@storybook/preset-react-webpack@10.6.2", "", { "dependencies": { "@storybook/core-webpack": "10.6.2", "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", "@types/semver": "^7.7.1", "magic-string": "^1.1.0", "react-docgen": "^8.0.2", "resolve": "^1.22.8", "semver": "^7.7.3", "tsconfig-paths": "^4.2.0", "webpack": "5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.2", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-6gIpay0hCKEr/7rS8S+7KWcEvKPj895WCL/e10OiLoaAlPL3RpLIOyLZDN4CRxLhSJcJoSpXhhRqJWG8O5R+HQ=="], + "@storybook/preset-react-webpack": ["@storybook/preset-react-webpack@10.6.3", "", { "dependencies": { "@storybook/core-webpack": "10.6.3", "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", "@types/semver": "^7.7.1", "magic-string": "^1.1.0", "react-docgen": "^8.0.2", "resolve": "^1.22.8", "semver": "^7.7.3", "tsconfig-paths": "^4.2.0", "webpack": "5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.3", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-/olhjVgCXV24P8iS5vH78cRom2wSr8lDMt603zpZOF4ozb2uYLPaLbv8eD3vA/WyjHhy43zYE1bPhPwd8sBgkA=="], - "@storybook/react": ["@storybook/react@10.6.2", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "10.6.2", "react-docgen": "^8.0.2", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.2", "typescript": ">= 4.9.x" }, "optionalPeers": ["@types/react", "@types/react-dom", "typescript"] }, "sha512-kNWUU6Q8TrLmEqAzYmNYhC9A3i3kbjFxqIcLL3eu+o4gDu0afp1Of6Sy6jIF2qPGH05A3r6FWMzD03y4Mm5xRw=="], + "@storybook/react": ["@storybook/react@10.6.3", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "10.6.3", "react-docgen": "^8.0.2", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.3", "typescript": ">= 4.9.x" }, "optionalPeers": ["@types/react", "@types/react-dom", "typescript"] }, "sha512-3tfAkZNdUyx3I4IjM8dgbNWg4k0gj89Hi1MPiawfTybRRWhvnX2YqV9Ut4mrud7YzRv0v+U7+ZYEvj+maUPaow=="], "@storybook/react-docgen-typescript-plugin": ["@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0", "", { "dependencies": { "debug": "^4.1.1", "endent": "^2.0.1", "find-cache-dir": "^3.3.1", "flat-cache": "^3.0.4", "micromatch": "^4.0.2", "react-docgen-typescript": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "typescript": ">= 4.x", "webpack": ">= 4" } }, "sha512-KUqXC3oa9JuQ0kZJLBhVdS4lOneKTOopnNBK4tUAgoxWQ3u/IjzdueZjFr7gyBrXMoU6duutk3RQR9u8ZpYJ4Q=="], - "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.6.2", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.2" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-k0262Vqxjk3wHeg3g/QO/ENDrkC7kkbuwsFSXOdYr7Sl888tsF1DAS7uQ/gr+s/BRz1FLYRX7wBY3smEUV4MJg=="], + "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.6.3", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.3" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cJUuPrieiTl06kzlgaU89iG+CtLAgUTt0Xdvm8qFT9yR2qwafSaswhwYWlZCNX4nWuOu7eK7uzWTwzKqyDBOWQ=="], "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], @@ -1068,7 +1068,7 @@ "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], - "caniuse-lite": ["caniuse-lite@1.0.30001816", "", {}, "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001817", "", {}, "sha512-uZ1JG80BwhtMalIAGPxPfmNhHyd/B/nTdr3hITWlRamSyAdUStP1Gop01e7SD5q6J90ixJvWDbvmiFVfDgn4bw=="], "case-sensitive-paths-webpack-plugin": ["case-sensitive-paths-webpack-plugin@2.4.0", "", {}, "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw=="], @@ -1262,7 +1262,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.460", "", {}, "sha512-rRaVC7bNUyGsL+GMtmZfv3AfgG6za7gE9v5bT0PIbjEerddA0IQ+tCK05NFAXqCpNlrxBZqbFqquyxHRQONHTA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.461", "", {}, "sha512-3WZFARz1By+/rYcqQ0OLj4UcEdhd6tz2LEcDtKBUq7ey77EeJ1OtetY/5hjSUxw7dC5r5mA7FfkhRbqO9vnTCg=="], "elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="], @@ -1770,7 +1770,7 @@ "postcss-modules-values": ["postcss-modules-values@4.0.0", "", { "dependencies": { "icss-utils": "^5.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ=="], - "postcss-selector-parser": ["postcss-selector-parser@7.1.6", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw=="], + "postcss-selector-parser": ["postcss-selector-parser@7.1.7", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], @@ -1964,7 +1964,7 @@ "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - "storybook": ["storybook@10.6.2", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.3", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "11.21.2", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-qHDINMiapxFUxYb+xC3j8u7Y+85X3u+/PlytE/wmnyg+bU0agCW3jUmWP4vIh2MioO2jTK/2B3Lcw6IpRFzQBA=="], + "storybook": ["storybook@10.6.3", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.3", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "11.21.2", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-lNBvcZFG44C98KvQHYzfq1O46m+sl94zY1+na5/9E3ihtNMbvpOgYyMFMIcWYUvOMo3OM4q9jzvB73KU20bU8w=="], "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], diff --git a/package.json b/package.json index a5d857aa..dc0aceb4 100644 --- a/package.json +++ b/package.json @@ -57,12 +57,12 @@ }, "devDependencies": { "@biomejs/biome": "^2.5.15", - "@chromatic-com/storybook": "^5.5.1", - "@storybook/addon-a11y": "^10.6.2", - "@storybook/addon-docs": "^10.6.2", - "@storybook/addon-onboarding": "^10.6.2", - "@storybook/addon-vitest": "^10.6.2", - "@storybook/nextjs": "^10.6.2", + "@chromatic-com/storybook": "^5.6.0", + "@storybook/addon-a11y": "^10.6.3", + "@storybook/addon-docs": "^10.6.3", + "@storybook/addon-onboarding": "^10.6.3", + "@storybook/addon-vitest": "^10.6.3", + "@storybook/nextjs": "^10.6.3", "@tailwindcss/postcss": "^4.3.3", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", @@ -73,7 +73,7 @@ "@vitejs/plugin-react": "^6.1.2", "apca-w3": "^0.1.9", "jsdom": "^30.1.3", - "storybook": "^10.6.2", + "storybook": "^10.6.3", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", "vitest": "^5.0.5" From b287793e3890126582d6da42d57ce67debf85653 Mon Sep 17 00:00:00 2001 From: WeekendsuperHero <4048475+WeekendSuperhero@users.noreply.github.com> Date: Tue, 20 Oct 2026 06:41:04 -0700 Subject: [PATCH 7/7] test: cover combobox chips and carousel reduced motion; name chip remove buttons Addresses the Muse test-coverage review (TST-2-1, TST-2-2): - combobox: multiple-selection tests (chips render, a chip's remove button removes its value, choosing an option adds a chip) - carousel: autoplay advances when motion is allowed and never advances, with no pause control, under prefers-reduced-motion The chip test surfaced an unnamed icon-only remove button: ComboboxChip now labels it "Remove " (or `removeLabel`), as Base UI sets no name. --- CHANGELOG.md | 2 +- components/ui/combobox.tsx | 7 +++ public/r/combobox.json | 2 +- tests/combobox.test.tsx | 91 +++++++++++++++++++++++++++++++++- tests/upstream-parity.test.tsx | 60 +++++++++++++++++++++- 5 files changed, 158 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aa6028b..2213383b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - *Select*: the list is capped to the room on screen as well as 24rem (it ran off short viewports), the trigger shows the aria-invalid error state and rings only on keyboard focus, Group and Value stamp their data-slot, and icons in items and the value are laid out. - *Sheet*: content is a flex column, so its `gap-4` spaces header, body and footer, and the footer can pin to the bottom. - *Table*: `scrollX` lets a wide table scroll sideways instead of being clipped (opt-in: it makes the container capture a sticky header). - - *Accessibility*: `outline-hidden` instead of `outline-none` across the components (the same in normal rendering, but keeps keyboard focus visible in Windows High Contrast, where `outline-none` left menus, select, popover, slider and close buttons with none); Badge rings on keyboard focus only; Input's `error` prop sets aria-invalid; AlertTitle is a div, not an `
    ` in every page's outline. + - *Accessibility*: `outline-hidden` instead of `outline-none` across the components (the same in normal rendering, but keeps keyboard focus visible in Windows High Contrast, where `outline-none` left menus, select, popover, slider and close buttons with none); Badge rings on keyboard focus only; Input's `error` prop sets aria-invalid; AlertTitle is a div, not an `
    ` in every page's outline; a Combobox chip's remove button is named (`Remove `, or `removeLabel`), where it was an unnamed icon button, as Base UI sets no name of its own. - *Smaller*: data-slot on the dialog / sheet / drawer triggers and closes, popover trigger and anchor, accordion root, hover-card trigger, chart root and combobox icon / indicator; the tooltip no longer clips its own arrow; a Spinner inside a button, input-group addon or item is icon-sized; Field and Item descriptions align to the start (RTL); Breadcrumb's unused `separator` prop no longer reaches the DOM; chart tooltip / legend rows no longer share React keys; Skeleton accepts `ref` in its types. - **Aurora's horizon looked drawn on at night**: a cyan rule ran along the waterline, through the dark gaps between the curtains as much as under them. It was several things meeting on the line. Two full-width glow strips (above the line, and its reflection below) and the haze band all peaked exactly on it, the same at every x; both strips are gone, and the haze is flat-topped and thinner. The sky curtains ended hard 1vh past the line and showed through the water sheet as it faded in (a bright strip with a step under it), and the reflection's own blur ate its first few px (a dark one); both sets of curtains now run 3vh past the line, the sky ones clipped exactly on it and the mirror ones by the water box, so each is at full strength right to the line, and their skew pivots mirror each other through it. The water sheet's edge, meant to sit on the line, sat a quarter-percent above it (8% of its height rather than 4vh), and the ripples started at full strength on it (a 1px highlight on the line, a dark stripe 14px under it); the sheet is pinned in vh and the ripples fade in over 6vh below the line, on a still wrapper so the fade does not drift with them. Across the line, measured: night 113 → 111, day 170 → 169, no step, ridge or dip. One seam was left that an averaged profile cannot show: the reflection is blurred more than the sky (×1.5 against ×1.15), so where they meet every column changed profile, bright ones stepping down and the gaps between them stepping up, which averages out but drew a hard edge along the horizon on the static night scene (6.5 levels per column across one px in Safari, against 0.3–0.5 between other rows). At night the reflection now takes the sky's blur (`--au-mirror-blur-m`), and the step at the line is the same as between any two rows in WebKit (0.4) and Chromium (0.7). By day the softer mirror is unchanged: its reflection starts below the line, where it never meets the sky. - **Chase: eaten pellets grew back**: Pac's wake, which erases the pellets he eats, was a 34vw gradient that rode behind him, so the dots reappeared right behind him and the next pass ate them again. It is now a strip from his mouth back past the left edge, on its own runner in step with his, that fades only once he is off-screen, so the row stays eaten for the whole pass and refills between passes. Every wake now sits in a layer below every sprite, so a Pac or ghost crossing another lane is no longer painted over by that lane's wake. diff --git a/components/ui/combobox.tsx b/components/ui/combobox.tsx index 143c9f1f..c222fcfa 100644 --- a/components/ui/combobox.tsx +++ b/components/ui/combobox.tsx @@ -223,13 +223,19 @@ function ComboboxChips({ ); } +/* The remove button is an icon, so it carries its name: "Remove " when the chip's content is + plain text, else `removeLabel` (as in Base UI's own example). Base UI sets none, so it read as an + unnamed button to a screen reader. */ function ComboboxChip({ className, children, showRemove = true, + removeLabel, ...props }: ComboboxPrimitive.Chip.Props & { showRemove?: boolean; + /** Accessible name for the remove button; defaults to `Remove ` when the chip's content is text. */ + removeLabel?: string; }) { return ( } className="-ml-1 opacity-50 hover:opacity-100" data-slot="combobox-chip-remove" + aria-label={removeLabel ?? (typeof children === "string" || typeof children === "number" ? `Remove ${children}` : "Remove")} > diff --git a/public/r/combobox.json b/public/r/combobox.json index 02f46aef..912c1b23 100644 --- a/public/r/combobox.json +++ b/public/r/combobox.json @@ -36,7 +36,7 @@ }, { "path": "components/ui/combobox.tsx", - "content": "\"use client\";\n\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react\";\nimport { CaretDownIcon, CheckIcon, XIcon } from \"@phosphor-icons/react\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\nimport { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from \"./input-group\";\n\n/**\n * An input with a filtered list of options, single or multiple (chips), on Base UI's Combobox: the\n * primitive shadcn's combobox now uses, because Radix has none. The field is Sistine's InputGroup; the\n * popup is bordered, veiled glass like every other menu; the chips field is a bordered small-tier glass\n * surface like Input.\n *\n * Base UI marks state with presence attributes (`data-open`, `data-highlighted`, `data-disabled`) and\n * omits them when off, so the bare `data-disabled:` variant is safe here, unlike cmdk (see command.tsx).\n */\n\n/* Roles: the popup is a read-through overlay; the chips field is an input-sized surface. */\nconst POPUP_ROLE = {\n border: true,\n veil: true,\n};\nconst CHIPS_ROLE = {\n border: true,\n size: \"sm\" as const,\n};\n\nconst Combobox = ComboboxPrimitive.Root;\n\nfunction ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {\n return ;\n}\n\nfunction ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) {\n return (\n \n {children}\n \n \n );\n}\n\nfunction ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {\n return (\n }\n className={cn(className)}\n {...props}\n >\n \n \n );\n}\n\nfunction ComboboxInput({\n className,\n children,\n disabled = false,\n showTrigger = true,\n showClear = false,\n ...props\n}: ComboboxPrimitive.Input.Props & {\n showTrigger?: boolean;\n showClear?: boolean;\n}) {\n return (\n \n } {...props} />\n \n {showTrigger && (\n \n \n \n )}\n {showClear && }\n \n {children}\n \n );\n}\n\nfunction ComboboxContent({\n className,\n side = \"bottom\",\n sideOffset = 6,\n align = \"start\",\n alignOffset = 0,\n anchor,\n variant = \"glass\",\n ...props\n}: ComboboxPrimitive.Popup.Props &\n Pick &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : POPUP_ROLE, axes);\n\n return (\n \n \n \n \n \n );\n}\n\nfunction ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {\n return (\n \n );\n}\n\nfunction ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) {\n return (\n \n {children}\n }\n >\n \n \n \n );\n}\n\nfunction ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {\n return ;\n}\n\nfunction ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {\n return (\n \n );\n}\n\nfunction ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {\n return ;\n}\n\nfunction ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {\n return (\n \n );\n}\n\nfunction ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) {\n return ;\n}\n\nfunction ComboboxChips({\n className,\n ...props\n}: React.ComponentPropsWithRef & ComboboxPrimitive.Chips.Props & MaterialAxisProps) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(CHIPS_ROLE, axes);\n\n return (\n \n );\n}\n\nfunction ComboboxChip({\n className,\n children,\n showRemove = true,\n ...props\n}: ComboboxPrimitive.Chip.Props & {\n showRemove?: boolean;\n}) {\n return (\n \n {children}\n {showRemove && (\n }\n className=\"-ml-1 opacity-50 hover:opacity-100\"\n data-slot=\"combobox-chip-remove\"\n >\n \n \n )}\n \n );\n}\n\nfunction ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) {\n return (\n \n );\n}\n\nfunction useComboboxAnchor() {\n return React.useRef(null);\n}\n\nexport {\n Combobox,\n ComboboxChip,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxClear,\n ComboboxCollection,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxGroup,\n ComboboxInput,\n ComboboxItem,\n ComboboxLabel,\n ComboboxList,\n ComboboxSeparator,\n ComboboxTrigger,\n ComboboxValue,\n useComboboxAnchor,\n};\n", + "content": "\"use client\";\n\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react\";\nimport { CaretDownIcon, CheckIcon, XIcon } from \"@phosphor-icons/react\";\nimport * as React from \"react\";\n\nimport { type MaterialAxisProps, materialSurface, splitAxisProps } from \"@/lib/material\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"./button\";\nimport { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from \"./input-group\";\n\n/**\n * An input with a filtered list of options, single or multiple (chips), on Base UI's Combobox: the\n * primitive shadcn's combobox now uses, because Radix has none. The field is Sistine's InputGroup; the\n * popup is bordered, veiled glass like every other menu; the chips field is a bordered small-tier glass\n * surface like Input.\n *\n * Base UI marks state with presence attributes (`data-open`, `data-highlighted`, `data-disabled`) and\n * omits them when off, so the bare `data-disabled:` variant is safe here, unlike cmdk (see command.tsx).\n */\n\n/* Roles: the popup is a read-through overlay; the chips field is an input-sized surface. */\nconst POPUP_ROLE = {\n border: true,\n veil: true,\n};\nconst CHIPS_ROLE = {\n border: true,\n size: \"sm\" as const,\n};\n\nconst Combobox = ComboboxPrimitive.Root;\n\nfunction ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {\n return ;\n}\n\nfunction ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) {\n return (\n \n {children}\n \n \n );\n}\n\nfunction ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {\n return (\n }\n className={cn(className)}\n {...props}\n >\n \n \n );\n}\n\nfunction ComboboxInput({\n className,\n children,\n disabled = false,\n showTrigger = true,\n showClear = false,\n ...props\n}: ComboboxPrimitive.Input.Props & {\n showTrigger?: boolean;\n showClear?: boolean;\n}) {\n return (\n \n } {...props} />\n \n {showTrigger && (\n \n \n \n )}\n {showClear && }\n \n {children}\n \n );\n}\n\nfunction ComboboxContent({\n className,\n side = \"bottom\",\n sideOffset = 6,\n align = \"start\",\n alignOffset = 0,\n anchor,\n variant = \"glass\",\n ...props\n}: ComboboxPrimitive.Popup.Props &\n Pick &\n MaterialAxisProps & {\n variant?: \"default\" | \"glass\";\n }) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(variant === \"default\" ? null : POPUP_ROLE, axes);\n\n return (\n \n \n \n \n \n );\n}\n\nfunction ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {\n return (\n \n );\n}\n\nfunction ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) {\n return (\n \n {children}\n }\n >\n \n \n \n );\n}\n\nfunction ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {\n return ;\n}\n\nfunction ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {\n return (\n \n );\n}\n\nfunction ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {\n return ;\n}\n\nfunction ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {\n return (\n \n );\n}\n\nfunction ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) {\n return ;\n}\n\nfunction ComboboxChips({\n className,\n ...props\n}: React.ComponentPropsWithRef & ComboboxPrimitive.Chips.Props & MaterialAxisProps) {\n const [axes, rest] = splitAxisProps(props);\n const m = materialSurface(CHIPS_ROLE, axes);\n\n return (\n \n );\n}\n\n/* The remove button is an icon, so it carries its name: \"Remove \" when the chip's content is\n plain text, else `removeLabel` (as in Base UI's own example). Base UI sets none, so it read as an\n unnamed button to a screen reader. */\nfunction ComboboxChip({\n className,\n children,\n showRemove = true,\n removeLabel,\n ...props\n}: ComboboxPrimitive.Chip.Props & {\n showRemove?: boolean;\n /** Accessible name for the remove button; defaults to `Remove ` when the chip's content is text. */\n removeLabel?: string;\n}) {\n return (\n \n {children}\n {showRemove && (\n }\n className=\"-ml-1 opacity-50 hover:opacity-100\"\n data-slot=\"combobox-chip-remove\"\n aria-label={removeLabel ?? (typeof children === \"string\" || typeof children === \"number\" ? `Remove ${children}` : \"Remove\")}\n >\n \n \n )}\n \n );\n}\n\nfunction ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) {\n return (\n \n );\n}\n\nfunction useComboboxAnchor() {\n return React.useRef(null);\n}\n\nexport {\n Combobox,\n ComboboxChip,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxClear,\n ComboboxCollection,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxGroup,\n ComboboxInput,\n ComboboxItem,\n ComboboxLabel,\n ComboboxList,\n ComboboxSeparator,\n ComboboxTrigger,\n ComboboxValue,\n useComboboxAnchor,\n};\n", "type": "registry:component", "target": "components/ui/combobox.tsx" } diff --git a/tests/combobox.test.tsx b/tests/combobox.test.tsx index 3c8e9230..623ecde9 100644 --- a/tests/combobox.test.tsx +++ b/tests/combobox.test.tsx @@ -9,7 +9,18 @@ */ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it } from "vitest"; -import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxList } from "@/components/ui/combobox"; +import { + Combobox, + ComboboxChip, + ComboboxChips, + ComboboxChipsInput, + ComboboxContent, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxValue, + useComboboxAnchor, +} from "@/components/ui/combobox"; const fruits = [ "Apple", @@ -58,3 +69,81 @@ describe("Combobox", () => { expect(document.querySelector('[data-slot="combobox-content"]')).toHaveClass("glass", "glass-border", "glass-veil", "rounded-xl"); }); }); + +/* Multiple selection: each chosen value is a chip, and each chip's remove button (named "Remove ", + since it is an icon) takes that value back out. Composed as the Combobox story does. */ +function Chips({ onValueChange, defaultOpen }: { onValueChange?: (v: string[]) => void; defaultOpen?: boolean }) { + const anchor = useComboboxAnchor(); + return ( + + + {(values: string[]) => values.map((value) => {value})} + + + + + {(item: string) => ( + + {item} + + )} + + + + ); +} + +const chips = () => + [ + ...document.querySelectorAll('[data-slot="combobox-chip"]'), + ].map((c) => c.textContent); + +describe("Combobox with multiple selection", () => { + it("shows a chip for each selected value", () => { + render(); + expect(chips()).toEqual([ + "Apple", + "Banana", + ]); + }); + + it("removes a value through its chip's named remove button", async () => { + const changes: string[][] = []; + render( changes.push(v)} />); + fireEvent.click( + screen.getByRole("button", { + name: "Remove Apple", + }), + ); + await waitFor(() => + expect(chips()).toEqual([ + "Banana", + ]), + ); + expect(changes.at(-1)).toEqual([ + "Banana", + ]); + }); + + it("adds a chip when another option is chosen", async () => { + render(); + await waitFor(() => expect(screen.getByText("Cherry")).toBeInTheDocument()); + fireEvent.click(option("Cherry")); + await waitFor(() => + expect(chips()).toEqual([ + "Apple", + "Banana", + "Cherry", + ]), + ); + }); +}); diff --git a/tests/upstream-parity.test.tsx b/tests/upstream-parity.test.tsx index 1a7908fb..f62f732b 100644 --- a/tests/upstream-parity.test.tsx +++ b/tests/upstream-parity.test.tsx @@ -10,7 +10,7 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event"; import { ThemeProvider } from "next-themes"; import { toast } from "sonner"; -import { afterEach, beforeAll, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { Alert, AlertTitle } from "@/components/ui/alert"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { Carousel } from "@/components/ui/carousel"; @@ -205,6 +205,64 @@ describe("carousel", () => { }); }); +describe("carousel autoplay and reduced motion", () => { + // Captured per test, not at collection: the file's beforeAll installs the jsdom stub after collection. + let original: typeof window.matchMedia; + beforeEach(() => { + original = window.matchMedia; + }); + afterEach(() => { + window.matchMedia = original; + vi.useRealTimers(); + }); + const prefersReducedMotion = (reduce: boolean) => { + window.matchMedia = ((query: string) => ({ + matches: reduce && query.includes("prefers-reduced-motion: reduce"), + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + })) as unknown as typeof window.matchMedia; + }; + const autoplay = () => + render( + +
    One
    +
    Two
    +
    Three
    +
    , + ); + const current = () => document.querySelector('[aria-current="true"]')?.getAttribute("aria-label"); + + it("advances on its own when motion is allowed (the control)", () => { + prefersReducedMotion(false); + vi.useFakeTimers(); + autoplay(); + act(() => { + vi.advanceTimersByTime(250); + }); + expect(current()).toBe("Go to slide 3"); + }); + + it("never advances, and offers no pause control, under prefers-reduced-motion", () => { + prefersReducedMotion(true); + vi.useFakeTimers(); + autoplay(); + act(() => { + vi.advanceTimersByTime(1000); + }); + expect(current()).toBe("Go to slide 1"); + expect( + screen.queryByRole("button", { + name: /slideshow/, + }), + ).toBeNull(); + }); +}); + describe("sonner", () => { afterEach(() => { act(() => {