@loworbitstudio/visor 1.17.0 → 1.18.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-07-08T04:58:15.600Z",
3
+ "generated_at": "2026-07-09T19:24:03.748Z",
4
4
  "components": {
5
5
  "accessibility-specimen": {
6
6
  "changeType": "current",
@@ -224,6 +224,18 @@
224
224
  "breakingChange": false,
225
225
  "migrationNote": null
226
226
  },
227
+ "doc-frame": {
228
+ "changeType": "current",
229
+ "files": [],
230
+ "breakingChange": false,
231
+ "migrationNote": null
232
+ },
233
+ "doc-nav": {
234
+ "changeType": "current",
235
+ "files": [],
236
+ "breakingChange": false,
237
+ "migrationNote": null
238
+ },
227
239
  "dropdown-menu": {
228
240
  "changeType": "current",
229
241
  "files": [],
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync32 } from "fs";
5
- import { dirname as dirname12, join as join32 } from "path";
4
+ import { readFileSync as readFileSync33 } from "fs";
5
+ import { dirname as dirname13, join as join33 } from "path";
6
6
  import { fileURLToPath as fileURLToPath4 } from "url";
7
7
  import { Command as Command2 } from "commander";
8
8
 
@@ -8172,10 +8172,394 @@ function spawnCommand(cwd, options) {
8172
8172
  }
8173
8173
  }
8174
8174
 
8175
+ // src/commands/render.ts
8176
+ import { existsSync as existsSync30, mkdirSync as mkdirSync14, readFileSync as readFileSync32 } from "fs";
8177
+ import { dirname as dirname12, isAbsolute as isAbsolute6, resolve as resolve20 } from "path";
8178
+ var FIXTURES = {
8179
+ "stat-card": {
8180
+ default: {
8181
+ export: "StatCard",
8182
+ props: `{
8183
+ label: "Total Revenue",
8184
+ value: "$48,120",
8185
+ delta: { value: "+12.4%", direction: "up", label: "vs last month" },
8186
+ footer: "Updated moments ago",
8187
+ }`
8188
+ }
8189
+ },
8190
+ "doc-nav": {
8191
+ default: {
8192
+ export: "DocNav",
8193
+ interactiveTarget: '[data-slot="doc-nav-group-trigger"]',
8194
+ props: `{
8195
+ currentPath: "/docs/getting-started",
8196
+ docs: [
8197
+ { order: 0, label: "Overview", href: "/docs/overview" },
8198
+ { order: 1, label: "Getting Started", href: "/docs/getting-started" },
8199
+ { order: 2, label: "Installation", href: "/docs/installation" },
8200
+ { order: 1, label: "Dashboard", href: "/docs/veronica/dashboard", scope: ["Veronica"], group: "Veronica" },
8201
+ { order: 2, label: "Settings", href: "/docs/veronica/settings", scope: ["Veronica"], group: "Veronica" },
8202
+ { order: 1, label: "Overview", href: "/docs/solespark/overview", scope: ["SoleSpark"], group: "SoleSpark" },
8203
+ { order: 10, label: "Changelog", href: "/docs/changelog" },
8204
+ ],
8205
+ }`
8206
+ }
8207
+ },
8208
+ button: {
8209
+ default: {
8210
+ export: "Button",
8211
+ interactiveTarget: "button",
8212
+ props: `{ children: "Get started" }`
8213
+ }
8214
+ },
8215
+ badge: {
8216
+ default: {
8217
+ export: "Badge",
8218
+ props: `{ children: "New" }`
8219
+ }
8220
+ }
8221
+ };
8222
+ var VALID_MODES = /* @__PURE__ */ new Set(["light", "dark"]);
8223
+ var VALID_STATES = /* @__PURE__ */ new Set(["default", "hover", "focus", "active"]);
8224
+ function missingDepsError(missing) {
8225
+ const installArgs = missing.join(" ");
8226
+ const needsBrowsers = missing.includes("playwright");
8227
+ return {
8228
+ code: "OPTIONAL_DEP_MISSING",
8229
+ message: `\`visor render\` needs ${missing.join(" and ")}, which ${missing.length > 1 ? "are" : "is"} not installed.`,
8230
+ hint: `Install with: npm install -D ${installArgs}${needsBrowsers ? " && npx playwright install chromium" : ""}`
8231
+ };
8232
+ }
8233
+ function pascalCase(kebab) {
8234
+ return kebab.split(/[-_]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
8235
+ }
8236
+ function firstExisting(candidates) {
8237
+ for (const c of candidates) {
8238
+ if (existsSync30(c)) return c;
8239
+ }
8240
+ return null;
8241
+ }
8242
+ function resolveComponentFile(cwd, name) {
8243
+ return firstExisting([
8244
+ resolve20(cwd, "components", "ui", name, `${name}.tsx`),
8245
+ resolve20(cwd, "components", "devtools", name, `${name}.tsx`),
8246
+ resolve20(cwd, "components", "ui", name, "index.tsx")
8247
+ ]);
8248
+ }
8249
+ function resolveThemeCssFile(cwd, slug2) {
8250
+ return firstExisting([
8251
+ resolve20(cwd, "packages", "docs", "app", `${slug2}-theme.css`),
8252
+ resolve20(cwd, "app", `${slug2}-theme.css`),
8253
+ resolve20(cwd, `${slug2}-theme.css`)
8254
+ ]);
8255
+ }
8256
+ function resolveTokensCssFile(cwd) {
8257
+ return firstExisting([
8258
+ resolve20(cwd, "packages", "tokens", "dist", "tokens.css"),
8259
+ resolve20(cwd, "node_modules", "@loworbitstudio", "visor-core", "dist", "tokens.css")
8260
+ ]);
8261
+ }
8262
+ async function loadOptional(moduleName) {
8263
+ try {
8264
+ return await import(moduleName);
8265
+ } catch {
8266
+ return null;
8267
+ }
8268
+ }
8269
+ var ANIMATION_DISABLE_CSS = "*, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; caret-color: transparent !important; }";
8270
+ async function settle(page, fontsTimeout = 5e3) {
8271
+ await page.waitForLoadState("networkidle").catch(() => {
8272
+ });
8273
+ await page.addStyleTag({ content: ANIMATION_DISABLE_CSS });
8274
+ await page.evaluate(
8275
+ `new Promise(function (r) { var t = setTimeout(r, ${fontsTimeout}); if (document.fonts && document.fonts.ready) { document.fonts.ready.then(function () { clearTimeout(t); r(); }); } else { clearTimeout(t); r(); }})`
8276
+ );
8277
+ }
8278
+ function buildHtml(opts) {
8279
+ const htmlClass = opts.mode === "dark" ? "dark" : "";
8280
+ return `<!doctype html>
8281
+ <html lang="en" class="${htmlClass}" style="color-scheme: ${opts.mode}">
8282
+ <head>
8283
+ <meta charset="utf-8" />
8284
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8285
+ <style data-visor="tokens">
8286
+ ${opts.tokensCss}
8287
+ </style>
8288
+ <style data-visor="theme">
8289
+ ${opts.themeCss}
8290
+ </style>
8291
+ <style data-visor="component">
8292
+ ${opts.componentCss}
8293
+ </style>
8294
+ <style data-visor="harness">
8295
+ html, body { margin: 0; padding: 0; }
8296
+ #theme-scope {
8297
+ box-sizing: border-box;
8298
+ min-height: 100vh;
8299
+ padding: 48px;
8300
+ display: flex;
8301
+ align-items: center;
8302
+ justify-content: center;
8303
+ background: var(--surface-page, #ffffff);
8304
+ color: var(--text-primary, #111827);
8305
+ font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
8306
+ }
8307
+ #root { width: 100%; max-width: 420px; }
8308
+ </style>
8309
+ </head>
8310
+ <body>
8311
+ <div id="theme-scope" class="${opts.themeClass}">
8312
+ <div id="root"></div>
8313
+ </div>
8314
+ <script>
8315
+ ${opts.bundleJs}
8316
+ </script>
8317
+ </body>
8318
+ </html>`;
8319
+ }
8320
+ function buildEntrySource(componentFile, fixture, exportName) {
8321
+ return `import * as React from "react";
8322
+ import { createRoot } from "react-dom/client";
8323
+ import * as __mod from ${JSON.stringify(componentFile)};
8324
+
8325
+ function __resolveComponent(mod, preferred) {
8326
+ if (preferred && mod[preferred]) return mod[preferred];
8327
+ if (mod.default) return mod.default;
8328
+ var isComp = function (v) {
8329
+ return typeof v === "function" || (v && typeof v === "object" && "$$typeof" in v);
8330
+ };
8331
+ for (var k in mod) { if (isComp(mod[k])) return mod[k]; }
8332
+ throw new Error("visor render: no React component export found");
8333
+ }
8334
+
8335
+ var __Component = __resolveComponent(__mod, ${JSON.stringify(exportName)});
8336
+ var __props = ${fixture.props};
8337
+ createRoot(document.getElementById("root")).render(
8338
+ React.createElement(__Component, __props)
8339
+ );`;
8340
+ }
8341
+ async function renderCommand(component, cwd, options) {
8342
+ const json = options.json ?? false;
8343
+ const mode = (options.mode ?? "light").toLowerCase();
8344
+ const state = (options.state ?? "default").toLowerCase();
8345
+ const fail3 = (code, message, extra) => {
8346
+ if (json) {
8347
+ console.log(JSON.stringify({ success: false, error: { code, message }, ...extra }, null, 2));
8348
+ } else {
8349
+ logger.error(message);
8350
+ if (extra?.hint) logger.item(String(extra.hint));
8351
+ }
8352
+ process.exit(1);
8353
+ };
8354
+ if (!VALID_MODES.has(mode)) {
8355
+ fail3("BAD_MODE", `Invalid --mode "${mode}". Use "light" or "dark".`);
8356
+ }
8357
+ if (!VALID_STATES.has(state)) {
8358
+ fail3("BAD_STATE", `Invalid --state "${state}". Use one of: ${[...VALID_STATES].join(", ")}.`);
8359
+ }
8360
+ const componentFile = resolveComponentFile(cwd, component);
8361
+ if (!componentFile) {
8362
+ fail3(
8363
+ "COMPONENT_NOT_FOUND",
8364
+ `Component "${component}" not found under components/ui/. Expected components/ui/${component}/${component}.tsx.`
8365
+ );
8366
+ }
8367
+ const themeCssFile = resolveThemeCssFile(cwd, options.theme);
8368
+ if (!themeCssFile) {
8369
+ fail3(
8370
+ "THEME_NOT_FOUND",
8371
+ `Theme "${options.theme}" not found. Expected packages/docs/app/${options.theme}-theme.css.`,
8372
+ { hint: "Run `visor theme sync` to (re)generate theme CSS, or check the slug." }
8373
+ );
8374
+ }
8375
+ const tokensCssFile = resolveTokensCssFile(cwd);
8376
+ if (!tokensCssFile) {
8377
+ fail3(
8378
+ "TOKENS_NOT_FOUND",
8379
+ "Emitted tokens.css not found. Expected packages/tokens/dist/tokens.css.",
8380
+ { hint: "Run `npm run build -w packages/tokens` first." }
8381
+ );
8382
+ }
8383
+ const fixtureName = options.fixture ?? "default";
8384
+ const componentFixtures = FIXTURES[component];
8385
+ const fixture = componentFixtures?.[fixtureName] ?? { props: "{}" };
8386
+ if (options.fixture && !componentFixtures?.[fixtureName]) {
8387
+ fail3(
8388
+ "FIXTURE_NOT_FOUND",
8389
+ `Fixture "${fixtureName}" not registered for "${component}". Available: ${componentFixtures ? Object.keys(componentFixtures).join(", ") : "(none)"}.`
8390
+ );
8391
+ }
8392
+ if (!componentFixtures && !json) {
8393
+ logger.warn(
8394
+ `No fixture registered for "${component}" \u2014 rendering with empty props. Add one to FIXTURES in render.ts for representative content.`
8395
+ );
8396
+ }
8397
+ const exportName = fixture.export ?? pascalCase(component);
8398
+ const esbuild = await loadOptional("esbuild");
8399
+ const playwright = await loadOptional("playwright");
8400
+ const missing = [];
8401
+ if (!esbuild) missing.push("esbuild");
8402
+ if (!playwright) missing.push("playwright");
8403
+ if (missing.length > 0) {
8404
+ const dep = missingDepsError(missing);
8405
+ fail3(dep.code, dep.message, { missing, hint: dep.hint });
8406
+ }
8407
+ let componentCss = "";
8408
+ let bundleJs = "";
8409
+ try {
8410
+ const entrySource = buildEntrySource(componentFile, fixture, exportName);
8411
+ const result = await esbuild.build({
8412
+ stdin: {
8413
+ contents: entrySource,
8414
+ resolveDir: cwd,
8415
+ loader: "tsx",
8416
+ sourcefile: "visor-render-entry.tsx"
8417
+ },
8418
+ bundle: true,
8419
+ format: "iife",
8420
+ platform: "browser",
8421
+ jsx: "automatic",
8422
+ write: false,
8423
+ outdir: "visor-render-out",
8424
+ define: { "process.env.NODE_ENV": '"production"' },
8425
+ banner: { js: "globalThis.process = globalThis.process || { env: {} };" },
8426
+ logLevel: "silent"
8427
+ });
8428
+ for (const file of result.outputFiles ?? []) {
8429
+ if (file.path.endsWith(".css")) componentCss += file.text;
8430
+ else if (file.path.endsWith(".js")) bundleJs += file.text;
8431
+ }
8432
+ } catch (err) {
8433
+ const message = err instanceof Error ? err.message : String(err);
8434
+ fail3("BUNDLE_FAILED", `Failed to bundle ${component}: ${message}`);
8435
+ }
8436
+ const themeClass = `${options.theme}-theme`;
8437
+ const html = buildHtml({
8438
+ tokensCss: readFileSync32(tokensCssFile, "utf-8"),
8439
+ themeCss: readFileSync32(themeCssFile, "utf-8"),
8440
+ componentCss,
8441
+ bundleJs,
8442
+ themeClass,
8443
+ mode
8444
+ });
8445
+ const width = Number(options.width ?? 720);
8446
+ const height = Number(options.height ?? 640);
8447
+ const outPath = resolveOutPath(cwd, options.out, component, options.theme, mode, state);
8448
+ mkdirSync14(dirname12(outPath), { recursive: true });
8449
+ const browser = await playwright.chromium.launch();
8450
+ let probe;
8451
+ try {
8452
+ const context = await browser.newContext({
8453
+ viewport: { width, height },
8454
+ colorScheme: mode,
8455
+ deviceScaleFactor: 2
8456
+ });
8457
+ const page = await context.newPage();
8458
+ await page.setContent(html, { waitUntil: "load" });
8459
+ await page.waitForFunction(
8460
+ "document.getElementById('root') && document.getElementById('root').childElementCount > 0",
8461
+ void 0,
8462
+ { timeout: 1e4 }
8463
+ ).catch(() => {
8464
+ });
8465
+ await settle(page);
8466
+ probe = await page.evaluate(
8467
+ `(function () {
8468
+ var scope = document.getElementById("theme-scope");
8469
+ var html = document.documentElement;
8470
+ var read = function (el, prop) {
8471
+ return getComputedStyle(el).getPropertyValue(prop).trim();
8472
+ };
8473
+ var themed = read(scope, "--surface-card");
8474
+ var base = read(html, "--surface-card");
8475
+ return {
8476
+ themedSurfaceCard: themed,
8477
+ baseSurfaceCard: base,
8478
+ themedBg: getComputedStyle(scope).backgroundColor,
8479
+ mapped: themed !== "" && themed !== base,
8480
+ };
8481
+ })()`
8482
+ );
8483
+ if (state !== "default") {
8484
+ const target = fixture.interactiveTarget ?? "#root button, #root a, #root input, #root [tabindex]";
8485
+ const el = await page.$(target);
8486
+ if (el) {
8487
+ if (state === "hover") await el.hover();
8488
+ else if (state === "focus") await el.focus();
8489
+ else if (state === "active") {
8490
+ await el.hover();
8491
+ await page.mouse.down();
8492
+ }
8493
+ await page.waitForTimeout(80);
8494
+ } else if (!json) {
8495
+ logger.warn(`No interactive target ("${target}") found for state "${state}".`);
8496
+ }
8497
+ }
8498
+ await page.screenshot({ path: outPath, fullPage: true });
8499
+ if (state === "active") await page.mouse.up().catch(() => {
8500
+ });
8501
+ } finally {
8502
+ await browser.close();
8503
+ }
8504
+ const fileSize = existsSync30(outPath) ? statBytes(outPath) : 0;
8505
+ if (json) {
8506
+ console.log(
8507
+ JSON.stringify(
8508
+ {
8509
+ success: true,
8510
+ component,
8511
+ theme: options.theme,
8512
+ mode,
8513
+ state,
8514
+ fixture: fixtureName,
8515
+ out: outPath,
8516
+ bytes: fileSize,
8517
+ probe
8518
+ },
8519
+ null,
8520
+ 2
8521
+ )
8522
+ );
8523
+ return;
8524
+ }
8525
+ logger.success(`Rendered ${component} \xB7 ${options.theme} \xB7 ${mode}${state !== "default" ? ` \xB7 ${state}` : ""}`);
8526
+ logger.item(`\u2192 ${outPath} (${formatBytes2(fileSize)})`);
8527
+ logger.blank();
8528
+ if (probe.mapped) {
8529
+ logger.success(
8530
+ `Themed --surface-card resolved to ${probe.themedSurfaceCard} (base primitive: ${probe.baseSurfaceCard}) \u2014 theme mapping applied.`
8531
+ );
8532
+ } else {
8533
+ logger.warn(
8534
+ `Themed --surface-card (${probe.themedSurfaceCard}) equals the base primitive (${probe.baseSurfaceCard}). The theme override did not apply \u2014 check @layer/mode scoping (VI-511).`
8535
+ );
8536
+ }
8537
+ }
8538
+ function resolveOutPath(cwd, out, component, theme2, mode, state) {
8539
+ if (out) {
8540
+ return isAbsolute6(out) ? out : resolve20(cwd, out);
8541
+ }
8542
+ const suffix = state === "default" ? "" : `__${state}`;
8543
+ return resolve20(cwd, ".visor", "renders", `${component}__${theme2}__${mode}${suffix}.png`);
8544
+ }
8545
+ function statBytes(path2) {
8546
+ try {
8547
+ return readFileSync32(path2).length;
8548
+ } catch {
8549
+ return 0;
8550
+ }
8551
+ }
8552
+ function formatBytes2(bytes) {
8553
+ if (bytes < 1024) return `${bytes} B`;
8554
+ const kb = bytes / 1024;
8555
+ if (kb < 1024) return `${kb.toFixed(1)} KB`;
8556
+ return `${(kb / 1024).toFixed(1)} MB`;
8557
+ }
8558
+
8175
8559
  // src/index.ts
8176
- var __dirname2 = dirname12(fileURLToPath4(import.meta.url));
8560
+ var __dirname2 = dirname13(fileURLToPath4(import.meta.url));
8177
8561
  var pkg = JSON.parse(
8178
- readFileSync32(join32(__dirname2, "..", "package.json"), "utf-8")
8562
+ readFileSync33(join33(__dirname2, "..", "package.json"), "utf-8")
8179
8563
  );
8180
8564
  var program = new Command2();
8181
8565
  program.name("visor").description("CLI for the Visor design system").version(pkg.version);
@@ -8373,4 +8757,9 @@ program.command("spawn").description(
8373
8757
  ).option("--from <identifier>", "blessed build to spawn: blessed:{shape}:{pattern}").option("--theme <id>", "theme id (or path to a .visor.yaml) to re-skin the fork with").option("--theme-file <path>", "explicit path to a theme.visor.yaml \u2014 bypasses --theme name resolution").option("--output <path>", "destination directory for the forked project").option("--blessed-dir <path>", "override the blessed-build root (default: VISOR_BLESSED_DIR or ~/Code/low-orbit/low-orbit-playbook/design-prototypes)").option("--install", "run npm install in the forked project (default: skip)").option("--validate", "validate the applied theme after forking (default: skip)").option("--list-blessed", "list all discoverable blessed builds and exit").option("--json", "output structured JSON (for AI agents)").action((options) => {
8374
8758
  spawnCommand(process.cwd(), options);
8375
8759
  });
8760
+ program.command("render").description(
8761
+ "Render a single component to a PNG using the real emitted tokens + real theme CSS + the real component (serverless, no next dev). A per-component render-fidelity harness."
8762
+ ).argument("<component>", "component name under components/ui/ (e.g. doc-nav, stat-card)").requiredOption("--theme <slug>", "theme slug (e.g. space, neutral) \u2014 resolves packages/docs/app/<slug>-theme.css").option("--mode <mode>", "color mode: light or dark", "light").option("--state <state>", "interactive state to capture: default, hover, focus, active", "default").option("--fixture <name>", "named fixture for the component (default: 'default')").option("-o, --out <path>", "output PNG path (default: .visor/renders/<component>__<theme>__<mode>.png)").option("--width <px>", "viewport width in px", "720").option("--height <px>", "viewport height in px", "640").option("--json", "output structured JSON (for AI agents)").action(async (component, options) => {
8763
+ await renderCommand(component, process.cwd(), options);
8764
+ });
8376
8765
  program.parse();
@@ -2795,6 +2795,57 @@
2795
2795
  }
2796
2796
  ]
2797
2797
  },
2798
+ {
2799
+ "name": "doc-nav",
2800
+ "type": "registry:ui",
2801
+ "description": "A manifest-driven, grouped/collapsible, multi-product-aware documentation navigation. Renders peer collapsible groups from a manifest slice — a pinned Shared set, accordion product groups (one open at a time), and an Appendix bucket for ad-hoc docs — resolving the active pill from currentPath. The group row wraps and is never an overflow-x scroll strip. Replaces the vanilla-JS doc nav.",
2802
+ "category": "navigation",
2803
+ "dependencies": [
2804
+ "@phosphor-icons/react",
2805
+ "@loworbitstudio/visor-core"
2806
+ ],
2807
+ "registryDependencies": [
2808
+ "utils"
2809
+ ],
2810
+ "files": [
2811
+ {
2812
+ "path": "components/ui/doc-nav/doc-nav.tsx",
2813
+ "type": "registry:ui",
2814
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n CaretDownIcon,\n CaretRightIcon,\n ArrowSquareOutIcon,\n} from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./doc-nav.module.css\"\n\n/**\n * Order threshold at or above which a doc with no group and no scope is\n * treated as an ad-hoc \"random\" doc and tucked into the Appendix bucket.\n * Mirrors the PL-2185 rule: no scope + no group + order >= 10 -> Appendix.\n */\nconst APPENDIX_ORDER_THRESHOLD = 10\n\nconst APPENDIX_ID = \"appendix\"\nconst APPENDIX_LABEL = \"Appendix\"\nconst SHARED_ID = \"shared\"\nconst SHARED_LABEL = \"Shared\"\n\n/**\n * A single documentation entry — the manifest slice DocFrame passes down.\n * Every field but `order`/`label`/`href` is optional; absence of `scope`,\n * `group`, and `tier` reproduces the flat, single-row behaviour.\n */\nexport interface DocEntry {\n /** Sort order within the resolved group. `0` marks the hub/overview entry. */\n order: number\n /** Visible pill label. */\n label: string\n /** Destination URL (route or static `/docs/*.html`). */\n href: string\n /** Manifest kind (`route`, `local-html`, `external`, …). Used to detect external links. */\n kind?: string\n /** PL-2177 product scope — which product group(s) the doc belongs to. */\n scope?: string[]\n /** PL-2185 nav section-label hint. Defaults from `scope`. */\n group?: string\n /** PL-2170 load depth — referenced here, owned there. */\n tier?: number\n /** Force external treatment (new tab + badge). Defaults from `kind`/`href`. */\n external?: boolean\n}\n\nexport interface DocNavProps\n extends Omit<React.ComponentProps<\"nav\">, \"children\"> {\n /** The manifest slice for the active view (Shared + product-scoped), pre-filtered by DocFrame. */\n docs: DocEntry[]\n /** Which product group is open (accordion). Absent → single-product mode (no accordion). */\n activeProduct?: string\n /**\n * Accordion callback — expand a product group (the parent collapses the\n * sibling by swapping `activeProduct`). Absent → groups render as plain,\n * independently expandable links (static-doc mode).\n */\n onProductToggle?: (id: string) => void\n /** Groups that stay open regardless of the accordion. Default `[\"shared\"]`. */\n pinnedGroups?: string[]\n /** Resolves the active pill and auto-expands its group. */\n currentPath: string\n /** Whether non-active, non-pinned groups start collapsed. Default `true` — the anti-wall rule. */\n defaultCollapsed?: boolean\n}\n\ntype GroupRole = \"pinned\" | \"collapsible\" | \"appendix\"\n\ninterface ResolvedGroup {\n id: string\n label: string\n role: GroupRole\n entries: DocEntry[]\n}\n\n// ─── helpers ─────────────────────────────────────────────────────────────────\n\nfunction slug(value: string): string {\n return value.trim().toLowerCase().replace(/\\s+/g, \"-\")\n}\n\nfunction titleCase(value: string): string {\n if (value.length === 0) return value\n return value.charAt(0).toUpperCase() + value.slice(1)\n}\n\n/** Strip query + hash so a static `href` compares cleanly against `currentPath`. */\nfunction normalizePath(value: string): string {\n return value.replace(/[?#].*$/, \"\")\n}\n\nfunction hrefMatchesPath(href: string, currentPath: string): boolean {\n if (!href) return false\n return normalizePath(href) === normalizePath(currentPath)\n}\n\nfunction isExternalEntry(entry: DocEntry): boolean {\n if (typeof entry.external === \"boolean\") return entry.external\n if (entry.kind === \"external\") return true\n return /^https?:\\/\\//.test(entry.href)\n}\n\n/** Resolve which group an entry belongs to, following the PL-2185 rules. */\nfunction groupKeyFor(entry: DocEntry): { id: string; label: string } {\n const hasGroup = typeof entry.group === \"string\" && entry.group.length > 0\n const hasScope = Array.isArray(entry.scope) && entry.scope.length > 0\n\n if (!hasGroup && !hasScope && entry.order >= APPENDIX_ORDER_THRESHOLD) {\n return { id: APPENDIX_ID, label: APPENDIX_LABEL }\n }\n if (hasGroup) {\n return { id: slug(entry.group as string), label: entry.group as string }\n }\n if (hasScope) {\n const first = (entry.scope as string[])[0]\n return { id: slug(first), label: titleCase(first) }\n }\n return { id: SHARED_ID, label: SHARED_LABEL }\n}\n\n/** Bucket the docs into ordered, non-empty groups (pinned → collapsible → appendix). */\nfunction resolveGroups(\n docs: DocEntry[],\n pinnedGroups: string[]\n): ResolvedGroup[] {\n const byId = new Map<string, ResolvedGroup>()\n const appearance: string[] = []\n\n for (const entry of docs) {\n const key = groupKeyFor(entry)\n let group = byId.get(key.id)\n if (!group) {\n const role: GroupRole =\n key.id === APPENDIX_ID\n ? \"appendix\"\n : pinnedGroups.includes(key.id)\n ? \"pinned\"\n : \"collapsible\"\n group = { id: key.id, label: key.label, role, entries: [] }\n byId.set(key.id, group)\n appearance.push(key.id)\n }\n group.entries.push(entry)\n }\n\n for (const group of byId.values()) {\n group.entries.sort((a, b) => a.order - b.order)\n }\n\n const rank = (group: ResolvedGroup): number => {\n if (group.role === \"pinned\") {\n const idx = pinnedGroups.indexOf(group.id)\n return idx === -1 ? 0 : idx\n }\n if (group.role === \"appendix\") return Number.MAX_SAFE_INTEGER\n return 1000 + appearance.indexOf(group.id)\n }\n\n return appearance\n .map((id) => byId.get(id) as ResolvedGroup)\n .sort((a, b) => rank(a) - rank(b))\n}\n\n// ─── DocNav ──────────────────────────────────────────────────────────────────\n\nconst DocNav = React.forwardRef<HTMLElement, DocNavProps>(\n (\n {\n docs,\n activeProduct,\n onProductToggle,\n pinnedGroups = [SHARED_ID],\n currentPath,\n defaultCollapsed = true,\n className,\n \"aria-label\": ariaLabel,\n ...props\n },\n ref\n ) => {\n const groups = React.useMemo(\n () => resolveGroups(docs, pinnedGroups),\n [docs, pinnedGroups]\n )\n\n // The group holding the active doc — always expanded on load.\n const activeGroupId = React.useMemo(() => {\n for (const group of groups) {\n if (group.entries.some((e) => hrefMatchesPath(e.href, currentPath))) {\n return group.id\n }\n }\n return null\n }, [groups, currentPath])\n\n const isControlled = onProductToggle != null\n\n // Uncontrolled expand state (static-doc mode + appendix, which is never a product).\n const [openSet, setOpenSet] = React.useState<Set<string>>(() => {\n const initial = new Set<string>()\n if (activeGroupId) initial.add(activeGroupId)\n if (activeProduct) initial.add(activeProduct)\n for (const group of groups) {\n if (group.role === \"pinned\") continue\n if (group.role === \"appendix\") {\n // A lone ad-hoc doc renders inline; two or more collapse.\n if (group.entries.length <= 1) initial.add(group.id)\n continue\n }\n if (!defaultCollapsed) initial.add(group.id)\n }\n return initial\n })\n\n const isOpen = (group: ResolvedGroup): boolean => {\n if (group.role === \"pinned\") return true\n if (group.role === \"appendix\") return openSet.has(group.id)\n // Collapsible product group.\n if (isControlled) {\n return group.id === activeProduct || group.id === activeGroupId\n }\n return openSet.has(group.id) || group.id === activeGroupId\n }\n\n const toggle = (group: ResolvedGroup): void => {\n if (group.role === \"pinned\") return\n if (group.role === \"collapsible\" && isControlled) {\n onProductToggle(group.id)\n return\n }\n setOpenSet((prev) => {\n const next = new Set(prev)\n if (next.has(group.id)) next.delete(group.id)\n else next.add(group.id)\n return next\n })\n }\n\n return (\n <nav\n ref={ref}\n aria-label={ariaLabel ?? \"Documentation\"}\n data-slot=\"doc-nav\"\n className={cn(styles.root, className)}\n {...props}\n >\n {groups.map((group) => (\n <DocNavGroup\n key={group.id}\n group={group}\n open={isOpen(group)}\n currentPath={currentPath}\n onToggle={() => toggle(group)}\n />\n ))}\n </nav>\n )\n }\n)\nDocNav.displayName = \"DocNav\"\n\n// ─── DocNavGroup (internal) ──────────────────────────────────────────────────\n\ninterface DocNavGroupProps {\n group: ResolvedGroup\n open: boolean\n currentPath: string\n onToggle: () => void\n}\n\nfunction DocNavGroup({ group, open, currentPath, onToggle }: DocNavGroupProps) {\n const panelId = React.useId()\n const isPinned = group.role === \"pinned\"\n const Caret = open ? CaretDownIcon : CaretRightIcon\n\n const head = (\n <>\n <Caret className={styles.caret} weight=\"fill\" aria-hidden=\"true\" />\n <span className={styles.scopeDot} aria-hidden=\"true\" />\n <span className={styles.groupLabel}>{group.label}</span>\n {!open && <span className={styles.count}>{group.entries.length}</span>}\n </>\n )\n\n return (\n <div\n data-slot=\"doc-nav-group\"\n data-group={group.id}\n data-role={group.role}\n data-state={open ? \"open\" : \"closed\"}\n className={cn(\n styles.group,\n isPinned && styles.groupPinned,\n !open && styles.groupCollapsed\n )}\n >\n {isPinned ? (\n <div className={styles.head}>{head}</div>\n ) : (\n <button\n type=\"button\"\n data-slot=\"doc-nav-group-trigger\"\n className={cn(styles.head, styles.headButton)}\n aria-expanded={open}\n aria-controls={panelId}\n onClick={onToggle}\n >\n {head}\n </button>\n )}\n\n {open && (\n <div id={panelId} className={styles.pills}>\n {group.entries.map((entry, index) => (\n <DocNavPill\n key={entry.href}\n entry={entry}\n active={hrefMatchesPath(entry.href, currentPath)}\n // Hub = the group's overview entry: an explicit order-0 doc, or the\n // lead pill of the pinned Shared set (which often starts at order 1).\n isHub={entry.order === 0 || (group.role === \"pinned\" && index === 0)}\n />\n ))}\n </div>\n )}\n </div>\n )\n}\n\n// ─── DocNavPill (internal) ───────────────────────────────────────────────────\n\ninterface DocNavPillProps {\n entry: DocEntry\n active: boolean\n /** Marks the group's hub/overview entry — a leading accent dot. */\n isHub: boolean\n}\n\nfunction DocNavPill({ entry, active, isHub }: DocNavPillProps) {\n const external = isExternalEntry(entry)\n\n return (\n <a\n href={entry.href}\n data-slot=\"doc-nav-pill\"\n data-active={active || undefined}\n data-hub={isHub || undefined}\n aria-current={active ? \"page\" : undefined}\n className={cn(styles.pill, active && styles.pillActive, isHub && styles.pillHub)}\n target={external ? \"_blank\" : undefined}\n rel={external ? \"noopener noreferrer\" : undefined}\n >\n <span className={styles.pillNum}>{entry.order}</span>\n <span className={styles.pillLabel}>{entry.label}</span>\n {external && (\n <>\n <ArrowSquareOutIcon className={styles.pillExternal} aria-hidden=\"true\" />\n <span className={styles.srOnly}>(opens in a new tab)</span>\n </>\n )}\n </a>\n )\n}\n\nexport { DocNav }\n"
2815
+ },
2816
+ {
2817
+ "path": "components/ui/doc-nav/doc-nav.module.css",
2818
+ "type": "registry:ui",
2819
+ "content": "/* DocNav — manifest-driven, grouped/collapsible doc navigation.\n *\n * Renders peer, collapsible groups from a manifest slice: a pinned Shared set,\n * accordion product groups (one open at a time), and an Appendix bucket for\n * ad-hoc docs. The group row wraps (flex-wrap) and is NEVER an overflow-x\n * scroll strip — the run-off-the-edge defect this component exists to kill.\n *\n * Theme-agnostic: every value references a CSS custom property token.\n */\n\n/* Root — the wrapping row of peer groups. */\n.root {\n /* Accent-pop indirection (B2): the active pill / caret / hub / pinned frame\n * all route through this single token so a consumer (DocFrame) can retint the\n * whole nav in one place. Defaults to the theme's vivid `--accent` — not\n * `--primary`, which reads dull on muted-primary themes such as Strata. */\n --doc-nav-accent: var(--accent, #2563eb);\n /* Group cluster radius — a controlled, fixed radius the doc-nav owns, NOT the\n * theme `--radius-*` scale, which decorative themes inflate far past what a\n * dense nav cluster wants (for example Strata inflates `--radius-xl` to 32px).\n * The approved design pins ~12px per theme for exactly this reason; any single\n * theme radius token renders ~6px on some themes and ~32px on others.\n * Overridable by a consumer/theme via this variable. */\n --doc-nav-group-radius: 0.75rem;\n /* Resting pill fill — a recessed well, darker than the group's `--surface-card`\n * in BOTH modes. The surface ramp inverts light↔dark (page is darkest in dark\n * but lightest in light), so a single surface token can't stay \"below\" the card;\n * mixing the card toward neutral-950 darkens it directionally in either mode.\n * Overridable by a consumer/theme. */\n --doc-nav-pill-bg: color-mix(\n in srgb,\n var(--surface-card, #111827),\n var(--color-neutral-950, #030712) 20%\n );\n display: flex;\n flex-wrap: wrap;\n align-items: flex-start;\n gap: var(--spacing-2, 0.5rem);\n /* Deliberately no overflow-x: groups wrap down, never scroll sideways. */\n}\n\n/* Group — a bordered, surface-backed cluster: header + pill row. */\n.group {\n display: inline-flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-2, 0.5rem);\n border: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n border-radius: var(--doc-nav-group-radius, var(--radius-md, 0.375rem));\n background: var(--surface-card, #ffffff);\n}\n\n/* Pinned — the Shared group stays open; a subtle frame keyed to the group hue.\n * Routed through overridable tokens (P5) so borderless themes (Animal / ENTR)\n * can null the frame — set `--doc-nav-pin-border: transparent` and\n * `--doc-nav-pin-bg: var(--surface-card)`. Default derives from the group's\n * shared hue, not `--primary` (which read as an unwanted border on those\n * borderless themes). */\n.groupPinned {\n border-color: var(\n --doc-nav-pin-border,\n color-mix(in srgb, var(--doc-nav-group-accent, var(--info, #0ea5e9)) 32%, transparent)\n );\n background: var(\n --doc-nav-pin-bg,\n color-mix(in srgb, var(--doc-nav-group-accent, var(--info, #0ea5e9)) 6%, var(--surface-card, #ffffff))\n );\n}\n\n/* Collapsed — a group reduces to a single pill-shaped chip. */\n.groupCollapsed {\n padding: 0;\n border-color: transparent;\n background: transparent;\n}\n\n/* Header — the group label row. As a chip when collapsed, plain when open. */\n.head {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.14em;\n text-transform: uppercase;\n /* Collapse the label's line box to the glyph height so the all-caps text\n * optically centers against the dot/caret (matching `.pill`). Without this the\n * caps ride high — the empty descender space pushes the line-box center below\n * the visual center of the caps. */\n line-height: 1;\n color: var(--text-secondary, #4b5563);\n}\n\n/* Button variant — collapsible group heads are clickable. */\n.headButton {\n cursor: pointer;\n border: 0;\n background: transparent;\n font-family: inherit;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.headButton:hover {\n color: var(--text-primary, #111827);\n}\n\n.headButton:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n border-radius: var(--radius-sm, 0.25rem);\n}\n\n/* Collapsed group head — reads as a resting chip. */\n.groupCollapsed .head {\n border: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n border-radius: var(--radius-full, 9999px);\n /* A collapsed chip stands alone in the nav row, so it carries more vertical\n * padding than the tight open-group label (the design gives it ~7px 12px). */\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n /* The chip reads as the group's card surface (matching the design's chip-bg),\n * not a lighter subtle surface — `--surface-subtle` resolves as a mid-gray in\n * dark themes. */\n background: var(--surface-card, #111827);\n}\n\n/* Caret — accent-popped fill glyph (P6). */\n.caret {\n flex-shrink: 0;\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n}\n\n/* Scope dot — a per-group grouping cue, color-coded with a soft glow (P1).\n * `color` carries the group hue so `currentColor` drives both the fill and the\n * glow; a consumer overrides the whole group via `--doc-nav-group-accent`.\n * Default (product groups) is the vivid accent; pinned/appendix retint below. */\n.scopeDot {\n width: var(--spacing-2, 0.5rem);\n height: var(--spacing-2, 0.5rem);\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n color: var(--doc-nav-group-accent, var(--accent, #2563eb));\n background: currentColor;\n box-shadow: 0 0 var(--spacing-2, 0.5rem)\n color-mix(in srgb, currentColor 60%, transparent);\n}\n\n/* Shared (pinned) group reads in the info hue. */\n.groupPinned .scopeDot {\n color: var(--doc-nav-group-accent, var(--info, #0ea5e9));\n}\n\n/* Appendix reads muted and un-glowed — an ad-hoc bucket, not a product. */\n.group[data-role=\"appendix\"] .scopeDot {\n color: var(--doc-nav-group-accent, var(--text-tertiary, #9ca3af));\n box-shadow: none;\n}\n\n/* Group label text. */\n.groupLabel {\n min-width: 0;\n}\n\n/* Trailing count badge — a hairline mono pill on a collapsed group (N2). */\n.count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n min-width: var(--spacing-4, 1rem);\n padding: 0 calc(var(--spacing-1, 0.25rem) * 1.5);\n border: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n border-radius: var(--radius-full, 9999px);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n /* ~9px — a quarter down from the xs step, matching the design's tiny badge. */\n font-size: calc(var(--font-size-xs, 0.75rem) * 0.75);\n font-variant-numeric: tabular-nums;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Pill row — wraps within the group. */\n.pills {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-1, 0.25rem);\n}\n\n/* Pill — a single doc link. Mono · UPPERCASE · letter-spaced (B1). */\n.pill {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n border-radius: var(--radius-full, 9999px);\n background: var(--doc-nav-pill-bg, var(--surface-card, #111827));\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n line-height: 1;\n white-space: nowrap;\n color: var(--text-secondary, #4b5563);\n text-decoration: none;\n cursor: pointer;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n border-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n background-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.pill:hover {\n color: var(--text-primary, #111827);\n border-color: var(--hairline, var(--border-default, #e5e7eb));\n}\n\n.pill:focus-visible {\n outline: var(--focus-ring-width, 2px) solid\n var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n/* Shared-group pills carry the group's tint at rest (P4) — hover/active win. */\n.groupPinned .pill:not(.pillActive) {\n color: var(--doc-nav-group-accent, var(--info, #0ea5e9));\n}\n\n.groupPinned .pill:not(.pillActive):hover {\n color: var(--text-primary, #111827);\n}\n\n/* Active — the current doc, resolved from currentPath. Contrast-safe (VI-612):\n * the label is `--text-primary` (always legible), NOT raw `--doc-nav-accent`,\n * which is unreadable when a theme's accent is white/near-white (Blacklight, say)\n * on a light card. The active pill reads as active via a distinct opaque surface\n * plus an accent ring blended with a hairline so it never fully vanishes. */\n.pillActive {\n color: var(--text-primary, #111827);\n border-color: color-mix(\n in srgb,\n var(--doc-nav-accent, var(--accent, #2563eb)) 55%,\n var(--hairline, var(--border-default, #e5e7eb))\n );\n background: var(\n --surface-selected,\n color-mix(\n in srgb,\n var(--surface-card, #111827),\n var(--doc-nav-accent, var(--accent, #2563eb)) 14%\n )\n );\n}\n\n/* Hub — the group's overview entry gets a leading accent dot with a glow (N1).\n * `color` carries the accent so `currentColor` drives fill + glow together. */\n.pillHub::before {\n content: \"\";\n width: calc(var(--spacing-1, 0.25rem) * 1.25);\n height: calc(var(--spacing-1, 0.25rem) * 1.25);\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n background: currentColor;\n box-shadow: 0 0 var(--spacing-2, 0.5rem)\n color-mix(in srgb, currentColor 60%, transparent);\n}\n\n/* Leading order number — mono, a step under the pill (B1). */\n.pillNum {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-xs, 0.75rem) * 0.8);\n font-variant-numeric: tabular-nums;\n color: var(--text-tertiary, #9ca3af);\n}\n\n.pillActive .pillNum {\n color: var(--doc-nav-accent, var(--accent, #2563eb));\n}\n\n/* Label text. */\n.pillLabel {\n min-width: 0;\n}\n\n/* External-link badge. */\n.pillExternal {\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Visually-hidden text for assistive technology. */\n.srOnly {\n position: absolute;\n width: var(--stroke-width-thin, 1px);\n height: var(--stroke-width-thin, 1px);\n padding: 0;\n margin: calc(-1 * var(--stroke-width-thin, 1px));\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n"
2820
+ }
2821
+ ]
2822
+ },
2823
+ {
2824
+ "name": "doc-frame",
2825
+ "type": "registry:ui",
2826
+ "description": "The themed doc-page shell that wraps DocNav — a sticky header with a flexible brand/logo slot, the DocNav slot, and a content wrapper for the doc. Reads a single docs manifest, derives the active product, and passes the slice + active-state down to DocNav. Themed entirely by Visor tokens. Replaces the vanilla-JS doc shell (nav.js + docs.css) on the React/route track.",
2827
+ "category": "navigation",
2828
+ "dependencies": [
2829
+ "@phosphor-icons/react",
2830
+ "@loworbitstudio/visor-core"
2831
+ ],
2832
+ "registryDependencies": [
2833
+ "utils",
2834
+ "doc-nav"
2835
+ ],
2836
+ "files": [
2837
+ {
2838
+ "path": "components/ui/doc-frame/doc-frame.tsx",
2839
+ "type": "registry:ui",
2840
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { CompassIcon } from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport { DocNav, type DocEntry } from \"../doc-nav/doc-nav\"\nimport styles from \"./doc-frame.module.css\"\n\n/**\n * PL-2185 · VI-609 — the themed doc-page shell that wraps <DocNav>.\n *\n * DocFrame owns the page: theme tokens, a sticky header (a flexible brand/logo\n * slot, an OVERVIEW home pill, and a right-aligned meta slot), the <DocNav>\n * slot, and the content wrapper (the doc as children). It reads the single\n * `manifest`, derives the active product, and passes the slice + active-state\n * down to DocNav. Everything is themed by Visor tokens, so the shell adopts the\n * active project theme without modification.\n *\n * Replaces the vanilla-JS doc shell (nav.js + docs.css). `children` accepts any\n * content — a Next route's MDX, or a static doc's HTML body injected server-side\n * by a downstream catch-all route — so one React frame serves both tracks.\n */\n\n// ─── manifest types ───────────────────────────────────────────────────────────\n\n/**\n * A product entry in the PL-2177 roster. `id` matches the values in each\n * `DocEntry.scope[]` and the DocNav group id; `label` is the visible name\n * (defaults to a title-cased id).\n */\nexport interface DocProductEntry {\n /** Product id — matches `DocEntry.scope[]` values and the DocNav group id. */\n id: string\n /** Visible product name. Defaults to a title-cased `id`. */\n label?: string\n}\n\n/**\n * The parsed docs manifest — the single input DocFrame derives everything from.\n * Additive over PL-2177: `docs` is required; `products`, `brand`, and\n * `dispositions` are optional. Absence of `products` (or a single product) =\n * single-product mode: DocNav degrades to one grouped row, no accordion.\n */\nexport interface DocsManifest {\n /** Every documentation entry across the shared set and all products. */\n docs: DocEntry[]\n /**\n * PL-2177 product roster. Absent or length ≤ 1 → single-product mode.\n * When omitted, products are inferred from the distinct `scope` values in\n * `docs`.\n */\n products?: DocProductEntry[]\n /**\n * Text wordmark shown when no `logo` prop is passed and the active theme\n * ships no `brand.logo` SVG (the final fallback in the logo resolution order).\n */\n brand?: string\n /** PL-2170 disposition map — referenced, owned there. Opaque pass-through. */\n dispositions?: Record<string, unknown>\n}\n\n/** The OVERVIEW / home pill in the header — a link back to the docs hub. */\nexport interface DocFrameHome {\n /** Destination for the home pill. */\n href: string\n /** Visible label — rendered mono, UPPERCASE (e.g. `\"Overview\"`). */\n label: string\n}\n\n// ─── props ─────────────────────────────────────────────────────────────────────\n\nexport interface DocFrameProps\n extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /** The parsed manifest — the single input everything derives from. */\n manifest: DocsManifest\n /**\n * The brand slot. Any node — an `<img>` of an SVG, an inline `<svg>`, or a\n * full component (an animated mark, a Visor `<Brand>`). Resolution order:\n * explicit `logo` → the active theme's `brand.logo` SVG (mode-aware via\n * `--brand-logo`, upgraded only once it loads) → the manifest `brand` text.\n */\n logo?: React.ReactNode\n /**\n * The OVERVIEW / home pill rendered after the brand — a bordered mono chip\n * with a leading compass glyph. Omit to hide it.\n */\n home?: DocFrameHome\n /**\n * A right-aligned breadcrumb / status slot in the header (mono, UPPERCASE,\n * `--text-tertiary`) — e.g. `ARTIST · BUILD-READY`. Any node.\n */\n meta?: React.ReactNode\n /**\n * Which product group is expanded (the accordion). Defaults to the route's\n * product, else the first product in the roster. Absent roster →\n * single-product mode.\n */\n activeProduct?: string\n /**\n * The active route, for active-state resolution. Next consumers pass\n * `usePathname()`; a static page passes `location.pathname`. Defaults to\n * `window.location.pathname` in the browser (framework-agnostic — no hard\n * next/navigation dependency), or `\"/\"` during SSR.\n */\n currentPath?: string\n /**\n * A Visor theme class name applied to the shell root, scoping all doc-shell\n * CSS variables. Defaults to the app's ambient theme (inherited from an\n * ancestor). e.g. `theme=\"strata-theme\"`.\n */\n theme?: string\n /**\n * Per-group nav accent overrides, keyed by DocNav group id or role, driving\n * its `--doc-nav-group-accent` hook. Merged over the sensible default palette\n * (`pro → --warning`, the amber Pro dot); DocNav already defaults\n * shared → --info, other products → --accent, appendix → --text-tertiary.\n * Values are CSS colors or `var(--token)` references.\n */\n groupAccents?: Record<string, string>\n /**\n * Force the borderless treatment — null the pinned Shared group's frame\n * (`--doc-nav-pin-border: transparent`) and settle its fill onto the card\n * surface, for themes that carry structure from surface contrast, not borders.\n * A theme that is borderless by design nulls the token in its own CSS; this\n * prop is the per-instance escape hatch.\n */\n borderless?: boolean\n /** The doc content, rendered in the content wrapper below the nav. */\n children: React.ReactNode\n}\n\n// ─── constants ─────────────────────────────────────────────────────────────────\n\n/**\n * Default per-group nav accents. DocNav already resolves shared → --info,\n * products → --accent, and appendix → --text-tertiary via its own fallbacks;\n * the only design-intent override the frame supplies is the amber Pro dot.\n */\nconst DEFAULT_GROUP_ACCENTS: Record<string, string> = {\n pro: \"var(--warning, #d97706)\",\n}\n\n// ─── helpers ───────────────────────────────────────────────────────────────────\n\nfunction titleCase(value: string): string {\n if (value.length === 0) return value\n return value.charAt(0).toUpperCase() + value.slice(1)\n}\n\n/** Strip query + hash so an href compares cleanly against the current path. */\nfunction normalizePath(value: string): string {\n return value.replace(/[?#].*$/, \"\")\n}\n\n/** Extract the URL from a computed `url(\"…\")` custom-property value, if any. */\nfunction cssUrl(value: string): string | null {\n const match = value.match(/url\\(\\s*[\"']?([^\"')]+)[\"']?\\s*\\)/)\n return match ? match[1] : null\n}\n\n/**\n * Resolve the product roster: the explicit `manifest.products`, else inferred\n * from the distinct `scope` values across `docs` (first-appearance order).\n */\nfunction resolveProducts(manifest: DocsManifest): DocProductEntry[] {\n if (manifest.products && manifest.products.length > 0) {\n return manifest.products\n }\n const seen = new Map<string, DocProductEntry>()\n for (const doc of manifest.docs) {\n if (!Array.isArray(doc.scope)) continue\n for (const id of doc.scope) {\n if (!seen.has(id)) seen.set(id, { id, label: titleCase(id) })\n }\n }\n return [...seen.values()]\n}\n\n/** The product owning the doc at `path` (its first `scope`), if any. */\nfunction productForPath(docs: DocEntry[], path: string): string | undefined {\n const target = normalizePath(path)\n for (const doc of docs) {\n if (normalizePath(doc.href) !== target) continue\n if (Array.isArray(doc.scope) && doc.scope.length > 0) return doc.scope[0]\n return undefined\n }\n return undefined\n}\n\n// ─── brand slot ────────────────────────────────────────────────────────────────\n\n/**\n * The default brand slot: the active theme's `brand.logo` SVG when the theme\n * ships one (via the `--brand-logo` custom property, mode-aware light/dark),\n * else the manifest `brand` text wordmark (a leading glyph chip + the name).\n *\n * SSR-safe + hardened by construction: the resting state renders the text\n * wordmark (always visible), and a client effect upgrades to the theme logo\n * only once the image actually loads — a 404 keeps the wordmark, never an empty\n * slot, and there is never a flash of invisibility on reload.\n */\nfunction DocFrameBrand({ brand }: { brand?: string }) {\n const ref = React.useRef<HTMLSpanElement>(null)\n const [hasThemeLogo, setHasThemeLogo] = React.useState(false)\n\n React.useEffect(() => {\n const el = ref.current\n if (!el || typeof window === \"undefined\") return\n const value = window\n .getComputedStyle(el)\n .getPropertyValue(\"--brand-logo\")\n .trim()\n const url = value && value !== \"none\" ? cssUrl(value) : null\n if (!url) {\n setHasThemeLogo(false)\n return\n }\n // Hardening: probe-load the logo and only upgrade once it resolves, so a\n // failed load (a 404 theme-logo URL) leaves the visible wordmark in place.\n let cancelled = false\n const probe = new window.Image()\n probe.onload = () => {\n if (!cancelled) setHasThemeLogo(true)\n }\n probe.onerror = () => {\n if (!cancelled) setHasThemeLogo(false)\n }\n probe.src = url\n return () => {\n cancelled = true\n }\n }, [])\n\n const glyphChar = brand?.trim().charAt(0).toUpperCase()\n\n return (\n <span\n ref={ref}\n className={styles.brand}\n data-slot=\"doc-frame-brand\"\n data-theme-logo={hasThemeLogo || undefined}\n >\n <span\n className={styles.themeLogo}\n role=\"img\"\n aria-label={brand ?? \"Documentation\"}\n />\n {brand ? (\n <span className={styles.wordmark}>\n {glyphChar ? (\n <span className={styles.glyph} aria-hidden=\"true\">\n {glyphChar}\n </span>\n ) : null}\n {brand}\n </span>\n ) : null}\n </span>\n )\n}\n\n// ─── DocFrame ────────────────────────────────────────────────────────────────\n\nconst DocFrame = React.forwardRef<HTMLDivElement, DocFrameProps>(\n (\n {\n manifest,\n logo,\n home,\n meta,\n activeProduct,\n currentPath,\n theme,\n groupAccents,\n borderless,\n children,\n className,\n style,\n ...props\n },\n ref\n ) => {\n const resolvedPath =\n currentPath ??\n (typeof window !== \"undefined\" ? window.location.pathname : \"/\")\n\n const products = React.useMemo(() => resolveProducts(manifest), [manifest])\n const isMultiProduct = products.length > 1\n\n // Seed the open accordion product: explicit prop → the route's product →\n // the first product in the roster.\n const seededProduct =\n activeProduct ??\n productForPath(manifest.docs, resolvedPath) ??\n products[0]?.id\n\n const [openProduct, setOpenProduct] = React.useState<string | undefined>(\n seededProduct\n )\n\n // Keep the open product in sync when a controlling `activeProduct` changes.\n React.useEffect(() => {\n if (activeProduct) setOpenProduct(activeProduct)\n }, [activeProduct])\n\n const resolvedLogo = logo ?? <DocFrameBrand brand={manifest.brand} />\n\n // Borderless drops the pinned-group frame via `--doc-nav-pin-border`. Set it\n // explicitly with the `borderless` prop; a theme that is borderless by design\n // nulls the token in its own CSS rather than the component hardcoding (and\n // leaking) private theme names into the public bundle.\n const isBorderless = borderless ?? false\n\n // Per-group nav accents → DocNav's `--doc-nav-group-accent` hook. Rendered\n // as a frame-scoped <style> keyed to this instance so it never leaks to a\n // sibling DocFrame. DocNav only *reads* the variable (via a fallback), so\n // any value set here wins with no specificity battle.\n const frameId = React.useId()\n const resolvedAccents: Record<string, string> = {\n ...DEFAULT_GROUP_ACCENTS,\n ...groupAccents,\n }\n const accentCss = Object.entries(resolvedAccents)\n .map(\n ([key, value]) =>\n `[data-doc-frame=\"${frameId}\"] [data-group=\"${key}\"],` +\n `[data-doc-frame=\"${frameId}\"] [data-role=\"${key}\"]` +\n `{--doc-nav-group-accent:${value}}`\n )\n .join(\"\")\n\n const frameStyle = {\n ...(isBorderless\n ? {\n \"--doc-nav-pin-border\": \"transparent\",\n \"--doc-nav-pin-bg\": \"var(--surface-card)\",\n }\n : {}),\n ...style,\n } as React.CSSProperties\n\n return (\n <div\n ref={ref}\n data-slot=\"doc-frame\"\n data-doc-frame={frameId}\n data-active-product={isMultiProduct ? openProduct : undefined}\n data-borderless={isBorderless || undefined}\n className={cn(styles.frame, theme, className)}\n style={frameStyle}\n {...props}\n >\n {accentCss ? <style>{accentCss}</style> : null}\n <header data-slot=\"doc-frame-header\" className={styles.chrome}>\n <div className={styles.headerRow}>\n <div className={styles.brandSlot} data-slot=\"doc-frame-brand-slot\">\n {resolvedLogo}\n </div>\n {home ? (\n <a\n href={home.href}\n className={styles.home}\n data-slot=\"doc-frame-home\"\n >\n <CompassIcon\n className={styles.homeIcon}\n weight=\"regular\"\n aria-hidden=\"true\"\n />\n <span>{home.label}</span>\n </a>\n ) : null}\n <div className={styles.spacer} aria-hidden=\"true\" />\n {meta ? (\n <div className={styles.meta} data-slot=\"doc-frame-meta\">\n {meta}\n </div>\n ) : null}\n </div>\n <div className={styles.navRow} data-slot=\"doc-frame-nav\">\n <DocNav\n docs={manifest.docs}\n currentPath={resolvedPath}\n activeProduct={isMultiProduct ? openProduct : undefined}\n onProductToggle={isMultiProduct ? setOpenProduct : undefined}\n />\n </div>\n </header>\n\n <div className={styles.content} data-slot=\"doc-frame-content\">\n {children}\n </div>\n </div>\n )\n }\n)\nDocFrame.displayName = \"DocFrame\"\n\nexport { DocFrame }\n"
2841
+ },
2842
+ {
2843
+ "path": "components/ui/doc-frame/doc-frame.module.css",
2844
+ "type": "registry:ui",
2845
+ "content": "/* DocFrame — the themed doc-page shell that wraps DocNav.\n *\n * A sticky header (a flexible brand/logo slot + an OVERVIEW home pill + a\n * right-aligned meta slot, then the DocNav slot) above a content wrapper (the\n * doc as children). The header sticks to the page scroll so the brand + nav\n * never leave view. Theme-agnostic: every value references a CSS custom property\n * token, so the shell adopts the active project theme.\n *\n * No ancestor sets overflow: hidden/auto — that would create a scroll container\n * and break the viewport-anchored sticky header.\n */\n\n/* Shell root — a centered, max-width column. */\n.frame {\n /* A tight, controlled glyph radius the frame owns — NOT the theme `--radius-*`\n * scale, which decorative themes inflate far past what a 24px brand chip wants\n * (Strata inflates `--radius-md` to 10px, `--radius-xl` to 32px). Overridable\n * by a consumer/theme via this variable. */\n --doc-frame-glyph-radius: 0.375rem;\n display: flex;\n flex-direction: column;\n min-height: 100%;\n max-width: var(--doc-frame-max-width, 75rem);\n margin-inline: auto;\n background: var(--surface-page, #ffffff);\n color: var(--text-primary, #111827);\n}\n\n/* Sticky chrome — the header row + nav row. The page surface (opaque, so\n * scrolled content never bleeds through) sits below the card-surfaced nav\n * groups + chips, which read lighter on top. Sticks to the nearest scroll\n * ancestor (the page/viewport). */\n.chrome {\n position: sticky;\n top: 0;\n z-index: 10;\n display: flex;\n flex-direction: column;\n background: var(--surface-page, #ffffff);\n border-bottom: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n}\n\n/* Header row — brand/logo slot, then the OVERVIEW home pill, a flexible spacer,\n * and the right-aligned meta slot. A hairline rule separates it from the nav. */\n.headerRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n flex-wrap: wrap;\n padding: var(--spacing-3, 0.75rem) var(--spacing-5, 1.25rem);\n border-bottom: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n}\n\n.brandSlot {\n display: inline-flex;\n align-items: center;\n min-width: 0;\n}\n\n.spacer {\n flex: 1 1 auto;\n}\n\n/* Nav row — holds the DocNav slot. */\n.navRow {\n display: flex;\n min-width: 0;\n padding: var(--spacing-3, 0.75rem) var(--spacing-5, 1.25rem);\n}\n\n/* Content wrapper — the doc, as children. */\n.content {\n flex: 1 1 auto;\n padding: var(--spacing-6, 1.5rem) var(--spacing-5, 1.25rem)\n var(--spacing-8, 2rem);\n}\n\n/* ─── brand slot ─────────────────────────────────────────────────────────── */\n\n/* Wraps the theme-logo layer + the text wordmark. */\n.brand {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n min-width: 0;\n}\n\n/* Text wordmark — the resting/fallback brand (a leading glyph chip + the name).\n * Mode-adaptive via --text-primary. */\n.wordmark {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n font-size: var(--font-size-lg, 1.125rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: -0.01em;\n line-height: 1.1;\n color: var(--text-primary, #111827);\n}\n\n/* Brand glyph — a single-letter mark in the theme's primary brand color, its\n * guaranteed-contrasting on-primary text. A controlled small radius (see\n * `--doc-frame-glyph-radius`), not the ballooning theme radius scale. */\n.glyph {\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: var(--spacing-6, 1.5rem);\n height: var(--spacing-6, 1.5rem);\n border-radius: var(--doc-frame-glyph-radius, 0.375rem);\n background: var(--primary, #111827);\n color: var(--primary-text, #ffffff);\n font-size: var(--font-size-xs, 0.75rem);\n font-weight: var(--font-weight-bold, 700);\n line-height: 1;\n}\n\n/* Theme-logo layer — the active theme's brand logo SVG (mode-aware via the\n * --brand-logo custom property). Hidden until the client effect confirms the\n * theme ships one AND it loads, so the resting state is always the visible\n * wordmark and a failed load never leaves an empty slot. */\n.themeLogo {\n display: none;\n height: var(--doc-frame-logo-height, var(--spacing-5, 1.25rem));\n /* An explicit min-width keeps a masked/tinted mark from collapsing to a\n * zero-width box on themes that ship no --brand-logo-aspect-ratio. */\n min-width: var(--spacing-8, 2rem);\n aspect-ratio: var(--brand-logo-aspect-ratio, 3 / 1);\n background-image: var(--brand-logo);\n background-repeat: no-repeat;\n background-position: left center;\n background-size: contain;\n}\n\n/* When the theme provides --brand-logo, show the image and drop the wordmark. */\n.brand[data-theme-logo] .themeLogo {\n display: inline-block;\n}\n\n.brand[data-theme-logo] .wordmark {\n display: none;\n}\n\n/* ─── header chrome — home pill + meta ───────────────────────────────────── */\n\n/* OVERVIEW / home pill — a bordered mono chip with a leading compass glyph. */\n.home {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n flex-shrink: 0;\n /* ~6px 12px — a compact chip that clears the mono caps. */\n padding: calc(var(--spacing-1, 0.25rem) * 1.5) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb));\n border-radius: var(--radius-full, 9999px);\n background: var(--surface-card, #ffffff);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.06em;\n /* Collapse the line box to the glyph height so the all-caps label optically\n * centers against the compass icon. */\n line-height: 1;\n text-transform: uppercase;\n color: var(--text-tertiary, #9ca3af);\n text-decoration: none;\n white-space: nowrap;\n transition: color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease),\n border-color var(--motion-duration-fast, 150ms)\n var(--motion-easing-standard, ease);\n}\n\n.home:hover {\n color: var(--text-primary, #111827);\n border-color: var(--hairline-strong, var(--border-default, #d1d5db));\n}\n\n.home: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/* Compass glyph — a hair larger than the caps (the design's 13px vs 11px);\n * inherits the pill color via currentColor. */\n.homeIcon {\n flex-shrink: 0;\n width: var(--font-size-sm, 0.875rem);\n height: var(--font-size-sm, 0.875rem);\n}\n\n/* Meta slot — a right-aligned breadcrumb/status line, mono UPPERCASE, tertiary. */\n.meta {\n flex-shrink: 0;\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-medium, 500);\n letter-spacing: 0.12em;\n line-height: 1;\n text-transform: uppercase;\n color: var(--text-tertiary, #9ca3af);\n white-space: nowrap;\n}\n\n/* ─── mobile ─────────────────────────────────────────────────────────────── */\n\n@media (max-width: 640px) {\n .headerRow {\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n }\n\n .navRow {\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n }\n\n .content {\n padding: var(--spacing-5, 1.25rem) var(--spacing-4, 1rem)\n var(--spacing-6, 1.5rem);\n }\n}\n"
2846
+ }
2847
+ ]
2848
+ },
2798
2849
  {
2799
2850
  "name": "key-value-list",
2800
2851
  "type": "registry:ui",
@@ -5037,7 +5088,7 @@
5037
5088
  {
5038
5089
  "path": "components/devtools/source-inspector/visor-component-names.generated.ts",
5039
5090
  "type": "registry:devtool",
5040
- "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 \"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 \"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 \"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 \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"DialogFooter\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\n \"DialogTitle\",\n \"DialogTrigger\",\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 \"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 \"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 \"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 \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RecordSection\",\n \"RightRailList\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\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 \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\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 \"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 \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\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"
5091
+ "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 \"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 \"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 \"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 \"DialogClose\",\n \"DialogContent\",\n \"DialogDescription\",\n \"DialogFooter\",\n \"DialogHeader\",\n \"DialogOverlay\",\n \"DialogPortal\",\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 \"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 \"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 \"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 \"RadiusScale\",\n \"RadiusSection\",\n \"RadiusSlide\",\n \"RecordSection\",\n \"RightRailList\",\n \"ScoreIndicator\",\n \"ScrollArea\",\n \"ScrollBar\",\n \"SearchInput\",\n \"SectionHeader\",\n \"SectionIntro\",\n \"SectionNav\",\n \"SectionNavItem\",\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 \"SheetDescription\",\n \"SheetFooter\",\n \"SheetHeader\",\n \"SheetOverlay\",\n \"SheetPortal\",\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 \"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 \"Timeline\",\n \"TimelineContent\",\n \"TimelineDescription\",\n \"TimelineIcon\",\n \"TimelineItem\",\n \"TimelineTimestamp\",\n \"TimelineTitle\",\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"
5041
5092
  }
5042
5093
  ]
5043
5094
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.4.0",
3
- "generated_at": "2026-07-08T04:58:15.594Z",
3
+ "generated_at": "2026-07-09T19:24:03.742Z",
4
4
  "components": {
5
5
  "accessibility-specimen": {
6
6
  "category": "specimen",
@@ -3429,6 +3429,213 @@
3429
3429
  ],
3430
3430
  "example": "<Dialog>\n <DialogTrigger asChild>\n <Button>Edit profile</Button>\n </DialogTrigger>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>Edit profile</DialogTitle>\n <DialogDescription>Make changes to your profile here.</DialogDescription>\n </DialogHeader>\n {/* form content */}\n </DialogContent>\n</Dialog>\n"
3431
3431
  },
3432
+ "doc-frame": {
3433
+ "category": "navigation",
3434
+ "description": "The themed doc-page shell that wraps DocNav. A sticky header with a flexible brand/logo slot, the DocNav slot, and a content wrapper for the doc. Reads a single docs manifest, derives the active product, and passes the slice + active-state down to DocNav. Themed entirely by Visor tokens. Replaces the vanilla-JS doc shell (nav.js + docs.css) on the React/route track.",
3435
+ "when_to_use": [
3436
+ "The surrounding themed page shell for a documentation site — sticky header, brand/logo slot, and content wrapper around a manifest-driven DocNav",
3437
+ "A doc host that must theme its whole chrome from Visor tokens and adopt the active project theme without modification",
3438
+ "A brand/logo slot that resolves an explicit logo, then the active theme's mode-aware brand logo, then a text wordmark",
3439
+ "Multi-product doc sets where the frame derives the active product from the route and drives the DocNav accordion"
3440
+ ],
3441
+ "when_not_to_use": [
3442
+ "The nav internals themselves — the peer groups, accordion, pinned Shared, and Appendix (use DocNav — rendered in the slot)",
3443
+ "Top-level site navigation with brand and actions across an app (use Navbar)",
3444
+ "A generic app page layout or shell unrelated to docs (use Container, Stack, or a page-header composition)",
3445
+ "Manifest parsing, tiering, or routing resolution (owned upstream; DocFrame consumes the parsed manifest)"
3446
+ ],
3447
+ "variants": {},
3448
+ "props": [
3449
+ {
3450
+ "name": "manifest",
3451
+ "type": "DocsManifest",
3452
+ "required": true,
3453
+ "description": "The parsed manifest (docs[], optional products[], optional brand text, optional dispositions) — the single input everything derives from."
3454
+ },
3455
+ {
3456
+ "name": "children",
3457
+ "type": "React.ReactNode",
3458
+ "required": true,
3459
+ "description": "The doc content, rendered in the content wrapper below the nav."
3460
+ },
3461
+ {
3462
+ "name": "logo",
3463
+ "type": "React.ReactNode",
3464
+ "description": "The brand slot. Any node — an <img> of an SVG, an inline <svg>, or a full component. Resolution order — explicit logo → the active theme's brand logo SVG (mode-aware via --brand-logo, upgraded only once it loads so a 404 keeps the wordmark) → the manifest brand text (a leading glyph chip + the name)."
3465
+ },
3466
+ {
3467
+ "name": "home",
3468
+ "type": "DocFrameHome",
3469
+ "description": "The OVERVIEW / home pill rendered after the brand — a bordered mono chip with a leading compass glyph and a { href, label }. Omit to hide it."
3470
+ },
3471
+ {
3472
+ "name": "meta",
3473
+ "type": "React.ReactNode",
3474
+ "description": "A right-aligned breadcrumb / status slot in the header (mono, UPPERCASE, --text-tertiary) — e.g. ARTIST · BUILD-READY."
3475
+ },
3476
+ {
3477
+ "name": "groupAccents",
3478
+ "type": "Record<string, string>",
3479
+ "description": "Per-group nav accent overrides keyed by DocNav group id or role, driving its --doc-nav-group-accent hook. Merged over the default palette (pro → --warning, the amber Pro dot); DocNav already defaults shared → --info, other products → --accent, appendix → --text-tertiary."
3480
+ },
3481
+ {
3482
+ "name": "borderless",
3483
+ "type": "boolean",
3484
+ "description": "Force the borderless treatment — null the pinned Shared group's frame for borderless themes (Animal / ENTR) that carry structure from surface contrast. Auto-detected from theme for the known borderless set; pass explicitly when the theme is applied by an ancestor."
3485
+ },
3486
+ {
3487
+ "name": "activeProduct",
3488
+ "type": "string",
3489
+ "description": "Which product group is expanded (the accordion). Defaults to the route's product, else the first product in the roster. Absent roster → single-product mode."
3490
+ },
3491
+ {
3492
+ "name": "currentPath",
3493
+ "type": "string",
3494
+ "description": "The active route for active-state. Next consumers pass usePathname(); a static page passes location.pathname. Defaults to window.location.pathname in the browser, or \"/\" during SSR."
3495
+ },
3496
+ {
3497
+ "name": "theme",
3498
+ "type": "string",
3499
+ "description": "A Visor theme class name applied to the shell root, scoping all doc-shell CSS variables. Defaults to the app's ambient theme."
3500
+ }
3501
+ ],
3502
+ "dependencies": [
3503
+ "@phosphor-icons/react",
3504
+ "@loworbitstudio/visor-core"
3505
+ ],
3506
+ "tokens_used": [
3507
+ "--border-default",
3508
+ "--border-focus",
3509
+ "--brand-logo",
3510
+ "--brand-logo-aspect-ratio",
3511
+ "--doc-frame-glyph-radius",
3512
+ "--doc-frame-logo-height",
3513
+ "--doc-frame-max-width",
3514
+ "--focus-ring-offset",
3515
+ "--focus-ring-width",
3516
+ "--font-mono",
3517
+ "--font-size-2xs",
3518
+ "--font-size-lg",
3519
+ "--font-size-sm",
3520
+ "--font-size-xs",
3521
+ "--font-weight-bold",
3522
+ "--font-weight-medium",
3523
+ "--hairline",
3524
+ "--hairline-strong",
3525
+ "--motion-duration-fast",
3526
+ "--motion-easing-standard",
3527
+ "--primary",
3528
+ "--primary-text",
3529
+ "--radius-full",
3530
+ "--spacing-1",
3531
+ "--spacing-2",
3532
+ "--spacing-3",
3533
+ "--spacing-4",
3534
+ "--spacing-5",
3535
+ "--spacing-6",
3536
+ "--spacing-8",
3537
+ "--stroke-width-thin",
3538
+ "--surface-card",
3539
+ "--surface-page",
3540
+ "--text-primary",
3541
+ "--text-tertiary"
3542
+ ],
3543
+ "example": "import { DocFrame, type DocsManifest } from '@/components/ui/doc-frame/doc-frame'\n\nconst manifest: DocsManifest = {\n brand: 'Blacklight',\n products: [{ id: 'artist' }, { id: 'pro' }],\n docs: [\n { order: 1, label: 'Charter', href: '/docs/charter.html', group: 'Shared' },\n { order: 3, label: 'Screens', href: '/docs/artist/screens', scope: ['artist'], group: 'Artist' },\n { order: 2, label: 'Journeys', href: '/docs/pro/journeys', scope: ['pro'], group: 'Pro' },\n { order: 11, label: 'Q3 Audit', href: '/docs/q3-audit.html' },\n ],\n}\n\nexport function Shell({ children }: { children: React.ReactNode }) {\n return (\n <DocFrame manifest={manifest} currentPath=\"/docs/artist/screens\">\n {children}\n </DocFrame>\n )\n}\n"
3544
+ },
3545
+ "doc-nav": {
3546
+ "category": "navigation",
3547
+ "description": "A manifest-driven, grouped/collapsible, multi-product-aware documentation navigation. Renders peer collapsible groups from a manifest slice — a pinned Shared set, accordion product groups (one open at a time), and an Appendix bucket for ad-hoc docs — resolving the active pill from the current path. The group row wraps and is never an overflow-x scroll strip. Replaces the vanilla-JS doc nav (nav.js).",
3548
+ "when_to_use": [
3549
+ "A documentation shell that must group docs by product/section and collapse them, driven by a docs manifest",
3550
+ "Multi-product doc sets where a pinned Shared group sits beside accordion product groups (switching product = expanding its group)",
3551
+ "Ad-hoc/one-off docs with no canonical slot that should tuck into an Appendix instead of widening the row",
3552
+ "Any doc nav that must never run off the edge — groups wrap, they do not scroll horizontally"
3553
+ ],
3554
+ "when_not_to_use": [
3555
+ "The surrounding themed page shell, sticky header, and brand/logo slot (use DocFrame — VI-609)",
3556
+ "In-app section sub-navigation with count pills across a detail view (use SectionNav)",
3557
+ "Top-level site navigation with brand + actions (use Navbar)",
3558
+ "A single expandable section (use Collapsible) or a grouped side rail (use Sidebar)"
3559
+ ],
3560
+ "variants": {},
3561
+ "props": [
3562
+ {
3563
+ "name": "docs",
3564
+ "type": "DocEntry[]",
3565
+ "required": true,
3566
+ "description": "The manifest slice for the active view (Shared + product-scoped), pre-filtered by DocFrame. Each DocEntry carries order, label, href, and optional kind/scope/group/tier/external."
3567
+ },
3568
+ {
3569
+ "name": "currentPath",
3570
+ "type": "string",
3571
+ "required": true,
3572
+ "description": "The active route (or static location.pathname). Resolves the active pill (with aria-current=\"page\") and auto-expands its group."
3573
+ },
3574
+ {
3575
+ "name": "activeProduct",
3576
+ "type": "string",
3577
+ "description": "Which product group is open (the accordion). Absent → single-product mode with no accordion."
3578
+ },
3579
+ {
3580
+ "name": "onProductToggle",
3581
+ "type": "(id: string) => void",
3582
+ "description": "Accordion callback — the parent expands a product group and collapses the sibling by swapping activeProduct. Absent → groups render as plain, independently expandable links (static-doc mode)."
3583
+ },
3584
+ {
3585
+ "name": "pinnedGroups",
3586
+ "type": "string[]",
3587
+ "default": "[\"shared\"]",
3588
+ "description": "Group ids that stay open regardless of the accordion."
3589
+ },
3590
+ {
3591
+ "name": "defaultCollapsed",
3592
+ "type": "boolean",
3593
+ "default": "true",
3594
+ "description": "Whether non-active, non-pinned groups start collapsed — the anti-wall rule."
3595
+ }
3596
+ ],
3597
+ "dependencies": [
3598
+ "@phosphor-icons/react",
3599
+ "@loworbitstudio/visor-core"
3600
+ ],
3601
+ "tokens_used": [
3602
+ "--accent",
3603
+ "--border-default",
3604
+ "--border-focus",
3605
+ "--color-neutral-950",
3606
+ "--doc-nav-accent",
3607
+ "--doc-nav-group-accent",
3608
+ "--doc-nav-group-radius",
3609
+ "--doc-nav-pill-bg",
3610
+ "--doc-nav-pin-bg",
3611
+ "--doc-nav-pin-border",
3612
+ "--focus-ring-offset",
3613
+ "--focus-ring-width",
3614
+ "--font-mono",
3615
+ "--font-size-2xs",
3616
+ "--font-size-xs",
3617
+ "--font-weight-bold",
3618
+ "--font-weight-medium",
3619
+ "--hairline",
3620
+ "--info",
3621
+ "--motion-duration-fast",
3622
+ "--motion-easing-standard",
3623
+ "--radius-full",
3624
+ "--radius-md",
3625
+ "--radius-sm",
3626
+ "--spacing-1",
3627
+ "--spacing-2",
3628
+ "--spacing-3",
3629
+ "--spacing-4",
3630
+ "--stroke-width-thin",
3631
+ "--surface-card",
3632
+ "--surface-selected",
3633
+ "--text-primary",
3634
+ "--text-secondary",
3635
+ "--text-tertiary"
3636
+ ],
3637
+ "example": "import { DocNav, type DocEntry } from '@/components/ui/doc-nav/doc-nav'\n\nconst docs: DocEntry[] = [\n { order: 0, label: 'Overview', href: '/docs', group: 'Shared' },\n { order: 1, label: 'Charter', href: '/docs/charter', group: 'Shared' },\n { order: 3, label: 'Screens', href: '/docs/artist-screens', scope: ['artist'], group: 'Artist' },\n { order: 2, label: 'Journeys', href: '/docs/pro-journeys', scope: ['pro'], group: 'Pro' },\n { order: 11, label: 'Q3 Audit', href: '/docs/q3-audit' },\n]\n\nexport function Nav({ product }: { product: string }) {\n const [open, setOpen] = React.useState(product)\n return (\n <DocNav\n docs={docs}\n currentPath=\"/docs/artist-screens\"\n activeProduct={open}\n onProductToggle={setOpen}\n />\n )\n}\n"
3638
+ },
3432
3639
  "dropdown-menu": {
3433
3640
  "category": "overlay",
3434
3641
  "description": "Menu of actions or options that opens from a trigger button.",
@@ -12110,6 +12317,8 @@
12110
12317
  "navigation": [
12111
12318
  "breadcrumb",
12112
12319
  "command",
12320
+ "doc-frame",
12321
+ "doc-nav",
12113
12322
  "navbar",
12114
12323
  "page-header",
12115
12324
  "pagination",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor",
3
- "version": "1.17.0",
3
+ "version": "1.18.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": {