@multiplatform.one/theme 6.1.0 → 6.4.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 +73 -0
- package/package.json +22 -9
- package/src/dates.spec.ts +75 -0
- package/src/dates.ts +130 -0
- package/src/devWarn.spec.ts +144 -0
- package/src/devWarn.ts +235 -0
- package/src/figma/figmaTokens.spec.ts +244 -0
- package/src/figma/figmaTokens.ts +503 -0
- package/src/font.ts +6 -8
- package/src/fonts/createFontLoader.web.ts +15 -4
- package/src/index.ts +7 -0
- package/src/keyboardFocusRing.ts +45 -0
- package/src/menuRow.spec.ts +32 -0
- package/src/menuRow.ts +31 -0
- package/src/theme/FontKnobStyles.native.tsx +24 -0
- package/src/theme/FontKnobStyles.tsx +256 -0
- package/src/theme/animations/reactNative.ts +6 -1
- package/src/theme/chartPalette.spec.ts +159 -0
- package/src/theme/chartPalette.ts +170 -0
- package/src/theme/colorRules.ts +33 -1
- package/src/theme/cookies.spec.ts +32 -0
- package/src/theme/cookiesPersistence.spec.ts +86 -0
- package/src/theme/cornerSmoothing.native.ts +16 -0
- package/src/theme/cornerSmoothing.spec.ts +27 -0
- package/src/theme/cornerSmoothing.ts +46 -0
- package/src/theme/createDefaultThemeConfig.ts +32 -8
- package/src/theme/createThemeConfig.spec.ts +137 -0
- package/src/theme/createThemes.ts +77 -2
- package/src/theme/defaults/accent.ts +39 -0
- package/src/theme/defaults/base.ts +63 -0
- package/src/theme/defaults/builderOptions.ts +174 -0
- package/src/theme/defaults/fonts.spec.ts +87 -0
- package/src/theme/defaults/fonts.ts +89 -0
- package/src/theme/defaults/index.ts +4 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +196 -2
- package/src/theme/devtools/buildSnapshot.ts +11 -0
- package/src/theme/devtools/knobIcons.tsx +141 -0
- package/src/theme/devtools/vizConstants.ts +1 -1
- package/src/theme/focusState.spec.ts +174 -0
- package/src/theme/focusState.ts +112 -0
- package/src/theme/fontKnobStyles.spec.ts +98 -0
- package/src/theme/hairline.native.ts +43 -0
- package/src/theme/hairline.spec.ts +63 -0
- package/src/theme/hairline.ts +101 -0
- package/src/theme/index.ts +8 -0
- package/src/theme/intent.spec.tsx +2 -2
- package/src/theme/intents.ts +6 -2
- package/src/theme/knobs.ts +134 -2
- package/src/theme/layoutTokens.spec.ts +75 -0
- package/src/theme/layoutTokens.ts +213 -0
- package/src/theme/layoutTokensHooks.spec.tsx +157 -0
- package/src/theme/presets.spec.ts +17 -0
- package/src/theme/presets.ts +22 -2
- package/src/theme/readableColor.ts +101 -0
- package/src/theme/recipes.ts +130 -7
- package/src/theme/resolveKnobs.spec.ts +560 -21
- package/src/theme/resolveKnobs.ts +267 -23
- package/src/theme/shared.tsx +26 -2
- package/src/theme/useResolvedKnobs.ts +67 -26
- package/src/theme/useResolvedKnobsBehavior.spec.tsx +166 -0
- package/types/audit/constraintAudit.d.ts +83 -0
- package/types/audit/constraintAudit.d.ts.map +1 -0
- package/types/audit/diffReport.d.ts +45 -0
- package/types/audit/diffReport.d.ts.map +1 -0
- package/types/audit/index.d.ts +5 -0
- package/types/audit/index.d.ts.map +1 -0
- package/types/dates.d.ts +52 -0
- package/types/dates.d.ts.map +1 -0
- package/types/devWarn.d.ts +104 -0
- package/types/devWarn.d.ts.map +1 -0
- package/types/figma/figmaTokens.d.ts +168 -0
- package/types/figma/figmaTokens.d.ts.map +1 -0
- package/types/font.d.ts +5 -0
- package/types/font.d.ts.map +1 -0
- package/types/fonts/createFontLoader.d.ts +3 -0
- package/types/fonts/createFontLoader.d.ts.map +1 -0
- package/types/fonts/createFontLoader.web.d.ts +3 -0
- package/types/fonts/createFontLoader.web.d.ts.map +1 -0
- package/types/fonts/index.d.ts +3 -0
- package/types/fonts/index.d.ts.map +1 -0
- package/types/fonts/types.d.ts +12 -0
- package/types/fonts/types.d.ts.map +1 -0
- package/types/index.d.ts +9 -0
- package/types/index.d.ts.map +1 -0
- package/types/keyboardFocusRing.d.ts +32 -0
- package/types/keyboardFocusRing.d.ts.map +1 -0
- package/types/menuRow.d.ts +32 -0
- package/types/menuRow.d.ts.map +1 -0
- package/types/theme/FontKnobStyles.d.ts +49 -0
- package/types/theme/FontKnobStyles.d.ts.map +1 -0
- package/types/theme/FontKnobStyles.native.d.ts +12 -0
- package/types/theme/FontKnobStyles.native.d.ts.map +1 -0
- package/types/theme/Intent.d.ts +20 -0
- package/types/theme/Intent.d.ts.map +1 -0
- package/types/theme/Preset.d.ts +15 -0
- package/types/theme/Preset.d.ts.map +1 -0
- package/types/theme/PresetContext.d.ts +18 -0
- package/types/theme/PresetContext.d.ts.map +1 -0
- package/types/theme/Tint.d.ts +19 -0
- package/types/theme/Tint.d.ts.map +1 -0
- package/types/theme/animations/css.d.ts +24 -0
- package/types/theme/animations/css.d.ts.map +1 -0
- package/types/theme/animations/index.d.ts +2 -0
- package/types/theme/animations/index.d.ts.map +1 -0
- package/types/theme/animations/index.web.d.ts +2 -0
- package/types/theme/animations/index.web.d.ts.map +1 -0
- package/types/theme/animations/reactNative.d.ts +104 -0
- package/types/theme/animations/reactNative.d.ts.map +1 -0
- package/types/theme/chartPalette.d.ts +62 -0
- package/types/theme/chartPalette.d.ts.map +1 -0
- package/types/theme/colorRules.d.ts +69 -0
- package/types/theme/colorRules.d.ts.map +1 -0
- package/types/theme/cookies.d.ts +76 -0
- package/types/theme/cookies.d.ts.map +1 -0
- package/types/theme/cornerSmoothing.d.ts +31 -0
- package/types/theme/cornerSmoothing.d.ts.map +1 -0
- package/types/theme/cornerSmoothing.native.d.ts +14 -0
- package/types/theme/cornerSmoothing.native.d.ts.map +1 -0
- package/types/theme/createDefaultThemeConfig.d.ts +37 -0
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -0
- package/types/theme/createThemes.d.ts +38 -0
- package/types/theme/createThemes.d.ts.map +1 -0
- package/types/theme/defaults/accent.d.ts +3 -0
- package/types/theme/defaults/accent.d.ts.map +1 -0
- package/types/theme/defaults/base.d.ts +3 -0
- package/types/theme/defaults/base.d.ts.map +1 -0
- package/types/theme/defaults/builderOptions.d.ts +3 -0
- package/types/theme/defaults/builderOptions.d.ts.map +1 -0
- package/types/theme/defaults/fonts.d.ts +7 -0
- package/types/theme/defaults/fonts.d.ts.map +1 -0
- package/types/theme/defaults/index.d.ts +5 -0
- package/types/theme/defaults/index.d.ts.map +1 -0
- package/types/theme/devtools/ColorLineVisualizer.d.ts +17 -0
- package/types/theme/devtools/ColorLineVisualizer.d.ts.map +1 -0
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts +87 -0
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -0
- package/types/theme/devtools/buildSnapshot.d.ts +12 -0
- package/types/theme/devtools/buildSnapshot.d.ts.map +1 -0
- package/types/theme/devtools/knobIcons.d.ts +33 -0
- package/types/theme/devtools/knobIcons.d.ts.map +1 -0
- package/types/theme/devtools/vizConstants.d.ts +5 -0
- package/types/theme/devtools/vizConstants.d.ts.map +1 -0
- package/types/theme/focusState.d.ts +42 -0
- package/types/theme/focusState.d.ts.map +1 -0
- package/types/theme/hairline.d.ts +88 -0
- package/types/theme/hairline.d.ts.map +1 -0
- package/types/theme/hairline.native.d.ts +42 -0
- package/types/theme/hairline.native.d.ts.map +1 -0
- package/types/theme/index.d.ts +29 -0
- package/types/theme/index.d.ts.map +1 -0
- package/types/theme/intents.d.ts +3 -0
- package/types/theme/intents.d.ts.map +1 -0
- package/types/theme/knobs.d.ts +236 -0
- package/types/theme/knobs.d.ts.map +1 -0
- package/types/theme/layoutTokens.d.ts +104 -0
- package/types/theme/layoutTokens.d.ts.map +1 -0
- package/types/theme/preset.types.d.ts +21 -0
- package/types/theme/preset.types.d.ts.map +1 -0
- package/types/theme/presets.d.ts +4 -0
- package/types/theme/presets.d.ts.map +1 -0
- package/types/theme/readableColor.d.ts +17 -0
- package/types/theme/readableColor.d.ts.map +1 -0
- package/types/theme/recipes.d.ts +200 -0
- package/types/theme/recipes.d.ts.map +1 -0
- package/types/theme/resolveKnobs.d.ts +22 -0
- package/types/theme/resolveKnobs.d.ts.map +1 -0
- package/types/theme/shared.d.ts +40 -0
- package/types/theme/shared.d.ts.map +1 -0
- package/types/theme/theme.d.ts +19 -0
- package/types/theme/theme.d.ts.map +1 -0
- package/types/theme/theme.native.d.ts +10 -0
- package/types/theme/theme.native.d.ts.map +1 -0
- package/types/theme/useColorScale.d.ts +7 -0
- package/types/theme/useColorScale.d.ts.map +1 -0
- package/types/theme/useColorScale.native.d.ts +6 -0
- package/types/theme/useColorScale.native.d.ts.map +1 -0
- package/types/theme/useResolvedKnobs.d.ts +32 -0
- package/types/theme/useResolvedKnobs.d.ts.map +1 -0
- package/types/theme/useTheme.d.ts +31 -0
- package/types/theme/useTheme.d.ts.map +1 -0
- package/types/theme/useTheme.native.d.ts +10 -0
- package/types/theme/useTheme.native.d.ts.map +1 -0
- package/CHANGELOG.md +0 -53
- package/src/theme/__snapshots__/resolveKnobs.spec.ts.snap +0 -69
- package/src/theme/pokemonScreen.spec.ts +0 -109
- package/tsconfig.json +0 -11
- package/vitest.config.mjs +0 -9
|
@@ -165,6 +165,38 @@ describe("computeOverrides", () => {
|
|
|
165
165
|
const overrides = computeOverrides(defaultKnobs, current);
|
|
166
166
|
expect(Object.keys(overrides)).toEqual(["fillStyle"]);
|
|
167
167
|
});
|
|
168
|
+
|
|
169
|
+
it("persists density override via mp.ov.* pattern", () => {
|
|
170
|
+
const current: Knobs = { ...defaultKnobs, density: "compact" };
|
|
171
|
+
const overrides = computeOverrides(defaultKnobs, current);
|
|
172
|
+
expect(overrides).toEqual({ density: "compact" });
|
|
173
|
+
|
|
174
|
+
const { toSet } = writeOverridesCookie(overrides);
|
|
175
|
+
expect(toSet[0].key).toBe("mp.ov.0");
|
|
176
|
+
expect(JSON.parse(toSet[0].value)).toEqual({ density: "compact" });
|
|
177
|
+
|
|
178
|
+
const roundtrip = readOverridesCookie({ "mp.ov.0": toSet[0].value });
|
|
179
|
+
expect(roundtrip).toEqual({ density: "compact" });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("persists house-decision overrides via mp.ov.* pattern", () => {
|
|
183
|
+
const current: Knobs = {
|
|
184
|
+
...defaultKnobs,
|
|
185
|
+
fieldLabelPlacement: "side",
|
|
186
|
+
tableZebra: "on",
|
|
187
|
+
formAutofocus: "on",
|
|
188
|
+
};
|
|
189
|
+
const overrides = computeOverrides(defaultKnobs, current);
|
|
190
|
+
expect(overrides).toEqual({
|
|
191
|
+
fieldLabelPlacement: "side",
|
|
192
|
+
tableZebra: "on",
|
|
193
|
+
formAutofocus: "on",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const { toSet } = writeOverridesCookie(overrides);
|
|
197
|
+
expect(JSON.parse(toSet[0].value)).toEqual(overrides);
|
|
198
|
+
expect(readOverridesCookie({ "mp.ov.0": toSet[0].value })).toEqual(overrides);
|
|
199
|
+
});
|
|
168
200
|
});
|
|
169
201
|
|
|
170
202
|
describe("getCookieWatchList", () => {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie persistence round-trip specs — the non-React persistence surface
|
|
3
|
+
* (`persistPresetOverrides` / `loadPresetOverrides` / `getCookieWatchList`)
|
|
4
|
+
* that theme devtools and SSR hosts call directly. Locks down: overrides
|
|
5
|
+
* survive a write→read round-trip through document.cookie, oversized
|
|
6
|
+
* payloads chunk across `mp.ov.N` cookies and reassemble, stale chunks are
|
|
7
|
+
* cleaned up, and the SSR path reads from a raw cookie map without touching
|
|
8
|
+
* the document.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
12
|
+
import type { Knobs } from "./knobs";
|
|
13
|
+
import {
|
|
14
|
+
cookieOverridesPrefix,
|
|
15
|
+
cookiePreset,
|
|
16
|
+
getCookieWatchList,
|
|
17
|
+
loadPresetOverrides,
|
|
18
|
+
persistPresetOverrides,
|
|
19
|
+
} from "./cookies";
|
|
20
|
+
|
|
21
|
+
function clearThemeCookies() {
|
|
22
|
+
for (const key of getCookieWatchList()) {
|
|
23
|
+
document.cookie = `${key}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
clearThemeCookies();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("getCookieWatchList", () => {
|
|
32
|
+
it("watches the preset cookie plus every override chunk slot", () => {
|
|
33
|
+
const list = getCookieWatchList();
|
|
34
|
+
expect(list).toContain(cookiePreset);
|
|
35
|
+
expect(list).toContain(`${cookieOverridesPrefix}.0`);
|
|
36
|
+
expect(list).toContain(`${cookieOverridesPrefix}.9`);
|
|
37
|
+
// preset + 10 chunk slots
|
|
38
|
+
expect(list).toHaveLength(11);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("persistPresetOverrides / loadPresetOverrides", () => {
|
|
43
|
+
it("round-trips small overrides through document cookies", () => {
|
|
44
|
+
persistPresetOverrides({ borderRadius: "large", fillStyle: "filled" } as Partial<Knobs>);
|
|
45
|
+
expect(loadPresetOverrides()).toEqual({ borderRadius: "large", fillStyle: "filled" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns an empty object when nothing is persisted", () => {
|
|
49
|
+
expect(loadPresetOverrides()).toEqual({});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("overwrites previous overrides instead of merging them", () => {
|
|
53
|
+
persistPresetOverrides({ borderRadius: "large" } as Partial<Knobs>);
|
|
54
|
+
persistPresetOverrides({ elevation: "small" } as Partial<Knobs>);
|
|
55
|
+
expect(loadPresetOverrides()).toEqual({ elevation: "small" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("chunks oversized overrides across multiple cookies and reassembles them", () => {
|
|
59
|
+
const bigValue = "x".repeat(8000);
|
|
60
|
+
persistPresetOverrides({ animation: bigValue } as unknown as Partial<Knobs>);
|
|
61
|
+
// The serialized JSON exceeds the 3.5 KB split threshold → several chunks.
|
|
62
|
+
expect(document.cookie).toContain(`${cookieOverridesPrefix}.0=`);
|
|
63
|
+
expect(document.cookie).toContain(`${cookieOverridesPrefix}.1=`);
|
|
64
|
+
expect(loadPresetOverrides()).toEqual({ animation: bigValue });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("cleans up stale chunk cookies when the payload shrinks", () => {
|
|
68
|
+
const bigValue = "y".repeat(8000);
|
|
69
|
+
persistPresetOverrides({ animation: bigValue } as unknown as Partial<Knobs>);
|
|
70
|
+
persistPresetOverrides({ borderRadius: "small" } as Partial<Knobs>);
|
|
71
|
+
expect(loadPresetOverrides()).toEqual({ borderRadius: "small" });
|
|
72
|
+
expect(document.cookie).not.toContain(`${cookieOverridesPrefix}.1=`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("reads from a raw cookie map (SSR) without touching the document", () => {
|
|
76
|
+
const raw = {
|
|
77
|
+
[`${cookieOverridesPrefix}.0`]: JSON.stringify({ space: "large" }),
|
|
78
|
+
};
|
|
79
|
+
expect(loadPresetOverrides(raw)).toEqual({ space: "large" });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("treats corrupted cookie JSON as no overrides", () => {
|
|
83
|
+
const raw = { [`${cookieOverridesPrefix}.0`]: "{broken" };
|
|
84
|
+
expect(loadPresetOverrides(raw)).toEqual({});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPTICS corner smoothing (Axiom 15) — native twin of `cornerSmoothing.ts`.
|
|
3
|
+
*
|
|
4
|
+
* React Native has no `corner-shape`; native resolves `cornerSmoothing` to
|
|
5
|
+
* `round` (resolveKnobs emits no class on native) until a SquircleView
|
|
6
|
+
* adoption lands (phase 2). Exports stay shape-compatible for shared code.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Class attached on web only — never emitted into native fragments. */
|
|
10
|
+
export const cornerSmoothClassName = "mp-corner-smooth";
|
|
11
|
+
|
|
12
|
+
/** Web-only stylesheet — empty on native. */
|
|
13
|
+
export const cornerSmoothingCss = "";
|
|
14
|
+
|
|
15
|
+
/** Web-only stylesheet mount — no-op on native. */
|
|
16
|
+
export function ensureCornerSmoothingStyles(): void {}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
cornerSmoothClassName,
|
|
4
|
+
cornerSmoothingCss,
|
|
5
|
+
ensureCornerSmoothingStyles,
|
|
6
|
+
} from "./cornerSmoothing";
|
|
7
|
+
|
|
8
|
+
describe("cornerSmoothing (Axiom 15 OPTICS)", () => {
|
|
9
|
+
it("scopes the squircle declaration to the fragment class", () => {
|
|
10
|
+
expect(cornerSmoothClassName).toBe("mp-corner-smooth");
|
|
11
|
+
expect(cornerSmoothingCss).toBe(".mp-corner-smooth { corner-shape: squircle; }");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("stylesheet mount", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
document.getElementById("mp-corner-smoothing-styles")?.remove();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("mounts the stylesheet once (idempotent)", () => {
|
|
20
|
+
ensureCornerSmoothingStyles();
|
|
21
|
+
ensureCornerSmoothingStyles();
|
|
22
|
+
const tags = document.querySelectorAll("#mp-corner-smoothing-styles");
|
|
23
|
+
expect(tags).toHaveLength(1);
|
|
24
|
+
expect(tags[0]?.textContent).toBe(cornerSmoothingCss);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPTICS corner smoothing (Axiom 15): `cornerSmoothing: "smooth"` upgrades
|
|
3
|
+
* circular-arc corners to continuous curvature — CSS
|
|
4
|
+
* `corner-shape: squircle` (≡ `superellipse(2)`, ≈ Apple's continuous
|
|
5
|
+
* corners / Figma smoothing 0.6) — as a WEB PROGRESSIVE ENHANCEMENT.
|
|
6
|
+
* Chromium 139+ paints it; every other engine drops the unknown declaration
|
|
7
|
+
* and keeps today's plain `border-radius` arc, so no `@supports` gate is
|
|
8
|
+
* needed for correctness.
|
|
9
|
+
*
|
|
10
|
+
* Delivery is a class + one mounted stylesheet (the hairline pattern):
|
|
11
|
+
* Tamagui strips unknown keys from the `style` prop (`style` is in
|
|
12
|
+
* skipProps and `cornerShape` is not a valid style key), but merges
|
|
13
|
+
* `className` into its atomic classes — so `resolveKnobs` attaches
|
|
14
|
+
* `className: cornerSmoothClassName` to every radius-bearing fragment when
|
|
15
|
+
* the knob is `smooth`, and this module owns the CSS those classes resolve
|
|
16
|
+
* against.
|
|
17
|
+
*
|
|
18
|
+
* Native twin: `cornerSmoothing.native.ts` (no CSS classes — native resolves
|
|
19
|
+
* to `round` until a SquircleView adoption lands, phase 2).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Class attached to radius-bearing knob fragments at `cornerSmoothing: "smooth"`. */
|
|
23
|
+
export const cornerSmoothClassName = "mp-corner-smooth";
|
|
24
|
+
|
|
25
|
+
const STYLE_TAG_ID = "mp-corner-smoothing-styles";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The squircle declaration. `corner-shape` composes with the element's
|
|
29
|
+
* existing `border-radius` (it reshapes the same corner box), so the
|
|
30
|
+
* fallback in non-supporting engines is automatic. Exported for unit tests.
|
|
31
|
+
*/
|
|
32
|
+
export const cornerSmoothingCss = `.${cornerSmoothClassName} { corner-shape: squircle; }`;
|
|
33
|
+
|
|
34
|
+
/** Idempotently mount the corner-smoothing stylesheet (no-op off-DOM). */
|
|
35
|
+
export function ensureCornerSmoothingStyles(): void {
|
|
36
|
+
if (typeof document === "undefined") return;
|
|
37
|
+
if (document.getElementById(STYLE_TAG_ID)) return;
|
|
38
|
+
const tag = document.createElement("style");
|
|
39
|
+
tag.id = STYLE_TAG_ID;
|
|
40
|
+
tag.textContent = cornerSmoothingCss;
|
|
41
|
+
document.head.appendChild(tag);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Static CSS (no knob/theme dependency) — mount as soon as the module loads
|
|
45
|
+
// in a DOM environment so the class resolves before any smooth render.
|
|
46
|
+
ensureCornerSmoothingStyles();
|
|
@@ -9,17 +9,21 @@ import {
|
|
|
9
9
|
type ThemeBuilderDefinition,
|
|
10
10
|
createThemesBuilder,
|
|
11
11
|
} from "./createThemes";
|
|
12
|
+
import { defaultAccentTheme } from "./defaults/accent";
|
|
13
|
+
import { defaultBaseTheme } from "./defaults/base";
|
|
14
|
+
import { defaultBuilderOptions } from "./defaults/builderOptions";
|
|
15
|
+
import { defaultBodyFont, defaultHeadingFont } from "./defaults/fonts";
|
|
12
16
|
import type { Preset } from "./preset.types";
|
|
13
17
|
import { type ThemeConfig, createThemeConfig } from "./shared";
|
|
14
18
|
|
|
15
19
|
export interface CreateDefaultThemeConfigOptions {
|
|
16
|
-
/** Base theme palette (dark/light with optional shadows) */
|
|
17
|
-
base
|
|
18
|
-
/** Accent theme palette (defaults to
|
|
20
|
+
/** Base theme palette (dark/light with optional shadows; defaults to the built-in neutral ramps) */
|
|
21
|
+
base?: BaseThemeBuilderDefinition;
|
|
22
|
+
/** Accent theme palette (defaults to the built-in accent ramps) */
|
|
19
23
|
accent?: ThemeBuilderDefinition;
|
|
20
24
|
/** Map of extra named themes with their palettes */
|
|
21
25
|
additionalThemes?: Record<string, ThemeBuilderDefinition>;
|
|
22
|
-
/** Font definitions */
|
|
26
|
+
/** Font definitions (merged over the default heading/body fonts) */
|
|
23
27
|
fonts?: Record<string, GenericFont>;
|
|
24
28
|
/** Animation config (defaults to framework reanimated animations) */
|
|
25
29
|
animations?: CreateTamaguiProps["animations"];
|
|
@@ -35,9 +39,24 @@ export interface CreateDefaultThemeConfigOptions {
|
|
|
35
39
|
defaultPreset?: string;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Builds a complete Tamagui theme config with batteries included: calling it
|
|
44
|
+
* with zero options yields the framework's polished defaults — the neutral
|
|
45
|
+
* Radix-mauve-curve base ramps, the solid-surface brand accent ramps, Inter
|
|
46
|
+
* heading/body fonts and the interaction-ramp `getTheme` theme builder.
|
|
47
|
+
* Every option overrides its piece independently: `base`, `accent` and
|
|
48
|
+
* `builderOptions` each replace their default, while `fonts` are spread OVER
|
|
49
|
+
* the defaults so a partial font map keeps the default heading/body.
|
|
50
|
+
*/
|
|
51
|
+
export function createDefaultThemeConfig(
|
|
52
|
+
options: CreateDefaultThemeConfigOptions = {},
|
|
53
|
+
): ThemeConfig {
|
|
54
|
+
const base = options.base ?? defaultBaseTheme;
|
|
55
|
+
const accent = options.accent ?? defaultAccentTheme;
|
|
56
|
+
const themesBuilder = createThemesBuilder(base, accent, {
|
|
57
|
+
...defaultBuilderOptions,
|
|
58
|
+
...options.builderOptions,
|
|
59
|
+
});
|
|
41
60
|
const builtThemes: Record<string, Record<string, string>> = {
|
|
42
61
|
...themesBuilder.themes(),
|
|
43
62
|
};
|
|
@@ -57,7 +76,12 @@ export function createDefaultThemeConfig(options: CreateDefaultThemeConfigOption
|
|
|
57
76
|
...defaultConfig,
|
|
58
77
|
animations: options.animations ?? animations,
|
|
59
78
|
disableSSR: false,
|
|
60
|
-
fonts:
|
|
79
|
+
fonts: {
|
|
80
|
+
...defaultConfig.fonts,
|
|
81
|
+
heading: defaultHeadingFont,
|
|
82
|
+
body: defaultBodyFont,
|
|
83
|
+
...options.fonts,
|
|
84
|
+
},
|
|
61
85
|
shorthands,
|
|
62
86
|
shouldAddPrefersColorThemes: true,
|
|
63
87
|
themeClassNameOnRoot: true,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createThemeConfig specs — the app-level config factory. Locks down the
|
|
3
|
+
* `themeColors` extraction contract (a color exists only when BOTH
|
|
4
|
+
* light_/dark_ variants exist, semantic/state sub-themes are excluded,
|
|
5
|
+
* output is sorted), the root-theme-class settings defaults, and preset
|
|
6
|
+
* registration through the shared registry.
|
|
7
|
+
*
|
|
8
|
+
* `createTamagui` is mocked pass-through: this spec pins OUR factory's
|
|
9
|
+
* behavior, not Tamagui's config processing.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
|
|
14
|
+
vi.mock("tamagui", () => ({
|
|
15
|
+
// Identity-ish mock: createThemeConfig only forwards the object and reads
|
|
16
|
+
// nothing back from Tamagui's processing.
|
|
17
|
+
createTamagui: vi.fn((config: Record<string, unknown>) => config),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { createTamagui } from "tamagui";
|
|
21
|
+
import type { ThemeName } from "@tamagui/web";
|
|
22
|
+
import { defaultKnobs } from "./knobs";
|
|
23
|
+
import type { Preset } from "./preset.types";
|
|
24
|
+
import { clearPresets, createThemeConfig, getPreset, getPresetNames } from "./shared";
|
|
25
|
+
|
|
26
|
+
const mockedCreateTamagui = vi.mocked(createTamagui);
|
|
27
|
+
|
|
28
|
+
function makeThemes(names: string[]): Record<string, object> {
|
|
29
|
+
return Object.fromEntries(names.map((name) => [name, {}]));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makePreset(): Preset {
|
|
33
|
+
return {
|
|
34
|
+
theme: "light" as ThemeName,
|
|
35
|
+
knobs: { ...defaultKnobs },
|
|
36
|
+
intents: {},
|
|
37
|
+
tints: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
clearPresets();
|
|
43
|
+
mockedCreateTamagui.mockClear();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("createThemeConfig — themeColors extraction", () => {
|
|
47
|
+
it("extracts colors that exist in both light and dark variants, sorted", () => {
|
|
48
|
+
const config = createThemeConfig({
|
|
49
|
+
themes: makeThemes(["light_blue", "dark_blue", "light_red", "dark_red", "light", "dark"]),
|
|
50
|
+
} as never);
|
|
51
|
+
expect(config.themeColors).toEqual(["blue", "red"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("drops colors missing one scheme", () => {
|
|
55
|
+
const config = createThemeConfig({
|
|
56
|
+
themes: makeThemes(["light_blue", "dark_blue", "light_orphan"]),
|
|
57
|
+
} as never);
|
|
58
|
+
expect(config.themeColors).toEqual(["blue"]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("excludes semantic and state sub-themes from the picker list", () => {
|
|
62
|
+
const config = createThemeConfig({
|
|
63
|
+
themes: makeThemes([
|
|
64
|
+
"light_blue",
|
|
65
|
+
"dark_blue",
|
|
66
|
+
"light_error",
|
|
67
|
+
"dark_error",
|
|
68
|
+
"light_success",
|
|
69
|
+
"dark_success",
|
|
70
|
+
"light_warning",
|
|
71
|
+
"dark_warning",
|
|
72
|
+
"light_accent",
|
|
73
|
+
"dark_accent",
|
|
74
|
+
"light_active",
|
|
75
|
+
"dark_active",
|
|
76
|
+
"light_alt1",
|
|
77
|
+
"dark_alt1",
|
|
78
|
+
"light_alt2",
|
|
79
|
+
"dark_alt2",
|
|
80
|
+
]),
|
|
81
|
+
} as never);
|
|
82
|
+
expect(config.themeColors).toEqual(["blue"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("ignores mixed-case and nested sub-theme names", () => {
|
|
86
|
+
const config = createThemeConfig({
|
|
87
|
+
themes: makeThemes([
|
|
88
|
+
"light_blue",
|
|
89
|
+
"dark_blue",
|
|
90
|
+
"light_Blue",
|
|
91
|
+
"dark_Blue",
|
|
92
|
+
"light_blue_Button",
|
|
93
|
+
"dark_blue_Button",
|
|
94
|
+
]),
|
|
95
|
+
} as never);
|
|
96
|
+
expect(config.themeColors).toEqual(["blue"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns an empty list when the config carries no themes", () => {
|
|
100
|
+
const config = createThemeConfig({} as never);
|
|
101
|
+
expect(config.themeColors).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("createThemeConfig — settings and registration", () => {
|
|
106
|
+
it("defaults the root theme class settings on and keeps user settings override", () => {
|
|
107
|
+
createThemeConfig({ themes: {} } as never);
|
|
108
|
+
const forwarded = mockedCreateTamagui.mock.calls[0][0] as Record<string, any>;
|
|
109
|
+
expect(forwarded.settings.disableRootThemeClass).toBe(false);
|
|
110
|
+
expect(forwarded.settings.themeClassNameOnRoot).toBe(true);
|
|
111
|
+
|
|
112
|
+
mockedCreateTamagui.mockClear();
|
|
113
|
+
createThemeConfig({
|
|
114
|
+
themes: {},
|
|
115
|
+
settings: { themeClassNameOnRoot: false },
|
|
116
|
+
} as never);
|
|
117
|
+
const overridden = mockedCreateTamagui.mock.calls[0][0] as Record<string, any>;
|
|
118
|
+
expect(overridden.settings.themeClassNameOnRoot).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("registers provided presets in the shared registry", () => {
|
|
122
|
+
const material = makePreset();
|
|
123
|
+
const config = createThemeConfig({ themes: {} } as never, {
|
|
124
|
+
presets: { material },
|
|
125
|
+
defaultPreset: "material",
|
|
126
|
+
});
|
|
127
|
+
expect(getPresetNames()).toContain("material");
|
|
128
|
+
expect(getPreset("material")).toBe(material);
|
|
129
|
+
expect(config.defaultPreset).toBe("material");
|
|
130
|
+
expect(config.presets).toEqual({ material });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("registers nothing without presets", () => {
|
|
134
|
+
createThemeConfig({ themes: {} } as never);
|
|
135
|
+
expect(getPresetNames()).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -58,6 +58,81 @@ export interface CreateThemesBuilderOptions {
|
|
|
58
58
|
getTheme?: (props: GetThemeProps) => Record<string, string | number>;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Radix hue names whose sub-themes act as decorative TINTS (Tint.tsx cycling,
|
|
62
|
+
// the storybook `color` global, `theme="red"` field tinting). Under a tint,
|
|
63
|
+
// surfaces/borders/solids keep the hue but TEXT must stay readable neutral:
|
|
64
|
+
// muted/secondary text and user-authored content must never render saturated
|
|
65
|
+
// accent (SB-D-14/SB-D-20). Exported so theme-identity consumers (chart
|
|
66
|
+
// palette) share the one tint registry.
|
|
67
|
+
export const tintHueNames = new Set([
|
|
68
|
+
"amber",
|
|
69
|
+
"blue",
|
|
70
|
+
"bronze",
|
|
71
|
+
"brown",
|
|
72
|
+
"crimson",
|
|
73
|
+
"cyan",
|
|
74
|
+
"gold",
|
|
75
|
+
"grass",
|
|
76
|
+
"gray",
|
|
77
|
+
"green",
|
|
78
|
+
"indigo",
|
|
79
|
+
"lime",
|
|
80
|
+
"mauve",
|
|
81
|
+
"mint",
|
|
82
|
+
"olive",
|
|
83
|
+
"orange",
|
|
84
|
+
"pink",
|
|
85
|
+
"plum",
|
|
86
|
+
"purple",
|
|
87
|
+
"red",
|
|
88
|
+
"sage",
|
|
89
|
+
"sand",
|
|
90
|
+
"sky",
|
|
91
|
+
"slate",
|
|
92
|
+
"teal",
|
|
93
|
+
"tomato",
|
|
94
|
+
"violet",
|
|
95
|
+
"yellow",
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
// Text-tier tokens re-anchored to the neutral scheme ramp inside tint themes.
|
|
99
|
+
const tintNeutralTextKeys = [
|
|
100
|
+
"color",
|
|
101
|
+
"colorHover",
|
|
102
|
+
"colorPress",
|
|
103
|
+
"colorFocus",
|
|
104
|
+
"color11",
|
|
105
|
+
"color12",
|
|
106
|
+
"placeholderColor",
|
|
107
|
+
] as const;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Rebuild tint sub-themes (light_purple, dark_red_active, ...) so their
|
|
111
|
+
* text tiers come from the neutral base scheme while backgrounds, borders
|
|
112
|
+
* and solid steps (1-10) keep the hue. Only keys the tint theme already
|
|
113
|
+
* defines are replaced; returns new objects, never mutates inputs.
|
|
114
|
+
*/
|
|
115
|
+
function neutralizeTintText(
|
|
116
|
+
themes: Record<string, Record<string, string>>,
|
|
117
|
+
): Record<string, Record<string, string>> {
|
|
118
|
+
const out: Record<string, Record<string, string>> = { ...themes };
|
|
119
|
+
for (const [name, theme] of Object.entries(themes)) {
|
|
120
|
+
const parts = name.split("_");
|
|
121
|
+
if (parts.length < 2) continue;
|
|
122
|
+
const scheme = parts[0];
|
|
123
|
+
if (scheme !== "light" && scheme !== "dark") continue;
|
|
124
|
+
if (!tintHueNames.has(parts[1])) continue;
|
|
125
|
+
const base = themes[scheme];
|
|
126
|
+
if (!base) continue;
|
|
127
|
+
const patched: Record<string, string> = { ...theme };
|
|
128
|
+
for (const key of tintNeutralTextKeys) {
|
|
129
|
+
if (key in patched && base[key] != null) patched[key] = base[key];
|
|
130
|
+
}
|
|
131
|
+
out[name] = patched;
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
61
136
|
const defaultLightShadows: Shadows = {
|
|
62
137
|
shadow1: "rgba(0,0,0,0.12)",
|
|
63
138
|
shadow2: "rgba(0,0,0,0.16)",
|
|
@@ -248,11 +323,11 @@ export function createThemesBuilder(
|
|
|
248
323
|
const initialTheme = tamaguiCreateThemes(
|
|
249
324
|
createThemesProps as Parameters<typeof tamaguiCreateThemes>[0],
|
|
250
325
|
);
|
|
251
|
-
const themes = {
|
|
326
|
+
const themes = neutralizeTintText({
|
|
252
327
|
...grayTheme,
|
|
253
328
|
...tamaguiThemes,
|
|
254
329
|
...initialTheme,
|
|
255
|
-
};
|
|
330
|
+
});
|
|
256
331
|
return {
|
|
257
332
|
themes() {
|
|
258
333
|
return themes;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ThemeBuilderDefinition } from "../createThemes";
|
|
2
|
+
|
|
3
|
+
export const defaultAccentTheme: ThemeBuilderDefinition = {
|
|
4
|
+
darkPalette: [
|
|
5
|
+
"hsla(250, 50%, 35%, 1)",
|
|
6
|
+
"hsla(250, 50%, 38%, 1)",
|
|
7
|
+
"hsla(250, 50%, 41%, 1)",
|
|
8
|
+
"hsla(250, 50%, 43%, 1)",
|
|
9
|
+
"hsla(250, 50%, 46%, 1)",
|
|
10
|
+
"hsla(250, 50%, 49%, 1)",
|
|
11
|
+
"hsla(250, 50%, 52%, 1)",
|
|
12
|
+
"hsla(250, 50%, 54%, 1)",
|
|
13
|
+
"hsla(250, 50%, 57%, 1)",
|
|
14
|
+
"hsla(250, 50%, 60%, 1)",
|
|
15
|
+
"hsla(250, 50%, 90%, 1)",
|
|
16
|
+
"hsla(250, 50%, 95%, 1)",
|
|
17
|
+
],
|
|
18
|
+
// Mirrors the darkPalette's shape: SOLID brand surfaces across [0..9] with
|
|
19
|
+
// near-white foreground anchors at [10..11]. The theme-builder takes
|
|
20
|
+
// palette[11] as the sub-theme `color` and palette[3] as the Button
|
|
21
|
+
// surface — the old ramp (95%->40%, no light anchor) put 40%-purple text
|
|
22
|
+
// on a 62%-purple button (2.3:1). Solid-with-white matches how tamagui v4
|
|
23
|
+
// ships its accent (bg 9%, color white, 17.9:1); here: button 48%/98%
|
|
24
|
+
// = 7.7:1.
|
|
25
|
+
lightPalette: [
|
|
26
|
+
"hsla(250, 50%, 62%, 1)",
|
|
27
|
+
"hsla(250, 50%, 58%, 1)",
|
|
28
|
+
"hsla(250, 50%, 54%, 1)",
|
|
29
|
+
"hsla(250, 50%, 48%, 1)",
|
|
30
|
+
"hsla(250, 50%, 46%, 1)",
|
|
31
|
+
"hsla(250, 50%, 44%, 1)",
|
|
32
|
+
"hsla(250, 50%, 42%, 1)",
|
|
33
|
+
"hsla(250, 50%, 40%, 1)",
|
|
34
|
+
"hsla(250, 50%, 38%, 1)",
|
|
35
|
+
"hsla(250, 50%, 36%, 1)",
|
|
36
|
+
"hsla(250, 50%, 95%, 1)",
|
|
37
|
+
"hsla(250, 50%, 98%, 1)",
|
|
38
|
+
],
|
|
39
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { BaseThemeBuilderDefinition } from "../createThemes";
|
|
2
|
+
|
|
3
|
+
export const defaultBaseTheme: BaseThemeBuilderDefinition = {
|
|
4
|
+
// Radix-mauve-shaped neutral ramps with a slight purple tint (hue 270)
|
|
5
|
+
// harmonizing with the hsla(250) accent: near-white surfaces (1-2),
|
|
6
|
+
// component/hover steps (3-5), borders (6-8), solid/muted (9-10),
|
|
7
|
+
// readable text (11-12).
|
|
8
|
+
//
|
|
9
|
+
// Step 10 (muted-ink tier) is the catalog's de-facto "muted text" step —
|
|
10
|
+
// placeholderColor, and ~20 components/stories set `color="$color10"` on
|
|
11
|
+
// hints, captions, counts, subtitles and placeholders (UX-P01/UX-015).
|
|
12
|
+
// At its old lightness (light 52% / dark 48%) it measured 3.76–3.98:1 on
|
|
13
|
+
// color1/color2 — under the AA 4.5:1 floor for normal text (Axiom 12
|
|
14
|
+
// LEGIBLE FLOOR). Re-anchored to light 46% / dark 54%, the lowest change
|
|
15
|
+
// that clears 4.5:1 on BOTH page surfaces in both schemes (light 4.9/4.7,
|
|
16
|
+
// dark 4.8/4.6) while staying a distinct tier above color11. Its non-text
|
|
17
|
+
// roles (boundary rings, spinners, slider thumb — all ≥3:1 targets) only
|
|
18
|
+
// gain contrast, so the shift is one-directional safe.
|
|
19
|
+
darkPalette: [
|
|
20
|
+
"hsla(270, 6%, 8.7%, 1)",
|
|
21
|
+
"hsla(270, 6%, 10.5%, 1)",
|
|
22
|
+
"hsla(270, 6%, 13.5%, 1)",
|
|
23
|
+
"hsla(270, 6%, 16%, 1)",
|
|
24
|
+
"hsla(270, 6%, 18.5%, 1)",
|
|
25
|
+
"hsla(270, 6%, 21.5%, 1)",
|
|
26
|
+
"hsla(270, 6%, 25.5%, 1)",
|
|
27
|
+
"hsla(270, 6%, 32%, 1)",
|
|
28
|
+
"hsla(270, 6%, 43%, 1)",
|
|
29
|
+
"hsla(270, 6%, 54%, 1)",
|
|
30
|
+
"hsla(270, 6%, 62.5%, 1)",
|
|
31
|
+
"hsla(270, 6%, 94%, 1)",
|
|
32
|
+
],
|
|
33
|
+
lightPalette: [
|
|
34
|
+
"hsla(270, 6%, 99.5%, 1)",
|
|
35
|
+
"hsla(270, 6%, 98.2%, 1)",
|
|
36
|
+
"hsla(270, 6%, 95.2%, 1)",
|
|
37
|
+
"hsla(270, 6%, 92.5%, 1)",
|
|
38
|
+
"hsla(270, 6%, 90%, 1)",
|
|
39
|
+
"hsla(270, 6%, 87%, 1)",
|
|
40
|
+
"hsla(270, 6%, 82.5%, 1)",
|
|
41
|
+
"hsla(270, 6%, 75%, 1)",
|
|
42
|
+
"hsla(270, 6%, 56.5%, 1)",
|
|
43
|
+
"hsla(270, 6%, 46%, 1)",
|
|
44
|
+
"hsla(270, 6%, 40%, 1)",
|
|
45
|
+
"hsla(270, 6%, 14%, 1)",
|
|
46
|
+
],
|
|
47
|
+
lightShadows: {
|
|
48
|
+
shadow1: "rgba(0,0,0,0.04)",
|
|
49
|
+
shadow2: "rgba(0,0,0,0.08)",
|
|
50
|
+
shadow3: "rgba(0,0,0,0.16)",
|
|
51
|
+
shadow4: "rgba(0,0,0,0.24)",
|
|
52
|
+
shadow5: "rgba(0,0,0,0.32)",
|
|
53
|
+
shadow6: "rgba(0,0,0,0.4)",
|
|
54
|
+
},
|
|
55
|
+
darkShadows: {
|
|
56
|
+
shadow1: "rgba(0,0,0,0.2)",
|
|
57
|
+
shadow2: "rgba(0,0,0,0.3)",
|
|
58
|
+
shadow3: "rgba(0,0,0,0.4)",
|
|
59
|
+
shadow4: "rgba(0,0,0,0.5)",
|
|
60
|
+
shadow5: "rgba(0,0,0,0.6)",
|
|
61
|
+
shadow6: "rgba(0,0,0,0.7)",
|
|
62
|
+
},
|
|
63
|
+
};
|