@vectoriox/iox-ui 5.17.0 → 5.18.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.
@@ -8694,8 +8694,43 @@ declare const FONT_FAMILY_TOKENS: readonly ["font-display", "font-body", "font-l
8694
8694
  * uploaded family named `</style><script>` would otherwise close the element.
8695
8695
  */
8696
8696
  declare function cssString(value: string): string;
8697
- /** One `@font-face` per file, carrying its real weight and style — so bold is never synthesised. */
8697
+ /** The file extensions a studio may upload as a font, and the descriptor format each one IS (§5b). */
8698
+ declare const FONT_FILE_FORMATS: Readonly<Record<string, FontFormat>>;
8699
+ declare const FONT_FILE_EXTENSIONS: ReadonlyArray<string>;
8700
+ /** For a file input's `accept` — the same list the server validates against. */
8701
+ declare const FONT_FILE_ACCEPT: string;
8702
+ /** The CSS weights a face can be declared at, for an editor that offers them. */
8703
+ declare const FONT_WEIGHTS: ReadonlyArray<number>;
8704
+ declare const FONT_STYLES: ReadonlyArray<FontStyle>;
8705
+ /** A stored key's original file name: the folder prefix and the `-<timestamp>` upload suffix removed. */
8706
+ declare function fontFileName(key: string | null | undefined): string;
8707
+ /** The descriptor format of a font file, from its extension — null for anything we do not serve. */
8708
+ declare function fontFormatOf(fileNameOrKey: string | null | undefined): FontFormat | null;
8709
+ /** The family a file had before DS5 — its name without the extension. Legacy files with no parsed
8710
+ * name table keep it, so a page saved with `font-family: 'Rubik-Regular'` keeps its face (D5). */
8711
+ declare function fontFileStem(key: string | null | undefined): string;
8712
+ /** A weight as CSS can use it: 100–900, a bad value → 400. */
8713
+ declare function normalizeFontWeight(w: unknown): number;
8714
+ declare function normalizeFontStyle(style: unknown): FontStyle;
8715
+ /** CSS family names match case-insensitively; this is the one comparison key. */
8716
+ declare function fontFamilyKey(family: string | null | undefined): string;
8717
+ /**
8718
+ * One `@font-face` per FACE (family × weight × style), carrying its real weight and style — so bold is
8719
+ * never synthesised. Files that declare the same face (a woff2 and a ttf of one cut) share one rule
8720
+ * whose `src` lists them best format first, which is what the two retired forks did per file stem.
8721
+ */
8698
8722
  declare function buildFontFaceCss(descriptors: ReadonlyArray<FontDescriptor> | null | undefined): string;
8723
+ /** An uploaded family: every file sharing a family name (case-insensitively), with its real cuts. */
8724
+ interface FontFamilyGroup {
8725
+ family: string;
8726
+ /** Distinct weights, ascending. */
8727
+ weights: number[];
8728
+ styles: FontStyle[];
8729
+ /** Every file of the family, lightest first. */
8730
+ faces: FontDescriptor[];
8731
+ }
8732
+ /** Files → families (§5b "Group into families"): the Files tile, the typeface dropdown and Design ▸ Type. */
8733
+ declare function groupFontFamilies(descriptors: ReadonlyArray<FontDescriptor> | null | undefined): FontFamilyGroup[];
8699
8734
  /** The family names in a CSS `font-family` stack, unquoted, in order. */
8700
8735
  declare function parseFontFamilies(stack: string): string[];
8701
8736
  /** Where Google serves the stylesheets, and the origin the font FILES come from (preconnect). */
@@ -8719,6 +8754,28 @@ interface FontsForTokensOptions {
8719
8754
  * one bad id cannot sink the others), in role order.
8720
8755
  */
8721
8756
  declare function fontsForTokens(tokens: ReadonlyArray<DesignTokenLike> | null | undefined, opts?: FontsForTokensOptions): string[];
8757
+ type FontFamilyToken = (typeof FONT_FAMILY_TOKENS)[number];
8758
+ /** True when `stack` (a CSS `font-family` value) names `family` anywhere in it. */
8759
+ declare function stackNamesFamily(stack: string | null | undefined, family: string | null | undefined): boolean;
8760
+ /**
8761
+ * The font tokens whose value names `family` — what the delete guard asks before an uploaded family's
8762
+ * last file goes (§5b): deleting it would silently drop those roles to their fallback.
8763
+ */
8764
+ declare function fontRoleReferences(tokens: ReadonlyArray<DesignTokenLike> | null | undefined, family: string | null | undefined): FontFamilyToken[];
8765
+ /**
8766
+ * The face CSS font matching picks for `weight` from `faces` (CSS Fonts §5.2): 400 tries 500 next,
8767
+ * 500 tries 400; lighter weights look down first, heavier ones up. So an `h1` at 600 over a family
8768
+ * uploaded at 400 + 700 renders the real 700 cut — the one worth preloading.
8769
+ */
8770
+ declare function matchFontWeight<T extends {
8771
+ weight: number;
8772
+ }>(faces: ReadonlyArray<T>, weight: number): T | null;
8773
+ /**
8774
+ * The uploaded files the SSR head preloads (§5b — no flash of the fallback face): for the display and
8775
+ * body roles, the family each token names (its first uploaded family), the woff2 normal-style cut CSS
8776
+ * matching would pick for that role's weights. One file per weight that renders, never the family.
8777
+ */
8778
+ declare function fontPreloads(descriptors: ReadonlyArray<FontDescriptor> | null | undefined, tokens: ReadonlyArray<DesignTokenLike> | null | undefined): FontDescriptor[];
8722
8779
 
8723
8780
  /**
8724
8781
  * Which `--iox-<name>` a piece of COMPONENT CSS may reference (design-system §3b-bis: "component CSS
@@ -8857,6 +8914,26 @@ interface SwissMigration {
8857
8914
  * cycle; it migrates like any other Swiss row.
8858
8915
  */
8859
8916
  declare function planSwissMigration(existing: ReadonlyArray<StoredDesignTokenRow>, stored: StoredDesign): SwissMigration | null;
8917
+ /** What deleting an uploaded family does to the look (§5b delete guard). */
8918
+ interface FontFamilyRemoval {
8919
+ /** The font tokens that name the family — empty means nothing references it and nothing changes. */
8920
+ roles: FontFamilyToken[];
8921
+ /** The design without the family: every font role it held goes back to the pack's pairing. */
8922
+ choices: DesignChoices;
8923
+ /** The token writes: the look's own merge, plus any OTHER row still naming the family (one typed
8924
+ * in the Tokens panel), rewritten to the pack's value too — a deleted face must not stay named. */
8925
+ plan: FlavorSeedPlan;
8926
+ /** Per role, the family it falls back to — what the guard tells the author before they confirm. */
8927
+ fallbacks: Array<{
8928
+ role: FontFamilyToken;
8929
+ family: string;
8930
+ }>;
8931
+ }
8932
+ /**
8933
+ * Plan the removal of an uploaded family from the look: the roles naming it fall back to the pack's
8934
+ * own font (the design's pairing), as the guard promises — "Nothing else changes".
8935
+ */
8936
+ declare function planFontFamilyRemoval(existing: ReadonlyArray<StoredDesignTokenRow>, previous: StoredDesign, family: string): FontFamilyRemoval;
8860
8937
 
8861
- 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, choicesForPack, colorSchemeClass, colorSchemeOf, compileSchemeCss, compileTokenCss, compileTokenCssBlocks, cssString, defaultDesignChoices, defaultElementStyleFor, deriveAccentTokens, deriveBrandTokens, designBaseline, designConfigEntries, designModeOf, designTokenRepairs, designTokenRows, elementStyleById, elementStyleClass, elementStylesFor, emptyImageLabelSrc, emptyImagePlaceholder, 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 };
8862
- export type { AccentBackgrounds, AccentTokenName, ApplyElementStyleResult, DensityId, DesignChoices, DesignClassSource, DesignConfigEntry, DesignMode, DesignTokenGroup, DesignTokenLike, DesignTokenRow, DesignTokenTier, DesignTokenType, ElementStyle, ElementStyleTarget, ElementStyleTraitLike, FlavorPack, FlavorPackV2, FlavorRevertPlan, FlavorRevertSnapshot, FlavorSeedPlan, FlavorSpecToken, FlavorTokenDef, FontDescriptor, FontFormat, FontPair, FontRoleToken, FontStyle, FontsForTokensOptions, LinearSpring, MotionId, OkLab, OwnedTrait, OwnedTraitStatus, PalettePreset, ResolvedMode, Rgb, SchemeId, SeedBaseline, SeedExpectation, ShapeId, StoredDesign, StoredDesignTokenRow, SwissMigration, SwitchElementStyleResult, TokenCssBlocks, TokenCssOptions };
8938
+ 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 };
8939
+ export type { AccentBackgrounds, AccentTokenName, ApplyElementStyleResult, DensityId, DesignChoices, DesignClassSource, DesignConfigEntry, DesignMode, DesignTokenGroup, DesignTokenLike, DesignTokenRow, DesignTokenTier, DesignTokenType, ElementStyle, ElementStyleTarget, ElementStyleTraitLike, FlavorPack, FlavorPackV2, FlavorRevertPlan, FlavorRevertSnapshot, FlavorSeedPlan, FlavorSpecToken, FlavorTokenDef, FontDescriptor, FontFamilyGroup, FontFamilyRemoval, FontFamilyToken, FontFormat, FontPair, FontRoleToken, FontStyle, FontsForTokensOptions, LinearSpring, MotionId, OkLab, OwnedTrait, OwnedTraitStatus, PalettePreset, ResolvedMode, Rgb, SchemeId, SeedBaseline, SeedExpectation, ShapeId, StoredDesign, StoredDesignTokenRow, SwissMigration, SwitchElementStyleResult, TokenCssBlocks, TokenCssOptions };
@@ -10461,28 +10461,107 @@ function cssString(value) {
10461
10461
  .replace(/>/g, '\\3e ');
10462
10462
  return `'${body}'`;
10463
10463
  }
10464
- const clampWeight = (w) => {
10464
+ /** The file extensions a studio may upload as a font, and the descriptor format each one IS (§5b). */
10465
+ const FONT_FILE_FORMATS = Object.freeze({
10466
+ woff2: 'woff2', woff: 'woff', ttf: 'truetype', otf: 'opentype',
10467
+ });
10468
+ const FONT_FILE_EXTENSIONS = Object.keys(FONT_FILE_FORMATS);
10469
+ /** For a file input's `accept` — the same list the server validates against. */
10470
+ const FONT_FILE_ACCEPT = FONT_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',');
10471
+ /** The CSS weights a face can be declared at, for an editor that offers them. */
10472
+ const FONT_WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900];
10473
+ const FONT_STYLES = ['normal', 'italic'];
10474
+ /** When two files declare the same face, the browser should try the smallest, best-supported first. */
10475
+ const FORMAT_PREFERENCE = ['woff2', 'woff', 'opentype', 'truetype'];
10476
+ const UPLOAD_SUFFIX = /-\d+$/;
10477
+ /** A stored key's original file name: the folder prefix and the `-<timestamp>` upload suffix removed. */
10478
+ function fontFileName(key) {
10479
+ const name = String(key ?? '');
10480
+ return name.substring(name.lastIndexOf('/') + 1).replace(UPLOAD_SUFFIX, '');
10481
+ }
10482
+ /** The descriptor format of a font file, from its extension — null for anything we do not serve. */
10483
+ function fontFormatOf(fileNameOrKey) {
10484
+ const ext = /\.([a-z0-9]+)$/i.exec(fontFileName(fileNameOrKey))?.[1]?.toLowerCase() ?? '';
10485
+ return FONT_FILE_FORMATS[ext] ?? null;
10486
+ }
10487
+ /** The family a file had before DS5 — its name without the extension. Legacy files with no parsed
10488
+ * name table keep it, so a page saved with `font-family: 'Rubik-Regular'` keeps its face (D5). */
10489
+ function fontFileStem(key) {
10490
+ return fontFileName(key).replace(/\.[^.]+$/, '');
10491
+ }
10492
+ /** A weight as CSS can use it: 100–900, a bad value → 400. */
10493
+ function normalizeFontWeight(w) {
10465
10494
  const n = Math.round(Number(w));
10466
10495
  return Number.isFinite(n) && n > 0 ? Math.min(MAX_WEIGHT, Math.max(MIN_WEIGHT, n)) : DEFAULT_WEIGHT;
10496
+ }
10497
+ function normalizeFontStyle(style) {
10498
+ return style === 'italic' ? 'italic' : 'normal';
10499
+ }
10500
+ /** CSS family names match case-insensitively; this is the one comparison key. */
10501
+ function fontFamilyKey(family) {
10502
+ return String(family ?? '').trim().toLowerCase();
10503
+ }
10504
+ const usableDescriptor = (d) => !!d && typeof d.family === 'string' && d.family.trim() !== '' && typeof d.url === 'string' && d.url !== '';
10505
+ const preference = (format) => {
10506
+ const i = FORMAT_PREFERENCE.indexOf(format);
10507
+ return i < 0 ? FORMAT_PREFERENCE.length : i;
10467
10508
  };
10468
- /** One `@font-face` per file, carrying its real weight and style — so bold is never synthesised. */
10509
+ /**
10510
+ * One `@font-face` per FACE (family × weight × style), carrying its real weight and style — so bold is
10511
+ * never synthesised. Files that declare the same face (a woff2 and a ttf of one cut) share one rule
10512
+ * whose `src` lists them best format first, which is what the two retired forks did per file stem.
10513
+ */
10469
10514
  function buildFontFaceCss(descriptors) {
10470
- return (descriptors ?? [])
10471
- .filter(d => !!d && typeof d.family === 'string' && d.family.trim() !== '' && typeof d.url === 'string' && d.url !== '')
10472
- .map(d => {
10473
- const format = FONT_FORMATS.includes(d.format) ? ` format(${cssString(d.format)})` : '';
10515
+ const faces = new Map();
10516
+ for (const d of (descriptors ?? []).filter(usableDescriptor)) {
10517
+ const family = d.family.trim();
10518
+ const weight = normalizeFontWeight(d.weight);
10519
+ const style = normalizeFontStyle(d.style);
10520
+ const id = `${fontFamilyKey(family)}|${weight}|${style}`;
10521
+ const face = faces.get(id) ?? { family, weight, style, files: [] };
10522
+ face.files.push(d);
10523
+ faces.set(id, face);
10524
+ }
10525
+ return [...faces.values()]
10526
+ .map(face => {
10527
+ const src = [...face.files]
10528
+ .sort((a, b) => preference(a.format) - preference(b.format))
10529
+ .map(d => `url(${cssString(d.url)})${FONT_FORMATS.includes(d.format) ? ` format(${cssString(d.format)})` : ''}`)
10530
+ .join(', ');
10474
10531
  return [
10475
10532
  '@font-face {',
10476
- ` font-family: ${cssString(d.family.trim())};`,
10477
- ` src: url(${cssString(d.url)})${format};`,
10478
- ` font-weight: ${clampWeight(d.weight)};`,
10479
- ` font-style: ${d.style === 'italic' ? 'italic' : 'normal'};`,
10533
+ ` font-family: ${cssString(face.family)};`,
10534
+ ` src: ${src};`,
10535
+ ` font-weight: ${face.weight};`,
10536
+ ` font-style: ${face.style};`,
10480
10537
  ' font-display: swap;',
10481
10538
  '}',
10482
10539
  ].join('\n');
10483
10540
  })
10484
10541
  .join('\n');
10485
10542
  }
10543
+ /** Files → families (§5b "Group into families"): the Files tile, the typeface dropdown and Design ▸ Type. */
10544
+ function groupFontFamilies(descriptors) {
10545
+ const groups = new Map();
10546
+ for (const d of (descriptors ?? []).filter(usableDescriptor)) {
10547
+ const key = fontFamilyKey(d.family);
10548
+ const group = groups.get(key) ?? { family: d.family.trim(), weights: [], styles: [], faces: [] };
10549
+ group.faces.push(d);
10550
+ groups.set(key, group);
10551
+ }
10552
+ return [...groups.values()]
10553
+ .map(g => {
10554
+ const faces = [...g.faces].sort((a, b) => normalizeFontWeight(a.weight) - normalizeFontWeight(b.weight)
10555
+ || normalizeFontStyle(a.style).localeCompare(normalizeFontStyle(b.style)));
10556
+ return {
10557
+ family: g.family,
10558
+ weights: [...new Set(faces.map(f => normalizeFontWeight(f.weight)))].sort((a, b) => a - b),
10559
+ styles: FONT_STYLES.filter(s => faces.some(f => normalizeFontStyle(f.style) === s)),
10560
+ faces,
10561
+ };
10562
+ })
10563
+ .sort((a, b) => a.family.localeCompare(b.family));
10564
+ }
10486
10565
  /** The family names in a CSS `font-family` stack, unquoted, in order. */
10487
10566
  function parseFontFamilies(stack) {
10488
10567
  return String(stack ?? '')
@@ -10529,6 +10608,70 @@ function fontsForTokens(tokens, opts = {}) {
10529
10608
  }
10530
10609
  return ids;
10531
10610
  }
10611
+ /** True when `stack` (a CSS `font-family` value) names `family` anywhere in it. */
10612
+ function stackNamesFamily(stack, family) {
10613
+ const key = fontFamilyKey(family);
10614
+ return key !== '' && parseFontFamilies(stack ?? '').some(f => fontFamilyKey(f) === key);
10615
+ }
10616
+ /**
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.
10619
+ */
10620
+ function fontRoleReferences(tokens, family) {
10621
+ 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)));
10623
+ }
10624
+ /**
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.
10628
+ */
10629
+ function matchFontWeight(faces, weight) {
10630
+ if (!faces?.length)
10631
+ return null;
10632
+ const want = normalizeFontWeight(weight);
10633
+ const exact = faces.find(f => normalizeFontWeight(f.weight) === want);
10634
+ if (exact)
10635
+ return exact;
10636
+ const w = (f) => normalizeFontWeight(f.weight);
10637
+ const below = (max) => [...faces].filter(f => w(f) < max).sort((a, b) => w(b) - w(a))[0] ?? null;
10638
+ const above = (min) => [...faces].filter(f => w(f) > min).sort((a, b) => w(a) - w(b))[0] ?? null;
10639
+ if (want >= 400 && want <= 500) {
10640
+ const within = [...faces].filter(f => w(f) > want && w(f) <= 500).sort((a, b) => w(a) - w(b))[0];
10641
+ return within ?? below(want) ?? above(500);
10642
+ }
10643
+ return want < 400 ? (below(want) ?? above(want)) : (above(want) ?? below(want));
10644
+ }
10645
+ /** The roles a preload serves, and the weight tokens whose cuts render above the fold. */
10646
+ const PRELOAD_ROLES = [
10647
+ { role: 'font-display', weights: ['weight-display', 'weight-heading'], fallback: 700 },
10648
+ { role: 'font-body', weights: ['weight-body', 'weight-strong'], fallback: DEFAULT_WEIGHT },
10649
+ ];
10650
+ const PRELOAD_FORMAT = 'woff2';
10651
+ /**
10652
+ * The uploaded files the SSR head preloads (§5b — no flash of the fallback face): for the display and
10653
+ * body roles, the family each token names (its first uploaded family), the woff2 normal-style cut CSS
10654
+ * matching would pick for that role's weights. One file per weight that renders, never the family.
10655
+ */
10656
+ function fontPreloads(descriptors, tokens) {
10657
+ const families = groupFontFamilies(descriptors);
10658
+ if (!families.length)
10659
+ return [];
10660
+ const resolved = resolveTokenValues(tokens);
10661
+ const out = new Map();
10662
+ for (const { role, weights, fallback } of PRELOAD_ROLES) {
10663
+ const named = parseFontFamilies(resolved[role] ?? '').map(fontFamilyKey);
10664
+ const family = named.map(key => families.find(f => fontFamilyKey(f.family) === key)).find(Boolean);
10665
+ const faces = family?.faces.filter(f => f.format === PRELOAD_FORMAT && normalizeFontStyle(f.style) === 'normal') ?? [];
10666
+ for (const token of weights) {
10667
+ const weight = Number.parseInt(resolved[token] ?? '', 10);
10668
+ const face = matchFontWeight(faces, Number.isFinite(weight) ? weight : fallback);
10669
+ if (face)
10670
+ out.set(face.url, face);
10671
+ }
10672
+ }
10673
+ return [...out.values()];
10674
+ }
10532
10675
 
10533
10676
  /**
10534
10677
  * A flavor's COMPLETE token set for a studio's choices (design-system plan §3a/§3e). A flavor IS
@@ -11057,6 +11200,34 @@ function planSwissMigration(existing, stored) {
11057
11200
  }
11058
11201
  return { choices, plan };
11059
11202
  }
11203
+ /**
11204
+ * Plan the removal of an uploaded family from the look: the roles naming it fall back to the pack's
11205
+ * own font (the design's pairing), as the guard promises — "Nothing else changes".
11206
+ */
11207
+ function planFontFamilyRemoval(existing, previous, family) {
11208
+ const roles = fontRoleReferences(existing, family);
11209
+ const fonts = Object.fromEntries(Object.entries(previous.choices.fonts ?? {})
11210
+ .filter(([, chosen]) => fontFamilyKey(chosen) !== fontFamilyKey(family)));
11211
+ const choices = { ...previous.choices };
11212
+ if (Object.keys(fonts).length)
11213
+ choices.fonts = fonts;
11214
+ else
11215
+ delete choices.fonts;
11216
+ if (!roles.length)
11217
+ return { roles, choices: previous.choices, plan: { create: [], update: [], kept: [] }, fallbacks: [] };
11218
+ const plan = planDesignApply(existing, previous, choices);
11219
+ const desired = new Map(designTokenRows(choices).map(r => [r.name, r]));
11220
+ const planned = new Set(plan.update.map(u => u.id));
11221
+ for (const row of existing ?? []) {
11222
+ const want = desired.get(row.name);
11223
+ if (!row._id || !want || planned.has(row._id) || !roles.includes(row.name))
11224
+ continue;
11225
+ plan.update.push({ id: row._id, data: { value: want.value, values: want.values ?? {}, source: want.source } });
11226
+ plan.kept = plan.kept.filter(name => name !== row.name);
11227
+ }
11228
+ const fallbacks = roles.map(role => ({ role, family: parseFontFamilies(desired.get(role)?.value ?? '')[0] ?? '' }));
11229
+ return { roles, choices, plan, fallbacks };
11230
+ }
11060
11231
 
11061
11232
  /**
11062
11233
  * @vectoriox/iox-ui/design-core — the shared, FRAMEWORK-FREE core of the design system
@@ -11073,5 +11244,5 @@ function planSwissMigration(existing, stored) {
11073
11244
  * Generated bundle index. Do not edit.
11074
11245
  */
11075
11246
 
11076
- 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, choicesForPack, colorSchemeClass, colorSchemeOf, compileSchemeCss, compileTokenCss, compileTokenCssBlocks, cssString, defaultDesignChoices, defaultElementStyleFor, deriveAccentTokens, deriveBrandTokens, designBaseline, designConfigEntries, designModeOf, designTokenRepairs, designTokenRows, elementStyleById, elementStyleClass, elementStylesFor, emptyImageLabelSrc, emptyImagePlaceholder, 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 };
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 };
11077
11248
  //# sourceMappingURL=vectoriox-iox-ui-design-core.mjs.map