brandrail 0.1.0 → 0.3.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 +2 -2
- package/dist/brandrail.mjs +365 -139
- package/package.json +9 -10
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Keep AI-generated UI on-brand. `brandrail` checks code and live pages against yo
|
|
|
4
4
|
(Brand Kit), and writes your rules into the files AI coding tools read.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
-
npx brandrail init # brandrail.config.json + AGENTS.md, CLAUDE.md, Cursor/Copilot/Windsurf/Gemini rules
|
|
7
|
+
npx brandrail init # brandrail.config.json, src/brandrail.tokens.css + AGENTS.md, CLAUDE.md, Cursor/Copilot/Windsurf/Gemini rules
|
|
8
8
|
npx brandrail check # violations + brand score for this repo
|
|
9
9
|
npx brandrail scan <url> # check a running page in a real browser
|
|
10
10
|
```
|
|
@@ -23,7 +23,7 @@ npx brandrail check --changed --upload # only changed lines, results in the d
|
|
|
23
23
|
| --- | --- |
|
|
24
24
|
| `check [paths]` | `--format pretty\|json\|markdown\|github`, `--fail-below 80`, `--changed [base]`, `--upload` |
|
|
25
25
|
| `scan <url>` | `--pages 3`, `--storage-state auth.json` (pages behind a login), `--wait 2000`, `--upload`. Needs `npm i -D playwright && npx playwright install chromium` |
|
|
26
|
-
| `init` | `--from-api`, `--mcp`, `--no-rules`, `--force
|
|
26
|
+
| `init` | `--from-api`, `--mcp`, `--tokens-css <file>`, `--no-rules`, `--force`. Writes the Brand Kit as CSS variables; import that file once at your app entry |
|
|
27
27
|
| `import <tokens.json>` | DTCG, Tokens Studio or Figma variables JSON into the config. `--replace`, `--primitives` |
|
|
28
28
|
| `rules` | List the rules |
|
|
29
29
|
|
package/dist/brandrail.mjs
CHANGED
|
@@ -175,10 +175,10 @@ function resolveConfig(config) {
|
|
|
175
175
|
if (!l.type && !l.path.startsWith("color")) continue;
|
|
176
176
|
const rgb = parseColor(l.value);
|
|
177
177
|
if (!rgb) continue;
|
|
178
|
-
const
|
|
179
|
-
colors.push({ path: l.path, cssVar, hex: toHex(rgb), lab: rgbToLab(rgb), alpha: rgb.a });
|
|
178
|
+
const cssVar2 = `--${l.path.replace(/\./g, "-")}`;
|
|
179
|
+
colors.push({ path: l.path, cssVar: cssVar2, hex: toHex(rgb), lab: rgbToLab(rgb), alpha: rgb.a });
|
|
180
180
|
const dark = typeof l.dark === "string" ? parseColor(l.dark) : null;
|
|
181
|
-
if (dark) colors.push({ path: l.path, cssVar, hex: toHex(dark), lab: rgbToLab(dark), alpha: dark.a, mode: "dark" });
|
|
181
|
+
if (dark) colors.push({ path: l.path, cssVar: cssVar2, hex: toHex(dark), lab: rgbToLab(dark), alpha: dark.a, mode: "dark" });
|
|
182
182
|
}
|
|
183
183
|
const spaceLeaves = all.filter((l) => /^(space|spacing)\./.test(l.path));
|
|
184
184
|
const spacing = [0, 1, 2];
|
|
@@ -604,7 +604,7 @@ function makeIgnore(globs) {
|
|
|
604
604
|
}
|
|
605
605
|
|
|
606
606
|
// ../engine/src/check.ts
|
|
607
|
-
var ENGINE_VERSION = "0.
|
|
607
|
+
var ENGINE_VERSION = "0.3.0";
|
|
608
608
|
var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
|
|
609
609
|
function computeScore(counts, lines) {
|
|
610
610
|
const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
|
|
@@ -673,7 +673,7 @@ function check(files, configInput, opts = {}) {
|
|
|
673
673
|
}
|
|
674
674
|
|
|
675
675
|
// ../engine/src/report.ts
|
|
676
|
-
var RENDERED_ONLY = { "text-contrast": "Low text contrast", "font-size-scale": "Off-scale font size" };
|
|
676
|
+
var RENDERED_ONLY = { "text-contrast": "Low text contrast", "font-size-scale": "Off-scale font size", "page-rendered": "Blank page" };
|
|
677
677
|
var title = (id) => RULES.find((r) => r.id === id)?.title ?? RENDERED_ONLY[id] ?? id;
|
|
678
678
|
var ansi = (on) => {
|
|
679
679
|
const w = (code) => (s) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
@@ -784,129 +784,6 @@ var starterConfig = {
|
|
|
784
784
|
ignore: []
|
|
785
785
|
};
|
|
786
786
|
|
|
787
|
-
// ../engine/src/instructions.ts
|
|
788
|
-
var RULES_START = "<!-- brandrail:start -->";
|
|
789
|
-
var RULES_END = "<!-- brandrail:end -->";
|
|
790
|
-
function generateInstructions(config, opts = {}) {
|
|
791
|
-
const r = resolveConfig(config);
|
|
792
|
-
const lines = [];
|
|
793
|
-
lines.push(`# Design system rules${opts.org ? ` \u2014 ${opts.org}` : ""}`);
|
|
794
|
-
lines.push("");
|
|
795
|
-
lines.push(
|
|
796
|
-
`All UI in this repo must follow the Brand Kit${r.tokenVersion ? ` (v${r.tokenVersion})` : ""}. These rules are checked on every pull request by Brandrail.`
|
|
797
|
-
);
|
|
798
|
-
lines.push("");
|
|
799
|
-
if (r.components.length) {
|
|
800
|
-
lines.push("## Components");
|
|
801
|
-
lines.push("Always use these components. Never re-create them with raw HTML elements:");
|
|
802
|
-
for (const c of r.components) {
|
|
803
|
-
const tags = [...r.replaces].filter(([, v]) => v === c).map(([t]) => `<${t}>`);
|
|
804
|
-
lines.push(`- \`${c.name}\` from \`${c.importPath}\`${tags.length ? ` (instead of ${tags.join(", ")})` : ""}`);
|
|
805
|
-
}
|
|
806
|
-
lines.push("");
|
|
807
|
-
}
|
|
808
|
-
if (r.colors.length) {
|
|
809
|
-
lines.push("## Colours");
|
|
810
|
-
lines.push("Use CSS variables only. Never write hex, rgb() or hsl() values, and never use Tailwind palette classes like `bg-blue-500`.");
|
|
811
|
-
for (const c of r.colors) {
|
|
812
|
-
if (c.mode) continue;
|
|
813
|
-
const dark = r.colors.find((d) => d.mode === "dark" && d.path === c.path);
|
|
814
|
-
lines.push(`- \`var(${c.cssVar})\` \u2014 ${c.path} (${c.hex}${dark ? `, dark ${dark.hex}` : ""})`);
|
|
815
|
-
}
|
|
816
|
-
lines.push("");
|
|
817
|
-
}
|
|
818
|
-
lines.push("## Spacing, radius and type");
|
|
819
|
-
lines.push(`- Spacing (padding, margin, gap) must be one of: ${r.spacing.filter((s) => s > 2).map((s) => `${s}px`).join(", ")}.`);
|
|
820
|
-
lines.push(`- Corner radius must be one of: ${r.radii.filter((x) => x > 0 && x < 999).map((x) => `${x}px`).join(", ")} (or fully rounded).`);
|
|
821
|
-
if (r.fonts.length) lines.push(`- Fonts: use ${r.fontVars.map((v) => `\`var(${v})\``).join(" or ")}; never set another font-family.`);
|
|
822
|
-
lines.push("");
|
|
823
|
-
lines.push("## Before you finish");
|
|
824
|
-
lines.push("- If the Brandrail MCP server is connected, call `validate_snippet` on the UI you wrote and fix every violation.");
|
|
825
|
-
lines.push("- Otherwise run `npx brandrail check` and fix errors before committing.");
|
|
826
|
-
lines.push("");
|
|
827
|
-
lines.push("<details><summary>Checked rules</summary>");
|
|
828
|
-
lines.push("");
|
|
829
|
-
for (const rule of RULES) {
|
|
830
|
-
if (r.rules[rule.id] === "off") continue;
|
|
831
|
-
lines.push(`- **${rule.title}** (\`${rule.id}\`): ${rule.description}`);
|
|
832
|
-
}
|
|
833
|
-
lines.push("");
|
|
834
|
-
lines.push("</details>");
|
|
835
|
-
return lines.join("\n");
|
|
836
|
-
}
|
|
837
|
-
function upsertSection(existing, section) {
|
|
838
|
-
const block = `${RULES_START}
|
|
839
|
-
${section}
|
|
840
|
-
${RULES_END}`;
|
|
841
|
-
const start = existing.indexOf(RULES_START);
|
|
842
|
-
const end = existing.indexOf(RULES_END);
|
|
843
|
-
if (start !== -1 && end > start) return existing.slice(0, start) + block + existing.slice(end + RULES_END.length);
|
|
844
|
-
return existing.trim() ? `${existing.trimEnd()}
|
|
845
|
-
|
|
846
|
-
${block}
|
|
847
|
-
` : `${block}
|
|
848
|
-
`;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// ../engine/src/mcp-config.ts
|
|
852
|
-
var json = (o) => `${JSON.stringify(o, null, 2)}
|
|
853
|
-
`;
|
|
854
|
-
function mcpClientConfigs(url) {
|
|
855
|
-
return [
|
|
856
|
-
{
|
|
857
|
-
id: "claude-code",
|
|
858
|
-
name: "Claude Code",
|
|
859
|
-
file: ".mcp.json",
|
|
860
|
-
projectFile: true,
|
|
861
|
-
content: json({ mcpServers: { brandrail: { type: "http", url, headers: { Authorization: "Bearer ${BRANDRAIL_KEY}" } } } }),
|
|
862
|
-
command: `claude mcp add --transport http brandrail ${url} --header "Authorization: Bearer $BRANDRAIL_KEY"`
|
|
863
|
-
},
|
|
864
|
-
{
|
|
865
|
-
id: "codex",
|
|
866
|
-
name: "Codex",
|
|
867
|
-
file: "~/.codex/config.toml",
|
|
868
|
-
projectFile: false,
|
|
869
|
-
content: `[mcp_servers.brandrail]
|
|
870
|
-
url = "${url}"
|
|
871
|
-
bearer_token_env_var = "BRANDRAIL_KEY"
|
|
872
|
-
`,
|
|
873
|
-
command: `codex mcp add brandrail --url ${url} --bearer-token-env-var BRANDRAIL_KEY`
|
|
874
|
-
},
|
|
875
|
-
{
|
|
876
|
-
id: "cursor",
|
|
877
|
-
name: "Cursor",
|
|
878
|
-
file: ".cursor/mcp.json",
|
|
879
|
-
projectFile: true,
|
|
880
|
-
content: json({ mcpServers: { brandrail: { url, headers: { Authorization: "Bearer ${env:BRANDRAIL_KEY}" } } } })
|
|
881
|
-
},
|
|
882
|
-
{
|
|
883
|
-
id: "vscode",
|
|
884
|
-
name: "GitHub Copilot (VS Code)",
|
|
885
|
-
file: ".vscode/mcp.json",
|
|
886
|
-
projectFile: true,
|
|
887
|
-
content: json({
|
|
888
|
-
inputs: [{ type: "promptString", id: "brandrail-key", description: "Brandrail API key", password: true }],
|
|
889
|
-
servers: { brandrail: { type: "http", url, headers: { Authorization: "Bearer ${input:brandrail-key}" } } }
|
|
890
|
-
})
|
|
891
|
-
},
|
|
892
|
-
{
|
|
893
|
-
id: "windsurf",
|
|
894
|
-
name: "Windsurf",
|
|
895
|
-
file: "~/.codeium/windsurf/mcp_config.json",
|
|
896
|
-
projectFile: false,
|
|
897
|
-
content: json({ mcpServers: { brandrail: { serverUrl: url, headers: { Authorization: "Bearer ${env:BRANDRAIL_KEY}" } } } })
|
|
898
|
-
}
|
|
899
|
-
];
|
|
900
|
-
}
|
|
901
|
-
var RULE_FILES = [
|
|
902
|
-
{ file: "AGENTS.md", readBy: "Codex, Jules, Amp, Cursor and other agents" },
|
|
903
|
-
{ file: "CLAUDE.md", readBy: "Claude Code" },
|
|
904
|
-
{ file: ".cursor/rules/brandrail.mdc", readBy: "Cursor" },
|
|
905
|
-
{ file: ".github/copilot-instructions.md", readBy: "GitHub Copilot" },
|
|
906
|
-
{ file: ".windsurf/rules/brandrail.md", readBy: "Windsurf" },
|
|
907
|
-
{ file: "GEMINI.md", readBy: "Gemini CLI" }
|
|
908
|
-
];
|
|
909
|
-
|
|
910
787
|
// ../engine/src/tokens.ts
|
|
911
788
|
var EXT = "dev.brandrail";
|
|
912
789
|
function slug(s) {
|
|
@@ -1374,10 +1251,150 @@ function mergeTokens(base, incoming) {
|
|
|
1374
1251
|
return buildDtcg([...m.values()]);
|
|
1375
1252
|
}
|
|
1376
1253
|
|
|
1254
|
+
// ../engine/src/instructions.ts
|
|
1255
|
+
var KIT_PACKAGE = "@brandrail/kit";
|
|
1256
|
+
var RULES_START = "<!-- brandrail:start -->";
|
|
1257
|
+
var RULES_END = "<!-- brandrail:end -->";
|
|
1258
|
+
function generateInstructions(config, opts = {}) {
|
|
1259
|
+
const r = resolveConfig(config);
|
|
1260
|
+
const lines = [];
|
|
1261
|
+
lines.push(`# Design system rules${opts.org ? ` \u2014 ${opts.org}` : ""}`);
|
|
1262
|
+
lines.push("");
|
|
1263
|
+
lines.push(
|
|
1264
|
+
`All UI in this repo must follow the Brand Kit${r.tokenVersion ? ` (v${r.tokenVersion})` : ""}. These rules are checked on every pull request by Brandrail.`
|
|
1265
|
+
);
|
|
1266
|
+
lines.push("");
|
|
1267
|
+
const usesKit = r.components.some((c) => c.importPath === KIT_PACKAGE);
|
|
1268
|
+
if (opts.tokensCss || usesKit) {
|
|
1269
|
+
lines.push("## Setup");
|
|
1270
|
+
if (opts.tokensCss) {
|
|
1271
|
+
lines.push(`- \`${opts.tokensCss}\` defines every CSS variable below (light and dark). Import it once at the app entry, before other styles. Don't edit it: it's generated from the Brand Kit.`);
|
|
1272
|
+
}
|
|
1273
|
+
if (usesKit) {
|
|
1274
|
+
lines.push(`- Import \`${KIT_PACKAGE}/styles.css\` once at the app entry. Pass \`theme="brandrail"\` to kit components, and \`colorMode={ColorMode.DARK}\` (from \`${KIT_PACKAGE}\`) for dark mode. The Brand Kit styles them through the CSS variables; don't override their colours, spacing or fonts with your own CSS.`);
|
|
1275
|
+
}
|
|
1276
|
+
lines.push("");
|
|
1277
|
+
}
|
|
1278
|
+
if (r.components.length) {
|
|
1279
|
+
lines.push("## Components");
|
|
1280
|
+
lines.push("Always use these components. Never re-create them with raw HTML elements:");
|
|
1281
|
+
for (const c of r.components) {
|
|
1282
|
+
const tags = [...r.replaces].filter(([, v]) => v === c).map(([t]) => `<${t}>`);
|
|
1283
|
+
lines.push(`- \`${c.name}\` from \`${c.importPath}\`${tags.length ? ` (instead of ${tags.join(", ")})` : ""}`);
|
|
1284
|
+
}
|
|
1285
|
+
lines.push("");
|
|
1286
|
+
}
|
|
1287
|
+
if (r.colors.length) {
|
|
1288
|
+
lines.push("## Colours");
|
|
1289
|
+
lines.push("Use CSS variables only. Never write hex, rgb() or hsl() values, and never use Tailwind palette classes like `bg-blue-500`.");
|
|
1290
|
+
for (const c of r.colors) {
|
|
1291
|
+
if (c.mode) continue;
|
|
1292
|
+
const dark = r.colors.find((d) => d.mode === "dark" && d.path === c.path);
|
|
1293
|
+
lines.push(`- \`var(${c.cssVar})\` \u2014 ${c.path} (${c.hex}${dark ? `, dark ${dark.hex}` : ""})`);
|
|
1294
|
+
}
|
|
1295
|
+
lines.push("");
|
|
1296
|
+
}
|
|
1297
|
+
lines.push("## Spacing, radius and type");
|
|
1298
|
+
const named = (prefix) => flattenTokens(config.tokens).filter((t) => prefix.test(t.path)).map((t) => `\`var(--${t.path.replace(/\./g, "-")})\` ${t.value}`);
|
|
1299
|
+
const spaceVars = named(/^(space|spacing)\./);
|
|
1300
|
+
const radiusVars = named(/^radius\./);
|
|
1301
|
+
const sizeVars = named(/^font\.size\./);
|
|
1302
|
+
lines.push(`- Spacing (padding, margin, gap) must be one of: ${r.spacing.filter((s) => s > 2).map((s) => `${s}px`).join(", ")}.${spaceVars.length ? ` Prefer the variables: ${spaceVars.join(", ")}.` : ""}`);
|
|
1303
|
+
lines.push(`- Corner radius must be one of: ${r.radii.filter((x) => x > 0 && x < 999).map((x) => `${x}px`).join(", ")} (or fully rounded).${radiusVars.length ? ` Variables: ${radiusVars.join(", ")}.` : ""}`);
|
|
1304
|
+
if (sizeVars.length) lines.push(`- Font sizes come from the type scale: ${sizeVars.join(", ")}. Matching line heights are \`--font-line-height-*\`.`);
|
|
1305
|
+
if (r.fonts.length) lines.push(`- Fonts: use ${r.fontVars.map((v) => `\`var(${v})\``).join(" or ")}; never set another font-family.`);
|
|
1306
|
+
lines.push("");
|
|
1307
|
+
lines.push("## Before you finish");
|
|
1308
|
+
lines.push("- If the Brandrail MCP server is connected, call `validate_snippet` on the UI you wrote and fix every violation.");
|
|
1309
|
+
lines.push("- Otherwise run `npx brandrail check` and fix errors before committing.");
|
|
1310
|
+
lines.push("");
|
|
1311
|
+
lines.push("<details><summary>Checked rules</summary>");
|
|
1312
|
+
lines.push("");
|
|
1313
|
+
for (const rule of RULES) {
|
|
1314
|
+
if (r.rules[rule.id] === "off") continue;
|
|
1315
|
+
lines.push(`- **${rule.title}** (\`${rule.id}\`): ${rule.description}`);
|
|
1316
|
+
}
|
|
1317
|
+
lines.push("");
|
|
1318
|
+
lines.push("</details>");
|
|
1319
|
+
return lines.join("\n");
|
|
1320
|
+
}
|
|
1321
|
+
function upsertSection(existing, section) {
|
|
1322
|
+
const block = `${RULES_START}
|
|
1323
|
+
${section}
|
|
1324
|
+
${RULES_END}`;
|
|
1325
|
+
const start = existing.indexOf(RULES_START);
|
|
1326
|
+
const end = existing.indexOf(RULES_END);
|
|
1327
|
+
if (start !== -1 && end > start) return existing.slice(0, start) + block + existing.slice(end + RULES_END.length);
|
|
1328
|
+
return existing.trim() ? `${existing.trimEnd()}
|
|
1329
|
+
|
|
1330
|
+
${block}
|
|
1331
|
+
` : `${block}
|
|
1332
|
+
`;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// ../engine/src/mcp-config.ts
|
|
1336
|
+
var json = (o) => `${JSON.stringify(o, null, 2)}
|
|
1337
|
+
`;
|
|
1338
|
+
function mcpClientConfigs(url) {
|
|
1339
|
+
return [
|
|
1340
|
+
{
|
|
1341
|
+
id: "claude-code",
|
|
1342
|
+
name: "Claude Code",
|
|
1343
|
+
file: ".mcp.json",
|
|
1344
|
+
projectFile: true,
|
|
1345
|
+
content: json({ mcpServers: { brandrail: { type: "http", url, headers: { Authorization: "Bearer ${BRANDRAIL_KEY}" } } } }),
|
|
1346
|
+
command: `claude mcp add --transport http brandrail ${url} --header "Authorization: Bearer $BRANDRAIL_KEY"`
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
id: "codex",
|
|
1350
|
+
name: "Codex",
|
|
1351
|
+
file: "~/.codex/config.toml",
|
|
1352
|
+
projectFile: false,
|
|
1353
|
+
content: `[mcp_servers.brandrail]
|
|
1354
|
+
url = "${url}"
|
|
1355
|
+
bearer_token_env_var = "BRANDRAIL_KEY"
|
|
1356
|
+
`,
|
|
1357
|
+
command: `codex mcp add brandrail --url ${url} --bearer-token-env-var BRANDRAIL_KEY`
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
id: "cursor",
|
|
1361
|
+
name: "Cursor",
|
|
1362
|
+
file: ".cursor/mcp.json",
|
|
1363
|
+
projectFile: true,
|
|
1364
|
+
content: json({ mcpServers: { brandrail: { url, headers: { Authorization: "Bearer ${env:BRANDRAIL_KEY}" } } } })
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
id: "vscode",
|
|
1368
|
+
name: "GitHub Copilot (VS Code)",
|
|
1369
|
+
file: ".vscode/mcp.json",
|
|
1370
|
+
projectFile: true,
|
|
1371
|
+
content: json({
|
|
1372
|
+
inputs: [{ type: "promptString", id: "brandrail-key", description: "Brandrail API key", password: true }],
|
|
1373
|
+
servers: { brandrail: { type: "http", url, headers: { Authorization: "Bearer ${input:brandrail-key}" } } }
|
|
1374
|
+
})
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
id: "windsurf",
|
|
1378
|
+
name: "Windsurf",
|
|
1379
|
+
file: "~/.codeium/windsurf/mcp_config.json",
|
|
1380
|
+
projectFile: false,
|
|
1381
|
+
content: json({ mcpServers: { brandrail: { serverUrl: url, headers: { Authorization: "Bearer ${env:BRANDRAIL_KEY}" } } } })
|
|
1382
|
+
}
|
|
1383
|
+
];
|
|
1384
|
+
}
|
|
1385
|
+
var RULE_FILES = [
|
|
1386
|
+
{ file: "AGENTS.md", readBy: "Codex, Jules, Amp, Cursor and other agents" },
|
|
1387
|
+
{ file: "CLAUDE.md", readBy: "Claude Code" },
|
|
1388
|
+
{ file: ".cursor/rules/brandrail.mdc", readBy: "Cursor" },
|
|
1389
|
+
{ file: ".github/copilot-instructions.md", readBy: "GitHub Copilot" },
|
|
1390
|
+
{ file: ".windsurf/rules/brandrail.md", readBy: "Windsurf" },
|
|
1391
|
+
{ file: "GEMINI.md", readBy: "Gemini CLI" }
|
|
1392
|
+
];
|
|
1393
|
+
|
|
1377
1394
|
// ../engine/src/rendered.ts
|
|
1378
1395
|
function collectRenderedNodes(maxNodes = 3e3) {
|
|
1379
1396
|
const SKIP = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "IFRAME", "HEAD", "META", "LINK", "BR", "WBR", "SOURCE", "TRACK"]);
|
|
1380
|
-
const
|
|
1397
|
+
const px3 = (v) => {
|
|
1381
1398
|
const n = parseFloat(v);
|
|
1382
1399
|
return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0;
|
|
1383
1400
|
};
|
|
@@ -1425,8 +1442,8 @@ function collectRenderedNodes(maxNodes = 3e3) {
|
|
|
1425
1442
|
const r = el.getBoundingClientRect();
|
|
1426
1443
|
if (r.width < 1 || r.height < 1) continue;
|
|
1427
1444
|
const text = Array.from(el.childNodes).filter((n) => n.nodeType === 3).map((n) => n.textContent ?? "").join(" ").replace(/\s+/g, " ").trim();
|
|
1428
|
-
const borderW = Math.max(
|
|
1429
|
-
const borderSide =
|
|
1445
|
+
const borderW = Math.max(px3(cs.borderTopWidth), px3(cs.borderRightWidth), px3(cs.borderBottomWidth), px3(cs.borderLeftWidth));
|
|
1446
|
+
const borderSide = px3(cs.borderTopWidth) > 0 ? cs.borderTopColor : px3(cs.borderBottomWidth) > 0 ? cs.borderBottomColor : px3(cs.borderLeftWidth) > 0 ? cs.borderLeftColor : cs.borderRightColor;
|
|
1430
1447
|
nodes.push({
|
|
1431
1448
|
selector: selectorOf(el),
|
|
1432
1449
|
tag: el.tagName.toLowerCase(),
|
|
@@ -1436,12 +1453,12 @@ function collectRenderedNodes(maxNodes = 3e3) {
|
|
|
1436
1453
|
borderColor: borderW > 0 && cs.borderTopStyle !== "none" ? borderSide : void 0,
|
|
1437
1454
|
effectiveBackground: effectiveBg(el),
|
|
1438
1455
|
fontFamily: cs.fontFamily,
|
|
1439
|
-
fontSize:
|
|
1456
|
+
fontSize: px3(cs.fontSize),
|
|
1440
1457
|
fontWeight: parseInt(cs.fontWeight, 10) || 400,
|
|
1441
|
-
radius: Math.max(
|
|
1458
|
+
radius: Math.max(px3(cs.borderTopLeftRadius), px3(cs.borderTopRightRadius), px3(cs.borderBottomLeftRadius), px3(cs.borderBottomRightRadius)),
|
|
1442
1459
|
minSide: Math.round(Math.min(r.width, r.height)),
|
|
1443
|
-
padding: [
|
|
1444
|
-
gap: cs.display.includes("flex") || cs.display.includes("grid") ? [
|
|
1460
|
+
padding: [px3(cs.paddingTop), px3(cs.paddingRight), px3(cs.paddingBottom), px3(cs.paddingLeft)],
|
|
1461
|
+
gap: cs.display.includes("flex") || cs.display.includes("grid") ? [px3(cs.rowGap), px3(cs.columnGap)] : []
|
|
1445
1462
|
});
|
|
1446
1463
|
}
|
|
1447
1464
|
const links = Array.from(document.querySelectorAll("a[href]")).map((a) => a.href).filter((h) => {
|
|
@@ -1587,6 +1604,17 @@ function checkRendered(pages, configInput, opts = {}) {
|
|
|
1587
1604
|
}
|
|
1588
1605
|
}
|
|
1589
1606
|
const path = pagePath(page.url);
|
|
1607
|
+
if (page.nodes.length < 5) {
|
|
1608
|
+
violations.push({
|
|
1609
|
+
ruleId: "page-rendered",
|
|
1610
|
+
severity: "error",
|
|
1611
|
+
file: path,
|
|
1612
|
+
line: 0,
|
|
1613
|
+
column: 0,
|
|
1614
|
+
message: `The page rendered ${page.nodes.length} visible element${page.nodes.length === 1 ? "" : "s"}, so there was nothing to check`,
|
|
1615
|
+
suggestion: "Open it in a browser: it may have crashed, need a login (use --storage-state), or need longer to load (--wait)."
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1590
1618
|
for (const g of groups.values()) {
|
|
1591
1619
|
violations.push({
|
|
1592
1620
|
ruleId: g.ruleId,
|
|
@@ -1631,6 +1659,176 @@ function pagePath(url) {
|
|
|
1631
1659
|
}
|
|
1632
1660
|
}
|
|
1633
1661
|
|
|
1662
|
+
// ../engine/src/tokens-css.ts
|
|
1663
|
+
var cssVar = (path) => `--${path.replace(/\./g, "-")}`;
|
|
1664
|
+
function generateTokensCss(tokens, opts = {}) {
|
|
1665
|
+
const flat = flattenTokens(tokens);
|
|
1666
|
+
const head = `/* Generated by Brandrail from the ${opts.org ? `${opts.org} ` : ""}Brand Kit${opts.version ? ` v${opts.version}` : ""}. Do not edit: run \`npx brandrail init --from-api\` to update. */`;
|
|
1667
|
+
const light = flat.map((t) => ` ${cssVar(t.path)}: ${t.type === "fontFamily" ? t.value : t.value};`);
|
|
1668
|
+
const dark = flat.filter((t) => t.dark).map((t) => ` ${cssVar(t.path)}: ${t.dark};`);
|
|
1669
|
+
const out = [head, "", ":root {", ...light, "}"];
|
|
1670
|
+
if (dark.length) {
|
|
1671
|
+
out.push("", '[data-theme="dark"] {', ...dark, "}", "", "@media (prefers-color-scheme: dark) {", ' :root:not([data-theme="light"]) {', ...dark.map((l) => ` ${l}`), " }", "}");
|
|
1672
|
+
}
|
|
1673
|
+
return `${out.join("\n")}
|
|
1674
|
+
`;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// ../engine/src/kit-bridge.ts
|
|
1678
|
+
var ROLE_PATTERNS = {
|
|
1679
|
+
canvas: [/canvas\.default$/, /^color\.(bg|background)(\.default)?$/, /surface(\.1|\.default)?$/, /canvas$/, /(bg|background)/],
|
|
1680
|
+
subtle: [/canvas\.subtle$/, /(bg|background)\.subtle$/, /surface\.2$/, /fill\.1$/, /subtle$/],
|
|
1681
|
+
fg: [/fg\.default$/, /text\.(default|primary|dark-primary)$/, /^color\.(fg|text|foreground)$/, /fg|text/],
|
|
1682
|
+
muted: [/fg\.muted$/, /text\.(muted|secondary|dark-secondary)$/, /muted|secondary/],
|
|
1683
|
+
border: [/border\.default$/, /^color\.(base\.)?border$/, /border/],
|
|
1684
|
+
primary: [/action\.primary$/, /^color\.(brand\.)?primary$/, /brand(-active)?$/, /primary/, /brand/],
|
|
1685
|
+
accent: [/accent(\.default)?$/, /link/, /info/, /accent/],
|
|
1686
|
+
danger: [/danger(\.default|-active)?$/, /error/, /danger/]
|
|
1687
|
+
};
|
|
1688
|
+
var ROLE_FALLBACK = {
|
|
1689
|
+
canvas: ["#ffffff", "#0d1117"],
|
|
1690
|
+
subtle: ["#f6f8fa", "#151b23"],
|
|
1691
|
+
fg: ["#1f2328", "#f0f6fc"],
|
|
1692
|
+
muted: ["#59636e", "#9198a1"],
|
|
1693
|
+
border: ["#d1d9e0", "#3d444d"],
|
|
1694
|
+
primary: ["#1f883d", "#238636"],
|
|
1695
|
+
accent: ["#0969da", "#4493f8"],
|
|
1696
|
+
danger: ["#cf222e", "#f85149"]
|
|
1697
|
+
};
|
|
1698
|
+
var HEX = /^#(?:[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
1699
|
+
function pickColorRoles(colors, dark = false) {
|
|
1700
|
+
const out = {};
|
|
1701
|
+
for (const role of Object.keys(ROLE_PATTERNS)) {
|
|
1702
|
+
let hit;
|
|
1703
|
+
for (const re of ROLE_PATTERNS[role]) {
|
|
1704
|
+
hit = colors.find((c) => re.test(c.path) && HEX.test(c.value));
|
|
1705
|
+
if (hit) break;
|
|
1706
|
+
}
|
|
1707
|
+
const fb = ROLE_FALLBACK[role][dark ? 1 : 0];
|
|
1708
|
+
if (!hit) out[role] = { value: fb };
|
|
1709
|
+
else if (!dark) out[role] = { value: hit.value, path: hit.path };
|
|
1710
|
+
else if (hit.dark) out[role] = { value: hit.dark, path: hit.path };
|
|
1711
|
+
else out[role] = role === "primary" || role === "accent" || role === "danger" ? { value: hit.value, path: hit.path } : { value: fb, path: hit.path };
|
|
1712
|
+
}
|
|
1713
|
+
return out;
|
|
1714
|
+
}
|
|
1715
|
+
var KIT_SPACING = { 3: 4, 4: 6, 5: 8, 6: 10, 7: 12, 8: 16, 9: 20, 10: 24, 11: 28, 12: 32, 13: 36, 14: 40, 15: 44, 16: 48, 17: 56, 18: 64 };
|
|
1716
|
+
var KIT_RADIUS = { 1: 2, 2: 4, 3: 6, 4: 8, 5: 10, 6: 12, 7: 16, 8: 22, 9: 28 };
|
|
1717
|
+
var KIT_RADIUS_NAME = { 2: "sm", 3: "md", 6: "lg" };
|
|
1718
|
+
var KIT_TYPE = [
|
|
1719
|
+
["Xs", 12, 18],
|
|
1720
|
+
["Sm", 14, 22],
|
|
1721
|
+
["Base", 16, 24],
|
|
1722
|
+
["Md", 16, 24],
|
|
1723
|
+
["Lg", 18, 28],
|
|
1724
|
+
["Xl", 20, 32],
|
|
1725
|
+
["2xl", 24, 38],
|
|
1726
|
+
["3xl", 30, 42],
|
|
1727
|
+
["4xl", 36, 52]
|
|
1728
|
+
];
|
|
1729
|
+
var KIT_THEMES = ["brandrail", "themeA", "themeB"];
|
|
1730
|
+
var px2 = (v) => {
|
|
1731
|
+
const m = v.trim().match(/^(-?\d*\.?\d+)(px|rem)?$/);
|
|
1732
|
+
return m ? Number.parseFloat(m[1]) * (m[2] === "rem" ? 16 : 1) : null;
|
|
1733
|
+
};
|
|
1734
|
+
var nearest2 = (values, v) => values.reduce((best, x) => Math.abs(x - v) < Math.abs(best - v) ? x : best, values[0]);
|
|
1735
|
+
function luminance(hex) {
|
|
1736
|
+
const n = hex.replace("#", "").slice(0, 6);
|
|
1737
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(n.slice(i, i + 2), 16) / 255).map((c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
1738
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1739
|
+
}
|
|
1740
|
+
var contrast = (a, b) => {
|
|
1741
|
+
const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);
|
|
1742
|
+
return (x + 0.05) / (y + 0.05);
|
|
1743
|
+
};
|
|
1744
|
+
function onColor(bg, candidates) {
|
|
1745
|
+
return candidates.filter((c) => HEX.test(c)).sort((a, b) => contrast(b, bg) - contrast(a, bg))[0] ?? "#ffffff";
|
|
1746
|
+
}
|
|
1747
|
+
function scalesOf(flat) {
|
|
1748
|
+
const leaf = (p) => p.split(".").pop();
|
|
1749
|
+
const spacing = flat.filter((t) => /^(space|spacing)\./.test(t.path) && !/\.unit$/.test(t.path)).map((t) => px2(t.value)).filter((v) => v !== null && v > 0 && v < 999);
|
|
1750
|
+
const unit = flat.find((t) => /^space\.unit$/.test(t.path));
|
|
1751
|
+
if (!spacing.length && unit && px2(unit.value)) for (const m of [0.5, 1, 1.5, 2, 3, 4, 5, 6]) spacing.push(px2(unit.value) * m);
|
|
1752
|
+
const radii = flat.filter((t) => /^radius\./.test(t.path)).map((t) => ({ name: leaf(t.path), value: px2(t.value) ?? -1 })).filter((r) => r.value > 0 && r.value < 999);
|
|
1753
|
+
const lineHeights = new Map(flat.filter((t) => /^font\.line-height\./.test(t.path)).map((t) => [leaf(t.path), px2(t.value) ?? void 0]));
|
|
1754
|
+
const type = flat.filter((t) => /^font\.size\./.test(t.path)).map((t) => ({ name: leaf(t.path), size: px2(t.value) ?? 0, lineHeight: lineHeights.get(leaf(t.path)) })).filter((t) => t.size > 0);
|
|
1755
|
+
const font = flat.find((t) => t.type === "fontFamily" || /^font\.family\./.test(t.path))?.value;
|
|
1756
|
+
return { spacing, radii, type, font };
|
|
1757
|
+
}
|
|
1758
|
+
function scaleVars(s) {
|
|
1759
|
+
const out = [];
|
|
1760
|
+
if (s.spacing.length) for (const [n, v] of Object.entries(KIT_SPACING)) out.push(`--spacing${n}: ${nearest2(s.spacing, v)}px;`);
|
|
1761
|
+
if (s.radii.length) {
|
|
1762
|
+
const values = s.radii.map((r) => r.value);
|
|
1763
|
+
for (const [n, v] of Object.entries(KIT_RADIUS)) {
|
|
1764
|
+
const named = s.radii.find((r) => r.name === KIT_RADIUS_NAME[Number(n)]);
|
|
1765
|
+
out.push(`--defaultRadius-rad${n}: ${named ? named.value : nearest2(values, v)}px;`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
if (s.type.length) {
|
|
1769
|
+
const sizes = s.type.map((t) => t.size);
|
|
1770
|
+
for (const [name, size, lh] of KIT_TYPE) {
|
|
1771
|
+
const step = s.type.find((t) => t.name.toLowerCase() === name.toLowerCase()) ?? s.type.find((t) => t.size === nearest2(sizes, size));
|
|
1772
|
+
out.push(`--fontA-fontSize${name}: ${step.size}px;`, `--fontA-lineHeight${name}: ${step.lineHeight ?? Math.round(step.size * lh / size)}px;`);
|
|
1773
|
+
}
|
|
1774
|
+
const base = s.type.find((t) => t.name === "base") ?? s.type.find((t) => t.size === nearest2(sizes, 14));
|
|
1775
|
+
out.push(`--fontSizeBase: ${base.size}px;`);
|
|
1776
|
+
}
|
|
1777
|
+
return out;
|
|
1778
|
+
}
|
|
1779
|
+
function colorVars(colors, dark, font) {
|
|
1780
|
+
const r = pickColorRoles(colors, dark);
|
|
1781
|
+
const find = (re) => {
|
|
1782
|
+
const c = colors.find((x) => re.test(x.path));
|
|
1783
|
+
return c ? dark ? c.dark ?? c.value : c.value : void 0;
|
|
1784
|
+
};
|
|
1785
|
+
const success = find(/success/);
|
|
1786
|
+
const warning = find(/warning|attention/);
|
|
1787
|
+
const mix = (a, b, pct) => `color-mix(in srgb, ${a} ${pct}%, ${b})`;
|
|
1788
|
+
const v = {
|
|
1789
|
+
baseSurface1: r.canvas.value,
|
|
1790
|
+
baseSurface2: r.subtle.value,
|
|
1791
|
+
baseSurface3: mix(r.subtle.value, r.border.value, 70),
|
|
1792
|
+
baseFill1: r.subtle.value,
|
|
1793
|
+
baseFill2: mix(r.subtle.value, r.border.value, 50),
|
|
1794
|
+
baseFill3: r.border.value,
|
|
1795
|
+
baseFill4: mix(r.muted.value, r.border.value, 60),
|
|
1796
|
+
baseFill5: r.muted.value,
|
|
1797
|
+
baseBorder: r.border.value,
|
|
1798
|
+
baseDisabled: mix(r.fg.value, "transparent", 28),
|
|
1799
|
+
textDarkPrimary: r.fg.value,
|
|
1800
|
+
textDarkSecondary: r.muted.value,
|
|
1801
|
+
textDarkTertiary: mix(r.muted.value, r.canvas.value, 75),
|
|
1802
|
+
textLightPrimary: onColor(r.primary.value, [pickColorRoles(colors, false).canvas.value, pickColorRoles(colors, false).fg.value]),
|
|
1803
|
+
stateBrandActive: r.primary.value,
|
|
1804
|
+
stateBrandHover: mix(r.primary.value, "black", 88),
|
|
1805
|
+
stateBrandSelected: mix(r.primary.value, "black", 75),
|
|
1806
|
+
stateDangerActive: r.danger.value,
|
|
1807
|
+
stateDangerHover: mix(r.danger.value, "black", 85),
|
|
1808
|
+
stateDangerSelected: mix(r.danger.value, r.canvas.value, 14),
|
|
1809
|
+
stateHighlightActive: r.accent.value,
|
|
1810
|
+
stateHighlightHover: mix(r.accent.value, r.canvas.value, 85),
|
|
1811
|
+
stateHighlightSelected: mix(r.accent.value, r.canvas.value, 14)
|
|
1812
|
+
};
|
|
1813
|
+
if (success) Object.assign(v, { stateSuccessActive: success, stateSuccessHover: mix(success, "black", 88), stateSuccessSelected: mix(success, r.canvas.value, 14) });
|
|
1814
|
+
if (warning) Object.assign(v, { stateWarningActive: warning, stateWarningHover: mix(warning, "black", 88), stateWarningSelected: mix(warning, r.canvas.value, 14) });
|
|
1815
|
+
if (font) Object.assign(v, { "fontA-fontFamily": font, "fontB-fontFamily": font });
|
|
1816
|
+
return Object.entries(v).map(([k, val]) => `--${k}: ${val};`);
|
|
1817
|
+
}
|
|
1818
|
+
function kitBridgeCss(tokens, opts = {}) {
|
|
1819
|
+
const flat = flattenTokens(tokens);
|
|
1820
|
+
const colors = flat.filter((t) => t.type === "color").map((t) => ({ path: t.path, value: t.value, dark: t.dark }));
|
|
1821
|
+
const sc = scalesOf(flat);
|
|
1822
|
+
const scales = scaleVars(sc);
|
|
1823
|
+
const block = (sel2, lines) => lines.length ? `${sel2} {
|
|
1824
|
+
${lines.join("\n ")}
|
|
1825
|
+
}` : "";
|
|
1826
|
+
const themes = `:is(${KIT_THEMES.map((t) => `.${t}`).join(", ")})`;
|
|
1827
|
+
const s = opts.scope;
|
|
1828
|
+
const sel = s ? { scale: s, light: `${s}, ${s} .light`, dark: `${s} .dark, ${s}.dark` } : { scale: "html:root", light: `html:root, html:root ${themes}.light`, dark: `html:root ${themes}.dark` };
|
|
1829
|
+
return [block(sel.scale, scales), block(sel.light, colorVars(colors, false, sc.font)), block(sel.dark, colorVars(colors, true, sc.font))].filter(Boolean).join("\n");
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1634
1832
|
// src/api.ts
|
|
1635
1833
|
var DEFAULT_API = "https://web-phi-nine-77.vercel.app";
|
|
1636
1834
|
function apiBase() {
|
|
@@ -1834,6 +2032,7 @@ Options for init
|
|
|
1834
2032
|
--no-rules Don't write AGENTS.md (Codex), CLAUDE.md, Cursor, Copilot, Windsurf, Gemini rule files
|
|
1835
2033
|
--mcp Also add the Brandrail MCP server to .mcp.json, .cursor/mcp.json, .vscode/mcp.json
|
|
1836
2034
|
and print the Codex / Windsurf config
|
|
2035
|
+
--tokens-css <file> Where to write the CSS variables (default src/brandrail.tokens.css); --no-tokens-css to skip
|
|
1837
2036
|
--force Overwrite an existing brandrail.config.json
|
|
1838
2037
|
|
|
1839
2038
|
Options for scan
|
|
@@ -1863,7 +2062,7 @@ function parseArgs(argv) {
|
|
|
1863
2062
|
const [k, v] = a.slice(2).split("=", 2);
|
|
1864
2063
|
const next = rest[i + 1];
|
|
1865
2064
|
if (v !== void 0) flags[k] = v;
|
|
1866
|
-
else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width"].includes(k)) {
|
|
2065
|
+
else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width", "tokens-css"].includes(k)) {
|
|
1867
2066
|
flags[k] = next;
|
|
1868
2067
|
i++;
|
|
1869
2068
|
} else flags[k] = true;
|
|
@@ -1934,8 +2133,25 @@ Brand score ${report.score} is below ${threshold}.`);
|
|
|
1934
2133
|
}
|
|
1935
2134
|
return 0;
|
|
1936
2135
|
}
|
|
1937
|
-
function
|
|
1938
|
-
|
|
2136
|
+
function tokensCssPath(root, config, flags) {
|
|
2137
|
+
if (typeof flags["tokens-css"] === "string") return flags["tokens-css"];
|
|
2138
|
+
if (config.tokensCss) return config.tokensCss;
|
|
2139
|
+
return existsSync3(join3(root, "src")) ? "src/brandrail.tokens.css" : "brandrail.tokens.css";
|
|
2140
|
+
}
|
|
2141
|
+
function writeTokensCss(root, rel, config, org) {
|
|
2142
|
+
const abs = join3(root, rel);
|
|
2143
|
+
mkdirSync(dirname2(abs), { recursive: true });
|
|
2144
|
+
let css = generateTokensCss(config.tokens, { version: config.tokenVersion, org });
|
|
2145
|
+
const usesKit = (config.components ?? []).some((c) => c.importPath === "@brandrail/kit");
|
|
2146
|
+
if (usesKit) css += `
|
|
2147
|
+
/* @brandrail/kit follows the Brand Kit: colours, font, spacing, radius and type scale. */
|
|
2148
|
+
${kitBridgeCss(config.tokens)}
|
|
2149
|
+
`;
|
|
2150
|
+
writeFileSync(abs, css);
|
|
2151
|
+
return usesKit;
|
|
2152
|
+
}
|
|
2153
|
+
function writeRuleFiles(root, config, org, tokensCss) {
|
|
2154
|
+
const md = generateInstructions(config, { org, tokensCss });
|
|
1939
2155
|
const written = [];
|
|
1940
2156
|
const write = (rel, content) => {
|
|
1941
2157
|
const abs = join3(root, rel);
|
|
@@ -2018,12 +2234,22 @@ async function cmdInit(cwd, flags) {
|
|
|
2018
2234
|
if (existsSync3(target) && !flags.force && !flags["from-api"]) {
|
|
2019
2235
|
log(`${CONFIG_FILE} already exists (use --force to overwrite). Updating rule files from it.`);
|
|
2020
2236
|
config = readConfig(target);
|
|
2237
|
+
org ??= config.org;
|
|
2021
2238
|
} else {
|
|
2239
|
+
if (existsSync3(target)) {
|
|
2240
|
+
const prev = readConfig(target);
|
|
2241
|
+
config = { ...prev, ...config, rules: { ...prev.rules, ...config.rules }, ignore: prev.ignore ?? config.ignore, tokensCss: prev.tokensCss };
|
|
2242
|
+
}
|
|
2022
2243
|
writeFileSync(target, `${JSON.stringify(config, null, 2)}
|
|
2023
2244
|
`);
|
|
2024
2245
|
log(`\u2714 wrote ${CONFIG_FILE}${org ? ` from ${org}'s Brand Kit` : " (starter tokens \u2014 edit them or use --from-api)"}`);
|
|
2025
2246
|
}
|
|
2026
|
-
|
|
2247
|
+
const css = flags["tokens-css"] === false ? void 0 : tokensCssPath(cwd, config, flags);
|
|
2248
|
+
if (css) {
|
|
2249
|
+
const kit = writeTokensCss(cwd, css, config, org);
|
|
2250
|
+
log(`\u2714 wrote ${css} (the Brand Kit as CSS variables${kit ? ", and @brandrail/kit themed from it" : ""} \u2014 import it once at your app entry)`);
|
|
2251
|
+
}
|
|
2252
|
+
if (flags.rules !== false) for (const f of writeRuleFiles(cwd, config, org, css)) log(`\u2714 updated ${f}`);
|
|
2027
2253
|
if (flags.mcp) writeMcpConfigs(cwd);
|
|
2028
2254
|
log("\nNext: npx brandrail check");
|
|
2029
2255
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brandrail",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Keep AI-generated UI on-brand: checks code and live pages against your design system, and wires your Brand Kit into Claude Code, Cursor, Copilot and Codex.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"design-system",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"license": "UNLICENSED",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"bin": {
|
|
26
|
-
"brandrail": "
|
|
26
|
+
"brandrail": "dist/brandrail.mjs"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
@@ -35,16 +35,15 @@
|
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/brandrail.mjs --external:playwright --external:playwright-core --banner:js='#!/usr/bin/env node'",
|
|
40
|
-
"typecheck": "tsc --noEmit",
|
|
41
|
-
"prepublishOnly": "pnpm run build"
|
|
42
|
-
},
|
|
43
38
|
"devDependencies": {
|
|
44
|
-
"@brandrail/engine": "workspace:*",
|
|
45
39
|
"@types/node": "^22",
|
|
46
40
|
"esbuild": "^0.28.2",
|
|
47
41
|
"playwright-core": "1.63.0",
|
|
48
|
-
"typescript": "5.9.3"
|
|
42
|
+
"typescript": "5.9.3",
|
|
43
|
+
"@brandrail/engine": "0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/brandrail.mjs --external:playwright --external:playwright-core --banner:js='#!/usr/bin/env node' && chmod +x dist/brandrail.mjs",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
49
48
|
}
|
|
50
|
-
}
|
|
49
|
+
}
|