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.
- package/dist/brandrail.mjs +120 -4
- package/package.json +1 -1
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.3.
|
|
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
|
-
|
|
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.
|
|
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",
|