@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.
- package/dist/configs/CanvasConfig.d.ts +24 -0
- package/dist/configs/react-native-system.d.ts +20 -2
- package/dist/configs/react-native-system.js +1 -1
- package/dist/configs/system.d.ts +18 -0
- package/dist/configs/system.js +5 -1
- package/dist/entities/native/NativeStyleProperty.d.ts +2 -2
- package/dist/entities/system/Component.d.ts +12 -0
- package/dist/entities/system/Component.js +6 -0
- package/dist/entities/system/Font.d.ts +25 -1
- package/dist/entities/system/Font.js +34 -1
- package/dist/entities/system/Token.js +1 -1
- package/dist/entities/system/defineComponent.d.ts +32 -11
- package/dist/entities/system/defineComponent.js +10 -1
- package/dist/entities/system/icon-library.js +5 -1
- package/dist/entities/system/style-bag.js +1 -0
- package/dist/framework/Config.d.ts +16 -1
- package/dist/framework/Config.js +33 -1
- package/dist/framework/projections.d.ts +23 -2
- package/dist/framework/projections.js +67 -11
- package/dist/framework/render-spec.js +16 -1
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/framework/utils/field-schema.js +3 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/migrations/2.0.0/v1-artifact.d.ts +20 -0
- package/dist/migrations/2.0.0/v1-artifact.js +358 -18
- package/dist/migrations/20260912161840_preview_conditions.d.ts +9 -0
- package/dist/migrations/20260912161840_preview_conditions.js +10 -0
- package/dist/migrations/20260912164927_font_variable.d.ts +18 -0
- package/dist/migrations/20260912164927_font_variable.js +10 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- 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
|
-
|
|
322
|
-
|
|
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: "
|
|
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
|
-
/**
|
|
343
|
-
*
|
|
344
|
-
|
|
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))
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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] =
|
|
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
|
-
|
|
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 };
|