@cyclone-ui/theme-builder 0.0.0-stage → 0.0.5
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 +26 -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 +26 -0
- package/dist/preprocessor.d.cts.map +1 -0
- package/dist/preprocessor.d.mts +26 -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 +1507 -0
- package/test/preprocessor.spec.ts +497 -0
- package/test/shiki-theme-mapping.spec.ts +157 -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,497 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { tamaguiPreprocessor } from "../src/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-primary text colors for theme color groups", () => {
|
|
60
|
+
const result = tamaguiPreprocessor({
|
|
61
|
+
color: {
|
|
62
|
+
danger: { primary: colorToken("#336699") },
|
|
63
|
+
warning: { primary: colorToken("#ffee99") }
|
|
64
|
+
}
|
|
65
|
+
}) as any;
|
|
66
|
+
|
|
67
|
+
expect(result.color.danger["on-primary"]).toMatchObject({
|
|
68
|
+
$value: "#FAFAFA",
|
|
69
|
+
$description:
|
|
70
|
+
"Generated text and icon color on danger primary backgrounds"
|
|
71
|
+
});
|
|
72
|
+
expect(result.color.warning["on-primary"].$value).toBe("#151518");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("generates missing secondary 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: { primary: colorToken(lightDanger) }
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
dark: {
|
|
86
|
+
color: {
|
|
87
|
+
$description: "The dark theme colors",
|
|
88
|
+
danger: { primary: colorToken(darkDanger) }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}) as any;
|
|
92
|
+
const light = result.light.color.danger.secondary;
|
|
93
|
+
const dark = result.dark.color.danger.secondary;
|
|
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(
|
|
109
|
+
light.$value,
|
|
110
|
+
result.light.color.danger["on-secondary"].$value
|
|
111
|
+
)
|
|
112
|
+
).toBeGreaterThanOrEqual(7.25);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("preserves explicit companions and skips root colors", () => {
|
|
116
|
+
const result = tamaguiPreprocessor({
|
|
117
|
+
color: {
|
|
118
|
+
$description: "The light theme colors",
|
|
119
|
+
emphasis: colorToken("#336699"),
|
|
120
|
+
info: {
|
|
121
|
+
primary: colorToken("#336699"),
|
|
122
|
+
"on-primary": colorToken("#f0f0f0"),
|
|
123
|
+
secondary: colorToken("#abcdef")
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}) as any;
|
|
127
|
+
|
|
128
|
+
expect(result.color.info["on-primary"]).toMatchObject({
|
|
129
|
+
$value: "#f0f0f0",
|
|
130
|
+
theme: "info"
|
|
131
|
+
});
|
|
132
|
+
expect(result.color.info.secondary.$value).toBe("#abcdef");
|
|
133
|
+
expect(result.color.info["on-secondary"].$value).toBe("#151518");
|
|
134
|
+
expect(result.color).not.toHaveProperty("on-emphasis");
|
|
135
|
+
expect(result.color).not.toHaveProperty("emphasis-active");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("preserves the status hue when generated colors require gamut mapping", () => {
|
|
139
|
+
const danger = "#ff0000";
|
|
140
|
+
const result = tamaguiPreprocessor({
|
|
141
|
+
light: {
|
|
142
|
+
color: {
|
|
143
|
+
$description: "The light theme colors",
|
|
144
|
+
danger: { primary: colorToken(danger) }
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
dark: {
|
|
148
|
+
color: {
|
|
149
|
+
$description: "The dark theme colors",
|
|
150
|
+
danger: { primary: colorToken(danger) }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}) as any;
|
|
154
|
+
|
|
155
|
+
for (const color of [
|
|
156
|
+
result.light.color.danger.secondary.$value,
|
|
157
|
+
result.dark.color.danger.secondary.$value
|
|
158
|
+
]) {
|
|
159
|
+
expect(
|
|
160
|
+
hueDistance(oklchHue(color), oklchHue(danger))
|
|
161
|
+
).toBeLessThanOrEqual(1.5);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("resolves primary references before generating companions", () => {
|
|
166
|
+
const result = tamaguiPreprocessor({
|
|
167
|
+
palette: { red: colorToken("#336699") },
|
|
168
|
+
light: {
|
|
169
|
+
color: {
|
|
170
|
+
$description: "The light theme colors",
|
|
171
|
+
danger: { primary: colorToken("{palette.red}") }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}) as any;
|
|
175
|
+
|
|
176
|
+
expect(result.light.color.danger["on-primary"].$value).toBe("#FAFAFA");
|
|
177
|
+
expect(result.light.color.danger.secondary.$value).toMatch(
|
|
178
|
+
/^#[0-9a-f]{6}$/
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("derives low-saturation states from the contrast primitive and scales saturated colors", () => {
|
|
183
|
+
const color = {
|
|
184
|
+
base: { primary: colorToken("#777777") },
|
|
185
|
+
danger: { primary: colorToken("#336699") }
|
|
186
|
+
};
|
|
187
|
+
const result = tamaguiPreprocessor({
|
|
188
|
+
dark: { color: { $description: "The dark theme colors", ...color } },
|
|
189
|
+
light: { color: { $description: "The light theme colors", ...color } }
|
|
190
|
+
}) as any;
|
|
191
|
+
const dark = result.dark.color;
|
|
192
|
+
const light = result.light.color;
|
|
193
|
+
|
|
194
|
+
expect({
|
|
195
|
+
darkActive: dark.base["primary-active"].$value,
|
|
196
|
+
lightActive: light.base["primary-active"].$value,
|
|
197
|
+
darkActiveDescription: dark.base["primary-active"].$description,
|
|
198
|
+
darkInactive: dark.base["primary-inactive"].$description,
|
|
199
|
+
darkDanger: dark.danger["primary-active"].$description,
|
|
200
|
+
lightDanger: light.danger["primary-active"].$description,
|
|
201
|
+
dangerInactive: dark.danger["primary-inactive"].$description
|
|
202
|
+
}).toEqual({
|
|
203
|
+
darkActive: "#ffffff",
|
|
204
|
+
lightActive: "#0c0c0d",
|
|
205
|
+
darkActiveDescription: "active state at dark contrast primitive",
|
|
206
|
+
darkInactive: "inactive state at 40% darker",
|
|
207
|
+
darkDanger: "active state at 25% brighter",
|
|
208
|
+
lightDanger: "active state at 25% darker",
|
|
209
|
+
dangerInactive: "inactive state at 20% darker"
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("shifts hovered colors near the contrast primitive toward the middle", () => {
|
|
214
|
+
const result = tamaguiPreprocessor({
|
|
215
|
+
dark: {
|
|
216
|
+
color: {
|
|
217
|
+
$description: "The dark theme colors",
|
|
218
|
+
base: { primary: colorToken("#f1f1f1") }
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
light: {
|
|
222
|
+
color: {
|
|
223
|
+
$description: "The light theme colors",
|
|
224
|
+
base: { primary: colorToken("#1f1f21") }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}) as any;
|
|
228
|
+
const dark = result.dark.color.base;
|
|
229
|
+
const light = result.light.color.base;
|
|
230
|
+
|
|
231
|
+
expect({
|
|
232
|
+
darkHovered: dark["primary-hovered"].$description,
|
|
233
|
+
lightHovered: light["primary-hovered"].$description,
|
|
234
|
+
darkActive: dark["primary-active"].$value,
|
|
235
|
+
lightActive: light["primary-active"].$value
|
|
236
|
+
}).toEqual({
|
|
237
|
+
darkHovered: "hovered state at 25% darker",
|
|
238
|
+
lightHovered: "hovered state at 25% lighter",
|
|
239
|
+
darkActive: "#ffffff",
|
|
240
|
+
lightActive: "#000000"
|
|
241
|
+
});
|
|
242
|
+
expect(relativeLuminance(dark["primary-hovered"].$value)).toBeLessThan(
|
|
243
|
+
relativeLuminance("#f1f1f1")
|
|
244
|
+
);
|
|
245
|
+
expect(relativeLuminance(light["primary-hovered"].$value)).toBeGreaterThan(
|
|
246
|
+
relativeLuminance("#1f1f21")
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("increases hairline active strength", () => {
|
|
251
|
+
const result = tamaguiPreprocessor({
|
|
252
|
+
color: { hairline: colorToken("#333333") }
|
|
253
|
+
}) as any;
|
|
254
|
+
const saturated = tamaguiPreprocessor({
|
|
255
|
+
color: { hairline: colorToken("#336699") }
|
|
256
|
+
}) as any;
|
|
257
|
+
|
|
258
|
+
expect({
|
|
259
|
+
hairline: result.color["hairline-active"].$description,
|
|
260
|
+
hairlineSaturated: saturated.color["hairline-active"].$description
|
|
261
|
+
}).toEqual({
|
|
262
|
+
hairline: "active state at 80% brighter",
|
|
263
|
+
hairlineSaturated: "active state at 40% brighter"
|
|
264
|
+
});
|
|
265
|
+
expect(result.color["hairline-active"].group).toBe("hairline");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("generates hovered, active, inactive, and disabled states only for target colors", () => {
|
|
269
|
+
const result = tamaguiPreprocessor({
|
|
270
|
+
color: {
|
|
271
|
+
sunken: { ...colorToken("#111111"), group: "background" },
|
|
272
|
+
canvas: { ...colorToken("#222222"), group: "background" },
|
|
273
|
+
emphasis: { ...colorToken("#f5f5f5"), group: "foreground" },
|
|
274
|
+
ink: { ...colorToken("#777777"), group: "foreground" },
|
|
275
|
+
subtle: { ...colorToken("#777777"), group: "foreground" },
|
|
276
|
+
subtlest: colorToken("#777777"),
|
|
277
|
+
hairline: colorToken("#777777"),
|
|
278
|
+
base: {
|
|
279
|
+
primary: colorToken("#f5f5f5"),
|
|
280
|
+
secondary: colorToken("#777777")
|
|
281
|
+
},
|
|
282
|
+
danger: {
|
|
283
|
+
primary: colorToken("#ff0000"),
|
|
284
|
+
"on-primary": colorToken("#ffffff"),
|
|
285
|
+
secondary: colorToken("#330000")
|
|
286
|
+
},
|
|
287
|
+
backdrop: colorToken("#00000066"),
|
|
288
|
+
data: { red: { emphasis: colorToken("#555555") } }
|
|
289
|
+
}
|
|
290
|
+
}) as any;
|
|
291
|
+
const states = ["hovered", "active", "inactive", "disabled"];
|
|
292
|
+
|
|
293
|
+
for (const group of [result.color.base, result.color.danger]) {
|
|
294
|
+
for (const key of ["primary", "secondary"]) {
|
|
295
|
+
for (const state of states) {
|
|
296
|
+
expect(group).toHaveProperty(`${key}-${state}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
expect(group).not.toHaveProperty("on-primary-active");
|
|
300
|
+
expect(group).not.toHaveProperty("on-secondary-active");
|
|
301
|
+
}
|
|
302
|
+
for (const state of states) {
|
|
303
|
+
for (const key of [
|
|
304
|
+
"hairline",
|
|
305
|
+
"emphasis",
|
|
306
|
+
"ink",
|
|
307
|
+
"subtle",
|
|
308
|
+
"sunken",
|
|
309
|
+
"canvas"
|
|
310
|
+
]) {
|
|
311
|
+
expect(result.color).toHaveProperty(`${key}-${state}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
expect(result.color["ink-active"].group).toBe("foreground");
|
|
315
|
+
|
|
316
|
+
for (const key of ["subtlest", "backdrop"]) {
|
|
317
|
+
expect(result.color).toHaveProperty(key);
|
|
318
|
+
expect(result.color).not.toHaveProperty(`${key}-active`);
|
|
319
|
+
}
|
|
320
|
+
expect(result.color.data.red).not.toHaveProperty("emphasis-active");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("does not overwrite explicit state colors", () => {
|
|
324
|
+
const result = tamaguiPreprocessor({
|
|
325
|
+
color: {
|
|
326
|
+
hairline: colorToken("#336699"),
|
|
327
|
+
"hairline-active": colorToken("#fedcba"),
|
|
328
|
+
danger: {
|
|
329
|
+
primary: colorToken("#336699"),
|
|
330
|
+
"primary-active": colorToken("#abcdef")
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}) as any;
|
|
334
|
+
|
|
335
|
+
expect(result.color["hairline-active"].$value).toBe("#fedcba");
|
|
336
|
+
expect(result.color.danger["primary-active"].$value).toBe("#abcdef");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("skips theme color groups whose primary is not a color", () => {
|
|
340
|
+
const result = tamaguiPreprocessor({
|
|
341
|
+
color: {
|
|
342
|
+
danger: {
|
|
343
|
+
primary: {
|
|
344
|
+
$type: "shadow",
|
|
345
|
+
$value: { color: "#336699", offsetX: 0 }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}) as any;
|
|
350
|
+
|
|
351
|
+
expect(result.color.danger).not.toHaveProperty("primary-active");
|
|
352
|
+
expect(result.color.danger).not.toHaveProperty("on-primary");
|
|
353
|
+
expect(result.color.danger.primary.$value.color).toBe("#336699");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("desaturates colored disabled variants and fades them over the disabled base", () => {
|
|
357
|
+
const result = tamaguiPreprocessor({
|
|
358
|
+
color: { danger: { primary: colorToken("#ff000080") } }
|
|
359
|
+
}) as any;
|
|
360
|
+
|
|
361
|
+
expect(result.color.danger["primary-disabled"]).toMatchObject({
|
|
362
|
+
$description:
|
|
363
|
+
"disabled state at 85% saturation, 35% over oklch(0.3839 0.0045 286.26)",
|
|
364
|
+
$value: expect.stringMatching(/^#[0-9a-f]{6}$/)
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("assigns the group name as theme and group to theme color tokens", () => {
|
|
369
|
+
const result = tamaguiPreprocessor({
|
|
370
|
+
color: {
|
|
371
|
+
$description: "The dark theme colors",
|
|
372
|
+
emphasis: colorToken("#eeeeee"),
|
|
373
|
+
hairline: colorToken("#777777"),
|
|
374
|
+
base: {
|
|
375
|
+
primary: colorToken("#eeeeee"),
|
|
376
|
+
"on-primary": colorToken("#111111"),
|
|
377
|
+
secondary: { ...colorToken("#444444"), theme: "custom" }
|
|
378
|
+
},
|
|
379
|
+
warning: { primary: colorToken("#ffee99") }
|
|
380
|
+
}
|
|
381
|
+
}) as any;
|
|
382
|
+
const { base, warning } = result.color;
|
|
383
|
+
|
|
384
|
+
expect(result.color.emphasis.theme).toBeUndefined();
|
|
385
|
+
expect(result.color.hairline.theme).toBeUndefined();
|
|
386
|
+
expect(base.primary).toMatchObject({
|
|
387
|
+
$value: "#eeeeee",
|
|
388
|
+
theme: "base",
|
|
389
|
+
group: "base"
|
|
390
|
+
});
|
|
391
|
+
expect(base["on-primary"]).toMatchObject({
|
|
392
|
+
$value: "#111111",
|
|
393
|
+
theme: "base"
|
|
394
|
+
});
|
|
395
|
+
expect(base["primary-active"].theme).toBe("base");
|
|
396
|
+
expect(base.secondary.theme).toBe("custom");
|
|
397
|
+
for (const key of [
|
|
398
|
+
"primary",
|
|
399
|
+
"on-primary",
|
|
400
|
+
"secondary",
|
|
401
|
+
"on-secondary",
|
|
402
|
+
"secondary-active",
|
|
403
|
+
"primary-inactive"
|
|
404
|
+
]) {
|
|
405
|
+
expect(warning[key]).toMatchObject({
|
|
406
|
+
theme: "warning",
|
|
407
|
+
group: "warning"
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("generates focus rings for theme color groups", () => {
|
|
413
|
+
const result = tamaguiPreprocessor({
|
|
414
|
+
color: {
|
|
415
|
+
elevated: { ...colorToken("#222222"), group: "background" },
|
|
416
|
+
emphasis: colorToken("#eeeeee"),
|
|
417
|
+
base: { primary: colorToken("#eeeeee") },
|
|
418
|
+
danger: { primary: colorToken("#336699") }
|
|
419
|
+
}
|
|
420
|
+
}) as any;
|
|
421
|
+
|
|
422
|
+
expect(Object.keys(result.ring).sort()).toEqual([
|
|
423
|
+
"base",
|
|
424
|
+
"base-offset",
|
|
425
|
+
"base-subtle",
|
|
426
|
+
"base-subtle-offset",
|
|
427
|
+
"danger",
|
|
428
|
+
"danger-offset",
|
|
429
|
+
"danger-subtle",
|
|
430
|
+
"danger-subtle-offset"
|
|
431
|
+
]);
|
|
432
|
+
expect(result.ring["base-offset"]).toMatchObject({ theme: "base" });
|
|
433
|
+
expect(result.ring.base).toMatchObject({
|
|
434
|
+
theme: "base",
|
|
435
|
+
$value: [{ color: "#eeeeee1a", spread: { value: 3, unit: "px" } }]
|
|
436
|
+
});
|
|
437
|
+
expect(result.ring["danger-offset"]).toMatchObject({
|
|
438
|
+
theme: "danger",
|
|
439
|
+
$value: [
|
|
440
|
+
{ color: "{color.elevated}" },
|
|
441
|
+
{ color: "#33669926", spread: { value: 6, unit: "px" } }
|
|
442
|
+
]
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("applies ring opacity by saturation and preserves offset masks", () => {
|
|
447
|
+
const result = tamaguiPreprocessor({
|
|
448
|
+
color: { ink: { secondary: colorToken("#336699") } },
|
|
449
|
+
semantic: {
|
|
450
|
+
ring: {
|
|
451
|
+
neutral: {
|
|
452
|
+
$type: "shadow",
|
|
453
|
+
$value: { color: "#777777", offsetX: 0 }
|
|
454
|
+
},
|
|
455
|
+
saturated: {
|
|
456
|
+
$type: "shadow",
|
|
457
|
+
$value: {
|
|
458
|
+
color: "{color.ink.secondary}",
|
|
459
|
+
offsetX: { value: 0, unit: "px" },
|
|
460
|
+
offsetY: { value: 0, unit: "px" },
|
|
461
|
+
blur: { value: 0, unit: "px" },
|
|
462
|
+
spread: { value: 2, unit: "px" }
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
layered: {
|
|
466
|
+
$type: "shadow",
|
|
467
|
+
$value: [
|
|
468
|
+
{ color: "#ff0000", offsetX: 0 },
|
|
469
|
+
{ color: "#00ff00", offsetX: 0 }
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
shadow: {
|
|
474
|
+
regular: {
|
|
475
|
+
$type: "shadow",
|
|
476
|
+
$value: { color: "#336699", offsetX: 0 }
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
expect(result).toMatchObject({
|
|
483
|
+
semantic: {
|
|
484
|
+
ring: {
|
|
485
|
+
neutral: { $value: { color: "#7777771a" } },
|
|
486
|
+
saturated: { $value: { color: "#33669926" } },
|
|
487
|
+
layered: {
|
|
488
|
+
$value: [{ color: "#ff0000" }, { color: "#00ff0026" }]
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
shadow: {
|
|
492
|
+
regular: { $value: { color: "#336699" } }
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
emphasis: color(`${prefix}-emphasis`),
|
|
66
|
+
ink: color(`${prefix}-ink`),
|
|
67
|
+
subtle: color(`${prefix}-subtle`),
|
|
68
|
+
hairline: color(`${prefix}-hairline`),
|
|
69
|
+
canvas: color(`${prefix}-canvas`),
|
|
70
|
+
sunken: color(`${prefix}-sunken`),
|
|
71
|
+
danger: { primary: color(`${prefix}-danger`) },
|
|
72
|
+
warning: { primary: color(`${prefix}-warning`) },
|
|
73
|
+
success: { primary: color(`${prefix}-success`) },
|
|
74
|
+
info: { primary: color(`${prefix}-info`) },
|
|
75
|
+
discovery: { primary: color(`${prefix}-discovery`) }
|
|
76
|
+
} as Record<string, unknown>
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const setting = (theme: MappedTheme, scope: string) =>
|
|
80
|
+
theme.settings.find(candidate => candidate.scope.includes(scope))?.settings
|
|
81
|
+
.foreground;
|
|
82
|
+
|
|
83
|
+
describe("Shiki theme mapping", () => {
|
|
84
|
+
it("maps the latest semantic token roles for each named theme", async () => {
|
|
85
|
+
const config = (await import("../../../razorwind.config")).default as {
|
|
86
|
+
plugins: unknown[];
|
|
87
|
+
};
|
|
88
|
+
const shikiPlugin = config.plugins.find(
|
|
89
|
+
(plugin): plugin is ShikiPlugin =>
|
|
90
|
+
typeof plugin === "object" &&
|
|
91
|
+
plugin !== null &&
|
|
92
|
+
"mapTheme" in plugin &&
|
|
93
|
+
typeof plugin.mapTheme === "function"
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(shikiPlugin).toBeDefined();
|
|
97
|
+
|
|
98
|
+
const dark = shikiPlugin!.mapTheme({
|
|
99
|
+
theme: "dark",
|
|
100
|
+
tokens: semanticColors("dark")
|
|
101
|
+
}) as MappedTheme;
|
|
102
|
+
const light = shikiPlugin!.mapTheme({
|
|
103
|
+
theme: "light",
|
|
104
|
+
tokens: semanticColors("light")
|
|
105
|
+
}) as MappedTheme;
|
|
106
|
+
|
|
107
|
+
expect(dark.fg).toBe("dark-emphasis");
|
|
108
|
+
expect(dark.colors).toMatchObject({
|
|
109
|
+
"editor.background": "dark-canvas",
|
|
110
|
+
"editorCursor.foreground": "dark-info",
|
|
111
|
+
"editor.selectionBackground": "dark-emphasis",
|
|
112
|
+
"editor.selectionForeground": "dark-canvas",
|
|
113
|
+
"editor.inactiveSelectionBackground": "dark-sunken",
|
|
114
|
+
"editorIndentGuide.background1": "dark-hairline",
|
|
115
|
+
"editorIndentGuide.activeBackground1": "dark-subtle"
|
|
116
|
+
});
|
|
117
|
+
expect(setting(dark, "comment")).toBe("dark-subtle");
|
|
118
|
+
expect(setting(dark, "string")).toBe("dark-success");
|
|
119
|
+
expect(setting(dark, "constant")).toBe("dark-warning");
|
|
120
|
+
expect(setting(dark, "keyword")).toBe("dark-info");
|
|
121
|
+
expect(setting(dark, "entity.name.function")).toBe("dark-info");
|
|
122
|
+
expect(setting(dark, "entity.name.type")).toBe("dark-discovery");
|
|
123
|
+
expect(setting(dark, "entity.other.attribute-name")).toBe("dark-info");
|
|
124
|
+
expect(setting(dark, "invalid")).toBe("dark-danger");
|
|
125
|
+
|
|
126
|
+
expect(light.fg).toBe("light-emphasis");
|
|
127
|
+
expect(light.colors["editor.background"]).toBe("light-canvas");
|
|
128
|
+
expect(setting(light, "string")).toBe("light-success");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("resolves token aliases to literal colors", async () => {
|
|
132
|
+
const config = (await import("../../../razorwind.config")).default as {
|
|
133
|
+
plugins: unknown[];
|
|
134
|
+
};
|
|
135
|
+
const shikiPlugin = config.plugins.find(
|
|
136
|
+
(plugin): plugin is ShikiPlugin =>
|
|
137
|
+
typeof plugin === "object" &&
|
|
138
|
+
plugin !== null &&
|
|
139
|
+
"mapTheme" in plugin &&
|
|
140
|
+
typeof plugin.mapTheme === "function"
|
|
141
|
+
);
|
|
142
|
+
const tokens = semanticColors("light");
|
|
143
|
+
|
|
144
|
+
tokens.color.subtle = color("{color.neutral.7}");
|
|
145
|
+
Object.assign(tokens.color, {
|
|
146
|
+
neutral: { 7: color("#959698") }
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const light = shikiPlugin!.mapTheme({
|
|
150
|
+
theme: "light",
|
|
151
|
+
tokens
|
|
152
|
+
}) as MappedTheme;
|
|
153
|
+
|
|
154
|
+
expect(light.colors["editorLineNumber.foreground"]).toBe("#959698");
|
|
155
|
+
expect(setting(light, "comment")).toBe("#959698");
|
|
156
|
+
});
|
|
157
|
+
});
|