@cdx-ui/styles 0.0.1-beta.8 → 0.0.1-beta.80
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,296 @@
|
|
|
1
|
+
import { generateColorScale, generatePalettesFromInputs, deriveBaseColorKey } from '../palette';
|
|
2
|
+
|
|
3
|
+
const PALETTE_STEPS = [
|
|
4
|
+
'50',
|
|
5
|
+
'100',
|
|
6
|
+
'200',
|
|
7
|
+
'300',
|
|
8
|
+
'400',
|
|
9
|
+
'500',
|
|
10
|
+
'600',
|
|
11
|
+
'700',
|
|
12
|
+
'800',
|
|
13
|
+
'900',
|
|
14
|
+
'950',
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Calculate WCAG 2.x relative luminance from a hex string.
|
|
19
|
+
* @see https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
|
|
20
|
+
*/
|
|
21
|
+
function relativeLuminance(hex: string): number {
|
|
22
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
23
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
24
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
25
|
+
|
|
26
|
+
const toLinear = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
27
|
+
|
|
28
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** WCAG 2.x contrast ratio between two hex colours. */
|
|
32
|
+
function contrastRatio(hex1: string, hex2: string): number {
|
|
33
|
+
const l1 = relativeLuminance(hex1);
|
|
34
|
+
const l2 = relativeLuminance(hex2);
|
|
35
|
+
const lighter = Math.max(l1, l2);
|
|
36
|
+
const darker = Math.min(l1, l2);
|
|
37
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// generateColorScale
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
describe('generateColorScale', () => {
|
|
45
|
+
describe('valid hex input → correct 11-step output structure', () => {
|
|
46
|
+
it('returns all 11 contrast steps plus the input key for brand', () => {
|
|
47
|
+
const result = generateColorScale('#081728', 'brand');
|
|
48
|
+
|
|
49
|
+
for (const step of PALETTE_STEPS) {
|
|
50
|
+
expect(`color.brand.${step}` in result).toBe(true);
|
|
51
|
+
}
|
|
52
|
+
expect('color.brand.input' in result).toBe(true);
|
|
53
|
+
expect(Object.keys(result)).toHaveLength(12);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns all 11 contrast steps plus the input key for accent', () => {
|
|
57
|
+
const result = generateColorScale('#ffcf23', 'accent');
|
|
58
|
+
|
|
59
|
+
for (const step of PALETTE_STEPS) {
|
|
60
|
+
expect(`color.accent.${step}` in result).toBe(true);
|
|
61
|
+
}
|
|
62
|
+
expect('color.accent.input' in result).toBe(true);
|
|
63
|
+
expect(Object.keys(result)).toHaveLength(12);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns all 11 contrast steps plus the input key for base', () => {
|
|
67
|
+
const result = generateColorScale('#797986', 'base');
|
|
68
|
+
|
|
69
|
+
for (const step of PALETTE_STEPS) {
|
|
70
|
+
expect(`color.base.${step}` in result).toBe(true);
|
|
71
|
+
}
|
|
72
|
+
expect('color.base.input' in result).toBe(true);
|
|
73
|
+
expect(Object.keys(result)).toHaveLength(12);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('all generated values are valid 7-character hex strings', () => {
|
|
77
|
+
const result = generateColorScale('#0052cc', 'brand');
|
|
78
|
+
|
|
79
|
+
for (const value of Object.values(result)) {
|
|
80
|
+
expect(value).toMatch(/^#[0-9a-f]{6}$/);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('input preservation', () => {
|
|
86
|
+
it('preserves the original hex (7-char) verbatim in the input key', () => {
|
|
87
|
+
const result = generateColorScale('#081728', 'brand');
|
|
88
|
+
expect(result['color.brand.input']).toBe('#081728');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('normalises shorthand hex (#RGB → #RRGGBB) in the input key', () => {
|
|
92
|
+
const result = generateColorScale('#F00', 'brand');
|
|
93
|
+
expect(result['color.brand.input']).toBe('#ff0000');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('lowercases the input hex', () => {
|
|
97
|
+
const result = generateColorScale('#AABBCC', 'accent');
|
|
98
|
+
expect(result['color.accent.input']).toBe('#aabbcc');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('invalid hex handling', () => {
|
|
103
|
+
it('throws for a string without leading #', () => {
|
|
104
|
+
expect(() => generateColorScale('081728', 'brand')).toThrow(TypeError);
|
|
105
|
+
expect(() => generateColorScale('081728', 'brand')).toThrow(/must be a 4-character/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('throws for wrong-length hex', () => {
|
|
109
|
+
expect(() => generateColorScale('#0817', 'brand')).toThrow(TypeError);
|
|
110
|
+
expect(() => generateColorScale('#08172', 'brand')).toThrow(TypeError);
|
|
111
|
+
expect(() => generateColorScale('#08172800', 'brand')).toThrow(TypeError);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('throws for non-hex characters', () => {
|
|
115
|
+
expect(() => generateColorScale('#GGHHII', 'brand')).toThrow(TypeError);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('throws for empty string', () => {
|
|
119
|
+
expect(() => generateColorScale('', 'brand')).toThrow(TypeError);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('throws for non-string input', () => {
|
|
123
|
+
expect(() => generateColorScale(123 as unknown as string, 'brand')).toThrow(TypeError);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('contrast ratio spot-checks', () => {
|
|
128
|
+
const WHITE = '#ffffff';
|
|
129
|
+
|
|
130
|
+
it('brand palette: step 700 meets WCAG AA for normal text (≥ 4.5:1)', () => {
|
|
131
|
+
const result = generateColorScale('#081728', 'brand');
|
|
132
|
+
const ratio = contrastRatio(result['color.brand.700'], WHITE);
|
|
133
|
+
expect(ratio).toBeGreaterThanOrEqual(4.5);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('brand palette: steps are ordered from lightest (50) to darkest (950)', () => {
|
|
137
|
+
const result = generateColorScale('#081728', 'brand');
|
|
138
|
+
const luminances = PALETTE_STEPS.map((step) =>
|
|
139
|
+
relativeLuminance(result[`color.brand.${step}`]),
|
|
140
|
+
);
|
|
141
|
+
for (let i = 1; i < luminances.length; i++) {
|
|
142
|
+
expect(luminances[i]).toBeLessThanOrEqual(luminances[i - 1]);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('accent palette: step 700 meets WCAG AA for normal text (≥ 4.5:1)', () => {
|
|
147
|
+
const result = generateColorScale('#ffcf23', 'accent');
|
|
148
|
+
const ratio = contrastRatio(result['color.accent.700'], WHITE);
|
|
149
|
+
expect(ratio).toBeGreaterThanOrEqual(4.5);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('accent palette: step 50 has very low contrast against white (< 1.5:1)', () => {
|
|
153
|
+
const result = generateColorScale('#ffcf23', 'accent');
|
|
154
|
+
const ratio = contrastRatio(result['color.accent.50'], WHITE);
|
|
155
|
+
expect(ratio).toBeLessThan(1.5);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// deriveBaseColorKey
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/** Saturation component (0–1) of a hex color in HSL. */
|
|
165
|
+
function saturation(hex: string): number {
|
|
166
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
167
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
168
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
169
|
+
const max = Math.max(r, g, b);
|
|
170
|
+
const min = Math.min(r, g, b);
|
|
171
|
+
const l = (max + min) / 2;
|
|
172
|
+
const delta = max - min;
|
|
173
|
+
if (delta === 0) return 0;
|
|
174
|
+
return l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Hue component (degrees) of a hex color in HSL. */
|
|
178
|
+
function hue(hex: string): number {
|
|
179
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
180
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
181
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
182
|
+
const max = Math.max(r, g, b);
|
|
183
|
+
const min = Math.min(r, g, b);
|
|
184
|
+
const delta = max - min;
|
|
185
|
+
if (delta === 0) return 0;
|
|
186
|
+
let h: number;
|
|
187
|
+
if (max === r) h = (g - b) / delta + (g < b ? 6 : 0);
|
|
188
|
+
else if (max === g) h = (b - r) / delta + 2;
|
|
189
|
+
else h = (r - g) / delta + 4;
|
|
190
|
+
return h * 60;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
describe('deriveBaseColorKey', () => {
|
|
194
|
+
it('returns a normalised 7-character hex string', () => {
|
|
195
|
+
expect(deriveBaseColorKey('#346099')).toMatch(/^#[0-9a-f]{6}$/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('lowers saturation to ~5%', () => {
|
|
199
|
+
// ~5% saturation rounded through 8-bit channels lands close to 0.05.
|
|
200
|
+
expect(saturation(deriveBaseColorKey('#346099'))).toBeCloseTo(0.05, 2);
|
|
201
|
+
expect(saturation(deriveBaseColorKey('#ff0000'))).toBeCloseTo(0.05, 2);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('preserves the brand hue (within rounding tolerance)', () => {
|
|
205
|
+
// At 5% saturation the channels are nearly equal, so 8-bit rounding can
|
|
206
|
+
// nudge the hue by a couple of degrees — assert a small absolute tolerance.
|
|
207
|
+
expect(Math.abs(hue(deriveBaseColorKey('#346099')) - hue('#346099'))).toBeLessThan(5);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('accepts shorthand hex and normalises the result', () => {
|
|
211
|
+
expect(deriveBaseColorKey('#36c')).toMatch(/^#[0-9a-f]{6}$/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('throws for invalid hex input', () => {
|
|
215
|
+
expect(() => deriveBaseColorKey('nope')).toThrow(TypeError);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// generatePalettesFromInputs
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
describe('generatePalettesFromInputs', () => {
|
|
224
|
+
it('generates palettes for both brand and accent colour inputs', () => {
|
|
225
|
+
const result = generatePalettesFromInputs({
|
|
226
|
+
brandPrimary: '#0052cc',
|
|
227
|
+
accentPrimary: '#FF8C42',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
for (const step of PALETTE_STEPS) {
|
|
231
|
+
expect(`color.brand.${step}` in result).toBe(true);
|
|
232
|
+
expect(`color.accent.${step}` in result).toBe(true);
|
|
233
|
+
}
|
|
234
|
+
expect(result['color.brand.input']).toBe('#0052cc');
|
|
235
|
+
expect(result['color.accent.input']).toBe('#ff8c42');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('skips non-colour inputs (e.g. displayFont)', () => {
|
|
239
|
+
const result = generatePalettesFromInputs({
|
|
240
|
+
brandPrimary: '#0052cc',
|
|
241
|
+
displayFont: 'Poppins',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect('color.brand.input' in result).toBe(true);
|
|
245
|
+
expect(Object.keys(result).every((k) => k.startsWith('color.'))).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('skips colour-input keys with non-string values', () => {
|
|
249
|
+
const result = generatePalettesFromInputs({
|
|
250
|
+
brandPrimary: 42 as unknown as string,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(Object.keys(result)).toHaveLength(0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('returns an empty object when inputs is empty', () => {
|
|
257
|
+
const result = generatePalettesFromInputs({});
|
|
258
|
+
expect(result).toEqual({});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('generates only brand palette when only brandPrimary is provided', () => {
|
|
262
|
+
const result = generatePalettesFromInputs({
|
|
263
|
+
brandPrimary: '#081728',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(Object.keys(result)).toHaveLength(12);
|
|
267
|
+
for (const key of Object.keys(result)) {
|
|
268
|
+
expect(key).toMatch(/^color\.brand\./);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('generates a base palette from a basePrimary input', () => {
|
|
273
|
+
const result = generatePalettesFromInputs({
|
|
274
|
+
basePrimary: deriveBaseColorKey('#346099'),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
for (const step of PALETTE_STEPS) {
|
|
278
|
+
expect(`color.base.${step}` in result).toBe(true);
|
|
279
|
+
}
|
|
280
|
+
expect('color.base.input' in result).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('generates brand, accent, and base palettes together', () => {
|
|
284
|
+
const result = generatePalettesFromInputs({
|
|
285
|
+
brandPrimary: '#0052cc',
|
|
286
|
+
accentPrimary: '#ff8c42',
|
|
287
|
+
basePrimary: deriveBaseColorKey('#0052cc'),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
for (const step of PALETTE_STEPS) {
|
|
291
|
+
expect(`color.brand.${step}` in result).toBe(true);
|
|
292
|
+
expect(`color.accent.${step}` in result).toBe(true);
|
|
293
|
+
expect(`color.base.${step}` in result).toBe(true);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
});
|