@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,276 @@
1
+ import type React from "react";
2
+ import { type ReactNode, createElement } from "react";
3
+ import { renderToString } from "react-dom/server";
4
+ import { describe, expect, it, vi } from "vitest";
5
+
6
+ // Mock Tamagui's <Theme> so we can inspect the chosen theme name
7
+ vi.mock("tamagui", () => ({
8
+ Theme: ({ name, children }: { name: string; children: ReactNode }) =>
9
+ createElement("div", { "data-tamagui-theme": name }, children),
10
+ }));
11
+
12
+ import type { ThemeName } from "@tamagui/web";
13
+ import { Intent } from "./Intent";
14
+ import { PresetContext, type PresetContextValue } from "./PresetContext";
15
+ import type { Preset as PresetType } from "./preset.types";
16
+ import { defaultKnobs } from "./knobs";
17
+ import type { ResolvedKnobs } from "./recipes";
18
+ import { useResolvedKnobs, useIntentContext } from "./useResolvedKnobs";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ function makePreset(intents: PresetType["intents"] = {}): PresetType {
25
+ return {
26
+ theme: "light" as ThemeName,
27
+ knobs: defaultKnobs,
28
+ intents,
29
+ tints: [],
30
+ };
31
+ }
32
+
33
+ function wrapWithPreset(preset: PresetType, children: ReactNode): ReactNode {
34
+ const ctx: PresetContextValue = { preset };
35
+ return createElement(PresetContext.Provider, { value: ctx }, children);
36
+ }
37
+
38
+ function renderToHtml(node: ReactNode): string {
39
+ return renderToString(node as React.ReactElement);
40
+ }
41
+
42
+ /** Return an array of all `data-tamagui-theme` values in rendered HTML. */
43
+ function collectThemes(html: string): string[] {
44
+ return Array.from(html.matchAll(/data-tamagui-theme="([^"]+)"/g), (m) => m[1]);
45
+ }
46
+
47
+ /** Create a probe component that captures resolved knobs from useResolvedKnobs. */
48
+ function createRecipesCapture(options?: Parameters<typeof useResolvedKnobs>[0]) {
49
+ let captured: ResolvedKnobs | undefined;
50
+ function Probe() {
51
+ captured = useResolvedKnobs(options);
52
+ return null;
53
+ }
54
+ return {
55
+ Probe,
56
+ get result(): ResolvedKnobs {
57
+ if (!captured) throw new Error("Probe was not rendered");
58
+ return captured;
59
+ },
60
+ };
61
+ }
62
+
63
+ /** Create a probe that captures the intent context value. */
64
+ function createIntentCapture() {
65
+ let captured: string | null | undefined;
66
+ function Probe() {
67
+ captured = useIntentContext();
68
+ return createElement("span", null, "probe");
69
+ }
70
+ return {
71
+ Probe,
72
+ get result(): string | null {
73
+ if (captured === undefined) throw new Error("Probe was not rendered");
74
+ return captured;
75
+ },
76
+ };
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Intent context tests
81
+ // ---------------------------------------------------------------------------
82
+
83
+ describe("Intent", () => {
84
+ it("wraps children in <Theme name={name}> by default", () => {
85
+ const html = renderToHtml(
86
+ wrapWithPreset(
87
+ makePreset(),
88
+ createElement(Intent, { name: "error" }, createElement("span", null, "hello")),
89
+ ),
90
+ );
91
+ const themes = collectThemes(html);
92
+ expect(themes).toContain("error");
93
+ expect(html).toContain("hello");
94
+ });
95
+
96
+ it("uses explicit theme prop over intent name for Tamagui <Theme>", () => {
97
+ const html = renderToHtml(
98
+ wrapWithPreset(
99
+ makePreset(),
100
+ createElement(
101
+ Intent,
102
+ { name: "error", theme: "red" as ThemeName },
103
+ createElement("span", null, "hello"),
104
+ ),
105
+ ),
106
+ );
107
+ const themes = collectThemes(html);
108
+ expect(themes).toContain("red");
109
+ expect(themes).not.toContain("error");
110
+ });
111
+
112
+ it("provides intent name via IntentContext (useIntentContext)", () => {
113
+ const capture = createIntentCapture();
114
+
115
+ renderToHtml(
116
+ wrapWithPreset(
117
+ makePreset(),
118
+ createElement(Intent, { name: "error" }, createElement(capture.Probe)),
119
+ ),
120
+ );
121
+ expect(capture.result).toBe("error");
122
+ });
123
+
124
+ it("nesting: innermost <Intent> wins for context", () => {
125
+ const capture = createIntentCapture();
126
+
127
+ renderToHtml(
128
+ wrapWithPreset(
129
+ makePreset(),
130
+ createElement(
131
+ Intent,
132
+ { name: "error" },
133
+ createElement(Intent, { name: "success" }, createElement(capture.Probe)),
134
+ ),
135
+ ),
136
+ );
137
+ expect(capture.result).toBe("success");
138
+ });
139
+
140
+ it("nesting: innermost <Intent> produces both Tamagui themes in DOM order", () => {
141
+ const html = renderToHtml(
142
+ wrapWithPreset(
143
+ makePreset(),
144
+ createElement(
145
+ Intent,
146
+ { name: "error" },
147
+ createElement(Intent, { name: "success" }, createElement("span", null, "inner")),
148
+ ),
149
+ ),
150
+ );
151
+ const themes = collectThemes(html);
152
+ expect(themes).toEqual(["error", "success"]);
153
+ });
154
+ });
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // useResolvedKnobs + Intent integration
158
+ // ---------------------------------------------------------------------------
159
+
160
+ describe("useResolvedKnobs with Intent", () => {
161
+ it("resolves default intent overrides from context", () => {
162
+ const capture = createRecipesCapture();
163
+
164
+ renderToHtml(
165
+ wrapWithPreset(
166
+ makePreset(),
167
+ createElement(Intent, { name: "error" }, createElement(capture.Probe)),
168
+ ),
169
+ );
170
+
171
+ // "error" defaultIntent has textAccent: "high" → passed through
172
+ expect(capture.result.knobProps.textAccent).toBe("high");
173
+ });
174
+
175
+ it("resolves intent via explicit option overriding context", () => {
176
+ const capture = createRecipesCapture({ intent: "warning" });
177
+
178
+ renderToHtml(
179
+ wrapWithPreset(
180
+ makePreset(),
181
+ createElement(Intent, { name: "error" }, createElement(capture.Probe)),
182
+ ),
183
+ );
184
+
185
+ // "warning" has textAccent: "high" → passed through
186
+ expect(capture.result.knobProps.textAccent).toBe("high");
187
+ });
188
+
189
+ it("resolves component-specific intent overrides (Button + error)", () => {
190
+ const capture = createRecipesCapture({ component: "Button" });
191
+
192
+ renderToHtml(
193
+ wrapWithPreset(
194
+ makePreset(),
195
+ createElement(Intent, { name: "error" }, createElement(capture.Probe)),
196
+ ),
197
+ );
198
+
199
+ // "error" → Button: { elevation: "small" } → resolved to "$1"
200
+ expect(capture.result.knobProps.elevation).toBe("$1");
201
+ });
202
+
203
+ it("resolves component-specific intent overrides via explicit option", () => {
204
+ const capture = createRecipesCapture({ intent: "error", component: "Button" });
205
+
206
+ renderToHtml(wrapWithPreset(makePreset(), createElement(capture.Probe)));
207
+
208
+ // "error" → Button: { elevation: "small" } → resolved to "$1"
209
+ expect(capture.result.knobProps.elevation).toBe("$1");
210
+ });
211
+
212
+ it("preset intent overrides take precedence over defaultIntents", () => {
213
+ const presetWithIntents = makePreset({
214
+ error: {
215
+ textAccent: "low",
216
+ Button: { elevation: "large" },
217
+ },
218
+ });
219
+
220
+ const capture = createRecipesCapture({ intent: "error", component: "Button" });
221
+
222
+ renderToHtml(wrapWithPreset(presetWithIntents, createElement(capture.Probe)));
223
+
224
+ // Preset says textAccent: "low" → passed through
225
+ expect(capture.result.knobProps.textAccent).toBe("low");
226
+
227
+ // Preset says Button.elevation: "large" → resolved to "$4"
228
+ expect(capture.result.knobProps.elevation).toBe("$4");
229
+ });
230
+
231
+ it("falls back to defaultIntents when preset has no intent overrides", () => {
232
+ const capture = createRecipesCapture({ intent: "accent", component: "Button" });
233
+
234
+ renderToHtml(wrapWithPreset(makePreset(), createElement(capture.Probe)));
235
+
236
+ // Component override: Button.fillStyle = "filled" overrides the intent-level "outlined"
237
+ expect(capture.result.knobProps.outlined).toBe(false);
238
+ });
239
+
240
+ it("accent intent without component uses outlined fill from defaultIntents", () => {
241
+ const capture = createRecipesCapture({ intent: "accent" });
242
+
243
+ renderToHtml(wrapWithPreset(makePreset(), createElement(capture.Probe)));
244
+
245
+ // accent defaultIntent: fillStyle: "outlined"
246
+ expect(capture.result.knobProps.outlined).toBe(true);
247
+ });
248
+
249
+ it("nesting intents: innermost wins for recipe resolution", () => {
250
+ const capture = createRecipesCapture({ component: "Button" });
251
+
252
+ renderToHtml(
253
+ wrapWithPreset(
254
+ makePreset(),
255
+ createElement(
256
+ Intent,
257
+ { name: "error" },
258
+ createElement(Intent, { name: "success" }, createElement(capture.Probe)),
259
+ ),
260
+ ),
261
+ );
262
+
263
+ // innermost is "success" → Button: { elevation: "none" } → undefined
264
+ expect(capture.result.knobProps.elevation).toBeUndefined();
265
+ });
266
+
267
+ it("no intent context and no option → base knobs only", () => {
268
+ const capture = createRecipesCapture();
269
+
270
+ renderToHtml(wrapWithPreset(makePreset(), createElement(capture.Probe)));
271
+
272
+ // Default knobs: elevation "none" → resolved to undefined
273
+ expect(capture.result.knobProps.elevation).toBeUndefined();
274
+ expect(capture.result.knobProps.textAccent).toBe("high");
275
+ });
276
+ });
@@ -0,0 +1,22 @@
1
+ import type { IntentOverride } from "./preset.types";
2
+
3
+ export const defaultIntents: Record<string, IntentOverride> = {
4
+ accent: {
5
+ fillStyle: "outlined",
6
+ textAccent: "high",
7
+ Button: { fillStyle: "filled" },
8
+ },
9
+ error: {
10
+ textAccent: "high",
11
+ Button: { elevation: "small" },
12
+ Input: { borderWidth: "medium" },
13
+ },
14
+ warning: {
15
+ textAccent: "high",
16
+ Input: { borderWidth: "small" },
17
+ },
18
+ success: {
19
+ textAccent: "high",
20
+ Button: { elevation: "none" },
21
+ },
22
+ };
@@ -0,0 +1,147 @@
1
+ // ── Structure ──────────────────────────────────────────────
2
+
3
+ export const FillStyle = { Filled: "filled", Outlined: "outlined" } as const;
4
+ export type FillStyle = (typeof FillStyle)[keyof typeof FillStyle];
5
+
6
+ export const BorderRadius = {
7
+ None: "none",
8
+ Small: "small",
9
+ Medium: "medium",
10
+ Large: "large",
11
+ Full: "full",
12
+ } as const;
13
+ export type BorderRadius = (typeof BorderRadius)[keyof typeof BorderRadius];
14
+
15
+ export const BorderWidth = {
16
+ None: "none",
17
+ Small: "small",
18
+ Medium: "medium",
19
+ Large: "large",
20
+ } as const;
21
+ export type BorderWidth = (typeof BorderWidth)[keyof typeof BorderWidth];
22
+
23
+ export const Elevation = {
24
+ None: "none",
25
+ Small: "small",
26
+ Medium: "medium",
27
+ Large: "large",
28
+ } as const;
29
+ export type Elevation = (typeof Elevation)[keyof typeof Elevation];
30
+
31
+ export const Space = {
32
+ Small: "small",
33
+ Medium: "medium",
34
+ Large: "large",
35
+ } as const;
36
+ export type Space = (typeof Space)[keyof typeof Space];
37
+
38
+ export const Size = {
39
+ Small: "small",
40
+ Medium: "medium",
41
+ Large: "large",
42
+ } as const;
43
+ export type Size = (typeof Size)[keyof typeof Size];
44
+
45
+ // ── Emphasis ───────────────────────────────────────────────
46
+
47
+ export const TextAccent = {
48
+ Low: "low",
49
+ Medium: "medium",
50
+ High: "high",
51
+ } as const;
52
+ export type TextAccent = (typeof TextAccent)[keyof typeof TextAccent];
53
+
54
+ // ── Typography ─────────────────────────────────────────────
55
+
56
+ export const FontCategory = {
57
+ SansSerif: "sans-serif",
58
+ Serif: "serif",
59
+ Mono: "mono",
60
+ Slab: "slab",
61
+ Rounded: "rounded",
62
+ Condensed: "condensed",
63
+ Cursive: "cursive",
64
+ Handwriting: "handwriting",
65
+ Pixel: "pixel",
66
+ Blackletter: "blackletter",
67
+ Geometric: "geometric",
68
+ } as const;
69
+ export type FontCategory = (typeof FontCategory)[keyof typeof FontCategory];
70
+
71
+ export const HeadingFont = FontCategory;
72
+ export type HeadingFont = FontCategory;
73
+
74
+ export const BodyFont = FontCategory;
75
+ export type BodyFont = FontCategory;
76
+
77
+ export const FontWeight = {
78
+ Regular: "regular",
79
+ Bold: "bold",
80
+ } as const;
81
+ export type FontWeight = (typeof FontWeight)[keyof typeof FontWeight];
82
+
83
+ // ── Animation ──────────────────────────────────────────────
84
+
85
+ import type { AnimationName } from "./animations/index";
86
+
87
+ export type Animation = "none" | AnimationName;
88
+
89
+ // ── Interaction state overrides ────────────────────────────
90
+ // Structural knobs that can vary per interaction state.
91
+
92
+ export interface StateKnobs {
93
+ fillStyle?: FillStyle;
94
+ borderRadius?: BorderRadius;
95
+ borderWidth?: BorderWidth;
96
+ elevation?: Elevation;
97
+ space?: Space;
98
+ textAccent?: TextAccent;
99
+ headingFont?: HeadingFont;
100
+ bodyFont?: BodyFont;
101
+ fontWeight?: FontWeight;
102
+ }
103
+
104
+ export const interactionStates = ["hover", "press", "focus", "focusVisible"] as const;
105
+ export type InteractionState = (typeof interactionStates)[number];
106
+
107
+ // ── Knobs interface ────────────────────────────────────────
108
+
109
+ export interface Knobs {
110
+ fillStyle: FillStyle;
111
+ borderRadius: BorderRadius;
112
+ borderWidth: BorderWidth;
113
+ elevation: Elevation;
114
+ space: Space;
115
+ size: Size;
116
+ textAccent: TextAccent;
117
+ headingFont: HeadingFont;
118
+ bodyFont: BodyFont;
119
+ fontWeight: FontWeight;
120
+ animation: Animation;
121
+ hover: StateKnobs;
122
+ press: StateKnobs;
123
+ focus: StateKnobs;
124
+ focusVisible: StateKnobs;
125
+ }
126
+
127
+ // ── Default knobs ──────────────────────────────────────────
128
+
129
+ export const emptyStateKnobs: StateKnobs = {};
130
+
131
+ export const defaultKnobs: Knobs = {
132
+ fillStyle: "filled",
133
+ borderRadius: "medium",
134
+ borderWidth: "small",
135
+ elevation: "none",
136
+ space: "medium",
137
+ size: "medium",
138
+ textAccent: "high",
139
+ headingFont: "sans-serif",
140
+ bodyFont: "sans-serif",
141
+ fontWeight: "regular",
142
+ animation: "quick",
143
+ hover: emptyStateKnobs,
144
+ press: emptyStateKnobs,
145
+ focus: emptyStateKnobs,
146
+ focusVisible: emptyStateKnobs,
147
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Task Group 5.1 — Pokemon screen source-code analysis tests.
3
+ *
4
+ * These tests verify that the refactored Pokemon files follow the
5
+ * Tamagui-native Card pattern and contain zero hardcoded styling values.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const root = path.resolve(__dirname, "../../../..");
16
+
17
+ function readSource(relativePath: string): string {
18
+ return fs.readFileSync(path.resolve(root, relativePath), "utf-8");
19
+ }
20
+
21
+ /**
22
+ * Returns all non-comment, non-import lines from a source file.
23
+ * Strips single-line comments and block comments for cleaner matching.
24
+ */
25
+ function codeLines(source: string): string[] {
26
+ return source
27
+ .replace(/\/\*[\s\S]*?\*\//g, "") // strip block comments
28
+ .split("\n")
29
+ .map((l) => l.replace(/\/\/.*$/, "").trim()) // strip line comments
30
+ .filter((l) => l.length > 0 && !l.startsWith("import ") && !l.startsWith("export type"));
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Pokemon source files under test
35
+ // ---------------------------------------------------------------------------
36
+
37
+ const pokemonFiles = [
38
+ "features/pokemon/screen.tsx",
39
+ "features/pokemon/detail.tsx",
40
+ "features/pokemon/components/PokemonCard.tsx",
41
+ "features/pokemon/components/MoveList.tsx",
42
+ "features/pokemon/components/AbilityList.tsx",
43
+ "features/pokemon/components/TypeList.tsx",
44
+ "features/pokemon/components/PokemonStats.tsx",
45
+ "public/frappe-ui/src/FrappeImageView.tsx",
46
+ ];
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Tests
50
+ // ---------------------------------------------------------------------------
51
+
52
+ describe("Pokemon screen — Card rendering with native Tamagui props", () => {
53
+ it("screen.tsx uses Card with elevation, borderWidth, and Card.Background/Card.Footer", () => {
54
+ const src = readSource("features/pokemon/screen.tsx");
55
+ expect(src).toContain("<Card");
56
+ expect(src).toMatch(/elevation/);
57
+ expect(src).toMatch(/borderWidth/);
58
+ expect(src).toContain("Card.Background");
59
+ expect(src).toContain("Card.Footer");
60
+ });
61
+
62
+ it("screen.tsx uses ToggleGroup for view switcher instead of manual Button toggles", () => {
63
+ const src = readSource("features/pokemon/screen.tsx");
64
+ expect(src).toContain("ToggleGroup");
65
+ });
66
+
67
+ it("FrappeImageView.tsx uses Card with elevation, borderWidth, and sizeToken", () => {
68
+ const src = readSource("public/frappe-ui/src/FrappeImageView.tsx");
69
+ expect(src).toContain("<Card");
70
+ expect(src).toMatch(/elevation/);
71
+ expect(src).toMatch(/borderWidth/);
72
+ expect(src).toContain("sizeToken");
73
+ });
74
+ });
75
+
76
+ describe("Pokemon screen — no hardcoded styling values", () => {
77
+ for (const filePath of pokemonFiles) {
78
+ it(`${filePath} has no hardcoded borderRadius, padding, gap, or elevation overrides`, () => {
79
+ const src = readSource(filePath);
80
+ const lines = codeLines(src);
81
+
82
+ for (const line of lines) {
83
+ // Skip lines that are prop definitions on Card/etc (elevate, bordered are fine)
84
+ // Skip lines that are Tamagui token references like "$3", "$4"
85
+ // We're looking for raw numeric values assigned to styling props
86
+
87
+ // No hardcoded borderRadius with raw numbers (e.g. borderRadius={12})
88
+ expect(line).not.toMatch(/borderRadius[=:{]\s*\d+(?!\s*\})/);
89
+
90
+ // No manual elevation={number} (should use `elevate` boolean instead)
91
+ // Allow elevation on Progress.Indicator or as Tamagui token strings
92
+ if (!line.includes("Progress")) {
93
+ expect(line).not.toMatch(/elevation[=:{]\s*\d+/);
94
+ }
95
+
96
+ // No spreading of borderRadiusProps (Card handles via size)
97
+ expect(line).not.toMatch(/\.\.\.themeProps\.borderRadiusProps/);
98
+
99
+ // No spreading of elevationProps (Card handles via elevate)
100
+ expect(line).not.toMatch(/\.\.\.themeProps\.elevationProps/);
101
+ }
102
+ });
103
+ }
104
+
105
+ it("screen.tsx uses sizeToken on Card components", () => {
106
+ const src = readSource("features/pokemon/screen.tsx");
107
+ expect(src).toContain("sizeToken");
108
+ });
109
+ });