@vectoriox/iox-ui 5.18.0 → 5.20.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.
@@ -1481,9 +1481,14 @@ const SPEC_ELEMENT_STYLES = [
1481
1481
  "appliesTo": [
1482
1482
  "Container",
1483
1483
  "LinkedContainer",
1484
- "Section"
1484
+ "Section",
1485
+ "List",
1486
+ "ListItem",
1487
+ "Repeater"
1485
1488
  ],
1486
- "base": {}
1489
+ "base": {
1490
+ "color": "inherit"
1491
+ }
1487
1492
  },
1488
1493
  {
1489
1494
  "id": "card",
@@ -1495,6 +1500,7 @@ const SPEC_ELEMENT_STYLES = [
1495
1500
  "Card"
1496
1501
  ],
1497
1502
  "base": {
1503
+ "color": "inherit",
1498
1504
  "backgroundColor": "var(--iox-card-bg)",
1499
1505
  "borderWidth": "var(--iox-card-border-width)",
1500
1506
  "borderStyle": "solid",
@@ -1522,6 +1528,7 @@ const SPEC_ELEMENT_STYLES = [
1522
1528
  "LinkedContainer"
1523
1529
  ],
1524
1530
  "base": {
1531
+ "color": "inherit",
1525
1532
  "backgroundColor": "var(--iox-panel-bg)",
1526
1533
  "borderRadius": "var(--iox-panel-radius)",
1527
1534
  "padding": "var(--iox-panel-pad)"
@@ -1535,6 +1542,7 @@ const SPEC_ELEMENT_STYLES = [
1535
1542
  "Container"
1536
1543
  ],
1537
1544
  "base": {
1545
+ "color": "inherit",
1538
1546
  "backgroundColor": "var(--iox-nav-bg)",
1539
1547
  "borderWidth": "var(--iox-nav-border-width)",
1540
1548
  "borderStyle": "solid",
@@ -1658,6 +1666,9 @@ const SPEC_DEFAULT_ELEMENT_STYLE = {
1658
1666
  "Section": "section",
1659
1667
  "Container": "box",
1660
1668
  "LinkedContainer": "box",
1669
+ "List": "box",
1670
+ "ListItem": "box",
1671
+ "Repeater": "box",
1661
1672
  "Card": "card",
1662
1673
  "ButtonBlock": "button-primary",
1663
1674
  "SubmitButton": "button-primary",
@@ -10441,16 +10452,38 @@ function deriveAccentTokens(seed, against, pack = {}) {
10441
10452
  */
10442
10453
  /** Family name → Google Fonts css2 `family=` id. Replaces the engine's private GOOGLE_FONTS map. */
10443
10454
  const FONT_CATALOG = SPEC_FONTS;
10455
+ /**
10456
+ * Why iox-cms refused a font upload. The server answers `{ code }` beside its message; the admin
10457
+ * translates the code (`filebox.fontUpload.<code>`), so the author reads why in their language.
10458
+ */
10459
+ var FontUploadError;
10460
+ (function (FontUploadError) {
10461
+ FontUploadError["NotAFont"] = "not_a_font";
10462
+ FontUploadError["Empty"] = "empty";
10463
+ FontUploadError["Unreadable"] = "unreadable";
10464
+ FontUploadError["TooLarge"] = "too_large";
10465
+ })(FontUploadError || (FontUploadError = {}));
10466
+ /**
10467
+ * The largest font FILE a studio may upload: 10 MB. A full static or variable Latin/Hebrew/Arabic
10468
+ * family is well under it (Inter's variable TTF is 0.9 MB); an unsubsetted CJK family is not, and
10469
+ * must be subset first. iox-cms stops reading the request at this size; the admin checks it before
10470
+ * sending.
10471
+ */
10472
+ const MAX_FONT_FILE_BYTES = 10 * 1024 * 1024;
10444
10473
  /** The token roles whose values name the families a page renders in. */
10445
10474
  const FONT_FAMILY_TOKENS = ['font-display', 'font-body', 'font-label', 'font-mono'];
10446
10475
  const DEFAULT_WEIGHT = 400;
10447
10476
  const MIN_WEIGHT = 100;
10448
10477
  const MAX_WEIGHT = 900;
10478
+ /** The bounds CSS gives `font-weight`; a variable font's axis may reach them (Roboto Flex: 100–1000). */
10479
+ const CSS_MIN_WEIGHT = 1;
10480
+ const CSS_MAX_WEIGHT = 1000;
10449
10481
  const FONT_FORMATS = ['woff2', 'woff', 'truetype', 'opentype'];
10450
10482
  /**
10451
10483
  * A CSS string literal, single-quoted: `\` and `'` escaped, line breaks and NUL dropped, and `<` /
10452
10484
  * `>` hex-escaped (`\3c `) — the face CSS is inlined into a `<style>` by the SSR engine, where an
10453
- * uploaded family named `</style><script>` would otherwise close the element.
10485
+ * uploaded family named `</style><script>` would otherwise close the element. `parseFontFamilies` is
10486
+ * its exact inverse (escapes included), which is what keeps a family's name one value everywhere.
10454
10487
  */
10455
10488
  function cssString(value) {
10456
10489
  const body = String(value)
@@ -10461,6 +10494,175 @@ function cssString(value) {
10461
10494
  .replace(/>/g, '\\3e ');
10462
10495
  return `'${body}'`;
10463
10496
  }
10497
+ /** ONE family as a `font-family` entry — always quoted, so a family named `Acme, Serif` or `serif` is
10498
+ * that family, never a list or a generic. */
10499
+ function formatFontFamily(family) {
10500
+ return cssString(String(family ?? '').trim());
10501
+ }
10502
+ /** Families → a `font-family` stack, each quoted (`formatFontFamily`); `parseFontFamilies` reverses it. */
10503
+ function formatFontFamilies(families) {
10504
+ return (families ?? []).filter(f => String(f ?? '').trim() !== '').map(formatFontFamily).join(', ');
10505
+ }
10506
+ const HEX = /[0-9a-f]/i;
10507
+ const CSS_WS = /[ \t\n\r\f]/;
10508
+ const REPLACEMENT = '\uFFFD';
10509
+ /** A CSS escape at `s[at] === '\\'` → [the character it stands for, the index after it] (CSS Syntax §4.3.7). */
10510
+ function readEscape(s, at, inString) {
10511
+ let i = at + 1;
10512
+ if (i >= s.length)
10513
+ return ['', i];
10514
+ if (inString && /[\n\r\f]/.test(s[i]))
10515
+ return ['', s[i] === '\r' && s[i + 1] === '\n' ? i + 2 : i + 1];
10516
+ if (HEX.test(s[i])) {
10517
+ let hex = '';
10518
+ while (i < s.length && hex.length < 6 && HEX.test(s[i]))
10519
+ hex += s[i++];
10520
+ if (s[i] === '\r' && s[i + 1] === '\n')
10521
+ i += 2;
10522
+ else if (i < s.length && CSS_WS.test(s[i]))
10523
+ i++;
10524
+ const cp = parseInt(hex, 16);
10525
+ const valid = cp > 0 && cp <= 0x10ffff && !(cp >= 0xd800 && cp <= 0xdfff);
10526
+ return [valid ? String.fromCodePoint(cp) : REPLACEMENT, i];
10527
+ }
10528
+ const cp = s.codePointAt(i);
10529
+ const ch = String.fromCodePoint(cp);
10530
+ return [ch, i + ch.length];
10531
+ }
10532
+ /** A stack's entries, each as the family it names and the exact text that named it. */
10533
+ function splitFontStack(stack) {
10534
+ const s = String(stack ?? '');
10535
+ const out = [];
10536
+ let i = 0;
10537
+ while (i < s.length) {
10538
+ while (i < s.length && CSS_WS.test(s[i]))
10539
+ i++;
10540
+ if (i >= s.length)
10541
+ break;
10542
+ const start = i;
10543
+ let family = '';
10544
+ const quote = s[i];
10545
+ if (quote === '"' || quote === "'") {
10546
+ i++;
10547
+ while (i < s.length && s[i] !== quote) {
10548
+ if (s[i] === '\\') {
10549
+ const [ch, next] = readEscape(s, i, true);
10550
+ family += ch;
10551
+ i = next;
10552
+ }
10553
+ else
10554
+ family += s[i++];
10555
+ }
10556
+ i++;
10557
+ while (i < s.length && s[i] !== ',')
10558
+ i++;
10559
+ }
10560
+ else {
10561
+ const words = [];
10562
+ let word = '';
10563
+ while (i < s.length && s[i] !== ',') {
10564
+ if (s[i] === '\\') {
10565
+ const [ch, next] = readEscape(s, i, false);
10566
+ word += ch;
10567
+ i = next;
10568
+ }
10569
+ else if (CSS_WS.test(s[i])) {
10570
+ if (word)
10571
+ words.push(word);
10572
+ word = '';
10573
+ i++;
10574
+ }
10575
+ else
10576
+ word += s[i++];
10577
+ }
10578
+ if (word)
10579
+ words.push(word);
10580
+ family = words.join(' ');
10581
+ }
10582
+ const raw = s.slice(start, Math.min(i, s.length)).trim();
10583
+ if (family.trim() !== '')
10584
+ out.push({ family, raw });
10585
+ if (s[i] === ',')
10586
+ i++;
10587
+ }
10588
+ return out;
10589
+ }
10590
+ /** The entries of `stack` that do NOT name `family`, as written — what a fallback list keeps. */
10591
+ function fontStackWithout(stack, family) {
10592
+ const key = fontFamilyKey(family);
10593
+ return splitFontStack(stack).filter(e => fontFamilyKey(e.family) !== key).map(e => e.raw).join(', ');
10594
+ }
10595
+ const WEIGHT_NUMBER = /^\d+(?:\.\d+)?$/;
10596
+ const WEIGHT_RANGE = /^(\d+(?:\.\d+)?)(?:\s*[-\u2013\u2014]\s*|\s+)(\d+(?:\.\d+)?)$/;
10597
+ const clampWeight = (n, lo, hi) => Math.min(hi, Math.max(lo, Math.round(n)));
10598
+ /**
10599
+ * Anything a file, a descriptor or an author may hold as a weight → its range, or null when it is
10600
+ * not one: `700`, `'700'`, `'100 900'`, `'100-900'`, `'100–900'` (the tile's own spelling). A static
10601
+ * weight clamps to 100–900 (as before); range ends clamp to CSS's 1–1000.
10602
+ */
10603
+ function parseFontWeight(value) {
10604
+ const text = typeof value === 'number' ? String(value) : typeof value === 'string' ? value.trim() : '';
10605
+ if (WEIGHT_NUMBER.test(text)) {
10606
+ const n = Number(text);
10607
+ if (!(n > 0))
10608
+ return null;
10609
+ const w = clampWeight(n, MIN_WEIGHT, MAX_WEIGHT);
10610
+ return { min: w, max: w };
10611
+ }
10612
+ const m = WEIGHT_RANGE.exec(text);
10613
+ if (!m)
10614
+ return null;
10615
+ const a = clampWeight(Number(m[1]), CSS_MIN_WEIGHT, CSS_MAX_WEIGHT);
10616
+ const b = clampWeight(Number(m[2]), CSS_MIN_WEIGHT, CSS_MAX_WEIGHT);
10617
+ if (!(a > 0) || !(b > 0))
10618
+ return null;
10619
+ return a === b ? { min: a, max: a } : { min: Math.min(a, b), max: Math.max(a, b) };
10620
+ }
10621
+ /** A range in the stored / descriptor / CSS form: a number for one weight, `'<min> <max>'` for a span. */
10622
+ function fontWeightValue(range) {
10623
+ return range.min === range.max ? range.min : `${range.min} ${range.max}`;
10624
+ }
10625
+ /** Any weight → the stored form; a bad value → 400 (as `normalizeFontWeight`). */
10626
+ function normalizeFontWeightValue(value) {
10627
+ return fontWeightValue(parseFontWeight(value) ?? { min: DEFAULT_WEIGHT, max: DEFAULT_WEIGHT });
10628
+ }
10629
+ /** Any weight → its range; a bad value → 400–400. */
10630
+ function fontWeightRange(value) {
10631
+ return parseFontWeight(value) ?? { min: DEFAULT_WEIGHT, max: DEFAULT_WEIGHT };
10632
+ }
10633
+ /** True when the weight spans more than one value (a variable font's wght axis). */
10634
+ function isFontWeightRange(value) {
10635
+ const r = parseFontWeight(value);
10636
+ return !!r && r.min !== r.max;
10637
+ }
10638
+ /** How a weight reads on screen: `700`, or `100–900` for a range. */
10639
+ function formatFontWeight(value) {
10640
+ const r = fontWeightRange(value);
10641
+ return r.min === r.max ? String(r.min) : `${r.min}\u2013${r.max}`;
10642
+ }
10643
+ /**
10644
+ * A weight an author or a client SENT, held to what a face can be — or null: a whole number 100–900,
10645
+ * or a range of whole numbers 1 ≤ min < max ≤ 1000. Nothing is clamped or rounded here; a value that
10646
+ * would need it is refused (the PUT correction's rule).
10647
+ */
10648
+ function checkFontWeight(value) {
10649
+ const text = typeof value === 'number' ? String(value) : typeof value === 'string' ? value.trim() : '';
10650
+ if (/^\d+$/.test(text)) {
10651
+ const n = Number(text);
10652
+ return n >= MIN_WEIGHT && n <= MAX_WEIGHT ? n : null;
10653
+ }
10654
+ const m = /^(\d+)(?:\s*[-\u2013\u2014]\s*|\s+)(\d+)$/.exec(text);
10655
+ if (!m)
10656
+ return null;
10657
+ const min = Number(m[1]);
10658
+ const max = Number(m[2]);
10659
+ return min >= CSS_MIN_WEIGHT && max <= CSS_MAX_WEIGHT && min < max ? `${min} ${max}` : null;
10660
+ }
10661
+ /** True when a face of `weight` renders `wanted` from its own file (inside its range). */
10662
+ function fontWeightCovers(weight, wanted) {
10663
+ const r = fontWeightRange(weight);
10664
+ return wanted >= r.min && wanted <= r.max;
10665
+ }
10464
10666
  /** The file extensions a studio may upload as a font, and the descriptor format each one IS (§5b). */
10465
10667
  const FONT_FILE_FORMATS = Object.freeze({
10466
10668
  woff2: 'woff2', woff: 'woff', ttf: 'truetype', otf: 'opentype',
@@ -10489,7 +10691,8 @@ function fontFormatOf(fileNameOrKey) {
10489
10691
  function fontFileStem(key) {
10490
10692
  return fontFileName(key).replace(/\.[^.]+$/, '');
10491
10693
  }
10492
- /** A weight as CSS can use it: 100–900, a bad value → 400. */
10694
+ /** A single weight as CSS can use it: 100–900, a bad value → 400. A range is not a single weight —
10695
+ * use `normalizeFontWeightValue` for a face. */
10493
10696
  function normalizeFontWeight(w) {
10494
10697
  const n = Math.round(Number(w));
10495
10698
  return Number.isFinite(n) && n > 0 ? Math.min(MAX_WEIGHT, Math.max(MIN_WEIGHT, n)) : DEFAULT_WEIGHT;
@@ -10515,7 +10718,7 @@ function buildFontFaceCss(descriptors) {
10515
10718
  const faces = new Map();
10516
10719
  for (const d of (descriptors ?? []).filter(usableDescriptor)) {
10517
10720
  const family = d.family.trim();
10518
- const weight = normalizeFontWeight(d.weight);
10721
+ const weight = normalizeFontWeightValue(d.weight);
10519
10722
  const style = normalizeFontStyle(d.style);
10520
10723
  const id = `${fontFamilyKey(family)}|${weight}|${style}`;
10521
10724
  const face = faces.get(id) ?? { family, weight, style, files: [] };
@@ -10530,7 +10733,7 @@ function buildFontFaceCss(descriptors) {
10530
10733
  .join(', ');
10531
10734
  return [
10532
10735
  '@font-face {',
10533
- ` font-family: ${cssString(face.family)};`,
10736
+ ` font-family: ${formatFontFamily(face.family)};`,
10534
10737
  ` src: ${src};`,
10535
10738
  ` font-weight: ${face.weight};`,
10536
10739
  ` font-style: ${face.style};`,
@@ -10540,6 +10743,18 @@ function buildFontFaceCss(descriptors) {
10540
10743
  })
10541
10744
  .join('\n');
10542
10745
  }
10746
+ /** Lighter first: by the range's lower end, then its upper end (a static 100 before a 100–900 file). */
10747
+ function compareFontWeights(a, b) {
10748
+ const ra = fontWeightRange(a);
10749
+ const rb = fontWeightRange(b);
10750
+ return ra.min - rb.min || ra.max - rb.max;
10751
+ }
10752
+ /** The weight a family's preview renders at: 400 when a face covers it, else its lightest. */
10753
+ function previewFontWeight(weights) {
10754
+ if (!weights?.length || weights.some(w => fontWeightCovers(w, DEFAULT_WEIGHT)))
10755
+ return DEFAULT_WEIGHT;
10756
+ return fontWeightRange([...weights].sort(compareFontWeights)[0]).min;
10757
+ }
10543
10758
  /** Files → families (§5b "Group into families"): the Files tile, the typeface dropdown and Design ▸ Type. */
10544
10759
  function groupFontFamilies(descriptors) {
10545
10760
  const groups = new Map();
@@ -10551,23 +10766,30 @@ function groupFontFamilies(descriptors) {
10551
10766
  }
10552
10767
  return [...groups.values()]
10553
10768
  .map(g => {
10554
- const faces = [...g.faces].sort((a, b) => normalizeFontWeight(a.weight) - normalizeFontWeight(b.weight)
10769
+ const faces = [...g.faces].sort((a, b) => compareFontWeights(a.weight, b.weight)
10555
10770
  || normalizeFontStyle(a.style).localeCompare(normalizeFontStyle(b.style)));
10771
+ const weights = new Map();
10772
+ for (const f of faces) {
10773
+ const w = normalizeFontWeightValue(f.weight);
10774
+ weights.set(String(w), w);
10775
+ }
10556
10776
  return {
10557
10777
  family: g.family,
10558
- weights: [...new Set(faces.map(f => normalizeFontWeight(f.weight)))].sort((a, b) => a - b),
10778
+ weights: [...weights.values()].sort(compareFontWeights),
10559
10779
  styles: FONT_STYLES.filter(s => faces.some(f => normalizeFontStyle(f.style) === s)),
10560
10780
  faces,
10561
10781
  };
10562
10782
  })
10563
10783
  .sort((a, b) => a.family.localeCompare(b.family));
10564
10784
  }
10565
- /** The family names in a CSS `font-family` stack, unquoted, in order. */
10785
+ /**
10786
+ * The family names in a CSS `font-family` stack, in order — the exact inverse of `formatFontFamily` /
10787
+ * `formatFontFamilies`: quotes removed, CSS escapes decoded (`\'`, `\\`, `\3c `), a comma INSIDE
10788
+ * quotes kept, an unquoted name's words joined by one space. Every reader of a font token goes
10789
+ * through this; a naive split on `,` is how `O'Neil Sans` stopped matching its own token.
10790
+ */
10566
10791
  function parseFontFamilies(stack) {
10567
- return String(stack ?? '')
10568
- .split(',')
10569
- .map(f => f.trim().replace(/^(['"])(.*)\1$/, '$2').trim())
10570
- .filter(Boolean);
10792
+ return splitFontStack(stack).map(e => e.family);
10571
10793
  }
10572
10794
  /** Where Google serves the stylesheets, and the origin the font FILES come from (preconnect). */
10573
10795
  const GOOGLE_FONTS_CSS_ORIGIN = 'https://fonts.googleapis.com';
@@ -10598,12 +10820,13 @@ function fontsForTokens(tokens, opts = {}) {
10598
10820
  const ids = [];
10599
10821
  for (const role of roles) {
10600
10822
  const row = rows.find(t => t.name === role);
10601
- for (const family of parseFontFamilies(row?.value ?? '')) {
10602
- const id = catalogFontId(family);
10603
- if (id && !seen.has(id)) {
10604
- seen.add(id);
10605
- ids.push(id);
10606
- }
10823
+ // The role's FIRST catalog family is the one that renders (or stands in for an uploaded
10824
+ // family that is missing — `userFontStack` puts the look's font right after the author's);
10825
+ // a catalog family further down is never reached, so it is never loaded.
10826
+ const id = parseFontFamilies(row?.value ?? '').map(catalogFontId).find((x) => !!x);
10827
+ if (id && !seen.has(id)) {
10828
+ seen.add(id);
10829
+ ids.push(id);
10607
10830
  }
10608
10831
  }
10609
10832
  return ids;
@@ -10614,26 +10837,37 @@ function stackNamesFamily(stack, family) {
10614
10837
  return key !== '' && parseFontFamilies(stack ?? '').some(f => fontFamilyKey(f) === key);
10615
10838
  }
10616
10839
  /**
10617
- * The font tokens whose value names `family` — what the delete guard asks before an uploaded family's
10618
- * last file goes (§5b): deleting it would silently drop those roles to their fallback.
10840
+ * The font tokens that render in `family` — their stack LEADS with it — what the delete guard asks
10841
+ * before an uploaded family's last file goes (§5b): deleting it would drop those roles to their
10842
+ * fallback. A role naming it further down (the look's font behind the author's, `userFontStack`) does
10843
+ * not render in it, and rewriting that role would throw the author's own choice away.
10619
10844
  */
10620
10845
  function fontRoleReferences(tokens, family) {
10846
+ const key = fontFamilyKey(family);
10847
+ if (!key)
10848
+ return [];
10621
10849
  const rows = (tokens ?? []).filter(t => !!t && typeof t.name === 'string');
10622
- return FONT_FAMILY_TOKENS.filter(role => rows.some(t => t.name === role && stackNamesFamily(t.value, family)));
10850
+ return FONT_FAMILY_TOKENS.filter(role => rows.some(t => t.name === role
10851
+ && fontFamilyKey(parseFontFamilies(t.value ?? '')[0]) === key));
10623
10852
  }
10624
10853
  /**
10625
- * The face CSS font matching picks for `weight` from `faces` (CSS Fonts §5.2): 400 tries 500 next,
10626
- * 500 tries 400; lighter weights look down first, heavier ones up. So an `h1` at 600 over a family
10627
- * uploaded at 400 + 700 renders the real 700 cut — the one worth preloading.
10854
+ * The face CSS font matching picks for `weight` from `faces` (CSS Fonts §5.2): a face whose range
10855
+ * holds the weight is exact (a variable file covers every weight on its axis); otherwise 400 tries
10856
+ * 500 next, 500 tries 400; lighter weights look down first, heavier ones up — each face measured at
10857
+ * its nearest end. So an `h1` at 600 over a family uploaded at 400 + 700 renders the real 700 cut —
10858
+ * the one worth preloading.
10628
10859
  */
10629
10860
  function matchFontWeight(faces, weight) {
10630
10861
  if (!faces?.length)
10631
10862
  return null;
10632
10863
  const want = normalizeFontWeight(weight);
10633
- const exact = faces.find(f => normalizeFontWeight(f.weight) === want);
10864
+ const exact = faces.find(f => fontWeightCovers(f.weight, want));
10634
10865
  if (exact)
10635
10866
  return exact;
10636
- const w = (f) => normalizeFontWeight(f.weight);
10867
+ const w = (f) => {
10868
+ const r = fontWeightRange(f.weight);
10869
+ return r.max < want ? r.max : r.min;
10870
+ };
10637
10871
  const below = (max) => [...faces].filter(f => w(f) < max).sort((a, b) => w(b) - w(a))[0] ?? null;
10638
10872
  const above = (min) => [...faces].filter(f => w(f) > min).sort((a, b) => w(a) - w(b))[0] ?? null;
10639
10873
  if (want >= 400 && want <= 500) {
@@ -10672,6 +10906,35 @@ function fontPreloads(descriptors, tokens) {
10672
10906
  }
10673
10907
  return [...out.values()];
10674
10908
  }
10909
+ /** CSS generic families and the faces every visitor's system already has — never "missing". */
10910
+ const ALWAYS_AVAILABLE_FAMILIES = new Set([
10911
+ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui', 'ui-serif', 'ui-sans-serif',
10912
+ 'ui-monospace', 'ui-rounded', 'emoji', 'math', 'fangsong', '-apple-system', 'blinkmacsystemfont',
10913
+ 'arial', 'helvetica', 'helvetica neue', 'georgia', 'times new roman', 'times', 'courier new', 'courier',
10914
+ 'verdana', 'tahoma', 'trebuchet ms', 'segoe ui', 'roboto', 'menlo', 'monaco', 'consolas',
10915
+ ]);
10916
+ /**
10917
+ * Board 8's "the same check runs when a font token points at a missing family": the font roles whose
10918
+ * FIRST family is not uploaded (`uploaded` — the org's families), not in the catalog and not a system
10919
+ * face. Only meaningful over a listing that SUCCEEDED — an empty bucket because storage is down would
10920
+ * otherwise report every uploaded family missing.
10921
+ */
10922
+ function missingFontRoles(tokens, uploaded) {
10923
+ const have = new Set((uploaded ?? []).map(fontFamilyKey));
10924
+ const available = (family) => {
10925
+ const key = fontFamilyKey(family);
10926
+ return have.has(key) || ALWAYS_AVAILABLE_FAMILIES.has(key) || catalogFontId(family) !== null;
10927
+ };
10928
+ const resolved = resolveTokenValues(tokens);
10929
+ const out = [];
10930
+ for (const role of FONT_FAMILY_TOKENS) {
10931
+ const families = parseFontFamilies(resolved[role] ?? '');
10932
+ if (!families.length || /^var\(/i.test(families[0]) || available(families[0]))
10933
+ continue;
10934
+ out.push({ role, family: families[0], fallback: families.slice(1).find(available) ?? '' });
10935
+ }
10936
+ return out;
10937
+ }
10675
10938
 
10676
10939
  /**
10677
10940
  * A flavor's COMPLETE token set for a studio's choices (design-system plan §3a/§3e). A flavor IS
@@ -10699,9 +10962,16 @@ function defaultDesignChoices(pack) {
10699
10962
  ...pack.defaults,
10700
10963
  };
10701
10964
  }
10702
- /** `'<Family>', system-ui, sans-serif` — the value a font role holds once the author picks a face. */
10703
- function userFontStack(family) {
10704
- return `${cssString(family.trim())}, ${USER_FONT_FALLBACK}`;
10965
+ /**
10966
+ * The value a font role holds once the author picks a face: `'<Family>', <the look's own stack>` —
10967
+ * e.g. `'Rubik', 'Inter Tight', system-ui, -apple-system, 'Segoe UI', sans-serif`. The look's font
10968
+ * follows the author's, so a family that later stops existing (deleted outside the delete guard, a
10969
+ * bucket emptied by hand) renders in the pack's font — Board 8's promise — in every renderer, with no
10970
+ * runtime check. With no look stack: `system-ui, sans-serif`.
10971
+ */
10972
+ function userFontStack(family, packStack) {
10973
+ const fallback = fontStackWithout(packStack, family) || USER_FONT_FALLBACK;
10974
+ return `${formatFontFamily(family)}, ${fallback}`;
10705
10975
  }
10706
10976
  const toRow = (t, source) => ({
10707
10977
  name: t.name,
@@ -10739,7 +11009,7 @@ function buildFlavorTokens(pack, choices) {
10739
11009
  set(BUTTON_RADIUS, choices.pillButtons ? PILL_RADIUS : (buttonRadius?.overridden ? buttonRadius.value : CONTROL_RADIUS));
10740
11010
  for (const [role, family] of Object.entries(choices.fonts ?? {})) {
10741
11011
  if (typeof family === 'string' && family.trim() !== '')
10742
- set(role, userFontStack(family), USER_TOKEN_SOURCE);
11012
+ set(role, userFontStack(family, byName.get(role)?.value), USER_TOKEN_SOURCE);
10743
11013
  }
10744
11014
  if (choices.seed && choices.seed.toLowerCase() !== pack.seed.toLowerCase()) {
10745
11015
  const bg = byName.get('bg');
@@ -11244,5 +11514,5 @@ function planFontFamilyRemoval(existing, previous, family) {
11244
11514
  * Generated bundle index. Do not edit.
11245
11515
  */
11246
11516
 
11247
- 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, FONT_FILE_ACCEPT, FONT_FILE_EXTENSIONS, FONT_FILE_FORMATS, FONT_STYLES, FONT_WEIGHTS, 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, choicesForPack, colorSchemeClass, colorSchemeOf, compileSchemeCss, compileTokenCss, compileTokenCssBlocks, cssString, defaultDesignChoices, defaultElementStyleFor, deriveAccentTokens, deriveBrandTokens, designBaseline, designConfigEntries, designModeOf, designTokenRepairs, designTokenRows, elementStyleById, elementStyleClass, elementStylesFor, emptyImageLabelSrc, emptyImagePlaceholder, emptyImageSrc, flavorPackById, flavorSourceStamp, fontFamilyKey, fontFileName, fontFileStem, fontFormatOf, fontPreloads, fontRoleReferences, fontsForTokens, googleFontHref, groupFontFamilies, hasElementStyleMarker, isDesignTokenGroup, isDesignTokenTier, isDesignTokenType, isRetiredFlavorId, legacyRuleTokenReferences, matchFontWeight, missingDesignRows, nodeDesignClasses, normalizeFontStyle, normalizeFontWeight, oklabToRgb, ownedTraitStatus, packDesignChoices, parseFontFamilies, parseHexColor, planDesignApply, planFlavorRevert, planFlavorSeed, planFontFamilyRemoval, planSwissMigration, readDesignConfig, relativeLuminance, resolveDesignChoices, resolveTokenReferences, resolveTokenValues, revertAvailable, rgbToOklab, sameDesignChoices, specTokenNames, springToLinear, stackNamesFamily, styleKeyToKebab, switchElementStyle, syncNodeDesignClasses, toHexColor, tokenReferences, tokensToDarkVarMap, tokensToVarMap, unknownTokenReferences, userFontStack, validateFlavorPack, wcagContrastRatio };
11517
+ 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, FONT_FILE_ACCEPT, FONT_FILE_EXTENSIONS, FONT_FILE_FORMATS, FONT_STYLES, FONT_WEIGHTS, FontUploadError, GOOGLE_FONTS_CSS_ORIGIN, GOOGLE_FONTS_FILE_ORIGIN, MAX_FONT_FILE_BYTES, 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, checkFontWeight, choicesForPack, colorSchemeClass, colorSchemeOf, compareFontWeights, compileSchemeCss, compileTokenCss, compileTokenCssBlocks, cssString, defaultDesignChoices, defaultElementStyleFor, deriveAccentTokens, deriveBrandTokens, designBaseline, designConfigEntries, designModeOf, designTokenRepairs, designTokenRows, elementStyleById, elementStyleClass, elementStylesFor, emptyImageLabelSrc, emptyImagePlaceholder, emptyImageSrc, flavorPackById, flavorSourceStamp, fontFamilyKey, fontFileName, fontFileStem, fontFormatOf, fontPreloads, fontRoleReferences, fontStackWithout, fontWeightCovers, fontWeightRange, fontWeightValue, fontsForTokens, formatFontFamilies, formatFontFamily, formatFontWeight, googleFontHref, groupFontFamilies, hasElementStyleMarker, isDesignTokenGroup, isDesignTokenTier, isDesignTokenType, isFontWeightRange, isRetiredFlavorId, legacyRuleTokenReferences, matchFontWeight, missingDesignRows, missingFontRoles, nodeDesignClasses, normalizeFontStyle, normalizeFontWeight, normalizeFontWeightValue, oklabToRgb, ownedTraitStatus, packDesignChoices, parseFontFamilies, parseFontWeight, parseHexColor, planDesignApply, planFlavorRevert, planFlavorSeed, planFontFamilyRemoval, planSwissMigration, previewFontWeight, readDesignConfig, relativeLuminance, resolveDesignChoices, resolveTokenReferences, resolveTokenValues, revertAvailable, rgbToOklab, sameDesignChoices, specTokenNames, springToLinear, stackNamesFamily, styleKeyToKebab, switchElementStyle, syncNodeDesignClasses, toHexColor, tokenReferences, tokensToDarkVarMap, tokensToVarMap, unknownTokenReferences, userFontStack, validateFlavorPack, wcagContrastRatio };
11248
11518
  //# sourceMappingURL=vectoriox-iox-ui-design-core.mjs.map