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.
Files changed (2) hide show
  1. package/dist/brandrail.mjs +83 -9
  2. package/package.json +1 -1
@@ -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 \`schemas\`. ${load}`.trim();
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 { framework, importLine, tag, setup, example: u.example?.trim() || void 0, lang: LANG[kind], props: u.props?.length ? u.props : void 0 };
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
- const props = u?.props;
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.0";
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
- if (opts.tokensCss || usesKit) {
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.0",
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",