brandrail 0.3.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 +120 -4
  2. package/package.json +1 -1
@@ -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.3.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" } },
@@ -1280,7 +1391,12 @@ function generateInstructions(config, opts = {}) {
1280
1391
  lines.push("Always use these components. Never re-create them with raw HTML elements:");
1281
1392
  for (const c of r.components) {
1282
1393
  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(", ")})` : ""}`);
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.");
1284
1400
  }
1285
1401
  lines.push("");
1286
1402
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brandrail",
3
- "version": "0.3.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",