@cyclone-ui/theme-builder 0.0.0-stage → 0.0.4
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/CHANGELOG.md +15 -0
- package/LICENSE +187 -0
- package/README.md +305 -2
- package/dist/generate.cjs +1 -0
- package/dist/generate.d.cts +5 -0
- package/dist/generate.d.cts.map +1 -0
- package/dist/generate.d.mts +6 -0
- package/dist/generate.d.mts.map +1 -0
- package/dist/generate.mjs +2 -0
- package/dist/generate.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +1 -0
- package/dist/preprocessor.cjs +1 -0
- package/dist/preprocessor.d.cts +4 -0
- package/dist/preprocessor.d.cts.map +1 -0
- package/dist/preprocessor.d.mts +5 -0
- package/dist/preprocessor.d.mts.map +1 -0
- package/dist/preprocessor.mjs +2 -0
- package/dist/preprocessor.mjs.map +1 -0
- package/package.json +70 -3
- package/powerlines.config.ts +28 -0
- package/project.json +8 -0
- package/src/generate.ts +66 -0
- package/src/index.ts +22 -0
- package/src/preprocessor.ts +1524 -0
- package/test/preprocessor.spec.ts +518 -0
- package/test/shiki-theme-mapping.spec.ts +160 -0
- package/test/storybook-theme-mapping.spec.ts +118 -0
- package/tsconfig.json +13 -0
- package/tsconfig.spec.json +16 -0
- package/vitest.config.mts +24 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { tamaguiPreprocessor } from "./tamagui-preprocessor";
|
|
4
|
+
|
|
5
|
+
const colorToken = (value: string) => ({
|
|
6
|
+
$type: "color",
|
|
7
|
+
$value: value
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const relativeLuminance = (hex: string): number => {
|
|
11
|
+
const channel = (offset: number) => {
|
|
12
|
+
const value = Number.parseInt(hex.slice(offset, offset + 2), 16) / 255;
|
|
13
|
+
|
|
14
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return 0.2126 * channel(1) + 0.7152 * channel(3) + 0.0722 * channel(5);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const contrastRatio = (first: string, second: string): number => {
|
|
21
|
+
const lighter = Math.max(relativeLuminance(first), relativeLuminance(second));
|
|
22
|
+
const darker = Math.min(relativeLuminance(first), relativeLuminance(second));
|
|
23
|
+
|
|
24
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const oklchHue = (hex: string): number => {
|
|
28
|
+
const linearChannel = (offset: number) => {
|
|
29
|
+
const value = Number.parseInt(hex.slice(offset, offset + 2), 16) / 255;
|
|
30
|
+
|
|
31
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
32
|
+
};
|
|
33
|
+
const red = linearChannel(1);
|
|
34
|
+
const green = linearChannel(3);
|
|
35
|
+
const blue = linearChannel(5);
|
|
36
|
+
const l = Math.cbrt(
|
|
37
|
+
0.412_221_470_8 * red + 0.536_332_536_3 * green + 0.051_445_992_9 * blue
|
|
38
|
+
);
|
|
39
|
+
const m = Math.cbrt(
|
|
40
|
+
0.211_903_498_2 * red + 0.680_699_545_1 * green + 0.107_396_956_6 * blue
|
|
41
|
+
);
|
|
42
|
+
const s = Math.cbrt(
|
|
43
|
+
0.088_302_461_9 * red + 0.281_718_837_6 * green + 0.629_978_700_5 * blue
|
|
44
|
+
);
|
|
45
|
+
const a = 1.977_998_495_1 * l - 2.428_592_205 * m + 0.450_593_709_9 * s;
|
|
46
|
+
const b = 0.025_904_037_1 * l + 0.782_771_766_2 * m - 0.808_675_766 * s;
|
|
47
|
+
const hue = (Math.atan2(b, a) * 180) / Math.PI;
|
|
48
|
+
|
|
49
|
+
return hue < 0 ? hue + 360 : hue;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const hueDistance = (first: number, second: number): number => {
|
|
53
|
+
const difference = Math.abs(first - second) % 360;
|
|
54
|
+
|
|
55
|
+
return Math.min(difference, 360 - difference);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe("tamaguiPreprocessor", () => {
|
|
59
|
+
it("generates missing on-* text colors for status colors", () => {
|
|
60
|
+
const result = tamaguiPreprocessor({
|
|
61
|
+
color: {
|
|
62
|
+
danger: colorToken("#336699"),
|
|
63
|
+
warning: colorToken("#ffee99")
|
|
64
|
+
}
|
|
65
|
+
}) as any;
|
|
66
|
+
|
|
67
|
+
expect(result.color["on-danger-accent"]).toMatchObject({
|
|
68
|
+
$value: "#FAFAFA",
|
|
69
|
+
$description: "Generated text and icon color on danger backgrounds"
|
|
70
|
+
});
|
|
71
|
+
expect(result.color["on-warning-accent"].$value).toBe("#151518");
|
|
72
|
+
expect(result.color).not.toHaveProperty("on-danger");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("generates missing *-muted backgrounds from the parent theme", () => {
|
|
76
|
+
const lightDanger = "#336699";
|
|
77
|
+
const darkDanger = "#99ccff";
|
|
78
|
+
const result = tamaguiPreprocessor({
|
|
79
|
+
light: {
|
|
80
|
+
color: {
|
|
81
|
+
$description: "The light theme colors",
|
|
82
|
+
danger: colorToken(lightDanger)
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
dark: {
|
|
86
|
+
color: {
|
|
87
|
+
$description: "The dark theme colors",
|
|
88
|
+
danger: colorToken(darkDanger)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}) as any;
|
|
92
|
+
const light = result.light.color["danger-muted"];
|
|
93
|
+
const dark = result.dark.color["danger-muted"];
|
|
94
|
+
|
|
95
|
+
expect(light.$description).toBe(
|
|
96
|
+
"Generated low-emphasis danger background for the light theme"
|
|
97
|
+
);
|
|
98
|
+
expect(dark.$description).toBe(
|
|
99
|
+
"Generated low-emphasis danger background for the dark theme"
|
|
100
|
+
);
|
|
101
|
+
expect(relativeLuminance(light.$value)).toBeGreaterThan(
|
|
102
|
+
relativeLuminance(lightDanger)
|
|
103
|
+
);
|
|
104
|
+
expect(relativeLuminance(dark.$value)).toBeLessThan(
|
|
105
|
+
relativeLuminance(darkDanger)
|
|
106
|
+
);
|
|
107
|
+
expect(
|
|
108
|
+
contrastRatio(light.$value, result.light.color["on-danger-muted"].$value)
|
|
109
|
+
).toBeGreaterThanOrEqual(6.6);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("preserves explicit on-* and *-muted colors and skips primary companions", () => {
|
|
113
|
+
const result = tamaguiPreprocessor({
|
|
114
|
+
color: {
|
|
115
|
+
$description: "The light theme colors",
|
|
116
|
+
primary: colorToken("#336699"),
|
|
117
|
+
info: colorToken("#336699"),
|
|
118
|
+
"on-info": colorToken("#f0f0f0"),
|
|
119
|
+
"info-muted": colorToken("#abcdef")
|
|
120
|
+
}
|
|
121
|
+
}) as any;
|
|
122
|
+
|
|
123
|
+
expect(result.color["on-info-accent"]).toMatchObject({
|
|
124
|
+
$value: "#f0f0f0",
|
|
125
|
+
theme: "info"
|
|
126
|
+
});
|
|
127
|
+
expect(result.color).not.toHaveProperty("on-info");
|
|
128
|
+
expect(result.color["info-muted"].$value).toBe("#abcdef");
|
|
129
|
+
expect(result.color).not.toHaveProperty("on-primary");
|
|
130
|
+
expect(result.color).not.toHaveProperty("primary-muted");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("preserves the status hue when generated colors require gamut mapping", () => {
|
|
134
|
+
const danger = "#ff0000";
|
|
135
|
+
const result = tamaguiPreprocessor({
|
|
136
|
+
light: {
|
|
137
|
+
color: {
|
|
138
|
+
$description: "The light theme colors",
|
|
139
|
+
danger: colorToken(danger)
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
dark: {
|
|
143
|
+
color: {
|
|
144
|
+
$description: "The dark theme colors",
|
|
145
|
+
danger: colorToken(danger)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}) as any;
|
|
149
|
+
|
|
150
|
+
for (const color of [
|
|
151
|
+
result.light.color["danger-muted"].$value,
|
|
152
|
+
result.dark.color["danger-muted"].$value
|
|
153
|
+
]) {
|
|
154
|
+
expect(
|
|
155
|
+
hueDistance(oklchHue(color), oklchHue(danger))
|
|
156
|
+
).toBeLessThanOrEqual(1);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("resolves status references before generating companions", () => {
|
|
161
|
+
const result = tamaguiPreprocessor({
|
|
162
|
+
palette: { red: colorToken("#336699") },
|
|
163
|
+
light: {
|
|
164
|
+
color: {
|
|
165
|
+
$description: "The light theme colors",
|
|
166
|
+
danger: colorToken("{palette.red}")
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}) as any;
|
|
170
|
+
|
|
171
|
+
expect(result.light.color["on-danger-accent"].$value).toBe("#FAFAFA");
|
|
172
|
+
expect(result.light.color["danger-muted"].$value).toMatch(/^#[0-9a-f]{6}$/);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("derives low-saturation states from the contrast primitive and scales saturated colors", () => {
|
|
176
|
+
const color = {
|
|
177
|
+
primary: colorToken("#777777"),
|
|
178
|
+
danger: colorToken("#336699")
|
|
179
|
+
};
|
|
180
|
+
const result = tamaguiPreprocessor({
|
|
181
|
+
dark: { color: { $description: "The dark theme colors", ...color } },
|
|
182
|
+
light: { color: { $description: "The light theme colors", ...color } }
|
|
183
|
+
}) as any;
|
|
184
|
+
const dark = result.dark.color;
|
|
185
|
+
const light = result.light.color;
|
|
186
|
+
|
|
187
|
+
expect({
|
|
188
|
+
darkActive: dark["base-active"].$value,
|
|
189
|
+
lightActive: light["base-active"].$value,
|
|
190
|
+
darkActiveDescription: dark["base-active"].$description,
|
|
191
|
+
darkInactive: dark["base-inactive"].$description,
|
|
192
|
+
darkDanger: dark["danger-active"].$description,
|
|
193
|
+
lightDanger: light["danger-active"].$description,
|
|
194
|
+
dangerInactive: dark["danger-inactive"].$description
|
|
195
|
+
}).toEqual({
|
|
196
|
+
darkActive: "#ffffff",
|
|
197
|
+
lightActive: "#0c0c0d",
|
|
198
|
+
darkActiveDescription: "active state at dark contrast primitive",
|
|
199
|
+
darkInactive: "inactive state at 40% darker",
|
|
200
|
+
darkDanger: "active state at 19% brighter",
|
|
201
|
+
lightDanger: "active state at 19% darker",
|
|
202
|
+
dangerInactive: "inactive state at 20% darker"
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("shifts active colors already near the contrast primitive toward the middle", () => {
|
|
207
|
+
const result = tamaguiPreprocessor({
|
|
208
|
+
dark: {
|
|
209
|
+
color: {
|
|
210
|
+
$description: "The dark theme colors",
|
|
211
|
+
primary: colorToken("#f1f1f1")
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
light: {
|
|
215
|
+
color: {
|
|
216
|
+
$description: "The light theme colors",
|
|
217
|
+
primary: colorToken("#1f1f21")
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}) as any;
|
|
221
|
+
|
|
222
|
+
expect({
|
|
223
|
+
dark: result.dark.color["base-active"].$description,
|
|
224
|
+
light: result.light.color["base-active"].$description
|
|
225
|
+
}).toEqual({
|
|
226
|
+
dark: "active state at 10% darker",
|
|
227
|
+
light: "active state at 10% lighter"
|
|
228
|
+
});
|
|
229
|
+
expect(
|
|
230
|
+
relativeLuminance(result.dark.color["base-active"].$value)
|
|
231
|
+
).toBeLessThan(relativeLuminance("#f1f1f1"));
|
|
232
|
+
expect(
|
|
233
|
+
relativeLuminance(result.light.color["base-active"].$value)
|
|
234
|
+
).toBeGreaterThan(relativeLuminance("#1f1f21"));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("keeps low-saturation surface states subtler than other neutral colors", () => {
|
|
238
|
+
const result = tamaguiPreprocessor({
|
|
239
|
+
color: {
|
|
240
|
+
$description: "The dark theme colors",
|
|
241
|
+
primary: colorToken("#333333"),
|
|
242
|
+
surface: { canvas: colorToken("#333333") }
|
|
243
|
+
}
|
|
244
|
+
}) as any;
|
|
245
|
+
|
|
246
|
+
expect({
|
|
247
|
+
primaryActive: result.color["base-active"].$value,
|
|
248
|
+
canvasActive: result.color.surface["canvas-active"].$description,
|
|
249
|
+
canvasInactive: result.color.surface["canvas-inactive"].$description
|
|
250
|
+
}).toEqual({
|
|
251
|
+
primaryActive: "#ffffff",
|
|
252
|
+
canvasActive: "active state at 10% brighter",
|
|
253
|
+
canvasInactive: "inactive state at 20% darker"
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("increases outline active strength", () => {
|
|
258
|
+
const result = tamaguiPreprocessor({
|
|
259
|
+
color: {
|
|
260
|
+
outline: colorToken("#333333")
|
|
261
|
+
}
|
|
262
|
+
}) as any;
|
|
263
|
+
const saturated = tamaguiPreprocessor({
|
|
264
|
+
color: { outline: colorToken("#336699") }
|
|
265
|
+
}) as any;
|
|
266
|
+
|
|
267
|
+
expect({
|
|
268
|
+
outline: result.color["outline-active"].$description,
|
|
269
|
+
outlineSaturated: saturated.color["outline-active"].$description
|
|
270
|
+
}).toEqual({
|
|
271
|
+
outline: "active state at 80% brighter",
|
|
272
|
+
outlineSaturated: "active state at 40% brighter"
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("generates active, inactive, and disabled states only for target colors", () => {
|
|
277
|
+
const result = tamaguiPreprocessor({
|
|
278
|
+
color: {
|
|
279
|
+
surface: {
|
|
280
|
+
sunken: colorToken("#111111"),
|
|
281
|
+
canvas: colorToken("#222222")
|
|
282
|
+
},
|
|
283
|
+
primary: colorToken("#f5f5f5"),
|
|
284
|
+
"on-primary": colorToken("#111111"),
|
|
285
|
+
secondary: colorToken("#777777"),
|
|
286
|
+
tertiary: colorToken("#777777"),
|
|
287
|
+
outline: colorToken("#777777"),
|
|
288
|
+
danger: colorToken("#ff0000"),
|
|
289
|
+
"on-danger": colorToken("#ffffff"),
|
|
290
|
+
"danger-muted": colorToken("#330000"),
|
|
291
|
+
backdrop: colorToken("#00000066"),
|
|
292
|
+
data: { red: { emphasis: colorToken("#555555") } }
|
|
293
|
+
}
|
|
294
|
+
}) as any;
|
|
295
|
+
|
|
296
|
+
expect(Object.keys(result.color.surface).sort()).toEqual([
|
|
297
|
+
"canvas",
|
|
298
|
+
"canvas-active",
|
|
299
|
+
"canvas-disabled",
|
|
300
|
+
"canvas-inactive",
|
|
301
|
+
"sunken",
|
|
302
|
+
"sunken-active",
|
|
303
|
+
"sunken-disabled",
|
|
304
|
+
"sunken-inactive"
|
|
305
|
+
]);
|
|
306
|
+
for (const key of [
|
|
307
|
+
"base",
|
|
308
|
+
"base-muted",
|
|
309
|
+
"outline",
|
|
310
|
+
"danger",
|
|
311
|
+
"danger-muted"
|
|
312
|
+
]) {
|
|
313
|
+
for (const state of ["active", "inactive", "disabled"]) {
|
|
314
|
+
expect(result.color).toHaveProperty(`${key}-${state}`);
|
|
315
|
+
}
|
|
316
|
+
expect(result.color).not.toHaveProperty(`${key}-hover`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const key of ["primary", "secondary", "tertiary", "on-primary"]) {
|
|
320
|
+
expect(result.color).toHaveProperty(key);
|
|
321
|
+
expect(result.color).not.toHaveProperty(`${key}-active`);
|
|
322
|
+
}
|
|
323
|
+
expect(result.color).not.toHaveProperty("base-accent-active");
|
|
324
|
+
expect(result.color).not.toHaveProperty("on-base-accent-active");
|
|
325
|
+
expect(result.color).not.toHaveProperty("on-danger-active");
|
|
326
|
+
expect(result.color).not.toHaveProperty("backdrop-active");
|
|
327
|
+
expect(result.color.data.red).not.toHaveProperty("emphasis-active");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("does not overwrite explicit state colors", () => {
|
|
331
|
+
const result = tamaguiPreprocessor({
|
|
332
|
+
color: {
|
|
333
|
+
surface: {
|
|
334
|
+
canvas: colorToken("#222222"),
|
|
335
|
+
"canvas-active": colorToken("#abcdef")
|
|
336
|
+
},
|
|
337
|
+
outline: colorToken("#336699"),
|
|
338
|
+
"outline-active": colorToken("#fedcba")
|
|
339
|
+
}
|
|
340
|
+
}) as any;
|
|
341
|
+
|
|
342
|
+
expect(result.color.surface["canvas-active"].$value).toBe("#abcdef");
|
|
343
|
+
expect(result.color["outline-active"].$value).toBe("#fedcba");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("does not generate states for non-color tokens in target groups", () => {
|
|
347
|
+
const result = tamaguiPreprocessor({
|
|
348
|
+
color: {
|
|
349
|
+
surface: {
|
|
350
|
+
shadow: {
|
|
351
|
+
$type: "shadow",
|
|
352
|
+
$value: { color: "#336699", offsetX: 0 }
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}) as any;
|
|
357
|
+
|
|
358
|
+
expect(result.color.surface).not.toHaveProperty("shadow-active");
|
|
359
|
+
expect(result.color.surface.shadow.$value.color).toBe("#336699");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("desaturates colored disabled variants and fades them over the disabled base", () => {
|
|
363
|
+
const result = tamaguiPreprocessor({
|
|
364
|
+
color: { danger: colorToken("#ff000080") }
|
|
365
|
+
}) as any;
|
|
366
|
+
|
|
367
|
+
expect(result.color["danger-disabled"]).toMatchObject({
|
|
368
|
+
$description:
|
|
369
|
+
"disabled state at 85% saturation, 13% over oklch(0.3839 0.0045 286.26)",
|
|
370
|
+
$value: expect.stringMatching(/^#[0-9a-f]{6}$/)
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("fades greyscale surface disabled variants over the disabled base", () => {
|
|
375
|
+
const result = tamaguiPreprocessor({
|
|
376
|
+
color: { surface: { canvas: colorToken("#ffffff") } }
|
|
377
|
+
}) as any;
|
|
378
|
+
|
|
379
|
+
expect(result.color.surface["canvas-disabled"]).toMatchObject({
|
|
380
|
+
$description: "disabled state at 13% over oklch(0.3839 0.0045 286.26)",
|
|
381
|
+
$value: "#5b5b5d"
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("assigns themes to base aliases and status colors", () => {
|
|
386
|
+
const result = tamaguiPreprocessor({
|
|
387
|
+
color: {
|
|
388
|
+
$description: "The dark theme colors",
|
|
389
|
+
primary: colorToken("#eeeeee"),
|
|
390
|
+
"on-primary": colorToken("#111111"),
|
|
391
|
+
tertiary: { ...colorToken("#444444"), theme: "custom" },
|
|
392
|
+
outline: colorToken("#777777"),
|
|
393
|
+
warning: colorToken("#ffee99")
|
|
394
|
+
}
|
|
395
|
+
}) as any;
|
|
396
|
+
|
|
397
|
+
expect(result.color.primary.theme).toBeUndefined();
|
|
398
|
+
expect(result.color["on-primary"].theme).toBeUndefined();
|
|
399
|
+
expect(result.color.base).toMatchObject({
|
|
400
|
+
$value: "#eeeeee",
|
|
401
|
+
theme: "base",
|
|
402
|
+
group: "base"
|
|
403
|
+
});
|
|
404
|
+
expect(result.color["base-accent"]).toMatchObject({
|
|
405
|
+
$value: "{color.base}",
|
|
406
|
+
theme: "base"
|
|
407
|
+
});
|
|
408
|
+
expect(result.color["base-active"].theme).toBe("base");
|
|
409
|
+
expect(result.color).not.toHaveProperty("on-base");
|
|
410
|
+
expect(result.color["on-base-accent"]).toMatchObject({
|
|
411
|
+
$value: "#111111",
|
|
412
|
+
theme: "base"
|
|
413
|
+
});
|
|
414
|
+
expect(result.color.tertiary.theme).toBe("custom");
|
|
415
|
+
expect(result.color).not.toHaveProperty("tertiary-disabled");
|
|
416
|
+
expect(result.color.outline.theme).toBeUndefined();
|
|
417
|
+
expect(result.color.warning.theme).toBe("warning");
|
|
418
|
+
expect(result.color["warning-accent"]).toMatchObject({
|
|
419
|
+
$value: "{color.warning}",
|
|
420
|
+
theme: "warning",
|
|
421
|
+
group: "warning"
|
|
422
|
+
});
|
|
423
|
+
expect(result.color["on-warning-accent"]).toMatchObject({
|
|
424
|
+
theme: "warning",
|
|
425
|
+
group: "warning"
|
|
426
|
+
});
|
|
427
|
+
expect(result.color["on-warning-muted"].theme).toBe("warning");
|
|
428
|
+
expect(result.color["warning-muted"].theme).toBe("warning");
|
|
429
|
+
expect(result.color["warning-muted-active"].theme).toBe("warning");
|
|
430
|
+
expect(result.color["warning-inactive"].theme).toBe("warning");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("generates focus rings for base aliases and status colors", () => {
|
|
434
|
+
const result = tamaguiPreprocessor({
|
|
435
|
+
color: {
|
|
436
|
+
surface: { elevated: colorToken("#222222") },
|
|
437
|
+
primary: colorToken("#eeeeee"),
|
|
438
|
+
outline: colorToken("#777777"),
|
|
439
|
+
danger: colorToken("#336699")
|
|
440
|
+
}
|
|
441
|
+
}) as any;
|
|
442
|
+
|
|
443
|
+
expect(Object.keys(result.ring).sort()).toEqual([
|
|
444
|
+
"base",
|
|
445
|
+
"base-offset",
|
|
446
|
+
"base-subtle",
|
|
447
|
+
"base-subtle-offset",
|
|
448
|
+
"danger",
|
|
449
|
+
"danger-offset",
|
|
450
|
+
"danger-subtle",
|
|
451
|
+
"danger-subtle-offset"
|
|
452
|
+
]);
|
|
453
|
+
expect(result.ring["base-offset"]).toMatchObject({ theme: "base" });
|
|
454
|
+
expect(result.ring.base).toMatchObject({
|
|
455
|
+
theme: "base",
|
|
456
|
+
$value: [{ color: "#eeeeee14", spread: { value: 3, unit: "px" } }]
|
|
457
|
+
});
|
|
458
|
+
expect(result.ring["danger-offset"]).toMatchObject({
|
|
459
|
+
theme: "danger",
|
|
460
|
+
$value: [
|
|
461
|
+
{ color: "{color.surface-elevated}" },
|
|
462
|
+
{ color: "#33669926", spread: { value: 6, unit: "px" } }
|
|
463
|
+
]
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("applies ring opacity by saturation and preserves offset masks", () => {
|
|
468
|
+
const result = tamaguiPreprocessor({
|
|
469
|
+
color: { ink: { secondary: colorToken("#336699") } },
|
|
470
|
+
semantic: {
|
|
471
|
+
ring: {
|
|
472
|
+
neutral: {
|
|
473
|
+
$type: "shadow",
|
|
474
|
+
$value: { color: "#777777", offsetX: 0 }
|
|
475
|
+
},
|
|
476
|
+
saturated: {
|
|
477
|
+
$type: "shadow",
|
|
478
|
+
$value: {
|
|
479
|
+
color: "{color.ink.secondary}",
|
|
480
|
+
offsetX: { value: 0, unit: "px" },
|
|
481
|
+
offsetY: { value: 0, unit: "px" },
|
|
482
|
+
blur: { value: 0, unit: "px" },
|
|
483
|
+
spread: { value: 2, unit: "px" }
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
layered: {
|
|
487
|
+
$type: "shadow",
|
|
488
|
+
$value: [
|
|
489
|
+
{ color: "#ff0000", offsetX: 0 },
|
|
490
|
+
{ color: "#00ff00", offsetX: 0 }
|
|
491
|
+
]
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
shadow: {
|
|
495
|
+
regular: {
|
|
496
|
+
$type: "shadow",
|
|
497
|
+
$value: { color: "#336699", offsetX: 0 }
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
expect(result).toMatchObject({
|
|
504
|
+
semantic: {
|
|
505
|
+
ring: {
|
|
506
|
+
neutral: { $value: { color: "#77777714" } },
|
|
507
|
+
saturated: { $value: { color: "#33669926" } },
|
|
508
|
+
layered: {
|
|
509
|
+
$value: [{ color: "#ff0000" }, { color: "#00ff0026" }]
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
shadow: {
|
|
513
|
+
regular: { $value: { color: "#336699" } }
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("@razorwind/color-variants", () => ({
|
|
4
|
+
default: () => ({ name: "color-variants" })
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("@razorwind/core", () => ({
|
|
8
|
+
defineConfig: <T>(config: T) => config
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("@razorwind/core/plugin", () => ({
|
|
12
|
+
definePlugin: <T>(plugin: T) => plugin
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@razorwind/css/generate", () => ({
|
|
16
|
+
default: (options: unknown = {}) => options
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("@razorwind/design-md/generate", () => ({
|
|
20
|
+
default: (options: unknown = {}) => options
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("@razorwind/docgen/generate", () => ({
|
|
24
|
+
default: (options: unknown = {}) => options
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("@razorwind/llms/generate", () => ({
|
|
28
|
+
default: (options: unknown = {}) => options
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("@razorwind/shadcn/generate", () => ({
|
|
32
|
+
default: (options: unknown = {}) => options
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("@razorwind/storybook/generate", () => ({
|
|
36
|
+
default: (options: unknown = {}) => options
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock("@razorwind/tamagui/generate", () => ({
|
|
40
|
+
default: (options: unknown = {}) => options
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock("@razorwind/shiki/generate", () => ({
|
|
44
|
+
default: (options: unknown) => options
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
type Token = { $value: string };
|
|
48
|
+
type MappedTheme = {
|
|
49
|
+
colors: Record<string, string>;
|
|
50
|
+
fg: string;
|
|
51
|
+
name: string;
|
|
52
|
+
settings: Array<{
|
|
53
|
+
scope: string[];
|
|
54
|
+
settings: { foreground: string };
|
|
55
|
+
}>;
|
|
56
|
+
};
|
|
57
|
+
type ShikiPlugin = {
|
|
58
|
+
mapTheme: (tokens: unknown) => MappedTheme | MappedTheme[];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const color = ($value: string): Token => ({ $value });
|
|
62
|
+
|
|
63
|
+
const semanticColors = (prefix: string) => ({
|
|
64
|
+
color: {
|
|
65
|
+
primary: color(`${prefix}-primary`),
|
|
66
|
+
secondary: color(`${prefix}-secondary`),
|
|
67
|
+
tertiary: color(`${prefix}-tertiary`),
|
|
68
|
+
border: color(`${prefix}-border`),
|
|
69
|
+
"surface-canvas": color(`${prefix}-surface-canvas`),
|
|
70
|
+
"surface-sunken": color(`${prefix}-surface-sunken`),
|
|
71
|
+
danger: color(`${prefix}-danger`),
|
|
72
|
+
warning: color(`${prefix}-warning`),
|
|
73
|
+
success: color(`${prefix}-success`),
|
|
74
|
+
info: color(`${prefix}-info`),
|
|
75
|
+
discovery: color(`${prefix}-discovery`)
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const setting = (
|
|
80
|
+
theme: MappedTheme,
|
|
81
|
+
scope: string
|
|
82
|
+
) =>
|
|
83
|
+
theme.settings.find(candidate => candidate.scope.includes(scope))?.settings
|
|
84
|
+
.foreground;
|
|
85
|
+
|
|
86
|
+
describe("Shiki theme mapping", () => {
|
|
87
|
+
it("maps the latest semantic token roles for each named theme", async () => {
|
|
88
|
+
const config = (await import("../../../razorwind.config")).default as {
|
|
89
|
+
plugins: unknown[];
|
|
90
|
+
};
|
|
91
|
+
const shikiPlugin = config.plugins.find(
|
|
92
|
+
(plugin): plugin is ShikiPlugin =>
|
|
93
|
+
typeof plugin === "object" &&
|
|
94
|
+
plugin !== null &&
|
|
95
|
+
"mapTheme" in plugin &&
|
|
96
|
+
typeof plugin.mapTheme === "function"
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(shikiPlugin).toBeDefined();
|
|
100
|
+
|
|
101
|
+
const dark = shikiPlugin!.mapTheme({
|
|
102
|
+
theme: "dark",
|
|
103
|
+
tokens: semanticColors("dark")
|
|
104
|
+
}) as MappedTheme;
|
|
105
|
+
const light = shikiPlugin!.mapTheme({
|
|
106
|
+
theme: "light",
|
|
107
|
+
tokens: semanticColors("light")
|
|
108
|
+
}) as MappedTheme;
|
|
109
|
+
|
|
110
|
+
expect(dark.fg).toBe("dark-primary");
|
|
111
|
+
expect(dark.colors).toMatchObject({
|
|
112
|
+
"editor.background": "dark-surface-canvas",
|
|
113
|
+
"editorCursor.foreground": "dark-info",
|
|
114
|
+
"editor.selectionBackground": "dark-primary",
|
|
115
|
+
"editor.selectionForeground": "dark-surface-canvas",
|
|
116
|
+
"editor.inactiveSelectionBackground": "dark-surface-sunken",
|
|
117
|
+
"editorIndentGuide.background1": "dark-border",
|
|
118
|
+
"editorIndentGuide.activeBackground1": "dark-tertiary"
|
|
119
|
+
});
|
|
120
|
+
expect(setting(dark, "comment")).toBe("dark-tertiary");
|
|
121
|
+
expect(setting(dark, "string")).toBe("dark-success");
|
|
122
|
+
expect(setting(dark, "constant")).toBe("dark-warning");
|
|
123
|
+
expect(setting(dark, "keyword")).toBe("dark-info");
|
|
124
|
+
expect(setting(dark, "entity.name.function")).toBe("dark-info");
|
|
125
|
+
expect(setting(dark, "entity.name.type")).toBe("dark-discovery");
|
|
126
|
+
expect(setting(dark, "entity.other.attribute-name")).toBe("dark-info");
|
|
127
|
+
expect(setting(dark, "invalid")).toBe("dark-danger");
|
|
128
|
+
|
|
129
|
+
expect(light.fg).toBe("light-primary");
|
|
130
|
+
expect(light.colors["editor.background"]).toBe("light-surface-canvas");
|
|
131
|
+
expect(setting(light, "string")).toBe("light-success");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("resolves token aliases to literal colors", async () => {
|
|
135
|
+
const config = (await import("../../../razorwind.config")).default as {
|
|
136
|
+
plugins: unknown[];
|
|
137
|
+
};
|
|
138
|
+
const shikiPlugin = config.plugins.find(
|
|
139
|
+
(plugin): plugin is ShikiPlugin =>
|
|
140
|
+
typeof plugin === "object" &&
|
|
141
|
+
plugin !== null &&
|
|
142
|
+
"mapTheme" in plugin &&
|
|
143
|
+
typeof plugin.mapTheme === "function"
|
|
144
|
+
);
|
|
145
|
+
const tokens = semanticColors("light");
|
|
146
|
+
|
|
147
|
+
tokens.color.tertiary = color("{color.neutral.7}");
|
|
148
|
+
Object.assign(tokens.color, {
|
|
149
|
+
neutral: { 7: color("#959698") }
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const light = shikiPlugin!.mapTheme({
|
|
153
|
+
theme: "light",
|
|
154
|
+
tokens
|
|
155
|
+
}) as MappedTheme;
|
|
156
|
+
|
|
157
|
+
expect(light.colors["editorLineNumber.foreground"]).toBe("#959698");
|
|
158
|
+
expect(setting(light, "comment")).toBe("#959698");
|
|
159
|
+
});
|
|
160
|
+
});
|