@vectoriox/iox-ui 5.44.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.
- package/design-core/index.d.ts +759 -77
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +1054 -106
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/package.json +1 -1
- package/src/design-core/design-apply.ts +17 -4
- package/src/design-core/design-token.model.ts +2 -0
- package/src/design-core/flavor-spec.generated.ts +959 -91
- package/src/design-core/fonts.ts +62 -12
- package/src/design-core/public-api.ts +1 -0
- package/src/design-core/style-presets.ts +71 -0
package/src/design-core/fonts.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
530
|
-
*
|
|
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
|
|
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);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { SPEC_MOODS, SPEC_STYLE_PRESETS } from './flavor-spec.generated';
|
|
2
|
+
import { resolveDesignChoices } from './design-settings';
|
|
3
|
+
import {
|
|
4
|
+
DensityId, DesignChoices, DesignMode, DesignMotion, MotionId, ShapeId, TintId,
|
|
5
|
+
} from './design-token.model';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Style presets (AI plan rev 3 §3f, H1-1) — a style is a preset over what exists, never a stylesheet: a
|
|
9
|
+
* named `DesignChoices` + the site's motion character + the colour modes it passed the contrast audit in.
|
|
10
|
+
* Generated 1:1 from the contract (`stylePresets`, AA-gated per preset × mode in build-spec.mjs); Design ▸
|
|
11
|
+
* Look lists them, ranked by the harness (`@vectoriox/iox-ui/harness-core`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The closed mood vocabulary a preset carries (≤ 3) and an analysis returns. */
|
|
15
|
+
export type MoodId = keyof typeof SPEC_MOODS;
|
|
16
|
+
export const MOOD_IDS = Object.keys(SPEC_MOODS) as ReadonlyArray<MoodId>;
|
|
17
|
+
export const MOOD_LABELS: Readonly<Record<MoodId, string>> = SPEC_MOODS;
|
|
18
|
+
|
|
19
|
+
/** A colour mode a preset was audited in (`auto` is offered on top when it passed both). */
|
|
20
|
+
export type PresetMode = Exclude<DesignMode, 'auto'>;
|
|
21
|
+
|
|
22
|
+
export interface StylePreset {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly blurb: string;
|
|
26
|
+
readonly flavorId: string;
|
|
27
|
+
readonly seed: string;
|
|
28
|
+
readonly tint: TintId;
|
|
29
|
+
readonly pairId: string;
|
|
30
|
+
readonly shape: ShapeId;
|
|
31
|
+
readonly density: DensityId;
|
|
32
|
+
readonly motion: MotionId;
|
|
33
|
+
readonly pillButtons: boolean;
|
|
34
|
+
readonly moves: Readonly<DesignMotion>;
|
|
35
|
+
/** The modes it PASSED the audit in; the first is its default. */
|
|
36
|
+
readonly modes: ReadonlyArray<PresetMode>;
|
|
37
|
+
readonly moods: ReadonlyArray<MoodId>;
|
|
38
|
+
/** The UI UX Pro Max styles it stands for (attribution and the mapping table). */
|
|
39
|
+
readonly uupm: ReadonlyArray<string>;
|
|
40
|
+
/** In the M1-alpha slice. */
|
|
41
|
+
readonly slice: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The 24, catalogue order (four per flavor). Assignment here proves the generated data fits the type. */
|
|
45
|
+
export const STYLE_PRESETS: ReadonlyArray<StylePreset> = SPEC_STYLE_PRESETS;
|
|
46
|
+
|
|
47
|
+
export function stylePresetById(id: string | null | undefined): StylePreset | undefined {
|
|
48
|
+
return id ? STYLE_PRESETS.find(p => p.id === id) : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const FOLLOW_VISITOR: DesignMode = 'auto';
|
|
52
|
+
|
|
53
|
+
/** The modes Design offers for a preset: the audited ones, then "Follow visitor" when it passed both (H-D3). */
|
|
54
|
+
export function presetModes(preset: StylePreset): DesignMode[] {
|
|
55
|
+
return preset.modes.length > 1 ? [...preset.modes, FOLLOW_VISITOR] : [...preset.modes];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* What picking a preset means — the complete `DesignChoices`, its motion included — in `mode` (default:
|
|
60
|
+
* its first). A mode it was not audited in is REFUSED (null): a light-only preset is never worn dark.
|
|
61
|
+
*/
|
|
62
|
+
export function presetChoices(id: string, mode?: DesignMode): DesignChoices | null {
|
|
63
|
+
const preset = stylePresetById(id);
|
|
64
|
+
if (!preset) return null;
|
|
65
|
+
const wear = mode ?? preset.modes[0];
|
|
66
|
+
if (!presetModes(preset).includes(wear)) return null;
|
|
67
|
+
return resolveDesignChoices({
|
|
68
|
+
flavorId: preset.flavorId, seed: preset.seed, pairId: preset.pairId, shape: preset.shape, density: preset.density,
|
|
69
|
+
motion: preset.motion, pillButtons: preset.pillButtons, mode: wear, tint: preset.tint, ...preset.moves,
|
|
70
|
+
});
|
|
71
|
+
}
|