@ankhorage/surface 0.2.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/dist/examples/DocsExamples.d.ts.map +1 -1
- package/dist/examples/DocsExamples.js +0 -2
- package/dist/examples/DocsExamples.js.map +1 -1
- package/dist/internal/resolvers/resolveControlSize.d.ts +2 -2
- package/dist/internal/resolvers/resolveControlSize.d.ts.map +1 -1
- package/dist/internal/resolvers/resolveControlSize.js.map +1 -1
- package/dist/internal/resolvers/resolveIconSize.d.ts +2 -2
- package/dist/internal/resolvers/resolveIconSize.d.ts.map +1 -1
- package/dist/internal/resolvers/resolveIconSize.js.map +1 -1
- package/dist/internal/resolvers/resolveInteractiveColors.d.ts +3 -3
- package/dist/internal/resolvers/resolveInteractiveColors.d.ts.map +1 -1
- package/dist/internal/resolvers/resolveInteractiveColors.js.map +1 -1
- package/dist/internal/resolvers/resolveSelectionControlColors.d.ts +2 -2
- package/dist/internal/resolvers/resolveSelectionControlColors.d.ts.map +1 -1
- package/dist/internal/resolvers/resolveSelectionControlColors.js.map +1 -1
- package/dist/internal/resolvers/resolveTextColor.d.ts +3 -3
- package/dist/internal/resolvers/resolveTextColor.d.ts.map +1 -1
- package/dist/internal/resolvers/resolveTextColor.js.map +1 -1
- package/dist/internal/resolvers/resolveTextStyles.d.ts +3 -3
- package/dist/internal/resolvers/resolveTextStyles.d.ts.map +1 -1
- package/dist/internal/resolvers/resolveTextStyles.js.map +1 -1
- package/dist/internal/resolvers/resolveTone.d.ts +2 -2
- package/dist/internal/resolvers/resolveTone.d.ts.map +1 -1
- package/dist/internal/resolvers/resolveTone.js.map +1 -1
- package/dist/layout/Container.d.ts +2 -2
- package/dist/layout/Container.d.ts.map +1 -1
- package/dist/layout/Container.js.map +1 -1
- package/dist/layout/helpers.d.ts +9 -9
- package/dist/layout/helpers.d.ts.map +1 -1
- package/dist/layout/helpers.js.map +1 -1
- package/dist/primitives/heading/resolveHeadingStyle.d.ts +2 -2
- package/dist/primitives/heading/resolveHeadingStyle.d.ts.map +1 -1
- package/dist/primitives/heading/resolveHeadingStyle.js.map +1 -1
- package/dist/primitives/icon/Icon.d.ts +3 -3
- package/dist/primitives/icon/Icon.d.ts.map +1 -1
- package/dist/primitives/icon/Icon.js.map +1 -1
- package/dist/theme/ThemeContext.d.ts +3 -3
- package/dist/theme/ThemeContext.d.ts.map +1 -1
- package/dist/theme/ThemeContext.js.map +1 -1
- package/dist/theme/colorEngine.d.ts +10 -11
- package/dist/theme/colorEngine.d.ts.map +1 -1
- package/dist/theme/colorEngine.js +102 -412
- package/dist/theme/colorEngine.js.map +1 -1
- package/dist/theme/createTheme.d.ts +3 -3
- package/dist/theme/createTheme.d.ts.map +1 -1
- package/dist/theme/createTheme.js +2 -4
- package/dist/theme/createTheme.js.map +1 -1
- package/dist/theme/types.d.ts +5 -17
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/theme/types.js.map +1 -1
- package/package.json +3 -4
- package/src/examples/DocsExamples.tsx +0 -2
- package/src/internal/resolvers/resolveControlSize.ts +5 -2
- package/src/internal/resolvers/resolveIconSize.ts +2 -2
- package/src/internal/resolvers/resolveInteractiveColors.ts +3 -3
- package/src/internal/resolvers/resolveSelectionControlColors.ts +2 -2
- package/src/internal/resolvers/resolveTextColor.ts +3 -3
- package/src/internal/resolvers/resolveTextStyles.ts +6 -6
- package/src/internal/resolvers/resolveTone.ts +2 -2
- package/src/layout/Container.tsx +2 -2
- package/src/layout/helpers.test.ts +2 -2
- package/src/layout/helpers.ts +12 -9
- package/src/primitives/heading/resolveHeadingStyle.ts +2 -2
- package/src/primitives/icon/Icon.tsx +3 -3
- package/src/theme/ThemeContext.tsx +2 -2
- package/src/theme/colorEngine.test.ts +158 -154
- package/src/theme/colorEngine.ts +128 -477
- package/src/theme/createTheme.ts +6 -8
- package/src/theme/types.ts +15 -18
- package/src/utils/deepMerge.test.ts +0 -4
package/src/theme/colorEngine.ts
CHANGED
|
@@ -1,314 +1,113 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
|
+
ColorSwatch,
|
|
3
|
+
GeneratedThemeModeColors,
|
|
4
|
+
GeneratedThemeSwatches,
|
|
5
|
+
HexColor,
|
|
6
|
+
SemanticColorReferenceMap,
|
|
7
|
+
SemanticColorToken,
|
|
8
|
+
} from '@ankhorage/color-theory';
|
|
9
|
+
import {
|
|
10
|
+
generateColorSwatch,
|
|
11
|
+
generateThemeModeColors,
|
|
12
|
+
getReadableForeground,
|
|
13
|
+
parseHexColorOrThrow,
|
|
14
|
+
} from '@ankhorage/color-theory';
|
|
15
|
+
import type { ThemeConfig } from '@ankhorage/contracts';
|
|
2
16
|
|
|
3
17
|
import type {
|
|
4
18
|
ActionSemantics,
|
|
5
19
|
BorderSemantics,
|
|
6
|
-
ColorHarmony,
|
|
7
|
-
ColorScale,
|
|
8
|
-
ColorTone,
|
|
9
20
|
ContentSemantics,
|
|
10
21
|
NeutralSemantics,
|
|
11
22
|
RoleSemantics,
|
|
12
23
|
SurfaceSemantics,
|
|
13
|
-
ThemeConfig,
|
|
14
24
|
ThemeSemantics,
|
|
15
25
|
} from './types';
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
l: number;
|
|
22
|
-
c: number;
|
|
23
|
-
h?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const SCALE_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const;
|
|
27
|
-
|
|
28
|
-
type RolePaletteKind =
|
|
29
|
-
| 'grayscale'
|
|
30
|
-
| 'neutral'
|
|
31
|
-
| 'pastel'
|
|
32
|
-
| 'earth'
|
|
33
|
-
| 'mineral'
|
|
34
|
-
| 'muted'
|
|
35
|
-
| 'jewel'
|
|
36
|
-
| 'fluorescent'
|
|
37
|
-
| 'obsidian'
|
|
38
|
-
| 'vaporwave'
|
|
39
|
-
| 'monochromeAccent';
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Deterministic Lightness Curves (OKLCH L)
|
|
43
|
-
*/
|
|
44
|
-
const LIGHTNESS_CURVES = {
|
|
45
|
-
light: [0.98, 0.95, 0.9, 0.82, 0.72, 0.62, 0.52, 0.42, 0.32, 0.22, 0.15],
|
|
46
|
-
dark: [0.12, 0.16, 0.2, 0.26, 0.34, 0.62, 0.7, 0.78, 0.86, 0.92, 0.96],
|
|
47
|
-
} as const;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Lightness anchors per internal palette kind
|
|
51
|
-
*/
|
|
52
|
-
const ROLE_PALETTE_LIGHTNESS_ANCHORS: Record<RolePaletteKind, number> = {
|
|
53
|
-
grayscale: 0.62,
|
|
54
|
-
neutral: 0.62,
|
|
55
|
-
pastel: 0.7,
|
|
56
|
-
earth: 0.55,
|
|
57
|
-
mineral: 0.58,
|
|
58
|
-
muted: 0.6,
|
|
59
|
-
jewel: 0.62,
|
|
60
|
-
fluorescent: 0.65,
|
|
61
|
-
obsidian: 0.12,
|
|
62
|
-
vaporwave: 0.7,
|
|
63
|
-
monochromeAccent: 0.62,
|
|
64
|
-
};
|
|
27
|
+
// Fixed hex values for semantic status colors (not theme-generated)
|
|
28
|
+
const DANGER_HEX = parseHexColorOrThrow('#ef4444');
|
|
29
|
+
const SUCCESS_HEX = parseHexColorOrThrow('#22c55e');
|
|
30
|
+
const WARNING_HEX = parseHexColorOrThrow('#f59e0b');
|
|
65
31
|
|
|
66
32
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
33
|
+
* Surface semantic resolver: maps color-theory SemanticColorToken references
|
|
34
|
+
* to hex values from the generated swatches.
|
|
69
35
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
vaporwave: 0.2,
|
|
86
|
-
monochromeAccent: 0.02,
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Chroma Hierarchy Rule
|
|
91
|
-
*/
|
|
92
|
-
const CHROMA_HIERARCHY = {
|
|
93
|
-
primary: 1.0,
|
|
94
|
-
secondary: 0.7,
|
|
95
|
-
accent: 0.4,
|
|
96
|
-
surface: 0.1,
|
|
97
|
-
} as const;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Semantic Step Mapping
|
|
101
|
-
*/
|
|
102
|
-
const SEMANTIC_STEPS = {
|
|
103
|
-
light: {
|
|
104
|
-
bg: 0, // 50
|
|
105
|
-
bgSubtle: 1, // 100
|
|
106
|
-
surface: 1, // 100
|
|
107
|
-
surfaceHover: 2, // 200
|
|
108
|
-
surfaceActive: 3, // 300
|
|
109
|
-
border: 3, // 300
|
|
110
|
-
borderStrong: 4, // 400
|
|
111
|
-
divider: 2, // 200
|
|
112
|
-
text: 9, // 900
|
|
113
|
-
textMuted: 7, // 700
|
|
114
|
-
textSubtle: 6, // 600
|
|
115
|
-
solid: 5, // 500
|
|
116
|
-
softBg: 1, // 100
|
|
117
|
-
softHover: 2, // 200
|
|
118
|
-
softActive: 3, // 300
|
|
119
|
-
outline: 4, // 400
|
|
120
|
-
},
|
|
121
|
-
dark: {
|
|
122
|
-
bg: 0, // 50
|
|
123
|
-
bgSubtle: 1, // 100
|
|
124
|
-
surface: 1, // 100
|
|
125
|
-
surfaceHover: 2, // 200
|
|
126
|
-
surfaceActive: 3, // 300
|
|
127
|
-
border: 3, // 300
|
|
128
|
-
borderStrong: 4, // 400
|
|
129
|
-
divider: 2, // 200
|
|
130
|
-
text: 10, // 950
|
|
131
|
-
textMuted: 8, // 800
|
|
132
|
-
textSubtle: 7, // 700
|
|
133
|
-
solid: 5, // 500
|
|
134
|
-
softBg: 1, // 100
|
|
135
|
-
softHover: 2, // 200
|
|
136
|
-
softActive: 3, // 300
|
|
137
|
-
outline: 4, // 400
|
|
138
|
-
},
|
|
139
|
-
} as const;
|
|
140
|
-
|
|
141
|
-
export function generateColorScale(baseColor: OklchColor, isDark: boolean): ColorScale {
|
|
142
|
-
const scale: Partial<ColorScale> = {};
|
|
143
|
-
const curve = isDark ? LIGHTNESS_CURVES.dark : LIGHTNESS_CURVES.light;
|
|
144
|
-
|
|
145
|
-
// Dark-mode chroma rule: reduce chroma by 25%
|
|
146
|
-
const chromaMultiplier = isDark ? 0.75 : 1.0;
|
|
147
|
-
const targetChroma = baseColor.c * chromaMultiplier;
|
|
148
|
-
|
|
149
|
-
SCALE_STEPS.forEach((step, index) => {
|
|
150
|
-
const lightness = curve[index];
|
|
151
|
-
const chromaByStep = CHROMA_BY_STEP[index];
|
|
152
|
-
if (lightness === undefined || chromaByStep === undefined) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
// Apply chroma falloff curve per step
|
|
156
|
-
const stepChroma = targetChroma * chromaByStep;
|
|
157
|
-
const stepColor = { ...baseColor, l: lightness, c: stepChroma };
|
|
158
|
-
scale[step as keyof ColorScale] = formatHex(stepColor);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
return scale as ColorScale;
|
|
36
|
+
export type SurfaceSemanticColors = Record<SemanticColorToken, HexColor>;
|
|
37
|
+
|
|
38
|
+
export function resolveSemanticColors(
|
|
39
|
+
generated: GeneratedThemeModeColors,
|
|
40
|
+
references: SemanticColorReferenceMap,
|
|
41
|
+
): SurfaceSemanticColors {
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
Object.entries(references).map(([token, ref]) => {
|
|
44
|
+
const swatch = generated.swatches[ref.role];
|
|
45
|
+
if (!swatch) {
|
|
46
|
+
throw new Error(`Missing swatch for role '${ref.role}' (token: '${token}')`);
|
|
47
|
+
}
|
|
48
|
+
return [token, swatch[ref.step]];
|
|
49
|
+
}),
|
|
50
|
+
) as SurfaceSemanticColors;
|
|
162
51
|
}
|
|
163
52
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return [h, (h + 120) % 360, (h + 240) % 360];
|
|
180
|
-
case 'tetradic':
|
|
181
|
-
return [h, (h + 90) % 360, (h + 180) % 360, (h + 270) % 360];
|
|
182
|
-
default:
|
|
183
|
-
return [h];
|
|
53
|
+
function buildNeutralSemantics(neutralSwatch: ColorSwatch, isDark: boolean): NeutralSemantics {
|
|
54
|
+
if (isDark) {
|
|
55
|
+
return {
|
|
56
|
+
bg: neutralSwatch[950],
|
|
57
|
+
bgSubtle: neutralSwatch[900],
|
|
58
|
+
surface: neutralSwatch[900],
|
|
59
|
+
surfaceHover: neutralSwatch[800],
|
|
60
|
+
surfaceActive: neutralSwatch[700],
|
|
61
|
+
border: neutralSwatch[800],
|
|
62
|
+
borderStrong: neutralSwatch[600],
|
|
63
|
+
divider: neutralSwatch[800],
|
|
64
|
+
text: neutralSwatch[50],
|
|
65
|
+
textMuted: neutralSwatch[200],
|
|
66
|
+
textSubtle: neutralSwatch[300],
|
|
67
|
+
};
|
|
184
68
|
}
|
|
69
|
+
return {
|
|
70
|
+
bg: neutralSwatch[50],
|
|
71
|
+
bgSubtle: neutralSwatch[100],
|
|
72
|
+
surface: neutralSwatch[100],
|
|
73
|
+
surfaceHover: neutralSwatch[200],
|
|
74
|
+
surfaceActive: neutralSwatch[300],
|
|
75
|
+
border: neutralSwatch[200],
|
|
76
|
+
borderStrong: neutralSwatch[300],
|
|
77
|
+
divider: neutralSwatch[200],
|
|
78
|
+
text: neutralSwatch[900],
|
|
79
|
+
textMuted: neutralSwatch[700],
|
|
80
|
+
textSubtle: neutralSwatch[600],
|
|
81
|
+
};
|
|
185
82
|
}
|
|
186
83
|
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
primary: RolePaletteKind;
|
|
191
|
-
secondary: RolePaletteKind;
|
|
192
|
-
accent: RolePaletteKind;
|
|
193
|
-
highlight: RolePaletteKind;
|
|
194
|
-
} {
|
|
195
|
-
switch (colorTone) {
|
|
196
|
-
case 'pastel':
|
|
197
|
-
return {
|
|
198
|
-
bg: 'pastel',
|
|
199
|
-
surface: 'pastel',
|
|
200
|
-
primary: 'jewel',
|
|
201
|
-
secondary: 'jewel',
|
|
202
|
-
accent: 'jewel',
|
|
203
|
-
highlight: 'fluorescent',
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
case 'earth':
|
|
207
|
-
return {
|
|
208
|
-
bg: 'earth',
|
|
209
|
-
surface: 'earth',
|
|
210
|
-
primary: 'mineral',
|
|
211
|
-
secondary: 'mineral',
|
|
212
|
-
accent: 'jewel',
|
|
213
|
-
highlight: 'jewel',
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
case 'mineral':
|
|
217
|
-
return {
|
|
218
|
-
bg: 'mineral',
|
|
219
|
-
surface: 'mineral',
|
|
220
|
-
primary: 'jewel',
|
|
221
|
-
secondary: 'mineral',
|
|
222
|
-
accent: 'jewel',
|
|
223
|
-
highlight: 'fluorescent',
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
case 'muted':
|
|
227
|
-
return {
|
|
228
|
-
bg: 'muted',
|
|
229
|
-
surface: 'muted',
|
|
230
|
-
primary: 'jewel',
|
|
231
|
-
secondary: 'muted',
|
|
232
|
-
accent: 'jewel',
|
|
233
|
-
highlight: 'fluorescent',
|
|
234
|
-
};
|
|
84
|
+
function buildRoleSemantics(swatch: ColorSwatch, isDark: boolean): RoleSemantics {
|
|
85
|
+
const base = swatch[500];
|
|
86
|
+
const { foreground: onSolidText } = getReadableForeground(base);
|
|
235
87
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return {
|
|
248
|
-
bg: 'obsidian',
|
|
249
|
-
surface: 'obsidian',
|
|
250
|
-
primary: 'fluorescent',
|
|
251
|
-
secondary: 'jewel',
|
|
252
|
-
accent: 'fluorescent',
|
|
253
|
-
highlight: 'fluorescent',
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
case 'obsidian':
|
|
257
|
-
return {
|
|
258
|
-
bg: 'obsidian',
|
|
259
|
-
surface: 'obsidian',
|
|
260
|
-
primary: 'fluorescent',
|
|
261
|
-
secondary: 'jewel',
|
|
262
|
-
accent: 'fluorescent',
|
|
263
|
-
highlight: 'fluorescent',
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
case 'vaporwave':
|
|
267
|
-
return {
|
|
268
|
-
bg: 'pastel',
|
|
269
|
-
surface: 'pastel',
|
|
270
|
-
primary: 'fluorescent',
|
|
271
|
-
secondary: 'jewel',
|
|
272
|
-
accent: 'fluorescent',
|
|
273
|
-
highlight: 'fluorescent',
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
case 'monochromeAccent':
|
|
277
|
-
return {
|
|
278
|
-
bg: 'grayscale',
|
|
279
|
-
surface: 'grayscale',
|
|
280
|
-
primary: 'jewel',
|
|
281
|
-
secondary: 'grayscale',
|
|
282
|
-
accent: 'jewel',
|
|
283
|
-
highlight: 'fluorescent',
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
default: // neutral
|
|
287
|
-
return {
|
|
288
|
-
bg: 'grayscale',
|
|
289
|
-
surface: 'grayscale',
|
|
290
|
-
primary: 'jewel',
|
|
291
|
-
secondary: 'pastel',
|
|
292
|
-
accent: 'jewel',
|
|
293
|
-
highlight: 'fluorescent',
|
|
294
|
-
};
|
|
88
|
+
if (isDark) {
|
|
89
|
+
return {
|
|
90
|
+
base,
|
|
91
|
+
hover: swatch[400],
|
|
92
|
+
strong: swatch[300],
|
|
93
|
+
softBg: swatch[900],
|
|
94
|
+
softHover: swatch[800],
|
|
95
|
+
softActive: swatch[700],
|
|
96
|
+
outline: swatch[500],
|
|
97
|
+
onSolidText,
|
|
98
|
+
};
|
|
295
99
|
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Simple OKLCH L-based contrast check
|
|
300
|
-
*/
|
|
301
|
-
function getBestContrast(solidHex: string, neutral50: string, neutral950: string): string {
|
|
302
|
-
const solid = oklch(solidHex);
|
|
303
|
-
const n50 = oklch(neutral50);
|
|
304
|
-
const n950 = oklch(neutral950);
|
|
305
100
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
101
|
+
return {
|
|
102
|
+
base,
|
|
103
|
+
hover: swatch[600],
|
|
104
|
+
strong: swatch[700],
|
|
105
|
+
softBg: swatch[100],
|
|
106
|
+
softHover: swatch[200],
|
|
107
|
+
softActive: swatch[300],
|
|
108
|
+
outline: swatch[400],
|
|
109
|
+
onSolidText,
|
|
110
|
+
};
|
|
312
111
|
}
|
|
313
112
|
|
|
314
113
|
export function generatePalette(
|
|
@@ -316,200 +115,37 @@ export function generatePalette(
|
|
|
316
115
|
mode: 'light' | 'dark' = 'light',
|
|
317
116
|
): {
|
|
318
117
|
colors: Record<string, string>;
|
|
319
|
-
|
|
118
|
+
swatches: GeneratedThemeSwatches;
|
|
320
119
|
semantics: ThemeSemantics;
|
|
321
120
|
} {
|
|
322
121
|
const modeConfig = mode === 'dark' ? config.dark : config.light;
|
|
323
|
-
const { primaryColor, harmony, colorTone } = modeConfig;
|
|
324
|
-
|
|
325
|
-
let base = oklch(primaryColor);
|
|
326
|
-
if (!base) {
|
|
327
|
-
console.warn(
|
|
328
|
-
`[colorEngine] Invalid primary color: "${primaryColor}". Falling back to default blue.`,
|
|
329
|
-
);
|
|
330
|
-
base = oklch('#3B82F6');
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const baseHue = base?.h ?? 0;
|
|
334
|
-
const hues = getHarmonyHues(baseHue, harmony);
|
|
335
|
-
const rolePalette = getColorToneRolePalette(colorTone);
|
|
336
|
-
|
|
337
|
-
// 1. Resolve Chromas
|
|
338
|
-
const getC = (t: RolePaletteKind) => ROLE_PALETTE_CHROMA_ANCHORS[t];
|
|
339
|
-
const getHue = (index: number, fallback: number) => hues[index] ?? fallback;
|
|
340
|
-
|
|
341
|
-
const primaryChroma = getC(rolePalette.primary);
|
|
342
|
-
const secondaryChroma = getC(rolePalette.secondary) * CHROMA_HIERARCHY.secondary;
|
|
343
|
-
const tertiaryChroma = getC(rolePalette.accent) * CHROMA_HIERARCHY.accent;
|
|
344
|
-
const highlightChroma = getC(rolePalette.highlight);
|
|
345
|
-
const surfaceChroma = Math.min(0.02, getC(rolePalette.bg) * CHROMA_HIERARCHY.surface);
|
|
346
|
-
|
|
347
|
-
// 2. Stable Role Assignment
|
|
348
|
-
let pHue = getHue(0, baseHue);
|
|
349
|
-
let sHue = getHue(0, baseHue);
|
|
350
|
-
let aHue = getHue(0, baseHue);
|
|
351
|
-
let hHue =
|
|
352
|
-
harmony === 'tetradic' ? getHue(3, getHue(0, baseHue)) : (getHue(0, baseHue) + 60) % 360; // Yellow-ish offset if no tetradic
|
|
353
|
-
|
|
354
|
-
switch (harmony) {
|
|
355
|
-
case 'complementary':
|
|
356
|
-
pHue = getHue(0, pHue);
|
|
357
|
-
aHue = getHue(1, pHue);
|
|
358
|
-
sHue = (getHue(0, pHue) + 30) % 360;
|
|
359
|
-
break;
|
|
360
|
-
case 'splitComplementary':
|
|
361
|
-
case 'triadic':
|
|
362
|
-
pHue = getHue(0, pHue);
|
|
363
|
-
sHue = getHue(1, pHue);
|
|
364
|
-
aHue = getHue(2, sHue);
|
|
365
|
-
break;
|
|
366
|
-
case 'tetradic':
|
|
367
|
-
pHue = getHue(0, pHue);
|
|
368
|
-
sHue = getHue(1, pHue);
|
|
369
|
-
aHue = getHue(2, sHue);
|
|
370
|
-
hHue = getHue(3, aHue);
|
|
371
|
-
break;
|
|
372
|
-
case 'analogous':
|
|
373
|
-
pHue = getHue(1, pHue);
|
|
374
|
-
sHue = getHue(0, pHue);
|
|
375
|
-
aHue = getHue(2, sHue);
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// 3. Build Bases with tuned lightness
|
|
380
|
-
const getL = (t: RolePaletteKind) => ROLE_PALETTE_LIGHTNESS_ANCHORS[t];
|
|
381
|
-
|
|
382
|
-
const primaryBase: OklchColor = {
|
|
383
|
-
mode: 'oklch',
|
|
384
|
-
l: getL(rolePalette.primary),
|
|
385
|
-
c: primaryChroma,
|
|
386
|
-
h: pHue,
|
|
387
|
-
};
|
|
388
|
-
const secondaryBase: OklchColor = {
|
|
389
|
-
mode: 'oklch',
|
|
390
|
-
l: getL(rolePalette.secondary),
|
|
391
|
-
c: secondaryChroma,
|
|
392
|
-
h: sHue,
|
|
393
|
-
};
|
|
394
|
-
const accentBase: OklchColor = {
|
|
395
|
-
mode: 'oklch',
|
|
396
|
-
l: getL(rolePalette.accent),
|
|
397
|
-
c: tertiaryChroma,
|
|
398
|
-
h: aHue,
|
|
399
|
-
};
|
|
400
|
-
const highlightBase: OklchColor = {
|
|
401
|
-
mode: 'oklch',
|
|
402
|
-
l: getL(rolePalette.highlight),
|
|
403
|
-
c: highlightChroma,
|
|
404
|
-
h: hHue,
|
|
405
|
-
};
|
|
406
|
-
const dangerBase: OklchColor = {
|
|
407
|
-
mode: 'oklch',
|
|
408
|
-
l: 0.6,
|
|
409
|
-
c: 0.2,
|
|
410
|
-
h: 25,
|
|
411
|
-
};
|
|
412
|
-
const successBase: OklchColor = {
|
|
413
|
-
mode: 'oklch',
|
|
414
|
-
l: 0.6,
|
|
415
|
-
c: 0.2,
|
|
416
|
-
h: 145,
|
|
417
|
-
};
|
|
418
|
-
const warningBase: OklchColor = {
|
|
419
|
-
mode: 'oklch',
|
|
420
|
-
l: 0.75,
|
|
421
|
-
c: 0.15,
|
|
422
|
-
h: 85,
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
const neutralBase: OklchColor =
|
|
426
|
-
surfaceChroma === 0
|
|
427
|
-
? { mode: 'oklch', l: 0.62, c: 0 }
|
|
428
|
-
: { mode: 'oklch', l: 0.62, c: surfaceChroma, h: 260 };
|
|
429
|
-
|
|
430
|
-
// 4. Generate Scales
|
|
431
122
|
const isDark = mode === 'dark';
|
|
432
|
-
const scales = {
|
|
433
|
-
primary: generateColorScale(primaryBase, isDark),
|
|
434
|
-
secondary: generateColorScale(secondaryBase, isDark),
|
|
435
|
-
accent: generateColorScale(accentBase, isDark),
|
|
436
|
-
highlight: generateColorScale(highlightBase, isDark),
|
|
437
|
-
neutral: generateColorScale(neutralBase, isDark),
|
|
438
|
-
danger: generateColorScale(dangerBase, isDark),
|
|
439
|
-
success: generateColorScale(successBase, isDark),
|
|
440
|
-
warning: generateColorScale(warningBase, isDark),
|
|
441
|
-
};
|
|
442
123
|
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
const getStep = (s: ColorScale, idx: number) => {
|
|
446
|
-
const key = SCALE_STEPS[idx] ?? SCALE_STEPS[SCALE_STEPS.length - 1] ?? 950;
|
|
447
|
-
return s[key];
|
|
448
|
-
};
|
|
124
|
+
// Throws deterministically on invalid primary color
|
|
125
|
+
parseHexColorOrThrow(modeConfig.primaryColor);
|
|
449
126
|
|
|
450
|
-
const
|
|
451
|
-
bg: getStep(scale, steps.bg),
|
|
452
|
-
bgSubtle: getStep(scale, steps.bgSubtle),
|
|
453
|
-
surface: getStep(scale, steps.surface),
|
|
454
|
-
surfaceHover: getStep(scale, steps.surfaceHover),
|
|
455
|
-
surfaceActive: getStep(scale, steps.surfaceActive),
|
|
456
|
-
border: getStep(scale, steps.border),
|
|
457
|
-
borderStrong: getStep(scale, steps.borderStrong),
|
|
458
|
-
divider: getStep(scale, steps.divider),
|
|
459
|
-
text: getStep(scale, steps.text),
|
|
460
|
-
textMuted: getStep(scale, steps.textMuted),
|
|
461
|
-
textSubtle: getStep(scale, steps.textSubtle),
|
|
462
|
-
});
|
|
127
|
+
const generated = generateThemeModeColors(modeConfig);
|
|
463
128
|
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
const softChromaLimit = 0.08;
|
|
129
|
+
const { swatches } = generated;
|
|
130
|
+
const neutralSwatch = swatches.neutral;
|
|
467
131
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const hex = getStep(scale, idx);
|
|
471
|
-
const color = oklch(hex);
|
|
472
|
-
if (color && color.c > softChromaLimit) {
|
|
473
|
-
return formatHex({ ...color, c: softChromaLimit });
|
|
474
|
-
}
|
|
475
|
-
return hex;
|
|
476
|
-
};
|
|
132
|
+
const neutral = buildNeutralSemantics(neutralSwatch, isDark);
|
|
133
|
+
const brand = buildRoleSemantics(swatches.primary, isDark);
|
|
477
134
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
softBg: getSoftStep(steps.softBg),
|
|
483
|
-
softHover: getSoftStep(steps.softHover),
|
|
484
|
-
softActive: getSoftStep(steps.softActive),
|
|
485
|
-
outline: getStep(scale, steps.outline),
|
|
486
|
-
onSolidText: getBestContrast(solid, getStep(neutralScale, 0), getStep(neutralScale, 10)),
|
|
487
|
-
};
|
|
488
|
-
};
|
|
135
|
+
// Fallback to primary swatch for missing ordinal roles
|
|
136
|
+
const secondarySwatch = swatches.secondary ?? swatches.primary;
|
|
137
|
+
const tertiarySwatch = swatches.tertiary ?? swatches.primary;
|
|
138
|
+
const quaternarySwatch = swatches.quaternary ?? swatches.primary;
|
|
489
139
|
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
const danger = getColorMapping(scales.danger, scales.neutral);
|
|
494
|
-
const success = getColorMapping(scales.success, scales.neutral);
|
|
495
|
-
const warning = getColorMapping(scales.warning, scales.neutral);
|
|
140
|
+
const dangerSwatch = generateColorSwatch(DANGER_HEX).swatch;
|
|
141
|
+
const successSwatch = generateColorSwatch(SUCCESS_HEX).swatch;
|
|
142
|
+
const warningSwatch = generateColorSwatch(WARNING_HEX).swatch;
|
|
496
143
|
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
accent: getStep(scales.accent, steps.solid),
|
|
501
|
-
highlight: getStep(scales.highlight, steps.solid),
|
|
502
|
-
background: neutral.bg,
|
|
503
|
-
surface: neutral.surface,
|
|
504
|
-
text: neutral.text,
|
|
505
|
-
textSecondary: neutral.textMuted,
|
|
506
|
-
border: neutral.border,
|
|
507
|
-
error: getStep(scales.danger, steps.solid),
|
|
508
|
-
success: getStep(scales.success, steps.solid),
|
|
509
|
-
warning: getStep(scales.warning, steps.solid),
|
|
510
|
-
};
|
|
144
|
+
const danger = buildRoleSemantics(dangerSwatch, isDark);
|
|
145
|
+
const success = buildRoleSemantics(successSwatch, isDark);
|
|
146
|
+
const warning = buildRoleSemantics(warningSwatch, isDark);
|
|
511
147
|
|
|
512
|
-
const
|
|
148
|
+
const surfaceSemantics: SurfaceSemantics = {
|
|
513
149
|
default: neutral.surface,
|
|
514
150
|
subtle: neutral.bgSubtle,
|
|
515
151
|
raised: neutral.surface,
|
|
@@ -530,23 +166,38 @@ export function generatePalette(
|
|
|
530
166
|
|
|
531
167
|
const action: ActionSemantics = {
|
|
532
168
|
primary: brand,
|
|
533
|
-
neutral:
|
|
169
|
+
neutral: buildRoleSemantics(neutralSwatch, isDark),
|
|
534
170
|
danger,
|
|
535
171
|
};
|
|
536
172
|
|
|
173
|
+
const colors: Record<string, string> = {
|
|
174
|
+
primary: brand.base,
|
|
175
|
+
secondary: secondarySwatch[500],
|
|
176
|
+
accent: tertiarySwatch[500],
|
|
177
|
+
highlight: quaternarySwatch[500],
|
|
178
|
+
background: neutral.bg,
|
|
179
|
+
surface: neutral.surface,
|
|
180
|
+
text: neutral.text,
|
|
181
|
+
textSecondary: neutral.textMuted,
|
|
182
|
+
border: neutral.border,
|
|
183
|
+
error: danger.base,
|
|
184
|
+
success: success.base,
|
|
185
|
+
warning: warning.base,
|
|
186
|
+
};
|
|
187
|
+
|
|
537
188
|
return {
|
|
538
189
|
colors,
|
|
539
|
-
|
|
190
|
+
swatches,
|
|
540
191
|
semantics: {
|
|
541
192
|
neutral,
|
|
542
193
|
brand,
|
|
543
|
-
secondary:
|
|
544
|
-
accent:
|
|
545
|
-
highlight:
|
|
194
|
+
secondary: buildRoleSemantics(secondarySwatch, isDark),
|
|
195
|
+
accent: buildRoleSemantics(tertiarySwatch, isDark),
|
|
196
|
+
highlight: buildRoleSemantics(quaternarySwatch, isDark),
|
|
546
197
|
danger,
|
|
547
198
|
success,
|
|
548
199
|
warning,
|
|
549
|
-
surface,
|
|
200
|
+
surface: surfaceSemantics,
|
|
550
201
|
content,
|
|
551
202
|
border,
|
|
552
203
|
action,
|