brandrail 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brandrail.mjs +301 -16
- package/package.json +8 -9
package/dist/brandrail.mjs
CHANGED
|
@@ -484,6 +484,114 @@ var imgAlt = {
|
|
|
484
484
|
return out;
|
|
485
485
|
}
|
|
486
486
|
};
|
|
487
|
+
var NATIVE_ATTRS = new Set(
|
|
488
|
+
"id className style title role tabIndex hidden lang dir name value defaultValue defaultChecked checked disabled required readOnly autoFocus autoComplete placeholder type min max step maxLength minLength pattern form accept multiple href target rel download children".split(" ")
|
|
489
|
+
);
|
|
490
|
+
var ALWAYS_OK = /* @__PURE__ */ new Set(["key", "ref"]);
|
|
491
|
+
function importedComponents(code, components) {
|
|
492
|
+
const out = /* @__PURE__ */ new Map();
|
|
493
|
+
for (const m of matches(/\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\2/g, code)) {
|
|
494
|
+
const from = m[3];
|
|
495
|
+
for (const part of m[1].split(",")) {
|
|
496
|
+
const [orig, alias] = part.trim().replace(/^type\s+/, "").split(/\s+as\s+/).map((s) => s.trim());
|
|
497
|
+
if (!orig) continue;
|
|
498
|
+
const comp = components.find((c) => c.name === orig && c.importPath === from && c.props?.length);
|
|
499
|
+
if (comp) out.set(alias || orig, comp);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
function skipBalanced(code, i) {
|
|
505
|
+
const open = code[i];
|
|
506
|
+
if (open === '"' || open === "'") {
|
|
507
|
+
const end = code.indexOf(open, i + 1);
|
|
508
|
+
return end === -1 ? code.length : end + 1;
|
|
509
|
+
}
|
|
510
|
+
let depth = 0;
|
|
511
|
+
let quote = null;
|
|
512
|
+
for (let j = i; j < code.length; j++) {
|
|
513
|
+
const c = code[j];
|
|
514
|
+
if (quote) {
|
|
515
|
+
if (c === "\\") j++;
|
|
516
|
+
else if (c === quote) quote = null;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (c === '"' || c === "'" || c === "`") quote = c;
|
|
520
|
+
else if (c === "{") depth++;
|
|
521
|
+
else if (c === "}" && --depth === 0) return j + 1;
|
|
522
|
+
}
|
|
523
|
+
return code.length;
|
|
524
|
+
}
|
|
525
|
+
function parseAttributes(code, start) {
|
|
526
|
+
const attrs = [];
|
|
527
|
+
let spread = false;
|
|
528
|
+
let i = start;
|
|
529
|
+
while (i < code.length) {
|
|
530
|
+
while (/\s/.test(code[i] ?? "")) i++;
|
|
531
|
+
const c = code[i];
|
|
532
|
+
if (c === void 0) return null;
|
|
533
|
+
if (c === ">") return { attrs, spread, selfClosing: false };
|
|
534
|
+
if (c === "/" && code[i + 1] === ">") return { attrs, spread, selfClosing: true };
|
|
535
|
+
if (c === "{") {
|
|
536
|
+
spread = true;
|
|
537
|
+
i = skipBalanced(code, i);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const name = /^[A-Za-z_$][\w$:.-]*/.exec(code.slice(i, i + 80))?.[0];
|
|
541
|
+
if (!name) return null;
|
|
542
|
+
attrs.push({ name, offset: i });
|
|
543
|
+
i += name.length;
|
|
544
|
+
while (/\s/.test(code[i] ?? "")) i++;
|
|
545
|
+
if (code[i] === "=") {
|
|
546
|
+
i++;
|
|
547
|
+
while (/\s/.test(code[i] ?? "")) i++;
|
|
548
|
+
i = skipBalanced(code, i);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
var componentProps = {
|
|
554
|
+
id: "component-props",
|
|
555
|
+
title: "Unknown or missing prop",
|
|
556
|
+
description: "Design system components only accept their documented props, and required props must be passed.",
|
|
557
|
+
defaultSeverity: "error",
|
|
558
|
+
kinds: ["jsx"],
|
|
559
|
+
check({ code, config }) {
|
|
560
|
+
if (!config.components.some((c) => c.props?.length)) return [];
|
|
561
|
+
const imported = importedComponents(code, config.components);
|
|
562
|
+
if (imported.size === 0) return [];
|
|
563
|
+
const out = [];
|
|
564
|
+
const names = [...imported.keys()].map((n) => n.replace(/[$]/g, "\\$")).join("|");
|
|
565
|
+
for (const m of matches(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
|
|
566
|
+
if (/[\w)\]]\s*$/.test(code.slice(Math.max(0, m.index - 20), m.index))) continue;
|
|
567
|
+
const comp = imported.get(m[1]);
|
|
568
|
+
const parsed = parseAttributes(code, m.index + m[0].length);
|
|
569
|
+
if (!parsed) continue;
|
|
570
|
+
const own = new Map(comp.props.map((p) => [p.name, p]));
|
|
571
|
+
const list = comp.props.map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
|
|
572
|
+
for (const a of parsed.attrs) {
|
|
573
|
+
if (own.has(a.name) || ALWAYS_OK.has(a.name) || a.name.includes("-")) continue;
|
|
574
|
+
if (comp.htmlAttributes && (NATIVE_ATTRS.has(a.name) || /^on[A-Z]/.test(a.name) || /^aria/.test(a.name))) continue;
|
|
575
|
+
const sibling = config.components.find(
|
|
576
|
+
(c) => c !== comp && c.importPath === comp.importPath && (c.name.startsWith(comp.name) || comp.name.startsWith(c.name)) && c.props?.some((p) => p.name === a.name)
|
|
577
|
+
);
|
|
578
|
+
out.push({
|
|
579
|
+
offset: a.offset,
|
|
580
|
+
message: `<${comp.name}> has no "${a.name}" prop`,
|
|
581
|
+
suggestion: sibling ? `Use <${sibling.name}>, which has "${a.name}", or remove it. ${comp.name} props: ${list}` : `Remove it. ${comp.name} props: ${list}`
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
if (!parsed.spread) {
|
|
585
|
+
const passed = new Set(parsed.attrs.map((a) => a.name));
|
|
586
|
+
for (const p of comp.props) {
|
|
587
|
+
if (!p.required || passed.has(p.name) || p.name === "children" && !parsed.selfClosing) continue;
|
|
588
|
+
out.push({ offset: m.index, message: `<${comp.name}> is missing the required "${p.name}" prop`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
487
595
|
var RULES = [
|
|
488
596
|
noHardcodedColor,
|
|
489
597
|
noRawPaletteClass,
|
|
@@ -492,7 +600,8 @@ var RULES = [
|
|
|
492
600
|
spacingScale,
|
|
493
601
|
fontFamily,
|
|
494
602
|
radiusScale,
|
|
495
|
-
imgAlt
|
|
603
|
+
imgAlt,
|
|
604
|
+
componentProps
|
|
496
605
|
];
|
|
497
606
|
var RULE_IDS = RULES.map((r) => r.id);
|
|
498
607
|
|
|
@@ -604,7 +713,7 @@ function makeIgnore(globs) {
|
|
|
604
713
|
}
|
|
605
714
|
|
|
606
715
|
// ../engine/src/check.ts
|
|
607
|
-
var ENGINE_VERSION = "0.
|
|
716
|
+
var ENGINE_VERSION = "0.3.1";
|
|
608
717
|
var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
|
|
609
718
|
function computeScore(counts, lines) {
|
|
610
719
|
const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
|
|
@@ -765,7 +874,9 @@ var starterConfig = {
|
|
|
765
874
|
subtle: { $type: "color", $value: "#f6f8fa", $description: "Headers, wells" }
|
|
766
875
|
},
|
|
767
876
|
border: { default: { $type: "color", $value: "#d1d9e0", $description: "Borders and dividers" } },
|
|
768
|
-
danger: { default: { $type: "color", $value: "#cf222e", $description: "Destructive actions, errors" } }
|
|
877
|
+
danger: { default: { $type: "color", $value: "#cf222e", $description: "Destructive actions, errors" } },
|
|
878
|
+
success: { default: { $type: "color", $value: "#1a7f37", $description: "Success messages, completed states" } },
|
|
879
|
+
attention: { default: { $type: "color", $value: "#9a6700", $description: "Warnings, needs attention" } }
|
|
769
880
|
},
|
|
770
881
|
font: {
|
|
771
882
|
family: { sans: { $type: "fontFamily", $value: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif" } },
|
|
@@ -1252,6 +1363,7 @@ function mergeTokens(base, incoming) {
|
|
|
1252
1363
|
}
|
|
1253
1364
|
|
|
1254
1365
|
// ../engine/src/instructions.ts
|
|
1366
|
+
var KIT_PACKAGE = "@brandrail/kit";
|
|
1255
1367
|
var RULES_START = "<!-- brandrail:start -->";
|
|
1256
1368
|
var RULES_END = "<!-- brandrail:end -->";
|
|
1257
1369
|
function generateInstructions(config, opts = {}) {
|
|
@@ -1263,9 +1375,15 @@ function generateInstructions(config, opts = {}) {
|
|
|
1263
1375
|
`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.`
|
|
1264
1376
|
);
|
|
1265
1377
|
lines.push("");
|
|
1266
|
-
|
|
1378
|
+
const usesKit = r.components.some((c) => c.importPath === KIT_PACKAGE);
|
|
1379
|
+
if (opts.tokensCss || usesKit) {
|
|
1267
1380
|
lines.push("## Setup");
|
|
1268
|
-
|
|
1381
|
+
if (opts.tokensCss) {
|
|
1382
|
+
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.`);
|
|
1383
|
+
}
|
|
1384
|
+
if (usesKit) {
|
|
1385
|
+
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.`);
|
|
1386
|
+
}
|
|
1269
1387
|
lines.push("");
|
|
1270
1388
|
}
|
|
1271
1389
|
if (r.components.length) {
|
|
@@ -1273,7 +1391,12 @@ function generateInstructions(config, opts = {}) {
|
|
|
1273
1391
|
lines.push("Always use these components. Never re-create them with raw HTML elements:");
|
|
1274
1392
|
for (const c of r.components) {
|
|
1275
1393
|
const tags = [...r.replaces].filter(([, v]) => v === c).map(([t]) => `<${t}>`);
|
|
1276
|
-
|
|
1394
|
+
const props = c.props?.length ? ` \u2014 props: ${c.props.map((p) => p.required ? `${p.name}*` : p.name).join(", ")}` : "";
|
|
1395
|
+
lines.push(`- \`${c.name}\` from \`${c.importPath}\`${tags.length ? ` (instead of ${tags.join(", ")})` : ""}${props}`);
|
|
1396
|
+
}
|
|
1397
|
+
if (r.components.some((c) => c.props?.length)) {
|
|
1398
|
+
lines.push("");
|
|
1399
|
+
lines.push("Pass only the props listed for each component (* = required). Any other prop is a type error and fails the check: if you need something a component doesn't take, look for a related component (often one with a longer name, like AvatarLabel next to Avatar) or compose it with design tokens.");
|
|
1277
1400
|
}
|
|
1278
1401
|
lines.push("");
|
|
1279
1402
|
}
|
|
@@ -1387,7 +1510,7 @@ var RULE_FILES = [
|
|
|
1387
1510
|
// ../engine/src/rendered.ts
|
|
1388
1511
|
function collectRenderedNodes(maxNodes = 3e3) {
|
|
1389
1512
|
const SKIP = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "IFRAME", "HEAD", "META", "LINK", "BR", "WBR", "SOURCE", "TRACK"]);
|
|
1390
|
-
const
|
|
1513
|
+
const px3 = (v) => {
|
|
1391
1514
|
const n = parseFloat(v);
|
|
1392
1515
|
return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0;
|
|
1393
1516
|
};
|
|
@@ -1435,8 +1558,8 @@ function collectRenderedNodes(maxNodes = 3e3) {
|
|
|
1435
1558
|
const r = el.getBoundingClientRect();
|
|
1436
1559
|
if (r.width < 1 || r.height < 1) continue;
|
|
1437
1560
|
const text = Array.from(el.childNodes).filter((n) => n.nodeType === 3).map((n) => n.textContent ?? "").join(" ").replace(/\s+/g, " ").trim();
|
|
1438
|
-
const borderW = Math.max(
|
|
1439
|
-
const borderSide =
|
|
1561
|
+
const borderW = Math.max(px3(cs.borderTopWidth), px3(cs.borderRightWidth), px3(cs.borderBottomWidth), px3(cs.borderLeftWidth));
|
|
1562
|
+
const borderSide = px3(cs.borderTopWidth) > 0 ? cs.borderTopColor : px3(cs.borderBottomWidth) > 0 ? cs.borderBottomColor : px3(cs.borderLeftWidth) > 0 ? cs.borderLeftColor : cs.borderRightColor;
|
|
1440
1563
|
nodes.push({
|
|
1441
1564
|
selector: selectorOf(el),
|
|
1442
1565
|
tag: el.tagName.toLowerCase(),
|
|
@@ -1446,12 +1569,12 @@ function collectRenderedNodes(maxNodes = 3e3) {
|
|
|
1446
1569
|
borderColor: borderW > 0 && cs.borderTopStyle !== "none" ? borderSide : void 0,
|
|
1447
1570
|
effectiveBackground: effectiveBg(el),
|
|
1448
1571
|
fontFamily: cs.fontFamily,
|
|
1449
|
-
fontSize:
|
|
1572
|
+
fontSize: px3(cs.fontSize),
|
|
1450
1573
|
fontWeight: parseInt(cs.fontWeight, 10) || 400,
|
|
1451
|
-
radius: Math.max(
|
|
1574
|
+
radius: Math.max(px3(cs.borderTopLeftRadius), px3(cs.borderTopRightRadius), px3(cs.borderBottomLeftRadius), px3(cs.borderBottomRightRadius)),
|
|
1452
1575
|
minSide: Math.round(Math.min(r.width, r.height)),
|
|
1453
|
-
padding: [
|
|
1454
|
-
gap: cs.display.includes("flex") || cs.display.includes("grid") ? [
|
|
1576
|
+
padding: [px3(cs.paddingTop), px3(cs.paddingRight), px3(cs.paddingBottom), px3(cs.paddingLeft)],
|
|
1577
|
+
gap: cs.display.includes("flex") || cs.display.includes("grid") ? [px3(cs.rowGap), px3(cs.columnGap)] : []
|
|
1455
1578
|
});
|
|
1456
1579
|
}
|
|
1457
1580
|
const links = Array.from(document.querySelectorAll("a[href]")).map((a) => a.href).filter((h) => {
|
|
@@ -1667,6 +1790,161 @@ function generateTokensCss(tokens, opts = {}) {
|
|
|
1667
1790
|
`;
|
|
1668
1791
|
}
|
|
1669
1792
|
|
|
1793
|
+
// ../engine/src/kit-bridge.ts
|
|
1794
|
+
var ROLE_PATTERNS = {
|
|
1795
|
+
canvas: [/canvas\.default$/, /^color\.(bg|background)(\.default)?$/, /surface(\.1|\.default)?$/, /canvas$/, /(bg|background)/],
|
|
1796
|
+
subtle: [/canvas\.subtle$/, /(bg|background)\.subtle$/, /surface\.2$/, /fill\.1$/, /subtle$/],
|
|
1797
|
+
fg: [/fg\.default$/, /text\.(default|primary|dark-primary)$/, /^color\.(fg|text|foreground)$/, /fg|text/],
|
|
1798
|
+
muted: [/fg\.muted$/, /text\.(muted|secondary|dark-secondary)$/, /muted|secondary/],
|
|
1799
|
+
border: [/border\.default$/, /^color\.(base\.)?border$/, /border/],
|
|
1800
|
+
primary: [/action\.primary$/, /^color\.(brand\.)?primary$/, /brand(-active)?$/, /primary/, /brand/],
|
|
1801
|
+
accent: [/accent(\.default)?$/, /link/, /info/, /accent/],
|
|
1802
|
+
danger: [/danger(\.default|-active)?$/, /error/, /danger/]
|
|
1803
|
+
};
|
|
1804
|
+
var ROLE_FALLBACK = {
|
|
1805
|
+
canvas: ["#ffffff", "#0d1117"],
|
|
1806
|
+
subtle: ["#f6f8fa", "#151b23"],
|
|
1807
|
+
fg: ["#1f2328", "#f0f6fc"],
|
|
1808
|
+
muted: ["#59636e", "#9198a1"],
|
|
1809
|
+
border: ["#d1d9e0", "#3d444d"],
|
|
1810
|
+
primary: ["#1f883d", "#238636"],
|
|
1811
|
+
accent: ["#0969da", "#4493f8"],
|
|
1812
|
+
danger: ["#cf222e", "#f85149"]
|
|
1813
|
+
};
|
|
1814
|
+
var HEX = /^#(?:[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
1815
|
+
function pickColorRoles(colors, dark = false) {
|
|
1816
|
+
const out = {};
|
|
1817
|
+
for (const role of Object.keys(ROLE_PATTERNS)) {
|
|
1818
|
+
let hit;
|
|
1819
|
+
for (const re of ROLE_PATTERNS[role]) {
|
|
1820
|
+
hit = colors.find((c) => re.test(c.path) && HEX.test(c.value));
|
|
1821
|
+
if (hit) break;
|
|
1822
|
+
}
|
|
1823
|
+
const fb = ROLE_FALLBACK[role][dark ? 1 : 0];
|
|
1824
|
+
if (!hit) out[role] = { value: fb };
|
|
1825
|
+
else if (!dark) out[role] = { value: hit.value, path: hit.path };
|
|
1826
|
+
else if (hit.dark) out[role] = { value: hit.dark, path: hit.path };
|
|
1827
|
+
else out[role] = role === "primary" || role === "accent" || role === "danger" ? { value: hit.value, path: hit.path } : { value: fb, path: hit.path };
|
|
1828
|
+
}
|
|
1829
|
+
return out;
|
|
1830
|
+
}
|
|
1831
|
+
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 };
|
|
1832
|
+
var KIT_RADIUS = { 1: 2, 2: 4, 3: 6, 4: 8, 5: 10, 6: 12, 7: 16, 8: 22, 9: 28 };
|
|
1833
|
+
var KIT_RADIUS_NAME = { 2: "sm", 3: "md", 6: "lg" };
|
|
1834
|
+
var KIT_TYPE = [
|
|
1835
|
+
["Xs", 12, 18],
|
|
1836
|
+
["Sm", 14, 22],
|
|
1837
|
+
["Base", 16, 24],
|
|
1838
|
+
["Md", 16, 24],
|
|
1839
|
+
["Lg", 18, 28],
|
|
1840
|
+
["Xl", 20, 32],
|
|
1841
|
+
["2xl", 24, 38],
|
|
1842
|
+
["3xl", 30, 42],
|
|
1843
|
+
["4xl", 36, 52]
|
|
1844
|
+
];
|
|
1845
|
+
var KIT_THEMES = ["brandrail", "themeA", "themeB"];
|
|
1846
|
+
var px2 = (v) => {
|
|
1847
|
+
const m = v.trim().match(/^(-?\d*\.?\d+)(px|rem)?$/);
|
|
1848
|
+
return m ? Number.parseFloat(m[1]) * (m[2] === "rem" ? 16 : 1) : null;
|
|
1849
|
+
};
|
|
1850
|
+
var nearest2 = (values, v) => values.reduce((best, x) => Math.abs(x - v) < Math.abs(best - v) ? x : best, values[0]);
|
|
1851
|
+
function luminance(hex) {
|
|
1852
|
+
const n = hex.replace("#", "").slice(0, 6);
|
|
1853
|
+
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);
|
|
1854
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1855
|
+
}
|
|
1856
|
+
var contrast = (a, b) => {
|
|
1857
|
+
const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);
|
|
1858
|
+
return (x + 0.05) / (y + 0.05);
|
|
1859
|
+
};
|
|
1860
|
+
function onColor(bg, candidates) {
|
|
1861
|
+
return candidates.filter((c) => HEX.test(c)).sort((a, b) => contrast(b, bg) - contrast(a, bg))[0] ?? "#ffffff";
|
|
1862
|
+
}
|
|
1863
|
+
function scalesOf(flat) {
|
|
1864
|
+
const leaf = (p) => p.split(".").pop();
|
|
1865
|
+
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);
|
|
1866
|
+
const unit = flat.find((t) => /^space\.unit$/.test(t.path));
|
|
1867
|
+
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);
|
|
1868
|
+
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);
|
|
1869
|
+
const lineHeights = new Map(flat.filter((t) => /^font\.line-height\./.test(t.path)).map((t) => [leaf(t.path), px2(t.value) ?? void 0]));
|
|
1870
|
+
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);
|
|
1871
|
+
const font = flat.find((t) => t.type === "fontFamily" || /^font\.family\./.test(t.path))?.value;
|
|
1872
|
+
return { spacing, radii, type, font };
|
|
1873
|
+
}
|
|
1874
|
+
function scaleVars(s) {
|
|
1875
|
+
const out = [];
|
|
1876
|
+
if (s.spacing.length) for (const [n, v] of Object.entries(KIT_SPACING)) out.push(`--spacing${n}: ${nearest2(s.spacing, v)}px;`);
|
|
1877
|
+
if (s.radii.length) {
|
|
1878
|
+
const values = s.radii.map((r) => r.value);
|
|
1879
|
+
for (const [n, v] of Object.entries(KIT_RADIUS)) {
|
|
1880
|
+
const named = s.radii.find((r) => r.name === KIT_RADIUS_NAME[Number(n)]);
|
|
1881
|
+
out.push(`--defaultRadius-rad${n}: ${named ? named.value : nearest2(values, v)}px;`);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
if (s.type.length) {
|
|
1885
|
+
const sizes = s.type.map((t) => t.size);
|
|
1886
|
+
for (const [name, size, lh] of KIT_TYPE) {
|
|
1887
|
+
const step = s.type.find((t) => t.name.toLowerCase() === name.toLowerCase()) ?? s.type.find((t) => t.size === nearest2(sizes, size));
|
|
1888
|
+
out.push(`--fontA-fontSize${name}: ${step.size}px;`, `--fontA-lineHeight${name}: ${step.lineHeight ?? Math.round(step.size * lh / size)}px;`);
|
|
1889
|
+
}
|
|
1890
|
+
const base = s.type.find((t) => t.name === "base") ?? s.type.find((t) => t.size === nearest2(sizes, 14));
|
|
1891
|
+
out.push(`--fontSizeBase: ${base.size}px;`);
|
|
1892
|
+
}
|
|
1893
|
+
return out;
|
|
1894
|
+
}
|
|
1895
|
+
function colorVars(colors, dark, font) {
|
|
1896
|
+
const r = pickColorRoles(colors, dark);
|
|
1897
|
+
const find = (re) => {
|
|
1898
|
+
const c = colors.find((x) => re.test(x.path));
|
|
1899
|
+
return c ? dark ? c.dark ?? c.value : c.value : void 0;
|
|
1900
|
+
};
|
|
1901
|
+
const success = find(/success/);
|
|
1902
|
+
const warning = find(/warning|attention/);
|
|
1903
|
+
const mix = (a, b, pct) => `color-mix(in srgb, ${a} ${pct}%, ${b})`;
|
|
1904
|
+
const v = {
|
|
1905
|
+
baseSurface1: r.canvas.value,
|
|
1906
|
+
baseSurface2: r.subtle.value,
|
|
1907
|
+
baseSurface3: mix(r.subtle.value, r.border.value, 70),
|
|
1908
|
+
baseFill1: r.subtle.value,
|
|
1909
|
+
baseFill2: mix(r.subtle.value, r.border.value, 50),
|
|
1910
|
+
baseFill3: r.border.value,
|
|
1911
|
+
baseFill4: mix(r.muted.value, r.border.value, 60),
|
|
1912
|
+
baseFill5: r.muted.value,
|
|
1913
|
+
baseBorder: r.border.value,
|
|
1914
|
+
baseDisabled: mix(r.fg.value, "transparent", 28),
|
|
1915
|
+
textDarkPrimary: r.fg.value,
|
|
1916
|
+
textDarkSecondary: r.muted.value,
|
|
1917
|
+
textDarkTertiary: mix(r.muted.value, r.canvas.value, 75),
|
|
1918
|
+
textLightPrimary: onColor(r.primary.value, [pickColorRoles(colors, false).canvas.value, pickColorRoles(colors, false).fg.value]),
|
|
1919
|
+
stateBrandActive: r.primary.value,
|
|
1920
|
+
stateBrandHover: mix(r.primary.value, "black", 88),
|
|
1921
|
+
stateBrandSelected: mix(r.primary.value, "black", 75),
|
|
1922
|
+
stateDangerActive: r.danger.value,
|
|
1923
|
+
stateDangerHover: mix(r.danger.value, "black", 85),
|
|
1924
|
+
stateDangerSelected: mix(r.danger.value, r.canvas.value, 14),
|
|
1925
|
+
stateHighlightActive: r.accent.value,
|
|
1926
|
+
stateHighlightHover: mix(r.accent.value, r.canvas.value, 85),
|
|
1927
|
+
stateHighlightSelected: mix(r.accent.value, r.canvas.value, 14)
|
|
1928
|
+
};
|
|
1929
|
+
if (success) Object.assign(v, { stateSuccessActive: success, stateSuccessHover: mix(success, "black", 88), stateSuccessSelected: mix(success, r.canvas.value, 14) });
|
|
1930
|
+
if (warning) Object.assign(v, { stateWarningActive: warning, stateWarningHover: mix(warning, "black", 88), stateWarningSelected: mix(warning, r.canvas.value, 14) });
|
|
1931
|
+
if (font) Object.assign(v, { "fontA-fontFamily": font, "fontB-fontFamily": font });
|
|
1932
|
+
return Object.entries(v).map(([k, val]) => `--${k}: ${val};`);
|
|
1933
|
+
}
|
|
1934
|
+
function kitBridgeCss(tokens, opts = {}) {
|
|
1935
|
+
const flat = flattenTokens(tokens);
|
|
1936
|
+
const colors = flat.filter((t) => t.type === "color").map((t) => ({ path: t.path, value: t.value, dark: t.dark }));
|
|
1937
|
+
const sc = scalesOf(flat);
|
|
1938
|
+
const scales = scaleVars(sc);
|
|
1939
|
+
const block = (sel2, lines) => lines.length ? `${sel2} {
|
|
1940
|
+
${lines.join("\n ")}
|
|
1941
|
+
}` : "";
|
|
1942
|
+
const themes = `:is(${KIT_THEMES.map((t) => `.${t}`).join(", ")})`;
|
|
1943
|
+
const s = opts.scope;
|
|
1944
|
+
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` };
|
|
1945
|
+
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");
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1670
1948
|
// src/api.ts
|
|
1671
1949
|
var DEFAULT_API = "https://web-phi-nine-77.vercel.app";
|
|
1672
1950
|
function apiBase() {
|
|
@@ -1979,7 +2257,14 @@ function tokensCssPath(root, config, flags) {
|
|
|
1979
2257
|
function writeTokensCss(root, rel, config, org) {
|
|
1980
2258
|
const abs = join3(root, rel);
|
|
1981
2259
|
mkdirSync(dirname2(abs), { recursive: true });
|
|
1982
|
-
|
|
2260
|
+
let css = generateTokensCss(config.tokens, { version: config.tokenVersion, org });
|
|
2261
|
+
const usesKit = (config.components ?? []).some((c) => c.importPath === "@brandrail/kit");
|
|
2262
|
+
if (usesKit) css += `
|
|
2263
|
+
/* @brandrail/kit follows the Brand Kit: colours, font, spacing, radius and type scale. */
|
|
2264
|
+
${kitBridgeCss(config.tokens)}
|
|
2265
|
+
`;
|
|
2266
|
+
writeFileSync(abs, css);
|
|
2267
|
+
return usesKit;
|
|
1983
2268
|
}
|
|
1984
2269
|
function writeRuleFiles(root, config, org, tokensCss) {
|
|
1985
2270
|
const md = generateInstructions(config, { org, tokensCss });
|
|
@@ -2077,8 +2362,8 @@ async function cmdInit(cwd, flags) {
|
|
|
2077
2362
|
}
|
|
2078
2363
|
const css = flags["tokens-css"] === false ? void 0 : tokensCssPath(cwd, config, flags);
|
|
2079
2364
|
if (css) {
|
|
2080
|
-
writeTokensCss(cwd, css, config, org);
|
|
2081
|
-
log(`\u2714 wrote ${css} (the Brand Kit as CSS variables \u2014 import it once at your app entry)`);
|
|
2365
|
+
const kit = writeTokensCss(cwd, css, config, org);
|
|
2366
|
+
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)`);
|
|
2082
2367
|
}
|
|
2083
2368
|
if (flags.rules !== false) for (const f of writeRuleFiles(cwd, config, org, css)) log(`\u2714 updated ${f}`);
|
|
2084
2369
|
if (flags.mcp) writeMcpConfigs(cwd);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brandrail",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -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' && chmod +x dist/brandrail.mjs",
|
|
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
|
+
}
|