@loworbitstudio/visor 1.34.0 → 1.36.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.4.0",
3
- "generated_at": "2026-10-04T04:36:38.223Z",
3
+ "generated_at": "2026-10-04T22:55:53.114Z",
4
4
  "components": {
5
5
  "accessibility-specimen": {
6
6
  "changeType": "current",
@@ -56,6 +56,12 @@
56
56
  "breakingChange": false,
57
57
  "migrationNote": null
58
58
  },
59
+ "bottom-nav": {
60
+ "changeType": "current",
61
+ "files": [],
62
+ "breakingChange": false,
63
+ "migrationNote": null
64
+ },
59
65
  "box": {
60
66
  "changeType": "current",
61
67
  "files": [],
@@ -494,6 +500,12 @@
494
500
  "breakingChange": false,
495
501
  "migrationNote": null
496
502
  },
503
+ "position-picker": {
504
+ "changeType": "current",
505
+ "files": [],
506
+ "breakingChange": false,
507
+ "migrationNote": null
508
+ },
497
509
  "progress": {
498
510
  "changeType": "current",
499
511
  "files": [],
package/dist/index.js CHANGED
@@ -8760,6 +8760,9 @@ function spawnCommand(cwd, options) {
8760
8760
  import { existsSync as existsSync31, mkdirSync as mkdirSync14, readFileSync as readFileSync33 } from "fs";
8761
8761
  import { dirname as dirname15, isAbsolute as isAbsolute7, resolve as resolve21 } from "path";
8762
8762
  var ICON_X = `React.createElement("svg", { viewBox: "0 0 256 256", width: "1em", height: "1em", fill: "currentColor", "aria-hidden": true }, React.createElement("path", { d: "M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z" }))`;
8763
+ var navIcon = (shape) => `function (p) { return React.createElement("svg", { viewBox: "0 0 256 256", width: p.size || 24, height: p.size || 24, className: p.className, fill: p.weight === "fill" ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 16, strokeLinejoin: "round", "aria-hidden": true }, React.createElement("path", { d: ${JSON.stringify(shape)} })); }`;
8764
+ var NAV_SHAPES = ["M40 56h176v144H40z", "M128 32l96 176H32z", "M128 32a96 96 0 1 0 0 192a96 96 0 0 0 0-192z", "M48 64h160v128H48zM48 112h160", "M40 200V120M104 200V64M168 200V96M216 200V152", "M64 216V96l64-64l64 64v120z", "M128 40a40 40 0 1 0 0 80a40 40 0 0 0 0-80zM48 216c0-44 36-72 80-72s80 28 80 72z"];
8765
+ var NAV_ICONS = NAV_SHAPES.map(navIcon);
8763
8766
  var FIXTURES = {
8764
8767
  // VI-680: hairline consumers. `__mod` is the component module, so a fixture can
8765
8768
  // reach the compound parts a single export does not expose.
@@ -9145,6 +9148,27 @@ var FIXTURES = {
9145
9148
  invalid: { export: "OTPInput", interactiveTarget: "input", props: `{ value: "428519", invalid: true, errorMessage: "That code didn't work. Check it and try again." }` },
9146
9149
  trailing: { export: "OTPInput", interactiveTarget: "input", props: `{ value: "428519", trailing: React.createElement("button", { type: "button" }, "Resend") }` }
9147
9150
  },
9151
+ // VI-672: `default` is the grid on its own ground; `on-image` draws it over a busy
9152
+ // synthetic photograph (turbulence, one light half and one dark half) so each
9153
+ // target's scrim ring is checked against both. The picker is its own frame here:
9154
+ // `position: relative` and a size stand in for the positioned photo frame.
9155
+ "position-picker": {
9156
+ default: { export: "PositionPicker", interactiveTarget: '[role="radio"]', props: `{ "aria-label": "Focal point", defaultValue: { y: "center", x: "center" } }` },
9157
+ "on-image": {
9158
+ export: "PositionPicker",
9159
+ interactiveTarget: '[role="radio"]',
9160
+ props: `(function () {
9161
+ var svg = "<svg xmlns='http://www.w3.org/2000/svg' width='480' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.06' numOctaves='4' seed='7'/><feColorMatrix values='1.4 0 0 0 0.05 0 1.2 0 0 0.05 0 0 1.1 0 0.05 0 0 0 1 0'/></filter><rect width='480' height='300' fill='#d9c8a6'/><rect width='480' height='300' filter='url(#n)'/><rect x='0' y='0' width='240' height='300' fill='#f4efe4' opacity='0.55'/><rect x='240' y='0' width='240' height='300' fill='#0b0b14' opacity='0.6'/></svg>";
9162
+ return {
9163
+ variant: "on-image",
9164
+ "aria-label": "Focal point",
9165
+ defaultValue: { y: "top", x: "right" },
9166
+ style: { position: "relative", inset: "auto", width: 480, height: 300, backgroundImage: "url('data:image/svg+xml;utf8," + encodeURIComponent(svg).replace(/'/g, "%27") + "')", backgroundSize: "cover" },
9167
+ };
9168
+ })()`
9169
+ },
9170
+ thumbnail: { export: "PositionPicker", interactiveTarget: '[role="radio"]', props: `{ variant: "on-image", "aria-label": "Focal point", defaultValue: { y: "bottom", x: "left" }, style: { position: "relative", inset: "auto", width: 40, height: 28, background: "#667" } }` }
9171
+ },
9148
9172
  // VI-624: `default` is the closed field; the popover fixtures render open so the
9149
9173
  // two-column (24-hour) and three-column (12-hour) anatomy can be inspected.
9150
9174
  "time-picker": {
@@ -9172,6 +9196,54 @@ var FIXTURES = {
9172
9196
  export: "Select",
9173
9197
  interactiveTarget: "button",
9174
9198
  props: `{ children: React.createElement(__mod.SelectTrigger, { variant: "borderless" }, React.createElement(__mod.SelectValue, { placeholder: "Pick a plan" })) }`
9199
+ },
9200
+ // VI-692: forced open so the portaled listbox is in the shot.
9201
+ open: {
9202
+ export: "Select",
9203
+ props: `{ open: true, defaultValue: "pro", children: [React.createElement(__mod.SelectTrigger, { key: "t" }, React.createElement(__mod.SelectValue, { placeholder: "Pick a plan" })), React.createElement(__mod.SelectContent, { key: "c" }, [React.createElement(__mod.SelectItem, { key: "a", value: "free" }, "Free"), React.createElement(__mod.SelectItem, { key: "b", value: "pro" }, "Pro"), React.createElement(__mod.SelectItem, { key: "d", value: "team" }, "Team")])] }`
9204
+ }
9205
+ },
9206
+ // VI-692: forced open so the portaled tip is in the shot.
9207
+ tooltip: {
9208
+ default: { export: "TooltipProvider", props: `{ children: React.createElement(__mod.Tooltip, { open: true }, [React.createElement(__mod.TooltipTrigger, { key: "t", asChild: true }, React.createElement("button", { type: "button" }, "Hover me")), React.createElement(__mod.TooltipContent, { key: "c" }, "Tooltip on the theme")]) }` }
9209
+ },
9210
+ // VI-683: an icon rail under an app bar (position="contained"), collapsed to
9211
+ // icons. Render with `--width 1024 --state focus` to open the first item's
9212
+ // tooltip (the sidebar is mobile at or below 768px, and mobile never shows
9213
+ // it); the second item wears the unread dot.
9214
+ sidebar: {
9215
+ default: {
9216
+ export: "SidebarProvider",
9217
+ interactiveTarget: '[data-sidebar="menu-button"]',
9218
+ props: `(function () {
9219
+ var h = React.createElement;
9220
+ var glyph = function (d) {
9221
+ return h("svg", { viewBox: "0 0 256 256", width: 20, height: 20, fill: "currentColor", "aria-hidden": true }, h("path", { d: d }));
9222
+ };
9223
+ var items = [
9224
+ ["Edit EPK", "M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63Z"],
9225
+ ["Inbox, 3 unread", "M32,136v56a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V136l-32-72a16,16,0,0,0-14.6-9.5H78.6A16,16,0,0,0,64,64Z", true],
9226
+ ["Calendar", "M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Z"],
9227
+ ];
9228
+ return {
9229
+ defaultOpen: false,
9230
+ style: { flexDirection: "column", minHeight: 360, width: 420 },
9231
+ children: [
9232
+ h("div", { key: "bar", style: { height: 48, display: "flex", alignItems: "center", padding: "0 16px", background: "var(--surface-card)", color: "var(--text-primary)", fontSize: 14 } }, "App bar"),
9233
+ h("div", { key: "body", style: { display: "flex", flex: 1 } }, [
9234
+ h(__mod.Sidebar, { key: "rail", collapsible: "icon", position: "contained" },
9235
+ h(__mod.SidebarMenu, null, items.map(function (it) {
9236
+ return h(__mod.SidebarMenuItem, { key: it[0] }, [
9237
+ h(__mod.SidebarMenuButton, { key: "b", asChild: true, tooltip: it[0] }, h("a", { href: "#" }, glyph(it[1]))),
9238
+ it[2] ? h(__mod.SidebarMenuBadge, { key: "d", "aria-hidden": true, style: { top: 4, right: 4, minWidth: 8, height: 8, padding: 0, borderRadius: "50%", background: "var(--text-primary)" } }) : null,
9239
+ ]);
9240
+ }))
9241
+ ),
9242
+ h(__mod.SidebarInset, { key: "main" }, h("div", { style: { padding: 16, fontSize: 14 } }, "Page content")),
9243
+ ]),
9244
+ ],
9245
+ };
9246
+ })()`
9175
9247
  }
9176
9248
  },
9177
9249
  checkbox: {
@@ -9298,6 +9370,18 @@ var FIXTURES = {
9298
9370
  dot: { export: "SaveStatus", props: `{ status: "saved", dot: true }` },
9299
9371
  "dot-unsaved": { export: "SaveStatus", props: `{ status: "unsaved", dot: true }` }
9300
9372
  },
9373
+ // VI-673: the phone foot bar. `fixed: false` lays it in the render frame instead of pinning it.
9374
+ "bottom-nav": {
9375
+ default: {
9376
+ export: "BottomNav",
9377
+ interactiveTarget: "a",
9378
+ props: `{ "aria-label": "Workspace", fixed: false, items: [{ label: "Edit", icon: ${NAV_ICONS[0]}, href: "#edit", active: true }, { label: "Inbox", icon: ${NAV_ICONS[1]}, href: "#inbox", mark: 3 }, { label: "Calendar", icon: ${NAV_ICONS[2]}, href: "#calendar" }, { label: "Invoices", icon: ${NAV_ICONS[3]}, href: "#invoices" }, { label: "Analytics", icon: ${NAV_ICONS[4]}, href: "#analytics" }, { label: "Team", icon: ${NAV_ICONS[6]}, href: "#team", group: "admin" }, { label: "Settings", icon: ${NAV_ICONS[5]}, href: "#settings", group: "admin" }] }`
9379
+ },
9380
+ labelled: {
9381
+ export: "BottomNav",
9382
+ props: `{ "aria-label": "Workspace", fixed: false, showLabels: true, items: [{ label: "Home", icon: ${NAV_ICONS[5]}, href: "#home", active: true }, { label: "Inbox", icon: ${NAV_ICONS[1]}, href: "#inbox", mark: true }, { label: "Settings", icon: ${NAV_ICONS[5]}, href: "#settings" }] }`
9383
+ }
9384
+ },
9301
9385
  // VI-615: the single-select pill group. `fullwidth` fills the render frame.
9302
9386
  "segmented-control": {
9303
9387
  default: {
@@ -9525,6 +9609,11 @@ ${opts.componentCss}
9525
9609
  </style>
9526
9610
  <style data-visor="harness">
9527
9611
  html, body { margin: 0; padding: 0; }
9612
+ body {
9613
+ color: var(--text-primary, #111827);
9614
+ /* Same token the nextjs adapter's origination binds to body (VI-616). */
9615
+ font-family: var(--font-body, var(--font-sans, system-ui, -apple-system, sans-serif));
9616
+ }
9528
9617
  #theme-scope {
9529
9618
  box-sizing: border-box;
9530
9619
  min-height: 100vh;
@@ -9533,15 +9622,12 @@ html, body { margin: 0; padding: 0; }
9533
9622
  align-items: center;
9534
9623
  justify-content: center;
9535
9624
  background: var(--surface-page, #ffffff);
9536
- color: var(--text-primary, #111827);
9537
- /* Same token the nextjs adapter's origination binds to body (VI-616). */
9538
- font-family: var(--font-body, var(--font-sans, system-ui, -apple-system, sans-serif));
9539
9625
  }
9540
9626
  #root { width: 100%; max-width: 420px; }
9541
9627
  </style>
9542
9628
  </head>
9543
- <body>
9544
- <div id="theme-scope" class="${opts.themeClass}">
9629
+ <body class="${opts.themeClass}">
9630
+ <div id="theme-scope">
9545
9631
  <div id="root"></div>
9546
9632
  </div>
9547
9633
  <script>
@@ -9708,11 +9794,12 @@ async function renderCommand(component, cwd, options) {
9708
9794
  probe = await page.evaluate(
9709
9795
  `(function () {
9710
9796
  var scope = document.getElementById("theme-scope");
9797
+ var body = document.body;
9711
9798
  var html = document.documentElement;
9712
9799
  var read = function (el, prop) {
9713
9800
  return getComputedStyle(el).getPropertyValue(prop).trim();
9714
9801
  };
9715
- var themed = read(scope, "--surface-card");
9802
+ var themed = read(body, "--surface-card");
9716
9803
  var base = read(html, "--surface-card");
9717
9804
  return {
9718
9805
  themedSurfaceCard: themed,
@@ -46,7 +46,7 @@
46
46
  {
47
47
  "path": "components/ui/heading/heading.module.css",
48
48
  "type": "registry:ui",
49
- "content": "/* Heading base styles */\n.base {\n font-family: var(--font-heading, ui-sans-serif, system-ui, sans-serif);\n color: var(--text-primary, #111827);\n line-height: var(--line-height-tight, 1.25);\n margin: 0;\n}\n\n/* Sizes */\n.sizeXs {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n.sizeSm {\n font-size: var(--font-size-base, 1rem);\n}\n\n.sizeMd {\n font-size: var(--font-size-lg, 1.125rem);\n}\n\n.sizeLg {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.sizeXl {\n font-size: var(--font-size-2xl, 1.5rem);\n}\n\n.size2xl {\n font-size: var(--font-size-4xl, 2.25rem);\n}\n\n/* Weights */\n\n/*\n * VI-639: the theme's own heading weight. `--font-weight-heading` carries what\n * this theme says headings weigh; `--font-weight-semibold` is a step on the\n * named ramp and no longer doubles as that channel. Falls back through semibold\n * so a consumer on an older tokens package keeps the previous resolution.\n */\n.weightHeading {\n font-weight: var(--font-weight-heading, var(--font-weight-semibold, 600));\n}\n\n.weightNormal {\n font-weight: var(--font-weight-normal, 400);\n}\n\n.weightMedium {\n font-weight: var(--font-weight-medium, 500);\n}\n\n.weightSemibold {\n font-weight: var(--font-weight-semibold, 600);\n}\n\n.weightBold {\n font-weight: var(--font-weight-bold, 700);\n}\n"
49
+ "content": "/* Heading base styles */\n.base {\n font-family: var(--font-heading, ui-sans-serif, system-ui, sans-serif);\n color: var(--text-primary, #111827);\n line-height: var(--line-height-tight, 1.25);\n /* VI-687: theme-set heading casing; inherit (the CSS default) when unset. */\n text-transform: var(--font-heading-text-transform, inherit);\n margin: 0;\n}\n\n/* Sizes */\n.sizeXs {\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n.sizeSm {\n font-size: var(--font-size-base, 1rem);\n}\n\n.sizeMd {\n font-size: var(--font-size-lg, 1.125rem);\n}\n\n.sizeLg {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.sizeXl {\n font-size: var(--font-size-2xl, 1.5rem);\n}\n\n.size2xl {\n font-size: var(--font-size-4xl, 2.25rem);\n}\n\n/* Weights */\n\n/*\n * VI-639: the theme's own heading weight. `--font-weight-heading` carries what\n * this theme says headings weigh; `--font-weight-semibold` is a step on the\n * named ramp and no longer doubles as that channel. Falls back through semibold\n * so a consumer on an older tokens package keeps the previous resolution.\n */\n.weightHeading {\n font-weight: var(--font-weight-heading, var(--font-weight-semibold, 600));\n}\n\n.weightNormal {\n font-weight: var(--font-weight-normal, 400);\n}\n\n.weightMedium {\n font-weight: var(--font-weight-medium, 500);\n}\n\n.weightSemibold {\n font-weight: var(--font-weight-semibold, 600);\n}\n\n.weightBold {\n font-weight: var(--font-weight-bold, 700);\n}\n"
50
50
  }
51
51
  ]
52
52
  },
@@ -322,7 +322,7 @@
322
322
  {
323
323
  "path": "components/ui/select/select.tsx",
324
324
  "type": "registry:ui",
325
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { CaretDownIcon, CaretUpIcon, CheckIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./select.module.css\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {\n size?: \"sm\" | \"md\" | \"lg\"\n /**\n * Visual variant of the closed trigger. `\"default\"` keeps the 1px border on\n * the standard control surface. `\"borderless\"` drops the border and sits on\n * `--field-control-bg`, matching a borderless Input inside an in-dialog form\n * context (one that sets `--field-control-bg: var(--surface-subtle)`).\n * Opt-in — default is unchanged.\n */\n variant?: \"default\" | \"borderless\"\n }\n>(({ className, size = \"md\", variant = \"default\", children, ...props }, ref) => {\n const sizeClass = {\n sm: styles.triggerSizeSm,\n md: styles.triggerSizeMd,\n lg: styles.triggerSizeLg,\n }[size]\n\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n data-size={size}\n data-variant={variant}\n className={cn(\n styles.trigger,\n sizeClass,\n variant === \"borderless\" && styles.triggerBorderless,\n className\n )}\n ref={ref}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <CaretDownIcon className={styles.triggerIcon} />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n )\n})\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectContent = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n data-slot=\"select-content\"\n className={cn(styles.content, className)}\n position={position}\n ref={ref}\n {...props}\n >\n <SelectScrollUpButton />\n <SelectPrimitive.Viewport\n className={cn(\n styles.viewport,\n position === \"popper\" && styles.viewportPopper\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n <SelectScrollDownButton />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n )\n})\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.Label\n data-slot=\"select-label\"\n className={cn(styles.label, className)}\n ref={ref}\n {...props}\n />\n )\n})\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => {\n return (\n <SelectPrimitive.Item\n data-slot=\"select-item\"\n className={cn(styles.item, className)}\n ref={ref}\n {...props}\n >\n <span className={styles.itemIndicatorWrapper}>\n <SelectPrimitive.ItemIndicator>\n <CheckIcon className={styles.itemIndicatorIcon} />\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n )\n})\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.Separator\n data-slot=\"select-separator\"\n className={cn(styles.separator, className)}\n ref={ref}\n {...props}\n />\n )\n})\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.ScrollUpButton\n data-slot=\"select-scroll-up-button\"\n className={cn(styles.scrollButton, className)}\n ref={ref}\n {...props}\n >\n <CaretUpIcon className={styles.scrollButtonIcon} />\n </SelectPrimitive.ScrollUpButton>\n )\n})\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.ScrollDownButton\n data-slot=\"select-scroll-down-button\"\n className={cn(styles.scrollButton, className)}\n ref={ref}\n {...props}\n >\n <CaretDownIcon className={styles.scrollButtonIcon} />\n </SelectPrimitive.ScrollDownButton>\n )\n})\nSelectScrollDownButton.displayName =\n SelectPrimitive.ScrollDownButton.displayName\n\nexport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectScrollDownButton,\n SelectScrollUpButton,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n}\n"
325
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { CaretDownIcon, CaretUpIcon, CheckIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./select.module.css\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {\n size?: \"sm\" | \"md\" | \"lg\"\n /**\n * Visual variant of the closed trigger. `\"default\"` keeps the 1px border on\n * the standard control surface. `\"borderless\"` drops the border and sits on\n * `--field-control-bg`, matching a borderless Input inside an in-dialog form\n * context (one that sets `--field-control-bg: var(--surface-subtle)`).\n * Opt-in — default is unchanged.\n */\n variant?: \"default\" | \"borderless\"\n }\n>(({ className, size = \"md\", variant = \"default\", children, ...props }, ref) => {\n const sizeClass = {\n sm: styles.triggerSizeSm,\n md: styles.triggerSizeMd,\n lg: styles.triggerSizeLg,\n }[size]\n\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n data-size={size}\n data-variant={variant}\n className={cn(\n styles.trigger,\n sizeClass,\n variant === \"borderless\" && styles.triggerBorderless,\n className\n )}\n ref={ref}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <CaretDownIcon className={styles.triggerIcon} />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n )\n})\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\ninterface SelectContentProps\n extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> {\n /**\n * Element the menu portals into. Defaults to `document.body`. Pass the root\n * of a class-scoped theme so the menu inherits that theme's tokens.\n */\n container?: React.ComponentPropsWithoutRef<typeof SelectPrimitive.Portal>[\"container\"]\n}\n\nconst SelectContent = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Content>,\n SelectContentProps\n>(({ className, children, position = \"popper\", container, ...props }, ref) => {\n return (\n <SelectPrimitive.Portal container={container}>\n <SelectPrimitive.Content\n data-slot=\"select-content\"\n className={cn(styles.content, className)}\n position={position}\n ref={ref}\n {...props}\n >\n <SelectScrollUpButton />\n <SelectPrimitive.Viewport\n className={cn(\n styles.viewport,\n position === \"popper\" && styles.viewportPopper\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n <SelectScrollDownButton />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n )\n})\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.Label\n data-slot=\"select-label\"\n className={cn(styles.label, className)}\n ref={ref}\n {...props}\n />\n )\n})\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => {\n return (\n <SelectPrimitive.Item\n data-slot=\"select-item\"\n className={cn(styles.item, className)}\n ref={ref}\n {...props}\n >\n <span className={styles.itemIndicatorWrapper}>\n <SelectPrimitive.ItemIndicator>\n <CheckIcon className={styles.itemIndicatorIcon} />\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n )\n})\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.Separator\n data-slot=\"select-separator\"\n className={cn(styles.separator, className)}\n ref={ref}\n {...props}\n />\n )\n})\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.ScrollUpButton\n data-slot=\"select-scroll-up-button\"\n className={cn(styles.scrollButton, className)}\n ref={ref}\n {...props}\n >\n <CaretUpIcon className={styles.scrollButtonIcon} />\n </SelectPrimitive.ScrollUpButton>\n )\n})\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => {\n return (\n <SelectPrimitive.ScrollDownButton\n data-slot=\"select-scroll-down-button\"\n className={cn(styles.scrollButton, className)}\n ref={ref}\n {...props}\n >\n <CaretDownIcon className={styles.scrollButtonIcon} />\n </SelectPrimitive.ScrollDownButton>\n )\n})\nSelectScrollDownButton.displayName =\n SelectPrimitive.ScrollDownButton.displayName\n\nexport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectScrollDownButton,\n SelectScrollUpButton,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n}\nexport type { SelectContentProps }\n"
326
326
  },
327
327
  {
328
328
  "path": "components/ui/select/select.module.css",
@@ -353,7 +353,7 @@
353
353
  {
354
354
  "path": "components/ui/switch/switch.module.css",
355
355
  "type": "registry:ui",
356
- "content": "/* Switch root styles */\n.root {\n position: relative;\n display: inline-flex;\n flex-shrink: 0;\n align-items: center;\n border-radius: 9999px;\n /* VI-655: the border only holds box geometry. The track's resting edge is an\n inset outline on the shared --control-edge-* tokens; it is transparent\n unless a theme sets --switch-edge-color, and --control-edge-width: 0\n removes it with every other control's edge. */\n border: 1px solid transparent;\n background-color: var(--switch-track-bg, var(--border-default, #e5e7eb));\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--switch-edge-color, transparent);\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n cursor: pointer;\n}\n\n@media (hover: hover) {\n .root:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n background-color: var(--border-strong, #d1d5db);\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch. */\n.root:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.root:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.root[aria-invalid=\"true\"] {\n outline: var(--control-state-edge-width, 1px) solid var(--border-error, #ef4444);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 15%, transparent);\n}\n\n.root[data-state=\"checked\"] {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n}\n\n/* Size variants\n The default size takes its dimensions from the theme (VI-656):\n --switch-track-width / -height, and --switch-knob-inset, the gap between the\n knob and the track's outer edge on every side. The private properties below\n exist only while --switch-knob-inset is set; unset, they are invalid, so the\n knob falls back to its shipped 1rem size and position and the track keeps\n whatever inline padding it had (revert-layer). Set, the track's inline\n padding goes to 0 so the inset alone places the knob. The 1px is the\n geometry border (see .root), which the inset is measured across. */\n.sizeDefault {\n height: var(--switch-track-height, 1.15rem);\n width: var(--switch-track-width, 2rem);\n --_switch-pad: calc(var(--switch-knob-inset) * 0);\n --_switch-knob-size: calc(var(--switch-track-height, 1.15rem) - 2 * var(--switch-knob-inset));\n --_switch-knob-start: calc(var(--switch-knob-inset) - 1px);\n}\n\n/* In visor-base so an unset inset leaves the track's padding to whatever reset\n or browser default applies, and a set inset zeroes it only where no consumer\n rule speaks. */\n@layer visor-base {\n .sizeDefault {\n padding-inline: var(--_switch-pad, revert-layer);\n }\n}\n\n.sizeSm {\n height: 0.875rem;\n width: 1.5rem;\n}\n\n/* Thumb */\n.thumb {\n display: block;\n border-radius: 9999px;\n background-color: var(--surface-page, #ffffff);\n box-shadow: var(--shadow-sm);\n transition: transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n pointer-events: none;\n}\n\n/* Thumb size — derived from parent via CSS nesting */\n.sizeDefault .thumb {\n width: var(--_switch-knob-size, 1rem);\n height: var(--_switch-knob-size, 1rem);\n}\n\n.sizeSm .thumb {\n width: 0.75rem;\n height: 0.75rem;\n}\n\n.root[data-state=\"unchecked\"] .thumb {\n transform: translateX(0);\n}\n\n.sizeDefault[data-state=\"unchecked\"] .thumb {\n transform: translateX(var(--_switch-knob-start, 0));\n}\n\n/* The knob's right edge sits the same distance from the track's right edge as\n its left edge does from the left: flush inside the 1px geometry border by\n default (2rem - 1rem - 2px, the shipped 100% - 2px), or the inset when set. */\n.sizeDefault[data-state=\"checked\"] .thumb {\n transform: translateX(calc(var(--switch-track-width, 2rem) - 100% - 2px - var(--_switch-knob-start, 0px)));\n}\n\n.sizeSm[data-state=\"checked\"] .thumb {\n transform: translateX(calc(100% - 2px));\n}\n"
356
+ "content": "/* Switch root styles */\n.root {\n position: relative;\n display: inline-flex;\n flex-shrink: 0;\n align-items: center;\n border-radius: 9999px;\n /* VI-655: the border only holds box geometry. The track's resting edge is an\n inset outline on the shared --control-edge-* tokens; it is transparent\n unless a theme sets --switch-edge-color, and --control-edge-width: 0\n removes it with every other control's edge. */\n border: 1px solid transparent;\n background-color: var(--switch-track-bg, var(--border-default, #e5e7eb));\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), outline-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--switch-edge-color, transparent);\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n cursor: pointer;\n}\n\n/* VI-693: hover reads its own keys, so a borderless theme (border-strong set\n transparent) keeps a visible track under the pointer. Unset, both states\n repaint with --border-strong, as they always have. The checked rule is one\n class more specific, so it wins over the unchecked one. */\n@media (hover: hover) {\n .root:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n background-color: var(--switch-track-hover-bg, var(--border-strong, #d1d5db));\n }\n\n .root[data-state=\"checked\"]:hover:not(:focus-visible):not(:disabled):not([aria-invalid=\"true\"]) {\n background-color: var(--switch-track-hover-bg-checked, var(--border-strong, #d1d5db));\n }\n}\n\n/* Focus and invalid draw at --control-state-edge-width, which the edge switch\n does not touch. */\n.root:focus-visible {\n outline: var(--control-state-edge-width, 1px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent);\n}\n\n.root:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.root[aria-invalid=\"true\"] {\n outline: var(--control-state-edge-width, 1px) solid var(--border-error, #ef4444);\n outline-offset: calc(-1 * var(--control-state-edge-width, 1px));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-error, #ef4444) 15%, transparent);\n}\n\n.root[data-state=\"checked\"] {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n}\n\n/* Size variants\n The default size takes its dimensions from the theme (VI-656):\n --switch-track-width / -height, and --switch-knob-inset, the gap between the\n knob and the track's outer edge on every side. The private properties below\n exist only while --switch-knob-inset is set; unset, they are invalid, so the\n knob falls back to its shipped 1rem size and position and the track keeps\n whatever inline padding it had (revert-layer). Set, the track's inline\n padding goes to 0 so the inset alone places the knob. The 1px is the\n geometry border (see .root), which the inset is measured across. */\n.sizeDefault {\n height: var(--switch-track-height, 1.15rem);\n width: var(--switch-track-width, 2rem);\n --_switch-pad: calc(var(--switch-knob-inset) * 0);\n --_switch-knob-size: calc(var(--switch-track-height, 1.15rem) - 2 * var(--switch-knob-inset));\n --_switch-knob-start: calc(var(--switch-knob-inset) - 1px);\n}\n\n/* In visor-base so an unset inset leaves the track's padding to whatever reset\n or browser default applies, and a set inset zeroes it only where no consumer\n rule speaks. */\n@layer visor-base {\n .sizeDefault {\n padding-inline: var(--_switch-pad, revert-layer);\n }\n}\n\n.sizeSm {\n height: 0.875rem;\n width: 1.5rem;\n}\n\n/* Thumb */\n.thumb {\n display: block;\n border-radius: 9999px;\n background-color: var(--switch-knob-bg, var(--surface-page, #ffffff));\n box-shadow: var(--shadow-sm);\n transition: transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n pointer-events: none;\n}\n\n/* Thumb size — derived from parent via CSS nesting */\n.sizeDefault .thumb {\n width: var(--_switch-knob-size, 1rem);\n height: var(--_switch-knob-size, 1rem);\n}\n\n.sizeSm .thumb {\n width: 0.75rem;\n height: 0.75rem;\n}\n\n/* VI-693: the knob has a key per state. A dark theme's page colour is a black\n knob, which an unchecked track cannot hold at 3:1 (WCAG 1.4.11). */\n.root[data-state=\"checked\"] .thumb {\n background-color: var(--switch-knob-bg-checked, var(--surface-page, #ffffff));\n}\n\n.root[data-state=\"unchecked\"] .thumb {\n transform: translateX(0);\n}\n\n.sizeDefault[data-state=\"unchecked\"] .thumb {\n transform: translateX(var(--_switch-knob-start, 0));\n}\n\n/* The knob's right edge sits the same distance from the track's right edge as\n its left edge does from the left: flush inside the 1px geometry border by\n default (2rem - 1rem - 2px, the shipped 100% - 2px), or the inset when set. */\n.sizeDefault[data-state=\"checked\"] .thumb {\n transform: translateX(calc(var(--switch-track-width, 2rem) - 100% - 2px - var(--_switch-knob-start, 0px)));\n}\n\n.sizeSm[data-state=\"checked\"] .thumb {\n transform: translateX(calc(100% - 2px));\n}\n"
357
357
  }
358
358
  ]
359
359
  },
@@ -534,6 +534,30 @@
534
534
  }
535
535
  ]
536
536
  },
537
+ {
538
+ "name": "bottom-nav",
539
+ "type": "registry:ui",
540
+ "description": "A bottom navigation bar of icon destinations for phone widths: filled active destination, an optional unread mark, a hairline group break, safe-area padding, and its height published as --bottom-nav-height.",
541
+ "category": "navigation",
542
+ "dependencies": [
543
+ "@loworbitstudio/visor-core"
544
+ ],
545
+ "registryDependencies": [
546
+ "utils"
547
+ ],
548
+ "files": [
549
+ {
550
+ "path": "components/ui/bottom-nav/bottom-nav.tsx",
551
+ "type": "registry:ui",
552
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./bottom-nav.module.css\"\n\n/* ─── Types ─────────────────────────────────────────────────────────── */\n\n/** An icon component that takes a Phosphor-style `weight`. A Phosphor icon fits as is. */\nexport type BottomNavIcon = React.ComponentType<{\n weight?: \"regular\" | \"fill\"\n size?: number | string\n className?: string\n \"aria-hidden\"?: boolean | \"true\" | \"false\"\n}>\n\nexport interface BottomNavItem {\n /** Stable key. Falls back to `href`, then `label`. */\n key?: string\n /** The destination's name. Visually hidden unless the bar has `showLabels`. */\n label: string\n /** Icon component. The active destination renders it with `weight=\"fill\"`. */\n icon: BottomNavIcon\n /** Renders an `<a>`. Without it the destination is a `<button>`. */\n href?: string\n /** Fires on click, for an `<a>` as well as a `<button>`. */\n onSelect?: (event: React.MouseEvent<HTMLElement>) => void\n /** The destination that is open. Sets `aria-current=\"page\"` and fills the icon. */\n active?: boolean\n /**\n * A mark on the destination: `true` for a dot alone, a number for a count.\n * The mark is a solid dot and never draws the number; the count lives in the\n * accessible name (\"Inbox, 3 unread\"). `0` and `false` draw nothing.\n */\n mark?: boolean | number\n /** The word that follows the count in the accessible name. Default \"unread\". */\n markLabel?: string\n /** A hairline is drawn before the first destination of each new `group`. Not announced. */\n group?: string\n}\n\nexport interface BottomNavProps\n extends Omit<React.ComponentPropsWithoutRef<\"nav\">, \"children\"> {\n items: BottomNavItem[]\n /** Show each label under its icon. Default false: the label is visually hidden. */\n showLabels?: boolean\n /**\n * Pin the bar to the foot of the viewport (default) and publish its height as\n * `--bottom-nav-height` on the document root. `false` lays it out in flow, for\n * a frame or a preview, and publishes nothing.\n */\n fixed?: boolean\n}\n\n/* ─── Helpers ───────────────────────────────────────────────────────── */\n\n/** The destination's accessible name: the label, plus the mark's meaning. */\nexport function bottomNavItemName(item: BottomNavItem): string {\n const word = item.markLabel ?? \"unread\"\n if (typeof item.mark === \"number\" && item.mark > 0) {\n return `${item.label}, ${item.mark} ${word}`\n }\n if (item.mark === true) return `${item.label}, ${word}`\n return item.label\n}\n\nconst HEIGHT_TOKEN = \"--bottom-nav-height\"\n\n/**\n * Publishes the bar's rendered height (safe-area pad included) as\n * `--bottom-nav-height` on the document root, so a page can set\n * `scroll-padding-bottom: var(--bottom-nav-height)` and a focused field scrolls\n * clear of the bar (WCAG 2.4.11). Removed on unmount.\n */\nfunction usePublishHeight(\n ref: React.RefObject<HTMLElement | null>,\n enabled: boolean,\n) {\n React.useLayoutEffect(() => {\n const node = ref.current\n if (!enabled || !node) return\n const root = document.documentElement\n const publish = () => {\n root.style.setProperty(HEIGHT_TOKEN, `${node.getBoundingClientRect().height}px`)\n }\n publish()\n if (typeof ResizeObserver === \"undefined\") return () => root.style.removeProperty(HEIGHT_TOKEN)\n const observer = new ResizeObserver(publish)\n observer.observe(node)\n return () => {\n observer.disconnect()\n root.style.removeProperty(HEIGHT_TOKEN)\n }\n }, [ref, enabled])\n}\n\n/* ─── BottomNav ─────────────────────────────────────────────────────── */\n\nconst BottomNav = React.forwardRef<HTMLElement, BottomNavProps>(\n ({ items, showLabels = false, fixed = true, className, ...props }, ref) => {\n const navRef = React.useRef<HTMLElement | null>(null)\n const mergedRef = React.useCallback(\n (node: HTMLElement | null) => {\n navRef.current = node\n if (typeof ref === \"function\") ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLElement | null>).current = node\n },\n [ref],\n )\n usePublishHeight(navRef, fixed)\n\n const children: React.ReactNode[] = []\n items.forEach((item, index) => {\n const itemKey = item.key ?? item.href ?? item.label\n const previous = items[index - 1]\n if (previous && (previous.group ?? \"\") !== (item.group ?? \"\")) {\n children.push(\n <li\n key={`${itemKey}-group`}\n className={styles.group}\n role=\"presentation\"\n aria-hidden=\"true\"\n data-slot=\"bottom-nav-group\"\n />,\n )\n }\n\n const Icon = item.icon\n const hasMark =\n item.mark === true || (typeof item.mark === \"number\" && item.mark > 0)\n const content = (\n <>\n <span className={styles.iconBox}>\n <Icon\n className={styles.icon}\n size={24}\n weight={item.active ? \"fill\" : \"regular\"}\n aria-hidden=\"true\"\n />\n {hasMark && (\n <span className={styles.mark} aria-hidden=\"true\" data-slot=\"bottom-nav-mark\" />\n )}\n </span>\n <span className={showLabels ? styles.label : styles.hiddenLabel}>{item.label}</span>\n </>\n )\n const shared = {\n className: styles.item,\n \"aria-label\": bottomNavItemName(item),\n \"aria-current\": item.active ? (\"page\" as const) : undefined,\n \"data-active\": item.active ? \"true\" : undefined,\n \"data-labelled\": showLabels ? \"true\" : undefined,\n \"data-slot\": \"bottom-nav-item\",\n onClick: item.onSelect,\n }\n children.push(\n <li key={itemKey} className={styles.cell}>\n {item.href !== undefined ? (\n <a href={item.href} {...shared}>\n {content}\n </a>\n ) : (\n <button type=\"button\" {...shared}>\n {content}\n </button>\n )}\n </li>,\n )\n })\n\n return (\n <nav\n {...props}\n ref={mergedRef}\n data-slot=\"bottom-nav\"\n data-fixed={fixed ? \"true\" : undefined}\n className={cn(styles.root, className)}\n >\n <ul className={styles.list}>{children}</ul>\n </nav>\n )\n },\n)\nBottomNav.displayName = \"BottomNav\"\n\nexport { BottomNav }\n"
553
+ },
554
+ {
555
+ "path": "components/ui/bottom-nav/bottom-nav.module.css",
556
+ "type": "registry:ui",
557
+ "content": "/* BottomNav: a row of icon destinations at the foot of a phone screen. The\n resting top edge is a hairline: it reads --hairline-width (an inset shadow,\n never a border), so --hairline-width: 0 turns it off with no layout\n shift. Focus draws at the focus ring width, which that switch does not touch. */\n\n.root {\n box-sizing: border-box;\n width: 100%;\n background-color: var(--surface-card, #ffffff);\n color: var(--text-secondary, #6b7280);\n /* The home indicator never sits on a destination. */\n padding-bottom: env(safe-area-inset-bottom, 0px);\n box-shadow: inset 0 var(--hairline-width, var(--stroke-width-thin, 1px)) 0 var(--hairline, var(--border-default, #e5e7eb));\n}\n\n.root[data-fixed=\"true\"] {\n position: fixed;\n inset-inline: 0;\n bottom: 0;\n z-index: 20;\n}\n\n.list {\n display: flex;\n align-items: center;\n justify-content: space-around;\n gap: var(--spacing-1, 0.25rem);\n margin: 0;\n padding: var(--spacing-2, 0.5rem) var(--spacing-1, 0.25rem);\n list-style: none;\n}\n\n.cell {\n display: flex;\n flex: 1 1 0;\n justify-content: center;\n min-width: 0;\n}\n\n.item {\n position: relative;\n display: inline-flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-1, 0.25rem);\n box-sizing: border-box;\n /* 48px: the whole item is the target (WCAG 2.5.8 asks 24px). */\n min-width: var(--spacing-12, 3rem);\n min-height: var(--spacing-12, 3rem);\n margin: 0;\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n border: 0;\n border-radius: var(--radius-md, 0.375rem);\n background-color: transparent;\n color: inherit;\n text-decoration: none;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n transition:\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.item:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #111827);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n@media (hover: hover) {\n .item:hover {\n background-color: var(--surface-interactive-hover, #f3f4f6);\n color: var(--text-primary, #111827);\n }\n}\n\n/* Active: a lifted fill and the filled icon (the component swaps the glyph's\n weight), so colour is never the only cue (WCAG 1.4.1). */\n.item[aria-current=\"page\"] {\n background-color: var(--surface-interactive-active, #e5e7eb);\n color: var(--text-primary, #111827);\n}\n\n.iconBox {\n position: relative;\n display: inline-flex;\n}\n\n.icon {\n display: block;\n flex-shrink: 0;\n}\n\n.label {\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: 1;\n white-space: nowrap;\n}\n\n.hiddenLabel {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n/* The unread mark: a solid 8px circle in the accent, no number, at the icon's\n top-right corner. Its meaning is in the destination's accessible name. */\n.mark {\n position: absolute;\n top: calc(-1 * var(--spacing-1, 0.25rem));\n right: calc(-1 * var(--spacing-1, 0.25rem));\n width: var(--spacing-2, 0.5rem);\n height: var(--spacing-2, 0.5rem);\n border-radius: 50%;\n background-color: var(--accent, #0369a1);\n}\n\n/* The group break: a vertical hairline between destinations, not announced. It\n reads Separator's colour token first, so it stays drawn on a borderless theme\n whose --hairline is transparent (blacklight-app binds the separator colour for this). */\n.group {\n flex: 0 0 auto;\n align-self: stretch;\n width: var(--hairline-width, var(--stroke-width-thin, 1px));\n margin-block: var(--spacing-2, 0.5rem);\n background-color: var(--separator-color, var(--hairline, var(--border-default, #e5e7eb)));\n}\n\n@media (prefers-reduced-motion: reduce) {\n .item {\n transition: none;\n }\n}\n"
558
+ }
559
+ ]
560
+ },
537
561
  {
538
562
  "name": "separator",
539
563
  "type": "registry:ui",
@@ -659,7 +683,7 @@
659
683
  {
660
684
  "path": "components/ui/tooltip/tooltip.module.css",
661
685
  "type": "registry:ui",
662
- "content": "/* Tooltip content */\n.content {\n z-index: 50;\n display: inline-flex;\n width: fit-content;\n max-width: 20rem;\n align-items: center;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--tooltip-bg, var(--surface-overlay, #111827));\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--tooltip-text, var(--text-inverse, #ffffff));\n box-shadow: var(--shadow-md);\n animation-duration: var(--motion-duration-150, 150ms);\n animation-timing-function: var(--motion-easing-enter, ease-out);\n}\n\n.content[data-state=\"delayed-open\"] {\n animation-name: fadeIn;\n}\n\n.content[data-state=\"closed\"] {\n animation-name: fadeOut;\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@keyframes fadeOut {\n from {\n opacity: 1;\n transform: scale(1);\n }\n to {\n opacity: 0;\n transform: scale(0.95);\n }\n}\n"
686
+ "content": "/* Tooltip content */\n.content {\n z-index: 50;\n display: inline-flex;\n width: fit-content;\n max-width: 20rem;\n align-items: center;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n border-radius: var(--radius-lg, 0.5rem);\n background-color: var(--tooltip-bg, var(--surface-overlay, #111827));\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n color: var(--tooltip-text, var(--text-inverse, #ffffff));\n box-shadow: var(--shadow-md);\n animation-duration: var(--motion-duration-150, 150ms);\n animation-timing-function: var(--motion-easing-enter, ease-out);\n}\n\n/* Dark mode default (VI-683). Both of the inverse pair resolve dark there, so\n the label vanishes. Flip to the primary ink as ground under the inverse ink\n (the dark ink Calendar, Badge and Chip also read). A theme binding of\n --tooltip-bg / --tooltip-text still wins; light mode is untouched.\n :where() keeps this at the specificity of .content. */\n:global(:where(.dark)) .content,\n:global(:where([data-theme=\"dark\"])) .content {\n background-color: var(--tooltip-bg, var(--text-primary, #f9fafb));\n color: var(--tooltip-text, var(--text-inverse, #111827));\n}\n\n.content[data-state=\"delayed-open\"] {\n animation-name: fadeIn;\n}\n\n.content[data-state=\"closed\"] {\n animation-name: fadeOut;\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@keyframes fadeOut {\n from {\n opacity: 1;\n transform: scale(1);\n }\n to {\n opacity: 0;\n transform: scale(0.95);\n }\n}\n"
663
687
  }
664
688
  ]
665
689
  },
@@ -975,23 +999,25 @@
975
999
  "description": "A feature-rich collapsible sidebar component with provider, menu, groups, and sub-menus.",
976
1000
  "category": "layout",
977
1001
  "dependencies": [
1002
+ "@radix-ui/react-slot",
978
1003
  "class-variance-authority",
979
1004
  "@phosphor-icons/react",
980
1005
  "@loworbitstudio/visor-core"
981
1006
  ],
982
1007
  "registryDependencies": [
983
- "utils"
1008
+ "utils",
1009
+ "tooltip"
984
1010
  ],
985
1011
  "files": [
986
1012
  {
987
1013
  "path": "components/ui/sidebar/sidebar.tsx",
988
1014
  "type": "registry:ui",
989
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { SidebarIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./sidebar.module.css\"\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\"\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7\nconst SIDEBAR_WIDTH = \"16rem\"\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\"\nconst SIDEBAR_WIDTH_ICON = \"3rem\"\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\"\nconst SIDEBAR_MOBILE_MEDIA_QUERY = \"(max-width: 768px)\"\n\n// `isMobile` is derived from a `matchMedia` subscription via\n// `useSyncExternalStore` (below) instead of a `useState` + `useEffect` pair.\n// This keeps SSR safe — the server snapshot is always `false` — while avoiding\n// a synchronous `setState` inside an effect body, which the React 19 compiler\n// lint (`react-hooks/set-state-in-effect`) flags as a cascading-render hazard.\nfunction subscribeIsMobile(callback: () => void) {\n const mql = window.matchMedia(SIDEBAR_MOBILE_MEDIA_QUERY)\n mql.addEventListener(\"change\", callback)\n return () => mql.removeEventListener(\"change\", callback)\n}\n\nfunction getIsMobileSnapshot() {\n return window.matchMedia(SIDEBAR_MOBILE_MEDIA_QUERY).matches\n}\n\nfunction getIsMobileServerSnapshot() {\n return false\n}\n\ntype SidebarContextProps = {\n state: \"expanded\" | \"collapsed\"\n open: boolean\n setOpen: (open: boolean) => void\n openMobile: boolean\n setOpenMobile: (open: boolean) => void\n isMobile: boolean\n toggleSidebar: () => void\n}\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null)\n\nfunction useSidebar() {\n const context = React.useContext(SidebarContext)\n if (!context) {\n throw new Error(\"useSidebar must be used within a SidebarProvider.\")\n }\n return context\n}\n\nexport interface SidebarProviderProps extends React.ComponentProps<\"div\"> {\n defaultOpen?: boolean\n open?: boolean\n onOpenChange?: (open: boolean) => void\n}\n\nconst SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(\n (\n {\n defaultOpen = true,\n open: openProp,\n onOpenChange: setOpenProp,\n className,\n style,\n children,\n ...props\n },\n ref\n ) => {\n const isMobile = React.useSyncExternalStore(\n subscribeIsMobile,\n getIsMobileSnapshot,\n getIsMobileServerSnapshot\n )\n const [openMobile, setOpenMobile] = React.useState(false)\n const [_open, _setOpen] = React.useState(defaultOpen)\n const open = openProp ?? _open\n\n const setOpen = React.useCallback(\n (value: boolean | ((value: boolean) => boolean)) => {\n const openState = typeof value === \"function\" ? value(open) : value\n if (setOpenProp) {\n setOpenProp(openState)\n } else {\n _setOpen(openState)\n }\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`\n },\n [setOpenProp, open]\n )\n\n const toggleSidebar = React.useCallback(() => {\n return isMobile\n ? setOpenMobile((prev) => !prev)\n : setOpen((prev) => !prev)\n }, [isMobile, setOpen, setOpenMobile])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n toggleSidebar()\n }\n }\n window.addEventListener(\"keydown\", handleKeyDown)\n return () => window.removeEventListener(\"keydown\", handleKeyDown)\n }, [toggleSidebar])\n\n const state = open ? \"expanded\" : \"collapsed\"\n\n const contextValue = React.useMemo<SidebarContextProps>(\n () => ({\n state,\n open,\n setOpen,\n isMobile,\n openMobile,\n setOpenMobile,\n toggleSidebar,\n }),\n [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]\n )\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <div\n ref={ref}\n data-slot=\"sidebar-wrapper\"\n style={\n {\n \"--sidebar-width\": SIDEBAR_WIDTH,\n \"--sidebar-width-mobile\": SIDEBAR_WIDTH_MOBILE,\n \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n ...style,\n } as React.CSSProperties\n }\n className={cn(styles.wrapper, className)}\n {...props}\n >\n {children}\n </div>\n </SidebarContext.Provider>\n )\n }\n)\nSidebarProvider.displayName = \"SidebarProvider\"\n\nexport interface SidebarProps extends React.ComponentProps<\"div\"> {\n side?: \"left\" | \"right\"\n variant?: \"sidebar\" | \"floating\" | \"inset\"\n collapsible?: \"offcanvas\" | \"icon\" | \"none\"\n}\n\nconst Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(\n (\n {\n side = \"left\",\n variant = \"sidebar\",\n collapsible = \"offcanvas\",\n className,\n children,\n ...props\n },\n ref\n ) => {\n const { state } = useSidebar()\n\n if (collapsible === \"none\") {\n return (\n <div\n ref={ref}\n data-slot=\"sidebar\"\n className={cn(styles.sidebarStatic, className)}\n {...props}\n >\n {children}\n </div>\n )\n }\n\n return (\n <div\n ref={ref}\n className={cn(styles.sidebarContainer, className)}\n data-state={state}\n data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n data-variant={variant}\n data-side={side}\n data-slot=\"sidebar\"\n {...props}\n >\n <div\n data-slot=\"sidebar-gap\"\n className={styles.sidebarGap}\n />\n <div\n data-slot=\"sidebar-inner\"\n data-side={side}\n className={styles.sidebarInner}\n >\n <div\n data-sidebar=\"sidebar\"\n className={styles.sidebarContent}\n >\n {children}\n </div>\n </div>\n </div>\n )\n }\n)\nSidebar.displayName = \"Sidebar\"\n\nconst SidebarTrigger = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\">\n>(({ className, onClick, children, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <button\n ref={ref}\n data-sidebar=\"trigger\"\n data-slot=\"sidebar-trigger\"\n className={cn(styles.trigger, className)}\n onClick={(event) => {\n onClick?.(event)\n toggleSidebar()\n }}\n {...props}\n >\n {children ?? (\n <>\n <SidebarIcon />\n <span className={styles.srOnly}>Toggle Sidebar</span>\n </>\n )}\n </button>\n )\n})\nSidebarTrigger.displayName = \"SidebarTrigger\"\n\nconst SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<\"button\">>(\n ({ className, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <button\n ref={ref}\n data-sidebar=\"rail\"\n data-slot=\"sidebar-rail\"\n aria-label=\"Toggle Sidebar\"\n tabIndex={-1}\n onClick={toggleSidebar}\n title=\"Toggle Sidebar\"\n className={cn(styles.rail, className)}\n {...props}\n />\n )\n }\n)\nSidebarRail.displayName = \"SidebarRail\"\n\nconst SidebarInset = React.forwardRef<HTMLElement, React.ComponentProps<\"main\">>(\n ({ className, ...props }, ref) => (\n <main\n ref={ref}\n data-slot=\"sidebar-inset\"\n className={cn(styles.inset, className)}\n {...props}\n />\n )\n)\nSidebarInset.displayName = \"SidebarInset\"\n\nconst SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-header\"\n data-sidebar=\"header\"\n className={cn(styles.header, className)}\n {...props}\n />\n )\n)\nSidebarHeader.displayName = \"SidebarHeader\"\n\nconst SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-footer\"\n data-sidebar=\"footer\"\n className={cn(styles.footer, className)}\n {...props}\n />\n )\n)\nSidebarFooter.displayName = \"SidebarFooter\"\n\nconst SidebarSeparator = React.forwardRef<HTMLHRElement, React.ComponentProps<\"hr\">>(\n ({ className, ...props }, ref) => (\n <hr\n ref={ref}\n data-slot=\"sidebar-separator\"\n data-sidebar=\"separator\"\n className={cn(styles.separator, className)}\n {...props}\n />\n )\n)\nSidebarSeparator.displayName = \"SidebarSeparator\"\n\nconst SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-content\"\n data-sidebar=\"content\"\n className={cn(styles.contentArea, className)}\n {...props}\n />\n )\n)\nSidebarContent.displayName = \"SidebarContent\"\n\nconst SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group\"\n data-sidebar=\"group\"\n className={cn(styles.group, className)}\n {...props}\n />\n )\n)\nSidebarGroup.displayName = \"SidebarGroup\"\n\nconst SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group-label\"\n data-sidebar=\"group-label\"\n className={cn(styles.groupLabel, className)}\n {...props}\n />\n )\n)\nSidebarGroupLabel.displayName = \"SidebarGroupLabel\"\n\nconst SidebarGroupAction = React.forwardRef<HTMLButtonElement, React.ComponentProps<\"button\">>(\n ({ className, ...props }, ref) => (\n <button\n ref={ref}\n data-slot=\"sidebar-group-action\"\n data-sidebar=\"group-action\"\n className={cn(styles.groupAction, className)}\n {...props}\n />\n )\n)\nSidebarGroupAction.displayName = \"SidebarGroupAction\"\n\nconst SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group-content\"\n data-sidebar=\"group-content\"\n className={cn(styles.groupContent, className)}\n {...props}\n />\n )\n)\nSidebarGroupContent.displayName = \"SidebarGroupContent\"\n\nconst SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(\n ({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-slot=\"sidebar-menu\"\n data-sidebar=\"menu\"\n className={cn(styles.menu, className)}\n {...props}\n />\n )\n)\nSidebarMenu.displayName = \"SidebarMenu\"\n\nconst SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(\n ({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-slot=\"sidebar-menu-item\"\n data-sidebar=\"menu-item\"\n className={cn(styles.menuItem, className)}\n {...props}\n />\n )\n)\nSidebarMenuItem.displayName = \"SidebarMenuItem\"\n\nconst sidebarMenuButtonVariants = cva(styles.menuButton, {\n variants: {\n variant: {\n default: styles.menuButtonDefault,\n outline: styles.menuButtonOutline,\n },\n size: {\n default: styles.menuButtonSizeDefault,\n sm: styles.menuButtonSizeSm,\n lg: styles.menuButtonSizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n})\n\nexport interface SidebarMenuButtonProps\n extends React.ComponentProps<\"button\">,\n VariantProps<typeof sidebarMenuButtonVariants> {\n asChild?: boolean\n isActive?: boolean\n tooltip?: string\n}\n\nconst SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(\n ({ className, variant = \"default\", size = \"default\", isActive, children, ...props }, ref) => (\n <button\n ref={ref}\n data-slot=\"sidebar-menu-button\"\n data-sidebar=\"menu-button\"\n data-size={size}\n data-active={isActive || undefined}\n className={cn(sidebarMenuButtonVariants({ variant, size }), className)}\n {...props}\n >\n {children}\n </button>\n )\n)\nSidebarMenuButton.displayName = \"SidebarMenuButton\"\n\nconst SidebarMenuAction = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\"> & { showOnHover?: boolean }\n>(({ className, showOnHover, ...props }, ref) => (\n <button\n ref={ref}\n data-slot=\"sidebar-menu-action\"\n data-sidebar=\"menu-action\"\n className={cn(\n styles.menuAction,\n showOnHover && styles.menuActionHover,\n className\n )}\n {...props}\n />\n))\nSidebarMenuAction.displayName = \"SidebarMenuAction\"\n\nconst SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-menu-badge\"\n data-sidebar=\"menu-badge\"\n className={cn(styles.menuBadge, className)}\n {...props}\n />\n )\n)\nSidebarMenuBadge.displayName = \"SidebarMenuBadge\"\n\nconst SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(\n ({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-slot=\"sidebar-menu-sub\"\n data-sidebar=\"menu-sub\"\n className={cn(styles.menuSub, className)}\n {...props}\n />\n )\n)\nSidebarMenuSub.displayName = \"SidebarMenuSub\"\n\nconst SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(\n ({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-slot=\"sidebar-menu-sub-item\"\n data-sidebar=\"menu-sub-item\"\n className={cn(styles.menuSubItem, className)}\n {...props}\n />\n )\n)\nSidebarMenuSubItem.displayName = \"SidebarMenuSubItem\"\n\nexport interface SidebarMenuSubButtonProps extends React.ComponentProps<\"a\"> {\n asChild?: boolean\n size?: \"sm\" | \"md\"\n isActive?: boolean\n}\n\nconst SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(\n ({ className, size = \"md\", isActive, ...props }, ref) => (\n <a\n ref={ref}\n data-slot=\"sidebar-menu-sub-button\"\n data-sidebar=\"menu-sub-button\"\n data-size={size}\n data-active={isActive || undefined}\n className={cn(\n styles.menuSubButton,\n size === \"sm\" && styles.menuSubButtonSm,\n className\n )}\n {...props}\n />\n )\n)\nSidebarMenuSubButton.displayName = \"SidebarMenuSubButton\"\n\nexport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupAction,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarInset,\n SidebarMenu,\n SidebarMenuAction,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSub,\n SidebarMenuSubButton,\n SidebarMenuSubItem,\n SidebarProvider,\n SidebarRail,\n SidebarSeparator,\n SidebarTrigger,\n useSidebar,\n sidebarMenuButtonVariants,\n}\n"
1015
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { SidebarIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n type TooltipContentProps,\n} from \"../tooltip/tooltip\"\nimport styles from \"./sidebar.module.css\"\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\"\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7\nconst SIDEBAR_WIDTH = \"16rem\"\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\"\nconst SIDEBAR_WIDTH_ICON = \"3rem\"\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\"\nconst SIDEBAR_MOBILE_MEDIA_QUERY = \"(max-width: 768px)\"\n\n// `isMobile` is derived from a `matchMedia` subscription via\n// `useSyncExternalStore` (below) instead of a `useState` + `useEffect` pair.\n// This keeps SSR safe — the server snapshot is always `false` — while avoiding\n// a synchronous `setState` inside an effect body, which the React 19 compiler\n// lint (`react-hooks/set-state-in-effect`) flags as a cascading-render hazard.\nfunction subscribeIsMobile(callback: () => void) {\n const mql = window.matchMedia(SIDEBAR_MOBILE_MEDIA_QUERY)\n mql.addEventListener(\"change\", callback)\n return () => mql.removeEventListener(\"change\", callback)\n}\n\nfunction getIsMobileSnapshot() {\n return window.matchMedia(SIDEBAR_MOBILE_MEDIA_QUERY).matches\n}\n\nfunction getIsMobileServerSnapshot() {\n return false\n}\n\ntype SidebarContextProps = {\n state: \"expanded\" | \"collapsed\"\n open: boolean\n setOpen: (open: boolean) => void\n openMobile: boolean\n setOpenMobile: (open: boolean) => void\n isMobile: boolean\n toggleSidebar: () => void\n /** Used by Sidebar to tell the provider what kind of sidebars are mounted. */\n registerSidebar: (collapsible: SidebarCollapsible) => () => void\n}\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null)\n\n/**\n * What the nearest Sidebar tells its menu buttons: whether a button's\n * `tooltip` applies, and where the tooltip portals (the Sidebar's own element,\n * so a class-scoped theme on an ancestor of the sidebar reaches it).\n */\ntype SidebarScopeProps = {\n collapsible: SidebarCollapsible\n container: HTMLElement | null\n}\n\nconst SidebarScopeContext = React.createContext<SidebarScopeProps | null>(null)\n\nfunction useSidebar() {\n const context = React.useContext(SidebarContext)\n if (!context) {\n throw new Error(\"useSidebar must be used within a SidebarProvider.\")\n }\n return context\n}\n\nexport interface SidebarProviderProps extends React.ComponentProps<\"div\"> {\n defaultOpen?: boolean\n open?: boolean\n onOpenChange?: (open: boolean) => void\n /**\n * Binds Cmd/Ctrl+B to toggle the sidebar. Defaults to `true`. The binding is\n * skipped, and `preventDefault` is not called, while the only sidebars\n * mounted are `collapsible=\"none\"` (nothing would toggle). Pass `false` to\n * never bind it.\n */\n keyboardShortcut?: boolean\n}\n\nconst SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(\n (\n {\n defaultOpen = true,\n open: openProp,\n onOpenChange: setOpenProp,\n keyboardShortcut = true,\n className,\n style,\n children,\n ...props\n },\n ref\n ) => {\n const isMobile = React.useSyncExternalStore(\n subscribeIsMobile,\n getIsMobileSnapshot,\n getIsMobileServerSnapshot\n )\n const [openMobile, setOpenMobile] = React.useState(false)\n const [_open, _setOpen] = React.useState(defaultOpen)\n const open = openProp ?? _open\n\n const setOpen = React.useCallback(\n (value: boolean | ((value: boolean) => boolean)) => {\n const openState = typeof value === \"function\" ? value(open) : value\n if (setOpenProp) {\n setOpenProp(openState)\n } else {\n _setOpen(openState)\n }\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`\n },\n [setOpenProp, open]\n )\n\n const toggleSidebar = React.useCallback(() => {\n return isMobile\n ? setOpenMobile((prev) => !prev)\n : setOpen((prev) => !prev)\n }, [isMobile, setOpen, setOpenMobile])\n\n // How many collapsible and static (`collapsible=\"none\"`) sidebars are\n // mounted under this provider. A static-only provider has nothing to toggle.\n const mountedRef = React.useRef({ collapsible: 0, none: 0 })\n const registerSidebar = React.useCallback((collapsible: SidebarCollapsible) => {\n const key = collapsible === \"none\" ? \"none\" : \"collapsible\"\n mountedRef.current[key] += 1\n return () => {\n mountedRef.current[key] -= 1\n }\n }, [])\n\n React.useEffect(() => {\n if (!keyboardShortcut) return\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {\n const { collapsible, none } = mountedRef.current\n if (none > 0 && collapsible === 0) return\n event.preventDefault()\n toggleSidebar()\n }\n }\n window.addEventListener(\"keydown\", handleKeyDown)\n return () => window.removeEventListener(\"keydown\", handleKeyDown)\n }, [keyboardShortcut, toggleSidebar])\n\n const state = open ? \"expanded\" : \"collapsed\"\n\n const contextValue = React.useMemo<SidebarContextProps>(\n () => ({\n state,\n open,\n setOpen,\n isMobile,\n openMobile,\n setOpenMobile,\n toggleSidebar,\n registerSidebar,\n }),\n [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, registerSidebar]\n )\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <TooltipProvider delayDuration={200}>\n <div\n ref={ref}\n data-slot=\"sidebar-wrapper\"\n style={\n {\n \"--sidebar-width\": SIDEBAR_WIDTH,\n \"--sidebar-width-mobile\": SIDEBAR_WIDTH_MOBILE,\n \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n ...style,\n } as React.CSSProperties\n }\n className={cn(styles.wrapper, className)}\n {...props}\n >\n {children}\n </div>\n </TooltipProvider>\n </SidebarContext.Provider>\n )\n }\n)\nSidebarProvider.displayName = \"SidebarProvider\"\n\ntype SidebarCollapsible = \"offcanvas\" | \"icon\" | \"none\"\n\nexport interface SidebarProps extends React.ComponentProps<\"div\"> {\n side?: \"left\" | \"right\"\n variant?: \"sidebar\" | \"floating\" | \"inset\"\n collapsible?: SidebarCollapsible\n /**\n * `fixed` (default) pins the sidebar to the viewport at full height, the\n * page-sidebar shape. `contained` lays it out inside its parent instead, so\n * an icon rail can sit under an app bar without covering it. A contained\n * sidebar is as tall as the SidebarProvider that holds it.\n */\n position?: \"fixed\" | \"contained\"\n}\n\nconst Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(\n (\n {\n side = \"left\",\n variant = \"sidebar\",\n collapsible = \"offcanvas\",\n position = \"fixed\",\n className,\n children,\n ...props\n },\n ref\n ) => {\n const { state, registerSidebar } = useSidebar()\n const [element, setElement] = React.useState<HTMLDivElement | null>(null)\n const setRefs = React.useCallback(\n (node: HTMLDivElement | null) => {\n setElement(node)\n if (typeof ref === \"function\") ref(node)\n else if (ref) ref.current = node\n },\n [ref]\n )\n\n React.useEffect(() => registerSidebar(collapsible), [registerSidebar, collapsible])\n\n const scope = React.useMemo<SidebarScopeProps>(\n () => ({ collapsible, container: element }),\n [collapsible, element]\n )\n\n if (collapsible === \"none\") {\n return (\n <SidebarScopeContext.Provider value={scope}>\n <div\n ref={setRefs}\n data-slot=\"sidebar\"\n className={cn(styles.sidebarStatic, className)}\n {...props}\n >\n {children}\n </div>\n </SidebarScopeContext.Provider>\n )\n }\n\n return (\n <SidebarScopeContext.Provider value={scope}>\n <div\n ref={setRefs}\n className={cn(styles.sidebarContainer, className)}\n data-state={state}\n data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n data-variant={variant}\n data-position={position}\n data-side={side}\n data-slot=\"sidebar\"\n {...props}\n >\n <div\n data-slot=\"sidebar-gap\"\n className={styles.sidebarGap}\n />\n <div\n data-slot=\"sidebar-inner\"\n data-side={side}\n className={styles.sidebarInner}\n >\n <div\n data-sidebar=\"sidebar\"\n className={styles.sidebarContent}\n >\n {children}\n </div>\n </div>\n </div>\n </SidebarScopeContext.Provider>\n )\n }\n)\nSidebar.displayName = \"Sidebar\"\n\nconst SidebarTrigger = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\">\n>(({ className, onClick, children, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <button\n ref={ref}\n data-sidebar=\"trigger\"\n data-slot=\"sidebar-trigger\"\n className={cn(styles.trigger, className)}\n onClick={(event) => {\n onClick?.(event)\n toggleSidebar()\n }}\n {...props}\n >\n {children ?? (\n <>\n <SidebarIcon />\n <span className={styles.srOnly}>Toggle Sidebar</span>\n </>\n )}\n </button>\n )\n})\nSidebarTrigger.displayName = \"SidebarTrigger\"\n\nconst SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<\"button\">>(\n ({ className, ...props }, ref) => {\n const { toggleSidebar } = useSidebar()\n\n return (\n <button\n ref={ref}\n data-sidebar=\"rail\"\n data-slot=\"sidebar-rail\"\n aria-label=\"Toggle Sidebar\"\n tabIndex={-1}\n onClick={toggleSidebar}\n title=\"Toggle Sidebar\"\n className={cn(styles.rail, className)}\n {...props}\n />\n )\n }\n)\nSidebarRail.displayName = \"SidebarRail\"\n\nconst SidebarInset = React.forwardRef<HTMLElement, React.ComponentProps<\"main\">>(\n ({ className, ...props }, ref) => (\n <main\n ref={ref}\n data-slot=\"sidebar-inset\"\n className={cn(styles.inset, className)}\n {...props}\n />\n )\n)\nSidebarInset.displayName = \"SidebarInset\"\n\nconst SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-header\"\n data-sidebar=\"header\"\n className={cn(styles.header, className)}\n {...props}\n />\n )\n)\nSidebarHeader.displayName = \"SidebarHeader\"\n\nconst SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-footer\"\n data-sidebar=\"footer\"\n className={cn(styles.footer, className)}\n {...props}\n />\n )\n)\nSidebarFooter.displayName = \"SidebarFooter\"\n\nconst SidebarSeparator = React.forwardRef<HTMLHRElement, React.ComponentProps<\"hr\">>(\n ({ className, ...props }, ref) => (\n <hr\n ref={ref}\n data-slot=\"sidebar-separator\"\n data-sidebar=\"separator\"\n className={cn(styles.separator, className)}\n {...props}\n />\n )\n)\nSidebarSeparator.displayName = \"SidebarSeparator\"\n\nconst SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-content\"\n data-sidebar=\"content\"\n className={cn(styles.contentArea, className)}\n {...props}\n />\n )\n)\nSidebarContent.displayName = \"SidebarContent\"\n\nconst SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group\"\n data-sidebar=\"group\"\n className={cn(styles.group, className)}\n {...props}\n />\n )\n)\nSidebarGroup.displayName = \"SidebarGroup\"\n\nconst SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group-label\"\n data-sidebar=\"group-label\"\n className={cn(styles.groupLabel, className)}\n {...props}\n />\n )\n)\nSidebarGroupLabel.displayName = \"SidebarGroupLabel\"\n\nconst SidebarGroupAction = React.forwardRef<HTMLButtonElement, React.ComponentProps<\"button\">>(\n ({ className, ...props }, ref) => (\n <button\n ref={ref}\n data-slot=\"sidebar-group-action\"\n data-sidebar=\"group-action\"\n className={cn(styles.groupAction, className)}\n {...props}\n />\n )\n)\nSidebarGroupAction.displayName = \"SidebarGroupAction\"\n\nconst SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-group-content\"\n data-sidebar=\"group-content\"\n className={cn(styles.groupContent, className)}\n {...props}\n />\n )\n)\nSidebarGroupContent.displayName = \"SidebarGroupContent\"\n\nconst SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(\n ({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-slot=\"sidebar-menu\"\n data-sidebar=\"menu\"\n className={cn(styles.menu, className)}\n {...props}\n />\n )\n)\nSidebarMenu.displayName = \"SidebarMenu\"\n\nconst SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(\n ({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-slot=\"sidebar-menu-item\"\n data-sidebar=\"menu-item\"\n className={cn(styles.menuItem, className)}\n {...props}\n />\n )\n)\nSidebarMenuItem.displayName = \"SidebarMenuItem\"\n\nconst sidebarMenuButtonVariants = cva(styles.menuButton, {\n variants: {\n variant: {\n default: styles.menuButtonDefault,\n outline: styles.menuButtonOutline,\n },\n size: {\n default: styles.menuButtonSizeDefault,\n sm: styles.menuButtonSizeSm,\n lg: styles.menuButtonSizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n})\n\nexport type SidebarMenuButtonTooltip =\n | string\n | (Omit<TooltipContentProps, \"children\"> & { children: React.ReactNode })\n\nexport interface SidebarMenuButtonProps\n extends React.ComponentProps<\"button\">,\n VariantProps<typeof sidebarMenuButtonVariants> {\n /** Render the child (a Next `<Link>`, an `<a>`) as the button, via Radix Slot. */\n asChild?: boolean\n isActive?: boolean\n /**\n * Shown in a Tooltip when the sidebar is an icon rail: collapsed to icons\n * (`collapsible=\"icon\"`), or a `collapsible=\"none\"` sidebar (a static rail has\n * no collapsed state, so passing `tooltip` is the opt-in). Never shown on\n * mobile or in an expanded sidebar, where the visible label names the item.\n * A string (or a string `children`) also names the button for assistive tech\n * while the label is hidden, unless the button sets its own `aria-label` or\n * `aria-labelledby`. Pass an object to set `side`, `container` and the rest of\n * TooltipContent's props; the tooltip portals into the Sidebar by default.\n */\n tooltip?: SidebarMenuButtonTooltip\n}\n\nconst SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(\n (\n {\n asChild = false,\n className,\n variant = \"default\",\n size = \"default\",\n isActive,\n tooltip,\n children,\n ...props\n },\n ref\n ) => {\n const { state, isMobile } = useSidebar()\n const scope = React.useContext(SidebarScopeContext)\n const [tooltipOpen, setTooltipOpen] = React.useState(false)\n\n const tooltipApplies =\n tooltip != null &&\n !isMobile &&\n scope != null &&\n (scope.collapsible === \"none\" ||\n (scope.collapsible === \"icon\" && state === \"collapsed\"))\n\n const tooltipProps = typeof tooltip === \"object\" ? tooltip : undefined\n const tooltipLabel = typeof tooltip === \"string\" ? tooltip : tooltipProps?.children\n const needsName =\n tooltipApplies &&\n typeof tooltipLabel === \"string\" &&\n props[\"aria-label\"] === undefined &&\n props[\"aria-labelledby\"] === undefined\n\n const Comp = asChild ? Slot : \"button\"\n const button = (\n <Comp\n ref={ref}\n data-slot=\"sidebar-menu-button\"\n data-sidebar=\"menu-button\"\n data-size={size}\n data-active={isActive || undefined}\n className={cn(sidebarMenuButtonVariants({ variant, size }), className)}\n {...(needsName ? { \"aria-label\": tooltipLabel as string } : null)}\n {...props}\n >\n {children}\n </Comp>\n )\n\n if (tooltip == null) return button\n\n const { children: tooltipChildren, ...contentProps } =\n typeof tooltip === \"object\" ? tooltip : { children: tooltip }\n\n return (\n <Tooltip open={tooltipApplies && tooltipOpen} onOpenChange={setTooltipOpen}>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent\n side=\"right\"\n container={scope?.container}\n {...contentProps}\n >\n {tooltipChildren}\n </TooltipContent>\n </Tooltip>\n )\n }\n)\nSidebarMenuButton.displayName = \"SidebarMenuButton\"\n\nconst SidebarMenuAction = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\"> & { showOnHover?: boolean }\n>(({ className, showOnHover, ...props }, ref) => (\n <button\n ref={ref}\n data-slot=\"sidebar-menu-action\"\n data-sidebar=\"menu-action\"\n className={cn(\n styles.menuAction,\n showOnHover && styles.menuActionHover,\n className\n )}\n {...props}\n />\n))\nSidebarMenuAction.displayName = \"SidebarMenuAction\"\n\nconst SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<\"div\">>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"sidebar-menu-badge\"\n data-sidebar=\"menu-badge\"\n className={cn(styles.menuBadge, className)}\n {...props}\n />\n )\n)\nSidebarMenuBadge.displayName = \"SidebarMenuBadge\"\n\nconst SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<\"ul\">>(\n ({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-slot=\"sidebar-menu-sub\"\n data-sidebar=\"menu-sub\"\n className={cn(styles.menuSub, className)}\n {...props}\n />\n )\n)\nSidebarMenuSub.displayName = \"SidebarMenuSub\"\n\nconst SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<\"li\">>(\n ({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-slot=\"sidebar-menu-sub-item\"\n data-sidebar=\"menu-sub-item\"\n className={cn(styles.menuSubItem, className)}\n {...props}\n />\n )\n)\nSidebarMenuSubItem.displayName = \"SidebarMenuSubItem\"\n\nexport interface SidebarMenuSubButtonProps extends React.ComponentProps<\"a\"> {\n asChild?: boolean\n size?: \"sm\" | \"md\"\n isActive?: boolean\n}\n\nconst SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(\n ({ className, size = \"md\", isActive, ...props }, ref) => (\n <a\n ref={ref}\n data-slot=\"sidebar-menu-sub-button\"\n data-sidebar=\"menu-sub-button\"\n data-size={size}\n data-active={isActive || undefined}\n className={cn(\n styles.menuSubButton,\n size === \"sm\" && styles.menuSubButtonSm,\n className\n )}\n {...props}\n />\n )\n)\nSidebarMenuSubButton.displayName = \"SidebarMenuSubButton\"\n\nexport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupAction,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarInset,\n SidebarMenu,\n SidebarMenuAction,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSub,\n SidebarMenuSubButton,\n SidebarMenuSubItem,\n SidebarProvider,\n SidebarRail,\n SidebarSeparator,\n SidebarTrigger,\n useSidebar,\n sidebarMenuButtonVariants,\n}\n"
990
1016
  },
991
1017
  {
992
1018
  "path": "components/ui/sidebar/sidebar.module.css",
993
1019
  "type": "registry:ui",
994
- "content": "/* Sidebar wrapper */\n.wrapper {\n display: flex;\n min-height: 100svh;\n width: 100%;\n background-color: var(--sidebar-bg);\n}\n\n/* Sidebar static (collapsible=none) */\n.sidebarStatic {\n display: flex;\n height: 100%;\n width: var(--sidebar-width, 16rem);\n flex-direction: column;\n background-color: var(--sidebar-bg);\n color: var(--sidebar-text);\n}\n\n/* Sidebar container (collapsible versions) */\n.sidebarContainer {\n position: relative;\n color: var(--sidebar-text);\n}\n\n/* Sidebar gap (reserves layout space) */\n.sidebarGap {\n width: var(--sidebar-width, 16rem);\n background: transparent;\n transition: width var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n flex-shrink: 0;\n}\n\n.sidebarContainer[data-state=\"collapsed\"][data-collapsible=\"offcanvas\"] .sidebarGap {\n width: 0;\n}\n\n.sidebarContainer[data-state=\"collapsed\"][data-collapsible=\"icon\"] .sidebarGap {\n width: var(--sidebar-width-icon, 3rem);\n}\n\n/* Sidebar inner */\n.sidebarInner {\n position: fixed;\n inset-block: 0;\n z-index: 10;\n display: flex;\n height: 100svh;\n width: var(--sidebar-width, 16rem);\n transition: left var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), right var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), width var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n.sidebarInner[data-side=\"left\"] {\n left: 0;\n}\n\n.sidebarContainer[data-state=\"collapsed\"][data-collapsible=\"offcanvas\"] .sidebarInner[data-side=\"left\"] {\n left: calc(var(--sidebar-width, 16rem) * -1);\n}\n\n.sidebarInner[data-side=\"right\"] {\n right: 0;\n}\n\n.sidebarContainer[data-state=\"collapsed\"][data-collapsible=\"offcanvas\"] .sidebarInner[data-side=\"right\"] {\n right: calc(var(--sidebar-width, 16rem) * -1);\n}\n\n.sidebarContainer[data-state=\"collapsed\"][data-collapsible=\"icon\"] .sidebarInner {\n width: var(--sidebar-width-icon, 3rem);\n}\n\n/* Sidebar content container */\n.sidebarContent {\n display: flex;\n width: 100%;\n height: 100%;\n flex-direction: column;\n background-color: var(--sidebar-bg);\n overflow: hidden;\n}\n\n.sidebarContainer[data-variant=\"floating\"] .sidebarContent,\n.sidebarContainer[data-variant=\"inset\"] .sidebarContent {\n border-radius: var(--radius-lg, 0.5rem);\n box-shadow: var(--shadow-sm);\n}\n\n/* Trigger button */\n.trigger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: transparent;\n cursor: pointer;\n border-radius: var(--radius-sm, 0.25rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.5);\n color: var(--text-primary, #111827);\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.trigger:active {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .trigger:hover {\n background-color: var(--surface-muted, #f3f4f6);\n }\n}\n\n.trigger:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Rail */\n.rail {\n position: absolute;\n inset-block: 0;\n z-index: 20;\n width: 1rem;\n border: none;\n background: transparent;\n cursor: col-resize;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n@media (hover: hover) {\n .rail:hover {\n background-color: var(--sidebar-border);\n }\n}\n\n/* Inset main content area */\n.inset {\n position: relative;\n display: flex;\n width: 100%;\n flex: 1;\n flex-direction: column;\n background-color: var(--surface-page, #ffffff);\n}\n\n/* Header */\n.header {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--sidebar-header-padding, var(--spacing-2, 0.5rem));\n}\n\n/* Footer */\n.footer {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--sidebar-footer-padding, var(--spacing-2, 0.5rem));\n}\n\n/* Separator */\n.separator {\n margin-inline: var(--spacing-2, 0.5rem);\n border: none;\n height: 1px;\n background-color: var(--sidebar-border);\n}\n\n/* Content area */\n.contentArea {\n display: flex;\n min-height: 0;\n flex: 1;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n overflow: auto;\n padding: var(--sidebar-content-padding, var(--spacing-1, 0.25rem));\n}\n\n/* Group */\n.group {\n position: relative;\n display: flex;\n width: 100%;\n min-width: 0;\n flex-direction: column;\n padding: var(--spacing-2, 0.5rem);\n}\n\n/* Group label */\n.groupLabel {\n display: flex;\n height: var(--sidebar-group-label-height, 2rem);\n flex-shrink: 0;\n align-items: center;\n border-radius: var(--radius-sm, 0.25rem);\n padding-inline: var(--spacing-2, 0.5rem);\n font-size: var(--sidebar-group-label-font-size, var(--font-size-xs, 0.75rem));\n font-weight: var(--sidebar-group-label-font-weight, var(--font-weight-medium, 500));\n letter-spacing: var(--sidebar-group-label-letter-spacing, inherit);\n text-transform: var(--sidebar-group-label-text-transform, inherit);\n color: var(--sidebar-text-muted);\n transition: margin var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), opacity var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Group action */\n.groupAction {\n position: absolute;\n top: 0.875rem;\n right: 0.75rem;\n display: flex;\n aspect-ratio: 1;\n width: 1.25rem;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: none;\n background: transparent;\n cursor: pointer;\n padding: 0;\n color: var(--sidebar-text);\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.groupAction:active {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .groupAction:hover {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n }\n}\n\n/* Group content */\n.groupContent {\n width: 100%;\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Menu */\n.menu {\n display: flex;\n width: 100%;\n min-width: 0;\n flex-direction: column;\n gap: var(--sidebar-menu-gap, var(--spacing-1, 0.25rem));\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/* Menu item */\n.menuItem {\n position: relative;\n}\n\n/* Menu button base — the nav-item treatment. VI-625 hooks let a theme pin the\n item shape (radius, height, inset, type) once instead of every admin shell\n re-deriving it. */\n.menuButton {\n display: flex;\n width: 100%;\n align-items: center;\n gap: var(--sidebar-item-gap, var(--spacing-2, 0.5rem));\n overflow: hidden;\n border-radius: var(--sidebar-item-radius, var(--radius-md, 0.375rem));\n border: none;\n text-align: left;\n font-size: var(--sidebar-item-font-size, var(--font-size-sm, 0.875rem));\n font-weight: var(--sidebar-item-font-weight, inherit);\n letter-spacing: var(--sidebar-item-letter-spacing, inherit);\n text-transform: var(--sidebar-item-text-transform, inherit);\n cursor: pointer;\n background: transparent;\n color: var(--sidebar-text);\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n}\n\n.menuButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.menuButton:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.menuButton[data-active] {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n font-weight: var(--sidebar-item-active-font-weight, var(--font-weight-medium, 500));\n}\n\n/* Menu button variants */\n.menuButton:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .menuButtonDefault:hover {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n }\n}\n\n.menuButtonOutline {\n background-color: var(--surface-page, #ffffff);\n box-shadow: inset 0 0 0 1px var(--sidebar-border);\n}\n\n@media (hover: hover) {\n .menuButtonOutline:hover {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n }\n}\n\n/* Menu button sizes */\n.menuButtonSizeDefault {\n height: var(--sidebar-item-height, 2.25rem);\n padding: var(--sidebar-item-padding, 0 var(--spacing-3, 0.75rem));\n font-size: var(--sidebar-item-font-size, var(--font-size-sm, 0.875rem));\n}\n\n.menuButtonSizeSm {\n height: 2rem;\n padding: 0 calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.menuButtonSizeLg {\n height: 3.5rem;\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Menu action */\n.menuAction {\n position: absolute;\n top: 0.375rem;\n right: 0.25rem;\n display: flex;\n aspect-ratio: 1;\n width: 1.25rem;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: none;\n background: transparent;\n cursor: pointer;\n padding: 0;\n color: var(--sidebar-text);\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.menuAction:active {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .menuAction:hover {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n }\n}\n\n.menuActionHover {\n opacity: 0;\n}\n\n@media (hover: hover) {\n .menuItem:hover .menuActionHover {\n opacity: 1;\n }\n}\n\n.menuItem:focus-within .menuActionHover {\n opacity: 1;\n}\n\n/* Menu badge */\n.menuBadge {\n pointer-events: none;\n position: absolute;\n right: 0.25rem;\n display: flex;\n height: 1.25rem;\n min-width: 1.25rem;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n padding-inline: var(--spacing-1, 0.25rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--sidebar-text);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n\n/* Menu sub */\n.menuSub {\n margin-inline: calc(var(--spacing-3, 0.75rem) + var(--spacing-1, 0.25rem) / 2);\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n border-left: 1px solid var(--sidebar-border);\n padding-inline: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n padding-block: calc(var(--spacing-1, 0.25rem) / 2);\n list-style: none;\n margin: 0;\n margin-inline: calc(var(--spacing-3, 0.75rem) + var(--spacing-1, 0.25rem) / 2);\n}\n\n/* Menu sub item */\n.menuSubItem {\n position: relative;\n}\n\n/* Menu sub button */\n.menuSubButton {\n display: flex;\n height: 1.75rem;\n min-width: 0;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n overflow: hidden;\n border-radius: var(--radius-sm, 0.25rem);\n padding-inline: var(--spacing-2, 0.5rem);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--sidebar-text);\n text-decoration: none;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n}\n\n.menuSubButton:active {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .menuSubButton:hover {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n }\n}\n\n.menuSubButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.menuSubButton[data-active] {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n}\n\n.menuSubButtonSm {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n/* Screen reader only */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n"
1020
+ "content": "/* Sidebar wrapper */\n.wrapper {\n display: flex;\n min-height: 100svh;\n width: 100%;\n background-color: var(--sidebar-bg);\n}\n\n/* Sidebar static (collapsible=none) */\n.sidebarStatic {\n display: flex;\n height: 100%;\n width: var(--sidebar-width, 16rem);\n flex-direction: column;\n background-color: var(--sidebar-bg);\n color: var(--sidebar-text);\n}\n\n/* Sidebar container (collapsible versions) */\n.sidebarContainer {\n position: relative;\n color: var(--sidebar-text);\n}\n\n/* Sidebar gap (reserves layout space) */\n.sidebarGap {\n width: var(--sidebar-width, 16rem);\n background: transparent;\n transition: width var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n flex-shrink: 0;\n}\n\n.sidebarContainer[data-state=\"collapsed\"][data-collapsible=\"offcanvas\"] .sidebarGap {\n width: 0;\n}\n\n.sidebarContainer[data-state=\"collapsed\"][data-collapsible=\"icon\"] .sidebarGap {\n width: var(--sidebar-width-icon, 3rem);\n}\n\n/* Sidebar inner */\n.sidebarInner {\n position: fixed;\n inset-block: 0;\n z-index: 10;\n display: flex;\n height: 100svh;\n width: var(--sidebar-width, 16rem);\n transition: left var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), right var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), width var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n.sidebarInner[data-side=\"left\"] {\n left: 0;\n}\n\n.sidebarContainer[data-state=\"collapsed\"][data-collapsible=\"offcanvas\"] .sidebarInner[data-side=\"left\"] {\n left: calc(var(--sidebar-width, 16rem) * -1);\n}\n\n.sidebarInner[data-side=\"right\"] {\n right: 0;\n}\n\n.sidebarContainer[data-state=\"collapsed\"][data-collapsible=\"offcanvas\"] .sidebarInner[data-side=\"right\"] {\n right: calc(var(--sidebar-width, 16rem) * -1);\n}\n\n.sidebarContainer[data-state=\"collapsed\"][data-collapsible=\"icon\"] .sidebarInner {\n width: var(--sidebar-width-icon, 3rem);\n}\n\n/* Contained (position=\"contained\"): the sidebar lays out inside its parent\n instead of pinning to the viewport, so a rail can sit under an app bar. */\n.sidebarContainer[data-position=\"contained\"] .sidebarInner {\n position: absolute;\n height: auto;\n}\n\n/* Sidebar content container */\n.sidebarContent {\n display: flex;\n width: 100%;\n height: 100%;\n flex-direction: column;\n background-color: var(--sidebar-bg);\n overflow: hidden;\n}\n\n.sidebarContainer[data-variant=\"floating\"] .sidebarContent,\n.sidebarContainer[data-variant=\"inset\"] .sidebarContent {\n border-radius: var(--radius-lg, 0.5rem);\n box-shadow: var(--shadow-sm);\n}\n\n/* Trigger button */\n.trigger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: transparent;\n cursor: pointer;\n border-radius: var(--radius-sm, 0.25rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.5);\n color: var(--text-primary, #111827);\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.trigger:active {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .trigger:hover {\n background-color: var(--surface-muted, #f3f4f6);\n }\n}\n\n.trigger:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Rail */\n.rail {\n position: absolute;\n inset-block: 0;\n z-index: 20;\n width: 1rem;\n border: none;\n background: transparent;\n cursor: col-resize;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n@media (hover: hover) {\n .rail:hover {\n background-color: var(--sidebar-border);\n }\n}\n\n/* Inset main content area */\n.inset {\n position: relative;\n display: flex;\n width: 100%;\n flex: 1;\n flex-direction: column;\n background-color: var(--surface-page, #ffffff);\n}\n\n/* Header */\n.header {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--sidebar-header-padding, var(--spacing-2, 0.5rem));\n}\n\n/* Footer */\n.footer {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--sidebar-footer-padding, var(--spacing-2, 0.5rem));\n}\n\n/* Separator */\n.separator {\n margin-inline: var(--spacing-2, 0.5rem);\n border: none;\n height: 1px;\n background-color: var(--sidebar-border);\n}\n\n/* Content area */\n.contentArea {\n display: flex;\n min-height: 0;\n flex: 1;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n overflow: auto;\n padding: var(--sidebar-content-padding, var(--spacing-1, 0.25rem));\n}\n\n/* Group */\n.group {\n position: relative;\n display: flex;\n width: 100%;\n min-width: 0;\n flex-direction: column;\n padding: var(--spacing-2, 0.5rem);\n}\n\n/* Group label */\n.groupLabel {\n display: flex;\n height: var(--sidebar-group-label-height, 2rem);\n flex-shrink: 0;\n align-items: center;\n border-radius: var(--radius-sm, 0.25rem);\n padding-inline: var(--spacing-2, 0.5rem);\n font-size: var(--sidebar-group-label-font-size, var(--font-size-xs, 0.75rem));\n font-weight: var(--sidebar-group-label-font-weight, var(--font-weight-medium, 500));\n letter-spacing: var(--sidebar-group-label-letter-spacing, inherit);\n text-transform: var(--sidebar-group-label-text-transform, inherit);\n color: var(--sidebar-text-muted);\n transition: margin var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out), opacity var(--motion-duration-normal, 200ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Group action */\n.groupAction {\n position: absolute;\n top: 0.875rem;\n right: 0.75rem;\n display: flex;\n aspect-ratio: 1;\n width: 1.25rem;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: none;\n background: transparent;\n cursor: pointer;\n padding: 0;\n color: var(--sidebar-text);\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.groupAction:active {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .groupAction:hover {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n }\n}\n\n/* Group content */\n.groupContent {\n width: 100%;\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Menu */\n.menu {\n display: flex;\n width: 100%;\n min-width: 0;\n flex-direction: column;\n gap: var(--sidebar-menu-gap, var(--spacing-1, 0.25rem));\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/* Menu item */\n.menuItem {\n position: relative;\n}\n\n/* Menu button base — the nav-item treatment. VI-625 hooks let a theme pin the\n item shape (radius, height, inset, type) once instead of every admin shell\n re-deriving it. */\n.menuButton {\n display: flex;\n width: 100%;\n align-items: center;\n gap: var(--sidebar-item-gap, var(--spacing-2, 0.5rem));\n overflow: hidden;\n border-radius: var(--sidebar-item-radius, var(--radius-md, 0.375rem));\n border: none;\n text-align: left;\n font-size: var(--sidebar-item-font-size, var(--font-size-sm, 0.875rem));\n font-weight: var(--sidebar-item-font-weight, inherit);\n letter-spacing: var(--sidebar-item-letter-spacing, inherit);\n text-transform: var(--sidebar-item-text-transform, inherit);\n cursor: pointer;\n background: transparent;\n color: var(--sidebar-text);\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n}\n\n/* Inset ring: .sidebarContent clips overflow, so an outset ring is cut to a\n bottom line on the icon rail. Drawing it inside the button (width + offset token) keeps all four\n sides visible (WCAG 2.4.7) without consumer CSS. */\n.menuButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: calc(-1 * (var(--focus-ring-width, 2px) + var(--focus-ring-offset, 0px)));\n}\n\n.menuButton:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n.menuButton[data-active] {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n font-weight: var(--sidebar-item-active-font-weight, var(--font-weight-medium, 500));\n}\n\n/* Menu button variants */\n.menuButton:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .menuButtonDefault:hover {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n }\n}\n\n.menuButtonOutline {\n background-color: var(--surface-page, #ffffff);\n box-shadow: inset 0 0 0 1px var(--sidebar-border);\n}\n\n@media (hover: hover) {\n .menuButtonOutline:hover {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n }\n}\n\n/* Menu button sizes */\n.menuButtonSizeDefault {\n height: var(--sidebar-item-height, 2.25rem);\n padding: var(--sidebar-item-padding, 0 var(--spacing-3, 0.75rem));\n font-size: var(--sidebar-item-font-size, var(--font-size-sm, 0.875rem));\n}\n\n.menuButtonSizeSm {\n height: 2rem;\n padding: 0 calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.menuButtonSizeLg {\n height: 3.5rem;\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--font-size-sm, 0.875rem);\n}\n\n/* Menu action */\n.menuAction {\n position: absolute;\n top: 0.375rem;\n right: 0.25rem;\n display: flex;\n aspect-ratio: 1;\n width: 1.25rem;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n border: none;\n background: transparent;\n cursor: pointer;\n padding: 0;\n color: var(--sidebar-text);\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n.menuAction:active {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .menuAction:hover {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n }\n}\n\n.menuActionHover {\n opacity: 0;\n}\n\n@media (hover: hover) {\n .menuItem:hover .menuActionHover {\n opacity: 1;\n }\n}\n\n.menuItem:focus-within .menuActionHover {\n opacity: 1;\n}\n\n/* Menu badge */\n.menuBadge {\n pointer-events: none;\n position: absolute;\n right: 0.25rem;\n display: flex;\n height: 1.25rem;\n min-width: 1.25rem;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-sm, 0.25rem);\n padding-inline: var(--spacing-1, 0.25rem);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-medium, 500);\n color: var(--sidebar-text);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n\n/* Menu sub */\n.menuSub {\n margin-inline: calc(var(--spacing-3, 0.75rem) + var(--spacing-1, 0.25rem) / 2);\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n border-left: 1px solid var(--sidebar-border);\n padding-inline: calc(var(--spacing-2, 0.5rem) + var(--spacing-1, 0.25rem) / 2);\n padding-block: calc(var(--spacing-1, 0.25rem) / 2);\n list-style: none;\n margin: 0;\n margin-inline: calc(var(--spacing-3, 0.75rem) + var(--spacing-1, 0.25rem) / 2);\n}\n\n/* Menu sub item */\n.menuSubItem {\n position: relative;\n}\n\n/* Menu sub button */\n.menuSubButton {\n display: flex;\n height: 1.75rem;\n min-width: 0;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n overflow: hidden;\n border-radius: var(--radius-sm, 0.25rem);\n padding-inline: var(--spacing-2, 0.5rem);\n font-size: var(--font-size-sm, 0.875rem);\n color: var(--sidebar-text);\n text-decoration: none;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n outline: none;\n}\n\n.menuSubButton:active {\n transform: translateY(1px);\n transition: none;\n}\n\n@media (hover: hover) {\n .menuSubButton:hover {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n }\n}\n\n.menuSubButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.menuSubButton[data-active] {\n background-color: var(--sidebar-accent-bg);\n color: var(--sidebar-accent-text);\n}\n\n.menuSubButtonSm {\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n/* Screen reader only */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n"
995
1021
  }
996
1022
  ]
997
1023
  },
@@ -2116,6 +2142,30 @@
2116
2142
  }
2117
2143
  ]
2118
2144
  },
2145
+ {
2146
+ "name": "position-picker",
2147
+ "type": "registry:ui",
2148
+ "description": "A 3x3 anchor grid: nine targets, one chosen, the value a { y, x } pair. Draws on its own ground or over a photograph.",
2149
+ "category": "form",
2150
+ "dependencies": [
2151
+ "@loworbitstudio/visor-core"
2152
+ ],
2153
+ "registryDependencies": [
2154
+ "utils"
2155
+ ],
2156
+ "files": [
2157
+ {
2158
+ "path": "components/ui/position-picker/position-picker.tsx",
2159
+ "type": "registry:ui",
2160
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./position-picker.module.css\"\n\nexport type PositionY = \"top\" | \"center\" | \"bottom\"\nexport type PositionX = \"left\" | \"center\" | \"right\"\n\n/** The chosen anchor: a vertical and a horizontal position. */\nexport interface PositionValue {\n y: PositionY\n x: PositionX\n}\n\nexport type PositionPickerVariant = \"default\" | \"on-image\"\n\nconst ROWS: readonly PositionY[] = [\"top\", \"center\", \"bottom\"]\nconst COLS: readonly PositionX[] = [\"left\", \"center\", \"right\"]\n\n/** The accessible name of one target: \"Top left\", \"Center\", \"Bottom right\". */\nfunction targetName({ y, x }: PositionValue): string {\n if (y === \"center\" && x === \"center\") return \"Center\"\n const name = y === \"center\" ? `Center ${x}` : x === \"center\" ? `${y} center` : `${y} ${x}`\n return name.charAt(0).toUpperCase() + name.slice(1)\n}\n\nexport interface PositionPickerTargetState {\n selected: boolean\n}\n\nexport interface PositionPickerProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\" | \"children\"> {\n /** Controlled value. `null` is controlled with nothing chosen. */\n value?: PositionValue | null\n /** Initial value when uncontrolled. */\n defaultValue?: PositionValue | null\n /** Called with the new `{ y, x }` pair when a target is chosen. */\n onValueChange?: (value: PositionValue) => void\n /**\n * `default` draws the grid on its own ground. `on-image` fills a positioned\n * parent (the photograph's frame) and gives every target its own scrim ring,\n * so each keeps 3:1 against any photograph without touching the photo.\n */\n variant?: PositionPickerVariant\n disabled?: boolean\n /**\n * Replaces the drawn dot of one target, for example an alignment icon in the\n * occupied row. Decorative: the target keeps its own accessible name.\n */\n renderTarget?: (position: PositionValue, state: PositionPickerTargetState) => React.ReactNode\n}\n\nconst PositionPicker = React.forwardRef<HTMLDivElement, PositionPickerProps>(\n (\n {\n value: valueProp,\n defaultValue = null,\n onValueChange,\n variant = \"default\",\n disabled = false,\n renderTarget,\n className,\n ...props\n },\n ref,\n ) => {\n const isControlled = valueProp !== undefined\n const [inner, setInner] = React.useState<PositionValue | null>(defaultValue)\n const value = isControlled ? valueProp : inner\n const buttons = React.useRef<Array<HTMLButtonElement | null>>([])\n\n const select = (index: number) => {\n const next: PositionValue = { y: ROWS[Math.floor(index / 3)], x: COLS[index % 3] }\n if (!isControlled) setInner(next)\n onValueChange?.(next)\n }\n\n const selectedIndex = value ? ROWS.indexOf(value.y) * 3 + COLS.indexOf(value.x) : -1\n // Roving tab stop: the chosen target, or the first when nothing is chosen.\n const tabStop = selectedIndex === -1 ? 0 : selectedIndex\n\n const move = (from: number, key: string): number | null => {\n const row = Math.floor(from / 3)\n const col = from % 3\n switch (key) {\n case \"ArrowLeft\": return row * 3 + ((col + 2) % 3)\n case \"ArrowRight\": return row * 3 + ((col + 1) % 3)\n case \"ArrowUp\": return ((row + 2) % 3) * 3 + col\n case \"ArrowDown\": return ((row + 1) % 3) * 3 + col\n case \"Home\": return 0\n case \"End\": return 8\n default: return null\n }\n }\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, index: number) => {\n const to = move(index, event.key)\n if (to === null) return\n event.preventDefault()\n buttons.current[to]?.focus()\n select(to)\n }\n\n return (\n <div\n ref={ref}\n role=\"radiogroup\"\n aria-disabled={disabled || undefined}\n data-slot=\"position-picker\"\n data-variant={variant}\n className={cn(styles.root, variant === \"on-image\" && styles.onImage, className)}\n {...props}\n >\n {ROWS.flatMap((y, r) =>\n COLS.map((x, c) => {\n const index = r * 3 + c\n const position: PositionValue = { y, x }\n const selected = index === selectedIndex\n return (\n <button\n key={index}\n ref={(node) => {\n buttons.current[index] = node\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n aria-label={targetName(position)}\n tabIndex={index === tabStop ? 0 : -1}\n disabled={disabled}\n data-slot=\"position-picker-target\"\n data-state={selected ? \"checked\" : \"unchecked\"}\n data-y={y}\n data-x={x}\n className={styles.target}\n onClick={() => select(index)}\n onKeyDown={(event) => handleKeyDown(event, index)}\n >\n <span className={styles.mark} aria-hidden=\"true\">\n {renderTarget?.(position, { selected })}\n </span>\n </button>\n )\n }),\n )}\n </div>\n )\n },\n)\nPositionPicker.displayName = \"PositionPicker\"\n\nexport { PositionPicker }\n"
2161
+ },
2162
+ {
2163
+ "path": "components/ui/position-picker/position-picker.module.css",
2164
+ "type": "registry:ui",
2165
+ "content": "/* PositionPicker: nine targets in a 3x3 grid, one chosen.\n\n Target size (WCAG 2.5.8): every target's hit area is at least\n --position-picker-target-size (24px) square, even though the drawn dot is\n smaller. The grid grows to fit three targets a side rather than shrinking\n them. --position-picker-target-size is the per-component override. */\n.root {\n --_target: var(--position-picker-target-size, var(--spacing-6, 1.5rem));\n position: relative;\n display: inline-grid;\n grid-template-columns: repeat(3, minmax(var(--_target), 1fr));\n grid-template-rows: repeat(3, minmax(var(--_target), 1fr));\n min-width: calc(3 * var(--_target));\n min-height: calc(3 * var(--_target));\n /* The standalone grid sits on its own ground. The resting edge is an inset\n outline on the shared --control-edge-* tokens (VI-655), never a border, so\n --control-edge-width: 0 turns it off. */\n padding: var(--spacing-1, 0.25rem);\n border-radius: var(--radius-md, 0.375rem);\n background-color: var(--position-picker-bg, var(--surface-subtle, #f9fafb));\n outline: var(--control-edge-width, 1px) var(--control-edge-style, solid) var(--position-picker-border, var(--control-edge-color, var(--border-default, #e5e7eb)));\n outline-offset: calc(-1 * var(--control-edge-width, 1px));\n}\n\n/* Standalone targets are a comfortable size, not the bare minimum. */\n.root:not(.onImage) {\n --_target: var(--position-picker-target-size, var(--spacing-8, 2rem));\n}\n\n/* On an image the grid fills the photograph's frame (a positioned parent). It\n has no ground of its own: the photo is the ground, and each target carries\n its own scrim ring below. On a small thumbnail min-width/min-height win over\n the frame, so the grid grows to the target size instead of shrinking. */\n.onImage {\n --_scrim: var(--position-picker-scrim, color-mix(in srgb, var(--color-black, #000000) 80%, transparent));\n position: absolute;\n inset: 0;\n display: grid;\n padding: 0;\n border-radius: 0;\n background-color: transparent;\n outline: none;\n}\n\n.target {\n display: grid;\n place-items: center;\n min-width: var(--_target);\n min-height: var(--_target);\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: var(--radius-sm, 0.25rem);\n background: transparent;\n color: var(--text-primary, #111827);\n cursor: pointer;\n}\n\n.target:disabled {\n cursor: not-allowed;\n opacity: var(--opacity-50, 0.5);\n}\n\n/* The drawn dot. Smaller than the hit area on purpose. */\n.mark {\n display: grid;\n place-items: center;\n width: var(--spacing-3, 0.75rem);\n height: var(--spacing-3, 0.75rem);\n border-radius: var(--radius-full, 9999px);\n background-color: var(--position-picker-mark, var(--text-tertiary, #9ca3af));\n transition: width var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n height var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n@media (hover: hover) {\n .target:hover:not(:disabled) .mark {\n background-color: var(--text-secondary, #6b7280);\n }\n}\n\n/* Chosen: filled with the accent, larger, and ringed. Size and ring carry the\n state as well as colour (1.4.1). */\n.target[data-state=\"checked\"] .mark {\n width: var(--spacing-4, 1rem);\n height: var(--spacing-4, 1rem);\n background-color: var(--position-picker-mark-selected, var(--interactive-primary-bg, var(--primary, #111827)));\n color: var(--interactive-primary-text, #f9fafb);\n box-shadow: 0 0 0 var(--stroke-width-medium, 2px) var(--position-picker-ring, var(--surface-subtle, #f9fafb)),\n 0 0 0 calc(var(--stroke-width-medium, 2px) * 2) var(--position-picker-mark-selected, var(--interactive-primary-bg, var(--primary, #111827)));\n}\n\n/* A custom glyph (an alignment icon) fills the mark and sizes to it. */\n.mark > svg {\n width: 100%;\n height: 100%;\n}\n\n/* Focus is the one ring the edge switch never removes. */\n.target:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, #111827);\n outline-offset: calc(-1 * var(--focus-ring-offset, 2px));\n}\n\n/* ---------------------------------------------------------------------------\n On image. The dot is a fixed light mark inside a dark scrim ring, whatever\n the theme mode: the photograph is the background, and any photograph can be\n light or dark. Light-on-dark-ring keeps 3:1 over light and dark regions\n alike. The scrim is a fixed black, not a theme surface: a photograph does not\n follow the theme. Nothing is applied to the photo itself (no filter).\n --------------------------------------------------------------------------- */\n.onImage .mark {\n background-color: var(--position-picker-image-mark, var(--color-white, #ffffff));\n box-shadow: 0 0 0 var(--stroke-width-medium, 2px) var(--_scrim);\n}\n\n@media (hover: hover) {\n .onImage .target:hover:not(:disabled) .mark {\n background-color: var(--position-picker-image-mark, var(--color-white, #ffffff));\n width: var(--spacing-4, 1rem);\n height: var(--spacing-4, 1rem);\n }\n}\n\n.onImage .target[data-state=\"checked\"] .mark {\n background-color: var(--position-picker-mark-selected, var(--interactive-primary-bg, var(--primary, #111827)));\n box-shadow: 0 0 0 var(--stroke-width-medium, 2px) var(--position-picker-image-mark, var(--color-white, #ffffff)),\n 0 0 0 calc(var(--stroke-width-medium, 2px) * 2) var(--_scrim);\n}\n\n/* The photo can put any colour under the focus ring, so it gets a scrim halo. */\n.onImage .target:focus-visible {\n outline-color: var(--position-picker-image-mark, var(--color-white, #ffffff));\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) var(--_scrim);\n}\n"
2166
+ }
2167
+ ]
2168
+ },
2119
2169
  {
2120
2170
  "name": "tin-input",
2121
2171
  "type": "registry:ui",
@@ -2854,7 +2904,7 @@
2854
2904
  {
2855
2905
  "path": "components/ui/page-header/page-header.module.css",
2856
2906
  "type": "registry:ui",
2857
- "content": "/* Page Header base: container query wrapper.\n\n VI-625: `--page-header-title-size` / `-title-family` used to be *declared*\n here. An element-level declaration outranks anything a theme emits into a\n cascade layer on :root, so the documented \"themes can set these\" was never\n actually true — the component shadowed the theme. Both use sites already\n carry the identical fallback inline, so dropping the declarations is\n value-preserving (`var(--x, 3.5rem)` with no --x resolves to 3.5rem, exactly\n as it did when --x was declared as 3.5rem) and makes the hooks genuinely\n theme-bindable. `--page-header-title-leading` was already unset for the same\n reason: the title falls back to its per-rule default line-height (tight for\n the standard title, `1` for the marquee scale); the `leading` prop sets it. */\n.base {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n gap: var(--page-header-gap, var(--spacing-4, 1rem));\n container-type: inline-size;\n container-name: page-header;\n color: var(--page-header-text-color, var(--text-primary, #111827));\n}\n\n/* Size variants control vertical rhythm */\n.sizeSm {\n gap: var(--spacing-3, 0.75rem);\n}\n\n.sizeMd {\n gap: var(--spacing-4, 1rem);\n}\n\n.sizeLg {\n gap: var(--spacing-5, 1.25rem);\n}\n\n/* Breadcrumb slot sits above the title row */\n.breadcrumb {\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n/* Main row: text block on the left, actions on the right */\n.row {\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n gap: var(--spacing-6, 1.5rem);\n min-width: 0;\n}\n\n/* When a leading media/identity slot is present, the row forms an identity\n lockup: the media sits before the text stack a tight gap from it. Scoped to\n `data-has-leading` so the default (text|actions) row is untouched for\n consumers that don't pass `leading`. The expanding text block keeps actions\n pinned right via the inherited `justify-content: space-between`. */\n.row[data-has-leading] {\n align-items: center;\n gap: var(--page-header-leading-gap, var(--spacing-3, 0.75rem));\n}\n\n/* Leading media/identity slot: sits before the text block, vertically\n centered against the title/description stack, never shrinks. (Reverts the\n VI-539 top-align, which assumed a different slot — the blessed admin\n identity lockup centers the media. Superseded per the admin-editorial\n reconcile plan.) */\n.leading {\n display: flex;\n align-items: center;\n flex: 0 0 auto;\n}\n\n/* Text block: eyebrow, title, description stacked */\n.text {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n min-width: 0;\n flex: 1 1 auto;\n}\n\n.sizeSm .text {\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeLg .text {\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* Eyebrow: small uppercase label above the title */\n.eyebrow {\n font-size: var(--page-header-eyebrow-font-size, var(--font-size-xs, 0.75rem));\n font-weight: var(--page-header-eyebrow-font-weight, var(--font-weight-semibold, 600));\n letter-spacing: var(--page-header-eyebrow-letter-spacing, var(--letter-spacing-wide, 0.05em));\n text-transform: var(--page-header-eyebrow-text-transform, uppercase);\n color: var(--page-header-eyebrow-color, var(--text-tertiary, #6b7280));\n line-height: var(--line-height-tight, 1.25);\n}\n\n/* Title */\n.title {\n margin: 0;\n font-family: var(--font-family-heading, inherit);\n font-size: var(--page-header-title-font-size, var(--font-size-2xl, 1.5rem));\n font-weight: var(--page-header-title-font-weight, var(--font-weight-semibold, 600));\n line-height: var(--page-header-title-leading, var(--line-height-tight, 1.2));\n color: var(--page-header-title-color, var(--text-primary, #111827));\n letter-spacing: var(--page-header-title-letter-spacing, var(--letter-spacing-tight, -0.01em));\n}\n\n.sizeSm .title {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.sizeLg .title {\n font-size: var(--font-size-3xl, 1.875rem);\n}\n\n/* Title typography overrides (orthogonal to size variants).\n `data-title-size=\"marquee\"` pulls the title up to a hero scale via the\n `--page-header-title-size` custom property. Raw-string consumers pass an\n inline `--page-header-title-size` and rely on the same rule. */\n.title[data-title-size=\"marquee\"] {\n font-size: var(--page-header-title-size, 3.5rem);\n line-height: var(--page-header-title-leading, 1);\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n}\n\n/* `data-title-family=\"display\"` switches to the display family with a\n graceful fallback to the heading family when no display font is themed.\n VI-625: `--admin-ui-marquee-family` sits in the chain so the admin-ui\n pattern's marquee role drives the hero title when a theme binds it. */\n.title[data-title-family=\"display\"] {\n font-family: var(--page-header-title-family, var(--admin-ui-marquee-family, var(--font-display, var(--font-family-heading, inherit))));\n}\n\n/* Description */\n.description {\n margin: 0;\n font-size: var(--page-header-description-font-size, var(--font-size-sm, 0.875rem));\n line-height: var(--line-height-relaxed, 1.6);\n color: var(--page-header-description-color, var(--text-secondary, #6b7280));\n max-width: 65ch;\n}\n\n.sizeLg .description {\n font-size: var(--font-size-base, 1rem);\n}\n\n/* Actions slot: cluster on the right */\n.actions {\n display: flex;\n align-items: center;\n gap: var(--page-header-actions-gap, var(--spacing-2, 0.5rem));\n flex: 0 0 auto;\n flex-wrap: wrap;\n justify-content: flex-end;\n}\n\n/* Container query: stack to column below a small container width */\n@container page-header (max-width: 480px) {\n .row {\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-4, 1rem);\n }\n\n .actions {\n width: 100%;\n justify-content: flex-start;\n }\n}\n"
2907
+ "content": "/* Page Header base: container query wrapper.\n\n VI-625: `--page-header-title-size` / `-title-family` used to be *declared*\n here. An element-level declaration outranks anything a theme emits into a\n cascade layer on :root, so the documented \"themes can set these\" was never\n actually true — the component shadowed the theme. Both use sites already\n carry the identical fallback inline, so dropping the declarations is\n value-preserving (`var(--x, 3.5rem)` with no --x resolves to 3.5rem, exactly\n as it did when --x was declared as 3.5rem) and makes the hooks genuinely\n theme-bindable. `--page-header-title-leading` was already unset for the same\n reason: the title falls back to its per-rule default line-height (tight for\n the standard title, `1` for the marquee scale); the `leading` prop sets it. */\n.base {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n gap: var(--page-header-gap, var(--spacing-4, 1rem));\n container-type: inline-size;\n container-name: page-header;\n color: var(--page-header-text-color, var(--text-primary, #111827));\n}\n\n/* Size variants control vertical rhythm */\n.sizeSm {\n gap: var(--spacing-3, 0.75rem);\n}\n\n.sizeMd {\n gap: var(--spacing-4, 1rem);\n}\n\n.sizeLg {\n gap: var(--spacing-5, 1.25rem);\n}\n\n/* Breadcrumb slot sits above the title row */\n.breadcrumb {\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n/* Main row: text block on the left, actions on the right */\n.row {\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n gap: var(--spacing-6, 1.5rem);\n min-width: 0;\n}\n\n/* When a leading media/identity slot is present, the row forms an identity\n lockup: the media sits before the text stack a tight gap from it. Scoped to\n `data-has-leading` so the default (text|actions) row is untouched for\n consumers that don't pass `leading`. The expanding text block keeps actions\n pinned right via the inherited `justify-content: space-between`. */\n.row[data-has-leading] {\n align-items: center;\n gap: var(--page-header-leading-gap, var(--spacing-3, 0.75rem));\n}\n\n/* Leading media/identity slot: sits before the text block, vertically\n centered against the title/description stack, never shrinks. (Reverts the\n VI-539 top-align, which assumed a different slot — the blessed admin\n identity lockup centers the media. Superseded per the admin-editorial\n reconcile plan.) */\n.leading {\n display: flex;\n align-items: center;\n flex: 0 0 auto;\n}\n\n/* Text block: eyebrow, title, description stacked */\n.text {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n min-width: 0;\n flex: 1 1 auto;\n}\n\n.sizeSm .text {\n gap: var(--spacing-1, 0.25rem);\n}\n\n.sizeLg .text {\n gap: var(--spacing-3, 0.75rem);\n}\n\n/* Eyebrow: small uppercase label above the title */\n.eyebrow {\n font-size: var(--page-header-eyebrow-font-size, var(--font-size-xs, 0.75rem));\n font-weight: var(--page-header-eyebrow-font-weight, var(--font-weight-semibold, 600));\n letter-spacing: var(--page-header-eyebrow-letter-spacing, var(--letter-spacing-wide, 0.05em));\n text-transform: var(--page-header-eyebrow-text-transform, uppercase);\n color: var(--page-header-eyebrow-color, var(--text-tertiary, #6b7280));\n line-height: var(--line-height-tight, 1.25);\n}\n\n/* Title */\n.title {\n margin: 0;\n font-family: var(--font-family-heading, inherit);\n font-size: var(--page-header-title-font-size, var(--font-size-2xl, 1.5rem));\n font-weight: var(--page-header-title-font-weight, var(--font-weight-semibold, 600));\n line-height: var(--page-header-title-leading, var(--line-height-tight, 1.2));\n color: var(--page-header-title-color, var(--text-primary, #111827));\n letter-spacing: var(--page-header-title-letter-spacing, var(--letter-spacing-tight, -0.01em));\n}\n\n.sizeSm .title {\n font-size: var(--font-size-xl, 1.25rem);\n}\n\n.sizeLg .title {\n font-size: var(--font-size-3xl, 1.875rem);\n}\n\n/* Title typography overrides (orthogonal to size variants).\n `data-title-size=\"marquee\"` pulls the title up to a hero scale via the\n `--page-header-title-size` custom property. Raw-string consumers pass an\n inline `--page-header-title-size` and rely on the same rule. */\n.title[data-title-size=\"marquee\"] {\n font-size: var(--page-header-title-size, 3.5rem);\n line-height: var(--page-header-title-leading, 1);\n letter-spacing: var(--letter-spacing-tight, -0.01em);\n}\n\n/* `data-title-family=\"display\"` switches to the display family with a\n graceful fallback to the heading family when no display font is themed.\n VI-625: `--admin-ui-marquee-family` sits in the chain so the admin-ui\n pattern's marquee role drives the hero title when a theme binds it. */\n.title[data-title-family=\"display\"] {\n font-family: var(--page-header-title-family, var(--admin-ui-marquee-family, var(--font-display, var(--font-family-heading, inherit))));\n /* VI-687: theme-set display casing; inherit when unset. */\n text-transform: var(--font-display-text-transform, inherit);\n}\n\n/* Description */\n.description {\n margin: 0;\n font-size: var(--page-header-description-font-size, var(--font-size-sm, 0.875rem));\n line-height: var(--line-height-relaxed, 1.6);\n color: var(--page-header-description-color, var(--text-secondary, #6b7280));\n max-width: 65ch;\n}\n\n.sizeLg .description {\n font-size: var(--font-size-base, 1rem);\n}\n\n/* Actions slot: cluster on the right */\n.actions {\n display: flex;\n align-items: center;\n gap: var(--page-header-actions-gap, var(--spacing-2, 0.5rem));\n flex: 0 0 auto;\n flex-wrap: wrap;\n justify-content: flex-end;\n}\n\n/* Container query: stack to column below a small container width */\n@container page-header (max-width: 480px) {\n .row {\n flex-direction: column;\n align-items: flex-start;\n gap: var(--spacing-4, 1rem);\n }\n\n .actions {\n width: 100%;\n justify-content: flex-start;\n }\n}\n"
2858
2908
  }
2859
2909
  ]
2860
2910
  },
@@ -2927,7 +2977,7 @@
2927
2977
  {
2928
2978
  "path": "components/ui/section-intro/section-intro.module.css",
2929
2979
  "type": "registry:ui",
2930
- "content": "/* SectionIntro — marketing eyebrow + display heading + optional lede\n *\n * The eyebrow color is intentionally driven through a CSS custom property\n * (`--section-intro-eyebrow-color`) so that consumers can override it at\n * any ancestor level and have it track a live-rewritten brand accent var\n * (for example a keyed `--color-acid` that swaps between artist palette entries).\n * Default fallback chain: --section-intro-eyebrow-color → --accent-default\n * → --interactive-primary-bg → #111827 (Tailwind Gray-900).\n *\n * Alignment is controlled by data-align on the root (set by the `align`\n * prop). Both \"left\" and \"center\" are defined here.\n */\n\n.root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n width: 100%;\n min-width: 0;\n}\n\n/* ── Alignment variants ─────────────────────────────────────────────────── */\n\n.root[data-align=\"left\"] {\n align-items: flex-start;\n text-align: left;\n}\n\n.root[data-align=\"center\"] {\n align-items: center;\n text-align: center;\n}\n\n/* ── Eyebrow ────────────────────────────────────────────────────────────── */\n\n.eyebrow {\n margin: 0;\n font-family: var(--font-mono, ui-monospace, monospace);\n font-size: var(--font-size-xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: var(--letter-spacing-widest, 0.2em);\n text-transform: uppercase;\n line-height: var(--line-height-tight, 1.25);\n /*\n * Live-rewriteable eyebrow color. Override --section-intro-eyebrow-color\n * at any ancestor (for example on the <section> that contains this component) to\n * track a keyed brand accent that changes at runtime.\n */\n color: var(\n --section-intro-eyebrow-color,\n var(--accent-default, var(--interactive-primary-bg, #111827))\n );\n}\n\n/* ── Heading ────────────────────────────────────────────────────────────── */\n\n.heading {\n margin: 0;\n font-family: var(--font-display, var(--font-family-heading, inherit));\n font-size: clamp(2.5rem, 6vw, 4rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: var(--line-height-none, 1.05);\n letter-spacing: var(--letter-spacing-tight, -0.02em);\n color: var(--text-primary, #111827);\n text-wrap: balance;\n max-width: 18ch;\n}\n\n.root[data-align=\"center\"] .heading {\n max-width: 22ch;\n}\n\n/* ── Lede ───────────────────────────────────────────────────────────────── */\n\n.lede {\n margin: 0;\n font-size: var(--font-size-base, 1rem);\n line-height: var(--line-height-relaxed, 1.6);\n font-weight: var(--font-weight-light, 300);\n color: var(--text-secondary, #6b7280);\n max-width: 54ch;\n}\n\n.root[data-align=\"center\"] .lede {\n max-width: 60ch;\n}\n"
2980
+ "content": "/* SectionIntro — marketing eyebrow + display heading + optional lede\n *\n * The eyebrow color is intentionally driven through a CSS custom property\n * (`--section-intro-eyebrow-color`) so that consumers can override it at\n * any ancestor level and have it track a live-rewritten brand accent var\n * (for example a keyed `--color-acid` that swaps between artist palette entries).\n * Default fallback chain: --section-intro-eyebrow-color → --accent-default\n * → --interactive-primary-bg → #111827 (Tailwind Gray-900).\n *\n * Alignment is controlled by data-align on the root (set by the `align`\n * prop). Both \"left\" and \"center\" are defined here.\n */\n\n.root {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n width: 100%;\n min-width: 0;\n}\n\n/* ── Alignment variants ─────────────────────────────────────────────────── */\n\n.root[data-align=\"left\"] {\n align-items: flex-start;\n text-align: left;\n}\n\n.root[data-align=\"center\"] {\n align-items: center;\n text-align: center;\n}\n\n/* ── Eyebrow ────────────────────────────────────────────────────────────── */\n\n.eyebrow {\n margin: 0;\n font-family: var(--font-mono, ui-monospace, monospace);\n font-size: var(--font-size-xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: var(--letter-spacing-widest, 0.2em);\n text-transform: uppercase;\n line-height: var(--line-height-tight, 1.25);\n /*\n * Live-rewriteable eyebrow color. Override --section-intro-eyebrow-color\n * at any ancestor (for example on the <section> that contains this component) to\n * track a keyed brand accent that changes at runtime.\n */\n color: var(\n --section-intro-eyebrow-color,\n var(--accent-default, var(--interactive-primary-bg, #111827))\n );\n}\n\n/* ── Heading ────────────────────────────────────────────────────────────── */\n\n.heading {\n margin: 0;\n font-family: var(--font-display, var(--font-family-heading, inherit));\n /* VI-687: theme-set display casing; inherit when unset. */\n text-transform: var(--font-display-text-transform, inherit);\n font-size: clamp(2.5rem, 6vw, 4rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: var(--line-height-none, 1.05);\n letter-spacing: var(--letter-spacing-tight, -0.02em);\n color: var(--text-primary, #111827);\n text-wrap: balance;\n max-width: 18ch;\n}\n\n.root[data-align=\"center\"] .heading {\n max-width: 22ch;\n}\n\n/* ── Lede ───────────────────────────────────────────────────────────────── */\n\n.lede {\n margin: 0;\n font-size: var(--font-size-base, 1rem);\n line-height: var(--line-height-relaxed, 1.6);\n font-weight: var(--font-weight-light, 300);\n color: var(--text-secondary, #6b7280);\n max-width: 54ch;\n}\n\n.root[data-align=\"center\"] .lede {\n max-width: 60ch;\n}\n"
2931
2981
  }
2932
2982
  ]
2933
2983
  },
@@ -5343,7 +5393,7 @@
5343
5393
  {
5344
5394
  "path": "components/devtools/source-inspector/visor-component-names.generated.ts",
5345
5395
  "type": "registry:devtool",
5346
- "content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActionRow\",\n \"ActionRowList\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetail\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"AmbientGlow\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BrowserFrame\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Capture\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Column\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"Composer\",\n \"ComposerContext\",\n \"ComposerField\",\n \"ComposerSend\",\n \"ComposerSpacer\",\n \"ComposerToolbar\",\n \"ComposerToolButton\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ConflictBanner\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogBack\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogContext\",\n \"DialogDescription\",\n \"DialogField\",\n \"DialogFieldControl\",\n \"DialogFieldLabel\",\n \"DialogFooter\",\n \"DialogForm\",\n \"DialogFormBody\",\n \"DialogFormClose\",\n \"DialogFormContent\",\n \"DialogFormDescription\",\n \"DialogFormFooter\",\n \"DialogFormHeader\",\n \"DialogFormTitle\",\n \"DialogFormTrigger\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogScrim\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DocFrame\",\n \"DocFrameBrand\",\n \"DocNav\",\n \"DocNavGroup\",\n \"DocNavPill\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"EditableBlock\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ErrorPlacard\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"FidelityMirror\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"InlineEdit\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MonthCalendar\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"PlatformChip\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadioGroupVariantContext\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RecordSection\",\n \"RightRailList\",\n \"SaveStatus\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"SegmentedControl\",\n \"SegmentedProgress\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"SensitivePanel\",\n \"Separator\",\n \"SessionTimeout\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetContext\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetScrim\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"SkeletonDetail\",\n \"SkeletonList\",\n \"SkeletonTable\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SlowNetworkBar\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"SpecimenCard\",\n \"SpecimenCardFooter\",\n \"Sphere\",\n \"SpherePlayground\",\n \"Spinner\",\n \"SpinnerOrb\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"StructuredPrompt\",\n \"StructuredPromptBody\",\n \"StructuredPromptHeader\",\n \"StructuredPromptHint\",\n \"StructuredPromptSlot\",\n \"SuccessLiveRegion\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"ThinkingOrb\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TimePicker\",\n \"TinInput\",\n \"TitleSlide\",\n \"ToastCard\",\n \"ToastCardStack\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"Vignette\",\n \"WorkspaceSwitcher\",\n])\n"
5396
+ "content": "// THIS FILE IS GENERATED BY scripts/generate-visor-component-names.ts.\n// Do not edit by hand. Re-run `npm run generate:component-names` after\n// adding, removing, or renaming a Visor component.\n//\n// Source of truth: registry/registry-{ui,blocks,deck,visual,devtools}.ts\n// Used by: components/devtools/source-inspector/* (VI-311)\n\nexport const VISOR_COMPONENT_NAMES: ReadonlySet<string> = new Set([\n \"AccessibilitySection\",\n \"AccessibilitySlide\",\n \"AccessibilitySpecimen\",\n \"Accordion\",\n \"AccordionContent\",\n \"AccordionItem\",\n \"AccordionTrigger\",\n \"ActionRow\",\n \"ActionRowList\",\n \"ActivityFeed\",\n \"ActivityFeedContext\",\n \"ActivityFeedItem\",\n \"ActivityFeedRoot\",\n \"AdminDashboard\",\n \"AdminDetail\",\n \"AdminDetailDrawer\",\n \"AdminListPage\",\n \"AdminListPageInner\",\n \"AdminSettingsPage\",\n \"AdminShell\",\n \"AdminTabbedEditor\",\n \"AdminWizard\",\n \"Alert\",\n \"AlertActions\",\n \"AlertDescription\",\n \"AlertTitle\",\n \"AmbientGlow\",\n \"Avatar\",\n \"AvatarFallback\",\n \"AvatarImage\",\n \"AvatarStack\",\n \"Badge\",\n \"Banner\",\n \"BannerAction\",\n \"BannerDescription\",\n \"BannerTitle\",\n \"BentoGrid\",\n \"BentoTile\",\n \"BentoTileBody\",\n \"BentoTileDescription\",\n \"BentoTileFigure\",\n \"BentoTileHeadline\",\n \"BentoTileMedia\",\n \"BentoTileMeta\",\n \"BentoTileTitle\",\n \"BottomNav\",\n \"Box\",\n \"Breadcrumb\",\n \"BreadcrumbEllipsis\",\n \"BreadcrumbItem\",\n \"BreadcrumbLink\",\n \"BreadcrumbList\",\n \"BreadcrumbPage\",\n \"BreadcrumbSeparator\",\n \"BrowserFrame\",\n \"BulkActionBar\",\n \"Button\",\n \"ButtonSpecimenSection\",\n \"ButtonSpecimenSlide\",\n \"Calendar\",\n \"Capture\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"Column\",\n \"Combobox\",\n \"ComboboxContent\",\n \"ComboboxContext\",\n \"ComboboxEmpty\",\n \"ComboboxGroup\",\n \"ComboboxInput\",\n \"ComboboxItem\",\n \"ComboboxSeparator\",\n \"Command\",\n \"CommandDialog\",\n \"CommandEmpty\",\n \"CommandGroup\",\n \"CommandInput\",\n \"CommandItem\",\n \"CommandList\",\n \"CommandLoading\",\n \"CommandSeparator\",\n \"CommandShortcut\",\n \"ComponentShowcaseContent\",\n \"ComponentShowcaseSection\",\n \"ComponentShowcaseSlide\",\n \"Composer\",\n \"ComposerContext\",\n \"ComposerField\",\n \"ComposerSend\",\n \"ComposerSpacer\",\n \"ComposerToolbar\",\n \"ComposerToolButton\",\n \"ConceptSlide\",\n \"ConfigurationPanel\",\n \"ConfirmDialog\",\n \"ConflictBanner\",\n \"Container\",\n \"ContextMenu\",\n \"ContextMenuCheckboxItem\",\n \"ContextMenuContent\",\n \"ContextMenuGroup\",\n \"ContextMenuItem\",\n \"ContextMenuLabel\",\n \"ContextMenuPortal\",\n \"ContextMenuRadioGroup\",\n \"ContextMenuRadioItem\",\n \"ContextMenuSeparator\",\n \"ContextMenuShortcut\",\n \"ContextMenuSub\",\n \"ContextMenuSubContent\",\n \"ContextMenuSubTrigger\",\n \"ContextMenuTrigger\",\n \"Controls\",\n \"CtaSection\",\n \"DataTable\",\n \"DataTableInner\",\n \"DatePicker\",\n \"DateRangePicker\",\n \"DeckContext\",\n \"DeckFooter\",\n \"DeckLayout\",\n \"DeckProvider\",\n \"DeckRenderer\",\n \"DesignSystemDeck\",\n \"DesignSystemSpecimen\",\n \"Dialog\",\n \"DialogBack\",\n \"DialogClose\",\n \"DialogContent\",\n \"DialogContext\",\n \"DialogDescription\",\n \"DialogField\",\n \"DialogFieldControl\",\n \"DialogFieldLabel\",\n \"DialogFooter\",\n \"DialogForm\",\n \"DialogFormBody\",\n \"DialogFormClose\",\n \"DialogFormContent\",\n \"DialogFormDescription\",\n \"DialogFormFooter\",\n \"DialogFormHeader\",\n \"DialogFormTitle\",\n \"DialogFormTrigger\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogScrim\",\n \"DialogTitle\",\n \"DialogTrigger\",\n \"DocFrame\",\n \"DocFrameBrand\",\n \"DocNav\",\n \"DocNavGroup\",\n \"DocNavPill\",\n \"DotNav\",\n \"DropdownMenu\",\n \"DropdownMenuCheckboxItem\",\n \"DropdownMenuContent\",\n \"DropdownMenuGroup\",\n \"DropdownMenuItem\",\n \"DropdownMenuLabel\",\n \"DropdownMenuPortal\",\n \"DropdownMenuRadioGroup\",\n \"DropdownMenuRadioItem\",\n \"DropdownMenuSeparator\",\n \"DropdownMenuShortcut\",\n \"DropdownMenuSub\",\n \"DropdownMenuSubContent\",\n \"DropdownMenuSubTrigger\",\n \"DropdownMenuTrigger\",\n \"EditableBlock\",\n \"ElevationCard\",\n \"ElevationSlide\",\n \"EmptyState\",\n \"ErrorPlacard\",\n \"ExportMenu\",\n \"FeaturesGrid\",\n \"FidelityMirror\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"InlineEdit\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MonthCalendar\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"PlatformChip\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PositionPicker\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"RadioGroupVariantContext\",\n \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RecordSection\",\n \"RightRailList\",\n \"SaveStatus\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\n \"SegmentedControl\",\n \"SegmentedProgress\",\n \"Select\",\n \"SelectContent\",\n \"SelectGroup\",\n \"SelectionListContext\",\n \"SelectItem\",\n \"SelectLabel\",\n \"SelectScrollDownButton\",\n \"SelectScrollUpButton\",\n \"SelectSeparator\",\n \"SelectTrigger\",\n \"SelectValue\",\n \"SemanticColorGrid\",\n \"SemanticColorItem\",\n \"SemanticTokensSlide\",\n \"SensitivePanel\",\n \"Separator\",\n \"SessionTimeout\",\n \"ShadowSection\",\n \"Sheet\",\n \"SheetClose\",\n \"SheetContent\",\n \"SheetContext\",\n \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\n \"SheetScrim\",\n \"SheetTitle\",\n \"SheetTrigger\",\n \"Sidebar\",\n \"SidebarContent\",\n \"SidebarContext\",\n \"SidebarFooter\",\n \"SidebarGroup\",\n \"SidebarGroupAction\",\n \"SidebarGroupContent\",\n \"SidebarGroupLabel\",\n \"SidebarHeader\",\n \"SidebarInset\",\n \"SidebarMenu\",\n \"SidebarMenuAction\",\n \"SidebarMenuBadge\",\n \"SidebarMenuButton\",\n \"SidebarMenuItem\",\n \"SidebarMenuSub\",\n \"SidebarMenuSubButton\",\n \"SidebarMenuSubItem\",\n \"SidebarProvider\",\n \"SidebarRail\",\n \"SidebarScopeContext\",\n \"SidebarSeparator\",\n \"SidebarTrigger\",\n \"Skeleton\",\n \"SkeletonDetail\",\n \"SkeletonList\",\n \"SkeletonTable\",\n \"Slide\",\n \"SlideHeader\",\n \"Slider\",\n \"SliderControl\",\n \"SlideThemeContext\",\n \"SlideThemeProvider\",\n \"SlowNetworkBar\",\n \"SourceInspector\",\n \"SourceInspectorContext\",\n \"SourceInspectorDevImpl\",\n \"SourceInspectorProvider\",\n \"SourceInspectorRunner\",\n \"SourceInspectorToggle\",\n \"SpacingScale\",\n \"SpacingSection\",\n \"SpacingSlide\",\n \"Sparkline\",\n \"SpecimenCard\",\n \"SpecimenCardFooter\",\n \"Sphere\",\n \"SpherePlayground\",\n \"Spinner\",\n \"SpinnerOrb\",\n \"Stack\",\n \"Stage\",\n \"StatCard\",\n \"StatHero\",\n \"StationSpectrum\",\n \"StatusBadge\",\n \"StatusColorsSlide\",\n \"StatusDot\",\n \"Stepper\",\n \"StepperContext\",\n \"StepperDescription\",\n \"StepperItem\",\n \"StepperSeparator\",\n \"StepperTitle\",\n \"StepperTrigger\",\n \"StepsSection\",\n \"StructuredPrompt\",\n \"StructuredPromptBody\",\n \"StructuredPromptHeader\",\n \"StructuredPromptHint\",\n \"StructuredPromptSlot\",\n \"SuccessLiveRegion\",\n \"SurfaceRow\",\n \"SurfaceScaleStack\",\n \"SurfaceSection\",\n \"Switch\",\n \"Table\",\n \"TableBody\",\n \"TableCaption\",\n \"TableCell\",\n \"TableFooter\",\n \"TableHead\",\n \"TableHeader\",\n \"TableRow\",\n \"Tabs\",\n \"TabsContent\",\n \"TabsList\",\n \"TabsTrigger\",\n \"TagInput\",\n \"TestimonialAttribution\",\n \"TestimonialSection\",\n \"Text\",\n \"Textarea\",\n \"ThemeArchitectureSlide\",\n \"ThemeColorsSlide\",\n \"ThemeSwitcher\",\n \"ThinkingOrb\",\n \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\n \"TimePicker\",\n \"TinInput\",\n \"TitleSlide\",\n \"ToastCard\",\n \"ToastCardStack\",\n \"Toaster\",\n \"TOCSlide\",\n \"ToggleButton\",\n \"ToggleDevImpl\",\n \"ToggleGroup\",\n \"ToggleGroupContext\",\n \"ToggleGroupItem\",\n \"Tooltip\",\n \"TooltipContent\",\n \"TooltipProvider\",\n \"TooltipTrigger\",\n \"TypeBodySlide\",\n \"TypeDisplaySlide\",\n \"TypeScaleStack\",\n \"TypeSpecimen\",\n \"TypographySection\",\n \"Vignette\",\n \"WorkspaceSwitcher\",\n])\n"
5347
5397
  }
5348
5398
  ]
5349
5399
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.4.0",
3
- "generated_at": "2026-10-04T04:36:38.216Z",
3
+ "generated_at": "2026-10-04T22:55:53.108Z",
4
4
  "components": {
5
5
  "accessibility-specimen": {
6
6
  "category": "specimen",
@@ -880,6 +880,76 @@
880
880
  ],
881
881
  "example": "<BentoGrid cols={{ base: 1, md: 2 }} gap=\"4\">\n <BentoTile span=\"full\" aspect=\"21/9\">\n <BentoTileMedia src=\"/projects/knowmentum.jpg\" alt=\"Knowmentum project hero\" />\n <BentoTileBody>\n <h3>Knowmentum</h3>\n <p>Knowledge platform for high-performers.</p>\n </BentoTileBody>\n </BentoTile>\n\n <BentoTile span=\"half\" aspect=\"2/1\" href=\"https://animal.nyc/\" target=\"_blank\">\n <BentoTileMedia src=\"/projects/animal-nyc.jpg\" alt=\"Animal NYC\" />\n <BentoTileBody>\n <h3>Animal NYC</h3>\n </BentoTileBody>\n </BentoTile>\n\n <BentoTile span=\"half\" aspect=\"2/1\" href=\"https://animal.la/\" target=\"_blank\">\n <BentoTileMedia src=\"/projects/animal-la.jpg\" alt=\"Animal LA\" />\n <BentoTileBody>\n <h3>Animal LA</h3>\n </BentoTileBody>\n </BentoTile>\n\n <BentoTile span=\"half\" aspect=\"4/3\" fit=\"contain\">\n <BentoTileMedia src=\"/logos/client-logo.png\" alt=\"Client logo\" />\n <BentoTileBody>\n <p>Natural-fit logo tile</p>\n </BentoTileBody>\n </BentoTile>\n</BentoGrid>\n"
882
882
  },
883
+ "bottom-nav": {
884
+ "category": "navigation",
885
+ "description": "A bottom navigation bar of icon destinations for phone widths. Filled active destination, an optional unread mark, a hairline group break, safe-area padding, and its height published as --bottom-nav-height.",
886
+ "when_to_use": [
887
+ "The primary destinations of an app at phone width, as a row fixed to the foot of the screen (the desktop rail becomes the foot bar)",
888
+ "3 to 7 top-level destinations that each deserve one tap",
889
+ "A destination that sometimes has something waiting on it (an unread dot on Inbox)"
890
+ ],
891
+ "when_not_to_use": [
892
+ "A top bar of site links (use Navbar)",
893
+ "A collapsible side rail on wide screens (use Sidebar)",
894
+ "Switching a view in place without changing the page (use SegmentedControl or Tabs)",
895
+ "More than 7 destinations (group the rest behind one More destination)"
896
+ ],
897
+ "variants": {
898
+ "showLabels": [
899
+ true,
900
+ false
901
+ ],
902
+ "fixed": [
903
+ true,
904
+ false
905
+ ]
906
+ },
907
+ "props": [
908
+ {
909
+ "name": "items",
910
+ "type": "{ label: string; icon: BottomNavIcon; href?: string; onSelect?: (e) => void; active?: boolean; mark?: boolean | number; markLabel?: string; group?: string; key?: string }[]",
911
+ "description": "The destinations, in order. A hairline is drawn before the first destination of each new group."
912
+ },
913
+ {
914
+ "name": "showLabels",
915
+ "type": "boolean",
916
+ "default": false,
917
+ "description": "Show each label under its icon. Otherwise the label is visually hidden."
918
+ },
919
+ {
920
+ "name": "fixed",
921
+ "type": "boolean",
922
+ "default": true,
923
+ "description": "Pin to the foot of the viewport and publish --bottom-nav-height on the document root."
924
+ }
925
+ ],
926
+ "dependencies": [],
927
+ "tokens_used": [
928
+ "--accent",
929
+ "--border-default",
930
+ "--border-focus",
931
+ "--focus-ring-offset",
932
+ "--focus-ring-width",
933
+ "--font-size-xs",
934
+ "--font-weight-medium",
935
+ "--hairline",
936
+ "--hairline-width",
937
+ "--motion-duration-150",
938
+ "--motion-easing-default",
939
+ "--radius-md",
940
+ "--separator-color",
941
+ "--spacing-1",
942
+ "--spacing-12",
943
+ "--spacing-2",
944
+ "--stroke-width-thin",
945
+ "--surface-card",
946
+ "--surface-interactive-active",
947
+ "--surface-interactive-hover",
948
+ "--text-primary",
949
+ "--text-secondary"
950
+ ],
951
+ "example": "<BottomNav\n aria-label=\"Workspace\"\n items={[\n { label: \"Edit\", icon: PencilSimple, href: \"/edit\", active: true },\n { label: \"Inbox\", icon: Tray, href: \"/inbox\", mark: 3 },\n { label: \"Team\", icon: UsersThree, href: \"/team\", group: \"admin\" },\n { label: \"Settings\", icon: Gear, href: \"/settings\", group: \"admin\" },\n ]}\n/>\n"
952
+ },
883
953
  "box": {
884
954
  "category": "layout",
885
955
  "description": "Universal layout wrapper for padding, margin, background, border, and border-radius via Visor design tokens.",
@@ -5244,6 +5314,7 @@
5244
5314
  ],
5245
5315
  "tokens_used": [
5246
5316
  "--font-heading",
5317
+ "--font-heading-text-transform",
5247
5318
  "--font-size-2xl",
5248
5319
  "--font-size-4xl",
5249
5320
  "--font-size-base",
@@ -6909,6 +6980,7 @@
6909
6980
  "tokens_used": [
6910
6981
  "--admin-ui-marquee-family",
6911
6982
  "--font-display",
6983
+ "--font-display-text-transform",
6912
6984
  "--font-family-heading",
6913
6985
  "--font-size-2xl",
6914
6986
  "--font-size-3xl",
@@ -7269,6 +7341,96 @@
7269
7341
  ],
7270
7342
  "example": "<Popover>\n <PopoverTrigger asChild>\n <Button variant=\"outline\">Filter</Button>\n </PopoverTrigger>\n <PopoverContent>\n <p>Filter options here</p>\n <PopoverFooter>\n <Button variant=\"ghost\">Cancel</Button>\n <Button>Apply</Button>\n </PopoverFooter>\n </PopoverContent>\n</Popover>\n"
7271
7343
  },
7344
+ "position-picker": {
7345
+ "category": "form",
7346
+ "description": "A 3x3 anchor grid. Nine targets, one chosen, the value a { y, x } pair. Draws on its own ground or over a photograph.",
7347
+ "when_to_use": [
7348
+ "Choosing a focal point over a photograph (nine dots on the image)",
7349
+ "Placing a logo, title or block at one of nine anchors",
7350
+ "Any choice of one of nine positions, in two dimensions"
7351
+ ],
7352
+ "when_not_to_use": [
7353
+ "A choice along one axis only (use RadioGroup or SegmentedControl)",
7354
+ "A free-form coordinate or percentage (use Slider or two NumberInputs)",
7355
+ "Dragging a point to an exact spot (this picks from nine, it does not drag)"
7356
+ ],
7357
+ "props": [
7358
+ {
7359
+ "name": "value",
7360
+ "type": "PositionValue | null",
7361
+ "description": "Controlled value, a { y: 'top' | 'center' | 'bottom', x: 'left' | 'center' | 'right' } pair. null is controlled with nothing chosen."
7362
+ },
7363
+ {
7364
+ "name": "defaultValue",
7365
+ "type": "PositionValue | null",
7366
+ "description": "Initial value when uncontrolled."
7367
+ },
7368
+ {
7369
+ "name": "onValueChange",
7370
+ "type": "(value: PositionValue) => void",
7371
+ "description": "Called with the chosen { y, x } pair."
7372
+ },
7373
+ {
7374
+ "name": "variant",
7375
+ "type": "'default' | 'on-image'",
7376
+ "default": "'default'",
7377
+ "description": "default draws the grid on its own ground. on-image fills a positioned parent (the photograph's frame) and gives every target its own scrim ring."
7378
+ },
7379
+ {
7380
+ "name": "disabled",
7381
+ "type": "boolean",
7382
+ "default": "false",
7383
+ "description": "Disables all nine targets."
7384
+ },
7385
+ {
7386
+ "name": "renderTarget",
7387
+ "type": "(position: PositionValue, state: { selected: boolean }) => ReactNode",
7388
+ "description": "Replaces the drawn dot of one target, for example an alignment icon in the occupied row. Decorative; the target keeps its accessible name."
7389
+ }
7390
+ ],
7391
+ "dependencies": [],
7392
+ "tokens_used": [
7393
+ "--_scrim",
7394
+ "--_target",
7395
+ "--border-default",
7396
+ "--border-focus",
7397
+ "--color-black",
7398
+ "--color-white",
7399
+ "--control-edge-color",
7400
+ "--control-edge-style",
7401
+ "--control-edge-width",
7402
+ "--focus-ring-offset",
7403
+ "--focus-ring-width",
7404
+ "--interactive-primary-bg",
7405
+ "--interactive-primary-text",
7406
+ "--motion-duration-150",
7407
+ "--motion-easing-default",
7408
+ "--opacity-50",
7409
+ "--position-picker-bg",
7410
+ "--position-picker-border",
7411
+ "--position-picker-image-mark",
7412
+ "--position-picker-mark",
7413
+ "--position-picker-mark-selected",
7414
+ "--position-picker-ring",
7415
+ "--position-picker-scrim",
7416
+ "--position-picker-target-size",
7417
+ "--primary",
7418
+ "--radius-full",
7419
+ "--radius-md",
7420
+ "--radius-sm",
7421
+ "--spacing-1",
7422
+ "--spacing-3",
7423
+ "--spacing-4",
7424
+ "--spacing-6",
7425
+ "--spacing-8",
7426
+ "--stroke-width-medium",
7427
+ "--surface-subtle",
7428
+ "--text-primary",
7429
+ "--text-secondary",
7430
+ "--text-tertiary"
7431
+ ],
7432
+ "example": "<PositionPicker\n aria-label=\"Focal point\"\n defaultValue={{ y: \"center\", x: \"center\" }}\n onValueChange={(position) => console.log(position)}\n/>\n"
7433
+ },
7272
7434
  "progress": {
7273
7435
  "category": "feedback",
7274
7436
  "description": "Horizontal bar indicating completion percentage of a task.",
@@ -7910,6 +8072,7 @@
7910
8072
  "tokens_used": [
7911
8073
  "--accent-default",
7912
8074
  "--font-display",
8075
+ "--font-display-text-transform",
7913
8076
  "--font-family-heading",
7914
8077
  "--font-mono",
7915
8078
  "--font-size-base",
@@ -8208,6 +8371,12 @@
8208
8371
  "name": "placeholder",
8209
8372
  "type": "string",
8210
8373
  "description": "Shown via SelectValue when no value is selected"
8374
+ },
8375
+ {
8376
+ "name": "container",
8377
+ "type": "HTMLElement | null",
8378
+ "default": "document.body",
8379
+ "description": "SelectContent only. Element the menu portals into. Pass the root of a class-scoped theme so the menu reads that theme's tokens instead of the page's.\n"
8211
8380
  }
8212
8381
  ],
8213
8382
  "sub_components": [
@@ -8221,7 +8390,7 @@
8221
8390
  },
8222
8391
  {
8223
8392
  "name": "SelectContent",
8224
- "description": "The dropdown container with scroll buttons and portal positioning."
8393
+ "description": "The dropdown container with scroll buttons and portal positioning (document.body unless `container` is set)."
8225
8394
  },
8226
8395
  {
8227
8396
  "name": "SelectItem",
@@ -8604,11 +8773,34 @@
8604
8773
  "type": "'offcanvas' | 'icon' | 'none'",
8605
8774
  "default": "offcanvas"
8606
8775
  },
8776
+ {
8777
+ "name": "position",
8778
+ "type": "'fixed' | 'contained'",
8779
+ "default": "fixed",
8780
+ "description": "Sidebar prop. `fixed` pins the sidebar to the viewport at full height. `contained` lays it out inside its parent, so an icon rail can sit under an app bar without covering it.\n"
8781
+ },
8607
8782
  {
8608
8783
  "name": "defaultOpen",
8609
8784
  "type": "boolean",
8610
8785
  "default": true,
8611
8786
  "description": "Initial open state (SidebarProvider prop)"
8787
+ },
8788
+ {
8789
+ "name": "keyboardShortcut",
8790
+ "type": "boolean",
8791
+ "default": true,
8792
+ "description": "SidebarProvider prop. Binds Cmd/Ctrl+B to toggle the sidebar. Skipped (no preventDefault) when only collapsible=\"none\" sidebars are mounted; false never binds it.\n"
8793
+ },
8794
+ {
8795
+ "name": "asChild",
8796
+ "type": "boolean",
8797
+ "default": false,
8798
+ "description": "SidebarMenuButton prop. Renders the child (a Next Link or anchor) as the item via Radix Slot."
8799
+ },
8800
+ {
8801
+ "name": "tooltip",
8802
+ "type": "string | (TooltipContentProps & { children: ReactNode })",
8803
+ "description": "SidebarMenuButton prop. Shown in a Tooltip when the sidebar is an icon rail (collapsible=\"icon\" while collapsed, or collapsible=\"none\"); never on mobile or when expanded. A string also names the button (aria-label) unless the button sets its own. Portals into the Sidebar so a scoped theme reaches it; pass `container` to override.\n"
8612
8804
  }
8613
8805
  ],
8614
8806
  "sub_components": [
@@ -8698,6 +8890,7 @@
8698
8890
  }
8699
8891
  ],
8700
8892
  "dependencies": [
8893
+ "@radix-ui/react-slot",
8701
8894
  "class-variance-authority",
8702
8895
  "@phosphor-icons/react"
8703
8896
  ],
@@ -10116,9 +10309,13 @@
10116
10309
  "--shadow-sm",
10117
10310
  "--surface-page",
10118
10311
  "--switch-edge-color",
10312
+ "--switch-knob-bg",
10313
+ "--switch-knob-bg-checked",
10119
10314
  "--switch-knob-inset",
10120
10315
  "--switch-track-bg",
10121
10316
  "--switch-track-height",
10317
+ "--switch-track-hover-bg",
10318
+ "--switch-track-hover-bg-checked",
10122
10319
  "--switch-track-width"
10123
10320
  ],
10124
10321
  "example": "<div style={{ display: \"flex\", alignItems: \"center\", gap: \"0.5rem\" }}>\n <Switch id=\"notifications\" />\n <Label htmlFor=\"notifications\">Enable notifications</Label>\n</div>\n"
@@ -11305,6 +11502,7 @@
11305
11502
  "--spacing-3",
11306
11503
  "--surface-overlay",
11307
11504
  "--text-inverse",
11505
+ "--text-primary",
11308
11506
  "--tooltip-bg",
11309
11507
  "--tooltip-text"
11310
11508
  ],
@@ -13755,18 +13953,8 @@
13755
13953
  "toast-card",
13756
13954
  "tooltip"
13757
13955
  ],
13758
- "layout": [
13759
- "box",
13760
- "collapsible",
13761
- "container",
13762
- "grid",
13763
- "inline",
13764
- "scroll-area",
13765
- "separator",
13766
- "sidebar",
13767
- "stack"
13768
- ],
13769
13956
  "navigation": [
13957
+ "bottom-nav",
13770
13958
  "breadcrumb",
13771
13959
  "command",
13772
13960
  "doc-frame",
@@ -13780,6 +13968,17 @@
13780
13968
  "stepper",
13781
13969
  "tabs"
13782
13970
  ],
13971
+ "layout": [
13972
+ "box",
13973
+ "collapsible",
13974
+ "container",
13975
+ "grid",
13976
+ "inline",
13977
+ "scroll-area",
13978
+ "separator",
13979
+ "sidebar",
13980
+ "stack"
13981
+ ],
13783
13982
  "form": [
13784
13983
  "button",
13785
13984
  "calendar",
@@ -13802,6 +14001,7 @@
13802
14001
  "otp-input",
13803
14002
  "password-input",
13804
14003
  "phone-input",
14004
+ "position-picker",
13805
14005
  "radio-group",
13806
14006
  "search-input",
13807
14007
  "segmented-control",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor",
3
- "version": "1.34.0",
3
+ "version": "1.36.0",
4
4
  "description": "CLI for the Visor design system — add components, hooks, and utilities to your project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@aws-sdk/client-s3": "^3.1021.0",
48
48
  "@babel/parser": "^7.26.0",
49
- "@loworbitstudio/visor-theme-engine": "^0.28.0",
49
+ "@loworbitstudio/visor-theme-engine": "^0.29.0",
50
50
  "commander": "^13.1.0",
51
51
  "diff": "^8.0.4",
52
52
  "picocolors": "^1.1.1",