brandrail 0.9.0 → 0.10.0
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 +708 -94
- package/package.json +3 -3
package/dist/brandrail.mjs
CHANGED
|
@@ -1444,15 +1444,15 @@ var require_rules = __commonJS({
|
|
|
1444
1444
|
}
|
|
1445
1445
|
exports.isJSONType = isJSONType;
|
|
1446
1446
|
function getRules() {
|
|
1447
|
-
const
|
|
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: { ...
|
|
1455
|
-
rules: [{ rules: [] },
|
|
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
|
|
3193
|
+
const head2 = hextets.slice(0, bestStart).join(":");
|
|
3194
3194
|
const tail = hextets.slice(bestStart + bestLength).join(":");
|
|
3195
|
-
return
|
|
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
|
|
3387
|
-
if (isHexPair(
|
|
3388
|
-
const normalizedHex =
|
|
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
|
|
3409
|
-
if (isHexPair(
|
|
3410
|
-
const normalizedHex =
|
|
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
|
|
3451
|
-
if (isHexPair(
|
|
3452
|
-
output2 += "%" +
|
|
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
|
|
3489
|
-
if (isHexPair(
|
|
3490
|
-
output2 += "%" +
|
|
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
|
|
3536
|
-
if (isHexPair(
|
|
3537
|
-
const normalizedHex =
|
|
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
|
|
3576
|
-
if (isHexPair(
|
|
3577
|
-
output2 += "%" +
|
|
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(
|
|
7748
|
-
let h =
|
|
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
|
|
7772
|
-
const alpha = parts[3] !== void 0 ?
|
|
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) =>
|
|
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 = (
|
|
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
|
|
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 ? `${
|
|
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
|
|
7875
|
-
return rgb.a < 1 ? `${
|
|
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
|
|
8199
|
+
const groups2 = [];
|
|
8200
8200
|
for (const m of leafSets) {
|
|
8201
|
-
const g =
|
|
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
|
|
8208
|
+
else groups2.push([m]);
|
|
8209
8209
|
}
|
|
8210
8210
|
const byLeaf = /* @__PURE__ */ new Map();
|
|
8211
|
-
|
|
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 =
|
|
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
|
|
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
|
|
8545
|
-
colors.push({ path: l.path, cssVar:
|
|
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:
|
|
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("
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
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 "
|
|
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.
|
|
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
|
|
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(
|
|
10333
|
-
const borderSide =
|
|
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:
|
|
10381
|
+
fontSize: px4(cs.fontSize),
|
|
10344
10382
|
fontWeight: parseInt(cs.fontWeight, 10) || 400,
|
|
10345
|
-
radius: Math.max(
|
|
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: [
|
|
10348
|
-
gap: cs.display.includes("flex") || cs.display.includes("grid") ? [
|
|
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
|
|
10382
|
-
return rgb.a < 1 ? `${
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = [
|
|
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(
|
|
10623
|
-
const n =
|
|
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, "'");
|
|
|
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
|
-
|
|
11526
|
-
|
|
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: () =>
|
|
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(
|
|
13315
|
-
const cleanHex =
|
|
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: () =>
|
|
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
|
|
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
|
|
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 \`${
|
|
41508
|
-
lines.push(`| ${l.path} | \`${
|
|
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(", ")}`);
|
|
@@ -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
|
|
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,
|
|
42977
|
+
pages: Math.max(1, num2("pages", 1, 20)),
|
|
42404
42978
|
storageState: typeof flags["storage-state"] === "string" ? flags["storage-state"] : void 0,
|
|
42405
|
-
waitMs:
|
|
42406
|
-
width:
|
|
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":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brandrail",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
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",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"playwright-core": "1.63.0",
|
|
44
44
|
"typescript": "5.9.3",
|
|
45
45
|
"zod": "4.6.7",
|
|
46
|
-
"@brandrail/
|
|
47
|
-
"@brandrail/
|
|
46
|
+
"@brandrail/engine": "0.1.0",
|
|
47
|
+
"@brandrail/mcp": "0.1.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/brandrail.mjs --external:playwright --external:playwright-core --banner:js='#!/usr/bin/env node' && chmod +x dist/brandrail.mjs",
|