brandrail 0.4.0 → 0.4.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 +83 -9
- package/package.json +1 -1
package/dist/brandrail.mjs
CHANGED
|
@@ -723,7 +723,7 @@ function webComponentSetup(target, u, tag) {
|
|
|
723
723
|
const load = u.importPath ? `Load it once with \`import "${u.importPath}";\` at the app entry.` : "";
|
|
724
724
|
switch (target) {
|
|
725
725
|
case "angular":
|
|
726
|
-
return `Web component: add \`CUSTOM_ELEMENTS_SCHEMA\` to the component's
|
|
726
|
+
return `Web component: add \`CUSTOM_ELEMENTS_SCHEMA\` to the \`schemas\` of every NgModule or standalone component whose template uses it (with module federation, each remote's modules too). ${load} Importing it again in another bundle is harmless.`.trim();
|
|
727
727
|
case "vue":
|
|
728
728
|
return `Web component: set \`compilerOptions.isCustomElement = (tag) => tag.startsWith("${prefixOf(tag)}")\` in the Vue config. ${load}`.trim();
|
|
729
729
|
case "html":
|
|
@@ -778,8 +778,48 @@ function fromUsage(c, framework, kind, u) {
|
|
|
778
778
|
if (path && !setup) setup = /\.css(\?|$)/.test(path) ? `Include \`<link rel="stylesheet" href="${path}">\` once.` : /\.m?js(\?|$)/.test(path) ? `Include \`<script type="module" src="${path}"></script>\` once.` : void 0;
|
|
779
779
|
break;
|
|
780
780
|
}
|
|
781
|
-
return {
|
|
781
|
+
return {
|
|
782
|
+
framework,
|
|
783
|
+
importLine,
|
|
784
|
+
tag,
|
|
785
|
+
setup,
|
|
786
|
+
example: u.example?.trim() || void 0,
|
|
787
|
+
lang: LANG[kind],
|
|
788
|
+
props: u.props?.length ? u.props : void 0,
|
|
789
|
+
events: u.events?.length ? u.events : void 0,
|
|
790
|
+
customElement: kind === "web-components" || kind === "html" && tag.includes("-")
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
var VALUE_PROPS = ["value", "checked", "selected", "selected-date", "selectedDate"];
|
|
794
|
+
function bindingHowTo(u) {
|
|
795
|
+
const ev = u.events?.[0];
|
|
796
|
+
if (!ev) return [];
|
|
797
|
+
const camel = (n) => n.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
798
|
+
const vp = u.props?.find((p) => VALUE_PROPS.includes(p.name))?.name;
|
|
799
|
+
const valueProp = vp ? camel(vp) : void 0;
|
|
800
|
+
const dashed = (u.props ?? []).find((p) => p.name.includes("-"))?.name;
|
|
801
|
+
const caseNote = dashed ? ` Static attributes are kebab-case (\`${dashed}="\u2026"\`); bindings use the camelCase property (\`${u.framework === "vue" ? ":" : "["}${camel(dashed)}${u.framework === "vue" ? "" : "]"}="expr"\`), never \`${u.framework === "vue" ? ":" : "["}${dashed}${u.framework === "vue" ? "" : "]"}\`.` : "";
|
|
802
|
+
const attrs = (u.props ?? []).filter((p) => p.required).map((p) => `${p.name}="\u2026"`).join(" ");
|
|
803
|
+
const open = (extra) => `<${u.tag}${attrs ? ` ${attrs}` : ""}${extra}></${u.tag}>`;
|
|
804
|
+
const ce = u.customElement || u.via === "web-components";
|
|
805
|
+
switch (u.framework) {
|
|
806
|
+
case "angular":
|
|
807
|
+
if (!ce) return [`Outputs: bind with (${ev.name})="on${pascal(ev.name)}($event)".`];
|
|
808
|
+
return [
|
|
809
|
+
`Inputs: \`[prop]="expr"\` (objects and arrays work too). Events: \`(${ev.name})\` with the value in \`$any($event).detail\` (Angular types \`$event\` on custom elements as a plain Event, so \`$event.detail\` doesn't compile).${caseNote}`,
|
|
810
|
+
`Example: \`${open(`${valueProp ? ` [${valueProp}]="model"` : ""} (${ev.name})="model = $any($event).detail"`)}\``
|
|
811
|
+
];
|
|
812
|
+
case "vue":
|
|
813
|
+
return [
|
|
814
|
+
ce ? `Props: \`:prop="expr"\`. Events: \`@${ev.name}\` with the value in \`$event.detail\`.${caseNote} Example: \`${open(`${valueProp ? ` :${valueProp}="model"` : ""} @${ev.name}="model = $event.detail"`)}\`` : `Events: \`@${ev.name}="handler"\`.`
|
|
815
|
+
];
|
|
816
|
+
case "react":
|
|
817
|
+
return [];
|
|
818
|
+
default:
|
|
819
|
+
return [`Events: \`el.addEventListener("${ev.name}", (e) => e.detail)\`. Set lists and objects as properties (\`el.items = [\u2026]\`) or as JSON attributes.`];
|
|
820
|
+
}
|
|
782
821
|
}
|
|
822
|
+
var pascal = (s) => s.replace(/(^|[-_:])(\w)/g, (_, __, c) => c.toUpperCase());
|
|
783
823
|
var markup = (u) => `<${u.tag}>`;
|
|
784
824
|
function detectFramework(pkg) {
|
|
785
825
|
if (!pkg) return "html";
|
|
@@ -1219,12 +1259,16 @@ function propName(raw) {
|
|
|
1219
1259
|
return n;
|
|
1220
1260
|
}
|
|
1221
1261
|
var sameName = (a, b) => a.replace(/-/g, "").toLowerCase() === b.replace(/-/g, "").toLowerCase();
|
|
1262
|
+
var NATIVE_EVENT = /^(?:click|dblclick|auxclick|contextmenu|focus|blur|focusin|focusout|key(?:down|up|press)|mouse\w+|pointer\w+|touch\w+|wheel|drag\w*|drop|scroll|animation\w+|transition\w+)$/;
|
|
1263
|
+
function eventName(raw) {
|
|
1264
|
+
const m = /^\(([^)]+)\)$|^on-(.+)$|^@([^.]+)|^v-on:([^.]+)/.exec(raw);
|
|
1265
|
+
return m ? (m[1] ?? m[2] ?? m[3] ?? m[4]).replace(/\..*$/, "") : null;
|
|
1266
|
+
}
|
|
1222
1267
|
function templateProps({ code, config, framework }) {
|
|
1223
1268
|
const tagged = [];
|
|
1224
1269
|
for (const comp of config.components) {
|
|
1225
1270
|
const u = resolveUsage(comp, framework);
|
|
1226
|
-
|
|
1227
|
-
if (u && props?.length) tagged.push({ comp, tag: u.tag, props });
|
|
1271
|
+
if (u && (u.props?.length || u.events?.length)) tagged.push({ comp, tag: u.tag, props: u.props, events: u.events, ce: Boolean(u.customElement || u.via) });
|
|
1228
1272
|
}
|
|
1229
1273
|
if (!tagged.length) return [];
|
|
1230
1274
|
const out = [];
|
|
@@ -1238,19 +1282,39 @@ function templateProps({ code, config, framework }) {
|
|
|
1238
1282
|
const t = forms.get(m[1]);
|
|
1239
1283
|
const parsed = parseHtmlAttributes(code, m.index + m[0].length);
|
|
1240
1284
|
if (!parsed) continue;
|
|
1241
|
-
const list = t.props.map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
|
|
1285
|
+
const list = (t.props ?? []).map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
|
|
1242
1286
|
const passed = [];
|
|
1243
1287
|
for (const a of parsed.attrs) {
|
|
1244
1288
|
const raw = a.name;
|
|
1289
|
+
const ev = eventName(raw);
|
|
1290
|
+
if (ev) {
|
|
1291
|
+
if (!t.events?.length || NATIVE_EVENT.test(ev) || t.events.some((e) => e.name === ev)) continue;
|
|
1292
|
+
const like = t.events.find((e) => e.name.endsWith(`-${ev}`) || e.name.endsWith(ev)) ?? (t.events.length === 1 ? t.events[0] : void 0);
|
|
1293
|
+
out.push({
|
|
1294
|
+
offset: a.offset,
|
|
1295
|
+
message: `<${t.tag}> has no "${ev}" event`,
|
|
1296
|
+
suggestion: `${like ? `Listen to ${framework === "angular" ? `(${like.name})` : framework === "vue" ? `@${like.name}` : `"${like.name}"`}${like.detail ? ` and read ${framework === "angular" ? "$any($event).detail" : framework === "vue" ? "$event.detail" : "event.detail"} (${like.detail})` : ""}. ` : ""}Events: ${t.events.map((e) => e.name).join(", ")}`
|
|
1297
|
+
});
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1245
1300
|
if (/^v-model$/.test(raw)) passed.push("modelValue", "value");
|
|
1246
1301
|
if (/^\[\(ngModel\)\]$/.test(raw)) passed.push("value");
|
|
1247
1302
|
const n = propName(raw);
|
|
1248
|
-
if (!n) continue;
|
|
1303
|
+
if (!n || !t.props?.length) continue;
|
|
1249
1304
|
passed.push(n);
|
|
1305
|
+
if (t.ce && n.includes("-") && /^(?:\[(?!attr\.)|:|v-bind:|bind-)/.test(raw)) {
|
|
1306
|
+
const camel = n.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1307
|
+
out.push({
|
|
1308
|
+
offset: a.offset,
|
|
1309
|
+
message: `${raw} binds a property that doesn't exist on <${t.tag}>`,
|
|
1310
|
+
suggestion: `Bind the camelCase property: ${framework === "vue" ? `:${camel}` : `[${camel}]`}="\u2026" (kebab-case is only for static attributes: ${n}="\u2026")`
|
|
1311
|
+
});
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1250
1314
|
if (t.props.some((p) => sameName(p.name, n))) continue;
|
|
1251
1315
|
out.push({ offset: a.offset, message: `<${t.tag}> has no "${n}" input`, suggestion: `Remove it. ${t.comp.name} takes: ${list}` });
|
|
1252
1316
|
}
|
|
1253
|
-
for (const p of t.props) {
|
|
1317
|
+
for (const p of t.props ?? []) {
|
|
1254
1318
|
if (!p.required || passed.some((n) => sameName(n, p.name))) continue;
|
|
1255
1319
|
if (p.name === "children" || p.name === "content") continue;
|
|
1256
1320
|
out.push({ offset: m.index, message: `<${t.tag}> is missing the required "${p.name}" input`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
|
|
@@ -1453,7 +1517,7 @@ function makeIgnore(globs) {
|
|
|
1453
1517
|
}
|
|
1454
1518
|
|
|
1455
1519
|
// ../engine/src/check.ts
|
|
1456
|
-
var ENGINE_VERSION = "0.4.
|
|
1520
|
+
var ENGINE_VERSION = "0.4.1";
|
|
1457
1521
|
var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
|
|
1458
1522
|
function computeScore(counts, lines) {
|
|
1459
1523
|
const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
|
|
@@ -2249,8 +2313,13 @@ function generateInstructions(config, opts = {}) {
|
|
|
2249
2313
|
);
|
|
2250
2314
|
lines.push("");
|
|
2251
2315
|
const usesKit = fw === "react" && r.components.some((c) => c.importPath === KIT_PACKAGE);
|
|
2252
|
-
|
|
2316
|
+
const usesKitElements = fw !== "react" && r.components.some((c) => resolveUsage(c, fw)?.importLine?.includes(`"${KIT_PACKAGE}/`));
|
|
2317
|
+
if (opts.tokensCss || usesKit || usesKitElements) {
|
|
2253
2318
|
lines.push("## Setup");
|
|
2319
|
+
if (usesKit || usesKitElements) lines.push(`- The kit comes from npm: \`npm i ${KIT_PACKAGE}\` (once per app, if it isn't in package.json yet).`);
|
|
2320
|
+
if (usesKitElements) {
|
|
2321
|
+
lines.push(`- \`import "${KIT_PACKAGE}/elements";\` once at the app entry registers every \`<br-*>\` element. Don't import the React components from \`${KIT_PACKAGE}\` in this project.`);
|
|
2322
|
+
}
|
|
2254
2323
|
if (opts.tokensCss) {
|
|
2255
2324
|
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.`);
|
|
2256
2325
|
}
|
|
@@ -2272,6 +2341,11 @@ function generateInstructions(config, opts = {}) {
|
|
|
2272
2341
|
const how = fw === "react" ? `\`${c.name}\` from \`${c.importPath}\`` : `\`${markup(u)}\`${u.importLine ? ` (\`${u.importLine}\`)` : ""}`;
|
|
2273
2342
|
lines.push(`- ${how}${fw === "react" ? "" : ` \u2014 ${c.name}`}${tags.length ? ` (instead of ${tags.join(", ")})` : ""}${props}`);
|
|
2274
2343
|
}
|
|
2344
|
+
const evented = here.find((x2) => x2.u.events?.length && (x2.u.customElement || x2.u.via));
|
|
2345
|
+
if (evented && fw !== "react") {
|
|
2346
|
+
lines.push(`- Events: ${here.filter((x2) => x2.u.events?.length).slice(0, 12).map((x2) => `\`<${x2.u.tag}>\` ${x2.u.events.map((e) => `\`${e.name}\``).join(", ")}`).join("; ")}.`);
|
|
2347
|
+
for (const b of bindingHowTo(evented.u)) lines.push(`- ${b}`);
|
|
2348
|
+
}
|
|
2275
2349
|
const setups = [...new Set(here.map((x2) => x2.u.setup).filter(Boolean))];
|
|
2276
2350
|
if (setups.length && setups.length <= 3) for (const s of setups) lines.push(`- Setup: ${s}`);
|
|
2277
2351
|
else if (setups.length) lines.push("- Each component's setup is in its docs (MCP `get_component_docs`).");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brandrail",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.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",
|