brandrail 0.3.1 → 0.4.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.
Files changed (2) hide show
  1. package/dist/brandrail.mjs +1177 -130
  2. package/package.json +3 -2
@@ -1,8 +1,491 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
5
- import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
4
+ import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync } from "node:fs";
5
+ import { basename, dirname as dirname2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
6
+
7
+ // ../../node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
8
+ import { createRequire } from "module";
9
+ var require2 = createRequire("/");
10
+ var _a;
11
+ var Worker;
12
+ var isMarkedAsUntransferable;
13
+ try {
14
+ _a = require2("worker_threads"), Worker = _a.Worker, isMarkedAsUntransferable = _a.isMarkedAsUntransferable;
15
+ } catch (e) {
16
+ }
17
+ var u8 = Uint8Array;
18
+ var u16 = Uint16Array;
19
+ var i32 = Int32Array;
20
+ var fleb = new u8([
21
+ 0,
22
+ 0,
23
+ 0,
24
+ 0,
25
+ 0,
26
+ 0,
27
+ 0,
28
+ 0,
29
+ 1,
30
+ 1,
31
+ 1,
32
+ 1,
33
+ 2,
34
+ 2,
35
+ 2,
36
+ 2,
37
+ 3,
38
+ 3,
39
+ 3,
40
+ 3,
41
+ 4,
42
+ 4,
43
+ 4,
44
+ 4,
45
+ 5,
46
+ 5,
47
+ 5,
48
+ 5,
49
+ 0,
50
+ /* unused */
51
+ 0,
52
+ 0,
53
+ /* impossible */
54
+ 0
55
+ ]);
56
+ var fdeb = new u8([
57
+ 0,
58
+ 0,
59
+ 0,
60
+ 0,
61
+ 1,
62
+ 1,
63
+ 2,
64
+ 2,
65
+ 3,
66
+ 3,
67
+ 4,
68
+ 4,
69
+ 5,
70
+ 5,
71
+ 6,
72
+ 6,
73
+ 7,
74
+ 7,
75
+ 8,
76
+ 8,
77
+ 9,
78
+ 9,
79
+ 10,
80
+ 10,
81
+ 11,
82
+ 11,
83
+ 12,
84
+ 12,
85
+ 13,
86
+ 13,
87
+ /* unused */
88
+ 0,
89
+ 0
90
+ ]);
91
+ var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
92
+ var freb = function(eb, start) {
93
+ var b = new u16(31);
94
+ for (var i2 = 0; i2 < 31; ++i2) {
95
+ b[i2] = start += 1 << eb[i2 - 1];
96
+ }
97
+ var r = new i32(b[30]);
98
+ for (var i2 = 1; i2 < 30; ++i2) {
99
+ for (var j = b[i2]; j < b[i2 + 1]; ++j) {
100
+ r[j] = j - b[i2] << 5 | i2;
101
+ }
102
+ }
103
+ return { b, r };
104
+ };
105
+ var _a = freb(fleb, 2);
106
+ var fl = _a.b;
107
+ var revfl = _a.r;
108
+ fl[28] = 258, revfl[258] = 28;
109
+ var _b = freb(fdeb, 0);
110
+ var fd = _b.b;
111
+ var revfd = _b.r;
112
+ var rev = new u16(32768);
113
+ for (i = 0; i < 32768; ++i) {
114
+ x = (i & 43690) >> 1 | (i & 21845) << 1;
115
+ x = (x & 52428) >> 2 | (x & 13107) << 2;
116
+ x = (x & 61680) >> 4 | (x & 3855) << 4;
117
+ rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
118
+ }
119
+ var x;
120
+ var i;
121
+ var hMap = (function(cd, mb, r) {
122
+ var s = cd.length;
123
+ var i2 = 0;
124
+ var l = new u16(mb);
125
+ for (; i2 < s; ++i2) {
126
+ if (cd[i2])
127
+ ++l[cd[i2] - 1];
128
+ }
129
+ var le = new u16(mb);
130
+ for (i2 = 1; i2 < mb; ++i2) {
131
+ le[i2] = le[i2 - 1] + l[i2 - 1] << 1;
132
+ }
133
+ var co;
134
+ if (r) {
135
+ co = new u16(1 << mb);
136
+ var rvb = 15 - mb;
137
+ for (i2 = 0; i2 < s; ++i2) {
138
+ if (cd[i2]) {
139
+ var sv = i2 << 4 | cd[i2];
140
+ var r_1 = mb - cd[i2];
141
+ var v = le[cd[i2] - 1]++ << r_1;
142
+ for (var m = v | (1 << r_1) - 1; v <= m; ++v) {
143
+ co[rev[v] >> rvb] = sv;
144
+ }
145
+ }
146
+ }
147
+ } else {
148
+ co = new u16(s);
149
+ for (i2 = 0; i2 < s; ++i2) {
150
+ if (cd[i2]) {
151
+ co[i2] = rev[le[cd[i2] - 1]++] >> 15 - cd[i2];
152
+ }
153
+ }
154
+ }
155
+ return co;
156
+ });
157
+ var flt = new u8(288);
158
+ for (i = 0; i < 144; ++i)
159
+ flt[i] = 8;
160
+ var i;
161
+ for (i = 144; i < 256; ++i)
162
+ flt[i] = 9;
163
+ var i;
164
+ for (i = 256; i < 280; ++i)
165
+ flt[i] = 7;
166
+ var i;
167
+ for (i = 280; i < 288; ++i)
168
+ flt[i] = 8;
169
+ var i;
170
+ var fdt = new u8(32);
171
+ for (i = 0; i < 32; ++i)
172
+ fdt[i] = 5;
173
+ var i;
174
+ var flrm = /* @__PURE__ */ hMap(flt, 9, 1);
175
+ var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
176
+ var max = function(a) {
177
+ var m = a[0];
178
+ for (var i2 = 1; i2 < a.length; ++i2) {
179
+ if (a[i2] > m)
180
+ m = a[i2];
181
+ }
182
+ return m;
183
+ };
184
+ var bits = function(d, p, m) {
185
+ var o = p / 8 | 0;
186
+ return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
187
+ };
188
+ var bits16 = function(d, p) {
189
+ var o = p / 8 | 0;
190
+ return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
191
+ };
192
+ var shft = function(p) {
193
+ return (p + 7) / 8 | 0;
194
+ };
195
+ var slc = function(v, s, e) {
196
+ if (s == null || s < 0)
197
+ s = 0;
198
+ if (e == null || e > v.length)
199
+ e = v.length;
200
+ return new u8(v.subarray(s, e));
201
+ };
202
+ var ec = [
203
+ "unexpected EOF",
204
+ "invalid block type",
205
+ "invalid length/literal",
206
+ "invalid distance",
207
+ "stream finished",
208
+ "no stream handler",
209
+ ,
210
+ // determined by compression function
211
+ "no callback",
212
+ "invalid UTF-8 data",
213
+ "extra field too long",
214
+ "date not in range 1980-2099",
215
+ "filename too long",
216
+ "stream finishing",
217
+ "invalid zip data"
218
+ // determined by unknown compression method
219
+ ];
220
+ var err = function(ind, msg, nt) {
221
+ var e = new Error(msg || ec[ind]);
222
+ e.code = ind;
223
+ if (Error.captureStackTrace)
224
+ Error.captureStackTrace(e, err);
225
+ if (!nt)
226
+ throw e;
227
+ return e;
228
+ };
229
+ var inflt = function(dat, st, buf, dict) {
230
+ var sl = dat.length, dl = dict ? dict.length : 0;
231
+ if (!sl || st.f && !st.l)
232
+ return buf || new u8(0);
233
+ var noBuf = !buf;
234
+ var resize = noBuf || st.i != 2;
235
+ var noSt = st.i;
236
+ if (noBuf)
237
+ buf = new u8(sl * 3);
238
+ var cbuf = function(l2) {
239
+ var bl = buf.length;
240
+ if (l2 > bl) {
241
+ var nbuf = new u8(Math.max(bl * 2, l2));
242
+ nbuf.set(buf);
243
+ buf = nbuf;
244
+ }
245
+ };
246
+ var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
247
+ var tbts = sl * 8;
248
+ do {
249
+ if (!lm) {
250
+ final = bits(dat, pos, 1);
251
+ var type = bits(dat, pos + 1, 3);
252
+ pos += 3;
253
+ if (!type) {
254
+ var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
255
+ if (t > sl) {
256
+ if (noSt)
257
+ err(0);
258
+ break;
259
+ }
260
+ if (resize)
261
+ cbuf(bt + l);
262
+ buf.set(dat.subarray(s, t), bt);
263
+ st.b = bt += l, st.p = pos = t * 8, st.f = final;
264
+ continue;
265
+ } else if (type == 1)
266
+ lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
267
+ else if (type == 2) {
268
+ var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
269
+ var tl = hLit + bits(dat, pos + 5, 31) + 1;
270
+ pos += 14;
271
+ var ldt = new u8(tl);
272
+ var clt = new u8(19);
273
+ for (var i2 = 0; i2 < hcLen; ++i2) {
274
+ clt[clim[i2]] = bits(dat, pos + i2 * 3, 7);
275
+ }
276
+ pos += hcLen * 3;
277
+ var clb = max(clt), clbmsk = (1 << clb) - 1;
278
+ var clm = hMap(clt, clb, 1);
279
+ for (var i2 = 0; i2 < tl; ) {
280
+ var r = clm[bits(dat, pos, clbmsk)];
281
+ pos += r & 15;
282
+ var s = r >> 4;
283
+ if (s < 16) {
284
+ ldt[i2++] = s;
285
+ } else {
286
+ var c = 0, n = 0;
287
+ if (s == 16)
288
+ n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i2 - 1];
289
+ else if (s == 17)
290
+ n = 3 + bits(dat, pos, 7), pos += 3;
291
+ else if (s == 18)
292
+ n = 11 + bits(dat, pos, 127), pos += 7;
293
+ while (n--)
294
+ ldt[i2++] = c;
295
+ }
296
+ }
297
+ var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
298
+ lbt = max(lt);
299
+ dbt = max(dt);
300
+ lm = hMap(lt, lbt, 1);
301
+ dm = hMap(dt, dbt, 1);
302
+ } else
303
+ err(1);
304
+ if (pos > tbts) {
305
+ if (noSt)
306
+ err(0);
307
+ break;
308
+ }
309
+ }
310
+ if (resize)
311
+ cbuf(bt + 131072);
312
+ var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
313
+ var lpos = pos;
314
+ for (; ; lpos = pos) {
315
+ var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
316
+ pos += c & 15;
317
+ if (pos > tbts) {
318
+ if (noSt)
319
+ err(0);
320
+ break;
321
+ }
322
+ if (!c)
323
+ err(2);
324
+ if (sym < 256)
325
+ buf[bt++] = sym;
326
+ else if (sym == 256) {
327
+ lpos = pos, lm = null;
328
+ break;
329
+ } else {
330
+ var add = sym - 254;
331
+ if (sym > 264) {
332
+ var i2 = sym - 257, b = fleb[i2];
333
+ add = bits(dat, pos, (1 << b) - 1) + fl[i2];
334
+ pos += b;
335
+ }
336
+ var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
337
+ if (!d)
338
+ err(3);
339
+ pos += d & 15;
340
+ var dt = fd[dsym];
341
+ if (dsym > 3) {
342
+ var b = fdeb[dsym];
343
+ dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
344
+ }
345
+ if (pos > tbts) {
346
+ if (noSt)
347
+ err(0);
348
+ break;
349
+ }
350
+ if (resize)
351
+ cbuf(bt + 131072);
352
+ var end = bt + add;
353
+ if (bt < dt) {
354
+ var shift = dl - dt, dend = Math.min(dt, end);
355
+ if (shift + bt < 0)
356
+ err(3);
357
+ for (; bt < dend; ++bt)
358
+ buf[bt] = dict[shift + bt];
359
+ }
360
+ for (; bt < end; ++bt)
361
+ buf[bt] = buf[bt - dt];
362
+ }
363
+ }
364
+ st.l = lm, st.p = lpos, st.b = bt, st.f = final;
365
+ if (lm)
366
+ final = 1, st.m = lbt, st.d = dm, st.n = dbt;
367
+ } while (!final);
368
+ return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
369
+ };
370
+ var et = /* @__PURE__ */ new u8(0);
371
+ var b2 = function(d, b) {
372
+ return d[b] | d[b + 1] << 8;
373
+ };
374
+ var b4 = function(d, b) {
375
+ return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0;
376
+ };
377
+ var b8 = function(d, b) {
378
+ return b4(d, b) + b4(d, b + 4) * 4294967296;
379
+ };
380
+ function inflateSync(data, opts) {
381
+ return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary);
382
+ }
383
+ var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
384
+ var tds = 0;
385
+ try {
386
+ td.decode(et, { stream: true });
387
+ tds = 1;
388
+ } catch (e) {
389
+ }
390
+ var dutf8 = function(d) {
391
+ for (var r = "", i2 = 0; ; ) {
392
+ var c = d[i2++];
393
+ var eb = (c > 127) + (c > 223) + (c > 239);
394
+ if (i2 + eb > d.length)
395
+ return { s: r, r: slc(d, i2 - 1) };
396
+ if (!eb)
397
+ r += String.fromCharCode(c);
398
+ else if (eb == 3) {
399
+ c = ((c & 15) << 18 | (d[i2++] & 63) << 12 | (d[i2++] & 63) << 6 | d[i2++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023);
400
+ } else if (eb & 1)
401
+ r += String.fromCharCode((c & 31) << 6 | d[i2++] & 63);
402
+ else
403
+ r += String.fromCharCode((c & 15) << 12 | (d[i2++] & 63) << 6 | d[i2++] & 63);
404
+ }
405
+ };
406
+ function strFromU8(dat, latin1) {
407
+ if (latin1) {
408
+ var r = "";
409
+ for (var i2 = 0; i2 < dat.length; i2 += 16384)
410
+ r += String.fromCharCode.apply(null, dat.subarray(i2, i2 + 16384));
411
+ return r;
412
+ } else if (td) {
413
+ return td.decode(dat);
414
+ } else {
415
+ var _a2 = dutf8(dat), s = _a2.s, r = _a2.r;
416
+ if (r.length)
417
+ err(8);
418
+ return s;
419
+ }
420
+ }
421
+ var slzh = function(d, b) {
422
+ return b + 30 + b2(d, b + 26) + b2(d, b + 28);
423
+ };
424
+ var zh = function(d, b, z) {
425
+ var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
426
+ var _a2 = z64hs(d, es, efl, z, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
427
+ return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
428
+ };
429
+ var z64hs = function(d, b, l, z, sc, su, off) {
430
+ var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
431
+ var nf = nsc + nsu + noff;
432
+ if (z && nf) {
433
+ for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
434
+ if (b2(d, b) == 1) {
435
+ return [
436
+ nsc ? b8(d, b + 4 + 8 * nsu) : sc,
437
+ nsu ? b8(d, b + 4) : su,
438
+ noff ? b8(d, b + 4 + 8 * (nsu + nsc)) : off,
439
+ 1
440
+ ];
441
+ }
442
+ }
443
+ if (z < 2)
444
+ err(13);
445
+ }
446
+ return [sc, su, off, 0];
447
+ };
448
+ function unzipSync(data, opts) {
449
+ var files = {};
450
+ var e = data.length - 22;
451
+ for (; b4(data, e) != 101010256; --e) {
452
+ if (!e || data.length - e > 65558)
453
+ err(13);
454
+ }
455
+ ;
456
+ var c = b2(data, e + 8);
457
+ if (!c)
458
+ return {};
459
+ var o = b4(data, e + 16);
460
+ var z = b4(data, e - 20) == 117853008;
461
+ if (z) {
462
+ var ze = b4(data, e - 12);
463
+ z = b4(data, ze) == 101075792;
464
+ if (z) {
465
+ c = b4(data, ze + 32);
466
+ o = b4(data, ze + 48);
467
+ }
468
+ }
469
+ var fltr = opts && opts.filter;
470
+ for (var i2 = 0; i2 < c; ++i2) {
471
+ var _a2 = zh(data, o, z), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
472
+ o = no;
473
+ if (!fltr || fltr({
474
+ name: fn,
475
+ size: sc,
476
+ originalSize: su,
477
+ compression: c_2
478
+ })) {
479
+ if (!c_2)
480
+ files[fn] = slc(data, b, b + sc);
481
+ else if (c_2 == 8)
482
+ files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
483
+ else
484
+ err(14, "unknown compression type " + c_2);
485
+ }
486
+ }
487
+ return files;
488
+ }
6
489
 
7
490
  // ../engine/src/color.ts
8
491
  var clamp = (n, lo = 0, hi = 255) => Math.min(hi, Math.max(lo, n));
@@ -11,7 +494,7 @@ function parseHex(hex) {
11
494
  if (!/^[0-9a-f]+$/.test(h)) return null;
12
495
  if (h.length === 3 || h.length === 4) h = h.split("").map((c) => c + c).join("");
13
496
  if (h.length !== 6 && h.length !== 8) return null;
14
- const n = (i) => parseInt(h.slice(i, i + 2), 16);
497
+ const n = (i2) => parseInt(h.slice(i2, i2 + 2), 16);
15
498
  return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) / 255 : 1 };
16
499
  }
17
500
  function hslToRgb(h, s, l) {
@@ -30,12 +513,12 @@ function parseColor(input) {
30
513
  if (!m) return null;
31
514
  const parts = m[2].split(/[\s,/]+/).filter(Boolean);
32
515
  if (parts.length < 3) return null;
33
- const num = (p, max) => p.endsWith("%") ? parseFloat(p) / 100 * max : parseFloat(p);
516
+ const num = (p, max2) => p.endsWith("%") ? parseFloat(p) / 100 * max2 : parseFloat(p);
34
517
  const alpha = parts[3] !== void 0 ? num(parts[3], 1) : 1;
35
518
  if (m[1].startsWith("rgb")) {
36
- const [r2, g2, b2] = parts.slice(0, 3).map((p) => num(p, 255));
37
- if ([r2, g2, b2].some((v) => Number.isNaN(v))) return null;
38
- return { r: clamp(r2), g: clamp(g2), b: clamp(b2), a: alpha };
519
+ const [r2, g2, b3] = parts.slice(0, 3).map((p) => num(p, 255));
520
+ if ([r2, g2, b3].some((v) => Number.isNaN(v))) return null;
521
+ return { r: clamp(r2), g: clamp(g2), b: clamp(b3), a: alpha };
39
522
  }
40
523
  const [h, sat, l] = [parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2])];
41
524
  if ([h, sat, l].some((v) => Number.isNaN(v))) return null;
@@ -51,11 +534,11 @@ function rgbToLab({ r, g, b }) {
51
534
  return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
52
535
  };
53
536
  const [R, G, B] = [lin(r), lin(g), lin(b)];
54
- const x = (R * 0.4124 + G * 0.3576 + B * 0.1805) / 0.95047;
537
+ const x2 = (R * 0.4124 + G * 0.3576 + B * 0.1805) / 0.95047;
55
538
  const y = R * 0.2126 + G * 0.7152 + B * 0.0722;
56
539
  const z = (R * 0.0193 + G * 0.1192 + B * 0.9505) / 1.08883;
57
540
  const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
58
- return { l: 116 * f(y) - 16, a: 500 * (f(x) - f(y)), b: 200 * (f(y) - f(z)) };
541
+ return { l: 116 * f(y) - 16, a: 500 * (f(x2) - f(y)), b: 200 * (f(y) - f(z)) };
59
542
  }
60
543
  function deltaE(a, b) {
61
544
  return Math.sqrt((a.l - b.l) ** 2 + (a.a - b.a) ** 2 + (a.b - b.b) ** 2);
@@ -68,7 +551,7 @@ function contrastRatio(a, b) {
68
551
  });
69
552
  return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
70
553
  };
71
- const [l1, l2] = [lum(a), lum(b)].sort((x, y) => y - x);
554
+ const [l1, l2] = [lum(a), lum(b)].sort((x2, y) => y - x2);
72
555
  return (l1 + 0.05) / (l2 + 0.05);
73
556
  }
74
557
 
@@ -165,7 +648,7 @@ function normalizeFontFamily(value) {
165
648
  function isGenericFont(family) {
166
649
  return GENERIC_FONTS.has(family) || family.startsWith("var(");
167
650
  }
168
- var uniqSorted = (xs) => [...new Set(xs.map((x) => Math.round(x * 100) / 100))].sort((a, b) => a - b);
651
+ var uniqSorted = (xs) => [...new Set(xs.map((x2) => Math.round(x2 * 100) / 100))].sort((a, b) => a - b);
169
652
  function resolveConfig(config) {
170
653
  const all = leaves(config.tokens);
171
654
  const colors = [];
@@ -218,13 +701,100 @@ function resolveConfig(config) {
218
701
  rules: config.rules ?? {},
219
702
  ignore: [...DEFAULT_IGNORE, ...config.ignore ?? []],
220
703
  forbiddenImports: config.forbiddenImports ?? DEFAULT_FORBIDDEN_IMPORTS,
221
- colorTolerance: config.colorTolerance ?? 2.5
704
+ colorTolerance: config.colorTolerance ?? 2.5,
705
+ framework: config.framework
222
706
  };
223
707
  }
224
708
 
709
+ // ../engine/src/frameworks.ts
710
+ var FRAMEWORKS = ["react", "angular", "vue", "web-components", "html"];
711
+ var FRAMEWORK_LABEL = {
712
+ react: "React",
713
+ angular: "Angular",
714
+ vue: "Vue",
715
+ "web-components": "Web Components",
716
+ html: "Plain HTML"
717
+ };
718
+ var LANG = { react: "tsx", angular: "html", vue: "vue", "web-components": "html", html: "html" };
719
+ var isFramework = (x2) => typeof x2 === "string" && FRAMEWORKS.includes(x2);
720
+ var kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/\./g, "-").toLowerCase();
721
+ var prefixOf = (tag) => tag.includes("-") ? `${tag.split("-")[0]}-` : tag;
722
+ function webComponentSetup(target, u, tag) {
723
+ const load = u.importPath ? `Load it once with \`import "${u.importPath}";\` at the app entry.` : "";
724
+ switch (target) {
725
+ case "angular":
726
+ return `Web component: add \`CUSTOM_ELEMENTS_SCHEMA\` to the component's \`schemas\`. ${load}`.trim();
727
+ case "vue":
728
+ return `Web component: set \`compilerOptions.isCustomElement = (tag) => tag.startsWith("${prefixOf(tag)}")\` in the Vue config. ${load}`.trim();
729
+ case "html":
730
+ return u.importPath ? `Web component: load \`${u.importPath}\` once (a bundler import or \`<script type="module">\`).` : void 0;
731
+ default:
732
+ return u.setup ?? (load || void 0);
733
+ }
734
+ }
735
+ function resolveUsage(c, framework) {
736
+ if (framework === "react") {
737
+ return {
738
+ framework,
739
+ importLine: `import { ${c.name} } from "${c.importPath}";`,
740
+ tag: c.name,
741
+ example: c.example,
742
+ lang: LANG.react,
743
+ props: c.props
744
+ };
745
+ }
746
+ const own = c.frameworks?.[framework];
747
+ if (own) return fromUsage(c, framework, framework, own);
748
+ const wc = c.frameworks?.["web-components"];
749
+ if (wc) {
750
+ const r = fromUsage(c, framework, "web-components", wc);
751
+ return { ...r, via: "web-components", setup: webComponentSetup(framework, wc, r.tag), importLine: framework === "html" ? void 0 : r.importLine };
752
+ }
753
+ return null;
754
+ }
755
+ function fromUsage(c, framework, kind, u) {
756
+ const path = u.importPath?.trim();
757
+ let tag = u.tag?.trim();
758
+ let importLine;
759
+ let setup = u.setup?.trim() || void 0;
760
+ switch (kind) {
761
+ case "angular":
762
+ tag ||= kebab(c.name);
763
+ if (path && u.importName) {
764
+ importLine = `import { ${u.importName} } from "${path}";`;
765
+ setup ??= `Add \`${u.importName}\` to the standalone component's \`imports\` (or the NgModule's).`;
766
+ }
767
+ break;
768
+ case "vue":
769
+ tag ||= u.importName || c.name;
770
+ if (path) importLine = `import { ${u.importName || tag} } from "${path}";`;
771
+ break;
772
+ case "web-components":
773
+ tag ||= kebab(c.name);
774
+ if (path) importLine = `import "${path}";`;
775
+ break;
776
+ case "html":
777
+ tag ||= c.replaces?.[0]?.replace(/\[.*$/, "") || "div";
778
+ if (path && !setup) setup = /\.css(\?|$)/.test(path) ? `Include \`<link rel="stylesheet" href="${path}">\` once.` : /\.m?js(\?|$)/.test(path) ? `Include \`<script type="module" src="${path}"></script>\` once.` : void 0;
779
+ break;
780
+ }
781
+ return { framework, importLine, tag, setup, example: u.example?.trim() || void 0, lang: LANG[kind], props: u.props?.length ? u.props : void 0 };
782
+ }
783
+ var markup = (u) => `<${u.tag}>`;
784
+ function detectFramework(pkg) {
785
+ if (!pkg) return "html";
786
+ const deps = { ...pkg.peerDependencies, ...pkg.devDependencies, ...pkg.dependencies };
787
+ const has = (name) => name in deps;
788
+ if (has("@angular/core")) return "angular";
789
+ if (has("vue") || has("nuxt")) return "vue";
790
+ if (has("react") || has("next") || has("preact")) return "react";
791
+ if (has("svelte") || has("lit") || has("@stencil/core") || has("@sveltejs/kit")) return "web-components";
792
+ return "html";
793
+ }
794
+
225
795
  // ../engine/src/rules.ts
226
- var MARKUP = ["jsx", "vue", "svelte", "html"];
227
- var ALL = ["jsx", "vue", "svelte", "html", "css"];
796
+ var MARKUP = ["jsx", "vue", "svelte", "html", "template"];
797
+ var ALL = ["jsx", "vue", "svelte", "html", "template", "css"];
228
798
  function* matches(re, text) {
229
799
  const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g");
230
800
  let m;
@@ -233,8 +803,8 @@ function* matches(re, text) {
233
803
  if (m[0].length === 0) g.lastIndex++;
234
804
  }
235
805
  }
236
- var lineBefore = (code, offset, max = 80) => {
237
- const start = Math.max(code.lastIndexOf("\n", offset - 1) + 1, offset - max);
806
+ var lineBefore = (code, offset, max2 = 80) => {
807
+ const start = Math.max(code.lastIndexOf("\n", offset - 1) + 1, offset - max2);
238
808
  return code.slice(start, offset);
239
809
  };
240
810
  var lineOf = (code, offset) => {
@@ -244,9 +814,9 @@ var lineOf = (code, offset) => {
244
814
  };
245
815
  var fmt = (n) => `${Math.round(n * 100) / 100}px`;
246
816
  function neighbours(scale, v) {
247
- const lower = [...scale].reverse().find((x) => x <= v);
248
- const upper = scale.find((x) => x >= v);
249
- const opts = [...new Set([lower, upper].filter((x) => x !== void 0))];
817
+ const lower = [...scale].reverse().find((x2) => x2 <= v);
818
+ const upper = scale.find((x2) => x2 >= v);
819
+ const opts = [...new Set([lower, upper].filter((x2) => x2 !== void 0))];
250
820
  return opts.map(fmt).join(" or ");
251
821
  }
252
822
  function pxValues(value) {
@@ -257,6 +827,38 @@ function pxValues(value) {
257
827
  }
258
828
  return out;
259
829
  }
830
+ var kebab2 = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
831
+ function styleBindings(code) {
832
+ const out = [];
833
+ const withUnit = (v, unit) => unit && /^-?\d*\.?\d+$/.test(v.trim()) ? `${v.trim()}${unit === "%" ? "%" : unit}` : v;
834
+ for (const m of matches(/(['"])([a-zA-Z-]+)(?:\.(px|rem|em|%))?\1\s*:\s*(?:(['"`])([^'"`\n]*)\4|(-?\d*\.?\d+)(?![\w.]))/g, code)) {
835
+ const value = m[5] ?? m[6];
836
+ const offset = m.index + m[0].length - value.length - (m[5] !== void 0 ? 1 : 0);
837
+ out.push({ prop: kebab2(m[2]), value: withUnit(value, m[3]), offset });
838
+ }
839
+ for (const m of matches(/(?<![\w.'"-])((?:padding|margin)(?:Top|Right|Bottom|Left|Inline|Block)(?:Start|End)?|rowGap|columnGap|border(?:Top|Bottom)?(?:Left|Right)?Radius)\s*:\s*(['"`])([^'"`\n]*\d(?:px|rem)[^'"`\n]*)\2/g, code)) {
840
+ out.push({ prop: kebab2(m[1]), value: m[3], offset: m.index + m[0].length - m[3].length - 1 });
841
+ }
842
+ for (const m of matches(/\[style\.([a-zA-Z-]+)(?:\.(px|rem|em|%))?\]\s*=\s*(["'])([\s\S]*?)\3/g, code)) {
843
+ const inner = m[4].trim();
844
+ const lit = /^(['"`])(.*)\1$/.exec(inner)?.[2] ?? (/^-?\d*\.?\d+$/.test(inner) ? inner : null);
845
+ if (lit === null) continue;
846
+ out.push({ prop: kebab2(m[1]), value: withUnit(lit, m[2]), offset: m.index + m[0].length - 1 - m[4].length + m[4].indexOf(inner) });
847
+ }
848
+ return out;
849
+ }
850
+ function declValue(v, kind) {
851
+ if (kind === "css") return v;
852
+ const q = v[0];
853
+ if (q === '"' || q === "'" || q === "`") {
854
+ const end = v.indexOf(q, 1);
855
+ return end === -1 ? v : v.slice(0, end + 1);
856
+ }
857
+ const cut = v.search(/["'`>]/);
858
+ return cut === -1 ? v : v.slice(0, cut);
859
+ }
860
+ var SPACING_PROP = /^(?:(?:padding|margin)(?:-(?:top|right|bottom|left|inline|block)(?:-(?:start|end))?)?|gap|row-gap|column-gap)$/;
861
+ var RADIUS_PROP = /^border(?:-(?:top|bottom|start|end)-(?:left|right|start|end))?-radius$/;
260
862
  var noHardcodedColor = {
261
863
  id: "no-hardcoded-color",
262
864
  title: "Hard-coded colour",
@@ -342,7 +944,7 @@ var spacingScale = {
342
944
  const ok = (n) => scale.some((s) => Math.abs(s - n) < 0.01);
343
945
  for (const m of matches(/(?<![\w-])(padding|margin|gap|row-gap|column-gap)(?:-(?:top|right|bottom|left|inline|block)(?:-(?:start|end))?)?\s*:\s*([^;{}\n]+)/g, code)) {
344
946
  const valueStart = m.index + m[0].length - m[2].length;
345
- for (const v of pxValues(m[2])) {
947
+ for (const v of pxValues(declValue(m[2], kind))) {
346
948
  if (!ok(v.n)) out.push({ offset: valueStart + v.index, message: `${m[1]} ${fmt(v.n)} is off the spacing scale`, suggestion: `Use ${neighbours(scale, v.n)}` });
347
949
  }
348
950
  }
@@ -355,6 +957,12 @@ var spacingScale = {
355
957
  const n = Math.abs(parseFloat(m[2])) * (m[3] === "rem" ? 16 : 1);
356
958
  if (!ok(n)) out.push({ offset: m.index, message: `${m[0]} is off the spacing scale`, suggestion: `Use ${neighbours(scale, n)}` });
357
959
  }
960
+ for (const b of styleBindings(code)) {
961
+ if (!SPACING_PROP.test(b.prop)) continue;
962
+ for (const v of pxValues(b.value)) {
963
+ if (!ok(v.n)) out.push({ offset: b.offset + Math.min(v.index, b.value.length), message: `${b.prop} ${fmt(v.n)} is off the spacing scale`, suggestion: `Use ${neighbours(scale, v.n)}` });
964
+ }
965
+ }
358
966
  }
359
967
  return out;
360
968
  }
@@ -370,7 +978,7 @@ var radiusScale = {
370
978
  const ok = (n) => n >= 999 || config.radii.some((r) => Math.abs(r - n) < 0.01);
371
979
  for (const m of matches(/(?<![\w-])border(?:-(?:top|bottom|start|end)-(?:left|right|start|end))?-radius\s*:\s*([^;{}\n]+)/g, code)) {
372
980
  const valueStart = m.index + m[0].length - m[1].length;
373
- for (const v of pxValues(m[1])) {
981
+ for (const v of pxValues(declValue(m[1], kind))) {
374
982
  if (!ok(v.n)) out.push({ offset: valueStart + v.index, message: `border-radius ${fmt(v.n)} is not a radius token`, suggestion: `Use ${neighbours(config.radii.filter((r) => r < 999), v.n)}` });
375
983
  }
376
984
  }
@@ -383,6 +991,12 @@ var radiusScale = {
383
991
  const n = parseFloat(m[1]);
384
992
  if (!ok(n)) out.push({ offset: m.index, message: `${m[0]} is not a radius token`, suggestion: `Use ${neighbours(config.radii.filter((r) => r < 999), n)}` });
385
993
  }
994
+ for (const b of styleBindings(code)) {
995
+ if (!RADIUS_PROP.test(b.prop)) continue;
996
+ for (const v of pxValues(b.value)) {
997
+ if (!ok(v.n)) out.push({ offset: b.offset, message: `${b.prop} ${fmt(v.n)} is not a radius token`, suggestion: `Use ${neighbours(config.radii.filter((r) => r < 999), v.n)}` });
998
+ }
999
+ }
386
1000
  }
387
1001
  return out;
388
1002
  }
@@ -406,10 +1020,11 @@ var fontFamily = {
406
1020
  suggestion: `Use var(--font-family-sans) (${allowed[0]})`
407
1021
  });
408
1022
  };
409
- for (const m of matches(/(?<![\w-])font-family\s*:\s*([^;{}\n]+)/g, code)) test(m[1], m.index);
1023
+ for (const m of matches(/(?<![\w-])font-family\s*:\s*([^;{}\n]+)/g, code)) test(declValue(m[1], kind), m.index);
410
1024
  if (kind !== "css") {
411
1025
  for (const m of matches(/(?<![\w-])fontFamily\s*:\s*(['"`])([^'"`]+)\1/g, code)) test(m[2], m.index);
412
1026
  for (const m of matches(/(?<![\w-])font-\[([^\]]+)\]/g, code)) test(m[1].replace(/_/g, " "), m.index);
1027
+ for (const b of styleBindings(code)) if (b.prop === "font-family") test(b.value, b.offset);
413
1028
  }
414
1029
  return out;
415
1030
  }
@@ -421,7 +1036,7 @@ var preferOfficialComponent = {
421
1036
  description: "Use the design system component instead of a raw HTML element.",
422
1037
  defaultSeverity: "error",
423
1038
  kinds: MARKUP,
424
- check({ code, config }) {
1039
+ check({ code, config, framework }) {
425
1040
  if (config.replaces.size === 0) return [];
426
1041
  const out = [];
427
1042
  const baseTags = [...new Set([...config.replaces.keys()].map((k) => k.replace(/\[.*$/, "").replace(/[^a-z0-9-]/g, "")))].filter(Boolean);
@@ -436,16 +1051,28 @@ var preferOfficialComponent = {
436
1051
  else if (tag === "input" && !TEXT_INPUT_TYPES.has(type)) comp = type === "submit" || type === "button" ? config.replaces.get("button") : void 0;
437
1052
  }
438
1053
  if (!comp) continue;
1054
+ const usage = resolveUsage(comp, framework);
1055
+ if (!usage) continue;
439
1056
  if (new RegExp(`(?:function|const|class)\\s+${comp.name}\\b`).test(code)) continue;
1057
+ if (framework !== "react" && implementsTag(code, usage.tag)) continue;
440
1058
  out.push({
441
1059
  offset: m.index,
442
1060
  message: `Raw <${tag}${tag === "input" && /type=/.test(attrs) ? ` type="${attrs.match(/type\s*=\s*\{?\s*["'`]([\w-]+)/)?.[1]}"` : ""}> re-implements ${comp.name}`,
443
- suggestion: `import { ${comp.name} } from "${comp.importPath}"`
1061
+ suggestion: useSuggestion(comp, usage)
444
1062
  });
445
1063
  }
446
1064
  return out;
447
1065
  }
448
1066
  };
1067
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1068
+ var implementsTag = (code, tag) => new RegExp(`selector\\s*:\\s*['"\`]${escapeRe(tag)}['"\`]|customElements\\.define\\(\\s*['"\`]${escapeRe(tag)}['"\`]|@customElement\\(\\s*['"\`]${escapeRe(tag)}['"\`]`).test(code);
1069
+ function useSuggestion(comp, u) {
1070
+ if (u.framework === "react") return `import { ${comp.name} } from "${comp.importPath}"`;
1071
+ const parts = [`Use <${u.tag}>`];
1072
+ if (u.importLine) parts.push(`(${u.importLine})`);
1073
+ if (u.setup && u.via) parts.push(`\u2014 ${u.setup}`);
1074
+ return parts.join(" ");
1075
+ }
449
1076
  var forbiddenImport = {
450
1077
  id: "forbidden-import",
451
1078
  title: "Other UI library",
@@ -501,15 +1128,15 @@ function importedComponents(code, components) {
501
1128
  }
502
1129
  return out;
503
1130
  }
504
- function skipBalanced(code, i) {
505
- const open = code[i];
1131
+ function skipBalanced(code, i2) {
1132
+ const open = code[i2];
506
1133
  if (open === '"' || open === "'") {
507
- const end = code.indexOf(open, i + 1);
1134
+ const end = code.indexOf(open, i2 + 1);
508
1135
  return end === -1 ? code.length : end + 1;
509
1136
  }
510
1137
  let depth = 0;
511
1138
  let quote = null;
512
- for (let j = i; j < code.length; j++) {
1139
+ for (let j = i2; j < code.length; j++) {
513
1140
  const c = code[j];
514
1141
  if (quote) {
515
1142
  if (c === "\\") j++;
@@ -525,38 +1152,121 @@ function skipBalanced(code, i) {
525
1152
  function parseAttributes(code, start) {
526
1153
  const attrs = [];
527
1154
  let spread = false;
528
- let i = start;
529
- while (i < code.length) {
530
- while (/\s/.test(code[i] ?? "")) i++;
531
- const c = code[i];
1155
+ let i2 = start;
1156
+ while (i2 < code.length) {
1157
+ while (/\s/.test(code[i2] ?? "")) i2++;
1158
+ const c = code[i2];
532
1159
  if (c === void 0) return null;
533
1160
  if (c === ">") return { attrs, spread, selfClosing: false };
534
- if (c === "/" && code[i + 1] === ">") return { attrs, spread, selfClosing: true };
1161
+ if (c === "/" && code[i2 + 1] === ">") return { attrs, spread, selfClosing: true };
535
1162
  if (c === "{") {
536
1163
  spread = true;
537
- i = skipBalanced(code, i);
1164
+ i2 = skipBalanced(code, i2);
1165
+ continue;
1166
+ }
1167
+ const name = /^[A-Za-z_$][\w$:.-]*/.exec(code.slice(i2, i2 + 80))?.[0];
1168
+ if (!name) return null;
1169
+ attrs.push({ name, offset: i2 });
1170
+ i2 += name.length;
1171
+ while (/\s/.test(code[i2] ?? "")) i2++;
1172
+ if (code[i2] === "=") {
1173
+ i2++;
1174
+ while (/\s/.test(code[i2] ?? "")) i2++;
1175
+ i2 = skipBalanced(code, i2);
1176
+ }
1177
+ }
1178
+ return null;
1179
+ }
1180
+ function parseHtmlAttributes(code, start) {
1181
+ const attrs = [];
1182
+ let i2 = start;
1183
+ while (i2 < code.length) {
1184
+ while (/\s/.test(code[i2] ?? "")) i2++;
1185
+ const c = code[i2];
1186
+ if (c === void 0) return null;
1187
+ if (c === ">" || c === "/" && code[i2 + 1] === ">") return { attrs, end: i2 };
1188
+ if (c === "{" || c === "<" && code[i2 + 1] === "?") {
1189
+ const close = c === "{" ? code[i2 + 1] === "%" ? "%}" : "}}" : "?>";
1190
+ const e = code.indexOf(close, i2 + 2);
1191
+ i2 = e === -1 ? code.length : e + close.length;
538
1192
  continue;
539
1193
  }
540
- const name = /^[A-Za-z_$][\w$:.-]*/.exec(code.slice(i, i + 80))?.[0];
1194
+ const name = /^[^\s=>/"'{]+/.exec(code.slice(i2, i2 + 120))?.[0];
541
1195
  if (!name) return null;
542
- attrs.push({ name, offset: i });
543
- i += name.length;
544
- while (/\s/.test(code[i] ?? "")) i++;
545
- if (code[i] === "=") {
546
- i++;
547
- while (/\s/.test(code[i] ?? "")) i++;
548
- i = skipBalanced(code, i);
1196
+ attrs.push({ name, offset: i2 });
1197
+ i2 += name.length;
1198
+ while (/\s/.test(code[i2] ?? "")) i2++;
1199
+ if (code[i2] === "=") {
1200
+ i2++;
1201
+ while (/\s/.test(code[i2] ?? "")) i2++;
1202
+ const q = code[i2];
1203
+ if (q === '"' || q === "'") {
1204
+ const e = code.indexOf(q, i2 + 1);
1205
+ i2 = e === -1 ? code.length : e + 1;
1206
+ } else while (i2 < code.length && !/[\s>]/.test(code[i2])) i2++;
549
1207
  }
550
1208
  }
551
1209
  return null;
552
1210
  }
1211
+ var HTML_GLOBAL = /^(?:id|class|style|title|role|hidden|tabindex|lang|dir|slot|part|is|key|ref|name|for|form|aria-.+|data-.+)$/;
1212
+ function propName(raw) {
1213
+ let n = raw;
1214
+ if (/^(?:\(|@|on-|v-on:|\*|#|let-|ref-|v-(?!bind:|model:))/.test(n)) return null;
1215
+ n = n.replace(/^v-bind:|^v-model:|^bind-|^:/, "").replace(/^\[\(?|\)?\]$/g, "");
1216
+ if (/^(?:attr|class|style)\./.test(n) || /^ng[A-Z]/.test(n) || /^(?:formControl|formControlName|formGroup|routerLink|routerLinkActive)$/.test(n)) return null;
1217
+ n = n.replace(/\.(?:px|rem|em|%)$/, "").replace(/\..*$/, "");
1218
+ if (HTML_GLOBAL.test(n.toLowerCase())) return null;
1219
+ return n;
1220
+ }
1221
+ var sameName = (a, b) => a.replace(/-/g, "").toLowerCase() === b.replace(/-/g, "").toLowerCase();
1222
+ function templateProps({ code, config, framework }) {
1223
+ const tagged = [];
1224
+ for (const comp of config.components) {
1225
+ const u = resolveUsage(comp, framework);
1226
+ const props = u?.props;
1227
+ if (u && props?.length) tagged.push({ comp, tag: u.tag, props });
1228
+ }
1229
+ if (!tagged.length) return [];
1230
+ const out = [];
1231
+ const forms = /* @__PURE__ */ new Map();
1232
+ for (const t of tagged) {
1233
+ forms.set(t.tag, t);
1234
+ if (framework === "vue") forms.set(t.tag.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), t);
1235
+ }
1236
+ const names = [...forms.keys()].sort((a, b) => b.length - a.length).map(escapeRe).join("|");
1237
+ for (const m of matches(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
1238
+ const t = forms.get(m[1]);
1239
+ const parsed = parseHtmlAttributes(code, m.index + m[0].length);
1240
+ if (!parsed) continue;
1241
+ const list = t.props.map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
1242
+ const passed = [];
1243
+ for (const a of parsed.attrs) {
1244
+ const raw = a.name;
1245
+ if (/^v-model$/.test(raw)) passed.push("modelValue", "value");
1246
+ if (/^\[\(ngModel\)\]$/.test(raw)) passed.push("value");
1247
+ const n = propName(raw);
1248
+ if (!n) continue;
1249
+ passed.push(n);
1250
+ if (t.props.some((p) => sameName(p.name, n))) continue;
1251
+ out.push({ offset: a.offset, message: `<${t.tag}> has no "${n}" input`, suggestion: `Remove it. ${t.comp.name} takes: ${list}` });
1252
+ }
1253
+ for (const p of t.props) {
1254
+ if (!p.required || passed.some((n) => sameName(n, p.name))) continue;
1255
+ if (p.name === "children" || p.name === "content") continue;
1256
+ out.push({ offset: m.index, message: `<${t.tag}> is missing the required "${p.name}" input`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
1257
+ }
1258
+ }
1259
+ return out;
1260
+ }
553
1261
  var componentProps = {
554
1262
  id: "component-props",
555
1263
  title: "Unknown or missing prop",
556
1264
  description: "Design system components only accept their documented props, and required props must be passed.",
557
1265
  defaultSeverity: "error",
558
- kinds: ["jsx"],
559
- check({ code, config }) {
1266
+ kinds: MARKUP,
1267
+ check(ctx) {
1268
+ if (ctx.framework !== "react" || ctx.kind !== "jsx") return templateProps(ctx);
1269
+ const { code, config } = ctx;
560
1270
  if (!config.components.some((c) => c.props?.length)) return [];
561
1271
  const imported = importedComponents(code, config.components);
562
1272
  if (imported.size === 0) return [];
@@ -606,8 +1316,10 @@ var RULES = [
606
1316
  var RULE_IDS = RULES.map((r) => r.id);
607
1317
 
608
1318
  // ../engine/src/source.ts
1319
+ var TEMPLATE_EXTENSIONS = ["mustache", "hbs", "handlebars", "php", "phtml", "twig", "njk", "liquid"];
609
1320
  function fileKind(path) {
610
1321
  const ext = path.toLowerCase().split(".").pop() ?? "";
1322
+ if (TEMPLATE_EXTENSIONS.includes(ext)) return "template";
611
1323
  if (["tsx", "jsx", "ts", "js", "mjs", "cjs", "mdx"].includes(ext)) return "jsx";
612
1324
  if (ext === "vue") return "vue";
613
1325
  if (ext === "svelte") return "svelte";
@@ -615,47 +1327,75 @@ function fileKind(path) {
615
1327
  if (["css", "scss", "sass", "less", "pcss"].includes(ext)) return "css";
616
1328
  return "other";
617
1329
  }
618
- var SCANNABLE = /* @__PURE__ */ new Set(["jsx", "vue", "svelte", "html", "css"]);
1330
+ var SCANNABLE = /* @__PURE__ */ new Set(["jsx", "vue", "svelte", "html", "template", "css"]);
1331
+ var ANGULAR_COMPONENT = /@Component\s*\(\s*\{/;
1332
+ var ANGULAR_TEMPLATE = /\s(?:\[[\w.()-]+\]|\([\w.]+\)|\*ng[A-Z]\w*)\s*=|@(?:if|for|switch)\s*\(/;
1333
+ function fileFramework(path, kind, code, project) {
1334
+ const p = path.toLowerCase();
1335
+ switch (kind) {
1336
+ case "vue":
1337
+ return "vue";
1338
+ case "svelte":
1339
+ return "web-components";
1340
+ case "template":
1341
+ return "html";
1342
+ case "css":
1343
+ return project ?? "react";
1344
+ case "html":
1345
+ if (p.endsWith(".component.html") || project === "angular" || ANGULAR_TEMPLATE.test(code)) return "angular";
1346
+ return project === "vue" || project === "web-components" ? project : "html";
1347
+ case "jsx":
1348
+ if (ANGULAR_COMPONENT.test(code)) return "angular";
1349
+ if (/\.(tsx|jsx|mdx)$/.test(p)) return "react";
1350
+ return project ?? "react";
1351
+ default:
1352
+ return project ?? "react";
1353
+ }
1354
+ }
619
1355
  var blank = (s) => s.replace(/[^\n]/g, " ");
620
1356
  function stripComments(code, kind) {
621
1357
  let out = code;
622
- if (kind === "html" || kind === "vue" || kind === "svelte") {
1358
+ if (kind === "html" || kind === "vue" || kind === "svelte" || kind === "template") {
623
1359
  out = out.replace(/<!--[\s\S]*?-->/g, blank);
624
1360
  }
1361
+ if (kind === "template") {
1362
+ out = out.replace(/\{\{!--[\s\S]*?--\}\}|\{\{![\s\S]*?\}\}|\{\{--[\s\S]*?--\}\}|\{#[\s\S]*?#\}|\{%-?\s*comment\s*-?%\}[\s\S]*?\{%-?\s*endcomment\s*-?%\}/g, blank).replace(/<\?(?:php|=)?[\s\S]*?(?:\?>|$)/g, (block) => block.replace(/\/\*[\s\S]*?\*\/|(?<![:"'])\/\/[^\n]*?(?=\?>|\n|$)|(?<=\s)#[^\n]*?(?=\?>|\n|$)/g, blank));
1363
+ return out;
1364
+ }
625
1365
  if (kind === "css") {
626
1366
  return out.replace(/\/\*[\s\S]*?\*\//g, blank).replace(/(^|[^:])\/\/[^\n]*/g, (m, p1) => p1 + blank(m.slice(p1.length)));
627
1367
  }
628
1368
  const chars = out.split("");
629
- let i = 0;
1369
+ let i2 = 0;
630
1370
  let quote = null;
631
- while (i < chars.length) {
632
- const c = chars[i];
633
- const n = chars[i + 1];
1371
+ while (i2 < chars.length) {
1372
+ const c = chars[i2];
1373
+ const n = chars[i2 + 1];
634
1374
  if (quote) {
635
- if (c === "\\") i += 2;
1375
+ if (c === "\\") i2 += 2;
636
1376
  else {
637
1377
  if (c === quote) quote = null;
638
- i++;
1378
+ i2++;
639
1379
  }
640
1380
  continue;
641
1381
  }
642
1382
  if (c === '"' || c === "'" || c === "`") {
643
1383
  quote = c;
644
- i++;
1384
+ i2++;
645
1385
  continue;
646
1386
  }
647
1387
  if (c === "/" && n === "*") {
648
- const end = out.indexOf("*/", i + 2);
1388
+ const end = out.indexOf("*/", i2 + 2);
649
1389
  const stop = end === -1 ? chars.length : end + 2;
650
- for (let j = i; j < stop; j++) if (chars[j] !== "\n") chars[j] = " ";
651
- i = stop;
1390
+ for (let j = i2; j < stop; j++) if (chars[j] !== "\n") chars[j] = " ";
1391
+ i2 = stop;
652
1392
  continue;
653
1393
  }
654
- if (c === "/" && n === "/" && chars[i - 1] !== ":" && kind === "jsx") {
655
- while (i < chars.length && chars[i] !== "\n") chars[i++] = " ";
1394
+ if (c === "/" && n === "/" && chars[i2 - 1] !== ":" && kind === "jsx") {
1395
+ while (i2 < chars.length && chars[i2] !== "\n") chars[i2++] = " ";
656
1396
  continue;
657
1397
  }
658
- i++;
1398
+ i2++;
659
1399
  }
660
1400
  return chars.join("");
661
1401
  }
@@ -664,7 +1404,7 @@ var LineIndex = class {
664
1404
  text;
665
1405
  constructor(text) {
666
1406
  this.text = text;
667
- for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) this.starts.push(i + 1);
1407
+ for (let i2 = 0; i2 < text.length; i2++) if (text.charCodeAt(i2) === 10) this.starts.push(i2 + 1);
668
1408
  }
669
1409
  position(offset) {
670
1410
  let lo = 0;
@@ -687,19 +1427,19 @@ var LineIndex = class {
687
1427
  };
688
1428
  function globToRegExp(glob) {
689
1429
  let re = "";
690
- for (let i = 0; i < glob.length; i++) {
691
- const c = glob[i];
1430
+ for (let i2 = 0; i2 < glob.length; i2++) {
1431
+ const c = glob[i2];
692
1432
  if (c === "*") {
693
- if (glob[i + 1] === "*") {
694
- const slash = glob[i + 2] === "/";
1433
+ if (glob[i2 + 1] === "*") {
1434
+ const slash = glob[i2 + 2] === "/";
695
1435
  re += slash ? "(?:.*/)?" : ".*";
696
- i += slash ? 2 : 1;
1436
+ i2 += slash ? 2 : 1;
697
1437
  } else re += "[^/]*";
698
1438
  } else if (c === "?") re += "[^/]";
699
1439
  else if (c === "{") {
700
- const end = glob.indexOf("}", i);
701
- re += `(?:${glob.slice(i + 1, end).split(",").map((s) => s.replace(/[.+^$()|[\]\\]/g, "\\$&")).join("|")})`;
702
- i = end;
1440
+ const end = glob.indexOf("}", i2);
1441
+ re += `(?:${glob.slice(i2 + 1, end).split(",").map((s) => s.replace(/[.+^$()|[\]\\]/g, "\\$&")).join("|")})`;
1442
+ i2 = end;
703
1443
  } else re += c.replace(/[.+^$()|[\]\\]/g, "\\$&");
704
1444
  }
705
1445
  return new RegExp(`^${re}$`);
@@ -713,7 +1453,7 @@ function makeIgnore(globs) {
713
1453
  }
714
1454
 
715
1455
  // ../engine/src/check.ts
716
- var ENGINE_VERSION = "0.3.1";
1456
+ var ENGINE_VERSION = "0.4.0";
717
1457
  var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
718
1458
  function computeScore(counts, lines) {
719
1459
  const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
@@ -738,11 +1478,12 @@ function check(files, configInput, opts = {}) {
738
1478
  const idx = new LineIndex(file.content);
739
1479
  linesScanned += only ? only.size : file.content.split("\n").filter((l) => l.trim()).length;
740
1480
  const code = stripComments(file.content, kind);
1481
+ const framework = fileFramework(path, kind, code, config.framework);
741
1482
  const seen = /* @__PURE__ */ new Set();
742
1483
  for (const rule of RULES) {
743
1484
  const setting = config.rules[rule.id];
744
1485
  if (setting === "off" || !rule.kinds.includes(kind)) continue;
745
- for (const f of rule.check({ path, kind, code, config })) {
1486
+ for (const f of rule.check({ path, kind, code, config, framework })) {
746
1487
  const { line, column } = idx.position(f.offset);
747
1488
  if (only && !only.has(line)) continue;
748
1489
  const key = `${rule.id}:${line}:${column}`;
@@ -789,12 +1530,12 @@ var ansi = (on) => {
789
1530
  return { red: w("31"), yellow: w("33"), cyan: w("36"), green: w("32"), dim: w("2"), bold: w("1") };
790
1531
  };
791
1532
  var rendered = (r) => r.violations.length > 0 && r.violations.every((v) => v.line === 0);
792
- function formatPretty(report, { color = true, max = 200 } = {}) {
1533
+ function formatPretty(report, { color = true, max: max2 = 200 } = {}) {
793
1534
  const c = ansi(color);
794
1535
  const sev = { error: c.red, warning: c.yellow, info: c.cyan };
795
1536
  const lines = [];
796
1537
  let lastFile = "";
797
- for (const v of report.violations.slice(0, max)) {
1538
+ for (const v of report.violations.slice(0, max2)) {
798
1539
  if (v.file !== lastFile) {
799
1540
  lines.push("", c.bold(v.file));
800
1541
  lastFile = v.file;
@@ -804,7 +1545,7 @@ function formatPretty(report, { color = true, max = 200 } = {}) {
804
1545
  if (v.suggestion) lines.push(` ${" ".repeat(16)} ${c.green("\u2192")} ${v.suggestion}`);
805
1546
  if (rendered2 && v.snippet) lines.push(` ${" ".repeat(16)} ${c.dim(`on ${v.snippet}`)}`);
806
1547
  }
807
- if (report.violations.length > max) lines.push("", c.dim(`\u2026 and ${report.violations.length - max} more`));
1548
+ if (report.violations.length > max2) lines.push("", c.dim(`\u2026 and ${report.violations.length - max2} more`));
808
1549
  const s = report.score;
809
1550
  const scoreStr = s >= 80 ? c.green(String(s)) : s >= 65 ? c.yellow(String(s)) : c.red(String(s));
810
1551
  lines.push(
@@ -816,9 +1557,9 @@ function formatPretty(report, { color = true, max = 200 } = {}) {
816
1557
  }
817
1558
  var esc = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
818
1559
  var escProp = (s) => esc(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
819
- function formatGithubAnnotations(report, max = 50) {
1560
+ function formatGithubAnnotations(report, max2 = 50) {
820
1561
  const level = { error: "error", warning: "warning", info: "notice" };
821
- return report.violations.slice(0, max).map(
1562
+ return report.violations.slice(0, max2).map(
822
1563
  (v) => `::${level[v.severity]} file=${escProp(v.file)},line=${v.line},col=${v.column},title=${escProp(`Brandrail: ${title(v.ruleId)}`)}::${esc(
823
1564
  v.suggestion ? `${v.message}. ${v.suggestion}` : v.message
824
1565
  )}`
@@ -931,6 +1672,17 @@ function fontWeight(v) {
931
1672
  return WEIGHTS[slug(v)] ?? (/^\d{3}$/.test(v.trim()) ? v.trim() : v);
932
1673
  }
933
1674
  function colorValue(v) {
1675
+ if (v && typeof v === "object" && ("components" in v || "hex" in v)) {
1676
+ const c = v;
1677
+ const alpha = typeof c.alpha === "number" ? c.alpha : 1;
1678
+ const withAlpha = (hex2) => alpha < 1 ? `${hex2.slice(0, 7)}${Math.round(alpha * 255).toString(16).padStart(2, "0")}` : hex2.slice(0, 7);
1679
+ if (typeof c.hex === "string" && /^#[0-9a-f]{6}/i.test(c.hex)) return withAlpha(c.hex.toLowerCase());
1680
+ const comps = c.components?.map((x2) => typeof x2 === "number" ? x2 : 0);
1681
+ if (comps?.length === 3 && (!c.colorSpace || /srgb|display-p3/i.test(c.colorSpace))) {
1682
+ return withAlpha(toHex({ r: comps[0] * 255, g: comps[1] * 255, b: comps[2] * 255, a: 1 }));
1683
+ }
1684
+ return null;
1685
+ }
934
1686
  if (v && typeof v === "object" && "r" in v) {
935
1687
  const c = v;
936
1688
  const hex2 = toHex({ r: c.r * 255, g: c.g * 255, b: c.b * 255, a: 1 });
@@ -948,7 +1700,14 @@ function colorValue(v) {
948
1700
  const hex = toHex(rgb);
949
1701
  return rgb.a < 1 ? `${hex}${Math.round(rgb.a * 255).toString(16).padStart(2, "0")}` : hex;
950
1702
  }
951
- var dim = (v) => typeof v === "number" ? `${Math.round(v * 100) / 100}px` : String(v).trim();
1703
+ var dim = (v) => {
1704
+ if (v && typeof v === "object" && "value" in v) {
1705
+ const d = v;
1706
+ const n = typeof d.value === "number" ? d.value : parseFloat(d.value);
1707
+ return d.unit === "rem" ? `${Math.round(n * 16 * 100) / 100}px` : `${Math.round(n * 100) / 100}px`;
1708
+ }
1709
+ return typeof v === "number" ? `${Math.round(v * 100) / 100}px` : String(v).trim();
1710
+ };
952
1711
  function classify(name, kind, value, opts = {}) {
953
1712
  const parts = segs(name);
954
1713
  const hay = slug(`${opts.context ?? ""} ${name}`);
@@ -980,7 +1739,7 @@ function classify(name, kind, value, opts = {}) {
980
1739
  return null;
981
1740
  }
982
1741
  const v = dim(value);
983
- const has = (...scope) => scope.some((x) => scopes.includes(x));
1742
+ const has = (...scope) => scope.some((x2) => scopes.includes(x2));
984
1743
  const nm = (...prefixes) => stripPrefix(last, ...prefixes) || last;
985
1744
  if (has("CORNER_RADIUS") || /radius|(^|-)rad(-|$)|corner|rounded/.test(hay)) return { path: `radius.${nm("radius", "rad", "corner", "rounded")}`, type: "dimension", value: v };
986
1745
  if (has("FONT_SIZE") || /font-size|text-size/.test(hay)) return { path: `font.size.${nm("font-size", "text-size", "size")}`, type: "dimension", value: v };
@@ -990,7 +1749,7 @@ function classify(name, kind, value, opts = {}) {
990
1749
  if (/icon/.test(last)) return { path: `size.icon.${nm("icon")}`, type: "dimension", value: v };
991
1750
  if (has("GAP") || /spac|gap|padding|margin|inset|(^|-)space(-|$)/.test(hay)) return { path: `space.${nm("spacing", "space", "gap")}`, type: "dimension", value: v };
992
1751
  if (has("STROKE_FLOAT") || /stroke|border-width/.test(hay)) return { path: `border.width.${nm("border-width", "stroke")}`, type: "dimension", value: v };
993
- if (has("WIDTH_HEIGHT") || /size|width|height/.test(hay)) return { path: ["size", ...rest.filter((x) => x !== "size"), last].join("."), type: "dimension", value: v };
1752
+ if (has("WIDTH_HEIGHT") || /size|width|height/.test(hay)) return { path: ["size", ...rest.filter((x2) => x2 !== "size"), last].join("."), type: "dimension", value: v };
994
1753
  return { path: ["number", ...parts].join("."), type: "number", value: String(value) };
995
1754
  }
996
1755
  function rawLeaves(node, prefix, out, studio, inherited) {
@@ -1051,8 +1810,8 @@ function flattenTokens(doc) {
1051
1810
  if (!v) continue;
1052
1811
  const d = r.dark !== void 0 ? colorValue(r.dark) : null;
1053
1812
  out.push({ path: r.path, type: "color", value: v, ...d ? { dark: d } : {}, ...r.description ? { description: r.description } : {} });
1054
- } else if (typeof r.value === "string" || typeof r.value === "number") {
1055
- const type = t === "fontfamily" || t === "fontfamilies" ? "fontFamily" : t === "fontweight" || t === "fontweights" ? "fontWeight" : TYPE_KIND[t] === "number" ? "dimension" : t === "number" ? "number" : /^font\.family\./.test(r.path) ? "fontFamily" : typeof r.value === "number" ? "number" : /px$|rem$/.test(String(r.value)) ? "dimension" : "string";
1813
+ } else if (typeof r.value === "string" || typeof r.value === "number" || r.value && typeof r.value === "object" && "unit" in r.value) {
1814
+ const type = typeof r.value === "object" ? "dimension" : t === "fontfamily" || t === "fontfamilies" ? "fontFamily" : t === "fontweight" || t === "fontweights" ? "fontWeight" : TYPE_KIND[t] === "number" ? "dimension" : t === "number" ? "number" : /^font\.family\./.test(r.path) ? "fontFamily" : typeof r.value === "number" ? "number" : /px$|rem$/.test(String(r.value)) ? "dimension" : "string";
1056
1815
  out.push({ path: r.path, type, value: type === "dimension" ? dim(r.value) : String(r.value), ...r.description ? { description: r.description } : {} });
1057
1816
  }
1058
1817
  }
@@ -1111,8 +1870,8 @@ function buildDtcg(tokens) {
1111
1870
  for (const t of tokens) {
1112
1871
  const parts = t.path.split(".");
1113
1872
  let node = tree;
1114
- parts.forEach((p, i) => {
1115
- if (i === parts.length - 1) {
1873
+ parts.forEach((p, i2) => {
1874
+ if (i2 === parts.length - 1) {
1116
1875
  node[p] = {
1117
1876
  $type: t.type,
1118
1877
  $value: t.value,
@@ -1209,7 +1968,7 @@ function fromFigmaVariables(meta, opts = {}) {
1209
1968
  if (!c) continue;
1210
1969
  let darkValue;
1211
1970
  if (c.type === "color") {
1212
- const hasDarkSomewhere = Object.values(cols).some((x) => x.modes.some((m) => DARK.test(m.name)));
1971
+ const hasDarkSomewhere = Object.values(cols).some((x2) => x2.modes.some((m) => DARK.test(m.name)));
1213
1972
  if (hasDarkSomewhere) {
1214
1973
  const d = colorValue(resolve4(v, "dark"));
1215
1974
  if (d && d !== c.value) darkValue = d;
@@ -1243,7 +2002,7 @@ function fromFigmaStyles(styles, opts = {}) {
1243
2002
  }
1244
2003
  }
1245
2004
  const fams = [...families.entries()].sort((a, b) => b[1] - a[1]);
1246
- fams.forEach(([f], i) => out.push({ path: `font.family.${i === 0 ? "sans" : /mono|code/i.test(f) ? "mono" : slug(f)}`, type: "fontFamily", value: f }));
2005
+ fams.forEach(([f], i2) => out.push({ path: `font.family.${i2 === 0 ? "sans" : /mono|code/i.test(f) ? "mono" : slug(f)}`, type: "fontFamily", value: f }));
1247
2006
  const notes = skipped ? [`${skipped} fill style${skipped === 1 ? "" : "s"} (gradients or images) were skipped.`] : [];
1248
2007
  return finish("figma-styles", out, notes, opts);
1249
2008
  }
@@ -1261,7 +2020,7 @@ function fromModeObject(obj, opts = {}) {
1261
2020
  const leafSets = modes.map(([name, v]) => {
1262
2021
  const l = [];
1263
2022
  plainLeaves(v, [], l);
1264
- return { name, leaves: l, keys: new Set(l.map((x) => x.key.join("/"))) };
2023
+ return { name, leaves: l, keys: new Set(l.map((x2) => x2.key.join("/"))) };
1265
2024
  });
1266
2025
  const groups = [];
1267
2026
  for (const m of leafSets) {
@@ -1310,7 +2069,7 @@ function fromModeObject(obj, opts = {}) {
1310
2069
  if (!c) continue;
1311
2070
  let d;
1312
2071
  if (c.type === "color") {
1313
- const darkLeaf = dark && dark !== def ? dark.leaves.find((x) => x.key.join("/") === l.key.join("/")) : void 0;
2072
+ const darkLeaf = dark && dark !== def ? dark.leaves.find((x2) => x2.key.join("/") === l.key.join("/")) : void 0;
1314
2073
  const dv = colorValue(deref(darkLeaf ? darkLeaf.value : l.value, "dark"));
1315
2074
  if (dv && dv !== c.value) d = dv;
1316
2075
  }
@@ -1320,12 +2079,125 @@ function fromModeObject(obj, opts = {}) {
1320
2079
  for (const p of pending) out.push({ ...p.t, referenced: referenced.has(p.key) });
1321
2080
  return finish("mode-object", out, notes, opts);
1322
2081
  }
2082
+ var figmaExt = (node, key) => {
2083
+ const ext = node.$extensions;
2084
+ if (!ext) return void 0;
2085
+ if (`com.figma.${key}` in ext) return ext[`com.figma.${key}`];
2086
+ const nested = ext["com.figma"];
2087
+ return nested?.[key];
2088
+ };
2089
+ function fileLeaves(node, prefix, out, inherited) {
2090
+ if (!node || typeof node !== "object" || Array.isArray(node)) return;
2091
+ const o = node;
2092
+ const groupType = o.$type ?? inherited;
2093
+ if ("$value" in o) {
2094
+ const alias = figmaExt(o, "aliasData");
2095
+ const scopes = figmaExt(o, "scopes");
2096
+ out.push({
2097
+ path: prefix,
2098
+ type: o.$type ?? groupType,
2099
+ value: o.$value,
2100
+ description: o.$description,
2101
+ scopes: Array.isArray(scopes) ? scopes : void 0,
2102
+ aliasTarget: alias?.targetVariableName?.replace(/\//g, "."),
2103
+ hidden: figmaExt(o, "hiddenFromPublishing") === true || void 0
2104
+ });
2105
+ return;
2106
+ }
2107
+ for (const [k, v] of Object.entries(o)) {
2108
+ if (k.startsWith("$")) continue;
2109
+ fileLeaves(v, prefix ? `${prefix}.${k}` : k, out, groupType);
2110
+ }
2111
+ }
2112
+ var hasFigmaExt = (node, depth = 0) => {
2113
+ if (!node || typeof node !== "object" || depth > 12) return false;
2114
+ const ext = node.$extensions;
2115
+ if (ext && Object.keys(ext).some((k) => k.startsWith("com.figma"))) return true;
2116
+ return Object.values(node).some((v) => hasFigmaExt(v, depth + 1));
2117
+ };
2118
+ function modeOf(file) {
2119
+ const root = file.data ?? {};
2120
+ const parts = file.name.replace(/\\/g, "/").split("/").filter(Boolean);
2121
+ const base = (parts.pop() ?? "tokens").replace(/(\.tokens)?\.json$/i, "");
2122
+ const named = figmaExt(root, "modeName");
2123
+ const split = base.match(/^(.+?)[\s._-]+(light|dark|night|day|default|mode[\s_-]*\d+)$/i);
2124
+ return {
2125
+ collection: parts.join("/") || (split ? split[1] : ""),
2126
+ mode: typeof named === "string" ? named : split ? split[2] : base
2127
+ };
2128
+ }
2129
+ function importTokenFiles(files, opts = {}) {
2130
+ const parsed = files.filter((f) => f.data && typeof f.data === "object");
2131
+ if (!parsed.length) throw new Error("No JSON token files found.");
2132
+ const figma = parsed.some((f) => hasFigmaExt(f.data));
2133
+ if (parsed.length === 1 && !figma) return importTokens(parsed[0].data, opts);
2134
+ const notes = [];
2135
+ const withMode = parsed.map((f) => ({ ...f, ...modeOf(f) }));
2136
+ const byCollection = /* @__PURE__ */ new Map();
2137
+ for (const f of withMode) byCollection.set(f.collection, [...byCollection.get(f.collection) ?? [], f]);
2138
+ const light = /* @__PURE__ */ new Map();
2139
+ const darkOverrides = /* @__PURE__ */ new Map();
2140
+ for (const [collection, list] of byCollection) {
2141
+ const dark = list.find((f) => DARK.test(f.mode));
2142
+ const def = list.find((f) => LIGHT.test(f.mode) && !DARK.test(f.mode)) ?? list.find((f) => !DARK.test(f.mode)) ?? list[0];
2143
+ if (list.length > 1) {
2144
+ const label = collection ? `\u201C${collection}\u201D` : "The files";
2145
+ notes.push(`${label}: modes ${list.map((f) => f.mode).join(", ")}; used \u201C${def.mode}\u201D${dark && dark !== def ? ` (and \u201C${dark.mode}\u201D as dark)` : ""}.`);
2146
+ }
2147
+ const read = (f, into) => {
2148
+ const leaves2 = [];
2149
+ fileLeaves(f.data, "", leaves2);
2150
+ for (const l of leaves2) into.set(l.path, { path: l.path, type: l.type, value: l.value, description: l.description, leaf: l });
2151
+ };
2152
+ read(def, light);
2153
+ if (dark && dark !== def) read(dark, darkOverrides);
2154
+ }
2155
+ const lightRaws = resolveRefs([...light.values()]);
2156
+ const darkRaws = resolveRefs([...light.values()].map((r) => darkOverrides.get(r.path) ?? r));
2157
+ const darkByPath = new Map(darkRaws.map((r) => [r.path, r.value]));
2158
+ const referenced = /* @__PURE__ */ new Set();
2159
+ for (const r of [...light.values(), ...darkOverrides.values()]) {
2160
+ if (r.leaf.aliasTarget) referenced.add(r.leaf.aliasTarget);
2161
+ const m = typeof r.value === "string" ? r.value.trim().match(REF) : null;
2162
+ if (m) referenced.add(m[1]);
2163
+ }
2164
+ const out = [];
2165
+ let hidden = 0;
2166
+ for (const r of lightRaws) {
2167
+ const leaf = light.get(r.path).leaf;
2168
+ if (leaf.hidden) {
2169
+ hidden++;
2170
+ continue;
2171
+ }
2172
+ const t = (r.type ?? "").toLowerCase();
2173
+ const kind = t === "color" ? "color" : t === "boolean" ? "boolean" : t === "string" || t === "fontfamily" || t === "fontweight" ? "string" : typeof r.value === "string" && !/^-?[\d.]+(px|rem)?$/.test(r.value) && !colorValue(r.value) ? "string" : colorValue(r.value) && t !== "number" && t !== "dimension" ? "color" : "number";
2174
+ let tok;
2175
+ if (figma) {
2176
+ const value = kind === "number" && r.value && typeof r.value === "object" ? parseFloat(dim(r.value)) : r.value;
2177
+ tok = classify(r.path.replace(/\./g, "/"), kind, value, { scopes: leaf.scopes?.filter((x2) => x2 !== "ALL_SCOPES") });
2178
+ } else {
2179
+ const flat = flattenTokens({ x: { $type: r.type, $value: r.value } })[0];
2180
+ tok = flat ? { path: flat.type === "color" && !r.path.startsWith("color.") ? `color.${r.path}` : r.path, type: flat.type, value: flat.value } : null;
2181
+ }
2182
+ if (!tok) continue;
2183
+ let darkValue;
2184
+ if (tok.type === "color" && darkOverrides.size) {
2185
+ const d = colorValue(darkByPath.get(r.path));
2186
+ if (d && d !== tok.value) darkValue = d;
2187
+ }
2188
+ out.push({ ...tok, ...darkValue ? { dark: darkValue } : {}, ...r.description ? { description: r.description } : {}, referenced: referenced.has(r.path) });
2189
+ }
2190
+ if (hidden) notes.push(`${hidden} variable${hidden === 1 ? "" : "s"} hidden from publishing were skipped.`);
2191
+ return finish(figma ? "figma-export" : "dtcg", out, notes, opts);
2192
+ }
1323
2193
  function importTokens(input, opts = {}) {
1324
2194
  if (!input || typeof input !== "object") throw new Error("Expected a JSON object.");
1325
2195
  const o = input;
2196
+ if (Array.isArray(o.$files)) return importTokenFiles(o.$files, opts);
1326
2197
  const meta = o.meta ?? o;
1327
2198
  if (meta.variables && meta.variableCollections) return fromFigmaVariables(meta, opts);
1328
2199
  if (Array.isArray(o.styles) && o.styles.every((s) => s && "styleType" in s)) return fromFigmaStyles(o.styles, opts);
2200
+ if (hasDollarValue(o) && hasFigmaExt(o)) return importTokenFiles([{ name: "tokens.json", data: o }], opts);
1329
2201
  if (hasDollarValue(o) || hasStudioValue(o)) {
1330
2202
  const studio = !hasDollarValue(o);
1331
2203
  const flat = flattenTokens(o);
@@ -1368,6 +2240,7 @@ var RULES_START = "<!-- brandrail:start -->";
1368
2240
  var RULES_END = "<!-- brandrail:end -->";
1369
2241
  function generateInstructions(config, opts = {}) {
1370
2242
  const r = resolveConfig(config);
2243
+ const fw = opts.framework ?? config.framework ?? "react";
1371
2244
  const lines = [];
1372
2245
  lines.push(`# Design system rules${opts.org ? ` \u2014 ${opts.org}` : ""}`);
1373
2246
  lines.push("");
@@ -1375,7 +2248,7 @@ function generateInstructions(config, opts = {}) {
1375
2248
  `All UI in this repo must follow the Brand Kit${r.tokenVersion ? ` (v${r.tokenVersion})` : ""}. These rules are checked on every pull request by Brandrail.`
1376
2249
  );
1377
2250
  lines.push("");
1378
- const usesKit = r.components.some((c) => c.importPath === KIT_PACKAGE);
2251
+ const usesKit = fw === "react" && r.components.some((c) => c.importPath === KIT_PACKAGE);
1379
2252
  if (opts.tokensCss || usesKit) {
1380
2253
  lines.push("## Setup");
1381
2254
  if (opts.tokensCss) {
@@ -1387,17 +2260,29 @@ function generateInstructions(config, opts = {}) {
1387
2260
  lines.push("");
1388
2261
  }
1389
2262
  if (r.components.length) {
2263
+ const usable = r.components.map((c) => ({ c, u: resolveUsage(c, fw) }));
2264
+ const here = usable.filter((x2) => x2.u);
2265
+ const missing = usable.filter((x2) => !x2.u).map((x2) => x2.c.name);
1390
2266
  lines.push("## Components");
1391
- lines.push("Always use these components. Never re-create them with raw HTML elements:");
1392
- for (const c of r.components) {
2267
+ if (fw !== "react") lines.push(`This is a${fw === "angular" ? "n" : ""} ${FRAMEWORK_LABEL[fw]} project: use the ${FRAMEWORK_LABEL[fw]} form of each component shown here.`);
2268
+ if (here.length) lines.push("Always use these components. Never re-create them with raw HTML elements:");
2269
+ for (const { c, u } of here) {
1393
2270
  const tags = [...r.replaces].filter(([, v]) => v === c).map(([t]) => `<${t}>`);
1394
- const props = c.props?.length ? ` \u2014 props: ${c.props.map((p) => p.required ? `${p.name}*` : p.name).join(", ")}` : "";
1395
- lines.push(`- \`${c.name}\` from \`${c.importPath}\`${tags.length ? ` (instead of ${tags.join(", ")})` : ""}${props}`);
1396
- }
1397
- if (r.components.some((c) => c.props?.length)) {
2271
+ const props = u.props?.length ? ` \u2014 props: ${u.props.map((p) => p.required ? `${p.name}*` : p.name).join(", ")}` : "";
2272
+ const how = fw === "react" ? `\`${c.name}\` from \`${c.importPath}\`` : `\`${markup(u)}\`${u.importLine ? ` (\`${u.importLine}\`)` : ""}`;
2273
+ lines.push(`- ${how}${fw === "react" ? "" : ` \u2014 ${c.name}`}${tags.length ? ` (instead of ${tags.join(", ")})` : ""}${props}`);
2274
+ }
2275
+ const setups = [...new Set(here.map((x2) => x2.u.setup).filter(Boolean))];
2276
+ if (setups.length && setups.length <= 3) for (const s of setups) lines.push(`- Setup: ${s}`);
2277
+ else if (setups.length) lines.push("- Each component's setup is in its docs (MCP `get_component_docs`).");
2278
+ if (here.some((x2) => x2.u.props?.length)) {
1398
2279
  lines.push("");
1399
2280
  lines.push("Pass only the props listed for each component (* = required). Any other prop is a type error and fails the check: if you need something a component doesn't take, look for a related component (often one with a longer name, like AvatarLabel next to Avatar) or compose it with design tokens.");
1400
2281
  }
2282
+ if (missing.length) {
2283
+ lines.push("");
2284
+ lines.push(`No ${FRAMEWORK_LABEL[fw]} version yet: ${missing.join(", ")}. Build these with semantic HTML and the design tokens below; don't install another UI library.`);
2285
+ }
1401
2286
  lines.push("");
1402
2287
  }
1403
2288
  if (r.colors.length) {
@@ -1416,7 +2301,7 @@ function generateInstructions(config, opts = {}) {
1416
2301
  const radiusVars = named(/^radius\./);
1417
2302
  const sizeVars = named(/^font\.size\./);
1418
2303
  lines.push(`- Spacing (padding, margin, gap) must be one of: ${r.spacing.filter((s) => s > 2).map((s) => `${s}px`).join(", ")}.${spaceVars.length ? ` Prefer the variables: ${spaceVars.join(", ")}.` : ""}`);
1419
- lines.push(`- Corner radius must be one of: ${r.radii.filter((x) => x > 0 && x < 999).map((x) => `${x}px`).join(", ")} (or fully rounded).${radiusVars.length ? ` Variables: ${radiusVars.join(", ")}.` : ""}`);
2304
+ lines.push(`- Corner radius must be one of: ${r.radii.filter((x2) => x2 > 0 && x2 < 999).map((x2) => `${x2}px`).join(", ")} (or fully rounded).${radiusVars.length ? ` Variables: ${radiusVars.join(", ")}.` : ""}`);
1420
2305
  if (sizeVars.length) lines.push(`- Font sizes come from the type scale: ${sizeVars.join(", ")}. Matching line heights are \`--font-line-height-*\`.`);
1421
2306
  if (r.fonts.length) lines.push(`- Fonts: use ${r.fontVars.map((v) => `\`var(${v})\``).join(" or ")}; never set another font-family.`);
1422
2307
  lines.push("");
@@ -1459,7 +2344,7 @@ function mcpClientConfigs(url) {
1459
2344
  file: ".mcp.json",
1460
2345
  projectFile: true,
1461
2346
  content: json({ mcpServers: { brandrail: { type: "http", url, headers: { Authorization: "Bearer ${BRANDRAIL_KEY}" } } } }),
1462
- command: `claude mcp add --transport http brandrail ${url} --header "Authorization: Bearer $BRANDRAIL_KEY"`
2347
+ command: `claude mcp add --transport http brandrail "${url}" --header "Authorization: Bearer $BRANDRAIL_KEY"`
1463
2348
  },
1464
2349
  {
1465
2350
  id: "codex",
@@ -1470,7 +2355,7 @@ function mcpClientConfigs(url) {
1470
2355
  url = "${url}"
1471
2356
  bearer_token_env_var = "BRANDRAIL_KEY"
1472
2357
  `,
1473
- command: `codex mcp add brandrail --url ${url} --bearer-token-env-var BRANDRAIL_KEY`
2358
+ command: `codex mcp add brandrail --url "${url}" --bearer-token-env-var BRANDRAIL_KEY`
1474
2359
  },
1475
2360
  {
1476
2361
  id: "cursor",
@@ -1589,9 +2474,9 @@ function collectRenderedNodes(maxNodes = 3e3) {
1589
2474
  }
1590
2475
  var near = (scale, v, tol = 0.5) => scale.some((s) => Math.abs(s - v) <= tol);
1591
2476
  var neighbours2 = (scale, v) => {
1592
- const lo = [...scale].reverse().find((x) => x <= v);
1593
- const hi = scale.find((x) => x >= v);
1594
- return [...new Set([lo, hi].filter((x) => x !== void 0))].map((x) => `${x}px`).join(" or ");
2477
+ const lo = [...scale].reverse().find((x2) => x2 <= v);
2478
+ const hi = scale.find((x2) => x2 >= v);
2479
+ return [...new Set([lo, hi].filter((x2) => x2 !== void 0))].map((x2) => `${x2}px`).join(" or ");
1595
2480
  };
1596
2481
  function closestToken(config, rgb) {
1597
2482
  const lab = rgbToLab(rgb);
@@ -1657,17 +2542,17 @@ function checkRendered(pages, configInput, opts = {}) {
1657
2542
  const bg = n.effectiveBackground ? parseColor(n.effectiveBackground) : null;
1658
2543
  if (fg && bg && fg.a > 0.05) {
1659
2544
  const blended = fg.a < 1 ? { r: fg.r * fg.a + bg.r * (1 - fg.a), g: fg.g * fg.a + bg.g * (1 - fg.a), b: fg.b * fg.a + bg.b * (1 - fg.a), a: 1 } : fg;
1660
- const ratio = contrastRatio(blended, bg);
2545
+ const ratio2 = contrastRatio(blended, bg);
1661
2546
  const large = n.fontSize >= 24 || n.fontSize >= 18.66 && n.fontWeight >= 700;
1662
2547
  const min = large ? 3 : 4.5;
1663
- if (ratio < min) {
2548
+ if (ratio2 < min) {
1664
2549
  const pair = `${colorLabel(fg)} on ${toHex(bg)}`;
1665
2550
  add(
1666
2551
  `contrast:${pair}:${large}`,
1667
2552
  {
1668
2553
  ruleId: "text-contrast",
1669
2554
  severity: "error",
1670
- message: (c) => `${pair} is ${ratio.toFixed(2)}:1, below ${min}:1${large ? " for large text" : ""} (${c} element${c === 1 ? "" : "s"})`,
2555
+ message: (c) => `${pair} is ${ratio2.toFixed(2)}:1, below ${min}:1${large ? " for large text" : ""} (${c} element${c === 1 ? "" : "s"})`,
1671
2556
  suggestion: "Use the Brand Kit's text colours on its background colours, e.g. fg.default on canvas.default",
1672
2557
  sample: n.text
1673
2558
  },
@@ -1847,15 +2732,15 @@ var px2 = (v) => {
1847
2732
  const m = v.trim().match(/^(-?\d*\.?\d+)(px|rem)?$/);
1848
2733
  return m ? Number.parseFloat(m[1]) * (m[2] === "rem" ? 16 : 1) : null;
1849
2734
  };
1850
- var nearest2 = (values, v) => values.reduce((best, x) => Math.abs(x - v) < Math.abs(best - v) ? x : best, values[0]);
2735
+ var nearest2 = (values, v) => values.reduce((best, x2) => Math.abs(x2 - v) < Math.abs(best - v) ? x2 : best, values[0]);
1851
2736
  function luminance(hex) {
1852
2737
  const n = hex.replace("#", "").slice(0, 6);
1853
- const [r, g, b] = [0, 2, 4].map((i) => parseInt(n.slice(i, i + 2), 16) / 255).map((c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
2738
+ 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);
1854
2739
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
1855
2740
  }
1856
2741
  var contrast = (a, b) => {
1857
- const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);
1858
- return (x + 0.05) / (y + 0.05);
2742
+ const [x2, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);
2743
+ return (x2 + 0.05) / (y + 0.05);
1859
2744
  };
1860
2745
  function onColor(bg, candidates) {
1861
2746
  return candidates.filter((c) => HEX.test(c)).sort((a, b) => contrast(b, bg) - contrast(a, bg))[0] ?? "#ffffff";
@@ -1895,7 +2780,7 @@ function scaleVars(s) {
1895
2780
  function colorVars(colors, dark, font) {
1896
2781
  const r = pickColorRoles(colors, dark);
1897
2782
  const find = (re) => {
1898
- const c = colors.find((x) => re.test(x.path));
2783
+ const c = colors.find((x2) => re.test(x2.path));
1899
2784
  return c ? dark ? c.dark ?? c.value : c.value : void 0;
1900
2785
  };
1901
2786
  const success = find(/success/);
@@ -1945,8 +2830,126 @@ function kitBridgeCss(tokens, opts = {}) {
1945
2830
  return [block(sel.scale, scales), block(sel.light, colorVars(colors, false, sc.font)), block(sel.dark, colorVars(colors, true, sc.font))].filter(Boolean).join("\n");
1946
2831
  }
1947
2832
 
2833
+ // ../engine/src/brand-kit-audit.ts
2834
+ var ratio = (a, b) => {
2835
+ const x2 = parseColor(a);
2836
+ const y = parseColor(b);
2837
+ return x2 && y ? Math.round(contrastRatio(x2, y) * 100) / 100 : 21;
2838
+ };
2839
+ var r1 = (n) => `${Math.round(n * 10) / 10}:1`;
2840
+ var STATUS = [
2841
+ { key: "success", re: /success|positive/, label: "success", usedBy: "success notifications, checks and toggles" },
2842
+ { key: "warning", re: /warning|attention|caution/, label: "warning", usedBy: "warning notifications and states" },
2843
+ { key: "danger", re: /danger|error|critical|negative/, label: "danger / error", usedBy: "errors, destructive buttons and invalid fields" }
2844
+ ];
2845
+ function auditBrandKit(tokens) {
2846
+ const colors = flattenTokens(tokens).filter((t) => t.type === "color").map((t) => ({ path: t.path, value: t.value, dark: t.dark }));
2847
+ const issues = [];
2848
+ if (!colors.length) return issues;
2849
+ const modes = colors.some((c) => c.dark) ? ["light", "dark"] : ["light"];
2850
+ const light = pickColorRoles(colors, false);
2851
+ for (const mode of modes) {
2852
+ const dark = mode === "dark";
2853
+ const r = pickColorRoles(colors, dark);
2854
+ const where = modes.length > 1 ? ` (${mode} mode)` : "";
2855
+ const paths = (...xs) => xs.filter((x2) => !!x2);
2856
+ const text = ratio(r.fg.value, r.canvas.value);
2857
+ if (text < 4.5) {
2858
+ issues.push({
2859
+ severity: "error",
2860
+ code: "text-contrast",
2861
+ mode,
2862
+ paths: paths(r.fg.path, r.canvas.path),
2863
+ message: `Body text on the page background is ${r1(text)}${where}. It needs at least 4.5:1 to be readable.`
2864
+ });
2865
+ }
2866
+ const muted = ratio(r.muted.value, r.canvas.value);
2867
+ if (muted < 4.5) {
2868
+ issues.push({
2869
+ severity: muted < 3 ? "error" : "warning",
2870
+ code: "muted-contrast",
2871
+ mode,
2872
+ paths: paths(r.muted.path, r.canvas.path),
2873
+ message: `Secondary text on the page background is ${r1(muted)}${where}. Captions and help text need 4.5:1; darken ${r.muted.path ?? "the muted colour"}.`
2874
+ });
2875
+ }
2876
+ for (const [role, what] of [
2877
+ ["primary", "primary buttons"],
2878
+ ["danger", "destructive buttons"]
2879
+ ]) {
2880
+ if (!r[role].path) continue;
2881
+ const fill = r[role].value;
2882
+ const best = Math.max(ratio(light.canvas.value, fill), ratio(light.fg.value, fill));
2883
+ if (best < 4.5) {
2884
+ issues.push({
2885
+ severity: best < 3 ? "error" : "warning",
2886
+ code: "on-fill-contrast",
2887
+ mode,
2888
+ paths: [r[role].path],
2889
+ message: `Labels on ${what} are at most ${r1(best)}${where}: neither your background nor your text colour reads well on ${fill}. Use a darker or lighter ${r[role].path} (4.5:1 for text).`
2890
+ });
2891
+ }
2892
+ }
2893
+ for (const [role, what] of [
2894
+ ["primary", "ghost buttons, selected items and focus rings"],
2895
+ ["accent", "links and highlights"]
2896
+ ]) {
2897
+ if (!r[role].path) continue;
2898
+ const vs = ratio(r[role].value, r.canvas.value);
2899
+ if (vs < 4.5) {
2900
+ issues.push({
2901
+ severity: vs < 3 ? "error" : "warning",
2902
+ code: "status-contrast",
2903
+ mode,
2904
+ paths: [r[role].path],
2905
+ message: `${r[role].path} (${r[role].value}) is ${r1(vs)} on the page background${where}. Components use it as text and outlines for ${what}, which need 4.5:1 for text and 3:1 for icons and borders. Darken it, or add a darker variant for text.`
2906
+ });
2907
+ }
2908
+ }
2909
+ for (const s of STATUS) {
2910
+ const c = colors.find((x2) => s.re.test(x2.path));
2911
+ if (!c) continue;
2912
+ const value = dark ? c.dark ?? c.value : c.value;
2913
+ const vs = ratio(value, r.canvas.value);
2914
+ if (vs < 3) {
2915
+ issues.push({
2916
+ severity: "warning",
2917
+ code: "status-contrast",
2918
+ mode,
2919
+ paths: [c.path],
2920
+ message: `${c.path} (${value}) is ${r1(vs)} on the page background${where}: too light for text or icons (3:1 for icons, 4.5:1 for text). It's fine as a fill behind dark text; add a darker variant for text.`
2921
+ });
2922
+ }
2923
+ }
2924
+ }
2925
+ if (!light.primary.path) {
2926
+ issues.push({
2927
+ severity: "warning",
2928
+ code: "missing-color",
2929
+ paths: [],
2930
+ message: "No primary colour (e.g. color.action.primary). Buttons and links fall back to the kit's default green."
2931
+ });
2932
+ }
2933
+ for (const s of STATUS) {
2934
+ if (colors.some((x2) => s.re.test(x2.path))) continue;
2935
+ issues.push({
2936
+ severity: "warning",
2937
+ code: "missing-color",
2938
+ paths: [],
2939
+ message: `No ${s.label} colour (e.g. color.status.${s.key}). It's used for ${s.usedBy}, which fall back to the kit's own colour instead of your brand.`
2940
+ });
2941
+ }
2942
+ const seen = /* @__PURE__ */ new Set();
2943
+ return issues.filter((i2) => {
2944
+ const key = `${i2.code}:${i2.paths.join(",")}:${i2.message.replace(/ \((light|dark) mode\)/, "").replace(/[\d.]+:1/g, "")}`;
2945
+ if (seen.has(key)) return false;
2946
+ seen.add(key);
2947
+ return true;
2948
+ }).sort((a, b) => a.severity === b.severity ? 0 : a.severity === "error" ? -1 : 1);
2949
+ }
2950
+
1948
2951
  // src/api.ts
1949
- var DEFAULT_API = "https://web-phi-nine-77.vercel.app";
2952
+ var DEFAULT_API = "https://www.brandrail.com.au";
1950
2953
  function apiBase() {
1951
2954
  return (process.env.BRANDRAIL_API_URL ?? DEFAULT_API).replace(/\/$/, "");
1952
2955
  }
@@ -2057,7 +3060,7 @@ function parseChangedLines(diff) {
2057
3060
  const start = Number(h[1]);
2058
3061
  const count = h[2] === void 0 ? 1 : Number(h[2]);
2059
3062
  const set = out.get(file);
2060
- for (let i = 0; i < count; i++) set.add(start + i);
3063
+ for (let i2 = 0; i2 < count; i2++) set.add(start + i2);
2061
3064
  }
2062
3065
  }
2063
3066
  return out;
@@ -2070,13 +3073,13 @@ function changedLines(cwd, base) {
2070
3073
 
2071
3074
  // src/scan.ts
2072
3075
  import { existsSync as existsSync2 } from "node:fs";
2073
- import { createRequire } from "node:module";
3076
+ import { createRequire as createRequire2 } from "node:module";
2074
3077
  import { join as join2, resolve as resolve2 } from "node:path";
2075
3078
  function loadPlaywright(cwd) {
2076
3079
  for (const base of [join2(cwd, "package.json"), import.meta.url]) {
2077
3080
  for (const name of ["playwright", "playwright-core"]) {
2078
3081
  try {
2079
- return createRequire(base)(name);
3082
+ return createRequire2(base)(name);
2080
3083
  } catch {
2081
3084
  }
2082
3085
  }
@@ -2132,7 +3135,7 @@ Usage
2132
3135
  brandrail check [paths...] Scan files and print violations
2133
3136
  brandrail init Create brandrail.config.json and AI rule files
2134
3137
  brandrail scan <url> Check a running page (localhost, staging, behind a login) in a real browser
2135
- brandrail import <file> Load tokens (DTCG, Tokens Studio, Figma variables JSON) into the config
3138
+ brandrail import <files\u2026> Load tokens into the config: DTCG, Tokens Studio, Figma variables JSON, or Figma's Export modes ZIP
2136
3139
  brandrail rules List the rules
2137
3140
 
2138
3141
  Options for check
@@ -2148,6 +3151,7 @@ Options for init
2148
3151
  --no-rules Don't write AGENTS.md (Codex), CLAUDE.md, Cursor, Copilot, Windsurf, Gemini rule files
2149
3152
  --mcp Also add the Brandrail MCP server to .mcp.json, .cursor/mcp.json, .vscode/mcp.json
2150
3153
  and print the Codex / Windsurf config
3154
+ --framework <name> react | angular | vue | web-components | html (default: detected from package.json)
2151
3155
  --tokens-css <file> Where to write the CSS variables (default src/brandrail.tokens.css); --no-tokens-css to skip
2152
3156
  --force Overwrite an existing brandrail.config.json
2153
3157
 
@@ -2171,16 +3175,16 @@ function parseArgs(argv) {
2171
3175
  const [cmd = "help", ...rest] = argv;
2172
3176
  const positional = [];
2173
3177
  const flags = {};
2174
- for (let i = 0; i < rest.length; i++) {
2175
- const a = rest[i];
3178
+ for (let i2 = 0; i2 < rest.length; i2++) {
3179
+ const a = rest[i2];
2176
3180
  if (a.startsWith("--no-")) flags[a.slice(5)] = false;
2177
3181
  else if (a.startsWith("--")) {
2178
3182
  const [k, v] = a.slice(2).split("=", 2);
2179
- const next = rest[i + 1];
3183
+ const next = rest[i2 + 1];
2180
3184
  if (v !== void 0) flags[k] = v;
2181
- else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width", "tokens-css"].includes(k)) {
3185
+ else if (next !== void 0 && !next.startsWith("-") && ["config", "format", "fail-below", "changed", "pages", "storage-state", "wait", "width", "tokens-css", "framework"].includes(k)) {
2182
3186
  flags[k] = next;
2183
- i++;
3187
+ i2++;
2184
3188
  } else flags[k] = true;
2185
3189
  } else positional.push(a);
2186
3190
  }
@@ -2280,7 +3284,7 @@ function writeRuleFiles(root, config, org, tokensCss) {
2280
3284
  if (file.endsWith(".mdc")) {
2281
3285
  write(file, () => `---
2282
3286
  description: Design system rules from Brandrail
2283
- globs: ["**/*.{tsx,jsx,vue,svelte,html,css,scss}"]
3287
+ globs: ["**/*.{tsx,jsx,ts,vue,svelte,html,css,scss,php,twig,hbs,mustache}"]
2284
3288
  alwaysApply: true
2285
3289
  ---
2286
3290
 
@@ -2300,8 +3304,8 @@ ${md}
2300
3304
  }
2301
3305
  return written;
2302
3306
  }
2303
- function writeMcpConfigs(root) {
2304
- const url = `${apiBase()}/api/mcp`;
3307
+ function writeMcpConfigs(root, framework) {
3308
+ const url = `${apiBase()}/api/mcp${framework === "react" ? "" : `?framework=${framework}`}`;
2305
3309
  for (const c of mcpClientConfigs(url)) {
2306
3310
  if (!c.projectFile) continue;
2307
3311
  const abs = join3(root, c.file);
@@ -2315,7 +3319,7 @@ function writeMcpConfigs(root) {
2315
3319
  for (const [k, v] of Object.entries(ours)) {
2316
3320
  if (Array.isArray(v)) {
2317
3321
  const prev = Array.isArray(existing[k]) ? existing[k] : [];
2318
- merged[k] = [...prev.filter((x) => !v.some((y) => y.id === x.id)), ...v];
3322
+ merged[k] = [...prev.filter((x2) => !v.some((y) => y.id === x2.id)), ...v];
2319
3323
  } else merged[k] = { ...existing[k] ?? {}, ...v };
2320
3324
  }
2321
3325
  } catch {
@@ -2327,7 +3331,7 @@ function writeMcpConfigs(root) {
2327
3331
  `);
2328
3332
  log(`\u2714 added brandrail to ${c.file} (${c.name})`);
2329
3333
  }
2330
- for (const c of mcpClientConfigs(url).filter((x) => !x.projectFile)) {
3334
+ for (const c of mcpClientConfigs(url).filter((x2) => !x2.projectFile)) {
2331
3335
  log(`
2332
3336
  ${c.name}: ${c.command ? `run
2333
3337
  ${c.command}
@@ -2336,6 +3340,21 @@ ${c.content.replace(/^/gm, " ")}`);
2336
3340
  }
2337
3341
  log("\nSet BRANDRAIL_KEY in your shell (an API key from Dashboard \u2192 Connect) before starting the AI tool.");
2338
3342
  }
3343
+ function projectFramework(cwd, config, flags) {
3344
+ const asked = flags.framework;
3345
+ if (typeof asked === "string") {
3346
+ if (!isFramework(asked)) throw new Error(`Unknown --framework "${asked}". Use one of: ${FRAMEWORKS.join(", ")}.`);
3347
+ return asked;
3348
+ }
3349
+ if (config.framework && isFramework(config.framework)) return config.framework;
3350
+ const pkg = join3(cwd, "package.json");
3351
+ if (!existsSync3(pkg)) return detectFramework(null);
3352
+ try {
3353
+ return detectFramework(JSON.parse(readFileSync2(pkg, "utf8")));
3354
+ } catch {
3355
+ return "react";
3356
+ }
3357
+ }
2339
3358
  async function cmdInit(cwd, flags) {
2340
3359
  const target = join3(cwd, CONFIG_FILE);
2341
3360
  let config = starterConfig;
@@ -2360,13 +3379,20 @@ async function cmdInit(cwd, flags) {
2360
3379
  `);
2361
3380
  log(`\u2714 wrote ${CONFIG_FILE}${org ? ` from ${org}'s Brand Kit` : " (starter tokens \u2014 edit them or use --from-api)"}`);
2362
3381
  }
3382
+ const framework = projectFramework(cwd, config, flags);
3383
+ if (config.framework !== framework) {
3384
+ config = { ...config, framework };
3385
+ writeFileSync(target, `${JSON.stringify(config, null, 2)}
3386
+ `);
3387
+ }
3388
+ log(`\u2714 framework: ${FRAMEWORK_LABEL[framework]}${flags.framework ? "" : " (detected; change with --framework)"}`);
2363
3389
  const css = flags["tokens-css"] === false ? void 0 : tokensCssPath(cwd, config, flags);
2364
3390
  if (css) {
2365
3391
  const kit = writeTokensCss(cwd, css, config, org);
2366
3392
  log(`\u2714 wrote ${css} (the Brand Kit as CSS variables${kit ? ", and @brandrail/kit themed from it" : ""} \u2014 import it once at your app entry)`);
2367
3393
  }
2368
3394
  if (flags.rules !== false) for (const f of writeRuleFiles(cwd, config, org, css)) log(`\u2714 updated ${f}`);
2369
- if (flags.mcp) writeMcpConfigs(cwd);
3395
+ if (flags.mcp) writeMcpConfigs(cwd, framework);
2370
3396
  log("\nNext: npx brandrail check");
2371
3397
  return 0;
2372
3398
  }
@@ -2374,7 +3400,7 @@ async function cmdScan(cwd, positional, flags) {
2374
3400
  const url = positional[0];
2375
3401
  if (!url) throw new Error("Usage: brandrail scan <url> [--pages 3] [--storage-state auth.json] [--upload]");
2376
3402
  const { config, source } = await loadConfig(cwd, flags);
2377
- const num = (k, d, max) => typeof flags[k] === "string" ? Math.min(Math.max(Number(flags[k]) || d, 0), max) : d;
3403
+ const num = (k, d, max2) => typeof flags[k] === "string" ? Math.min(Math.max(Number(flags[k]) || d, 0), max2) : d;
2378
3404
  log(`Scanning ${url} \u2026`);
2379
3405
  const { report, pages } = await scanPages(
2380
3406
  cwd,
@@ -2423,11 +3449,31 @@ Brand score ${report.score} is below ${threshold}.`);
2423
3449
  }
2424
3450
  return 0;
2425
3451
  }
3452
+ function readTokenInputs(cwd, inputs) {
3453
+ const files = [];
3454
+ const add = (name, text) => files.push({ name, data: JSON.parse(text) });
3455
+ const walk = (abs, rel) => {
3456
+ if (statSync2(abs).isDirectory()) {
3457
+ for (const e of readdirSync2(abs)) if (!e.startsWith(".")) walk(join3(abs, e), rel ? `${rel}/${e}` : e);
3458
+ } else if (/\.zip$/i.test(abs)) {
3459
+ const entries = unzipSync(new Uint8Array(readFileSync2(abs)), { filter: (e) => /\.json$/i.test(e.name) && !/(^|\/)(__MACOSX|\.)/.test(e.name) });
3460
+ for (const [name, bytes] of Object.entries(entries)) add(name, strFromU8(bytes));
3461
+ } else if (/\.json$/i.test(abs)) {
3462
+ add(rel || basename(abs), readFileSync2(abs, "utf8"));
3463
+ }
3464
+ };
3465
+ for (const input of inputs) {
3466
+ const abs = resolve3(cwd, input);
3467
+ walk(abs, statSync2(abs).isDirectory() ? "" : basename(abs));
3468
+ }
3469
+ return files;
3470
+ }
2426
3471
  function cmdImport(cwd, positional, flags) {
2427
- const file = positional[0];
2428
- if (!file) throw new Error("Usage: brandrail import <tokens.json> [--replace] [--primitives]");
2429
- const raw = JSON.parse(readFileSync2(resolve3(cwd, file), "utf8"));
2430
- const res = importTokens(raw, { includePrimitives: !!flags.primitives });
3472
+ if (!positional.length) throw new Error("Usage: brandrail import <tokens.json | folder | export.zip> [more files\u2026] [--replace] [--primitives]");
3473
+ const files = readTokenInputs(cwd, positional);
3474
+ if (!files.length) throw new Error("No .json token files found.");
3475
+ const res = importTokenFiles(files, { includePrimitives: !!flags.primitives });
3476
+ const file = files.length === 1 ? positional[0] : `${files.length} files`;
2431
3477
  const path = (typeof flags.config === "string" ? resolve3(cwd, flags.config) : findConfig(cwd)) ?? join3(cwd, CONFIG_FILE);
2432
3478
  const config = existsSync3(path) ? readConfig(path) : { ...starterConfig, tokens: {} };
2433
3479
  const next = flags.replace ? res.tokens : mergeTokens(config.tokens, res.tokens);
@@ -2436,6 +3482,7 @@ function cmdImport(cwd, positional, flags) {
2436
3482
  `);
2437
3483
  log(`\u2714 ${res.count} tokens from ${file} (${res.format})${res.primitives ? `, ${res.primitives} palette primitives left out (--primitives to keep)` : ""}`);
2438
3484
  for (const n of res.notes) log(` \xB7 ${n}`);
3485
+ for (const i2 of auditBrandKit(next)) log(` ${i2.severity === "error" ? "\u2716" : "!"} ${i2.message}`);
2439
3486
  log(`\u2714 ${relative2(cwd, path) || CONFIG_FILE}: ${d.changed.length} changed, ${d.added.length} added, ${d.removed.length} removed`);
2440
3487
  log("\nNext: npx brandrail init (to refresh AI rule files), then npx brandrail check");
2441
3488
  return 0;
@@ -2471,8 +3518,8 @@ async function main(argv = process.argv.slice(2)) {
2471
3518
  }
2472
3519
  main().then(
2473
3520
  (code) => process.exit(code),
2474
- (err) => {
2475
- log(`brandrail: ${err.message}`);
3521
+ (err2) => {
3522
+ log(`brandrail: ${err2.message}`);
2476
3523
  process.exit(2);
2477
3524
  }
2478
3525
  );