@loworbitstudio/visor 1.12.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 +46 -0
- package/dist/CHANGELOG.json +1 -1
- package/dist/index.js +260 -119
- package/dist/registry.json +4 -4
- package/dist/visor-manifest.json +2 -2
- package/package.json +2 -2
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).
|
package/dist/CHANGELOG.json
CHANGED
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
1218
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
2402
|
-
import { resolve as
|
|
2403
|
-
import { generateTheme, generateThemeData as
|
|
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 =
|
|
2573
|
+
const filePath = resolve5(cwd, file);
|
|
2433
2574
|
let yamlContent;
|
|
2434
2575
|
try {
|
|
2435
|
-
yamlContent =
|
|
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 =
|
|
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 =
|
|
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
|
|
2604
|
-
import { resolve as
|
|
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 =
|
|
2753
|
+
const filePath = resolve6(cwd, file ?? ".visor.yaml");
|
|
2613
2754
|
let yamlContent;
|
|
2614
2755
|
try {
|
|
2615
|
-
yamlContent =
|
|
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
|
|
2668
|
-
import { resolve as
|
|
2669
|
-
import { parse as
|
|
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 =
|
|
2814
|
+
const filePath = resolve7(cwd, file);
|
|
2674
2815
|
let fileContent;
|
|
2675
2816
|
try {
|
|
2676
|
-
fileContent =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
2877
|
-
import { resolve as
|
|
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 =
|
|
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 =
|
|
3119
|
+
const resolved = resolve9(filePath);
|
|
2979
3120
|
if (seen.has(resolved)) return;
|
|
2980
3121
|
if (!existsSync9(resolved)) return;
|
|
2981
3122
|
try {
|
|
2982
|
-
const content =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
3349
|
-
import { resolve as
|
|
3350
|
-
import { generateThemeData as
|
|
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
|
|
3355
|
-
import { resolve as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
3622
|
+
resolvedScript = resolve10(scriptPath);
|
|
3482
3623
|
}
|
|
3483
3624
|
try {
|
|
3484
3625
|
resolvedPrefix = realpathSync(expectedPrefix);
|
|
3485
3626
|
} catch {
|
|
3486
|
-
resolvedPrefix =
|
|
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 =
|
|
3717
|
+
const filePath = resolve11(cwd, file);
|
|
3577
3718
|
let yamlContent;
|
|
3578
3719
|
try {
|
|
3579
|
-
yamlContent =
|
|
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 =
|
|
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 =
|
|
3640
|
-
themeConfigContent =
|
|
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 ||
|
|
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
|
|
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 =
|
|
3787
|
-
themeConfigContent =
|
|
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
|
|
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
|
|
3848
|
-
import { parse as
|
|
3849
|
-
import { generateThemeData as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4305
|
-
themeConfigContent =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
4731
|
-
import { resolve as
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
5546
|
-
import { resolve as
|
|
5547
|
-
import { parse as
|
|
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 =
|
|
5577
|
-
const parsed =
|
|
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 =
|
|
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 =
|
|
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
|
|
5784
|
-
import { isAbsolute as isAbsolute2, join as join22, resolve as
|
|
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
|
|
5790
|
-
import { dirname as dirname8, resolve as
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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,
|
|
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 :
|
|
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 :
|
|
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 :
|
|
7062
|
+
candidates.push(isAbsolute2(themeFile) ? themeFile : resolve16(cwd, themeFile));
|
|
6922
7063
|
}
|
|
6923
|
-
candidates.push(isAbsolute2(theme2) ? 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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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);
|
package/dist/registry.json
CHANGED
|
@@ -468,7 +468,7 @@
|
|
|
468
468
|
{
|
|
469
469
|
"path": "components/ui/avatar/avatar.tsx",
|
|
470
470
|
"type": "registry:ui",
|
|
471
|
-
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./avatar.module.css\"\nimport stackStyles from \"./avatar-stack.module.css\"\n\n// ─── AvatarStack item type ───────────────────────────────────────────────────\n\n/**\n * Rich item form for AvatarStack. A plain string or `undefined` is also\n * accepted (backward-compatible shorthand for an image-src-only item).\n */\nexport interface AvatarStackItem {\n /** Initials rendered as fallback (and primary) content — e.g. \"AR\". */\n initials?: React.ReactNode\n /** Optional image source. When present, the image covers the disc. */\n src?: string\n /** Accessible label for the disc — e.g. the member name. */\n alt?: string\n /**\n * Per-avatar style escape hatch — carries the gradient `background` + text\n * `color` for the editorial gradient discs (see `getMemberAvatarStyle`).\n */\n style?: React.CSSProperties\n}\n\nexport interface AvatarProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> {\n size?: \"sm\" | \"default\" | \"lg\"\n}\n\nconst Avatar = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Root>,\n AvatarProps\n>(({ className, size = \"default\", ...props }, ref) => {\n return (\n <AvatarPrimitive.Root\n ref={ref}\n data-slot=\"avatar\"\n data-size={size}\n className={cn(\n styles.avatar,\n size === \"sm\" && styles.avatarSm,\n size === \"lg\" && styles.avatarLg,\n className\n )}\n {...props}\n />\n )\n})\nAvatar.displayName = \"Avatar\"\n\nconst AvatarImage = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Image>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => {\n return (\n <AvatarPrimitive.Image\n ref={ref}\n data-slot=\"avatar-image\"\n className={cn(styles.avatarImage, className)}\n {...props}\n />\n )\n})\nAvatarImage.displayName = \"AvatarImage\"\n\nconst AvatarFallback = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Fallback>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => {\n return (\n <AvatarPrimitive.Fallback\n ref={ref}\n data-slot=\"avatar-fallback\"\n className={cn(styles.avatarFallback, className)}\n {...props}\n />\n )\n})\nAvatarFallback.displayName = \"AvatarFallback\"\n\nexport interface AvatarStackProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"role\" | \"aria-label\"> {\n /**\n * Avatars to render, in display order. Each entry is either:\n * - A plain image URL string (backward-compatible shorthand)\n * - `undefined` — renders the `·` fallback disc, useful for server-truncated lists\n * - An `AvatarStackItem` object with `initials`, `src`, `alt`, and/or `style`\n */\n avatars: (string | undefined | AvatarStackItem)[]\n /**\n * Total member count. May exceed `avatars.length` when the caller has\n * server-truncated the avatar URLs and only knows the count. The overflow\n * indicator is computed against this value.\n */\n total: number\n /**\n * Maximum number of avatar slots rendered before the `+N` overflow\n * indicator. Defaults to `6`.\n */\n max?: number\n /**\n * Explicit \"+N\" override. When provided, this value is used verbatim\n * instead of the value derived from `total - visible.length`. Useful\n * when the caller has a pre-computed overflow count.\n */\n overflowCount?: number\n /** Avatar size. Defaults to `\"sm\"`. */\n size?: \"sm\" | \"default\" | \"lg\"\n /**\n * Accessible label override. Defaults to ``${total} members``.\n */\n label?: string\n}\n\n/** Normalize a raw avatar entry to an `AvatarStackItem`. */\nfunction toItem(entry: string | undefined | AvatarStackItem): AvatarStackItem {\n if (entry === undefined || entry === null) return {}\n if (typeof entry === \"string\") return { src: entry, alt: \"\" }\n return entry\n}\n\nconst AvatarStack = React.forwardRef<HTMLDivElement, AvatarStackProps>(\n function AvatarStack(\n {\n avatars,\n total,\n max = 6,\n overflowCount,\n size = \"sm\",\n label,\n className,\n ...rest\n },\n ref,\n ) {\n const visible = avatars.slice(0, max)\n const derivedOverflow = Math.max(0, total - visible.length)\n const overflow = overflowCount ?? derivedOverflow\n const ariaLabel = label ?? `${total} members`\n\n return (\n <div\n ref={ref}\n role=\"img\"\n aria-label={ariaLabel}\n data-slot=\"avatar-stack\"\n data-size={size}\n className={cn(stackStyles.root, className)}\n {...rest}\n >\n {visible.map((entry, index) => {\n const item = toItem(entry)\n return (\n <Avatar\n key={index}\n size={size}\n className={stackStyles.avatar}\n style={item.style}\n data-stack-item=\"\"\n >\n {item.src ? (\n <AvatarImage src={item.src} alt={item.alt ?? \"\"} />\n ) : item.initials != null ? (\n <AvatarFallback className={stackStyles.initialsDisc}>\n {item.initials}\n </AvatarFallback>\n ) : (\n <AvatarFallback>·</AvatarFallback>\n )}\n </Avatar>\n )\n })}\n {overflow > 0 ? (\n <Avatar\n size={size}\n className={stackStyles.avatar}\n data-stack-overflow=\"\"\n >\n <AvatarFallback
|
|
471
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./avatar.module.css\"\nimport stackStyles from \"./avatar-stack.module.css\"\n\n// ─── AvatarStack item type ───────────────────────────────────────────────────\n\n/**\n * Rich item form for AvatarStack. A plain string or `undefined` is also\n * accepted (backward-compatible shorthand for an image-src-only item).\n */\nexport interface AvatarStackItem {\n /** Initials rendered as fallback (and primary) content — e.g. \"AR\". */\n initials?: React.ReactNode\n /** Optional image source. When present, the image covers the disc. */\n src?: string\n /** Accessible label for the disc — e.g. the member name. */\n alt?: string\n /**\n * Per-avatar style escape hatch — carries the gradient `background` + text\n * `color` for the editorial gradient discs (see `getMemberAvatarStyle`).\n */\n style?: React.CSSProperties\n}\n\nexport interface AvatarProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> {\n size?: \"sm\" | \"default\" | \"lg\"\n}\n\nconst Avatar = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Root>,\n AvatarProps\n>(({ className, size = \"default\", ...props }, ref) => {\n return (\n <AvatarPrimitive.Root\n ref={ref}\n data-slot=\"avatar\"\n data-size={size}\n className={cn(\n styles.avatar,\n size === \"sm\" && styles.avatarSm,\n size === \"lg\" && styles.avatarLg,\n className\n )}\n {...props}\n />\n )\n})\nAvatar.displayName = \"Avatar\"\n\nconst AvatarImage = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Image>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => {\n return (\n <AvatarPrimitive.Image\n ref={ref}\n data-slot=\"avatar-image\"\n className={cn(styles.avatarImage, className)}\n {...props}\n />\n )\n})\nAvatarImage.displayName = \"AvatarImage\"\n\nconst AvatarFallback = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Fallback>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => {\n return (\n <AvatarPrimitive.Fallback\n ref={ref}\n data-slot=\"avatar-fallback\"\n className={cn(styles.avatarFallback, className)}\n {...props}\n />\n )\n})\nAvatarFallback.displayName = \"AvatarFallback\"\n\nexport interface AvatarStackProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"role\" | \"aria-label\"> {\n /**\n * Avatars to render, in display order. Each entry is either:\n * - A plain image URL string (backward-compatible shorthand)\n * - `undefined` — renders the `·` fallback disc, useful for server-truncated lists\n * - An `AvatarStackItem` object with `initials`, `src`, `alt`, and/or `style`\n */\n avatars: (string | undefined | AvatarStackItem)[]\n /**\n * Total member count. May exceed `avatars.length` when the caller has\n * server-truncated the avatar URLs and only knows the count. The overflow\n * indicator is computed against this value.\n */\n total: number\n /**\n * Maximum number of avatar slots rendered before the `+N` overflow\n * indicator. Defaults to `6`.\n */\n max?: number\n /**\n * Explicit \"+N\" override. When provided, this value is used verbatim\n * instead of the value derived from `total - visible.length`. Useful\n * when the caller has a pre-computed overflow count.\n */\n overflowCount?: number\n /** Avatar size. Defaults to `\"sm\"`. */\n size?: \"sm\" | \"default\" | \"lg\"\n /**\n * Accessible label override. Defaults to ``${total} members``.\n */\n label?: string\n}\n\n/** Normalize a raw avatar entry to an `AvatarStackItem`. */\nfunction toItem(entry: string | undefined | AvatarStackItem): AvatarStackItem {\n if (entry === undefined || entry === null) return {}\n if (typeof entry === \"string\") return { src: entry, alt: \"\" }\n return entry\n}\n\nconst AvatarStack = React.forwardRef<HTMLDivElement, AvatarStackProps>(\n function AvatarStack(\n {\n avatars,\n total,\n max = 6,\n overflowCount,\n size = \"sm\",\n label,\n className,\n ...rest\n },\n ref,\n ) {\n const visible = avatars.slice(0, max)\n const derivedOverflow = Math.max(0, total - visible.length)\n const overflow = overflowCount ?? derivedOverflow\n const ariaLabel = label ?? `${total} members`\n\n return (\n <div\n ref={ref}\n role=\"img\"\n aria-label={ariaLabel}\n data-slot=\"avatar-stack\"\n data-size={size}\n className={cn(stackStyles.root, className)}\n {...rest}\n >\n {visible.map((entry, index) => {\n const item = toItem(entry)\n return (\n <Avatar\n key={index}\n size={size}\n className={stackStyles.avatar}\n style={item.style}\n data-stack-item=\"\"\n >\n {item.src ? (\n <AvatarImage src={item.src} alt={item.alt ?? \"\"} />\n ) : item.initials != null ? (\n <AvatarFallback className={stackStyles.initialsDisc}>\n {item.initials}\n </AvatarFallback>\n ) : (\n <AvatarFallback>·</AvatarFallback>\n )}\n </Avatar>\n )\n })}\n {overflow > 0 ? (\n <Avatar\n size={size}\n className={stackStyles.avatar}\n data-stack-overflow=\"\"\n >\n <AvatarFallback className={stackStyles.overflowDisc}>\n +{overflow}\n </AvatarFallback>\n </Avatar>\n ) : null}\n </div>\n )\n },\n)\n\nAvatarStack.displayName = \"AvatarStack\"\n\nexport { Avatar, AvatarImage, AvatarFallback, AvatarStack }\n"
|
|
472
472
|
},
|
|
473
473
|
{
|
|
474
474
|
"path": "components/ui/avatar/avatar.module.css",
|
|
@@ -478,7 +478,7 @@
|
|
|
478
478
|
{
|
|
479
479
|
"path": "components/ui/avatar/avatar-stack.module.css",
|
|
480
480
|
"type": "registry:ui",
|
|
481
|
-
"content": "/* AvatarStack root */\n.root {\n display: inline-flex;\n flex-direction: row;\n align-items: center;\n isolation: isolate;\n}\n\n/* Each avatar gets a ring matching the parent surface so adjacent avatars\n read as separate circles. Avatar's own `overflow: hidden` clips inner\n shadows, so the ring is projected outward via box-shadow. */\n.avatar {\n box-shadow: 0 0 0 var(--stroke-width-medium, 2px) var(--surface-default, #ffffff);\n}\n\n/* Overlap every avatar except the first. Later siblings stack on top of\n earlier ones because `isolation: isolate` establishes a single stacking\n context and DOM order wins inside it. */\n.avatar:not(:first-child) {\n margin-inline-start: calc(-1 * var(--spacing-2, 0.5rem));\n}\n\n/* When an AvatarStackItem carries a per-item `style` with gradient\n background + color, the fallback must be transparent so the gradient\n on the parent Avatar shows through. Only applied to the initials\n fallback inside a stack disc — plain Avatar usage is unaffected. */\n.initialsDisc {\n background-color: transparent;\n color: inherit;\n}\n"
|
|
481
|
+
"content": "/* AvatarStack root */\n.root {\n display: inline-flex;\n flex-direction: row;\n align-items: center;\n isolation: isolate;\n}\n\n/* Each avatar gets a ring matching the parent surface so adjacent avatars\n read as separate circles. Avatar's own `overflow: hidden` clips inner\n shadows, so the ring is projected outward via box-shadow.\n\n `--avatar-stack-ring` is the per-surface role hook: it defaults to\n `--surface-default` (byte-identical for existing consumers). A stack that\n sits on a different tier sets ONE custom property on a wrapper — for example\n `--avatar-stack-ring: var(--surface-card)` — instead of reaching into the\n component's `data-slot` internals (VI-578). */\n.avatar {\n box-shadow: 0 0 0 var(--stroke-width-medium, 2px) var(--avatar-stack-ring, var(--surface-default, #ffffff));\n}\n\n/* Overlap every avatar except the first. Later siblings stack on top of\n earlier ones because `isolation: isolate` establishes a single stacking\n context and DOM order wins inside it. */\n.avatar:not(:first-child) {\n margin-inline-start: calc(-1 * var(--spacing-2, 0.5rem));\n}\n\n/* When an AvatarStackItem carries a per-item `style` with gradient\n background + color, the fallback must be transparent so the gradient\n on the parent Avatar shows through. Only applied to the initials\n fallback inside a stack disc — plain Avatar usage is unaffected. */\n.initialsDisc {\n background-color: transparent;\n color: inherit;\n}\n\n/* The \"+N\" overflow disc carries 3–4 glyphs. Editorial density shrinks the\n default disc to 28px, but the base avatar stylesheet only adds explicit\n fallback font-sizes for the sm and lg sizes, so a default-size overflow\n count keeps the 14px base and clips behind overflow:hidden. Pin it to 11px\n here (matching the blessed avatar overflow treatment), absorbing the\n consumer's former font-size-sm overlay hack (VI-578). Default density is\n unchanged; the more specific lg fallback rule still wins for lg discs. */\n:global([data-density=\"editorial\"]) .overflowDisc {\n font-size: 11px;\n}\n"
|
|
482
482
|
}
|
|
483
483
|
]
|
|
484
484
|
},
|
|
@@ -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
|
|
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
|
|
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
|
},
|
package/dist/visor-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.4.0",
|
|
3
|
-
"generated_at": "2026-
|
|
3
|
+
"generated_at": "2026-07-02T17:50:48.085Z",
|
|
4
4
|
"components": {
|
|
5
5
|
"accessibility-specimen": {
|
|
6
6
|
"category": "specimen",
|
|
@@ -311,7 +311,7 @@
|
|
|
311
311
|
},
|
|
312
312
|
{
|
|
313
313
|
"name": "AvatarStack",
|
|
314
|
-
"description": "Compound primitive for overlapping avatar groups with a `+N` overflow indicator. Each entry in `avatars` is a plain image-URL string, `undefined` (renders a placeholder disc), or an `AvatarStackItem` object with `initials`, `src`, `alt`, and an optional `style` escape hatch for per-disc gradient backgrounds. Accepts a `total` count (supports server-truncated lists), a `max` cap (default 6), an explicit `overflowCount` override, a `size` pass-through (`\"sm\" | \"default\" | \"lg\"`, default `\"sm\"`), and an optional accessible `label` override. Renders `role=\"img\"` with `data-slot=\"avatar-stack\"
|
|
314
|
+
"description": "Compound primitive for overlapping avatar groups with a `+N` overflow indicator. Each entry in `avatars` is a plain image-URL string, `undefined` (renders a placeholder disc), or an `AvatarStackItem` object with `initials`, `src`, `alt`, and an optional `style` escape hatch for per-disc gradient backgrounds. Accepts a `total` count (supports server-truncated lists), a `max` cap (default 6), an explicit `overflowCount` override, a `size` pass-through (`\"sm\" | \"default\" | \"lg\"`, default `\"sm\"`), and an optional accessible `label` override. Renders `role=\"img\"` with `data-slot=\"avatar-stack\"`. Each disc rings itself against the parent surface via the `--avatar-stack-ring` role hook (defaults to `--surface-default`); a stack sitting on a different tier sets that one custom property on a wrapper (e.g. `--avatar-stack-ring: var(--surface-card)`) rather than reaching into `data-slot` internals. Under `data-density=\"editorial\"` the `+N` overflow disc font drops to 11px so the count never clips in the smaller disc.\n"
|
|
315
315
|
}
|
|
316
316
|
],
|
|
317
317
|
"dependencies": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loworbitstudio/visor",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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",
|