@loworbitstudio/visor 1.16.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.4.0",
3
- "generated_at": "2026-07-07T17:14:24.752Z",
3
+ "generated_at": "2026-07-09T19:24:03.748Z",
4
4
  "components": {
5
5
  "accessibility-specimen": {
6
6
  "changeType": "current",
@@ -224,6 +224,18 @@
224
224
  "breakingChange": false,
225
225
  "migrationNote": null
226
226
  },
227
+ "doc-frame": {
228
+ "changeType": "current",
229
+ "files": [],
230
+ "breakingChange": false,
231
+ "migrationNote": null
232
+ },
233
+ "doc-nav": {
234
+ "changeType": "current",
235
+ "files": [],
236
+ "breakingChange": false,
237
+ "migrationNote": null
238
+ },
227
239
  "dropdown-menu": {
228
240
  "changeType": "current",
229
241
  "files": [],
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync32 } from "fs";
5
- import { dirname as dirname12, join as join32 } from "path";
4
+ import { readFileSync as readFileSync33 } from "fs";
5
+ import { dirname as dirname13, join as join33 } from "path";
6
6
  import { fileURLToPath as fileURLToPath4 } from "url";
7
7
  import { Command as Command2 } from "commander";
8
8
 
@@ -35,7 +35,7 @@ function loadManifest() {
35
35
  function findItem(registry, name) {
36
36
  return registry.items.find((item) => item.name === name);
37
37
  }
38
- function resolveTransitiveDeps(registry, names, onWarning) {
38
+ function resolveTransitiveDeps(registry, names, onWarning, includeSuggested = false) {
39
39
  const resolved = /* @__PURE__ */ new Map();
40
40
  const queue = names.map((n) => ({
41
41
  name: n,
@@ -49,10 +49,11 @@ function resolveTransitiveDeps(registry, names, onWarning) {
49
49
  throw new Error(`Registry item "${name}" not found.`);
50
50
  }
51
51
  resolved.set(name, item);
52
- if (item.registryDependencies) {
52
+ const walkDeps = includeSuggested ? [...item.registryDependencies ?? [], ...item.suggestedDependencies ?? []] : item.registryDependencies;
53
+ if (walkDeps) {
53
54
  const childAncestors = new Set(ancestors);
54
55
  childAncestors.add(name);
55
- for (const dep of item.registryDependencies) {
56
+ for (const dep of walkDeps) {
56
57
  if (childAncestors.has(dep)) {
57
58
  onWarning?.(`Circular registry dependency: ${name} \u2192 ${dep}`);
58
59
  } else if (!resolved.has(dep)) {
@@ -63,6 +64,17 @@ function resolveTransitiveDeps(registry, names, onWarning) {
63
64
  }
64
65
  return Array.from(resolved.values());
65
66
  }
67
+ function collectSuggestedDeps(registry, rootNames, resolvedNames) {
68
+ const suggested = /* @__PURE__ */ new Set();
69
+ for (const name of rootNames) {
70
+ const item = findItem(registry, name);
71
+ if (!item?.suggestedDependencies) continue;
72
+ for (const dep of item.suggestedDependencies) {
73
+ if (!resolvedNames.has(dep)) suggested.add(dep);
74
+ }
75
+ }
76
+ return Array.from(suggested).sort();
77
+ }
66
78
  function collectDependencies(items) {
67
79
  const deps = /* @__PURE__ */ new Set();
68
80
  const devDeps = /* @__PURE__ */ new Set();
@@ -2169,6 +2181,7 @@ function runFlutterPubGet(cwd, bin) {
2169
2181
  function addCommand(components, cwd, options = {}) {
2170
2182
  const json = options.json ?? false;
2171
2183
  const dryRun = options.dryRun ?? false;
2184
+ const withSuggested = options.withSuggested ?? false;
2172
2185
  const target = options.target ?? "react";
2173
2186
  const prefix = dryRun ? "[dry-run] " : "";
2174
2187
  let autoInitialized = false;
@@ -2318,9 +2331,14 @@ function addCommand(components, cwd, options = {}) {
2318
2331
  const circularWarnings = [];
2319
2332
  let items;
2320
2333
  try {
2321
- items = resolveTransitiveDeps(targetRegistry, canonicalNames, (msg) => {
2322
- circularWarnings.push(msg);
2323
- });
2334
+ items = resolveTransitiveDeps(
2335
+ targetRegistry,
2336
+ canonicalNames,
2337
+ (msg) => {
2338
+ circularWarnings.push(msg);
2339
+ },
2340
+ withSuggested
2341
+ );
2324
2342
  } catch (error) {
2325
2343
  if (json) {
2326
2344
  const message = error instanceof Error ? error.message : String(error);
@@ -2334,6 +2352,8 @@ function addCommand(components, cwd, options = {}) {
2334
2352
  logger.warn(warning);
2335
2353
  }
2336
2354
  }
2355
+ const resolvedNames = new Set(items.map((i) => i.name));
2356
+ const suggestedAvailable = withSuggested ? [] : collectSuggestedDeps(targetRegistry, canonicalNames, resolvedNames);
2337
2357
  if (!json) {
2338
2358
  logger.info(
2339
2359
  `Resolving ${itemNames.length} item(s) \u2192 ${items.length} total (with dependencies)`
@@ -2501,6 +2521,7 @@ function addCommand(components, cwd, options = {}) {
2501
2521
  autoInitialized,
2502
2522
  requested: itemNames,
2503
2523
  resolved: items.map((i) => i.name),
2524
+ ...suggestedAvailable.length > 0 ? { suggested: suggestedAvailable } : {},
2504
2525
  files: { written: writtenFiles, skipped: skippedFiles },
2505
2526
  dependencies: { installed: installedDeps, failed: failedDeps },
2506
2527
  warnings
@@ -2511,6 +2532,17 @@ function addCommand(components, cwd, options = {}) {
2511
2532
  );
2512
2533
  process.exit(failedDeps.length > 0 ? 1 : 0);
2513
2534
  }
2535
+ if (suggestedAvailable.length > 0) {
2536
+ logger.blank();
2537
+ logger.info(
2538
+ `Suggested slot-fill components (not installed): ${suggestedAvailable.join(", ")}`
2539
+ );
2540
+ logger.info(
2541
+ ` These fill the block's slots but aren't required to render it. Add with:`
2542
+ );
2543
+ logger.info(` npx visor add ${canonicalNames.join(" ")} --block --with-suggested`);
2544
+ logger.info(` or: npx visor add ${suggestedAvailable.join(" ")}`);
2545
+ }
2514
2546
  if (failedDeps.length > 0) {
2515
2547
  process.exit(1);
2516
2548
  }
@@ -8140,10 +8172,394 @@ function spawnCommand(cwd, options) {
8140
8172
  }
8141
8173
  }
8142
8174
 
8175
+ // src/commands/render.ts
8176
+ import { existsSync as existsSync30, mkdirSync as mkdirSync14, readFileSync as readFileSync32 } from "fs";
8177
+ import { dirname as dirname12, isAbsolute as isAbsolute6, resolve as resolve20 } from "path";
8178
+ var FIXTURES = {
8179
+ "stat-card": {
8180
+ default: {
8181
+ export: "StatCard",
8182
+ props: `{
8183
+ label: "Total Revenue",
8184
+ value: "$48,120",
8185
+ delta: { value: "+12.4%", direction: "up", label: "vs last month" },
8186
+ footer: "Updated moments ago",
8187
+ }`
8188
+ }
8189
+ },
8190
+ "doc-nav": {
8191
+ default: {
8192
+ export: "DocNav",
8193
+ interactiveTarget: '[data-slot="doc-nav-group-trigger"]',
8194
+ props: `{
8195
+ currentPath: "/docs/getting-started",
8196
+ docs: [
8197
+ { order: 0, label: "Overview", href: "/docs/overview" },
8198
+ { order: 1, label: "Getting Started", href: "/docs/getting-started" },
8199
+ { order: 2, label: "Installation", href: "/docs/installation" },
8200
+ { order: 1, label: "Dashboard", href: "/docs/veronica/dashboard", scope: ["Veronica"], group: "Veronica" },
8201
+ { order: 2, label: "Settings", href: "/docs/veronica/settings", scope: ["Veronica"], group: "Veronica" },
8202
+ { order: 1, label: "Overview", href: "/docs/solespark/overview", scope: ["SoleSpark"], group: "SoleSpark" },
8203
+ { order: 10, label: "Changelog", href: "/docs/changelog" },
8204
+ ],
8205
+ }`
8206
+ }
8207
+ },
8208
+ button: {
8209
+ default: {
8210
+ export: "Button",
8211
+ interactiveTarget: "button",
8212
+ props: `{ children: "Get started" }`
8213
+ }
8214
+ },
8215
+ badge: {
8216
+ default: {
8217
+ export: "Badge",
8218
+ props: `{ children: "New" }`
8219
+ }
8220
+ }
8221
+ };
8222
+ var VALID_MODES = /* @__PURE__ */ new Set(["light", "dark"]);
8223
+ var VALID_STATES = /* @__PURE__ */ new Set(["default", "hover", "focus", "active"]);
8224
+ function missingDepsError(missing) {
8225
+ const installArgs = missing.join(" ");
8226
+ const needsBrowsers = missing.includes("playwright");
8227
+ return {
8228
+ code: "OPTIONAL_DEP_MISSING",
8229
+ message: `\`visor render\` needs ${missing.join(" and ")}, which ${missing.length > 1 ? "are" : "is"} not installed.`,
8230
+ hint: `Install with: npm install -D ${installArgs}${needsBrowsers ? " && npx playwright install chromium" : ""}`
8231
+ };
8232
+ }
8233
+ function pascalCase(kebab) {
8234
+ return kebab.split(/[-_]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
8235
+ }
8236
+ function firstExisting(candidates) {
8237
+ for (const c of candidates) {
8238
+ if (existsSync30(c)) return c;
8239
+ }
8240
+ return null;
8241
+ }
8242
+ function resolveComponentFile(cwd, name) {
8243
+ return firstExisting([
8244
+ resolve20(cwd, "components", "ui", name, `${name}.tsx`),
8245
+ resolve20(cwd, "components", "devtools", name, `${name}.tsx`),
8246
+ resolve20(cwd, "components", "ui", name, "index.tsx")
8247
+ ]);
8248
+ }
8249
+ function resolveThemeCssFile(cwd, slug2) {
8250
+ return firstExisting([
8251
+ resolve20(cwd, "packages", "docs", "app", `${slug2}-theme.css`),
8252
+ resolve20(cwd, "app", `${slug2}-theme.css`),
8253
+ resolve20(cwd, `${slug2}-theme.css`)
8254
+ ]);
8255
+ }
8256
+ function resolveTokensCssFile(cwd) {
8257
+ return firstExisting([
8258
+ resolve20(cwd, "packages", "tokens", "dist", "tokens.css"),
8259
+ resolve20(cwd, "node_modules", "@loworbitstudio", "visor-core", "dist", "tokens.css")
8260
+ ]);
8261
+ }
8262
+ async function loadOptional(moduleName) {
8263
+ try {
8264
+ return await import(moduleName);
8265
+ } catch {
8266
+ return null;
8267
+ }
8268
+ }
8269
+ var ANIMATION_DISABLE_CSS = "*, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; caret-color: transparent !important; }";
8270
+ async function settle(page, fontsTimeout = 5e3) {
8271
+ await page.waitForLoadState("networkidle").catch(() => {
8272
+ });
8273
+ await page.addStyleTag({ content: ANIMATION_DISABLE_CSS });
8274
+ await page.evaluate(
8275
+ `new Promise(function (r) { var t = setTimeout(r, ${fontsTimeout}); if (document.fonts && document.fonts.ready) { document.fonts.ready.then(function () { clearTimeout(t); r(); }); } else { clearTimeout(t); r(); }})`
8276
+ );
8277
+ }
8278
+ function buildHtml(opts) {
8279
+ const htmlClass = opts.mode === "dark" ? "dark" : "";
8280
+ return `<!doctype html>
8281
+ <html lang="en" class="${htmlClass}" style="color-scheme: ${opts.mode}">
8282
+ <head>
8283
+ <meta charset="utf-8" />
8284
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8285
+ <style data-visor="tokens">
8286
+ ${opts.tokensCss}
8287
+ </style>
8288
+ <style data-visor="theme">
8289
+ ${opts.themeCss}
8290
+ </style>
8291
+ <style data-visor="component">
8292
+ ${opts.componentCss}
8293
+ </style>
8294
+ <style data-visor="harness">
8295
+ html, body { margin: 0; padding: 0; }
8296
+ #theme-scope {
8297
+ box-sizing: border-box;
8298
+ min-height: 100vh;
8299
+ padding: 48px;
8300
+ display: flex;
8301
+ align-items: center;
8302
+ justify-content: center;
8303
+ background: var(--surface-page, #ffffff);
8304
+ color: var(--text-primary, #111827);
8305
+ font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
8306
+ }
8307
+ #root { width: 100%; max-width: 420px; }
8308
+ </style>
8309
+ </head>
8310
+ <body>
8311
+ <div id="theme-scope" class="${opts.themeClass}">
8312
+ <div id="root"></div>
8313
+ </div>
8314
+ <script>
8315
+ ${opts.bundleJs}
8316
+ </script>
8317
+ </body>
8318
+ </html>`;
8319
+ }
8320
+ function buildEntrySource(componentFile, fixture, exportName) {
8321
+ return `import * as React from "react";
8322
+ import { createRoot } from "react-dom/client";
8323
+ import * as __mod from ${JSON.stringify(componentFile)};
8324
+
8325
+ function __resolveComponent(mod, preferred) {
8326
+ if (preferred && mod[preferred]) return mod[preferred];
8327
+ if (mod.default) return mod.default;
8328
+ var isComp = function (v) {
8329
+ return typeof v === "function" || (v && typeof v === "object" && "$$typeof" in v);
8330
+ };
8331
+ for (var k in mod) { if (isComp(mod[k])) return mod[k]; }
8332
+ throw new Error("visor render: no React component export found");
8333
+ }
8334
+
8335
+ var __Component = __resolveComponent(__mod, ${JSON.stringify(exportName)});
8336
+ var __props = ${fixture.props};
8337
+ createRoot(document.getElementById("root")).render(
8338
+ React.createElement(__Component, __props)
8339
+ );`;
8340
+ }
8341
+ async function renderCommand(component, cwd, options) {
8342
+ const json = options.json ?? false;
8343
+ const mode = (options.mode ?? "light").toLowerCase();
8344
+ const state = (options.state ?? "default").toLowerCase();
8345
+ const fail3 = (code, message, extra) => {
8346
+ if (json) {
8347
+ console.log(JSON.stringify({ success: false, error: { code, message }, ...extra }, null, 2));
8348
+ } else {
8349
+ logger.error(message);
8350
+ if (extra?.hint) logger.item(String(extra.hint));
8351
+ }
8352
+ process.exit(1);
8353
+ };
8354
+ if (!VALID_MODES.has(mode)) {
8355
+ fail3("BAD_MODE", `Invalid --mode "${mode}". Use "light" or "dark".`);
8356
+ }
8357
+ if (!VALID_STATES.has(state)) {
8358
+ fail3("BAD_STATE", `Invalid --state "${state}". Use one of: ${[...VALID_STATES].join(", ")}.`);
8359
+ }
8360
+ const componentFile = resolveComponentFile(cwd, component);
8361
+ if (!componentFile) {
8362
+ fail3(
8363
+ "COMPONENT_NOT_FOUND",
8364
+ `Component "${component}" not found under components/ui/. Expected components/ui/${component}/${component}.tsx.`
8365
+ );
8366
+ }
8367
+ const themeCssFile = resolveThemeCssFile(cwd, options.theme);
8368
+ if (!themeCssFile) {
8369
+ fail3(
8370
+ "THEME_NOT_FOUND",
8371
+ `Theme "${options.theme}" not found. Expected packages/docs/app/${options.theme}-theme.css.`,
8372
+ { hint: "Run `visor theme sync` to (re)generate theme CSS, or check the slug." }
8373
+ );
8374
+ }
8375
+ const tokensCssFile = resolveTokensCssFile(cwd);
8376
+ if (!tokensCssFile) {
8377
+ fail3(
8378
+ "TOKENS_NOT_FOUND",
8379
+ "Emitted tokens.css not found. Expected packages/tokens/dist/tokens.css.",
8380
+ { hint: "Run `npm run build -w packages/tokens` first." }
8381
+ );
8382
+ }
8383
+ const fixtureName = options.fixture ?? "default";
8384
+ const componentFixtures = FIXTURES[component];
8385
+ const fixture = componentFixtures?.[fixtureName] ?? { props: "{}" };
8386
+ if (options.fixture && !componentFixtures?.[fixtureName]) {
8387
+ fail3(
8388
+ "FIXTURE_NOT_FOUND",
8389
+ `Fixture "${fixtureName}" not registered for "${component}". Available: ${componentFixtures ? Object.keys(componentFixtures).join(", ") : "(none)"}.`
8390
+ );
8391
+ }
8392
+ if (!componentFixtures && !json) {
8393
+ logger.warn(
8394
+ `No fixture registered for "${component}" \u2014 rendering with empty props. Add one to FIXTURES in render.ts for representative content.`
8395
+ );
8396
+ }
8397
+ const exportName = fixture.export ?? pascalCase(component);
8398
+ const esbuild = await loadOptional("esbuild");
8399
+ const playwright = await loadOptional("playwright");
8400
+ const missing = [];
8401
+ if (!esbuild) missing.push("esbuild");
8402
+ if (!playwright) missing.push("playwright");
8403
+ if (missing.length > 0) {
8404
+ const dep = missingDepsError(missing);
8405
+ fail3(dep.code, dep.message, { missing, hint: dep.hint });
8406
+ }
8407
+ let componentCss = "";
8408
+ let bundleJs = "";
8409
+ try {
8410
+ const entrySource = buildEntrySource(componentFile, fixture, exportName);
8411
+ const result = await esbuild.build({
8412
+ stdin: {
8413
+ contents: entrySource,
8414
+ resolveDir: cwd,
8415
+ loader: "tsx",
8416
+ sourcefile: "visor-render-entry.tsx"
8417
+ },
8418
+ bundle: true,
8419
+ format: "iife",
8420
+ platform: "browser",
8421
+ jsx: "automatic",
8422
+ write: false,
8423
+ outdir: "visor-render-out",
8424
+ define: { "process.env.NODE_ENV": '"production"' },
8425
+ banner: { js: "globalThis.process = globalThis.process || { env: {} };" },
8426
+ logLevel: "silent"
8427
+ });
8428
+ for (const file of result.outputFiles ?? []) {
8429
+ if (file.path.endsWith(".css")) componentCss += file.text;
8430
+ else if (file.path.endsWith(".js")) bundleJs += file.text;
8431
+ }
8432
+ } catch (err) {
8433
+ const message = err instanceof Error ? err.message : String(err);
8434
+ fail3("BUNDLE_FAILED", `Failed to bundle ${component}: ${message}`);
8435
+ }
8436
+ const themeClass = `${options.theme}-theme`;
8437
+ const html = buildHtml({
8438
+ tokensCss: readFileSync32(tokensCssFile, "utf-8"),
8439
+ themeCss: readFileSync32(themeCssFile, "utf-8"),
8440
+ componentCss,
8441
+ bundleJs,
8442
+ themeClass,
8443
+ mode
8444
+ });
8445
+ const width = Number(options.width ?? 720);
8446
+ const height = Number(options.height ?? 640);
8447
+ const outPath = resolveOutPath(cwd, options.out, component, options.theme, mode, state);
8448
+ mkdirSync14(dirname12(outPath), { recursive: true });
8449
+ const browser = await playwright.chromium.launch();
8450
+ let probe;
8451
+ try {
8452
+ const context = await browser.newContext({
8453
+ viewport: { width, height },
8454
+ colorScheme: mode,
8455
+ deviceScaleFactor: 2
8456
+ });
8457
+ const page = await context.newPage();
8458
+ await page.setContent(html, { waitUntil: "load" });
8459
+ await page.waitForFunction(
8460
+ "document.getElementById('root') && document.getElementById('root').childElementCount > 0",
8461
+ void 0,
8462
+ { timeout: 1e4 }
8463
+ ).catch(() => {
8464
+ });
8465
+ await settle(page);
8466
+ probe = await page.evaluate(
8467
+ `(function () {
8468
+ var scope = document.getElementById("theme-scope");
8469
+ var html = document.documentElement;
8470
+ var read = function (el, prop) {
8471
+ return getComputedStyle(el).getPropertyValue(prop).trim();
8472
+ };
8473
+ var themed = read(scope, "--surface-card");
8474
+ var base = read(html, "--surface-card");
8475
+ return {
8476
+ themedSurfaceCard: themed,
8477
+ baseSurfaceCard: base,
8478
+ themedBg: getComputedStyle(scope).backgroundColor,
8479
+ mapped: themed !== "" && themed !== base,
8480
+ };
8481
+ })()`
8482
+ );
8483
+ if (state !== "default") {
8484
+ const target = fixture.interactiveTarget ?? "#root button, #root a, #root input, #root [tabindex]";
8485
+ const el = await page.$(target);
8486
+ if (el) {
8487
+ if (state === "hover") await el.hover();
8488
+ else if (state === "focus") await el.focus();
8489
+ else if (state === "active") {
8490
+ await el.hover();
8491
+ await page.mouse.down();
8492
+ }
8493
+ await page.waitForTimeout(80);
8494
+ } else if (!json) {
8495
+ logger.warn(`No interactive target ("${target}") found for state "${state}".`);
8496
+ }
8497
+ }
8498
+ await page.screenshot({ path: outPath, fullPage: true });
8499
+ if (state === "active") await page.mouse.up().catch(() => {
8500
+ });
8501
+ } finally {
8502
+ await browser.close();
8503
+ }
8504
+ const fileSize = existsSync30(outPath) ? statBytes(outPath) : 0;
8505
+ if (json) {
8506
+ console.log(
8507
+ JSON.stringify(
8508
+ {
8509
+ success: true,
8510
+ component,
8511
+ theme: options.theme,
8512
+ mode,
8513
+ state,
8514
+ fixture: fixtureName,
8515
+ out: outPath,
8516
+ bytes: fileSize,
8517
+ probe
8518
+ },
8519
+ null,
8520
+ 2
8521
+ )
8522
+ );
8523
+ return;
8524
+ }
8525
+ logger.success(`Rendered ${component} \xB7 ${options.theme} \xB7 ${mode}${state !== "default" ? ` \xB7 ${state}` : ""}`);
8526
+ logger.item(`\u2192 ${outPath} (${formatBytes2(fileSize)})`);
8527
+ logger.blank();
8528
+ if (probe.mapped) {
8529
+ logger.success(
8530
+ `Themed --surface-card resolved to ${probe.themedSurfaceCard} (base primitive: ${probe.baseSurfaceCard}) \u2014 theme mapping applied.`
8531
+ );
8532
+ } else {
8533
+ logger.warn(
8534
+ `Themed --surface-card (${probe.themedSurfaceCard}) equals the base primitive (${probe.baseSurfaceCard}). The theme override did not apply \u2014 check @layer/mode scoping (VI-511).`
8535
+ );
8536
+ }
8537
+ }
8538
+ function resolveOutPath(cwd, out, component, theme2, mode, state) {
8539
+ if (out) {
8540
+ return isAbsolute6(out) ? out : resolve20(cwd, out);
8541
+ }
8542
+ const suffix = state === "default" ? "" : `__${state}`;
8543
+ return resolve20(cwd, ".visor", "renders", `${component}__${theme2}__${mode}${suffix}.png`);
8544
+ }
8545
+ function statBytes(path2) {
8546
+ try {
8547
+ return readFileSync32(path2).length;
8548
+ } catch {
8549
+ return 0;
8550
+ }
8551
+ }
8552
+ function formatBytes2(bytes) {
8553
+ if (bytes < 1024) return `${bytes} B`;
8554
+ const kb = bytes / 1024;
8555
+ if (kb < 1024) return `${kb.toFixed(1)} KB`;
8556
+ return `${(kb / 1024).toFixed(1)} MB`;
8557
+ }
8558
+
8143
8559
  // src/index.ts
8144
- var __dirname2 = dirname12(fileURLToPath4(import.meta.url));
8560
+ var __dirname2 = dirname13(fileURLToPath4(import.meta.url));
8145
8561
  var pkg = JSON.parse(
8146
- readFileSync32(join32(__dirname2, "..", "package.json"), "utf-8")
8562
+ readFileSync33(join33(__dirname2, "..", "package.json"), "utf-8")
8147
8563
  );
8148
8564
  var program = new Command2();
8149
8565
  program.name("visor").description("CLI for the Visor design system").version(pkg.version);
@@ -8154,9 +8570,9 @@ program.command("init").description("Initialize Visor \u2014 with --template nex
8154
8570
  program.command("list").description("List all available registry items").option("--json", "output structured JSON (for AI agents)").option("--category <name>", "filter items by category").action((options) => {
8155
8571
  listCommand(process.cwd(), options);
8156
8572
  });
8157
- program.command("add").description("Add components, hooks, blocks, or utilities to your project").argument("[items...]", "names of registry items to add").option("--overwrite", "overwrite existing files", false).option("--category <name>", "install all items from a category").option("--block", "install blocks instead of components").option("--target <platform>", "target platform: react (default) or flutter", "react").option("--dry-run", "preview what would be added without writing files").option("--json", "output structured JSON (for AI agents)").action((items, options) => {
8573
+ program.command("add").description("Add components, hooks, blocks, or utilities to your project").argument("[items...]", "names of registry items to add").option("--overwrite", "overwrite existing files", false).option("--category <name>", "install all items from a category").option("--block", "install blocks instead of components").option("--with-suggested", "also install a block's suggested slot-fill components (e.g. breadcrumb, dropdown-menu, sidebar for admin-shell)").option("--target <platform>", "target platform: react (default) or flutter", "react").option("--dry-run", "preview what would be added without writing files").option("--json", "output structured JSON (for AI agents)").action((items, options) => {
8158
8574
  const target = options.target === "flutter" ? "flutter" : "react";
8159
- addCommand(items, process.cwd(), { overwrite: options.overwrite, category: options.category, block: options.block, target, dryRun: options.dryRun, json: options.json });
8575
+ addCommand(items, process.cwd(), { overwrite: options.overwrite, category: options.category, block: options.block, withSuggested: options.withSuggested, target, dryRun: options.dryRun, json: options.json });
8160
8576
  });
8161
8577
  program.command("diff").description(
8162
8578
  "Show differences between local files and the registry"
@@ -8341,4 +8757,9 @@ program.command("spawn").description(
8341
8757
  ).option("--from <identifier>", "blessed build to spawn: blessed:{shape}:{pattern}").option("--theme <id>", "theme id (or path to a .visor.yaml) to re-skin the fork with").option("--theme-file <path>", "explicit path to a theme.visor.yaml \u2014 bypasses --theme name resolution").option("--output <path>", "destination directory for the forked project").option("--blessed-dir <path>", "override the blessed-build root (default: VISOR_BLESSED_DIR or ~/Code/low-orbit/low-orbit-playbook/design-prototypes)").option("--install", "run npm install in the forked project (default: skip)").option("--validate", "validate the applied theme after forking (default: skip)").option("--list-blessed", "list all discoverable blessed builds and exit").option("--json", "output structured JSON (for AI agents)").action((options) => {
8342
8758
  spawnCommand(process.cwd(), options);
8343
8759
  });
8760
+ program.command("render").description(
8761
+ "Render a single component to a PNG using the real emitted tokens + real theme CSS + the real component (serverless, no next dev). A per-component render-fidelity harness."
8762
+ ).argument("<component>", "component name under components/ui/ (e.g. doc-nav, stat-card)").requiredOption("--theme <slug>", "theme slug (e.g. space, neutral) \u2014 resolves packages/docs/app/<slug>-theme.css").option("--mode <mode>", "color mode: light or dark", "light").option("--state <state>", "interactive state to capture: default, hover, focus, active", "default").option("--fixture <name>", "named fixture for the component (default: 'default')").option("-o, --out <path>", "output PNG path (default: .visor/renders/<component>__<theme>__<mode>.png)").option("--width <px>", "viewport width in px", "720").option("--height <px>", "viewport height in px", "640").option("--json", "output structured JSON (for AI agents)").action(async (component, options) => {
8763
+ await renderCommand(component, process.cwd(), options);
8764
+ });
8344
8765
  program.parse();