@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
package/src/devWarn.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-shaken DEV guardrails (blueprint §5).
|
|
3
|
+
* No-ops when `NODE_ENV === "production"`.
|
|
4
|
+
*
|
|
5
|
+
* W12 owns `outline-none` + `positive-tabindex`. Other codes are catalogued
|
|
6
|
+
* here so consumers share one tree-shaken entry point.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type DevWarnCode =
|
|
10
|
+
| "placeholder-as-label"
|
|
11
|
+
| "two-primaries"
|
|
12
|
+
| "banned-error-word"
|
|
13
|
+
| "bare-disabled"
|
|
14
|
+
| "cancel-disabled"
|
|
15
|
+
| "dialog-too-many-actions"
|
|
16
|
+
| "select-too-few-options"
|
|
17
|
+
| "positive-tabindex"
|
|
18
|
+
| "tooltip-on-disabled"
|
|
19
|
+
| "outline-none"
|
|
20
|
+
// DG-A11Y-02 — a second level-1 heading mounted under one page scope.
|
|
21
|
+
// Emitted by PageHeadingScope in @multiplatform.one/components.
|
|
22
|
+
| "multiple-h1";
|
|
23
|
+
|
|
24
|
+
export type DevWarnDetail = {
|
|
25
|
+
component?: string;
|
|
26
|
+
id?: string;
|
|
27
|
+
value?: unknown;
|
|
28
|
+
word?: string;
|
|
29
|
+
count?: number;
|
|
30
|
+
suggest?: string;
|
|
31
|
+
region?: string;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const seen = new Set<string>();
|
|
36
|
+
|
|
37
|
+
function isDev(): boolean {
|
|
38
|
+
return process.env.NODE_ENV !== "production";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Emit a once-per-key console warning in non-production builds.
|
|
43
|
+
*/
|
|
44
|
+
export function devWarn(code: DevWarnCode, detail: DevWarnDetail = {}): void {
|
|
45
|
+
if (!isDev()) return;
|
|
46
|
+
const key = [
|
|
47
|
+
code,
|
|
48
|
+
detail.component ?? "",
|
|
49
|
+
detail.id ?? "",
|
|
50
|
+
detail.region ?? "",
|
|
51
|
+
String(detail.value ?? ""),
|
|
52
|
+
detail.word ?? "",
|
|
53
|
+
detail.count ?? "",
|
|
54
|
+
].join(":");
|
|
55
|
+
if (seen.has(key)) return;
|
|
56
|
+
seen.add(key);
|
|
57
|
+
const bits = [
|
|
58
|
+
detail.component ? `component=${detail.component}` : undefined,
|
|
59
|
+
detail.id ? `id=${detail.id}` : undefined,
|
|
60
|
+
detail.region ? `region=${detail.region}` : undefined,
|
|
61
|
+
detail.value !== undefined ? `value=${String(detail.value)}` : undefined,
|
|
62
|
+
detail.word ? `word=${detail.word}` : undefined,
|
|
63
|
+
detail.count !== undefined ? `count=${detail.count}` : undefined,
|
|
64
|
+
detail.suggest ? `suggest=${detail.suggest}` : undefined,
|
|
65
|
+
].filter(Boolean);
|
|
66
|
+
const suffix = bits.length ? ` (${bits.join(", ")})` : "";
|
|
67
|
+
console.warn(`[theme] ${code}${suffix}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @internal test helper */
|
|
71
|
+
export function __resetDevWarnSeen(): void {
|
|
72
|
+
seen.clear();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Alias kept for callers that imported the W12-era name. */
|
|
76
|
+
export const themeDevWarn = devWarn;
|
|
77
|
+
export const __resetThemeDevWarnSeen = __resetDevWarnSeen;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* DG-FORM-02 / DG-FORM-11: placeholder must not be the only label.
|
|
81
|
+
*/
|
|
82
|
+
export function warnPlaceholderAsLabel(args: {
|
|
83
|
+
placeholder?: string | null;
|
|
84
|
+
label?: unknown;
|
|
85
|
+
ariaLabel?: string | null;
|
|
86
|
+
id?: string;
|
|
87
|
+
component?: string;
|
|
88
|
+
}): void {
|
|
89
|
+
if (!isDev()) return;
|
|
90
|
+
const placeholder = typeof args.placeholder === "string" ? args.placeholder.trim() : "";
|
|
91
|
+
if (!placeholder) return;
|
|
92
|
+
if (args.label != null && args.label !== false && args.label !== "") return;
|
|
93
|
+
const ariaLabel = typeof args.ariaLabel === "string" ? args.ariaLabel.trim() : "";
|
|
94
|
+
if (ariaLabel) return;
|
|
95
|
+
devWarn("placeholder-as-label", {
|
|
96
|
+
component: args.component ?? "FieldLayout",
|
|
97
|
+
id: args.id,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const BANNED_ERROR_WORDS = ["invalid", "error", "please", "sorry", "oops"] as const;
|
|
102
|
+
|
|
103
|
+
/** DG-COPY-02 / DG-VAL-03 — banned words in error copy. */
|
|
104
|
+
export function warnBannedErrorWords(
|
|
105
|
+
message: string | null | undefined,
|
|
106
|
+
detail: DevWarnDetail = {},
|
|
107
|
+
): void {
|
|
108
|
+
if (!isDev() || !message) return;
|
|
109
|
+
const lower = message.toLowerCase();
|
|
110
|
+
for (const word of BANNED_ERROR_WORDS) {
|
|
111
|
+
if (new RegExp(`\\b${word}\\b`, "i").test(lower)) {
|
|
112
|
+
devWarn("banned-error-word", { ...detail, word });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** DG-ST-03 — bare `disabled` without reason / explain. */
|
|
119
|
+
export function warnBareDisabled(args: {
|
|
120
|
+
disabled?: boolean;
|
|
121
|
+
disabledReason?: string | null;
|
|
122
|
+
skip?: boolean;
|
|
123
|
+
component?: string;
|
|
124
|
+
id?: string;
|
|
125
|
+
}): void {
|
|
126
|
+
if (!isDev() || !args.disabled || args.skip) return;
|
|
127
|
+
if (typeof args.disabledReason === "string" && args.disabledReason.trim()) return;
|
|
128
|
+
devWarn("bare-disabled", { component: args.component, id: args.id });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* DG-ACT-05 — the cancel/dismiss action never renders disabled (while confirm
|
|
133
|
+
* may be). Enforcing callers strip the prop and report the attempt here.
|
|
134
|
+
*/
|
|
135
|
+
export function warnCancelDisabled(args: {
|
|
136
|
+
disabled?: boolean;
|
|
137
|
+
component?: string;
|
|
138
|
+
id?: string;
|
|
139
|
+
}): void {
|
|
140
|
+
if (!isDev() || !args.disabled) return;
|
|
141
|
+
devWarn("cancel-disabled", { component: args.component, id: args.id });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** DG-OVL-04 — dialog action rows cap at this many actions by default. */
|
|
145
|
+
export const DIALOG_MAX_ACTIONS = 2;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* DG-OVL-04 — max 2 actions in a dialog by default. Warn-only (never a hard
|
|
149
|
+
* block); `allowManyActions` is the explicit eject acknowledging the extra
|
|
150
|
+
* actions.
|
|
151
|
+
*/
|
|
152
|
+
export function warnDialogTooManyActions(args: {
|
|
153
|
+
count: number;
|
|
154
|
+
allowManyActions?: boolean;
|
|
155
|
+
component?: string;
|
|
156
|
+
id?: string;
|
|
157
|
+
}): void {
|
|
158
|
+
if (!isDev() || args.allowManyActions || args.count <= DIALOG_MAX_ACTIONS) return;
|
|
159
|
+
devWarn("dialog-too-many-actions", {
|
|
160
|
+
component: args.component,
|
|
161
|
+
id: args.id,
|
|
162
|
+
count: args.count,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* DG-CTRL — Select with too few options; suggest RadioGroup / ToggleGroup.
|
|
168
|
+
*/
|
|
169
|
+
export function warnSelectTooFewOptions(args: {
|
|
170
|
+
count: number;
|
|
171
|
+
multiple?: boolean;
|
|
172
|
+
component?: string;
|
|
173
|
+
id?: string;
|
|
174
|
+
}): void {
|
|
175
|
+
if (!isDev() || args.multiple || args.count > 5) return;
|
|
176
|
+
const suggest = args.count <= 2 ? "ToggleGroup" : "RadioGroup";
|
|
177
|
+
devWarn("select-too-few-options", {
|
|
178
|
+
component: args.component,
|
|
179
|
+
id: args.id,
|
|
180
|
+
count: args.count,
|
|
181
|
+
suggest,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** DG-A11Y-03 — never `tabIndex` > 0. */
|
|
186
|
+
export function warnPositiveTabIndex(args: {
|
|
187
|
+
value?: number | string | null;
|
|
188
|
+
component?: string;
|
|
189
|
+
id?: string;
|
|
190
|
+
}): void {
|
|
191
|
+
if (!isDev()) return;
|
|
192
|
+
const n = typeof args.value === "string" ? Number(args.value) : args.value;
|
|
193
|
+
if (typeof n !== "number" || Number.isNaN(n) || n <= 0) return;
|
|
194
|
+
devWarn("positive-tabindex", {
|
|
195
|
+
component: args.component,
|
|
196
|
+
id: args.id,
|
|
197
|
+
value: n,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** DG-OVL-05 — no hover tooltip on disabled controls. */
|
|
202
|
+
export function warnTooltipOnDisabled(args: {
|
|
203
|
+
childDisabled?: boolean;
|
|
204
|
+
component?: string;
|
|
205
|
+
id?: string;
|
|
206
|
+
}): void {
|
|
207
|
+
if (!isDev() || !args.childDisabled) return;
|
|
208
|
+
devWarn("tooltip-on-disabled", {
|
|
209
|
+
component: args.component ?? "Tooltip",
|
|
210
|
+
id: args.id,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
type OutlineLike =
|
|
215
|
+
| {
|
|
216
|
+
outline?: string | number;
|
|
217
|
+
outlineStyle?: string;
|
|
218
|
+
outlineWidth?: number;
|
|
219
|
+
outlineColor?: string;
|
|
220
|
+
}
|
|
221
|
+
| null
|
|
222
|
+
| undefined;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* DG-A11Y-01 / W12 — warn when a focus style strips the outline.
|
|
226
|
+
* Call on `focusVisibleStyle` / `focusStyle` only — not resting
|
|
227
|
+
* `outlineWidth: 0` + transparent (border-flicker rest).
|
|
228
|
+
*/
|
|
229
|
+
export function warnOutlineNone(style: OutlineLike, detail: DevWarnDetail = {}): void {
|
|
230
|
+
if (!isDev() || !style) return;
|
|
231
|
+
const stripped =
|
|
232
|
+
style.outline === "none" || style.outlineStyle === "none" || style.outlineWidth === 0;
|
|
233
|
+
if (!stripped) return;
|
|
234
|
+
devWarn("outline-none", detail);
|
|
235
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import * as Colors from "@tamagui/colors";
|
|
2
|
+
import { tokens } from "@tamagui/themes";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { normalizeToHex } from "../theme/colorRules";
|
|
5
|
+
import { defaultAccentTheme } from "../theme/defaults/accent";
|
|
6
|
+
import { defaultBaseTheme } from "../theme/defaults/base";
|
|
7
|
+
import { interTrackingPx } from "../theme/defaults/fonts";
|
|
8
|
+
import { FIGMA_THEME_MODES, buildFigmaTokens, figmaTokenName } from "./figmaTokens";
|
|
9
|
+
|
|
10
|
+
const fontFamilies = {
|
|
11
|
+
"sans-serif": "Inter",
|
|
12
|
+
pixel: "'Silkscreen', 'Press Start 2P', monospace",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The Tamagui theme-builder normalizes palette colors (hex→hsla) and may
|
|
17
|
+
* round interior steps, so source↔built comparisons must be colorimetric.
|
|
18
|
+
* True when the two CSS colors are within `tolerance` per RGB channel.
|
|
19
|
+
*/
|
|
20
|
+
function sameColor(a: string, b: string, tolerance = 3): boolean {
|
|
21
|
+
const hexA = normalizeToHex(a);
|
|
22
|
+
const hexB = normalizeToHex(b);
|
|
23
|
+
if (!hexA || !hexB) return false;
|
|
24
|
+
for (let i = 1; i < 7; i += 2) {
|
|
25
|
+
const channelA = Number.parseInt(hexA.slice(i, i + 2), 16);
|
|
26
|
+
const channelB = Number.parseInt(hexB.slice(i, i + 2), 16);
|
|
27
|
+
if (Math.abs(channelA - channelB) > tolerance) return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("buildFigmaTokens", () => {
|
|
33
|
+
const result = buildFigmaTokens({ fontFamilies });
|
|
34
|
+
|
|
35
|
+
it("is deterministic (pure transform, golden-comparable)", () => {
|
|
36
|
+
expect(JSON.stringify(result)).toBe(JSON.stringify(buildFigmaTokens({ fontFamilies })));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("palette", () => {
|
|
40
|
+
it("exports the 9 spec theme modes with 12-step light+dark ramps", () => {
|
|
41
|
+
expect(Object.keys(result.palette.themes)).toEqual([...FIGMA_THEME_MODES]);
|
|
42
|
+
for (const theme of Object.values(result.palette.themes)) {
|
|
43
|
+
expect(theme.light).toHaveLength(12);
|
|
44
|
+
expect(theme.dark).toHaveLength(12);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("derives base/accent ramps from the canonical default palettes", () => {
|
|
49
|
+
// The theme-builder rounds/normalizes palette values, so the export
|
|
50
|
+
// reflects the BUILT runtime themes — every step must still be
|
|
51
|
+
// colorimetrically the canonical source step.
|
|
52
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
53
|
+
const built = result.palette.themes.base[scheme];
|
|
54
|
+
const source =
|
|
55
|
+
scheme === "light" ? defaultBaseTheme.lightPalette : defaultBaseTheme.darkPalette;
|
|
56
|
+
for (let i = 0; i < 12; i++) {
|
|
57
|
+
expect(sameColor(built[i], source[i])).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
expect(defaultAccentTheme.lightPalette).toContain(result.palette.themes.accent.light[0]);
|
|
61
|
+
expect(defaultAccentTheme.darkPalette).toContain(result.palette.themes.accent.dark[0]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("derives intent ramps from @tamagui/colors (error=red, success=green, warning=yellow)", () => {
|
|
65
|
+
// Built themes carry hsla-normalized values; compare colorimetrically.
|
|
66
|
+
const includesColor = (ramp: string[], value: string) =>
|
|
67
|
+
ramp.some((step) => sameColor(step, value));
|
|
68
|
+
expect(includesColor(result.palette.themes.error.light, Colors.red.red9)).toBe(true);
|
|
69
|
+
expect(includesColor(result.palette.themes.error.dark, Colors.redDark.red9)).toBe(true);
|
|
70
|
+
expect(includesColor(result.palette.themes.success.light, Colors.green.green9)).toBe(true);
|
|
71
|
+
expect(includesColor(result.palette.themes.warning.light, Colors.yellow.yellow9)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("exports tint ramps with neutralized text tiers (runtime reality, not stock Radix)", () => {
|
|
75
|
+
// neutralizeTintText re-anchors color11/color12 of tint sub-themes to
|
|
76
|
+
// the neutral base scheme.
|
|
77
|
+
expect(result.palette.themes.orange.light[11]).toBe(result.palette.themes.base.light[11]);
|
|
78
|
+
expect(result.palette.themes.purple.dark[10]).toBe(result.palette.themes.base.dark[10]);
|
|
79
|
+
// Non-text steps keep the hue.
|
|
80
|
+
expect(result.palette.themes.orange.light[8]).not.toBe(result.palette.themes.base.light[8]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("exports 6 shadow steps per scheme from the base theme", () => {
|
|
84
|
+
expect(Object.keys(result.palette.shadows.light)).toHaveLength(6);
|
|
85
|
+
expect(result.palette.shadows.light.shadow1).toBe(defaultBaseTheme.lightShadows?.shadow1);
|
|
86
|
+
expect(result.palette.shadows.dark.shadow6).toBe(defaultBaseTheme.darkShadows?.shadow6);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("semantic", () => {
|
|
91
|
+
it("derives the current interaction-ramp aliases (post-knobs rebuild)", () => {
|
|
92
|
+
// These pin the drift the May 2026 spec missed: borderColor moved from
|
|
93
|
+
// color7 to color5, textMuted from color10 to color11.
|
|
94
|
+
expect(result.semantic.borderColor?.alias).toBe("color5");
|
|
95
|
+
expect(result.semantic.borderColorHover?.alias).toBe("color6");
|
|
96
|
+
expect(result.semantic.textMuted?.alias).toBe("color11");
|
|
97
|
+
expect(result.semantic.backgroundPress?.alias).toBe("color5");
|
|
98
|
+
expect(result.semantic.backgroundActive?.alias).toBe("color4");
|
|
99
|
+
expect(result.semantic.placeholderColor?.alias).toBe("color10");
|
|
100
|
+
expect(result.semantic.surfaceMuted?.alias).toBe("color10");
|
|
101
|
+
expect(result.semantic.borderStrong?.alias).toBe("color7");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("exports the computed focus ring color (outlineColor) with per-scheme values", () => {
|
|
105
|
+
const outline = result.semantic.outlineColor;
|
|
106
|
+
expect(outline).toBeDefined();
|
|
107
|
+
expect(outline?.light).toBeTruthy();
|
|
108
|
+
expect(outline?.dark).toBeTruthy();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("derives textAccent knob tiers as text-low/medium/high", () => {
|
|
112
|
+
expect(result.semantic["text-low"]?.alias).toBe("color11");
|
|
113
|
+
expect(result.semantic["text-medium"]?.alias).toBe("color11");
|
|
114
|
+
expect(result.semantic["text-high"]?.alias).toBe("color12");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("excludes raw Radix hue steps (amber1, blueA12, ...) from semantic", () => {
|
|
118
|
+
const keys = Object.keys(result.semantic);
|
|
119
|
+
expect(keys.some((key) => /^(amber|blue|red|mauve)A?\d+$/.test(key))).toBe(false);
|
|
120
|
+
// Semantic surface is role tokens only — small, not the 600+ hue dump.
|
|
121
|
+
expect(keys.length).toBeGreaterThan(20);
|
|
122
|
+
expect(keys.length).toBeLessThan(100);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("only aliases when light and dark agree; raw entries keep both values", () => {
|
|
126
|
+
for (const entry of Object.values(result.semantic)) {
|
|
127
|
+
if (entry.alias === null) {
|
|
128
|
+
expect(typeof entry.light).toBe("string");
|
|
129
|
+
expect(typeof entry.dark).toBe("string");
|
|
130
|
+
} else {
|
|
131
|
+
expect(entry.alias).toMatch(/^color([1-9]|1[0-2])$/);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("sizing", () => {
|
|
138
|
+
it("mirrors @tamagui/themes tokens with figma-safe names", () => {
|
|
139
|
+
const sizeTrue = (tokens.size as Record<string, { val: number }>).true.val;
|
|
140
|
+
expect(result.sizing["size-true"]).toBe(sizeTrue);
|
|
141
|
+
const radius4 = (tokens.radius as Record<string, { val: number }>)["4"].val;
|
|
142
|
+
expect(result.sizing["radius-4"]).toBe(radius4);
|
|
143
|
+
expect(result.sizing["space-1-5"]).toBe(
|
|
144
|
+
(tokens.space as Record<string, { val: number }>)["1.5"].val,
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("exports knob-shaped borderWidth values including the 0.5 hairline", () => {
|
|
149
|
+
expect(result.sizing["borderWidth-none"]).toBe(0);
|
|
150
|
+
expect(result.sizing["borderWidth-small"]).toBe(0.5);
|
|
151
|
+
expect(result.sizing["borderWidth-medium"]).toBe(1);
|
|
152
|
+
expect(result.sizing["borderWidth-large"]).toBe(2);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("knobs (resolved via resolveKnobs — no copied maps)", () => {
|
|
157
|
+
it("exports current borderRadius modes (medium=$4, not the May-era $3)", () => {
|
|
158
|
+
const { modes, default: def } = result.knobs.borderRadius;
|
|
159
|
+
expect(def).toBe("medium");
|
|
160
|
+
expect(modes.none.token).toBe("$0");
|
|
161
|
+
expect(modes.small.token).toBe("$2");
|
|
162
|
+
expect(modes.medium.token).toBe("$4");
|
|
163
|
+
expect(modes.large.token).toBe("$6");
|
|
164
|
+
expect(modes.full.token).toBe("$12");
|
|
165
|
+
expect(modes.medium.figmaName).toBe("radius-4");
|
|
166
|
+
expect(modes.medium.px).toBe(result.sizing["radius-4"]);
|
|
167
|
+
// Outer/nested radius fragment landed with the knobs rebuild.
|
|
168
|
+
expect(modes.full.outerToken).toBe("$5");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("exports borderWidth knob modes", () => {
|
|
172
|
+
expect(result.knobs.borderWidth.modes).toEqual({
|
|
173
|
+
none: 0,
|
|
174
|
+
small: 0.5,
|
|
175
|
+
medium: 1,
|
|
176
|
+
large: 2,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("exports platform-normalized elevation modes", () => {
|
|
181
|
+
const { modes } = result.knobs.elevation;
|
|
182
|
+
expect(modes.none).toEqual({ sizeToken: null, px: null });
|
|
183
|
+
expect(modes.small.sizeToken).toBe("$1");
|
|
184
|
+
expect(modes.small.px).toBe((tokens.size as Record<string, { val: number }>)["1"].val);
|
|
185
|
+
expect(modes.medium.sizeToken).toBe("$2");
|
|
186
|
+
expect(modes.large.sizeToken).toBe("$4");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("exports fontWeight, space, size and cornerSmoothing modes", () => {
|
|
190
|
+
expect(result.knobs.fontWeight.modes).toEqual({ regular: "400", bold: "700" });
|
|
191
|
+
expect(result.knobs.space.modes.medium.paddingFigmaName).toBe("space-4");
|
|
192
|
+
expect(result.knobs.space.modes.medium.gapFigmaName).toBe("space-4");
|
|
193
|
+
expect(result.knobs.space.modes.medium.gapLgFigmaName).toBe("space-5");
|
|
194
|
+
expect(result.knobs.size.modes).toEqual({
|
|
195
|
+
small: { token: "$3", figmaName: "size-3" },
|
|
196
|
+
medium: { token: "$4", figmaName: "size-4" },
|
|
197
|
+
large: { token: "$5", figmaName: "size-5" },
|
|
198
|
+
});
|
|
199
|
+
expect(result.knobs.cornerSmoothing.modes).toEqual({ round: 0, smooth: 0.6 });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("exports density compact stepping", () => {
|
|
203
|
+
expect(result.knobs.density.default).toBe("comfortable");
|
|
204
|
+
expect(result.knobs.density.compact.size).toEqual({
|
|
205
|
+
small: "small",
|
|
206
|
+
medium: "small",
|
|
207
|
+
large: "medium",
|
|
208
|
+
});
|
|
209
|
+
expect(result.knobs.density.compact.space).toEqual({
|
|
210
|
+
small: "small",
|
|
211
|
+
medium: "small",
|
|
212
|
+
large: "medium",
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("typography (closes deferred task 10.0)", () => {
|
|
218
|
+
it("exports heading/body size + lineHeight tables from the built default fonts", () => {
|
|
219
|
+
// Heading scale is 1.4x Inter base: H1 $10 = round(46 * 1.4) = 64.
|
|
220
|
+
expect(result.typography.heading.size["10"]).toBe(64);
|
|
221
|
+
expect(result.typography.body.size.true).toBe(14);
|
|
222
|
+
// lineHeight = size + 10 (defaults/fonts.ts).
|
|
223
|
+
expect(result.typography.body.lineHeight.true).toBe(24);
|
|
224
|
+
expect(result.typography.heading.lineHeight["10"]).toBe(74);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("exports Inter dynamic tracking per size", () => {
|
|
228
|
+
const body = result.typography.body;
|
|
229
|
+
expect(body.letterSpacing.true).toBe(interTrackingPx(14));
|
|
230
|
+
expect(Object.keys(body.letterSpacing).length).toBeGreaterThanOrEqual(16);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("exports category stacks with a Figma-usable first family", () => {
|
|
234
|
+
expect(result.typography.categories.pixel.figmaFamily).toBe("Silkscreen");
|
|
235
|
+
expect(result.typography.categories["sans-serif"].figmaFamily).toBe("Inter");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("figmaTokenName converts Tamagui token keys to Figma-safe names", () => {
|
|
240
|
+
expect(figmaTokenName("size", "1.5")).toBe("size-1-5");
|
|
241
|
+
expect(figmaTokenName("space", "true")).toBe("space-true");
|
|
242
|
+
expect(figmaTokenName("space", "-0.25")).toBe("space--0-25");
|
|
243
|
+
});
|
|
244
|
+
});
|