@vectoriox/iox-ui 5.19.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.
|
@@ -10452,16 +10452,38 @@ function deriveAccentTokens(seed, against, pack = {}) {
|
|
|
10452
10452
|
*/
|
|
10453
10453
|
/** Family name → Google Fonts css2 `family=` id. Replaces the engine's private GOOGLE_FONTS map. */
|
|
10454
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;
|
|
10455
10473
|
/** The token roles whose values name the families a page renders in. */
|
|
10456
10474
|
const FONT_FAMILY_TOKENS = ['font-display', 'font-body', 'font-label', 'font-mono'];
|
|
10457
10475
|
const DEFAULT_WEIGHT = 400;
|
|
10458
10476
|
const MIN_WEIGHT = 100;
|
|
10459
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;
|
|
10460
10481
|
const FONT_FORMATS = ['woff2', 'woff', 'truetype', 'opentype'];
|
|
10461
10482
|
/**
|
|
10462
10483
|
* A CSS string literal, single-quoted: `\` and `'` escaped, line breaks and NUL dropped, and `<` /
|
|
10463
10484
|
* `>` hex-escaped (`\3c `) — the face CSS is inlined into a `<style>` by the SSR engine, where an
|
|
10464
|
-
* 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.
|
|
10465
10487
|
*/
|
|
10466
10488
|
function cssString(value) {
|
|
10467
10489
|
const body = String(value)
|
|
@@ -10472,6 +10494,175 @@ function cssString(value) {
|
|
|
10472
10494
|
.replace(/>/g, '\\3e ');
|
|
10473
10495
|
return `'${body}'`;
|
|
10474
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
|
+
}
|
|
10475
10666
|
/** The file extensions a studio may upload as a font, and the descriptor format each one IS (§5b). */
|
|
10476
10667
|
const FONT_FILE_FORMATS = Object.freeze({
|
|
10477
10668
|
woff2: 'woff2', woff: 'woff', ttf: 'truetype', otf: 'opentype',
|
|
@@ -10500,7 +10691,8 @@ function fontFormatOf(fileNameOrKey) {
|
|
|
10500
10691
|
function fontFileStem(key) {
|
|
10501
10692
|
return fontFileName(key).replace(/\.[^.]+$/, '');
|
|
10502
10693
|
}
|
|
10503
|
-
/** 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. */
|
|
10504
10696
|
function normalizeFontWeight(w) {
|
|
10505
10697
|
const n = Math.round(Number(w));
|
|
10506
10698
|
return Number.isFinite(n) && n > 0 ? Math.min(MAX_WEIGHT, Math.max(MIN_WEIGHT, n)) : DEFAULT_WEIGHT;
|
|
@@ -10526,7 +10718,7 @@ function buildFontFaceCss(descriptors) {
|
|
|
10526
10718
|
const faces = new Map();
|
|
10527
10719
|
for (const d of (descriptors ?? []).filter(usableDescriptor)) {
|
|
10528
10720
|
const family = d.family.trim();
|
|
10529
|
-
const weight =
|
|
10721
|
+
const weight = normalizeFontWeightValue(d.weight);
|
|
10530
10722
|
const style = normalizeFontStyle(d.style);
|
|
10531
10723
|
const id = `${fontFamilyKey(family)}|${weight}|${style}`;
|
|
10532
10724
|
const face = faces.get(id) ?? { family, weight, style, files: [] };
|
|
@@ -10541,7 +10733,7 @@ function buildFontFaceCss(descriptors) {
|
|
|
10541
10733
|
.join(', ');
|
|
10542
10734
|
return [
|
|
10543
10735
|
'@font-face {',
|
|
10544
|
-
` font-family: ${
|
|
10736
|
+
` font-family: ${formatFontFamily(face.family)};`,
|
|
10545
10737
|
` src: ${src};`,
|
|
10546
10738
|
` font-weight: ${face.weight};`,
|
|
10547
10739
|
` font-style: ${face.style};`,
|
|
@@ -10551,6 +10743,18 @@ function buildFontFaceCss(descriptors) {
|
|
|
10551
10743
|
})
|
|
10552
10744
|
.join('\n');
|
|
10553
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
|
+
}
|
|
10554
10758
|
/** Files → families (§5b "Group into families"): the Files tile, the typeface dropdown and Design ▸ Type. */
|
|
10555
10759
|
function groupFontFamilies(descriptors) {
|
|
10556
10760
|
const groups = new Map();
|
|
@@ -10562,23 +10766,30 @@ function groupFontFamilies(descriptors) {
|
|
|
10562
10766
|
}
|
|
10563
10767
|
return [...groups.values()]
|
|
10564
10768
|
.map(g => {
|
|
10565
|
-
const faces = [...g.faces].sort((a, b) =>
|
|
10769
|
+
const faces = [...g.faces].sort((a, b) => compareFontWeights(a.weight, b.weight)
|
|
10566
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
|
+
}
|
|
10567
10776
|
return {
|
|
10568
10777
|
family: g.family,
|
|
10569
|
-
weights: [...
|
|
10778
|
+
weights: [...weights.values()].sort(compareFontWeights),
|
|
10570
10779
|
styles: FONT_STYLES.filter(s => faces.some(f => normalizeFontStyle(f.style) === s)),
|
|
10571
10780
|
faces,
|
|
10572
10781
|
};
|
|
10573
10782
|
})
|
|
10574
10783
|
.sort((a, b) => a.family.localeCompare(b.family));
|
|
10575
10784
|
}
|
|
10576
|
-
/**
|
|
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
|
+
*/
|
|
10577
10791
|
function parseFontFamilies(stack) {
|
|
10578
|
-
return
|
|
10579
|
-
.split(',')
|
|
10580
|
-
.map(f => f.trim().replace(/^(['"])(.*)\1$/, '$2').trim())
|
|
10581
|
-
.filter(Boolean);
|
|
10792
|
+
return splitFontStack(stack).map(e => e.family);
|
|
10582
10793
|
}
|
|
10583
10794
|
/** Where Google serves the stylesheets, and the origin the font FILES come from (preconnect). */
|
|
10584
10795
|
const GOOGLE_FONTS_CSS_ORIGIN = 'https://fonts.googleapis.com';
|
|
@@ -10609,12 +10820,13 @@ function fontsForTokens(tokens, opts = {}) {
|
|
|
10609
10820
|
const ids = [];
|
|
10610
10821
|
for (const role of roles) {
|
|
10611
10822
|
const row = rows.find(t => t.name === role);
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
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);
|
|
10618
10830
|
}
|
|
10619
10831
|
}
|
|
10620
10832
|
return ids;
|
|
@@ -10625,26 +10837,37 @@ function stackNamesFamily(stack, family) {
|
|
|
10625
10837
|
return key !== '' && parseFontFamilies(stack ?? '').some(f => fontFamilyKey(f) === key);
|
|
10626
10838
|
}
|
|
10627
10839
|
/**
|
|
10628
|
-
* The font tokens
|
|
10629
|
-
* last file goes (§5b): deleting it would
|
|
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.
|
|
10630
10844
|
*/
|
|
10631
10845
|
function fontRoleReferences(tokens, family) {
|
|
10846
|
+
const key = fontFamilyKey(family);
|
|
10847
|
+
if (!key)
|
|
10848
|
+
return [];
|
|
10632
10849
|
const rows = (tokens ?? []).filter(t => !!t && typeof t.name === 'string');
|
|
10633
|
-
return FONT_FAMILY_TOKENS.filter(role => rows.some(t => t.name === role
|
|
10850
|
+
return FONT_FAMILY_TOKENS.filter(role => rows.some(t => t.name === role
|
|
10851
|
+
&& fontFamilyKey(parseFontFamilies(t.value ?? '')[0]) === key));
|
|
10634
10852
|
}
|
|
10635
10853
|
/**
|
|
10636
|
-
* The face CSS font matching picks for `weight` from `faces` (CSS Fonts §5.2):
|
|
10637
|
-
*
|
|
10638
|
-
*
|
|
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.
|
|
10639
10859
|
*/
|
|
10640
10860
|
function matchFontWeight(faces, weight) {
|
|
10641
10861
|
if (!faces?.length)
|
|
10642
10862
|
return null;
|
|
10643
10863
|
const want = normalizeFontWeight(weight);
|
|
10644
|
-
const exact = faces.find(f =>
|
|
10864
|
+
const exact = faces.find(f => fontWeightCovers(f.weight, want));
|
|
10645
10865
|
if (exact)
|
|
10646
10866
|
return exact;
|
|
10647
|
-
const w = (f) =>
|
|
10867
|
+
const w = (f) => {
|
|
10868
|
+
const r = fontWeightRange(f.weight);
|
|
10869
|
+
return r.max < want ? r.max : r.min;
|
|
10870
|
+
};
|
|
10648
10871
|
const below = (max) => [...faces].filter(f => w(f) < max).sort((a, b) => w(b) - w(a))[0] ?? null;
|
|
10649
10872
|
const above = (min) => [...faces].filter(f => w(f) > min).sort((a, b) => w(a) - w(b))[0] ?? null;
|
|
10650
10873
|
if (want >= 400 && want <= 500) {
|
|
@@ -10683,6 +10906,35 @@ function fontPreloads(descriptors, tokens) {
|
|
|
10683
10906
|
}
|
|
10684
10907
|
return [...out.values()];
|
|
10685
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
|
+
}
|
|
10686
10938
|
|
|
10687
10939
|
/**
|
|
10688
10940
|
* A flavor's COMPLETE token set for a studio's choices (design-system plan §3a/§3e). A flavor IS
|
|
@@ -10710,9 +10962,16 @@ function defaultDesignChoices(pack) {
|
|
|
10710
10962
|
...pack.defaults,
|
|
10711
10963
|
};
|
|
10712
10964
|
}
|
|
10713
|
-
/**
|
|
10714
|
-
|
|
10715
|
-
|
|
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}`;
|
|
10716
10975
|
}
|
|
10717
10976
|
const toRow = (t, source) => ({
|
|
10718
10977
|
name: t.name,
|
|
@@ -10750,7 +11009,7 @@ function buildFlavorTokens(pack, choices) {
|
|
|
10750
11009
|
set(BUTTON_RADIUS, choices.pillButtons ? PILL_RADIUS : (buttonRadius?.overridden ? buttonRadius.value : CONTROL_RADIUS));
|
|
10751
11010
|
for (const [role, family] of Object.entries(choices.fonts ?? {})) {
|
|
10752
11011
|
if (typeof family === 'string' && family.trim() !== '')
|
|
10753
|
-
set(role, userFontStack(family), USER_TOKEN_SOURCE);
|
|
11012
|
+
set(role, userFontStack(family, byName.get(role)?.value), USER_TOKEN_SOURCE);
|
|
10754
11013
|
}
|
|
10755
11014
|
if (choices.seed && choices.seed.toLowerCase() !== pack.seed.toLowerCase()) {
|
|
10756
11015
|
const bg = byName.get('bg');
|
|
@@ -11255,5 +11514,5 @@ function planFontFamilyRemoval(existing, previous, family) {
|
|
|
11255
11514
|
* Generated bundle index. Do not edit.
|
|
11256
11515
|
*/
|
|
11257
11516
|
|
|
11258
|
-
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 };
|
|
11259
11518
|
//# sourceMappingURL=vectoriox-iox-ui-design-core.mjs.map
|