@multiplatform.one/theme 7.4.1 → 7.6.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/README.md +2 -1
- package/package.json +5 -5
- package/src/audit/index.ts +19 -0
- package/src/audit/themeMatrix.spec.ts +274 -0
- package/src/audit/themeMatrix.ts +630 -0
- package/src/figma/figmaTokens.spec.ts +25 -21
- package/src/figma/figmaTokens.ts +2 -2
- package/src/font.ts +37 -25
- package/src/index.ts +12 -0
- package/src/keyboardFocusRing.ts +12 -6
- package/src/theme/FontKnobStyles.tsx +14 -31
- package/src/theme/Tint.tsx +3 -2
- package/src/theme/animations/css.ts +14 -1
- package/src/theme/colorRules.spec.ts +306 -2
- package/src/theme/colorRules.ts +118 -0
- package/src/theme/cornerSmoothing.native.ts +39 -2
- package/src/theme/cornerSmoothing.spec.ts +270 -3
- package/src/theme/cornerSmoothing.ts +147 -10
- package/src/theme/createDefaultThemeConfig.spec.ts +107 -1
- package/src/theme/createDefaultThemeConfig.ts +27 -8
- package/src/theme/defaults/builderOptions.ts +23 -10
- package/src/theme/defaults/categoryFonts.ts +24 -0
- package/src/theme/defaults/fonts.spec.ts +163 -10
- package/src/theme/defaults/fonts.theme.spec.tsx +139 -0
- package/src/theme/defaults/fonts.ts +214 -50
- package/src/theme/defaults/index.ts +1 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +0 -11
- package/src/theme/focusState.spec.ts +119 -1
- package/src/theme/focusState.ts +32 -7
- package/src/theme/fontCategoryMetrics.ts +57 -0
- package/src/theme/fontCategoryStacks.ts +37 -0
- package/src/theme/fontKnobStyles.spec.ts +15 -2
- package/src/theme/glyphPaint.spec.ts +26 -0
- package/src/theme/glyphPaint.ts +46 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/knobs.ts +18 -2
- package/src/theme/layoutTokens.spec.ts +3 -1
- package/src/theme/layoutTokens.ts +47 -4
- package/src/theme/mpo13Proof.spec.ts +3 -3
- package/src/theme/mpo19Proof.spec.ts +285 -0
- package/src/theme/presets.ts +1 -1
- package/src/theme/radiusClass.spec.ts +394 -0
- package/src/theme/radiusClass.ts +203 -4
- package/src/theme/radiusClassTable.generated.ts +121 -0
- package/src/theme/recipeInputs.ts +223 -36
- package/src/theme/recipes.ts +68 -4
- package/src/theme/resolveKnobs.spec.ts +155 -13
- package/src/theme/resolveKnobs.ts +193 -27
- package/src/theme/sizeLadder.spec.ts +84 -0
- package/src/theme/sizeRecipes.spec.ts +19 -6
- package/src/theme/sizeRecipes.ts +43 -13
- package/src/theme/theme.native.tsx +2 -0
- package/src/theme/theme.provider.spec.ts +9 -0
- package/src/theme/theme.tsx +6 -3
- package/src/theme/tint.spec.tsx +36 -35
- package/src/theme/useResolvedKnobs.ts +6 -3
- package/types/audit/index.d.ts +2 -0
- package/types/audit/index.d.ts.map +1 -1
- package/types/audit/themeMatrix.d.ts +172 -0
- package/types/audit/themeMatrix.d.ts.map +1 -0
- package/types/figma/figmaTokens.d.ts +1 -1
- package/types/figma/figmaTokens.d.ts.map +1 -1
- package/types/font.d.ts.map +1 -1
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -1
- package/types/keyboardFocusRing.d.ts +11 -5
- package/types/keyboardFocusRing.d.ts.map +1 -1
- package/types/theme/FontKnobStyles.d.ts +8 -12
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Tint.d.ts +2 -1
- package/types/theme/Tint.d.ts.map +1 -1
- package/types/theme/animations/css.d.ts +15 -2
- package/types/theme/animations/css.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +59 -1
- package/types/theme/colorRules.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.d.ts +42 -20
- package/types/theme/cornerSmoothing.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.native.d.ts +22 -2
- package/types/theme/cornerSmoothing.native.d.ts.map +1 -1
- package/types/theme/createDefaultThemeConfig.d.ts +21 -3
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/categoryFonts.d.ts +23 -0
- package/types/theme/defaults/categoryFonts.d.ts.map +1 -0
- package/types/theme/defaults/fonts.d.ts +140 -2
- package/types/theme/defaults/fonts.d.ts.map +1 -1
- package/types/theme/defaults/index.d.ts +1 -0
- package/types/theme/defaults/index.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/focusState.d.ts +32 -6
- package/types/theme/focusState.d.ts.map +1 -1
- package/types/theme/fontCategoryMetrics.d.ts +36 -0
- package/types/theme/fontCategoryMetrics.d.ts.map +1 -0
- package/types/theme/fontCategoryStacks.d.ts +24 -0
- package/types/theme/fontCategoryStacks.d.ts.map +1 -0
- package/types/theme/glyphPaint.d.ts +16 -0
- package/types/theme/glyphPaint.d.ts.map +1 -0
- package/types/theme/index.d.ts +4 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +10 -2
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +46 -4
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/radiusClass.d.ts +96 -4
- package/types/theme/radiusClass.d.ts.map +1 -1
- package/types/theme/radiusClassTable.generated.d.ts +146 -0
- package/types/theme/radiusClassTable.generated.d.ts.map +1 -0
- package/types/theme/recipeInputs.d.ts +85 -14
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +78 -4
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +12 -1
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +27 -14
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/theme.d.ts.map +1 -1
- package/types/theme/theme.native.d.ts.map +1 -1
- package/types/theme/useResolvedKnobs.d.ts +2 -2
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
|
@@ -1,21 +1,76 @@
|
|
|
1
|
+
import { defaultConfig } from "@tamagui/config/v5";
|
|
1
2
|
import { createInterFont } from "@tamagui/font-inter";
|
|
3
|
+
// `@tamagui/web`, not `tamagui`: createDefaultThemeConfig.spec.ts mocks the
|
|
4
|
+
// whole `tamagui` module down to `createTamagui`, and this module is on that
|
|
5
|
+
// spec's import graph.
|
|
6
|
+
import { createFont, type GenericFont } from "@tamagui/web";
|
|
7
|
+
import { flooredLineHeight, fontCategoryMetrics } from "../fontCategoryMetrics";
|
|
8
|
+
import { fontCategoryStacks } from "../fontCategoryStacks";
|
|
9
|
+
import type { FontCategory } from "../knobs";
|
|
2
10
|
|
|
3
11
|
// Default purpose fonts for components that reference $heading / $body
|
|
4
12
|
// directly, both backed by Inter.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
13
|
+
//
|
|
14
|
+
// MPO-19 X8 + X9. These tables used to be @tamagui/config/v4's — headings on a
|
|
15
|
+
// 1.4x scale (H1 $10 = 64px) at whatever weight the knob happened to default
|
|
16
|
+
// to, body at 14px with `lineHeight = size + 10`. Against the v5 era this
|
|
17
|
+
// package now pins (see `TAMAGUI_ERA` in createDefaultThemeConfig.ts) that is a
|
|
18
|
+
// different typographic voice, not a variation on one: H1 was 64px/400 where
|
|
19
|
+
// stock v5 is 40px/800, large and light against compact and heavy. And
|
|
20
|
+
// `size + 10` is not a ratio at all — 1.91 at 11px, 1.22 at 46px — which is
|
|
21
|
+
// why v5 replaced it with an explicit taper.
|
|
22
|
+
//
|
|
23
|
+
// So the tables are no longer transcribed. They are READ from
|
|
24
|
+
// `defaultConfig.fonts`, which means the era pin is load-bearing rather than
|
|
25
|
+
// decorative: re-point it and the type scale follows, and there is no local
|
|
26
|
+
// copy that can drift from upstream between pins. That also retires the
|
|
27
|
+
// hand-mirrored `interBaseSizes` table this file used to carry purely so
|
|
28
|
+
// tracking could be computed per key — the size table it needed IS the one
|
|
29
|
+
// upstream ships.
|
|
10
30
|
//
|
|
11
31
|
// Tracking-by-size (Axiom 15 OPTICS + LC-11): letter-spacing is a function
|
|
12
32
|
// of the rendered size, not a constant — slightly loose at caption sizes,
|
|
13
33
|
// increasingly tight at display sizes (the same optical behavior Apple's SF
|
|
14
34
|
// implements per point size). The curve is Inter's own dynamic-metrics
|
|
15
35
|
// model (rsms.me/inter/dynmetrics): tracking(em) = a + b·e^(c·size).
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
36
|
+
//
|
|
37
|
+
// ── Category fonts (MPO-44, re-derived against the era pin) ──────────────
|
|
38
|
+
//
|
|
39
|
+
// A bodyFont stop is a FAMILY choice. It must move the family and nothing
|
|
40
|
+
// else, so every category font is built from the era's own `$body` tables
|
|
41
|
+
// rather than a ramp of its own. Measured on the label text node before this
|
|
42
|
+
// change, both schemes:
|
|
43
|
+
//
|
|
44
|
+
// sans-serif Inter 15px/23px
|
|
45
|
+
// serif Georgia 14px/24px
|
|
46
|
+
// mono ui-monospace 14px/24px
|
|
47
|
+
// rounded Nunito 14px/24px
|
|
48
|
+
//
|
|
49
|
+
// Registering the families (the first half of MPO-44) stopped the collapse to
|
|
50
|
+
// -apple-system 14px/normal, but the era pin landed after it and moved `$body`
|
|
51
|
+
// to 15/23 — so ten of eleven stops were left on a second ladder, and flipping
|
|
52
|
+
// the knob resized the label by a pixel and moved its leading. Same defect
|
|
53
|
+
// class as the one MPO-44 was filed for: the knob was still taking type off
|
|
54
|
+
// the ramp, just more quietly.
|
|
55
|
+
//
|
|
56
|
+
// Three deliberate departures from `$body`, all upward and all LC-11:
|
|
57
|
+
//
|
|
58
|
+
// 1. Leading is floored per category — `flooredLineHeight` raises the era's
|
|
59
|
+
// value where the taper falls under the category's ratio, never lowers
|
|
60
|
+
// it. Only display steps move; every text step is `$body`'s own value.
|
|
61
|
+
// 2. Tracking is 0, not Inter's curve. That curve is Inter-specific, and an
|
|
62
|
+
// empty table is not neutral: the class Tamagui emits for `$mono` does
|
|
63
|
+
// not declare `--f-letterSpacing-*`, so the value fell through to the
|
|
64
|
+
// `:root` Inter one and Georgia and Nunito rendered at Inter's −0.13px.
|
|
65
|
+
// There is no measured optical curve for these faces, so 0 is the honest
|
|
66
|
+
// value rather than a borrowed one.
|
|
67
|
+
// 3. Weight is the era's (400 at every step). The previous default was 300,
|
|
68
|
+
// which is off the reference's label weight in the other direction.
|
|
69
|
+
//
|
|
70
|
+
// `$body` itself is untouched: it IS upstream's table under the pin, and
|
|
71
|
+
// MPO-19 owns it. Its ratio dips below LC-11's 1.4 body floor from $11 up;
|
|
72
|
+
// FontKnobStyles floors that scope on web, and correcting the table would
|
|
73
|
+
// have to be argued on MPO-19, not smuggled in here.
|
|
19
74
|
|
|
20
75
|
const TRACKING_A = -0.0223;
|
|
21
76
|
const TRACKING_B = 0.185;
|
|
@@ -31,59 +86,168 @@ export function interTrackingPx(sizePx: number): number {
|
|
|
31
86
|
return Math.round(interTrackingEm(sizePx) * sizePx * 100) / 100;
|
|
32
87
|
}
|
|
33
88
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Legacy `size + 10` leading. Kept for `createDefaultFont` callers that pass
|
|
91
|
+
* their own size table and therefore have no era leading to floor.
|
|
92
|
+
*/
|
|
93
|
+
export const BODY_LINE_HEIGHT_OFFSET = 10;
|
|
94
|
+
|
|
95
|
+
export function bodyLineHeightPx(sizePx: number): number {
|
|
96
|
+
return sizePx + BODY_LINE_HEIGHT_OFFSET;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A tamagui font table with Variables unwrapped and `$` prefixes stripped.
|
|
101
|
+
* Upstream hands these back either raw or wrapped depending on where in the
|
|
102
|
+
* config pipeline they were read, and `createInterFont` wants them raw.
|
|
103
|
+
*/
|
|
104
|
+
function plain<T>(raw: unknown): Record<string, T> {
|
|
105
|
+
const out: Record<string, T> = {};
|
|
106
|
+
for (const [key, value] of Object.entries((raw ?? {}) as Record<string, T | { val?: T }>)) {
|
|
107
|
+
const unwrapped =
|
|
108
|
+
value && typeof value === "object" && "val" in value
|
|
109
|
+
? (value as { val?: T }).val
|
|
110
|
+
: (value as T);
|
|
111
|
+
if (unwrapped !== undefined) out[key.replace(/^\$/, "")] = unwrapped;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A tamagui font table's numbers, with Variables and `$` prefixes unwrapped. */
|
|
117
|
+
const numbers = (raw: unknown) => plain<number>(raw);
|
|
118
|
+
|
|
119
|
+
/** Inter dynamic tracking for every key of a size table. */
|
|
120
|
+
function trackingFor(sizes: Record<string, number>): Record<string, number> {
|
|
121
|
+
return Object.fromEntries(Object.entries(sizes).map(([key, px]) => [key, interTrackingPx(px)]));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Zero tracking for every key of a size table (LC-11, non-Inter faces). */
|
|
125
|
+
function neutralTracking(sizes: Record<string, number>): Record<string, number> {
|
|
126
|
+
return Object.fromEntries(Object.keys(sizes).map((key) => [key, 0]));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const v5Body = defaultConfig.fonts.body;
|
|
130
|
+
const v5Heading = defaultConfig.fonts.heading;
|
|
131
|
+
const bodySizes = numbers(v5Body.size);
|
|
132
|
+
const bodyLineHeights = numbers(v5Body.lineHeight);
|
|
133
|
+
const bodyWeights = plain<string>(v5Body.weight);
|
|
134
|
+
const headingSizes = numbers(v5Heading.size);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The era's body size table. Exported under its historical name because
|
|
138
|
+
* `createDefaultFont` defaults to it, so a consumer-registered category font
|
|
139
|
+
* lands on the same ladder as the built-in ones instead of a private copy
|
|
140
|
+
* that drifts at the next pin (MPO-19 X8/X9).
|
|
141
|
+
*/
|
|
142
|
+
export const bodyFontSizes: Record<string, number> = bodySizes;
|
|
143
|
+
|
|
144
|
+
/** The era's body leading, before any per-category floor. */
|
|
145
|
+
export const bodyFontLineHeights: Record<string, number> = bodyLineHeights;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Inter faces the workspace ships. v5 heading weights are sparse — 600 up to
|
|
149
|
+
* $5, 700 at $6–$8, 800 from $9 — and A4's reading of that is the point:
|
|
150
|
+
* everything between inherits, so bold is rare and therefore means something.
|
|
151
|
+
* Each declared weight needs a face or the browser synthesises one.
|
|
152
|
+
*/
|
|
153
|
+
const interFaces = {
|
|
154
|
+
400: { normal: "Inter" },
|
|
155
|
+
600: { normal: "InterSemiBold" },
|
|
156
|
+
700: { normal: "InterBold" },
|
|
157
|
+
800: { normal: "InterExtraBold" },
|
|
158
|
+
} as const;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Concrete family stacks for each `headingFont`/`bodyFont` category. Alias of
|
|
162
|
+
* the one table in `fontCategoryStacks.ts`, which `FontKnobStyles` also reads
|
|
163
|
+
* for its `--f-family` fallback — one stop cannot render two faces.
|
|
164
|
+
*/
|
|
165
|
+
export const fontFamilyStacks = fontCategoryStacks;
|
|
166
|
+
|
|
167
|
+
export type FontFamilyCategory = FontCategory;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Leading for one category: the era's body table with the category's LC-11
|
|
171
|
+
* floor applied. Every text step comes back as `$body`'s own value; only the
|
|
172
|
+
* display steps, where the era's taper eases below the floor, move up.
|
|
173
|
+
*/
|
|
174
|
+
export function categoryLineHeights(category: FontCategory): Record<string, number> {
|
|
175
|
+
const minRatio = (fontCategoryMetrics[category] ?? fontCategoryMetrics["sans-serif"]).body;
|
|
59
176
|
return Object.fromEntries(
|
|
60
|
-
Object.entries(
|
|
177
|
+
Object.entries(bodySizes).map(([key, px]) => [
|
|
178
|
+
key,
|
|
179
|
+
flooredLineHeight(px, bodyLineHeights[key] ?? bodyLineHeightPx(px), minRatio),
|
|
180
|
+
]),
|
|
61
181
|
);
|
|
62
182
|
}
|
|
63
183
|
|
|
64
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Category font on the `$body` ladder: same sizes (including `true`), same
|
|
186
|
+
* weights, leading floored per LC-11, tracking neutral. A missing `true` key
|
|
187
|
+
* is what collapsed `$serif`/`$mono`/`$rounded` to the system face at
|
|
188
|
+
* `line-height: normal` (MPO-44).
|
|
189
|
+
*/
|
|
190
|
+
export function createCategoryFont(category: FontCategory, family: string): GenericFont {
|
|
191
|
+
return createFont({
|
|
192
|
+
family,
|
|
193
|
+
size: bodySizes,
|
|
194
|
+
lineHeight: categoryLineHeights(category),
|
|
195
|
+
weight: bodyWeights,
|
|
196
|
+
letterSpacing: neutralTracking(bodySizes),
|
|
197
|
+
}) as GenericFont;
|
|
198
|
+
}
|
|
65
199
|
|
|
66
200
|
export const defaultHeadingFont = createInterFont(
|
|
67
201
|
{
|
|
68
|
-
face:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
sizeSize: headingSizeOf,
|
|
75
|
-
sizeLineHeight: (size) => size + 10,
|
|
202
|
+
face: interFaces,
|
|
203
|
+
size: headingSizes,
|
|
204
|
+
lineHeight: numbers(v5Heading.lineHeight),
|
|
205
|
+
weight: plain<string>(v5Heading.weight),
|
|
206
|
+
letterSpacing: trackingFor(headingSizes),
|
|
76
207
|
},
|
|
208
|
+
{ sizeSize: (size) => size },
|
|
77
209
|
);
|
|
78
210
|
|
|
79
211
|
export const defaultBodyFont = createInterFont(
|
|
80
212
|
{
|
|
81
|
-
face:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
sizeLineHeight: (size) => size + 10,
|
|
213
|
+
face: interFaces,
|
|
214
|
+
size: bodySizes,
|
|
215
|
+
lineHeight: bodyLineHeights,
|
|
216
|
+
weight: bodyWeights,
|
|
217
|
+
letterSpacing: trackingFor(bodySizes),
|
|
88
218
|
},
|
|
219
|
+
{ sizeSize: (size) => size },
|
|
89
220
|
);
|
|
221
|
+
|
|
222
|
+
export const defaultSerifFont = createCategoryFont("serif", fontFamilyStacks.serif);
|
|
223
|
+
export const defaultMonoFont = createCategoryFont("mono", fontFamilyStacks.mono);
|
|
224
|
+
export const defaultSlabFont = createCategoryFont("slab", fontFamilyStacks.slab);
|
|
225
|
+
export const defaultRoundedFont = createCategoryFont("rounded", fontFamilyStacks.rounded);
|
|
226
|
+
export const defaultCondensedFont = createCategoryFont("condensed", fontFamilyStacks.condensed);
|
|
227
|
+
export const defaultCursiveFont = createCategoryFont("cursive", fontFamilyStacks.cursive);
|
|
228
|
+
export const defaultHandwritingFont = createCategoryFont(
|
|
229
|
+
"handwriting",
|
|
230
|
+
fontFamilyStacks.handwriting,
|
|
231
|
+
);
|
|
232
|
+
export const defaultPixelFont = createCategoryFont("pixel", fontFamilyStacks.pixel);
|
|
233
|
+
export const defaultBlackletterFont = createCategoryFont(
|
|
234
|
+
"blackletter",
|
|
235
|
+
fontFamilyStacks.blackletter,
|
|
236
|
+
);
|
|
237
|
+
export const defaultGeometricFont = createCategoryFont("geometric", fontFamilyStacks.geometric);
|
|
238
|
+
|
|
239
|
+
/** All purpose + category fonts registered by `createDefaultThemeConfig`. */
|
|
240
|
+
export const defaultFonts = {
|
|
241
|
+
heading: defaultHeadingFont,
|
|
242
|
+
body: defaultBodyFont,
|
|
243
|
+
serif: defaultSerifFont,
|
|
244
|
+
mono: defaultMonoFont,
|
|
245
|
+
slab: defaultSlabFont,
|
|
246
|
+
rounded: defaultRoundedFont,
|
|
247
|
+
condensed: defaultCondensedFont,
|
|
248
|
+
cursive: defaultCursiveFont,
|
|
249
|
+
handwriting: defaultHandwritingFont,
|
|
250
|
+
pixel: defaultPixelFont,
|
|
251
|
+
blackletter: defaultBlackletterFont,
|
|
252
|
+
geometric: defaultGeometricFont,
|
|
253
|
+
} as const;
|
|
@@ -1170,15 +1170,6 @@ export function ThemeDevtoolsPanel({
|
|
|
1170
1170
|
}
|
|
1171
1171
|
}
|
|
1172
1172
|
}
|
|
1173
|
-
const fill = partial.fillStyle ?? knobs.fillStyle;
|
|
1174
|
-
const borderWidth = partial.borderWidth ?? knobs.borderWidth;
|
|
1175
|
-
if (fill === "outlined" && borderWidth === "none") {
|
|
1176
|
-
if (!pinnedKnobs.has("borderWidth")) {
|
|
1177
|
-
partial.borderWidth = pick(["small", "medium", "large"] as const);
|
|
1178
|
-
} else {
|
|
1179
|
-
partial.fillStyle = "filled";
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
1173
|
setOverrides(newOverrides);
|
|
1183
1174
|
setKnobs(partial);
|
|
1184
1175
|
if (!pinnedKnobs.has("color") && themeColors && onThemeColorChange) {
|
|
@@ -1200,8 +1191,6 @@ export function ThemeDevtoolsPanel({
|
|
|
1200
1191
|
expandedKnobs,
|
|
1201
1192
|
activeProfile,
|
|
1202
1193
|
animationOptions,
|
|
1203
|
-
knobs.fillStyle,
|
|
1204
|
-
knobs.borderWidth,
|
|
1205
1194
|
]);
|
|
1206
1195
|
|
|
1207
1196
|
// -------------------------------------------------------------------------
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { __resetDevWarnSeen, warnOutlineNone, warnPositiveTabIndex } from "../devWarn";
|
|
3
|
+
import { compositeFocusRingCss, keyboardFocusRingProps } from "../keyboardFocusRing";
|
|
3
4
|
import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
4
5
|
import { createThemesBuilder } from "./createThemes";
|
|
5
6
|
import { defaultAccentTheme } from "./defaults/accent";
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
FOCUS_RING_MIN_CONTRAST,
|
|
12
13
|
FOCUS_RING_MIN_WIDTH,
|
|
13
14
|
FOCUS_VISIBLE_RING,
|
|
15
|
+
FOCUS_VISIBLE_RING_INSET,
|
|
14
16
|
STATE_LAYER_HOVER,
|
|
15
17
|
STATE_LAYER_PRESS,
|
|
16
18
|
withDefaultStateLayer,
|
|
@@ -19,10 +21,13 @@ import { defaultKnobs } from "./knobs";
|
|
|
19
21
|
import { resolveKnobs } from "./resolveKnobs";
|
|
20
22
|
|
|
21
23
|
describe("focusState tokens (DG-A11Y-01 / DG-STATE-01 / W12)", () => {
|
|
22
|
-
it("FOCUS_VISIBLE_RING meets ≥2px solid outline contract", () => {
|
|
24
|
+
it("FOCUS_VISIBLE_RING meets ≥2px solid outline contract at offset 0 (F1)", () => {
|
|
23
25
|
expect(FOCUS_VISIBLE_RING.outlineWidth).toBeGreaterThanOrEqual(FOCUS_RING_MIN_WIDTH);
|
|
24
26
|
expect(FOCUS_VISIBLE_RING.outlineStyle).toBe("solid");
|
|
25
27
|
expect(FOCUS_VISIBLE_RING.outlineColor).toBe("$outlineColor");
|
|
28
|
+
// Board verify pass F1: offset 0 — not −2 (band inside the fill, where it
|
|
29
|
+
// measured 1.17–1.31:1 on solid intents) and not +2 (a gapped halo).
|
|
30
|
+
expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(0);
|
|
26
31
|
expect(FOCUS_RING_MIN_CONTRAST).toBeGreaterThanOrEqual(3);
|
|
27
32
|
});
|
|
28
33
|
|
|
@@ -107,6 +112,119 @@ describe("$outlineColor token contrast (DF-03 / DG-A11Y-01)", () => {
|
|
|
107
112
|
expect(Math.min(c1, c2)).toBeGreaterThanOrEqual(FOCUS_RING_MIN_CONTRAST);
|
|
108
113
|
});
|
|
109
114
|
}
|
|
115
|
+
|
|
116
|
+
// MPO-41 — assert the RATIO on the surface the band actually draws on, not
|
|
117
|
+
// just the declaration. At offset 0 the band paints OUTSIDE the border box,
|
|
118
|
+
// on the page ground behind the control; intent Buttons pin the parent-scope
|
|
119
|
+
// `$outlineColor` literal on the ring (LC-51), so the pair that must clear
|
|
120
|
+
// the floor is base `outlineColor` × base ground — for accent and error
|
|
121
|
+
// controls alike, in both schemes.
|
|
122
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
123
|
+
for (const intent of ["accent", "error"] as const) {
|
|
124
|
+
it(`${scheme} ${intent}: offset-0 ring clears ≥3:1 on the page ground it draws on`, () => {
|
|
125
|
+
const base = themes[scheme];
|
|
126
|
+
expect(themes[`${scheme}_${intent}`]).toBeTruthy();
|
|
127
|
+
const ring = normalizeToHex(base.outlineColor);
|
|
128
|
+
const ground = normalizeToHex(base.color1);
|
|
129
|
+
expect(ring).toBeTruthy();
|
|
130
|
+
expect(ground).toBeTruthy();
|
|
131
|
+
expect(
|
|
132
|
+
contrastRatio(relativeLuminance(ring!), relativeLuminance(ground!)),
|
|
133
|
+
).toBeGreaterThanOrEqual(FOCUS_RING_MIN_CONTRAST);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
it("the retired flat inset failed the floor over the solid intent fills (why offset is 0)", () => {
|
|
139
|
+
// The failures that forced the flip (MPO-41 / MPO-19 X7): a −2 band paints
|
|
140
|
+
// INSIDE the control, so it had to contrast against the solid intent fill.
|
|
141
|
+
//
|
|
142
|
+
// Provenance, because the two numbers are easy to conflate. The headline
|
|
143
|
+
// figures — light accent 1.27:1, light error 1.17:1, dark error 1.31:1 —
|
|
144
|
+
// are RENDERED measurements off the action-and-feedback board at DPR 2,
|
|
145
|
+
// ring node against the Button's painted background. This test computes a
|
|
146
|
+
// DIFFERENT pair: the base `outlineColor` against the intent sub-theme's
|
|
147
|
+
// `color9` ink step (1.67 / 1.55 / 1.44 in this build). Same verdict, same
|
|
148
|
+
// reason, different surfaces — the board's error fills (#ce2c31 / #ff9794)
|
|
149
|
+
// are not in these default sub-theme ramps at all, so the fill side is the
|
|
150
|
+
// board's to own and not this package's.
|
|
151
|
+
//
|
|
152
|
+
// What this assertion IS: no sub-theme ink step the inset band could land
|
|
153
|
+
// on clears the floor, which keeps the offset-0 geometry load-bearing. If
|
|
154
|
+
// the ramps ever lighten past 3:1 this test goes stale, not wrong.
|
|
155
|
+
for (const [scheme, intent] of [
|
|
156
|
+
["light", "accent"],
|
|
157
|
+
["light", "error"],
|
|
158
|
+
["dark", "error"],
|
|
159
|
+
] as const) {
|
|
160
|
+
const ring = normalizeToHex(themes[scheme].outlineColor);
|
|
161
|
+
const fill = normalizeToHex(themes[`${scheme}_${intent}`].color9);
|
|
162
|
+
expect(ring).toBeTruthy();
|
|
163
|
+
expect(fill).toBeTruthy();
|
|
164
|
+
expect(contrastRatio(relativeLuminance(ring!), relativeLuminance(fill!))).toBeLessThan(
|
|
165
|
+
FOCUS_RING_MIN_CONTRAST,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("the ring still paints inside a clipped / fused group (LC-71 §4 inset carve-out)", () => {
|
|
172
|
+
it("FOCUS_VISIBLE_RING_INSET is the same ring pulled fully inside the border box", () => {
|
|
173
|
+
expect(FOCUS_VISIBLE_RING_INSET).toMatchObject({
|
|
174
|
+
outlineWidth: FOCUS_RING_MIN_WIDTH,
|
|
175
|
+
outlineStyle: "solid",
|
|
176
|
+
outlineColor: "$outlineColor",
|
|
177
|
+
outlineOffset: -2,
|
|
178
|
+
});
|
|
179
|
+
// The band spans [outlineOffset, outlineOffset + outlineWidth] from the
|
|
180
|
+
// border edge. Outer edge ≤ 0 means a clipping ancestor (fused group
|
|
181
|
+
// clipped at the group radius, menu overlay, table frame — LC-72
|
|
182
|
+
// CONTAINER-CLIP) cannot sever any part of it.
|
|
183
|
+
expect(
|
|
184
|
+
FOCUS_VISIBLE_RING_INSET.outlineOffset + FOCUS_VISIBLE_RING_INSET.outlineWidth,
|
|
185
|
+
).toBeLessThanOrEqual(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("keyboardFocusRingProps — the manual ring for roving-focus composites — pins the inset", () => {
|
|
189
|
+
expect(keyboardFocusRingProps).toEqual(FOCUS_VISIBLE_RING_INSET);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("ensureFocusVisibleRing honors the explicit per-call-site inset and defaults to 0", () => {
|
|
193
|
+
expect(ensureFocusVisibleRing({ outlineOffset: -2 }).outlineOffset).toBe(-2);
|
|
194
|
+
expect(ensureFocusVisibleRing({ outlineWidth: 3 }).outlineOffset).toBe(0);
|
|
195
|
+
expect(ensureFocusVisibleRing().outlineOffset).toBe(0);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("LC-71: keyboard-only; measure the ring / text node, not the frame", () => {
|
|
200
|
+
it("standalone ring paints a 2px band on the ring-carrying node's box at offset 0", () => {
|
|
201
|
+
// Measure the node that paints the outline (the ring carrier / text node),
|
|
202
|
+
// never an ancestor Frame. Band = [offset, offset + width] from THAT box.
|
|
203
|
+
const { outlineOffset, outlineWidth } = FOCUS_VISIBLE_RING;
|
|
204
|
+
expect(outlineWidth).toBe(2);
|
|
205
|
+
expect(outlineOffset).toBe(0);
|
|
206
|
+
expect(outlineOffset + outlineWidth).toBe(2);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("chip dismiss rings the glyph node, not the 44px hit-box frame", () => {
|
|
210
|
+
const frameRule = compositeFocusRingCss.match(/\.mp-chip-dismiss:focus-visible\s*\{([^}]+)\}/);
|
|
211
|
+
const ringRule = compositeFocusRingCss.match(
|
|
212
|
+
/\.mp-chip-dismiss:focus-visible \.mp-chip-dismiss-ring\s*\{([^}]+)\}/,
|
|
213
|
+
);
|
|
214
|
+
expect(frameRule?.[1]).toMatch(/outline:\s*none/);
|
|
215
|
+
expect(ringRule?.[1]).toMatch(/outline:\s*2px\s+solid/);
|
|
216
|
+
expect(ringRule?.[1]).toMatch(/outline-offset:\s*0/);
|
|
217
|
+
expect(ringRule?.[1]).not.toMatch(/outline-offset:\s*2px/);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("non-text composites ring on :focus-visible only; text-entry keeps the any-focus carve-out", () => {
|
|
221
|
+
expect(compositeFocusRingCss).toMatch(/\.mp-composite-ring:focus-visible/);
|
|
222
|
+
expect(compositeFocusRingCss).toMatch(/\.mp-composite-ring-deep:has\(:focus-visible\)/);
|
|
223
|
+
expect(compositeFocusRingCss).toMatch(/\.mp-chip-dismiss:focus-visible/);
|
|
224
|
+
// Text-entry carve-out (LC-71 §1a): focusing an input means typing.
|
|
225
|
+
expect(compositeFocusRingCss).toMatch(/:has\(input:focus\)/);
|
|
226
|
+
expect(compositeFocusRingCss).toMatch(/:has\(textarea:focus\)/);
|
|
227
|
+
});
|
|
110
228
|
});
|
|
111
229
|
|
|
112
230
|
describe("resolveKnobs control recipes (W12)", () => {
|
package/src/theme/focusState.ts
CHANGED
|
@@ -47,18 +47,43 @@ export function focusRingColorMeetsContrast(ringColor: string, surfaceColor: str
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* ONE ring geometry (DG-A11Y-01): 2px solid `$outlineColor
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
50
|
+
* ONE ring geometry (DG-A11Y-01 / LC-71 §4): 2px solid `$outlineColor` at
|
|
51
|
+
* offset **0** — the band hugs the border box and paints OUTSIDE it, on the
|
|
52
|
+
* surface behind the control (reference §10; board F1 verify pass: offset 0,
|
|
53
|
+
* not −2 and not +2). Offset 0 is what makes the ≥3:1 floor computable: the
|
|
54
|
+
* ring contrasts against the page ground, not the control's own fill. The
|
|
55
|
+
* old flat inset (−2) painted the band INSIDE solid intent fills and sank to
|
|
56
|
+
* 1.17–1.31:1 (light accent 1.27, light error 1.17, dark error 1.31) where
|
|
57
|
+
* the same ring on the page ground clears 5.78–6.50:1 (MPO-41 / MPO-19 X7).
|
|
58
|
+
*
|
|
59
|
+
* The inset did not die — it moved to `FOCUS_VISIBLE_RING_INSET`, an
|
|
60
|
+
* explicit per-call-site carve-out for clipping ancestors. MPO-19 X7 names
|
|
61
|
+
* the same offset `FOCUS_RING_CLIPPED_OFFSET` so call sites do not type `-2`.
|
|
56
62
|
*/
|
|
63
|
+
export const FOCUS_RING_CLIPPED_OFFSET = -2;
|
|
57
64
|
export const FOCUS_VISIBLE_RING = {
|
|
58
65
|
outlineWidth: FOCUS_RING_MIN_WIDTH,
|
|
59
66
|
outlineStyle: "solid" as const,
|
|
60
67
|
outlineColor: "$outlineColor",
|
|
61
|
-
outlineOffset:
|
|
68
|
+
outlineOffset: 0,
|
|
69
|
+
} satisfies ControlStateProps;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The LC-71 §4 clipping carve-out: the SAME ring, pulled fully inside the
|
|
73
|
+
* border box (offset −2 + width 2 ⇒ the band spans [−2, 0] from the edge, so
|
|
74
|
+
* no clipping ancestor can sever it). ONLY for items inside `overflow`
|
|
75
|
+
* clipping — fused/segmented groups clipped at the group radius, edge-to-edge
|
|
76
|
+
* menu rows under a clipped overlay (LC-72 CONTAINER-CLIP), table cells.
|
|
77
|
+
* Standalone controls take `FOCUS_VISIBLE_RING`; call sites opt into the
|
|
78
|
+
* inset explicitly (this constant, or
|
|
79
|
+
* `ensureFocusVisibleRing({ outlineOffset: -2 })`). Verification measures the
|
|
80
|
+
* painted composition, never the declared offset alone — a ring box inset
|
|
81
|
+
* from the boundary reports a compliant offset while painting inside it
|
|
82
|
+
* (the F1 pseudoInset trap, mpo-fields-measured.md §7).
|
|
83
|
+
*/
|
|
84
|
+
export const FOCUS_VISIBLE_RING_INSET = {
|
|
85
|
+
...FOCUS_VISIBLE_RING,
|
|
86
|
+
outlineOffset: FOCUS_RING_CLIPPED_OFFSET,
|
|
62
87
|
} satisfies ControlStateProps;
|
|
63
88
|
|
|
64
89
|
/**
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { FontCategory } from "./knobs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-category legibility floors (LC-11 FONT-FAMILY-METRICS).
|
|
5
|
+
*
|
|
6
|
+
* `heading` / `body` are minimum lineHeight:fontSize ratios. `letterSpacing:
|
|
7
|
+
* "neutral"` marks the families where Inter's tracking-by-size curve is
|
|
8
|
+
* actively wrong — fixed-pitch faces must keep their pitch, and connected
|
|
9
|
+
* scripts break when tracked apart.
|
|
10
|
+
*
|
|
11
|
+
* Two consumers read this table and they must never drift apart:
|
|
12
|
+
*
|
|
13
|
+
* - `FontKnobStyles` emits `--f-lineHeight-N` overrides on the `.font_body`
|
|
14
|
+
* / `.font_heading` CSS scopes, which is how the knob restyles text that
|
|
15
|
+
* never asked for a category token (web only).
|
|
16
|
+
* - `defaults/fonts.ts` bakes the same floors into each registered category
|
|
17
|
+
* font's own lineHeight table, which is the path the knob actually takes:
|
|
18
|
+
* `bodyFont: "serif"` renders `.font_serif`, and the CSS scope rules above
|
|
19
|
+
* are keyed to `.font_body`, so they never reach it. Native has no CSS at
|
|
20
|
+
* all and depends on the table entirely.
|
|
21
|
+
*
|
|
22
|
+
* "sans-serif" is the default scope and is listed for completeness — the
|
|
23
|
+
* `$body` / `$heading` tables are upstream's under the v5 era pin (MPO-19
|
|
24
|
+
* X8/X9) and this file does not rewrite them.
|
|
25
|
+
*/
|
|
26
|
+
export const fontCategoryMetrics: Record<
|
|
27
|
+
FontCategory,
|
|
28
|
+
{ heading: number; body: number; letterSpacing: "inherit" | "neutral" }
|
|
29
|
+
> = {
|
|
30
|
+
"sans-serif": { heading: 1.15, body: 1.4, letterSpacing: "inherit" },
|
|
31
|
+
serif: { heading: 1.2, body: 1.4, letterSpacing: "inherit" },
|
|
32
|
+
mono: { heading: 1.25, body: 1.45, letterSpacing: "neutral" },
|
|
33
|
+
// Heading floors below are sized to each configured face's em box
|
|
34
|
+
// (ascent+descent) so display headings never clip (LC-11): Roboto Slab
|
|
35
|
+
// ≈1.32, Nunito ≈1.36, Silkscreen ≈1.3, Poppins/Futura ≈1.4.
|
|
36
|
+
slab: { heading: 1.35, body: 1.4, letterSpacing: "inherit" },
|
|
37
|
+
rounded: { heading: 1.4, body: 1.4, letterSpacing: "inherit" },
|
|
38
|
+
condensed: { heading: 1.15, body: 1.4, letterSpacing: "inherit" },
|
|
39
|
+
cursive: { heading: 1.3, body: 1.5, letterSpacing: "neutral" },
|
|
40
|
+
handwriting: { heading: 1.25, body: 1.45, letterSpacing: "neutral" },
|
|
41
|
+
pixel: { heading: 1.35, body: 1.4, letterSpacing: "neutral" },
|
|
42
|
+
blackletter: { heading: 1.3, body: 1.45, letterSpacing: "neutral" },
|
|
43
|
+
geometric: { heading: 1.45, body: 1.4, letterSpacing: "inherit" },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Leading for one size step: the era's own value, raised to the category's
|
|
48
|
+
* floor when the era's taper falls under it. Never below the base, so a
|
|
49
|
+
* category can only ever ask for MORE room than `$body` takes, never less.
|
|
50
|
+
*/
|
|
51
|
+
export function flooredLineHeight(
|
|
52
|
+
sizePx: number,
|
|
53
|
+
baseLineHeightPx: number,
|
|
54
|
+
minRatio: number,
|
|
55
|
+
): number {
|
|
56
|
+
return Math.max(baseLineHeightPx, Math.round(sizePx * minRatio));
|
|
57
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FontCategory } from "./knobs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one concrete family stack per headingFont/bodyFont category.
|
|
5
|
+
*
|
|
6
|
+
* Both paths read THIS table, so a stop cannot render one face through the
|
|
7
|
+
* registered font and another through the CSS fallback:
|
|
8
|
+
*
|
|
9
|
+
* - `defaults/fonts.ts` builds the registered `$serif` / `$mono` / … fonts
|
|
10
|
+
* from it (the path the knob takes on every platform).
|
|
11
|
+
* - `FontKnobStyles` falls back to it for `--f-family` when a consumer's
|
|
12
|
+
* config has no font registered for the category (live SHC console had
|
|
13
|
+
* the style tag and skipped the family).
|
|
14
|
+
*
|
|
15
|
+
* Distinct FIRST families throughout, because a text-node measurement reads
|
|
16
|
+
* the first resolvable name — three stops that all begin at a system
|
|
17
|
+
* fallback is the MPO-44 defect.
|
|
18
|
+
*
|
|
19
|
+
* Tracks `packages/config/fonts.ts` (serif/mono/geometric are system aliases,
|
|
20
|
+
* the rest are shipped webfonts). `mono` is the one deliberate widening: the
|
|
21
|
+
* template's bare `monospace` keyword leaves the face to the UA, so this
|
|
22
|
+
* names the platform faces ahead of it and keeps the keyword last.
|
|
23
|
+
*/
|
|
24
|
+
export const fontCategoryStacks: Record<FontCategory, string> = {
|
|
25
|
+
"sans-serif":
|
|
26
|
+
'Inter, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
27
|
+
serif: "Georgia, 'Times New Roman', Times, serif",
|
|
28
|
+
mono: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
29
|
+
slab: "'Roboto Slab', 'Rockwell', 'Courier New', serif",
|
|
30
|
+
rounded: "'Nunito', 'Varela Round', system-ui, sans-serif",
|
|
31
|
+
condensed: "'Roboto Condensed', 'Arial Narrow', sans-serif",
|
|
32
|
+
cursive: "'Dancing Script', 'Pacifico', cursive",
|
|
33
|
+
handwriting: "'Caveat', 'Patrick Hand', 'Comic Sans MS', cursive",
|
|
34
|
+
pixel: "'Silkscreen', 'Press Start 2P', monospace",
|
|
35
|
+
blackletter: "'UnifrakturCook', 'Fraktur', serif",
|
|
36
|
+
geometric: "'Futura', 'Poppins', 'Century Gothic', sans-serif",
|
|
37
|
+
};
|
|
@@ -55,9 +55,22 @@ describe("buildFontKnobCss", () => {
|
|
|
55
55
|
expect(css).toContain(":root .font_body { --f-family: monospace !important");
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it("
|
|
58
|
+
it("emits a fallback --f-family when serif is not registered in Tamagui (live SHC)", () => {
|
|
59
|
+
// Live console 2026-08-29: #mp-font-knob-styles was present but had no
|
|
60
|
+
// --f-family because SHC never registers a `serif` font. The knob still
|
|
61
|
+
// has to write a family, or headingFont/bodyFont are a no-op on painted
|
|
62
|
+
// $heading/$body text. Georgia is a system face — no webfont required.
|
|
63
|
+
const css = buildFontKnobCss("serif", "sans-serif", resolveFamily, noTables);
|
|
64
|
+
expect(css).toContain(
|
|
65
|
+
":root .font_heading { --f-family: Georgia, 'Times New Roman', Times, serif !important; }",
|
|
66
|
+
);
|
|
67
|
+
expect(css).toContain(":where(:root .font_heading) { line-height:");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("emits a fallback --f-family for other unregistered categories and keeps the category leading", () => {
|
|
59
71
|
const css = buildFontKnobCss("cursive", "cursive", resolveFamily, noTables);
|
|
60
|
-
expect(css).
|
|
72
|
+
expect(css).toContain(":root .font_heading { --f-family:");
|
|
73
|
+
expect(css).toContain("cursive !important");
|
|
61
74
|
// cursive heading floor 1.3 beats the 1.25 minimum; body max(1.5, 1.5)
|
|
62
75
|
expect(css).toContain(":where(:root .font_heading) { line-height: 1.3; }");
|
|
63
76
|
expect(css).toContain(":where(:root .font_body) { line-height: 1.5; }");
|