brandrail 0.9.0 → 0.10.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.
@@ -1444,15 +1444,15 @@ var require_rules = __commonJS({
1444
1444
  }
1445
1445
  exports.isJSONType = isJSONType;
1446
1446
  function getRules() {
1447
- const groups = {
1447
+ const groups2 = {
1448
1448
  number: { type: "number", rules: [] },
1449
1449
  string: { type: "string", rules: [] },
1450
1450
  array: { type: "array", rules: [] },
1451
1451
  object: { type: "object", rules: [] }
1452
1452
  };
1453
1453
  return {
1454
- types: { ...groups, integer: true, boolean: true, null: true },
1455
- rules: [{ rules: [] }, groups.number, groups.string, groups.array, groups.object],
1454
+ types: { ...groups2, integer: true, boolean: true, null: true },
1455
+ rules: [{ rules: [] }, groups2.number, groups2.string, groups2.array, groups2.object],
1456
1456
  post: { rules: [] },
1457
1457
  all: {},
1458
1458
  keywords: {}
@@ -3190,9 +3190,9 @@ var require_utils = __commonJS({
3190
3190
  }
3191
3191
  }
3192
3192
  if (bestLength < 2) return hextets.join(":");
3193
- const head = hextets.slice(0, bestStart).join(":");
3193
+ const head2 = hextets.slice(0, bestStart).join(":");
3194
3194
  const tail = hextets.slice(bestStart + bestLength).join(":");
3195
- return head + "::" + tail;
3195
+ return head2 + "::" + tail;
3196
3196
  }
3197
3197
  function normalizeIPv6Address(input2) {
3198
3198
  const compression = input2.indexOf("::");
@@ -3383,9 +3383,9 @@ var require_utils = __commonJS({
3383
3383
  let output2 = "";
3384
3384
  for (let i2 = 0; i2 < input2.length; i2++) {
3385
3385
  if (input2[i2] === "%" && i2 + 2 < input2.length) {
3386
- const hex3 = input2.slice(i2 + 1, i2 + 3);
3387
- if (isHexPair(hex3)) {
3388
- const normalizedHex = hex3.toUpperCase();
3386
+ const hex4 = input2.slice(i2 + 1, i2 + 3);
3387
+ if (isHexPair(hex4)) {
3388
+ const normalizedHex = hex4.toUpperCase();
3389
3389
  const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3390
3390
  if (decodeUnreserved && isUnreserved(decoded)) {
3391
3391
  output2 += decoded;
@@ -3405,9 +3405,9 @@ var require_utils = __commonJS({
3405
3405
  for (let i2 = 0; i2 < input2.length; i2++) {
3406
3406
  const ch = input2[i2];
3407
3407
  if (ch === "%" && i2 + 2 < input2.length) {
3408
- const hex3 = input2.slice(i2 + 1, i2 + 3);
3409
- if (isHexPair(hex3)) {
3410
- const normalizedHex = hex3.toUpperCase();
3408
+ const hex4 = input2.slice(i2 + 1, i2 + 3);
3409
+ if (isHexPair(hex4)) {
3410
+ const normalizedHex = hex4.toUpperCase();
3411
3411
  const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3412
3412
  if (decoded !== "." && isUnreserved(decoded)) {
3413
3413
  output2 += decoded;
@@ -3447,9 +3447,9 @@ var require_utils = __commonJS({
3447
3447
  for (let i2 = 0; i2 < input2.length; i2++) {
3448
3448
  const ch = input2[i2];
3449
3449
  if (ch === "%" && i2 + 2 < input2.length) {
3450
- const hex3 = input2.slice(i2 + 1, i2 + 3);
3451
- if (isHexPair(hex3)) {
3452
- output2 += "%" + hex3.toUpperCase();
3450
+ const hex4 = input2.slice(i2 + 1, i2 + 3);
3451
+ if (isHexPair(hex4)) {
3452
+ output2 += "%" + hex4.toUpperCase();
3453
3453
  i2 += 2;
3454
3454
  continue;
3455
3455
  }
@@ -3485,9 +3485,9 @@ var require_utils = __commonJS({
3485
3485
  for (let i2 = 0; i2 < input2.length; i2++) {
3486
3486
  const ch = input2[i2];
3487
3487
  if (ch === "%" && i2 + 2 < input2.length) {
3488
- const hex3 = input2.slice(i2 + 1, i2 + 3);
3489
- if (isHexPair(hex3)) {
3490
- output2 += "%" + hex3.toUpperCase();
3488
+ const hex4 = input2.slice(i2 + 1, i2 + 3);
3489
+ if (isHexPair(hex4)) {
3490
+ output2 += "%" + hex4.toUpperCase();
3491
3491
  i2 += 2;
3492
3492
  continue;
3493
3493
  }
@@ -3532,9 +3532,9 @@ var require_utils = __commonJS({
3532
3532
  for (let i2 = 0; i2 < input2.length; i2++) {
3533
3533
  const ch = input2[i2];
3534
3534
  if (ch === "%" && i2 + 2 < input2.length) {
3535
- const hex3 = input2.slice(i2 + 1, i2 + 3);
3536
- if (isHexPair(hex3)) {
3537
- const normalizedHex = hex3.toUpperCase();
3535
+ const hex4 = input2.slice(i2 + 1, i2 + 3);
3536
+ if (isHexPair(hex4)) {
3537
+ const normalizedHex = hex4.toUpperCase();
3538
3538
  const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3539
3539
  if (isUnreserved(decoded)) {
3540
3540
  output2 += decoded;
@@ -3572,9 +3572,9 @@ var require_utils = __commonJS({
3572
3572
  let output2 = "";
3573
3573
  for (let i2 = 0; i2 < input2.length; i2++) {
3574
3574
  if (input2[i2] === "%" && i2 + 2 < input2.length) {
3575
- const hex3 = input2.slice(i2 + 1, i2 + 3);
3576
- if (isHexPair(hex3)) {
3577
- output2 += "%" + hex3.toUpperCase();
3575
+ const hex4 = input2.slice(i2 + 1, i2 + 3);
3576
+ if (isHexPair(hex4)) {
3577
+ output2 += "%" + hex4.toUpperCase();
3578
3578
  i2 += 2;
3579
3579
  continue;
3580
3580
  }
@@ -7744,8 +7744,8 @@ function unzipSync(data2, opts) {
7744
7744
 
7745
7745
  // ../engine/src/color.ts
7746
7746
  var clamp = (n, lo = 0, hi = 255) => Math.min(hi, Math.max(lo, n));
7747
- function parseHex(hex3) {
7748
- let h = hex3.replace(/^#/, "").toLowerCase();
7747
+ function parseHex(hex4) {
7748
+ let h = hex4.replace(/^#/, "").toLowerCase();
7749
7749
  if (!/^[0-9a-f]+$/.test(h)) return null;
7750
7750
  if (h.length === 3 || h.length === 4) h = h.split("").map((c) => c + c).join("");
7751
7751
  if (h.length !== 6 && h.length !== 8) return null;
@@ -7768,10 +7768,10 @@ function parseColor(input2) {
7768
7768
  if (!m) return null;
7769
7769
  const parts = m[2].split(/[\s,/]+/).filter(Boolean);
7770
7770
  if (parts.length < 3) return null;
7771
- const num = (p, max2) => p.endsWith("%") ? parseFloat(p) / 100 * max2 : parseFloat(p);
7772
- const alpha = parts[3] !== void 0 ? num(parts[3], 1) : 1;
7771
+ const num2 = (p, max2) => p.endsWith("%") ? parseFloat(p) / 100 * max2 : parseFloat(p);
7772
+ const alpha = parts[3] !== void 0 ? num2(parts[3], 1) : 1;
7773
7773
  if (m[1].startsWith("rgb")) {
7774
- const [r2, g2, b3] = parts.slice(0, 3).map((p) => num(p, 255));
7774
+ const [r2, g2, b3] = parts.slice(0, 3).map((p) => num2(p, 255));
7775
7775
  if ([r2, g2, b3].some((v) => Number.isNaN(v))) return null;
7776
7776
  return { r: clamp(r2), g: clamp(g2), b: clamp(b3), a: alpha };
7777
7777
  }
@@ -7849,7 +7849,7 @@ function colorValue(v) {
7849
7849
  if (v && typeof v === "object" && ("components" in v || "hex" in v)) {
7850
7850
  const c = v;
7851
7851
  const alpha = typeof c.alpha === "number" ? c.alpha : 1;
7852
- const withAlpha = (hex4) => alpha < 1 ? `${hex4.slice(0, 7)}${Math.round(alpha * 255).toString(16).padStart(2, "0")}` : hex4.slice(0, 7);
7852
+ const withAlpha = (hex5) => alpha < 1 ? `${hex5.slice(0, 7)}${Math.round(alpha * 255).toString(16).padStart(2, "0")}` : hex5.slice(0, 7);
7853
7853
  if (typeof c.hex === "string" && /^#[0-9a-f]{6}/i.test(c.hex)) return withAlpha(c.hex.toLowerCase());
7854
7854
  const comps = c.components?.map((x2) => typeof x2 === "number" ? x2 : 0);
7855
7855
  if (comps?.length === 3 && (!c.colorSpace || /srgb|display-p3/i.test(c.colorSpace))) {
@@ -7859,9 +7859,9 @@ function colorValue(v) {
7859
7859
  }
7860
7860
  if (v && typeof v === "object" && "r" in v) {
7861
7861
  const c = v;
7862
- const hex4 = toHex({ r: c.r * 255, g: c.g * 255, b: c.b * 255, a: 1 });
7862
+ const hex5 = toHex({ r: c.r * 255, g: c.g * 255, b: c.b * 255, a: 1 });
7863
7863
  const a = c.a ?? 1;
7864
- return a < 1 ? `${hex4}${Math.round(a * 255).toString(16).padStart(2, "0")}` : hex4;
7864
+ return a < 1 ? `${hex5}${Math.round(a * 255).toString(16).padStart(2, "0")}` : hex5;
7865
7865
  }
7866
7866
  if (typeof v !== "string") return null;
7867
7867
  const s = v.trim();
@@ -7871,8 +7871,8 @@ function colorValue(v) {
7871
7871
  }
7872
7872
  const rgb = parseColor(s);
7873
7873
  if (!rgb) return null;
7874
- const hex3 = toHex(rgb);
7875
- return rgb.a < 1 ? `${hex3}${Math.round(rgb.a * 255).toString(16).padStart(2, "0")}` : hex3;
7874
+ const hex4 = toHex(rgb);
7875
+ return rgb.a < 1 ? `${hex4}${Math.round(rgb.a * 255).toString(16).padStart(2, "0")}` : hex4;
7876
7876
  }
7877
7877
  var dim = (v) => {
7878
7878
  if (v && typeof v === "object" && "value" in v) {
@@ -8196,19 +8196,19 @@ function fromModeObject(obj, opts = {}) {
8196
8196
  plainLeaves(v, [], l);
8197
8197
  return { name, leaves: l, keys: new Set(l.map((x2) => x2.key.join("/"))) };
8198
8198
  });
8199
- const groups = [];
8199
+ const groups2 = [];
8200
8200
  for (const m of leafSets) {
8201
- const g = groups.find((grp) => {
8201
+ const g = groups2.find((grp) => {
8202
8202
  const a = grp[0].keys;
8203
8203
  let same = 0;
8204
8204
  for (const k of m.keys) if (a.has(k)) same++;
8205
8205
  return same / Math.max(a.size, m.keys.size) >= 0.8;
8206
8206
  });
8207
8207
  if (g) g.push(m);
8208
- else groups.push([m]);
8208
+ else groups2.push([m]);
8209
8209
  }
8210
8210
  const byLeaf = /* @__PURE__ */ new Map();
8211
- groups.forEach((grp, gi) => {
8211
+ groups2.forEach((grp, gi) => {
8212
8212
  for (const m of grp) for (const l of m.leaves) {
8213
8213
  const k = l.key[l.key.length - 1];
8214
8214
  const e = byLeaf.get(k) ?? { group: gi, values: /* @__PURE__ */ new Map() };
@@ -8224,13 +8224,13 @@ function fromModeObject(obj, opts = {}) {
8224
8224
  const ref = byLeaf.get(v) ?? byLeaf.get(v.replace(REF, "$1"));
8225
8225
  if (!ref) return v;
8226
8226
  referenced.add(v);
8227
- const grp = groups[ref.group];
8227
+ const grp = groups2[ref.group];
8228
8228
  const mode = pick2(grp, want) ?? pick2(grp, "default");
8229
8229
  return deref(ref.values.get(mode.name) ?? [...ref.values.values()][0], want, depth + 1);
8230
8230
  };
8231
8231
  const out = [];
8232
8232
  const pending = [];
8233
- for (const grp of groups) {
8233
+ for (const grp of groups2) {
8234
8234
  const def = pick2(grp, "default");
8235
8235
  const dark = pick2(grp, "dark");
8236
8236
  if (grp.length > 1) notes.push(`${grp.map((m) => m.name).join(" / ")} look like modes of one collection; used \u201C${def.name}\u201D${dark && dark !== def ? ` (and \u201C${dark.name}\u201D as dark)` : ""}.`);
@@ -8541,10 +8541,10 @@ function resolveConfig(config2) {
8541
8541
  if (!l.type && !l.path.startsWith("color")) continue;
8542
8542
  const rgb = parseColor(l.value);
8543
8543
  if (!rgb) continue;
8544
- const cssVar3 = `--${l.path.replace(/\./g, "-")}`;
8545
- colors.push({ path: l.path, cssVar: cssVar3, hex: toHex(rgb), lab: rgbToLab(rgb), alpha: rgb.a });
8544
+ const cssVar4 = `--${l.path.replace(/\./g, "-")}`;
8545
+ colors.push({ path: l.path, cssVar: cssVar4, hex: toHex(rgb), lab: rgbToLab(rgb), alpha: rgb.a });
8546
8546
  const dark = typeof l.dark === "string" ? parseColor(l.dark) : null;
8547
- if (dark) colors.push({ path: l.path, cssVar: cssVar3, hex: toHex(dark), lab: rgbToLab(dark), alpha: dark.a, mode: "dark" });
8547
+ if (dark) colors.push({ path: l.path, cssVar: cssVar4, hex: toHex(dark), lab: rgbToLab(dark), alpha: dark.a, mode: "dark" });
8548
8548
  }
8549
8549
  const spaceLeaves = all.filter((l) => /^(space|spacing)\./.test(l.path));
8550
8550
  const spacing = [0, 1, 2];
@@ -8592,15 +8592,17 @@ function resolveConfig(config2) {
8592
8592
  }
8593
8593
 
8594
8594
  // ../engine/src/frameworks.ts
8595
- var FRAMEWORKS = ["react", "angular", "vue", "web-components", "html"];
8595
+ var FRAMEWORKS = ["react", "angular", "vue", "svelte", "solid", "web-components", "html"];
8596
8596
  var FRAMEWORK_LABEL = {
8597
8597
  react: "React",
8598
8598
  angular: "Angular",
8599
8599
  vue: "Vue",
8600
+ svelte: "Svelte",
8601
+ solid: "Solid",
8600
8602
  "web-components": "Web Components",
8601
8603
  html: "Plain HTML"
8602
8604
  };
8603
- var LANG = { react: "tsx", angular: "html", vue: "vue", "web-components": "html", html: "html" };
8605
+ var LANG = { react: "tsx", angular: "html", vue: "vue", svelte: "svelte", solid: "tsx", "web-components": "html", html: "html" };
8604
8606
  var isFramework = (x2) => typeof x2 === "string" && FRAMEWORKS.includes(x2);
8605
8607
  var kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/\./g, "-").toLowerCase();
8606
8608
  var prefixOf = (tag) => tag.includes("-") ? `${tag.split("-")[0]}-` : tag;
@@ -8611,6 +8613,10 @@ function webComponentSetup(target, u, tag) {
8611
8613
  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();
8612
8614
  case "vue":
8613
8615
  return `Web component: set \`compilerOptions.isCustomElement = (tag) => tag.startsWith("${prefixOf(tag)}")\` in the Vue config. ${load}`.trim();
8616
+ case "svelte":
8617
+ return `Web component: ${load || "load it once at the app entry."} In SvelteKit, import it in the root +layout.svelte inside onMount (custom elements need the browser). Svelte sets properties on custom elements, so lists and objects pass directly.`.trim();
8618
+ case "solid":
8619
+ return `Web component: ${load || "load it once at the app entry."} For TypeScript, add the ${prefixOf(tag)}* tags to JSX.IntrinsicElements once (declare module "solid-js" { namespace JSX { interface IntrinsicElements { \u2026 } } }); page templates include the file.`.trim();
8614
8620
  case "html":
8615
8621
  return u.importPath ? `Web component: load \`${u.importPath}\` once (a bundler import or \`<script type="module">\`).` : void 0;
8616
8622
  default:
@@ -8651,8 +8657,10 @@ function fromUsage(c, framework, kind, u) {
8651
8657
  }
8652
8658
  break;
8653
8659
  case "vue":
8660
+ case "svelte":
8661
+ case "solid":
8654
8662
  tag ||= u.importName || c.name;
8655
- if (path) importLine = `import { ${u.importName || tag} } from "${path}";`;
8663
+ if (path) importLine = kind === "svelte" && !u.importName ? `import ${tag} from "${path}";` : `import { ${u.importName || tag} } from "${path}";`;
8656
8664
  break;
8657
8665
  case "web-components":
8658
8666
  tag ||= kebab(c.name);
@@ -8698,6 +8706,18 @@ function bindingHowTo(u) {
8698
8706
  return [
8699
8707
  ce ? `Props: \`:prop="expr"\`. Events: \`@${ev.name}\` with the value in \`$event.detail\`.${caseNote} Example: \`${open2(`${valueProp ? ` :${valueProp}="model"` : ""} @${ev.name}="model = $event.detail"`)}\`` : `Events: \`@${ev.name}="handler"\`.`
8700
8708
  ];
8709
+ case "svelte":
8710
+ if (!ce) return [`Events: \`on${ev.name}={handler}\` (Svelte 5) or \`on:${ev.name}\` (Svelte 4).`];
8711
+ return [
8712
+ `Props: \`prop={expr}\` (Svelte sets the element's property, so lists and objects work). Events: \`on${ev.name}={(e) => \u2026}\` with the value in \`e.detail\` (Svelte 5; \`on:${ev.name}\` in Svelte 4).`,
8713
+ `Example: \`${open2(`${valueProp ? ` ${valueProp}={value}` : ""} on${ev.name}={(e) => (value = e.detail)}`)}\``
8714
+ ];
8715
+ case "solid":
8716
+ if (!ce) return [`Events: \`on${pascal(ev.name)}={handler}\`.`];
8717
+ return [
8718
+ `Props: static strings as attributes; lists, objects and signals with \`prop:name={value()}\` (Solid sets attributes otherwise). Events: \`on:${ev.name}={(e) => \u2026}\` with the value in \`e.detail\`.`,
8719
+ `Example: \`${open2(`${valueProp ? ` prop:${valueProp}={value()}` : ""} on:${ev.name}={(e) => setValue(e.detail)}`)}\``
8720
+ ];
8701
8721
  case "react":
8702
8722
  return [];
8703
8723
  default:
@@ -8715,8 +8735,10 @@ function detectFramework(pkg) {
8715
8735
  const has = (name) => name in deps;
8716
8736
  if (has("@angular/core")) return "angular";
8717
8737
  if (has("vue") || has("nuxt")) return "vue";
8738
+ if (has("solid-js") || has("@solidjs/start")) return "solid";
8739
+ if (has("svelte") || has("@sveltejs/kit")) return "svelte";
8718
8740
  if (has("react") || has("next") || has("preact")) return "react";
8719
- if (has("svelte") || has("lit") || has("@stencil/core") || has("@sveltejs/kit")) return "web-components";
8741
+ if (has("lit") || has("@stencil/core")) return "web-components";
8720
8742
  return "html";
8721
8743
  }
8722
8744
 
@@ -9502,6 +9524,13 @@ function parseHtmlAttributes(code, start) {
9502
9524
  const c = code[i2];
9503
9525
  if (c === void 0) return null;
9504
9526
  if (c === ">" || c === "/" && code[i2 + 1] === ">") return { attrs, end: i2 };
9527
+ if (c === "{" && code[i2 + 1] !== "{" && code[i2 + 1] !== "%") {
9528
+ const end = skipBalanced(code, i2);
9529
+ const inner = code.slice(i2 + 1, end - 1).trim();
9530
+ if (/^[A-Za-z_$][\w$]*$/.test(inner)) attrs.push({ name: inner, offset: i2 });
9531
+ i2 = end;
9532
+ continue;
9533
+ }
9505
9534
  if (c === "{" || c === "<" && code[i2 + 1] === "?") {
9506
9535
  const close = c === "{" ? code[i2 + 1] === "%" ? "%}" : "}}" : "?>";
9507
9536
  const e = code.indexOf(close, i2 + 2);
@@ -9517,7 +9546,8 @@ function parseHtmlAttributes(code, start) {
9517
9546
  i2++;
9518
9547
  while (/\s/.test(code[i2] ?? "")) i2++;
9519
9548
  const q = code[i2];
9520
- if (q === '"' || q === "'") {
9549
+ if (q === "{") i2 = skipBalanced(code, i2);
9550
+ else if (q === '"' || q === "'") {
9521
9551
  const e = code.indexOf(q, i2 + 1);
9522
9552
  i2 = e === -1 ? code.length : e + 1;
9523
9553
  } else while (i2 < code.length && !/[\s>]/.test(code[i2])) i2++;
@@ -9526,9 +9556,12 @@ function parseHtmlAttributes(code, start) {
9526
9556
  return null;
9527
9557
  }
9528
9558
  var HTML_GLOBAL = /^(?:id|class|style|title|role|hidden|tabindex|lang|dir|slot|part|is|key|ref|name|for|form|aria-.+|data-.+)$/;
9529
- function propName(raw) {
9559
+ function propName(raw, framework) {
9530
9560
  let n = raw;
9531
9561
  if (/^(?:\(|@|on-|v-on:|\*|#|let-|ref-|v-(?!bind:|model:))/.test(n)) return null;
9562
+ if (/^(?:class|style|use|transition|in|out|animate|let|ref|on|oncapture|use):/.test(n)) return null;
9563
+ n = n.replace(/^(?:bind|prop|attr|bool):/, "");
9564
+ if (framework === "svelte" && /^on[a-z]/.test(n)) return null;
9532
9565
  n = n.replace(/^v-bind:|^v-model:|^bind-|^:/, "").replace(/^\[\(?|\)?\]$/g, "");
9533
9566
  if (/^(?:attr|class|style)\./.test(n) || /^ng[A-Z]/.test(n) || /^(?:formControl|formControlName|formGroup|routerLink|routerLinkActive)$/.test(n)) return null;
9534
9567
  n = n.replace(/\.(?:px|rem|em|%)$/, "").replace(/\..*$/, "");
@@ -9537,10 +9570,14 @@ function propName(raw) {
9537
9570
  }
9538
9571
  var sameName = (a, b) => a.replace(/-/g, "").toLowerCase() === b.replace(/-/g, "").toLowerCase();
9539
9572
  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+)$/;
9540
- function eventName(raw) {
9541
- const m = /^\(([^)]+)\)$|^on-(.+)$|^@([^.]+)|^v-on:([^.]+)/.exec(raw);
9542
- return m ? (m[1] ?? m[2] ?? m[3] ?? m[4]).replace(/\..*$/, "") : null;
9573
+ function eventName(raw, framework) {
9574
+ const m = /^\(([^)]+)\)$|^on-(.+)$|^@([^.]+)|^v-on:([^.]+)|^on(?:capture)?:([^|]+)/.exec(raw);
9575
+ if (m) return (m[1] ?? m[2] ?? m[3] ?? m[4] ?? m[5]).replace(/\..*$/, "");
9576
+ if (framework === "svelte" && /^on[a-z][\w-]*$/.test(raw)) return raw.slice(2);
9577
+ return null;
9543
9578
  }
9579
+ var listen = (framework, ev) => framework === "angular" ? `(${ev})` : framework === "vue" ? `@${ev}` : framework === "svelte" ? `on${ev}={\u2026}` : framework === "solid" ? `on:${ev}={\u2026}` : `"${ev}"`;
9580
+ var detailOf = (framework) => framework === "angular" ? "$any($event).detail" : framework === "vue" ? "$event.detail" : framework === "svelte" || framework === "solid" ? "e.detail" : "event.detail";
9544
9581
  function templateProps({ code, config: config2, framework }) {
9545
9582
  const tagged = [];
9546
9583
  for (const comp of config2.components) {
@@ -9563,23 +9600,23 @@ function templateProps({ code, config: config2, framework }) {
9563
9600
  const passed = [];
9564
9601
  for (const a of parsed.attrs) {
9565
9602
  const raw = a.name;
9566
- const ev = eventName(raw);
9603
+ const ev = eventName(raw, framework);
9567
9604
  if (ev) {
9568
9605
  if (!t.events?.length || NATIVE_EVENT.test(ev) || t.events.some((e) => e.name === ev)) continue;
9569
9606
  const like = t.events.find((e) => e.name.endsWith(`-${ev}`) || e.name.endsWith(ev)) ?? (t.events.length === 1 ? t.events[0] : void 0);
9570
9607
  out.push({
9571
9608
  offset: a.offset,
9572
9609
  message: `<${t.tag}> has no "${ev}" event`,
9573
- 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(", ")}`
9610
+ suggestion: `${like ? `Listen to ${listen(framework, like.name)}${like.detail ? ` and read ${detailOf(framework)} (${like.detail})` : ""}. ` : ""}Events: ${t.events.map((e) => e.name).join(", ")}`
9574
9611
  });
9575
9612
  continue;
9576
9613
  }
9577
9614
  if (/^v-model$/.test(raw)) passed.push("modelValue", "value");
9578
9615
  if (/^\[\(ngModel\)\]$/.test(raw)) passed.push("value");
9579
- const n = propName(raw);
9616
+ const n = propName(raw, framework);
9580
9617
  if (!n || !t.props?.length) continue;
9581
9618
  passed.push(n);
9582
- if (t.ce && n.includes("-") && /^(?:\[(?!attr\.)|:|v-bind:|bind-)/.test(raw)) {
9619
+ if (t.ce && n.includes("-") && (framework === "angular" || framework === "vue") && /^(?:\[(?!attr\.)|:|v-bind:|bind-)/.test(raw)) {
9583
9620
  const camel2 = n.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
9584
9621
  out.push({
9585
9622
  offset: a.offset,
@@ -9767,7 +9804,7 @@ function fileFramework(path, kind, code, project) {
9767
9804
  case "vue":
9768
9805
  return "vue";
9769
9806
  case "svelte":
9770
- return "web-components";
9807
+ return "svelte";
9771
9808
  case "template":
9772
9809
  return "html";
9773
9810
  case "css":
@@ -9777,6 +9814,7 @@ function fileFramework(path, kind, code, project) {
9777
9814
  return project === "vue" || project === "web-components" ? project : "html";
9778
9815
  case "jsx":
9779
9816
  if (ANGULAR_COMPONENT.test(code)) return "angular";
9817
+ if (project === "solid" && /\.(tsx|jsx)$/.test(p)) return "solid";
9780
9818
  if (/\.(tsx|jsx|mdx)$/.test(p)) return "react";
9781
9819
  return project ?? "react";
9782
9820
  default:
@@ -9884,7 +9922,7 @@ function makeIgnore(globs) {
9884
9922
  }
9885
9923
 
9886
9924
  // ../engine/src/check.ts
9887
- var ENGINE_VERSION = "0.9.0";
9925
+ var ENGINE_VERSION = "0.10.0";
9888
9926
  var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
9889
9927
  function computeScore(counts, lines) {
9890
9928
  const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
@@ -10281,7 +10319,7 @@ var RULE_FILES = [
10281
10319
  // ../engine/src/rendered.ts
10282
10320
  function collectRenderedNodes(maxNodes = 3e3) {
10283
10321
  const SKIP = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "IFRAME", "HEAD", "META", "LINK", "BR", "WBR", "SOURCE", "TRACK"]);
10284
- const px3 = (v) => {
10322
+ const px4 = (v) => {
10285
10323
  const n = parseFloat(v);
10286
10324
  return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0;
10287
10325
  };
@@ -10329,8 +10367,8 @@ function collectRenderedNodes(maxNodes = 3e3) {
10329
10367
  const r = el.getBoundingClientRect();
10330
10368
  if (r.width < 1 || r.height < 1) continue;
10331
10369
  const text2 = Array.from(el.childNodes).filter((n) => n.nodeType === 3).map((n) => n.textContent ?? "").join(" ").replace(/\s+/g, " ").trim();
10332
- const borderW = Math.max(px3(cs.borderTopWidth), px3(cs.borderRightWidth), px3(cs.borderBottomWidth), px3(cs.borderLeftWidth));
10333
- const borderSide = px3(cs.borderTopWidth) > 0 ? cs.borderTopColor : px3(cs.borderBottomWidth) > 0 ? cs.borderBottomColor : px3(cs.borderLeftWidth) > 0 ? cs.borderLeftColor : cs.borderRightColor;
10370
+ const borderW = Math.max(px4(cs.borderTopWidth), px4(cs.borderRightWidth), px4(cs.borderBottomWidth), px4(cs.borderLeftWidth));
10371
+ const borderSide = px4(cs.borderTopWidth) > 0 ? cs.borderTopColor : px4(cs.borderBottomWidth) > 0 ? cs.borderBottomColor : px4(cs.borderLeftWidth) > 0 ? cs.borderLeftColor : cs.borderRightColor;
10334
10372
  nodes.push({
10335
10373
  selector: selectorOf(el),
10336
10374
  tag: el.tagName.toLowerCase(),
@@ -10340,12 +10378,12 @@ function collectRenderedNodes(maxNodes = 3e3) {
10340
10378
  borderColor: borderW > 0 && cs.borderTopStyle !== "none" ? borderSide : void 0,
10341
10379
  effectiveBackground: effectiveBg(el),
10342
10380
  fontFamily: cs.fontFamily,
10343
- fontSize: px3(cs.fontSize),
10381
+ fontSize: px4(cs.fontSize),
10344
10382
  fontWeight: parseInt(cs.fontWeight, 10) || 400,
10345
- radius: Math.max(px3(cs.borderTopLeftRadius), px3(cs.borderTopRightRadius), px3(cs.borderBottomLeftRadius), px3(cs.borderBottomRightRadius)),
10383
+ radius: Math.max(px4(cs.borderTopLeftRadius), px4(cs.borderTopRightRadius), px4(cs.borderBottomLeftRadius), px4(cs.borderBottomRightRadius)),
10346
10384
  minSide: Math.round(Math.min(r.width, r.height)),
10347
- padding: [px3(cs.paddingTop), px3(cs.paddingRight), px3(cs.paddingBottom), px3(cs.paddingLeft)],
10348
- gap: cs.display.includes("flex") || cs.display.includes("grid") ? [px3(cs.rowGap), px3(cs.columnGap)] : []
10385
+ padding: [px4(cs.paddingTop), px4(cs.paddingRight), px4(cs.paddingBottom), px4(cs.paddingLeft)],
10386
+ gap: cs.display.includes("flex") || cs.display.includes("grid") ? [px4(cs.rowGap), px4(cs.columnGap)] : []
10349
10387
  });
10350
10388
  }
10351
10389
  const links = Array.from(document.querySelectorAll("a[href]")).map((a) => a.href).filter((h) => {
@@ -10378,8 +10416,8 @@ function closestToken(config2, rgb) {
10378
10416
  return { token: best, distance: bestD };
10379
10417
  }
10380
10418
  function colorLabel(rgb) {
10381
- const hex3 = toHex(rgb);
10382
- return rgb.a < 1 ? `${hex3} at ${Math.round(rgb.a * 100)}%` : hex3;
10419
+ const hex4 = toHex(rgb);
10420
+ return rgb.a < 1 ? `${hex4} at ${Math.round(rgb.a * 100)}%` : hex4;
10383
10421
  }
10384
10422
  function checkRendered(pages, configInput, opts = {}) {
10385
10423
  const t0 = Date.now();
@@ -10391,14 +10429,14 @@ function checkRendered(pages, configInput, opts = {}) {
10391
10429
  const spacing = config2.spacing;
10392
10430
  const radii = config2.radii.filter((r) => r < 999);
10393
10431
  for (const page of pages) {
10394
- const groups = /* @__PURE__ */ new Map();
10432
+ const groups2 = /* @__PURE__ */ new Map();
10395
10433
  const add = (key, g, selector) => {
10396
10434
  const sev = config2.rules[g.ruleId];
10397
10435
  if (sev === "off") return;
10398
- const cur = groups.get(key) ?? { ...g, severity: sev ?? g.severity, selectors: [], count: 0 };
10436
+ const cur = groups2.get(key) ?? { ...g, severity: sev ?? g.severity, selectors: [], count: 0 };
10399
10437
  cur.count++;
10400
10438
  if (cur.selectors.length < examples && !cur.selectors.includes(selector)) cur.selectors.push(selector);
10401
- groups.set(key, cur);
10439
+ groups2.set(key, cur);
10402
10440
  };
10403
10441
  const colorCheck = (raw, where, selector) => {
10404
10442
  if (!raw || !config2.colors.length) return;
@@ -10502,7 +10540,7 @@ function checkRendered(pages, configInput, opts = {}) {
10502
10540
  suggestion: "Open it in a browser: it may have crashed, need a login (use --storage-state), or need longer to load (--wait)."
10503
10541
  });
10504
10542
  }
10505
- for (const g of groups.values()) {
10543
+ for (const g of groups2.values()) {
10506
10544
  violations.push({
10507
10545
  ruleId: g.ruleId,
10508
10546
  severity: g.severity,
@@ -10550,10 +10588,10 @@ function pagePath(url2) {
10550
10588
  var cssVar = (path) => `--${path.replace(/\./g, "-")}`;
10551
10589
  function generateTokensCss(tokens, opts = {}) {
10552
10590
  const flat = flattenTokens(tokens);
10553
- const head = `/* Generated by Brandrail from the ${opts.org ? `${opts.org} ` : ""}Brand Kit${opts.version ? ` v${opts.version}` : ""}. Do not edit: run \`npx brandrail init --from-api\` to update. */`;
10591
+ const head2 = `/* Generated by Brandrail from the ${opts.org ? `${opts.org} ` : ""}Brand Kit${opts.version ? ` v${opts.version}` : ""}. Do not edit: run \`npx brandrail init --from-api\` to update. */`;
10554
10592
  const light = flat.map((t) => ` ${cssVar(t.path)}: ${t.type === "fontFamily" ? t.value : t.value};`);
10555
10593
  const dark = flat.filter((t) => t.dark).map((t) => ` ${cssVar(t.path)}: ${t.dark};`);
10556
- const out = [head, "", ":root {", ...light, "}"];
10594
+ const out = [head2, "", ":root {", ...light, "}"];
10557
10595
  if (dark.length) {
10558
10596
  out.push("", '[data-theme="dark"] {', ...dark, "}", "", "@media (prefers-color-scheme: dark) {", ' :root:not([data-theme="light"]) {', ...dark.map((l) => ` ${l}`), " }", "}");
10559
10597
  }
@@ -10619,8 +10657,8 @@ var px2 = (v) => {
10619
10657
  return m ? Number.parseFloat(m[1]) * (m[2] === "rem" ? 16 : 1) : null;
10620
10658
  };
10621
10659
  var nearest2 = (values, v) => values.reduce((best, x2) => Math.abs(x2 - v) < Math.abs(best - v) ? x2 : best, values[0]);
10622
- function luminance(hex3) {
10623
- const n = hex3.replace("#", "").slice(0, 6);
10660
+ function luminance(hex4) {
10661
+ const n = hex4.replace("#", "").slice(0, 6);
10624
10662
  const [r, g, b] = [0, 2, 4].map((i2) => parseInt(n.slice(i2, i2 + 2), 16) / 255).map((c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
10625
10663
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
10626
10664
  }
@@ -11519,11 +11557,12 @@ var jsonAttr = (v) => JSON.stringify(v).replace(/'/g, "&#39;");
11519
11557
  var ind = (d) => " ".repeat(d);
11520
11558
  function missing(part, d, ctx) {
11521
11559
  const msg = `${pascal2(part.node.part)}: no ${part.node.component} in your registry for ${FRAMEWORK_LABEL[ctx.fw]}. Ask find_component for "${part.node.purpose.toLowerCase()}", or build it with semantic HTML and the design tokens.`;
11522
- return ctx.fw === "react" ? `${ind(d)}{/* ${msg} */}` : `${ind(d)}<!-- ${msg} -->`;
11560
+ return ctx.fw === "react" || ctx.fw === "solid" ? `${ind(d)}{/* ${msg} */}` : `${ind(d)}<!-- ${msg} -->`;
11523
11561
  }
11524
11562
  function actStatement(a, ctx, value) {
11525
- if ("store" in a) return ctx.fw === "react" ? `${setter(a.store)}(${value})` : `${ctx.fw === "angular" ? "this." : ""}${a.store} = ${value}`;
11526
- const assign = ctx.fw === "react" ? `${setter(a.set)}(${a.to})` : ctx.fw === "vue" ? `${a.set}.value = ${a.to}` : `this.${a.set} = ${a.to}`;
11563
+ const signals = ctx.fw === "react" || ctx.fw === "solid";
11564
+ if ("store" in a) return signals ? `${setter(a.store)}(${value})` : `${ctx.fw === "angular" ? "this." : ""}${a.store} = ${value}`;
11565
+ const assign = signals ? `${setter(a.set)}(${a.to})` : ctx.fw === "vue" ? `${a.set}.value = ${a.to}` : ctx.fw === "svelte" ? `${a.set} = ${a.to}` : `this.${a.set} = ${a.to}`;
11527
11566
  return a.when ? `if (${value} === ${JSON.stringify(a.when)}) ${assign};` : `${assign};`;
11528
11567
  }
11529
11568
  function renderNode(n, d, ctx) {
@@ -11582,6 +11621,8 @@ function renderMarkup(part, d, ctx) {
11582
11621
  const needsId = fw === "html" || fw === "web-components";
11583
11622
  const hasEvents = Object.keys(node2.on ?? {}).length > 0;
11584
11623
  if (needsId && (hasEvents || isBoundTarget(node2, ctx))) attrs.push(`id="${id}"`);
11624
+ const custom2 = Boolean(usage.customElement || usage.via === "web-components");
11625
+ if (fw === "svelte" || fw === "solid") return renderSignals(part, d, ctx, custom2);
11585
11626
  for (const [prop, name, value] of acceptedProps(part, fw)) {
11586
11627
  if (node2.bind?.[prop]) continue;
11587
11628
  if (isData(value)) {
@@ -11602,7 +11643,6 @@ function renderMarkup(part, d, ctx) {
11602
11643
  }
11603
11644
  for (const [prop, a] of Object.entries(node2.on ?? {})) {
11604
11645
  const ev = eventFor(part, prop);
11605
- const custom2 = usage.customElement || usage.via === "web-components";
11606
11646
  const detail = fw === "angular" ? custom2 ? "$any($event).detail" : "$event" : custom2 ? "$event.detail" : "$event";
11607
11647
  if (fw === "angular" || fw === "vue") {
11608
11648
  if ("store" in a) attrs.push(`${fw === "angular" ? `(${ev})` : `@${ev}`}="${a.store} = ${detail}"`);
@@ -11626,6 +11666,35 @@ function renderMarkup(part, d, ctx) {
11626
11666
  if (!node2.children?.length) return `${open2}</${usage.tag}>`;
11627
11667
  return [open2, ...node2.children.map((c) => renderNode(c, d + 1, ctx)), `${ind(d)}</${usage.tag}>`].join("\n");
11628
11668
  }
11669
+ function renderSignals(part, d, ctx, custom2) {
11670
+ const { node: node2, usage } = part;
11671
+ const solid = ctx.fw === "solid";
11672
+ const attrs = [];
11673
+ const bindName = (prop, name) => custom2 ? solid ? `prop:${camel(prop)}` : camel(prop) : name;
11674
+ for (const [prop, name, value] of acceptedProps(part, ctx.fw)) {
11675
+ if (node2.bind?.[prop]) continue;
11676
+ if (isData(value)) attrs.push(`${bindName(prop, name)}={${value.$data}}`);
11677
+ else if (typeof value === "string") attrs.push(`${name}="${attrEsc(value)}"`);
11678
+ else if (value === true && !solid) attrs.push(name);
11679
+ else attrs.push(`${custom2 && !solid ? name : bindName(prop, name)}={${JSON.stringify(value)}}`);
11680
+ }
11681
+ for (const [prop, v] of Object.entries(node2.bind ?? {})) attrs.push(`${bindName(prop, prop)}={${v}${solid ? "()" : ""}}`);
11682
+ for (const [prop, a] of Object.entries(node2.on ?? {})) {
11683
+ const ev = eventFor(part, prop);
11684
+ const on = custom2 ? solid ? `on:${ev}` : `on${ev}` : prop;
11685
+ const arg = custom2 ? "e: CustomEvent" : "value: string";
11686
+ const val = custom2 ? "e.detail" : "value";
11687
+ if ("store" in a) attrs.push(`${on}={(${arg}) => ${solid ? `${setter(a.store)}(${val})` : `(${a.store} = ${val})`}}`);
11688
+ else if (a.when) {
11689
+ const name = `on${pascal2(node2.part)}${pascal2(prop.replace(/^on/, ""))}`;
11690
+ ctx.handlers.push({ name, param: true, body: [actStatement(a, ctx, "value")] });
11691
+ attrs.push(`${on}={(${arg}) => ${name}(${val})}`);
11692
+ } else attrs.push(`${on}={() => ${solid ? `${setter(a.set)}(${a.to})` : `(${a.set} = ${a.to})`}}`);
11693
+ }
11694
+ const open2 = `${ind(d)}<${usage.tag}${attrs.length ? ` ${attrs.join(" ")}` : ""}`;
11695
+ if (!node2.children?.length) return custom2 || !solid ? `${open2}></${usage.tag}>` : `${open2} />`;
11696
+ return [`${open2}>`, ...node2.children.map((c) => renderNode(c, d + 1, ctx)), `${ind(d)}</${usage.tag}>`].join("\n");
11697
+ }
11629
11698
  function boundTargets(stateVar, ctx) {
11630
11699
  const out = [];
11631
11700
  walk(ctx.tpl.tree, (n) => {
@@ -11663,7 +11732,14 @@ function renderTemplate(config2, template, framework) {
11663
11732
  if (fw === "react" && [...parts.values()].some((p) => p.usage && p.component?.importPath === KIT2)) {
11664
11733
  setup.add(`Import \`${KIT2}/styles.css\` and \`brandrail.tokens.css\` once at the app entry.`);
11665
11734
  } else setup.add("Import `brandrail.tokens.css` (from `npx brandrail init`) once at the app entry, so the layout and components get the Brand Kit's variables.");
11666
- const body = renderNode(template.tree, fw === "react" ? 2 : fw === "vue" ? 1 : 0, ctx);
11735
+ const body = renderNode(template.tree, fw === "react" || fw === "solid" ? 2 : fw === "vue" ? 1 : 0, ctx);
11736
+ const isCe = (p) => Boolean(p.usage && (p.usage.via === "web-components" || p.usage.customElement));
11737
+ const cePackages = [
11738
+ ...new Set(
11739
+ [...parts.values()].filter(isCe).map((p) => p.component?.frameworks?.["web-components"]?.importPath ?? p.component?.frameworks?.html?.importPath).filter((x2) => !!x2 && !/\.css(\?|$)/.test(x2))
11740
+ )
11741
+ ];
11742
+ const ownImports = [...new Set([...parts.values()].filter((p) => p.usage && !isCe(p) && p.usage.importLine).map((p) => p.usage.importLine))];
11667
11743
  const usedData = Object.keys(template.data).filter((k) => JSON.stringify(template.tree).includes(`"$data":"${k}"`));
11668
11744
  const dataDecl = (decl, kw, pad = "", key = decl) => `${kw} ${decl} = ${literal(template.data[key], pad)};`;
11669
11745
  const files = [];
@@ -11736,6 +11812,72 @@ function renderTemplate(config2, template, framework) {
11736
11812
  { path: `${base}-page.component.html`, lang: "html", content: body + "\n" },
11737
11813
  { path: `${base}-page.component.css`, lang: "css", content: css + "\n" }
11738
11814
  );
11815
+ } else if (fw === "svelte") {
11816
+ const state = Object.entries(template.state);
11817
+ const script = [
11818
+ `<script lang="ts">`,
11819
+ ...cePackages.length ? [`import { onMount } from "svelte";`] : [],
11820
+ ...ownImports,
11821
+ `import "./${base}-page.css";`,
11822
+ "",
11823
+ ...cePackages.length ? ["// Custom elements need the browser: load them once the page is mounted (fine with SvelteKit's server rendering).", `onMount(() => {
11824
+ ${cePackages.map((u) => ` import("${u}");`).join("\n")}
11825
+ });`, ""] : [],
11826
+ ...state.map(([k, v]) => `let ${k} = $state(${JSON.stringify(v)});`),
11827
+ ...usedData.map((k) => dataDecl(k, "const")),
11828
+ ...ctx.handlers.map((h) => `
11829
+ function ${h.name}(value: string) {
11830
+ ${h.body.join("\n ")}
11831
+ }`),
11832
+ `</script>`
11833
+ ].join("\n");
11834
+ files.push({ path: `${pascal2(base)}Page.svelte`, lang: "svelte", content: `${script}
11835
+
11836
+ ${body}
11837
+ ` }, { path: `${base}-page.css`, lang: "css", content: css + "\n" });
11838
+ } else if (fw === "solid") {
11839
+ const state = Object.entries(template.state);
11840
+ const solidImports = [...state.length ? ["createSignal"] : [], ...cePackages.length ? ["onMount"] : []];
11841
+ const code = [
11842
+ ...solidImports.length ? [`import { ${solidImports.join(", ")} } from "solid-js";`] : [],
11843
+ ...ownImports,
11844
+ `import "./${base}-page.css";`,
11845
+ "",
11846
+ ...usedData.flatMap((k) => [dataDecl(k, "const"), ""]),
11847
+ `export default function ${Name}() {`,
11848
+ ...state.map(([k, v]) => ` const [${k}, ${setter(k)}] = createSignal(${JSON.stringify(v)});`),
11849
+ ...ctx.handlers.map((h) => ` const ${h.name} = (value: string) => {
11850
+ ${h.body.join("\n ")}
11851
+ };`),
11852
+ ...cePackages.length ? [" // Custom elements need the browser: load them once the page is in the DOM (fine with SolidStart's server rendering).", ...cePackages.map((u) => ` onMount(() => void import("${u}"));`)] : [],
11853
+ "",
11854
+ " return (",
11855
+ body,
11856
+ " );",
11857
+ "}",
11858
+ ""
11859
+ ].join("\n");
11860
+ files.push({ path: `${pascal2(base)}Page.tsx`, lang: "tsx", content: code }, { path: `${base}-page.css`, lang: "css", content: css + "\n" });
11861
+ const prefixes = [...new Set([...parts.values()].filter(isCe).map((p) => p.usage.tag.includes("-") ? `${p.usage.tag.split("-")[0]}-` : p.usage.tag))];
11862
+ if (prefixes.length) {
11863
+ files.push({
11864
+ path: "brandrail-elements.d.ts",
11865
+ lang: "ts",
11866
+ content: [
11867
+ "// Lets TypeScript accept the design system's custom elements in Solid TSX. Keep one copy in the app.",
11868
+ 'import "solid-js";',
11869
+ "",
11870
+ 'declare module "solid-js" {',
11871
+ " namespace JSX {",
11872
+ " interface IntrinsicElements {",
11873
+ ...prefixes.map((p) => ` [tag: \`${p}\${string}\`]: any;`),
11874
+ " }",
11875
+ " }",
11876
+ "}",
11877
+ ""
11878
+ ].join("\n")
11879
+ });
11880
+ }
11739
11881
  } else if (fw === "vue") {
11740
11882
  const state = Object.entries(template.state);
11741
11883
  const script = [
@@ -11829,6 +11971,433 @@ function templateList() {
11829
11971
  return PAGE_TEMPLATES.map((t) => `- \`${t.id}\` \u2014 ${t.name}: ${t.description}`).join("\n");
11830
11972
  }
11831
11973
 
11974
+ // ../engine/src/platforms.ts
11975
+ var EXPORT_FORMATS = [
11976
+ { id: "css", label: "CSS variables", platform: "web", description: "One variable per token, dark mode included" },
11977
+ { id: "tailwind", label: "Tailwind v4", platform: "web", description: "@theme mapping onto the CSS variables" },
11978
+ { id: "json", label: "DTCG JSON", platform: "web", description: "The W3C design tokens file" },
11979
+ { id: "ios", label: "iOS (SwiftUI)", platform: "mobile", description: "Color, spacing, radius and Font extensions; dark mode follows the system" },
11980
+ { id: "compose", label: "Android (Compose)", platform: "mobile", description: "Kotlin: colour schemes for light and dark, dp and sp scales, TextStyles" },
11981
+ { id: "android-xml", label: "Android (XML)", platform: "mobile", description: "colors.xml with a values-night copy, and dimens.xml" },
11982
+ { id: "react-native", label: "React Native", platform: "mobile", description: "A typed module with light and dark colours and a useBrandColors hook" },
11983
+ { id: "flutter", label: "Flutter", platform: "mobile", description: "A ThemeExtension for colours, plus spacing, radius and TextStyles" }
11984
+ ];
11985
+ var hexToRgba = (hex4) => {
11986
+ const m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(hex4.trim());
11987
+ if (!m) return null;
11988
+ const n = Number.parseInt(m[1], 16);
11989
+ return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255, a: m[2] ? Number.parseInt(m[2], 16) / 255 : 1 };
11990
+ };
11991
+ var px3 = (v) => {
11992
+ const m = /^(-?\d*\.?\d+)(px|rem|em)?$/.exec(v.trim());
11993
+ if (!m) return null;
11994
+ const n = Number(m[1]);
11995
+ return m[2] === "rem" || m[2] === "em" ? n * 16 : n;
11996
+ };
11997
+ function tokenIdentifier(segments, prefix) {
11998
+ const words = [...prefix ? [prefix] : [], ...segments].flatMap((s) => s.split(/[^A-Za-z0-9]+/)).filter(Boolean);
11999
+ const id = words.map((w, i2) => i2 === 0 ? w.charAt(0).toLowerCase() + w.slice(1) : w.charAt(0).toUpperCase() + w.slice(1)).join("");
12000
+ return /^[0-9]/.test(id) ? `t${id}` : id || "token";
12001
+ }
12002
+ var GENERIC = /^(-apple-system|blinkmacsystemfont|system-ui|ui-sans-serif|ui-serif|ui-monospace|sfmono-regular|segoe ui|roboto|helvetica|helvetica neue|arial|noto sans|sans-serif|serif|monospace|menlo|consolas)$/i;
12003
+ function bundledFamily(stack) {
12004
+ for (const raw of stack.split(",")) {
12005
+ const f = raw.trim().replace(/^["']|["']$/g, "");
12006
+ if (f && !GENERIC.test(f)) return f;
12007
+ }
12008
+ return null;
12009
+ }
12010
+ var RESERVED = new Set(
12011
+ "as break case catch class const continue default do else enum extends extension false final for fun func if import in interface is let new null object operator package private protocol public return static struct super switch this throw true try type typealias val var void when where while with yield".split(" ")
12012
+ );
12013
+ function groups(tokens) {
12014
+ const flat = flattenTokens(tokens);
12015
+ const deprecated = new Map(tokenDeprecations(tokens).map((d) => [d.path, d]));
12016
+ const g = { colors: [], spacing: [], radius: [], type: [], fonts: [], other: [] };
12017
+ const sizes = /* @__PURE__ */ new Map();
12018
+ const lines = /* @__PURE__ */ new Map();
12019
+ const weights = /* @__PURE__ */ new Map();
12020
+ const used = /* @__PURE__ */ new Set();
12021
+ const unique = (raw) => {
12022
+ const id = RESERVED.has(raw) ? `${raw}Token` : raw;
12023
+ let out = id;
12024
+ for (let i2 = 2; used.has(out); i2++) out = `${id}${i2}`;
12025
+ used.add(out);
12026
+ return out;
12027
+ };
12028
+ for (const t of flat) {
12029
+ const seg = t.path.split(".");
12030
+ if (t.type === "color") {
12031
+ const light = hexToRgba(t.value);
12032
+ if (!light) continue;
12033
+ const d = deprecated.get(t.path);
12034
+ g.colors.push({
12035
+ name: unique(tokenIdentifier(seg[0] === "color" ? seg.slice(1) : seg)),
12036
+ path: t.path,
12037
+ light,
12038
+ dark: t.dark && hexToRgba(t.dark) || light,
12039
+ description: t.description?.replace(/\s+/g, " ").trim() || void 0,
12040
+ deprecated: d ? `Deprecated${d.replacement ? `: use ${d.replacement}` : ""}${d.removeAfter ? ` (removed after ${d.removeAfter})` : ""}${d.reason ? `. ${d.reason.replace(/\s+/g, " ")}` : ""}` : void 0
12041
+ });
12042
+ } else if (t.path.startsWith("font.size.")) {
12043
+ const v = px3(t.value);
12044
+ if (v !== null) sizes.set(seg.slice(2).join("."), v);
12045
+ } else if (t.path.startsWith("font.line-height.")) {
12046
+ const v = px3(t.value);
12047
+ if (v !== null) lines.set(seg.slice(2).join("."), v);
12048
+ } else if (t.path.startsWith("font.weight.") || t.type === "fontWeight") {
12049
+ const v = Number.parseInt(t.value, 10);
12050
+ if (Number.isFinite(v)) weights.set(seg.slice(2).join("."), v);
12051
+ } else if (t.type === "fontFamily" || t.path.startsWith("font.family.")) {
12052
+ g.fonts.push({ name: unique(tokenIdentifier(seg.slice(2).length ? seg.slice(2) : seg)), path: t.path, family: bundledFamily(t.value), stack: t.value });
12053
+ } else if (/^(space|spacing)\./.test(t.path)) {
12054
+ const v = px3(t.value);
12055
+ if (v !== null && !/\.unit$/.test(t.path)) g.spacing.push({ name: unique(tokenIdentifier(seg.slice(1), "space")), path: t.path, px: v });
12056
+ } else if (/^(radius|radii)\./.test(t.path)) {
12057
+ const v = px3(t.value);
12058
+ if (v !== null) g.radius.push({ name: unique(tokenIdentifier(seg.slice(1), "radius")), path: t.path, px: Math.min(v, 9999) });
12059
+ } else if (t.type === "dimension" || t.type === "number") {
12060
+ const v = px3(t.value);
12061
+ if (v !== null) g.other.push({ name: unique(tokenIdentifier(seg)), path: t.path, px: v });
12062
+ }
12063
+ }
12064
+ for (const [step, size] of sizes) {
12065
+ g.type.push({ name: unique(tokenIdentifier(step.split("."), "text")), size, lineHeight: lines.get(step), weight: weights.get(step) ?? 400 });
12066
+ }
12067
+ g.type.sort((a, b) => a.size - b.size);
12068
+ return g;
12069
+ }
12070
+ var num = (n, places = 2) => Number.isInteger(n) ? String(n) : String(Math.round(n * 10 ** places) / 10 ** places);
12071
+ var hex2 = (n) => Math.round(n).toString(16).padStart(2, "0").toUpperCase();
12072
+ var argb = (c) => `0x${hex2(c.a * 255)}${hex2(c.r)}${hex2(c.g)}${hex2(c.b)}`;
12073
+ var rgbHex = (c) => `#${hex2(c.r)}${hex2(c.g)}${hex2(c.b)}${c.a < 1 ? hex2(c.a * 255) : ""}`.toLowerCase();
12074
+ var head = (comment, opts) => `${comment} Generated by Brandrail from the ${opts.org ? `${opts.org} ` : ""}Brand Kit${opts.version ? ` v${opts.version}` : ""}. Do not edit: run \`npx brandrail export\` to update.`;
12075
+ function swift(g, o) {
12076
+ const ui = (c) => `rgb(0x${hex2(c.r)}${hex2(c.g)}${hex2(c.b)}${c.a < 1 ? `, ${num(c.a, 3)}` : ""})`;
12077
+ const font = g.fonts[0];
12078
+ const lines = [
12079
+ head("//", o),
12080
+ "import SwiftUI",
12081
+ "#if canImport(UIKit)",
12082
+ "import UIKit",
12083
+ "",
12084
+ "private func rgb(_ hex: UInt32, _ alpha: CGFloat = 1) -> UIColor {",
12085
+ " UIColor(red: CGFloat((hex >> 16) & 0xFF) / 255, green: CGFloat((hex >> 8) & 0xFF) / 255, blue: CGFloat(hex & 0xFF) / 255, alpha: alpha)",
12086
+ "}",
12087
+ "",
12088
+ "private func dynamic(_ light: UIColor, _ dark: UIColor) -> Color {",
12089
+ " Color(UIColor { $0.userInterfaceStyle == .dark ? dark : light })",
12090
+ "}",
12091
+ "",
12092
+ "/// Brand colours. Each one switches with the system's light or dark appearance.",
12093
+ "public enum BrandColor {",
12094
+ ...g.colors.flatMap((c) => [
12095
+ ...c.description ? [` /// ${c.description}`] : [],
12096
+ ...c.deprecated ? [` @available(*, deprecated, message: ${JSON.stringify(c.deprecated)})`] : [],
12097
+ ` public static let ${c.name} = dynamic(${ui(c.light)}, ${ui(c.dark)})`
12098
+ ]),
12099
+ "}",
12100
+ "#endif",
12101
+ "",
12102
+ "/// Spacing scale in points. Padding, margins and stack spacing use these.",
12103
+ "public enum BrandSpacing {",
12104
+ ...g.spacing.map((s) => ` public static let ${s.name}: CGFloat = ${num(s.px)}`),
12105
+ "}",
12106
+ "",
12107
+ "/// Corner radius scale in points.",
12108
+ "public enum BrandRadius {",
12109
+ ...g.radius.map((r) => ` public static let ${r.name}: CGFloat = ${num(r.px)}`),
12110
+ "}",
12111
+ "",
12112
+ `/// The type scale${font?.family ? `, in ${font.family} (add the font files to the app and Info.plist)` : " in the system font"}.`,
12113
+ "public enum BrandFont {",
12114
+ ...g.type.map((t) => {
12115
+ const w = t.weight >= 700 ? ".bold" : t.weight >= 600 ? ".semibold" : t.weight >= 500 ? ".medium" : t.weight <= 300 ? ".light" : ".regular";
12116
+ return font?.family ? ` public static let ${t.name} = Font.custom("${font.family}", size: ${num(t.size)}).weight(${w})` : ` public static let ${t.name} = Font.system(size: ${num(t.size)}, weight: ${w})`;
12117
+ }),
12118
+ "",
12119
+ " /// Extra space between lines for each step (line height minus font size), for .lineSpacing().",
12120
+ ...g.type.filter((t) => t.lineHeight).map((t) => ` public static let ${t.name}LineSpacing: CGFloat = ${num(Math.max(0, t.lineHeight - t.size))}`),
12121
+ "}",
12122
+ ""
12123
+ ];
12124
+ return {
12125
+ format: "ios",
12126
+ files: [{ path: "BrandTokens.swift", content: lines.join("\n") }],
12127
+ usage: [
12128
+ "Add BrandTokens.swift to the app target.",
12129
+ "Use the names in views: .foregroundStyle(BrandColor.fgDefault), .padding(BrandSpacing.space4), .font(BrandFont.textBase).",
12130
+ "Dark mode follows the system appearance automatically."
12131
+ ]
12132
+ };
12133
+ }
12134
+ function compose(g, o, pkg = "com.brand.tokens") {
12135
+ const weight = (w) => `FontWeight(${Math.round(w / 100) * 100})`;
12136
+ const font = g.fonts[0];
12137
+ const content = [
12138
+ head("//", o),
12139
+ `package ${pkg}`,
12140
+ "",
12141
+ "import androidx.compose.foundation.isSystemInDarkTheme",
12142
+ "import androidx.compose.runtime.Composable",
12143
+ "import androidx.compose.runtime.Immutable",
12144
+ "import androidx.compose.runtime.ReadOnlyComposable",
12145
+ "import androidx.compose.runtime.staticCompositionLocalOf",
12146
+ "import androidx.compose.ui.graphics.Color",
12147
+ "import androidx.compose.ui.text.TextStyle",
12148
+ "import androidx.compose.ui.text.font.FontWeight",
12149
+ "import androidx.compose.ui.unit.dp",
12150
+ "import androidx.compose.ui.unit.sp",
12151
+ "",
12152
+ "/** Brand colours. Read them with BrandTheme.colors, which follows the system's dark theme. */",
12153
+ "@Immutable",
12154
+ "data class BrandColors(",
12155
+ ...g.colors.map((c) => ` ${c.deprecated ? `@Deprecated(${JSON.stringify(c.deprecated).replace(/\$/g, "\\$")}) ` : ""}val ${c.name}: Color,`),
12156
+ ")",
12157
+ "",
12158
+ "val BrandLightColors = BrandColors(",
12159
+ ...g.colors.map((c) => ` ${c.name} = Color(${argb(c.light)}),`),
12160
+ ")",
12161
+ "",
12162
+ "val BrandDarkColors = BrandColors(",
12163
+ ...g.colors.map((c) => ` ${c.name} = Color(${argb(c.dark)}),`),
12164
+ ")",
12165
+ "",
12166
+ "val LocalBrandColors = staticCompositionLocalOf { BrandLightColors }",
12167
+ "",
12168
+ "object BrandTheme {",
12169
+ " val colors: BrandColors",
12170
+ " @Composable @ReadOnlyComposable get() = LocalBrandColors.current",
12171
+ "}",
12172
+ "",
12173
+ "/** Picks light or dark colours from the system setting. Wrap the app (or a screen) in it. */",
12174
+ "@Composable",
12175
+ "fun brandColors(darkTheme: Boolean = isSystemInDarkTheme()): BrandColors = if (darkTheme) BrandDarkColors else BrandLightColors",
12176
+ "",
12177
+ "/** Spacing scale. Padding, margins and Arrangement.spacedBy use these. */",
12178
+ "object BrandSpacing {",
12179
+ ...g.spacing.map((s) => ` val ${s.name} = ${num(s.px)}.dp`),
12180
+ "}",
12181
+ "",
12182
+ "/** Corner radius scale: RoundedCornerShape(BrandRadius.radiusMd). */",
12183
+ "object BrandRadius {",
12184
+ ...g.radius.map((r) => ` val ${r.name} = ${num(r.px)}.dp`),
12185
+ "}",
12186
+ "",
12187
+ `/** The type scale${font?.family ? `. Set fontFamily to ${font.family} once it's in res/font` : ""}. */`,
12188
+ "object BrandType {",
12189
+ ...g.type.map((t) => ` val ${t.name} = TextStyle(fontSize = ${num(t.size)}.sp${t.lineHeight ? `, lineHeight = ${num(t.lineHeight)}.sp` : ""}, fontWeight = ${weight(t.weight)})`),
12190
+ "}",
12191
+ ""
12192
+ ].join("\n");
12193
+ return {
12194
+ format: "compose",
12195
+ files: [{ path: "BrandTokens.kt", content }],
12196
+ usage: [
12197
+ `Add BrandTokens.kt to the app module (package ${pkg}; change it to yours).`,
12198
+ "Provide the colours once: CompositionLocalProvider(LocalBrandColors provides brandColors()) { App() }",
12199
+ "Then use BrandTheme.colors.actionPrimary, Modifier.padding(BrandSpacing.space4), BrandType.textBase."
12200
+ ]
12201
+ };
12202
+ }
12203
+ function androidXml(g, o) {
12204
+ const res = (id) => `brand_${id.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase()}`;
12205
+ const color = (c) => `#${hex2(c.a * 255)}${hex2(c.r)}${hex2(c.g)}${hex2(c.b)}`;
12206
+ const xml = (body) => [`<?xml version="1.0" encoding="utf-8"?>`, `<!-- ${head("", o).trim()} -->`, "<resources>", ...body, "</resources>", ""].join("\n");
12207
+ return {
12208
+ format: "android-xml",
12209
+ files: [
12210
+ { path: "values/brand_colors.xml", content: xml(g.colors.map((c) => ` <color name="${res(c.name)}">${color(c.light)}</color>${c.deprecated ? ` <!-- ${c.deprecated} -->` : ""}`)) },
12211
+ { path: "values-night/brand_colors.xml", content: xml(g.colors.map((c) => ` <color name="${res(c.name)}">${color(c.dark)}</color>`)) },
12212
+ {
12213
+ path: "values/brand_dimens.xml",
12214
+ content: xml([
12215
+ ...g.spacing.map((s) => ` <dimen name="${res(s.name)}">${num(s.px)}dp</dimen>`),
12216
+ ...g.radius.map((r) => ` <dimen name="${res(r.name)}">${num(r.px)}dp</dimen>`),
12217
+ ...g.type.map((t) => ` <dimen name="${res(t.name)}_size">${num(t.size)}sp</dimen>`),
12218
+ ...g.type.filter((t) => t.lineHeight).map((t) => ` <dimen name="${res(t.name)}_line_height">${num(t.lineHeight)}sp</dimen>`)
12219
+ ])
12220
+ }
12221
+ ],
12222
+ usage: [
12223
+ "Copy the three files into app/src/main/res/ (values-night switches the colours in dark mode).",
12224
+ "Use them as @color/brand_action_primary and @dimen/brand_space4 in layouts, or R.color.brand_action_primary in code."
12225
+ ]
12226
+ };
12227
+ }
12228
+ function reactNative(g, o) {
12229
+ const font = g.fonts[0];
12230
+ const obj = (pairs, pad = " ") => pairs.map(([k, v]) => `${pad}${k}: ${v},`).join("\n");
12231
+ const content = [
12232
+ head("//", o),
12233
+ 'import { useColorScheme } from "react-native";',
12234
+ "",
12235
+ "/** Brand colours in light mode. */",
12236
+ `export const lightColors = {
12237
+ ${obj(g.colors.map((c) => [c.name, JSON.stringify(rgbHex(c.light))]))}
12238
+ } as const;`,
12239
+ "",
12240
+ "/** Brand colours in dark mode. */",
12241
+ `export const darkColors: { [K in keyof typeof lightColors]: string } = {
12242
+ ${obj(g.colors.map((c) => [c.name, JSON.stringify(rgbHex(c.dark))]))}
12243
+ };`,
12244
+ "",
12245
+ "export type BrandColors = { [K in keyof typeof lightColors]: string };",
12246
+ "",
12247
+ "/** The colours for the system's light or dark appearance. */",
12248
+ "export function useBrandColors(): BrandColors {",
12249
+ ' return useColorScheme() === "dark" ? darkColors : lightColors;',
12250
+ "}",
12251
+ "",
12252
+ "/** Spacing scale, for padding, margin and gap. */",
12253
+ `export const spacing = {
12254
+ ${obj(g.spacing.map((s) => [s.name, num(s.px)]))}
12255
+ } as const;`,
12256
+ "",
12257
+ "/** Corner radius scale, for borderRadius. */",
12258
+ `export const radius = {
12259
+ ${obj(g.radius.map((r) => [r.name, num(r.px)]))}
12260
+ } as const;`,
12261
+ "",
12262
+ `/** The type scale as Text styles${font?.family ? ` (${font.family} must be linked into the app)` : ""}. */`,
12263
+ `export const type = {
12264
+ ${obj(
12265
+ g.type.map((t) => [
12266
+ t.name,
12267
+ `{ ${[`fontSize: ${num(t.size)}`, ...t.lineHeight ? [`lineHeight: ${num(t.lineHeight)}`] : [], `fontWeight: "${Math.round(t.weight / 100) * 100}" as const`, ...font?.family ? [`fontFamily: ${JSON.stringify(font.family)}`] : []].join(", ")} }`
12268
+ ])
12269
+ )}
12270
+ } as const;`,
12271
+ ""
12272
+ ].join("\n");
12273
+ return {
12274
+ format: "react-native",
12275
+ files: [{ path: "brandTokens.ts", content }],
12276
+ usage: [
12277
+ "Add brandTokens.ts to the app (e.g. src/theme/).",
12278
+ "In a component: const colors = useBrandColors(); style={{ backgroundColor: colors.canvasDefault, padding: spacing.space4 }} and <Text style={type.textBase}>."
12279
+ ]
12280
+ };
12281
+ }
12282
+ function flutter(g, o) {
12283
+ const font = g.fonts[0];
12284
+ const fields = g.colors;
12285
+ const content = [
12286
+ head("//", o),
12287
+ "import 'package:flutter/material.dart';",
12288
+ "",
12289
+ "/// Brand colours as a ThemeExtension: add BrandColors.light and BrandColors.dark to your ThemeData,",
12290
+ "/// then read them with Theme.of(context).extension<BrandColors>()!.",
12291
+ "@immutable",
12292
+ "class BrandColors extends ThemeExtension<BrandColors> {",
12293
+ " const BrandColors({",
12294
+ ...fields.map((c) => ` required this.${c.name},`),
12295
+ " });",
12296
+ "",
12297
+ ...fields.flatMap((c) => [...c.description ? [` /// ${c.description}`] : [], ...c.deprecated ? [` @Deprecated('${c.deprecated.replace(/[\\'$]/g, "\\$&")}')`] : [], ` final Color ${c.name};`]),
12298
+ "",
12299
+ " static const light = BrandColors(",
12300
+ ...fields.map((c) => ` ${c.name}: Color(${argb(c.light)}),`),
12301
+ " );",
12302
+ "",
12303
+ " static const dark = BrandColors(",
12304
+ ...fields.map((c) => ` ${c.name}: Color(${argb(c.dark)}),`),
12305
+ " );",
12306
+ "",
12307
+ " @override",
12308
+ " BrandColors copyWith({",
12309
+ ...fields.map((c) => ` Color? ${c.name},`),
12310
+ " }) {",
12311
+ " return BrandColors(",
12312
+ ...fields.map((c) => ` ${c.name}: ${c.name} ?? this.${c.name},`),
12313
+ " );",
12314
+ " }",
12315
+ "",
12316
+ " @override",
12317
+ " BrandColors lerp(ThemeExtension<BrandColors>? other, double t) {",
12318
+ " if (other is! BrandColors) return this;",
12319
+ " return BrandColors(",
12320
+ ...fields.map((c) => ` ${c.name}: Color.lerp(${c.name}, other.${c.name}, t)!,`),
12321
+ " );",
12322
+ " }",
12323
+ "}",
12324
+ "",
12325
+ "/// Spacing scale, for EdgeInsets and SizedBox.",
12326
+ "abstract final class BrandSpacing {",
12327
+ ...g.spacing.map((s) => ` static const double ${s.name} = ${num(s.px)};`),
12328
+ "}",
12329
+ "",
12330
+ "/// Corner radius scale: BorderRadius.circular(BrandRadius.radiusMd).",
12331
+ "abstract final class BrandRadius {",
12332
+ ...g.radius.map((r) => ` static const double ${r.name} = ${num(r.px)};`),
12333
+ "}",
12334
+ "",
12335
+ `/// The type scale${font?.family ? ` (declare the ${font.family} font in pubspec.yaml)` : ""}.`,
12336
+ "abstract final class BrandText {",
12337
+ ...g.type.map((t) => {
12338
+ const parts = [`fontSize: ${num(t.size)}`, ...t.lineHeight ? [`height: ${num(t.lineHeight / t.size, 3)}`] : [], `fontWeight: FontWeight.w${Math.min(900, Math.max(100, Math.round(t.weight / 100) * 100))}`, ...font?.family ? [`fontFamily: '${font.family.replace(/'/g, "\\'")}'`] : []];
12339
+ return ` static const TextStyle ${t.name} = TextStyle(${parts.join(", ")});`;
12340
+ }),
12341
+ "}",
12342
+ ""
12343
+ ].join("\n");
12344
+ return {
12345
+ format: "flutter",
12346
+ files: [{ path: "brand_tokens.dart", content }],
12347
+ usage: [
12348
+ "Add brand_tokens.dart to lib/.",
12349
+ "ThemeData(extensions: [BrandColors.light]) and darkTheme: ThemeData(extensions: [BrandColors.dark]).",
12350
+ "Then Theme.of(context).extension<BrandColors>()!.actionPrimary, EdgeInsets.all(BrandSpacing.space4), BrandText.textBase."
12351
+ ]
12352
+ };
12353
+ }
12354
+ var cssVar2 = (path) => `--${path.replace(/\./g, "-")}`;
12355
+ function tailwind(flat) {
12356
+ const step = (p, re) => p.replace(re, "").replace(/\./g, "-");
12357
+ const lines = [
12358
+ ...flat.filter((t) => t.type === "color").map((t) => ` --color-${step(t.path, /^color\./)}: var(${cssVar2(t.path)});`),
12359
+ ...flat.filter((t) => t.path.startsWith("font.family.")).map((t) => ` --font-${step(t.path, /^font\.family\./)}: var(${cssVar2(t.path)});`),
12360
+ ...flat.filter((t) => t.path.startsWith("font.size.")).flatMap((t) => {
12361
+ const name = step(t.path, /^font\.size\./);
12362
+ const lh = flat.find((x2) => x2.path === `font.line-height.${t.path.slice("font.size.".length)}`);
12363
+ return [` --text-${name}: ${t.value};`, ...lh ? [` --text-${name}--line-height: ${lh.value};`] : []];
12364
+ }),
12365
+ ...flat.filter((t) => /^(radius|radii)\./.test(t.path)).map((t) => ` --radius-${step(t.path, /^(radius|radii)\./)}: ${t.value};`),
12366
+ ...flat.filter((t) => /^(space|spacing)\./.test(t.path) && !/\.unit$/.test(t.path)).map((t) => ` --spacing-${step(t.path, /^(space|spacing)\./)}: ${t.value};`)
12367
+ ];
12368
+ return `/* Paste after the CSS variables export, in your main stylesheet */
12369
+ @theme inline {
12370
+ --color-*: initial;
12371
+ --spacing-*: initial;
12372
+ --radius-*: initial;
12373
+ --text-*: initial;
12374
+ ${lines.join("\n")}
12375
+ }
12376
+ `;
12377
+ }
12378
+ function exportTokens(tokens, format, opts = {}) {
12379
+ switch (format) {
12380
+ case "css":
12381
+ return { format, files: [{ path: "brandrail.tokens.css", content: generateTokensCss(tokens, opts) }], usage: ["Import it once at the app entry, before other styles."] };
12382
+ case "tailwind":
12383
+ return { format, files: [{ path: "brandrail-theme.css", content: tailwind(flattenTokens(tokens)) }], usage: ["Import it after brandrail.tokens.css in the stylesheet that loads Tailwind v4."] };
12384
+ case "json":
12385
+ return { format, files: [{ path: "tokens.json", content: `${JSON.stringify(tokens ?? {}, null, 2)}
12386
+ ` }], usage: ["Feed it to Style Dictionary, Tokens Studio or any DTCG tool."] };
12387
+ case "ios":
12388
+ return swift(groups(tokens), opts);
12389
+ case "compose":
12390
+ return compose(groups(tokens), opts);
12391
+ case "android-xml":
12392
+ return androidXml(groups(tokens), opts);
12393
+ case "react-native":
12394
+ return reactNative(groups(tokens), opts);
12395
+ case "flutter":
12396
+ return flutter(groups(tokens), opts);
12397
+ }
12398
+ }
12399
+ var isExportFormat = (x2) => EXPORT_FORMATS.some((f) => f.id === x2);
12400
+
11832
12401
  // src/api.ts
11833
12402
  var DEFAULT_API = "https://www.brandrail.com.au";
11834
12403
  function apiBase() {
@@ -12150,7 +12719,7 @@ __export(external_exports, {
12150
12719
  gte: () => _gte,
12151
12720
  guid: () => guid2,
12152
12721
  hash: () => hash,
12153
- hex: () => hex2,
12722
+ hex: () => hex3,
12154
12723
  hostname: () => hostname3,
12155
12724
  httpUrl: () => httpUrl,
12156
12725
  iban: () => iban2,
@@ -13311,8 +13880,8 @@ function base64urlToUint8Array(base64url3) {
13311
13880
  function uint8ArrayToBase64url(bytes) {
13312
13881
  return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
13313
13882
  }
13314
- function hexToUint8Array(hex3) {
13315
- const cleanHex = hex3.replace(/^0x/, "");
13883
+ function hexToUint8Array(hex4) {
13884
+ const cleanHex = hex4.replace(/^0x/, "");
13316
13885
  if (cleanHex.length % 2 !== 0) {
13317
13886
  throw new Error("Invalid hex string length");
13318
13887
  }
@@ -29089,7 +29658,7 @@ __export(schemas_exports2, {
29089
29658
  function: () => _function,
29090
29659
  guid: () => guid2,
29091
29660
  hash: () => hash,
29092
- hex: () => hex2,
29661
+ hex: () => hex3,
29093
29662
  hostname: () => hostname3,
29094
29663
  httpUrl: () => httpUrl,
29095
29664
  iban: () => iban2,
@@ -29782,7 +30351,7 @@ function stringFormat(format, fnOrRegex, _params = {}) {
29782
30351
  function hostname3(_params) {
29783
30352
  return _stringFormat(ZodCustomStringFormat, "hostname", hostname2, _params);
29784
30353
  }
29785
- function hex2(_params) {
30354
+ function hex3(_params) {
29786
30355
  return _stringFormat(ZodCustomStringFormat, "hex", hex, _params);
29787
30356
  }
29788
30357
  function currencyCode2(_params) {
@@ -41421,7 +41990,7 @@ var CATEGORY = {
41421
41990
  radius: (l) => /^(radius|radii)\./.test(l.path),
41422
41991
  typography: (l) => l.path.startsWith("font.") || l.type === "fontFamily"
41423
41992
  };
41424
- var cssVar2 = (path) => `var(--${path.replace(/\./g, "-")})`;
41993
+ var cssVar3 = (path) => `var(--${path.replace(/\./g, "-")})`;
41425
41994
  function componentDoc(c, fw) {
41426
41995
  const u = resolveUsage(c, fw);
41427
41996
  const others = availableFrameworks(c).filter((f) => f !== fw).map((f) => FRAMEWORK_LABEL[f]);
@@ -41467,13 +42036,13 @@ function componentDoc(c, fw) {
41467
42036
  if (others.length) lines.push("", `Also available for: ${others.join(", ")}.`);
41468
42037
  return lines.join("\n");
41469
42038
  }
41470
- var frameworkInput = external_exports.enum(FRAMEWORKS).optional().describe("The project's UI framework (from its package.json: @angular/core \u2192 angular, vue \u2192 vue, react \u2192 react). Defaults to the one set for this connection.");
42039
+ var frameworkInput = external_exports.enum(FRAMEWORKS).optional().describe("The project's UI framework (from its package.json: @angular/core \u2192 angular, vue \u2192 vue, svelte \u2192 svelte, solid-js \u2192 solid, react \u2192 react). Defaults to the one set for this connection.");
41471
42040
  var text = (t) => ({ content: [{ type: "text", text: t }] });
41472
42041
  function createBrandrailMcp(ctx) {
41473
42042
  const server = new McpServer(
41474
42043
  { name: "brandrail", title: `Brandrail \u2014 ${ctx.org} design system`, version: ENGINE_VERSION },
41475
42044
  {
41476
- instructions: `Design system for ${ctx.org}. Before writing UI: call get_guidelines once. For a new page or screen, start from get_template. Use find_component for every UI element (pass \`framework\` \u2014 angular, vue, react, web-components or html \u2014 when the project isn't ${ctx.framework ? FRAMEWORK_LABEL[ctx.framework] : "React"}) (use only the props it lists) and get_design_tokens for colours/spacing. After writing UI: call validate_snippet once with all changed files and fix every violation before finishing.`
42045
+ instructions: `Design system for ${ctx.org}. Before writing UI: call get_guidelines once. For a new page or screen, start from get_template. Use find_component for every UI element (pass \`framework\` \u2014 angular, vue, svelte, solid, react, web-components or html \u2014 when the project isn't ${ctx.framework ? FRAMEWORK_LABEL[ctx.framework] : "React"}) (use only the props it lists) and get_design_tokens for colours/spacing. After writing UI: call validate_snippet once with all changed files and fix every violation before finishing.`
41477
42046
  }
41478
42047
  );
41479
42048
  const resolved = resolveConfig(ctx.config);
@@ -41504,8 +42073,8 @@ function createBrandrailMcp(ctx) {
41504
42073
  lines.push("| Token | Use | Value | Meaning |", "| --- | --- | --- | --- |");
41505
42074
  for (const l of pick2) {
41506
42075
  const dep = resolved.deprecatedTokens.get(`--${l.path.replace(/\./g, "-")}`);
41507
- const note = dep ? `**Deprecated**${dep.replacement ? `: use \`${cssVar2(dep.replacement)}\`` : ""}${dep.removeAfter ? ` (removed after ${dep.removeAfter})` : ""}.` : l.description ?? "";
41508
- lines.push(`| ${l.path} | \`${cssVar2(l.path)}\` | ${String(l.value)}${l.dark ? ` (dark ${String(l.dark)})` : ""} | ${note} |`);
42076
+ const note = dep ? `**Deprecated**${dep.replacement ? `: use \`${cssVar3(dep.replacement)}\`` : ""}${dep.removeAfter ? ` (removed after ${dep.removeAfter})` : ""}.` : l.description ?? "";
42077
+ lines.push(`| ${l.path} | \`${cssVar3(l.path)}\` | ${String(l.value)}${l.dark ? ` (dark ${String(l.dark)})` : ""} | ${note} |`);
41509
42078
  }
41510
42079
  if (pick2.some((l) => l.dark)) lines.push("", "Colours with a dark value switch automatically: always use the variable, never the hex, so both themes work.");
41511
42080
  if (category === "all" || category === "spacing") lines.push("", `Allowed spacing (px): ${resolved.spacing.filter((s) => s > 2).join(", ")}`);
@@ -41517,7 +42086,7 @@ function createBrandrailMcp(ctx) {
41517
42086
  "find_component",
41518
42087
  {
41519
42088
  title: "Find a design system component",
41520
- description: 'Finds the component to use for a UI need, e.g. "date picker", "primary action", "modal", "dropdown of trucks". Returns the import, rules and an example. Always use these instead of raw HTML elements or other UI libraries.',
42089
+ description: 'Finds the component to use for a UI need, e.g. "date picker", "primary action", "modal", "dropdown of countries". Returns the import, rules and an example. Always use these instead of raw HTML elements or other UI libraries.',
41521
42090
  inputSchema: {
41522
42091
  intent: external_exports.string().min(1).max(300).describe("What you need, in plain words"),
41523
42092
  limit: external_exports.number().int().min(1).max(10).default(3),
@@ -41604,7 +42173,7 @@ ${migrationMapMarkdown(resolved, fwOf(framework), libraries)}`)
41604
42173
  files: external_exports.array(external_exports.object({ filename: external_exports.string().min(1).max(200), code: external_exports.string().min(1).max(1e5) })).min(1).max(20).optional().describe("Every file you changed, checked together as one result. Preferred over code/filename."),
41605
42174
  code: external_exports.string().min(1).max(1e5).optional().describe("A single file or snippet (when not using files)"),
41606
42175
  filename: external_exports.string().max(200).default("snippet.tsx").describe("File name for code; the extension decides how it's parsed"),
41607
- project: external_exports.string().max(120).optional().describe("Repo or app name, e.g. fleet-portal (shown in the dashboard)")
42176
+ project: external_exports.string().max(120).optional().describe("Repo or app name, e.g. customer-portal (shown in the dashboard)")
41608
42177
  },
41609
42178
  annotations: { readOnlyHint: true, openWorldHint: false }
41610
42179
  },
@@ -41959,6 +42528,7 @@ Usage
41959
42528
  brandrail import <files\u2026> Load tokens into the config: DTCG, Tokens Studio, Figma variables JSON, or Figma's Export modes ZIP
41960
42529
  brandrail rules List the rules
41961
42530
  brandrail template [name] Start a page (list, detail, form, settings, dashboard, sign-in) from your components
42531
+ brandrail export <formats\u2026> Write the Brand Kit for other platforms: ios, compose, android-xml, react-native, flutter, css, tailwind, json
41962
42532
 
41963
42533
  Options for check
41964
42534
  --config <file> Config file (default: nearest brandrail.config.json)
@@ -41975,7 +42545,7 @@ Options for init
41975
42545
  --no-rules Don't write AGENTS.md (Codex), CLAUDE.md, Cursor, Copilot, Windsurf, Gemini rule files
41976
42546
  --mcp Also add the Brandrail MCP server to .mcp.json, .cursor/mcp.json, .vscode/mcp.json
41977
42547
  and print the Codex / Windsurf config
41978
- --framework <name> react | angular | vue | web-components | html (default: detected from package.json)
42548
+ --framework <name> react | angular | vue | svelte | solid | web-components | html (default: detected from package.json)
41979
42549
  --tokens-css <file> Where to write the CSS variables (default src/brandrail.tokens.css); --no-tokens-css to skip
41980
42550
  --force Overwrite an existing brandrail.config.json
41981
42551
 
@@ -42009,6 +42579,10 @@ Options for import
42009
42579
  --replace Replace the config's tokens instead of merging
42010
42580
  --primitives Keep raw palette ramps (gray.100\u2026900) that semantic tokens point at
42011
42581
 
42582
+ Options for export
42583
+ --out <folder> Where to write (default: here; with several formats, a folder per format)
42584
+ --from-api Use the published Brand Kit instead of brandrail.config.json
42585
+
42012
42586
  Options for template
42013
42587
  --framework <name> Override the detected framework
42014
42588
  --out <folder> Where to write the files (default: here)
@@ -42393,17 +42967,17 @@ async function cmdScan(cwd, positional, flags) {
42393
42967
  const url2 = positional[0];
42394
42968
  if (!url2) throw new Error("Usage: brandrail scan <url> [--pages 3] [--storage-state auth.json] [--upload]");
42395
42969
  const { config: config2, source } = await loadConfig(cwd, flags);
42396
- const num = (k, d, max2) => typeof flags[k] === "string" ? Math.min(Math.max(Number(flags[k]) || d, 0), max2) : d;
42970
+ const num2 = (k, d, max2) => typeof flags[k] === "string" ? Math.min(Math.max(Number(flags[k]) || d, 0), max2) : d;
42397
42971
  log2(`Scanning ${url2} \u2026`);
42398
42972
  const { report, pages } = await scanPages(
42399
42973
  cwd,
42400
42974
  url2,
42401
42975
  config2,
42402
42976
  {
42403
- pages: Math.max(1, num("pages", 1, 20)),
42977
+ pages: Math.max(1, num2("pages", 1, 20)),
42404
42978
  storageState: typeof flags["storage-state"] === "string" ? flags["storage-state"] : void 0,
42405
- waitMs: num("wait", 0, 3e4),
42406
- width: num("width", 1280, 3840) || 1280
42979
+ waitMs: num2("wait", 0, 3e4),
42980
+ width: num2("width", 1280, 3840) || 1280
42407
42981
  },
42408
42982
  log2
42409
42983
  );
@@ -42480,6 +43054,44 @@ function cmdImport(cwd, positional, flags) {
42480
43054
  log2("\nNext: npx brandrail init (to refresh AI rule files), then npx brandrail check");
42481
43055
  return 0;
42482
43056
  }
43057
+ async function cmdExport(cwd, positional, flags) {
43058
+ if (!positional.length) {
43059
+ process.stdout.write(`Formats:
43060
+ ${EXPORT_FORMATS.map((f) => ` ${f.id.padEnd(14)} ${f.label}: ${f.description}`).join("\n")}
43061
+
43062
+ Usage: brandrail export <format\u2026> [--out <folder>] [--from-api]
43063
+ `);
43064
+ return 0;
43065
+ }
43066
+ const bad = positional.filter((f) => !isExportFormat(f));
43067
+ if (bad.length) throw new Error(`Unknown format: ${bad.join(", ")}. Use one of: ${EXPORT_FORMATS.map((f) => f.id).join(", ")}.`);
43068
+ let config2;
43069
+ let org;
43070
+ if (flags["from-api"]) {
43071
+ const key = resolveKey();
43072
+ if (!key) throw new Error(`--from-api: ${NO_KEY}`);
43073
+ const local = findConfig(cwd);
43074
+ const res = await fetchBrandKit(key, local ? readConfig(local).brandKitVersion : void 0);
43075
+ config2 = res.config;
43076
+ org = res.org;
43077
+ } else {
43078
+ ({ config: config2 } = await loadConfig(cwd, flags));
43079
+ org = config2.org;
43080
+ }
43081
+ const base = resolve3(cwd, typeof flags.out === "string" ? flags.out : ".");
43082
+ for (const format of new Set(positional)) {
43083
+ const r = exportTokens(config2.tokens, format, { org, version: config2.tokenVersion });
43084
+ const dir = positional.length > 1 ? join7(base, format) : base;
43085
+ for (const f of r.files) {
43086
+ const p = join7(dir, f.path);
43087
+ mkdirSync4(dirname4(p), { recursive: true });
43088
+ writeFileSync4(p, f.content);
43089
+ }
43090
+ log2(`\u2714 ${EXPORT_FORMATS.find((x2) => x2.id === format).label}: ${r.files.map((f) => relative3(cwd, join7(dir, f.path))).join(", ")}`);
43091
+ for (const u of r.usage) log2(` \xB7 ${u}`);
43092
+ }
43093
+ return 0;
43094
+ }
42483
43095
  async function cmdTemplate(cwd, positional, flags) {
42484
43096
  if (!positional.length) {
42485
43097
  process.stdout.write(`Page templates, built from your registry's components:
@@ -42533,6 +43145,8 @@ async function main(argv = process.argv.slice(2)) {
42533
43145
  return cmdRules();
42534
43146
  case "template":
42535
43147
  return cmdTemplate(cwd, positional, flags);
43148
+ case "export":
43149
+ return cmdExport(cwd, positional, flags);
42536
43150
  case "connect":
42537
43151
  return cmdConnect(cwd, flags);
42538
43152
  case "mcp":