@multiplatform.one/theme 7.39.0 → 7.40.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/package.json +5 -5
- package/src/theme/Preset.tsx +30 -5
- package/src/theme/PresetContext.ts +5 -0
- package/src/theme/boundaryFloor.spec.ts +3 -0
- package/src/theme/colorThemeAccent.spec.ts +54 -0
- package/src/theme/createDefaultThemeConfig.spec.ts +5 -3
- package/src/theme/createThemes.ts +144 -27
- package/src/theme/preset.spec.tsx +57 -1
- package/src/theme/subsetThemes.spec.ts +1 -1
- package/src/theme/switchThumb.spec.ts +7 -5
- package/types/theme/Preset.d.ts +12 -1
- package/types/theme/Preset.d.ts.map +1 -1
- package/types/theme/PresetContext.d.ts +5 -0
- package/types/theme/PresetContext.d.ts.map +1 -1
- package/types/theme/createThemes.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/theme",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.40.0",
|
|
4
4
|
"description": "Tamagui theme system for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multiplatform",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"access": "public"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@multiplatform.one/platform": "^7.
|
|
63
|
-
"@multiplatform.one/store": "^7.
|
|
62
|
+
"@multiplatform.one/platform": "^7.40.0",
|
|
63
|
+
"@multiplatform.one/store": "^7.40.0",
|
|
64
64
|
"@tamagui/animations-react-native": "2.7.6",
|
|
65
65
|
"@tamagui/colors": "2.7.6",
|
|
66
66
|
"@tamagui/config": "2.7.6",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"react-cookie": "^8.1.2"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@multiplatform.one/config": "7.
|
|
77
|
-
"@multiplatform.one/test-utils": "7.
|
|
76
|
+
"@multiplatform.one/config": "7.40.0",
|
|
77
|
+
"@multiplatform.one/test-utils": "7.40.0",
|
|
78
78
|
"@tamagui/animations-css": "2.7.6",
|
|
79
79
|
"@tamagui/animations-reanimated": "2.7.6",
|
|
80
80
|
"@tamagui/font-inter": "2.7.6",
|
package/src/theme/Preset.tsx
CHANGED
|
@@ -6,10 +6,24 @@ import { defaultKnobs, type Knobs } from "./knobs";
|
|
|
6
6
|
import type { Preset as PresetType } from "./preset.types";
|
|
7
7
|
import { getPreset } from "./shared";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* FZ-7715. A preset owns shape, density, type and its default accent. A colour
|
|
11
|
+
* theme the user picks is the accent, so it replaces the preset's `theme` and
|
|
12
|
+
* every decorative tint; the preset's own colour shows only with no pick.
|
|
13
|
+
*/
|
|
14
|
+
export function applyColorTheme(preset: PresetType, color?: ThemeName | ""): PresetType {
|
|
15
|
+
if (!color) return preset;
|
|
16
|
+
return { ...preset, theme: color, tints: [color] };
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
export interface PresetProps extends PropsWithChildren {
|
|
10
20
|
/** Named preset to apply. Looked up from the preset registry. */
|
|
11
21
|
preset?: string;
|
|
12
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Colour theme (e.g. "blue", "red"). Wraps children in <Theme /> and beats
|
|
24
|
+
* the preset's tint, through nested named presets too. `""` drops an
|
|
25
|
+
* inherited pick and returns to the preset's own colour.
|
|
26
|
+
*/
|
|
13
27
|
theme?: ThemeName;
|
|
14
28
|
/** Partial knob overrides merged on top of the preset's knobs. */
|
|
15
29
|
overrides?: Partial<Knobs>;
|
|
@@ -25,8 +39,9 @@ export function Preset({
|
|
|
25
39
|
cascade = true,
|
|
26
40
|
}: PresetProps) {
|
|
27
41
|
const parentCtx = useContext(PresetContext);
|
|
42
|
+
const color = theme === undefined ? (cascade ? parentCtx?.color : undefined) : theme || undefined;
|
|
28
43
|
|
|
29
|
-
const
|
|
44
|
+
const composed = useMemo((): PresetContextValue => {
|
|
30
45
|
// 1. Determine base preset
|
|
31
46
|
let base: PresetType;
|
|
32
47
|
|
|
@@ -87,11 +102,21 @@ export function Preset({
|
|
|
87
102
|
return { preset: base, ...scope };
|
|
88
103
|
}, [cascade, parentCtx, presetName, overrides]);
|
|
89
104
|
|
|
105
|
+
const ctxValue = useMemo((): PresetContextValue => {
|
|
106
|
+
if (color) return { ...composed, preset: applyColorTheme(composed.preset, color), color };
|
|
107
|
+
if (theme !== "") return composed;
|
|
108
|
+
// The cascaded preset already carries the parent's colour: take theme and
|
|
109
|
+
// tints back from the named (or the provider's) preset.
|
|
110
|
+
const own = getPreset(presetName ?? parentCtx?.presetName ?? "") ?? composed.preset;
|
|
111
|
+
return { ...composed, preset: { ...composed.preset, theme: own.theme, tints: own.tints } };
|
|
112
|
+
}, [composed, color, theme, presetName, parentCtx]);
|
|
113
|
+
|
|
90
114
|
let element = <PresetContext.Provider value={ctxValue}>{children}</PresetContext.Provider>;
|
|
91
115
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
// An explicit colour wraps in its Tamagui theme; `""` re-applies the preset's own.
|
|
117
|
+
const wrap = theme ? theme : theme === "" ? ctxValue.preset.theme : undefined;
|
|
118
|
+
if (wrap) {
|
|
119
|
+
element = <TamaguiTheme name={wrap}>{element}</TamaguiTheme>;
|
|
95
120
|
}
|
|
96
121
|
|
|
97
122
|
return element;
|
|
@@ -10,6 +10,11 @@ export interface PresetContextValue {
|
|
|
10
10
|
presetName?: string;
|
|
11
11
|
/** Id of the installed token scale; changes after a switch re-render every knob reader. */
|
|
12
12
|
scaleId?: string;
|
|
13
|
+
/**
|
|
14
|
+
* The colour theme a user picked, when one was. It is the accent: it beats
|
|
15
|
+
* the preset's own `theme` and `tints`, and survives a nested named preset.
|
|
16
|
+
*/
|
|
17
|
+
color?: ThemeName;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export const PresetContext = createContext<PresetContextValue | null>(null);
|
|
@@ -33,6 +33,9 @@ describe("DF-06 / WCAG 1.4.11: control boundaries hold 3:1", () => {
|
|
|
33
33
|
expect(theme.boundaryColor).toBeTruthy();
|
|
34
34
|
const surfaces = [theme.background, theme.color3, theme.color5];
|
|
35
35
|
const minimum = (ink: string) => Math.min(...surfaces.map((surface) => ratio(ink, surface)));
|
|
36
|
+
expect(minimum(theme.boundaryColor)).toBeGreaterThanOrEqual(minContrastRatio);
|
|
37
|
+
// FZ-7715's hue solids are built after getTheme: the floor is all they owe.
|
|
38
|
+
if (!inputThemes[name]) return;
|
|
36
39
|
// Tint text is neutralized after getBaseTheme; selection uses the input ramp.
|
|
37
40
|
const candidates = [8, 9, 10, 11, 12].map((step) => inputThemes[name][`color${step}`]);
|
|
38
41
|
const firstHolding = candidates.find((ink) => minimum(ink) >= minContrastRatio);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { configWithoutAnimations } from "@tamagui/config";
|
|
2
|
+
import { createTamagui } from "tamagui";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
5
|
+
import { createThemesBuilder, houseComponentThemes } from "./createThemes";
|
|
6
|
+
import { defaultAccentTheme } from "./defaults/accent";
|
|
7
|
+
import { defaultBaseTheme } from "./defaults/base";
|
|
8
|
+
import { defaultBuilderOptions } from "./defaults/builderOptions";
|
|
9
|
+
|
|
10
|
+
const themes = createThemesBuilder(
|
|
11
|
+
defaultBaseTheme,
|
|
12
|
+
defaultAccentTheme,
|
|
13
|
+
defaultBuilderOptions,
|
|
14
|
+
).themes();
|
|
15
|
+
const config = createTamagui({ ...configWithoutAnimations, themes });
|
|
16
|
+
const hues = ["blue", "red", "green", "yellow", "orange", "pink", "purple", "gray"];
|
|
17
|
+
const ratio = (a: string, b: string) =>
|
|
18
|
+
contrastRatio(relativeLuminance(normalizeToHex(a)!), relativeLuminance(normalizeToHex(b)!));
|
|
19
|
+
|
|
20
|
+
describe("FZ-7715: a colour theme owns its accent", () => {
|
|
21
|
+
for (const scheme of ["light", "dark"]) {
|
|
22
|
+
for (const hue of hues) {
|
|
23
|
+
it(`${scheme} ${hue} uses its own ramp for accent tokens and selected controls`, () => {
|
|
24
|
+
const scope = themes[`${scheme}_${hue}`];
|
|
25
|
+
const ramp = Array.from({ length: 12 }, (_, i) => themes[scheme][`${hue}${i + 1}`]);
|
|
26
|
+
expect(ramp).toContain(scope.accentBackground);
|
|
27
|
+
expect(ramp).toContain(scope.accentColor);
|
|
28
|
+
expect(scope.accentBackground).not.toBe(themes[scheme].accentBackground);
|
|
29
|
+
expect(ratio(scope.accentBackground, scope.color1)).toBeGreaterThanOrEqual(3);
|
|
30
|
+
for (const surface of [scope.color1, scope.color2, scope.color3]) {
|
|
31
|
+
expect(ratio(scope.accentColor, surface)).toBeGreaterThanOrEqual(4.5);
|
|
32
|
+
}
|
|
33
|
+
const selected = config.themes[`${scheme}_${hue}_accent`] as unknown as Record<
|
|
34
|
+
string,
|
|
35
|
+
{ val: string }
|
|
36
|
+
>;
|
|
37
|
+
expect(selected.color5.val).toBe(scope.color5);
|
|
38
|
+
expect(selected.background.val).toBe(scope.accentBackground);
|
|
39
|
+
expect(ratio(selected.background.val, selected.color.val)).toBeGreaterThanOrEqual(4.5);
|
|
40
|
+
expect(ratio(selected.background.val, selected.outlineColor.val)).toBeGreaterThanOrEqual(3);
|
|
41
|
+
for (const ink of ["color11", "color12", "textMuted", "accentBackground"]) {
|
|
42
|
+
expect(ratio(selected.background.val, selected[ink].val)).toBeGreaterThanOrEqual(4.5);
|
|
43
|
+
}
|
|
44
|
+
for (const component of Object.keys(houseComponentThemes)) {
|
|
45
|
+
const child = config.themes[`${scheme}_${hue}_${component}`] as unknown as Record<
|
|
46
|
+
string,
|
|
47
|
+
{ val: string }
|
|
48
|
+
>;
|
|
49
|
+
expect(child.accentBackground.val).toBe(scope.accentBackground);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
@@ -411,10 +411,12 @@ describe("the v5 theme matrix", () => {
|
|
|
411
411
|
expect(Object.keys(custom).some((name) => /_[A-Z]/.test(name))).toBe(false);
|
|
412
412
|
});
|
|
413
413
|
|
|
414
|
-
|
|
414
|
+
// FZ-7715 added one accent solid per hue and scheme (16 themes) so
|
|
415
|
+
// `theme="accent"` follows a picked colour; the budget moved 260 -> 280 KB.
|
|
416
|
+
it("ships 478 themes and the heading-scope category fonts in under 280 KB of CSS (the v4 matrix and stock spread shipped 913 KB)", async () => {
|
|
415
417
|
const { createTamagui: build } = await vi.importActual<typeof import("tamagui")>("tamagui");
|
|
416
418
|
const css = build(config.tamagui).getCSS();
|
|
417
|
-
expect(Object.keys(themes)).toHaveLength(
|
|
418
|
-
expect(Buffer.byteLength(css, "utf8")).toBeLessThan(
|
|
419
|
+
expect(Object.keys(themes)).toHaveLength(478);
|
|
420
|
+
expect(Buffer.byteLength(css, "utf8")).toBeLessThan(280_000);
|
|
419
421
|
});
|
|
420
422
|
});
|
|
@@ -4,8 +4,10 @@ import { defaultChildrenThemes, v5Templates } from "@tamagui/themes/v5";
|
|
|
4
4
|
import {
|
|
5
5
|
aaTextContrastRatio,
|
|
6
6
|
contrastRatio,
|
|
7
|
+
holdInkFloor,
|
|
7
8
|
minContrastRatio,
|
|
8
9
|
normalizeToHex,
|
|
10
|
+
pickReadableForeground,
|
|
9
11
|
relativeLuminance,
|
|
10
12
|
} from "./colorRules";
|
|
11
13
|
|
|
@@ -329,33 +331,45 @@ function holdSwitchTrack(
|
|
|
329
331
|
for (const scheme of ["light", "dark"] as const) {
|
|
330
332
|
const base = themes[scheme];
|
|
331
333
|
if (!base) continue;
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
const surfaces = [base.background, base.color3].map(luminanceOf);
|
|
335
|
-
if (thumb === null || on === null || surfaces.some((lum) => lum === null)) continue;
|
|
336
|
-
const holds = (step: number, onPage: boolean) => {
|
|
337
|
-
const lum = luminanceOf(base[`gray${step}`]);
|
|
338
|
-
return (
|
|
339
|
-
lum !== null &&
|
|
340
|
-
contrastRatio(lum, thumb) >= minContrastRatio &&
|
|
341
|
-
contrastRatio(lum, on) >= switchStateContrastRatio &&
|
|
342
|
-
(!onPage || surfaces.every((surface) => contrastRatio(lum, surface!) >= minContrastRatio))
|
|
343
|
-
);
|
|
344
|
-
};
|
|
345
|
-
const off =
|
|
346
|
-
switchOffSteps.find((step) => holds(step, true)) ??
|
|
347
|
-
switchOffSteps.find((step) => holds(step, false));
|
|
348
|
-
if (off === undefined) continue;
|
|
349
|
-
const paint = {
|
|
350
|
-
switchTrackOff: base[`gray${off}`],
|
|
351
|
-
switchTrackOffHover: base[`gray${off - 1}`],
|
|
352
|
-
switchTrackOffPress: base[`gray${off + 1}`],
|
|
353
|
-
};
|
|
354
|
-
out[scheme] = { ...base, ...paint };
|
|
334
|
+
const paint = switchTrackPaint(base, base.accentBackground, [base.background, base.color3]);
|
|
335
|
+
if (paint) out[scheme] = { ...base, ...paint };
|
|
355
336
|
}
|
|
356
337
|
return out;
|
|
357
338
|
}
|
|
358
339
|
|
|
340
|
+
/**
|
|
341
|
+
* The off track for one on-fill: the first grey step in `steps` that holds
|
|
342
|
+
* under the thumb and apart from `onFill`, on the page too when one can.
|
|
343
|
+
* Hover is one step toward the page and press one away, clamped to the ramp.
|
|
344
|
+
*/
|
|
345
|
+
function switchTrackPaint(
|
|
346
|
+
base: Record<string, string>,
|
|
347
|
+
onFill: string | undefined,
|
|
348
|
+
pageSurfaces: (string | undefined)[],
|
|
349
|
+
steps: readonly number[] = switchOffSteps,
|
|
350
|
+
): Record<string, string> | undefined {
|
|
351
|
+
const thumb = luminanceOf(base.switchThumb ?? "#ffffff");
|
|
352
|
+
const on = luminanceOf(onFill);
|
|
353
|
+
const surfaces = pageSurfaces.map(luminanceOf);
|
|
354
|
+
if (thumb === null || on === null || surfaces.some((lum) => lum === null)) return undefined;
|
|
355
|
+
const holds = (step: number, onPage: boolean) => {
|
|
356
|
+
const lum = luminanceOf(base[`gray${step}`]);
|
|
357
|
+
return (
|
|
358
|
+
lum !== null &&
|
|
359
|
+
contrastRatio(lum, thumb) >= minContrastRatio &&
|
|
360
|
+
contrastRatio(lum, on) >= switchStateContrastRatio &&
|
|
361
|
+
(!onPage || surfaces.every((surface) => contrastRatio(lum, surface!) >= minContrastRatio))
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
const off = steps.find((step) => holds(step, true)) ?? steps.find((step) => holds(step, false));
|
|
365
|
+
if (off === undefined) return undefined;
|
|
366
|
+
return {
|
|
367
|
+
switchTrackOff: base[`gray${off}`],
|
|
368
|
+
switchTrackOffHover: base[`gray${off - 1}`],
|
|
369
|
+
switchTrackOffPress: base[`gray${Math.min(off + 1, 12)}`],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
359
373
|
/** The hues the code syntax palette paints (MPO-468). */
|
|
360
374
|
const syntaxInkHues = ["purple", "green", "blue", "orange", "red"] as const;
|
|
361
375
|
|
|
@@ -441,6 +455,107 @@ function holdNamedInkFloor(
|
|
|
441
455
|
return out;
|
|
442
456
|
}
|
|
443
457
|
|
|
458
|
+
/**
|
|
459
|
+
* FZ-7715. A colour theme is the accent. The house accent is the neutral
|
|
460
|
+
* inverse, and the hue sub-themes (light_green, dark_pink, ...) inherited it,
|
|
461
|
+
* so `theme="accent"` and `$accentBackground` stayed black under any colour a
|
|
462
|
+
* user or a preset picked. Each hue scope gets an accent fill from its own
|
|
463
|
+
* ramp, held to 3:1 on its page with a 4.5:1 label, accent ink held to 4.5:1
|
|
464
|
+
* on color1..color3, and a `<scheme>_<hue>_accent` solid that Tamagui resolves
|
|
465
|
+
* for `theme="accent"` (and every component under it) inside the hue.
|
|
466
|
+
*/
|
|
467
|
+
function holdColorThemeAccent(
|
|
468
|
+
themes: Record<string, Record<string, string>>,
|
|
469
|
+
): Record<string, Record<string, string>> {
|
|
470
|
+
const out: Record<string, Record<string, string>> = { ...themes };
|
|
471
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
472
|
+
const base = themes[scheme];
|
|
473
|
+
if (!base) continue;
|
|
474
|
+
for (const hue of childrenHues) {
|
|
475
|
+
const scopeName = `${scheme}_${hue}`;
|
|
476
|
+
const scope = themes[scopeName];
|
|
477
|
+
const ramp = Array.from({ length: 12 }, (_, index) => base[`${hue}${index + 1}`]);
|
|
478
|
+
if (!scope || ramp.some((step) => !step)) continue;
|
|
479
|
+
const paper = luminanceOf(scope.color1);
|
|
480
|
+
const ink = luminanceOf(scope.color12);
|
|
481
|
+
const surfaces = [scope.color1, scope.color2, scope.color3].map(luminanceOf);
|
|
482
|
+
if (paper === null || ink === null || surfaces.some((lum) => lum === null)) continue;
|
|
483
|
+
const fill = nearestHoldingStep(
|
|
484
|
+
ramp[8],
|
|
485
|
+
ramp.slice(0, 11),
|
|
486
|
+
(lum) =>
|
|
487
|
+
Math.max(contrastRatio(lum, paper), contrastRatio(lum, ink)) >= aaTextContrastRatio &&
|
|
488
|
+
contrastRatio(lum, paper) >= minContrastRatio,
|
|
489
|
+
);
|
|
490
|
+
const accentInk = nearestHoldingStep(ramp[10], ramp, (lum) =>
|
|
491
|
+
surfaces.every((surface) => contrastRatio(lum, surface as number) >= aaTextContrastRatio),
|
|
492
|
+
);
|
|
493
|
+
const fillStep = ramp.indexOf(fill);
|
|
494
|
+
const fillHover = ramp[Math.min(fillStep + 1, ramp.length - 1)];
|
|
495
|
+
const onFill = holdInkFloor(fill, pickReadableForeground(fill, scope.color1, scope.color12));
|
|
496
|
+
const accent: Record<string, string> = { accentBackground: fill, accentColor: accentInk };
|
|
497
|
+
// A hue fill can sit on the root's off grey (light pink on gray10): walk
|
|
498
|
+
// outward from the root's step to the nearest one that holds again.
|
|
499
|
+
const rootOff = Number(
|
|
500
|
+
Object.keys(base)
|
|
501
|
+
.find((key) => /^gray\d+$/.test(key) && base[key] === base.switchTrackOff)
|
|
502
|
+
?.slice(4),
|
|
503
|
+
);
|
|
504
|
+
const rootOn = luminanceOf(base.switchTrackOff);
|
|
505
|
+
const fillLum = luminanceOf(fill);
|
|
506
|
+
const rootHolds =
|
|
507
|
+
rootOn !== null &&
|
|
508
|
+
fillLum !== null &&
|
|
509
|
+
contrastRatio(rootOn, fillLum) >= switchStateContrastRatio;
|
|
510
|
+
const outward = Array.from({ length: 10 }, (_, index) => index + 3).sort(
|
|
511
|
+
(a, b) => Math.abs(a - rootOff) - Math.abs(b - rootOff) || b - a,
|
|
512
|
+
);
|
|
513
|
+
const track =
|
|
514
|
+
rootHolds || Number.isNaN(rootOff)
|
|
515
|
+
? undefined
|
|
516
|
+
: switchTrackPaint(base, fill, [scope.background, scope.color3], outward);
|
|
517
|
+
for (const [name, theme] of Object.entries(themes)) {
|
|
518
|
+
if (name === scopeName) {
|
|
519
|
+
out[name] = { ...theme, ...accent, ...track };
|
|
520
|
+
} else if (name.startsWith(`${scopeName}_`)) {
|
|
521
|
+
const owned = Object.keys(accent).filter((key) => key in theme);
|
|
522
|
+
if (owned.length > 0) {
|
|
523
|
+
out[name] = { ...theme, ...Object.fromEntries(owned.map((key) => [key, accent[key]])) };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// Only what the solid changes: the rest resolves through the hue scope,
|
|
528
|
+
// which keeps the CSS sheet under its budget. Ink, focus ring and any
|
|
529
|
+
// nested accent mark take the fill's label so none vanishes into it.
|
|
530
|
+
out[`${scopeName}_accent`] = {
|
|
531
|
+
background: fill,
|
|
532
|
+
backgroundHover: fillHover,
|
|
533
|
+
backgroundPress: fillHover,
|
|
534
|
+
backgroundFocus: fillHover,
|
|
535
|
+
backgroundActive: fill,
|
|
536
|
+
borderColor: fill,
|
|
537
|
+
borderColorHover: fillHover,
|
|
538
|
+
borderColorFocus: fillHover,
|
|
539
|
+
borderColorPress: fillHover,
|
|
540
|
+
color: onFill,
|
|
541
|
+
colorHover: onFill,
|
|
542
|
+
colorPress: onFill,
|
|
543
|
+
colorFocus: onFill,
|
|
544
|
+
placeholderColor: onFill,
|
|
545
|
+
boundaryColor: onFill,
|
|
546
|
+
outlineColor: onFill,
|
|
547
|
+
color10: onFill,
|
|
548
|
+
color11: onFill,
|
|
549
|
+
color12: onFill,
|
|
550
|
+
textMuted: onFill,
|
|
551
|
+
accentBackground: onFill,
|
|
552
|
+
accentColor: onFill,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return out;
|
|
557
|
+
}
|
|
558
|
+
|
|
444
559
|
const defaultLightShadows: Shadows = {
|
|
445
560
|
shadow1: "rgba(0,0,0,0.04)",
|
|
446
561
|
shadow2: "rgba(0,0,0,0.08)",
|
|
@@ -539,10 +654,12 @@ export function createThemesBuilder(
|
|
|
539
654
|
const initialTheme = tamaguiCreateThemes(
|
|
540
655
|
createThemesProps as Parameters<typeof tamaguiCreateThemes>[0],
|
|
541
656
|
);
|
|
542
|
-
const themes =
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
657
|
+
const themes = holdColorThemeAccent(
|
|
658
|
+
holdSwitchTrack(
|
|
659
|
+
holdSyntaxFloor(
|
|
660
|
+
holdNamedInkFloor(
|
|
661
|
+
holdAccentFloor(neutralizeSemanticEntryText(neutralizeTintText(initialTheme))),
|
|
662
|
+
),
|
|
546
663
|
),
|
|
547
664
|
),
|
|
548
665
|
);
|
|
@@ -10,7 +10,7 @@ vi.mock("tamagui", () => ({
|
|
|
10
10
|
|
|
11
11
|
import type { ThemeName } from "@tamagui/web";
|
|
12
12
|
import { Preset } from "./Preset";
|
|
13
|
-
import { type PresetContextValue, usePresetContext } from "./PresetContext";
|
|
13
|
+
import { PresetContext, type PresetContextValue, usePresetContext } from "./PresetContext";
|
|
14
14
|
import { defaultKnobs } from "./knobs";
|
|
15
15
|
import type { Preset as PresetType } from "./preset.types";
|
|
16
16
|
import { clearPresets, registerPreset } from "./shared";
|
|
@@ -177,6 +177,62 @@ describe("Preset component", () => {
|
|
|
177
177
|
expect(html).toContain('data-tamagui-theme="blue"');
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
it("uses an explicit colour for the preset metadata and every decorative tint", () => {
|
|
181
|
+
registerPreset("bold", boldPreset);
|
|
182
|
+
const capture = createPresetContextCapture();
|
|
183
|
+
renderToString(
|
|
184
|
+
<Preset preset="bold" theme={"green" as ThemeName}>
|
|
185
|
+
<capture.Capture />
|
|
186
|
+
</Preset>,
|
|
187
|
+
);
|
|
188
|
+
expect(capture.result?.preset.theme).toBe("green");
|
|
189
|
+
expect(capture.result?.preset.tints).toEqual(["green"]);
|
|
190
|
+
expect(capture.result?.preset.knobs).toEqual(boldPreset.knobs);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("keeps the explicit colour through a nested named preset", () => {
|
|
194
|
+
registerPreset("bold", boldPreset);
|
|
195
|
+
const capture = createPresetContextCapture();
|
|
196
|
+
renderToString(
|
|
197
|
+
<Preset theme={"green" as ThemeName}>
|
|
198
|
+
<Preset preset="bold">
|
|
199
|
+
<capture.Capture />
|
|
200
|
+
</Preset>
|
|
201
|
+
</Preset>,
|
|
202
|
+
);
|
|
203
|
+
expect(capture.result?.preset.theme).toBe("green");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("returns to the preset tint when the colour is reset to default", () => {
|
|
207
|
+
registerPreset("bold", boldPreset);
|
|
208
|
+
const capture = createPresetContextCapture();
|
|
209
|
+
const html = renderToString(
|
|
210
|
+
<Preset theme={"green" as ThemeName}>
|
|
211
|
+
<Preset preset="bold" theme={"" as ThemeName}>
|
|
212
|
+
<capture.Capture />
|
|
213
|
+
</Preset>
|
|
214
|
+
</Preset>,
|
|
215
|
+
);
|
|
216
|
+
expect(capture.result?.preset.theme).toBe("blue");
|
|
217
|
+
expect(capture.result?.preset.tints).toEqual(boldPreset.tints);
|
|
218
|
+
expect(html).toContain('data-tamagui-theme="blue"');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("drops an inherited colour on an unnamed reset", () => {
|
|
222
|
+
registerPreset("bold", boldPreset);
|
|
223
|
+
const capture = createPresetContextCapture();
|
|
224
|
+
renderToString(
|
|
225
|
+
<PresetContext.Provider value={{ preset: boldPreset, presetName: "bold" }}>
|
|
226
|
+
<Preset theme={"green" as ThemeName}>
|
|
227
|
+
<Preset theme={"" as ThemeName}>
|
|
228
|
+
<capture.Capture />
|
|
229
|
+
</Preset>
|
|
230
|
+
</Preset>
|
|
231
|
+
</PresetContext.Provider>,
|
|
232
|
+
);
|
|
233
|
+
expect(capture.result?.preset.tints).toEqual(boldPreset.tints);
|
|
234
|
+
});
|
|
235
|
+
|
|
180
236
|
it("does not wrap with Tamagui <Theme /> when theme prop is absent", () => {
|
|
181
237
|
const capture = createPresetContextCapture();
|
|
182
238
|
|
|
@@ -79,7 +79,7 @@ describe("subsetThemes over the house matrix", () => {
|
|
|
79
79
|
components: ["Button"],
|
|
80
80
|
tints: ["accent", "active", "alt1", "alt2", "error", "success", "warning"],
|
|
81
81
|
});
|
|
82
|
-
expect(Object.keys(built)).toHaveLength(
|
|
82
|
+
expect(Object.keys(built)).toHaveLength(478);
|
|
83
83
|
expect(Object.keys(kept)).toHaveLength(44);
|
|
84
84
|
for (const name of ["light", "dark", "light_accent", "light_Button", "dark_error_Button"]) {
|
|
85
85
|
expect(kept).toHaveProperty(name);
|
|
@@ -46,11 +46,13 @@ describe("switch thumb", () => {
|
|
|
46
46
|
const theme = config.themes[name] as Record<string, { val: string }>;
|
|
47
47
|
const scheme = name.split("_")[0] as "light" | "dark";
|
|
48
48
|
const off = theme.switchTrackOff.val;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
49
|
+
// FZ-7715: a hue scope's own fill can move its off track off the root's step.
|
|
50
|
+
const step = Array.from({ length: 10 }, (_, i) => i + 3).find(
|
|
51
|
+
(n) => theme[`gray${n}`].val === off,
|
|
52
|
+
)!;
|
|
53
|
+
if (name === scheme) expect(step).toBe(scheme === "light" ? 10 : 7);
|
|
54
|
+
expect(theme.switchTrackOffHover.val).toBe(theme[`gray${step - 1}`].val);
|
|
55
|
+
expect(theme.switchTrackOffPress.val).toBe(theme[`gray${Math.min(step + 1, 12)}`].val);
|
|
54
56
|
expect(ratio(off, theme.switchThumb.val)).toBeGreaterThanOrEqual(minContrastRatio);
|
|
55
57
|
expect(ratio(off, theme.accentBackground.val)).toBeGreaterThanOrEqual(1.5);
|
|
56
58
|
if (scheme === "light") {
|
package/types/theme/Preset.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { type PropsWithChildren } from "react";
|
|
2
2
|
import type { ThemeName } from "@tamagui/web";
|
|
3
3
|
import { type Knobs } from "./knobs";
|
|
4
|
+
import type { Preset as PresetType } from "./preset.types";
|
|
5
|
+
/**
|
|
6
|
+
* FZ-7715. A preset owns shape, density, type and its default accent. A colour
|
|
7
|
+
* theme the user picks is the accent, so it replaces the preset's `theme` and
|
|
8
|
+
* every decorative tint; the preset's own colour shows only with no pick.
|
|
9
|
+
*/
|
|
10
|
+
export declare function applyColorTheme(preset: PresetType, color?: ThemeName | ""): PresetType;
|
|
4
11
|
export interface PresetProps extends PropsWithChildren {
|
|
5
12
|
/** Named preset to apply. Looked up from the preset registry. */
|
|
6
13
|
preset?: string;
|
|
7
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Colour theme (e.g. "blue", "red"). Wraps children in <Theme /> and beats
|
|
16
|
+
* the preset's tint, through nested named presets too. `""` drops an
|
|
17
|
+
* inherited pick and returns to the preset's own colour.
|
|
18
|
+
*/
|
|
8
19
|
theme?: ThemeName;
|
|
9
20
|
/** Partial knob overrides merged on top of the preset's knobs. */
|
|
10
21
|
overrides?: Partial<Knobs>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Preset.d.ts","sourceRoot":"","sources":["../../src/theme/Preset.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAuB,MAAM,OAAO,CAAC;AAEpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAgB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Preset.d.ts","sourceRoot":"","sources":["../../src/theme/Preset.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAuB,MAAM,OAAO,CAAC;AAEpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAgB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAG3D;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,EAAE,GAAG,UAAU,CAGtF;AAED,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC3B,uFAAuF;IACvF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,MAAM,CAAC,EACrB,QAAQ,EACR,MAAM,EAAE,UAAU,EAClB,KAAK,EACL,SAAS,EACT,OAAc,GACf,EAAE,WAAW,2CAmFb"}
|
|
@@ -8,6 +8,11 @@ export interface PresetContextValue {
|
|
|
8
8
|
presetName?: string;
|
|
9
9
|
/** Id of the installed token scale; changes after a switch re-render every knob reader. */
|
|
10
10
|
scaleId?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The colour theme a user picked, when one was. It is the accent: it beats
|
|
13
|
+
* the preset's own `theme` and `tints`, and survives a nested named preset.
|
|
14
|
+
*/
|
|
15
|
+
color?: ThemeName;
|
|
11
16
|
}
|
|
12
17
|
export declare const PresetContext: import("react").Context<PresetContextValue | null>;
|
|
13
18
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PresetContext.d.ts","sourceRoot":"","sources":["../../src/theme/PresetContext.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC3B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,OAAO,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"PresetContext.d.ts","sourceRoot":"","sources":["../../src/theme/PresetContext.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC3B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,oDAAiD,CAAC;AAE5E;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,kBAAkB,GAAG,IAAI,CAE5D;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,SAAS,EAAE,CAG5C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createThemes.d.ts","sourceRoot":"","sources":["../../src/theme/createThemes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,IAAI,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"createThemes.d.ts","sourceRoot":"","sources":["../../src/theme/createThemes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,IAAI,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAwC7E,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAevB,CAAC;AAEX,eAAO,MAAM,wBAAwB;;;;;;;;;;CAI3B,CAAC;AAEX,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB,CACrC,WAAW,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACvC,YAAY,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE;IAExC,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B,CACzC,WAAW,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACvC,YAAY,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACxC,WAAW,SAAS,OAAO,GAAG,OAAO,EACrC,YAAY,SAAS,OAAO,GAAG,OAAO,CACtC,SAAQ,sBAAsB,CAAC,WAAW,EAAE,YAAY,CAAC;IACzD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,eAAe,CAAC,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC;IAC/E,mBAAmB,CAAC,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC;IACvF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACzF;AA8BD,eAAO,MAAM,YAAY,aA6BvB,CAAC;AAuaH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,0BAA0B,EACrC,WAAW,EAAE,sBAAsB,EACnC,OAAO,CAAC,EAAE,0BAA0B;;aA+EzB,CAAC,SAAS,MAAM,QAAQ,CAAC,SAAS,sBAAsB,GAuB1D,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,SAAS,CAAC,EAAE;;+BAAkC;EAG7E"}
|