@loworbitstudio/visor 1.17.0 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.json +19 -1
- package/dist/index.js +484 -4
- package/dist/registry.json +77 -1
- package/dist/visor-manifest.json +345 -1
- package/package.json +1 -1
package/dist/CHANGELOG.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.4.0",
|
|
3
|
-
"generated_at": "2026-07-
|
|
3
|
+
"generated_at": "2026-07-15T17:44:38.986Z",
|
|
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": [],
|
|
@@ -254,6 +266,12 @@
|
|
|
254
266
|
"breakingChange": false,
|
|
255
267
|
"migrationNote": null
|
|
256
268
|
},
|
|
269
|
+
"fidelity-mirror": {
|
|
270
|
+
"changeType": "current",
|
|
271
|
+
"files": [],
|
|
272
|
+
"breakingChange": false,
|
|
273
|
+
"migrationNote": null
|
|
274
|
+
},
|
|
257
275
|
"field": {
|
|
258
276
|
"changeType": "current",
|
|
259
277
|
"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
|
|
5
|
-
import { dirname as
|
|
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,485 @@ 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
|
+
"fidelity-mirror": {
|
|
8209
|
+
default: {
|
|
8210
|
+
export: "FidelityMirror",
|
|
8211
|
+
interactiveTarget: '[data-slot="fidelity-mirror-overlay"]',
|
|
8212
|
+
// Mirrors the approved spec's Example A — the web split DRIFT compare of a
|
|
8213
|
+
// doc-nav cluster (design-left / built-right), so the rendered diff is
|
|
8214
|
+
// meaningful for the render-vs-design self-check. Captures are lightweight
|
|
8215
|
+
// inline nodes (React is in scope in the fixture) — no external assets.
|
|
8216
|
+
props: `(function () {
|
|
8217
|
+
var cluster = function (drift) {
|
|
8218
|
+
return React.createElement(
|
|
8219
|
+
"div",
|
|
8220
|
+
{
|
|
8221
|
+
style: {
|
|
8222
|
+
display: "flex", flexDirection: "column", gap: "10px",
|
|
8223
|
+
fontFamily: "var(--font-sans)",
|
|
8224
|
+
},
|
|
8225
|
+
},
|
|
8226
|
+
React.createElement(
|
|
8227
|
+
"div",
|
|
8228
|
+
{ style: { display: "flex", alignItems: "center", gap: "var(--spacing-2)" } },
|
|
8229
|
+
React.createElement("span", {
|
|
8230
|
+
style: {
|
|
8231
|
+
width: "20px", height: "20px", borderRadius: "6px",
|
|
8232
|
+
display: "grid", placeItems: "center",
|
|
8233
|
+
background: "var(--primary)", color: "var(--color-white)",
|
|
8234
|
+
fontFamily: "var(--font-mono)", fontSize: "10px", fontWeight: 700,
|
|
8235
|
+
},
|
|
8236
|
+
}, "V"),
|
|
8237
|
+
React.createElement("span", {
|
|
8238
|
+
style: { fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: "14px", color: "var(--text-primary)" },
|
|
8239
|
+
}, "Visor")
|
|
8240
|
+
),
|
|
8241
|
+
React.createElement(
|
|
8242
|
+
"div",
|
|
8243
|
+
{
|
|
8244
|
+
style: {
|
|
8245
|
+
display: "inline-flex", flexDirection: "column", gap: "var(--spacing-2)",
|
|
8246
|
+
padding: "var(--spacing-2)",
|
|
8247
|
+
border: "1px solid var(--hairline)",
|
|
8248
|
+
borderRadius: drift ? "22px" : "12px",
|
|
8249
|
+
background: "var(--surface-card)",
|
|
8250
|
+
},
|
|
8251
|
+
},
|
|
8252
|
+
React.createElement("span", {
|
|
8253
|
+
style: {
|
|
8254
|
+
fontFamily: "var(--font-mono)", fontSize: "9px", fontWeight: 700,
|
|
8255
|
+
letterSpacing: drift ? "0" : "0.14em",
|
|
8256
|
+
textTransform: drift ? "none" : "uppercase",
|
|
8257
|
+
color: "var(--text-secondary)",
|
|
8258
|
+
},
|
|
8259
|
+
}, "Shared"),
|
|
8260
|
+
React.createElement(
|
|
8261
|
+
"div",
|
|
8262
|
+
{ style: { display: "flex", gap: "var(--spacing-1)" } },
|
|
8263
|
+
["Overview", "Tokens", "Themes"].map(function (label, i) {
|
|
8264
|
+
return React.createElement("span", {
|
|
8265
|
+
key: label,
|
|
8266
|
+
style: {
|
|
8267
|
+
padding: "5px 9px", borderRadius: "9999px",
|
|
8268
|
+
border: "1px solid transparent",
|
|
8269
|
+
background: drift
|
|
8270
|
+
? "var(--surface-subtle)"
|
|
8271
|
+
: "color-mix(in srgb, var(--surface-card), var(--color-neutral-950) 22%)",
|
|
8272
|
+
fontFamily: "var(--font-mono)", fontSize: "9px", fontWeight: 500,
|
|
8273
|
+
letterSpacing: "0.05em", textTransform: "uppercase",
|
|
8274
|
+
color: "var(--text-secondary)",
|
|
8275
|
+
},
|
|
8276
|
+
}, label)
|
|
8277
|
+
})
|
|
8278
|
+
)
|
|
8279
|
+
)
|
|
8280
|
+
)
|
|
8281
|
+
}
|
|
8282
|
+
return {
|
|
8283
|
+
title: "DocNav",
|
|
8284
|
+
subtitle: "Fidelity Mirror \xB7 VI-611",
|
|
8285
|
+
platform: "web",
|
|
8286
|
+
verdict: "drift",
|
|
8287
|
+
score: "3 deltas",
|
|
8288
|
+
design: { content: cluster(false), meta: "index.html" },
|
|
8289
|
+
built: { content: cluster(true), meta: "route \xB7 /doc-nav" },
|
|
8290
|
+
deltas: [
|
|
8291
|
+
{ class: "radius", description: "Group cluster corners 22px -> 12px.", position: { top: "34px", left: "12px" } },
|
|
8292
|
+
{ class: "color", description: "Resting pills read --surface-subtle.", position: { top: "52px", left: "96px" } },
|
|
8293
|
+
{ class: "type", description: "Group labels are title-case.", position: { top: "34px", right: "12px" } },
|
|
8294
|
+
],
|
|
8295
|
+
}
|
|
8296
|
+
})()`
|
|
8297
|
+
}
|
|
8298
|
+
},
|
|
8299
|
+
button: {
|
|
8300
|
+
default: {
|
|
8301
|
+
export: "Button",
|
|
8302
|
+
interactiveTarget: "button",
|
|
8303
|
+
props: `{ children: "Get started" }`
|
|
8304
|
+
}
|
|
8305
|
+
},
|
|
8306
|
+
badge: {
|
|
8307
|
+
default: {
|
|
8308
|
+
export: "Badge",
|
|
8309
|
+
props: `{ children: "New" }`
|
|
8310
|
+
}
|
|
8311
|
+
}
|
|
8312
|
+
};
|
|
8313
|
+
var VALID_MODES = /* @__PURE__ */ new Set(["light", "dark"]);
|
|
8314
|
+
var VALID_STATES = /* @__PURE__ */ new Set(["default", "hover", "focus", "active"]);
|
|
8315
|
+
function missingDepsError(missing) {
|
|
8316
|
+
const installArgs = missing.join(" ");
|
|
8317
|
+
const needsBrowsers = missing.includes("playwright");
|
|
8318
|
+
return {
|
|
8319
|
+
code: "OPTIONAL_DEP_MISSING",
|
|
8320
|
+
message: `\`visor render\` needs ${missing.join(" and ")}, which ${missing.length > 1 ? "are" : "is"} not installed.`,
|
|
8321
|
+
hint: `Install with: npm install -D ${installArgs}${needsBrowsers ? " && npx playwright install chromium" : ""}`
|
|
8322
|
+
};
|
|
8323
|
+
}
|
|
8324
|
+
function pascalCase(kebab) {
|
|
8325
|
+
return kebab.split(/[-_]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
8326
|
+
}
|
|
8327
|
+
function firstExisting(candidates) {
|
|
8328
|
+
for (const c of candidates) {
|
|
8329
|
+
if (existsSync30(c)) return c;
|
|
8330
|
+
}
|
|
8331
|
+
return null;
|
|
8332
|
+
}
|
|
8333
|
+
function resolveComponentFile(cwd, name) {
|
|
8334
|
+
return firstExisting([
|
|
8335
|
+
resolve20(cwd, "components", "ui", name, `${name}.tsx`),
|
|
8336
|
+
resolve20(cwd, "components", "devtools", name, `${name}.tsx`),
|
|
8337
|
+
resolve20(cwd, "components", "ui", name, "index.tsx")
|
|
8338
|
+
]);
|
|
8339
|
+
}
|
|
8340
|
+
function resolveThemeCssFile(cwd, slug2) {
|
|
8341
|
+
return firstExisting([
|
|
8342
|
+
resolve20(cwd, "packages", "docs", "app", `${slug2}-theme.css`),
|
|
8343
|
+
resolve20(cwd, "app", `${slug2}-theme.css`),
|
|
8344
|
+
resolve20(cwd, `${slug2}-theme.css`)
|
|
8345
|
+
]);
|
|
8346
|
+
}
|
|
8347
|
+
function resolveTokensCssFile(cwd) {
|
|
8348
|
+
return firstExisting([
|
|
8349
|
+
resolve20(cwd, "packages", "tokens", "dist", "tokens.css"),
|
|
8350
|
+
resolve20(cwd, "node_modules", "@loworbitstudio", "visor-core", "dist", "tokens.css")
|
|
8351
|
+
]);
|
|
8352
|
+
}
|
|
8353
|
+
async function loadOptional(moduleName) {
|
|
8354
|
+
try {
|
|
8355
|
+
return await import(moduleName);
|
|
8356
|
+
} catch {
|
|
8357
|
+
return null;
|
|
8358
|
+
}
|
|
8359
|
+
}
|
|
8360
|
+
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; }";
|
|
8361
|
+
async function settle(page, fontsTimeout = 5e3) {
|
|
8362
|
+
await page.waitForLoadState("networkidle").catch(() => {
|
|
8363
|
+
});
|
|
8364
|
+
await page.addStyleTag({ content: ANIMATION_DISABLE_CSS });
|
|
8365
|
+
await page.evaluate(
|
|
8366
|
+
`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(); }})`
|
|
8367
|
+
);
|
|
8368
|
+
}
|
|
8369
|
+
function buildHtml(opts) {
|
|
8370
|
+
const htmlClass = opts.mode === "dark" ? "dark" : "";
|
|
8371
|
+
return `<!doctype html>
|
|
8372
|
+
<html lang="en" class="${htmlClass}" style="color-scheme: ${opts.mode}">
|
|
8373
|
+
<head>
|
|
8374
|
+
<meta charset="utf-8" />
|
|
8375
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8376
|
+
<style data-visor="tokens">
|
|
8377
|
+
${opts.tokensCss}
|
|
8378
|
+
</style>
|
|
8379
|
+
<style data-visor="theme">
|
|
8380
|
+
${opts.themeCss}
|
|
8381
|
+
</style>
|
|
8382
|
+
<style data-visor="component">
|
|
8383
|
+
${opts.componentCss}
|
|
8384
|
+
</style>
|
|
8385
|
+
<style data-visor="harness">
|
|
8386
|
+
html, body { margin: 0; padding: 0; }
|
|
8387
|
+
#theme-scope {
|
|
8388
|
+
box-sizing: border-box;
|
|
8389
|
+
min-height: 100vh;
|
|
8390
|
+
padding: 48px;
|
|
8391
|
+
display: flex;
|
|
8392
|
+
align-items: center;
|
|
8393
|
+
justify-content: center;
|
|
8394
|
+
background: var(--surface-page, #ffffff);
|
|
8395
|
+
color: var(--text-primary, #111827);
|
|
8396
|
+
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
|
|
8397
|
+
}
|
|
8398
|
+
#root { width: 100%; max-width: 420px; }
|
|
8399
|
+
</style>
|
|
8400
|
+
</head>
|
|
8401
|
+
<body>
|
|
8402
|
+
<div id="theme-scope" class="${opts.themeClass}">
|
|
8403
|
+
<div id="root"></div>
|
|
8404
|
+
</div>
|
|
8405
|
+
<script>
|
|
8406
|
+
${opts.bundleJs}
|
|
8407
|
+
</script>
|
|
8408
|
+
</body>
|
|
8409
|
+
</html>`;
|
|
8410
|
+
}
|
|
8411
|
+
function buildEntrySource(componentFile, fixture, exportName) {
|
|
8412
|
+
return `import * as React from "react";
|
|
8413
|
+
import { createRoot } from "react-dom/client";
|
|
8414
|
+
import * as __mod from ${JSON.stringify(componentFile)};
|
|
8415
|
+
|
|
8416
|
+
function __resolveComponent(mod, preferred) {
|
|
8417
|
+
if (preferred && mod[preferred]) return mod[preferred];
|
|
8418
|
+
if (mod.default) return mod.default;
|
|
8419
|
+
var isComp = function (v) {
|
|
8420
|
+
return typeof v === "function" || (v && typeof v === "object" && "$$typeof" in v);
|
|
8421
|
+
};
|
|
8422
|
+
for (var k in mod) { if (isComp(mod[k])) return mod[k]; }
|
|
8423
|
+
throw new Error("visor render: no React component export found");
|
|
8424
|
+
}
|
|
8425
|
+
|
|
8426
|
+
var __Component = __resolveComponent(__mod, ${JSON.stringify(exportName)});
|
|
8427
|
+
var __props = ${fixture.props};
|
|
8428
|
+
createRoot(document.getElementById("root")).render(
|
|
8429
|
+
React.createElement(__Component, __props)
|
|
8430
|
+
);`;
|
|
8431
|
+
}
|
|
8432
|
+
async function renderCommand(component, cwd, options) {
|
|
8433
|
+
const json = options.json ?? false;
|
|
8434
|
+
const mode = (options.mode ?? "light").toLowerCase();
|
|
8435
|
+
const state = (options.state ?? "default").toLowerCase();
|
|
8436
|
+
const fail3 = (code, message, extra) => {
|
|
8437
|
+
if (json) {
|
|
8438
|
+
console.log(JSON.stringify({ success: false, error: { code, message }, ...extra }, null, 2));
|
|
8439
|
+
} else {
|
|
8440
|
+
logger.error(message);
|
|
8441
|
+
if (extra?.hint) logger.item(String(extra.hint));
|
|
8442
|
+
}
|
|
8443
|
+
process.exit(1);
|
|
8444
|
+
};
|
|
8445
|
+
if (!VALID_MODES.has(mode)) {
|
|
8446
|
+
fail3("BAD_MODE", `Invalid --mode "${mode}". Use "light" or "dark".`);
|
|
8447
|
+
}
|
|
8448
|
+
if (!VALID_STATES.has(state)) {
|
|
8449
|
+
fail3("BAD_STATE", `Invalid --state "${state}". Use one of: ${[...VALID_STATES].join(", ")}.`);
|
|
8450
|
+
}
|
|
8451
|
+
const componentFile = resolveComponentFile(cwd, component);
|
|
8452
|
+
if (!componentFile) {
|
|
8453
|
+
fail3(
|
|
8454
|
+
"COMPONENT_NOT_FOUND",
|
|
8455
|
+
`Component "${component}" not found under components/ui/. Expected components/ui/${component}/${component}.tsx.`
|
|
8456
|
+
);
|
|
8457
|
+
}
|
|
8458
|
+
const themeCssFile = resolveThemeCssFile(cwd, options.theme);
|
|
8459
|
+
if (!themeCssFile) {
|
|
8460
|
+
fail3(
|
|
8461
|
+
"THEME_NOT_FOUND",
|
|
8462
|
+
`Theme "${options.theme}" not found. Expected packages/docs/app/${options.theme}-theme.css.`,
|
|
8463
|
+
{ hint: "Run `visor theme sync` to (re)generate theme CSS, or check the slug." }
|
|
8464
|
+
);
|
|
8465
|
+
}
|
|
8466
|
+
const tokensCssFile = resolveTokensCssFile(cwd);
|
|
8467
|
+
if (!tokensCssFile) {
|
|
8468
|
+
fail3(
|
|
8469
|
+
"TOKENS_NOT_FOUND",
|
|
8470
|
+
"Emitted tokens.css not found. Expected packages/tokens/dist/tokens.css.",
|
|
8471
|
+
{ hint: "Run `npm run build -w packages/tokens` first." }
|
|
8472
|
+
);
|
|
8473
|
+
}
|
|
8474
|
+
const fixtureName = options.fixture ?? "default";
|
|
8475
|
+
const componentFixtures = FIXTURES[component];
|
|
8476
|
+
const fixture = componentFixtures?.[fixtureName] ?? { props: "{}" };
|
|
8477
|
+
if (options.fixture && !componentFixtures?.[fixtureName]) {
|
|
8478
|
+
fail3(
|
|
8479
|
+
"FIXTURE_NOT_FOUND",
|
|
8480
|
+
`Fixture "${fixtureName}" not registered for "${component}". Available: ${componentFixtures ? Object.keys(componentFixtures).join(", ") : "(none)"}.`
|
|
8481
|
+
);
|
|
8482
|
+
}
|
|
8483
|
+
if (!componentFixtures && !json) {
|
|
8484
|
+
logger.warn(
|
|
8485
|
+
`No fixture registered for "${component}" \u2014 rendering with empty props. Add one to FIXTURES in render.ts for representative content.`
|
|
8486
|
+
);
|
|
8487
|
+
}
|
|
8488
|
+
const exportName = fixture.export ?? pascalCase(component);
|
|
8489
|
+
const esbuild = await loadOptional("esbuild");
|
|
8490
|
+
const playwright = await loadOptional("playwright");
|
|
8491
|
+
const missing = [];
|
|
8492
|
+
if (!esbuild) missing.push("esbuild");
|
|
8493
|
+
if (!playwright) missing.push("playwright");
|
|
8494
|
+
if (missing.length > 0) {
|
|
8495
|
+
const dep = missingDepsError(missing);
|
|
8496
|
+
fail3(dep.code, dep.message, { missing, hint: dep.hint });
|
|
8497
|
+
}
|
|
8498
|
+
let componentCss = "";
|
|
8499
|
+
let bundleJs = "";
|
|
8500
|
+
try {
|
|
8501
|
+
const entrySource = buildEntrySource(componentFile, fixture, exportName);
|
|
8502
|
+
const result = await esbuild.build({
|
|
8503
|
+
stdin: {
|
|
8504
|
+
contents: entrySource,
|
|
8505
|
+
resolveDir: cwd,
|
|
8506
|
+
loader: "tsx",
|
|
8507
|
+
sourcefile: "visor-render-entry.tsx"
|
|
8508
|
+
},
|
|
8509
|
+
bundle: true,
|
|
8510
|
+
format: "iife",
|
|
8511
|
+
platform: "browser",
|
|
8512
|
+
jsx: "automatic",
|
|
8513
|
+
write: false,
|
|
8514
|
+
outdir: "visor-render-out",
|
|
8515
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
8516
|
+
banner: { js: "globalThis.process = globalThis.process || { env: {} };" },
|
|
8517
|
+
logLevel: "silent"
|
|
8518
|
+
});
|
|
8519
|
+
for (const file of result.outputFiles ?? []) {
|
|
8520
|
+
if (file.path.endsWith(".css")) componentCss += file.text;
|
|
8521
|
+
else if (file.path.endsWith(".js")) bundleJs += file.text;
|
|
8522
|
+
}
|
|
8523
|
+
} catch (err) {
|
|
8524
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8525
|
+
fail3("BUNDLE_FAILED", `Failed to bundle ${component}: ${message}`);
|
|
8526
|
+
}
|
|
8527
|
+
const themeClass = `${options.theme}-theme`;
|
|
8528
|
+
const html = buildHtml({
|
|
8529
|
+
tokensCss: readFileSync32(tokensCssFile, "utf-8"),
|
|
8530
|
+
themeCss: readFileSync32(themeCssFile, "utf-8"),
|
|
8531
|
+
componentCss,
|
|
8532
|
+
bundleJs,
|
|
8533
|
+
themeClass,
|
|
8534
|
+
mode
|
|
8535
|
+
});
|
|
8536
|
+
const width = Number(options.width ?? 720);
|
|
8537
|
+
const height = Number(options.height ?? 640);
|
|
8538
|
+
const outPath = resolveOutPath(cwd, options.out, component, options.theme, mode, state);
|
|
8539
|
+
mkdirSync14(dirname12(outPath), { recursive: true });
|
|
8540
|
+
const browser = await playwright.chromium.launch();
|
|
8541
|
+
let probe;
|
|
8542
|
+
try {
|
|
8543
|
+
const context = await browser.newContext({
|
|
8544
|
+
viewport: { width, height },
|
|
8545
|
+
colorScheme: mode,
|
|
8546
|
+
deviceScaleFactor: 2
|
|
8547
|
+
});
|
|
8548
|
+
const page = await context.newPage();
|
|
8549
|
+
await page.setContent(html, { waitUntil: "load" });
|
|
8550
|
+
await page.waitForFunction(
|
|
8551
|
+
"document.getElementById('root') && document.getElementById('root').childElementCount > 0",
|
|
8552
|
+
void 0,
|
|
8553
|
+
{ timeout: 1e4 }
|
|
8554
|
+
).catch(() => {
|
|
8555
|
+
});
|
|
8556
|
+
await settle(page);
|
|
8557
|
+
probe = await page.evaluate(
|
|
8558
|
+
`(function () {
|
|
8559
|
+
var scope = document.getElementById("theme-scope");
|
|
8560
|
+
var html = document.documentElement;
|
|
8561
|
+
var read = function (el, prop) {
|
|
8562
|
+
return getComputedStyle(el).getPropertyValue(prop).trim();
|
|
8563
|
+
};
|
|
8564
|
+
var themed = read(scope, "--surface-card");
|
|
8565
|
+
var base = read(html, "--surface-card");
|
|
8566
|
+
return {
|
|
8567
|
+
themedSurfaceCard: themed,
|
|
8568
|
+
baseSurfaceCard: base,
|
|
8569
|
+
themedBg: getComputedStyle(scope).backgroundColor,
|
|
8570
|
+
mapped: themed !== "" && themed !== base,
|
|
8571
|
+
};
|
|
8572
|
+
})()`
|
|
8573
|
+
);
|
|
8574
|
+
if (state !== "default") {
|
|
8575
|
+
const target = fixture.interactiveTarget ?? "#root button, #root a, #root input, #root [tabindex]";
|
|
8576
|
+
const el = await page.$(target);
|
|
8577
|
+
if (el) {
|
|
8578
|
+
if (state === "hover") await el.hover();
|
|
8579
|
+
else if (state === "focus") await el.focus();
|
|
8580
|
+
else if (state === "active") {
|
|
8581
|
+
await el.hover();
|
|
8582
|
+
await page.mouse.down();
|
|
8583
|
+
}
|
|
8584
|
+
await page.waitForTimeout(80);
|
|
8585
|
+
} else if (!json) {
|
|
8586
|
+
logger.warn(`No interactive target ("${target}") found for state "${state}".`);
|
|
8587
|
+
}
|
|
8588
|
+
}
|
|
8589
|
+
await page.screenshot({ path: outPath, fullPage: true });
|
|
8590
|
+
if (state === "active") await page.mouse.up().catch(() => {
|
|
8591
|
+
});
|
|
8592
|
+
} finally {
|
|
8593
|
+
await browser.close();
|
|
8594
|
+
}
|
|
8595
|
+
const fileSize = existsSync30(outPath) ? statBytes(outPath) : 0;
|
|
8596
|
+
if (json) {
|
|
8597
|
+
console.log(
|
|
8598
|
+
JSON.stringify(
|
|
8599
|
+
{
|
|
8600
|
+
success: true,
|
|
8601
|
+
component,
|
|
8602
|
+
theme: options.theme,
|
|
8603
|
+
mode,
|
|
8604
|
+
state,
|
|
8605
|
+
fixture: fixtureName,
|
|
8606
|
+
out: outPath,
|
|
8607
|
+
bytes: fileSize,
|
|
8608
|
+
probe
|
|
8609
|
+
},
|
|
8610
|
+
null,
|
|
8611
|
+
2
|
|
8612
|
+
)
|
|
8613
|
+
);
|
|
8614
|
+
return;
|
|
8615
|
+
}
|
|
8616
|
+
logger.success(`Rendered ${component} \xB7 ${options.theme} \xB7 ${mode}${state !== "default" ? ` \xB7 ${state}` : ""}`);
|
|
8617
|
+
logger.item(`\u2192 ${outPath} (${formatBytes2(fileSize)})`);
|
|
8618
|
+
logger.blank();
|
|
8619
|
+
if (probe.mapped) {
|
|
8620
|
+
logger.success(
|
|
8621
|
+
`Themed --surface-card resolved to ${probe.themedSurfaceCard} (base primitive: ${probe.baseSurfaceCard}) \u2014 theme mapping applied.`
|
|
8622
|
+
);
|
|
8623
|
+
} else {
|
|
8624
|
+
logger.warn(
|
|
8625
|
+
`Themed --surface-card (${probe.themedSurfaceCard}) equals the base primitive (${probe.baseSurfaceCard}). The theme override did not apply \u2014 check @layer/mode scoping (VI-511).`
|
|
8626
|
+
);
|
|
8627
|
+
}
|
|
8628
|
+
}
|
|
8629
|
+
function resolveOutPath(cwd, out, component, theme2, mode, state) {
|
|
8630
|
+
if (out) {
|
|
8631
|
+
return isAbsolute6(out) ? out : resolve20(cwd, out);
|
|
8632
|
+
}
|
|
8633
|
+
const suffix = state === "default" ? "" : `__${state}`;
|
|
8634
|
+
return resolve20(cwd, ".visor", "renders", `${component}__${theme2}__${mode}${suffix}.png`);
|
|
8635
|
+
}
|
|
8636
|
+
function statBytes(path2) {
|
|
8637
|
+
try {
|
|
8638
|
+
return readFileSync32(path2).length;
|
|
8639
|
+
} catch {
|
|
8640
|
+
return 0;
|
|
8641
|
+
}
|
|
8642
|
+
}
|
|
8643
|
+
function formatBytes2(bytes) {
|
|
8644
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
8645
|
+
const kb = bytes / 1024;
|
|
8646
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
8647
|
+
return `${(kb / 1024).toFixed(1)} MB`;
|
|
8648
|
+
}
|
|
8649
|
+
|
|
8175
8650
|
// src/index.ts
|
|
8176
|
-
var __dirname2 =
|
|
8651
|
+
var __dirname2 = dirname13(fileURLToPath4(import.meta.url));
|
|
8177
8652
|
var pkg = JSON.parse(
|
|
8178
|
-
|
|
8653
|
+
readFileSync33(join33(__dirname2, "..", "package.json"), "utf-8")
|
|
8179
8654
|
);
|
|
8180
8655
|
var program = new Command2();
|
|
8181
8656
|
program.name("visor").description("CLI for the Visor design system").version(pkg.version);
|
|
@@ -8373,4 +8848,9 @@ program.command("spawn").description(
|
|
|
8373
8848
|
).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
8849
|
spawnCommand(process.cwd(), options);
|
|
8375
8850
|
});
|
|
8851
|
+
program.command("render").description(
|
|
8852
|
+
"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."
|
|
8853
|
+
).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) => {
|
|
8854
|
+
await renderCommand(component, process.cwd(), options);
|
|
8855
|
+
});
|
|
8376
8856
|
program.parse();
|
package/dist/registry.json
CHANGED
|
@@ -2795,6 +2795,82 @@
|
|
|
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. Set in the theme's display face so a text\n * wordmark carries the theme's brand character (say, Animal's condensed\n * \"Sequel 100 Black 95\" rather than the body \"Ratio\"); degrades to the heading\n * face, then the ambient sans, on themes that ship no --font-display — matching\n * the display-font convention in stat-card / page-header / section-intro. */\n.wordmark {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n font-family: var(--font-display, var(--font-family-heading, inherit));\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
|
+
},
|
|
2849
|
+
{
|
|
2850
|
+
"name": "fidelity-mirror",
|
|
2851
|
+
"type": "registry:ui",
|
|
2852
|
+
"description": "The design-left / built-right comparison surface — renders a pure-HTML design source beside its Visor-TSX render (or a native/Flutter device-framed screenshot, or an external artifact) in recessed capture wells, with a verdict diff indicator, numbered delta callouts, a classed delta legend, and a drag-to-reveal overlay mode. Composes inside DocFrame's content and goes full-bleed on widescreen so each pane is ~50vw. The fidelity DISPLAY (the screenshot-diff GATE is a separate verification concern).",
|
|
2853
|
+
"category": "data-display",
|
|
2854
|
+
"dependencies": [
|
|
2855
|
+
"@phosphor-icons/react",
|
|
2856
|
+
"@loworbitstudio/visor-core"
|
|
2857
|
+
],
|
|
2858
|
+
"registryDependencies": [
|
|
2859
|
+
"utils"
|
|
2860
|
+
],
|
|
2861
|
+
"files": [
|
|
2862
|
+
{
|
|
2863
|
+
"path": "components/ui/fidelity-mirror/fidelity-mirror.tsx",
|
|
2864
|
+
"type": "registry:ui",
|
|
2865
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n ColumnsIcon,\n SquareHalfIcon,\n DeviceMobileIcon,\n BrowserIcon,\n ArrowsHorizontalIcon,\n} from \"@phosphor-icons/react\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./fidelity-mirror.module.css\"\n\n/**\n * FidelityMirror (VI-610) — the design-left / built-right comparison surface.\n *\n * PL-2139 shipped the fidelity GATE (screenshot-diff verification). This is the\n * fidelity DISPLAY: the side-by-side viewer that renders the Fidelity Mirror doc\n * type inside DocFrame's content. Gate VERIFIES; component DISPLAYS.\n *\n * REAL compare semantics (operator refinement): the LEFT pane is the *pure HTML\n * design* source, the RIGHT pane is the *Visor-ified TSX* render — never a\n * synthetically recolored side. The scope-dot hues carry SOURCE identity\n * (`--info` for the design, `--accent` for the built render), not decoration.\n *\n * The compare goes full-bleed on widescreen (`bleed`, default on) so each pane\n * is ~50vw with the center gutter equal to the outer padding. Under 768px the\n * panes stack design-over-built — never a sideways scroll strip.\n */\n\n// ─── types ─────────────────────────────────────────────────────────────────────\n\n/** The platform of the built surface — drives the built-side renderer + chip. */\nexport type FidelityPlatform = \"web\" | \"native\" | \"flutter\" | \"external\"\n\n/** Compare presentation: `split` (design-left / built-right) or `overlay` (slider). */\nexport type FidelityCompareMode = \"split\" | \"overlay\"\n\n/** The diff indicator's verdict — tints the status pill. */\nexport type FidelityVerdict = \"match\" | \"drift\" | \"fail\"\n\n/** The drift class a delta belongs to — classes the legend tag. */\nexport type FidelityDeltaClass = \"radius\" | \"color\" | \"spacing\" | \"type\" | \"align\"\n\n/** A single enumerated drift — a numbered callout on the built side + a legend row. */\nexport interface FidelityDelta {\n /** The drift class (mirrors the render-vs-design self-check taxonomy). */\n class: FidelityDeltaClass\n /** Short legend tag (defaults to the title-cased `class`). */\n label?: string\n /** The legend row copy (any node — supports inline `<b>`/`<code>`). */\n description: React.ReactNode\n /** Callout position over the built pane, as CSS inset values. Omit → no marker. */\n position?: { top?: string; left?: string; right?: string; bottom?: string }\n}\n\n/**\n * A captured surface for one side of the compare. `content` (an arbitrary node)\n * wins when present; otherwise the side renders from `src` per `kind` — an\n * `<img>` (image), or a live-route / external `<iframe>`.\n */\nexport interface FidelitySource {\n /** Renderer. Defaults by platform: web → route, external → external, else image. */\n kind?: \"image\" | \"route\" | \"external\" | \"node\"\n /** Image URL or iframe src. */\n src?: string\n /** Alt text (image) / iframe title (route/external). */\n title?: string\n /** Arbitrary rendered content — wins over `src` when present. */\n content?: React.ReactNode\n /** Pane label override (defaults per side + platform). */\n label?: string\n /** Right-aligned pane meta (mono) — e.g. `index.html`, `route · /doc-nav`. */\n meta?: string\n}\n\nexport interface FidelityMirrorProps\n extends Omit<React.ComponentProps<\"section\">, \"title\"> {\n /** The compared surface's name — the header title (e.g. a component/screen). */\n title: string\n /** Header subtitle under the title (e.g. `Fidelity Mirror · VI-611`). */\n subtitle?: string\n /** Platform of the built surface. Default `web`. */\n platform?: FidelityPlatform\n /** The DESIGN source — the pure-HTML design (left pane). */\n design: FidelitySource\n /** The BUILT source — the Visor-TSX render (right pane). */\n built: FidelitySource\n /** Initial compare mode. Default `split`. The header toggle switches in place. */\n mode?: FidelityCompareMode\n /** The diff verdict — the status pill. */\n verdict: FidelityVerdict\n /** Optional score/summary string on the pill (e.g. `99.4%`, `3 deltas`). */\n score?: string\n /** Enumerated drifts — numbered callouts + the classed legend. Omit → none. */\n deltas?: FidelityDelta[]\n /** Widescreen full-bleed layout (panes ~50vw). Default `true`. */\n bleed?: boolean\n /** Initial overlay reveal position, 0–100. Default `55`. */\n overlayPosition?: number\n /** Notified when the compare mode toggles. */\n onModeChange?: (mode: FidelityCompareMode) => void\n}\n\n// ─── constants ─────────────────────────────────────────────────────────────────\n\nconst DELTA_LABELS: Record<FidelityDeltaClass, string> = {\n radius: \"Radius\",\n color: \"Color\",\n spacing: \"Spacing\",\n type: \"Type\",\n align: \"Align\",\n}\n\n// ─── helpers ───────────────────────────────────────────────────────────────────\n\n/** The default renderer kind for a side, given the platform. */\nfunction defaultKind(platform: FidelityPlatform): FidelitySource[\"kind\"] {\n if (platform === \"web\") return \"route\"\n if (platform === \"external\") return \"external\"\n return \"image\"\n}\n\n/** Whether a platform frames its captures in a device bezel. */\nfunction isDeviceFramed(platform: FidelityPlatform): boolean {\n return platform === \"native\" || platform === \"flutter\"\n}\n\n/** Default pane label for a side, honoring the web HTML/TSX source identity. */\nfunction defaultLabel(\n side: \"design\" | \"built\",\n platform: FidelityPlatform\n): string {\n if (platform === \"web\") {\n return side === \"design\" ? \"Design · HTML\" : \"Built · Visor TSX\"\n }\n return side === \"design\" ? \"Design\" : \"Built\"\n}\n\n/** Overlay ribbon label — the web ribbons read \"HTML Design\" / \"Visor TSX\". */\nfunction ribbonLabel(\n side: \"design\" | \"built\",\n platform: FidelityPlatform\n): string {\n if (platform === \"web\") {\n return side === \"design\" ? \"HTML Design\" : \"Visor TSX\"\n }\n return side === \"design\" ? \"Design\" : \"Built\"\n}\n\n// ─── capture renderer ──────────────────────────────────────────────────────────\n\nfunction Capture({\n source,\n platform,\n}: {\n source: FidelitySource\n platform: FidelityPlatform\n}) {\n const framed = isDeviceFramed(platform)\n const kind = source.content ? \"node\" : source.kind ?? defaultKind(platform)\n\n let media: React.ReactNode\n if (kind === \"node\" || source.content) {\n media = source.content\n } else if (kind === \"image\") {\n media = (\n <img className={styles.media} src={source.src} alt={source.title ?? \"\"} />\n )\n } else {\n // route | external → a live iframe of the built/design surface.\n media = (\n <iframe\n className={styles.media}\n src={source.src}\n title={source.title ?? \"Captured surface\"}\n loading=\"lazy\"\n />\n )\n }\n\n return framed ? <div className={styles.bezel}>{media}</div> : <>{media}</>\n}\n\n// ─── FidelityMirror ────────────────────────────────────────────────────────────\n\nconst FidelityMirror = React.forwardRef<HTMLElement, FidelityMirrorProps>(\n (\n {\n title,\n subtitle,\n platform = \"web\",\n design,\n built,\n mode = \"split\",\n verdict,\n score,\n deltas,\n bleed = true,\n overlayPosition = 55,\n onModeChange,\n className,\n \"aria-label\": ariaLabel,\n ...props\n },\n ref\n ) => {\n const [activeMode, setActiveMode] = React.useState<FidelityCompareMode>(mode)\n const [split, setSplit] = React.useState<number>(overlayPosition)\n\n const setMode = (next: FidelityCompareMode) => {\n setActiveMode(next)\n onModeChange?.(next)\n }\n\n const hasDeltas = Array.isArray(deltas) && deltas.length > 0\n const designLabel = design.label ?? defaultLabel(\"design\", platform)\n const builtLabel = built.label ?? defaultLabel(\"built\", platform)\n\n return (\n <section\n ref={ref}\n data-slot=\"fidelity-mirror\"\n data-platform={platform}\n data-mode={activeMode}\n data-verdict={verdict}\n aria-label={ariaLabel ?? `Fidelity comparison: ${title}`}\n className={cn(styles.root, bleed && styles.bleed, className)}\n {...props}\n >\n {/* ── Header bar ── */}\n <div className={styles.bar}>\n <span className={styles.titleWrap}>\n <span className={styles.title}>{title}</span>\n {subtitle ? <span className={styles.subtitle}>{subtitle}</span> : null}\n </span>\n\n <PlatformChip platform={platform} />\n\n <span className={styles.spacer} aria-hidden=\"true\" />\n\n <span className={styles.status} data-verdict={verdict}>\n <span className={styles.statusGlyph} aria-hidden=\"true\" />\n {verdict}\n {score ? <span className={styles.score}>· {score}</span> : null}\n </span>\n\n <div className={styles.seg} role=\"group\" aria-label=\"Compare mode\">\n <button\n type=\"button\"\n className={styles.segBtn}\n data-slot=\"fidelity-mirror-split\"\n aria-pressed={activeMode === \"split\"}\n onClick={() => setMode(\"split\")}\n >\n <ColumnsIcon\n className={styles.segIcon}\n weight=\"bold\"\n aria-hidden=\"true\"\n />\n Split\n </button>\n <button\n type=\"button\"\n className={styles.segBtn}\n data-slot=\"fidelity-mirror-overlay\"\n aria-pressed={activeMode === \"overlay\"}\n onClick={() => setMode(\"overlay\")}\n >\n <SquareHalfIcon\n className={styles.segIcon}\n weight=\"bold\"\n aria-hidden=\"true\"\n />\n Overlay\n </button>\n </div>\n </div>\n\n {/* ── Body: split panes or overlay slider ── */}\n {activeMode === \"split\" ? (\n <div className={styles.panes} data-slot=\"fidelity-mirror-panes\">\n <div className={styles.pane} data-side=\"design\">\n <span className={styles.paneLabel}>\n <span className={styles.scopeDot} aria-hidden=\"true\" />\n {designLabel}\n {design.meta ? (\n <span className={styles.paneMeta}>{design.meta}</span>\n ) : null}\n </span>\n <div className={styles.well}>\n <Capture source={design} platform={platform} />\n </div>\n </div>\n\n <div className={styles.pane} data-side=\"built\">\n <span className={styles.paneLabel}>\n <span className={styles.scopeDot} aria-hidden=\"true\" />\n {builtLabel}\n {built.meta ? (\n <span className={styles.paneMeta}>{built.meta}</span>\n ) : null}\n </span>\n <div className={styles.well}>\n {hasDeltas\n ? deltas\n .filter((d) => d.position)\n .map((d, i) => (\n <span\n key={`${d.class}-${i}`}\n className={styles.delta}\n style={d.position}\n aria-hidden=\"true\"\n >\n {i + 1}\n </span>\n ))\n : null}\n <Capture source={built} platform={platform} />\n </div>\n </div>\n </div>\n ) : (\n <div className={styles.overlay} data-slot=\"fidelity-mirror-overlay-view\">\n <div\n className={styles.overlayFrame}\n style={{ \"--fm-split\": `${split}%` } as React.CSSProperties}\n >\n <span className={styles.ribbon} data-side=\"built\">\n {ribbonLabel(\"built\", platform)}\n </span>\n {/* Base layer: built render. */}\n <div className={styles.layer}>\n <Capture source={built} platform={platform} />\n </div>\n {/* Top layer: design, clipped to the reveal position. */}\n <div className={cn(styles.layer, styles.layerTop)}>\n <span className={styles.ribbon} data-side=\"design\">\n {ribbonLabel(\"design\", platform)}\n </span>\n <Capture source={design} platform={platform} />\n </div>\n <div className={styles.handle} aria-hidden=\"true\">\n <span className={styles.grip}>\n <ArrowsHorizontalIcon\n className={styles.gripIcon}\n weight=\"bold\"\n aria-hidden=\"true\"\n />\n </span>\n </div>\n </div>\n <input\n className={styles.range}\n type=\"range\"\n min={0}\n max={100}\n value={split}\n aria-label=\"Reveal position\"\n onChange={(e) => setSplit(Number(e.target.value))}\n />\n </div>\n )}\n\n {/* ── Delta legend ── */}\n {hasDeltas ? (\n <div className={styles.legend} data-slot=\"fidelity-mirror-legend\">\n <span className={styles.legendHead}>Deltas — built vs design</span>\n {deltas.map((d, i) => (\n <div className={styles.legendRow} key={`${d.class}-legend-${i}`}>\n <span className={styles.legendNum}>{i + 1}</span>\n <span className={styles.tag} data-class={d.class}>\n {d.label ?? DELTA_LABELS[d.class]}\n </span>\n <span className={styles.legendText}>{d.description}</span>\n </div>\n ))}\n </div>\n ) : null}\n </section>\n )\n }\n)\nFidelityMirror.displayName = \"FidelityMirror\"\n\n// ─── PlatformChip (internal) ────────────────────────────────────────────────────\n\nfunction PlatformChip({ platform }: { platform: FidelityPlatform }) {\n if (platform === \"web\") {\n return (\n <span className={styles.chip} data-slot=\"fidelity-mirror-chip\">\n <span className={styles.chipDot} aria-hidden=\"true\" />\n Web · Live Route\n </span>\n )\n }\n if (platform === \"native\") {\n return (\n <span className={styles.chip} data-slot=\"fidelity-mirror-chip\">\n <DeviceMobileIcon className={styles.chipIcon} aria-hidden=\"true\" />\n iOS · SwiftUI\n </span>\n )\n }\n if (platform === \"flutter\") {\n return (\n <span className={styles.chip} data-slot=\"fidelity-mirror-chip\">\n <DeviceMobileIcon className={styles.chipIcon} aria-hidden=\"true\" />\n Flutter\n </span>\n )\n }\n return (\n <span className={styles.chip} data-slot=\"fidelity-mirror-chip\">\n <BrowserIcon className={styles.chipIcon} aria-hidden=\"true\" />\n External\n </span>\n )\n}\n\nexport { FidelityMirror }\n"
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
"path": "components/ui/fidelity-mirror/fidelity-mirror.module.css",
|
|
2869
|
+
"type": "registry:ui",
|
|
2870
|
+
"content": "/* FidelityMirror — the design-left / built-right comparison surface.\n *\n * Renders the pure DESIGN source (left) beside the BUILT render (right) in\n * recessed capture wells, with a verdict/diff indicator, numbered delta\n * callouts, a classed delta legend, and a drag-to-reveal overlay mode. Composes\n * inside DocFrame's content, and goes full-bleed on widescreen so each pane is\n * ~50vw (the compare is meant for fullscreen monitors).\n *\n * Theme-agnostic: every value references a Visor token. The card/cluster radius\n * and the recessed well fill are component-OWNED (fixed) tokens, NOT the theme\n * `--radius-*` scale or a bare `--surface-*` token — decorative themes inflate\n * `--radius-xl` to 32px and lift `--surface-subtle` ABOVE the card in dark mode,\n * which is precisely the drift this component exists to surface.\n */\n\n/* Root — the framed compare. */\n.root {\n /* Fixed cluster radius the component OWNS — NOT the theme `--radius-*` scale,\n * which decorative themes inflate far past what a dense compare cluster wants\n * (Space inflates `--radius-xl` to 32px). Overridable by a consumer/theme. */\n --fm-radius: 0.75rem;\n --fm-well-radius: 0.625rem;\n /* Widescreen gutter/padding — the outer left/right padding AND the center\n * gutter resolve to the SAME value, so each pane is ~50vw: (100vw - 3*pad)/2. */\n --fm-pad: clamp(1.25rem, 3.2vw, 3.5rem);\n /* Recessed well fill — darker than `--surface-card` in BOTH modes (the DocNav\n * pill-bg convention). The surface ramp inverts light<->dark, so a single\n * surface token can't stay \"below\" the card; mixing the card toward\n * neutral-950 darkens it directionally in either mode. Overridable. */\n --fm-well: color-mix(\n in srgb,\n var(--surface-card, #111827),\n var(--color-neutral-950, #030712) 22%\n );\n /* Source identity hues (kept across split + overlay): design = info, built =\n * accent. These carry SOURCE identity, not decoration — do not retint. */\n --fm-design-hue: var(--info, #0ea5e9);\n --fm-built-hue: var(--accent, #2563eb);\n\n display: flex;\n flex-direction: column;\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--fm-radius);\n background: var(--surface-card, #ffffff);\n box-shadow: var(--shadow-md);\n overflow: hidden;\n font-family: var(--font-sans, system-ui, sans-serif);\n}\n\n/* Widescreen full-bleed — escape DocFrame's centered column and span the\n * viewport. Header + legend align to the panes' outer edge via `--fm-pad`; the\n * panes grid uses `gap: var(--fm-pad)` so each column resolves to ~50vw. */\n.bleed {\n width: 100vw;\n margin-left: calc(50% - 50vw);\n border-radius: 0;\n border-inline: 0;\n}\n.bleed .bar {\n padding-inline: var(--fm-pad);\n}\n.bleed .panes {\n padding-inline: var(--fm-pad);\n gap: var(--fm-pad);\n}\n.bleed .legend {\n padding-inline: var(--fm-pad);\n}\n.bleed .overlay {\n padding-inline: var(--fm-pad);\n}\n\n/* ── Header bar ──────────────────────────────────────────────────────────── */\n.bar {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n flex-wrap: wrap;\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n border-bottom: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n}\n.titleWrap {\n display: inline-flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) * 1.5);\n min-width: 0;\n}\n.title {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-sm, 0.8125rem);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.02em;\n line-height: 1;\n color: var(--text-primary, #111827);\n}\n.subtitle {\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 text-transform: uppercase;\n line-height: 1;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Spacer — pushes the status + toggle to the right edge. */\n.spacer {\n flex: 1 1 auto;\n}\n\n/* Platform chip — mono caps pill, hairline, card bg. */\n.chip {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.25) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #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 line-height: 1;\n text-transform: uppercase;\n white-space: nowrap;\n color: var(--text-secondary, #4b5563);\n}\n.chipDot {\n width: 0.375rem;\n height: 0.375rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--success, #22c55e);\n box-shadow: 0 0 0.375rem color-mix(in srgb, var(--success, #22c55e) 70%, transparent);\n}\n.chipIcon {\n width: 0.75rem;\n height: 0.75rem;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* Status pill — the diff indicator, tinted by verdict. */\n.status {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.25) var(--spacing-3, 0.75rem);\n border: var(--stroke-width-thin, 1px) solid transparent;\n border-radius: var(--radius-full, 9999px);\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.08em;\n line-height: 1;\n text-transform: uppercase;\n white-space: nowrap;\n}\n.status[data-verdict=\"match\"] {\n color: var(--success, #22c55e);\n border-color: color-mix(in srgb, var(--success, #22c55e) 40%, transparent);\n background: color-mix(in srgb, var(--success, #22c55e) 12%, var(--surface-card, #ffffff));\n}\n.status[data-verdict=\"drift\"] {\n color: var(--warning, #f59e0b);\n border-color: color-mix(in srgb, var(--warning, #f59e0b) 42%, transparent);\n background: color-mix(in srgb, var(--warning, #f59e0b) 12%, var(--surface-card, #ffffff));\n}\n.status[data-verdict=\"fail\"] {\n color: var(--destructive, #ef4444);\n border-color: color-mix(in srgb, var(--destructive, #ef4444) 42%, transparent);\n background: color-mix(in srgb, var(--destructive, #ef4444) 12%, var(--surface-card, #ffffff));\n}\n.statusGlyph {\n width: 0.625rem;\n height: 0.625rem;\n border-radius: var(--radius-full, 9999px);\n background: currentColor;\n box-shadow: 0 0 0.4375rem currentColor;\n}\n.score {\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.02em;\n opacity: var(--opacity-80, 0.85);\n}\n\n/* Segmented compare-mode toggle — split / overlay. */\n.seg {\n display: inline-flex;\n gap: calc(var(--spacing-1, 0.25rem) * 0.5);\n padding: calc(var(--spacing-1, 0.25rem) * 0.75);\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--radius-full, 9999px);\n background: var(--fm-well);\n}\n.segBtn {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n padding: calc(var(--spacing-1, 0.25rem) * 1.25) calc(var(--spacing-1, 0.25rem) * 2.5);\n border: 0;\n border-radius: var(--radius-full, 9999px);\n background: transparent;\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 text-transform: uppercase;\n line-height: 1;\n color: var(--text-tertiary, #9ca3af);\n cursor: pointer;\n transition: color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),\n background-color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);\n}\n.segIcon {\n width: 0.8125rem;\n height: 0.8125rem;\n}\n.segBtn[aria-pressed=\"true\"] {\n color: var(--text-primary, #111827);\n background: var(--surface-card, #ffffff);\n box-shadow: var(--shadow-md);\n}\n.segBtn: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/* ── Panes: side-by-side ─────────────────────────────────────────────────── */\n.panes {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: var(--spacing-4, 1rem);\n padding: var(--spacing-4, 1rem);\n align-items: start;\n}\n.pane {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n min-width: 0;\n}\n.paneLabel {\n display: inline-flex;\n align-items: center;\n gap: 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 line-height: 1;\n color: var(--text-secondary, #4b5563);\n}\n.scopeDot {\n width: 0.5rem;\n height: 0.5rem;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n background: currentColor;\n box-shadow: 0 0 0.5rem color-mix(in srgb, currentColor 60%, transparent);\n}\n.pane[data-side=\"design\"] .scopeDot {\n color: var(--fm-design-hue);\n}\n.pane[data-side=\"built\"] .scopeDot {\n color: var(--fm-built-hue);\n}\n.paneMeta {\n margin-left: auto;\n color: var(--text-tertiary, #9ca3af);\n letter-spacing: 0.1em;\n font-weight: var(--font-weight-medium, 500);\n}\n\n/* Capture well — the recessed frame that holds a captured surface. */\n.well {\n position: relative;\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--fm-well-radius);\n background: var(--fm-well);\n padding: var(--spacing-3, 0.75rem);\n overflow: hidden;\n}\n.pane[data-side=\"built\"] .well {\n box-shadow: inset 0 0 0 var(--stroke-width-thin, 1px)\n color-mix(in srgb, var(--fm-built-hue) 22%, transparent);\n}\n\n/* Captured media — an image or a live-route/external iframe fills the well. */\n.media {\n display: block;\n width: 100%;\n height: auto;\n border: 0;\n border-radius: calc(var(--fm-well-radius) - 0.25rem);\n}\n\n/* Device bezel — native / flutter screenshots read inside a subtle frame. */\n.bezel {\n width: 10.5rem;\n margin: 0 auto;\n border: 0.375rem solid color-mix(in srgb, var(--text-primary, #111827) 78%, transparent);\n border-radius: 1.625rem;\n overflow: hidden;\n background: var(--surface-page, #ffffff);\n box-shadow: var(--shadow-lg);\n}\n.bezel .media {\n border-radius: 0;\n}\n\n/* Delta callout marker — a numbered accent dot pinned over a drift location. */\n.delta {\n position: absolute;\n z-index: 3;\n display: grid;\n place-items: center;\n width: 1.25rem;\n height: 1.25rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--warning, #f59e0b);\n color: var(--color-neutral-950, #030712);\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 line-height: 1;\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--warning, #f59e0b) 30%, transparent),\n var(--shadow-md);\n}\n\n/* ── Overlay / slider mode ───────────────────────────────────────────────── */\n.overlay {\n position: relative;\n padding: var(--spacing-4, 1rem);\n}\n.overlayFrame {\n position: relative;\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--fm-well-radius);\n background: var(--fm-well);\n overflow: hidden;\n}\n.layer {\n padding: var(--spacing-3, 0.75rem);\n}\n/* Clear space below the corner ribbons so they never collide with content. */\n.overlayFrame .layer {\n padding-top: var(--spacing-10, 2.5rem);\n}\n.layerTop {\n position: absolute;\n inset: 0;\n /* Revealed portion driven by `--fm-split` (design shows on the left). */\n clip-path: inset(0 calc(100% - var(--fm-split, 55%)) 0 0);\n}\n.handle {\n position: absolute;\n top: 0;\n bottom: 0;\n left: var(--fm-split, 55%);\n width: 2px;\n background: var(--accent, #2563eb);\n box-shadow: 0 0 0.625rem color-mix(in srgb, var(--accent, #2563eb) 70%, transparent);\n z-index: 4;\n}\n.grip {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: grid;\n place-items: center;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--accent, #2563eb);\n color: var(--color-white, #ffffff);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #2563eb) 26%, transparent),\n var(--shadow-md);\n}\n.gripIcon {\n width: 0.9375rem;\n height: 0.9375rem;\n}\n.ribbon {\n position: absolute;\n z-index: 5;\n top: var(--spacing-3, 0.75rem);\n padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);\n border-radius: var(--radius-full, 9999px);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-2xs, 0.6875rem) * 0.9);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.12em;\n text-transform: uppercase;\n line-height: 1;\n color: var(--color-white, #ffffff);\n}\n.ribbon[data-side=\"design\"] {\n left: var(--spacing-3, 0.75rem);\n background: color-mix(in srgb, var(--fm-design-hue) 85%, var(--color-neutral-950, #030712));\n}\n.ribbon[data-side=\"built\"] {\n right: var(--spacing-3, 0.75rem);\n background: color-mix(in srgb, var(--fm-built-hue) 85%, var(--color-neutral-950, #030712));\n}\n.range {\n width: 100%;\n margin-top: var(--spacing-3, 0.75rem);\n accent-color: var(--accent, #2563eb);\n cursor: ew-resize;\n}\n.range: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/* ── Delta legend ────────────────────────────────────────────────────────── */\n.legend {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2, 0.5rem);\n padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);\n border-top: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n}\n.legendHead {\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 color: var(--text-tertiary, #9ca3af);\n}\n.legendRow {\n display: flex;\n align-items: center;\n gap: var(--spacing-3, 0.75rem);\n}\n.legendNum {\n flex-shrink: 0;\n display: grid;\n place-items: center;\n width: 1.125rem;\n height: 1.125rem;\n border-radius: var(--radius-full, 9999px);\n background: var(--warning, #f59e0b);\n color: var(--color-neutral-950, #030712);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-2xs, 0.6875rem) * 0.9);\n font-weight: var(--font-weight-bold, 700);\n}\n.tag {\n flex-shrink: 0;\n padding: calc(var(--spacing-1, 0.25rem) * 0.75) var(--spacing-2, 0.5rem);\n border: var(--stroke-width-thin, 1px) solid var(--hairline, #e5e7eb);\n border-radius: var(--radius-sm, 0.25rem);\n background: var(--surface-subtle, #f3f4f6);\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: calc(var(--font-size-2xs, 0.6875rem) * 0.9);\n font-weight: var(--font-weight-bold, 700);\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--text-secondary, #4b5563);\n}\n.legendText {\n font-family: var(--font-mono, ui-monospace, \"SFMono-Regular\", Menlo, monospace);\n font-size: var(--font-size-sm, 0.8125rem);\n color: var(--text-secondary, #4b5563);\n}\n.legendText b {\n color: var(--text-primary, #111827);\n font-weight: var(--font-weight-bold, 700);\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\n/* Responsive: panes stack under 768px (design over built) — NEVER a sideways\n * scroll strip (the defect the doc-set surfaces exist to kill). */\n@media (max-width: 767px) {\n .panes {\n grid-template-columns: 1fr;\n }\n .bar {\n gap: var(--spacing-2, 0.5rem);\n }\n}\n"
|
|
2871
|
+
}
|
|
2872
|
+
]
|
|
2873
|
+
},
|
|
2798
2874
|
{
|
|
2799
2875
|
"name": "key-value-list",
|
|
2800
2876
|
"type": "registry:ui",
|
|
@@ -5037,7 +5113,7 @@
|
|
|
5037
5113
|
{
|
|
5038
5114
|
"path": "components/devtools/source-inspector/visor-component-names.generated.ts",
|
|
5039
5115
|
"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"
|
|
5116
|
+
"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 \"Capture\",\n \"Card\",\n \"CardContent\",\n \"CardDescription\",\n \"CardFooter\",\n \"CardGrid\",\n \"CardHeader\",\n \"CardLift\",\n \"CardTitle\",\n \"Carousel\",\n \"CarouselContent\",\n \"CarouselContext\",\n \"CarouselGallery\",\n \"CarouselItem\",\n \"CarouselNext\",\n \"CarouselPrevious\",\n \"ChallengeCard\",\n \"ChallengeCardAction\",\n \"ChallengeCardActions\",\n \"ChallengeCardBody\",\n \"ChallengeCardGate\",\n \"ChallengeCardHeader\",\n \"ChartContainer\",\n \"ChartContext\",\n \"ChartLegend\",\n \"ChartLegendContent\",\n \"ChartStyle\",\n \"ChartTooltip\",\n \"ChartTooltipContent\",\n \"Checkbox\",\n \"CheckGroup\",\n \"CheckRow\",\n \"Chip\",\n \"ChipGroup\",\n \"ChipGroupContext\",\n \"ChipGroupItem\",\n \"ChoiceChip\",\n \"ChromeButton\",\n \"ClosingSlide\",\n \"CodeBlock\",\n \"Collapsible\",\n \"CollapsibleContent\",\n \"CollapsibleTrigger\",\n \"ColorBar\",\n \"ColorPaletteSection\",\n \"ColorPicker\",\n \"ColorPickerSurface\",\n \"ColorSwatch\",\n \"ColorSwatchGrid\",\n \"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 \"FidelityMirror\",\n \"Field\",\n \"FieldDescription\",\n \"FieldError\",\n \"FieldLabel\",\n \"Fieldset\",\n \"FieldsetLegend\",\n \"FileUpload\",\n \"FilterBar\",\n \"FilterChip\",\n \"FontShowcase\",\n \"FontShowcaseGrid\",\n \"Footer\",\n \"FooterSection\",\n \"Form\",\n \"FormError\",\n \"FormErrorDescription\",\n \"FormErrorTitle\",\n \"FormField\",\n \"FormSpecimenSection\",\n \"FormSpecimenSlide\",\n \"FullscreenOverlay\",\n \"FullscreenOverlayContent\",\n \"FullscreenOverlayTrigger\",\n \"GrainOverlay\",\n \"Grid\",\n \"Header\",\n \"Heading\",\n \"HeroGlow\",\n \"HeroSection\",\n \"HeroSlide\",\n \"HoverCard\",\n \"HoverCardContent\",\n \"HoverCardTrigger\",\n \"IconGrid\",\n \"IconGridSection\",\n \"IconSizeRow\",\n \"IconsSlide\",\n \"Image\",\n \"InfographicBar\",\n \"Inline\",\n \"Input\",\n \"Kbd\",\n \"KeyValueList\",\n \"Label\",\n \"Landing\",\n \"Lightbox\",\n \"LightboxContent\",\n \"LightboxContext\",\n \"LightboxTrigger\",\n \"LoginForm\",\n \"Marquee\",\n \"MarqueeBandRenderer\",\n \"MatrixCell\",\n \"MatrixTable\",\n \"MatrixTableInner\",\n \"Menubar\",\n \"MenubarCheckboxItem\",\n \"MenubarContent\",\n \"MenubarGroup\",\n \"MenubarItem\",\n \"MenubarLabel\",\n \"MenubarMenu\",\n \"MenubarRadioGroup\",\n \"MenubarRadioItem\",\n \"MenubarSeparator\",\n \"MenubarShortcut\",\n \"MenubarSub\",\n \"MenubarSubContent\",\n \"MenubarSubTrigger\",\n \"MenubarTrigger\",\n \"MonthCalendar\",\n \"MotionDuration\",\n \"MotionDurationSection\",\n \"MotionEasing\",\n \"MotionEasingSection\",\n \"MotionSlide\",\n \"NameRoster\",\n \"NameRosterItem\",\n \"Navbar\",\n \"NavbarBrand\",\n \"NavbarContent\",\n \"NavbarItem\",\n \"NavbarLink\",\n \"NavbarToggle\",\n \"NumberInput\",\n \"OfflineBanner\",\n \"OpacityBar\",\n \"OpacitySlide\",\n \"OTPInput\",\n \"PageHeader\",\n \"Pagination\",\n \"PaginationContent\",\n \"PaginationEllipsis\",\n \"PaginationItem\",\n \"PaginationLink\",\n \"PaginationNext\",\n \"PaginationPrevious\",\n \"PasswordInput\",\n \"PhoneInput\",\n \"PlatformChip\",\n \"Popover\",\n \"PopoverAnchor\",\n \"PopoverContent\",\n \"PopoverFooter\",\n \"PopoverSelectionItem\",\n \"PopoverSelectionLabel\",\n \"PopoverSelectionList\",\n \"PopoverTrigger\",\n \"PricingSection\",\n \"ProfileMenu\",\n \"Progress\",\n \"PrototypeReview\",\n \"QuickActions\",\n \"RadioGroup\",\n \"RadioGroupItem\",\n \"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
5117
|
}
|
|
5042
5118
|
]
|
|
5043
5119
|
},
|
package/dist/visor-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.4.0",
|
|
3
|
-
"generated_at": "2026-07-
|
|
3
|
+
"generated_at": "2026-07-15T17:44:38.978Z",
|
|
4
4
|
"components": {
|
|
5
5
|
"accessibility-specimen": {
|
|
6
6
|
"category": "specimen",
|
|
@@ -3429,6 +3429,215 @@
|
|
|
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-display",
|
|
3517
|
+
"--font-family-heading",
|
|
3518
|
+
"--font-mono",
|
|
3519
|
+
"--font-size-2xs",
|
|
3520
|
+
"--font-size-lg",
|
|
3521
|
+
"--font-size-sm",
|
|
3522
|
+
"--font-size-xs",
|
|
3523
|
+
"--font-weight-bold",
|
|
3524
|
+
"--font-weight-medium",
|
|
3525
|
+
"--hairline",
|
|
3526
|
+
"--hairline-strong",
|
|
3527
|
+
"--motion-duration-fast",
|
|
3528
|
+
"--motion-easing-standard",
|
|
3529
|
+
"--primary",
|
|
3530
|
+
"--primary-text",
|
|
3531
|
+
"--radius-full",
|
|
3532
|
+
"--spacing-1",
|
|
3533
|
+
"--spacing-2",
|
|
3534
|
+
"--spacing-3",
|
|
3535
|
+
"--spacing-4",
|
|
3536
|
+
"--spacing-5",
|
|
3537
|
+
"--spacing-6",
|
|
3538
|
+
"--spacing-8",
|
|
3539
|
+
"--stroke-width-thin",
|
|
3540
|
+
"--surface-card",
|
|
3541
|
+
"--surface-page",
|
|
3542
|
+
"--text-primary",
|
|
3543
|
+
"--text-tertiary"
|
|
3544
|
+
],
|
|
3545
|
+
"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"
|
|
3546
|
+
},
|
|
3547
|
+
"doc-nav": {
|
|
3548
|
+
"category": "navigation",
|
|
3549
|
+
"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).",
|
|
3550
|
+
"when_to_use": [
|
|
3551
|
+
"A documentation shell that must group docs by product/section and collapse them, driven by a docs manifest",
|
|
3552
|
+
"Multi-product doc sets where a pinned Shared group sits beside accordion product groups (switching product = expanding its group)",
|
|
3553
|
+
"Ad-hoc/one-off docs with no canonical slot that should tuck into an Appendix instead of widening the row",
|
|
3554
|
+
"Any doc nav that must never run off the edge — groups wrap, they do not scroll horizontally"
|
|
3555
|
+
],
|
|
3556
|
+
"when_not_to_use": [
|
|
3557
|
+
"The surrounding themed page shell, sticky header, and brand/logo slot (use DocFrame — VI-609)",
|
|
3558
|
+
"In-app section sub-navigation with count pills across a detail view (use SectionNav)",
|
|
3559
|
+
"Top-level site navigation with brand + actions (use Navbar)",
|
|
3560
|
+
"A single expandable section (use Collapsible) or a grouped side rail (use Sidebar)"
|
|
3561
|
+
],
|
|
3562
|
+
"variants": {},
|
|
3563
|
+
"props": [
|
|
3564
|
+
{
|
|
3565
|
+
"name": "docs",
|
|
3566
|
+
"type": "DocEntry[]",
|
|
3567
|
+
"required": true,
|
|
3568
|
+
"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."
|
|
3569
|
+
},
|
|
3570
|
+
{
|
|
3571
|
+
"name": "currentPath",
|
|
3572
|
+
"type": "string",
|
|
3573
|
+
"required": true,
|
|
3574
|
+
"description": "The active route (or static location.pathname). Resolves the active pill (with aria-current=\"page\") and auto-expands its group."
|
|
3575
|
+
},
|
|
3576
|
+
{
|
|
3577
|
+
"name": "activeProduct",
|
|
3578
|
+
"type": "string",
|
|
3579
|
+
"description": "Which product group is open (the accordion). Absent → single-product mode with no accordion."
|
|
3580
|
+
},
|
|
3581
|
+
{
|
|
3582
|
+
"name": "onProductToggle",
|
|
3583
|
+
"type": "(id: string) => void",
|
|
3584
|
+
"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)."
|
|
3585
|
+
},
|
|
3586
|
+
{
|
|
3587
|
+
"name": "pinnedGroups",
|
|
3588
|
+
"type": "string[]",
|
|
3589
|
+
"default": "[\"shared\"]",
|
|
3590
|
+
"description": "Group ids that stay open regardless of the accordion."
|
|
3591
|
+
},
|
|
3592
|
+
{
|
|
3593
|
+
"name": "defaultCollapsed",
|
|
3594
|
+
"type": "boolean",
|
|
3595
|
+
"default": "true",
|
|
3596
|
+
"description": "Whether non-active, non-pinned groups start collapsed — the anti-wall rule."
|
|
3597
|
+
}
|
|
3598
|
+
],
|
|
3599
|
+
"dependencies": [
|
|
3600
|
+
"@phosphor-icons/react",
|
|
3601
|
+
"@loworbitstudio/visor-core"
|
|
3602
|
+
],
|
|
3603
|
+
"tokens_used": [
|
|
3604
|
+
"--accent",
|
|
3605
|
+
"--border-default",
|
|
3606
|
+
"--border-focus",
|
|
3607
|
+
"--color-neutral-950",
|
|
3608
|
+
"--doc-nav-accent",
|
|
3609
|
+
"--doc-nav-group-accent",
|
|
3610
|
+
"--doc-nav-group-radius",
|
|
3611
|
+
"--doc-nav-pill-bg",
|
|
3612
|
+
"--doc-nav-pin-bg",
|
|
3613
|
+
"--doc-nav-pin-border",
|
|
3614
|
+
"--focus-ring-offset",
|
|
3615
|
+
"--focus-ring-width",
|
|
3616
|
+
"--font-mono",
|
|
3617
|
+
"--font-size-2xs",
|
|
3618
|
+
"--font-size-xs",
|
|
3619
|
+
"--font-weight-bold",
|
|
3620
|
+
"--font-weight-medium",
|
|
3621
|
+
"--hairline",
|
|
3622
|
+
"--info",
|
|
3623
|
+
"--motion-duration-fast",
|
|
3624
|
+
"--motion-easing-standard",
|
|
3625
|
+
"--radius-full",
|
|
3626
|
+
"--radius-md",
|
|
3627
|
+
"--radius-sm",
|
|
3628
|
+
"--spacing-1",
|
|
3629
|
+
"--spacing-2",
|
|
3630
|
+
"--spacing-3",
|
|
3631
|
+
"--spacing-4",
|
|
3632
|
+
"--stroke-width-thin",
|
|
3633
|
+
"--surface-card",
|
|
3634
|
+
"--surface-selected",
|
|
3635
|
+
"--text-primary",
|
|
3636
|
+
"--text-secondary",
|
|
3637
|
+
"--text-tertiary"
|
|
3638
|
+
],
|
|
3639
|
+
"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"
|
|
3640
|
+
},
|
|
3432
3641
|
"dropdown-menu": {
|
|
3433
3642
|
"category": "overlay",
|
|
3434
3643
|
"description": "Menu of actions or options that opens from a trigger button.",
|
|
@@ -3893,6 +4102,138 @@
|
|
|
3893
4102
|
],
|
|
3894
4103
|
"example": "import { WarningCircle } from '@phosphor-icons/react';\n\n<ErrorPlacard\n icon={<WarningCircle weight=\"fill\" />}\n title=\"Could not load members\"\n body=\"The request timed out. Check your connection and try again.\"\n actions={\n <>\n <Button variant=\"ghost\" size=\"sm\">Dismiss</Button>\n <Button variant=\"outline\" size=\"sm\">Retry</Button>\n </>\n }\n/>\n"
|
|
3895
4104
|
},
|
|
4105
|
+
"fidelity-mirror": {
|
|
4106
|
+
"category": "data-display",
|
|
4107
|
+
"description": "The design-left / built-right comparison surface — renders a pure-HTML design source beside its Visor-TSX render (or a native/Flutter device-framed screenshot set, or an external artifact) in recessed capture wells, with a verdict diff indicator, numbered delta callouts, a classed delta legend, and a drag-to-reveal overlay mode. Composes inside DocFrame's content and goes full-bleed on widescreen so each pane is ~50vw. This is the Fidelity Mirror DISPLAY; the screenshot-diff GATE (PL-2139) is a separate verification concern.",
|
|
4108
|
+
"when_to_use": [
|
|
4109
|
+
"A Fidelity Mirror doc surface that renders a design-vs-built comparison inside DocFrame, so the doc type is Borealis-native rather than hand-rolled per project",
|
|
4110
|
+
"Showing the pure HTML design (left) against the Visor-ified TSX render (right) with a diff verdict and enumerated, classed deltas",
|
|
4111
|
+
"Comparing a native (SwiftUI) or Flutter screen against its design inside a device bezel, or an external claude.ai artifact against its source",
|
|
4112
|
+
"A widescreen full-bleed compare where each pane spans ~50vw for pixel-level review on a fullscreen monitor"
|
|
4113
|
+
],
|
|
4114
|
+
"when_not_to_use": [
|
|
4115
|
+
"The fidelity GATE — screenshot-diff verification that a build matches its design (that is PL-2139 / Gate 16, a verification concern, not a display one)",
|
|
4116
|
+
"The themed doc-page shell / sticky header / brand slot (use DocFrame — VI-609)",
|
|
4117
|
+
"The grouped, collapsible doc navigation (use DocNav — VI-608)",
|
|
4118
|
+
"A generic two-up media comparison with no design-fidelity semantics (use a plain grid or Carousel)"
|
|
4119
|
+
],
|
|
4120
|
+
"variants": {},
|
|
4121
|
+
"props": [
|
|
4122
|
+
{
|
|
4123
|
+
"name": "title",
|
|
4124
|
+
"type": "string",
|
|
4125
|
+
"required": true,
|
|
4126
|
+
"description": "The compared surface's name — the header title (e.g. a component or screen name)."
|
|
4127
|
+
},
|
|
4128
|
+
{
|
|
4129
|
+
"name": "design",
|
|
4130
|
+
"type": "FidelitySource",
|
|
4131
|
+
"required": true,
|
|
4132
|
+
"description": "The DESIGN source (left pane) — the pure-HTML design. Renders `content` when present, else an `<img>` (image) or a live-route/external `<iframe>` from `src`. Carries an optional pane `label` + mono `meta`."
|
|
4133
|
+
},
|
|
4134
|
+
{
|
|
4135
|
+
"name": "built",
|
|
4136
|
+
"type": "FidelitySource",
|
|
4137
|
+
"required": true,
|
|
4138
|
+
"description": "The BUILT source (right pane) — the Visor-TSX render, or a device-framed screenshot for native/flutter. Same shape as `design`; the built pane also draws the numbered delta callouts."
|
|
4139
|
+
},
|
|
4140
|
+
{
|
|
4141
|
+
"name": "verdict",
|
|
4142
|
+
"type": "'match' | 'drift' | 'fail'",
|
|
4143
|
+
"required": true,
|
|
4144
|
+
"description": "The diff verdict — tints the status pill (the diff indicator). match → success, drift → warning, fail → destructive."
|
|
4145
|
+
},
|
|
4146
|
+
{
|
|
4147
|
+
"name": "platform",
|
|
4148
|
+
"type": "'web' | 'native' | 'flutter' | 'external'",
|
|
4149
|
+
"default": "'web'",
|
|
4150
|
+
"description": "Platform of the built surface. Drives the built-side renderer and the header chip — web → live-route iframe + \"Web · Live Route\"; native → SwiftUI screenshot in a device bezel + \"iOS · SwiftUI\"; flutter → device-framed image + \"Flutter\"; external → external artifact + \"External\"."
|
|
4151
|
+
},
|
|
4152
|
+
{
|
|
4153
|
+
"name": "mode",
|
|
4154
|
+
"type": "'split' | 'overlay'",
|
|
4155
|
+
"default": "'split'",
|
|
4156
|
+
"description": "Initial compare presentation — split (design-left / built-right in recessed wells) or overlay (drag-to-reveal slider seam). The header segmented toggle switches in place."
|
|
4157
|
+
},
|
|
4158
|
+
{
|
|
4159
|
+
"name": "score",
|
|
4160
|
+
"type": "string",
|
|
4161
|
+
"description": "Optional score/summary string on the status pill (e.g. \"99.4%\", \"3 deltas\")."
|
|
4162
|
+
},
|
|
4163
|
+
{
|
|
4164
|
+
"name": "deltas",
|
|
4165
|
+
"type": "FidelityDelta[]",
|
|
4166
|
+
"description": "Enumerated drifts — each renders a numbered callout on the built side (when it carries a `position`) and a classed legend row. Classes mirror the render-vs-design self-check taxonomy (radius | color | spacing | type | align). Omit → no callouts or legend (e.g. a MATCH case)."
|
|
4167
|
+
},
|
|
4168
|
+
{
|
|
4169
|
+
"name": "bleed",
|
|
4170
|
+
"type": "boolean",
|
|
4171
|
+
"default": "true",
|
|
4172
|
+
"description": "Widescreen full-bleed layout — the compare escapes DocFrame's centered column and spans the viewport so each pane is ~50vw with the center gutter equal to the outer padding. Under 768px the panes stack design-over-built."
|
|
4173
|
+
},
|
|
4174
|
+
{
|
|
4175
|
+
"name": "overlayPosition",
|
|
4176
|
+
"type": "number",
|
|
4177
|
+
"default": "55",
|
|
4178
|
+
"description": "Initial overlay reveal position (0–100), the percent of the design source shown from the left before the slider seam."
|
|
4179
|
+
},
|
|
4180
|
+
{
|
|
4181
|
+
"name": "onModeChange",
|
|
4182
|
+
"type": "(mode: 'split' | 'overlay') => void",
|
|
4183
|
+
"description": "Notified when the compare mode toggles via the header segmented control."
|
|
4184
|
+
}
|
|
4185
|
+
],
|
|
4186
|
+
"dependencies": [
|
|
4187
|
+
"@phosphor-icons/react",
|
|
4188
|
+
"@loworbitstudio/visor-core"
|
|
4189
|
+
],
|
|
4190
|
+
"tokens_used": [
|
|
4191
|
+
"--accent",
|
|
4192
|
+
"--border-focus",
|
|
4193
|
+
"--color-neutral-950",
|
|
4194
|
+
"--color-white",
|
|
4195
|
+
"--destructive",
|
|
4196
|
+
"--fm-built-hue",
|
|
4197
|
+
"--fm-design-hue",
|
|
4198
|
+
"--fm-pad",
|
|
4199
|
+
"--fm-radius",
|
|
4200
|
+
"--fm-split",
|
|
4201
|
+
"--fm-well",
|
|
4202
|
+
"--fm-well-radius",
|
|
4203
|
+
"--focus-ring-offset",
|
|
4204
|
+
"--focus-ring-width",
|
|
4205
|
+
"--font-mono",
|
|
4206
|
+
"--font-sans",
|
|
4207
|
+
"--font-size-2xs",
|
|
4208
|
+
"--font-size-sm",
|
|
4209
|
+
"--font-weight-bold",
|
|
4210
|
+
"--font-weight-medium",
|
|
4211
|
+
"--hairline",
|
|
4212
|
+
"--info",
|
|
4213
|
+
"--motion-duration-fast",
|
|
4214
|
+
"--motion-easing-standard",
|
|
4215
|
+
"--opacity-80",
|
|
4216
|
+
"--radius-full",
|
|
4217
|
+
"--radius-sm",
|
|
4218
|
+
"--shadow-lg",
|
|
4219
|
+
"--shadow-md",
|
|
4220
|
+
"--spacing-1",
|
|
4221
|
+
"--spacing-10",
|
|
4222
|
+
"--spacing-2",
|
|
4223
|
+
"--spacing-3",
|
|
4224
|
+
"--spacing-4",
|
|
4225
|
+
"--stroke-width-thin",
|
|
4226
|
+
"--success",
|
|
4227
|
+
"--surface-card",
|
|
4228
|
+
"--surface-page",
|
|
4229
|
+
"--surface-subtle",
|
|
4230
|
+
"--text-primary",
|
|
4231
|
+
"--text-secondary",
|
|
4232
|
+
"--text-tertiary",
|
|
4233
|
+
"--warning"
|
|
4234
|
+
],
|
|
4235
|
+
"example": "import { FidelityMirror } from '@/components/ui/fidelity-mirror/fidelity-mirror'\n\nexport function DocNavMirror() {\n return (\n <FidelityMirror\n title=\"DocNav\"\n subtitle=\"Fidelity Mirror · VI-611\"\n platform=\"web\"\n verdict=\"drift\"\n score=\"3 deltas\"\n design={{ content: <DesignCapture />, meta: 'index.html' }}\n built={{ content: <BuiltCapture />, meta: 'route · /doc-nav' }}\n deltas={[\n { class: 'radius', description: <>Group cluster corners <b>22px → 12px</b>.</>, position: { top: '34px', left: '12px' } },\n { class: 'color', description: <>Resting pills read <b>--surface-subtle</b> — should be a recessed well.</>, position: { top: '52px', left: '96px' } },\n { class: 'type', description: <>Group labels are <b>title-case</b> — should be all-caps mono.</>, position: { top: '34px', right: '12px' } },\n ]}\n />\n )\n}\n"
|
|
4236
|
+
},
|
|
3896
4237
|
"field": {
|
|
3897
4238
|
"category": "form",
|
|
3898
4239
|
"description": "A form field wrapper composing label, description, and error components.",
|
|
@@ -12053,6 +12394,7 @@
|
|
|
12053
12394
|
"bento-grid",
|
|
12054
12395
|
"card",
|
|
12055
12396
|
"chart",
|
|
12397
|
+
"fidelity-mirror",
|
|
12056
12398
|
"key-value-list",
|
|
12057
12399
|
"marquee",
|
|
12058
12400
|
"name-roster",
|
|
@@ -12110,6 +12452,8 @@
|
|
|
12110
12452
|
"navigation": [
|
|
12111
12453
|
"breadcrumb",
|
|
12112
12454
|
"command",
|
|
12455
|
+
"doc-frame",
|
|
12456
|
+
"doc-nav",
|
|
12113
12457
|
"navbar",
|
|
12114
12458
|
"page-header",
|
|
12115
12459
|
"pagination",
|