@cdx-ui/styles 0.0.1-beta.8 → 0.0.1-beta.81
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 +116 -20
- package/css/theme.css +201 -85
- package/css/vanilla.css +124 -54
- package/lib/commonjs/applyThemeOverride.js +154 -0
- package/lib/commonjs/applyThemeOverride.js.map +1 -0
- package/lib/commonjs/index.js +82 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/palette.js +262 -0
- package/lib/commonjs/palette.js.map +1 -0
- package/lib/commonjs/theming.js +255 -0
- package/lib/commonjs/theming.js.map +1 -0
- package/lib/commonjs/types.js +75 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useCdxFonts.js +7 -231
- package/lib/commonjs/useCdxFonts.js.map +1 -1
- package/lib/commonjs/useForgeFonts.js +237 -0
- package/lib/commonjs/useForgeFonts.js.map +1 -0
- package/lib/module/applyThemeOverride.js +149 -0
- package/lib/module/applyThemeOverride.js.map +1 -0
- package/lib/module/index.js +11 -20
- package/lib/module/index.js.map +1 -1
- package/lib/module/palette.js +257 -0
- package/lib/module/palette.js.map +1 -0
- package/lib/module/theming.js +239 -0
- package/lib/module/theming.js.map +1 -0
- package/lib/module/types.js +71 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useCdxFonts.js +2 -220
- package/lib/module/useCdxFonts.js.map +1 -1
- package/lib/module/useForgeFonts.js +223 -0
- package/lib/module/useForgeFonts.js.map +1 -0
- package/lib/runtime/prestige-vs-default.json +1 -0
- package/lib/runtime/pulse-vs-default.json +1 -0
- package/lib/runtime/token-to-css-var.json +672 -0
- package/lib/typescript/applyThemeOverride.d.ts +26 -0
- package/lib/typescript/applyThemeOverride.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +8 -57
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/palette.d.ts +60 -0
- package/lib/typescript/palette.d.ts.map +1 -0
- package/lib/typescript/theming.d.ts +40 -0
- package/lib/typescript/theming.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +90 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/useCdxFonts.d.ts +2 -11
- package/lib/typescript/useCdxFonts.d.ts.map +1 -1
- package/lib/typescript/useForgeFonts.d.ts +12 -0
- package/lib/typescript/useForgeFonts.d.ts.map +1 -0
- package/package.json +27 -8
- package/runtime/prestige-vs-default.json +1 -0
- package/runtime/pulse-vs-default.json +1 -0
- package/runtime/token-to-css-var.json +672 -0
- package/src/__tests__/applyThemeOverride.test.ts +552 -0
- package/src/__tests__/generateColorScale.test.ts +296 -0
- package/src/__tests__/theming.test.ts +647 -0
- package/src/applyThemeOverride.ts +139 -0
- package/src/index.ts +36 -60
- package/src/palette.ts +307 -0
- package/src/theming.ts +268 -0
- package/src/types.ts +112 -0
- package/src/useCdxFonts.ts +2 -230
- package/src/useForgeFonts.ts +230 -0
- package/tokens/presets/.manifest.json +3 -3
- package/tokens/presets/poise.json +319 -39
- package/tokens/presets/prestige.json +1 -1
- package/tokens/presets/pulse.json +1 -1
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import {
|
|
2
|
+
presetPatchToUniwindMaps,
|
|
3
|
+
themeOverrideToUniwindMaps,
|
|
4
|
+
isSchemaVersionSupported,
|
|
5
|
+
type RuntimeMap,
|
|
6
|
+
} from '../theming';
|
|
7
|
+
import type { ThemeOverride, TokenGroup } from '../types';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Mock runtime map — representative subset of the generated map
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const MOCK_RUNTIME_MAP: RuntimeMap = {
|
|
14
|
+
// Primitives (mode-independent)
|
|
15
|
+
'color.brand.50': '--color-brand-50',
|
|
16
|
+
'color.brand.100': '--color-brand-100',
|
|
17
|
+
'color.brand.200': '--color-brand-200',
|
|
18
|
+
'color.brand.300': '--color-brand-300',
|
|
19
|
+
'color.brand.400': '--color-brand-400',
|
|
20
|
+
'color.brand.500': '--color-brand-500',
|
|
21
|
+
'color.brand.600': '--color-brand-600',
|
|
22
|
+
'color.brand.700': '--color-brand-700',
|
|
23
|
+
'color.brand.800': '--color-brand-800',
|
|
24
|
+
'color.brand.900': '--color-brand-900',
|
|
25
|
+
'color.brand.950': '--color-brand-950',
|
|
26
|
+
'color.brand.input': '--color-brand-input',
|
|
27
|
+
'color.accent.50': '--color-accent-50',
|
|
28
|
+
'color.accent.100': '--color-accent-100',
|
|
29
|
+
'color.accent.200': '--color-accent-200',
|
|
30
|
+
'color.accent.300': '--color-accent-300',
|
|
31
|
+
'color.accent.400': '--color-accent-400',
|
|
32
|
+
'color.accent.500': '--color-accent-500',
|
|
33
|
+
'color.accent.600': '--color-accent-600',
|
|
34
|
+
'color.accent.700': '--color-accent-700',
|
|
35
|
+
'color.accent.800': '--color-accent-800',
|
|
36
|
+
'color.accent.900': '--color-accent-900',
|
|
37
|
+
'color.accent.950': '--color-accent-950',
|
|
38
|
+
'color.accent.input': '--color-accent-input',
|
|
39
|
+
'font.display': '--font-display',
|
|
40
|
+
|
|
41
|
+
// Mode-prefixed semantic tokens
|
|
42
|
+
'modes.light.color.surface.background': '--color-surface-background',
|
|
43
|
+
'modes.dark.color.surface.background': '--color-surface-background',
|
|
44
|
+
'modes.light.color.content.primary': '--color-content-primary',
|
|
45
|
+
'modes.dark.color.content.primary': '--color-content-primary',
|
|
46
|
+
'modes.light.color.stroke.primary': '--color-stroke-primary',
|
|
47
|
+
'modes.dark.color.stroke.primary': '--color-stroke-primary',
|
|
48
|
+
'modes.light.font.display': '--font-display',
|
|
49
|
+
'modes.dark.font.display': '--font-display',
|
|
50
|
+
'modes.light.border-radius.button': '--border-radius-button',
|
|
51
|
+
'modes.dark.border-radius.button': '--border-radius-button',
|
|
52
|
+
'modes.light.color.surface.action.strong': '--color-surface-action-strong',
|
|
53
|
+
'modes.dark.color.surface.action.strong': '--color-surface-action-strong',
|
|
54
|
+
'modes.light.color.content.link': '--color-content-link',
|
|
55
|
+
'modes.dark.color.content.link': '--color-content-link',
|
|
56
|
+
|
|
57
|
+
// Platform-prefixed tokens
|
|
58
|
+
'platform.web.font.sans': '--font-sans',
|
|
59
|
+
'platform.ios.font.sans': '--font-sans',
|
|
60
|
+
'platform.android.font.sans': '--font-sans',
|
|
61
|
+
'platform.web.font.mono': '--font-mono',
|
|
62
|
+
'platform.ios.font.mono': '--font-mono',
|
|
63
|
+
'platform.android.font.mono': '--font-mono',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// presetPatchToUniwindMaps
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
describe('presetPatchToUniwindMaps', () => {
|
|
71
|
+
it('converts a mode-scoped DTCG patch into per-mode variable maps', () => {
|
|
72
|
+
const patch: TokenGroup = {
|
|
73
|
+
modes: {
|
|
74
|
+
light: {
|
|
75
|
+
color: {
|
|
76
|
+
surface: {
|
|
77
|
+
background: { $type: 'color', $value: '#ffffff' },
|
|
78
|
+
},
|
|
79
|
+
content: {
|
|
80
|
+
primary: { $type: 'color', $value: '#1a1a1a' },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
dark: {
|
|
85
|
+
color: {
|
|
86
|
+
surface: {
|
|
87
|
+
background: { $type: 'color', $value: '#0d0d0d' },
|
|
88
|
+
},
|
|
89
|
+
content: {
|
|
90
|
+
primary: { $type: 'color', $value: '#f5f5f5' },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
} as unknown as TokenGroup;
|
|
96
|
+
|
|
97
|
+
const result = presetPatchToUniwindMaps(patch, MOCK_RUNTIME_MAP, 'web');
|
|
98
|
+
|
|
99
|
+
expect(result.light).toEqual({
|
|
100
|
+
'--color-surface-background': '#ffffff',
|
|
101
|
+
'--color-content-primary': '#1a1a1a',
|
|
102
|
+
});
|
|
103
|
+
expect(result.dark).toEqual({
|
|
104
|
+
'--color-surface-background': '#0d0d0d',
|
|
105
|
+
'--color-content-primary': '#f5f5f5',
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('places mode-independent primitives in BOTH light and dark maps', () => {
|
|
110
|
+
const patch: TokenGroup = {
|
|
111
|
+
color: {
|
|
112
|
+
brand: {
|
|
113
|
+
'500': { $type: 'color', $value: '#0052cc' },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
} as unknown as TokenGroup;
|
|
117
|
+
|
|
118
|
+
const result = presetPatchToUniwindMaps(patch, MOCK_RUNTIME_MAP, 'web');
|
|
119
|
+
|
|
120
|
+
expect(result.light['--color-brand-500']).toBe('#0052cc');
|
|
121
|
+
expect(result.dark['--color-brand-500']).toBe('#0052cc');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('filters platform tokens to only include matching runtimePlatform', () => {
|
|
125
|
+
const patch: TokenGroup = {
|
|
126
|
+
platform: {
|
|
127
|
+
web: {
|
|
128
|
+
font: {
|
|
129
|
+
sans: { $type: 'fontFamily', $value: 'Inter' },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
ios: {
|
|
133
|
+
font: {
|
|
134
|
+
sans: { $type: 'fontFamily', $value: 'SF Pro' },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
android: {
|
|
138
|
+
font: {
|
|
139
|
+
sans: { $type: 'fontFamily', $value: 'Roboto' },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
} as unknown as TokenGroup;
|
|
144
|
+
|
|
145
|
+
const resultWeb = presetPatchToUniwindMaps(patch, MOCK_RUNTIME_MAP, 'web');
|
|
146
|
+
expect(resultWeb.light['--font-sans']).toBe('Inter');
|
|
147
|
+
expect(resultWeb.dark['--font-sans']).toBe('Inter');
|
|
148
|
+
|
|
149
|
+
const resultIos = presetPatchToUniwindMaps(patch, MOCK_RUNTIME_MAP, 'ios');
|
|
150
|
+
expect(resultIos.light['--font-sans']).toBe('SF Pro');
|
|
151
|
+
expect(resultIos.dark['--font-sans']).toBe('SF Pro');
|
|
152
|
+
|
|
153
|
+
const resultAndroid = presetPatchToUniwindMaps(patch, MOCK_RUNTIME_MAP, 'android');
|
|
154
|
+
expect(resultAndroid.light['--font-sans']).toBe('Roboto');
|
|
155
|
+
expect(resultAndroid.dark['--font-sans']).toBe('Roboto');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('skips non-matching platform tokens', () => {
|
|
159
|
+
const patch: TokenGroup = {
|
|
160
|
+
platform: {
|
|
161
|
+
ios: {
|
|
162
|
+
font: {
|
|
163
|
+
mono: { $type: 'fontFamily', $value: 'Menlo' },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
} as unknown as TokenGroup;
|
|
168
|
+
|
|
169
|
+
const result = presetPatchToUniwindMaps(patch, MOCK_RUNTIME_MAP, 'web');
|
|
170
|
+
expect(result.light['--font-mono']).toBeUndefined();
|
|
171
|
+
expect(result.dark['--font-mono']).toBeUndefined();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('silently skips dot-paths not found in the runtime map', () => {
|
|
175
|
+
const patch: TokenGroup = {
|
|
176
|
+
modes: {
|
|
177
|
+
light: {
|
|
178
|
+
some: {
|
|
179
|
+
unknown: {
|
|
180
|
+
token: { $type: 'color', $value: '#abc123' },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
color: {
|
|
186
|
+
surface: {
|
|
187
|
+
background: { $type: 'color', $value: '#ffffff' },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
} as unknown as TokenGroup;
|
|
191
|
+
|
|
192
|
+
const result = presetPatchToUniwindMaps(patch, MOCK_RUNTIME_MAP, 'web');
|
|
193
|
+
|
|
194
|
+
expect(Object.keys(result.light)).toHaveLength(0);
|
|
195
|
+
expect(Object.keys(result.dark)).toHaveLength(0);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('handles mixed mode + primitive + platform tokens', () => {
|
|
199
|
+
const patch: TokenGroup = {
|
|
200
|
+
color: {
|
|
201
|
+
brand: {
|
|
202
|
+
'500': { $type: 'color', $value: '#336699' },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
modes: {
|
|
206
|
+
light: {
|
|
207
|
+
color: {
|
|
208
|
+
stroke: {
|
|
209
|
+
primary: { $type: 'color', $value: '#e0e0e0' },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
dark: {
|
|
214
|
+
color: {
|
|
215
|
+
stroke: {
|
|
216
|
+
primary: { $type: 'color', $value: '#333333' },
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
platform: {
|
|
222
|
+
ios: {
|
|
223
|
+
font: {
|
|
224
|
+
sans: { $type: 'fontFamily', $value: 'SF Pro Display' },
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
} as unknown as TokenGroup;
|
|
229
|
+
|
|
230
|
+
const result = presetPatchToUniwindMaps(patch, MOCK_RUNTIME_MAP, 'ios');
|
|
231
|
+
|
|
232
|
+
expect(result.light).toEqual({
|
|
233
|
+
'--color-brand-500': '#336699',
|
|
234
|
+
'--color-stroke-primary': '#e0e0e0',
|
|
235
|
+
'--font-sans': 'SF Pro Display',
|
|
236
|
+
});
|
|
237
|
+
expect(result.dark).toEqual({
|
|
238
|
+
'--color-brand-500': '#336699',
|
|
239
|
+
'--color-stroke-primary': '#333333',
|
|
240
|
+
'--font-sans': 'SF Pro Display',
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('converts numeric $value to string', () => {
|
|
245
|
+
const patch: TokenGroup = {
|
|
246
|
+
modes: {
|
|
247
|
+
light: {
|
|
248
|
+
'border-radius': {
|
|
249
|
+
button: { $type: 'dimension', $value: 8 },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
} as unknown as TokenGroup;
|
|
254
|
+
|
|
255
|
+
const result = presetPatchToUniwindMaps(patch, MOCK_RUNTIME_MAP, 'web');
|
|
256
|
+
expect(result.light['--border-radius-button']).toBe('8');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// themeOverrideToUniwindMaps
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
describe('themeOverrideToUniwindMaps', () => {
|
|
265
|
+
const baseOverride: ThemeOverride = {
|
|
266
|
+
$extensions: {
|
|
267
|
+
'com.forge.ui.themeOverride': {
|
|
268
|
+
basePreset: 'poise',
|
|
269
|
+
schemaVersion: '1.0.0',
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
describe('inputs only', () => {
|
|
275
|
+
it('generates palette from brandPrimary and places in both mode maps', () => {
|
|
276
|
+
const override: ThemeOverride = {
|
|
277
|
+
...baseOverride,
|
|
278
|
+
inputs: { brandPrimary: '#0052cc' },
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
282
|
+
|
|
283
|
+
expect(result.light['--color-brand-500']).toBeDefined();
|
|
284
|
+
expect(result.dark['--color-brand-500']).toBeDefined();
|
|
285
|
+
expect(result.light['--color-brand-input']).toBe('#0052cc');
|
|
286
|
+
expect(result.dark['--color-brand-input']).toBe('#0052cc');
|
|
287
|
+
expect(result.light['--color-brand-500']).toBe(result.dark['--color-brand-500']);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('generates palette from accentPrimary', () => {
|
|
291
|
+
const override: ThemeOverride = {
|
|
292
|
+
...baseOverride,
|
|
293
|
+
inputs: { accentPrimary: '#ff8c42' },
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
297
|
+
|
|
298
|
+
expect(result.light['--color-accent-input']).toBe('#ff8c42');
|
|
299
|
+
expect(result.dark['--color-accent-input']).toBe('#ff8c42');
|
|
300
|
+
expect(result.light['--color-accent-500']).toBeDefined();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('maps displayFont to font.display token path in both modes', () => {
|
|
304
|
+
const override: ThemeOverride = {
|
|
305
|
+
...baseOverride,
|
|
306
|
+
inputs: { displayFont: 'Crimson Pro' },
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
310
|
+
|
|
311
|
+
expect(result.light['--font-display']).toBe('Crimson Pro');
|
|
312
|
+
expect(result.dark['--font-display']).toBe('Crimson Pro');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('handles both colour and font inputs together', () => {
|
|
316
|
+
const override: ThemeOverride = {
|
|
317
|
+
...baseOverride,
|
|
318
|
+
inputs: {
|
|
319
|
+
brandPrimary: '#0052cc',
|
|
320
|
+
displayFont: 'Outfit',
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
325
|
+
|
|
326
|
+
expect(result.light['--color-brand-input']).toBe('#0052cc');
|
|
327
|
+
expect(result.light['--font-display']).toBe('Outfit');
|
|
328
|
+
expect(result.dark['--font-display']).toBe('Outfit');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('overrides only', () => {
|
|
333
|
+
it('maps explicit per-mode overrides to CSS variables', () => {
|
|
334
|
+
const override: ThemeOverride = {
|
|
335
|
+
...baseOverride,
|
|
336
|
+
overrides: {
|
|
337
|
+
light: {
|
|
338
|
+
'color.surface.background': '#fafafa',
|
|
339
|
+
'color.content.primary': '#111111',
|
|
340
|
+
},
|
|
341
|
+
dark: {
|
|
342
|
+
'color.surface.background': '#1a1a1a',
|
|
343
|
+
'color.content.primary': '#eeeeee',
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
349
|
+
|
|
350
|
+
expect(result.light['--color-surface-background']).toBe('#fafafa');
|
|
351
|
+
expect(result.light['--color-content-primary']).toBe('#111111');
|
|
352
|
+
expect(result.dark['--color-surface-background']).toBe('#1a1a1a');
|
|
353
|
+
expect(result.dark['--color-content-primary']).toBe('#eeeeee');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('handles light-only overrides without dark', () => {
|
|
357
|
+
const override: ThemeOverride = {
|
|
358
|
+
...baseOverride,
|
|
359
|
+
overrides: {
|
|
360
|
+
light: {
|
|
361
|
+
'color.surface.background': '#fefefe',
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
367
|
+
|
|
368
|
+
expect(result.light['--color-surface-background']).toBe('#fefefe');
|
|
369
|
+
expect(result.dark['--color-surface-background']).toBeUndefined();
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('inputs + overrides (collision handling)', () => {
|
|
374
|
+
it('explicit override wins over palette-generated value on collision', () => {
|
|
375
|
+
const override: ThemeOverride = {
|
|
376
|
+
...baseOverride,
|
|
377
|
+
inputs: { brandPrimary: '#0052cc' },
|
|
378
|
+
overrides: {
|
|
379
|
+
light: {
|
|
380
|
+
'color.brand.500': '#custom1',
|
|
381
|
+
},
|
|
382
|
+
dark: {
|
|
383
|
+
'color.brand.500': '#custom2',
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
389
|
+
|
|
390
|
+
expect(result.light['--color-brand-500']).toBe('#custom1');
|
|
391
|
+
expect(result.dark['--color-brand-500']).toBe('#custom2');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('non-colliding palette values coexist with explicit overrides', () => {
|
|
395
|
+
const override: ThemeOverride = {
|
|
396
|
+
...baseOverride,
|
|
397
|
+
inputs: { brandPrimary: '#0052cc' },
|
|
398
|
+
overrides: {
|
|
399
|
+
light: {
|
|
400
|
+
'color.surface.background': '#ffffff',
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
406
|
+
|
|
407
|
+
expect(result.light['--color-brand-input']).toBe('#0052cc');
|
|
408
|
+
expect(result.light['--color-surface-background']).toBe('#ffffff');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('font input survives when override targets different token', () => {
|
|
412
|
+
const override: ThemeOverride = {
|
|
413
|
+
...baseOverride,
|
|
414
|
+
inputs: { displayFont: 'Crimson Pro' },
|
|
415
|
+
overrides: {
|
|
416
|
+
light: {
|
|
417
|
+
'color.surface.background': '#ffffff',
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
423
|
+
|
|
424
|
+
expect(result.light['--font-display']).toBe('Crimson Pro');
|
|
425
|
+
expect(result.light['--color-surface-background']).toBe('#ffffff');
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('metadata-only override', () => {
|
|
430
|
+
it('returns empty maps when only $extensions is present', () => {
|
|
431
|
+
const override: ThemeOverride = {
|
|
432
|
+
$extensions: {
|
|
433
|
+
'com.forge.ui.themeOverride': {
|
|
434
|
+
basePreset: 'prestige',
|
|
435
|
+
schemaVersion: '1.0.0',
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
441
|
+
|
|
442
|
+
expect(result.light).toEqual({});
|
|
443
|
+
expect(result.dark).toEqual({});
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe('unknown input keys', () => {
|
|
448
|
+
it('silently ignores input keys not in INPUT_TOKEN_MAP', () => {
|
|
449
|
+
const override: ThemeOverride = {
|
|
450
|
+
...baseOverride,
|
|
451
|
+
inputs: {
|
|
452
|
+
brandPrimary: '#0052cc',
|
|
453
|
+
unknownKey: '#ff0000',
|
|
454
|
+
anotherUnknown: 'SomeFont',
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
459
|
+
|
|
460
|
+
expect(result.light['--color-brand-input']).toBe('#0052cc');
|
|
461
|
+
const allVars = Object.values(result.light);
|
|
462
|
+
expect(allVars).not.toContain('#ff0000');
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe('missing runtime map entries', () => {
|
|
467
|
+
it('silently skips override dot-paths not found in the runtime map', () => {
|
|
468
|
+
const override: ThemeOverride = {
|
|
469
|
+
...baseOverride,
|
|
470
|
+
overrides: {
|
|
471
|
+
light: {
|
|
472
|
+
'color.surface.background': '#ffffff',
|
|
473
|
+
'some.unknown.path': '#aaaaaa',
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
479
|
+
|
|
480
|
+
expect(result.light['--color-surface-background']).toBe('#ffffff');
|
|
481
|
+
expect(Object.keys(result.light)).toHaveLength(1);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
describe('token-reference aliases', () => {
|
|
486
|
+
it('resolves an alias override to a CSS var() reference', () => {
|
|
487
|
+
const override: ThemeOverride = {
|
|
488
|
+
...baseOverride,
|
|
489
|
+
overrides: {
|
|
490
|
+
light: { 'color.surface.action.strong': '{color.brand.700}' },
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
495
|
+
|
|
496
|
+
expect(result.light['--color-surface-action-strong']).toBe('var(--color-brand-700)');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('supports per-mode step shifts (brand.700 light / brand.300 dark)', () => {
|
|
500
|
+
const override: ThemeOverride = {
|
|
501
|
+
...baseOverride,
|
|
502
|
+
overrides: {
|
|
503
|
+
light: { 'color.surface.action.strong': '{color.brand.700}' },
|
|
504
|
+
dark: { 'color.surface.action.strong': '{color.brand.300}' },
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
509
|
+
|
|
510
|
+
expect(result.light['--color-surface-action-strong']).toBe('var(--color-brand-700)');
|
|
511
|
+
expect(result.dark['--color-surface-action-strong']).toBe('var(--color-brand-300)');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('resolves aliases targeting the accent scale', () => {
|
|
515
|
+
const override: ThemeOverride = {
|
|
516
|
+
...baseOverride,
|
|
517
|
+
overrides: {
|
|
518
|
+
light: { 'color.content.link': '{color.accent.600}' },
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
523
|
+
|
|
524
|
+
expect(result.light['--color-content-link']).toBe('var(--color-accent-600)');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('mixes alias and literal values in the same mode', () => {
|
|
528
|
+
const override: ThemeOverride = {
|
|
529
|
+
...baseOverride,
|
|
530
|
+
overrides: {
|
|
531
|
+
light: {
|
|
532
|
+
'color.surface.action.strong': '{color.brand.700}',
|
|
533
|
+
'color.surface.background': '#F5F7F6',
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
539
|
+
|
|
540
|
+
expect(result.light['--color-surface-action-strong']).toBe('var(--color-brand-700)');
|
|
541
|
+
expect(result.light['--color-surface-background']).toBe('#F5F7F6');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('passes a numeric literal through without alias treatment', () => {
|
|
545
|
+
const override: ThemeOverride = {
|
|
546
|
+
...baseOverride,
|
|
547
|
+
overrides: {
|
|
548
|
+
light: { 'border-radius.button': 8 },
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
553
|
+
|
|
554
|
+
expect(result.light['--border-radius-button']).toBe('8');
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('alias override wins over a palette-generated primitive on collision', () => {
|
|
558
|
+
const override: ThemeOverride = {
|
|
559
|
+
...baseOverride,
|
|
560
|
+
inputs: { brandPrimary: '#0052cc' },
|
|
561
|
+
overrides: {
|
|
562
|
+
light: { 'color.brand.500': '{color.brand.700}' },
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
567
|
+
|
|
568
|
+
expect(result.light['--color-brand-500']).toBe('var(--color-brand-700)');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('skips an alias whose referenced primitive is absent from the runtime map', () => {
|
|
572
|
+
const override: ThemeOverride = {
|
|
573
|
+
...baseOverride,
|
|
574
|
+
overrides: {
|
|
575
|
+
light: { 'color.surface.background': '{color.brand.999}' },
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
580
|
+
|
|
581
|
+
expect(result.light['--color-surface-background']).toBeUndefined();
|
|
582
|
+
expect(Object.keys(result.light)).toHaveLength(0);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('skips an alias targeting a non-primitive token (primitive-only constraint)', () => {
|
|
586
|
+
const override: ThemeOverride = {
|
|
587
|
+
...baseOverride,
|
|
588
|
+
overrides: {
|
|
589
|
+
// `color.content.primary` exists only as a mode-prefixed semantic
|
|
590
|
+
// token, not as a resolvable primitive path — so it is not a valid
|
|
591
|
+
// alias target and resolution skips it.
|
|
592
|
+
light: { 'color.surface.action.strong': '{color.content.primary}' },
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const result = themeOverrideToUniwindMaps(override, MOCK_RUNTIME_MAP, 'web');
|
|
597
|
+
|
|
598
|
+
expect(result.light['--color-surface-action-strong']).toBeUndefined();
|
|
599
|
+
expect(Object.keys(result.light)).toHaveLength(0);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
// isSchemaVersionSupported
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
|
|
608
|
+
describe('isSchemaVersionSupported', () => {
|
|
609
|
+
it('returns true for supported schema version (1.0.0)', () => {
|
|
610
|
+
const override: ThemeOverride = {
|
|
611
|
+
$extensions: {
|
|
612
|
+
'com.forge.ui.themeOverride': {
|
|
613
|
+
basePreset: 'poise',
|
|
614
|
+
schemaVersion: '1.0.0',
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
expect(isSchemaVersionSupported(override)).toBe(true);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('returns false for unsupported schema version', () => {
|
|
623
|
+
const override: ThemeOverride = {
|
|
624
|
+
$extensions: {
|
|
625
|
+
'com.forge.ui.themeOverride': {
|
|
626
|
+
basePreset: 'poise',
|
|
627
|
+
schemaVersion: '2.0.0',
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
expect(isSchemaVersionSupported(override)).toBe(false);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('returns false for empty string version', () => {
|
|
636
|
+
const override: ThemeOverride = {
|
|
637
|
+
$extensions: {
|
|
638
|
+
'com.forge.ui.themeOverride': {
|
|
639
|
+
basePreset: 'pulse',
|
|
640
|
+
schemaVersion: '',
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
expect(isSchemaVersionSupported(override)).toBe(false);
|
|
646
|
+
});
|
|
647
|
+
});
|