@loworbitstudio/visor 1.13.0 → 1.14.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/README.md CHANGED
@@ -175,6 +175,52 @@ Rules in `disabledRules` are skipped entirely — useful when a project intentio
175
175
  run: npx visor check design ./src --json
176
176
  ```
177
177
 
178
+ ## visor check theme-mode
179
+
180
+ Deterministic theme-mode gate. Reads a theme's declared `color-scheme` (`dark-only | light-only | adaptive`) and asserts that the app-root background (`--surface-page`) luminance matches the declared mode — catching the failure class where a `dark-only` brand ships a light app root (or vice versa), which structural oracle/freeze gates cannot see.
181
+
182
+ - `dark-only` → the dark-scope app-root background must be **dark** (luminance `< 0.2`)
183
+ - `light-only` → the light-scope app-root background must be **light** (luminance `>= 0.2`)
184
+ - `adaptive` (or no `color-scheme`) → **skipped** — no single mode to assert
185
+
186
+ The gate is fully deterministic and dependency-light: it reuses the theme engine's own resolution to compute the host page background the emitted CSS would carry, then reuses `getLuminance()`. No browser required.
187
+
188
+ ```bash
189
+ # Human-readable terminal output
190
+ npx visor check theme-mode ./my-theme.visor.yaml
191
+
192
+ # JSON output for pipeline wiring
193
+ npx visor check theme-mode ./my-theme.visor.yaml --json
194
+
195
+ # Advisory mode — report without failing CI
196
+ npx visor check theme-mode ./my-theme.visor.yaml --no-fail
197
+ ```
198
+
199
+ ### Output schema (--json)
200
+
201
+ ```json
202
+ {
203
+ "success": true,
204
+ "pass": false,
205
+ "skipped": false,
206
+ "mode": "dark-only",
207
+ "theme": "my-theme",
208
+ "computed_bg": "#ffffff",
209
+ "luminance": 1,
210
+ "threshold": 0.2,
211
+ "reason": "theme declares dark-only but app-root background \"#ffffff\" renders light (luminance 1.0000, threshold 0.2) — expected dark"
212
+ }
213
+ ```
214
+
215
+ On failure, `computed_bg` is the offending computed background color.
216
+
217
+ ### Exit codes
218
+
219
+ | Code | Meaning |
220
+ |------|---------|
221
+ | `0` | Mode matches, or `adaptive`/no-scheme (skipped), or `--no-fail` mode |
222
+ | `1` | Rendered mode does not match the declared `color-scheme` |
223
+
178
224
  ## Documentation
179
225
 
180
226
  Full docs at [visor.loworbit.studio](https://visor.loworbit.studio).
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.4.0",
3
- "generated_at": "2026-06-25T19:22:02.086Z",
3
+ "generated_at": "2026-07-02T17:50:48.092Z",
4
4
  "components": {
5
5
  "accessibility-specimen": {
6
6
  "changeType": "current",
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync27 } from "fs";
4
+ import { readFileSync as readFileSync28 } from "fs";
5
5
  import { dirname as dirname10, join as join25 } from "path";
6
6
  import { fileURLToPath as fileURLToPath4 } from "url";
7
7
  import { Command as Command2 } from "commander";
@@ -9,7 +9,7 @@ import { Command as Command2 } from "commander";
9
9
  // src/commands/check.ts
10
10
  import { Command } from "commander";
11
11
  import { statSync as statSync3 } from "fs";
12
- import { resolve as resolve3, dirname as dirname2 } from "path";
12
+ import { resolve as resolve4, dirname as dirname2 } from "path";
13
13
 
14
14
  // src/registry/resolve.ts
15
15
  import { readFileSync } from "fs";
@@ -867,6 +867,62 @@ function scanDesign(pathArg, options = {}) {
867
867
  };
868
868
  }
869
869
 
870
+ // src/check/theme-mode.ts
871
+ import { readFileSync as readFileSync4 } from "fs";
872
+ import { resolve as resolve3 } from "path";
873
+ import {
874
+ generateThemeData,
875
+ parseColor,
876
+ getLuminance
877
+ } from "@loworbitstudio/visor-theme-engine";
878
+ var LUMINANCE_THRESHOLD = 0.2;
879
+ function checkThemeModeSource(yamlSource, threshold = LUMINANCE_THRESHOLD) {
880
+ const data = generateThemeData(yamlSource);
881
+ const mode = data.config["color-scheme"];
882
+ const theme2 = data.config.name;
883
+ if (mode === "adaptive") {
884
+ return {
885
+ pass: true,
886
+ skipped: true,
887
+ mode,
888
+ theme: theme2,
889
+ computed_bg: null,
890
+ luminance: null,
891
+ threshold,
892
+ reason: "adaptive theme \u2014 supports both modes, no single background to assert"
893
+ };
894
+ }
895
+ const page = data.tokens.surface.page;
896
+ const computed_bg = mode === "dark-only" ? page.dark : page.light;
897
+ const parsed = parseColor(computed_bg);
898
+ if (!parsed) {
899
+ throw new Error(
900
+ `Could not parse app-root background color "${computed_bg}" for theme "${theme2}".`
901
+ );
902
+ }
903
+ const luminance = getLuminance(parsed.rgb[0], parsed.rgb[1], parsed.rgb[2]);
904
+ const isDark = luminance < threshold;
905
+ const pass = mode === "dark-only" ? isDark : !isDark;
906
+ const declaredWord = mode === "dark-only" ? "dark" : "light";
907
+ const actualWord = isDark ? "dark" : "light";
908
+ const reason = pass ? `theme declares ${mode}; app-root background renders ${actualWord} (luminance ${luminance.toFixed(4)}) \u2014 matches` : `theme declares ${mode} but app-root background "${computed_bg}" renders ${actualWord} (luminance ${luminance.toFixed(4)}, threshold ${threshold}) \u2014 expected ${declaredWord}`;
909
+ return {
910
+ pass,
911
+ skipped: false,
912
+ mode,
913
+ theme: theme2,
914
+ computed_bg,
915
+ luminance,
916
+ threshold,
917
+ reason
918
+ };
919
+ }
920
+ function checkThemeModeFile(filePath, threshold = LUMINANCE_THRESHOLD) {
921
+ const abs = resolve3(filePath);
922
+ const source = readFileSync4(abs, "utf-8");
923
+ return checkThemeModeSource(source, threshold);
924
+ }
925
+
870
926
  // src/utils/logger.ts
871
927
  import pc from "picocolors";
872
928
  var logger = {
@@ -1026,7 +1082,7 @@ async function checkDiffCommand(pathArg, options) {
1026
1082
  if (options.failOnHits) process.exit(1);
1027
1083
  }
1028
1084
  function checkDesignCommand(pathArg, options) {
1029
- const absPath = resolve3(pathArg);
1085
+ const absPath = resolve4(pathArg);
1030
1086
  const rcDir = statSync3(absPath).isDirectory() ? absPath : dirname2(absPath);
1031
1087
  const rc = loadVisorRc(rcDir);
1032
1088
  const result = scanDesign(absPath, {
@@ -1077,6 +1133,43 @@ function checkDesignCommand(pathArg, options) {
1077
1133
  logger.blank();
1078
1134
  if (!options.noFail && summary.errorCount > 0) process.exit(1);
1079
1135
  }
1136
+ function checkThemeModeCommand(pathArg, options) {
1137
+ let result;
1138
+ try {
1139
+ result = checkThemeModeFile(pathArg);
1140
+ } catch (err) {
1141
+ const message = err instanceof Error ? err.message : String(err);
1142
+ const useJson2 = options.json || options.format === "json";
1143
+ if (useJson2) {
1144
+ console.log(JSON.stringify({ success: false, error: message }, null, 2));
1145
+ } else {
1146
+ logger.error(message);
1147
+ }
1148
+ process.exit(1);
1149
+ return;
1150
+ }
1151
+ const useJson = options.json || options.format === "json";
1152
+ const shouldFail = options.fail !== false;
1153
+ if (useJson) {
1154
+ console.log(JSON.stringify({ success: true, ...result }, null, 2));
1155
+ if (shouldFail && !result.pass) process.exit(1);
1156
+ process.exit(0);
1157
+ return;
1158
+ }
1159
+ if (result.skipped) {
1160
+ logger.info(`\u20DD skipped \u2014 ${result.theme}: ${result.reason}`);
1161
+ process.exit(0);
1162
+ return;
1163
+ }
1164
+ if (result.pass) {
1165
+ logger.success(`${result.theme} \u2014 ${result.reason}`);
1166
+ process.exit(0);
1167
+ return;
1168
+ }
1169
+ logger.error(`${result.theme} \u2014 ${result.reason}`);
1170
+ logger.item(` offending background: ${pc2.cyan(result.computed_bg ?? "unknown")} (luminance ${result.luminance?.toFixed(4)})`);
1171
+ if (shouldFail) process.exit(1);
1172
+ }
1080
1173
  function checkCommand() {
1081
1174
  const check = new Command("check").description("Check Visor catalog \u2014 list items, test existence, scan JSX for native HTML");
1082
1175
  check.command("list").description("List all catalog items (components, blocks, hooks, patterns)").option("--type <type>", "filter by type: ui, blocks, hooks, patterns, all (default: all)").option("--json", "output structured JSON (for AI agents)").action((options) => {
@@ -1091,17 +1184,20 @@ function checkCommand() {
1091
1184
  check.command("design").description("Scan frontend code for Borealis design anti-patterns (deterministic, no LLM)").argument("<path>", "file path or directory to scan").option("--format <format>", "output format: json or human (default: human when TTY, json otherwise)").option("--errors-only", "report only error-severity rules (skip warnings)").option("--no-fail", "do not exit 1 on errors (advisory mode)").option("--json", "shorthand for --format json").action((pathArg, options) => {
1092
1185
  checkDesignCommand(pathArg, options);
1093
1186
  });
1187
+ check.command("theme-mode").description("Assert a theme's rendered app-root background matches its declared color-scheme (dark-only/light-only/adaptive)").argument("<path>", "path to a .visor.yaml theme file").option("--format <format>", "output format: json or human (default: human)").option("--no-fail", "do not exit 1 on a mode mismatch (advisory mode)").option("--json", "shorthand for --format json").action((pathArg, options) => {
1188
+ checkThemeModeCommand(pathArg, options);
1189
+ });
1094
1190
  return check;
1095
1191
  }
1096
1192
 
1097
1193
  // src/commands/init.ts
1098
- import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync, readFileSync as readFileSync6 } from "fs";
1194
+ import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync, readFileSync as readFileSync7 } from "fs";
1099
1195
  import { join as join6, dirname as dirname3 } from "path";
1100
1196
  import { fileURLToPath as fileURLToPath2 } from "url";
1101
1197
  import * as childProcess from "child_process";
1102
1198
 
1103
1199
  // src/config/config.ts
1104
- import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync2 } from "fs";
1200
+ import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync2 } from "fs";
1105
1201
  import { join as join4 } from "path";
1106
1202
 
1107
1203
  // src/config/defaults.ts
@@ -1131,7 +1227,7 @@ function loadConfig(cwd) {
1131
1227
  `No ${CONFIG_FILE} found. Run "visor init" first.`
1132
1228
  );
1133
1229
  }
1134
- const raw = readFileSync4(configPath, "utf-8");
1230
+ const raw = readFileSync5(configPath, "utf-8");
1135
1231
  const parsed = JSON.parse(raw);
1136
1232
  const knownKeys = /* @__PURE__ */ new Set(["paths"]);
1137
1233
  for (const key of Object.keys(parsed)) {
@@ -1168,12 +1264,12 @@ function writeConfig(cwd, config) {
1168
1264
 
1169
1265
  // src/utils/packages.ts
1170
1266
  import { execFileSync } from "child_process";
1171
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
1267
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
1172
1268
  import { join as join5 } from "path";
1173
1269
  function readPackageJson(cwd) {
1174
1270
  const pkgPath = join5(cwd, "package.json");
1175
1271
  if (!existsSync3(pkgPath)) return null;
1176
- return JSON.parse(readFileSync5(pkgPath, "utf-8"));
1272
+ return JSON.parse(readFileSync6(pkgPath, "utf-8"));
1177
1273
  }
1178
1274
  function isPackageInstalled(packageName, cwd) {
1179
1275
  const pkg2 = readPackageJson(cwd);
@@ -1198,6 +1294,7 @@ function installPackages(packages, cwd, dev = false) {
1198
1294
  }
1199
1295
 
1200
1296
  // src/commands/templates/nextjs.ts
1297
+ import { parse as parseYaml } from "yaml";
1201
1298
  var NEXTJS_PINNED_VERSION = "15.1.6";
1202
1299
  var CREATE_NEXT_APP_FLAGS = [
1203
1300
  "--ts",
@@ -1211,11 +1308,24 @@ var CREATE_NEXT_APP_FLAGS = [
1211
1308
  ];
1212
1309
  var NEXTJS_STARTER_YAML = `name: my-app
1213
1310
  version: 1
1311
+ # color-scheme controls the mode the scaffold applies at the root <html>:
1312
+ # adaptive \u2014 follow the OS via prefers-color-scheme (default)
1313
+ # dark-only \u2014 always dark (adds className="dark" + color-scheme: dark)
1314
+ # light-only \u2014 always light
1315
+ color-scheme: adaptive
1214
1316
  colors:
1215
1317
  primary: "#2563EB"
1216
1318
  `;
1217
- function generateNextjsLayout() {
1218
- return `import "./globals.css";
1319
+ function extractColorScheme(yamlContent) {
1320
+ const parsed = parseYaml(yamlContent);
1321
+ const v = parsed?.["color-scheme"];
1322
+ if (v === "dark-only" || v === "light-only" || v === "adaptive") return v;
1323
+ return void 0;
1324
+ }
1325
+ function generateNextjsLayout(colorScheme) {
1326
+ const forcedMode = colorScheme === "dark-only" ? "dark" : colorScheme === "light-only" ? "light" : void 0;
1327
+ if (forcedMode === void 0) {
1328
+ return `import "./globals.css";
1219
1329
  import { FOWT_SCRIPT } from "@loworbitstudio/visor-theme-engine/fowt";
1220
1330
  import type { Metadata } from "next";
1221
1331
  import type { ReactNode } from "react";
@@ -1235,11 +1345,41 @@ export default function RootLayout({ children }: { children: ReactNode }) {
1235
1345
  </html>
1236
1346
  );
1237
1347
  }
1348
+ `;
1349
+ }
1350
+ return `import "./globals.css";
1351
+ import { generateFowtScript } from "@loworbitstudio/visor-theme-engine/fowt";
1352
+ import type { Metadata } from "next";
1353
+ import type { ReactNode } from "react";
1354
+
1355
+ export const metadata: Metadata = {
1356
+ title: "My Visor App",
1357
+ description: "Built with Visor \u2014 Low Orbit Studio's design system.",
1358
+ };
1359
+
1360
+ // Theme declares \`color-scheme: ${colorScheme}\` \u2014 force ${forcedMode} at the root.
1361
+ const FOWT_SCRIPT = generateFowtScript({ defaultTheme: "${forcedMode}" });
1362
+
1363
+ export default function RootLayout({ children }: { children: ReactNode }) {
1364
+ return (
1365
+ <html
1366
+ lang="en"
1367
+ className="${forcedMode}"
1368
+ style={{ colorScheme: "${forcedMode}" }}
1369
+ suppressHydrationWarning
1370
+ >
1371
+ <head>
1372
+ <script>{FOWT_SCRIPT}</script>
1373
+ </head>
1374
+ <body>{children}</body>
1375
+ </html>
1376
+ );
1377
+ }
1238
1378
  `;
1239
1379
  }
1240
1380
 
1241
1381
  // src/commands/init.ts
1242
- import { generateThemeData } from "@loworbitstudio/visor-theme-engine";
1382
+ import { generateThemeData as generateThemeData2 } from "@loworbitstudio/visor-theme-engine";
1243
1383
  import { nextjsAdapter } from "@loworbitstudio/visor-theme-engine/adapters";
1244
1384
  function initCommand(cwd, options) {
1245
1385
  const json = options?.json ?? false;
@@ -1346,7 +1486,7 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
1346
1486
  logger.success("Created .visor.yaml");
1347
1487
  }
1348
1488
  }
1349
- const data = generateThemeData(NEXTJS_STARTER_YAML);
1489
+ const data = generateThemeData2(NEXTJS_STARTER_YAML);
1350
1490
  const css = nextjsAdapter({
1351
1491
  primitives: data.primitives,
1352
1492
  tokens: data.tokens,
@@ -1366,7 +1506,8 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
1366
1506
  logger.success("Created app/globals.css with theme tokens");
1367
1507
  }
1368
1508
  const layoutPath = join6(cwd, "app", "layout.tsx");
1369
- writeFileSync2(layoutPath, generateNextjsLayout(), "utf-8");
1509
+ const colorScheme = extractColorScheme(NEXTJS_STARTER_YAML);
1510
+ writeFileSync2(layoutPath, generateNextjsLayout(colorScheme), "utf-8");
1370
1511
  filesCreated.push("app/layout.tsx");
1371
1512
  if (!json) {
1372
1513
  logger.success("Wired app/layout.tsx with FOWT prevention and theme tokens");
@@ -1442,7 +1583,7 @@ function readVisorCliVersion() {
1442
1583
  const segments = new Array(i).fill("..");
1443
1584
  const candidate = join6(here, ...segments, "package.json");
1444
1585
  try {
1445
- const pkg2 = JSON.parse(readFileSync6(candidate, "utf-8"));
1586
+ const pkg2 = JSON.parse(readFileSync7(candidate, "utf-8"));
1446
1587
  if (pkg2.name === "@loworbitstudio/visor" && pkg2.version) {
1447
1588
  return pkg2.version;
1448
1589
  }
@@ -1457,7 +1598,7 @@ function readVisorCliVersion() {
1457
1598
  // src/utils/fs.ts
1458
1599
  import {
1459
1600
  writeFileSync as writeFileSync3,
1460
- readFileSync as readFileSync7,
1601
+ readFileSync as readFileSync8,
1461
1602
  existsSync as existsSync5,
1462
1603
  mkdirSync as mkdirSync2
1463
1604
  } from "fs";
@@ -1499,7 +1640,7 @@ function writeFile(filePath, content) {
1499
1640
  }
1500
1641
  function readFile(filePath) {
1501
1642
  if (!existsSync5(filePath)) return null;
1502
- return readFileSync7(filePath, "utf-8");
1643
+ return readFileSync8(filePath, "utf-8");
1503
1644
  }
1504
1645
  function fileExists(filePath) {
1505
1646
  return existsSync5(filePath);
@@ -1643,7 +1784,7 @@ function listCommand(cwd, options = {}) {
1643
1784
  }
1644
1785
 
1645
1786
  // src/utils/pubspec.ts
1646
- import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
1787
+ import { existsSync as existsSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
1647
1788
  import { join as join8 } from "path";
1648
1789
  import { parseDocument, YAMLMap } from "yaml";
1649
1790
  function mergePubspec(pubspecText, deps) {
@@ -1673,7 +1814,7 @@ function pubspecExists(cwd) {
1673
1814
  }
1674
1815
  function isPubPackageInstalled(packageName, cwd) {
1675
1816
  if (!pubspecExists(cwd)) return false;
1676
- const text = readFileSync8(pubspecPath(cwd), "utf-8");
1817
+ const text = readFileSync9(pubspecPath(cwd), "utf-8");
1677
1818
  const doc = parseDocument(text);
1678
1819
  const depsNode = doc.get("dependencies");
1679
1820
  if (!(depsNode instanceof YAMLMap)) return false;
@@ -1689,7 +1830,7 @@ function addPubDependencies(deps, cwd) {
1689
1830
  `No pubspec.yaml found at ${path2}. Run this command from a Flutter project root.`
1690
1831
  );
1691
1832
  }
1692
- const text = readFileSync8(path2, "utf-8");
1833
+ const text = readFileSync9(path2, "utf-8");
1693
1834
  const result = mergePubspec(text, deps);
1694
1835
  if (result.added.length > 0) {
1695
1836
  writeFileSync4(path2, result.text, "utf-8");
@@ -2398,9 +2539,9 @@ function infoCommand(name, cwd, options = {}) {
2398
2539
  }
2399
2540
 
2400
2541
  // src/commands/theme-apply.ts
2401
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
2402
- import { resolve as resolve4, dirname as dirname5, join as join10 } from "path";
2403
- import { generateTheme, generateThemeData as generateThemeData2 } from "@loworbitstudio/visor-theme-engine";
2542
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
2543
+ import { resolve as resolve5, dirname as dirname5, join as join10 } from "path";
2544
+ import { generateTheme, generateThemeData as generateThemeData3 } from "@loworbitstudio/visor-theme-engine";
2404
2545
  import {
2405
2546
  nextjsAdapter as nextjsAdapter2,
2406
2547
  fumadocsAdapter,
@@ -2429,10 +2570,10 @@ function defaultOutputPath(adapter, themeName) {
2429
2570
  }
2430
2571
  }
2431
2572
  function themeApplyCommand(file, cwd, options) {
2432
- const filePath = resolve4(cwd, file);
2573
+ const filePath = resolve5(cwd, file);
2433
2574
  let yamlContent;
2434
2575
  try {
2435
- yamlContent = readFileSync9(filePath, "utf-8");
2576
+ yamlContent = readFileSync10(filePath, "utf-8");
2436
2577
  } catch {
2437
2578
  if (options.json) {
2438
2579
  console.log(
@@ -2453,7 +2594,7 @@ function themeApplyCommand(file, cwd, options) {
2453
2594
  let sections;
2454
2595
  try {
2455
2596
  if (options.adapter) {
2456
- const data = generateThemeData2(yamlContent);
2597
+ const data = generateThemeData3(yamlContent);
2457
2598
  themeName = data.config.name;
2458
2599
  const adapterInput = {
2459
2600
  primitives: data.primitives,
@@ -2511,7 +2652,7 @@ function themeApplyCommand(file, cwd, options) {
2511
2652
  process.exit(1);
2512
2653
  }
2513
2654
  const outputTarget = options.output ?? defaultOutputPath(options.adapter, themeName);
2514
- const outputPath = resolve4(cwd, outputTarget);
2655
+ const outputPath = resolve5(cwd, outputTarget);
2515
2656
  if (fileMap) {
2516
2657
  try {
2517
2658
  mkdirSync3(outputPath, { recursive: true });
@@ -2600,8 +2741,8 @@ function formatSize(bytes) {
2600
2741
  }
2601
2742
 
2602
2743
  // src/commands/theme-export.ts
2603
- import { readFileSync as readFileSync10 } from "fs";
2604
- import { resolve as resolve5 } from "path";
2744
+ import { readFileSync as readFileSync11 } from "fs";
2745
+ import { resolve as resolve6 } from "path";
2605
2746
  import {
2606
2747
  parseConfig,
2607
2748
  resolveConfig,
@@ -2609,10 +2750,10 @@ import {
2609
2750
  exportTheme
2610
2751
  } from "@loworbitstudio/visor-theme-engine";
2611
2752
  function themeExportCommand(file, cwd, options) {
2612
- const filePath = resolve5(cwd, file ?? ".visor.yaml");
2753
+ const filePath = resolve6(cwd, file ?? ".visor.yaml");
2613
2754
  let yamlContent;
2614
2755
  try {
2615
- yamlContent = readFileSync10(filePath, "utf-8");
2756
+ yamlContent = readFileSync11(filePath, "utf-8");
2616
2757
  } catch {
2617
2758
  if (options.json) {
2618
2759
  console.log(
@@ -2664,16 +2805,16 @@ function themeExportCommand(file, cwd, options) {
2664
2805
  }
2665
2806
 
2666
2807
  // src/commands/theme-validate.ts
2667
- import { readFileSync as readFileSync11 } from "fs";
2668
- import { resolve as resolve6 } from "path";
2669
- import { parse as parseYaml } from "yaml";
2808
+ import { readFileSync as readFileSync12 } from "fs";
2809
+ import { resolve as resolve7 } from "path";
2810
+ import { parse as parseYaml2 } from "yaml";
2670
2811
  import { validate } from "@loworbitstudio/visor-theme-engine";
2671
2812
  import pc3 from "picocolors";
2672
2813
  function themeValidateCommand(file, cwd, options) {
2673
- const filePath = resolve6(cwd, file);
2814
+ const filePath = resolve7(cwd, file);
2674
2815
  let fileContent;
2675
2816
  try {
2676
- fileContent = readFileSync11(filePath, "utf-8");
2817
+ fileContent = readFileSync12(filePath, "utf-8");
2677
2818
  } catch {
2678
2819
  if (options.json) {
2679
2820
  console.log(
@@ -2697,7 +2838,7 @@ function themeValidateCommand(file, cwd, options) {
2697
2838
  }
2698
2839
  let parsed;
2699
2840
  try {
2700
- parsed = parseYaml(fileContent);
2841
+ parsed = parseYaml2(fileContent);
2701
2842
  } catch (err) {
2702
2843
  const message = err instanceof Error ? err.message : "Invalid YAML syntax";
2703
2844
  if (options.json) {
@@ -2763,7 +2904,7 @@ function printIssue(issue) {
2763
2904
  // src/commands/theme-verify.ts
2764
2905
  import { spawnSync as _spawnSync } from "child_process";
2765
2906
  import { existsSync as existsSync8 } from "fs";
2766
- import { resolve as resolve7 } from "path";
2907
+ import { resolve as resolve8 } from "path";
2767
2908
  function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
2768
2909
  const target = options.target ?? "flutter";
2769
2910
  if (target !== "flutter") {
@@ -2785,7 +2926,7 @@ function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
2785
2926
  }
2786
2927
  process.exit(1);
2787
2928
  }
2788
- const dirPath = resolve7(cwd, dir);
2929
+ const dirPath = resolve8(cwd, dir);
2789
2930
  if (!existsSync8(dirPath)) {
2790
2931
  if (options.json) {
2791
2932
  console.log(
@@ -2873,8 +3014,8 @@ function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
2873
3014
  }
2874
3015
 
2875
3016
  // src/commands/theme-extract.ts
2876
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync9, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
2877
- import { resolve as resolve8, join as join11, basename as basename2, extname as extname3, relative } from "path";
3017
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync6, existsSync as existsSync9, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
3018
+ import { resolve as resolve9, join as join11, basename as basename2, extname as extname3, relative } from "path";
2878
3019
  import { stringify as stringifyYaml } from "yaml";
2879
3020
  import {
2880
3021
  extractFromCSS,
@@ -2903,7 +3044,7 @@ var CSS_DIRS = [
2903
3044
  "packages/design-tokens"
2904
3045
  ];
2905
3046
  function themeExtractCommand(cwd, options) {
2906
- const targetDir = resolve8(cwd, options.from ?? ".");
3047
+ const targetDir = resolve9(cwd, options.from ?? ".");
2907
3048
  if (!existsSync9(targetDir)) {
2908
3049
  if (options.json) {
2909
3050
  console.log(JSON.stringify({ success: false, error: `Directory not found: ${targetDir}` }));
@@ -2975,11 +3116,11 @@ function collectCSSFiles(targetDir) {
2975
3116
  return files;
2976
3117
  }
2977
3118
  function addFileIfExists(filePath, files, seen) {
2978
- const resolved = resolve8(filePath);
3119
+ const resolved = resolve9(filePath);
2979
3120
  if (seen.has(resolved)) return;
2980
3121
  if (!existsSync9(resolved)) return;
2981
3122
  try {
2982
- const content = readFileSync12(resolved, "utf-8");
3123
+ const content = readFileSync13(resolved, "utf-8");
2983
3124
  if (content.includes("--")) {
2984
3125
  files.push({ path: resolved, content });
2985
3126
  seen.add(resolved);
@@ -3095,7 +3236,7 @@ function parseNextFontFromLayouts(targetDir) {
3095
3236
  const fullPath = join11(targetDir, relPath);
3096
3237
  if (!existsSync9(fullPath)) continue;
3097
3238
  try {
3098
- const content = readFileSync12(fullPath, "utf-8");
3239
+ const content = readFileSync13(fullPath, "utf-8");
3099
3240
  parseNextFontDeclarations(content, fontMap);
3100
3241
  } catch {
3101
3242
  }
@@ -3183,7 +3324,7 @@ function extractFontHints(targetDir) {
3183
3324
  const pkgPath = join11(targetDir, "package.json");
3184
3325
  if (!existsSync9(pkgPath)) return void 0;
3185
3326
  try {
3186
- const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3327
+ const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
3187
3328
  const allDeps = { ...pkg2.dependencies, ...pkg2.devDependencies };
3188
3329
  const fonts2 = [];
3189
3330
  for (const [dep, _] of Object.entries(allDeps)) {
@@ -3222,7 +3363,7 @@ function inferThemeName(targetDir) {
3222
3363
  const pkgPath = join11(targetDir, "package.json");
3223
3364
  if (existsSync9(pkgPath)) {
3224
3365
  try {
3225
- const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3366
+ const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
3226
3367
  if (pkg2.name) {
3227
3368
  const name = pkg2.name.replace(/^@[\w-]+\//, "");
3228
3369
  return `${name}-theme`;
@@ -3259,7 +3400,7 @@ function outputJSON(result, validationResult) {
3259
3400
  }
3260
3401
  function outputYAML(result, outputPath, cwd, validationResult) {
3261
3402
  const yamlStr = buildAnnotatedYAML(result);
3262
- const outFile = resolve8(cwd, outputPath ?? ".visor.yaml");
3403
+ const outFile = resolve9(cwd, outputPath ?? ".visor.yaml");
3263
3404
  const high = result.tokens.filter((t) => t.confidence === "high").length;
3264
3405
  const med = result.tokens.filter((t) => t.confidence === "medium").length;
3265
3406
  const low = result.tokens.filter((t) => t.confidence === "low").length;
@@ -3345,14 +3486,14 @@ function buildAnnotatedYAML(result) {
3345
3486
  }
3346
3487
 
3347
3488
  // src/commands/theme-register.ts
3348
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync7, mkdirSync as mkdirSync4, existsSync as existsSync11 } from "fs";
3349
- import { resolve as resolve10, join as join13 } from "path";
3350
- import { generateThemeData as generateThemeData3 } from "@loworbitstudio/visor-theme-engine";
3489
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync7, mkdirSync as mkdirSync4, existsSync as existsSync11 } from "fs";
3490
+ import { resolve as resolve11, join as join13 } from "path";
3491
+ import { generateThemeData as generateThemeData4 } from "@loworbitstudio/visor-theme-engine";
3351
3492
  import { docsAdapter as docsAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
3352
3493
 
3353
3494
  // src/utils/theme-helpers.ts
3354
- import { existsSync as existsSync10, lstatSync, readdirSync as readdirSync5, readFileSync as readFileSync13, readlinkSync, realpathSync, statSync as statSync6 } from "fs";
3355
- import { resolve as resolve9, dirname as dirname6, join as join12 } from "path";
3495
+ import { existsSync as existsSync10, lstatSync, readdirSync as readdirSync5, readFileSync as readFileSync14, readlinkSync, realpathSync, statSync as statSync6 } from "fs";
3496
+ import { resolve as resolve10, dirname as dirname6, join as join12 } from "path";
3356
3497
  import { execFileSync as execFileSync3 } from "child_process";
3357
3498
  function toSlug(name) {
3358
3499
  return name.toLowerCase().replace(/\s+/g, "-");
@@ -3361,7 +3502,7 @@ function toLabel(name) {
3361
3502
  return name.split(/[\s-]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
3362
3503
  }
3363
3504
  function findRepoRoot(startDir) {
3364
- let current = resolve9(startDir);
3505
+ let current = resolve10(startDir);
3365
3506
  while (true) {
3366
3507
  if (existsSync10(join12(current, "packages", "docs"))) {
3367
3508
  return current;
@@ -3380,7 +3521,7 @@ function findMainRepoRoot(startDir) {
3380
3521
  stdio: ["ignore", "pipe", "ignore"]
3381
3522
  }).trim();
3382
3523
  if (commonDir) {
3383
- const absoluteCommonDir = resolve9(startDir, commonDir);
3524
+ const absoluteCommonDir = resolve10(startDir, commonDir);
3384
3525
  const candidate = dirname6(absoluteCommonDir);
3385
3526
  if (existsSync10(join12(candidate, "packages", "docs"))) {
3386
3527
  return candidate;
@@ -3449,12 +3590,12 @@ function scanNestedThemeDir(dir) {
3449
3590
  return out;
3450
3591
  }
3451
3592
  function detectVisorWorkspace(cwd) {
3452
- let current = resolve9(cwd);
3593
+ let current = resolve10(cwd);
3453
3594
  while (true) {
3454
3595
  const pkgPath = join12(current, "package.json");
3455
3596
  if (existsSync10(pkgPath)) {
3456
3597
  try {
3457
- const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
3598
+ const pkg2 = JSON.parse(readFileSync14(pkgPath, "utf-8"));
3458
3599
  const workspaces = pkg2.workspaces ?? [];
3459
3600
  const hasCli = workspaces.some((w) => w.includes("packages/cli"));
3460
3601
  const hasEngine = workspaces.some((w) => w.includes("packages/theme-engine"));
@@ -3478,12 +3619,12 @@ function isLocalVisorBinary(workspaceRoot, scriptPath) {
3478
3619
  try {
3479
3620
  resolvedScript = realpathSync(scriptPath);
3480
3621
  } catch {
3481
- resolvedScript = resolve9(scriptPath);
3622
+ resolvedScript = resolve10(scriptPath);
3482
3623
  }
3483
3624
  try {
3484
3625
  resolvedPrefix = realpathSync(expectedPrefix);
3485
3626
  } catch {
3486
- resolvedPrefix = resolve9(expectedPrefix);
3627
+ resolvedPrefix = resolve10(expectedPrefix);
3487
3628
  }
3488
3629
  return resolvedScript.startsWith(resolvedPrefix + "/") || resolvedScript === resolvedPrefix;
3489
3630
  }
@@ -3573,10 +3714,10 @@ ${indent}${newEntry},
3573
3714
  return { updated, changed: true };
3574
3715
  }
3575
3716
  function themeRegisterCommand(file, cwd, options) {
3576
- const filePath = resolve10(cwd, file);
3717
+ const filePath = resolve11(cwd, file);
3577
3718
  let yamlContent;
3578
3719
  try {
3579
- yamlContent = readFileSync14(filePath, "utf-8");
3720
+ yamlContent = readFileSync15(filePath, "utf-8");
3580
3721
  } catch {
3581
3722
  if (options.json) {
3582
3723
  console.log(JSON.stringify({ success: false, error: `Could not read file: ${filePath}` }));
@@ -3588,7 +3729,7 @@ function themeRegisterCommand(file, cwd, options) {
3588
3729
  }
3589
3730
  let data;
3590
3731
  try {
3591
- data = generateThemeData3(yamlContent);
3732
+ data = generateThemeData4(yamlContent);
3592
3733
  } catch (err) {
3593
3734
  const message = err instanceof Error ? err.message : "Unknown error parsing theme";
3594
3735
  if (options.json) {
@@ -3636,8 +3777,8 @@ function themeRegisterCommand(file, cwd, options) {
3636
3777
  let globalsContent = "";
3637
3778
  let themeConfigContent = "";
3638
3779
  try {
3639
- globalsContent = readFileSync14(globalsPath, "utf-8");
3640
- themeConfigContent = readFileSync14(themeConfigPath, "utf-8");
3780
+ globalsContent = readFileSync15(globalsPath, "utf-8");
3781
+ themeConfigContent = readFileSync15(themeConfigPath, "utf-8");
3641
3782
  } catch (err) {
3642
3783
  const msg = err instanceof Error ? err.message : "Could not read docs files";
3643
3784
  if (options.json) {
@@ -3649,7 +3790,7 @@ function themeRegisterCommand(file, cwd, options) {
3649
3790
  return;
3650
3791
  }
3651
3792
  const cssExists = existsSync11(cssFilePath);
3652
- const cssChanged = !cssExists || readFileSync14(cssFilePath, "utf-8") !== css;
3793
+ const cssChanged = !cssExists || readFileSync15(cssFilePath, "utf-8") !== css;
3653
3794
  const { updated: newGlobals, changed: globalsChanged } = insertGlobalsImport(globalsContent, slug2);
3654
3795
  const { updated: newThemeConfig, changed: themeConfigChanged, error: configError } = insertThemeConfig(
3655
3796
  themeConfigContent,
@@ -3733,7 +3874,7 @@ function themeRegisterCommand(file, cwd, options) {
3733
3874
  }
3734
3875
 
3735
3876
  // src/commands/theme-unregister.ts
3736
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync8, existsSync as existsSync12, unlinkSync } from "fs";
3877
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync8, existsSync as existsSync12, unlinkSync } from "fs";
3737
3878
  import { join as join14 } from "path";
3738
3879
  function removeGlobalsImport(content, slug2) {
3739
3880
  const importLine = `@import './${slug2}-theme.css';`;
@@ -3783,8 +3924,8 @@ function themeUnregisterCommand(slug2, cwd, options) {
3783
3924
  let globalsContent = "";
3784
3925
  let themeConfigContent = "";
3785
3926
  try {
3786
- globalsContent = readFileSync15(globalsPath, "utf-8");
3787
- themeConfigContent = readFileSync15(themeConfigPath, "utf-8");
3927
+ globalsContent = readFileSync16(globalsPath, "utf-8");
3928
+ themeConfigContent = readFileSync16(themeConfigPath, "utf-8");
3788
3929
  } catch (err) {
3789
3930
  const msg = err instanceof Error ? err.message : "Could not read docs files";
3790
3931
  if (options.json) {
@@ -3836,7 +3977,7 @@ function themeUnregisterCommand(slug2, cwd, options) {
3836
3977
 
3837
3978
  // src/commands/theme-sync.ts
3838
3979
  import {
3839
- readFileSync as readFileSync16,
3980
+ readFileSync as readFileSync17,
3840
3981
  writeFileSync as writeFileSync9,
3841
3982
  mkdirSync as mkdirSync5,
3842
3983
  existsSync as existsSync13,
@@ -3844,9 +3985,9 @@ import {
3844
3985
  unlinkSync as unlinkSync2,
3845
3986
  copyFileSync
3846
3987
  } from "fs";
3847
- import { join as join15, basename as basename3, resolve as resolve11, sep, relative as relative2 } from "path";
3848
- import { parse as parseYaml2 } from "yaml";
3849
- import { generateThemeData as generateThemeData4, validateFontCoverage, formatFontCoverageError } from "@loworbitstudio/visor-theme-engine";
3988
+ import { join as join15, basename as basename3, resolve as resolve12, sep, relative as relative2 } from "path";
3989
+ import { parse as parseYaml3 } from "yaml";
3990
+ import { generateThemeData as generateThemeData5, validateFontCoverage, formatFontCoverageError } from "@loworbitstudio/visor-theme-engine";
3850
3991
  import { docsAdapter as docsAdapter3 } from "@loworbitstudio/visor-theme-engine/adapters";
3851
3992
  var PRIVATE_THEMES_REPO_URL = "git@github.com:low-orbit-studio/visor-themes-private.git";
3852
3993
  var PRIVATE_THEMES_ENV_VAR = "VISOR_THEMES_PRIVATE_PATH";
@@ -3884,7 +4025,7 @@ function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
3884
4025
  };
3885
4026
  const envPath = process.env[PRIVATE_THEMES_ENV_VAR];
3886
4027
  if (envPath && envPath.trim() !== "") {
3887
- const resolved = resolve11(envPath);
4028
+ const resolved = resolve12(envPath);
3888
4029
  if (!existsSync13(resolved)) {
3889
4030
  throw new Error(
3890
4031
  `${PRIVATE_THEMES_ENV_VAR} is set to "${envPath}" but the path does not exist. Expected a directory containing {slug}/theme.visor.yaml entries.`
@@ -4008,17 +4149,17 @@ function enforceWorkspaceGuard(cwd) {
4008
4149
  ].join("\n");
4009
4150
  }
4010
4151
  function extractGroup(yamlContent) {
4011
- const parsed = parseYaml2(yamlContent);
4152
+ const parsed = parseYaml3(yamlContent);
4012
4153
  if (typeof parsed?.group === "string") return parsed.group;
4013
4154
  return void 0;
4014
4155
  }
4015
4156
  function extractLabel(yamlContent) {
4016
- const parsed = parseYaml2(yamlContent);
4157
+ const parsed = parseYaml3(yamlContent);
4017
4158
  if (typeof parsed?.label === "string") return parsed.label;
4018
4159
  return void 0;
4019
4160
  }
4020
4161
  function extractDefaultMode(yamlContent) {
4021
- const parsed = parseYaml2(yamlContent);
4162
+ const parsed = parseYaml3(yamlContent);
4022
4163
  const v = parsed?.["default-mode"];
4023
4164
  if (v === "dark" || v === "light") return v;
4024
4165
  return void 0;
@@ -4251,14 +4392,14 @@ function themeSyncCommand(cwd, options) {
4251
4392
  const processFile = (filePath, isCustom, slugOverride) => {
4252
4393
  let yamlContent;
4253
4394
  try {
4254
- yamlContent = readFileSync16(filePath, "utf-8");
4395
+ yamlContent = readFileSync17(filePath, "utf-8");
4255
4396
  } catch {
4256
4397
  failures.push({ filePath, error: `Could not read: ${filePath}` });
4257
4398
  return;
4258
4399
  }
4259
4400
  let data;
4260
4401
  try {
4261
- data = generateThemeData4(yamlContent);
4402
+ data = generateThemeData5(yamlContent);
4262
4403
  } catch (err) {
4263
4404
  failures.push({
4264
4405
  filePath,
@@ -4301,8 +4442,8 @@ function themeSyncCommand(cwd, options) {
4301
4442
  let globalsContent;
4302
4443
  let themeConfigContent;
4303
4444
  try {
4304
- globalsContent = readFileSync16(globalsPath, "utf-8");
4305
- themeConfigContent = readFileSync16(themeConfigPath, "utf-8");
4445
+ globalsContent = readFileSync17(globalsPath, "utf-8");
4446
+ themeConfigContent = readFileSync17(themeConfigPath, "utf-8");
4306
4447
  } catch (err) {
4307
4448
  const msg = err instanceof Error ? err.message : "Could not read docs files";
4308
4449
  if (options.json) {
@@ -4436,7 +4577,7 @@ function themeSyncCommand(cwd, options) {
4436
4577
 
4437
4578
  // src/commands/theme-batch-apply-flutter.ts
4438
4579
  import {
4439
- readFileSync as readFileSync17,
4580
+ readFileSync as readFileSync18,
4440
4581
  writeFileSync as writeFileSync10,
4441
4582
  mkdirSync as mkdirSync6,
4442
4583
  existsSync as existsSync14,
@@ -4444,7 +4585,7 @@ import {
4444
4585
  rmSync
4445
4586
  } from "fs";
4446
4587
  import { join as join16, basename as basename4, dirname as dirname7 } from "path";
4447
- import { generateThemeData as generateThemeData5 } from "@loworbitstudio/visor-theme-engine";
4588
+ import { generateThemeData as generateThemeData6 } from "@loworbitstudio/visor-theme-engine";
4448
4589
  import { flutterAdapter as flutterAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
4449
4590
  function scanThemeDir2(dir) {
4450
4591
  if (!existsSync14(dir)) return [];
@@ -4604,14 +4745,14 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4604
4745
  for (const filePath of allFiles) {
4605
4746
  let yamlContent;
4606
4747
  try {
4607
- yamlContent = readFileSync17(filePath, "utf-8");
4748
+ yamlContent = readFileSync18(filePath, "utf-8");
4608
4749
  } catch {
4609
4750
  errors.push(`Could not read: ${filePath}`);
4610
4751
  continue;
4611
4752
  }
4612
4753
  let data;
4613
4754
  try {
4614
- data = generateThemeData5(yamlContent);
4755
+ data = generateThemeData6(yamlContent);
4615
4756
  } catch (err) {
4616
4757
  errors.push(
4617
4758
  `Failed to parse ${basename4(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`
@@ -4727,8 +4868,8 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4727
4868
  }
4728
4869
 
4729
4870
  // src/commands/fonts-add.ts
4730
- import { existsSync as existsSync15, statSync as statSync7, readdirSync as readdirSync8, readFileSync as readFileSync18 } from "fs";
4731
- import { resolve as resolve12, basename as basename5, extname as extname4 } from "path";
4871
+ import { existsSync as existsSync15, statSync as statSync7, readdirSync as readdirSync8, readFileSync as readFileSync19 } from "fs";
4872
+ import { resolve as resolve13, basename as basename5, extname as extname4 } from "path";
4732
4873
  import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
4733
4874
  function deriveFamilySlug(filename) {
4734
4875
  const name = basename5(filename, extname4(filename));
@@ -4772,7 +4913,7 @@ function deriveFamilySlug(filename) {
4772
4913
  return parts.join("-").toLowerCase();
4773
4914
  }
4774
4915
  function collectWoff2Files(inputPath) {
4775
- const resolved = resolve12(inputPath);
4916
+ const resolved = resolve13(inputPath);
4776
4917
  if (!existsSync15(resolved)) {
4777
4918
  throw new Error(`Path not found: ${resolved}`);
4778
4919
  }
@@ -4786,7 +4927,7 @@ function collectWoff2Files(inputPath) {
4786
4927
  return [resolved];
4787
4928
  }
4788
4929
  if (stat.isDirectory()) {
4789
- const files = readdirSync8(resolved).filter((f) => extname4(f).toLowerCase() === ".woff2").map((f) => resolve12(resolved, f));
4930
+ const files = readdirSync8(resolved).filter((f) => extname4(f).toLowerCase() === ".woff2").map((f) => resolve13(resolved, f));
4790
4931
  if (files.length === 0) {
4791
4932
  throw new Error(
4792
4933
  `No .woff2 files found in directory: ${resolved}`
@@ -4801,7 +4942,7 @@ function collectWoff2Files(inputPath) {
4801
4942
  throw new Error(`Path is neither a file nor a directory: ${resolved}`);
4802
4943
  }
4803
4944
  function getNonWoff2Fonts(inputPath) {
4804
- const resolved = resolve12(inputPath);
4945
+ const resolved = resolve13(inputPath);
4805
4946
  if (!existsSync15(resolved) || !statSync7(resolved).isDirectory()) {
4806
4947
  return [];
4807
4948
  }
@@ -4839,7 +4980,7 @@ function createR2Client(config) {
4839
4980
  });
4840
4981
  }
4841
4982
  async function uploadFile(client, bucket, key, filePath) {
4842
- const body = readFileSync18(filePath);
4983
+ const body = readFileSync19(filePath);
4843
4984
  await client.send(
4844
4985
  new PutObjectCommand({
4845
4986
  Bucket: bucket,
@@ -4855,7 +4996,7 @@ async function fontsAddCommand(inputPath, options) {
4855
4996
  const r2Config = getR2Config();
4856
4997
  const files = collectWoff2Files(inputPath);
4857
4998
  const familySlug = options.family ?? deriveFamilySlug(basename5(files[0]));
4858
- const resolved = resolve12(inputPath);
4999
+ const resolved = resolve13(inputPath);
4859
5000
  const nonWoff2 = statSync7(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
4860
5001
  if (!json) {
4861
5002
  logger.heading("Visor Font Upload");
@@ -5143,7 +5284,7 @@ function findCssFiles(dir, maxDepth = 3) {
5143
5284
  }
5144
5285
 
5145
5286
  // src/utils/patterns.ts
5146
- import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync20 } from "fs";
5287
+ import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync21 } from "fs";
5147
5288
  import { join as join18 } from "path";
5148
5289
  import { parse as parseYAML } from "yaml";
5149
5290
  function loadPatternsFromYaml(repoRoot) {
@@ -5153,7 +5294,7 @@ function loadPatternsFromYaml(repoRoot) {
5153
5294
  (f) => f.endsWith(".visor-pattern.yaml")
5154
5295
  );
5155
5296
  return files.map((file) => {
5156
- const content = readFileSync20(join18(patternsDir, file), "utf-8");
5297
+ const content = readFileSync21(join18(patternsDir, file), "utf-8");
5157
5298
  return parseYAML(content);
5158
5299
  }).filter(Boolean);
5159
5300
  }
@@ -5542,9 +5683,9 @@ Visor Tokens (${categoryLabel}) \u2014 ${tokens2.length} tokens
5542
5683
  }
5543
5684
 
5544
5685
  // src/commands/migrate-token-substitution.ts
5545
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync11, readdirSync as readdirSync11, statSync as statSync8, existsSync as existsSync18 } from "fs";
5546
- import { resolve as resolve13, join as join19, relative as relative4 } from "path";
5547
- import { parse as parseYaml3 } from "yaml";
5686
+ import { readFileSync as readFileSync22, writeFileSync as writeFileSync11, readdirSync as readdirSync11, statSync as statSync8, existsSync as existsSync18 } from "fs";
5687
+ import { resolve as resolve14, join as join19, relative as relative4 } from "path";
5688
+ import { parse as parseYaml4 } from "yaml";
5548
5689
  import pc4 from "picocolors";
5549
5690
  var V7_ENTR_SUBSTITUTION_MAP = {
5550
5691
  "--panel": "--surface-card",
@@ -5573,8 +5714,8 @@ function readMapFromThemeFile(themeId, cwd) {
5573
5714
  for (const candidate of candidates) {
5574
5715
  if (existsSync18(candidate)) {
5575
5716
  try {
5576
- const raw = readFileSync21(candidate, "utf-8");
5577
- const parsed = parseYaml3(raw);
5717
+ const raw = readFileSync22(candidate, "utf-8");
5718
+ const parsed = parseYaml4(raw);
5578
5719
  if (parsed?.migrate?.["token-substitution"]) {
5579
5720
  return parsed.migrate["token-substitution"];
5580
5721
  }
@@ -5657,7 +5798,7 @@ function runSubstitutionPass(targetPath, map, themeId) {
5657
5798
  const fileResults = [];
5658
5799
  let totalSubstitutions = 0;
5659
5800
  for (const file of files) {
5660
- const content = readFileSync21(file, "utf-8");
5801
+ const content = readFileSync22(file, "utf-8");
5661
5802
  const { newContent, substitutions } = applySubstitutionsToContent(content, map);
5662
5803
  if (substitutions.length > 0) {
5663
5804
  fileResults.push({ file, substitutions, originalContent: content, newContent });
@@ -5691,7 +5832,7 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
5691
5832
  process.exit(1);
5692
5833
  return;
5693
5834
  }
5694
- const targetPath = resolve13(cwd, targetArg ?? ".");
5835
+ const targetPath = resolve14(cwd, targetArg ?? ".");
5695
5836
  try {
5696
5837
  statSync8(targetPath);
5697
5838
  } catch {
@@ -5780,16 +5921,16 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
5780
5921
  }
5781
5922
 
5782
5923
  // src/commands/sandbox/init.ts
5783
- import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync24, rmSync as rmSync2 } from "fs";
5784
- import { isAbsolute as isAbsolute2, join as join22, resolve as resolve15, dirname as dirname9 } from "path";
5924
+ import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync25, rmSync as rmSync2 } from "fs";
5925
+ import { isAbsolute as isAbsolute2, join as join22, resolve as resolve16, dirname as dirname9 } from "path";
5785
5926
  import { fileURLToPath as fileURLToPath3 } from "url";
5786
5927
  import * as childProcess2 from "child_process";
5787
5928
 
5788
5929
  // src/commands/sandbox/parse-handoff.ts
5789
- import { readFileSync as readFileSync22, existsSync as existsSync19 } from "fs";
5790
- import { dirname as dirname8, resolve as resolve14, isAbsolute } from "path";
5930
+ import { readFileSync as readFileSync23, existsSync as existsSync19 } from "fs";
5931
+ import { dirname as dirname8, resolve as resolve15, isAbsolute } from "path";
5791
5932
  function parseHandoff(handoffPath) {
5792
- const text = readFileSync22(handoffPath, "utf-8");
5933
+ const text = readFileSync23(handoffPath, "utf-8");
5793
5934
  const lines2 = text.split("\n");
5794
5935
  const warnings = [];
5795
5936
  const pattern2 = extractPatternSlug(lines2, warnings);
@@ -5828,7 +5969,7 @@ function extractRecipePath(text, handoffPath, warnings) {
5828
5969
  }
5829
5970
  const href = m[2].trim();
5830
5971
  if (isAbsolute(href)) return href;
5831
- return resolve14(dirname8(handoffPath), href);
5972
+ return resolve15(dirname8(handoffPath), href);
5832
5973
  }
5833
5974
  var STATUS_PATTERNS = [
5834
5975
  { re: /NEW\s+gap/i, status: "gap-new" },
@@ -5875,7 +6016,7 @@ function extractPrimitives(text, warnings) {
5875
6016
  return primitives;
5876
6017
  }
5877
6018
  function extractRecipeArtifacts(recipePath, warnings) {
5878
- const text = readFileSync22(recipePath, "utf-8");
6019
+ const text = readFileSync23(recipePath, "utf-8");
5879
6020
  const screens = extractScreens(text);
5880
6021
  const mockShapes = extractMockFields(text, warnings);
5881
6022
  return { screens, mockShapes };
@@ -6527,7 +6668,7 @@ function sandboxIsEmpty(sandboxDir) {
6527
6668
  }
6528
6669
 
6529
6670
  // src/commands/sandbox/html-prototype.ts
6530
- import { mkdirSync as mkdirSync8, readdirSync as readdirSync13, readFileSync as readFileSync23, statSync as statSync9, writeFileSync as writeFileSync13 } from "fs";
6671
+ import { mkdirSync as mkdirSync8, readdirSync as readdirSync13, readFileSync as readFileSync24, statSync as statSync9, writeFileSync as writeFileSync13 } from "fs";
6531
6672
  import { join as join21 } from "path";
6532
6673
 
6533
6674
  // src/commands/sandbox/strip-chrome.ts
@@ -6777,11 +6918,11 @@ function copyTreeRelative(srcDir, destDir, relDir = "", stripChromeSelectors = [
6777
6918
  out.push(...copyTreeRelative(srcDir, destDir, join21(relDir, name), stripChromeSelectors));
6778
6919
  } else if (stat.isFile()) {
6779
6920
  if (stripChromeSelectors.length > 0 && name.toLowerCase().endsWith(".html")) {
6780
- const html = readFileSync23(srcPath, "utf-8");
6921
+ const html = readFileSync24(srcPath, "utf-8");
6781
6922
  const stripped = stripDocumentaryChrome(html, stripChromeSelectors);
6782
6923
  writeFileSync13(destPath, stripped);
6783
6924
  } else {
6784
- writeFileSync13(destPath, readFileSync23(srcPath));
6925
+ writeFileSync13(destPath, readFileSync24(srcPath));
6785
6926
  }
6786
6927
  out.push(join21("public", "prototype", relDir, name).replace(/\\/g, "/"));
6787
6928
  }
@@ -6835,7 +6976,7 @@ async function runInit(name, cwd, options) {
6835
6976
  if (!name || !/^[a-z0-9][a-z0-9-_]*$/i.test(name)) {
6836
6977
  throw new Error(`Invalid sandbox name '${name}'. Use letters, digits, '-' or '_'.`);
6837
6978
  }
6838
- const handoffPath = isAbsolute2(options.handoff) ? options.handoff : resolve15(cwd, options.handoff);
6979
+ const handoffPath = isAbsolute2(options.handoff) ? options.handoff : resolve16(cwd, options.handoff);
6839
6980
  if (!existsSync21(handoffPath)) {
6840
6981
  throw new Error(`Handoff manifest not found: ${handoffPath}`);
6841
6982
  }
@@ -6858,7 +6999,7 @@ async function runInit(name, cwd, options) {
6858
6999
  const port = await findOpenPort();
6859
7000
  let prototypeImport;
6860
7001
  if (options.fromHtmlPrototype) {
6861
- const prototypeDir = isAbsolute2(options.fromHtmlPrototype) ? options.fromHtmlPrototype : resolve15(cwd, options.fromHtmlPrototype);
7002
+ const prototypeDir = isAbsolute2(options.fromHtmlPrototype) ? options.fromHtmlPrototype : resolve16(cwd, options.fromHtmlPrototype);
6862
7003
  if (!existsSync21(prototypeDir)) {
6863
7004
  throw new Error(`HTML prototype directory not found: ${prototypeDir}`);
6864
7005
  }
@@ -6918,9 +7059,9 @@ async function runInit(name, cwd, options) {
6918
7059
  function applyThemeIfPossible(sandboxDir, manifest, theme2, cwd, json, themeFile) {
6919
7060
  const candidates = [];
6920
7061
  if (themeFile) {
6921
- candidates.push(isAbsolute2(themeFile) ? themeFile : resolve15(cwd, themeFile));
7062
+ candidates.push(isAbsolute2(themeFile) ? themeFile : resolve16(cwd, themeFile));
6922
7063
  }
6923
- candidates.push(isAbsolute2(theme2) ? theme2 : resolve15(cwd, theme2));
7064
+ candidates.push(isAbsolute2(theme2) ? theme2 : resolve16(cwd, theme2));
6924
7065
  const privateRoot = process.env.VISOR_THEMES_PRIVATE_PATH;
6925
7066
  if (privateRoot && privateRoot.length > 0) {
6926
7067
  candidates.push(join22(privateRoot, "themes", theme2, "theme.visor.yaml"));
@@ -7004,7 +7145,7 @@ function readCliVersion() {
7004
7145
  const segments = new Array(i).fill("..");
7005
7146
  const candidate = join22(here, ...segments, "package.json");
7006
7147
  try {
7007
- const pkg2 = JSON.parse(readFileSync24(candidate, "utf-8"));
7148
+ const pkg2 = JSON.parse(readFileSync25(candidate, "utf-8"));
7008
7149
  if (pkg2.name === "@loworbitstudio/visor" && pkg2.version) return pkg2.version;
7009
7150
  } catch {
7010
7151
  }
@@ -7015,7 +7156,7 @@ function readCliVersion() {
7015
7156
  }
7016
7157
 
7017
7158
  // src/commands/sandbox/dev.ts
7018
- import { existsSync as existsSync22, readFileSync as readFileSync25 } from "fs";
7159
+ import { existsSync as existsSync22, readFileSync as readFileSync26 } from "fs";
7019
7160
  import { join as join23 } from "path";
7020
7161
  import * as childProcess3 from "child_process";
7021
7162
  function sandboxDevCommand(cwd, options) {
@@ -7031,7 +7172,7 @@ function sandboxDevCommand(cwd, options) {
7031
7172
  }
7032
7173
  let config;
7033
7174
  try {
7034
- config = JSON.parse(readFileSync25(configPath, "utf-8"));
7175
+ config = JSON.parse(readFileSync26(configPath, "utf-8"));
7035
7176
  } catch (err) {
7036
7177
  fail(json, `Invalid sandbox.json at ${configPath}: ${err.message}`);
7037
7178
  return;
@@ -7091,7 +7232,7 @@ import {
7091
7232
  copyFileSync as copyFileSync2,
7092
7233
  existsSync as existsSync23,
7093
7234
  mkdirSync as mkdirSync10,
7094
- readFileSync as readFileSync26,
7235
+ readFileSync as readFileSync27,
7095
7236
  readdirSync as readdirSync14,
7096
7237
  rmSync as rmSync3,
7097
7238
  writeFileSync as writeFileSync14
@@ -7116,7 +7257,7 @@ function sandboxApproveCommand(cwd, options) {
7116
7257
  runCapture(sandboxDir, configPath, options.name, json);
7117
7258
  }
7118
7259
  function runCapture(sandboxDir, configPath, sandboxName, json) {
7119
- const config = JSON.parse(readFileSync26(configPath, "utf-8"));
7260
+ const config = JSON.parse(readFileSync27(configPath, "utf-8"));
7120
7261
  const captureScriptPath = join24(sandboxDir, "playwright.capture.mjs");
7121
7262
  writeFileSync14(captureScriptPath, captureScriptTemplate(), "utf-8");
7122
7263
  ensurePlaywrightInstalled(sandboxDir, json);
@@ -7254,7 +7395,7 @@ function fail2(json, message) {
7254
7395
  // src/index.ts
7255
7396
  var __dirname2 = dirname10(fileURLToPath4(import.meta.url));
7256
7397
  var pkg = JSON.parse(
7257
- readFileSync27(join25(__dirname2, "..", "package.json"), "utf-8")
7398
+ readFileSync28(join25(__dirname2, "..", "package.json"), "utf-8")
7258
7399
  );
7259
7400
  var program = new Command2();
7260
7401
  program.name("visor").description("CLI for the Visor design system").version(pkg.version);
@@ -2687,7 +2687,7 @@
2687
2687
  {
2688
2688
  "path": "components/ui/page-header/page-header.tsx",
2689
2689
  "type": "registry:ui",
2690
- "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./page-header.module.css\"\n\nconst pageHeaderVariants = cva(styles.base, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n})\n\ntype PageHeaderElement = \"header\" | \"section\" | \"div\"\ntype TitleElement = \"h1\" | \"h2\" | \"h3\"\n\n/** Token preset values for the `titleSize` prop. */\nconst TITLE_SIZE_TOKENS = [\"default\", \"marquee\"] as const\ntype TitleSizeToken = (typeof TITLE_SIZE_TOKENS)[number]\n\n/** Token preset values for the `titleFamily` prop. */\nconst TITLE_FAMILY_TOKENS = [\"heading\", \"display\"] as const\ntype TitleFamilyToken = (typeof TITLE_FAMILY_TOKENS)[number]\n\nfunction isTitleSizeToken(value: string): value is TitleSizeToken {\n return (TITLE_SIZE_TOKENS as readonly string[]).includes(value)\n}\n\nfunction isTitleFamilyToken(value: string): value is TitleFamilyToken {\n return (TITLE_FAMILY_TOKENS as readonly string[]).includes(value)\n}\n\nexport interface PageHeaderProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"title\">,\n VariantProps<typeof pageHeaderVariants> {\n /** Optional small uppercase label rendered above the title. */\n eyebrow?: React.ReactNode\n /** Page heading content. Rendered in the element given by `titleAs`. */\n title: React.ReactNode\n /** Optional supporting copy rendered below the title. */\n description?: React.ReactNode\n /** Optional ReactNode rendered above the title row (typically a Breadcrumb). */\n breadcrumb?: React.ReactNode\n /**\n * Optional media/identity node rendered to the LEFT of the title block inside\n * the title row (typically an Avatar or identity plate). When present the row\n * forms an identity lockup and the slot top-aligns against the title/description\n * stack (VI-539). Omitting it leaves the row exactly as the default\n * text|actions layout, so existing call sites render unchanged.\n */\n leading?: React.ReactNode\n /** Optional ReactNode rendered on the right side of the title row. */\n actions?: React.ReactNode\n /** Root element tag. Defaults to `header`. */\n as?: PageHeaderElement\n /** Heading level for the title. Defaults to `h1`. */\n titleAs?: TitleElement\n /**\n * Title font-size override. Token presets (`\"default\" | \"marquee\"`) map to\n * `data-title-size` attributes; any other string is forwarded as a raw CSS\n * length on the `--page-header-title-size` custom property.\n */\n titleSize?: \"default\" | \"marquee\" | (string & {})\n /**\n * Title font-family override. Token presets (`\"heading\" | \"display\"`) map\n * to `data-title-family` attributes; any other string is forwarded as a\n * raw CSS family on the `--page-header-title-family` custom property.\n */\n titleFamily?: \"heading\" | \"display\" | (string & {})\n /**\n * Title line-height override. Forwarded as a raw CSS `line-height` value\n * on the `--page-header-title-leading` custom property (a unitless number\n * like `1.05` or any CSS length). When omitted, the title falls back to its\n * default line-height (tight for the standard title, `1` for the marquee\n * scale), so existing call sites render unchanged.\n */\n titleLeading?: string | number\n}\n\nconst PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(\n (\n {\n className,\n size,\n eyebrow,\n title,\n description,\n breadcrumb,\n leading,\n actions,\n as = \"header\",\n titleAs = \"h1\",\n titleSize,\n titleFamily,\n titleLeading,\n ...props\n },\n ref\n ) => {\n const Root = as as React.ElementType\n const Title = titleAs as React.ElementType\n\n const titleSizeToken =\n typeof titleSize === \"string\" && isTitleSizeToken(titleSize)\n ? titleSize\n : undefined\n const titleFamilyToken =\n typeof titleFamily === \"string\" && isTitleFamilyToken(titleFamily)\n ? titleFamily\n : undefined\n\n const rawTitleSize =\n typeof titleSize === \"string\" && !titleSizeToken ? titleSize : undefined\n const rawTitleFamily =\n typeof titleFamily === \"string\" && !titleFamilyToken\n ? titleFamily\n : undefined\n\n const hasTitleLeading = titleLeading !== undefined && titleLeading !== null\n\n const titleStyle: React.CSSProperties | undefined =\n rawTitleSize || rawTitleFamily || hasTitleLeading\n ? {\n ...(rawTitleSize\n ? ({\n \"--page-header-title-size\": rawTitleSize,\n } as React.CSSProperties)\n : null),\n ...(rawTitleFamily\n ? ({\n \"--page-header-title-family\": rawTitleFamily,\n } as React.CSSProperties)\n : null),\n ...(hasTitleLeading\n ? ({\n \"--page-header-title-leading\": String(titleLeading),\n } as React.CSSProperties)\n : null),\n }\n : undefined\n\n // When a raw string is supplied, switch the title into the \"marquee\" /\n // \"display\" rules that consume the custom property so the override\n // actually takes effect. Token values map directly to data-attributes.\n const resolvedTitleSizeAttr =\n titleSizeToken ?? (rawTitleSize ? \"marquee\" : undefined)\n const resolvedTitleFamilyAttr =\n titleFamilyToken ?? (rawTitleFamily ? \"display\" : undefined)\n\n return (\n <Root\n ref={ref}\n data-slot=\"page-header\"\n className={cn(pageHeaderVariants({ size }), className)}\n {...props}\n >\n {breadcrumb ? (\n <div data-slot=\"page-header-breadcrumb\" className={styles.breadcrumb}>\n {breadcrumb}\n </div>\n ) : null}\n <div\n data-slot=\"page-header-row\"\n data-has-leading={leading ? \"\" : undefined}\n className={styles.row}\n >\n {leading ? (\n <div data-slot=\"page-header-leading\" className={styles.leading}>\n {leading}\n </div>\n ) : null}\n <div data-slot=\"page-header-text\" className={styles.text}>\n {eyebrow ? (\n <div data-slot=\"page-header-eyebrow\" className={styles.eyebrow}>\n {eyebrow}\n </div>\n ) : null}\n <Title\n data-slot=\"page-header-title\"\n data-title-size={resolvedTitleSizeAttr}\n data-title-family={resolvedTitleFamilyAttr}\n className={styles.title}\n style={titleStyle}\n >\n {title}\n </Title>\n {description ? (\n <p\n data-slot=\"page-header-description\"\n className={styles.description}\n >\n {description}\n </p>\n ) : null}\n </div>\n {actions ? (\n <div data-slot=\"page-header-actions\" className={styles.actions}>\n {actions}\n </div>\n ) : null}\n </div>\n </Root>\n )\n }\n)\nPageHeader.displayName = \"PageHeader\"\n\nexport { PageHeader, pageHeaderVariants }\n"
2690
+ "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./page-header.module.css\"\n\nconst pageHeaderVariants = cva(styles.base, {\n variants: {\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n})\n\ntype PageHeaderElement = \"header\" | \"section\" | \"div\"\ntype TitleElement = \"h1\" | \"h2\" | \"h3\"\n\n/** Token preset values for the `titleSize` prop. */\nconst TITLE_SIZE_TOKENS = [\"default\", \"marquee\"] as const\ntype TitleSizeToken = (typeof TITLE_SIZE_TOKENS)[number]\n\n/** Token preset values for the `titleFamily` prop. */\nconst TITLE_FAMILY_TOKENS = [\"heading\", \"display\"] as const\ntype TitleFamilyToken = (typeof TITLE_FAMILY_TOKENS)[number]\n\nfunction isTitleSizeToken(value: string): value is TitleSizeToken {\n return (TITLE_SIZE_TOKENS as readonly string[]).includes(value)\n}\n\nfunction isTitleFamilyToken(value: string): value is TitleFamilyToken {\n return (TITLE_FAMILY_TOKENS as readonly string[]).includes(value)\n}\n\nexport interface PageHeaderProps\n extends Omit<React.HTMLAttributes<HTMLElement>, \"title\">,\n VariantProps<typeof pageHeaderVariants> {\n /** Optional small uppercase label rendered above the title. */\n eyebrow?: React.ReactNode\n /** Page heading content. Rendered in the element given by `titleAs`. */\n title: React.ReactNode\n /** Optional supporting copy rendered below the title. */\n description?: React.ReactNode\n /** Optional ReactNode rendered above the title row (typically a Breadcrumb). */\n breadcrumb?: React.ReactNode\n /**\n * Optional media/identity node rendered to the LEFT of the title block inside\n * the title row (typically an Avatar or identity plate). When present the row\n * forms an identity lockup and the slot vertically centers against the\n * title/description stack (the VI-539 top-align was superseded by the VI-545\n * admin-editorial reconcile). Omitting it leaves the row exactly as the default\n * text|actions layout, so existing call sites render unchanged.\n */\n leading?: React.ReactNode\n /** Optional ReactNode rendered on the right side of the title row. */\n actions?: React.ReactNode\n /** Root element tag. Defaults to `header`. */\n as?: PageHeaderElement\n /** Heading level for the title. Defaults to `h1`. */\n titleAs?: TitleElement\n /**\n * Title font-size override. Token presets (`\"default\" | \"marquee\"`) map to\n * `data-title-size` attributes; any other string is forwarded as a raw CSS\n * length on the `--page-header-title-size` custom property.\n */\n titleSize?: \"default\" | \"marquee\" | (string & {})\n /**\n * Title font-family override. Token presets (`\"heading\" | \"display\"`) map\n * to `data-title-family` attributes; any other string is forwarded as a\n * raw CSS family on the `--page-header-title-family` custom property.\n */\n titleFamily?: \"heading\" | \"display\" | (string & {})\n /**\n * Title line-height override. Forwarded as a raw CSS `line-height` value\n * on the `--page-header-title-leading` custom property (a unitless number\n * like `1.05` or any CSS length). When omitted, the title falls back to its\n * default line-height (tight for the standard title, `1` for the marquee\n * scale), so existing call sites render unchanged.\n */\n titleLeading?: string | number\n}\n\nconst PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(\n (\n {\n className,\n size,\n eyebrow,\n title,\n description,\n breadcrumb,\n leading,\n actions,\n as = \"header\",\n titleAs = \"h1\",\n titleSize,\n titleFamily,\n titleLeading,\n ...props\n },\n ref\n ) => {\n const Root = as as React.ElementType\n const Title = titleAs as React.ElementType\n\n const titleSizeToken =\n typeof titleSize === \"string\" && isTitleSizeToken(titleSize)\n ? titleSize\n : undefined\n const titleFamilyToken =\n typeof titleFamily === \"string\" && isTitleFamilyToken(titleFamily)\n ? titleFamily\n : undefined\n\n const rawTitleSize =\n typeof titleSize === \"string\" && !titleSizeToken ? titleSize : undefined\n const rawTitleFamily =\n typeof titleFamily === \"string\" && !titleFamilyToken\n ? titleFamily\n : undefined\n\n const hasTitleLeading = titleLeading !== undefined && titleLeading !== null\n\n const titleStyle: React.CSSProperties | undefined =\n rawTitleSize || rawTitleFamily || hasTitleLeading\n ? {\n ...(rawTitleSize\n ? ({\n \"--page-header-title-size\": rawTitleSize,\n } as React.CSSProperties)\n : null),\n ...(rawTitleFamily\n ? ({\n \"--page-header-title-family\": rawTitleFamily,\n } as React.CSSProperties)\n : null),\n ...(hasTitleLeading\n ? ({\n \"--page-header-title-leading\": String(titleLeading),\n } as React.CSSProperties)\n : null),\n }\n : undefined\n\n // When a raw string is supplied, switch the title into the \"marquee\" /\n // \"display\" rules that consume the custom property so the override\n // actually takes effect. Token values map directly to data-attributes.\n const resolvedTitleSizeAttr =\n titleSizeToken ?? (rawTitleSize ? \"marquee\" : undefined)\n const resolvedTitleFamilyAttr =\n titleFamilyToken ?? (rawTitleFamily ? \"display\" : undefined)\n\n return (\n <Root\n ref={ref}\n data-slot=\"page-header\"\n className={cn(pageHeaderVariants({ size }), className)}\n {...props}\n >\n {breadcrumb ? (\n <div data-slot=\"page-header-breadcrumb\" className={styles.breadcrumb}>\n {breadcrumb}\n </div>\n ) : null}\n <div\n data-slot=\"page-header-row\"\n data-has-leading={leading ? \"\" : undefined}\n className={styles.row}\n >\n {leading ? (\n <div data-slot=\"page-header-leading\" className={styles.leading}>\n {leading}\n </div>\n ) : null}\n <div data-slot=\"page-header-text\" className={styles.text}>\n {eyebrow ? (\n <div data-slot=\"page-header-eyebrow\" className={styles.eyebrow}>\n {eyebrow}\n </div>\n ) : null}\n <Title\n data-slot=\"page-header-title\"\n data-title-size={resolvedTitleSizeAttr}\n data-title-family={resolvedTitleFamilyAttr}\n className={styles.title}\n style={titleStyle}\n >\n {title}\n </Title>\n {description ? (\n <p\n data-slot=\"page-header-description\"\n className={styles.description}\n >\n {description}\n </p>\n ) : null}\n </div>\n {actions ? (\n <div data-slot=\"page-header-actions\" className={styles.actions}>\n {actions}\n </div>\n ) : null}\n </div>\n </Root>\n )\n }\n)\nPageHeader.displayName = \"PageHeader\"\n\nexport { PageHeader, pageHeaderVariants }\n"
2691
2691
  },
2692
2692
  {
2693
2693
  "path": "components/ui/page-header/page-header.module.css",
@@ -4831,7 +4831,7 @@
4831
4831
  {
4832
4832
  "path": "components/visual/hero-glow/hero-glow.module.css",
4833
4833
  "type": "registry:ui",
4834
- "content": "/* HeroGlow: breathing radial glow band for hero media\n *\n * Port of bl-hero-glow (blacklight-website globals-css, BL-326).\n *\n * Color contract:\n * Color is driven exclusively by --glow-color, which the consumer sets\n * inline or resets every rAF frame (ex. keyed to the active artist color).\n * No hex values are baked in. Fallback is transparent so nothing renders\n * when the var is unset — intentional: callers must supply a color.\n *\n * Layout:\n * position: absolute with negative inset so the glow bleeds outside the\n * parent's box. The parent must be position: relative. pointer-events: none\n * ensures the glow never captures clicks.\n *\n * Animation:\n * @keyframes hero-glow-breathe: opacity 0.75 ↔ 1, scale 1 ↔ 1.03, 7s cycle.\n * prefers-reduced-motion: animation is disabled — glow appears at rest state.\n */\n\n/* ── Keyframe ────────────────────────────────────────────────────────────── */\n\n@keyframes hero-glow-breathe {\n 0%,\n 100% {\n opacity: 0.75;\n transform: scale(1);\n }\n 50% {\n opacity: 1;\n transform: scale(1.03);\n }\n}\n\n/* ── Root ────────────────────────────────────────────────────────────────── */\n\n.root {\n position: absolute;\n inset: -6% -8% -10%;\n pointer-events: none;\n background: radial-gradient(\n 70% 60% at 50% 55%,\n color-mix(in srgb, var(--glow-color, transparent) 16%, transparent),\n transparent 72%\n );\n animation: hero-glow-breathe 7s ease-in-out infinite;\n}\n\n/* ── Reduced motion ─────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .root {\n animation: none;\n opacity: 0.75;\n transform: scale(1);\n }\n}\n"
4834
+ "content": "/* HeroGlow: breathing radial glow band for hero media\n *\n * Port of bl-hero-glow (blacklight-website globals-css, BL-326).\n *\n * Color contract:\n * Color is driven exclusively by --glow-color, which the consumer sets\n * inline or resets every rAF frame (ex. keyed to the active artist color).\n * No hex values are baked in. Fallback is transparent so nothing renders\n * when the var is unset — intentional: callers must supply a color.\n *\n * Layout:\n * position: absolute with negative inset so the glow bleeds outside the\n * parent's box. The parent must be position: relative. pointer-events: none\n * ensures the glow never captures clicks.\n *\n * Animation:\n * @keyframes hero-glow-breathe: opacity 0.75 ↔ 1, scale 1 ↔ 1.03, 7s cycle.\n * prefers-reduced-motion: animation is disabled — glow rests at full opacity (1),\n * faithful to the bl-hero-glow origin (not the keyframe's 0.75 rest value).\n */\n\n/* ── Keyframe ────────────────────────────────────────────────────────────── */\n\n@keyframes hero-glow-breathe {\n 0%,\n 100% {\n opacity: 0.75;\n transform: scale(1);\n }\n 50% {\n opacity: 1;\n transform: scale(1.03);\n }\n}\n\n/* ── Root ────────────────────────────────────────────────────────────────── */\n\n.root {\n position: absolute;\n inset: -6% -8% -10%;\n pointer-events: none;\n background: radial-gradient(\n 70% 60% at 50% 55%,\n color-mix(in srgb, var(--glow-color, transparent) 16%, transparent),\n transparent 72%\n );\n animation: hero-glow-breathe 7s ease-in-out infinite;\n}\n\n/* ── Reduced motion ─────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .root {\n animation: none;\n /* Rest at full opacity, faithful to the bl-hero-glow origin (BL-326);\n do not re-normalize to the keyframe's 0.75 rest value (VI-581). */\n opacity: 1;\n transform: scale(1);\n }\n}\n"
4835
4835
  }
4836
4836
  ]
4837
4837
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.4.0",
3
- "generated_at": "2026-06-25T19:22:02.080Z",
3
+ "generated_at": "2026-07-02T17:50:48.085Z",
4
4
  "components": {
5
5
  "accessibility-specimen": {
6
6
  "category": "specimen",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "CLI for the Visor design system — add components, hooks, and utilities to your project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,7 +45,7 @@
45
45
  "dependencies": {
46
46
  "@aws-sdk/client-s3": "^3.1021.0",
47
47
  "@babel/parser": "^7.26.0",
48
- "@loworbitstudio/visor-theme-engine": "^0.17.0",
48
+ "@loworbitstudio/visor-theme-engine": "^0.17.1",
49
49
  "commander": "^13.1.0",
50
50
  "diff": "^8.0.4",
51
51
  "picocolors": "^1.1.1",