@vectoriox/iox-ui 5.14.0 → 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.
@@ -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$1 = 'var(';
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$1))
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$1) || d.includes(REFERENCE$1));
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 hue
9680
- * opinion F2's brand step reads. Kept until DS4 migrates `flavorId: swiss` orgs to Crisp; its four
9681
- * rows are the spec's `legacyAliases`.
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',
@@ -10367,19 +10437,39 @@ function parseFontFamilies(stack) {
10367
10437
  .map(f => f.trim().replace(/^(['"])(.*)\1$/, '$2').trim())
10368
10438
  .filter(Boolean);
10369
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/;
10370
10455
  /**
10371
- * The Google css2 ids to load for a token set: every catalog family named by a font role token,
10372
- * one id per family (T7 — one `<link>` per family, so one bad id cannot sink the others).
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.
10373
10459
  */
10374
- 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;
10375
10465
  const seen = new Set();
10376
10466
  const ids = [];
10377
- for (const role of FONT_FAMILY_TOKENS) {
10378
- const row = (tokens ?? []).find(t => t?.name === role);
10467
+ for (const role of roles) {
10468
+ const row = rows.find(t => t.name === role);
10379
10469
  for (const family of parseFontFamilies(row?.value ?? '')) {
10380
- const id = FONT_CATALOG[family];
10381
- if (id && !seen.has(family)) {
10382
- seen.add(family);
10470
+ const id = catalogFontId(family);
10471
+ if (id && !seen.has(id)) {
10472
+ seen.add(id);
10383
10473
  ids.push(id);
10384
10474
  }
10385
10475
  }
@@ -10524,42 +10614,51 @@ function planFlavorRevert(current, snapshot) {
10524
10614
  }
10525
10615
  return plan;
10526
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 ?? '');
10527
10619
  /**
10528
10620
  * The seeding MERGE: desired tokens vs existing rows, deciding create / update / keep. The §10 C2
10529
10621
  * rule — "an upgrade only rewrites untouched rows" — is a comparison against what the PREVIOUS
10530
- * seed wrote (`oldExpected`): a seeded row whose value still equals it is the machine's and may be
10531
- * rewritten; any drift means the author took it over, and their hand wins. A row without a
10532
- * `flavor:` stamp is never touched at all. `group`/`tier` ride every write.
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.
10533
10628
  */
10534
- function planFlavorSeed(desired, existing, stamp, oldExpected) {
10629
+ function planFlavorSeed(desired, existing, stamp, baseline) {
10535
10630
  const byName = new Map((existing ?? []).map(r => [r.name, r]));
10536
10631
  const plan = { create: [], update: [], kept: [] };
10537
10632
  for (const def of desired ?? []) {
10538
10633
  if (!def?.name)
10539
10634
  continue;
10540
10635
  const row = byName.get(def.name);
10636
+ const source = def.source ?? stamp;
10541
10637
  if (!row) {
10542
10638
  plan.create.push({
10543
10639
  name: def.name,
10544
10640
  type: def.type ?? DEFAULT_TOKEN_TYPE,
10545
10641
  value: def.value,
10546
10642
  ...(def.values ? { values: def.values } : {}),
10547
- source: stamp,
10643
+ source,
10548
10644
  ...classification(def),
10549
10645
  });
10550
10646
  continue;
10551
10647
  }
10552
- if (!isSeeded(row)) {
10648
+ const old = expectationFor(baseline, row);
10649
+ const designOwned = isSeeded(row) || (row.source === USER_TOKEN_SOURCE && !!old && sameValue(row, old));
10650
+ if (!designOwned) {
10553
10651
  plan.kept.push(def.name);
10554
10652
  continue;
10555
10653
  }
10556
10654
  const sameAsDesired = row.value === def.value && (row.values?.dark ?? '') === (def.values?.dark ?? '');
10557
- if (sameAsDesired)
10558
- continue; // already exactly this — nothing to write, nothing kept-against
10559
- const old = oldExpected?.get(def.name);
10560
- const untouched = !!old && row.value === old.value && (row.values?.dark ?? '') === (old.dark ?? '');
10561
- if (untouched && row._id) {
10562
- plan.update.push({ id: row._id, data: { value: def.value, values: def.values ?? {}, source: stamp, ...classification(def) } });
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) } });
10563
10662
  }
10564
10663
  else {
10565
10664
  plan.kept.push(def.name); // edited since seeding (or unverifiable) — the author's hand wins
@@ -10616,30 +10715,254 @@ function springToLinear(stiffness, damping) {
10616
10715
  }
10617
10716
 
10618
10717
  /**
10619
- * Which `--iox-<name>` a piece of COMPONENT CSS may reference (design-system §3b-bis: "component CSS
10620
- * may reference ONLY tokens defined in this spec"). DS2 shipped seven names nobody defines
10621
- * (`card-media-gap`, `gallery-overlay`, `dialog-close-bg`…): a token the contract does not define can
10622
- * never be set by a flavor, so it is a literal wearing a costume. One guard, run over BOTH
10623
- * renderers' component CSS (iox-ui's spec; the engine's `verify:token-names`).
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.
10624
10723
  */
10625
- /** The 161 names of the contract's two tiers, plus its legacy aliases. */
10626
- function specTokenNames() {
10627
- return new Set([
10628
- ...SPEC_TIERS.semantic.map(t => t.name),
10629
- ...SPEC_TIERS.component.map(t => t.name),
10630
- ...Object.keys(SPEC_LEGACY_ALIASES),
10631
- ]);
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);
10632
10747
  }
10633
- const REFERENCE = /var\(\s*--iox-([a-z0-9-]+)/gi;
10634
- /** Every distinct token a stylesheet READS (`var(--iox-x…)`), in first-seen order. */
10635
- function tokenReferences(css) {
10636
- 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);
10637
10751
  }
10638
- /** The referenced names that are neither spec tokens nor in `alsoAllowed`. */
10639
- function unknownTokenReferences(css, alsoAllowed = []) {
10640
- const spec = specTokenNames();
10641
- const extra = new Set(alsoAllowed);
10642
- return tokenReferences(css).filter(name => !spec.has(name) && !extra.has(name));
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 };
10643
10966
  }
10644
10967
 
10645
10968
  /**
@@ -10657,5 +10980,5 @@ function unknownTokenReferences(css, alsoAllowed = []) {
10657
10980
  * Generated bundle index. Do not edit.
10658
10981
  */
10659
10982
 
10660
- export { ACCENT_TOKEN_NAMES, BUTTON_VARIANT_TO_STYLE, COLOR_SCHEMES, COLOR_SCHEME_TRAIT, COLOR_SCHEME_TYPES, ColorScheme, DESIGN_TOKEN_GROUPS, DESIGN_TOKEN_TIERS, DESIGN_TOKEN_TYPES, ELEMENT_STYLES, ELEMENT_STYLE_MARKER_CLASS, ELEMENT_STYLE_STATES, EMPTY_IMAGE_ATTRIBUTE, EMPTY_IMAGE_SRC, 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, colorSchemeClass, colorSchemeOf, compileSchemeCss, compileTokenCss, compileTokenCssBlocks, cssString, defaultDesignChoices, defaultElementStyleFor, deriveAccentTokens, deriveBrandTokens, elementStyleById, elementStyleClass, elementStylesFor, emptyImageLabelSrc, emptyImageSrc, flavorSourceStamp, fontsForTokens, hasElementStyleMarker, isDesignTokenGroup, isDesignTokenTier, isDesignTokenType, nodeDesignClasses, oklabToRgb, ownedTraitStatus, parseFontFamilies, parseHexColor, planFlavorRevert, planFlavorSeed, relativeLuminance, resolveTokenReferences, resolveTokenValues, revertAvailable, rgbToOklab, specTokenNames, springToLinear, styleKeyToKebab, switchElementStyle, syncNodeDesignClasses, toHexColor, tokenReferences, tokensToDarkVarMap, tokensToVarMap, unknownTokenReferences, userFontStack, validateFlavorPack, wcagContrastRatio };
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 };
10661
10984
  //# sourceMappingURL=vectoriox-iox-ui-design-core.mjs.map