@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.
- package/dist/CHANGELOG.json +13 -1
- package/dist/index.js +433 -12
- package/dist/registry.json +109 -3
- package/dist/visor-manifest.json +247 -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-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
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
2322
|
-
|
|
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 =
|
|
8560
|
+
var __dirname2 = dirname13(fileURLToPath4(import.meta.url));
|
|
8145
8561
|
var pkg = JSON.parse(
|
|
8146
|
-
|
|
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();
|