brandrail 0.8.0 → 0.9.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 +1361 -1235
  2. package/package.json +3 -3
@@ -7013,10 +7013,10 @@ var require_formats = __commonJS({
7013
7013
  function isLeapYear(year) {
7014
7014
  return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
7015
7015
  }
7016
- var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
7016
+ var DATE2 = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
7017
7017
  var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
7018
7018
  function date5(str) {
7019
- const matches2 = DATE.exec(str);
7019
+ const matches2 = DATE2.exec(str);
7020
7020
  if (!matches2)
7021
7021
  return false;
7022
7022
  const year = +matches2[1];
@@ -7810,6 +7810,634 @@ function contrastRatio(a, b) {
7810
7810
  return (l1 + 0.05) / (l2 + 0.05);
7811
7811
  }
7812
7812
 
7813
+ // ../engine/src/tokens.ts
7814
+ var EXT = "dev.brandrail";
7815
+ function slug(s) {
7816
+ return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Za-z])(\d)/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
7817
+ }
7818
+ var segs = (name) => name.split(/[/$.]+/).map(slug).filter(Boolean);
7819
+ var stripPrefix = (s, ...prefixes) => {
7820
+ for (const p of prefixes) if (s === p) return "";
7821
+ for (const p of prefixes) if (s.startsWith(`${p}-`)) return s.slice(p.length + 1);
7822
+ return s;
7823
+ };
7824
+ var WEIGHTS = {
7825
+ thin: "100",
7826
+ hairline: "100",
7827
+ "extra-light": "200",
7828
+ "ultra-light": "200",
7829
+ light: "300",
7830
+ regular: "400",
7831
+ regulare: "400",
7832
+ normal: "400",
7833
+ book: "400",
7834
+ medium: "500",
7835
+ "semi-bold": "600",
7836
+ semibold: "600",
7837
+ "demi-bold": "600",
7838
+ bold: "700",
7839
+ "extra-bold": "800",
7840
+ "ultra-bold": "800",
7841
+ black: "900",
7842
+ heavy: "900"
7843
+ };
7844
+ function fontWeight(v) {
7845
+ if (typeof v === "number") return String(v);
7846
+ return WEIGHTS[slug(v)] ?? (/^\d{3}$/.test(v.trim()) ? v.trim() : v);
7847
+ }
7848
+ function colorValue(v) {
7849
+ if (v && typeof v === "object" && ("components" in v || "hex" in v)) {
7850
+ const c = v;
7851
+ const alpha = typeof c.alpha === "number" ? c.alpha : 1;
7852
+ const withAlpha = (hex4) => alpha < 1 ? `${hex4.slice(0, 7)}${Math.round(alpha * 255).toString(16).padStart(2, "0")}` : hex4.slice(0, 7);
7853
+ if (typeof c.hex === "string" && /^#[0-9a-f]{6}/i.test(c.hex)) return withAlpha(c.hex.toLowerCase());
7854
+ const comps = c.components?.map((x2) => typeof x2 === "number" ? x2 : 0);
7855
+ if (comps?.length === 3 && (!c.colorSpace || /srgb|display-p3/i.test(c.colorSpace))) {
7856
+ return withAlpha(toHex({ r: comps[0] * 255, g: comps[1] * 255, b: comps[2] * 255, a: 1 }));
7857
+ }
7858
+ return null;
7859
+ }
7860
+ if (v && typeof v === "object" && "r" in v) {
7861
+ const c = v;
7862
+ const hex4 = toHex({ r: c.r * 255, g: c.g * 255, b: c.b * 255, a: 1 });
7863
+ const a = c.a ?? 1;
7864
+ return a < 1 ? `${hex4}${Math.round(a * 255).toString(16).padStart(2, "0")}` : hex4;
7865
+ }
7866
+ if (typeof v !== "string") return null;
7867
+ const s = v.trim();
7868
+ if (/^#[0-9a-f]{3,8}$/i.test(s)) {
7869
+ const h = s.slice(1).toLowerCase();
7870
+ return h.length === 3 || h.length === 4 ? `#${[...h].map((c) => c + c).join("")}` : `#${h}`;
7871
+ }
7872
+ const rgb = parseColor(s);
7873
+ if (!rgb) return null;
7874
+ const hex3 = toHex(rgb);
7875
+ return rgb.a < 1 ? `${hex3}${Math.round(rgb.a * 255).toString(16).padStart(2, "0")}` : hex3;
7876
+ }
7877
+ var dim = (v) => {
7878
+ if (v && typeof v === "object" && "value" in v) {
7879
+ const d = v;
7880
+ const n = typeof d.value === "number" ? d.value : parseFloat(d.value);
7881
+ return d.unit === "rem" ? `${Math.round(n * 16 * 100) / 100}px` : `${Math.round(n * 100) / 100}px`;
7882
+ }
7883
+ return typeof v === "number" ? `${Math.round(v * 100) / 100}px` : String(v).trim();
7884
+ };
7885
+ function classify(name, kind, value, opts = {}) {
7886
+ const parts = segs(name);
7887
+ const hay = slug(`${opts.context ?? ""} ${name}`);
7888
+ const scopes = opts.scopes ?? [];
7889
+ const last = parts[parts.length - 1] ?? "";
7890
+ const rest = parts.slice(0, -1);
7891
+ if (kind === "boolean") return null;
7892
+ if (kind === "color") {
7893
+ const v2 = colorValue(value);
7894
+ if (!v2) return null;
7895
+ let p = parts[0] === "color" || parts[0] === "colors" ? parts.slice(1) : parts;
7896
+ if (p.length === 1) p = [stripPrefix(p[0], "colors", "color") || p[0]];
7897
+ const tail = p[p.length - 1];
7898
+ const m = tail.match(/^(.*[a-z])-(\d+)$/);
7899
+ if (m && p.length <= 2) p = [...p.slice(0, -1), m[1], m[2]];
7900
+ return { path: ["color", ...p].join("."), type: "color", value: v2 };
7901
+ }
7902
+ if (kind === "string") {
7903
+ const s = String(value);
7904
+ if (scopes.includes("FONT_FAMILY") || /font-family|typeface|font-name/.test(hay) || last === "font") {
7905
+ const n = stripPrefix(last, "font-family", "font", "family") || "sans";
7906
+ return { path: `font.family.${/mono|code/.test(s.toLowerCase()) && n === "sans" ? "mono" : n}`, type: "fontFamily", value: s };
7907
+ }
7908
+ if (scopes.includes("FONT_STYLE") || scopes.includes("FONT_WEIGHT") || /font-weight|weight/.test(hay)) {
7909
+ return { path: `font.weight.${stripPrefix(last, "font-weight", "weight") || last}`, type: "fontWeight", value: fontWeight(s) };
7910
+ }
7911
+ const c = colorValue(s);
7912
+ if (c && /^#|^rgb|^hsl/i.test(s.trim())) return classify(name, "color", s, opts);
7913
+ return null;
7914
+ }
7915
+ const v = dim(value);
7916
+ const has = (...scope) => scope.some((x2) => scopes.includes(x2));
7917
+ const nm = (...prefixes) => stripPrefix(last, ...prefixes) || last;
7918
+ if (has("CORNER_RADIUS") || /radius|(^|-)rad(-|$)|corner|rounded/.test(hay)) return { path: `radius.${nm("radius", "rad", "corner", "rounded")}`, type: "dimension", value: v };
7919
+ if (has("FONT_SIZE") || /font-size|text-size/.test(hay)) return { path: `font.size.${nm("font-size", "text-size", "size")}`, type: "dimension", value: v };
7920
+ if (has("LINE_HEIGHT") || /line-height|leading/.test(hay)) return { path: `font.line-height.${nm("line-height", "leading")}`, type: "dimension", value: v };
7921
+ if (has("LETTER_SPACING") || /letter-spacing|tracking/.test(hay)) return { path: `font.letter-spacing.${nm("letter-spacing", "tracking")}`, type: "dimension", value: v };
7922
+ if (has("FONT_WEIGHT") || /font-weight/.test(hay)) return { path: `font.weight.${nm("font-weight", "weight")}`, type: "fontWeight", value: String(value) };
7923
+ if (/icon/.test(last)) return { path: `size.icon.${nm("icon")}`, type: "dimension", value: v };
7924
+ if (has("GAP") || /spac|gap|padding|margin|inset|(^|-)space(-|$)/.test(hay)) return { path: `space.${nm("spacing", "space", "gap")}`, type: "dimension", value: v };
7925
+ if (has("STROKE_FLOAT") || /stroke|border-width/.test(hay)) return { path: `border.width.${nm("border-width", "stroke")}`, type: "dimension", value: v };
7926
+ if (has("WIDTH_HEIGHT") || /size|width|height/.test(hay)) return { path: ["size", ...rest.filter((x2) => x2 !== "size"), last].join("."), type: "dimension", value: v };
7927
+ return { path: ["number", ...parts].join("."), type: "number", value: String(value) };
7928
+ }
7929
+ function rawLeaves(node2, prefix, out, studio, inherited) {
7930
+ if (!node2 || typeof node2 !== "object" || Array.isArray(node2)) return;
7931
+ const o = node2;
7932
+ const groupType = o.$type ?? inherited;
7933
+ const isLeaf = "$value" in o || studio && "value" in o && (typeof o.value !== "object" || o.value === null || "type" in o);
7934
+ if (isLeaf) {
7935
+ const ext = o.$extensions?.[EXT];
7936
+ out.push({
7937
+ path: prefix,
7938
+ type: o.$type ?? o.type ?? groupType,
7939
+ value: "$value" in o ? o.$value : o.value,
7940
+ description: o.$description ?? o.description,
7941
+ dark: ext?.modes?.dark
7942
+ });
7943
+ return;
7944
+ }
7945
+ for (const [k, v] of Object.entries(o)) if (!k.startsWith("$")) rawLeaves(v, prefix ? `${prefix}.${k}` : k, out, studio, groupType);
7946
+ }
7947
+ var REF = /^\{([^}]+)\}$/;
7948
+ function resolveRefs(raws) {
7949
+ const byPath = new Map(raws.map((r) => [r.path, r]));
7950
+ const deref = (v, mode, depth = 0) => {
7951
+ if (typeof v !== "string" || depth > 10) return v;
7952
+ const m = v.trim().match(REF);
7953
+ if (!m) return v.replace(/\{([^}]+)\}/g, (all, p) => String(byPath.get(p)?.value ?? all));
7954
+ const target = byPath.get(m[1]);
7955
+ if (!target) return v;
7956
+ return deref(mode === "dark" && target.dark !== void 0 ? target.dark : target.value, mode, depth + 1);
7957
+ };
7958
+ return raws.map((r) => ({ ...r, value: deref(r.value, "light"), dark: r.dark !== void 0 ? deref(r.dark, "dark") : void 0 }));
7959
+ }
7960
+ var TYPE_KIND = {
7961
+ color: "color",
7962
+ dimension: "number",
7963
+ spacing: "number",
7964
+ sizing: "number",
7965
+ borderradius: "number",
7966
+ fontsizes: "number",
7967
+ fontsize: "number",
7968
+ lineheights: "number",
7969
+ lineheight: "number",
7970
+ letterspacing: "number",
7971
+ number: "number",
7972
+ fontfamilies: "string",
7973
+ fontfamily: "string",
7974
+ fontweights: "string",
7975
+ fontweight: "string"
7976
+ };
7977
+ function flattenTokens(doc) {
7978
+ const raws = resolveRefs(collectRaw(doc).raws);
7979
+ const out = [];
7980
+ for (const r of raws) {
7981
+ const t = (r.type ?? "").toLowerCase().replace(/[^a-z]/g, "");
7982
+ if (t === "color" || !t && r.path.startsWith("color")) {
7983
+ const v = colorValue(r.value);
7984
+ if (!v) continue;
7985
+ const d = r.dark !== void 0 ? colorValue(r.dark) : null;
7986
+ out.push({ path: r.path, type: "color", value: v, ...d ? { dark: d } : {}, ...r.description ? { description: r.description } : {} });
7987
+ } else if (typeof r.value === "string" || typeof r.value === "number" || r.value && typeof r.value === "object" && "unit" in r.value) {
7988
+ 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";
7989
+ out.push({ path: r.path, type, value: type === "dimension" ? dim(r.value) : String(r.value), ...r.description ? { description: r.description } : {} });
7990
+ }
7991
+ }
7992
+ return out;
7993
+ }
7994
+ function collectRaw(doc) {
7995
+ const o = doc ?? {};
7996
+ const studio = !hasDollarValue(o) && hasStudioValue(o);
7997
+ const order = o.$metadata?.tokenSetOrder;
7998
+ if (!studio) {
7999
+ const raws = [];
8000
+ rawLeaves(o, "", raws, false);
8001
+ return { raws, studio };
8002
+ }
8003
+ const setNames = (order ?? Object.keys(o).filter((k) => !k.startsWith("$"))).filter((k) => o[k] && typeof o[k] === "object");
8004
+ const looksLikeSets = setNames.length > 0 && setNames.every((k) => !hasLeafAtTop(o[k]));
8005
+ if (!looksLikeSets) {
8006
+ const raws = [];
8007
+ rawLeaves(o, "", raws, true);
8008
+ return { raws, studio };
8009
+ }
8010
+ const merged = /* @__PURE__ */ new Map();
8011
+ const darkSets = setNames.filter((n) => /dark/i.test(n));
8012
+ for (const set2 of setNames.filter((n) => !/dark/i.test(n))) {
8013
+ const raws = [];
8014
+ rawLeaves(o[set2], "", raws, true);
8015
+ for (const r of raws) merged.set(r.path, r);
8016
+ }
8017
+ for (const set2 of darkSets) {
8018
+ const raws = [];
8019
+ rawLeaves(o[set2], "", raws, true);
8020
+ for (const r of raws) {
8021
+ const base = merged.get(r.path);
8022
+ if (base) base.dark = r.value;
8023
+ else merged.set(r.path, r);
8024
+ }
8025
+ }
8026
+ return { raws: [...merged.values()], studio };
8027
+ }
8028
+ function hasDollarValue(node2, depth = 0) {
8029
+ if (!node2 || typeof node2 !== "object" || depth > 12) return false;
8030
+ if ("$value" in node2) return true;
8031
+ return Object.values(node2).some((v) => hasDollarValue(v, depth + 1));
8032
+ }
8033
+ function hasStudioValue(node2, depth = 0) {
8034
+ if (!node2 || typeof node2 !== "object" || depth > 12) return false;
8035
+ const o = node2;
8036
+ if ("value" in o && "type" in o) return true;
8037
+ return Object.values(o).some((v) => hasStudioValue(v, depth + 1));
8038
+ }
8039
+ function hasLeafAtTop(node2) {
8040
+ return !!node2 && typeof node2 === "object" && ("value" in node2 || "$value" in node2);
8041
+ }
8042
+ function buildDtcg(tokens) {
8043
+ const tree = {};
8044
+ for (const t of tokens) {
8045
+ const parts = t.path.split(".");
8046
+ let node2 = tree;
8047
+ parts.forEach((p, i2) => {
8048
+ if (i2 === parts.length - 1) {
8049
+ node2[p] = {
8050
+ $type: t.type,
8051
+ $value: t.value,
8052
+ ...t.description ? { $description: t.description } : {},
8053
+ ...t.dark && t.dark !== t.value ? { $extensions: { [EXT]: { modes: { dark: t.dark } } } } : {}
8054
+ };
8055
+ } else {
8056
+ const next = node2[p];
8057
+ if (next && typeof next === "object" && "$value" in next) node2[p] = { default: next };
8058
+ node2 = node2[p] ??= {};
8059
+ }
8060
+ });
8061
+ }
8062
+ return tree;
8063
+ }
8064
+ var isAlias = (v) => !!v && typeof v === "object" && v.type === "VARIABLE_ALIAS";
8065
+ var DARK = /dark|night/i;
8066
+ var LIGHT = /light|default|day|base/i;
8067
+ function finish(format, all, notes, opts) {
8068
+ const PALETTE2 = /^(color\.[a-z-]+)\.\d{1,3}$/;
8069
+ const usedFamilies = new Set(all.filter((t) => t.referenced).map((t) => t.path.match(PALETTE2)?.[1]).filter(Boolean));
8070
+ if (usedFamilies.size) {
8071
+ const steps = /* @__PURE__ */ new Map();
8072
+ for (const t of all) {
8073
+ const f = t.path.match(PALETTE2)?.[1];
8074
+ if (f) steps.set(f, (steps.get(f) ?? 0) + 1);
8075
+ }
8076
+ for (const t of all) {
8077
+ const f = t.path.match(PALETTE2)?.[1];
8078
+ if (f && (usedFamilies.has(f) || (steps.get(f) ?? 0) >= 3)) t.referenced = true;
8079
+ }
8080
+ }
8081
+ const hasSemantic = all.some((t) => !t.referenced) && all.some((t) => t.referenced);
8082
+ const keep = opts.includePrimitives || !hasSemantic ? all : all.filter((t) => !t.referenced);
8083
+ const primitives = hasSemantic ? all.filter((t) => t.referenced).length : 0;
8084
+ const seen = /* @__PURE__ */ new Set();
8085
+ const tokens = [];
8086
+ for (const { referenced: _r, ...t } of keep) {
8087
+ if (seen.has(t.path)) continue;
8088
+ seen.add(t.path);
8089
+ tokens.push(t);
8090
+ }
8091
+ tokens.sort((a, b) => a.path.localeCompare(b.path, "en", { numeric: true }));
8092
+ return { format, tokens: buildDtcg(tokens), count: tokens.length, primitives, notes };
8093
+ }
8094
+ function fromFigmaVariables(meta3, opts = {}) {
8095
+ const vars = meta3.variables ?? {};
8096
+ const cols = meta3.variableCollections ?? {};
8097
+ const notes = [];
8098
+ const referenced = /* @__PURE__ */ new Set();
8099
+ const modeFor = (col, want, likeName) => {
8100
+ if (likeName) {
8101
+ const same = col.modes.find((m) => m.name.toLowerCase() === likeName.toLowerCase());
8102
+ if (same) return same.modeId;
8103
+ }
8104
+ if (want === "dark") return col.modes.find((m) => DARK.test(m.name))?.modeId;
8105
+ return col.defaultModeId ?? col.modes[0]?.modeId;
8106
+ };
8107
+ const resolve4 = (v, want, modeName, depth = 0) => {
8108
+ const col = cols[v.variableCollectionId];
8109
+ if (!col) return void 0;
8110
+ const modeId = modeFor(col, want, modeName) ?? col.defaultModeId;
8111
+ const raw = v.valuesByMode[modeId] ?? Object.values(v.valuesByMode)[0];
8112
+ if (!isAlias(raw)) return raw;
8113
+ referenced.add(raw.id);
8114
+ const target = vars[raw.id];
8115
+ if (!target || depth > 10) return void 0;
8116
+ const name = col.modes.find((m) => m.modeId === modeId)?.name;
8117
+ return resolve4(target, want, name, depth + 1);
8118
+ };
8119
+ for (const v of Object.values(vars)) for (const val of Object.values(v.valuesByMode)) if (isAlias(val)) referenced.add(val.id);
8120
+ for (const col of Object.values(cols)) {
8121
+ if (col.remote) continue;
8122
+ const dark = col.modes.find((m) => DARK.test(m.name));
8123
+ const others = col.modes.filter((m) => m.modeId !== col.defaultModeId && m !== dark);
8124
+ if (others.length) {
8125
+ const def = col.modes.find((m) => m.modeId === col.defaultModeId)?.name;
8126
+ notes.push(`\u201C${col.name}\u201D has modes ${col.modes.map((m) => m.name).join(", ")}; used \u201C${def}\u201D${dark ? ` (and \u201C${dark.name}\u201D as dark)` : ""}.`);
8127
+ }
8128
+ }
8129
+ const out = [];
8130
+ let hidden = 0;
8131
+ for (const v of Object.values(vars)) {
8132
+ const col = cols[v.variableCollectionId];
8133
+ if (!col || v.remote || col.remote) continue;
8134
+ if (v.hiddenFromPublishing) {
8135
+ hidden++;
8136
+ continue;
8137
+ }
8138
+ const kind = v.resolvedType === "COLOR" ? "color" : v.resolvedType === "FLOAT" ? "number" : v.resolvedType === "STRING" ? "string" : "boolean";
8139
+ const value = resolve4(v, "default");
8140
+ if (value === void 0) continue;
8141
+ const c = classify(v.name, kind, value, { scopes: v.scopes?.filter((s) => s !== "ALL_SCOPES"), context: col.name });
8142
+ if (!c) continue;
8143
+ let darkValue;
8144
+ if (c.type === "color") {
8145
+ const hasDarkSomewhere = Object.values(cols).some((x2) => x2.modes.some((m) => DARK.test(m.name)));
8146
+ if (hasDarkSomewhere) {
8147
+ const d = colorValue(resolve4(v, "dark"));
8148
+ if (d && d !== c.value) darkValue = d;
8149
+ }
8150
+ }
8151
+ out.push({ ...c, ...darkValue ? { dark: darkValue } : {}, ...v.description ? { description: v.description } : {}, referenced: referenced.has(v.id) });
8152
+ }
8153
+ if (hidden) notes.push(`${hidden} variable${hidden === 1 ? "" : "s"} hidden from publishing were skipped.`);
8154
+ return finish("figma-variables", out, notes, opts);
8155
+ }
8156
+ function fromFigmaStyles(styles, opts = {}) {
8157
+ const out = [];
8158
+ const families = /* @__PURE__ */ new Map();
8159
+ let skipped = 0;
8160
+ for (const s of styles) {
8161
+ if (s.styleType === "FILL") {
8162
+ const fill = s.node?.fills?.find((f) => f.type === "SOLID" && f.visible !== false && f.color);
8163
+ if (!fill?.color) {
8164
+ skipped++;
8165
+ continue;
8166
+ }
8167
+ const c = classify(s.name, "color", { ...fill.color, a: (fill.color.a ?? 1) * (fill.opacity ?? 1) });
8168
+ if (c) out.push({ ...c, ...s.description ? { description: s.description } : {} });
8169
+ } else if (s.styleType === "TEXT" && s.node?.style) {
8170
+ const st = s.node.style;
8171
+ const name = segs(s.name).join("-");
8172
+ if (st.fontSize) out.push({ path: `font.size.${name}`, type: "dimension", value: dim(st.fontSize) });
8173
+ if (st.lineHeightPx) out.push({ path: `font.line-height.${name}`, type: "dimension", value: dim(Math.round(st.lineHeightPx)) });
8174
+ if (st.fontWeight) out.push({ path: `font.weight.${name}`, type: "fontWeight", value: String(st.fontWeight) });
8175
+ if (st.fontFamily) families.set(st.fontFamily, (families.get(st.fontFamily) ?? 0) + 1);
8176
+ }
8177
+ }
8178
+ const fams = [...families.entries()].sort((a, b) => b[1] - a[1]);
8179
+ fams.forEach(([f], i2) => out.push({ path: `font.family.${i2 === 0 ? "sans" : /mono|code/i.test(f) ? "mono" : slug(f)}`, type: "fontFamily", value: f }));
8180
+ const notes = skipped ? [`${skipped} fill style${skipped === 1 ? "" : "s"} (gradients or images) were skipped.`] : [];
8181
+ return finish("figma-styles", out, notes, opts);
8182
+ }
8183
+ function plainLeaves(node2, prefix, out) {
8184
+ if (node2 === null || node2 === void 0) return;
8185
+ if (typeof node2 !== "object" || typeof node2 === "object" && "r" in node2 && "g" in node2) {
8186
+ out.push({ key: prefix, value: node2 });
8187
+ return;
8188
+ }
8189
+ for (const [k, v] of Object.entries(node2)) plainLeaves(v, [...prefix, k], out);
8190
+ }
8191
+ function fromModeObject(obj, opts = {}) {
8192
+ const notes = [];
8193
+ const modes = Object.entries(obj).filter(([, v]) => v && typeof v === "object");
8194
+ const leafSets = modes.map(([name, v]) => {
8195
+ const l = [];
8196
+ plainLeaves(v, [], l);
8197
+ return { name, leaves: l, keys: new Set(l.map((x2) => x2.key.join("/"))) };
8198
+ });
8199
+ const groups = [];
8200
+ for (const m of leafSets) {
8201
+ const g = groups.find((grp) => {
8202
+ const a = grp[0].keys;
8203
+ let same = 0;
8204
+ for (const k of m.keys) if (a.has(k)) same++;
8205
+ return same / Math.max(a.size, m.keys.size) >= 0.8;
8206
+ });
8207
+ if (g) g.push(m);
8208
+ else groups.push([m]);
8209
+ }
8210
+ const byLeaf = /* @__PURE__ */ new Map();
8211
+ groups.forEach((grp, gi) => {
8212
+ for (const m of grp) for (const l of m.leaves) {
8213
+ const k = l.key[l.key.length - 1];
8214
+ const e = byLeaf.get(k) ?? { group: gi, values: /* @__PURE__ */ new Map() };
8215
+ e.values.set(m.name, l.value);
8216
+ byLeaf.set(k, e);
8217
+ }
8218
+ });
8219
+ const pick2 = (grp, want) => want === "dark" ? grp.find((m) => DARK.test(m.name)) : grp.find((m) => LIGHT.test(m.name) && !DARK.test(m.name)) ?? grp.find((m) => !DARK.test(m.name)) ?? grp[0];
8220
+ const referenced = /* @__PURE__ */ new Set();
8221
+ for (const m of leafSets) for (const l of m.leaves) if (typeof l.value === "string" && byLeaf.has(l.value)) referenced.add(l.value);
8222
+ const deref = (v, want, depth = 0) => {
8223
+ if (typeof v !== "string" || depth > 10) return v;
8224
+ const ref = byLeaf.get(v) ?? byLeaf.get(v.replace(REF, "$1"));
8225
+ if (!ref) return v;
8226
+ referenced.add(v);
8227
+ const grp = groups[ref.group];
8228
+ const mode = pick2(grp, want) ?? pick2(grp, "default");
8229
+ return deref(ref.values.get(mode.name) ?? [...ref.values.values()][0], want, depth + 1);
8230
+ };
8231
+ const out = [];
8232
+ const pending = [];
8233
+ for (const grp of groups) {
8234
+ const def = pick2(grp, "default");
8235
+ const dark = pick2(grp, "dark");
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)` : ""}.`);
8237
+ for (const l of def.leaves) {
8238
+ const key = l.key[l.key.length - 1];
8239
+ const container = l.key.length > 1 ? l.key[l.key.length - 2].toLowerCase() : "";
8240
+ const value = deref(l.value, "default");
8241
+ const kind = container === "color" || colorValue(value) && /^#|^rgb|^hsl/i.test(String(value)) ? "color" : container === "string" || typeof value === "string" && !/^-?[\d.]+(px|rem|%)?$/.test(value) ? "string" : "number";
8242
+ const c = classify(key, kind, kind === "number" && typeof value === "string" && /px$/.test(value) ? parseFloat(value) : value, { context: def.name });
8243
+ if (!c) continue;
8244
+ let d;
8245
+ if (c.type === "color") {
8246
+ const darkLeaf = dark && dark !== def ? dark.leaves.find((x2) => x2.key.join("/") === l.key.join("/")) : void 0;
8247
+ const dv = colorValue(deref(darkLeaf ? darkLeaf.value : l.value, "dark"));
8248
+ if (dv && dv !== c.value) d = dv;
8249
+ }
8250
+ pending.push({ key, t: { ...c, ...d ? { dark: d } : {} } });
8251
+ }
8252
+ }
8253
+ for (const p of pending) out.push({ ...p.t, referenced: referenced.has(p.key) });
8254
+ return finish("mode-object", out, notes, opts);
8255
+ }
8256
+ var figmaExt = (node2, key) => {
8257
+ const ext = node2.$extensions;
8258
+ if (!ext) return void 0;
8259
+ if (`com.figma.${key}` in ext) return ext[`com.figma.${key}`];
8260
+ const nested = ext["com.figma"];
8261
+ return nested?.[key];
8262
+ };
8263
+ function fileLeaves(node2, prefix, out, inherited) {
8264
+ if (!node2 || typeof node2 !== "object" || Array.isArray(node2)) return;
8265
+ const o = node2;
8266
+ const groupType = o.$type ?? inherited;
8267
+ if ("$value" in o) {
8268
+ const alias = figmaExt(o, "aliasData");
8269
+ const scopes = figmaExt(o, "scopes");
8270
+ out.push({
8271
+ path: prefix,
8272
+ type: o.$type ?? groupType,
8273
+ value: o.$value,
8274
+ description: o.$description,
8275
+ scopes: Array.isArray(scopes) ? scopes : void 0,
8276
+ aliasTarget: alias?.targetVariableName?.replace(/\//g, "."),
8277
+ hidden: figmaExt(o, "hiddenFromPublishing") === true || void 0
8278
+ });
8279
+ return;
8280
+ }
8281
+ for (const [k, v] of Object.entries(o)) {
8282
+ if (k.startsWith("$")) continue;
8283
+ fileLeaves(v, prefix ? `${prefix}.${k}` : k, out, groupType);
8284
+ }
8285
+ }
8286
+ var hasFigmaExt = (node2, depth = 0) => {
8287
+ if (!node2 || typeof node2 !== "object" || depth > 12) return false;
8288
+ const ext = node2.$extensions;
8289
+ if (ext && Object.keys(ext).some((k) => k.startsWith("com.figma"))) return true;
8290
+ return Object.values(node2).some((v) => hasFigmaExt(v, depth + 1));
8291
+ };
8292
+ function modeOf(file2) {
8293
+ const root = file2.data ?? {};
8294
+ const parts = file2.name.replace(/\\/g, "/").split("/").filter(Boolean);
8295
+ const base = (parts.pop() ?? "tokens").replace(/(\.tokens)?\.json$/i, "");
8296
+ const named = figmaExt(root, "modeName");
8297
+ const split = base.match(/^(.+?)[\s._-]+(light|dark|night|day|default|mode[\s_-]*\d+)$/i);
8298
+ return {
8299
+ collection: parts.join("/") || (split ? split[1] : ""),
8300
+ mode: typeof named === "string" ? named : split ? split[2] : base
8301
+ };
8302
+ }
8303
+ function importTokenFiles(files, opts = {}) {
8304
+ const parsed = files.filter((f) => f.data && typeof f.data === "object");
8305
+ if (!parsed.length) throw new Error("No JSON token files found.");
8306
+ const figma = parsed.some((f) => hasFigmaExt(f.data));
8307
+ if (parsed.length === 1 && !figma) return importTokens(parsed[0].data, opts);
8308
+ const notes = [];
8309
+ const withMode = parsed.map((f) => ({ ...f, ...modeOf(f) }));
8310
+ const byCollection = /* @__PURE__ */ new Map();
8311
+ for (const f of withMode) byCollection.set(f.collection, [...byCollection.get(f.collection) ?? [], f]);
8312
+ const light = /* @__PURE__ */ new Map();
8313
+ const darkOverrides = /* @__PURE__ */ new Map();
8314
+ for (const [collection, list] of byCollection) {
8315
+ const dark = list.find((f) => DARK.test(f.mode));
8316
+ const def = list.find((f) => LIGHT.test(f.mode) && !DARK.test(f.mode)) ?? list.find((f) => !DARK.test(f.mode)) ?? list[0];
8317
+ if (list.length > 1) {
8318
+ const label = collection ? `\u201C${collection}\u201D` : "The files";
8319
+ 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)` : ""}.`);
8320
+ }
8321
+ const read2 = (f, into) => {
8322
+ const leaves3 = [];
8323
+ fileLeaves(f.data, "", leaves3);
8324
+ for (const l of leaves3) into.set(l.path, { path: l.path, type: l.type, value: l.value, description: l.description, leaf: l });
8325
+ };
8326
+ read2(def, light);
8327
+ if (dark && dark !== def) read2(dark, darkOverrides);
8328
+ }
8329
+ const lightRaws = resolveRefs([...light.values()]);
8330
+ const darkRaws = resolveRefs([...light.values()].map((r) => darkOverrides.get(r.path) ?? r));
8331
+ const darkByPath = new Map(darkRaws.map((r) => [r.path, r.value]));
8332
+ const referenced = /* @__PURE__ */ new Set();
8333
+ for (const r of [...light.values(), ...darkOverrides.values()]) {
8334
+ if (r.leaf.aliasTarget) referenced.add(r.leaf.aliasTarget);
8335
+ const m = typeof r.value === "string" ? r.value.trim().match(REF) : null;
8336
+ if (m) referenced.add(m[1]);
8337
+ }
8338
+ const out = [];
8339
+ let hidden = 0;
8340
+ for (const r of lightRaws) {
8341
+ const leaf = light.get(r.path).leaf;
8342
+ if (leaf.hidden) {
8343
+ hidden++;
8344
+ continue;
8345
+ }
8346
+ const t = (r.type ?? "").toLowerCase();
8347
+ 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";
8348
+ let tok;
8349
+ if (figma) {
8350
+ const value = kind === "number" && r.value && typeof r.value === "object" ? parseFloat(dim(r.value)) : r.value;
8351
+ tok = classify(r.path.replace(/\./g, "/"), kind, value, { scopes: leaf.scopes?.filter((x2) => x2 !== "ALL_SCOPES") });
8352
+ } else {
8353
+ const flat = flattenTokens({ x: { $type: r.type, $value: r.value } })[0];
8354
+ tok = flat ? { path: flat.type === "color" && !r.path.startsWith("color.") ? `color.${r.path}` : r.path, type: flat.type, value: flat.value } : null;
8355
+ }
8356
+ if (!tok) continue;
8357
+ let darkValue;
8358
+ if (tok.type === "color" && darkOverrides.size) {
8359
+ const d = colorValue(darkByPath.get(r.path));
8360
+ if (d && d !== tok.value) darkValue = d;
8361
+ }
8362
+ out.push({ ...tok, ...darkValue ? { dark: darkValue } : {}, ...r.description ? { description: r.description } : {}, referenced: referenced.has(r.path) });
8363
+ }
8364
+ if (hidden) notes.push(`${hidden} variable${hidden === 1 ? "" : "s"} hidden from publishing were skipped.`);
8365
+ return finish(figma ? "figma-export" : "dtcg", out, notes, opts);
8366
+ }
8367
+ function importTokens(input2, opts = {}) {
8368
+ if (!input2 || typeof input2 !== "object") throw new Error("Expected a JSON object.");
8369
+ const o = input2;
8370
+ if (Array.isArray(o.$files)) return importTokenFiles(o.$files, opts);
8371
+ const meta3 = o.meta ?? o;
8372
+ if (meta3.variables && meta3.variableCollections) return fromFigmaVariables(meta3, opts);
8373
+ if (Array.isArray(o.styles) && o.styles.every((s) => s && "styleType" in s)) return fromFigmaStyles(o.styles, opts);
8374
+ if (hasDollarValue(o) && hasFigmaExt(o)) return importTokenFiles([{ name: "tokens.json", data: o }], opts);
8375
+ if (hasDollarValue(o) || hasStudioValue(o)) {
8376
+ const studio = !hasDollarValue(o);
8377
+ const flat = flattenTokens(o);
8378
+ if (!flat.length) throw new Error("No tokens found in that file.");
8379
+ const out = flat.map((t) => {
8380
+ if (t.type === "color" && !t.path.startsWith("color.")) return { ...t, path: `color.${t.path}` };
8381
+ return t;
8382
+ });
8383
+ const notes = studio ? [] : [];
8384
+ const res = finish(studio ? "tokens-studio" : "dtcg", out, notes, opts);
8385
+ return res;
8386
+ }
8387
+ const leaves3 = [];
8388
+ plainLeaves(o, [], leaves3);
8389
+ if (leaves3.length) return fromModeObject(o, opts);
8390
+ throw new Error("Couldn't find any tokens. Supported: DTCG, Tokens Studio, Figma variables JSON.");
8391
+ }
8392
+ function diffTokens(before, after) {
8393
+ const a = new Map(flattenTokens(before).map((t) => [t.path, t]));
8394
+ const b = new Map(flattenTokens(after).map((t) => [t.path, t]));
8395
+ const diff = { added: [], removed: [], changed: [], unchanged: 0 };
8396
+ for (const [p, t] of b) {
8397
+ const old = a.get(p);
8398
+ if (!old) diff.added.push(t);
8399
+ else if (old.value.toLowerCase() !== t.value.toLowerCase() || (old.dark ?? "").toLowerCase() !== (t.dark ?? "").toLowerCase()) diff.changed.push({ path: p, before: old, after: t });
8400
+ else diff.unchanged++;
8401
+ }
8402
+ for (const [p, t] of a) if (!b.has(p)) diff.removed.push(t);
8403
+ return diff;
8404
+ }
8405
+ function mergeTokens(base, incoming) {
8406
+ const m = new Map(flattenTokens(base).map((t) => [t.path, t]));
8407
+ for (const t of flattenTokens(incoming)) m.set(t.path, { ...m.get(t.path), ...t, description: t.description ?? m.get(t.path)?.description });
8408
+ return buildDtcg([...m.values()]);
8409
+ }
8410
+
8411
+ // ../engine/src/governance.ts
8412
+ var isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8413
+ var DATE = /^\d{4}-\d{2}-\d{2}$/;
8414
+ function tokenDeprecations(doc) {
8415
+ const out = [];
8416
+ const walk2 = (n, path) => {
8417
+ if (!isObj(n)) return;
8418
+ if ("$value" in n) {
8419
+ const d = n.$deprecated;
8420
+ if (d === void 0 || d === false) return;
8421
+ const ext = isObj(n.$extensions) && isObj(n.$extensions[EXT]) ? n.$extensions[EXT] : {};
8422
+ out.push({
8423
+ path,
8424
+ ...typeof ext.replacement === "string" && ext.replacement ? { replacement: ext.replacement } : {},
8425
+ ...typeof ext.removeAfter === "string" && DATE.test(ext.removeAfter) ? { removeAfter: ext.removeAfter } : {},
8426
+ ...typeof d === "string" && d.trim() ? { reason: d.trim() } : {}
8427
+ });
8428
+ return;
8429
+ }
8430
+ for (const [k, v] of Object.entries(n)) if (!k.startsWith("$")) walk2(v, path ? `${path}.${k}` : k);
8431
+ };
8432
+ walk2(doc, "");
8433
+ return out;
8434
+ }
8435
+ var describe = (d, cssVars) => {
8436
+ const v = (p) => cssVars && p.includes(".") ? `\`var(--${p.replace(/\./g, "-")})\`` : `\`${p}\``;
8437
+ return `${v(d.path)}${d.replacement ? ` \u2192 use ${v(d.replacement)}` : ""}${d.removeAfter ? ` (removed after ${d.removeAfter})` : ""}${d.reason ? `. ${d.reason}` : ""}`;
8438
+ };
8439
+ var deprecationLine = describe;
8440
+
7813
8441
  // ../engine/src/config.ts
7814
8442
  var DEFAULT_IGNORE = [
7815
8443
  "**/node_modules/**",
@@ -7937,6 +8565,7 @@ function resolveConfig(config2) {
7937
8565
  const components = config2.components ?? [];
7938
8566
  const replaces = /* @__PURE__ */ new Map();
7939
8567
  for (const c of components) {
8568
+ if (c.status === "deprecated") continue;
7940
8569
  const tags = c.replaces ?? NAME_TO_TAGS[c.name.toLowerCase()] ?? [];
7941
8570
  for (const t of tags) {
7942
8571
  const key = t.toLowerCase().replace(/\s+/g, "").replace(/["']/g, "");
@@ -7952,6 +8581,7 @@ function resolveConfig(config2) {
7952
8581
  fontVars: fontVars.length ? fontVars : ["--font-family-sans"],
7953
8582
  fontSizes: uniqSorted(all.filter((l) => /^font\.size\./.test(l.path)).map((l) => px(l.value)).filter((v) => v !== null)),
7954
8583
  components,
8584
+ deprecatedTokens: new Map(tokenDeprecations(config2.tokens).map((d) => [`--${d.path.replace(/\./g, "-")}`, d])),
7955
8585
  replaces,
7956
8586
  rules: config2.rules ?? {},
7957
8587
  ignore: [...DEFAULT_IGNORE, ...config2.ignore ?? []],
@@ -8677,1295 +9307,766 @@ var radiusScale = {
8677
9307
  }
8678
9308
  for (const m of matches(/(?<![\w-])rounded(?:-[a-z]{1,2})?-\[(\d*\.?\d+)px\]/g, code)) {
8679
9309
  const n = parseFloat(m[1]);
8680
- if (!ok(n)) out.push({ offset: m.index, message: `${m[0]} is not a radius token`, suggestion: `Use ${neighbours(config2.radii.filter((r) => r < 999), n)}` });
8681
- }
8682
- for (const b of styleBindings(code)) {
8683
- if (!RADIUS_PROP.test(b.prop)) continue;
8684
- for (const v of pxValues(b.value)) {
8685
- if (!ok(v.n)) out.push({ offset: b.offset, message: `${b.prop} ${fmt(v.n)} is not a radius token`, suggestion: `Use ${neighbours(config2.radii.filter((r) => r < 999), v.n)}` });
8686
- }
8687
- }
8688
- }
8689
- return out;
8690
- }
8691
- };
8692
- var fontFamily = {
8693
- id: "font-family",
8694
- title: "Unapproved font",
8695
- description: "Only the Brand Kit's font families may be used.",
8696
- defaultSeverity: "warning",
8697
- kinds: ALL,
8698
- check({ code, kind, config: config2 }) {
8699
- if (config2.fonts.length === 0) return [];
8700
- const out = [];
8701
- const allowed = config2.fonts;
8702
- const test = (value, offset) => {
8703
- const fam = normalizeFontFamily(value);
8704
- if (!fam || isGenericFont(fam) || allowed.includes(fam)) return;
8705
- out.push({
8706
- offset,
8707
- message: `Font "${fam}" is not in the Brand Kit`,
8708
- suggestion: `Use var(--font-family-sans) (${allowed[0]})`
8709
- });
8710
- };
8711
- for (const m of matches(/(?<![\w-])font-family\s*:\s*([^;{}\n]+)/g, code)) test(declValue(m[1], kind), m.index);
8712
- if (kind !== "css") {
8713
- for (const m of matches(/(?<![\w-])fontFamily\s*:\s*(['"`])([^'"`]+)\1/g, code)) test(m[2], m.index);
8714
- for (const m of matches(/(?<![\w-])font-\[([^\]]+)\]/g, code)) test(m[1].replace(/_/g, " "), m.index);
8715
- for (const b of styleBindings(code)) if (b.prop === "font-family") test(b.value, b.offset);
8716
- }
8717
- return out;
8718
- }
8719
- };
8720
- var TEXT_INPUT_TYPES = /* @__PURE__ */ new Set(["", "text", "email", "password", "number", "search", "tel", "url"]);
8721
- var preferOfficialComponent = {
8722
- id: "prefer-official-component",
8723
- title: "Re-invented component",
8724
- description: "Use the design system component instead of a raw HTML element.",
8725
- defaultSeverity: "error",
8726
- kinds: MARKUP,
8727
- check({ code, config: config2, framework }) {
8728
- if (config2.replaces.size === 0) return [];
8729
- const out = [];
8730
- const baseTags = [...new Set([...config2.replaces.keys()].map((k) => k.replace(/\[.*$/, "").replace(/[^a-z0-9-]/g, "")))].filter(Boolean);
8731
- for (const m of matches(new RegExp(`<(${baseTags.join("|")})(?=[\\s>/])([^>]*)>`, "g"), code)) {
8732
- const tag = m[1];
8733
- const attrs = m[2];
8734
- let comp = config2.replaces.get(tag);
8735
- if (tag === "input" || tag === "button") {
8736
- const type = (attrs.match(/\btype\s*=\s*\{?\s*["'`]([\w-]+)["'`]/)?.[1] ?? "").toLowerCase();
8737
- const specific = config2.replaces.get(`${tag}[type=${type}]`);
8738
- if (specific) comp = specific;
8739
- else if (tag === "input" && !TEXT_INPUT_TYPES.has(type)) comp = type === "submit" || type === "button" ? config2.replaces.get("button") : void 0;
8740
- }
8741
- if (!comp) continue;
8742
- const usage = resolveUsage(comp, framework);
8743
- if (!usage) continue;
8744
- if (new RegExp(`(?:function|const|class)\\s+${comp.name}\\b`).test(code)) continue;
8745
- if (framework !== "react" && implementsTag(code, usage.tag)) continue;
8746
- out.push({
8747
- offset: m.index,
8748
- message: `Raw <${tag}${tag === "input" && /type=/.test(attrs) ? ` type="${attrs.match(/type\s*=\s*\{?\s*["'`]([\w-]+)/)?.[1]}"` : ""}> re-implements ${comp.name}`,
8749
- suggestion: useSuggestion(comp, usage)
8750
- });
8751
- }
8752
- return out;
8753
- }
8754
- };
8755
- var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8756
- var implementsTag = (code, tag) => new RegExp(`selector\\s*:\\s*['"\`]${escapeRe(tag)}['"\`]|customElements\\.define\\(\\s*['"\`]${escapeRe(tag)}['"\`]|@customElement\\(\\s*['"\`]${escapeRe(tag)}['"\`]`).test(code);
8757
- function useSuggestion(comp, u) {
8758
- if (u.framework === "react") return `import { ${comp.name} } from "${comp.importPath}"`;
8759
- const parts = [`Use <${u.tag}>`];
8760
- if (u.importLine) parts.push(`(${u.importLine})`);
8761
- return parts.join(" ");
8762
- }
8763
- var forbiddenImport = {
8764
- id: "forbidden-import",
8765
- title: "Other UI library",
8766
- description: "Only the design system's component library may be imported.",
8767
- defaultSeverity: "error",
8768
- kinds: ALL,
8769
- check({ code, config: config2 }) {
8770
- const out = [];
8771
- const lib = config2.components[0]?.importPath;
8772
- const re = /(?:\bimport\s[^'";]*?from\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\(\s*|@import\s+(?:url\(\s*)?)(['"])([^'"]+)\1/g;
8773
- for (const m of matches(re, code)) {
8774
- const spec = m[2];
8775
- const bad = config2.forbiddenImports.find((p) => spec === p || spec.startsWith(`${p}/`));
8776
- if (!bad) continue;
8777
- out.push({
8778
- offset: m.index + m[0].indexOf(spec),
8779
- message: `Imports ${bad}, which isn't part of the design system`,
8780
- suggestion: lib ? `Use components from "${lib}"` : "Use the design system's components"
8781
- });
8782
- }
8783
- return out;
8784
- }
8785
- };
8786
- var imgAlt = {
8787
- id: "img-alt",
8788
- title: "Image without alt text",
8789
- description: "Every <img> needs an alt attribute (empty for decorative images).",
8790
- defaultSeverity: "warning",
8791
- kinds: MARKUP,
8792
- check({ code }) {
8793
- const out = [];
8794
- for (const m of matches(/<img(?=[\s/>])([^>]*)>/g, code)) {
8795
- if (/(?<![\w-])alt\s*=/.test(m[1]) || /\{\s*\.\.\./.test(m[1])) continue;
8796
- out.push({ offset: m.index, message: "<img> has no alt text", suggestion: 'Add alt="\u2026" describing the image, or alt="" if decorative' });
8797
- }
8798
- return out;
8799
- }
8800
- };
8801
- var NATIVE_ATTRS = new Set(
8802
- "id className style title role tabIndex hidden lang dir name value defaultValue defaultChecked checked disabled required readOnly autoFocus autoComplete placeholder type min max step maxLength minLength pattern form accept multiple href target rel download children".split(" ")
8803
- );
8804
- var ALWAYS_OK = /* @__PURE__ */ new Set(["key", "ref"]);
8805
- function importedComponents(code, components) {
8806
- const out = /* @__PURE__ */ new Map();
8807
- for (const m of matches(/\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\2/g, code)) {
8808
- const from = m[3];
8809
- for (const part of m[1].split(",")) {
8810
- const [orig, alias] = part.trim().replace(/^type\s+/, "").split(/\s+as\s+/).map((s) => s.trim());
8811
- if (!orig) continue;
8812
- const comp = components.find((c) => c.name === orig && c.importPath === from && c.props?.length);
8813
- if (comp) out.set(alias || orig, comp);
8814
- }
8815
- }
8816
- return out;
8817
- }
8818
- function skipBalanced(code, i2) {
8819
- const open2 = code[i2];
8820
- if (open2 === '"' || open2 === "'") {
8821
- const end = code.indexOf(open2, i2 + 1);
8822
- return end === -1 ? code.length : end + 1;
8823
- }
8824
- let depth = 0;
8825
- let quote = null;
8826
- for (let j = i2; j < code.length; j++) {
8827
- const c = code[j];
8828
- if (quote) {
8829
- if (c === "\\") j++;
8830
- else if (c === quote) quote = null;
8831
- continue;
8832
- }
8833
- if (c === '"' || c === "'" || c === "`") quote = c;
8834
- else if (c === "{") depth++;
8835
- else if (c === "}" && --depth === 0) return j + 1;
8836
- }
8837
- return code.length;
8838
- }
8839
- function parseAttributes(code, start) {
8840
- const attrs = [];
8841
- let spread = false;
8842
- let i2 = start;
8843
- while (i2 < code.length) {
8844
- while (/\s/.test(code[i2] ?? "")) i2++;
8845
- const c = code[i2];
8846
- if (c === void 0) return null;
8847
- if (c === ">") return { attrs, spread, selfClosing: false };
8848
- if (c === "/" && code[i2 + 1] === ">") return { attrs, spread, selfClosing: true };
8849
- if (c === "{") {
8850
- spread = true;
8851
- i2 = skipBalanced(code, i2);
8852
- continue;
8853
- }
8854
- const name = /^[A-Za-z_$][\w$:.-]*/.exec(code.slice(i2, i2 + 80))?.[0];
8855
- if (!name) return null;
8856
- attrs.push({ name, offset: i2 });
8857
- i2 += name.length;
8858
- while (/\s/.test(code[i2] ?? "")) i2++;
8859
- if (code[i2] === "=") {
8860
- i2++;
8861
- while (/\s/.test(code[i2] ?? "")) i2++;
8862
- i2 = skipBalanced(code, i2);
8863
- }
8864
- }
8865
- return null;
8866
- }
8867
- function parseHtmlAttributes(code, start) {
8868
- const attrs = [];
8869
- let i2 = start;
8870
- while (i2 < code.length) {
8871
- while (/\s/.test(code[i2] ?? "")) i2++;
8872
- const c = code[i2];
8873
- if (c === void 0) return null;
8874
- if (c === ">" || c === "/" && code[i2 + 1] === ">") return { attrs, end: i2 };
8875
- if (c === "{" || c === "<" && code[i2 + 1] === "?") {
8876
- const close = c === "{" ? code[i2 + 1] === "%" ? "%}" : "}}" : "?>";
8877
- const e = code.indexOf(close, i2 + 2);
8878
- i2 = e === -1 ? code.length : e + close.length;
8879
- continue;
8880
- }
8881
- const name = /^[^\s=>/"'{]+/.exec(code.slice(i2, i2 + 120))?.[0];
8882
- if (!name) return null;
8883
- attrs.push({ name, offset: i2 });
8884
- i2 += name.length;
8885
- while (/\s/.test(code[i2] ?? "")) i2++;
8886
- if (code[i2] === "=") {
8887
- i2++;
8888
- while (/\s/.test(code[i2] ?? "")) i2++;
8889
- const q = code[i2];
8890
- if (q === '"' || q === "'") {
8891
- const e = code.indexOf(q, i2 + 1);
8892
- i2 = e === -1 ? code.length : e + 1;
8893
- } else while (i2 < code.length && !/[\s>]/.test(code[i2])) i2++;
8894
- }
8895
- }
8896
- return null;
8897
- }
8898
- var HTML_GLOBAL = /^(?:id|class|style|title|role|hidden|tabindex|lang|dir|slot|part|is|key|ref|name|for|form|aria-.+|data-.+)$/;
8899
- function propName(raw) {
8900
- let n = raw;
8901
- if (/^(?:\(|@|on-|v-on:|\*|#|let-|ref-|v-(?!bind:|model:))/.test(n)) return null;
8902
- n = n.replace(/^v-bind:|^v-model:|^bind-|^:/, "").replace(/^\[\(?|\)?\]$/g, "");
8903
- if (/^(?:attr|class|style)\./.test(n) || /^ng[A-Z]/.test(n) || /^(?:formControl|formControlName|formGroup|routerLink|routerLinkActive)$/.test(n)) return null;
8904
- n = n.replace(/\.(?:px|rem|em|%)$/, "").replace(/\..*$/, "");
8905
- if (HTML_GLOBAL.test(n.toLowerCase())) return null;
8906
- return n;
8907
- }
8908
- var sameName = (a, b) => a.replace(/-/g, "").toLowerCase() === b.replace(/-/g, "").toLowerCase();
8909
- 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+)$/;
8910
- function eventName(raw) {
8911
- const m = /^\(([^)]+)\)$|^on-(.+)$|^@([^.]+)|^v-on:([^.]+)/.exec(raw);
8912
- return m ? (m[1] ?? m[2] ?? m[3] ?? m[4]).replace(/\..*$/, "") : null;
8913
- }
8914
- function templateProps({ code, config: config2, framework }) {
8915
- const tagged = [];
8916
- for (const comp of config2.components) {
8917
- const u = resolveUsage(comp, framework);
8918
- if (u && (u.props?.length || u.events?.length)) tagged.push({ comp, tag: u.tag, props: u.props, events: u.events, ce: Boolean(u.customElement || u.via) });
8919
- }
8920
- if (!tagged.length) return [];
8921
- const out = [];
8922
- const forms = /* @__PURE__ */ new Map();
8923
- for (const t of tagged) {
8924
- forms.set(t.tag, t);
8925
- if (framework === "vue") forms.set(t.tag.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), t);
8926
- }
8927
- const names = [...forms.keys()].sort((a, b) => b.length - a.length).map(escapeRe).join("|");
8928
- for (const m of matches(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
8929
- const t = forms.get(m[1]);
8930
- const parsed = parseHtmlAttributes(code, m.index + m[0].length);
8931
- if (!parsed) continue;
8932
- const list = (t.props ?? []).map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
8933
- const passed = [];
8934
- for (const a of parsed.attrs) {
8935
- const raw = a.name;
8936
- const ev = eventName(raw);
8937
- if (ev) {
8938
- if (!t.events?.length || NATIVE_EVENT.test(ev) || t.events.some((e) => e.name === ev)) continue;
8939
- const like = t.events.find((e) => e.name.endsWith(`-${ev}`) || e.name.endsWith(ev)) ?? (t.events.length === 1 ? t.events[0] : void 0);
8940
- out.push({
8941
- offset: a.offset,
8942
- message: `<${t.tag}> has no "${ev}" event`,
8943
- suggestion: `${like ? `Listen to ${framework === "angular" ? `(${like.name})` : framework === "vue" ? `@${like.name}` : `"${like.name}"`}${like.detail ? ` and read ${framework === "angular" ? "$any($event).detail" : framework === "vue" ? "$event.detail" : "event.detail"} (${like.detail})` : ""}. ` : ""}Events: ${t.events.map((e) => e.name).join(", ")}`
8944
- });
8945
- continue;
8946
- }
8947
- if (/^v-model$/.test(raw)) passed.push("modelValue", "value");
8948
- if (/^\[\(ngModel\)\]$/.test(raw)) passed.push("value");
8949
- const n = propName(raw);
8950
- if (!n || !t.props?.length) continue;
8951
- passed.push(n);
8952
- if (t.ce && n.includes("-") && /^(?:\[(?!attr\.)|:|v-bind:|bind-)/.test(raw)) {
8953
- const camel2 = n.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
8954
- out.push({
8955
- offset: a.offset,
8956
- message: `${raw} binds a property that doesn't exist on <${t.tag}>`,
8957
- suggestion: `Bind the camelCase property: ${framework === "vue" ? `:${camel2}` : `[${camel2}]`}="\u2026" (kebab-case is only for static attributes: ${n}="\u2026")`
8958
- });
8959
- continue;
8960
- }
8961
- if (t.props.some((p) => sameName(p.name, n))) continue;
8962
- out.push({ offset: a.offset, message: `<${t.tag}> has no "${n}" input`, suggestion: `Remove it. ${t.comp.name} takes: ${list}` });
8963
- }
8964
- for (const p of t.props ?? []) {
8965
- if (!p.required || passed.some((n) => sameName(n, p.name))) continue;
8966
- if (p.name === "children" || p.name === "content") continue;
8967
- out.push({ offset: m.index, message: `<${t.tag}> is missing the required "${p.name}" input`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
8968
- }
8969
- }
8970
- return out;
8971
- }
8972
- var componentProps = {
8973
- id: "component-props",
8974
- title: "Unknown or missing prop",
8975
- description: "Design system components only accept their documented props, and required props must be passed.",
8976
- defaultSeverity: "error",
8977
- kinds: MARKUP,
8978
- check(ctx) {
8979
- if (ctx.framework !== "react" || ctx.kind !== "jsx") return templateProps(ctx);
8980
- const { code, config: config2 } = ctx;
8981
- if (!config2.components.some((c) => c.props?.length)) return [];
8982
- const imported = importedComponents(code, config2.components);
8983
- if (imported.size === 0) return [];
8984
- const out = [];
8985
- const names = [...imported.keys()].map((n) => n.replace(/[$]/g, "\\$")).join("|");
8986
- for (const m of matches(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
8987
- if (/[\w)\]]\s*$/.test(code.slice(Math.max(0, m.index - 20), m.index))) continue;
8988
- const comp = imported.get(m[1]);
8989
- const parsed = parseAttributes(code, m.index + m[0].length);
8990
- if (!parsed) continue;
8991
- const own2 = new Map(comp.props.map((p) => [p.name, p]));
8992
- const list = comp.props.map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
8993
- for (const a of parsed.attrs) {
8994
- if (own2.has(a.name) || ALWAYS_OK.has(a.name) || a.name.includes("-")) continue;
8995
- if (comp.htmlAttributes && (NATIVE_ATTRS.has(a.name) || /^on[A-Z]/.test(a.name) || /^aria/.test(a.name))) continue;
8996
- const sibling = config2.components.find(
8997
- (c) => c !== comp && c.importPath === comp.importPath && (c.name.startsWith(comp.name) || comp.name.startsWith(c.name)) && c.props?.some((p) => p.name === a.name)
8998
- );
8999
- out.push({
9000
- offset: a.offset,
9001
- message: `<${comp.name}> has no "${a.name}" prop`,
9002
- suggestion: sibling ? `Use <${sibling.name}>, which has "${a.name}", or remove it. ${comp.name} props: ${list}` : `Remove it. ${comp.name} props: ${list}`
9003
- });
9004
- }
9005
- if (!parsed.spread) {
9006
- const passed = new Set(parsed.attrs.map((a) => a.name));
9007
- for (const p of comp.props) {
9008
- if (!p.required || passed.has(p.name) || p.name === "children" && !parsed.selfClosing) continue;
9009
- out.push({ offset: m.index, message: `<${comp.name}> is missing the required "${p.name}" prop`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
9310
+ if (!ok(n)) out.push({ offset: m.index, message: `${m[0]} is not a radius token`, suggestion: `Use ${neighbours(config2.radii.filter((r) => r < 999), n)}` });
9311
+ }
9312
+ for (const b of styleBindings(code)) {
9313
+ if (!RADIUS_PROP.test(b.prop)) continue;
9314
+ for (const v of pxValues(b.value)) {
9315
+ if (!ok(v.n)) out.push({ offset: b.offset, message: `${b.prop} ${fmt(v.n)} is not a radius token`, suggestion: `Use ${neighbours(config2.radii.filter((r) => r < 999), v.n)}` });
9010
9316
  }
9011
9317
  }
9012
9318
  }
9013
9319
  return out;
9014
9320
  }
9015
9321
  };
9016
- var legacyLibrary = {
9017
- id: "legacy-library",
9018
- title: "Other UI library in use",
9019
- description: "Pieces of Material, MUI, Bootstrap, Ant Design, Chakra or PrimeNG that a design system component replaces. The count is the migration still to do.",
9322
+ var fontFamily = {
9323
+ id: "font-family",
9324
+ title: "Unapproved font",
9325
+ description: "Only the Brand Kit's font families may be used.",
9020
9326
  defaultSeverity: "warning",
9021
- kinds: ["jsx", "vue", "svelte", "html", "template"],
9022
- check({ code, kind, config: config2, framework }) {
9327
+ kinds: ALL,
9328
+ check({ code, kind, config: config2 }) {
9329
+ if (config2.fonts.length === 0) return [];
9023
9330
  const out = [];
9024
- for (const u of findLegacyUsages(code, kind)) {
9025
- const comp = componentForConcept(config2, u.concept);
9026
- const usage = comp ? resolveUsage(comp, framework) : null;
9331
+ const allowed = config2.fonts;
9332
+ const test = (value, offset) => {
9333
+ const fam = normalizeFontFamily(value);
9334
+ if (!fam || isGenericFont(fam) || allowed.includes(fam)) return;
9027
9335
  out.push({
9028
- offset: u.offset,
9029
- message: `${u.what} (${LIBRARY_LABEL[u.library]}) should be ${comp ? comp.name : `a design system ${u.concept}`}`,
9030
- suggestion: comp && usage ? useSuggestion(comp, usage) : comp ? `${comp.name} has no ${FRAMEWORK_LABEL[framework]} version yet: add one in the registry, or build it with tokens` : `No ${u.concept} component in the registry yet: add one, or build it with tokens and semantic HTML`
9336
+ offset,
9337
+ message: `Font "${fam}" is not in the Brand Kit`,
9338
+ suggestion: `Use var(--font-family-sans) (${allowed[0]})`
9031
9339
  });
9340
+ };
9341
+ for (const m of matches(/(?<![\w-])font-family\s*:\s*([^;{}\n]+)/g, code)) test(declValue(m[1], kind), m.index);
9342
+ if (kind !== "css") {
9343
+ for (const m of matches(/(?<![\w-])fontFamily\s*:\s*(['"`])([^'"`]+)\1/g, code)) test(m[2], m.index);
9344
+ for (const m of matches(/(?<![\w-])font-\[([^\]]+)\]/g, code)) test(m[1].replace(/_/g, " "), m.index);
9345
+ for (const b of styleBindings(code)) if (b.prop === "font-family") test(b.value, b.offset);
9032
9346
  }
9033
9347
  return out;
9034
9348
  }
9035
9349
  };
9036
- var RULES = [
9037
- noHardcodedColor,
9038
- noRawPaletteClass,
9039
- preferOfficialComponent,
9040
- forbiddenImport,
9041
- spacingScale,
9042
- fontFamily,
9043
- radiusScale,
9044
- imgAlt,
9045
- componentProps,
9046
- legacyLibrary
9047
- ];
9048
- var RULE_IDS = RULES.map((r) => r.id);
9049
-
9050
- // ../engine/src/source.ts
9051
- var TEMPLATE_EXTENSIONS = ["mustache", "hbs", "handlebars", "php", "phtml", "twig", "njk", "liquid"];
9052
- function fileKind(path) {
9053
- const ext = path.toLowerCase().split(".").pop() ?? "";
9054
- if (TEMPLATE_EXTENSIONS.includes(ext)) return "template";
9055
- if (["tsx", "jsx", "ts", "js", "mjs", "cjs", "mdx"].includes(ext)) return "jsx";
9056
- if (ext === "vue") return "vue";
9057
- if (ext === "svelte") return "svelte";
9058
- if (["html", "htm", "astro"].includes(ext)) return "html";
9059
- if (["css", "scss", "sass", "less", "pcss"].includes(ext)) return "css";
9060
- return "other";
9061
- }
9062
- var SCANNABLE = /* @__PURE__ */ new Set(["jsx", "vue", "svelte", "html", "template", "css"]);
9063
- var ANGULAR_COMPONENT = /@Component\s*\(\s*\{/;
9064
- var ANGULAR_TEMPLATE = /\s(?:\[[\w.()-]+\]|\([\w.]+\)|\*ng[A-Z]\w*)\s*=|@(?:if|for|switch)\s*\(/;
9065
- function fileFramework(path, kind, code, project) {
9066
- const p = path.toLowerCase();
9067
- switch (kind) {
9068
- case "vue":
9069
- return "vue";
9070
- case "svelte":
9071
- return "web-components";
9072
- case "template":
9073
- return "html";
9074
- case "css":
9075
- return project ?? "react";
9076
- case "html":
9077
- if (p.endsWith(".component.html") || project === "angular" || ANGULAR_TEMPLATE.test(code)) return "angular";
9078
- return project === "vue" || project === "web-components" ? project : "html";
9079
- case "jsx":
9080
- if (ANGULAR_COMPONENT.test(code)) return "angular";
9081
- if (/\.(tsx|jsx|mdx)$/.test(p)) return "react";
9082
- return project ?? "react";
9083
- default:
9084
- return project ?? "react";
9085
- }
9086
- }
9087
- var blank = (s) => s.replace(/[^\n]/g, " ");
9088
- function stripComments(code, kind) {
9089
- let out = code;
9090
- if (kind === "html" || kind === "vue" || kind === "svelte" || kind === "template") {
9091
- out = out.replace(/<!--[\s\S]*?-->/g, blank);
9092
- }
9093
- if (kind === "template") {
9094
- 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));
9095
- return out;
9096
- }
9097
- if (kind === "css") {
9098
- return out.replace(/\/\*[\s\S]*?\*\//g, blank).replace(/(^|[^:])\/\/[^\n]*/g, (m, p1) => p1 + blank(m.slice(p1.length)));
9099
- }
9100
- const chars = out.split("");
9101
- let i2 = 0;
9102
- let quote = null;
9103
- while (i2 < chars.length) {
9104
- const c = chars[i2];
9105
- const n = chars[i2 + 1];
9106
- if (quote) {
9107
- if (c === "\\") i2 += 2;
9108
- else {
9109
- if (c === quote) quote = null;
9110
- i2++;
9350
+ var TEXT_INPUT_TYPES = /* @__PURE__ */ new Set(["", "text", "email", "password", "number", "search", "tel", "url"]);
9351
+ var preferOfficialComponent = {
9352
+ id: "prefer-official-component",
9353
+ title: "Re-invented component",
9354
+ description: "Use the design system component instead of a raw HTML element.",
9355
+ defaultSeverity: "error",
9356
+ kinds: MARKUP,
9357
+ check({ code, config: config2, framework }) {
9358
+ if (config2.replaces.size === 0) return [];
9359
+ const out = [];
9360
+ const baseTags = [...new Set([...config2.replaces.keys()].map((k) => k.replace(/\[.*$/, "").replace(/[^a-z0-9-]/g, "")))].filter(Boolean);
9361
+ for (const m of matches(new RegExp(`<(${baseTags.join("|")})(?=[\\s>/])([^>]*)>`, "g"), code)) {
9362
+ const tag = m[1];
9363
+ const attrs = m[2];
9364
+ let comp = config2.replaces.get(tag);
9365
+ if (tag === "input" || tag === "button") {
9366
+ const type = (attrs.match(/\btype\s*=\s*\{?\s*["'`]([\w-]+)["'`]/)?.[1] ?? "").toLowerCase();
9367
+ const specific = config2.replaces.get(`${tag}[type=${type}]`);
9368
+ if (specific) comp = specific;
9369
+ else if (tag === "input" && !TEXT_INPUT_TYPES.has(type)) comp = type === "submit" || type === "button" ? config2.replaces.get("button") : void 0;
9111
9370
  }
9112
- continue;
9113
- }
9114
- if (c === '"' || c === "'" || c === "`") {
9115
- quote = c;
9116
- i2++;
9117
- continue;
9118
- }
9119
- if (c === "/" && n === "*") {
9120
- const end = out.indexOf("*/", i2 + 2);
9121
- const stop = end === -1 ? chars.length : end + 2;
9122
- for (let j = i2; j < stop; j++) if (chars[j] !== "\n") chars[j] = " ";
9123
- i2 = stop;
9124
- continue;
9125
- }
9126
- if (c === "/" && n === "/" && chars[i2 - 1] !== ":" && kind === "jsx") {
9127
- while (i2 < chars.length && chars[i2] !== "\n") chars[i2++] = " ";
9128
- continue;
9129
- }
9130
- i2++;
9131
- }
9132
- return chars.join("");
9133
- }
9134
- var LineIndex = class {
9135
- starts = [0];
9136
- text;
9137
- constructor(text2) {
9138
- this.text = text2;
9139
- for (let i2 = 0; i2 < text2.length; i2++) if (text2.charCodeAt(i2) === 10) this.starts.push(i2 + 1);
9140
- }
9141
- position(offset) {
9142
- let lo = 0;
9143
- let hi = this.starts.length - 1;
9144
- while (lo < hi) {
9145
- const mid = lo + hi + 1 >> 1;
9146
- if (this.starts[mid] <= offset) lo = mid;
9147
- else hi = mid - 1;
9371
+ if (!comp) continue;
9372
+ const usage = resolveUsage(comp, framework);
9373
+ if (!usage) continue;
9374
+ if (new RegExp(`(?:function|const|class)\\s+${comp.name}\\b`).test(code)) continue;
9375
+ if (framework !== "react" && implementsTag(code, usage.tag)) continue;
9376
+ out.push({
9377
+ offset: m.index,
9378
+ message: `Raw <${tag}${tag === "input" && /type=/.test(attrs) ? ` type="${attrs.match(/type\s*=\s*\{?\s*["'`]([\w-]+)/)?.[1]}"` : ""}> re-implements ${comp.name}`,
9379
+ suggestion: useSuggestion(comp, usage)
9380
+ });
9148
9381
  }
9149
- return { line: lo + 1, column: offset - this.starts[lo] + 1 };
9150
- }
9151
- lineText(line) {
9152
- const start = this.starts[line - 1] ?? 0;
9153
- const end = this.starts[line] ?? this.text.length + 1;
9154
- return this.text.slice(start, end - 1);
9155
- }
9156
- get lineCount() {
9157
- return this.starts.length;
9382
+ return out;
9158
9383
  }
9159
9384
  };
9160
- function globToRegExp(glob) {
9161
- let re = "";
9162
- for (let i2 = 0; i2 < glob.length; i2++) {
9163
- const c = glob[i2];
9164
- if (c === "*") {
9165
- if (glob[i2 + 1] === "*") {
9166
- const slash = glob[i2 + 2] === "/";
9167
- re += slash ? "(?:.*/)?" : ".*";
9168
- i2 += slash ? 2 : 1;
9169
- } else re += "[^/]*";
9170
- } else if (c === "?") re += "[^/]";
9171
- else if (c === "{") {
9172
- const end = glob.indexOf("}", i2);
9173
- re += `(?:${glob.slice(i2 + 1, end).split(",").map((s) => s.replace(/[.+^$()|[\]\\]/g, "\\$&")).join("|")})`;
9174
- i2 = end;
9175
- } else re += c.replace(/[.+^$()|[\]\\]/g, "\\$&");
9176
- }
9177
- return new RegExp(`^${re}$`);
9178
- }
9179
- function makeIgnore(globs) {
9180
- const res = globs.map(globToRegExp);
9181
- return (path) => {
9182
- const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
9183
- return res.some((r) => r.test(p) || r.test(`/${p}`));
9184
- };
9185
- }
9186
-
9187
- // ../engine/src/check.ts
9188
- var ENGINE_VERSION = "0.8.0";
9189
- var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
9190
- function computeScore(counts, lines) {
9191
- const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
9192
- if (penalty === 0) return 100;
9193
- const perK = penalty / Math.max(1, lines / 1e3);
9194
- return Math.max(0, Math.min(99, Math.round(100 / (1 + perK / 25))));
9385
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9386
+ var implementsTag = (code, tag) => new RegExp(`selector\\s*:\\s*['"\`]${escapeRe(tag)}['"\`]|customElements\\.define\\(\\s*['"\`]${escapeRe(tag)}['"\`]|@customElement\\(\\s*['"\`]${escapeRe(tag)}['"\`]`).test(code);
9387
+ function useSuggestion(comp, u) {
9388
+ if (u.framework === "react") return `import { ${comp.name} } from "${comp.importPath}"`;
9389
+ const parts = [`Use <${u.tag}>`];
9390
+ if (u.importLine) parts.push(`(${u.importLine})`);
9391
+ return parts.join(" ");
9195
9392
  }
9196
- function check(files, configInput, opts = {}) {
9197
- const t0 = Date.now();
9198
- const config2 = "replaces" in configInput ? configInput : resolveConfig(configInput);
9199
- const ignored = makeIgnore(config2.ignore);
9200
- const violations = [];
9201
- let filesScanned = 0;
9202
- let linesScanned = 0;
9203
- for (const file2 of files) {
9204
- const path = file2.path.replace(/\\/g, "/").replace(/^\.\//, "");
9205
- const kind = fileKind(path);
9206
- if (!SCANNABLE.has(kind) || ignored(path)) continue;
9207
- const only = opts.onlyLines?.get(path);
9208
- if (opts.onlyLines && (!only || only.size === 0)) continue;
9209
- filesScanned++;
9210
- const idx = new LineIndex(file2.content);
9211
- linesScanned += only ? only.size : file2.content.split("\n").filter((l) => l.trim()).length;
9212
- const code = stripComments(file2.content, kind);
9213
- const framework = fileFramework(path, kind, code, config2.framework);
9214
- const seen = /* @__PURE__ */ new Set();
9215
- for (const rule of RULES) {
9216
- const setting = config2.rules[rule.id];
9217
- if (setting === "off" || !rule.kinds.includes(kind)) continue;
9218
- for (const f of rule.check({ path, kind, code, config: config2, framework })) {
9219
- const { line, column } = idx.position(f.offset);
9220
- if (only && !only.has(line)) continue;
9221
- const key = `${rule.id}:${line}:${column}`;
9222
- if (seen.has(key)) continue;
9223
- seen.add(key);
9224
- violations.push({
9225
- ruleId: rule.id,
9226
- severity: setting ?? f.severity ?? rule.defaultSeverity,
9227
- file: path,
9228
- line,
9229
- column,
9230
- message: f.message,
9231
- suggestion: f.suggestion,
9232
- snippet: opts.snippets === false ? void 0 : idx.lineText(line).trim().slice(0, 160)
9233
- });
9234
- }
9393
+ var forbiddenImport = {
9394
+ id: "forbidden-import",
9395
+ title: "Other UI library",
9396
+ description: "Only the design system's component library may be imported.",
9397
+ defaultSeverity: "error",
9398
+ kinds: ALL,
9399
+ check({ code, config: config2 }) {
9400
+ const out = [];
9401
+ const lib = config2.components[0]?.importPath;
9402
+ const re = /(?:\bimport\s[^'";]*?from\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\(\s*|@import\s+(?:url\(\s*)?)(['"])([^'"]+)\1/g;
9403
+ for (const m of matches(re, code)) {
9404
+ const spec = m[2];
9405
+ const bad = config2.forbiddenImports.find((p) => spec === p || spec.startsWith(`${p}/`));
9406
+ if (!bad) continue;
9407
+ out.push({
9408
+ offset: m.index + m[0].indexOf(spec),
9409
+ message: `Imports ${bad}, which isn't part of the design system`,
9410
+ suggestion: lib ? `Use components from "${lib}"` : "Use the design system's components"
9411
+ });
9235
9412
  }
9413
+ return out;
9236
9414
  }
9237
- violations.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
9238
- const counts = { error: 0, warning: 0, info: 0 };
9239
- const perRule = /* @__PURE__ */ new Map();
9240
- for (const v of violations) {
9241
- counts[v.severity]++;
9242
- perRule.set(v.ruleId, (perRule.get(v.ruleId) ?? 0) + 1);
9243
- }
9244
- return {
9245
- engineVersion: ENGINE_VERSION,
9246
- tokenVersion: config2.tokenVersion,
9247
- score: computeScore(counts, linesScanned),
9248
- counts,
9249
- filesScanned,
9250
- linesScanned,
9251
- violations,
9252
- byRule: [...perRule].map(([ruleId, count]) => ({ ruleId, count })).sort((a, b) => b.count - a.count),
9253
- durationMs: Date.now() - t0
9254
- };
9255
- }
9256
-
9257
- // ../engine/src/report.ts
9258
- var RENDERED_ONLY = { "text-contrast": "Low text contrast", "font-size-scale": "Off-scale font size", "page-rendered": "Blank page" };
9259
- var title = (id) => RULES.find((r) => r.id === id)?.title ?? RENDERED_ONLY[id] ?? id;
9260
- var ansi = (on) => {
9261
- const w = (code) => (s) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
9262
- return { red: w("31"), yellow: w("33"), cyan: w("36"), green: w("32"), dim: w("2"), bold: w("1") };
9263
9415
  };
9264
- var rendered = (r) => r.violations.length > 0 && r.violations.every((v) => v.line === 0);
9265
- function formatPretty(report, { color = true, max: max2 = 200 } = {}) {
9266
- const c = ansi(color);
9267
- const sev = { error: c.red, warning: c.yellow, info: c.cyan };
9268
- const lines = [];
9269
- let lastFile = "";
9270
- for (const v of report.violations.slice(0, max2)) {
9271
- if (v.file !== lastFile) {
9272
- lines.push("", c.bold(v.file));
9273
- lastFile = v.file;
9274
- }
9275
- const rendered2 = v.line === 0;
9276
- lines.push(` ${c.dim(rendered2 ? "".padEnd(8) : `${v.line}:${v.column}`.padEnd(8))} ${sev[v.severity](v.severity.padEnd(7))} ${v.message} ${c.dim(v.ruleId)}`);
9277
- if (v.suggestion) lines.push(` ${" ".repeat(16)} ${c.green("\u2192")} ${v.suggestion}`);
9278
- if (rendered2 && v.snippet) lines.push(` ${" ".repeat(16)} ${c.dim(`on ${v.snippet}`)}`);
9279
- }
9280
- if (report.violations.length > max2) lines.push("", c.dim(`\u2026 and ${report.violations.length - max2} more`));
9281
- const s = report.score;
9282
- const scoreStr = s >= 80 ? c.green(String(s)) : s >= 65 ? c.yellow(String(s)) : c.red(String(s));
9283
- lines.push(
9284
- "",
9285
- `${c.bold("Brand score")} ${scoreStr}/100 ${c.dim("\xB7")} ${c.red(`${report.counts.error} errors`)} ${c.dim("\xB7")} ${c.yellow(`${report.counts.warning} warnings`)} ${c.dim("\xB7")} ${c.cyan(`${report.counts.info} info`)}`,
9286
- c.dim(`${report.filesScanned} ${rendered(report) ? "pages" : "files"}, ${report.linesScanned} ${rendered(report) ? "elements" : "lines"} in ${report.durationMs}ms${report.tokenVersion ? ` \xB7 Brand Kit v${report.tokenVersion}` : ""}`)
9287
- );
9288
- return lines.join("\n");
9289
- }
9290
- var esc = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
9291
- var escProp = (s) => esc(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
9292
- function formatGithubAnnotations(report, max2 = 50) {
9293
- const level = { error: "error", warning: "warning", info: "notice" };
9294
- return report.violations.slice(0, max2).map(
9295
- (v) => `::${level[v.severity]} file=${escProp(v.file)},line=${v.line},col=${v.column},title=${escProp(`Brandrail: ${title(v.ruleId)}`)}::${esc(
9296
- v.suggestion ? `${v.message}. ${v.suggestion}` : v.message
9297
- )}`
9298
- ).join("\n");
9299
- }
9300
- var COMMENT_MARKER = "<!-- brandrail-report -->";
9301
- function formatMarkdown(report, opts = {}) {
9302
- const { threshold, maxRows = 30 } = opts;
9303
- const pass = threshold === void 0 || report.score >= threshold;
9304
- const badge = report.violations.length === 0 ? "\u{1F7E2}" : pass ? "\u{1F7E1}" : "\u{1F534}";
9305
- const out = [COMMENT_MARKER];
9306
- out.push(`## ${badge} Brand score: **${report.score}**/100${threshold !== void 0 ? ` (threshold ${threshold})` : ""}`);
9307
- out.push("");
9308
- out.push(
9309
- `**${report.counts.error}** errors \xB7 **${report.counts.warning}** warnings \xB7 **${report.counts.info}** info \u2014 ${report.filesScanned} files scanned${report.tokenVersion ? ` against Brand Kit v${report.tokenVersion}` : ""}`
9310
- );
9311
- if (report.violations.length === 0) {
9312
- out.push("", "Everything uses approved tokens and components. \u2728");
9313
- } else {
9314
- if (report.byRule.length) {
9315
- out.push("", "| Rule | Count |", "| --- | ---: |");
9316
- for (const r of report.byRule) out.push(`| ${title(r.ruleId)} \`${r.ruleId}\` | ${r.count} |`);
9317
- }
9318
- out.push("", "<details><summary>Violations</summary>", "", "| | Location | Issue | Fix |", "| --- | --- | --- | --- |");
9319
- const icon = { error: "\u{1F534}", warning: "\u{1F7E1}", info: "\u{1F535}" };
9320
- for (const v of report.violations.slice(0, maxRows)) {
9321
- const loc = v.line === 0 ? `${v.file} ${v.snippet ?? ""}`.trim() : `${v.file}:${v.line}`;
9322
- const url2 = opts.fileUrl?.(v);
9323
- const cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
9324
- out.push(`| ${icon[v.severity]} | ${url2 ? `[\`${loc}\`](${url2})` : `\`${loc}\``} | ${cell(v.message)} | ${cell(v.suggestion ?? "")} |`);
9416
+ var imgAlt = {
9417
+ id: "img-alt",
9418
+ title: "Image without alt text",
9419
+ description: "Every <img> needs an alt attribute (empty for decorative images).",
9420
+ defaultSeverity: "warning",
9421
+ kinds: MARKUP,
9422
+ check({ code }) {
9423
+ const out = [];
9424
+ for (const m of matches(/<img(?=[\s/>])([^>]*)>/g, code)) {
9425
+ if (/(?<![\w-])alt\s*=/.test(m[1]) || /\{\s*\.\.\./.test(m[1])) continue;
9426
+ out.push({ offset: m.index, message: "<img> has no alt text", suggestion: 'Add alt="\u2026" describing the image, or alt="" if decorative' });
9325
9427
  }
9326
- if (report.violations.length > maxRows) out.push("", `\u2026and ${report.violations.length - maxRows} more.`);
9327
- out.push("", "</details>");
9428
+ return out;
9328
9429
  }
9329
- out.push("", `<sub>Brandrail engine ${report.engineVersion}${opts.dashboardUrl ? ` \xB7 [Open in Brandrail](${opts.dashboardUrl})` : ""}</sub>`);
9330
- return out.join("\n");
9331
- }
9332
-
9333
- // ../engine/src/defaults.ts
9334
- var starterConfig = {
9335
- $schema: "https://brandrail.dev/schema/config.json",
9336
- tokenVersion: "0.0.0-local",
9337
- tokens: {
9338
- color: {
9339
- action: { primary: { $type: "color", $value: "#1f883d", $description: "One primary action per view" } },
9340
- accent: { default: { $type: "color", $value: "#0969da", $description: "Links, focus, selection" } },
9341
- fg: {
9342
- default: { $type: "color", $value: "#1f2328", $description: "Body text" },
9343
- muted: { $type: "color", $value: "#59636e", $description: "Secondary text" }
9344
- },
9345
- canvas: {
9346
- default: { $type: "color", $value: "#ffffff", $description: "Page background" },
9347
- subtle: { $type: "color", $value: "#f6f8fa", $description: "Headers, wells" }
9348
- },
9349
- border: { default: { $type: "color", $value: "#d1d9e0", $description: "Borders and dividers" } },
9350
- danger: { default: { $type: "color", $value: "#cf222e", $description: "Destructive actions, errors" } },
9351
- success: { default: { $type: "color", $value: "#1a7f37", $description: "Success messages, completed states" } },
9352
- attention: { default: { $type: "color", $value: "#9a6700", $description: "Warnings, needs attention" } }
9353
- },
9354
- font: {
9355
- family: { sans: { $type: "fontFamily", $value: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif" } },
9356
- size: { base: { $type: "dimension", $value: "14px" } }
9357
- },
9358
- radius: { md: { $type: "dimension", $value: "6px" } },
9359
- space: { unit: { $type: "dimension", $value: "8px" } }
9360
- },
9361
- components: [
9362
- { name: "Button", importPath: "@acme/ui" },
9363
- { name: "TextInput", importPath: "@acme/ui" },
9364
- { name: "Select", importPath: "@acme/ui" },
9365
- { name: "Textarea", importPath: "@acme/ui" }
9366
- ],
9367
- rules: {},
9368
- ignore: []
9369
- };
9370
-
9371
- // ../engine/src/tokens.ts
9372
- var EXT = "dev.brandrail";
9373
- function slug(s) {
9374
- return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Za-z])(\d)/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
9375
- }
9376
- var segs = (name) => name.split(/[/$.]+/).map(slug).filter(Boolean);
9377
- var stripPrefix = (s, ...prefixes) => {
9378
- for (const p of prefixes) if (s === p) return "";
9379
- for (const p of prefixes) if (s.startsWith(`${p}-`)) return s.slice(p.length + 1);
9380
- return s;
9381
- };
9382
- var WEIGHTS = {
9383
- thin: "100",
9384
- hairline: "100",
9385
- "extra-light": "200",
9386
- "ultra-light": "200",
9387
- light: "300",
9388
- regular: "400",
9389
- regulare: "400",
9390
- normal: "400",
9391
- book: "400",
9392
- medium: "500",
9393
- "semi-bold": "600",
9394
- semibold: "600",
9395
- "demi-bold": "600",
9396
- bold: "700",
9397
- "extra-bold": "800",
9398
- "ultra-bold": "800",
9399
- black: "900",
9400
- heavy: "900"
9401
9430
  };
9402
- function fontWeight(v) {
9403
- if (typeof v === "number") return String(v);
9404
- return WEIGHTS[slug(v)] ?? (/^\d{3}$/.test(v.trim()) ? v.trim() : v);
9405
- }
9406
- function colorValue(v) {
9407
- if (v && typeof v === "object" && ("components" in v || "hex" in v)) {
9408
- const c = v;
9409
- const alpha = typeof c.alpha === "number" ? c.alpha : 1;
9410
- const withAlpha = (hex4) => alpha < 1 ? `${hex4.slice(0, 7)}${Math.round(alpha * 255).toString(16).padStart(2, "0")}` : hex4.slice(0, 7);
9411
- if (typeof c.hex === "string" && /^#[0-9a-f]{6}/i.test(c.hex)) return withAlpha(c.hex.toLowerCase());
9412
- const comps = c.components?.map((x2) => typeof x2 === "number" ? x2 : 0);
9413
- if (comps?.length === 3 && (!c.colorSpace || /srgb|display-p3/i.test(c.colorSpace))) {
9414
- return withAlpha(toHex({ r: comps[0] * 255, g: comps[1] * 255, b: comps[2] * 255, a: 1 }));
9415
- }
9416
- return null;
9417
- }
9418
- if (v && typeof v === "object" && "r" in v) {
9419
- const c = v;
9420
- const hex4 = toHex({ r: c.r * 255, g: c.g * 255, b: c.b * 255, a: 1 });
9421
- const a = c.a ?? 1;
9422
- return a < 1 ? `${hex4}${Math.round(a * 255).toString(16).padStart(2, "0")}` : hex4;
9423
- }
9424
- if (typeof v !== "string") return null;
9425
- const s = v.trim();
9426
- if (/^#[0-9a-f]{3,8}$/i.test(s)) {
9427
- const h = s.slice(1).toLowerCase();
9428
- return h.length === 3 || h.length === 4 ? `#${[...h].map((c) => c + c).join("")}` : `#${h}`;
9429
- }
9430
- const rgb = parseColor(s);
9431
- if (!rgb) return null;
9432
- const hex3 = toHex(rgb);
9433
- return rgb.a < 1 ? `${hex3}${Math.round(rgb.a * 255).toString(16).padStart(2, "0")}` : hex3;
9434
- }
9435
- var dim = (v) => {
9436
- if (v && typeof v === "object" && "value" in v) {
9437
- const d = v;
9438
- const n = typeof d.value === "number" ? d.value : parseFloat(d.value);
9439
- return d.unit === "rem" ? `${Math.round(n * 16 * 100) / 100}px` : `${Math.round(n * 100) / 100}px`;
9431
+ var NATIVE_ATTRS = new Set(
9432
+ "id className style title role tabIndex hidden lang dir name value defaultValue defaultChecked checked disabled required readOnly autoFocus autoComplete placeholder type min max step maxLength minLength pattern form accept multiple href target rel download children".split(" ")
9433
+ );
9434
+ var ALWAYS_OK = /* @__PURE__ */ new Set(["key", "ref"]);
9435
+ function importedComponents(code, components) {
9436
+ const out = /* @__PURE__ */ new Map();
9437
+ for (const m of matches(/\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\2/g, code)) {
9438
+ const from = m[3];
9439
+ for (const part of m[1].split(",")) {
9440
+ const [orig, alias] = part.trim().replace(/^type\s+/, "").split(/\s+as\s+/).map((s) => s.trim());
9441
+ if (!orig) continue;
9442
+ const comp = components.find((c) => c.name === orig && c.importPath === from && c.props?.length);
9443
+ if (comp) out.set(alias || orig, comp);
9444
+ }
9440
9445
  }
9441
- return typeof v === "number" ? `${Math.round(v * 100) / 100}px` : String(v).trim();
9442
- };
9443
- function classify(name, kind, value, opts = {}) {
9444
- const parts = segs(name);
9445
- const hay = slug(`${opts.context ?? ""} ${name}`);
9446
- const scopes = opts.scopes ?? [];
9447
- const last = parts[parts.length - 1] ?? "";
9448
- const rest = parts.slice(0, -1);
9449
- if (kind === "boolean") return null;
9450
- if (kind === "color") {
9451
- const v2 = colorValue(value);
9452
- if (!v2) return null;
9453
- let p = parts[0] === "color" || parts[0] === "colors" ? parts.slice(1) : parts;
9454
- if (p.length === 1) p = [stripPrefix(p[0], "colors", "color") || p[0]];
9455
- const tail = p[p.length - 1];
9456
- const m = tail.match(/^(.*[a-z])-(\d+)$/);
9457
- if (m && p.length <= 2) p = [...p.slice(0, -1), m[1], m[2]];
9458
- return { path: ["color", ...p].join("."), type: "color", value: v2 };
9446
+ return out;
9447
+ }
9448
+ function skipBalanced(code, i2) {
9449
+ const open2 = code[i2];
9450
+ if (open2 === '"' || open2 === "'") {
9451
+ const end = code.indexOf(open2, i2 + 1);
9452
+ return end === -1 ? code.length : end + 1;
9459
9453
  }
9460
- if (kind === "string") {
9461
- const s = String(value);
9462
- if (scopes.includes("FONT_FAMILY") || /font-family|typeface|font-name/.test(hay) || last === "font") {
9463
- const n = stripPrefix(last, "font-family", "font", "family") || "sans";
9464
- return { path: `font.family.${/mono|code/.test(s.toLowerCase()) && n === "sans" ? "mono" : n}`, type: "fontFamily", value: s };
9454
+ let depth = 0;
9455
+ let quote = null;
9456
+ for (let j = i2; j < code.length; j++) {
9457
+ const c = code[j];
9458
+ if (quote) {
9459
+ if (c === "\\") j++;
9460
+ else if (c === quote) quote = null;
9461
+ continue;
9465
9462
  }
9466
- if (scopes.includes("FONT_STYLE") || scopes.includes("FONT_WEIGHT") || /font-weight|weight/.test(hay)) {
9467
- return { path: `font.weight.${stripPrefix(last, "font-weight", "weight") || last}`, type: "fontWeight", value: fontWeight(s) };
9463
+ if (c === '"' || c === "'" || c === "`") quote = c;
9464
+ else if (c === "{") depth++;
9465
+ else if (c === "}" && --depth === 0) return j + 1;
9466
+ }
9467
+ return code.length;
9468
+ }
9469
+ function parseAttributes(code, start) {
9470
+ const attrs = [];
9471
+ let spread = false;
9472
+ let i2 = start;
9473
+ while (i2 < code.length) {
9474
+ while (/\s/.test(code[i2] ?? "")) i2++;
9475
+ const c = code[i2];
9476
+ if (c === void 0) return null;
9477
+ if (c === ">") return { attrs, spread, selfClosing: false };
9478
+ if (c === "/" && code[i2 + 1] === ">") return { attrs, spread, selfClosing: true };
9479
+ if (c === "{") {
9480
+ spread = true;
9481
+ i2 = skipBalanced(code, i2);
9482
+ continue;
9483
+ }
9484
+ const name = /^[A-Za-z_$][\w$:.-]*/.exec(code.slice(i2, i2 + 80))?.[0];
9485
+ if (!name) return null;
9486
+ attrs.push({ name, offset: i2 });
9487
+ i2 += name.length;
9488
+ while (/\s/.test(code[i2] ?? "")) i2++;
9489
+ if (code[i2] === "=") {
9490
+ i2++;
9491
+ while (/\s/.test(code[i2] ?? "")) i2++;
9492
+ i2 = skipBalanced(code, i2);
9468
9493
  }
9469
- const c = colorValue(s);
9470
- if (c && /^#|^rgb|^hsl/i.test(s.trim())) return classify(name, "color", s, opts);
9471
- return null;
9472
9494
  }
9473
- const v = dim(value);
9474
- const has = (...scope) => scope.some((x2) => scopes.includes(x2));
9475
- const nm = (...prefixes) => stripPrefix(last, ...prefixes) || last;
9476
- if (has("CORNER_RADIUS") || /radius|(^|-)rad(-|$)|corner|rounded/.test(hay)) return { path: `radius.${nm("radius", "rad", "corner", "rounded")}`, type: "dimension", value: v };
9477
- if (has("FONT_SIZE") || /font-size|text-size/.test(hay)) return { path: `font.size.${nm("font-size", "text-size", "size")}`, type: "dimension", value: v };
9478
- if (has("LINE_HEIGHT") || /line-height|leading/.test(hay)) return { path: `font.line-height.${nm("line-height", "leading")}`, type: "dimension", value: v };
9479
- if (has("LETTER_SPACING") || /letter-spacing|tracking/.test(hay)) return { path: `font.letter-spacing.${nm("letter-spacing", "tracking")}`, type: "dimension", value: v };
9480
- if (has("FONT_WEIGHT") || /font-weight/.test(hay)) return { path: `font.weight.${nm("font-weight", "weight")}`, type: "fontWeight", value: String(value) };
9481
- if (/icon/.test(last)) return { path: `size.icon.${nm("icon")}`, type: "dimension", value: v };
9482
- if (has("GAP") || /spac|gap|padding|margin|inset|(^|-)space(-|$)/.test(hay)) return { path: `space.${nm("spacing", "space", "gap")}`, type: "dimension", value: v };
9483
- if (has("STROKE_FLOAT") || /stroke|border-width/.test(hay)) return { path: `border.width.${nm("border-width", "stroke")}`, type: "dimension", value: v };
9484
- if (has("WIDTH_HEIGHT") || /size|width|height/.test(hay)) return { path: ["size", ...rest.filter((x2) => x2 !== "size"), last].join("."), type: "dimension", value: v };
9485
- return { path: ["number", ...parts].join("."), type: "number", value: String(value) };
9495
+ return null;
9486
9496
  }
9487
- function rawLeaves(node2, prefix, out, studio, inherited) {
9488
- if (!node2 || typeof node2 !== "object" || Array.isArray(node2)) return;
9489
- const o = node2;
9490
- const groupType = o.$type ?? inherited;
9491
- const isLeaf = "$value" in o || studio && "value" in o && (typeof o.value !== "object" || o.value === null || "type" in o);
9492
- if (isLeaf) {
9493
- const ext = o.$extensions?.[EXT];
9494
- out.push({
9495
- path: prefix,
9496
- type: o.$type ?? o.type ?? groupType,
9497
- value: "$value" in o ? o.$value : o.value,
9498
- description: o.$description ?? o.description,
9499
- dark: ext?.modes?.dark
9500
- });
9501
- return;
9497
+ function parseHtmlAttributes(code, start) {
9498
+ const attrs = [];
9499
+ let i2 = start;
9500
+ while (i2 < code.length) {
9501
+ while (/\s/.test(code[i2] ?? "")) i2++;
9502
+ const c = code[i2];
9503
+ if (c === void 0) return null;
9504
+ if (c === ">" || c === "/" && code[i2 + 1] === ">") return { attrs, end: i2 };
9505
+ if (c === "{" || c === "<" && code[i2 + 1] === "?") {
9506
+ const close = c === "{" ? code[i2 + 1] === "%" ? "%}" : "}}" : "?>";
9507
+ const e = code.indexOf(close, i2 + 2);
9508
+ i2 = e === -1 ? code.length : e + close.length;
9509
+ continue;
9510
+ }
9511
+ const name = /^[^\s=>/"'{]+/.exec(code.slice(i2, i2 + 120))?.[0];
9512
+ if (!name) return null;
9513
+ attrs.push({ name, offset: i2 });
9514
+ i2 += name.length;
9515
+ while (/\s/.test(code[i2] ?? "")) i2++;
9516
+ if (code[i2] === "=") {
9517
+ i2++;
9518
+ while (/\s/.test(code[i2] ?? "")) i2++;
9519
+ const q = code[i2];
9520
+ if (q === '"' || q === "'") {
9521
+ const e = code.indexOf(q, i2 + 1);
9522
+ i2 = e === -1 ? code.length : e + 1;
9523
+ } else while (i2 < code.length && !/[\s>]/.test(code[i2])) i2++;
9524
+ }
9502
9525
  }
9503
- for (const [k, v] of Object.entries(o)) if (!k.startsWith("$")) rawLeaves(v, prefix ? `${prefix}.${k}` : k, out, studio, groupType);
9526
+ return null;
9504
9527
  }
9505
- var REF = /^\{([^}]+)\}$/;
9506
- function resolveRefs(raws) {
9507
- const byPath = new Map(raws.map((r) => [r.path, r]));
9508
- const deref = (v, mode, depth = 0) => {
9509
- if (typeof v !== "string" || depth > 10) return v;
9510
- const m = v.trim().match(REF);
9511
- if (!m) return v.replace(/\{([^}]+)\}/g, (all, p) => String(byPath.get(p)?.value ?? all));
9512
- const target = byPath.get(m[1]);
9513
- if (!target) return v;
9514
- return deref(mode === "dark" && target.dark !== void 0 ? target.dark : target.value, mode, depth + 1);
9515
- };
9516
- return raws.map((r) => ({ ...r, value: deref(r.value, "light"), dark: r.dark !== void 0 ? deref(r.dark, "dark") : void 0 }));
9528
+ 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) {
9530
+ let n = raw;
9531
+ if (/^(?:\(|@|on-|v-on:|\*|#|let-|ref-|v-(?!bind:|model:))/.test(n)) return null;
9532
+ n = n.replace(/^v-bind:|^v-model:|^bind-|^:/, "").replace(/^\[\(?|\)?\]$/g, "");
9533
+ if (/^(?:attr|class|style)\./.test(n) || /^ng[A-Z]/.test(n) || /^(?:formControl|formControlName|formGroup|routerLink|routerLinkActive)$/.test(n)) return null;
9534
+ n = n.replace(/\.(?:px|rem|em|%)$/, "").replace(/\..*$/, "");
9535
+ if (HTML_GLOBAL.test(n.toLowerCase())) return null;
9536
+ return n;
9517
9537
  }
9518
- var TYPE_KIND = {
9519
- color: "color",
9520
- dimension: "number",
9521
- spacing: "number",
9522
- sizing: "number",
9523
- borderradius: "number",
9524
- fontsizes: "number",
9525
- fontsize: "number",
9526
- lineheights: "number",
9527
- lineheight: "number",
9528
- letterspacing: "number",
9529
- number: "number",
9530
- fontfamilies: "string",
9531
- fontfamily: "string",
9532
- fontweights: "string",
9533
- fontweight: "string"
9534
- };
9535
- function flattenTokens(doc) {
9536
- const raws = resolveRefs(collectRaw(doc).raws);
9538
+ var sameName = (a, b) => a.replace(/-/g, "").toLowerCase() === b.replace(/-/g, "").toLowerCase();
9539
+ var NATIVE_EVENT = /^(?:click|dblclick|auxclick|contextmenu|focus|blur|focusin|focusout|key(?:down|up|press)|mouse\w+|pointer\w+|touch\w+|wheel|drag\w*|drop|scroll|animation\w+|transition\w+)$/;
9540
+ function eventName(raw) {
9541
+ const m = /^\(([^)]+)\)$|^on-(.+)$|^@([^.]+)|^v-on:([^.]+)/.exec(raw);
9542
+ return m ? (m[1] ?? m[2] ?? m[3] ?? m[4]).replace(/\..*$/, "") : null;
9543
+ }
9544
+ function templateProps({ code, config: config2, framework }) {
9545
+ const tagged = [];
9546
+ for (const comp of config2.components) {
9547
+ const u = resolveUsage(comp, framework);
9548
+ if (u && (u.props?.length || u.events?.length)) tagged.push({ comp, tag: u.tag, props: u.props, events: u.events, ce: Boolean(u.customElement || u.via) });
9549
+ }
9550
+ if (!tagged.length) return [];
9537
9551
  const out = [];
9538
- for (const r of raws) {
9539
- const t = (r.type ?? "").toLowerCase().replace(/[^a-z]/g, "");
9540
- if (t === "color" || !t && r.path.startsWith("color")) {
9541
- const v = colorValue(r.value);
9542
- if (!v) continue;
9543
- const d = r.dark !== void 0 ? colorValue(r.dark) : null;
9544
- out.push({ path: r.path, type: "color", value: v, ...d ? { dark: d } : {}, ...r.description ? { description: r.description } : {} });
9545
- } else if (typeof r.value === "string" || typeof r.value === "number" || r.value && typeof r.value === "object" && "unit" in r.value) {
9546
- 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";
9547
- out.push({ path: r.path, type, value: type === "dimension" ? dim(r.value) : String(r.value), ...r.description ? { description: r.description } : {} });
9552
+ const forms = /* @__PURE__ */ new Map();
9553
+ for (const t of tagged) {
9554
+ forms.set(t.tag, t);
9555
+ if (framework === "vue") forms.set(t.tag.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), t);
9556
+ }
9557
+ const names = [...forms.keys()].sort((a, b) => b.length - a.length).map(escapeRe).join("|");
9558
+ for (const m of matches(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
9559
+ const t = forms.get(m[1]);
9560
+ const parsed = parseHtmlAttributes(code, m.index + m[0].length);
9561
+ if (!parsed) continue;
9562
+ const list = (t.props ?? []).map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
9563
+ const passed = [];
9564
+ for (const a of parsed.attrs) {
9565
+ const raw = a.name;
9566
+ const ev = eventName(raw);
9567
+ if (ev) {
9568
+ if (!t.events?.length || NATIVE_EVENT.test(ev) || t.events.some((e) => e.name === ev)) continue;
9569
+ const like = t.events.find((e) => e.name.endsWith(`-${ev}`) || e.name.endsWith(ev)) ?? (t.events.length === 1 ? t.events[0] : void 0);
9570
+ out.push({
9571
+ offset: a.offset,
9572
+ message: `<${t.tag}> has no "${ev}" event`,
9573
+ suggestion: `${like ? `Listen to ${framework === "angular" ? `(${like.name})` : framework === "vue" ? `@${like.name}` : `"${like.name}"`}${like.detail ? ` and read ${framework === "angular" ? "$any($event).detail" : framework === "vue" ? "$event.detail" : "event.detail"} (${like.detail})` : ""}. ` : ""}Events: ${t.events.map((e) => e.name).join(", ")}`
9574
+ });
9575
+ continue;
9576
+ }
9577
+ if (/^v-model$/.test(raw)) passed.push("modelValue", "value");
9578
+ if (/^\[\(ngModel\)\]$/.test(raw)) passed.push("value");
9579
+ const n = propName(raw);
9580
+ if (!n || !t.props?.length) continue;
9581
+ passed.push(n);
9582
+ if (t.ce && n.includes("-") && /^(?:\[(?!attr\.)|:|v-bind:|bind-)/.test(raw)) {
9583
+ const camel2 = n.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
9584
+ out.push({
9585
+ offset: a.offset,
9586
+ message: `${raw} binds a property that doesn't exist on <${t.tag}>`,
9587
+ suggestion: `Bind the camelCase property: ${framework === "vue" ? `:${camel2}` : `[${camel2}]`}="\u2026" (kebab-case is only for static attributes: ${n}="\u2026")`
9588
+ });
9589
+ continue;
9590
+ }
9591
+ if (t.props.some((p) => sameName(p.name, n))) continue;
9592
+ out.push({ offset: a.offset, message: `<${t.tag}> has no "${n}" input`, suggestion: `Remove it. ${t.comp.name} takes: ${list}` });
9593
+ }
9594
+ for (const p of t.props ?? []) {
9595
+ if (!p.required || passed.some((n) => sameName(n, p.name))) continue;
9596
+ if (p.name === "children" || p.name === "content") continue;
9597
+ out.push({ offset: m.index, message: `<${t.tag}> is missing the required "${p.name}" input`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
9548
9598
  }
9549
9599
  }
9550
9600
  return out;
9551
9601
  }
9552
- function collectRaw(doc) {
9553
- const o = doc ?? {};
9554
- const studio = !hasDollarValue(o) && hasStudioValue(o);
9555
- const order = o.$metadata?.tokenSetOrder;
9556
- if (!studio) {
9557
- const raws = [];
9558
- rawLeaves(o, "", raws, false);
9559
- return { raws, studio };
9560
- }
9561
- const setNames = (order ?? Object.keys(o).filter((k) => !k.startsWith("$"))).filter((k) => o[k] && typeof o[k] === "object");
9562
- const looksLikeSets = setNames.length > 0 && setNames.every((k) => !hasLeafAtTop(o[k]));
9563
- if (!looksLikeSets) {
9564
- const raws = [];
9565
- rawLeaves(o, "", raws, true);
9566
- return { raws, studio };
9602
+ var componentProps = {
9603
+ id: "component-props",
9604
+ title: "Unknown or missing prop",
9605
+ description: "Design system components only accept their documented props, and required props must be passed.",
9606
+ defaultSeverity: "error",
9607
+ kinds: MARKUP,
9608
+ check(ctx) {
9609
+ if (ctx.framework !== "react" || ctx.kind !== "jsx") return templateProps(ctx);
9610
+ const { code, config: config2 } = ctx;
9611
+ if (!config2.components.some((c) => c.props?.length)) return [];
9612
+ const imported = importedComponents(code, config2.components);
9613
+ if (imported.size === 0) return [];
9614
+ const out = [];
9615
+ const names = [...imported.keys()].map((n) => n.replace(/[$]/g, "\\$")).join("|");
9616
+ for (const m of matches(new RegExp(`<(${names})(?=[\\s/>])`, "g"), code)) {
9617
+ if (/[\w)\]]\s*$/.test(code.slice(Math.max(0, m.index - 20), m.index))) continue;
9618
+ const comp = imported.get(m[1]);
9619
+ const parsed = parseAttributes(code, m.index + m[0].length);
9620
+ if (!parsed) continue;
9621
+ const own2 = new Map(comp.props.map((p) => [p.name, p]));
9622
+ const list = comp.props.map((p) => p.required ? `${p.name} (required)` : p.name).join(", ");
9623
+ for (const a of parsed.attrs) {
9624
+ if (own2.has(a.name) || ALWAYS_OK.has(a.name) || a.name.includes("-")) continue;
9625
+ if (comp.htmlAttributes && (NATIVE_ATTRS.has(a.name) || /^on[A-Z]/.test(a.name) || /^aria/.test(a.name))) continue;
9626
+ const sibling = config2.components.find(
9627
+ (c) => c !== comp && c.importPath === comp.importPath && (c.name.startsWith(comp.name) || comp.name.startsWith(c.name)) && c.props?.some((p) => p.name === a.name)
9628
+ );
9629
+ out.push({
9630
+ offset: a.offset,
9631
+ message: `<${comp.name}> has no "${a.name}" prop`,
9632
+ suggestion: sibling ? `Use <${sibling.name}>, which has "${a.name}", or remove it. ${comp.name} props: ${list}` : `Remove it. ${comp.name} props: ${list}`
9633
+ });
9634
+ }
9635
+ if (!parsed.spread) {
9636
+ const passed = new Set(parsed.attrs.map((a) => a.name));
9637
+ for (const p of comp.props) {
9638
+ if (!p.required || passed.has(p.name) || p.name === "children" && !parsed.selfClosing) continue;
9639
+ out.push({ offset: m.index, message: `<${comp.name}> is missing the required "${p.name}" prop`, suggestion: `Add ${p.name}${p.type ? ` (${p.type})` : ""}` });
9640
+ }
9641
+ }
9642
+ }
9643
+ return out;
9567
9644
  }
9568
- const merged = /* @__PURE__ */ new Map();
9569
- const darkSets = setNames.filter((n) => /dark/i.test(n));
9570
- for (const set2 of setNames.filter((n) => !/dark/i.test(n))) {
9571
- const raws = [];
9572
- rawLeaves(o[set2], "", raws, true);
9573
- for (const r of raws) merged.set(r.path, r);
9645
+ };
9646
+ var legacyLibrary = {
9647
+ id: "legacy-library",
9648
+ title: "Other UI library in use",
9649
+ description: "Pieces of Material, MUI, Bootstrap, Ant Design, Chakra or PrimeNG that a design system component replaces. The count is the migration still to do.",
9650
+ defaultSeverity: "warning",
9651
+ kinds: ["jsx", "vue", "svelte", "html", "template"],
9652
+ check({ code, kind, config: config2, framework }) {
9653
+ const out = [];
9654
+ for (const u of findLegacyUsages(code, kind)) {
9655
+ const comp = componentForConcept(config2, u.concept);
9656
+ const usage = comp ? resolveUsage(comp, framework) : null;
9657
+ out.push({
9658
+ offset: u.offset,
9659
+ message: `${u.what} (${LIBRARY_LABEL[u.library]}) should be ${comp ? comp.name : `a design system ${u.concept}`}`,
9660
+ suggestion: comp && usage ? useSuggestion(comp, usage) : comp ? `${comp.name} has no ${FRAMEWORK_LABEL[framework]} version yet: add one in the registry, or build it with tokens` : `No ${u.concept} component in the registry yet: add one, or build it with tokens and semantic HTML`
9661
+ });
9662
+ }
9663
+ return out;
9574
9664
  }
9575
- for (const set2 of darkSets) {
9576
- const raws = [];
9577
- rawLeaves(o[set2], "", raws, true);
9578
- for (const r of raws) {
9579
- const base = merged.get(r.path);
9580
- if (base) base.dark = r.value;
9581
- else merged.set(r.path, r);
9665
+ };
9666
+ var whenGone = (d) => d.removeAfter ? ` and will be removed after ${d.removeAfter}` : "";
9667
+ var deprecatedToken = {
9668
+ id: "deprecated-token",
9669
+ title: "Deprecated token",
9670
+ description: "Tokens the design system is retiring. They still work until their removal date; switch to the replacement.",
9671
+ defaultSeverity: "warning",
9672
+ kinds: ALL,
9673
+ check({ code, config: config2 }) {
9674
+ if (!config2.deprecatedTokens.size) return [];
9675
+ const out = [];
9676
+ for (const m of matches(/var\(\s*(--[\w-]+)/g, code)) {
9677
+ const d = config2.deprecatedTokens.get(m[1]);
9678
+ if (!d) continue;
9679
+ const repl = d.replacement ? `var(--${d.replacement.replace(/\./g, "-")})` : void 0;
9680
+ out.push({
9681
+ offset: m.index,
9682
+ message: `${m[1]} (${d.path}) is deprecated${whenGone(d)}`,
9683
+ suggestion: repl ? `Use ${repl}${d.reason ? `. ${d.reason}` : ""}` : d.reason || "Ask the design system team what replaces it"
9684
+ });
9582
9685
  }
9686
+ return out;
9583
9687
  }
9584
- return { raws: [...merged.values()], studio };
9585
- }
9586
- function hasDollarValue(node2, depth = 0) {
9587
- if (!node2 || typeof node2 !== "object" || depth > 12) return false;
9588
- if ("$value" in node2) return true;
9589
- return Object.values(node2).some((v) => hasDollarValue(v, depth + 1));
9590
- }
9591
- function hasStudioValue(node2, depth = 0) {
9592
- if (!node2 || typeof node2 !== "object" || depth > 12) return false;
9593
- const o = node2;
9594
- if ("value" in o && "type" in o) return true;
9595
- return Object.values(o).some((v) => hasStudioValue(v, depth + 1));
9596
- }
9597
- function hasLeafAtTop(node2) {
9598
- return !!node2 && typeof node2 === "object" && ("value" in node2 || "$value" in node2);
9599
- }
9600
- function buildDtcg(tokens) {
9601
- const tree = {};
9602
- for (const t of tokens) {
9603
- const parts = t.path.split(".");
9604
- let node2 = tree;
9605
- parts.forEach((p, i2) => {
9606
- if (i2 === parts.length - 1) {
9607
- node2[p] = {
9608
- $type: t.type,
9609
- $value: t.value,
9610
- ...t.description ? { $description: t.description } : {},
9611
- ...t.dark && t.dark !== t.value ? { $extensions: { [EXT]: { modes: { dark: t.dark } } } } : {}
9612
- };
9613
- } else {
9614
- const next = node2[p];
9615
- if (next && typeof next === "object" && "$value" in next) node2[p] = { default: next };
9616
- node2 = node2[p] ??= {};
9688
+ };
9689
+ var deprecatedComponent = {
9690
+ id: "deprecated-component",
9691
+ title: "Deprecated component",
9692
+ description: "Components the design system is retiring. They still work until their removal date; switch to the replacement.",
9693
+ defaultSeverity: "warning",
9694
+ kinds: MARKUP,
9695
+ check({ code, kind, config: config2, framework }) {
9696
+ const dep = config2.components.filter((c) => c.status === "deprecated");
9697
+ if (!dep.length) return [];
9698
+ const out = [];
9699
+ const report = (offset, c) => {
9700
+ const d = c.deprecation ?? {};
9701
+ out.push({
9702
+ offset,
9703
+ message: `${c.name} is deprecated${whenGone(d)}`,
9704
+ suggestion: d.replacement ? `Use ${d.replacement}${d.reason ? `. ${d.reason}` : ""}` : d.reason || "Ask the design system team what replaces it"
9705
+ });
9706
+ };
9707
+ if (framework === "react" && kind === "jsx") {
9708
+ for (const m of matches(/\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\2/g, code)) {
9709
+ for (const part of m[1].split(",")) {
9710
+ const [orig, alias] = part.trim().replace(/^type\s+/, "").split(/\s+as\s+/).map((s) => s.trim());
9711
+ const c = dep.find((x2) => x2.name === orig && x2.importPath === m[3]);
9712
+ if (!c) continue;
9713
+ const local = (alias || orig).replace(/[$]/g, "\\$");
9714
+ let used = false;
9715
+ for (const u of matches(new RegExp(`<${local}(?=[\\s/>])`, "g"), code)) {
9716
+ used = true;
9717
+ report(u.index, c);
9718
+ }
9719
+ if (!used) report(m.index, c);
9720
+ }
9617
9721
  }
9618
- });
9619
- }
9620
- return tree;
9621
- }
9622
- var isAlias = (v) => !!v && typeof v === "object" && v.type === "VARIABLE_ALIAS";
9623
- var DARK = /dark|night/i;
9624
- var LIGHT = /light|default|day|base/i;
9625
- function finish(format, all, notes, opts) {
9626
- const PALETTE2 = /^(color\.[a-z-]+)\.\d{1,3}$/;
9627
- const usedFamilies = new Set(all.filter((t) => t.referenced).map((t) => t.path.match(PALETTE2)?.[1]).filter(Boolean));
9628
- if (usedFamilies.size) {
9629
- const steps = /* @__PURE__ */ new Map();
9630
- for (const t of all) {
9631
- const f = t.path.match(PALETTE2)?.[1];
9632
- if (f) steps.set(f, (steps.get(f) ?? 0) + 1);
9722
+ return out;
9633
9723
  }
9634
- for (const t of all) {
9635
- const f = t.path.match(PALETTE2)?.[1];
9636
- if (f && (usedFamilies.has(f) || (steps.get(f) ?? 0) >= 3)) t.referenced = true;
9724
+ for (const c of dep) {
9725
+ const u = resolveUsage(c, framework);
9726
+ if (!u) continue;
9727
+ const tag = u.tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9728
+ for (const m of matches(new RegExp(`<${tag}(?=[\\s/>])`, "g"), code)) report(m.index, c);
9637
9729
  }
9730
+ return out;
9638
9731
  }
9639
- const hasSemantic = all.some((t) => !t.referenced) && all.some((t) => t.referenced);
9640
- const keep = opts.includePrimitives || !hasSemantic ? all : all.filter((t) => !t.referenced);
9641
- const primitives = hasSemantic ? all.filter((t) => t.referenced).length : 0;
9642
- const seen = /* @__PURE__ */ new Set();
9643
- const tokens = [];
9644
- for (const { referenced: _r, ...t } of keep) {
9645
- if (seen.has(t.path)) continue;
9646
- seen.add(t.path);
9647
- tokens.push(t);
9732
+ };
9733
+ var RULES = [
9734
+ noHardcodedColor,
9735
+ noRawPaletteClass,
9736
+ preferOfficialComponent,
9737
+ forbiddenImport,
9738
+ spacingScale,
9739
+ fontFamily,
9740
+ radiusScale,
9741
+ imgAlt,
9742
+ componentProps,
9743
+ legacyLibrary,
9744
+ deprecatedToken,
9745
+ deprecatedComponent
9746
+ ];
9747
+ var RULE_IDS = RULES.map((r) => r.id);
9748
+
9749
+ // ../engine/src/source.ts
9750
+ var TEMPLATE_EXTENSIONS = ["mustache", "hbs", "handlebars", "php", "phtml", "twig", "njk", "liquid"];
9751
+ function fileKind(path) {
9752
+ const ext = path.toLowerCase().split(".").pop() ?? "";
9753
+ if (TEMPLATE_EXTENSIONS.includes(ext)) return "template";
9754
+ if (["tsx", "jsx", "ts", "js", "mjs", "cjs", "mdx"].includes(ext)) return "jsx";
9755
+ if (ext === "vue") return "vue";
9756
+ if (ext === "svelte") return "svelte";
9757
+ if (["html", "htm", "astro"].includes(ext)) return "html";
9758
+ if (["css", "scss", "sass", "less", "pcss"].includes(ext)) return "css";
9759
+ return "other";
9760
+ }
9761
+ var SCANNABLE = /* @__PURE__ */ new Set(["jsx", "vue", "svelte", "html", "template", "css"]);
9762
+ var ANGULAR_COMPONENT = /@Component\s*\(\s*\{/;
9763
+ var ANGULAR_TEMPLATE = /\s(?:\[[\w.()-]+\]|\([\w.]+\)|\*ng[A-Z]\w*)\s*=|@(?:if|for|switch)\s*\(/;
9764
+ function fileFramework(path, kind, code, project) {
9765
+ const p = path.toLowerCase();
9766
+ switch (kind) {
9767
+ case "vue":
9768
+ return "vue";
9769
+ case "svelte":
9770
+ return "web-components";
9771
+ case "template":
9772
+ return "html";
9773
+ case "css":
9774
+ return project ?? "react";
9775
+ case "html":
9776
+ if (p.endsWith(".component.html") || project === "angular" || ANGULAR_TEMPLATE.test(code)) return "angular";
9777
+ return project === "vue" || project === "web-components" ? project : "html";
9778
+ case "jsx":
9779
+ if (ANGULAR_COMPONENT.test(code)) return "angular";
9780
+ if (/\.(tsx|jsx|mdx)$/.test(p)) return "react";
9781
+ return project ?? "react";
9782
+ default:
9783
+ return project ?? "react";
9648
9784
  }
9649
- tokens.sort((a, b) => a.path.localeCompare(b.path, "en", { numeric: true }));
9650
- return { format, tokens: buildDtcg(tokens), count: tokens.length, primitives, notes };
9651
9785
  }
9652
- function fromFigmaVariables(meta3, opts = {}) {
9653
- const vars = meta3.variables ?? {};
9654
- const cols = meta3.variableCollections ?? {};
9655
- const notes = [];
9656
- const referenced = /* @__PURE__ */ new Set();
9657
- const modeFor = (col, want, likeName) => {
9658
- if (likeName) {
9659
- const same = col.modes.find((m) => m.name.toLowerCase() === likeName.toLowerCase());
9660
- if (same) return same.modeId;
9661
- }
9662
- if (want === "dark") return col.modes.find((m) => DARK.test(m.name))?.modeId;
9663
- return col.defaultModeId ?? col.modes[0]?.modeId;
9664
- };
9665
- const resolve4 = (v, want, modeName, depth = 0) => {
9666
- const col = cols[v.variableCollectionId];
9667
- if (!col) return void 0;
9668
- const modeId = modeFor(col, want, modeName) ?? col.defaultModeId;
9669
- const raw = v.valuesByMode[modeId] ?? Object.values(v.valuesByMode)[0];
9670
- if (!isAlias(raw)) return raw;
9671
- referenced.add(raw.id);
9672
- const target = vars[raw.id];
9673
- if (!target || depth > 10) return void 0;
9674
- const name = col.modes.find((m) => m.modeId === modeId)?.name;
9675
- return resolve4(target, want, name, depth + 1);
9676
- };
9677
- for (const v of Object.values(vars)) for (const val of Object.values(v.valuesByMode)) if (isAlias(val)) referenced.add(val.id);
9678
- for (const col of Object.values(cols)) {
9679
- if (col.remote) continue;
9680
- const dark = col.modes.find((m) => DARK.test(m.name));
9681
- const others = col.modes.filter((m) => m.modeId !== col.defaultModeId && m !== dark);
9682
- if (others.length) {
9683
- const def = col.modes.find((m) => m.modeId === col.defaultModeId)?.name;
9684
- notes.push(`\u201C${col.name}\u201D has modes ${col.modes.map((m) => m.name).join(", ")}; used \u201C${def}\u201D${dark ? ` (and \u201C${dark.name}\u201D as dark)` : ""}.`);
9786
+ var blank = (s) => s.replace(/[^\n]/g, " ");
9787
+ function stripComments(code, kind) {
9788
+ let out = code;
9789
+ if (kind === "html" || kind === "vue" || kind === "svelte" || kind === "template") {
9790
+ out = out.replace(/<!--[\s\S]*?-->/g, blank);
9791
+ }
9792
+ if (kind === "template") {
9793
+ 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));
9794
+ return out;
9795
+ }
9796
+ if (kind === "css") {
9797
+ return out.replace(/\/\*[\s\S]*?\*\//g, blank).replace(/(^|[^:])\/\/[^\n]*/g, (m, p1) => p1 + blank(m.slice(p1.length)));
9798
+ }
9799
+ const chars = out.split("");
9800
+ let i2 = 0;
9801
+ let quote = null;
9802
+ while (i2 < chars.length) {
9803
+ const c = chars[i2];
9804
+ const n = chars[i2 + 1];
9805
+ if (quote) {
9806
+ if (c === "\\") i2 += 2;
9807
+ else {
9808
+ if (c === quote) quote = null;
9809
+ i2++;
9810
+ }
9811
+ continue;
9685
9812
  }
9686
- }
9687
- const out = [];
9688
- let hidden = 0;
9689
- for (const v of Object.values(vars)) {
9690
- const col = cols[v.variableCollectionId];
9691
- if (!col || v.remote || col.remote) continue;
9692
- if (v.hiddenFromPublishing) {
9693
- hidden++;
9813
+ if (c === '"' || c === "'" || c === "`") {
9814
+ quote = c;
9815
+ i2++;
9694
9816
  continue;
9695
9817
  }
9696
- const kind = v.resolvedType === "COLOR" ? "color" : v.resolvedType === "FLOAT" ? "number" : v.resolvedType === "STRING" ? "string" : "boolean";
9697
- const value = resolve4(v, "default");
9698
- if (value === void 0) continue;
9699
- const c = classify(v.name, kind, value, { scopes: v.scopes?.filter((s) => s !== "ALL_SCOPES"), context: col.name });
9700
- if (!c) continue;
9701
- let darkValue;
9702
- if (c.type === "color") {
9703
- const hasDarkSomewhere = Object.values(cols).some((x2) => x2.modes.some((m) => DARK.test(m.name)));
9704
- if (hasDarkSomewhere) {
9705
- const d = colorValue(resolve4(v, "dark"));
9706
- if (d && d !== c.value) darkValue = d;
9707
- }
9818
+ if (c === "/" && n === "*") {
9819
+ const end = out.indexOf("*/", i2 + 2);
9820
+ const stop = end === -1 ? chars.length : end + 2;
9821
+ for (let j = i2; j < stop; j++) if (chars[j] !== "\n") chars[j] = " ";
9822
+ i2 = stop;
9823
+ continue;
9708
9824
  }
9709
- out.push({ ...c, ...darkValue ? { dark: darkValue } : {}, ...v.description ? { description: v.description } : {}, referenced: referenced.has(v.id) });
9710
- }
9711
- if (hidden) notes.push(`${hidden} variable${hidden === 1 ? "" : "s"} hidden from publishing were skipped.`);
9712
- return finish("figma-variables", out, notes, opts);
9713
- }
9714
- function fromFigmaStyles(styles, opts = {}) {
9715
- const out = [];
9716
- const families = /* @__PURE__ */ new Map();
9717
- let skipped = 0;
9718
- for (const s of styles) {
9719
- if (s.styleType === "FILL") {
9720
- const fill = s.node?.fills?.find((f) => f.type === "SOLID" && f.visible !== false && f.color);
9721
- if (!fill?.color) {
9722
- skipped++;
9723
- continue;
9724
- }
9725
- const c = classify(s.name, "color", { ...fill.color, a: (fill.color.a ?? 1) * (fill.opacity ?? 1) });
9726
- if (c) out.push({ ...c, ...s.description ? { description: s.description } : {} });
9727
- } else if (s.styleType === "TEXT" && s.node?.style) {
9728
- const st = s.node.style;
9729
- const name = segs(s.name).join("-");
9730
- if (st.fontSize) out.push({ path: `font.size.${name}`, type: "dimension", value: dim(st.fontSize) });
9731
- if (st.lineHeightPx) out.push({ path: `font.line-height.${name}`, type: "dimension", value: dim(Math.round(st.lineHeightPx)) });
9732
- if (st.fontWeight) out.push({ path: `font.weight.${name}`, type: "fontWeight", value: String(st.fontWeight) });
9733
- if (st.fontFamily) families.set(st.fontFamily, (families.get(st.fontFamily) ?? 0) + 1);
9825
+ if (c === "/" && n === "/" && chars[i2 - 1] !== ":" && kind === "jsx") {
9826
+ while (i2 < chars.length && chars[i2] !== "\n") chars[i2++] = " ";
9827
+ continue;
9734
9828
  }
9829
+ i2++;
9735
9830
  }
9736
- const fams = [...families.entries()].sort((a, b) => b[1] - a[1]);
9737
- fams.forEach(([f], i2) => out.push({ path: `font.family.${i2 === 0 ? "sans" : /mono|code/i.test(f) ? "mono" : slug(f)}`, type: "fontFamily", value: f }));
9738
- const notes = skipped ? [`${skipped} fill style${skipped === 1 ? "" : "s"} (gradients or images) were skipped.`] : [];
9739
- return finish("figma-styles", out, notes, opts);
9740
- }
9741
- function plainLeaves(node2, prefix, out) {
9742
- if (node2 === null || node2 === void 0) return;
9743
- if (typeof node2 !== "object" || typeof node2 === "object" && "r" in node2 && "g" in node2) {
9744
- out.push({ key: prefix, value: node2 });
9745
- return;
9746
- }
9747
- for (const [k, v] of Object.entries(node2)) plainLeaves(v, [...prefix, k], out);
9831
+ return chars.join("");
9748
9832
  }
9749
- function fromModeObject(obj, opts = {}) {
9750
- const notes = [];
9751
- const modes = Object.entries(obj).filter(([, v]) => v && typeof v === "object");
9752
- const leafSets = modes.map(([name, v]) => {
9753
- const l = [];
9754
- plainLeaves(v, [], l);
9755
- return { name, leaves: l, keys: new Set(l.map((x2) => x2.key.join("/"))) };
9756
- });
9757
- const groups = [];
9758
- for (const m of leafSets) {
9759
- const g = groups.find((grp) => {
9760
- const a = grp[0].keys;
9761
- let same = 0;
9762
- for (const k of m.keys) if (a.has(k)) same++;
9763
- return same / Math.max(a.size, m.keys.size) >= 0.8;
9764
- });
9765
- if (g) g.push(m);
9766
- else groups.push([m]);
9833
+ var LineIndex = class {
9834
+ starts = [0];
9835
+ text;
9836
+ constructor(text2) {
9837
+ this.text = text2;
9838
+ for (let i2 = 0; i2 < text2.length; i2++) if (text2.charCodeAt(i2) === 10) this.starts.push(i2 + 1);
9767
9839
  }
9768
- const byLeaf = /* @__PURE__ */ new Map();
9769
- groups.forEach((grp, gi) => {
9770
- for (const m of grp) for (const l of m.leaves) {
9771
- const k = l.key[l.key.length - 1];
9772
- const e = byLeaf.get(k) ?? { group: gi, values: /* @__PURE__ */ new Map() };
9773
- e.values.set(m.name, l.value);
9774
- byLeaf.set(k, e);
9775
- }
9776
- });
9777
- const pick2 = (grp, want) => want === "dark" ? grp.find((m) => DARK.test(m.name)) : grp.find((m) => LIGHT.test(m.name) && !DARK.test(m.name)) ?? grp.find((m) => !DARK.test(m.name)) ?? grp[0];
9778
- const referenced = /* @__PURE__ */ new Set();
9779
- for (const m of leafSets) for (const l of m.leaves) if (typeof l.value === "string" && byLeaf.has(l.value)) referenced.add(l.value);
9780
- const deref = (v, want, depth = 0) => {
9781
- if (typeof v !== "string" || depth > 10) return v;
9782
- const ref = byLeaf.get(v) ?? byLeaf.get(v.replace(REF, "$1"));
9783
- if (!ref) return v;
9784
- referenced.add(v);
9785
- const grp = groups[ref.group];
9786
- const mode = pick2(grp, want) ?? pick2(grp, "default");
9787
- return deref(ref.values.get(mode.name) ?? [...ref.values.values()][0], want, depth + 1);
9788
- };
9789
- const out = [];
9790
- const pending = [];
9791
- for (const grp of groups) {
9792
- const def = pick2(grp, "default");
9793
- const dark = pick2(grp, "dark");
9794
- 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)` : ""}.`);
9795
- for (const l of def.leaves) {
9796
- const key = l.key[l.key.length - 1];
9797
- const container = l.key.length > 1 ? l.key[l.key.length - 2].toLowerCase() : "";
9798
- const value = deref(l.value, "default");
9799
- const kind = container === "color" || colorValue(value) && /^#|^rgb|^hsl/i.test(String(value)) ? "color" : container === "string" || typeof value === "string" && !/^-?[\d.]+(px|rem|%)?$/.test(value) ? "string" : "number";
9800
- const c = classify(key, kind, kind === "number" && typeof value === "string" && /px$/.test(value) ? parseFloat(value) : value, { context: def.name });
9801
- if (!c) continue;
9802
- let d;
9803
- if (c.type === "color") {
9804
- const darkLeaf = dark && dark !== def ? dark.leaves.find((x2) => x2.key.join("/") === l.key.join("/")) : void 0;
9805
- const dv = colorValue(deref(darkLeaf ? darkLeaf.value : l.value, "dark"));
9806
- if (dv && dv !== c.value) d = dv;
9807
- }
9808
- pending.push({ key, t: { ...c, ...d ? { dark: d } : {} } });
9840
+ position(offset) {
9841
+ let lo = 0;
9842
+ let hi = this.starts.length - 1;
9843
+ while (lo < hi) {
9844
+ const mid = lo + hi + 1 >> 1;
9845
+ if (this.starts[mid] <= offset) lo = mid;
9846
+ else hi = mid - 1;
9809
9847
  }
9848
+ return { line: lo + 1, column: offset - this.starts[lo] + 1 };
9810
9849
  }
9811
- for (const p of pending) out.push({ ...p.t, referenced: referenced.has(p.key) });
9812
- return finish("mode-object", out, notes, opts);
9813
- }
9814
- var figmaExt = (node2, key) => {
9815
- const ext = node2.$extensions;
9816
- if (!ext) return void 0;
9817
- if (`com.figma.${key}` in ext) return ext[`com.figma.${key}`];
9818
- const nested = ext["com.figma"];
9819
- return nested?.[key];
9820
- };
9821
- function fileLeaves(node2, prefix, out, inherited) {
9822
- if (!node2 || typeof node2 !== "object" || Array.isArray(node2)) return;
9823
- const o = node2;
9824
- const groupType = o.$type ?? inherited;
9825
- if ("$value" in o) {
9826
- const alias = figmaExt(o, "aliasData");
9827
- const scopes = figmaExt(o, "scopes");
9828
- out.push({
9829
- path: prefix,
9830
- type: o.$type ?? groupType,
9831
- value: o.$value,
9832
- description: o.$description,
9833
- scopes: Array.isArray(scopes) ? scopes : void 0,
9834
- aliasTarget: alias?.targetVariableName?.replace(/\//g, "."),
9835
- hidden: figmaExt(o, "hiddenFromPublishing") === true || void 0
9836
- });
9837
- return;
9850
+ lineText(line) {
9851
+ const start = this.starts[line - 1] ?? 0;
9852
+ const end = this.starts[line] ?? this.text.length + 1;
9853
+ return this.text.slice(start, end - 1);
9838
9854
  }
9839
- for (const [k, v] of Object.entries(o)) {
9840
- if (k.startsWith("$")) continue;
9841
- fileLeaves(v, prefix ? `${prefix}.${k}` : k, out, groupType);
9855
+ get lineCount() {
9856
+ return this.starts.length;
9842
9857
  }
9843
- }
9844
- var hasFigmaExt = (node2, depth = 0) => {
9845
- if (!node2 || typeof node2 !== "object" || depth > 12) return false;
9846
- const ext = node2.$extensions;
9847
- if (ext && Object.keys(ext).some((k) => k.startsWith("com.figma"))) return true;
9848
- return Object.values(node2).some((v) => hasFigmaExt(v, depth + 1));
9849
9858
  };
9850
- function modeOf(file2) {
9851
- const root = file2.data ?? {};
9852
- const parts = file2.name.replace(/\\/g, "/").split("/").filter(Boolean);
9853
- const base = (parts.pop() ?? "tokens").replace(/(\.tokens)?\.json$/i, "");
9854
- const named = figmaExt(root, "modeName");
9855
- const split = base.match(/^(.+?)[\s._-]+(light|dark|night|day|default|mode[\s_-]*\d+)$/i);
9856
- return {
9857
- collection: parts.join("/") || (split ? split[1] : ""),
9858
- mode: typeof named === "string" ? named : split ? split[2] : base
9859
+ function globToRegExp(glob) {
9860
+ let re = "";
9861
+ for (let i2 = 0; i2 < glob.length; i2++) {
9862
+ const c = glob[i2];
9863
+ if (c === "*") {
9864
+ if (glob[i2 + 1] === "*") {
9865
+ const slash = glob[i2 + 2] === "/";
9866
+ re += slash ? "(?:.*/)?" : ".*";
9867
+ i2 += slash ? 2 : 1;
9868
+ } else re += "[^/]*";
9869
+ } else if (c === "?") re += "[^/]";
9870
+ else if (c === "{") {
9871
+ const end = glob.indexOf("}", i2);
9872
+ re += `(?:${glob.slice(i2 + 1, end).split(",").map((s) => s.replace(/[.+^$()|[\]\\]/g, "\\$&")).join("|")})`;
9873
+ i2 = end;
9874
+ } else re += c.replace(/[.+^$()|[\]\\]/g, "\\$&");
9875
+ }
9876
+ return new RegExp(`^${re}$`);
9877
+ }
9878
+ function makeIgnore(globs) {
9879
+ const res = globs.map(globToRegExp);
9880
+ return (path) => {
9881
+ const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
9882
+ return res.some((r) => r.test(p) || r.test(`/${p}`));
9859
9883
  };
9860
9884
  }
9861
- function importTokenFiles(files, opts = {}) {
9862
- const parsed = files.filter((f) => f.data && typeof f.data === "object");
9863
- if (!parsed.length) throw new Error("No JSON token files found.");
9864
- const figma = parsed.some((f) => hasFigmaExt(f.data));
9865
- if (parsed.length === 1 && !figma) return importTokens(parsed[0].data, opts);
9866
- const notes = [];
9867
- const withMode = parsed.map((f) => ({ ...f, ...modeOf(f) }));
9868
- const byCollection = /* @__PURE__ */ new Map();
9869
- for (const f of withMode) byCollection.set(f.collection, [...byCollection.get(f.collection) ?? [], f]);
9870
- const light = /* @__PURE__ */ new Map();
9871
- const darkOverrides = /* @__PURE__ */ new Map();
9872
- for (const [collection, list] of byCollection) {
9873
- const dark = list.find((f) => DARK.test(f.mode));
9874
- const def = list.find((f) => LIGHT.test(f.mode) && !DARK.test(f.mode)) ?? list.find((f) => !DARK.test(f.mode)) ?? list[0];
9875
- if (list.length > 1) {
9876
- const label = collection ? `\u201C${collection}\u201D` : "The files";
9877
- 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)` : ""}.`);
9885
+
9886
+ // ../engine/src/check.ts
9887
+ var ENGINE_VERSION = "0.9.0";
9888
+ var WEIGHT = { error: 4, warning: 1.5, info: 0.25 };
9889
+ function computeScore(counts, lines) {
9890
+ const penalty = counts.error * WEIGHT.error + counts.warning * WEIGHT.warning + counts.info * WEIGHT.info;
9891
+ if (penalty === 0) return 100;
9892
+ const perK = penalty / Math.max(1, lines / 1e3);
9893
+ return Math.max(0, Math.min(99, Math.round(100 / (1 + perK / 25))));
9894
+ }
9895
+ function check(files, configInput, opts = {}) {
9896
+ const t0 = Date.now();
9897
+ const config2 = "replaces" in configInput ? configInput : resolveConfig(configInput);
9898
+ const ignored = makeIgnore(config2.ignore);
9899
+ const violations = [];
9900
+ let filesScanned = 0;
9901
+ let linesScanned = 0;
9902
+ for (const file2 of files) {
9903
+ const path = file2.path.replace(/\\/g, "/").replace(/^\.\//, "");
9904
+ const kind = fileKind(path);
9905
+ if (!SCANNABLE.has(kind) || ignored(path)) continue;
9906
+ const only = opts.onlyLines?.get(path);
9907
+ if (opts.onlyLines && (!only || only.size === 0)) continue;
9908
+ filesScanned++;
9909
+ const idx = new LineIndex(file2.content);
9910
+ linesScanned += only ? only.size : file2.content.split("\n").filter((l) => l.trim()).length;
9911
+ const code = stripComments(file2.content, kind);
9912
+ const framework = fileFramework(path, kind, code, config2.framework);
9913
+ const seen = /* @__PURE__ */ new Set();
9914
+ for (const rule of RULES) {
9915
+ const setting = config2.rules[rule.id];
9916
+ if (setting === "off" || !rule.kinds.includes(kind)) continue;
9917
+ for (const f of rule.check({ path, kind, code, config: config2, framework })) {
9918
+ const { line, column } = idx.position(f.offset);
9919
+ if (only && !only.has(line)) continue;
9920
+ const key = `${rule.id}:${line}:${column}`;
9921
+ if (seen.has(key)) continue;
9922
+ seen.add(key);
9923
+ violations.push({
9924
+ ruleId: rule.id,
9925
+ severity: setting ?? f.severity ?? rule.defaultSeverity,
9926
+ file: path,
9927
+ line,
9928
+ column,
9929
+ message: f.message,
9930
+ suggestion: f.suggestion,
9931
+ snippet: opts.snippets === false ? void 0 : idx.lineText(line).trim().slice(0, 160)
9932
+ });
9933
+ }
9878
9934
  }
9879
- const read2 = (f, into) => {
9880
- const leaves3 = [];
9881
- fileLeaves(f.data, "", leaves3);
9882
- for (const l of leaves3) into.set(l.path, { path: l.path, type: l.type, value: l.value, description: l.description, leaf: l });
9883
- };
9884
- read2(def, light);
9885
- if (dark && dark !== def) read2(dark, darkOverrides);
9886
9935
  }
9887
- const lightRaws = resolveRefs([...light.values()]);
9888
- const darkRaws = resolveRefs([...light.values()].map((r) => darkOverrides.get(r.path) ?? r));
9889
- const darkByPath = new Map(darkRaws.map((r) => [r.path, r.value]));
9890
- const referenced = /* @__PURE__ */ new Set();
9891
- for (const r of [...light.values(), ...darkOverrides.values()]) {
9892
- if (r.leaf.aliasTarget) referenced.add(r.leaf.aliasTarget);
9893
- const m = typeof r.value === "string" ? r.value.trim().match(REF) : null;
9894
- if (m) referenced.add(m[1]);
9936
+ violations.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
9937
+ const counts = { error: 0, warning: 0, info: 0 };
9938
+ const perRule = /* @__PURE__ */ new Map();
9939
+ for (const v of violations) {
9940
+ counts[v.severity]++;
9941
+ perRule.set(v.ruleId, (perRule.get(v.ruleId) ?? 0) + 1);
9895
9942
  }
9896
- const out = [];
9897
- let hidden = 0;
9898
- for (const r of lightRaws) {
9899
- const leaf = light.get(r.path).leaf;
9900
- if (leaf.hidden) {
9901
- hidden++;
9902
- continue;
9903
- }
9904
- const t = (r.type ?? "").toLowerCase();
9905
- 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";
9906
- let tok;
9907
- if (figma) {
9908
- const value = kind === "number" && r.value && typeof r.value === "object" ? parseFloat(dim(r.value)) : r.value;
9909
- tok = classify(r.path.replace(/\./g, "/"), kind, value, { scopes: leaf.scopes?.filter((x2) => x2 !== "ALL_SCOPES") });
9910
- } else {
9911
- const flat = flattenTokens({ x: { $type: r.type, $value: r.value } })[0];
9912
- tok = flat ? { path: flat.type === "color" && !r.path.startsWith("color.") ? `color.${r.path}` : r.path, type: flat.type, value: flat.value } : null;
9913
- }
9914
- if (!tok) continue;
9915
- let darkValue;
9916
- if (tok.type === "color" && darkOverrides.size) {
9917
- const d = colorValue(darkByPath.get(r.path));
9918
- if (d && d !== tok.value) darkValue = d;
9943
+ return {
9944
+ engineVersion: ENGINE_VERSION,
9945
+ tokenVersion: config2.tokenVersion,
9946
+ score: computeScore(counts, linesScanned),
9947
+ counts,
9948
+ filesScanned,
9949
+ linesScanned,
9950
+ violations,
9951
+ byRule: [...perRule].map(([ruleId, count]) => ({ ruleId, count })).sort((a, b) => b.count - a.count),
9952
+ durationMs: Date.now() - t0
9953
+ };
9954
+ }
9955
+
9956
+ // ../engine/src/report.ts
9957
+ var RENDERED_ONLY = { "text-contrast": "Low text contrast", "font-size-scale": "Off-scale font size", "page-rendered": "Blank page" };
9958
+ var title = (id) => RULES.find((r) => r.id === id)?.title ?? RENDERED_ONLY[id] ?? id;
9959
+ var ansi = (on) => {
9960
+ const w = (code) => (s) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
9961
+ return { red: w("31"), yellow: w("33"), cyan: w("36"), green: w("32"), dim: w("2"), bold: w("1") };
9962
+ };
9963
+ var rendered = (r) => r.violations.length > 0 && r.violations.every((v) => v.line === 0);
9964
+ function formatPretty(report, { color = true, max: max2 = 200 } = {}) {
9965
+ const c = ansi(color);
9966
+ const sev = { error: c.red, warning: c.yellow, info: c.cyan };
9967
+ const lines = [];
9968
+ let lastFile = "";
9969
+ for (const v of report.violations.slice(0, max2)) {
9970
+ if (v.file !== lastFile) {
9971
+ lines.push("", c.bold(v.file));
9972
+ lastFile = v.file;
9919
9973
  }
9920
- out.push({ ...tok, ...darkValue ? { dark: darkValue } : {}, ...r.description ? { description: r.description } : {}, referenced: referenced.has(r.path) });
9974
+ const rendered2 = v.line === 0;
9975
+ lines.push(` ${c.dim(rendered2 ? "".padEnd(8) : `${v.line}:${v.column}`.padEnd(8))} ${sev[v.severity](v.severity.padEnd(7))} ${v.message} ${c.dim(v.ruleId)}`);
9976
+ if (v.suggestion) lines.push(` ${" ".repeat(16)} ${c.green("\u2192")} ${v.suggestion}`);
9977
+ if (rendered2 && v.snippet) lines.push(` ${" ".repeat(16)} ${c.dim(`on ${v.snippet}`)}`);
9921
9978
  }
9922
- if (hidden) notes.push(`${hidden} variable${hidden === 1 ? "" : "s"} hidden from publishing were skipped.`);
9923
- return finish(figma ? "figma-export" : "dtcg", out, notes, opts);
9979
+ if (report.violations.length > max2) lines.push("", c.dim(`\u2026 and ${report.violations.length - max2} more`));
9980
+ const s = report.score;
9981
+ const scoreStr = s >= 80 ? c.green(String(s)) : s >= 65 ? c.yellow(String(s)) : c.red(String(s));
9982
+ lines.push(
9983
+ "",
9984
+ `${c.bold("Brand score")} ${scoreStr}/100 ${c.dim("\xB7")} ${c.red(`${report.counts.error} errors`)} ${c.dim("\xB7")} ${c.yellow(`${report.counts.warning} warnings`)} ${c.dim("\xB7")} ${c.cyan(`${report.counts.info} info`)}`,
9985
+ c.dim(`${report.filesScanned} ${rendered(report) ? "pages" : "files"}, ${report.linesScanned} ${rendered(report) ? "elements" : "lines"} in ${report.durationMs}ms${report.tokenVersion ? ` \xB7 Brand Kit v${report.tokenVersion}` : ""}`)
9986
+ );
9987
+ return lines.join("\n");
9924
9988
  }
9925
- function importTokens(input2, opts = {}) {
9926
- if (!input2 || typeof input2 !== "object") throw new Error("Expected a JSON object.");
9927
- const o = input2;
9928
- if (Array.isArray(o.$files)) return importTokenFiles(o.$files, opts);
9929
- const meta3 = o.meta ?? o;
9930
- if (meta3.variables && meta3.variableCollections) return fromFigmaVariables(meta3, opts);
9931
- if (Array.isArray(o.styles) && o.styles.every((s) => s && "styleType" in s)) return fromFigmaStyles(o.styles, opts);
9932
- if (hasDollarValue(o) && hasFigmaExt(o)) return importTokenFiles([{ name: "tokens.json", data: o }], opts);
9933
- if (hasDollarValue(o) || hasStudioValue(o)) {
9934
- const studio = !hasDollarValue(o);
9935
- const flat = flattenTokens(o);
9936
- if (!flat.length) throw new Error("No tokens found in that file.");
9937
- const out = flat.map((t) => {
9938
- if (t.type === "color" && !t.path.startsWith("color.")) return { ...t, path: `color.${t.path}` };
9939
- return t;
9940
- });
9941
- const notes = studio ? [] : [];
9942
- const res = finish(studio ? "tokens-studio" : "dtcg", out, notes, opts);
9943
- return res;
9944
- }
9945
- const leaves3 = [];
9946
- plainLeaves(o, [], leaves3);
9947
- if (leaves3.length) return fromModeObject(o, opts);
9948
- throw new Error("Couldn't find any tokens. Supported: DTCG, Tokens Studio, Figma variables JSON.");
9989
+ var esc = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
9990
+ var escProp = (s) => esc(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
9991
+ function formatGithubAnnotations(report, max2 = 50) {
9992
+ const level = { error: "error", warning: "warning", info: "notice" };
9993
+ return report.violations.slice(0, max2).map(
9994
+ (v) => `::${level[v.severity]} file=${escProp(v.file)},line=${v.line},col=${v.column},title=${escProp(`Brandrail: ${title(v.ruleId)}`)}::${esc(
9995
+ v.suggestion ? `${v.message}. ${v.suggestion}` : v.message
9996
+ )}`
9997
+ ).join("\n");
9949
9998
  }
9950
- function diffTokens(before, after) {
9951
- const a = new Map(flattenTokens(before).map((t) => [t.path, t]));
9952
- const b = new Map(flattenTokens(after).map((t) => [t.path, t]));
9953
- const diff = { added: [], removed: [], changed: [], unchanged: 0 };
9954
- for (const [p, t] of b) {
9955
- const old = a.get(p);
9956
- if (!old) diff.added.push(t);
9957
- else if (old.value.toLowerCase() !== t.value.toLowerCase() || (old.dark ?? "").toLowerCase() !== (t.dark ?? "").toLowerCase()) diff.changed.push({ path: p, before: old, after: t });
9958
- else diff.unchanged++;
9999
+ var COMMENT_MARKER = "<!-- brandrail-report -->";
10000
+ function formatMarkdown(report, opts = {}) {
10001
+ const { threshold, maxRows = 30 } = opts;
10002
+ const pass = threshold === void 0 || report.score >= threshold;
10003
+ const badge = report.violations.length === 0 ? "\u{1F7E2}" : pass ? "\u{1F7E1}" : "\u{1F534}";
10004
+ const out = [COMMENT_MARKER];
10005
+ out.push(`## ${badge} Brand score: **${report.score}**/100${threshold !== void 0 ? ` (threshold ${threshold})` : ""}`);
10006
+ out.push("");
10007
+ out.push(
10008
+ `**${report.counts.error}** errors \xB7 **${report.counts.warning}** warnings \xB7 **${report.counts.info}** info \u2014 ${report.filesScanned} files scanned${report.tokenVersion ? ` against Brand Kit v${report.tokenVersion}` : ""}`
10009
+ );
10010
+ if (report.violations.length === 0) {
10011
+ out.push("", "Everything uses approved tokens and components. \u2728");
10012
+ } else {
10013
+ if (report.byRule.length) {
10014
+ out.push("", "| Rule | Count |", "| --- | ---: |");
10015
+ for (const r of report.byRule) out.push(`| ${title(r.ruleId)} \`${r.ruleId}\` | ${r.count} |`);
10016
+ }
10017
+ out.push("", "<details><summary>Violations</summary>", "", "| | Location | Issue | Fix |", "| --- | --- | --- | --- |");
10018
+ const icon = { error: "\u{1F534}", warning: "\u{1F7E1}", info: "\u{1F535}" };
10019
+ for (const v of report.violations.slice(0, maxRows)) {
10020
+ const loc = v.line === 0 ? `${v.file} ${v.snippet ?? ""}`.trim() : `${v.file}:${v.line}`;
10021
+ const url2 = opts.fileUrl?.(v);
10022
+ const cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
10023
+ out.push(`| ${icon[v.severity]} | ${url2 ? `[\`${loc}\`](${url2})` : `\`${loc}\``} | ${cell(v.message)} | ${cell(v.suggestion ?? "")} |`);
10024
+ }
10025
+ if (report.violations.length > maxRows) out.push("", `\u2026and ${report.violations.length - maxRows} more.`);
10026
+ out.push("", "</details>");
9959
10027
  }
9960
- for (const [p, t] of a) if (!b.has(p)) diff.removed.push(t);
9961
- return diff;
9962
- }
9963
- function mergeTokens(base, incoming) {
9964
- const m = new Map(flattenTokens(base).map((t) => [t.path, t]));
9965
- for (const t of flattenTokens(incoming)) m.set(t.path, { ...m.get(t.path), ...t, description: t.description ?? m.get(t.path)?.description });
9966
- return buildDtcg([...m.values()]);
10028
+ out.push("", `<sub>Brandrail engine ${report.engineVersion}${opts.dashboardUrl ? ` \xB7 [Open in Brandrail](${opts.dashboardUrl})` : ""}</sub>`);
10029
+ return out.join("\n");
9967
10030
  }
9968
10031
 
10032
+ // ../engine/src/defaults.ts
10033
+ var starterConfig = {
10034
+ $schema: "https://brandrail.dev/schema/config.json",
10035
+ tokenVersion: "0.0.0-local",
10036
+ tokens: {
10037
+ color: {
10038
+ action: { primary: { $type: "color", $value: "#1f883d", $description: "One primary action per view" } },
10039
+ accent: { default: { $type: "color", $value: "#0969da", $description: "Links, focus, selection" } },
10040
+ fg: {
10041
+ default: { $type: "color", $value: "#1f2328", $description: "Body text" },
10042
+ muted: { $type: "color", $value: "#59636e", $description: "Secondary text" }
10043
+ },
10044
+ canvas: {
10045
+ default: { $type: "color", $value: "#ffffff", $description: "Page background" },
10046
+ subtle: { $type: "color", $value: "#f6f8fa", $description: "Headers, wells" }
10047
+ },
10048
+ border: { default: { $type: "color", $value: "#d1d9e0", $description: "Borders and dividers" } },
10049
+ danger: { default: { $type: "color", $value: "#cf222e", $description: "Destructive actions, errors" } },
10050
+ success: { default: { $type: "color", $value: "#1a7f37", $description: "Success messages, completed states" } },
10051
+ attention: { default: { $type: "color", $value: "#9a6700", $description: "Warnings, needs attention" } }
10052
+ },
10053
+ font: {
10054
+ family: { sans: { $type: "fontFamily", $value: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif" } },
10055
+ size: { base: { $type: "dimension", $value: "14px" } }
10056
+ },
10057
+ radius: { md: { $type: "dimension", $value: "6px" } },
10058
+ space: { unit: { $type: "dimension", $value: "8px" } }
10059
+ },
10060
+ components: [
10061
+ { name: "Button", importPath: "@acme/ui" },
10062
+ { name: "TextInput", importPath: "@acme/ui" },
10063
+ { name: "Select", importPath: "@acme/ui" },
10064
+ { name: "Textarea", importPath: "@acme/ui" }
10065
+ ],
10066
+ rules: {},
10067
+ ignore: []
10068
+ };
10069
+
9969
10070
  // ../engine/src/instructions.ts
9970
10071
  var KIT_PACKAGE = "@brandrail/kit";
9971
10072
  var RULES_START = "<!-- brandrail:start -->";
@@ -9996,8 +10097,9 @@ function generateInstructions(config2, opts = {}) {
9996
10097
  }
9997
10098
  lines.push("");
9998
10099
  }
9999
- if (r.components.length) {
10000
- const usable = r.components.map((c) => ({ c, u: resolveUsage(c, fw) }));
10100
+ const active = r.components.filter((c) => c.status !== "deprecated");
10101
+ if (active.length) {
10102
+ const usable = active.map((c) => ({ c, u: resolveUsage(c, fw) }));
10001
10103
  const here = usable.filter((x2) => x2.u);
10002
10104
  const missing2 = usable.filter((x2) => !x2.u).map((x2) => x2.c.name);
10003
10105
  lines.push("## Components");
@@ -10031,7 +10133,7 @@ function generateInstructions(config2, opts = {}) {
10031
10133
  lines.push("## Colours");
10032
10134
  lines.push("Use CSS variables only. Never write hex, rgb() or hsl() values, and never use Tailwind palette classes like `bg-blue-500`.");
10033
10135
  for (const c of r.colors) {
10034
- if (c.mode) continue;
10136
+ if (c.mode || r.deprecatedTokens.has(c.cssVar)) continue;
10035
10137
  const dark = r.colors.find((d) => d.mode === "dark" && d.path === c.path);
10036
10138
  lines.push(`- \`var(${c.cssVar})\` \u2014 ${c.path} (${c.hex}${dark ? `, dark ${dark.hex}` : ""})`);
10037
10139
  }
@@ -10047,6 +10149,14 @@ function generateInstructions(config2, opts = {}) {
10047
10149
  if (sizeVars.length) lines.push(`- Font sizes come from the type scale: ${sizeVars.join(", ")}. Matching line heights are \`--font-line-height-*\`.`);
10048
10150
  if (r.fonts.length) lines.push(`- Fonts: use ${r.fontVars.map((v) => `\`var(${v})\``).join(" or ")}; never set another font-family.`);
10049
10151
  lines.push("");
10152
+ const depComps = r.components.filter((c) => c.status === "deprecated");
10153
+ if (r.deprecatedTokens.size || depComps.length) {
10154
+ lines.push("## Deprecated: don't use in new code");
10155
+ lines.push("These still work until their removal date. Use the replacement, and switch existing uses when you touch that code (the Checker warns on each).");
10156
+ for (const d of r.deprecatedTokens.values()) lines.push(`- ${deprecationLine(d, true)}`);
10157
+ for (const c of depComps) lines.push(`- ${deprecationLine({ path: c.name, ...c.deprecation }, false)}`);
10158
+ lines.push("");
10159
+ }
10050
10160
  lines.push("## New pages");
10051
10161
  lines.push("- Building a new page or screen? Start from a page template: MCP `get_template` (list, detail, form, settings, dashboard, sign-in) or `npx brandrail template <name>`. It's built from these components and the spacing scale, and lists the page's rules (states, layout, accessibility). Rename its sample data; keep its structure.");
10052
10162
  lines.push("");
@@ -10803,7 +10913,8 @@ function mergeProjectConfig(prev, kit) {
10803
10913
  rules: { ...prev.rules, ...kit.rules },
10804
10914
  ignore: prev.ignore ?? kit.ignore,
10805
10915
  tokensCss: prev.tokensCss,
10806
- framework: prev.framework ?? kit.framework
10916
+ framework: prev.framework ?? kit.framework,
10917
+ brandKitVersion: prev.brandKitVersion
10807
10918
  };
10808
10919
  }
10809
10920
  function tokensCssContent(config2, org) {
@@ -11740,8 +11851,8 @@ async function call(path, init) {
11740
11851
  }
11741
11852
  return body;
11742
11853
  }
11743
- function fetchBrandKit(key) {
11744
- return call("/api/v1/brand-kit", { method: "GET", key });
11854
+ function fetchBrandKit(key, version2) {
11855
+ return call(`/api/v1/brand-kit${version2 ? `?version=${encodeURIComponent(version2)}` : ""}`, { method: "GET", key });
11745
11856
  }
11746
11857
  function uploadReport(key, payload) {
11747
11858
  const report = payload.uploadSnippets ? payload.report : { ...payload.report, violations: payload.report.violations.map(({ snippet: _snippet, ...v }) => v) };
@@ -12015,7 +12126,7 @@ __export(external_exports, {
12015
12126
  decode: () => decode2,
12016
12127
  decodeAsync: () => decodeAsync2,
12017
12128
  deepPartial: () => deepPartial,
12018
- describe: () => describe2,
12129
+ describe: () => describe3,
12019
12130
  discriminatedUnion: () => discriminatedUnion,
12020
12131
  e164: () => e1642,
12021
12132
  email: () => email2,
@@ -12421,7 +12532,7 @@ __export(core_exports2, {
12421
12532
  createToJSONSchemaMethod: () => createToJSONSchemaMethod,
12422
12533
  decode: () => decode,
12423
12534
  decodeAsync: () => decodeAsync,
12424
- describe: () => describe,
12535
+ describe: () => describe2,
12425
12536
  encode: () => encode,
12426
12537
  encodeAsync: () => encodeAsync,
12427
12538
  extractDefs: () => extractDefs,
@@ -27411,7 +27522,7 @@ function _check(fn, params) {
27411
27522
  return ch;
27412
27523
  }
27413
27524
  // @__NO_SIDE_EFFECTS__
27414
- function describe(description) {
27525
+ function describe2(description) {
27415
27526
  const ch = new $ZodCheck({ check: "describe" });
27416
27527
  ch._zod.onattach = [
27417
27528
  (inst) => {
@@ -28965,7 +29076,7 @@ __export(schemas_exports2, {
28965
29076
  currencyCode: () => currencyCode2,
28966
29077
  custom: () => custom,
28967
29078
  date: () => date2,
28968
- describe: () => describe2,
29079
+ describe: () => describe3,
28969
29080
  discriminatedUnion: () => discriminatedUnion,
28970
29081
  e164: () => e1642,
28971
29082
  email: () => email2,
@@ -30557,7 +30668,7 @@ function refine(fn, _params = {}) {
30557
30668
  function superRefine(fn, params) {
30558
30669
  return _superRefine(fn, params);
30559
30670
  }
30560
- var describe2 = describe;
30671
+ var describe3 = describe2;
30561
30672
  var meta2 = meta;
30562
30673
  var ZodInstanceOf = /* @__PURE__ */ $constructor("ZodInstanceOf", (inst, def) => {
30563
30674
  ZodCustom.init(inst, def);
@@ -41315,6 +41426,15 @@ function componentDoc(c, fw) {
41315
41426
  const u = resolveUsage(c, fw);
41316
41427
  const others = availableFrameworks(c).filter((f) => f !== fw).map((f) => FRAMEWORK_LABEL[f]);
41317
41428
  const title2 = `### ${c.name}${c.status && c.status !== "stable" ? ` (${c.status})` : ""}`;
41429
+ if (c.status === "deprecated") {
41430
+ const d = c.deprecation ?? {};
41431
+ return [
41432
+ title2,
41433
+ "",
41434
+ `Deprecated${d.removeAfter ? `: removed after ${d.removeAfter}` : ""}. Don't use it in new code${d.replacement ? `; use ${d.replacement} (get_component_docs "${d.replacement}")` : ""}.${d.reason ? ` ${d.reason}` : ""}`,
41435
+ "When you edit code that uses it, switch to the replacement."
41436
+ ].join("\n");
41437
+ }
41318
41438
  if (!u) {
41319
41439
  return [
41320
41440
  title2,
@@ -41382,7 +41502,11 @@ function createBrandrailMcp(ctx) {
41382
41502
  const pick2 = category === "all" ? all : all.filter(CATEGORY[category]);
41383
41503
  const lines = [`# ${ctx.org} design tokens${ctx.config.tokenVersion ? ` (Brand Kit v${ctx.config.tokenVersion})` : ""}`, ""];
41384
41504
  lines.push("| Token | Use | Value | Meaning |", "| --- | --- | --- | --- |");
41385
- for (const l of pick2) lines.push(`| ${l.path} | \`${cssVar2(l.path)}\` | ${String(l.value)}${l.dark ? ` (dark ${String(l.dark)})` : ""} | ${l.description ?? ""} |`);
41505
+ for (const l of pick2) {
41506
+ const dep = resolved.deprecatedTokens.get(`--${l.path.replace(/\./g, "-")}`);
41507
+ const note = dep ? `**Deprecated**${dep.replacement ? `: use \`${cssVar2(dep.replacement)}\`` : ""}${dep.removeAfter ? ` (removed after ${dep.removeAfter})` : ""}.` : l.description ?? "";
41508
+ lines.push(`| ${l.path} | \`${cssVar2(l.path)}\` | ${String(l.value)}${l.dark ? ` (dark ${String(l.dark)})` : ""} | ${note} |`);
41509
+ }
41386
41510
  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.");
41387
41511
  if (category === "all" || category === "spacing") lines.push("", `Allowed spacing (px): ${resolved.spacing.filter((s) => s > 2).join(", ")}`);
41388
41512
  if (category === "all" || category === "radius") lines.push(`Allowed radius (px): ${resolved.radii.filter((r) => r > 0 && r < 999).join(", ")}`);
@@ -41618,10 +41742,10 @@ function cachePath(key) {
41618
41742
  const id = createHash2("sha256").update(`${apiBase()}|${key}`).digest("hex").slice(0, 16);
41619
41743
  return join4(base, "brandrail", `brand-kit-${id}.json`);
41620
41744
  }
41621
- async function loadKit(key) {
41622
- const p = cachePath(key);
41745
+ async function loadKit(key, version2) {
41746
+ const p = cachePath(`${key}${version2 ? `@${version2}` : ""}`);
41623
41747
  try {
41624
- const kit = await fetchBrandKit(key);
41748
+ const kit = await fetchBrandKit(key, version2);
41625
41749
  mkdirSync2(dirname3(p), { recursive: true, mode: 448 });
41626
41750
  writeFileSync2(p, JSON.stringify({ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), ...kit }), { mode: 384 });
41627
41751
  return kit;
@@ -41639,7 +41763,7 @@ async function runLocalMcp(cwd, opts) {
41639
41763
  const configPath = findConfig(cwd);
41640
41764
  const local = configPath ? readConfig(configPath) : void 0;
41641
41765
  const key = resolveKey();
41642
- const kit = key ? await loadKit(key) : null;
41766
+ const kit = key ? await loadKit(key, local?.brandKitVersion) : null;
41643
41767
  let config2;
41644
41768
  let org;
41645
41769
  let published;
@@ -42111,9 +42235,11 @@ async function cmdInit(cwd, flags) {
42111
42235
  if (flags["from-api"]) {
42112
42236
  const key = resolveKey();
42113
42237
  if (!key) throw new Error(`--from-api: ${NO_KEY}`);
42114
- const res = await fetchBrandKit(key);
42115
- config2 = { $schema: starterConfig.$schema, ...res.config };
42238
+ const pinned = existsSync7(target) ? readConfig(target).brandKitVersion : void 0;
42239
+ const res = await fetchBrandKit(key, pinned);
42240
+ config2 = { $schema: starterConfig.$schema, ...res.config, ...pinned ? { brandKitVersion: pinned } : {} };
42116
42241
  org = res.org;
42242
+ if (pinned && res.latestVersion && res.latestVersion !== pinned) log2(`Pinned to Brand Kit v${pinned} (v${res.latestVersion} is the latest). Remove "brandKitVersion" from ${CONFIG_FILE2} to follow the latest.`);
42117
42243
  }
42118
42244
  if (existsSync7(target) && !flags.force && !flags["from-api"]) {
42119
42245
  log2(`${CONFIG_FILE2} already exists (use --force to overwrite). Updating rule files from it.`);