@multiplatform.one/theme 7.46.0 → 7.47.1
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 +33 -0
- package/expo.cjs +3 -0
- package/package.json +22 -6
- package/src/audit/constraintAudit.ts +2 -1
- package/src/audit/themeMatrix.ts +1 -0
- package/src/expo.spec.ts +83 -0
- package/src/expo.ts +57 -0
- package/src/expoConfig.spec.ts +67 -0
- package/src/figma/figmaTokens.spec.ts +4 -3
- package/src/theme/FontKnobStyles.tsx +13 -1
- package/src/theme/Heading.tsx +70 -0
- package/src/theme/Intent.stories.tsx +2 -2
- package/src/theme/Preset.stories.tsx +2 -2
- package/src/theme/Preset.tsx +4 -2
- package/src/theme/Surface.stories.tsx +2 -1
- package/src/theme/Surface.tsx +4 -2
- package/src/theme/Text.tsx +152 -0
- package/src/theme/ThemeProvider.stories.tsx +2 -1
- package/src/theme/Tint.stories.tsx +2 -1
- package/src/theme/accentFloor.spec.ts +5 -3
- package/src/theme/accentOnSurface.palette.spec.tsx +75 -0
- package/src/theme/accentOnSurface.spec.tsx +149 -0
- package/src/theme/colorThemeAccent.spec.ts +5 -2
- package/src/theme/cookies.spec.ts +2 -2
- package/src/theme/createDefaultThemeConfig.spec.ts +2 -2
- package/src/theme/createThemes.ts +91 -11
- package/src/theme/defaults/builderOptions.ts +18 -0
- package/src/theme/devtools/ColorLineVisualizer.tsx +3 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +2 -0
- package/src/theme/hairline.native.ts +3 -0
- package/src/theme/hairline.ts +9 -0
- package/src/theme/hairlineToken.spec.ts +110 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/knobs.ts +2 -2
- package/src/theme/mpo19Proof.spec.ts +4 -1
- package/src/theme/overlaySurfaces.tsx +9 -3
- package/src/theme/pageGutter.tsx +61 -0
- package/src/theme/preset.spec.tsx +29 -1
- package/src/theme/presets.spec.ts +1 -1
- package/src/theme/presets.ts +2 -2
- package/src/theme/ramps.spec.ts +53 -0
- package/src/theme/ramps.ts +75 -0
- package/src/theme/readableColor.ts +111 -70
- package/src/theme/recipes.ts +51 -2
- package/src/theme/resolveFontTokenFamily.spec.ts +44 -0
- package/src/theme/resolveKnobs.spec.ts +30 -20
- package/src/theme/resolveKnobs.ts +87 -20
- package/src/theme/selectedState.spec.ts +127 -0
- package/types/audit/constraintAudit.d.ts.map +1 -1
- package/types/audit/themeMatrix.d.ts.map +1 -1
- package/types/expo.d.ts +29 -0
- package/types/expo.d.ts.map +1 -0
- package/types/theme/FontKnobStyles.d.ts +7 -0
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Heading.d.ts +10 -0
- package/types/theme/Heading.d.ts.map +1 -0
- package/types/theme/Preset.d.ts.map +1 -1
- package/types/theme/Surface.d.ts +2 -1
- package/types/theme/Surface.d.ts.map +1 -1
- package/types/theme/Text.d.ts +13 -0
- package/types/theme/Text.d.ts.map +1 -0
- package/types/theme/colorRules.d.ts +1 -1
- package/types/theme/createThemes.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/fonts.d.ts +30 -30
- package/types/theme/devtools/ColorLineVisualizer.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/hairline.d.ts +8 -0
- package/types/theme/hairline.d.ts.map +1 -1
- package/types/theme/hairline.native.d.ts +2 -0
- package/types/theme/hairline.native.d.ts.map +1 -1
- package/types/theme/index.d.ts +4 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +1 -1
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/overlaySurfaces.d.ts.map +1 -1
- package/types/theme/pageGutter.d.ts +41 -0
- package/types/theme/pageGutter.d.ts.map +1 -0
- package/types/theme/ramps.d.ts +20 -0
- package/types/theme/ramps.d.ts.map +1 -0
- package/types/theme/readableColor.d.ts +24 -12
- package/types/theme/readableColor.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +44 -4
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +10 -4
- package/types/theme/resolveKnobs.d.ts.map +1 -1
|
@@ -97,45 +97,6 @@ export function useLegibleInkOn(fill_: string | undefined, preferred: string): s
|
|
|
97
97
|
}, [fill, preferred, preferredValue, paper, ink]);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
/**
|
|
101
|
-
* Accent-hued text/link color readable on the page surfaces (color1/color2).
|
|
102
|
-
* Scans the theme's own accent ramp for the most chromatic step that clears
|
|
103
|
-
* the AA text floor against BOTH page surfaces; falls back to readable neutral
|
|
104
|
-
* ink (`$color12`) when no accent step qualifies or the ramp is absent (e.g.
|
|
105
|
-
* tint sub-themes, where neutral text is the house default anyway).
|
|
106
|
-
*/
|
|
107
|
-
export function useAccentOnSurface(): string {
|
|
108
|
-
const theme = useTamaguiTheme() as unknown as Record<string, { val?: unknown } | undefined>;
|
|
109
|
-
const c1 = resolveTokenValue(theme, "color1");
|
|
110
|
-
const c2 = resolveTokenValue(theme, "color2");
|
|
111
|
-
const ink = resolveTokenValue(theme, "color12") ?? "$color12";
|
|
112
|
-
const steps: (string | undefined)[] = [];
|
|
113
|
-
for (let i = 1; i <= 12; i++) steps.push(resolveTokenValue(theme, `accent${i}`));
|
|
114
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
115
|
-
return useMemo(() => {
|
|
116
|
-
const bg1 = c1 ? normalizeToHex(c1) : null;
|
|
117
|
-
const bg2 = c2 ? normalizeToHex(c2) : null;
|
|
118
|
-
if (!bg1 || !bg2) return ink;
|
|
119
|
-
const l1 = relativeLuminance(bg1);
|
|
120
|
-
const l2 = relativeLuminance(bg2);
|
|
121
|
-
let best: string | undefined;
|
|
122
|
-
let bestMin = Number.POSITIVE_INFINITY;
|
|
123
|
-
for (const step of steps) {
|
|
124
|
-
const hex = step ? normalizeToHex(step) : null;
|
|
125
|
-
if (!step || !hex) continue;
|
|
126
|
-
const stepLum = relativeLuminance(hex);
|
|
127
|
-
const min = Math.min(contrastRatio(stepLum, l1), contrastRatio(stepLum, l2));
|
|
128
|
-
// Most chromatic passing step = the lowest contrast that still clears AA
|
|
129
|
-
// (accent steps get paler/whiter as contrast climbs).
|
|
130
|
-
if (min >= aaTextContrastRatio && min < bestMin) {
|
|
131
|
-
bestMin = min;
|
|
132
|
-
best = step;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return best ?? ink;
|
|
136
|
-
}, [c1, c2, ink, steps.join("|")]);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
100
|
/** Below this sRGB chroma a step reads as grey, not as a hue. */
|
|
140
101
|
const IDENTITY_CHROMA_FLOOR = 0.1;
|
|
141
102
|
|
|
@@ -144,25 +105,22 @@ function chromaOf(hex: string): number {
|
|
|
144
105
|
return Math.max(...rgb) - Math.min(...rgb);
|
|
145
106
|
}
|
|
146
107
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
* candidate that clears AA text contrast on every surface, else `ink`.
|
|
150
|
-
*/
|
|
151
|
-
export function resolveIdentityInk({
|
|
152
|
-
candidates,
|
|
153
|
-
surfaces,
|
|
154
|
-
ink,
|
|
155
|
-
}: {
|
|
156
|
-
candidates: readonly (string | undefined)[];
|
|
157
|
-
surfaces: readonly (string | undefined)[];
|
|
158
|
-
ink: string;
|
|
159
|
-
}): string {
|
|
160
|
-
const surfaceLums: number[] = [];
|
|
108
|
+
function surfaceLuminances(surfaces: readonly (string | undefined)[]): number[] | null {
|
|
109
|
+
const lums: number[] = [];
|
|
161
110
|
for (const surface of surfaces) {
|
|
162
111
|
const hex = surface ? normalizeToHex(surface) : null;
|
|
163
|
-
if (!hex) return
|
|
164
|
-
|
|
112
|
+
if (!hex) return null;
|
|
113
|
+
lums.push(relativeLuminance(hex));
|
|
165
114
|
}
|
|
115
|
+
return lums;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function pickIdentityInk(
|
|
119
|
+
candidates: readonly (string | undefined)[],
|
|
120
|
+
surfaces: readonly (string | undefined)[],
|
|
121
|
+
): string | undefined {
|
|
122
|
+
const surfaceLums = surfaceLuminances(surfaces);
|
|
123
|
+
if (!surfaceLums) return undefined;
|
|
166
124
|
let best: string | undefined;
|
|
167
125
|
let bestChroma = IDENTITY_CHROMA_FLOOR;
|
|
168
126
|
for (const candidate of candidates) {
|
|
@@ -176,31 +134,114 @@ export function resolveIdentityInk({
|
|
|
176
134
|
best = candidate;
|
|
177
135
|
}
|
|
178
136
|
}
|
|
137
|
+
return best;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Pure resolver behind {@link useIdentityInkOnSurface}: the most chromatic
|
|
142
|
+
* candidate that clears AA text contrast on every surface, else `ink`.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveIdentityInk({
|
|
145
|
+
candidates,
|
|
146
|
+
surfaces,
|
|
147
|
+
ink,
|
|
148
|
+
}: {
|
|
149
|
+
candidates: readonly (string | undefined)[];
|
|
150
|
+
surfaces: readonly (string | undefined)[];
|
|
151
|
+
ink: string;
|
|
152
|
+
}): string {
|
|
153
|
+
return pickIdentityInk(candidates, surfaces) ?? ink;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Pure resolver behind {@link useAccentOnSurface}. The identity hue first
|
|
158
|
+
* (`identity`, see {@link useIdentityInkOnSurface}), so a colour theme's
|
|
159
|
+
* accent ink is that colour. With no hue, the accent ramp's least-contrast
|
|
160
|
+
* step that still clears AA on every surface, else `ink`.
|
|
161
|
+
*/
|
|
162
|
+
export function resolveAccentOnSurface({
|
|
163
|
+
identity,
|
|
164
|
+
accentSteps,
|
|
165
|
+
surfaces,
|
|
166
|
+
ink,
|
|
167
|
+
}: {
|
|
168
|
+
identity: readonly (string | undefined)[];
|
|
169
|
+
accentSteps: readonly (string | undefined)[];
|
|
170
|
+
surfaces: readonly (string | undefined)[];
|
|
171
|
+
ink: string;
|
|
172
|
+
}): string {
|
|
173
|
+
const hued = pickIdentityInk(identity, surfaces);
|
|
174
|
+
if (hued) return hued;
|
|
175
|
+
const surfaceLums = surfaceLuminances(surfaces);
|
|
176
|
+
if (!surfaceLums) return ink;
|
|
177
|
+
let best: string | undefined;
|
|
178
|
+
let bestMin = Number.POSITIVE_INFINITY;
|
|
179
|
+
for (const step of accentSteps) {
|
|
180
|
+
const hex = step ? normalizeToHex(step) : null;
|
|
181
|
+
if (!step || !hex) continue;
|
|
182
|
+
const stepLum = relativeLuminance(hex);
|
|
183
|
+
const min = Math.min(...surfaceLums.map((surface) => contrastRatio(stepLum, surface)));
|
|
184
|
+
if (min >= aaTextContrastRatio && min < bestMin) {
|
|
185
|
+
bestMin = min;
|
|
186
|
+
best = step;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
179
189
|
return best ?? ink;
|
|
180
190
|
}
|
|
181
191
|
|
|
192
|
+
const accentStepKeys = Array.from({ length: 12 }, (_, index) => `accent${index + 1}`);
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The live theme's identity candidates and page surfaces. Under a tint
|
|
196
|
+
* (`dark_orange`) the hue is the tint's own ramp, whose text tiers the build
|
|
197
|
+
* hands back to neutral, so the candidates are its `color8`..`color10` plus
|
|
198
|
+
* the raw hue's steps 11 and 12; otherwise the accent ramp.
|
|
199
|
+
*/
|
|
200
|
+
function useIdentityInputs() {
|
|
201
|
+
const themeName = (useThemeName() as string | undefined) ?? "";
|
|
202
|
+
const theme = useTamaguiTheme() as unknown as Record<string, { val?: unknown } | undefined>;
|
|
203
|
+
const hue = themeName.split("_")[1];
|
|
204
|
+
const isTint = hue !== undefined && tintHueNames.has(hue as never);
|
|
205
|
+
const keys = isTint ? ["color8", "color9", "color10", `${hue}11`, `${hue}12`] : accentStepKeys;
|
|
206
|
+
return {
|
|
207
|
+
candidates: keys.map((key) => resolveTokenValue(theme, key)),
|
|
208
|
+
accentSteps: accentStepKeys.map((key) => resolveTokenValue(theme, key)),
|
|
209
|
+
c1: resolveTokenValue(theme, "color1"),
|
|
210
|
+
c2: resolveTokenValue(theme, "color2"),
|
|
211
|
+
ink: resolveTokenValue(theme, "color12") ?? "$color12",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Accent-hued text/link ink readable on the page surfaces (color1/color2),
|
|
217
|
+
* shared by links, Toast, DatePicker, table filters, Button and every other
|
|
218
|
+
* accent-ink consumer. Under a colour theme (a tint or a hue accent) it is
|
|
219
|
+
* the theme's identity hue, the same pick as {@link useIdentityInkOnSurface}:
|
|
220
|
+
* a tint leaves the accent ramp neutral, so reading the ramp alone painted
|
|
221
|
+
* grey there (FZ-7783, LC-123). With no hue it keeps the accent ramp's least-
|
|
222
|
+
* contrast step that clears AA, and readable neutral ink (`$color12`) when
|
|
223
|
+
* none does.
|
|
224
|
+
*/
|
|
225
|
+
export function useAccentOnSurface(): string {
|
|
226
|
+
const { candidates, accentSteps, c1, c2, ink } = useIdentityInputs();
|
|
227
|
+
const candidateKey = candidates.join("|");
|
|
228
|
+
const stepKey = accentSteps.join("|");
|
|
229
|
+
return useMemo(
|
|
230
|
+
() => resolveAccentOnSurface({ identity: candidates, accentSteps, surfaces: [c1, c2], ink }),
|
|
231
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
232
|
+
[candidateKey, stepKey, c1, c2, ink],
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
182
236
|
/**
|
|
183
237
|
* The theme's identity hue as ink on the page surfaces (color1/color2): the
|
|
184
|
-
* most chromatic step that clears AA on both.
|
|
185
|
-
* the hue is the tint's own ramp, whose text tiers the build hands back to
|
|
186
|
-
* neutral, so the candidates are its `color8`..`color10` plus the raw hue's
|
|
187
|
-
* steps 11 and 12; otherwise the accent ramp. A theme with no hue (a neutral
|
|
238
|
+
* most chromatic step that clears AA on both. A theme with no hue (a neutral
|
|
188
239
|
* accent) returns the primary ink (`color12`): a mark that means "current"
|
|
189
240
|
* never reads quieter than the secondary ink beside it, which
|
|
190
241
|
* {@link useAccentOnSurface} does there.
|
|
191
242
|
*/
|
|
192
243
|
export function useIdentityInkOnSurface(): string {
|
|
193
|
-
const
|
|
194
|
-
const theme = useTamaguiTheme() as unknown as Record<string, { val?: unknown } | undefined>;
|
|
195
|
-
const hue = themeName.split("_")[1];
|
|
196
|
-
const isTint = hue !== undefined && tintHueNames.has(hue as never);
|
|
197
|
-
const keys = isTint
|
|
198
|
-
? ["color8", "color9", "color10", `${hue}11`, `${hue}12`]
|
|
199
|
-
: Array.from({ length: 12 }, (_, index) => `accent${index + 1}`);
|
|
200
|
-
const candidates = keys.map((key) => resolveTokenValue(theme, key));
|
|
201
|
-
const c1 = resolveTokenValue(theme, "color1");
|
|
202
|
-
const c2 = resolveTokenValue(theme, "color2");
|
|
203
|
-
const ink = resolveTokenValue(theme, "color12") ?? "$color12";
|
|
244
|
+
const { candidates, c1, c2, ink } = useIdentityInputs();
|
|
204
245
|
const candidateKey = candidates.join("|");
|
|
205
246
|
return useMemo(
|
|
206
247
|
() => resolveIdentityInk({ candidates, surfaces: [c1, c2], ink }),
|
package/src/theme/recipes.ts
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import type { MotionRole, MotionRoleName } from "./motion";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ColorTokens,
|
|
4
|
+
FontLetterSpacingTokens,
|
|
5
|
+
FontLineHeightTokens,
|
|
6
|
+
FontSizeTokens,
|
|
7
|
+
TransitionProp,
|
|
8
|
+
} from "tamagui";
|
|
3
9
|
import type { Knobs } from "./knobs";
|
|
4
10
|
import type { HitSlopInsets } from "./layoutTokens";
|
|
5
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The page-title size fragment: the D-11 step, and below `$sm` its compact
|
|
14
|
+
* type step (LC-143). The compact entry carries type geometry only, so a
|
|
15
|
+
* preset's or consumer's weight and colour still win on a phone.
|
|
16
|
+
*/
|
|
17
|
+
export type PageTitleFragment = {
|
|
18
|
+
size: FontSizeTokens;
|
|
19
|
+
"$max-sm"?: {
|
|
20
|
+
fontSize: FontSizeTokens;
|
|
21
|
+
lineHeight: FontLineHeightTokens;
|
|
22
|
+
letterSpacing: FontLetterSpacingTokens;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
6
26
|
// ── State knob prop types (by concern) ───────────────────────────────────
|
|
7
27
|
|
|
8
28
|
export interface ControlStateProps {
|
|
@@ -216,6 +236,12 @@ export interface KnobProps {
|
|
|
216
236
|
shadowRadius: number;
|
|
217
237
|
} & CornerSmoothingCarrier;
|
|
218
238
|
inputSurface: { borderWidth: number };
|
|
239
|
+
/**
|
|
240
|
+
* LC-148: the `borderWidth` stop the edges above draw (outlined + none is
|
|
241
|
+
* `small`). The widths alone can't name it: on web `small` and `medium` both
|
|
242
|
+
* draw 1px and differ only in colour.
|
|
243
|
+
*/
|
|
244
|
+
borderWidthStop: Knobs["borderWidth"];
|
|
219
245
|
/** Container inset role (SP-GAP): `$space.$4` of the active scale. */
|
|
220
246
|
panelPadding: { padding: string };
|
|
221
247
|
/** Tight role (SP-GAP): label to control, chip to chip, icon to text. `$space.$2`. */
|
|
@@ -293,7 +319,7 @@ export interface KnobProps {
|
|
|
293
319
|
* screens land on the moderate step; the `hero` preset (or an explicit
|
|
294
320
|
* `pageTitleScale` override) re-opens the display scale for marketing.
|
|
295
321
|
*/
|
|
296
|
-
pageTitle:
|
|
322
|
+
pageTitle: PageTitleFragment;
|
|
297
323
|
/** color is set only for solid semantic-intent Buttons (error/warning/success). */
|
|
298
324
|
body: { fontFamily: string; fontWeight: string; color?: ColorTokens };
|
|
299
325
|
typography: {
|
|
@@ -323,6 +349,27 @@ export interface KnobProps {
|
|
|
323
349
|
formAutofocus: string;
|
|
324
350
|
}
|
|
325
351
|
|
|
352
|
+
// ── Selected state recipe (LC-131 SELECTED-STATE) ────────────────────
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* LC-131: the one drawing of "selected" every choice part merges (Chip,
|
|
356
|
+
* ToggleGroup item, Tabs trigger, segmented control, option row, colour
|
|
357
|
+
* swatch): a soft accent tint behind accent text. Colour only, so selecting
|
|
358
|
+
* never resizes a part: the part keeps its own border, border width and weight,
|
|
359
|
+
* which leaves the accent border to focus (LC-133).
|
|
360
|
+
*/
|
|
361
|
+
export interface SelectedRecipe {
|
|
362
|
+
/** The part's frame: its fill takes the soft accent tint. */
|
|
363
|
+
surfaceKnobProps: { backgroundColor: string };
|
|
364
|
+
/** The part's label and glyphs. */
|
|
365
|
+
textKnobProps: { color: string };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** State recipes beyond the four interaction maps. */
|
|
369
|
+
export interface StateRecipes {
|
|
370
|
+
selected: SelectedRecipe;
|
|
371
|
+
}
|
|
372
|
+
|
|
326
373
|
// ── Return type ───────────────────────────────────────────────────────────
|
|
327
374
|
|
|
328
375
|
export interface ResolvedKnobs {
|
|
@@ -332,6 +379,8 @@ export interface ResolvedKnobs {
|
|
|
332
379
|
elevation: KnobRecipe<ElevationStateProps>;
|
|
333
380
|
/** LC-40: resolved disabled treatment (named to avoid `disabled` prop clashes). */
|
|
334
381
|
disabledState: DisabledRecipe;
|
|
382
|
+
/** LC-131: what a part merges while it is selected. */
|
|
383
|
+
state: StateRecipes;
|
|
335
384
|
}
|
|
336
385
|
|
|
337
386
|
export type KnobPropsOverride = (knobProps: KnobProps, knobs: Knobs) => Partial<KnobProps>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveFontTokenFamily (FZ-7742): a resolved font token names the stack the
|
|
3
|
+
* Tamagui config registers for it, so text no Tamagui font reaches (mermaid's
|
|
4
|
+
* SVG labels) can follow the bodyFont knob.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
const { config } = vi.hoisted(() => ({ config: { current: undefined as unknown } }));
|
|
9
|
+
|
|
10
|
+
vi.mock("@tamagui/web", async (importOriginal) => ({
|
|
11
|
+
...(await importOriginal<typeof import("@tamagui/web")>()),
|
|
12
|
+
getConfig: () => {
|
|
13
|
+
if (!config.current) throw new Error("no Tamagui config");
|
|
14
|
+
return config.current;
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { getConfiguredFontFamily, resolveFontTokenFamily } from "./FontKnobStyles";
|
|
19
|
+
|
|
20
|
+
describe("resolveFontTokenFamily", () => {
|
|
21
|
+
it("reads a parsed variable family, a plain family, and a config font by key", () => {
|
|
22
|
+
config.current = {
|
|
23
|
+
fontsParsed: {
|
|
24
|
+
$body: { family: { val: "Inter, sans-serif" } },
|
|
25
|
+
$serif: { family: "Georgia, serif" },
|
|
26
|
+
},
|
|
27
|
+
fonts: { slab: { family: "'Roboto Slab', serif" } },
|
|
28
|
+
};
|
|
29
|
+
expect(resolveFontTokenFamily("$body")).toBe("Inter, sans-serif");
|
|
30
|
+
expect(resolveFontTokenFamily("$serif")).toBe("Georgia, serif");
|
|
31
|
+
expect(resolveFontTokenFamily("serif")).toBe("Georgia, serif");
|
|
32
|
+
expect(resolveFontTokenFamily("$slab")).toBe("'Roboto Slab', serif");
|
|
33
|
+
expect(getConfiguredFontFamily("serif")).toBe("Georgia, serif");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("is undefined for a token the config lacks, a non-string, and no runtime", () => {
|
|
37
|
+
config.current = { fontsParsed: {}, fonts: {} };
|
|
38
|
+
expect(resolveFontTokenFamily("$pixel")).toBeUndefined();
|
|
39
|
+
expect(resolveFontTokenFamily(undefined)).toBeUndefined();
|
|
40
|
+
expect(resolveFontTokenFamily("")).toBeUndefined();
|
|
41
|
+
config.current = undefined;
|
|
42
|
+
expect(resolveFontTokenFamily("$body")).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
sizedElevation,
|
|
33
33
|
} from "./resolveKnobs";
|
|
34
34
|
import { getActiveScaleTokens, setActiveScaleTokens } from "./scale/activeScale";
|
|
35
|
+
import { hairlineBorderWidth } from "./hairline";
|
|
35
36
|
import { generateScale } from "./scale/generateScale";
|
|
36
37
|
import { defaultScaleInputs } from "./scale/scaleInputs";
|
|
37
38
|
import {
|
|
@@ -1001,19 +1002,19 @@ describe("resolveKnobs", () => {
|
|
|
1001
1002
|
expect(knobProps.inputSurface.borderWidth).toBe(0);
|
|
1002
1003
|
});
|
|
1003
1004
|
|
|
1004
|
-
it("outlined + none still draws the hairline (small stop
|
|
1005
|
+
it("outlined + none still draws the hairline (the small stop's token)", () => {
|
|
1005
1006
|
const { knobProps } = resolveKnobs(knobsWith({ fillStyle: "outlined", borderWidth: "none" }));
|
|
1006
|
-
expect(knobProps.borderRadius.borderWidth).toBe(
|
|
1007
|
-
expect(knobProps.surface.borderWidth).toBe(
|
|
1008
|
-
expect(knobProps.elevatedSurface.borderWidth).toBe(
|
|
1009
|
-
expect(knobProps.inputSurface.borderWidth).toBe(
|
|
1007
|
+
expect(knobProps.borderRadius.borderWidth).toBe(hairlineBorderWidth);
|
|
1008
|
+
expect(knobProps.surface.borderWidth).toBe(hairlineBorderWidth);
|
|
1009
|
+
expect(knobProps.elevatedSurface.borderWidth).toBe(hairlineBorderWidth);
|
|
1010
|
+
expect(knobProps.inputSurface.borderWidth).toBe(hairlineBorderWidth);
|
|
1010
1011
|
});
|
|
1011
1012
|
|
|
1012
|
-
it("outlined + small stays at the hairline, not a hidden
|
|
1013
|
+
it("outlined + small stays at the hairline token, not a hidden floor", () => {
|
|
1013
1014
|
const { knobProps } = resolveKnobs(
|
|
1014
1015
|
knobsWith({ fillStyle: "outlined", borderWidth: "small" }),
|
|
1015
1016
|
);
|
|
1016
|
-
expect(knobProps.borderRadius.borderWidth).toBe(
|
|
1017
|
+
expect(knobProps.borderRadius.borderWidth).toBe(hairlineBorderWidth);
|
|
1017
1018
|
});
|
|
1018
1019
|
|
|
1019
1020
|
it("outlined + medium/large keep the requested width", () => {
|
|
@@ -1124,31 +1125,40 @@ describe("resolveKnobs", () => {
|
|
|
1124
1125
|
// ── pageTitleScale (D-11 hero-H1 dial) ──────────────────────
|
|
1125
1126
|
|
|
1126
1127
|
describe("pageTitleScale knob", () => {
|
|
1128
|
+
const compact = (step: string) => ({ fontSize: step, lineHeight: step, letterSpacing: step });
|
|
1129
|
+
const moderateTitle = { size: "$10", "$max-sm": compact("$9") };
|
|
1130
|
+
const displayTitle = { size: "$12", "$max-sm": compact("$11") };
|
|
1131
|
+
|
|
1127
1132
|
it("defaults to the moderate page-title step ($10 = 40px)", () => {
|
|
1128
|
-
expect(resolveKnobs(defaultKnobs).knobProps.pageTitle).toEqual(
|
|
1133
|
+
expect(resolveKnobs(defaultKnobs).knobProps.pageTitle).toEqual(moderateTitle);
|
|
1129
1134
|
});
|
|
1130
1135
|
|
|
1131
1136
|
it('"display" re-opens the hero step ($12 = 52px)', () => {
|
|
1132
|
-
expect(resolveKnobs(knobsWith({ pageTitleScale: "display" })).knobProps.pageTitle).toEqual(
|
|
1133
|
-
|
|
1134
|
-
|
|
1137
|
+
expect(resolveKnobs(knobsWith({ pageTitleScale: "display" })).knobProps.pageTitle).toEqual(
|
|
1138
|
+
displayTitle,
|
|
1139
|
+
);
|
|
1135
1140
|
});
|
|
1136
1141
|
|
|
1137
1142
|
it("an omitted knob still resolves moderate (preset constructions predate the dial)", () => {
|
|
1138
1143
|
const { pageTitleScale: _omitted, ...withoutDial } = defaultKnobs;
|
|
1139
|
-
expect(resolveKnobs(withoutDial as Knobs).knobProps.pageTitle).toEqual(
|
|
1144
|
+
expect(resolveKnobs(withoutDial as Knobs).knobProps.pageTitle).toEqual(moderateTitle);
|
|
1140
1145
|
});
|
|
1141
1146
|
|
|
1142
1147
|
it("resolvePageTitleScale is the one table both channels read", () => {
|
|
1143
|
-
expect(resolvePageTitleScale("moderate")).toEqual(
|
|
1144
|
-
expect(resolvePageTitleScale("display")).toEqual(
|
|
1148
|
+
expect(resolvePageTitleScale("moderate")).toEqual(moderateTitle);
|
|
1149
|
+
expect(resolvePageTitleScale("display")).toEqual(displayTitle);
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
it("drops one token below $sm, for both steps, as type geometry only (LC-143)", () => {
|
|
1153
|
+
expect(resolvePageTitleScale("moderate")["$max-sm"]).toEqual(compact("$9"));
|
|
1154
|
+
expect(resolvePageTitleScale("display")["$max-sm"]).toEqual(compact("$11"));
|
|
1145
1155
|
});
|
|
1146
1156
|
|
|
1147
1157
|
it("product presets ship moderate; `hero` is the only display opt-in", () => {
|
|
1148
1158
|
expect(defaultPreset.knobs.pageTitleScale).toBe("moderate");
|
|
1149
1159
|
expect(boldPreset.knobs.pageTitleScale).toBe("moderate");
|
|
1150
1160
|
expect(heroPreset.knobs.pageTitleScale).toBe("display");
|
|
1151
|
-
expect(resolveKnobs(heroPreset.knobs).knobProps.pageTitle).toEqual(
|
|
1161
|
+
expect(resolveKnobs(heroPreset.knobs).knobProps.pageTitle).toEqual(displayTitle);
|
|
1152
1162
|
});
|
|
1153
1163
|
|
|
1154
1164
|
it("`hero` differs from the default preset in nothing but the title step", () => {
|
|
@@ -1239,7 +1249,7 @@ describe("house decision knobs", () => {
|
|
|
1239
1249
|
it("defaultKnobs use recommended house defaults", () => {
|
|
1240
1250
|
expect(defaultKnobs.fieldLabelPlacement).toBe(FieldLabelPlacement.Top);
|
|
1241
1251
|
expect(defaultKnobs.requiredMarking).toBe(RequiredMarking.Minority);
|
|
1242
|
-
expect(defaultKnobs.tableZebra).toBe(TableZebra.
|
|
1252
|
+
expect(defaultKnobs.tableZebra).toBe(TableZebra.On);
|
|
1243
1253
|
expect(defaultKnobs.bulkBarPlacement).toBe(BulkBarPlacement.Top);
|
|
1244
1254
|
expect(defaultKnobs.selectAllScope).toBe(SelectAllScope.Page);
|
|
1245
1255
|
expect(defaultKnobs.timestampStyle).toBe(TimestampStyle.Absolute);
|
|
@@ -1251,7 +1261,7 @@ describe("house decision knobs", () => {
|
|
|
1251
1261
|
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
1252
1262
|
expect(knobProps.fieldLabelPlacement).toBe("top");
|
|
1253
1263
|
expect(knobProps.requiredMarking).toBe("minority");
|
|
1254
|
-
expect(knobProps.tableZebra).toBe("
|
|
1264
|
+
expect(knobProps.tableZebra).toBe("on");
|
|
1255
1265
|
expect(knobProps.bulkBarPlacement).toBe("top");
|
|
1256
1266
|
expect(knobProps.selectAllScope).toBe("page");
|
|
1257
1267
|
expect(knobProps.timestampStyle).toBe("absolute");
|
|
@@ -1264,7 +1274,7 @@ describe("house decision knobs", () => {
|
|
|
1264
1274
|
knobsWith({
|
|
1265
1275
|
fieldLabelPlacement: "floating",
|
|
1266
1276
|
requiredMarking: "optional",
|
|
1267
|
-
tableZebra: "
|
|
1277
|
+
tableZebra: "off",
|
|
1268
1278
|
bulkBarPlacement: "bottom",
|
|
1269
1279
|
selectAllScope: "filtered",
|
|
1270
1280
|
timestampStyle: "relative",
|
|
@@ -1274,7 +1284,7 @@ describe("house decision knobs", () => {
|
|
|
1274
1284
|
);
|
|
1275
1285
|
expect(knobProps.fieldLabelPlacement).toBe("floating");
|
|
1276
1286
|
expect(knobProps.requiredMarking).toBe("optional");
|
|
1277
|
-
expect(knobProps.tableZebra).toBe("
|
|
1287
|
+
expect(knobProps.tableZebra).toBe("off");
|
|
1278
1288
|
expect(knobProps.bulkBarPlacement).toBe("bottom");
|
|
1279
1289
|
expect(knobProps.selectAllScope).toBe("filtered");
|
|
1280
1290
|
expect(knobProps.timestampStyle).toBe("relative");
|
|
@@ -1287,7 +1297,7 @@ describe("house decision knobs", () => {
|
|
|
1287
1297
|
const housey = resolveKnobs(
|
|
1288
1298
|
knobsWith({
|
|
1289
1299
|
fieldLabelPlacement: "side",
|
|
1290
|
-
tableZebra: "
|
|
1300
|
+
tableZebra: "off",
|
|
1291
1301
|
formAutofocus: "on",
|
|
1292
1302
|
}),
|
|
1293
1303
|
);
|