@multiplatform.one/theme 6.0.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +201 -0
  3. package/package.json +85 -0
  4. package/src/audit/baselines/home-login.json +171 -0
  5. package/src/audit/baselines/home-login.png +0 -0
  6. package/src/audit/baselines/home-pokemon.json +355 -0
  7. package/src/audit/baselines/home-pokemon.png +0 -0
  8. package/src/audit/baselines/home.json +194 -0
  9. package/src/audit/baselines/home.png +0 -0
  10. package/src/audit/baselines/pokemon.json +251 -0
  11. package/src/audit/baselines/pokemon.png +0 -0
  12. package/src/audit/constraintAudit.ts +874 -0
  13. package/src/audit/diffReport.spec.ts +191 -0
  14. package/src/audit/diffReport.ts +344 -0
  15. package/src/audit/index.ts +10 -0
  16. package/src/font.ts +49 -0
  17. package/src/fonts/createFontLoader.ts +15 -0
  18. package/src/fonts/createFontLoader.web.ts +9 -0
  19. package/src/fonts/index.ts +2 -0
  20. package/src/fonts/types.ts +13 -0
  21. package/src/index.ts +3 -0
  22. package/src/theme/Intent.tsx +30 -0
  23. package/src/theme/Preset.tsx +93 -0
  24. package/src/theme/PresetContext.ts +27 -0
  25. package/src/theme/Tint.tsx +44 -0
  26. package/src/theme/__snapshots__/resolveKnobs.spec.ts.snap +69 -0
  27. package/src/theme/animations/css.ts +25 -0
  28. package/src/theme/animations/index.ts +1 -0
  29. package/src/theme/animations/index.web.ts +1 -0
  30. package/src/theme/animations/reactNative.ts +54 -0
  31. package/src/theme/colorRules.spec.ts +195 -0
  32. package/src/theme/colorRules.ts +166 -0
  33. package/src/theme/cookies.spec.ts +185 -0
  34. package/src/theme/cookies.ts +209 -0
  35. package/src/theme/createDefaultThemeConfig.ts +86 -0
  36. package/src/theme/createThemes.ts +286 -0
  37. package/src/theme/devtools/ColorLineVisualizer.tsx +261 -0
  38. package/src/theme/devtools/ThemeDevtoolsPanel.tsx +1657 -0
  39. package/src/theme/devtools/buildSnapshot.spec.ts +113 -0
  40. package/src/theme/devtools/buildSnapshot.ts +103 -0
  41. package/src/theme/devtools/knobIcons.tsx +217 -0
  42. package/src/theme/devtools/vizConstants.spec.ts +58 -0
  43. package/src/theme/devtools/vizConstants.ts +28 -0
  44. package/src/theme/index.ts +21 -0
  45. package/src/theme/intent.spec.tsx +276 -0
  46. package/src/theme/intents.ts +22 -0
  47. package/src/theme/knobs.ts +147 -0
  48. package/src/theme/pokemonScreen.spec.ts +109 -0
  49. package/src/theme/preset.spec.tsx +242 -0
  50. package/src/theme/preset.types.ts +22 -0
  51. package/src/theme/presets.spec.ts +252 -0
  52. package/src/theme/presets.ts +49 -0
  53. package/src/theme/recipes.ts +69 -0
  54. package/src/theme/resolveKnobs.spec.ts +571 -0
  55. package/src/theme/resolveKnobs.ts +226 -0
  56. package/src/theme/shared.spec.ts +84 -0
  57. package/src/theme/shared.tsx +131 -0
  58. package/src/theme/theme.native.tsx +65 -0
  59. package/src/theme/theme.tsx +190 -0
  60. package/src/theme/tint.spec.tsx +179 -0
  61. package/src/theme/useColorScale.native.ts +7 -0
  62. package/src/theme/useColorScale.ts +29 -0
  63. package/src/theme/useResolvedKnobs.ts +121 -0
  64. package/src/theme/useTheme.native.ts +49 -0
  65. package/src/theme/useTheme.ts +213 -0
  66. package/tsconfig.json +11 -0
  67. package/vitest.config.mjs +9 -0
@@ -0,0 +1,242 @@
1
+ import { type ReactNode, createElement } from "react";
2
+ import { renderToString } from "react-dom/server";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ // Mock Tamagui dependencies to avoid needing a full Tamagui config
6
+ vi.mock("tamagui", () => ({
7
+ Theme: ({ name, children }: { name: string; children: ReactNode }) =>
8
+ createElement("div", { "data-tamagui-theme": name }, children),
9
+ }));
10
+
11
+ import type { ThemeName } from "@tamagui/web";
12
+ import { Preset } from "./Preset";
13
+ import { type PresetContextValue, usePresetContext } from "./PresetContext";
14
+ import { defaultKnobs } from "./knobs";
15
+ import type { Preset as PresetType } from "./preset.types";
16
+ import { clearPresets, registerPreset } from "./shared";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Test fixtures
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const boldPreset: PresetType = {
23
+ theme: "blue" as ThemeName,
24
+ knobs: {
25
+ ...defaultKnobs,
26
+ fillStyle: "filled",
27
+ borderRadius: "large",
28
+ elevation: "large",
29
+ },
30
+ intents: {},
31
+ tints: ["blue" as ThemeName, "red" as ThemeName],
32
+ };
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Test helpers
36
+ // ---------------------------------------------------------------------------
37
+
38
+ function createPresetContextCapture() {
39
+ let captured: PresetContextValue | null = null;
40
+ function Capture() {
41
+ captured = usePresetContext();
42
+ return null;
43
+ }
44
+ return {
45
+ Capture,
46
+ get result() {
47
+ return captured;
48
+ },
49
+ };
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Tests
54
+ // ---------------------------------------------------------------------------
55
+
56
+ describe("Preset component", () => {
57
+ beforeEach(() => {
58
+ clearPresets();
59
+ });
60
+
61
+ // -------------------------------------------------------------------------
62
+ // 1. Default knobs
63
+ // -------------------------------------------------------------------------
64
+ it("provides default knobs when no preset is specified", () => {
65
+ const capture = createPresetContextCapture();
66
+
67
+ renderToString(createElement(Preset, null, createElement(capture.Capture)));
68
+
69
+ expect(capture.result?.preset.knobs).toEqual(defaultKnobs);
70
+ });
71
+
72
+ // -------------------------------------------------------------------------
73
+ // 2. Overrides merge on top of defaults
74
+ // -------------------------------------------------------------------------
75
+ it("merges overrides on top of defaults", () => {
76
+ const capture = createPresetContextCapture();
77
+
78
+ renderToString(
79
+ createElement(
80
+ Preset,
81
+ { overrides: { borderRadius: "full" } },
82
+ createElement(capture.Capture),
83
+ ),
84
+ );
85
+
86
+ expect(capture.result?.preset.knobs.borderRadius).toBe("full");
87
+ expect(capture.result?.preset.knobs.fillStyle).toBe(defaultKnobs.fillStyle);
88
+ });
89
+
90
+ // -------------------------------------------------------------------------
91
+ // 3. Cascading — child overrides merge on top of parent
92
+ // -------------------------------------------------------------------------
93
+ it("cascades child overrides on top of parent", () => {
94
+ const capture = createPresetContextCapture();
95
+
96
+ renderToString(
97
+ createElement(
98
+ Preset,
99
+ { overrides: { borderRadius: "full" } },
100
+ createElement(
101
+ Preset,
102
+ { overrides: { elevation: "large" } },
103
+ createElement(capture.Capture),
104
+ ),
105
+ ),
106
+ );
107
+
108
+ expect(capture.result?.preset.knobs.borderRadius).toBe("full");
109
+ expect(capture.result?.preset.knobs.elevation).toBe("large");
110
+ });
111
+
112
+ // -------------------------------------------------------------------------
113
+ // 4. cascade=false replaces parent entirely
114
+ // -------------------------------------------------------------------------
115
+ it("cascade={false} replaces parent entirely", () => {
116
+ const capture = createPresetContextCapture();
117
+
118
+ renderToString(
119
+ createElement(
120
+ Preset,
121
+ { overrides: { borderRadius: "full" } },
122
+ createElement(
123
+ Preset,
124
+ { cascade: false, overrides: { elevation: "large" } },
125
+ createElement(capture.Capture),
126
+ ),
127
+ ),
128
+ );
129
+
130
+ expect(capture.result?.preset.knobs.borderRadius).toBe(defaultKnobs.borderRadius);
131
+ expect(capture.result?.preset.knobs.elevation).toBe("large");
132
+ });
133
+
134
+ // -------------------------------------------------------------------------
135
+ // 5. Named preset resolution
136
+ // -------------------------------------------------------------------------
137
+ it("resolves a named preset from the registry", () => {
138
+ registerPreset("bold", boldPreset);
139
+ const capture = createPresetContextCapture();
140
+
141
+ renderToString(createElement(Preset, { preset: "bold" }, createElement(capture.Capture)));
142
+
143
+ expect(capture.result?.preset.knobs.borderRadius).toBe("large");
144
+ expect(capture.result?.preset.knobs.elevation).toBe("large");
145
+ expect(capture.result?.preset.theme).toBe("blue");
146
+ });
147
+
148
+ // -------------------------------------------------------------------------
149
+ // 6. Named preset with overrides
150
+ // -------------------------------------------------------------------------
151
+ it("applies overrides on top of a named preset", () => {
152
+ registerPreset("bold", boldPreset);
153
+ const capture = createPresetContextCapture();
154
+
155
+ renderToString(
156
+ createElement(
157
+ Preset,
158
+ { preset: "bold", overrides: { borderRadius: "none" } },
159
+ createElement(capture.Capture),
160
+ ),
161
+ );
162
+
163
+ expect(capture.result?.preset.knobs.borderRadius).toBe("none");
164
+ expect(capture.result?.preset.knobs.elevation).toBe("large");
165
+ });
166
+
167
+ // -------------------------------------------------------------------------
168
+ // 7. Tamagui <Theme /> wrapping
169
+ // -------------------------------------------------------------------------
170
+ it("wraps children with Tamagui <Theme /> when theme prop is set", () => {
171
+ const capture = createPresetContextCapture();
172
+
173
+ const html = renderToString(
174
+ createElement(Preset, { theme: "blue" as ThemeName }, createElement(capture.Capture)),
175
+ );
176
+
177
+ expect(html).toContain('data-tamagui-theme="blue"');
178
+ });
179
+
180
+ it("does not wrap with Tamagui <Theme /> when theme prop is absent", () => {
181
+ const capture = createPresetContextCapture();
182
+
183
+ const html = renderToString(createElement(Preset, null, createElement(capture.Capture)));
184
+
185
+ expect(html).not.toContain("data-tamagui-theme");
186
+ });
187
+
188
+ // -------------------------------------------------------------------------
189
+ // 8. Cascading named preset inside parent
190
+ // -------------------------------------------------------------------------
191
+ it("cascades a named preset on top of parent overrides", () => {
192
+ registerPreset("bold", boldPreset);
193
+ const capture = createPresetContextCapture();
194
+
195
+ renderToString(
196
+ createElement(
197
+ Preset,
198
+ { overrides: { fillStyle: "outlined" } },
199
+ createElement(Preset, { preset: "bold" }, createElement(capture.Capture)),
200
+ ),
201
+ );
202
+
203
+ // Named preset replaces base knobs in cascade mode
204
+ expect(capture.result?.preset.knobs.borderRadius).toBe("large");
205
+ // Parent override is carried forward
206
+ expect(capture.result?.preset.knobs.fillStyle).toBe("outlined");
207
+ });
208
+
209
+ // -------------------------------------------------------------------------
210
+ // 9. Unknown preset name falls back to defaults
211
+ // -------------------------------------------------------------------------
212
+ it("falls back to default knobs when preset name is not registered", () => {
213
+ const capture = createPresetContextCapture();
214
+
215
+ renderToString(
216
+ createElement(Preset, { preset: "nonexistent" }, createElement(capture.Capture)),
217
+ );
218
+
219
+ expect(capture.result?.preset.knobs).toEqual(defaultKnobs);
220
+ });
221
+
222
+ // -------------------------------------------------------------------------
223
+ // 10. scope prop does not exist on the type
224
+ // -------------------------------------------------------------------------
225
+ it("does not accept scope prop", () => {
226
+ // TypeScript-level check: `scope` should not be part of PresetProps.
227
+ // If this file compiles cleanly, the type check passes.
228
+ // We verify at runtime that passing an unknown prop doesn't break anything.
229
+ const capture = createPresetContextCapture();
230
+
231
+ renderToString(
232
+ createElement(
233
+ Preset,
234
+ // @ts-expect-error — scope is intentionally removed from PresetProps
235
+ { scope: "Form" },
236
+ createElement(capture.Capture),
237
+ ),
238
+ );
239
+
240
+ expect(capture.result?.preset.knobs).toEqual(defaultKnobs);
241
+ });
242
+ });
@@ -0,0 +1,22 @@
1
+ import type { ThemeName } from "@tamagui/web";
2
+ import type { Knobs } from "./knobs";
3
+
4
+ export interface Preset {
5
+ /** Tamagui color theme (accent color). */
6
+ theme: ThemeName;
7
+ /** Base structural knobs. */
8
+ knobs: Knobs;
9
+ /** Intent-specific knob overrides. */
10
+ intents: {
11
+ accent?: IntentOverride;
12
+ error?: IntentOverride;
13
+ warning?: IntentOverride;
14
+ success?: IntentOverride;
15
+ };
16
+ /** Tint sequence for color cycling across nested regions. */
17
+ tints: ThemeName[];
18
+ }
19
+
20
+ export type IntentOverride = Partial<Knobs> & {
21
+ [component: string]: Partial<Knobs> | string | undefined;
22
+ };
@@ -0,0 +1,252 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import type { ThemeName } from "@tamagui/web";
3
+ import type { Preset } from "./preset.types";
4
+ import {
5
+ registerPreset,
6
+ getPreset,
7
+ getPresetNames,
8
+ clearPresets,
9
+ resolveThemeFromPreset,
10
+ } from "./shared";
11
+ import {
12
+ cookiePreset,
13
+ cookieOverridesPrefix,
14
+ splitCookieValue,
15
+ joinSplitCookieValues,
16
+ } from "./cookies";
17
+ import { defaultIntents } from "./intents";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Preset fixtures
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const minimalPreset: Preset = {
24
+ theme: "gray" as ThemeName,
25
+ knobs: {
26
+ fillStyle: "outlined",
27
+ borderRadius: "none",
28
+ borderWidth: "none",
29
+ elevation: "none",
30
+ space: "small",
31
+ size: "small",
32
+ textAccent: "low",
33
+ headingFont: "sans-serif",
34
+ bodyFont: "sans-serif",
35
+ fontWeight: "regular",
36
+ animation: "none",
37
+ hover: {},
38
+ press: {},
39
+ focus: {},
40
+ focusVisible: {},
41
+ },
42
+ intents: defaultIntents,
43
+ tints: ["gray"] as ThemeName[],
44
+ };
45
+
46
+ const boldPreset: Preset = {
47
+ theme: "blue" as ThemeName,
48
+ knobs: {
49
+ fillStyle: "filled",
50
+ borderRadius: "full",
51
+ borderWidth: "large",
52
+ elevation: "medium",
53
+ space: "large",
54
+ size: "large",
55
+ textAccent: "high",
56
+ headingFont: "sans-serif",
57
+ bodyFont: "sans-serif",
58
+ fontWeight: "bold",
59
+ animation: "bouncy",
60
+ hover: {},
61
+ press: {},
62
+ focus: {},
63
+ focusVisible: {},
64
+ },
65
+ intents: defaultIntents,
66
+ tints: ["blue", "purple"] as ThemeName[],
67
+ };
68
+
69
+ const roundedPreset: Preset = {
70
+ theme: "purple" as ThemeName,
71
+ knobs: {
72
+ fillStyle: "filled",
73
+ borderRadius: "large",
74
+ borderWidth: "small",
75
+ elevation: "small",
76
+ space: "medium",
77
+ size: "medium",
78
+ textAccent: "high",
79
+ headingFont: "sans-serif",
80
+ bodyFont: "sans-serif",
81
+ fontWeight: "regular",
82
+ animation: "quick",
83
+ hover: {},
84
+ press: {},
85
+ focus: {},
86
+ focusVisible: {},
87
+ },
88
+ intents: defaultIntents,
89
+ tints: ["purple", "pink"] as ThemeName[],
90
+ };
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Tests
94
+ // ---------------------------------------------------------------------------
95
+
96
+ describe("Theme Preset System", () => {
97
+ beforeEach(() => {
98
+ clearPresets();
99
+ });
100
+
101
+ // -------------------------------------------------------------------------
102
+ // 1. Preset registration and lookup
103
+ // -------------------------------------------------------------------------
104
+ describe("preset registration and lookup", () => {
105
+ it("registers a preset and retrieves it by name", () => {
106
+ registerPreset("minimal", minimalPreset);
107
+ const result = getPreset("minimal");
108
+ expect(result).toEqual(minimalPreset);
109
+ });
110
+
111
+ it("returns undefined for an unregistered preset name", () => {
112
+ expect(getPreset("nonexistent")).toBeUndefined();
113
+ });
114
+
115
+ it("lists all registered preset names", () => {
116
+ registerPreset("minimal", minimalPreset);
117
+ registerPreset("bold", boldPreset);
118
+ registerPreset("rounded", roundedPreset);
119
+ const names = getPresetNames();
120
+ expect(names).toEqual(expect.arrayContaining(["minimal", "bold", "rounded"]));
121
+ expect(names).toHaveLength(3);
122
+ });
123
+
124
+ it("overwrites a preset when re-registered with the same name", () => {
125
+ registerPreset("minimal", minimalPreset);
126
+ const updated: Preset = {
127
+ ...minimalPreset,
128
+ knobs: { ...minimalPreset.knobs, space: "large" },
129
+ };
130
+ registerPreset("minimal", updated);
131
+ expect(getPreset("minimal")?.knobs.space).toBe("large");
132
+ });
133
+ });
134
+
135
+ // -------------------------------------------------------------------------
136
+ // 2. Cookie resolution order: preset config → overrides → active Theme
137
+ // -------------------------------------------------------------------------
138
+ describe("cookie resolution order", () => {
139
+ it("resolves theme from preset config when no overrides exist", () => {
140
+ registerPreset("minimal", minimalPreset);
141
+ const resolved = resolveThemeFromPreset("minimal", {});
142
+ expect(resolved.knobs.borderRadius).toBe("none");
143
+ expect(resolved.knobs.elevation).toBe("none");
144
+ expect(resolved.knobs.space).toBe("small");
145
+ });
146
+
147
+ it("merges overrides on top of preset config", () => {
148
+ registerPreset("minimal", minimalPreset);
149
+ const overrides = { borderRadius: "full" as const, space: "large" as const };
150
+ const resolved = resolveThemeFromPreset("minimal", overrides);
151
+ // Overrides win
152
+ expect(resolved.knobs.borderRadius).toBe("full");
153
+ expect(resolved.knobs.space).toBe("large");
154
+ // Preset defaults preserved for non-overridden props
155
+ expect(resolved.knobs.elevation).toBe("none");
156
+ expect(resolved.knobs.fillStyle).toBe("outlined");
157
+ });
158
+
159
+ it("falls back to default knobs when preset name is unknown", () => {
160
+ const resolved = resolveThemeFromPreset("nonexistent", {});
161
+ // Should not throw — returns default preset shape
162
+ expect(resolved).toBeDefined();
163
+ expect(resolved.knobs.borderRadius).toBeDefined();
164
+ });
165
+ });
166
+
167
+ // -------------------------------------------------------------------------
168
+ // 3. Switching preset resets overrides
169
+ // -------------------------------------------------------------------------
170
+ describe("switching preset resets overrides", () => {
171
+ it("resolveThemeFromPreset with a new preset ignores old overrides", () => {
172
+ registerPreset("minimal", minimalPreset);
173
+ registerPreset("bold", boldPreset);
174
+
175
+ // Start with minimal + overrides
176
+ const resolved1 = resolveThemeFromPreset("minimal", {
177
+ elevation: "medium",
178
+ });
179
+ expect(resolved1.knobs.elevation).toBe("medium");
180
+ expect(resolved1.knobs.borderRadius).toBe("none");
181
+
182
+ // Switch to bold — overrides should be empty (caller responsibility)
183
+ // When switching presets, the caller clears overrides
184
+ const resolved2 = resolveThemeFromPreset("bold", {});
185
+ expect(resolved2.knobs.elevation).toBe("medium"); // bold preset's value
186
+ expect(resolved2.knobs.borderRadius).toBe("full"); // bold preset's value
187
+ expect(resolved2.knobs.space).toBe("large"); // bold preset's value
188
+ });
189
+ });
190
+
191
+ // -------------------------------------------------------------------------
192
+ // 4. SSR preset resolution from cookies (synchronous)
193
+ // -------------------------------------------------------------------------
194
+ describe("SSR preset resolution from cookies", () => {
195
+ it("resolves synchronously from preset name and overrides", () => {
196
+ registerPreset("rounded", roundedPreset);
197
+
198
+ // Simulate SSR: we have raw cookie strings
199
+ const presetName = "rounded";
200
+ const overridesJson = JSON.stringify({ borderWidth: "large" });
201
+
202
+ // Resolution must be synchronous — no async, no promises
203
+ const overrides = JSON.parse(overridesJson);
204
+ const resolved = resolveThemeFromPreset(presetName, overrides);
205
+
206
+ expect(resolved.knobs.borderRadius).toBe("large"); // from preset
207
+ expect(resolved.knobs.borderWidth).toBe("large"); // from override
208
+ expect(resolved.theme).toBe("purple"); // from preset
209
+ });
210
+
211
+ it("handles empty overrides cookie gracefully", () => {
212
+ registerPreset("minimal", minimalPreset);
213
+ const resolved = resolveThemeFromPreset("minimal", {});
214
+ expect(resolved.knobs).toEqual(minimalPreset.knobs);
215
+ });
216
+ });
217
+
218
+ // -------------------------------------------------------------------------
219
+ // 5. mp.preset + mp.overrides cookie read/write
220
+ // -------------------------------------------------------------------------
221
+ describe("cookie splitting and reconstruction", () => {
222
+ it("splits a large cookie value into chunks", () => {
223
+ // Create a value larger than 3.5KB
224
+ const largeValue = JSON.stringify({
225
+ key: "x".repeat(4000),
226
+ });
227
+ const chunks = splitCookieValue(largeValue);
228
+ expect(chunks.length).toBeGreaterThan(1);
229
+ });
230
+
231
+ it("reconstructs a split cookie value from chunks", () => {
232
+ const original = JSON.stringify({
233
+ key: "x".repeat(4000),
234
+ });
235
+ const chunks = splitCookieValue(original);
236
+ const reconstructed = joinSplitCookieValues(chunks);
237
+ expect(reconstructed).toBe(original);
238
+ });
239
+
240
+ it("does not split a small cookie value", () => {
241
+ const smallValue = JSON.stringify({ borderRadius: "$6" });
242
+ const chunks = splitCookieValue(smallValue);
243
+ expect(chunks).toHaveLength(1);
244
+ expect(chunks[0]).toBe(smallValue);
245
+ });
246
+
247
+ it("cookie key constants are correct", () => {
248
+ expect(cookiePreset).toBe("mp.preset");
249
+ expect(cookieOverridesPrefix).toBe("mp.ov");
250
+ });
251
+ });
252
+ });
@@ -0,0 +1,49 @@
1
+ import type { ThemeName } from "@tamagui/web";
2
+ import { defaultIntents } from "./intents";
3
+ import type { Preset } from "./preset.types";
4
+
5
+ export const defaultPreset: Preset = {
6
+ theme: "" as ThemeName,
7
+ knobs: {
8
+ fillStyle: "filled",
9
+ borderRadius: "medium",
10
+ borderWidth: "small",
11
+ elevation: "none",
12
+ space: "medium",
13
+ size: "medium",
14
+ textAccent: "high",
15
+ headingFont: "sans-serif",
16
+ bodyFont: "sans-serif",
17
+ fontWeight: "regular",
18
+ animation: "quick",
19
+ hover: {},
20
+ press: {},
21
+ focus: {},
22
+ focusVisible: {},
23
+ },
24
+ intents: defaultIntents,
25
+ tints: ["orange", "yellow", "green", "blue", "purple", "pink", "red"] as ThemeName[],
26
+ };
27
+
28
+ export const boldPreset: Preset = {
29
+ theme: "pink" as ThemeName,
30
+ knobs: {
31
+ fillStyle: "filled",
32
+ borderRadius: "full",
33
+ borderWidth: "none",
34
+ elevation: "none",
35
+ space: "small",
36
+ size: "medium",
37
+ textAccent: "high",
38
+ headingFont: "sans-serif",
39
+ bodyFont: "sans-serif",
40
+ fontWeight: "bold",
41
+ animation: "bouncy",
42
+ hover: {},
43
+ press: {},
44
+ focus: {},
45
+ focusVisible: {},
46
+ },
47
+ intents: defaultIntents,
48
+ tints: ["pink", "purple", "pink", "purple", "pink", "purple", "pink"] as ThemeName[],
49
+ };
@@ -0,0 +1,69 @@
1
+ import type { TransitionProp } from "tamagui";
2
+ import type { Knobs } from "./knobs";
3
+
4
+ // ── State knob prop types (by concern) ───────────────────────────────────
5
+
6
+ export interface ControlStateProps {
7
+ borderRadius?: string;
8
+ borderWidth?: number;
9
+ backgroundColor?: string;
10
+ padding?: string;
11
+ }
12
+
13
+ export interface TextStateProps {
14
+ color?: string;
15
+ fontFamily?: string;
16
+ fontWeight?: string;
17
+ fontSize?: string;
18
+ }
19
+
20
+ export interface ElevationStateProps {
21
+ elevation?: string;
22
+ }
23
+
24
+ /**
25
+ * A recipe groups state-specific knob props by concern (control, text, elevation).
26
+ * Each value is the actual props object to assign to hoverStyle, pressStyle, etc.
27
+ */
28
+ export interface KnobRecipe<T> {
29
+ hoverKnobProps: T | undefined;
30
+ pressKnobProps: T | undefined;
31
+ focusKnobProps: T | undefined;
32
+ focusVisibleKnobProps: T | undefined;
33
+ }
34
+
35
+ // ── KnobProps: resolved style objects from knobs ──────────────────────────
36
+
37
+ export interface KnobProps {
38
+ borderRadius: { borderRadius: string; borderWidth: number };
39
+ borderRadiusOuter: { borderRadius: string };
40
+ elevation: string | undefined;
41
+ surface: { borderWidth: number };
42
+ inputSurface: { borderWidth: number };
43
+ panelPadding: { padding: string };
44
+ gap: { gap: string };
45
+ gapLg: { gap: string };
46
+ sizeToken: string;
47
+ heading: { fontFamily: string; fontWeight: string };
48
+ body: { fontFamily: string; fontWeight: string };
49
+ textWeight: { fontWeight: string };
50
+ transition: TransitionProp | undefined;
51
+ outlined: boolean;
52
+ pointy: boolean;
53
+ textAccent: string;
54
+ textAccentColor: string;
55
+ inputBackground: string;
56
+ space: string;
57
+ size: string;
58
+ }
59
+
60
+ // ── Return type ───────────────────────────────────────────────────────────
61
+
62
+ export interface ResolvedKnobs {
63
+ knobProps: KnobProps;
64
+ control: KnobRecipe<ControlStateProps>;
65
+ text: KnobRecipe<TextStateProps>;
66
+ elevation: KnobRecipe<ElevationStateProps>;
67
+ }
68
+
69
+ export type KnobPropsOverride = (knobProps: KnobProps, knobs: Knobs) => Partial<KnobProps>;