@yahoo/uds-create-config 3.0.6 → 3.0.7

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 (33) hide show
  1. package/dist/configs/CanvasConfig.d.ts +24 -0
  2. package/dist/configs/react-native-system.d.ts +20 -2
  3. package/dist/configs/react-native-system.js +1 -1
  4. package/dist/configs/system.d.ts +18 -0
  5. package/dist/configs/system.js +5 -1
  6. package/dist/entities/native/NativeStyleProperty.d.ts +2 -2
  7. package/dist/entities/system/Component.d.ts +12 -0
  8. package/dist/entities/system/Component.js +6 -0
  9. package/dist/entities/system/Font.d.ts +25 -1
  10. package/dist/entities/system/Font.js +34 -1
  11. package/dist/entities/system/Token.js +1 -1
  12. package/dist/entities/system/defineComponent.d.ts +32 -11
  13. package/dist/entities/system/defineComponent.js +10 -1
  14. package/dist/entities/system/icon-library.js +5 -1
  15. package/dist/entities/system/style-bag.js +1 -0
  16. package/dist/framework/Config.d.ts +16 -1
  17. package/dist/framework/Config.js +33 -1
  18. package/dist/framework/projections.d.ts +23 -2
  19. package/dist/framework/projections.js +67 -11
  20. package/dist/framework/render-spec.js +16 -1
  21. package/dist/framework/schema-version.d.ts +1 -1
  22. package/dist/framework/schema-version.js +5 -5
  23. package/dist/framework/utils/field-schema.js +3 -2
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +3 -3
  26. package/dist/migrations/2.0.0/v1-artifact.d.ts +20 -0
  27. package/dist/migrations/2.0.0/v1-artifact.js +358 -18
  28. package/dist/migrations/20260912161840_preview_conditions.d.ts +9 -0
  29. package/dist/migrations/20260912161840_preview_conditions.js +10 -0
  30. package/dist/migrations/20260912164927_font_variable.d.ts +18 -0
  31. package/dist/migrations/20260912164927_font_variable.js +10 -0
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +2 -2
@@ -1,14 +1,15 @@
1
1
  import { isRef, pathOf, splitRef } from "../../framework/utils/refs.js";
2
2
  import { alpha, darken, lighten, mix } from "../../entities/system/color.js";
3
+ import { Font } from "../../entities/system/Font.js";
3
4
  import { isCssProperty, kebabCase } from "../../css/values.js";
4
5
  import { Composite } from "../../entities/system/Composite.js";
5
6
  import { Modifier } from "../../entities/system/Modifier.js";
6
7
  import { StyleProperty } from "../../entities/system/StyleProperty.js";
8
+ import { cssVar } from "../../framework/projections.js";
7
9
  import { Component, canonicalWhen } from "../../entities/system/Component.js";
8
10
  import { Icon } from "../../entities/system/Icon.js";
9
11
  import { stableStringify } from "../../framework/utils/content-key.js";
10
12
  import { CanvasRole } from "../../entities/system/CanvasRole.js";
11
- import { Font } from "../../entities/system/Font.js";
12
13
  import { GlobalStyle, globalStyleName } from "../../entities/system/GlobalStyle.js";
13
14
  import { Guidance } from "../../entities/system/Guidance.js";
14
15
  import { Motion } from "../../entities/system/Motion.js";
@@ -115,6 +116,8 @@ const V1_DIAGNOSTIC_CODES = {
115
116
  "style-layer-undeclared": { loss: true },
116
117
  /** A component that took content without declaring `children`; the slot is declared for it. */
117
118
  "children-slot-inferred": { loss: false },
119
+ /** A variant whose values were all numbers and styled nothing — a number wearing an enum. */
120
+ "variant-numeric-to-number": { loss: false },
118
121
  /** CSS whose syntax or ordered entries cannot be represented by a global style. */
119
122
  "raw-css-unparsed": { loss: true },
120
123
  /** A selector modifier v1 also guarded with a media query; a v2 modifier activates one way. The
@@ -127,7 +130,11 @@ const V1_DIAGNOSTIC_CODES = {
127
130
  "ref-unresolved": { loss: true },
128
131
  /** A style bag key that is neither a style prop, a CSS property, nor a prop of the layer's
129
132
  * element. v1 wrote it as a declaration no browser parses, so it painted nothing. */
130
- "style-key-unrecognized": { loss: false }
133
+ "style-key-unrecognized": { loss: false },
134
+ /** A `font-family` token reaching its face through a `--<prefix>-font-family-*` variable that no
135
+ * declared font answers to. Kept as written, so it paints what it painted; it is just not an
136
+ * edge to a font. */
137
+ "font-var-unbound": { loss: false }
131
138
  };
132
139
  /** Module-scoped for the same reason `assetKinds` is: one load is in flight at a time. */
133
140
  let diagnose = () => {};
@@ -318,8 +325,10 @@ function buildModifiers(studio) {
318
325
  const value = typeof selector === "string" ? selector : String(selector);
319
326
  const name = modifierName(key);
320
327
  if (def.media) report("modifier-media-dropped", name, `v1 wrapped \`${value}\` in \`@media ${def.media}\`; a v2 modifier is a selector or a query, not both`);
321
- if (addressesPart(value)) {
322
- record[name] = Modifier.item({
328
+ const folder = partFolder(value);
329
+ if (folder) {
330
+ if (!(folder in record)) record[folder] = Modifier.group({ label: folder });
331
+ record[`${folder}/${name}`] = Modifier.item({
323
332
  type: "selector",
324
333
  value
325
334
  });
@@ -328,7 +337,7 @@ function buildModifiers(studio) {
328
337
  if (!stateGroup) {
329
338
  record.state = Modifier.group({
330
339
  type: "state",
331
- label: "State"
340
+ label: "state"
332
341
  });
333
342
  stateGroup = true;
334
343
  }
@@ -339,9 +348,12 @@ function buildModifiers(studio) {
339
348
  }
340
349
  return record;
341
350
  }
342
- /** A pseudo-element or a child/sibling combinator styles a part of the element rather than the
343
- * element under a condition, so it is a plain modifier rather than a state. */
344
- const addressesPart = (value) => /^\s*(::|>|~|\+)/.test(value);
351
+ /** The folder a part-addressing modifier is filed under: `pseudo` for a pseudo-element, `children`
352
+ * for a combinator that reaches a child or sibling; `undefined` for a state. */
353
+ function partFolder(value) {
354
+ if (/^\s*::/.test(value)) return "pseudo";
355
+ if (/^\s*(>|~|\+)/.test(value)) return "children";
356
+ }
345
357
  /** A modifier's config-v2 name. Old configs key modifiers by their style-bag form (`_dark`), but
346
358
  * the underscore is bag SYNTAX, not identity — so the entity is `dark`, which is what a ref points
347
359
  * at and what codegen already looks up after stripping a bag key's `_`. */
@@ -456,13 +468,22 @@ function convertProp(name, prop, ctx) {
456
468
  }
457
469
  case "variant": {
458
470
  const values = prop.values;
459
- if (Array.isArray(values)) return {
460
- decl: {
461
- type: "variant",
462
- value: values.map(String)
463
- },
464
- rules: []
465
- };
471
+ if (Array.isArray(values)) {
472
+ if (values.length > 0 && values.every((v) => typeof v === "number")) {
473
+ report("variant-numeric-to-number", `${ctx.name}/${name}`, `a variant of ${values.length} numbers with no styles is declared as a number`);
474
+ return {
475
+ decl: { type: "number" },
476
+ rules: []
477
+ };
478
+ }
479
+ return {
480
+ decl: {
481
+ type: "variant",
482
+ value: values.map(String)
483
+ },
484
+ rules: []
485
+ };
486
+ }
466
487
  const map = values ?? {};
467
488
  return {
468
489
  decl: {
@@ -614,7 +635,10 @@ function convertComponent({ name, raw: given, parent, components }) {
614
635
  }
615
636
  if (parentName && propName in parentProps && stableStringify(prop) === stableStringify(parentProps[propName])) continue;
616
637
  const { decl, rules } = convertProp(propName, prop, ctx);
617
- if (decl) props[propName] = decl;
638
+ if (decl) props[propName] = prop.__required === true ? {
639
+ ...decl,
640
+ required: true
641
+ } : decl;
618
642
  drafts.push(...rules);
619
643
  }
620
644
  if (parentName) {
@@ -666,6 +690,8 @@ function convertComponent({ name, raw: given, parent, components }) {
666
690
  ...existing.layers[layerName],
667
691
  ...bag
668
692
  };
693
+ delete styles[key];
694
+ styles[key] = existing;
669
695
  }
670
696
  if (Object.keys(styles).length > 0) out.styles = styles;
671
697
  if (raw.defaultProps) out.defaultProps = convertValue(raw.defaultProps);
@@ -875,20 +901,43 @@ function convert(studio, options) {
875
901
  });
876
902
  }
877
903
  const rawCss = Array.isArray(studio.rawCss) ? studio.rawCss : [studio.rawCss ?? ""];
904
+ const varPrefix = studio.prefix ?? "uds";
905
+ const tokenByVar = tokenVarIndex(tokenRecord, varPrefix);
878
906
  for (const [index, css] of rawCss.entries()) {
879
907
  const { rules, unparsed } = parseRawCss(css);
880
908
  for (const statement of unparsed) report("raw-css-unparsed", `rawCss/${index}`, statement);
881
909
  for (const { selector, declarations } of rules) {
910
+ if (selector === "@font-face" && adoptFontFace(fontRecord, declarations)) continue;
882
911
  const name = globalStyleName(selector, globalStyleNames);
883
912
  globalStyleNames.add(name);
884
913
  globalStyleRecord[name] = GlobalStyle.item({
885
914
  selector,
886
- declarations
915
+ declarations: withTokenRefs(declarations, tokenByVar)
887
916
  });
888
917
  }
889
918
  }
919
+ for (const [name, style] of Object.entries(globalStyleRecord)) {
920
+ const body = style;
921
+ if (!body.declarations) continue;
922
+ globalStyleRecord[name] = GlobalStyle.item({
923
+ selector: body.selector,
924
+ declarations: withTokenRefs(body.declarations, tokenByVar)
925
+ });
926
+ }
927
+ bindFontFamilyTokens({
928
+ tokenRecord,
929
+ fontRecord,
930
+ globalStyleRecord,
931
+ varPrefix,
932
+ elsewhere: [componentRecord, compositeRecord]
933
+ });
934
+ bindIconDomains(componentRecord, studio.assetGroups ?? {});
935
+ readBracketedTokens(componentRecord, tokenByVar);
936
+ readBracketedTokens(compositeRecord, tokenByVar);
890
937
  declareRawCssKeys(componentRecord, stylePropRecord);
938
+ const guidanceRecord = designPrinciplesGuidance(studio.designPrinciples);
891
939
  const records = {
940
+ ...Object.keys(guidanceRecord).length > 0 ? { guidance: guidanceRecord } : {},
892
941
  globalStyles: globalStyleRecord,
893
942
  canvasRoles: canvasRoleRecord,
894
943
  modifiers: modifierRecord,
@@ -928,6 +977,296 @@ function convert(studio, options) {
928
977
  return config;
929
978
  }
930
979
  /** The registered config: every record, then parts moved under their owners, then default guidance. */
980
+ /**
981
+ * An icon library's host component draws its domain from the library, not from a copy of it.
982
+ *
983
+ * v1 spelled the same fact twice: the asset group listed every glyph, and the component that renders
984
+ * one (`component: 'Icon'`) enumerated the same names as its `name` variant — 1,500 values copied
985
+ * into a component body — and the set's variants again as its `variant` values. config-v2 lets a
986
+ * variant DELEGATE (`value: Icon.ref('icons')`, `Icon.ref('icons#variants')`), which is how every
987
+ * hand-authored system writes it, so a host prop whose enumerated values are exactly the library's
988
+ * members (or its variants) is bound to the library here. Anything narrower or wider is the author's
989
+ * own list and stays one.
990
+ */
991
+ function bindIconDomains(componentRecord, assetGroups) {
992
+ const sameSet = (left, right) => left.length === right.length && new Set(left).size === left.length && right.every((name) => left.includes(name));
993
+ for (const [group, meta] of Object.entries(assetGroups)) {
994
+ if (meta.assetKind === "font" || !meta.component) continue;
995
+ const host = componentRecord[meta.component];
996
+ if (!host?.props) continue;
997
+ const members = Array.isArray(meta.members) ? meta.members : void 0;
998
+ for (const decl of Object.values(host.props)) {
999
+ if (decl?.type !== "variant" || !Array.isArray(decl.value)) continue;
1000
+ const values = decl.value;
1001
+ if (members && sameSet(values, members)) decl.value = Icon.ref(group);
1002
+ else if (meta.variants && sameSet(values, meta.variants)) decl.value = { __ref: `icon:${group}#variants` };
1003
+ }
1004
+ }
1005
+ }
1006
+ /**
1007
+ * Every token's CSS custom property → the token, for reading a v1 global style back to refs.
1008
+ *
1009
+ * v1 resolved `tokens.background.primary` to `var(--yos-background-primary)` at config time and wrote
1010
+ * the string into the artifact. config-v2 names a token by REF in a rule, so the graph sees the edge
1011
+ * and a rename cascades; the string is what `cssVar` derives from the path, so deriving it for every
1012
+ * token and matching is exact. A name two tokens would share is left out rather than guessed.
1013
+ */
1014
+ function tokenVarIndex(tokenRecord, varPrefix) {
1015
+ const byVar = /* @__PURE__ */ new Map();
1016
+ for (const [path, body] of Object.entries(tokenRecord)) {
1017
+ if (!body || typeof body !== "object" || !("value" in body)) continue;
1018
+ const name = cssVar(void 0, "token", path, varPrefix);
1019
+ byVar.set(name, byVar.has(name) ? null : path);
1020
+ }
1021
+ const unique = /* @__PURE__ */ new Map();
1022
+ for (const [name, path] of byVar) if (path !== null) unique.set(name, path);
1023
+ return unique;
1024
+ }
1025
+ /**
1026
+ * Declarations as a v2 rule authors them: a value that IS one token's variable (`var(--yos-x)`)
1027
+ * read back to the ref, and a property spelled camelCase (`backgroundColor`) so it needs no quoting.
1028
+ * A value only containing a variable (`1px solid var(--yos-x)`) is CSS text and stays as written; a
1029
+ * custom property (`--x`) or a vendor-prefixed one (`-webkit-x`) keeps its spelling, since neither
1030
+ * has a camelCase form the stylesheet emitter reads back. A nested block (an `@supports` body) is
1031
+ * walked the same way.
1032
+ */
1033
+ function withTokenRefs(declarations, tokenByVar) {
1034
+ return Object.fromEntries(Object.entries(declarations).map(([property, value]) => {
1035
+ const key = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(property) ? property.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase()) : property;
1036
+ if (value && typeof value === "object" && !Array.isArray(value)) return [key, withTokenRefs(value, tokenByVar)];
1037
+ if (typeof value !== "string") return [key, value];
1038
+ const match = /^var\((--[A-Za-z0-9_-]+)\)$/.exec(value.trim());
1039
+ const path = match?.[1] ? tokenByVar.get(match[1]) : void 0;
1040
+ return [key, path === void 0 ? value : Token.ref(path)];
1041
+ }));
1042
+ }
1043
+ /**
1044
+ * A bracket literal that only wraps one token's variable — `'[var(--yos-font-size-075)]'` — is that
1045
+ * token, written the long way round: v1 had no way to say "this token" in a bag whose prop did not
1046
+ * list it, so authors reached for the variable. Read back to the ref wherever a bag holds one, so the
1047
+ * emitter can print the leaf and the graph sees the edge. Walks every bag under `styles` (rule layers,
1048
+ * their modifier blocks) and a composite's `styles`.
1049
+ */
1050
+ function readBracketedTokens(record, tokenByVar) {
1051
+ if (tokenByVar.size === 0) return;
1052
+ const walk = (value) => {
1053
+ if (typeof value === "string") {
1054
+ const match = /^\[var\((--[A-Za-z0-9_-]+)\)\]$/.exec(value.trim());
1055
+ const path = match?.[1] ? tokenByVar.get(match[1]) : void 0;
1056
+ return path === void 0 ? value : Token.ref(path);
1057
+ }
1058
+ if (Array.isArray(value)) return value.map(walk);
1059
+ if (value && typeof value === "object" && !isRef(value)) {
1060
+ const out = value;
1061
+ for (const [key, member] of Object.entries(out)) out[key] = walk(member);
1062
+ return out;
1063
+ }
1064
+ return value;
1065
+ };
1066
+ for (const body of Object.values(record)) {
1067
+ if (!body || typeof body !== "object") continue;
1068
+ const styles = body.styles;
1069
+ if (styles && typeof styles === "object") walk(styles);
1070
+ }
1071
+ }
1072
+ /** A `var(--name, fallback…)` value: the variable and whatever follows the first comma. */
1073
+ const VAR_VALUE = /^var\(\s*(--[A-Za-z0-9_-]+)\s*(?:,\s*([\s\S]*?))?\s*\)$/;
1074
+ /** A `font-family` list as its members, quotes stripped: `"YA Sans VF", Helvetica Neue` → two. */
1075
+ function familiesOf(stack) {
1076
+ return stack.split(",").map((entry) => entry.trim().replace(/^['"]|['"]$/g, "").trim()).filter((entry) => entry.length > 0);
1077
+ }
1078
+ /**
1079
+ * Every `font-family` token bound to the Font it names, and the variable it used to name it by
1080
+ * retired.
1081
+ *
1082
+ * v1 had no way for a token to SAY "this family": a font was a `@font-face` in verbatim CSS plus a
1083
+ * `:root` custom property carrying its stack (`--yos-font-family-yas: "YA Sans VF", Helvetica Neue,
1084
+ * …`), and the token read the property — `var(--yos-font-family-yas, ui-sans-serif, …)`. A system
1085
+ * that registered its fonts as assets got the property from the build instead, named by the member
1086
+ * (`--yos-font-family-yas` for `fonts/yas`). Either way the token's value is the NAME of a variable,
1087
+ * so the graph holds no edge from token to font, and in v2 nothing emits the variable — the token
1088
+ * would resolve to its `var()` fallback and the brand face would silently not apply.
1089
+ *
1090
+ * config-v2 spells the edge as the value: `Token.item({ value: Font.ref('fonts/yas') })`, which the
1091
+ * emitter renders as the family's stack. So a token whose value is such a variable is rewritten to the
1092
+ * ref of the font it reached, found two ways, in order:
1093
+ *
1094
+ * 1. a font whose leaf IS the variable's suffix — the asset-member spelling (`fonts/yas`);
1095
+ * 2. a `:root` property of that name whose first family names a font by `fontFamily` — the
1096
+ * verbatim-CSS spelling, where `adoptFontFace` already made the font from the face and keyed
1097
+ * it by the family's slug (`fonts/ya-sans-vf`).
1098
+ *
1099
+ * A literal stack whose first family is a declared font (`'"YA Sans VF", Arial'`) binds the same way.
1100
+ *
1101
+ * The stack's tail is the family's `fallbackStack` when the font has none yet: from the `:root`
1102
+ * property where there is one (that is what actually painted — the `var()` fallback only applied when
1103
+ * the property was missing), else from the `var()` fallback. And a `:root` property that only fed the
1104
+ * tokens now bound is dropped: its stack lives on the font, and a rule elsewhere still spelling
1105
+ * `var(--yos-font-family-yas)` keeps it declared, since dropping it would break that rule.
1106
+ */
1107
+ function bindFontFamilyTokens({ tokenRecord, fontRecord, globalStyleRecord, varPrefix, elsewhere }) {
1108
+ const fonts = /* @__PURE__ */ new Map();
1109
+ for (const [path, body] of Object.entries(fontRecord)) if (body && typeof body === "object" && "fontFamily" in body) fonts.set(path, body);
1110
+ if (fonts.size === 0) return;
1111
+ const unique = (pairs) => {
1112
+ const seen = /* @__PURE__ */ new Map();
1113
+ for (const [key, path] of pairs) seen.set(key, seen.has(key) ? null : path);
1114
+ const out = /* @__PURE__ */ new Map();
1115
+ for (const [key, path] of seen) if (path !== null) out.set(key, path);
1116
+ return out;
1117
+ };
1118
+ const byLeaf = unique([...fonts.keys()].map((path) => [path.slice(path.lastIndexOf("/") + 1), path]));
1119
+ const byFamily = unique([...fonts].map(([path, body]) => [body.fontFamily.toLowerCase(), path]));
1120
+ const propertyPrefix = `--${varPrefix}-font-family-`;
1121
+ const rootVars = /* @__PURE__ */ new Map();
1122
+ for (const [name, style] of Object.entries(globalStyleRecord)) {
1123
+ const body = style;
1124
+ if (body.selector?.trim() !== ":root" || !body.declarations) continue;
1125
+ for (const [property, value] of Object.entries(body.declarations)) if (property.startsWith(propertyPrefix) && typeof value === "string") rootVars.set(property.slice(propertyPrefix.length), {
1126
+ style: name,
1127
+ property,
1128
+ stack: value
1129
+ });
1130
+ }
1131
+ /** The font a token value reaches, and the fallbacks that painted beneath it. */
1132
+ const bind = (value) => {
1133
+ const asVar = VAR_VALUE.exec(value.trim());
1134
+ if (asVar?.[1]) {
1135
+ if (!asVar[1].startsWith(propertyPrefix)) return void 0;
1136
+ const name = asVar[1].slice(propertyPrefix.length);
1137
+ const root = rootVars.get(name);
1138
+ const byMember = byLeaf.get(name);
1139
+ if (byMember !== void 0) return {
1140
+ path: byMember,
1141
+ fallbacks: root ? familiesOf(root.stack).slice(1) : fallbackList(asVar[2]),
1142
+ via: name
1143
+ };
1144
+ if (root) {
1145
+ const [family, ...rest] = familiesOf(root.stack);
1146
+ const path = family ? byFamily.get(family.toLowerCase()) : void 0;
1147
+ if (path !== void 0) return {
1148
+ path,
1149
+ fallbacks: rest,
1150
+ via: name
1151
+ };
1152
+ }
1153
+ return;
1154
+ }
1155
+ const [family, ...rest] = familiesOf(value);
1156
+ const path = family ? byFamily.get(family.toLowerCase()) : void 0;
1157
+ return path === void 0 ? void 0 : {
1158
+ path,
1159
+ fallbacks: rest
1160
+ };
1161
+ };
1162
+ const consumed = /* @__PURE__ */ new Set();
1163
+ for (const [tokenPath, body] of Object.entries(tokenRecord)) {
1164
+ if (!body || typeof body !== "object" || !("value" in body)) continue;
1165
+ const token = body;
1166
+ const slots = [{
1167
+ get: () => token.value,
1168
+ set: (next) => token.value = next
1169
+ }, ...Object.values(token.overrides ?? {}).map((override) => ({
1170
+ get: () => override.value,
1171
+ set: (next) => override.value = next
1172
+ }))];
1173
+ for (const slot of slots) {
1174
+ const value = slot.get();
1175
+ if (typeof value !== "string") continue;
1176
+ const asVar = VAR_VALUE.exec(value.trim());
1177
+ const isFontVar = asVar?.[1]?.startsWith(propertyPrefix) === true;
1178
+ if (!isFontVar && !familiesOf(value).some((f) => byFamily.has(f.toLowerCase()))) continue;
1179
+ const bound = bind(value);
1180
+ if (!bound) {
1181
+ if (isFontVar) report("font-var-unbound", `tokens/${tokenPath}`, `reads ${asVar?.[1]}, which no declared font answers to; kept as written`);
1182
+ continue;
1183
+ }
1184
+ const font = fonts.get(bound.path);
1185
+ if (!font.fallbackStack?.length && bound.fallbacks.length > 0) font.fallbackStack = bound.fallbacks;
1186
+ slot.set(Font.ref(bound.path));
1187
+ if (bound.via !== void 0) consumed.add(bound.via);
1188
+ }
1189
+ }
1190
+ if (consumed.size === 0) return;
1191
+ const remaining = JSON.stringify([
1192
+ tokenRecord,
1193
+ globalStyleRecord,
1194
+ ...elsewhere
1195
+ ]);
1196
+ for (const name of consumed) {
1197
+ const root = rootVars.get(name);
1198
+ if (!root || remaining.includes(`var(${root.property}`)) continue;
1199
+ const style = globalStyleRecord[root.style];
1200
+ if (!style.declarations) continue;
1201
+ delete style.declarations[root.property];
1202
+ if (Object.keys(style.declarations).length === 0) delete globalStyleRecord[root.style];
1203
+ }
1204
+ }
1205
+ /**
1206
+ * One `@font-face` rule folded into the `fonts` library as a file of the family it declares.
1207
+ *
1208
+ * The family's slug is its name in kebab case, files are keyed `<weight>-<style>` the way the Font
1209
+ * entity's `files` are authored, and a weight RANGE (`100 900`) marks the family variable. The first
1210
+ * `url(…) format(…)` pair is the file; a face with no parseable `src` or family is not adopted and
1211
+ * stays a global style. Returns whether the rule was taken.
1212
+ */
1213
+ function adoptFontFace(fontRecord, declarations) {
1214
+ const text = (property) => {
1215
+ const value = declarations[property];
1216
+ return typeof value === "string" ? value.trim() : void 0;
1217
+ };
1218
+ const family = text("font-family")?.replace(/^['"]|['"]$/g, "");
1219
+ const src = text("src");
1220
+ if (!family || !src) return false;
1221
+ const file = /url\((['"]?)([^'")]+)\1\)(?:\s*format\((['"]?)([^'")]+)\3\))?/.exec(src);
1222
+ if (!file?.[2]) return false;
1223
+ const url = file[2];
1224
+ const format = file[4]?.split(/\s+/)[0];
1225
+ const weightText = text("font-weight") ?? "400";
1226
+ const style = text("font-style") ?? "normal";
1227
+ const isRange = /\s/.test(weightText);
1228
+ const weight = !isRange && /^\d+$/.test(weightText) ? Number(weightText) : weightText;
1229
+ const path = `fonts/${family.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
1230
+ if (!("fonts" in fontRecord)) fontRecord.fonts = Font.group({ label: "Fonts" });
1231
+ const existing = fontRecord[path] ?? {
1232
+ fontFamily: family,
1233
+ files: {}
1234
+ };
1235
+ existing.files[`${weightText}-${style}`] = {
1236
+ src: url,
1237
+ ...format ? { format } : {},
1238
+ weight,
1239
+ style
1240
+ };
1241
+ if (isRange) existing.isVariableFont = true;
1242
+ fontRecord[path] = existing;
1243
+ return true;
1244
+ }
1245
+ /**
1246
+ * v1 `designPrinciples` as a `guidance` group.
1247
+ *
1248
+ * The list was prose for the model with no keys of its own, and a guidance item needs one. Each
1249
+ * principle is keyed by a slug of its leading words — `yahoo-purple-is-the-signature` — which reads
1250
+ * in Studio the way the reference system's hand-written keys do, and takes a numeric suffix only
1251
+ * where two principles open alike.
1252
+ */
1253
+ function designPrinciplesGuidance(principles) {
1254
+ const texts = (principles ?? []).filter((principle) => typeof principle === "string" && principle.trim().length > 0);
1255
+ if (texts.length === 0) return {};
1256
+ const record = { "design-principles": Guidance.group({
1257
+ label: "Design principles",
1258
+ description: "The system's brand and product principles, carried over from the v1 config's `designPrinciples`."
1259
+ }) };
1260
+ const used = /* @__PURE__ */ new Set();
1261
+ for (const text of texts) {
1262
+ const base = text.toLowerCase().replace(/`[^`]*`/g, " ").replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).slice(0, 5).join("-") || "principle";
1263
+ let key = base;
1264
+ for (let n = 2; used.has(key); n++) key = `${base}-${n}`;
1265
+ used.add(key);
1266
+ record[`design-principles/${key}`] = Guidance.item({ text: [text] });
1267
+ }
1268
+ return record;
1269
+ }
931
1270
  function assemble({ settings, records, renames }) {
932
1271
  let config = new System().register({ settings }).register(records);
933
1272
  if (renames.length > 0) config = config.run(renames.map(({ from, to }) => ({
@@ -936,7 +1275,8 @@ function assemble({ settings, records, renames }) {
936
1275
  path: from,
937
1276
  to
938
1277
  })));
939
- if (config.list("guidance").length === 0) config = config.register({ guidance: DEFAULT_GUIDANCE });
1278
+ const seeded = Object.fromEntries(Object.entries(DEFAULT_GUIDANCE).filter(([group]) => !Guidance.groups(config).some((held) => held.path === group)));
1279
+ if (Object.keys(seeded).length > 0) config = config.register({ guidance: seeded });
940
1280
  return config;
941
1281
  }
942
1282
  /** Kind → the record `register` takes it under. Read at call time: `System` is a cycle away. */
@@ -0,0 +1,9 @@
1
+ /**
2
+ * TODO: say what changed about the wire shape and why.
3
+ *
4
+ * See 20260908171448_settings_as_kind.ts for a complete worked example (frozen schemas, a slot
5
+ * transform), and .claude/rules/config-schema-migrations.md for the full mint checklist.
6
+ */
7
+ import type { SchemaMigration } from '../framework/schema-version';
8
+ export declare const PREVIEW_CONDITIONS_VERSION = 20260912161840;
9
+ export declare const previewConditionsMigration: SchemaMigration;
@@ -0,0 +1,10 @@
1
+ //#region src/migrations/20260912161840_preview_conditions.ts
2
+ const PREVIEW_CONDITIONS_VERSION = 20260912161840;
3
+ const previewConditionsMigration = {
4
+ version: PREVIEW_CONDITIONS_VERSION,
5
+ up(json) {
6
+ return json;
7
+ }
8
+ };
9
+ //#endregion
10
+ export { PREVIEW_CONDITIONS_VERSION, previewConditionsMigration };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A font gains an optional `variable`: the CSS custom property a HOST publishes the loaded face's
3
+ * family under, the way `next/font`'s `variable` option does (`--uds-font-family-inter` holds
4
+ * `'__Inter_a1b2c3', '__Inter_Fallback_a1b2c3'`).
5
+ *
6
+ * Why a field and not a token spelling: a `font-family` token that read such a property spelled it
7
+ * as a literal — `var(--uds-font-family-inter, ui-sans-serif, …)` — so the graph held no edge from
8
+ * the token to any font, and the token could not be `Font.ref(…)` without dropping the variable and
9
+ * with it the face the host loads. With the property on the font, the token is a ref and the stack
10
+ * the font stands for is `var(--uds-font-family-inter, "Inter", ui-sans-serif, …)`: the host's face
11
+ * first, the family by name beneath it, the fallbacks last — the same CSS the literal produced.
12
+ *
13
+ * Additive. A font stored without the field is a font the stylesheet addresses by family name,
14
+ * which is what every font stored before this version was.
15
+ */
16
+ import type { SchemaMigration } from '../framework/schema-version';
17
+ export declare const FONT_VARIABLE_VERSION = 20260912164927;
18
+ export declare const fontVariableMigration: SchemaMigration;
@@ -0,0 +1,10 @@
1
+ //#region src/migrations/20260912164927_font_variable.ts
2
+ const FONT_VARIABLE_VERSION = 20260912164927;
3
+ const fontVariableMigration = {
4
+ version: FONT_VARIABLE_VERSION,
5
+ up(json) {
6
+ return json;
7
+ }
8
+ };
9
+ //#endregion
10
+ export { FONT_VARIABLE_VERSION, fontVariableMigration };