@vectoriox/iox-ui 5.45.0 → 5.46.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.
@@ -1,4 +1,4 @@
1
- import { SPEC_FONTS } from './flavor-spec.generated';
1
+ import { SPEC_FONTS, SPEC_SCRIPTS } from './flavor-spec.generated';
2
2
  import { DesignTokenLike, resolveTokenValues } from './token-css';
3
3
 
4
4
  /**
@@ -429,6 +429,40 @@ export interface FontsForTokensOptions {
429
429
  * upload, and the browser picked Google's — DS5.1).
430
430
  */
431
431
  uploaded?: ReadonlyArray<string>;
432
+ /**
433
+ * The page's locale (`page.locale`). A locale whose script the Latin families cannot draw (`he` →
434
+ * Hebrew) also links each role's script PARTNER — the family the stack names after the first that draws
435
+ * it (H1-2). An English page loads no Hebrew file.
436
+ */
437
+ locale?: string | null;
438
+ }
439
+
440
+ /** A script the catalog's Latin families cannot draw, and the families that can (contract `scripts`). */
441
+ export type ScriptId = keyof typeof SPEC_SCRIPTS;
442
+ const SCRIPT_FAMILIES = new Map<ScriptId, Set<string>>(
443
+ (Object.entries(SPEC_SCRIPTS) as Array<[ScriptId, { families: ReadonlyArray<string> }]>)
444
+ .map(([script, def]) => [script, new Set(def.families.map(fontFamilyKey))]));
445
+
446
+ /** The scripts a page in `locale` needs drawn (`he`, `he-IL` → hebrew); none for a Latin locale. */
447
+ export function scriptsForLocale(locale: string | null | undefined): ScriptId[] {
448
+ const lang = String(locale ?? '').trim().toLowerCase().split(/[-_]/)[0];
449
+ return (Object.entries(SPEC_SCRIPTS) as Array<[ScriptId, { locales: ReadonlyArray<string> }]>)
450
+ .filter(([, def]) => def.locales.includes(lang)).map(([script]) => script);
451
+ }
452
+
453
+ /** True when `family` is a catalog family that draws `script`. */
454
+ export function drawsScript(family: string | null | undefined, script: ScriptId): boolean {
455
+ return !!SCRIPT_FAMILIES.get(script)?.has(fontFamilyKey(family));
456
+ }
457
+
458
+ /**
459
+ * A stack with its script PARTNERS removed — every family after the first that only stands there to draw
460
+ * a script. What the contract wrote before H1-2 placed the partners: a row seeded then still holds it, and
461
+ * is the look's own, not the author's (`designBaseline`).
462
+ */
463
+ export function withoutScriptPartners(stack: string | null | undefined): string {
464
+ const partner = (family: string) => [...SCRIPT_FAMILIES.values()].some(set => set.has(fontFamilyKey(family)));
465
+ return splitFontStack(stack).filter((e, i) => i === 0 || !partner(e.family)).map(e => e.raw).join(', ');
432
466
  }
433
467
 
434
468
  /**
@@ -445,23 +479,35 @@ export function fontsForTokens(
445
479
  || rows.some(t => t.name !== MONO_ROLE && MONO_REFERENCE.test(`${t.value ?? ''} ${t.values?.dark ?? ''}`));
446
480
  const roles = monoRead ? FONT_FAMILY_TOKENS : ALWAYS_LOADED_ROLES;
447
481
  const uploaded = new Set((opts.uploaded ?? []).map(fontFamilyKey));
482
+ const scripts = scriptsForLocale(opts.locale);
448
483
  const seen = new Set<string>();
449
484
  const ids: string[] = [];
485
+ const add = (id: string | null) => {
486
+ if (id && !seen.has(id)) {
487
+ seen.add(id);
488
+ ids.push(id);
489
+ }
490
+ };
450
491
  for (const role of roles) {
451
- const row = rows.find(t => t.name === role);
492
+ const families = parseFontFamilies(rows.find(t => t.name === role)?.value ?? '');
452
493
  // The role's FIRST family that renders decides: an uploaded one needs nothing from Google; the
453
494
  // first catalog one is linked (it renders, or stands in for an uploaded family that is missing
454
495
  // — `userFontStack` puts the look's font right after the author's). Nothing further down is
455
- // ever reached, so nothing further down is loaded.
496
+ // reached for a Latin glyph, so nothing further down is loaded for one.
497
+ let lead: string | null = null;
456
498
  let id: string | null = null;
457
- for (const family of parseFontFamilies(row?.value ?? '')) {
458
- if (uploaded.has(fontFamilyKey(family))) break;
499
+ for (const family of families) {
500
+ if (uploaded.has(fontFamilyKey(family))) { lead = family; break; }
459
501
  id = catalogFontId(family);
460
- if (id) break;
502
+ if (id) { lead = family; break; }
461
503
  }
462
- if (id && !seen.has(id)) {
463
- seen.add(id);
464
- ids.push(id);
504
+ add(id);
505
+ // …but a Hebrew glyph IS reached further down: the browser falls through the stack, per glyph, to the
506
+ // first family that draws it — the role's script partner, linked for a page whose locale needs it.
507
+ for (const script of scripts) {
508
+ if (lead && drawsScript(lead, script)) continue;
509
+ const partner = families.find(f => drawsScript(f, script) && !uploaded.has(fontFamilyKey(f)));
510
+ add(partner ? catalogFontId(partner) : null);
465
511
  }
466
512
  }
467
513
  return ids;
@@ -526,8 +572,10 @@ const PRELOAD_FORMAT: FontFormat = 'woff2';
526
572
 
527
573
  /**
528
574
  * The uploaded files the SSR head preloads (§5b — no flash of the fallback face): for the display and
529
- * body roles, the family each token names (its first uploaded family), the woff2 normal-style cut CSS
530
- * matching would pick for that role's weights. One file per weight that renders, never the family.
575
+ * body roles, the family the role RENDERS in — the stack's first family that is uploaded, in the catalog
576
+ * or on every system — when it is an uploaded one, the woff2 normal-style cut CSS matching would pick for
577
+ * that role's weights. One file per weight that renders, never the family. A family further down (a
578
+ * Hebrew partner, H1-2) draws only the glyphs the lead lacks: never preloaded.
531
579
  */
532
580
  export function fontPreloads(
533
581
  descriptors: ReadonlyArray<FontDescriptor> | null | undefined,
@@ -539,7 +587,9 @@ export function fontPreloads(
539
587
  const out = new Map<string, FontDescriptor>();
540
588
  for (const { role, weights, fallback } of PRELOAD_ROLES) {
541
589
  const named = parseFontFamilies(resolved[role] ?? '').map(fontFamilyKey);
542
- const family = named.map(key => families.find(f => fontFamilyKey(f.family) === key)).find(Boolean);
590
+ const uploadedFamily = (key: string) => families.find(f => fontFamilyKey(f.family) === key);
591
+ const lead = named.find(key => !!uploadedFamily(key) || !!catalogFontId(key) || ALWAYS_AVAILABLE_FAMILIES.has(key));
592
+ const family = lead ? uploadedFamily(lead) : undefined;
543
593
  const faces = family?.faces.filter(f => f.format === PRELOAD_FORMAT && normalizeFontStyle(f.style) === 'normal') ?? [];
544
594
  for (const token of weights) {
545
595
  const weight = Number.parseInt(resolved[token] ?? '', 10);