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.
Files changed (2) hide show
  1. package/dist/brandrail.mjs +301 -16
  2. package/package.json +8 -9
@@ -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.2.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
- if (opts.tokensCss) {
1378
+ const usesKit = r.components.some((c) => c.importPath === KIT_PACKAGE);
1379
+ if (opts.tokensCss || usesKit) {
1267
1380
  lines.push("## Setup");
1268
- 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.`);
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
- lines.push(`- \`${c.name}\` from \`${c.importPath}\`${tags.length ? ` (instead of ${tags.join(", ")})` : ""}`);
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 px2 = (v) => {
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(px2(cs.borderTopWidth), px2(cs.borderRightWidth), px2(cs.borderBottomWidth), px2(cs.borderLeftWidth));
1439
- const borderSide = px2(cs.borderTopWidth) > 0 ? cs.borderTopColor : px2(cs.borderBottomWidth) > 0 ? cs.borderBottomColor : px2(cs.borderLeftWidth) > 0 ? cs.borderLeftColor : cs.borderRightColor;
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: px2(cs.fontSize),
1572
+ fontSize: px3(cs.fontSize),
1450
1573
  fontWeight: parseInt(cs.fontWeight, 10) || 400,
1451
- radius: Math.max(px2(cs.borderTopLeftRadius), px2(cs.borderTopRightRadius), px2(cs.borderBottomLeftRadius), px2(cs.borderBottomRightRadius)),
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: [px2(cs.paddingTop), px2(cs.paddingRight), px2(cs.paddingBottom), px2(cs.paddingLeft)],
1454
- gap: cs.display.includes("flex") || cs.display.includes("grid") ? [px2(cs.rowGap), px2(cs.columnGap)] : []
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
- writeFileSync(abs, generateTokensCss(config.tokens, { version: config.tokenVersion, org }));
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.2.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
+ }