@kanzo-tech/ui 0.15.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics.d.ts.map +1 -1
- package/dist/composites/MadeWith.d.ts +2 -3
- package/dist/composites/MadeWith.d.ts.map +1 -1
- package/dist/composites/MadeWith.js.map +1 -1
- package/dist/composites/Preferences.js.map +1 -1
- package/dist/composites/theme-notice.d.ts +2 -2
- package/dist/composites/theme-notice.js.map +1 -1
- package/dist/simples/Link.d.ts +2 -2
- package/dist/simples/Link.js.map +1 -1
- package/package.json +3 -3
package/dist/analytics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,9 +3,8 @@ export interface MadeWithProps {
|
|
|
3
3
|
/**
|
|
4
4
|
* Where the brand points. Omit for a plain, unlinked line.
|
|
5
5
|
*
|
|
6
|
-
* There is no `linkComponent
|
|
7
|
-
*
|
|
8
|
-
* caller with a routed anchor composes the line itself — it is four elements.
|
|
6
|
+
* There is no `linkComponent`. A caller with a routed anchor composes the line
|
|
7
|
+
* itself — it is four elements.
|
|
9
8
|
*/
|
|
10
9
|
href?: string;
|
|
11
10
|
/** Words before the heart. Replace to translate. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MadeWith.d.ts","sourceRoot":"","sources":["../../src/composites/MadeWith.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKvC,MAAM,WAAW,aAAa;IAC5B
|
|
1
|
+
{"version":3,"file":"MadeWith.d.ts","sourceRoot":"","sources":["../../src/composites/MadeWith.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKvC,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,IAAI,EACJ,MAAoB,EACpB,KAAY,EACZ,SAAkB,EAClB,SAAS,EACT,QAAQ,GACT,EAAE,aAAa,+BAYf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MadeWith.js","sources":["../../src/composites/MadeWith.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { HeartIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Link } from \"../simples/Link\";\n\nexport interface MadeWithProps {\n /**\n * Where the brand points. Omit for a plain, unlinked line.\n *\n * There is no `linkComponent
|
|
1
|
+
{"version":3,"file":"MadeWith.js","sources":["../../src/composites/MadeWith.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { HeartIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Link } from \"../simples/Link\";\n\nexport interface MadeWithProps {\n /**\n * Where the brand points. Omit for a plain, unlinked line.\n *\n * There is no `linkComponent`. A caller with a routed anchor composes the line\n * itself — it is four elements.\n */\n href?: string;\n /** Words before the heart. Replace to translate. */\n prefix?: string;\n /** Words between the heart and the brand. Replace to translate. */\n infix?: string;\n /** Accessible name for the heart — it is read aloud, so it is a word, not a glyph. */\n loveLabel?: string;\n className?: string;\n /** The brand. Required, and children rather than a prop: it is markup. */\n children: ReactNode;\n}\n\n/**\n * \"Made with ♥ at <brand>\" — an attribution line, styled once.\n *\n * **Re-admitted after deletion, and narrower than what was deleted.** `396bfd7`\n * removed the original with a charge worth keeping in view: it \"hard-coded the\n * English 'Made with' and defaulted `by` to 'Kanzo', a brand name in a library\n * whose first admission rule is domain-freedom\". Both are gone. The brand is\n * children and has no default, so the library names nobody; the two English words\n * are props with English defaults, which is a convenience a caller can translate\n * rather than a domain the component carries. It also drops `linkComponent`, the\n * routing seam that went with `composites/link.tsx`.\n *\n * *What would reverse this:* a second product finding the sentence shape wrong —\n * a different word order, a brand that is not at the end, a line that needs two\n * links. At that point this is a layout with one arrangement pretending to be a\n * component, and the honest answer is that four elements are cheap to write.\n */\nexport function MadeWith({\n href,\n prefix = \"Made with\",\n infix = \"at\",\n loveLabel = \"love\",\n className,\n children,\n}: MadeWithProps) {\n return (\n <p\n data-slot=\"made-with\"\n className={cn(\"inline-flex items-center gap-1 text-muted-foreground text-xs\", className)}\n >\n {prefix}\n <HeartIcon role=\"img\" aria-label={loveLabel} className=\"size-3.5 fill-destructive text-destructive\" />\n {infix}\n {href ? <Link href={href}>{children}</Link> : <span className=\"font-medium text-foreground\">{children}</span>}\n </p>\n );\n}\n"],"names":["MadeWith","href","prefix","infix","loveLabel","className","children","jsxs","cn","HeartIcon","jsx","Link"],"mappings":";;;;AAyCO,SAASA,EAAS;AAAA,EACvB,MAAAC;AAAA,EACA,QAAAC,IAAS;AAAA,EACT,OAAAC,IAAQ;AAAA,EACR,WAAAC,IAAY;AAAA,EACZ,WAAAC;AAAA,EACA,UAAAC;AACF,GAAkB;AAChB,SACE,gBAAAC;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAWC,EAAG,gEAAgEH,CAAS;AAAA,MAEtF,UAAA;AAAA,QAAAH;AAAA,0BACAO,GAAA,EAAU,MAAK,OAAM,cAAYL,GAAW,WAAU,8CAA6C;AAAA,QACnGD;AAAA,QACAF,IAAO,gBAAAS,EAACC,GAAA,EAAK,MAAAV,GAAa,UAAAK,EAAA,CAAS,IAAU,gBAAAI,EAAC,QAAA,EAAK,WAAU,+BAA+B,UAAAJ,EAAA,CAAS;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG5G;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Preferences.js","sources":["../../src/composites/Preferences.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Dialog as ArkDialog } from \"@ark-ui/react/dialog\";\nimport { Portal } from \"@ark-ui/react/portal\";\nimport { InfoIcon, MoonIcon, PaletteIcon, SunIcon, XIcon } from \"lucide-react\";\n// Via the theme package's JS entry, not its raw `.json` subpath: a direct JSON subpath import\n// needs `with { type: \"json\" }` at runtime, and Rollup strips that attribute when bundling.\nimport {\n CORE_PREFS,\n prefBoolean,\n prefNumber,\n prefOptions,\n themeData,\n type Appearance,\n type CorePrefKey,\n type KanzoRadius,\n type PrefOption,\n type PrefSources,\n type SectionPrefDecl,\n} from \"@kanzo-tech/theme\";\nimport {\n useKanzoTheme,\n type FontOption,\n type ThemeContextValue,\n} from \"../theme/KanzoThemeProvider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { Alert, AlertDescription, AlertTitle } from \"../simples/alert.js\";\nimport { Badge } from \"../simples/badge.js\";\nimport { Button } from \"../simples/button.js\";\nimport { Field, FieldLabel, FieldLegend, FieldSet } from \"../simples/field.js\";\nimport {\n Dialog,\n DialogClose,\n DialogDescription,\n DialogTitle,\n DialogTrigger,\n} from \"../simples/dialog.js\";\nimport { RadioGroup as ArkRadioGroup } from \"@ark-ui/react/radio-group\";\nimport { RadioGroup, RadioGroupCard, RadioGroupLabel } from \"../simples/radio-group.js\";\nimport { Slider, SliderLabel } from \"../simples/slider.js\";\nimport { Switch } from \"../simples/switch.js\";\n\n/**\n * Preferences — a live theming selector (composite) for PRODUCT settings. A non-modal drawer\n * (metadata-form's UX) that drives {@link useKanzoTheme}, which writes `data-*` attributes to\n * `<html>` and toggles `.dark`, so every component re-skins with no changes. The default panel is\n * the canonical product set:\n *\n * <Preferences.Root>\n * <Preferences.Trigger />\n * <Preferences.Panel>\n * <Preferences.Colour /> <Preferences.Density /> <Preferences.Radius />\n * <Preferences.Font /> <Preferences.MonoFont />\n * </Preferences.Panel>\n * </Preferences.Root>\n *\n * or the all-in-one <Preferences />. Those five sections ARE the default panel body, plus a footer\n * of Reset · Done. Open with `t`, close with Escape.\n *\n * **No colour is AUTHORED here.** A tenant's identity is a palette DOCUMENT — derived and measured\n * once at onboarding, compiled to one stylesheet the server inlines — not something a user picks a\n * hue at a time. Palette, accent, base, chart scheme and the \"Copy CSS\" export all left with it;\n * what the export did belongs on the onboarding surface, which has a document to emit.\n *\n * `Colour` is not that coming back. It chooses among the things the TENANT published — the same kind\n * of choice `appearance` makes between one document's two modes, one level up — and it shows itself\n * only when there are two to choose from.\n *\n * **Appearance has no section of its own, and no control of its own either**: `Colour` draws one\n * card per side, and pressing a card wears that side. That IS the appearance control, and it is the\n * only one — an `AppearanceToggle` sat in the header until 2026-08-19 and was the same preference\n * wearing a second control twelve pixels from the first. It is deleted, not moved: it flipped\n * light ⇄ dark and so does a card, and \"follow the OS\" was never on it — that is `\"\"`, and Reset is\n * the way back.\n *\n * **What that costs, stated rather than discovered.** `Colour` hides itself below two published\n * choices, so a tenant that published one palette and one brand now has a panel with no appearance\n * control at all. That is a real hole and the reason this paragraph exists; the fix, if it is\n * wanted, is for the section to draw the two cards even where there is nothing to choose between —\n * not for the button to come back.\n *\n * It has two states and not three: \"follow the OS\" is `\"\"`, the absence of a pinned side, so the\n * way back to it is `Reset` — which UNSETS every preference rather than writing each default, and\n * therefore lands wherever the tenant's document says. That is the one thing this panel's footer\n * does that no other control can, and the declaration now offers the same value as an option.\n *\n * **What a tenant pinned or withheld is not drawn at all.** Every section asks `corePrefs[key]`\n * whether its axis is still offered, because a control the chain will ignore is a control that\n * visibly does nothing.\n *\n * **One renderer draws every preference**, the core's axes and a contributed section's alike:\n * `PrefControl` switches on `kind` and nothing else. Two sections are left, each because its\n * CONTROL differs rather than its data — `RadiusSection` is a slider over an ordered scale, and\n * `ColorSection` draws documents at full size with a live cascade preview. The three specimens a\n * generic control cannot draw are a lookup keyed by axis, not three components.\n */\n\n// Off the declaration, which is generated from the same table `themes.css` is emitted from. This\n// was typed here — beside a `themeData` import that already carried it — and a hand-copy disagrees\n// with the CSS the moment the generator changes. `?? []` is unreachable for a core axis (its\n// options are a literal list, never a source), and is how `prefOptions` says so in the type.\n//\n// It survives the collapse into one renderer because the radius control is a SLIDER and needs the\n// steps in order, as an array it can index. Every other axis reads its options off the declaration\n// at the point of drawing.\nconst RADII = (prefOptions(CORE_PREFS.radius) ?? []).map((o) => o.value as KanzoRadius);\n// ── Root: Ark Dialog (non-modal, live-preview) + hotkey ──────────────────────\nexport interface PreferencesRootProps {\n children: React.ReactNode;\n /**\n * Key that toggles the panel, e.g. `\"t\"`. **Opt-in — there is no default.**\n *\n * When set, this registers a `window` keydown listener that fires on the bare key (typing in\n * an input, textarea or contenteditable is ignored). A design system must not claim a\n * single, unmodified key in its host's global keymap without being asked, so omit this\n * unless the host has decided that key is free.\n */\n hotkey?: string;\n defaultOpen?: boolean;\n}\n\nfunction PreferencesRoot({ children, hotkey, defaultOpen = false }: PreferencesRootProps) {\n const [open, setOpen] = React.useState(defaultOpen);\n\n React.useEffect(() => {\n if (!hotkey) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.metaKey || e.ctrlKey || e.altKey || e.key.toLowerCase() !== hotkey) return;\n const el = document.activeElement;\n const typing =\n el instanceof HTMLElement &&\n (el.tagName === \"INPUT\" || el.tagName === \"TEXTAREA\" || el.isContentEditable);\n if (typing) return;\n e.preventDefault();\n setOpen((o) => !o);\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [hotkey]);\n\n // Non-modal so the app stays interactive and re-skins live behind the panel — no backdrop, no\n // scroll lock, no focus trap. But it still closes on an outside click, because that is what a\n // panel is expected to do and the alternative surprises people far more often than it helps.\n //\n // It used to pin `closeOnInteractOutside={false}` so you could click around the app and watch it\n // re-skin. That case survives: the hotkey reopens it where you left off, and the live preview was\n // never the reason to keep it open — every change applies on selection, not on close.\n //\n // `closeOnInteractOutside` is passed explicitly, and has to be. Zag derives its default from\n // `modal` (`closeOnInteractOutside: modal && !alertDialog` in dialog.machine.js), so a non-modal\n // dialog is non-dismissable *by default* — dropping the old explicit `false` changed nothing at\n // all. The two props look independent and are not.\n return (\n <Dialog\n open={open}\n onOpenChange={(e) => setOpen(e.open)}\n modal={false}\n closeOnInteractOutside={true}\n >\n {children}\n </Dialog>\n );\n}\n\n// ── Trigger: floating palette FAB ────────────────────────────────────────────\nfunction PreferencesTrigger({ className }: { className?: string }) {\n return (\n <DialogTrigger asChild>\n <Button\n type=\"button\"\n size=\"icon-md\"\n variant=\"outline\"\n aria-label=\"Theme preferences\"\n // `m-0`: a fixed FAB must not inherit a parent's flow spacing (`space-y-*`).\n // `bg-card`: `outline` is transparent by design, which reads as broken once the\n // button floats over arbitrary page content — a FAB needs an opaque surface.\n //\n // The `data-state` pair is load-bearing, not decoration. The panel is `modal={false}`, so\n // there is no backdrop dimming the page behind it — without an open state on the trigger,\n // nothing on screen says the panel is up. Ark's Dialog.Trigger already emits\n // `data-state=\"open\"`, so this needs no extra state.\n //\n // Deliberately NOT a `Toggle`: `Dialog.Trigger` gives `aria-expanded` +\n // `aria-haspopup=\"dialog\"`, which is the disclosure pattern this is. A toggle button\n // would report `aria-pressed` instead and drop the haspopup — worse semantics for a\n // control that reveals a panel. What was missing was the visual state, not the role.\n className={cn(\n \"fixed end-4 bottom-4 z-40 m-0 rounded-full bg-card shadow-lg\",\n \"data-[state=open]:bg-accent data-[state=open]:text-accent-foreground\",\n className,\n )}\n >\n <PaletteIcon />\n </Button>\n </DialogTrigger>\n );\n}\n\n// ── Panel: non-modal drawer pinned top-right (portaled Ark Dialog content) ────\nfunction PreferencesPanel({\n children,\n title = \"Preferences\",\n hint = \"Applied live · saved to this browser.\",\n}: {\n children?: React.ReactNode;\n title?: string;\n hint?: string;\n}) {\n return (\n <Portal>\n <ArkDialog.Positioner className=\"pointer-events-none fixed inset-0 z-50 flex items-start justify-end p-4\">\n <ArkDialog.Content\n data-slot=\"preferences-panel\"\n className={cn(\n // `w-96`, not `w-80`. The width was chosen when the panel held four axes and a colour\n // strip; it now holds five sections plus however many the packages a host installed\n // contribute, and it is the one surface here that grows with somebody else's decision.\n // The extra 64px is what lets a palette card depict a document rather than gesture at\n // one — see `ThemePreview`.\n \"pointer-events-auto relative flex max-h-[calc(100dvh-2rem)] w-96 flex-col overflow-hidden\",\n \"rounded-lg border border-border bg-popover text-popover-foreground shadow-xl\",\n \"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-right-4\",\n \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-right-4\",\n \"motion-reduce:animate-none!\",\n )}\n >\n {/* Header. Nothing but the title and the close X: a sun/moon toggle sat here, and it was\n the same preference wearing a second control twelve pixels from the first. `Colour`\n draws a card per side and pressing one wears it, which is the whole of what the\n button did — it cycled nothing, since \"follow the OS\" is `\"\"` and Reset is its way\n back. What that costs is stated on `ColorSection`: below two published choices the\n section returns null, and such a panel now offers no appearance control at all. */}\n <div className=\"px-5 pt-5 pb-3\">\n <DialogTitle className=\"font-heading text-base font-semibold\">{title}</DialogTitle>\n <DialogDescription className=\"mt-0.5 text-[length:var(--kanzo-font-size-small)] text-muted-foreground\">\n {hint}\n </DialogDescription>\n </div>\n <div className=\"absolute inset-e-3.5 top-3.5 flex items-center gap-0.5\">\n <DialogClose asChild>\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label=\"Close preferences\"\n className=\"opacity-64 hover:opacity-100\"\n >\n <XIcon />\n </Button>\n </DialogClose>\n </div>\n\n {/* Body — scrolls independently of the pinned header/footer.\n `[&>*]:shrink-0` is load-bearing, not hygiene. This is a COLUMN flex container\n inside a `max-h`-constrained panel, so its children are flex items that default\n to `flex-shrink: 1`. Once the sections are taller than the panel, the browser\n satisfies the constraint by SQUASHING every section rather than scrolling this\n box — sections collapse to a fraction of their height, their controls overlap\n and clip, and the lower ones become unreadable. That is the \"renders wrong /\n does not show all its fields\" bug: the panel was never scrolling at all. */}\n <form\n className=\"flex flex-1 flex-col gap-6 overflow-y-auto px-5 pb-5 [&>*]:shrink-0\"\n onSubmit={(e) => e.preventDefault()}\n >\n {children ?? (\n <>\n {/* Colour first, and ONE section: a palette and a brand are one choice at two\n grains, so the panel offers one list and everything below it is a different axis\n entirely. It renders nothing until the tenant published two choices, so the\n common panel is unchanged. */}\n <ColorSection />\n <DensitySection />\n <RadiusSection />\n <FontSection />\n <MonoFontSection />\n {/* Last, and in the same visual language as the five above. A package that owns a\n user-facing choice contributes it here rather than building a settings surface\n of its own beside this one — which is the whole difference between one product\n and several sharing a window. */}\n <ContributedSections />\n </>\n )}\n </form>\n\n {/* Footer — canonical Shark actions bar: top separator + muted surface. */}\n <PreferencesFooter />\n </ArkDialog.Content>\n </ArkDialog.Positioner>\n </Portal>\n );\n}\n\n/** Actions bar pinned to the bottom of the panel (Reset · Done). */\nfunction PreferencesFooter() {\n // `reset`, not `set({ ...DEFAULT_PREFS })`, which is what this was. Storage holds what a user\n // CHOSE, so spreading the defaults would write each axis explicitly — making Reset the one act\n // that pins somebody against their tenant's document. Unsetting lands wherever the chain says.\n const { reset } = useKanzoTheme();\n // No fill on the bar. The panel is `bg-popover`, and in dark `--popover` and `--muted` are the\n // same value, so a 48% muted wash composited to ΔE ~0 — the background contributed nothing and\n // the `border-t` was doing all the work. Same defect as the command, popover and dialog footers;\n // this one survived that sweep by living in `composites/`.\n return (\n <div className=\"flex items-center gap-2 border-t border-border px-4 py-3\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={reset}>\n Reset\n </Button>\n <DialogClose asChild>\n <Button type=\"button\" size=\"sm\" className=\"ms-auto\">\n Done\n </Button>\n </DialogClose>\n </div>\n );\n}\n\n// ── Section building blocks ──────────────────────────────────────────────────\n/**\n * A titled preferences row. This used to be a bare `<div>` plus a hand-rolled `GroupTitle` —\n * fixed typography, no `htmlFor`, no association with the control it titled, and in the\n * segmented cases the label was written TWICE (once visibly, once as `aria-label`).\n *\n * `Field` is precisely this, wired: it generates the id, connects the label to the control and\n * carries the state by context. The forms guide says so; the library's own flagship composite\n * was the one place not taking its own advice.\n */\nfunction PrefField({ label, children }: { label: React.ReactNode; children: React.ReactNode }) {\n return (\n <Field>\n <FieldLabel className={PREF_HEADING}>{label}</FieldLabel>\n {children}\n </Field>\n );\n}\n\n/**\n * ONE spelling for every heading this panel draws, and the reason it is a class rather than a\n * component: each machine dictates the ELEMENT its name lives on — a `<legend>` for a group, a\n * `<label>` for a single control, the slider's own label part, a plain `<span>` where a legend\n * would capture two radio groups at once. A `PreferencesHeading` component could only render one\n * of those, so what the four have in common is exactly this string and nothing else.\n *\n * Two details are load-bearing rather than taste. The size is `!` because `FieldLegend` sets its\n * own through a `data-[variant=…]:` selector, which outranks a plain class whatever the source\n * order — a non-important override there silently does nothing. And `block` is what makes `mb-2`\n * mean anything on `SliderLabel`, which is inline by default: the slider's heading sat one gap\n * tighter than the others for exactly that reason, and three headings at three spacings read as\n * three ranks once they share a grid.\n */\nconst PREF_HEADING = cn(\n \"mb-2 block font-medium uppercase tracking-wide text-muted-foreground\",\n \"text-[length:var(--kanzo-font-size-small)]!\",\n);\n\n/** The side cards' own titles — a card title, not a section heading, so only the size is shared. */\nconst PREF_LABEL_SIZE = \"text-[length:var(--kanzo-font-size-small)]\";\n\n/**\n * A titled GROUP of options — the container `PrefField` cannot be.\n *\n * `Field` addresses one control, and a radio group has one hidden input per item, so there is no\n * id for its label to point at; `radio-group.tsx` says exactly this, and says `FieldSet` +\n * `FieldLegend` is the answer.\n *\n * The legend really is the name, not a decoration beside one: Ark's `useRadioGroup` passes\n * `ids: { label: fieldset.ids.legend }` into the machine, and zag's root props set\n * `aria-labelledby` from it.\n */\nfunction PrefFieldSet({ label, children }: { label: React.ReactNode; children: React.ReactNode }) {\n return (\n <FieldSet className=\"gap-2\">\n <FieldLegend className={PREF_HEADING}>{label}</FieldLegend>\n {children}\n </FieldSet>\n );\n}\n\n// ── Sections ──────────────────────────────────────────────────────────────────\n/**\n * COLOUR — every choice the tenant published, as one list.\n *\n * **A palette and an identity are one abstraction with a parameter: how much of the document the\n * choice replaces.** An identity replaces the brand-derived slice and inherits every surface; a\n * palette replaces all of it. They always shared this control, this option type, the hide-below-two\n * rule and the retirement machinery — and the giveaway was the behaviour: changing palette *files and\n * restores* the identity, which is what containment does and what two sibling axes never would.\n *\n * So the panel shows one list. A palette that publishes several brands contributes one entry per\n * brand — `Bank · Retail`, `Bank · Private` — which is how VS Code and Slack present variants, and\n * which matches what the user is actually doing: making one choice. The model keeps the containment,\n * because that is what guarantees a tenant's brands share a neutral and stay one product.\n *\n * The prefix appears only when there is more than one palette to disambiguate against: a client with\n * a single palette and two brands sees `Retail` and `Private`, not their own name twice.\n */\nexport interface PreferencesColorProps {\n /** The legend, and one of the section's two library-authored strings (i18n). */\n label?: string;\n /** Heading of the notice shown when the tenant withdrew what this user had chosen. */\n retiredTitle?: string;\n /** Compose that notice's body. The argument is the retired **id**; its label went with it. */\n formatRetired?: (parts: { choice: string }) => string;\n /** Name a side. The section's third and last library-authored string (i18n). */\n formatSide?: (side: Appearance) => string;\n}\n\nconst DEFAULT_RETIRED_TITLE = \"Colours updated\";\nconst DEFAULT_RETIRED = ({ choice }: { choice: string }) =>\n `The colours you had chosen (${choice}) are no longer published, so these are the default ones.`;\n\n/**\n * One theme, drawn by ITSELF — a miniature of the interface rather than a list of its hexes.\n *\n * **Nothing here is data.** The theme is already in the page under its own `[data-theme]`, so this\n * span sets the attribute and every utility inside it resolves against that theme: `bg-primary` is\n * that tenant's brand, `bg-chart-3` is their third categorical slot. A strip of four hexes was the\n * alternative and it could not depict a theme — on the default's own, `--primary` and `--foreground`\n * are the same value, so two of the four chips were one colour and the control said nothing.\n *\n * **Three things this used to need and no longer does**, and they are the same three the whole\n * refactor removed:\n *\n * · *The appearance class.* A document carried two blocks, so a preview had to force a side with\n * `.light` / `.dark` or it would take the wrong one on a dark page. A theme IS a side.\n * · *A second attribute.* `data-identity` selected the brand within the document. A brand is a\n * theme, so there is one attribute.\n * · *The default's special case.* The default document was emitted at bare `:root` with no scope of\n * its own, so `[data-palette=\"kanzo\"]` matched nothing and the cell inherited whatever the page\n * wore — the class alone had to carry it. `themes/kanzo.css` answers to `:root` **and**\n * `[data-theme=\"kanzo\"]`, so the default selects like every other theme.\n *\n * The categorical strip draws all eight slots on purpose. Past a theme's `--chart-capacity` the\n * slot is `var(--muted-foreground)`, so a set that holds seven says so by going grey at the end.\n */\nfunction ThemePreview({ theme }: { theme: string }) {\n return (\n <span\n aria-hidden\n className={cn(\n // `h-16` at every width, tried and reverted at `@md:h-28`: the miniature's three rows are a\n // fixed amount of ink, so height bought empty background and the tile read as a page that\n // had failed to load rather than as a denser one. GitHub's tile is 2:1 because it is a\n // screenshot with a screenshot's worth of content in it.\n \"flex h-16 flex-col justify-between rounded-[4px] border border-border bg-background p-2\",\n )}\n data-slot=\"theme-preview\"\n data-theme={theme || undefined}\n >\n {/* Chrome: the brand fill, and two weights of ink on the page. */}\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-4 rounded-[2px] bg-primary\" />\n <span className=\"h-1 w-5 rounded-full bg-foreground\" />\n <span className=\"h-1 flex-1 rounded-full bg-muted-foreground\" />\n </span>\n\n {/* Two lines of code, indented — the half that makes the card true rather than decorative.\n A document derives seven syntax roles of its own, so Dracula's card paints Dracula's\n keywords; a card that stopped at surfaces and a brand would show six documents agreeing\n about the only part of themselves they share. This is the move GitHub's theme picker\n makes, and the reason it is the reference: the tile shows the thing being themed. */}\n <span className=\"flex flex-col gap-[3px] ps-1\">\n <span className=\"flex items-center gap-[3px]\">\n <span className=\"h-1 w-3 rounded-full bg-[var(--syntax-keyword)]\" />\n <span className=\"h-1 w-5 rounded-full bg-[var(--syntax-function)]\" />\n <span className=\"h-1 w-2 rounded-full bg-[var(--syntax-number)]\" />\n </span>\n <span className=\"flex items-center gap-[3px] ps-2\">\n <span className=\"h-1 w-4 rounded-full bg-[var(--syntax-property)]\" />\n <span className=\"h-1 w-6 rounded-full bg-[var(--syntax-string)]\" />\n <span className=\"h-1 w-2.5 rounded-full bg-[var(--syntax-type)]\" />\n </span>\n </span>\n\n {/* The categorical set, all eight slots. Past a document's capacity `compile` writes\n `var(--muted-foreground)`, so a set that holds seven says so by going grey at the end. */}\n <span className=\"flex items-center gap-px\">\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-1\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-2\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-3\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-4\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-5\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-6\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-7\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-8\" />\n </span>\n </span>\n );\n}\n\n/**\n * Wear a palette while the pointer is on it, and put back exactly what was there.\n *\n * This is the reference behaviour — VS Code and Zed preview a theme as you arrow through the list,\n * and revert when you leave — and it is affordable here for one reason: every document the tenant\n * publishes is already in the page, so previewing is two attribute writes and no fetch, no\n * stylesheet swap and no re-render.\n *\n * **It restores a snapshot rather than recomputing what should be there.** The provider removes an\n * axis attribute at its default and sets it otherwise, so \"put it back\" has two spellings depending\n * on preferences this component would have to read and agree with. Reading the element on the way\n * in cannot disagree with anything.\n */\nfunction useThemePreview() {\n const held = React.useRef<{ theme: string | null } | null>(null);\n\n const restore = React.useCallback(() => {\n const snapshot = held.current;\n if (!snapshot) return;\n held.current = null;\n const el = document.documentElement;\n if (snapshot.theme === null) el.removeAttribute(\"data-theme\");\n else el.setAttribute(\"data-theme\", snapshot.theme);\n }, []);\n\n const preview = React.useCallback((theme: string) => {\n const el = document.documentElement;\n // Only the FIRST entry into the list snapshots. Moving from one card to the next fires leave\n // and enter in an order the pointer decides, and re-snapshotting mid-sweep would file the theme\n // being previewed as the one to go back to.\n held.current ??= { theme: el.getAttribute(\"data-theme\") };\n if (theme) el.setAttribute(\"data-theme\", theme);\n else el.removeAttribute(\"data-theme\");\n }, []);\n\n // A panel closed mid-preview — Escape, a click on the trigger — never fires the leave handler,\n // and would leave the reader wearing a theme they did not choose.\n React.useEffect(() => restore, [restore]);\n\n return { preview, restore };\n}\n\n/** The two sides, in the order a settings page reads them. */\nconst APPEARANCES: Appearance[] = [\"light\", \"dark\"];\n\n/** `light` / `dark`, and the only two strings this section authors. */\nconst DEFAULT_SIDE_LABEL = (side: Appearance) => (side === \"light\" ? \"Light\" : \"Dark\");\n\ntype Entry = { key: string; label: string };\n\n/**\n * One side of the choice — which document this user wears in light, or in dark.\n *\n * **The miniature is painted with the document it is offering, forced to this side.** That is what\n * `compile`'s scoped selectors were designed for and what GitHub's own picker cannot do: their\n * tiles are drawn assets, ours is the real cascade answering. A card for the side you are not\n * currently in therefore shows what you would get, not a tinted guess.\n *\n * The chips are the palettes, and they are the same control as the card above them rather than a\n * second one: one radio group per side, so the two cards never fight over a single selection.\n *\n * **The tile IS the appearance control.** It was a `Use` button beside the title, which is the\n * failure this panel keeps removing everywhere else: a second control for a preference the surface\n * was already depicting. What you want is on screen at full size, so pressing it is the whole\n * gesture — the reference is GitHub's Appearance page, where the state is a bordered card and an\n * `Active` pill and there is no verb anywhere on it.\n *\n * A pressable region is a **button inside the container**, never the container merged with one:\n * `item.tsx` states the rule and this is the same case, forced twice over. The card is the radio\n * group's root (`role=\"radiogroup\"`), and the chips are `<label>`s — a button wrapping them would\n * be interactive content inside interactive content, and merging the roles would lose one of them.\n */\nfunction SideCard({\n entries,\n formatSide,\n live,\n onPreview,\n onRestore,\n selectedFor,\n setAppearance,\n setTheme,\n side,\n}: {\n entries: Entry[];\n formatSide: (side: Appearance) => string;\n /** Whether this is the side currently applied. Only then does hovering preview anything. */\n live: boolean;\n onPreview: (theme: string) => void;\n onRestore: () => void;\n resolvedTheme: string;\n selectedFor: (side: Appearance) => string;\n setAppearance: (appearance: Appearance) => void;\n setTheme: (theme: string, options?: { appearance?: Appearance }) => void;\n side: Appearance;\n}) {\n const selected = selectedFor(side);\n const current = entries.find((entry) => entry.key === selected) ?? entries[0];\n const Icon = side === \"light\" ? SunIcon : MoonIcon;\n\n return (\n // The CARD is the radio group, so it is named by the machine's own label part rather than\n // inheriting the fieldset's legend. Two groups under one legend both answered to \"Colour\", which\n // is a screen reader hearing the same name twice with no way to tell the sides apart — and an\n // `aria-label` could not fix it, because zag points `aria-labelledby` at the legend and that\n // wins. The Ark tie-break at `/docs/design/references` is the rule; this is the case.\n <RadioGroup\n className={cn(\n \"flex flex-col gap-2 rounded-lg border p-2.5 transition-colors\",\n // Selected the way `RadioGroupCard` spells selected, minus its `bg-primary/17`: the chips\n // inside are radio cards too, and a checked chip on a tinted card is the same wash twice\n // with only a border left to tell them apart. A ring buys the same emphasis and leaves the\n // fill for the control that has nothing else.\n live ? \"border-primary ring-1 ring-primary\" : \"border-border\",\n )}\n onBlur={onRestore}\n onPointerLeave={onRestore}\n onValueChange={(d) => {\n if (!d.value) return;\n onRestore();\n setTheme(d.value, { appearance: side });\n }}\n slot=\"preferences-side-card\"\n value={selected}\n >\n {/* Which side is applied right now, and it says so in a WORD: the border and the ring are\n colour, and colour cannot be the only thing carrying a state (1.4.1). Absolutely placed\n rather than in the header row, so it stays out of the button's accessible name — inside,\n every tile answered to \"Light Active\" — and `pointer-events-none` so the corner it covers\n still presses the tile. The root is `position: relative` already; zag sets it. */}\n {live ? (\n <Badge className=\"pointer-events-none absolute end-2.5 top-2.5\" variant=\"info\">\n Active\n </Badge>\n ) : null}\n\n {/* The tile: the side's name, and the document it would paint. `aria-pressed` and not a\n `role=\"radio\"` hand-rolled across two sibling groups — two toggles, each reporting its own\n state, is what a screen reader can follow here.\n\n `RadioGroupLabel` renders through `asChild` onto the span, so the machine's own label part\n IS the visible title (zag's label props carry no `htmlFor`, so a span is a legal host) and\n the group's accessible name is the word already on screen. Writing it twice — once for the\n eye, once in an `aria-label` — is the duplication this file removes everywhere else. */}\n <button\n aria-pressed={live}\n className={cn(\n \"-m-1 flex cursor-pointer flex-col gap-2 rounded-md p-1 text-start transition-colors\",\n \"hover:bg-foreground/14\",\n \"outline-none focus-visible:ring-[3px] focus-visible:ring-ring\",\n )}\n onClick={() => setAppearance(side)}\n type=\"button\"\n >\n {/* `pointer-events-none` because zag's label props carry `onClick: focus`, which is right\n when a label sits BESIDE its group and wrong when it sits inside a button: clicking the\n word `Light` pressed the tile and then moved focus into the chips, so where you clicked\n decided what happened. The label is text; the tile takes the click. */}\n <RadioGroupLabel asChild>\n <span className=\"pointer-events-none flex items-center gap-1.5\">\n <Icon className=\"size-3.5 text-muted-foreground\" />\n <span className={cn(PREF_LABEL_SIZE, \"font-medium\")}>{formatSide(side)}</span>\n </span>\n </RadioGroupLabel>\n\n <ThemePreview theme={current?.key ?? \"\"} />\n </button>\n\n {/* The chosen document's name — under the tile, with the chips it belongs to rather than\n inside the button, where it would have joined the tile's accessible name. */}\n <span className=\"truncate text-muted-foreground text-xs\">{current?.label}</span>\n\n <span className=\"flex flex-wrap gap-1\">\n {entries.map((entry) => (\n <RadioGroupCard\n // A chip, not a card: the depiction is one dot and the name is the accessible name.\n className=\"size-6 items-center justify-center rounded-full p-0\"\n key={entry.key}\n // Hovering only previews on the side being worn. Previewing the other one would repaint\n // the page into a mode the reader did not ask for, which is a worse lie than no preview.\n onFocus={live ? () => onPreview(entry.key) : undefined}\n onPointerEnter={live ? () => onPreview(entry.key) : undefined}\n title={entry.label}\n value={entry.key}\n >\n <span\n aria-hidden\n className=\"size-3.5 rounded-full bg-primary ring-1 ring-border\"\n data-slot=\"theme-chip\"\n data-theme={entry.key || undefined}\n />\n <ArkRadioGroup.ItemText className=\"sr-only\">{entry.label}</ArkRadioGroup.ItemText>\n </RadioGroupCard>\n ))}\n </span>\n </RadioGroup>\n );\n}\n\nfunction ColorSection({\n label = \"Colour\",\n retiredTitle = DEFAULT_RETIRED_TITLE,\n formatRetired = DEFAULT_RETIRED,\n formatSide = DEFAULT_SIDE_LABEL,\n}: PreferencesColorProps = {}) {\n const {\n corePrefs,\n defaultThemeFor,\n resolvedAppearance,\n resolvedTheme,\n retiredTheme,\n setAppearance,\n setTheme,\n themeByAppearance,\n themes,\n } = useKanzoTheme();\n const { preview, restore } = useThemePreview();\n\n // What the TENANT still lets this user choose. A pinned theme leaves exactly one to offer, so the\n // section disappears — which is the white-label case working, not a control failing. The\n // fall out of the existing `entries.length < 2` rule rather than adding a second way to hide.\n const offered = corePrefs.themeByAppearance?.offered === false\n ? themes.filter((theme) => theme.value === resolvedTheme)\n : themes;\n\n // **One entry per theme, and that is the whole of this now.** It used to be a flatMap over\n // documents that expanded each into its brands, prefixing labels when more than one document was\n // offered, and computing a `scope` because the default document had no selector of its own. A\n // brand is a theme and every theme selects the same way, so the list is the list.\n const entries: Entry[] = offered.map((theme) => ({ key: theme.value, label: theme.label }));\n\n if (entries.length < 2) return null;\n\n const retired = retiredTheme;\n\n /**\n * Which entry a side is wearing.\n *\n * For the applied side this is the RESOLVED theme, never the preference: an empty preference is a\n * deferral to the tenant, and the entry that reads as checked has to be the one on screen. For the\n * other side there is nothing on screen to agree with, so it falls back the same way the provider\n * would — the stored value, or the tenant's default.\n */\n const selectedFor = (side: Appearance) =>\n (side === resolvedAppearance ? resolvedTheme : themeByAppearance[side]) || defaultThemeFor(side);\n\n return (\n // A plain heading, not `PrefFieldSet`, and that is forced rather than chosen. Ark's\n // `useRadioGroup` takes its `ids.label` from an ambient fieldset's legend and zag sets\n // `aria-labelledby` from it, so inside one every group answers to \"Colour\" and the label part\n // cannot win. Two groups live here, so the section keeps a heading and each card carries its\n // own name.\n <div className=\"flex flex-col gap-2\">\n <span className={PREF_HEADING}>{label}</span>\n {/* Two sibling cards, one per side — GitHub's Appearance page, whose move this borrows: the\n tile shows the thing being themed rather than naming it.\n \n The semantics ARE theirs now, and that is new. GitHub pairs a day theme with a night theme\n because its themes are single-mode; ours used to each carry both, so a card meant *which\n document this user wears on the light side*. A theme is one mode, so a card is the light\n theme, and the per-appearance palette question closed by becoming the\n obvious shape rather than by being argued.\n \n A container query rather than a media query, because the question is how much room THIS\n section was given, not how big the window is. The same markup is one column inside a\n 384px drawer and two on a settings page, which is the whole claim that a section is\n independent of the surface that hosts it. */}\n <div className=\"@container\">\n <div className=\"grid gap-2 @md:grid-cols-2\">\n {APPEARANCES.map((side) => (\n <SideCard\n entries={entries}\n formatSide={formatSide}\n key={side}\n live={side === resolvedAppearance}\n onPreview={preview}\n onRestore={restore}\n resolvedTheme={resolvedTheme}\n selectedFor={selectedFor}\n setAppearance={setAppearance}\n setTheme={setTheme}\n side={side}\n />\n ))}\n </div>\n </div>\n {/* One notice for both, because there is one choice: whichever half the tenant withdrew, what\n the user lost is the colours they picked. No toast — a document is served, so the page they\n are reading is already the default one and nothing is about to change under them. */}\n {retired ? (\n <Alert variant=\"info\">\n <InfoIcon />\n <AlertTitle>{retiredTitle}</AlertTitle>\n <AlertDescription>{formatRetired({ choice: retired })}</AlertDescription>\n </Alert>\n ) : null}\n </div>\n );\n}\n\n/**\n * One declared preference, drawn — the switch on `kind`, in one place.\n *\n * **It draws the core's axes and a contributed section's alike**, which is the point: `Font`,\n * `Mono font` and `Density` were three components that differed only in which specimen they put on\n * a card, and a fourth surface rendering a contributed group had to re-decide what a `range` looks\n * like. The three arms are the primitives the panel already used: `choice` is a radio list,\n * `toggle` is `Switch`, `range` is `Slider`.\n *\n * A value is a string in storage for all three — see `SectionPrefDecl` — so each arm parses on the\n * way in with the section mechanism's own readers rather than a local `Number()` that would differ\n * from what the resolver validated against.\n *\n * `specimen` is the escape hatch, and it is the only one: a generic control cannot draw a typeface\n * in its own face or a size at its real size. What it may not do is change the CONTROL — a\n * declaration that needs a different one is a section of its own, and there are two of those left\n * ({@link RadiusSection}, {@link ColorSection}), each saying why in its own comment.\n */\nfunction PrefControl({\n name,\n onChange,\n pref,\n sources,\n specimen,\n}: {\n name: string;\n onChange: (next: string) => void;\n pref: { value: string; decl: SectionPrefDecl };\n /** What the tenant published, for a choice whose options name a source. */\n sources?: PrefSources;\n /** Drawn above each option's name. Its presence is also what lays the cards out in a row. */\n specimen?: (option: PrefOption) => React.ReactNode;\n}) {\n const { decl, value } = pref;\n // The declaration's name, and the key when it has none — which is what every surface drew before\n // a preference could carry one, and is still what an older manifest gets.\n const title = decl.label ?? name;\n\n if (decl.kind === \"toggle\") {\n // `Field` and nothing else, because Ark's Switch **does** read the ambient field context — its\n // hidden input comes out carrying `aria-labelledby=\"field::…::label\"`. That is the opposite of\n // `useSlider`, which reads none, and the difference is why `RadiusSection` needs the machine's\n // own label part and this does not. An `aria-label` here was tried and is exactly the\n // duplication this panel keeps removing: it lands on the `<label>` root, which has no role, so\n // it names nothing and hides that the wiring was already correct.\n //\n // The control's role is `checkbox`, not `switch`: Ark renders a hidden `input type=\"checkbox\"`\n // and does not set `role=\"switch\"` on it. Upstream's call, adopted verbatim.\n return (\n <PrefField label={title}>\n <Switch\n checked={prefBoolean(value)}\n onCheckedChange={(d) => onChange(String(d.checked === true))}\n />\n </PrefField>\n );\n }\n\n if (decl.kind === \"range\") {\n // `SliderLabel` is the machine's own label part — zag points every thumb's `aria-labelledby` at\n // it — so the visible label IS the name, the way `RadiusSection` does it.\n return (\n <Slider\n max={decl.max}\n min={decl.min}\n onValueChange={(d) => onChange(String(d.value[0] ?? decl.min))}\n step={decl.step}\n value={[prefNumber(value, decl)]}\n >\n <SliderLabel className={PREF_HEADING}>{title}</SliderLabel>\n </Slider>\n );\n }\n\n // `?? []` and not a throw: a choice whose source the host has not answered yet has nothing to\n // offer *this render*, and the value it resolved to is still applied. Drawing an empty group is\n // what `PreferencesColor` already does below two published documents.\n const options = prefOptions(decl, sources) ?? [];\n // A specimen is wide and wants its name under it; a bare name is a row in a list. One rule, read\n // off the data, rather than a layout prop each call site has to remember to pass.\n return (\n <PrefFieldSet label={title}>\n <RadioGroup\n className={specimen ? \"flex-row flex-wrap gap-2\" : \"gap-2\"}\n onValueChange={(d) => d.value && onChange(d.value)}\n value={value}\n >\n {options.map((option) => (\n <RadioGroupCard\n className={\n specimen\n ? \"min-w-0 flex-1 basis-20 flex-col items-center gap-1 px-2 py-2\"\n : \"items-center px-2.5 py-2\"\n }\n key={option.value}\n value={option.value}\n >\n {specimen?.(option)}\n <ArkRadioGroup.ItemText\n className={\n specimen\n ? \"w-full truncate text-center text-muted-foreground text-xs\"\n : \"text-xs\"\n }\n >\n {option.label}\n </ArkRadioGroup.ItemText>\n </RadioGroupCard>\n ))}\n </RadioGroup>\n </PrefFieldSet>\n );\n}\n\n/**\n * Whatever the packages this host installed contribute — one group per declared preference.\n *\n * **Nothing here names a package.** The host registers manifests on the provider, the provider\n * resolves them, and this renders what it is handed; `@kanzo-tech/ui` gains no reference to\n * `@kanzo-tech/graph` and a host that never installed it passes nothing and draws nothing.\n *\n * Three things it declines to draw, each for a reason that belongs to the section rather than to the\n * panel: a preference a tenant **pinned** or **withheld** (`offered` is false, and the two are the\n * same instruction here for different reasons upstream), and a namespace whose manifest declares\n * only tokens. That last one is filtered in the provider, so an empty legend cannot reach the DOM.\n *\n * A contributed preference gets `RadioGroupCard` and not something new, because the choice it\n * expresses is the one `Colour` and `Density` already express — pick one of these, they have names.\n */\nexport interface PreferencesSectionsProps {\n /**\n * Draw one namespace instead of all of them.\n *\n * **This is what makes a preference one thing with several surfaces.** A panel wants every\n * contributed section; a dock inside a canvas wants the graph's and nothing else; a settings page\n * may want them under its own headings. Without it a surface that is not the panel has to\n * hand-roll the controls — which is how the workspace's dock came to hold its own copy of the\n * graph's appearance in React state, a second store for a preference the provider was already\n * resolving.\n *\n * The namespace is the section's own — `\"graph\"` — and an unknown one draws nothing rather than\n * throwing: a host that removed an optional package should lose a control, not a page.\n */\n namespace?: string;\n /**\n * Draw only these preferences, by name, in this order.\n *\n * The third selection, and the one a **dock** needs: a canvas has a panel for the forces and a\n * panel for the picture, and both are the same section. Without it the surface either draws the\n * whole section in one place or goes back to hand-rolling — which is the state this mechanism was\n * built to end, so a selection that stops at the namespace stops one step short.\n *\n * Order is the caller's here, where a section's own order is the manifest's. That is the same\n * split `PreferencesColor` already makes: what to offer belongs to whoever declared it, how to\n * arrange a page belongs to the page. A name nothing declares draws nothing.\n */\n only?: readonly string[];\n}\n\nfunction ContributedSections({ namespace, only }: PreferencesSectionsProps = {}) {\n const { sectionPrefs, setSectionPref, themes } = useKanzoTheme();\n\n // The one list only a tenant can write, in the shape a declaration names it by. Built here and\n // handed down rather than read inside the control, so the same control renders under a test that\n // has no provider.\n const sources: PrefSources = React.useMemo(\n () => ({ themes: themes.map(({ label, value }) => ({ label, value })) }),\n [themes],\n );\n\n const drawn = namespace\n ? Object.entries(sectionPrefs).filter(([name]) => name === namespace)\n : Object.entries(sectionPrefs);\n\n // The caller's order when it named the set, the manifest's when it did not. The entry type is\n // read off the context rather than re-declared: it is `ResolvedPref & { decl }`, and a second\n // spelling of it here would be one more thing to keep in step.\n type Entry = ThemeContextValue[\"sectionPrefs\"][string][string];\n const chosen = (prefs: Record<string, Entry>): [string, Entry][] =>\n only\n ? only.flatMap((key) => {\n const pref = prefs[key];\n return pref ? [[key, pref] as [string, Entry]] : [];\n })\n : Object.entries(prefs);\n\n return (\n <>\n {drawn.map(([name, prefs]) =>\n chosen(prefs).map(([key, pref]) =>\n pref.offered ? (\n <PrefControl\n key={`${name}.${key}`}\n name={key}\n onChange={(next) => setSectionPref(name, { [key]: next })}\n pref={pref}\n sources={sources}\n />\n ) : null,\n ),\n )}\n </>\n );\n}\n\n// The one section where neither container above applies. A slider is a single control, so it is not\n// a `FieldSet`; and Ark's `useSlider` reads no ambient context at all — not Field, not Fieldset — so\n// a `FieldLabel` could not reach it either, which is why \"Radius\" was written twice. `SliderLabel`\n// is the machine's own label part: zag points every thumb's `aria-labelledby` at it by default, so\n// the visible label IS the name and there is nothing to repeat.\n/**\n * Whether to draw a control for a core axis at all.\n *\n * A tenant may PIN one — their product is square, and nobody chooses otherwise — or WITHHOLD it,\n * and both mean the same thing to a surface. The resolution already ignores a stored value in\n * either case, so a control drawn here would be one that visibly does nothing.\n */\nconst useOffered = (key: string) => useKanzoTheme().corePrefs[key]?.offered !== false;\n\nfunction RadiusSection() {\n const { radius, set } = useKanzoTheme();\n const offered = useOffered(\"radius\");\n const index = Math.max(0, RADII.indexOf(radius));\n if (!offered) return null;\n return (\n <Slider\n min={0}\n max={RADII.length - 1}\n step={1}\n value={[index]}\n onValueChange={(d) => set({ radius: RADII[d.value[0] ?? 3] ?? \"md\" })}\n showMarkers\n markerLabels={[...RADII]}\n >\n <SliderLabel className={PREF_HEADING}>Radius</SliderLabel>\n </Slider>\n );\n}\n\n/**\n * The specimens a generic control cannot draw, keyed by axis — the escape hatch, in one place.\n *\n * Three, and they replaced three components that differed in nothing else: `FontSection`,\n * `MonoFontSection` and `DensitySection` each wrapped the same radio list around the same card\n * around a different `<span>`. What is left is the span.\n *\n * They take the theme because two of them do: a host may replace `fonts` with its own stacks, and a\n * specimen showing a face the page does not use is worse than no specimen.\n */\ntype Specimen = (option: PrefOption, theme: ThemeContextValue) => React.ReactNode;\n\nconst face = (options: FontOption[], value: string) =>\n options.find((option) => option.value === value)?.preview;\n\nconst SPECIMENS: Record<string, Specimen> = {\n font: (option, { fonts }) => (\n <span className=\"text-xl leading-none text-foreground\" style={{ fontFamily: face(fonts, option.value) }}>\n Ag\n </span>\n ),\n monoFont: (option, { monoFonts }) => (\n <span\n className=\"text-xl leading-none text-foreground\"\n style={{ fontFamily: face(monoFonts, option.value) }}\n >\n Ag\n </span>\n ),\n // The root font-size everything scales from, drawn at its real size — `em` inside a card whose\n // own `font-size` is set is a true preview rather than a description of one. The pixel values are\n // the generated ones: a hand-copy here would disagree with the CSS the moment the generator moves.\n density: (option) => (\n <span\n className=\"flex items-center gap-1 leading-none text-foreground\"\n style={{ fontSize: themeData.densities[option.value as keyof typeof themeData.densities] }}\n >\n <span className=\"rounded-[0.25em] bg-primary px-[0.4em] py-[0.15em] text-[0.7em] font-medium text-primary-foreground\">\n Aa\n </span>\n <span className=\"text-[0.8em]\">abc</span>\n </span>\n ),\n};\n\n/**\n * One core axis, drawn by the one renderer.\n *\n * Everything that used to differ between the four is data now: the name, the options and the kind\n * are the declaration's, the specimen is a lookup, and whether to draw at all is the chain's\n * answer. What a host sees is unchanged — this is the same markup those components\n * emitted, which `Preferences.test.tsx` checks by rendering rather than by reading the source.\n */\nfunction CoreSection({ axis }: { axis: CorePrefKey }) {\n const theme = useKanzoTheme();\n const pref = theme.corePrefs[axis];\n if (!pref?.offered) return null;\n const specimen = SPECIMENS[axis];\n return (\n <PrefControl\n name={axis}\n onChange={(next) => theme.set({ [axis]: next })}\n pref={pref}\n sources={theme.sources}\n {...(specimen ? { specimen: (option: PrefOption) => specimen(option, theme) } : {})}\n />\n );\n}\n\nconst FontSection = () => <CoreSection axis=\"font\" />;\nconst MonoFontSection = () => <CoreSection axis=\"monoFont\" />;\nconst DensitySection = () => <CoreSection axis=\"density\" />;\n\nexport interface PreferencesProps extends Omit<PreferencesRootProps, \"children\"> {\n /** Restyle or reposition the floating trigger (it is `fixed bottom-4 end-4` by default). */\n triggerClassName?: string;\n}\n\n/**\n * All-in-one: a floating trigger + the full drawer.\n *\n * Forwards Root's props and lets the trigger be restyled or repositioned — it used to take\n * none, so a consumer could neither move the FAB nor reach the hotkey.\n *\n * There is no `Preferences.Root` / `.Panel` / `.Density` namespace. It was built with\n * `Object.assign`, and those statics do NOT survive React Server Components: once the module\n * becomes a client reference, `Preferences.Density` reads back as `undefined` and React throws\n * \"Element type is invalid\". It was a broken API kept beside the working one — and it was the\n * single counter-example to \"no component exports dot-notation\".\n */\nexport function Preferences({ triggerClassName, ...rootProps }: PreferencesProps = {}) {\n return (\n <PreferencesRoot {...rootProps}>\n <PreferencesTrigger className={triggerClassName} />\n <PreferencesPanel />\n </PreferencesRoot>\n );\n}\n\n/**\n * The parts as flat named exports — the API. They cross the RSC boundary intact, tree-shake per\n * part, and match how every other compound in this library is exported (`DialogContent`, not\n * `Dialog.Content`).\n */\nexport {\n PreferencesRoot,\n PreferencesTrigger,\n PreferencesPanel,\n PrefField as PreferencesField,\n PrefFieldSet as PreferencesFieldSet,\n ColorSection as PreferencesColor,\n // Flat like every other section, and for the reason the others are: a host composing its own\n // panel with `children` replaces the canonical set, and without this it would silently drop every\n // choice its installed packages contribute — which is the several-products-in-one-window failure\n // the mechanism exists to remove, reintroduced by the escape hatch.\n ContributedSections as PreferencesSections,\n RadiusSection as PreferencesRadius,\n FontSection as PreferencesFont,\n MonoFontSection as PreferencesMonoFont,\n DensitySection as PreferencesDensity,\n};\n"],"names":["Dialog"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Preferences.js","sources":["../../src/composites/Preferences.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Dialog as ArkDialog } from \"@ark-ui/react/dialog\";\nimport { Portal } from \"@ark-ui/react/portal\";\nimport { InfoIcon, MoonIcon, PaletteIcon, SunIcon, XIcon } from \"lucide-react\";\n// Via the theme package's JS entry, not its raw `.json` subpath: a direct JSON subpath import\n// needs `with { type: \"json\" }` at runtime, and Rollup strips that attribute when bundling.\nimport {\n CORE_PREFS,\n prefBoolean,\n prefNumber,\n prefOptions,\n themeData,\n type Appearance,\n type CorePrefKey,\n type KanzoRadius,\n type PrefOption,\n type PrefSources,\n type SectionPrefDecl,\n} from \"@kanzo-tech/theme\";\nimport {\n useKanzoTheme,\n type FontOption,\n type ThemeContextValue,\n} from \"../theme/KanzoThemeProvider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { Alert, AlertDescription, AlertTitle } from \"../simples/alert.js\";\nimport { Badge } from \"../simples/badge.js\";\nimport { Button } from \"../simples/button.js\";\nimport { Field, FieldLabel, FieldLegend, FieldSet } from \"../simples/field.js\";\nimport {\n Dialog,\n DialogClose,\n DialogDescription,\n DialogTitle,\n DialogTrigger,\n} from \"../simples/dialog.js\";\nimport { RadioGroup as ArkRadioGroup } from \"@ark-ui/react/radio-group\";\nimport { RadioGroup, RadioGroupCard, RadioGroupLabel } from \"../simples/radio-group.js\";\nimport { Slider, SliderLabel } from \"../simples/slider.js\";\nimport { Switch } from \"../simples/switch.js\";\n\n/**\n * Preferences — a live theming selector (composite) for PRODUCT settings. A non-modal drawer\n * (metadata-form's UX) that drives {@link useKanzoTheme}, which writes `data-*` attributes to\n * `<html>` and toggles `.dark`, so every component re-skins with no changes. The default panel is\n * the canonical product set:\n *\n * <PreferencesRoot>\n * <PreferencesTrigger />\n * <PreferencesPanel>\n * <PreferencesColor /> <PreferencesDensity /> <PreferencesRadius />\n * <PreferencesFont /> <PreferencesMonoFont />\n * </PreferencesPanel>\n * </PreferencesRoot>\n *\n * or the all-in-one <Preferences />. Those five sections ARE the default panel body, plus a footer\n * of Reset · Done. Open with `t`, close with Escape.\n *\n * **No colour is AUTHORED here.** A tenant's identity is a palette DOCUMENT — derived and measured\n * once at onboarding, compiled to one stylesheet the server inlines — not something a user picks a\n * hue at a time. Palette, accent, base, chart scheme and the \"Copy CSS\" export all left with it;\n * what the export did belongs on the onboarding surface, which has a document to emit.\n *\n * `Colour` is not that coming back. It chooses among the things the TENANT published — the same kind\n * of choice `appearance` makes between one document's two modes, one level up — and it shows itself\n * only when there are two to choose from.\n *\n * **Appearance has no section of its own, and no control of its own either**: `Colour` draws one\n * card per side, and pressing a card wears that side. That IS the appearance control, and it is the\n * only one — an `AppearanceToggle` sat in the header until 2026-08-19 and was the same preference\n * wearing a second control twelve pixels from the first. It is deleted, not moved: it flipped\n * light ⇄ dark and so does a card, and \"follow the OS\" was never on it — that is `\"\"`, and Reset is\n * the way back.\n *\n * **What that costs, stated rather than discovered.** `Colour` hides itself below two published\n * choices, so a tenant that published one palette and one brand now has a panel with no appearance\n * control at all. That is a real hole and the reason this paragraph exists; the fix, if it is\n * wanted, is for the section to draw the two cards even where there is nothing to choose between —\n * not for the button to come back.\n *\n * It has two states and not three: \"follow the OS\" is `\"\"`, the absence of a pinned side, so the\n * way back to it is `Reset` — which UNSETS every preference rather than writing each default, and\n * therefore lands wherever the tenant's document says. That is the one thing this panel's footer\n * does that no other control can, and the declaration now offers the same value as an option.\n *\n * **What a tenant pinned or withheld is not drawn at all.** Every section asks `corePrefs[key]`\n * whether its axis is still offered, because a control the chain will ignore is a control that\n * visibly does nothing.\n *\n * **One renderer draws every preference**, the core's axes and a contributed section's alike:\n * `PrefControl` switches on `kind` and nothing else. Two sections are left, each because its\n * CONTROL differs rather than its data — `RadiusSection` is a slider over an ordered scale, and\n * `ColorSection` draws documents at full size with a live cascade preview. The three specimens a\n * generic control cannot draw are a lookup keyed by axis, not three components.\n */\n\n// Off the declaration, which is generated from the same table `themes.css` is emitted from. This\n// was typed here — beside a `themeData` import that already carried it — and a hand-copy disagrees\n// with the CSS the moment the generator changes. `?? []` is unreachable for a core axis (its\n// options are a literal list, never a source), and is how `prefOptions` says so in the type.\n//\n// It survives the collapse into one renderer because the radius control is a SLIDER and needs the\n// steps in order, as an array it can index. Every other axis reads its options off the declaration\n// at the point of drawing.\nconst RADII = (prefOptions(CORE_PREFS.radius) ?? []).map((o) => o.value as KanzoRadius);\n// ── Root: Ark Dialog (non-modal, live-preview) + hotkey ──────────────────────\nexport interface PreferencesRootProps {\n children: React.ReactNode;\n /**\n * Key that toggles the panel, e.g. `\"t\"`. **Opt-in — there is no default.**\n *\n * When set, this registers a `window` keydown listener that fires on the bare key (typing in\n * an input, textarea or contenteditable is ignored). A design system must not claim a\n * single, unmodified key in its host's global keymap without being asked, so omit this\n * unless the host has decided that key is free.\n */\n hotkey?: string;\n defaultOpen?: boolean;\n}\n\nfunction PreferencesRoot({ children, hotkey, defaultOpen = false }: PreferencesRootProps) {\n const [open, setOpen] = React.useState(defaultOpen);\n\n React.useEffect(() => {\n if (!hotkey) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.metaKey || e.ctrlKey || e.altKey || e.key.toLowerCase() !== hotkey) return;\n const el = document.activeElement;\n const typing =\n el instanceof HTMLElement &&\n (el.tagName === \"INPUT\" || el.tagName === \"TEXTAREA\" || el.isContentEditable);\n if (typing) return;\n e.preventDefault();\n setOpen((o) => !o);\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [hotkey]);\n\n // Non-modal so the app stays interactive and re-skins live behind the panel — no backdrop, no\n // scroll lock, no focus trap. But it still closes on an outside click, because that is what a\n // panel is expected to do and the alternative surprises people far more often than it helps.\n //\n // It used to pin `closeOnInteractOutside={false}` so you could click around the app and watch it\n // re-skin. That case survives: the hotkey reopens it where you left off, and the live preview was\n // never the reason to keep it open — every change applies on selection, not on close.\n //\n // `closeOnInteractOutside` is passed explicitly, and has to be. Zag derives its default from\n // `modal` (`closeOnInteractOutside: modal && !alertDialog` in dialog.machine.js), so a non-modal\n // dialog is non-dismissable *by default* — dropping the old explicit `false` changed nothing at\n // all. The two props look independent and are not.\n return (\n <Dialog\n open={open}\n onOpenChange={(e) => setOpen(e.open)}\n modal={false}\n closeOnInteractOutside={true}\n >\n {children}\n </Dialog>\n );\n}\n\n// ── Trigger: floating palette FAB ────────────────────────────────────────────\nfunction PreferencesTrigger({ className }: { className?: string }) {\n return (\n <DialogTrigger asChild>\n <Button\n type=\"button\"\n size=\"icon-md\"\n variant=\"outline\"\n aria-label=\"Theme preferences\"\n // `m-0`: a fixed FAB must not inherit a parent's flow spacing (`space-y-*`).\n // `bg-card`: `outline` is transparent by design, which reads as broken once the\n // button floats over arbitrary page content — a FAB needs an opaque surface.\n //\n // The `data-state` pair is load-bearing, not decoration. The panel is `modal={false}`, so\n // there is no backdrop dimming the page behind it — without an open state on the trigger,\n // nothing on screen says the panel is up. Ark's Dialog.Trigger already emits\n // `data-state=\"open\"`, so this needs no extra state.\n //\n // Deliberately NOT a `Toggle`: `Dialog.Trigger` gives `aria-expanded` +\n // `aria-haspopup=\"dialog\"`, which is the disclosure pattern this is. A toggle button\n // would report `aria-pressed` instead and drop the haspopup — worse semantics for a\n // control that reveals a panel. What was missing was the visual state, not the role.\n className={cn(\n \"fixed end-4 bottom-4 z-40 m-0 rounded-full bg-card shadow-lg\",\n \"data-[state=open]:bg-accent data-[state=open]:text-accent-foreground\",\n className,\n )}\n >\n <PaletteIcon />\n </Button>\n </DialogTrigger>\n );\n}\n\n// ── Panel: non-modal drawer pinned top-right (portaled Ark Dialog content) ────\nfunction PreferencesPanel({\n children,\n title = \"Preferences\",\n hint = \"Applied live · saved to this browser.\",\n}: {\n children?: React.ReactNode;\n title?: string;\n hint?: string;\n}) {\n return (\n <Portal>\n <ArkDialog.Positioner className=\"pointer-events-none fixed inset-0 z-50 flex items-start justify-end p-4\">\n <ArkDialog.Content\n data-slot=\"preferences-panel\"\n className={cn(\n // `w-96`, not `w-80`. The width was chosen when the panel held four axes and a colour\n // strip; it now holds five sections plus however many the packages a host installed\n // contribute, and it is the one surface here that grows with somebody else's decision.\n // The extra 64px is what lets a palette card depict a document rather than gesture at\n // one — see `ThemePreview`.\n \"pointer-events-auto relative flex max-h-[calc(100dvh-2rem)] w-96 flex-col overflow-hidden\",\n \"rounded-lg border border-border bg-popover text-popover-foreground shadow-xl\",\n \"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-right-4\",\n \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-right-4\",\n \"motion-reduce:animate-none!\",\n )}\n >\n {/* Header. Nothing but the title and the close X: a sun/moon toggle sat here, and it was\n the same preference wearing a second control twelve pixels from the first. `Colour`\n draws a card per side and pressing one wears it, which is the whole of what the\n button did — it cycled nothing, since \"follow the OS\" is `\"\"` and Reset is its way\n back. What that costs is stated on `ColorSection`: below two published choices the\n section returns null, and such a panel now offers no appearance control at all. */}\n <div className=\"px-5 pt-5 pb-3\">\n <DialogTitle className=\"font-heading text-base font-semibold\">{title}</DialogTitle>\n <DialogDescription className=\"mt-0.5 text-[length:var(--kanzo-font-size-small)] text-muted-foreground\">\n {hint}\n </DialogDescription>\n </div>\n <div className=\"absolute inset-e-3.5 top-3.5 flex items-center gap-0.5\">\n <DialogClose asChild>\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label=\"Close preferences\"\n className=\"opacity-64 hover:opacity-100\"\n >\n <XIcon />\n </Button>\n </DialogClose>\n </div>\n\n {/* Body — scrolls independently of the pinned header/footer.\n `[&>*]:shrink-0` is load-bearing, not hygiene. This is a COLUMN flex container\n inside a `max-h`-constrained panel, so its children are flex items that default\n to `flex-shrink: 1`. Once the sections are taller than the panel, the browser\n satisfies the constraint by SQUASHING every section rather than scrolling this\n box — sections collapse to a fraction of their height, their controls overlap\n and clip, and the lower ones become unreadable. That is the \"renders wrong /\n does not show all its fields\" bug: the panel was never scrolling at all. */}\n <form\n className=\"flex flex-1 flex-col gap-6 overflow-y-auto px-5 pb-5 [&>*]:shrink-0\"\n onSubmit={(e) => e.preventDefault()}\n >\n {children ?? (\n <>\n {/* Colour first, and ONE section: a palette and a brand are one choice at two\n grains, so the panel offers one list and everything below it is a different axis\n entirely. It renders nothing until the tenant published two choices, so the\n common panel is unchanged. */}\n <ColorSection />\n <DensitySection />\n <RadiusSection />\n <FontSection />\n <MonoFontSection />\n {/* Last, and in the same visual language as the five above. A package that owns a\n user-facing choice contributes it here rather than building a settings surface\n of its own beside this one — which is the whole difference between one product\n and several sharing a window. */}\n <ContributedSections />\n </>\n )}\n </form>\n\n {/* Footer — canonical Shark actions bar: top separator + muted surface. */}\n <PreferencesFooter />\n </ArkDialog.Content>\n </ArkDialog.Positioner>\n </Portal>\n );\n}\n\n/** Actions bar pinned to the bottom of the panel (Reset · Done). */\nfunction PreferencesFooter() {\n // `reset`, not `set({ ...DEFAULT_PREFS })`, which is what this was. Storage holds what a user\n // CHOSE, so spreading the defaults would write each axis explicitly — making Reset the one act\n // that pins somebody against their tenant's document. Unsetting lands wherever the chain says.\n const { reset } = useKanzoTheme();\n // No fill on the bar. The panel is `bg-popover`, and in dark `--popover` and `--muted` are the\n // same value, so a 48% muted wash composited to ΔE ~0 — the background contributed nothing and\n // the `border-t` was doing all the work. Same defect as the command, popover and dialog footers;\n // this one survived that sweep by living in `composites/`.\n return (\n <div className=\"flex items-center gap-2 border-t border-border px-4 py-3\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={reset}>\n Reset\n </Button>\n <DialogClose asChild>\n <Button type=\"button\" size=\"sm\" className=\"ms-auto\">\n Done\n </Button>\n </DialogClose>\n </div>\n );\n}\n\n// ── Section building blocks ──────────────────────────────────────────────────\n/**\n * A titled preferences row. This used to be a bare `<div>` plus a hand-rolled `GroupTitle` —\n * fixed typography, no `htmlFor`, no association with the control it titled, and in the\n * segmented cases the label was written TWICE (once visibly, once as `aria-label`).\n *\n * `Field` is precisely this, wired: it generates the id, connects the label to the control and\n * carries the state by context. The forms guide says so; the library's own flagship composite\n * was the one place not taking its own advice.\n */\nfunction PrefField({ label, children }: { label: React.ReactNode; children: React.ReactNode }) {\n return (\n <Field>\n <FieldLabel className={PREF_HEADING}>{label}</FieldLabel>\n {children}\n </Field>\n );\n}\n\n/**\n * ONE spelling for every heading this panel draws, and the reason it is a class rather than a\n * component: each machine dictates the ELEMENT its name lives on — a `<legend>` for a group, a\n * `<label>` for a single control, the slider's own label part, a plain `<span>` where a legend\n * would capture two radio groups at once. A `PreferencesHeading` component could only render one\n * of those, so what the four have in common is exactly this string and nothing else.\n *\n * Two details are load-bearing rather than taste. The size is `!` because `FieldLegend` sets its\n * own through a `data-[variant=…]:` selector, which outranks a plain class whatever the source\n * order — a non-important override there silently does nothing. And `block` is what makes `mb-2`\n * mean anything on `SliderLabel`, which is inline by default: the slider's heading sat one gap\n * tighter than the others for exactly that reason, and three headings at three spacings read as\n * three ranks once they share a grid.\n */\nconst PREF_HEADING = cn(\n \"mb-2 block font-medium uppercase tracking-wide text-muted-foreground\",\n \"text-[length:var(--kanzo-font-size-small)]!\",\n);\n\n/** The side cards' own titles — a card title, not a section heading, so only the size is shared. */\nconst PREF_LABEL_SIZE = \"text-[length:var(--kanzo-font-size-small)]\";\n\n/**\n * A titled GROUP of options — the container `PrefField` cannot be.\n *\n * `Field` addresses one control, and a radio group has one hidden input per item, so there is no\n * id for its label to point at; `radio-group.tsx` says exactly this, and says `FieldSet` +\n * `FieldLegend` is the answer.\n *\n * The legend really is the name, not a decoration beside one: Ark's `useRadioGroup` passes\n * `ids: { label: fieldset.ids.legend }` into the machine, and zag's root props set\n * `aria-labelledby` from it.\n */\nfunction PrefFieldSet({ label, children }: { label: React.ReactNode; children: React.ReactNode }) {\n return (\n <FieldSet className=\"gap-2\">\n <FieldLegend className={PREF_HEADING}>{label}</FieldLegend>\n {children}\n </FieldSet>\n );\n}\n\n// ── Sections ──────────────────────────────────────────────────────────────────\n/**\n * COLOUR — every choice the tenant published, as one list.\n *\n * **A palette and an identity are one abstraction with a parameter: how much of the document the\n * choice replaces.** An identity replaces the brand-derived slice and inherits every surface; a\n * palette replaces all of it. They always shared this control, this option type, the hide-below-two\n * rule and the retirement machinery — and the giveaway was the behaviour: changing palette *files and\n * restores* the identity, which is what containment does and what two sibling axes never would.\n *\n * So the panel shows one list. A palette that publishes several brands contributes one entry per\n * brand — `Bank · Retail`, `Bank · Private` — which is how VS Code and Slack present variants, and\n * which matches what the user is actually doing: making one choice. The model keeps the containment,\n * because that is what guarantees a tenant's brands share a neutral and stay one product.\n *\n * The prefix appears only when there is more than one palette to disambiguate against: a client with\n * a single palette and two brands sees `Retail` and `Private`, not their own name twice.\n */\nexport interface PreferencesColorProps {\n /** The legend, and one of the section's two library-authored strings (i18n). */\n label?: string;\n /** Heading of the notice shown when the tenant withdrew what this user had chosen. */\n retiredTitle?: string;\n /** Compose that notice's body. The argument is the retired **id**; its label went with it. */\n formatRetired?: (parts: { choice: string }) => string;\n /** Name a side. The section's third and last library-authored string (i18n). */\n formatSide?: (side: Appearance) => string;\n}\n\nconst DEFAULT_RETIRED_TITLE = \"Colours updated\";\nconst DEFAULT_RETIRED = ({ choice }: { choice: string }) =>\n `The colours you had chosen (${choice}) are no longer published, so these are the default ones.`;\n\n/**\n * One theme, drawn by ITSELF — a miniature of the interface rather than a list of its hexes.\n *\n * **Nothing here is data.** The theme is already in the page under its own `[data-theme]`, so this\n * span sets the attribute and every utility inside it resolves against that theme: `bg-primary` is\n * that tenant's brand, `bg-chart-3` is their third categorical slot. A strip of four hexes was the\n * alternative and it could not depict a theme — on the default's own, `--primary` and `--foreground`\n * are the same value, so two of the four chips were one colour and the control said nothing.\n *\n * **Three things this used to need and no longer does**, and they are the same three the whole\n * refactor removed:\n *\n * · *The appearance class.* A document carried two blocks, so a preview had to force a side with\n * `.light` / `.dark` or it would take the wrong one on a dark page. A theme IS a side.\n * · *A second attribute.* `data-identity` selected the brand within the document. A brand is a\n * theme, so there is one attribute.\n * · *The default's special case.* The default document was emitted at bare `:root` with no scope of\n * its own, so `[data-palette=\"kanzo\"]` matched nothing and the cell inherited whatever the page\n * wore — the class alone had to carry it. `themes/kanzo.css` answers to `:root` **and**\n * `[data-theme=\"kanzo\"]`, so the default selects like every other theme.\n *\n * The categorical strip draws all eight slots on purpose. Past a theme's `--chart-capacity` the\n * slot is `var(--muted-foreground)`, so a set that holds seven says so by going grey at the end.\n */\nfunction ThemePreview({ theme }: { theme: string }) {\n return (\n <span\n aria-hidden\n className={cn(\n // `h-16` at every width, tried and reverted at `@md:h-28`: the miniature's three rows are a\n // fixed amount of ink, so height bought empty background and the tile read as a page that\n // had failed to load rather than as a denser one. GitHub's tile is 2:1 because it is a\n // screenshot with a screenshot's worth of content in it.\n \"flex h-16 flex-col justify-between rounded-[4px] border border-border bg-background p-2\",\n )}\n data-slot=\"theme-preview\"\n data-theme={theme || undefined}\n >\n {/* Chrome: the brand fill, and two weights of ink on the page. */}\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-4 rounded-[2px] bg-primary\" />\n <span className=\"h-1 w-5 rounded-full bg-foreground\" />\n <span className=\"h-1 flex-1 rounded-full bg-muted-foreground\" />\n </span>\n\n {/* Two lines of code, indented — the half that makes the card true rather than decorative.\n A document derives seven syntax roles of its own, so Dracula's card paints Dracula's\n keywords; a card that stopped at surfaces and a brand would show six documents agreeing\n about the only part of themselves they share. This is the move GitHub's theme picker\n makes, and the reason it is the reference: the tile shows the thing being themed. */}\n <span className=\"flex flex-col gap-[3px] ps-1\">\n <span className=\"flex items-center gap-[3px]\">\n <span className=\"h-1 w-3 rounded-full bg-[var(--syntax-keyword)]\" />\n <span className=\"h-1 w-5 rounded-full bg-[var(--syntax-function)]\" />\n <span className=\"h-1 w-2 rounded-full bg-[var(--syntax-number)]\" />\n </span>\n <span className=\"flex items-center gap-[3px] ps-2\">\n <span className=\"h-1 w-4 rounded-full bg-[var(--syntax-property)]\" />\n <span className=\"h-1 w-6 rounded-full bg-[var(--syntax-string)]\" />\n <span className=\"h-1 w-2.5 rounded-full bg-[var(--syntax-type)]\" />\n </span>\n </span>\n\n {/* The categorical set, all eight slots. Past a document's capacity `compile` writes\n `var(--muted-foreground)`, so a set that holds seven says so by going grey at the end. */}\n <span className=\"flex items-center gap-px\">\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-1\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-2\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-3\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-4\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-5\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-6\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-7\" />\n <span className=\"h-1.5 flex-1 rounded-[1px] bg-chart-8\" />\n </span>\n </span>\n );\n}\n\n/**\n * Wear a palette while the pointer is on it, and put back exactly what was there.\n *\n * This is the reference behaviour — VS Code and Zed preview a theme as you arrow through the list,\n * and revert when you leave — and it is affordable here for one reason: every document the tenant\n * publishes is already in the page, so previewing is two attribute writes and no fetch, no\n * stylesheet swap and no re-render.\n *\n * **It restores a snapshot rather than recomputing what should be there.** The provider removes an\n * axis attribute at its default and sets it otherwise, so \"put it back\" has two spellings depending\n * on preferences this component would have to read and agree with. Reading the element on the way\n * in cannot disagree with anything.\n */\nfunction useThemePreview() {\n const held = React.useRef<{ theme: string | null } | null>(null);\n\n const restore = React.useCallback(() => {\n const snapshot = held.current;\n if (!snapshot) return;\n held.current = null;\n const el = document.documentElement;\n if (snapshot.theme === null) el.removeAttribute(\"data-theme\");\n else el.setAttribute(\"data-theme\", snapshot.theme);\n }, []);\n\n const preview = React.useCallback((theme: string) => {\n const el = document.documentElement;\n // Only the FIRST entry into the list snapshots. Moving from one card to the next fires leave\n // and enter in an order the pointer decides, and re-snapshotting mid-sweep would file the theme\n // being previewed as the one to go back to.\n held.current ??= { theme: el.getAttribute(\"data-theme\") };\n if (theme) el.setAttribute(\"data-theme\", theme);\n else el.removeAttribute(\"data-theme\");\n }, []);\n\n // A panel closed mid-preview — Escape, a click on the trigger — never fires the leave handler,\n // and would leave the reader wearing a theme they did not choose.\n React.useEffect(() => restore, [restore]);\n\n return { preview, restore };\n}\n\n/** The two sides, in the order a settings page reads them. */\nconst APPEARANCES: Appearance[] = [\"light\", \"dark\"];\n\n/** `light` / `dark`, and the only two strings this section authors. */\nconst DEFAULT_SIDE_LABEL = (side: Appearance) => (side === \"light\" ? \"Light\" : \"Dark\");\n\ntype Entry = { key: string; label: string };\n\n/**\n * One side of the choice — which document this user wears in light, or in dark.\n *\n * **The miniature is painted with the document it is offering, forced to this side.** That is what\n * `compile`'s scoped selectors were designed for and what GitHub's own picker cannot do: their\n * tiles are drawn assets, ours is the real cascade answering. A card for the side you are not\n * currently in therefore shows what you would get, not a tinted guess.\n *\n * The chips are the palettes, and they are the same control as the card above them rather than a\n * second one: one radio group per side, so the two cards never fight over a single selection.\n *\n * **The tile IS the appearance control.** It was a `Use` button beside the title, which is the\n * failure this panel keeps removing everywhere else: a second control for a preference the surface\n * was already depicting. What you want is on screen at full size, so pressing it is the whole\n * gesture — the reference is GitHub's Appearance page, where the state is a bordered card and an\n * `Active` pill and there is no verb anywhere on it.\n *\n * A pressable region is a **button inside the container**, never the container merged with one:\n * `item.tsx` states the rule and this is the same case, forced twice over. The card is the radio\n * group's root (`role=\"radiogroup\"`), and the chips are `<label>`s — a button wrapping them would\n * be interactive content inside interactive content, and merging the roles would lose one of them.\n */\nfunction SideCard({\n entries,\n formatSide,\n live,\n onPreview,\n onRestore,\n selectedFor,\n setAppearance,\n setTheme,\n side,\n}: {\n entries: Entry[];\n formatSide: (side: Appearance) => string;\n /** Whether this is the side currently applied. Only then does hovering preview anything. */\n live: boolean;\n onPreview: (theme: string) => void;\n onRestore: () => void;\n resolvedTheme: string;\n selectedFor: (side: Appearance) => string;\n setAppearance: (appearance: Appearance) => void;\n setTheme: (theme: string, options?: { appearance?: Appearance }) => void;\n side: Appearance;\n}) {\n const selected = selectedFor(side);\n const current = entries.find((entry) => entry.key === selected) ?? entries[0];\n const Icon = side === \"light\" ? SunIcon : MoonIcon;\n\n return (\n // The CARD is the radio group, so it is named by the machine's own label part rather than\n // inheriting the fieldset's legend. Two groups under one legend both answered to \"Colour\", which\n // is a screen reader hearing the same name twice with no way to tell the sides apart — and an\n // `aria-label` could not fix it, because zag points `aria-labelledby` at the legend and that\n // wins. The Ark tie-break at `/docs/design/references` is the rule; this is the case.\n <RadioGroup\n className={cn(\n \"flex flex-col gap-2 rounded-lg border p-2.5 transition-colors\",\n // Selected the way `RadioGroupCard` spells selected, minus its `bg-primary/17`: the chips\n // inside are radio cards too, and a checked chip on a tinted card is the same wash twice\n // with only a border left to tell them apart. A ring buys the same emphasis and leaves the\n // fill for the control that has nothing else.\n live ? \"border-primary ring-1 ring-primary\" : \"border-border\",\n )}\n onBlur={onRestore}\n onPointerLeave={onRestore}\n onValueChange={(d) => {\n if (!d.value) return;\n onRestore();\n setTheme(d.value, { appearance: side });\n }}\n slot=\"preferences-side-card\"\n value={selected}\n >\n {/* Which side is applied right now, and it says so in a WORD: the border and the ring are\n colour, and colour cannot be the only thing carrying a state (1.4.1). Absolutely placed\n rather than in the header row, so it stays out of the button's accessible name — inside,\n every tile answered to \"Light Active\" — and `pointer-events-none` so the corner it covers\n still presses the tile. The root is `position: relative` already; zag sets it. */}\n {live ? (\n <Badge className=\"pointer-events-none absolute end-2.5 top-2.5\" variant=\"info\">\n Active\n </Badge>\n ) : null}\n\n {/* The tile: the side's name, and the document it would paint. `aria-pressed` and not a\n `role=\"radio\"` hand-rolled across two sibling groups — two toggles, each reporting its own\n state, is what a screen reader can follow here.\n\n `RadioGroupLabel` renders through `asChild` onto the span, so the machine's own label part\n IS the visible title (zag's label props carry no `htmlFor`, so a span is a legal host) and\n the group's accessible name is the word already on screen. Writing it twice — once for the\n eye, once in an `aria-label` — is the duplication this file removes everywhere else. */}\n <button\n aria-pressed={live}\n className={cn(\n \"-m-1 flex cursor-pointer flex-col gap-2 rounded-md p-1 text-start transition-colors\",\n \"hover:bg-foreground/14\",\n \"outline-none focus-visible:ring-[3px] focus-visible:ring-ring\",\n )}\n onClick={() => setAppearance(side)}\n type=\"button\"\n >\n {/* `pointer-events-none` because zag's label props carry `onClick: focus`, which is right\n when a label sits BESIDE its group and wrong when it sits inside a button: clicking the\n word `Light` pressed the tile and then moved focus into the chips, so where you clicked\n decided what happened. The label is text; the tile takes the click. */}\n <RadioGroupLabel asChild>\n <span className=\"pointer-events-none flex items-center gap-1.5\">\n <Icon className=\"size-3.5 text-muted-foreground\" />\n <span className={cn(PREF_LABEL_SIZE, \"font-medium\")}>{formatSide(side)}</span>\n </span>\n </RadioGroupLabel>\n\n <ThemePreview theme={current?.key ?? \"\"} />\n </button>\n\n {/* The chosen document's name — under the tile, with the chips it belongs to rather than\n inside the button, where it would have joined the tile's accessible name. */}\n <span className=\"truncate text-muted-foreground text-xs\">{current?.label}</span>\n\n <span className=\"flex flex-wrap gap-1\">\n {entries.map((entry) => (\n <RadioGroupCard\n // A chip, not a card: the depiction is one dot and the name is the accessible name.\n className=\"size-6 items-center justify-center rounded-full p-0\"\n key={entry.key}\n // Hovering only previews on the side being worn. Previewing the other one would repaint\n // the page into a mode the reader did not ask for, which is a worse lie than no preview.\n onFocus={live ? () => onPreview(entry.key) : undefined}\n onPointerEnter={live ? () => onPreview(entry.key) : undefined}\n title={entry.label}\n value={entry.key}\n >\n <span\n aria-hidden\n className=\"size-3.5 rounded-full bg-primary ring-1 ring-border\"\n data-slot=\"theme-chip\"\n data-theme={entry.key || undefined}\n />\n <ArkRadioGroup.ItemText className=\"sr-only\">{entry.label}</ArkRadioGroup.ItemText>\n </RadioGroupCard>\n ))}\n </span>\n </RadioGroup>\n );\n}\n\nfunction ColorSection({\n label = \"Colour\",\n retiredTitle = DEFAULT_RETIRED_TITLE,\n formatRetired = DEFAULT_RETIRED,\n formatSide = DEFAULT_SIDE_LABEL,\n}: PreferencesColorProps = {}) {\n const {\n corePrefs,\n defaultThemeFor,\n resolvedAppearance,\n resolvedTheme,\n retiredTheme,\n setAppearance,\n setTheme,\n themeByAppearance,\n themes,\n } = useKanzoTheme();\n const { preview, restore } = useThemePreview();\n\n // What the TENANT still lets this user choose. A pinned theme leaves exactly one to offer, so the\n // section disappears — which is the white-label case working, not a control failing. The\n // fall out of the existing `entries.length < 2` rule rather than adding a second way to hide.\n const offered = corePrefs.themeByAppearance?.offered === false\n ? themes.filter((theme) => theme.value === resolvedTheme)\n : themes;\n\n // **One entry per theme, and that is the whole of this now.** It used to be a flatMap over\n // documents that expanded each into its brands, prefixing labels when more than one document was\n // offered, and computing a `scope` because the default document had no selector of its own. A\n // brand is a theme and every theme selects the same way, so the list is the list.\n const entries: Entry[] = offered.map((theme) => ({ key: theme.value, label: theme.label }));\n\n if (entries.length < 2) return null;\n\n const retired = retiredTheme;\n\n /**\n * Which entry a side is wearing.\n *\n * For the applied side this is the RESOLVED theme, never the preference: an empty preference is a\n * deferral to the tenant, and the entry that reads as checked has to be the one on screen. For the\n * other side there is nothing on screen to agree with, so it falls back the same way the provider\n * would — the stored value, or the tenant's default.\n */\n const selectedFor = (side: Appearance) =>\n (side === resolvedAppearance ? resolvedTheme : themeByAppearance[side]) || defaultThemeFor(side);\n\n return (\n // A plain heading, not `PrefFieldSet`, and that is forced rather than chosen. Ark's\n // `useRadioGroup` takes its `ids.label` from an ambient fieldset's legend and zag sets\n // `aria-labelledby` from it, so inside one every group answers to \"Colour\" and the label part\n // cannot win. Two groups live here, so the section keeps a heading and each card carries its\n // own name.\n <div className=\"flex flex-col gap-2\">\n <span className={PREF_HEADING}>{label}</span>\n {/* Two sibling cards, one per side — GitHub's Appearance page, whose move this borrows: the\n tile shows the thing being themed rather than naming it.\n \n The semantics ARE theirs now, and that is new. GitHub pairs a day theme with a night theme\n because its themes are single-mode; ours used to each carry both, so a card meant *which\n document this user wears on the light side*. A theme is one mode, so a card is the light\n theme, and the per-appearance palette question closed by becoming the\n obvious shape rather than by being argued.\n \n A container query rather than a media query, because the question is how much room THIS\n section was given, not how big the window is. The same markup is one column inside a\n 384px drawer and two on a settings page, which is the whole claim that a section is\n independent of the surface that hosts it. */}\n <div className=\"@container\">\n <div className=\"grid gap-2 @md:grid-cols-2\">\n {APPEARANCES.map((side) => (\n <SideCard\n entries={entries}\n formatSide={formatSide}\n key={side}\n live={side === resolvedAppearance}\n onPreview={preview}\n onRestore={restore}\n resolvedTheme={resolvedTheme}\n selectedFor={selectedFor}\n setAppearance={setAppearance}\n setTheme={setTheme}\n side={side}\n />\n ))}\n </div>\n </div>\n {/* One notice for both, because there is one choice: whichever half the tenant withdrew, what\n the user lost is the colours they picked. No toast — a document is served, so the page they\n are reading is already the default one and nothing is about to change under them. */}\n {retired ? (\n <Alert variant=\"info\">\n <InfoIcon />\n <AlertTitle>{retiredTitle}</AlertTitle>\n <AlertDescription>{formatRetired({ choice: retired })}</AlertDescription>\n </Alert>\n ) : null}\n </div>\n );\n}\n\n/**\n * One declared preference, drawn — the switch on `kind`, in one place.\n *\n * **It draws the core's axes and a contributed section's alike**, which is the point: `Font`,\n * `Mono font` and `Density` were three components that differed only in which specimen they put on\n * a card, and a fourth surface rendering a contributed group had to re-decide what a `range` looks\n * like. The three arms are the primitives the panel already used: `choice` is a radio list,\n * `toggle` is `Switch`, `range` is `Slider`.\n *\n * A value is a string in storage for all three — see `SectionPrefDecl` — so each arm parses on the\n * way in with the section mechanism's own readers rather than a local `Number()` that would differ\n * from what the resolver validated against.\n *\n * `specimen` is the escape hatch, and it is the only one: a generic control cannot draw a typeface\n * in its own face or a size at its real size. What it may not do is change the CONTROL — a\n * declaration that needs a different one is a section of its own, and there are two of those left\n * ({@link RadiusSection}, {@link ColorSection}), each saying why in its own comment.\n */\nfunction PrefControl({\n name,\n onChange,\n pref,\n sources,\n specimen,\n}: {\n name: string;\n onChange: (next: string) => void;\n pref: { value: string; decl: SectionPrefDecl };\n /** What the tenant published, for a choice whose options name a source. */\n sources?: PrefSources;\n /** Drawn above each option's name. Its presence is also what lays the cards out in a row. */\n specimen?: (option: PrefOption) => React.ReactNode;\n}) {\n const { decl, value } = pref;\n // The declaration's name, and the key when it has none — which is what every surface drew before\n // a preference could carry one, and is still what an older manifest gets.\n const title = decl.label ?? name;\n\n if (decl.kind === \"toggle\") {\n // `Field` and nothing else, because Ark's Switch **does** read the ambient field context — its\n // hidden input comes out carrying `aria-labelledby=\"field::…::label\"`. That is the opposite of\n // `useSlider`, which reads none, and the difference is why `RadiusSection` needs the machine's\n // own label part and this does not. An `aria-label` here was tried and is exactly the\n // duplication this panel keeps removing: it lands on the `<label>` root, which has no role, so\n // it names nothing and hides that the wiring was already correct.\n //\n // The control's role is `checkbox`, not `switch`: Ark renders a hidden `input type=\"checkbox\"`\n // and does not set `role=\"switch\"` on it. Upstream's call, adopted verbatim.\n return (\n <PrefField label={title}>\n <Switch\n checked={prefBoolean(value)}\n onCheckedChange={(d) => onChange(String(d.checked === true))}\n />\n </PrefField>\n );\n }\n\n if (decl.kind === \"range\") {\n // `SliderLabel` is the machine's own label part — zag points every thumb's `aria-labelledby` at\n // it — so the visible label IS the name, the way `RadiusSection` does it.\n return (\n <Slider\n max={decl.max}\n min={decl.min}\n onValueChange={(d) => onChange(String(d.value[0] ?? decl.min))}\n step={decl.step}\n value={[prefNumber(value, decl)]}\n >\n <SliderLabel className={PREF_HEADING}>{title}</SliderLabel>\n </Slider>\n );\n }\n\n // `?? []` and not a throw: a choice whose source the host has not answered yet has nothing to\n // offer *this render*, and the value it resolved to is still applied. Drawing an empty group is\n // what `PreferencesColor` already does below two published documents.\n const options = prefOptions(decl, sources) ?? [];\n // A specimen is wide and wants its name under it; a bare name is a row in a list. One rule, read\n // off the data, rather than a layout prop each call site has to remember to pass.\n return (\n <PrefFieldSet label={title}>\n <RadioGroup\n className={specimen ? \"flex-row flex-wrap gap-2\" : \"gap-2\"}\n onValueChange={(d) => d.value && onChange(d.value)}\n value={value}\n >\n {options.map((option) => (\n <RadioGroupCard\n className={\n specimen\n ? \"min-w-0 flex-1 basis-20 flex-col items-center gap-1 px-2 py-2\"\n : \"items-center px-2.5 py-2\"\n }\n key={option.value}\n value={option.value}\n >\n {specimen?.(option)}\n <ArkRadioGroup.ItemText\n className={\n specimen\n ? \"w-full truncate text-center text-muted-foreground text-xs\"\n : \"text-xs\"\n }\n >\n {option.label}\n </ArkRadioGroup.ItemText>\n </RadioGroupCard>\n ))}\n </RadioGroup>\n </PrefFieldSet>\n );\n}\n\n/**\n * Whatever the packages this host installed contribute — one group per declared preference.\n *\n * **Nothing here names a package.** The host registers manifests on the provider, the provider\n * resolves them, and this renders what it is handed; `@kanzo-tech/ui` gains no reference to\n * `@kanzo-tech/graph` and a host that never installed it passes nothing and draws nothing.\n *\n * Three things it declines to draw, each for a reason that belongs to the section rather than to the\n * panel: a preference a tenant **pinned** or **withheld** (`offered` is false, and the two are the\n * same instruction here for different reasons upstream), and a namespace whose manifest declares\n * only tokens. That last one is filtered in the provider, so an empty legend cannot reach the DOM.\n *\n * A contributed preference gets `RadioGroupCard` and not something new, because the choice it\n * expresses is the one `Colour` and `Density` already express — pick one of these, they have names.\n */\nexport interface PreferencesSectionsProps {\n /**\n * Draw one namespace instead of all of them.\n *\n * **This is what makes a preference one thing with several surfaces.** A panel wants every\n * contributed section; a dock inside a canvas wants the graph's and nothing else; a settings page\n * may want them under its own headings. Without it a surface that is not the panel has to\n * hand-roll the controls — which is how the workspace's dock came to hold its own copy of the\n * graph's appearance in React state, a second store for a preference the provider was already\n * resolving.\n *\n * The namespace is the section's own — `\"graph\"` — and an unknown one draws nothing rather than\n * throwing: a host that removed an optional package should lose a control, not a page.\n */\n namespace?: string;\n /**\n * Draw only these preferences, by name, in this order.\n *\n * The third selection, and the one a **dock** needs: a canvas has a panel for the forces and a\n * panel for the picture, and both are the same section. Without it the surface either draws the\n * whole section in one place or goes back to hand-rolling — which is the state this mechanism was\n * built to end, so a selection that stops at the namespace stops one step short.\n *\n * Order is the caller's here, where a section's own order is the manifest's. That is the same\n * split `PreferencesColor` already makes: what to offer belongs to whoever declared it, how to\n * arrange a page belongs to the page. A name nothing declares draws nothing.\n */\n only?: readonly string[];\n}\n\nfunction ContributedSections({ namespace, only }: PreferencesSectionsProps = {}) {\n const { sectionPrefs, setSectionPref, themes } = useKanzoTheme();\n\n // The one list only a tenant can write, in the shape a declaration names it by. Built here and\n // handed down rather than read inside the control, so the same control renders under a test that\n // has no provider.\n const sources: PrefSources = React.useMemo(\n () => ({ themes: themes.map(({ label, value }) => ({ label, value })) }),\n [themes],\n );\n\n const drawn = namespace\n ? Object.entries(sectionPrefs).filter(([name]) => name === namespace)\n : Object.entries(sectionPrefs);\n\n // The caller's order when it named the set, the manifest's when it did not. The entry type is\n // read off the context rather than re-declared: it is `ResolvedPref & { decl }`, and a second\n // spelling of it here would be one more thing to keep in step.\n type Entry = ThemeContextValue[\"sectionPrefs\"][string][string];\n const chosen = (prefs: Record<string, Entry>): [string, Entry][] =>\n only\n ? only.flatMap((key) => {\n const pref = prefs[key];\n return pref ? [[key, pref] as [string, Entry]] : [];\n })\n : Object.entries(prefs);\n\n return (\n <>\n {drawn.map(([name, prefs]) =>\n chosen(prefs).map(([key, pref]) =>\n pref.offered ? (\n <PrefControl\n key={`${name}.${key}`}\n name={key}\n onChange={(next) => setSectionPref(name, { [key]: next })}\n pref={pref}\n sources={sources}\n />\n ) : null,\n ),\n )}\n </>\n );\n}\n\n// The one section where neither container above applies. A slider is a single control, so it is not\n// a `FieldSet`; and Ark's `useSlider` reads no ambient context at all — not Field, not Fieldset — so\n// a `FieldLabel` could not reach it either, which is why \"Radius\" was written twice. `SliderLabel`\n// is the machine's own label part: zag points every thumb's `aria-labelledby` at it by default, so\n// the visible label IS the name and there is nothing to repeat.\n/**\n * Whether to draw a control for a core axis at all.\n *\n * A tenant may PIN one — their product is square, and nobody chooses otherwise — or WITHHOLD it,\n * and both mean the same thing to a surface. The resolution already ignores a stored value in\n * either case, so a control drawn here would be one that visibly does nothing.\n */\nconst useOffered = (key: string) => useKanzoTheme().corePrefs[key]?.offered !== false;\n\nfunction RadiusSection() {\n const { radius, set } = useKanzoTheme();\n const offered = useOffered(\"radius\");\n const index = Math.max(0, RADII.indexOf(radius));\n if (!offered) return null;\n return (\n <Slider\n min={0}\n max={RADII.length - 1}\n step={1}\n value={[index]}\n onValueChange={(d) => set({ radius: RADII[d.value[0] ?? 3] ?? \"md\" })}\n showMarkers\n markerLabels={[...RADII]}\n >\n <SliderLabel className={PREF_HEADING}>Radius</SliderLabel>\n </Slider>\n );\n}\n\n/**\n * The specimens a generic control cannot draw, keyed by axis — the escape hatch, in one place.\n *\n * Three, and they replaced three components that differed in nothing else: `FontSection`,\n * `MonoFontSection` and `DensitySection` each wrapped the same radio list around the same card\n * around a different `<span>`. What is left is the span.\n *\n * They take the theme because two of them do: a host may replace `fonts` with its own stacks, and a\n * specimen showing a face the page does not use is worse than no specimen.\n */\ntype Specimen = (option: PrefOption, theme: ThemeContextValue) => React.ReactNode;\n\nconst face = (options: FontOption[], value: string) =>\n options.find((option) => option.value === value)?.preview;\n\nconst SPECIMENS: Record<string, Specimen> = {\n font: (option, { fonts }) => (\n <span className=\"text-xl leading-none text-foreground\" style={{ fontFamily: face(fonts, option.value) }}>\n Ag\n </span>\n ),\n monoFont: (option, { monoFonts }) => (\n <span\n className=\"text-xl leading-none text-foreground\"\n style={{ fontFamily: face(monoFonts, option.value) }}\n >\n Ag\n </span>\n ),\n // The root font-size everything scales from, drawn at its real size — `em` inside a card whose\n // own `font-size` is set is a true preview rather than a description of one. The pixel values are\n // the generated ones: a hand-copy here would disagree with the CSS the moment the generator moves.\n density: (option) => (\n <span\n className=\"flex items-center gap-1 leading-none text-foreground\"\n style={{ fontSize: themeData.densities[option.value as keyof typeof themeData.densities] }}\n >\n <span className=\"rounded-[0.25em] bg-primary px-[0.4em] py-[0.15em] text-[0.7em] font-medium text-primary-foreground\">\n Aa\n </span>\n <span className=\"text-[0.8em]\">abc</span>\n </span>\n ),\n};\n\n/**\n * One core axis, drawn by the one renderer.\n *\n * Everything that used to differ between the four is data now: the name, the options and the kind\n * are the declaration's, the specimen is a lookup, and whether to draw at all is the chain's\n * answer. What a host sees is unchanged — this is the same markup those components\n * emitted, which `Preferences.test.tsx` checks by rendering rather than by reading the source.\n */\nfunction CoreSection({ axis }: { axis: CorePrefKey }) {\n const theme = useKanzoTheme();\n const pref = theme.corePrefs[axis];\n if (!pref?.offered) return null;\n const specimen = SPECIMENS[axis];\n return (\n <PrefControl\n name={axis}\n onChange={(next) => theme.set({ [axis]: next })}\n pref={pref}\n sources={theme.sources}\n {...(specimen ? { specimen: (option: PrefOption) => specimen(option, theme) } : {})}\n />\n );\n}\n\nconst FontSection = () => <CoreSection axis=\"font\" />;\nconst MonoFontSection = () => <CoreSection axis=\"monoFont\" />;\nconst DensitySection = () => <CoreSection axis=\"density\" />;\n\nexport interface PreferencesProps extends Omit<PreferencesRootProps, \"children\"> {\n /** Restyle or reposition the floating trigger (it is `fixed bottom-4 end-4` by default). */\n triggerClassName?: string;\n}\n\n/**\n * All-in-one: a floating trigger + the full drawer.\n *\n * Forwards Root's props and lets the trigger be restyled or repositioned — it used to take\n * none, so a consumer could neither move the FAB nor reach the hotkey.\n *\n * There is no `Preferences.Root` / `.Panel` / `.Density` namespace. It was built with\n * `Object.assign`, and those statics do NOT survive React Server Components: once the module\n * becomes a client reference, `Preferences.Density` reads back as `undefined` and React throws\n * \"Element type is invalid\". It was a broken API kept beside the working one — and it was the\n * single counter-example to \"no component exports dot-notation\".\n */\nexport function Preferences({ triggerClassName, ...rootProps }: PreferencesProps = {}) {\n return (\n <PreferencesRoot {...rootProps}>\n <PreferencesTrigger className={triggerClassName} />\n <PreferencesPanel />\n </PreferencesRoot>\n );\n}\n\n/**\n * The parts as flat named exports — the API. They cross the RSC boundary intact, tree-shake per\n * part, and match how every other compound in this library is exported (`DialogContent`, not\n * `Dialog.Content`).\n */\nexport {\n PreferencesRoot,\n PreferencesTrigger,\n PreferencesPanel,\n PrefField as PreferencesField,\n PrefFieldSet as PreferencesFieldSet,\n ColorSection as PreferencesColor,\n // Flat like every other section, and for the reason the others are: a host composing its own\n // panel with `children` replaces the canonical set, and without this it would silently drop every\n // choice its installed packages contribute — which is the several-products-in-one-window failure\n // the mechanism exists to remove, reintroduced by the escape hatch.\n ContributedSections as PreferencesSections,\n RadiusSection as PreferencesRadius,\n FontSection as PreferencesFont,\n MonoFontSection as PreferencesMonoFont,\n DensitySection as PreferencesDensity,\n};\n"],"names":["Dialog"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CreateToasterReturn } from '@ark-ui/react/toast';
|
|
2
2
|
/**
|
|
3
3
|
* The copy for "the theme you chose is no longer published", shared by the two surfaces that
|
|
4
|
-
* say it: this toast, and the `Alert` inside `
|
|
4
|
+
* say it: this toast, and the `Alert` inside `PreferencesColor`. One state, said once.
|
|
5
5
|
*
|
|
6
6
|
* Both strings are **library-authored English**, which is exactly the distinction `PreferencesColor`'s `formatSide`
|
|
7
7
|
* draws with its `formatName`: the library wrote "Light" and "Dark", so a caller who translates the
|
|
@@ -46,7 +46,7 @@ export interface ThemeNoticeProps extends ThemeRetiredCopy {
|
|
|
46
46
|
* identity. What is left is that somebody chose gold and is looking at blue, and silence makes that
|
|
47
47
|
* read as a bug in our product rather than a change in their client's.
|
|
48
48
|
*
|
|
49
|
-
* The panel says the same thing in `
|
|
49
|
+
* The panel says the same thing in `PreferencesColor`, from the same state — and cannot always
|
|
50
50
|
* say it: that section hides itself below two published themes, so a tenant who retired their way
|
|
51
51
|
* down to one has this as the only surface left.
|
|
52
52
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme-notice.js","sources":["../../src/composites/theme-notice.tsx"],"sourcesContent":["\"use client\";\n\nimport type { CreateToasterReturn } from \"@ark-ui/react/toast\";\nimport * as React from \"react\";\nimport { toast as sharedToaster } from \"../simples/toast.js\";\nimport { useKanzoTheme } from \"../theme/KanzoThemeProvider.js\";\n\n/**\n * The copy for \"the theme you chose is no longer published\", shared by the two surfaces that\n * say it: this toast, and the `Alert` inside `
|
|
1
|
+
{"version":3,"file":"theme-notice.js","sources":["../../src/composites/theme-notice.tsx"],"sourcesContent":["\"use client\";\n\nimport type { CreateToasterReturn } from \"@ark-ui/react/toast\";\nimport * as React from \"react\";\nimport { toast as sharedToaster } from \"../simples/toast.js\";\nimport { useKanzoTheme } from \"../theme/KanzoThemeProvider.js\";\n\n/**\n * The copy for \"the theme you chose is no longer published\", shared by the two surfaces that\n * say it: this toast, and the `Alert` inside `PreferencesColor`. One state, said once.\n *\n * Both strings are **library-authored English**, which is exactly the distinction `PreferencesColor`'s `formatSide`\n * draws with its `formatName`: the library wrote \"Light\" and \"Dark\", so a caller who translates the\n * app has to be able to translate them. A theme's `label` is the opposite case — the client\n * authored it, at runtime — and takes no formatter, because a formatter over it would only let a\n * host decorate someone else's brand name.\n */\nexport interface ThemeRetiredCopy {\n /** Heading (i18n). */\n title?: string;\n /**\n * Compose the body. The default frame is English; a caller who translates `title` needs this\n * too, or the notice is half-translated.\n *\n * `theme` is the retired **name**, not a label: what the tenant withdrew is gone from\n * `identities`, so its label went with it. A host that keeps its own table of retired names is\n * the reason this is a function rather than a string.\n */\n formatDescription?: (parts: { theme: string }) => string;\n}\n\nconst DEFAULT_TITLE = \"Brand updated\";\nconst DEFAULT_DESCRIPTION = ({ theme }: { theme: string }) =>\n `“${theme}” is no longer offered here. You are seeing the default.`;\n\n/** Resolve the copy once, so the toast and the panel's Alert cannot drift into two wordings. */\nexport function themeRetiredCopy(\n { title = DEFAULT_TITLE, formatDescription = DEFAULT_DESCRIPTION }: ThemeRetiredCopy,\n theme: string,\n): { title: string; description: string } {\n return { title, description: formatDescription({ theme }) };\n}\n\nexport interface ThemeNoticeProps extends ThemeRetiredCopy {\n /** Feed a second, separately-placed viewport. Defaults to the shared `toast` instance. */\n toaster?: CreateToasterReturn;\n}\n\n/**\n * ThemeNotice — says, once, that the identity this user chose is no longer published.\n *\n * **Opt-in, and it renders nothing itself.** `KanzoThemeProvider` detects the retirement, clears\n * the preference and holds the id in `retiredTheme`; it draws no surface, because a themer that\n * draws its own surface is what `KanzoTheme` was deleted for (`index.test.ts` keeps it deleted).\n * So mount this beside the host's own `<Toaster />` — it feeds that viewport and returns `null`.\n *\n * Why it is worth saying at all: the cascade already handled the colour. An attribute selector with\n * no matching rule is inert, so a withdrawn id falls through to `:root`, which is the default\n * identity. What is left is that somebody chose gold and is looking at blue, and silence makes that\n * read as a bug in our product rather than a change in their client's.\n *\n * The panel says the same thing in `PreferencesColor`, from the same state — and cannot always\n * say it: that section hides itself below two published themes, so a tenant who retired their way\n * down to one has this as the only surface left.\n */\nexport const ThemeNotice = ({ toaster = sharedToaster, ...copy }: ThemeNoticeProps = {}) => {\n const { retiredTheme } = useKanzoTheme();\n const { title, description } = themeRetiredCopy(copy, retiredTheme ?? \"\");\n\n // Keyed on the id rather than a boolean: the provider's own ref guarantees it *detects* a\n // retirement once, not that this effect runs once. Two things still reach it — StrictMode's\n // double invocation when this mounts AFTER the detection (a host that renders the notice behind\n // a route), and an inline `formatDescription`, which is a new function on every render and so a\n // changed dep. Neither is the \"once per session\" question, which storage already answered.\n const said = React.useRef<string | null>(null);\n React.useEffect(() => {\n if (!retiredTheme || said.current === retiredTheme) return;\n said.current = retiredTheme;\n toaster.create({ title, description, type: \"info\" });\n }, [retiredTheme, title, description, toaster]);\n\n return null;\n};\n"],"names":[],"mappings":";;;;AA+BA;AAKO;AAIL;AACF;AAwBO;AACL;AASA;AACE;AAEmD;AAIvD;;;;;"}
|
package/dist/simples/Link.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { VariantProps } from 'tailwind-variants';
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
/**
|
|
4
4
|
* Link — a token-styled anchor, and the only component in the library emitting
|
|
5
|
-
* `data-slot="link"`. `variant` picks the treatment
|
|
6
|
-
*
|
|
5
|
+
* `data-slot="link"`. `variant` picks the treatment. It takes no `asChild`, so a router's link
|
|
6
|
+
* cannot wear it yet. (Generic layout stays in the product via Tailwind — the DS
|
|
7
7
|
* ships no `Flex`/`Box`/`Grid`.)
|
|
8
8
|
*
|
|
9
9
|
* `linkVariants` is module-level and deliberately not on the barrel, which is where every other
|
package/dist/simples/Link.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Link.js","sources":["../../src/simples/Link.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Link — a token-styled anchor, and the only component in the library emitting\n * `data-slot=\"link\"`. `variant` picks the treatment
|
|
1
|
+
{"version":3,"file":"Link.js","sources":["../../src/simples/Link.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Link — a token-styled anchor, and the only component in the library emitting\n * `data-slot=\"link\"`. `variant` picks the treatment. It takes no `asChild`, so a router's link\n * cannot wear it yet. (Generic layout stays in the product via Tailwind — the DS\n * ships no `Flex`/`Box`/`Grid`.)\n *\n * `linkVariants` is module-level and deliberately not on the barrel, which is where every other\n * recipe with no cross-module importer sits: exported, a variant object freezes a class list as\n * API. `index.test.ts` pins that.\n */\nexport const linkVariants = tv({\n base: \"rounded-sm underline-offset-4 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n variants: {\n variant: {\n default: \"text-primary hover:underline\",\n subtle: \"text-muted-foreground hover:text-foreground hover:underline\",\n plain: \"text-inherit no-underline hover:underline\",\n },\n },\n defaultVariants: { variant: \"default\" },\n});\n\nexport interface LinkProps extends React.ComponentProps<\"a\">, VariantProps<typeof linkVariants> {}\n\nexport function Link({ variant, className, ref, slot, ...rest }: LinkProps) {\n return <a ref={ref} className={cn(linkVariants({ variant }), className)} {...rest} data-slot={slot ?? \"link\"} />;\n}\nLink.displayName = \"Link\";\n"],"names":["linkVariants","tv","Link","variant","className","ref","slot","rest","cn"],"mappings":";;;;AAcO,MAAMA,IAAeC,EAAG;AAAA,EAC7B,MAAM;AAAA,EACN,UAAU;AAAA,IACR,SAAS;AAAA,MACP,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,OAAO;AAAA,IAAA;AAAA,EACT;AAAA,EAEF,iBAAiB,EAAE,SAAS,UAAA;AAC9B,CAAC;AAIM,SAASC,EAAK,EAAE,SAAAC,GAAS,WAAAC,GAAW,KAAAC,GAAK,MAAAC,GAAM,GAAGC,KAAmB;AAC1E,2BAAQ,KAAA,EAAE,KAAAF,GAAU,WAAWG,EAAGR,EAAa,EAAE,SAAAG,EAAA,CAAS,GAAGC,CAAS,GAAI,GAAGG,GAAM,aAAWD,KAAQ,QAAQ;AAChH;AACAJ,EAAK,cAAc;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kanzo-tech/ui",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Kanzo shared UI — domain-free components over Ark UI and tailwind-variants, themed by tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
"tailwind-merge": "^3.6.0",
|
|
96
96
|
"tailwind-variants": "^3.2.2",
|
|
97
97
|
"tw-animate-css": "^1.4.0",
|
|
98
|
-
"@kanzo-tech/mosaic": "0.15.
|
|
99
|
-
"@kanzo-tech/theme": "0.15.
|
|
98
|
+
"@kanzo-tech/mosaic": "0.15.1",
|
|
99
|
+
"@kanzo-tech/theme": "0.15.1"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|
|
102
102
|
"@codemirror/autocomplete": "^6.18.6",
|