@vectoriox/iox-ui 5.13.1 → 5.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/design-core/index.d.ts +266 -33
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +494 -77
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +39 -6
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/index.d.ts +25 -4
- package/package.json +1 -1
- package/src/design-core/design-apply.ts +116 -0
- package/src/design-core/design-settings.ts +194 -0
- package/src/design-core/element-styles.ts +4 -20
- package/src/design-core/empty-image.ts +52 -0
- package/src/design-core/flavor-seed.ts +39 -13
- package/src/design-core/flavor-spec.generated.ts +12 -4
- package/src/design-core/flavor.ts +6 -5
- package/src/design-core/fonts.ts +45 -8
- package/src/design-core/node-classes.ts +82 -0
- package/src/design-core/public-api.ts +4 -0
- package/src/design-core/token-css.ts +32 -2
- package/src/design-core/token-names.ts +21 -0
- package/styles/iox-design.css +30 -0
|
@@ -1071,7 +1071,7 @@ const SPEC_SPRINGS = {
|
|
|
1071
1071
|
}
|
|
1072
1072
|
};
|
|
1073
1073
|
const SPEC_FONTS = {
|
|
1074
|
-
"Inter": "Inter:wght@400;500;600;700",
|
|
1074
|
+
"Inter": "Inter:wght@300;400;500;600;700",
|
|
1075
1075
|
"Inter Tight": "Inter+Tight:wght@500;600;700",
|
|
1076
1076
|
"Manrope": "Manrope:wght@400;500;600;700",
|
|
1077
1077
|
"IBM Plex Sans": "IBM+Plex+Sans:wght@400;500;600;700",
|
|
@@ -1086,12 +1086,21 @@ const SPEC_FONTS = {
|
|
|
1086
1086
|
"Figtree": "Figtree:wght@400;500;600;700;800",
|
|
1087
1087
|
"Google Sans Flex": "Google+Sans+Flex:wght@400;500;600;700",
|
|
1088
1088
|
"Bricolage Grotesque": "Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700",
|
|
1089
|
-
"Nunito": "Nunito:wght@400;600;700;800",
|
|
1089
|
+
"Nunito": "Nunito:wght@300;400;600;700;800",
|
|
1090
1090
|
"DM Sans": "DM+Sans:wght@400;500;700",
|
|
1091
1091
|
"Archivo": "Archivo:wght@400;500;700",
|
|
1092
1092
|
"Archivo Black": "Archivo+Black",
|
|
1093
1093
|
"Syne": "Syne:wght@600;700;800",
|
|
1094
|
-
"Space Mono": "Space+Mono:wght@400;700"
|
|
1094
|
+
"Space Mono": "Space+Mono:wght@400;700",
|
|
1095
|
+
"Poppins": "Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400",
|
|
1096
|
+
"Roboto": "Roboto:ital,wght@0,300;0,400;0,500;0,700;1,400",
|
|
1097
|
+
"Lato": "Lato:ital,wght@0,300;0,400;0,700;1,400",
|
|
1098
|
+
"Open Sans": "Open+Sans:ital,wght@0,300;0,400;0,600;0,700;1,400",
|
|
1099
|
+
"Montserrat": "Montserrat:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400",
|
|
1100
|
+
"Raleway": "Raleway:wght@300;400;500;600;700",
|
|
1101
|
+
"Playfair Display": "Playfair+Display:ital,wght@0,400;0,500;0,700;1,400",
|
|
1102
|
+
"Oswald": "Oswald:wght@300;400;500;600;700",
|
|
1103
|
+
"Ubuntu": "Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,400"
|
|
1095
1104
|
};
|
|
1096
1105
|
const SPEC_SCHEMES = {
|
|
1097
1106
|
"default": {
|
|
@@ -1748,7 +1757,6 @@ const SPEC_FIELD_CLASS_BASELINE = {
|
|
|
1748
1757
|
const SPEC_LEGACY_ALIASES = {
|
|
1749
1758
|
"radius-control": "var(--iox-button-radius)",
|
|
1750
1759
|
"radius-surface": "var(--iox-card-radius)",
|
|
1751
|
-
"space-control": "var(--iox-button-pad)",
|
|
1752
1760
|
"space-stack": "var(--iox-space-gap)"
|
|
1753
1761
|
};
|
|
1754
1762
|
const SPEC_FLAVOR_PACKS = [
|
|
@@ -9498,13 +9506,61 @@ function styleKeyToKebab(key) {
|
|
|
9498
9506
|
return key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
|
|
9499
9507
|
}
|
|
9500
9508
|
|
|
9509
|
+
/**
|
|
9510
|
+
* Which `--iox-<name>` a piece of COMPONENT CSS may reference (design-system §3b-bis: "component CSS
|
|
9511
|
+
* may reference ONLY tokens defined in this spec"). DS2 shipped seven names nobody defines
|
|
9512
|
+
* (`card-media-gap`, `gallery-overlay`, `dialog-close-bg`…): a token the contract does not define can
|
|
9513
|
+
* never be set by a flavor, so it is a literal wearing a costume. One guard, run over BOTH
|
|
9514
|
+
* renderers' component CSS (iox-ui's spec; the engine's `verify:token-names`).
|
|
9515
|
+
*/
|
|
9516
|
+
/** The 161 names of the contract's two tiers, plus its legacy aliases. */
|
|
9517
|
+
function specTokenNames() {
|
|
9518
|
+
return new Set([
|
|
9519
|
+
...SPEC_TIERS.semantic.map(t => t.name),
|
|
9520
|
+
...SPEC_TIERS.component.map(t => t.name),
|
|
9521
|
+
...Object.keys(SPEC_LEGACY_ALIASES),
|
|
9522
|
+
]);
|
|
9523
|
+
}
|
|
9524
|
+
const REFERENCE$1 = /var\(\s*--iox-([a-z0-9-]+)/gi;
|
|
9525
|
+
/** Every distinct token a stylesheet READS (`var(--iox-x…)`), in first-seen order. */
|
|
9526
|
+
function tokenReferences(css) {
|
|
9527
|
+
return [...new Set(Array.from(css.matchAll(REFERENCE$1), m => m[1]))];
|
|
9528
|
+
}
|
|
9529
|
+
/** The referenced names that are neither spec tokens nor in `alsoAllowed`. */
|
|
9530
|
+
function unknownTokenReferences(css, alsoAllowed = []) {
|
|
9531
|
+
const spec = specTokenNames();
|
|
9532
|
+
const extra = new Set(alsoAllowed);
|
|
9533
|
+
return tokenReferences(css).filter(name => !spec.has(name) && !extra.has(name));
|
|
9534
|
+
}
|
|
9535
|
+
const LEGACY_SCOPE = /:not\(\s*\.iox-es\s*\)/;
|
|
9536
|
+
const RULE = /([^{}]+)\{([^{}]*)\}/g;
|
|
9537
|
+
/**
|
|
9538
|
+
* Tokens read by a LEGACY rule — any rule whose selector is scoped `:not(.iox-es)` (the pre-DS2 look
|
|
9539
|
+
* of a node that wears no element style, design-system §3b-bis). Such a rule must read NO token at
|
|
9540
|
+
* all: a token it reads repaints a pre-DS2 page the moment the org's rows are seeded or a flavor is
|
|
9541
|
+
* applied (D5). Returns `selector → names` for each offending rule; comments are ignored.
|
|
9542
|
+
*/
|
|
9543
|
+
function legacyRuleTokenReferences(css) {
|
|
9544
|
+
const text = String(css ?? '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
9545
|
+
const out = [];
|
|
9546
|
+
for (const m of text.matchAll(RULE)) {
|
|
9547
|
+
const selector = m[1].trim().replace(/\s+/g, ' ');
|
|
9548
|
+
if (!LEGACY_SCOPE.test(selector))
|
|
9549
|
+
continue;
|
|
9550
|
+
const tokens = tokenReferences(m[2]);
|
|
9551
|
+
if (tokens.length)
|
|
9552
|
+
out.push({ selector, tokens });
|
|
9553
|
+
}
|
|
9554
|
+
return out;
|
|
9555
|
+
}
|
|
9556
|
+
|
|
9501
9557
|
/** What `prefers-reduced-motion: reduce` collapses — the contract's complete list (`a11y.reducedMotion`). */
|
|
9502
9558
|
const REDUCED_MOTION_OVERRIDES = SPEC_A11Y.reducedMotion;
|
|
9503
9559
|
/** What `prefers-reduced-transparency: reduce` makes solid — the contract's `a11y.reducedTransparency`. */
|
|
9504
9560
|
const REDUCED_TRANSPARENCY_OVERRIDES = SPEC_A11Y.reducedTransparency;
|
|
9505
9561
|
const ROOT = ':root';
|
|
9506
9562
|
const DEFAULT_SCHEME = 'default';
|
|
9507
|
-
const REFERENCE
|
|
9563
|
+
const REFERENCE = 'var(';
|
|
9508
9564
|
const MEDIA_DARK = '(prefers-color-scheme: dark)';
|
|
9509
9565
|
const ACCESSIBILITY_MEDIA = [
|
|
9510
9566
|
{ query: '(prefers-reduced-motion: reduce)', overrides: REDUCED_MOTION_OVERRIDES },
|
|
@@ -9549,6 +9605,16 @@ const PLACEHOLDER = /\{\{\s*([\w-]+)\s*\}\}/g;
|
|
|
9549
9605
|
const LIGHT = 'light';
|
|
9550
9606
|
const DARK = 'dark';
|
|
9551
9607
|
const modeValue = (t, mode) => (mode === DARK ? darkOf(t) ?? t.value : t.value);
|
|
9608
|
+
/** The rows as ONE mode: each row's value in that mode, no dark override left. `auto` (or none) →
|
|
9609
|
+
* the rows unchanged. */
|
|
9610
|
+
function rowsForMode(rows, mode) {
|
|
9611
|
+
if (mode !== LIGHT && mode !== DARK)
|
|
9612
|
+
return [...rows];
|
|
9613
|
+
return rows.map(t => {
|
|
9614
|
+
const { values: _dropped, ...rest } = t;
|
|
9615
|
+
return { ...rest, value: modeValue(t, mode) };
|
|
9616
|
+
});
|
|
9617
|
+
}
|
|
9552
9618
|
/** Every token's ROOT value in one mode, with `var(--iox-…)` references resolved against the same
|
|
9553
9619
|
* set (recursively; a reference cycle or an unknown token is left as written). */
|
|
9554
9620
|
function resolveTokenValues(tokens, mode = LIGHT) {
|
|
@@ -9614,7 +9680,7 @@ function compileSchemeCss(tokens, scope) {
|
|
|
9614
9680
|
const resolved = resolveTokenValues(rows, LIGHT);
|
|
9615
9681
|
return nonDefaultSchemes().map(([id, set]) => rule(schemeSelector(id, scope), [
|
|
9616
9682
|
...schemeEntries(set, resolved),
|
|
9617
|
-
...rows.filter(t => !(t.name in set) && t.value.includes(REFERENCE
|
|
9683
|
+
...rows.filter(t => !(t.name in set) && t.value.includes(REFERENCE))
|
|
9618
9684
|
.map((t) => [t.name, t.value]),
|
|
9619
9685
|
])).filter(Boolean).join('\n');
|
|
9620
9686
|
}
|
|
@@ -9625,7 +9691,7 @@ function darkSchemeRules(rows, scope) {
|
|
|
9625
9691
|
const dark = resolveTokenValues(rows, DARK);
|
|
9626
9692
|
const redeclared = rows.filter(t => {
|
|
9627
9693
|
const d = darkOf(t);
|
|
9628
|
-
return d !== null && (t.value.includes(REFERENCE
|
|
9694
|
+
return d !== null && (t.value.includes(REFERENCE) || d.includes(REFERENCE));
|
|
9629
9695
|
});
|
|
9630
9696
|
return nonDefaultSchemes().map(([id, set]) => {
|
|
9631
9697
|
const swapped = Object.fromEntries(Object.entries(set).filter(([, v]) => hasPlaceholder(v)));
|
|
@@ -9641,10 +9707,14 @@ function presentOverrides(rows, overrides, onlyWithDark) {
|
|
|
9641
9707
|
}
|
|
9642
9708
|
/** The token CSS as the two halves a renderer places around its node rules. */
|
|
9643
9709
|
function compileTokenCssBlocks(tokens, opts = {}) {
|
|
9644
|
-
const rows = usableTokens(tokens);
|
|
9710
|
+
const rows = rowsForMode(usableTokens(tokens), opts.mode);
|
|
9645
9711
|
const root = opts.scope ?? ROOT;
|
|
9712
|
+
const present = new Set(rows.map(t => t.name));
|
|
9713
|
+
const unset = opts.isolate
|
|
9714
|
+
? [...specTokenNames()].filter(name => !present.has(name)).map((name) => [name, 'initial'])
|
|
9715
|
+
: [];
|
|
9646
9716
|
const base = [
|
|
9647
|
-
rule(root, rows.map((t) => [t.name, t.value])),
|
|
9717
|
+
rule(root, [...unset, ...rows.map((t) => [t.name, t.value])]),
|
|
9648
9718
|
compileSchemeCss(rows, opts.scope),
|
|
9649
9719
|
...ACCESSIBILITY_MEDIA.map(({ query, overrides }) => media(query, [rule(root, presentOverrides(rows, overrides, false), ' ')])),
|
|
9650
9720
|
].filter(Boolean).join('\n');
|
|
@@ -9676,9 +9746,9 @@ function validateFlavorPack(pack) {
|
|
|
9676
9746
|
return errors;
|
|
9677
9747
|
}
|
|
9678
9748
|
/**
|
|
9679
|
-
* Swiss — the F1 pack: four tokens (control/surface radius, control/stack spacing) and the
|
|
9680
|
-
* opinion F2's brand step
|
|
9681
|
-
*
|
|
9749
|
+
* Swiss — the retired F1 pack: four tokens (control/surface radius, control/stack spacing) and the
|
|
9750
|
+
* hue opinion F2's brand step read. Three of its rows become the spec's `legacyAliases` on migration;
|
|
9751
|
+
* `space-control` is a v2 token in every pack and takes Crisp's value.
|
|
9682
9752
|
*/
|
|
9683
9753
|
const SWISS_FLAVOR = {
|
|
9684
9754
|
id: 'swiss',
|
|
@@ -9890,33 +9960,16 @@ function switchElementStyle(target, fromId, toId, baseline = {}) {
|
|
|
9890
9960
|
}
|
|
9891
9961
|
/**
|
|
9892
9962
|
* The element-style MARKER (design-system §3b-bis, spec `elementStyleMarker`). A node that wears an
|
|
9893
|
-
* element style renders `iox-es iox-es-<id>` in BOTH renderers
|
|
9894
|
-
*
|
|
9895
|
-
*
|
|
9896
|
-
*
|
|
9963
|
+
* element style renders `iox-es iox-es-<id>` in BOTH renderers, through the one node-class helper
|
|
9964
|
+
* `nodeDesignClasses` (node-classes.ts); every piece of component CSS that carries the system look is
|
|
9965
|
+
* scoped to it, so a node saved before DS2 (no `elementStyle`) keeps its exact pre-DS2 component CSS
|
|
9966
|
+
* no matter which tokens the org holds.
|
|
9897
9967
|
*/
|
|
9898
9968
|
const ELEMENT_STYLE_MARKER_CLASS = 'iox-es';
|
|
9899
|
-
const MARKER_PREFIX = `${ELEMENT_STYLE_MARKER_CLASS}-`;
|
|
9969
|
+
const MARKER_PREFIX$1 = `${ELEMENT_STYLE_MARKER_CLASS}-`;
|
|
9900
9970
|
/** `iox-es-<id>` for a known style, else null — an unknown id never reaches a class attribute. */
|
|
9901
9971
|
function elementStyleClass(id) {
|
|
9902
|
-
return elementStyleById(id) ? `${MARKER_PREFIX}${id}` : null;
|
|
9903
|
-
}
|
|
9904
|
-
/** The marker classes a node renders: `'iox-es iox-es-<id>'`, or `''` for a node with no (known) style. */
|
|
9905
|
-
function elementStyleClasses(node) {
|
|
9906
|
-
const own = elementStyleClass(node?.elementStyle);
|
|
9907
|
-
return own ? `${ELEMENT_STYLE_MARKER_CLASS} ${own}` : '';
|
|
9908
|
-
}
|
|
9909
|
-
/** Make an element's marker match `elementStyle` — the live switch on the canvas, and the
|
|
9910
|
-
* component path's one-time stamp. Removes a stale `iox-es-<old>`; touches no other class. */
|
|
9911
|
-
function syncElementStyleMarker(el, elementStyle) {
|
|
9912
|
-
if (!el?.classList)
|
|
9913
|
-
return;
|
|
9914
|
-
const stale = Array.from(el.classList).filter(c => c === ELEMENT_STYLE_MARKER_CLASS || c.startsWith(MARKER_PREFIX));
|
|
9915
|
-
for (const c of stale)
|
|
9916
|
-
el.classList.remove(c);
|
|
9917
|
-
const classes = elementStyleClasses({ elementStyle });
|
|
9918
|
-
if (classes)
|
|
9919
|
-
el.classList.add(...classes.split(' '));
|
|
9972
|
+
return elementStyleById(id) ? `${MARKER_PREFIX$1}${id}` : null;
|
|
9920
9973
|
}
|
|
9921
9974
|
/**
|
|
9922
9975
|
* The spec's `headingLevelRule`: changing a Heading's level moves it to the new level's DEFAULT
|
|
@@ -9934,6 +9987,117 @@ function applyHeadingLevelRule(target, oldLevel, newLevel, baseline = {}) {
|
|
|
9934
9987
|
return switchElementStyle(target, from, to, baseline);
|
|
9935
9988
|
}
|
|
9936
9989
|
|
|
9990
|
+
/**
|
|
9991
|
+
* The design-system classes a node renders, in BOTH renderers (builder canvas, engine component
|
|
9992
|
+
* path, engine string path) — ONE helper, so the three can never disagree about which class a node
|
|
9993
|
+
* carries:
|
|
9994
|
+
*
|
|
9995
|
+
* - the element-style MARKER `iox-es iox-es-<id>` (design-system §3b-bis) — component CSS carrying
|
|
9996
|
+
* the system look is scoped to it, so a node saved before DS2 keeps its pre-DS2 look;
|
|
9997
|
+
* - the section colour scheme `iox-scheme-<id>` (§3c) for a non-default `colorScheme` — the class
|
|
9998
|
+
* `compileSchemeCss` re-declares tokens on, so everything inside re-resolves.
|
|
9999
|
+
*
|
|
10000
|
+
* Renderers read `elementStyle` and `colorScheme` for exactly this and nothing else.
|
|
10001
|
+
*/
|
|
10002
|
+
/** The section colour schemes — the contract's `schemes` keys. */
|
|
10003
|
+
var ColorScheme;
|
|
10004
|
+
(function (ColorScheme) {
|
|
10005
|
+
ColorScheme["Default"] = "default";
|
|
10006
|
+
ColorScheme["Subtle"] = "subtle";
|
|
10007
|
+
ColorScheme["Inverse"] = "inverse";
|
|
10008
|
+
ColorScheme["Accent"] = "accent";
|
|
10009
|
+
})(ColorScheme || (ColorScheme = {}));
|
|
10010
|
+
/** The content trait (stored in `props`, not a style prop) that picks a node's scheme. */
|
|
10011
|
+
const COLOR_SCHEME_TRAIT = 'colorScheme';
|
|
10012
|
+
/** The node types that offer a scheme (spec §3c: Section/Container; LinkedContainer is a Container that links). */
|
|
10013
|
+
const COLOR_SCHEME_TYPES = ['Section', 'Container', 'LinkedContainer'];
|
|
10014
|
+
/** Every scheme in contract order, with the contract's label (Default · Subtle · Inverse · Brand). */
|
|
10015
|
+
const COLOR_SCHEMES = Object.keys(SPEC_SCHEMES).map(id => ({ id, label: SPEC_SCHEMES[id].label }));
|
|
10016
|
+
const SCHEME_IDS = new Set(Object.keys(SPEC_SCHEMES));
|
|
10017
|
+
const SCHEME_PREFIX = 'iox-scheme-';
|
|
10018
|
+
const MARKER_PREFIX = `${ELEMENT_STYLE_MARKER_CLASS}-`;
|
|
10019
|
+
/** `iox-scheme-<id>` for a known NON-default scheme, else null — an unknown id never reaches a class attribute. */
|
|
10020
|
+
function colorSchemeClass(id) {
|
|
10021
|
+
return typeof id === 'string' && id !== ColorScheme.Default && SCHEME_IDS.has(id) ? `${SCHEME_PREFIX}${id}` : null;
|
|
10022
|
+
}
|
|
10023
|
+
/** A node's `colorScheme`, from `props` (serialized) or its trait (builder). */
|
|
10024
|
+
function colorSchemeOf(node) {
|
|
10025
|
+
if (!node)
|
|
10026
|
+
return null;
|
|
10027
|
+
const fromProps = node.props?.[COLOR_SCHEME_TRAIT];
|
|
10028
|
+
if (fromProps !== undefined)
|
|
10029
|
+
return typeof fromProps === 'string' ? fromProps : null;
|
|
10030
|
+
const trait = node.traits?.find(t => t?.name === COLOR_SCHEME_TRAIT)?.default;
|
|
10031
|
+
return typeof trait === 'string' ? trait : null;
|
|
10032
|
+
}
|
|
10033
|
+
/** True when the node wears a known element style — the marker is on it. */
|
|
10034
|
+
function hasElementStyleMarker(node) {
|
|
10035
|
+
return elementStyleClass(node?.elementStyle) !== null;
|
|
10036
|
+
}
|
|
10037
|
+
/** The classes a node renders: `'iox-es iox-es-card iox-scheme-inverse'`, or `''` for none. */
|
|
10038
|
+
function nodeDesignClasses(node) {
|
|
10039
|
+
const marker = elementStyleClass(node?.elementStyle);
|
|
10040
|
+
return [marker ? `${ELEMENT_STYLE_MARKER_CLASS} ${marker}` : '', colorSchemeClass(colorSchemeOf(node)) ?? '']
|
|
10041
|
+
.filter(Boolean).join(' ');
|
|
10042
|
+
}
|
|
10043
|
+
const isDesignClass = (c) => c === ELEMENT_STYLE_MARKER_CLASS || c.startsWith(MARKER_PREFIX) || c.startsWith(SCHEME_PREFIX);
|
|
10044
|
+
/** Make an element's design classes match the node — the live switch on the canvas and the
|
|
10045
|
+
* component path's one-time stamp. Removes stale ones; touches no other class. */
|
|
10046
|
+
function syncNodeDesignClasses(el, node) {
|
|
10047
|
+
if (!el?.classList)
|
|
10048
|
+
return;
|
|
10049
|
+
for (const c of Array.from(el.classList).filter(isDesignClass))
|
|
10050
|
+
el.classList.remove(c);
|
|
10051
|
+
const classes = nodeDesignClasses(node);
|
|
10052
|
+
if (classes)
|
|
10053
|
+
el.classList.add(...classes.split(' '));
|
|
10054
|
+
}
|
|
10055
|
+
|
|
10056
|
+
/**
|
|
10057
|
+
* Empty images (design-system DS3). A MARKED Image (`.iox-es-media`) with no source renders a box in
|
|
10058
|
+
* the `surface-2` token with its aspect ratio kept — on the canvas with an "Add image" label, on the
|
|
10059
|
+
* live site with nothing in it. An UNMARKED empty Image renders exactly as before (no `src`).
|
|
10060
|
+
*
|
|
10061
|
+
* Why a placeholder `src` instead of a src-less `<img>`: Chromium paints a src-less image as a broken
|
|
10062
|
+
* image (a frame, an icon and the alt text) and ignores `aspect-ratio` once alt text is present, and no
|
|
10063
|
+
* pseudo-element paints on an `<img>`. A real image with no natural size of its own (an SVG with no
|
|
10064
|
+
* width/height) is a proper replaced element: `aspect-ratio`/`object-fit` apply, the background shows
|
|
10065
|
+
* through, and its default object size (300×150) is the SAME for the canvas label and the live blank,
|
|
10066
|
+
* so both renderers lay the box out identically. The fill itself is shared CSS (`iox-design.css`)
|
|
10067
|
+
* keyed on {@link EMPTY_IMAGE_ATTRIBUTE}.
|
|
10068
|
+
*/
|
|
10069
|
+
/** Set on a marked Image while it has no source — the hook the shared fill rule keys on. */
|
|
10070
|
+
const EMPTY_IMAGE_ATTRIBUTE = 'data-iox-empty';
|
|
10071
|
+
const svgUri = (svg) => `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
10072
|
+
/** The live site's blank: an SVG with no intrinsic size and nothing in it. */
|
|
10073
|
+
const EMPTY_IMAGE_SRC = svgUri('<svg xmlns="http://www.w3.org/2000/svg"/>');
|
|
10074
|
+
const xmlEscape = (s) => s.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
10075
|
+
/**
|
|
10076
|
+
* The canvas blank: the same no-size SVG with `label` centred as a chip. The chip carries its own
|
|
10077
|
+
* light ground, so it reads on every flavor's `surface-2`, light or dark; it is builder chrome, never
|
|
10078
|
+
* site content. Width is estimated from the label length (an image cannot measure text).
|
|
10079
|
+
*/
|
|
10080
|
+
function emptyImageLabelSrc(label) {
|
|
10081
|
+
const text = xmlEscape(label);
|
|
10082
|
+
const width = Math.round(28 + label.length * 7);
|
|
10083
|
+
return svgUri('<svg xmlns="http://www.w3.org/2000/svg"><svg x="50%" y="50%" overflow="visible">'
|
|
10084
|
+
+ `<rect x="${-width / 2}" y="-13" width="${width}" height="26" rx="13" fill="#ffffff" fill-opacity="0.92"/>`
|
|
10085
|
+
+ '<text x="0" y="0" text-anchor="middle" dominant-baseline="central" font-family="system-ui, -apple-system, \'Segoe UI\', sans-serif" '
|
|
10086
|
+
+ `font-size="12" font-weight="500" fill="#1f1d1b">${text}</text></svg></svg>`);
|
|
10087
|
+
}
|
|
10088
|
+
/**
|
|
10089
|
+
* The `src` a node's Image renders when its own source is empty: the canvas label (when `label` is
|
|
10090
|
+
* given) or the live blank for a MARKED node; `null` — render exactly as before — for an unmarked
|
|
10091
|
+
* node or one that has a source.
|
|
10092
|
+
*/
|
|
10093
|
+
function emptyImageSrc(node, src, label) {
|
|
10094
|
+
if (src != null && src !== '')
|
|
10095
|
+
return null;
|
|
10096
|
+
if (!hasElementStyleMarker(node))
|
|
10097
|
+
return null;
|
|
10098
|
+
return label ? emptyImageLabelSrc(label) : EMPTY_IMAGE_SRC;
|
|
10099
|
+
}
|
|
10100
|
+
|
|
9937
10101
|
function parseHexColor(hex) {
|
|
9938
10102
|
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec((hex ?? '').trim());
|
|
9939
10103
|
if (!m)
|
|
@@ -10273,19 +10437,39 @@ function parseFontFamilies(stack) {
|
|
|
10273
10437
|
.map(f => f.trim().replace(/^(['"])(.*)\1$/, '$2').trim())
|
|
10274
10438
|
.filter(Boolean);
|
|
10275
10439
|
}
|
|
10440
|
+
/** Where Google serves the stylesheets, and the origin the font FILES come from (preconnect). */
|
|
10441
|
+
const GOOGLE_FONTS_CSS_ORIGIN = 'https://fonts.googleapis.com';
|
|
10442
|
+
const GOOGLE_FONTS_FILE_ORIGIN = 'https://fonts.gstatic.com';
|
|
10443
|
+
/** The ONE stylesheet URL for a catalog id — both renderers link exactly this (one per family, T7). */
|
|
10444
|
+
function googleFontHref(id) {
|
|
10445
|
+
return `${GOOGLE_FONTS_CSS_ORIGIN}/css2?family=${id}&display=swap`;
|
|
10446
|
+
}
|
|
10447
|
+
const CATALOG_BY_LOWER = new Map(Object.entries(FONT_CATALOG).map(([family, id]) => [family.toLowerCase(), id]));
|
|
10448
|
+
/** The catalog id of a family name (case-insensitive), or null — a system or uploaded face has none. */
|
|
10449
|
+
function catalogFontId(family) {
|
|
10450
|
+
return CATALOG_BY_LOWER.get(String(family ?? '').trim().toLowerCase()) ?? null;
|
|
10451
|
+
}
|
|
10452
|
+
const MONO_ROLE = 'font-mono';
|
|
10453
|
+
const ALWAYS_LOADED_ROLES = FONT_FAMILY_TOKENS.filter(role => role !== MONO_ROLE);
|
|
10454
|
+
const MONO_REFERENCE = /var\(\s*--iox-font-mono\b/;
|
|
10276
10455
|
/**
|
|
10277
|
-
* The Google css2 ids to load for a token set: every catalog family named by
|
|
10278
|
-
*
|
|
10456
|
+
* The Google css2 ids to load for a token set: every catalog family named by the display / body /
|
|
10457
|
+
* label roles, plus mono when referenced (above). One id per family (T7 — one `<link>` per family, so
|
|
10458
|
+
* one bad id cannot sink the others), in role order.
|
|
10279
10459
|
*/
|
|
10280
|
-
function fontsForTokens(tokens) {
|
|
10460
|
+
function fontsForTokens(tokens, opts = {}) {
|
|
10461
|
+
const rows = (tokens ?? []).filter(t => !!t && typeof t.name === 'string');
|
|
10462
|
+
const monoRead = MONO_REFERENCE.test(opts.usedCss ?? '')
|
|
10463
|
+
|| rows.some(t => t.name !== MONO_ROLE && MONO_REFERENCE.test(`${t.value ?? ''} ${t.values?.dark ?? ''}`));
|
|
10464
|
+
const roles = monoRead ? FONT_FAMILY_TOKENS : ALWAYS_LOADED_ROLES;
|
|
10281
10465
|
const seen = new Set();
|
|
10282
10466
|
const ids = [];
|
|
10283
|
-
for (const role of
|
|
10284
|
-
const row =
|
|
10467
|
+
for (const role of roles) {
|
|
10468
|
+
const row = rows.find(t => t.name === role);
|
|
10285
10469
|
for (const family of parseFontFamilies(row?.value ?? '')) {
|
|
10286
|
-
const id =
|
|
10287
|
-
if (id && !seen.has(
|
|
10288
|
-
seen.add(
|
|
10470
|
+
const id = catalogFontId(family);
|
|
10471
|
+
if (id && !seen.has(id)) {
|
|
10472
|
+
seen.add(id);
|
|
10289
10473
|
ids.push(id);
|
|
10290
10474
|
}
|
|
10291
10475
|
}
|
|
@@ -10430,42 +10614,51 @@ function planFlavorRevert(current, snapshot) {
|
|
|
10430
10614
|
}
|
|
10431
10615
|
return plan;
|
|
10432
10616
|
}
|
|
10617
|
+
const expectationFor = (baseline, row) => !baseline ? undefined : typeof baseline === 'function' ? baseline(row) : baseline.get(row.name);
|
|
10618
|
+
const sameValue = (row, e) => row.value === e.value && (row.values?.dark ?? '') === (e.dark ?? '');
|
|
10433
10619
|
/**
|
|
10434
10620
|
* The seeding MERGE: desired tokens vs existing rows, deciding create / update / keep. The §10 C2
|
|
10435
10621
|
* rule — "an upgrade only rewrites untouched rows" — is a comparison against what the PREVIOUS
|
|
10436
|
-
* seed wrote (`
|
|
10437
|
-
* rewritten; any drift means the author took it over, and their hand wins. A row
|
|
10438
|
-
* `flavor:` stamp
|
|
10622
|
+
* seed wrote (`baseline`): a seeded row whose value still equals it is the machine's and may be
|
|
10623
|
+
* rewritten; any drift means the author took it over, and their hand wins. A row the design did not
|
|
10624
|
+
* write is never touched: no `flavor:` stamp, unless it is a `user` row (a font the author CHOSE in
|
|
10625
|
+
* the design) whose value is exactly what the previous design wrote — that row is the design's own
|
|
10626
|
+
* choice, so a new choice may replace it. `group`/`tier` ride every write; a desired row's own
|
|
10627
|
+
* `source` (a chosen font's `user`) wins over the pack stamp.
|
|
10439
10628
|
*/
|
|
10440
|
-
function planFlavorSeed(desired, existing, stamp,
|
|
10629
|
+
function planFlavorSeed(desired, existing, stamp, baseline) {
|
|
10441
10630
|
const byName = new Map((existing ?? []).map(r => [r.name, r]));
|
|
10442
10631
|
const plan = { create: [], update: [], kept: [] };
|
|
10443
10632
|
for (const def of desired ?? []) {
|
|
10444
10633
|
if (!def?.name)
|
|
10445
10634
|
continue;
|
|
10446
10635
|
const row = byName.get(def.name);
|
|
10636
|
+
const source = def.source ?? stamp;
|
|
10447
10637
|
if (!row) {
|
|
10448
10638
|
plan.create.push({
|
|
10449
10639
|
name: def.name,
|
|
10450
10640
|
type: def.type ?? DEFAULT_TOKEN_TYPE,
|
|
10451
10641
|
value: def.value,
|
|
10452
10642
|
...(def.values ? { values: def.values } : {}),
|
|
10453
|
-
source
|
|
10643
|
+
source,
|
|
10454
10644
|
...classification(def),
|
|
10455
10645
|
});
|
|
10456
10646
|
continue;
|
|
10457
10647
|
}
|
|
10458
|
-
|
|
10648
|
+
const old = expectationFor(baseline, row);
|
|
10649
|
+
const designOwned = isSeeded(row) || (row.source === USER_TOKEN_SOURCE && !!old && sameValue(row, old));
|
|
10650
|
+
if (!designOwned) {
|
|
10459
10651
|
plan.kept.push(def.name);
|
|
10460
10652
|
continue;
|
|
10461
10653
|
}
|
|
10462
10654
|
const sameAsDesired = row.value === def.value && (row.values?.dark ?? '') === (def.values?.dark ?? '');
|
|
10463
|
-
if (sameAsDesired)
|
|
10464
|
-
continue; // already exactly this
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10655
|
+
if (sameAsDesired && (row.source ?? '') === source)
|
|
10656
|
+
continue; // already exactly this
|
|
10657
|
+
if (sameAsDesired && isSeeded(row) && source !== USER_TOKEN_SOURCE)
|
|
10658
|
+
continue; // same value, another pack's stamp — nothing to write
|
|
10659
|
+
const untouched = !!old && sameValue(row, old);
|
|
10660
|
+
if ((untouched || sameAsDesired) && row._id) {
|
|
10661
|
+
plan.update.push({ id: row._id, data: { value: def.value, values: def.values ?? {}, source, ...classification(def) } });
|
|
10469
10662
|
}
|
|
10470
10663
|
else {
|
|
10471
10664
|
plan.kept.push(def.name); // edited since seeding (or unverifiable) — the author's hand wins
|
|
@@ -10522,30 +10715,254 @@ function springToLinear(stiffness, damping) {
|
|
|
10522
10715
|
}
|
|
10523
10716
|
|
|
10524
10717
|
/**
|
|
10525
|
-
*
|
|
10526
|
-
*
|
|
10527
|
-
*
|
|
10528
|
-
*
|
|
10529
|
-
*
|
|
10718
|
+
* The `settings.design` row (design-system plan §3f), read and written by ONE pair of functions so
|
|
10719
|
+
* the builder, the server's public page read (the engine's mode) and the site build agree on what a
|
|
10720
|
+
* stored row MEANS. The row is the settings collection's generic `config: [{ key, value }]`; values
|
|
10721
|
+
* are strings there (Boolean "true"/"false", Object as JSON), but a reader also accepts the typed
|
|
10722
|
+
* value a client control may have sent.
|
|
10530
10723
|
*/
|
|
10531
|
-
/** The
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
]
|
|
10724
|
+
/** The look a studio wears until it picks one (DS2's interim seed, and DS6's first-run selection). */
|
|
10725
|
+
const DEFAULT_FLAVOR_ID = 'crisp';
|
|
10726
|
+
var DesignConfigKey;
|
|
10727
|
+
(function (DesignConfigKey) {
|
|
10728
|
+
DesignConfigKey["FlavorId"] = "flavorId";
|
|
10729
|
+
DesignConfigKey["FlavorVersion"] = "flavorVersion";
|
|
10730
|
+
DesignConfigKey["BrandSeed"] = "brandSeed";
|
|
10731
|
+
DesignConfigKey["PairId"] = "pairId";
|
|
10732
|
+
DesignConfigKey["Shape"] = "shape";
|
|
10733
|
+
DesignConfigKey["Density"] = "density";
|
|
10734
|
+
DesignConfigKey["Motion"] = "motion";
|
|
10735
|
+
DesignConfigKey["PillButtons"] = "pillButtons";
|
|
10736
|
+
DesignConfigKey["Mode"] = "mode";
|
|
10737
|
+
DesignConfigKey["Fonts"] = "fonts";
|
|
10738
|
+
DesignConfigKey["Revert"] = "revert";
|
|
10739
|
+
})(DesignConfigKey || (DesignConfigKey = {}));
|
|
10740
|
+
const MODES = SPEC_AXES.modes;
|
|
10741
|
+
const FONT_ROLE_TOKENS = SPEC_FONT_ROLES.map(r => r.token);
|
|
10742
|
+
/** The pack for an id; a retired id (Swiss) resolves to the pack that `replaces` it. */
|
|
10743
|
+
function flavorPackById(id) {
|
|
10744
|
+
if (!id)
|
|
10745
|
+
return undefined;
|
|
10746
|
+
return SPEC_FLAVORS.find(f => f.id === id) ?? SPEC_FLAVORS.find(f => f.replaces === id);
|
|
10538
10747
|
}
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
return [...new Set(Array.from(css.matchAll(REFERENCE), m => m[1]))];
|
|
10748
|
+
/** True when `id` names a retired flavor that a v2 pack replaces (Swiss → Crisp). */
|
|
10749
|
+
function isRetiredFlavorId(id) {
|
|
10750
|
+
return !!id && !SPEC_FLAVORS.some(f => f.id === id) && SPEC_FLAVORS.some(f => f.replaces === id);
|
|
10543
10751
|
}
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10752
|
+
const defaultPack = () => flavorPackById(DEFAULT_FLAVOR_ID);
|
|
10753
|
+
const asString = (v) => (typeof v === 'string' && v.trim() !== '' ? v.trim() : null);
|
|
10754
|
+
function asBoolean(v) {
|
|
10755
|
+
if (v === true || v === 'true')
|
|
10756
|
+
return true;
|
|
10757
|
+
if (v === false || v === 'false')
|
|
10758
|
+
return false;
|
|
10759
|
+
return null;
|
|
10760
|
+
}
|
|
10761
|
+
function asObject(v) {
|
|
10762
|
+
if (v && typeof v === 'object')
|
|
10763
|
+
return v;
|
|
10764
|
+
if (typeof v !== 'string' || v.trim() === '')
|
|
10765
|
+
return null;
|
|
10766
|
+
try {
|
|
10767
|
+
const parsed = JSON.parse(v);
|
|
10768
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
10769
|
+
}
|
|
10770
|
+
catch {
|
|
10771
|
+
return null;
|
|
10772
|
+
}
|
|
10773
|
+
}
|
|
10774
|
+
function asFonts(v) {
|
|
10775
|
+
const raw = asObject(v);
|
|
10776
|
+
if (!raw)
|
|
10777
|
+
return undefined;
|
|
10778
|
+
const fonts = {};
|
|
10779
|
+
for (const role of FONT_ROLE_TOKENS) {
|
|
10780
|
+
const family = raw[role];
|
|
10781
|
+
if (typeof family === 'string' && family.trim() !== '')
|
|
10782
|
+
fonts[role] = family.trim();
|
|
10783
|
+
}
|
|
10784
|
+
return Object.keys(fonts).length ? fonts : undefined;
|
|
10785
|
+
}
|
|
10786
|
+
const keyOf = (table, v) => v !== null && Object.prototype.hasOwnProperty.call(table, v) ? v : null;
|
|
10787
|
+
/** A config array → the key/value map (last entry wins, as the settings form writes it). */
|
|
10788
|
+
function configMap(config) {
|
|
10789
|
+
const map = new Map();
|
|
10790
|
+
for (const kv of config ?? [])
|
|
10791
|
+
if (kv && typeof kv.key === 'string')
|
|
10792
|
+
map.set(kv.key, kv.value);
|
|
10793
|
+
return map;
|
|
10794
|
+
}
|
|
10795
|
+
/**
|
|
10796
|
+
* A partial set of choices completed against its pack: every missing or invalid value falls back to
|
|
10797
|
+
* the pack's own default (an unknown pair → the pack's first, a bad seed → the pack's seed).
|
|
10798
|
+
*/
|
|
10799
|
+
function resolveDesignChoices(partial) {
|
|
10800
|
+
const pack = flavorPackById(partial?.flavorId) ?? defaultPack();
|
|
10801
|
+
const seed = asString(partial?.seed);
|
|
10802
|
+
const pairId = asString(partial?.pairId);
|
|
10803
|
+
const fonts = asFonts(partial?.fonts);
|
|
10804
|
+
return {
|
|
10805
|
+
flavorId: pack.id,
|
|
10806
|
+
seed: seed && parseHexColor(seed) ? seed : pack.seed,
|
|
10807
|
+
pairId: pairId && pack.pairs.some(p => p.id === pairId) ? pairId : pack.pairs[0]?.id ?? '',
|
|
10808
|
+
shape: keyOf(SPEC_AXES.shapes, asString(partial?.shape)) ?? pack.defaults.shape,
|
|
10809
|
+
density: keyOf(SPEC_AXES.densities, asString(partial?.density)) ?? pack.defaults.density,
|
|
10810
|
+
motion: keyOf(SPEC_AXES.motions, asString(partial?.motion)) ?? pack.defaults.motion,
|
|
10811
|
+
pillButtons: asBoolean(partial?.pillButtons) ?? pack.defaults.pillButtons,
|
|
10812
|
+
mode: (MODES.includes(asString(partial?.mode) ?? '') ? asString(partial?.mode) : pack.defaults.mode),
|
|
10813
|
+
...(fonts ? { fonts } : {}),
|
|
10814
|
+
};
|
|
10815
|
+
}
|
|
10816
|
+
/** The pack's own defaults for `flavorId` — what picking a card means before anything is tuned. */
|
|
10817
|
+
function packDesignChoices(flavorId) {
|
|
10818
|
+
return resolveDesignChoices({ flavorId });
|
|
10819
|
+
}
|
|
10820
|
+
/** A stored `settings.design` config → the resolved design. No row (or no config) → Crisp's defaults. */
|
|
10821
|
+
function readDesignConfig(config, hasRow = !!config) {
|
|
10822
|
+
const map = configMap(config);
|
|
10823
|
+
const storedFlavorId = asString(map.get(DesignConfigKey.FlavorId));
|
|
10824
|
+
const storedSeed = asString(map.get(DesignConfigKey.BrandSeed));
|
|
10825
|
+
const choices = resolveDesignChoices({
|
|
10826
|
+
flavorId: storedFlavorId ?? undefined,
|
|
10827
|
+
seed: storedSeed ?? undefined,
|
|
10828
|
+
pairId: asString(map.get(DesignConfigKey.PairId)) ?? undefined,
|
|
10829
|
+
shape: map.get(DesignConfigKey.Shape),
|
|
10830
|
+
density: map.get(DesignConfigKey.Density),
|
|
10831
|
+
motion: map.get(DesignConfigKey.Motion),
|
|
10832
|
+
pillButtons: asBoolean(map.get(DesignConfigKey.PillButtons)) ?? undefined,
|
|
10833
|
+
mode: map.get(DesignConfigKey.Mode),
|
|
10834
|
+
fonts: map.get(DesignConfigKey.Fonts),
|
|
10835
|
+
});
|
|
10836
|
+
return {
|
|
10837
|
+
choices,
|
|
10838
|
+
storedFlavorId,
|
|
10839
|
+
storedSeed,
|
|
10840
|
+
flavorVersion: asString(map.get(DesignConfigKey.FlavorVersion)),
|
|
10841
|
+
revert: asObject(map.get(DesignConfigKey.Revert)),
|
|
10842
|
+
hasRow,
|
|
10843
|
+
};
|
|
10844
|
+
}
|
|
10845
|
+
/** The colour mode a stored config compiles in — what the public page read hands the engine. */
|
|
10846
|
+
function designModeOf(config) {
|
|
10847
|
+
return readDesignConfig(config).choices.mode;
|
|
10848
|
+
}
|
|
10849
|
+
/** Resolved choices (+ the revert snapshot) → the config array a settings row stores. */
|
|
10850
|
+
function designConfigEntries(choices, flavorVersion, revert) {
|
|
10851
|
+
const c = resolveDesignChoices(choices);
|
|
10852
|
+
return [
|
|
10853
|
+
{ key: DesignConfigKey.FlavorId, value: c.flavorId },
|
|
10854
|
+
{ key: DesignConfigKey.FlavorVersion, value: String(flavorVersion) },
|
|
10855
|
+
{ key: DesignConfigKey.BrandSeed, value: c.seed },
|
|
10856
|
+
{ key: DesignConfigKey.PairId, value: c.pairId },
|
|
10857
|
+
{ key: DesignConfigKey.Shape, value: c.shape },
|
|
10858
|
+
{ key: DesignConfigKey.Density, value: c.density },
|
|
10859
|
+
{ key: DesignConfigKey.Motion, value: c.motion },
|
|
10860
|
+
{ key: DesignConfigKey.PillButtons, value: String(c.pillButtons) },
|
|
10861
|
+
{ key: DesignConfigKey.Mode, value: c.mode },
|
|
10862
|
+
...(c.fonts ? [{ key: DesignConfigKey.Fonts, value: JSON.stringify(c.fonts) }] : []),
|
|
10863
|
+
...(revert ? [{ key: DesignConfigKey.Revert, value: JSON.stringify(revert) }] : []),
|
|
10864
|
+
];
|
|
10865
|
+
}
|
|
10866
|
+
/** Two choice sets describe the same look (fonts compared by role). */
|
|
10867
|
+
function sameDesignChoices(a, b) {
|
|
10868
|
+
const x = resolveDesignChoices(a);
|
|
10869
|
+
const y = resolveDesignChoices(b);
|
|
10870
|
+
return x.flavorId === y.flavorId && x.seed.toLowerCase() === y.seed.toLowerCase() && x.pairId === y.pairId
|
|
10871
|
+
&& x.shape === y.shape && x.density === y.density && x.motion === y.motion
|
|
10872
|
+
&& x.pillButtons === y.pillButtons && x.mode === y.mode
|
|
10873
|
+
&& JSON.stringify(x.fonts ?? {}) === JSON.stringify(y.fonts ?? {});
|
|
10874
|
+
}
|
|
10875
|
+
|
|
10876
|
+
/**
|
|
10877
|
+
* Applying a look (design-system DS4): the COMPLETE `buildFlavorTokens` set for the chosen
|
|
10878
|
+
* `DesignChoices`, merged into the org's rows by the §10 C2 rule — rewrite only what the previous
|
|
10879
|
+
* seed wrote and nobody touched since, keep the author's rows, report what was kept. And the one
|
|
10880
|
+
* migration the contract names: an org still on Swiss becomes Crisp.
|
|
10881
|
+
* Pure plans; each caller (builder, the server-side site build) applies them with its own transport.
|
|
10882
|
+
*/
|
|
10883
|
+
const SEED_STAMP = /^flavor:([^@]+)@/;
|
|
10884
|
+
const expectations = (rows) => new Map(rows.map(r => [r.name, { value: r.value, ...(r.values?.dark ? { dark: r.values.dark } : {}) }]));
|
|
10885
|
+
/** Every row a design writes — `buildFlavorTokens` for its pack. Empty for an unknown pack. */
|
|
10886
|
+
function designTokenRows(choices) {
|
|
10887
|
+
const pack = flavorPackById(choices.flavorId);
|
|
10888
|
+
return pack ? buildFlavorTokens(pack, { ...choices, flavorId: pack.id }) : [];
|
|
10889
|
+
}
|
|
10890
|
+
/** What the F1/F2 Swiss seeding wrote: the four Swiss rows + the brand set derived from its seed. */
|
|
10891
|
+
function swissExpectations(seed) {
|
|
10892
|
+
const rows = SWISS_FLAVOR.tokens.map(t => ({ name: t.name, value: t.value, values: t.values }));
|
|
10893
|
+
if (seed) {
|
|
10894
|
+
try {
|
|
10895
|
+
rows.push(...deriveBrandTokens(seed, SWISS_FLAVOR).map(t => ({ name: t.name, value: t.value, values: t.values })));
|
|
10896
|
+
}
|
|
10897
|
+
catch { /* an unparseable stored seed proves nothing about the rows */ }
|
|
10898
|
+
}
|
|
10899
|
+
return expectations(rows);
|
|
10900
|
+
}
|
|
10901
|
+
/**
|
|
10902
|
+
* What the PREVIOUS design wrote, as a baseline keyed by the row's own stamp: a row Swiss stamped is
|
|
10903
|
+
* judged against what Swiss wrote, every other row against `buildFlavorTokens` of the stored choices
|
|
10904
|
+
* (for an org with no design row, Crisp's defaults — exactly what DS2's interim seed wrote). In a
|
|
10905
|
+
* Swiss org the only v2 rows are that interim seed's, so they are judged against Crisp's DEFAULTS,
|
|
10906
|
+
* never against the brand-seeded Crisp the migration is about to write.
|
|
10907
|
+
*/
|
|
10908
|
+
function designBaseline(previous) {
|
|
10909
|
+
const retired = isRetiredFlavorId(previous.storedFlavorId);
|
|
10910
|
+
const current = expectations(designTokenRows(retired ? packDesignChoices(previous.choices.flavorId) : previous.choices));
|
|
10911
|
+
const swiss = swissExpectations(retired ? previous.storedSeed : null);
|
|
10912
|
+
return (row) => {
|
|
10913
|
+
const stampedBy = SEED_STAMP.exec(row.source ?? '')?.[1];
|
|
10914
|
+
return stampedBy === SWISS_FLAVOR.id ? swiss.get(row.name) : current.get(row.name);
|
|
10915
|
+
};
|
|
10916
|
+
}
|
|
10917
|
+
/** Apply `next` over the org's rows, judged against `previous` — the plan FlavorApplyService runs. */
|
|
10918
|
+
function planDesignApply(existing, previous, next) {
|
|
10919
|
+
const pack = flavorPackById(next.flavorId);
|
|
10920
|
+
if (!pack)
|
|
10921
|
+
return { create: [], update: [], kept: [] };
|
|
10922
|
+
return planFlavorSeed(designTokenRows(next), existing, flavorSourceStamp(pack.id), designBaseline(previous));
|
|
10923
|
+
}
|
|
10924
|
+
/** The rows a design is missing by name — the builder-load top-up (never touches an existing row). */
|
|
10925
|
+
function missingDesignRows(existing, choices) {
|
|
10926
|
+
const have = new Set((existing ?? []).map(r => r.name));
|
|
10927
|
+
return designTokenRows(choices).filter(r => !have.has(r.name));
|
|
10928
|
+
}
|
|
10929
|
+
const LEGACY_ALIASES = SPEC_LEGACY_ALIASES;
|
|
10930
|
+
/**
|
|
10931
|
+
* Swiss → Crisp (spec `legacyAliases`; D1). Needed when the design row says a retired flavor, or when
|
|
10932
|
+
* there is no design row but the org holds Swiss-stamped alias rows (F1 seeded those before the row
|
|
10933
|
+
* existed). Idempotent: once the row says Crisp nothing is needed, and re-running the plan writes
|
|
10934
|
+
* nothing.
|
|
10935
|
+
*
|
|
10936
|
+
* - The look: `planDesignApply` from Swiss to Crisp — Swiss-stamped rows untouched since Swiss seeded
|
|
10937
|
+
* them (incl. F2's `surface` / `line` / `accent`) become Crisp's; edited ones are kept.
|
|
10938
|
+
* - The aliases: `radius-control` / `radius-surface` / `space-control` / `space-stack` are rewritten to
|
|
10939
|
+
* their alias reference ONLY while still machine-owned and untouched, so a node saved since F1 that
|
|
10940
|
+
* reads `var(--iox-radius-control)` keeps resolving — now to Crisp's button radius.
|
|
10941
|
+
*/
|
|
10942
|
+
function planSwissMigration(existing, stored) {
|
|
10943
|
+
const swissAliasRows = (existing ?? []).filter(r => r.name in LEGACY_ALIASES && SEED_STAMP.exec(r.source ?? '')?.[1] === SWISS_FLAVOR.id);
|
|
10944
|
+
const needed = isRetiredFlavorId(stored.storedFlavorId) || (!stored.hasRow && swissAliasRows.length > 0);
|
|
10945
|
+
if (!needed)
|
|
10946
|
+
return null;
|
|
10947
|
+
const choices = stored.choices;
|
|
10948
|
+
const previous = isRetiredFlavorId(stored.storedFlavorId) ? stored : { ...stored, storedFlavorId: SWISS_FLAVOR.id };
|
|
10949
|
+
const plan = planDesignApply(existing, previous, choices);
|
|
10950
|
+
const stamp = flavorSourceStamp(choices.flavorId);
|
|
10951
|
+
const swiss = swissExpectations(null);
|
|
10952
|
+
for (const row of (existing ?? []).filter(r => r.name in LEGACY_ALIASES)) {
|
|
10953
|
+
const alias = LEGACY_ALIASES[row.name];
|
|
10954
|
+
if (row.value === alias)
|
|
10955
|
+
continue;
|
|
10956
|
+
const was = swiss.get(row.name);
|
|
10957
|
+
const untouched = SEED_STAMP.test(row.source ?? '') && !!was && row.value === was.value && !row.values?.dark;
|
|
10958
|
+
if (untouched && row._id) {
|
|
10959
|
+
plan.update.push({ id: row._id, data: { value: alias, values: {}, source: stamp } });
|
|
10960
|
+
}
|
|
10961
|
+
else {
|
|
10962
|
+
plan.kept.push(row.name);
|
|
10963
|
+
}
|
|
10964
|
+
}
|
|
10965
|
+
return { choices, plan };
|
|
10549
10966
|
}
|
|
10550
10967
|
|
|
10551
10968
|
/**
|
|
@@ -10563,5 +10980,5 @@ function unknownTokenReferences(css, alsoAllowed = []) {
|
|
|
10563
10980
|
* Generated bundle index. Do not edit.
|
|
10564
10981
|
*/
|
|
10565
10982
|
|
|
10566
|
-
export { ACCENT_TOKEN_NAMES, BUTTON_VARIANT_TO_STYLE, DESIGN_TOKEN_GROUPS, DESIGN_TOKEN_TIERS, DESIGN_TOKEN_TYPES, ELEMENT_STYLES, ELEMENT_STYLE_MARKER_CLASS, ELEMENT_STYLE_STATES, ElementStyleState, FLAVOR_SPEC_VERSION, FONT_CATALOG, FONT_FAMILY_TOKENS, OwnedTraitLink, PALETTE_PRESETS, REDUCED_MOTION_OVERRIDES, REDUCED_TRANSPARENCY_OVERRIDES, REVERT_WINDOW_MS, SPEC_A11Y, SPEC_AXES, SPEC_BUTTON_VARIANT_TO_STYLE, SPEC_CONTRAST, SPEC_DEFAULT_ELEMENT_STYLE, SPEC_ELEMENT_STYLES, SPEC_FIELD_CLASS_BASELINE, SPEC_FLAVORS, SPEC_FLAVOR_PACKS, SPEC_FONTS, SPEC_FONT_ROLES, SPEC_LEGACY_ALIASES, SPEC_PALETTE_PRESETS, SPEC_SCHEMES, SPEC_SPRINGS, SPEC_TIERS, SPEC_TIER_IDS, SPEC_TOKEN_TYPES, SWISS_FLAVOR, USER_TOKEN_SOURCE, WCAG_AA_TEXT, apcaLc, applyElementStyle, applyHeadingLevelRule, buildFlavorTokens, buildFontFaceCss, compileSchemeCss, compileTokenCss, compileTokenCssBlocks, cssString, defaultDesignChoices, defaultElementStyleFor, deriveAccentTokens, deriveBrandTokens, elementStyleById, elementStyleClass,
|
|
10983
|
+
export { ACCENT_TOKEN_NAMES, BUTTON_VARIANT_TO_STYLE, COLOR_SCHEMES, COLOR_SCHEME_TRAIT, COLOR_SCHEME_TYPES, ColorScheme, DEFAULT_FLAVOR_ID, DESIGN_TOKEN_GROUPS, DESIGN_TOKEN_TIERS, DESIGN_TOKEN_TYPES, DesignConfigKey, ELEMENT_STYLES, ELEMENT_STYLE_MARKER_CLASS, ELEMENT_STYLE_STATES, EMPTY_IMAGE_ATTRIBUTE, EMPTY_IMAGE_SRC, ElementStyleState, FLAVOR_SPEC_VERSION, FONT_CATALOG, FONT_FAMILY_TOKENS, GOOGLE_FONTS_CSS_ORIGIN, GOOGLE_FONTS_FILE_ORIGIN, OwnedTraitLink, PALETTE_PRESETS, REDUCED_MOTION_OVERRIDES, REDUCED_TRANSPARENCY_OVERRIDES, REVERT_WINDOW_MS, SPEC_A11Y, SPEC_AXES, SPEC_BUTTON_VARIANT_TO_STYLE, SPEC_CONTRAST, SPEC_DEFAULT_ELEMENT_STYLE, SPEC_ELEMENT_STYLES, SPEC_FIELD_CLASS_BASELINE, SPEC_FLAVORS, SPEC_FLAVOR_PACKS, SPEC_FONTS, SPEC_FONT_ROLES, SPEC_LEGACY_ALIASES, SPEC_PALETTE_PRESETS, SPEC_SCHEMES, SPEC_SPRINGS, SPEC_TIERS, SPEC_TIER_IDS, SPEC_TOKEN_TYPES, SWISS_FLAVOR, USER_TOKEN_SOURCE, WCAG_AA_TEXT, apcaLc, applyElementStyle, applyHeadingLevelRule, buildFlavorTokens, buildFontFaceCss, catalogFontId, colorSchemeClass, colorSchemeOf, compileSchemeCss, compileTokenCss, compileTokenCssBlocks, cssString, defaultDesignChoices, defaultElementStyleFor, deriveAccentTokens, deriveBrandTokens, designBaseline, designConfigEntries, designModeOf, designTokenRows, elementStyleById, elementStyleClass, elementStylesFor, emptyImageLabelSrc, emptyImageSrc, flavorPackById, flavorSourceStamp, fontsForTokens, googleFontHref, hasElementStyleMarker, isDesignTokenGroup, isDesignTokenTier, isDesignTokenType, isRetiredFlavorId, legacyRuleTokenReferences, missingDesignRows, nodeDesignClasses, oklabToRgb, ownedTraitStatus, packDesignChoices, parseFontFamilies, parseHexColor, planDesignApply, planFlavorRevert, planFlavorSeed, planSwissMigration, readDesignConfig, relativeLuminance, resolveDesignChoices, resolveTokenReferences, resolveTokenValues, revertAvailable, rgbToOklab, sameDesignChoices, specTokenNames, springToLinear, styleKeyToKebab, switchElementStyle, syncNodeDesignClasses, toHexColor, tokenReferences, tokensToDarkVarMap, tokensToVarMap, unknownTokenReferences, userFontStack, validateFlavorPack, wcagContrastRatio };
|
|
10567
10984
|
//# sourceMappingURL=vectoriox-iox-ui-design-core.mjs.map
|