astralkit 0.5.0 → 0.7.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 +21 -0
- package/LICENSE +73 -34
- package/README.md +31 -1
- package/astralkit-theme-v4.css +120 -66
- package/astralkit-theme.css +120 -66
- package/astralkit-utilities.css +587 -2
- package/lib/presets/appearance-presets.d.ts +12 -0
- package/lib/presets/appearance-presets.js +102 -0
- package/lib/presets/chart-palettes.d.ts +29 -0
- package/lib/presets/chart-palettes.js +99 -0
- package/lib/presets/dials.d.ts +31 -0
- package/lib/presets/dials.js +330 -0
- package/lib/presets/font-pairings.d.ts +29 -0
- package/lib/presets/font-pairings.js +123 -0
- package/lib/presets/fonts.d.ts +16 -0
- package/lib/presets/fonts.js +76 -0
- package/lib/presets/index.d.ts +7 -0
- package/lib/presets/index.js +24 -0
- package/lib/presets/materials.d.ts +15 -0
- package/lib/presets/materials.js +245 -0
- package/lib/presets/theme-palettes.d.ts +90 -0
- package/lib/presets/theme-palettes.js +720 -0
- package/lib/theme-context.d.ts +17 -0
- package/lib/theme-context.js +33 -0
- package/package.json +9 -3
- package/palettes.css +961 -90
- package/primitives/animations.css +43 -1
- package/primitives/effects.css +373 -115
- package/tailwind.cjs +11 -3
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated 60-30-10 Theme Palettes for AstralKit Theme Builder
|
|
3
|
+
*
|
|
4
|
+
* Each palette follows the 60-30-10 rule:
|
|
5
|
+
* - 60% Dominant: background, surface (large areas)
|
|
6
|
+
* - 30% Secondary: cards, sidebar, secondary surfaces
|
|
7
|
+
* - 10% Accent: CTAs, links, highlights, primary actions
|
|
8
|
+
*
|
|
9
|
+
* Token mapping:
|
|
10
|
+
* - background: Page background (60%)
|
|
11
|
+
* - surface: Card/panel backgrounds (60% variant)
|
|
12
|
+
* - foreground: Primary text color
|
|
13
|
+
* - foregroundMuted: Secondary/body text
|
|
14
|
+
* - primary: Main accent / CTA color (10%)
|
|
15
|
+
* - secondary: Supporting color (30%)
|
|
16
|
+
* - accent: Highlight / tertiary color
|
|
17
|
+
* - border: Divider and border color
|
|
18
|
+
* - ring: Focus ring color (usually primary-based)
|
|
19
|
+
* - muted: Muted/disabled backgrounds
|
|
20
|
+
* - destructive: Error/danger color
|
|
21
|
+
*/
|
|
22
|
+
const MODE_BASE_COLORS = {
|
|
23
|
+
light: {
|
|
24
|
+
background: '#FAFAFA',
|
|
25
|
+
surface: '#FFFFFF',
|
|
26
|
+
foreground: '#171717',
|
|
27
|
+
foregroundMuted: '#525252',
|
|
28
|
+
border: '#DEE2E6',
|
|
29
|
+
muted: '#F5F5F5',
|
|
30
|
+
},
|
|
31
|
+
dark: {
|
|
32
|
+
background: '#18191B',
|
|
33
|
+
surface: '#111113',
|
|
34
|
+
foreground: '#F4F4F5',
|
|
35
|
+
foregroundMuted: '#A1A1AA',
|
|
36
|
+
border: '#2C2D30',
|
|
37
|
+
muted: '#1E1F21',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export const THEME_PALETTES = [
|
|
41
|
+
/* ─── Light Palettes ─── */
|
|
42
|
+
{
|
|
43
|
+
id: 'astral-default',
|
|
44
|
+
name: 'AstralKit',
|
|
45
|
+
description: 'The default AstralKit violet theme',
|
|
46
|
+
mode: 'light',
|
|
47
|
+
colors: {
|
|
48
|
+
background: '#FAF9F7',
|
|
49
|
+
surface: '#FFFFFF',
|
|
50
|
+
foreground: '#150A35',
|
|
51
|
+
foregroundMuted: '#544D6B',
|
|
52
|
+
primary: '#6933FF',
|
|
53
|
+
secondary: '#7C3AED',
|
|
54
|
+
accent: '#3523BD',
|
|
55
|
+
onAccent: '#FFFFFF',
|
|
56
|
+
border: '#E6E3EE',
|
|
57
|
+
ring: '#A585FF',
|
|
58
|
+
muted: '#F4F2F9',
|
|
59
|
+
destructive: '#DC2626',
|
|
60
|
+
brandGradient: {
|
|
61
|
+
from: '#A585FF',
|
|
62
|
+
via: '#6933FF',
|
|
63
|
+
to: '#3523BD',
|
|
64
|
+
hoverFrom: '#C4B5FD',
|
|
65
|
+
hoverVia: '#875CFF',
|
|
66
|
+
hoverTo: '#2C1CB3',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'warm-earth',
|
|
72
|
+
name: 'Warm Earth',
|
|
73
|
+
description: 'Teal primary with coral-red highlights — Happy Hues #8',
|
|
74
|
+
mode: 'light',
|
|
75
|
+
colors: {
|
|
76
|
+
background: '#F8F5F2',
|
|
77
|
+
surface: '#FFFFFF',
|
|
78
|
+
foreground: '#232323',
|
|
79
|
+
foregroundMuted: '#222525',
|
|
80
|
+
primary: '#078080',
|
|
81
|
+
secondary: '#F45D48',
|
|
82
|
+
accent: '#F45D48',
|
|
83
|
+
border: '#E8E2DB',
|
|
84
|
+
ring: '#078080',
|
|
85
|
+
muted: '#FEEFE8',
|
|
86
|
+
destructive: '#F45D48',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'slate-pro',
|
|
91
|
+
name: 'Slate Pro',
|
|
92
|
+
description: 'Professional gray with blue accent',
|
|
93
|
+
mode: 'light',
|
|
94
|
+
colors: {
|
|
95
|
+
background: '#F8FAFC',
|
|
96
|
+
surface: '#FFFFFF',
|
|
97
|
+
foreground: '#0F172A',
|
|
98
|
+
foregroundMuted: '#64748B',
|
|
99
|
+
primary: '#2563EB',
|
|
100
|
+
secondary: '#E2E8F0',
|
|
101
|
+
accent: '#7C3AED',
|
|
102
|
+
border: '#E2E8F0',
|
|
103
|
+
ring: '#2563EB',
|
|
104
|
+
muted: '#F1F5F9',
|
|
105
|
+
destructive: '#DC2626',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'indigo-craft',
|
|
110
|
+
name: 'Indigo Craft',
|
|
111
|
+
description: 'Warm neutral with indigo primary',
|
|
112
|
+
mode: 'light',
|
|
113
|
+
colors: {
|
|
114
|
+
background: '#FAFAF9',
|
|
115
|
+
surface: '#FFFFFF',
|
|
116
|
+
foreground: '#1C1917',
|
|
117
|
+
foregroundMuted: '#78716C',
|
|
118
|
+
primary: '#4F46E5',
|
|
119
|
+
secondary: '#EEF2FF',
|
|
120
|
+
accent: '#EC4899',
|
|
121
|
+
border: '#E7E5E4',
|
|
122
|
+
ring: '#4F46E5',
|
|
123
|
+
muted: '#F5F5F4',
|
|
124
|
+
destructive: '#DC2626',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'medtrackr',
|
|
129
|
+
name: 'MedTrackr Green',
|
|
130
|
+
description: 'Clean white with emerald primary and mint washes — healthcare CRM',
|
|
131
|
+
mode: 'light',
|
|
132
|
+
colors: {
|
|
133
|
+
background: '#F7F9F8',
|
|
134
|
+
surface: '#FFFFFF',
|
|
135
|
+
foreground: '#17211D',
|
|
136
|
+
foregroundMuted: '#5B6661',
|
|
137
|
+
primary: '#0FA47A',
|
|
138
|
+
secondary: '#0FA47A',
|
|
139
|
+
accent: '#0FA47A',
|
|
140
|
+
border: '#E2E8E5',
|
|
141
|
+
ring: '#0FA47A',
|
|
142
|
+
muted: '#ECF2EF',
|
|
143
|
+
destructive: '#E5484D',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'periwinkle',
|
|
148
|
+
name: 'Periwinkle',
|
|
149
|
+
description: 'Lavender-white canvas with vivid periwinkle — fitness SaaS',
|
|
150
|
+
mode: 'light',
|
|
151
|
+
colors: {
|
|
152
|
+
background: '#F4F3FB',
|
|
153
|
+
surface: '#FFFFFF',
|
|
154
|
+
foreground: '#1B1A2E',
|
|
155
|
+
foregroundMuted: '#5A5874',
|
|
156
|
+
primary: '#6F5CF1',
|
|
157
|
+
secondary: '#6F5CF1',
|
|
158
|
+
accent: '#6F5CF1',
|
|
159
|
+
border: '#E4E2F0',
|
|
160
|
+
ring: '#6F5CF1',
|
|
161
|
+
muted: '#ECEAF8',
|
|
162
|
+
destructive: '#E5484D',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'scout-cyan',
|
|
167
|
+
name: 'Scout Cyan',
|
|
168
|
+
description: 'Crisp white with deep sky-cyan — referral dashboard',
|
|
169
|
+
mode: 'light',
|
|
170
|
+
colors: {
|
|
171
|
+
background: '#F6FAFC',
|
|
172
|
+
surface: '#FFFFFF',
|
|
173
|
+
foreground: '#14262D',
|
|
174
|
+
foregroundMuted: '#587077',
|
|
175
|
+
primary: '#0E96C4',
|
|
176
|
+
secondary: '#0E96C4',
|
|
177
|
+
accent: '#0E96C4',
|
|
178
|
+
border: '#DEE9EE',
|
|
179
|
+
ring: '#0E96C4',
|
|
180
|
+
muted: '#EAF3F7',
|
|
181
|
+
destructive: '#E5484D',
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
/* ─── Brand Design System Palettes ─── */
|
|
185
|
+
{
|
|
186
|
+
id: 'pulse',
|
|
187
|
+
name: 'Pulse',
|
|
188
|
+
description: 'Bone neutrals, ink CTAs, volt-lime selection — fitness app energy',
|
|
189
|
+
mode: 'light',
|
|
190
|
+
colors: {
|
|
191
|
+
background: '#F3F3EF',
|
|
192
|
+
surface: '#FFFFFF',
|
|
193
|
+
foreground: '#0B0B0B',
|
|
194
|
+
foregroundMuted: '#55564F',
|
|
195
|
+
primary: '#D7FA4B',
|
|
196
|
+
secondary: '#D7FA4B',
|
|
197
|
+
accent: '#D7FA4B',
|
|
198
|
+
border: '#E4E4DC',
|
|
199
|
+
ring: '#D7FA4B',
|
|
200
|
+
muted: '#ECEDE5',
|
|
201
|
+
destructive: '#F04438',
|
|
202
|
+
sliderFill: '#0B0B0B',
|
|
203
|
+
sliderRing: '#0B0B0B',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'logozap',
|
|
208
|
+
name: 'LogoZap',
|
|
209
|
+
description: 'Warm paper with Zap Blue accent — brand design system experiment',
|
|
210
|
+
mode: 'light',
|
|
211
|
+
colors: {
|
|
212
|
+
background: '#FAFAF7',
|
|
213
|
+
surface: '#FFFFFF',
|
|
214
|
+
foreground: '#0A0A0A',
|
|
215
|
+
foregroundMuted: '#5A5A5A',
|
|
216
|
+
primary: '#1E7BFF',
|
|
217
|
+
secondary: '#CFE3FF',
|
|
218
|
+
accent: '#0B4FD9',
|
|
219
|
+
border: '#E4E2DA',
|
|
220
|
+
ring: '#1E7BFF',
|
|
221
|
+
muted: '#F0EFEA',
|
|
222
|
+
destructive: '#EF4444',
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
/* ─── Dark Palettes ─── */
|
|
226
|
+
{
|
|
227
|
+
id: 'volt-noir',
|
|
228
|
+
name: 'Volt Noir',
|
|
229
|
+
description: 'True black with volt-lime CTAs — dark sibling of Pulse',
|
|
230
|
+
mode: 'dark',
|
|
231
|
+
colors: {
|
|
232
|
+
background: '#0A0A0A',
|
|
233
|
+
surface: '#161616',
|
|
234
|
+
foreground: '#F2F2EF',
|
|
235
|
+
foregroundMuted: '#A6A6A0',
|
|
236
|
+
primary: '#D7F94C',
|
|
237
|
+
secondary: '#D7F94C',
|
|
238
|
+
accent: '#D7F94C',
|
|
239
|
+
border: '#262626',
|
|
240
|
+
ring: '#D7F94C',
|
|
241
|
+
muted: '#1F1F1F',
|
|
242
|
+
destructive: '#FF5A48',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: 'neon-noir',
|
|
247
|
+
name: 'Neon Noir',
|
|
248
|
+
description: 'Deep violet-black with periwinkle glow — neon casino energy',
|
|
249
|
+
mode: 'dark',
|
|
250
|
+
colors: {
|
|
251
|
+
background: '#0D0B16',
|
|
252
|
+
surface: '#171327',
|
|
253
|
+
foreground: '#EDEAF6',
|
|
254
|
+
foregroundMuted: '#9D97B8',
|
|
255
|
+
primary: '#8B7CF7',
|
|
256
|
+
secondary: '#8B7CF7',
|
|
257
|
+
accent: '#8B7CF7',
|
|
258
|
+
border: '#2B2542',
|
|
259
|
+
ring: '#8B7CF7',
|
|
260
|
+
muted: '#1E1936',
|
|
261
|
+
destructive: '#FF4D6A',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: 'astral-ultraviolet',
|
|
266
|
+
name: 'Astral Ultraviolet',
|
|
267
|
+
description: 'Deep violet-black with rich purple gradients and a small coral pulse',
|
|
268
|
+
mode: 'dark',
|
|
269
|
+
colors: {
|
|
270
|
+
background: '#070510',
|
|
271
|
+
surface: '#111024',
|
|
272
|
+
foreground: '#F8F5FF',
|
|
273
|
+
foregroundMuted: '#AAA2C8',
|
|
274
|
+
primary: '#6D5DFF',
|
|
275
|
+
secondary: '#A855F7',
|
|
276
|
+
accent: '#FF7A5C',
|
|
277
|
+
border: '#2B2545',
|
|
278
|
+
ring: '#6D5DFF',
|
|
279
|
+
muted: '#1B1830',
|
|
280
|
+
destructive: '#FF5A6E',
|
|
281
|
+
brandGradient: {
|
|
282
|
+
from: '#A78BFA',
|
|
283
|
+
via: '#6D5DFF',
|
|
284
|
+
to: '#5B21B6',
|
|
285
|
+
hoverFrom: '#C4B5FD',
|
|
286
|
+
hoverVia: '#8B7CFF',
|
|
287
|
+
hoverTo: '#6D28D9',
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
effects: {
|
|
291
|
+
glowPrimary: '#6D5DFF',
|
|
292
|
+
glowSecondary: '#A855F7',
|
|
293
|
+
glowTertiary: '#7C3AED',
|
|
294
|
+
glowWarm: '#FF7A5C',
|
|
295
|
+
meshBase: '#070510',
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: 'plasma-orbit',
|
|
300
|
+
name: 'Plasma Orbit',
|
|
301
|
+
description: 'Black-blue dashboard dark with electric indigo, cyan, and violet glow',
|
|
302
|
+
mode: 'dark',
|
|
303
|
+
colors: {
|
|
304
|
+
background: '#030712',
|
|
305
|
+
surface: '#0B1020',
|
|
306
|
+
foreground: '#F4F8FF',
|
|
307
|
+
foregroundMuted: '#9AA8C7',
|
|
308
|
+
primary: '#16A3FF',
|
|
309
|
+
secondary: '#6D5DFF',
|
|
310
|
+
accent: '#22D3EE',
|
|
311
|
+
border: '#1A2440',
|
|
312
|
+
ring: '#16A3FF',
|
|
313
|
+
muted: '#101831',
|
|
314
|
+
destructive: '#FF5A7A',
|
|
315
|
+
brandGradient: {
|
|
316
|
+
from: '#22D3EE',
|
|
317
|
+
via: '#16A3FF',
|
|
318
|
+
to: '#6D5DFF',
|
|
319
|
+
hoverFrom: '#67E8F9',
|
|
320
|
+
hoverVia: '#38BDF8',
|
|
321
|
+
hoverTo: '#8B7CFF',
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
effects: {
|
|
325
|
+
glowPrimary: '#16A3FF',
|
|
326
|
+
glowSecondary: '#22D3EE',
|
|
327
|
+
glowTertiary: '#6D5DFF',
|
|
328
|
+
glowWarm: '#8B5CF6',
|
|
329
|
+
meshBase: '#030712',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: 'lunar-citron',
|
|
334
|
+
name: 'Lunar Citron',
|
|
335
|
+
description: 'Obsidian surfaces with soft lavender depth and sharp citron energy',
|
|
336
|
+
mode: 'dark',
|
|
337
|
+
colors: {
|
|
338
|
+
background: '#090A0D',
|
|
339
|
+
surface: '#15141B',
|
|
340
|
+
foreground: '#F7F7EC',
|
|
341
|
+
foregroundMuted: '#A8A49A',
|
|
342
|
+
primary: '#E9FF5B',
|
|
343
|
+
secondary: '#8B7CF7',
|
|
344
|
+
accent: '#E9FF5B',
|
|
345
|
+
border: '#2C2A33',
|
|
346
|
+
ring: '#E9FF5B',
|
|
347
|
+
muted: '#1E1D24',
|
|
348
|
+
destructive: '#FF6B5F',
|
|
349
|
+
brandGradient: {
|
|
350
|
+
from: '#F4FF8C',
|
|
351
|
+
via: '#E9FF5B',
|
|
352
|
+
to: '#B6D932',
|
|
353
|
+
hoverFrom: '#F8FFAD',
|
|
354
|
+
hoverVia: '#EEFF72',
|
|
355
|
+
hoverTo: '#CBEA44',
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
effects: {
|
|
359
|
+
glowPrimary: '#E9FF5B',
|
|
360
|
+
glowSecondary: '#8B7CF7',
|
|
361
|
+
glowTertiary: '#C084FC',
|
|
362
|
+
glowWarm: '#FF9F68',
|
|
363
|
+
meshBase: '#090A0D',
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
id: 'magenta-circuit',
|
|
368
|
+
name: 'Magenta Circuit',
|
|
369
|
+
description: 'Charcoal-black with hot magenta, violet circuitry, and small data-green hits',
|
|
370
|
+
mode: 'dark',
|
|
371
|
+
colors: {
|
|
372
|
+
background: '#08070A',
|
|
373
|
+
surface: '#141016',
|
|
374
|
+
foreground: '#FFF7FB',
|
|
375
|
+
foregroundMuted: '#B7A9B7',
|
|
376
|
+
primary: '#E43DDC',
|
|
377
|
+
secondary: '#8B5CF6',
|
|
378
|
+
accent: '#FF5AA8',
|
|
379
|
+
border: '#2C1C2E',
|
|
380
|
+
ring: '#E43DDC',
|
|
381
|
+
muted: '#1F1721',
|
|
382
|
+
destructive: '#FF4D6D',
|
|
383
|
+
brandGradient: {
|
|
384
|
+
from: '#FF5FE7',
|
|
385
|
+
via: '#D946EF',
|
|
386
|
+
to: '#7C3AED',
|
|
387
|
+
hoverFrom: '#FF8CF0',
|
|
388
|
+
hoverVia: '#E879F9',
|
|
389
|
+
hoverTo: '#8B5CF6',
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
effects: {
|
|
393
|
+
glowPrimary: '#E43DDC',
|
|
394
|
+
glowSecondary: '#8B5CF6',
|
|
395
|
+
glowTertiary: '#FF5AA8',
|
|
396
|
+
glowWarm: '#22C55E',
|
|
397
|
+
meshBase: '#08070A',
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: 'deep-teal',
|
|
402
|
+
name: 'Deep Teal',
|
|
403
|
+
description: 'Dark ocean teal with golden accent — Happy Hues #10',
|
|
404
|
+
mode: 'dark',
|
|
405
|
+
colors: {
|
|
406
|
+
background: '#004643',
|
|
407
|
+
surface: '#001E1D',
|
|
408
|
+
foreground: '#FFFFFE',
|
|
409
|
+
foregroundMuted: '#ABD1C6',
|
|
410
|
+
primary: '#F9BC60',
|
|
411
|
+
secondary: '#ABD1C6',
|
|
412
|
+
accent: '#E16162',
|
|
413
|
+
border: '#0A5854',
|
|
414
|
+
ring: '#F9BC60',
|
|
415
|
+
muted: '#0A5854',
|
|
416
|
+
destructive: '#E16162',
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
];
|
|
420
|
+
/* ─── Color utility: lighten/darken hex ─── */
|
|
421
|
+
function hexToRgb(hex) {
|
|
422
|
+
const h = hex.replace('#', '');
|
|
423
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
424
|
+
}
|
|
425
|
+
function rgbToHex(r, g, b) {
|
|
426
|
+
return '#' + [r, g, b].map(v => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('');
|
|
427
|
+
}
|
|
428
|
+
function mixColor(hex, target, amount) {
|
|
429
|
+
const [r1, g1, b1] = hexToRgb(hex);
|
|
430
|
+
const [r2, g2, b2] = hexToRgb(target);
|
|
431
|
+
return rgbToHex(r1 + (r2 - r1) * amount, g1 + (g2 - g1) * amount, b1 + (b2 - b1) * amount);
|
|
432
|
+
}
|
|
433
|
+
function lighten(hex, amount) { return mixColor(hex, '#FFFFFF', amount); }
|
|
434
|
+
function darken(hex, amount) { return mixColor(hex, '#000000', amount); }
|
|
435
|
+
/** Relative luminance (0 dark – 1 light) for picking readable "on" colors. */
|
|
436
|
+
function luminance(hex) {
|
|
437
|
+
const [r, g, b] = hexToRgb(hex).map((v) => {
|
|
438
|
+
const s = v / 255;
|
|
439
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
440
|
+
});
|
|
441
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
442
|
+
}
|
|
443
|
+
/** WCAG contrast ratio between two hex colors. */
|
|
444
|
+
function contrastRatio(a, b) {
|
|
445
|
+
const l1 = luminance(a);
|
|
446
|
+
const l2 = luminance(b);
|
|
447
|
+
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
448
|
+
}
|
|
449
|
+
/** Ink-or-paper text for an arbitrary fill: dark text on light fills, light text on dark fills. */
|
|
450
|
+
function onColor(fill, darkOption, lightOption) {
|
|
451
|
+
return luminance(fill) > 0.45 ? darkOption : lightOption;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Keeps the chosen brand colors while swapping surface, text, border, and muted
|
|
455
|
+
* scaffolding to the current UI mode.
|
|
456
|
+
*/
|
|
457
|
+
export function resolvePaletteForMode(palette, mode) {
|
|
458
|
+
if (palette.mode === mode)
|
|
459
|
+
return palette;
|
|
460
|
+
const base = MODE_BASE_COLORS[mode];
|
|
461
|
+
return {
|
|
462
|
+
...palette,
|
|
463
|
+
mode,
|
|
464
|
+
colors: {
|
|
465
|
+
...palette.colors,
|
|
466
|
+
background: base.background,
|
|
467
|
+
surface: base.surface,
|
|
468
|
+
foreground: base.foreground,
|
|
469
|
+
foregroundMuted: base.foregroundMuted,
|
|
470
|
+
border: base.border,
|
|
471
|
+
muted: base.muted,
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Maps a ThemePalette to the actual --color-ak-* CSS variables
|
|
477
|
+
* that AstralKit components consume.
|
|
478
|
+
*
|
|
479
|
+
* This is the critical bridge between our 60-30-10 palette
|
|
480
|
+
* and the component design token system.
|
|
481
|
+
*/
|
|
482
|
+
export function paletteToAkVariables(palette, options) {
|
|
483
|
+
const resolvedPalette = options?.mode ? resolvePaletteForMode(palette, options.mode) : palette;
|
|
484
|
+
const c = resolvedPalette.colors;
|
|
485
|
+
const p = c.primary;
|
|
486
|
+
const isDark = resolvedPalette.mode === 'dark';
|
|
487
|
+
// Readable text on primary/accent fills regardless of how light they are
|
|
488
|
+
// (volt lime, yellow, etc. need ink text — not the background color).
|
|
489
|
+
const ink = isDark ? c.background : c.foreground;
|
|
490
|
+
const paper = isDark ? c.foreground : c.surface;
|
|
491
|
+
const onPrimary = onColor(p, ink, paper);
|
|
492
|
+
const secondary = c.secondary || c.accent;
|
|
493
|
+
const onAccent = c.onAccent ?? onColor(c.accent, ink, paper);
|
|
494
|
+
const onSecondary = onColor(secondary, ink, paper);
|
|
495
|
+
const accentSoft = mixColor(c.accent, c.surface, isDark ? 0.78 : 0.82);
|
|
496
|
+
const secondaryContainer = mixColor(secondary, c.surface, isDark ? 0.78 : 0.82);
|
|
497
|
+
const primaryContainer = isDark ? mixColor(p, c.surface, 0.76) : lighten(p, 0.88);
|
|
498
|
+
const onAccentSoft = onColor(accentSoft, ink, paper);
|
|
499
|
+
const onSecondaryContainer = onColor(secondaryContainer, ink, paper);
|
|
500
|
+
const onPrimaryContainer = onColor(primaryContainer, ink, paper);
|
|
501
|
+
const selectionInk = contrastRatio(p, primaryContainer) >= 4.5 ? p : onPrimaryContainer;
|
|
502
|
+
const gradientFrom = c.brandGradient?.from ?? (isDark ? lighten(p, 0.18) : lighten(p, 0.32));
|
|
503
|
+
const gradientVia = c.brandGradient?.via ?? p;
|
|
504
|
+
const gradientTo = c.brandGradient?.to ?? (isDark ? darken(p, 0.08) : darken(p, 0.26));
|
|
505
|
+
const gradientHoverFrom = c.brandGradient?.hoverFrom ?? (isDark ? lighten(p, 0.28) : lighten(p, 0.42));
|
|
506
|
+
const gradientHoverVia = c.brandGradient?.hoverVia ?? (isDark ? lighten(p, 0.08) : lighten(p, 0.12));
|
|
507
|
+
const gradientHoverTo = c.brandGradient?.hoverTo ?? (isDark ? darken(p, 0.02) : darken(p, 0.34));
|
|
508
|
+
const glowPrimary = resolvedPalette.effects?.glowPrimary ?? (isDark ? lighten(p, 0.18) : p);
|
|
509
|
+
const glowSecondary = resolvedPalette.effects?.glowSecondary ?? secondary;
|
|
510
|
+
const glowTertiary = resolvedPalette.effects?.glowTertiary ?? c.accent;
|
|
511
|
+
const glowWarm = resolvedPalette.effects?.glowWarm ?? c.accent;
|
|
512
|
+
const meshBase = resolvedPalette.effects?.meshBase ?? c.background;
|
|
513
|
+
const surface2 = c.muted || mixColor(c.background, c.foreground, 0.035);
|
|
514
|
+
const surface3 = mixColor(c.background, c.foreground, isDark ? 0.11 : 0.07);
|
|
515
|
+
const surface4 = mixColor(c.background, c.foreground, isDark ? 0.16 : 0.105);
|
|
516
|
+
const border = isDark ? 'rgba(255,255,255,0.10)' : c.border;
|
|
517
|
+
const borderSubtle = isDark ? 'rgba(255,255,255,0.06)' : mixColor(c.border, c.background, 0.5);
|
|
518
|
+
const borderStrong = isDark ? 'rgba(255,255,255,0.16)' : mixColor(c.border, c.foreground, 0.2);
|
|
519
|
+
const errorContainer = isDark ? mixColor(c.destructive, c.surface, 0.78) : lighten(c.destructive, 0.88);
|
|
520
|
+
const onErrorContainer = onColor(errorContainer, ink, paper);
|
|
521
|
+
// Links must read on the page background: too-light primaries fall back to ink.
|
|
522
|
+
const linkColor = !isDark && luminance(p) > 0.6 ? c.foreground : p;
|
|
523
|
+
return {
|
|
524
|
+
// Selection accent (10%): marks active/selected state in components.
|
|
525
|
+
// Components fall back to the ink system when these are absent.
|
|
526
|
+
'--color-ak-accent': c.accent,
|
|
527
|
+
'--color-ak-on-accent': onAccent,
|
|
528
|
+
'--color-ak-accent-soft': accentSoft,
|
|
529
|
+
'--color-ak-gradient-start': gradientFrom,
|
|
530
|
+
'--color-ak-gradient-mid': gradientVia,
|
|
531
|
+
'--color-ak-gradient-end': gradientTo,
|
|
532
|
+
'--gradient-ak-primary': `linear-gradient(135deg, ${gradientFrom} 0%, ${gradientVia} 52%, ${gradientTo} 100%)`,
|
|
533
|
+
'--gradient-ak-primary-hover': `linear-gradient(135deg, ${gradientHoverFrom} 0%, ${gradientHoverVia} 48%, ${gradientHoverTo} 100%)`,
|
|
534
|
+
'--color-ak-glow-primary': glowPrimary,
|
|
535
|
+
// Hero media: palettes lift the default grayscale so photography takes color.
|
|
536
|
+
'--ak-hero-media-filter': 'none',
|
|
537
|
+
// Hero accent: the raw brand primary — stars/ratings/indicators on dark heroes
|
|
538
|
+
// match the vivid color CTAs use (volt lime, bright purple), never a washed tint.
|
|
539
|
+
'--color-ak-hero-accent': p,
|
|
540
|
+
'--color-ak-glow-secondary': glowSecondary,
|
|
541
|
+
'--color-ak-glow-tertiary': glowTertiary,
|
|
542
|
+
'--color-ak-glow-warm': glowWarm,
|
|
543
|
+
'--color-ak-mesh-base': meshBase,
|
|
544
|
+
'--gradient-ak-neon-border': `conic-gradient(from var(--ak-border-angle, 180deg) at 50% 50%, transparent 0 18%, color-mix(in srgb, ${glowPrimary} 90%, transparent) 34%, color-mix(in srgb, ${glowSecondary} 78%, transparent) 50%, color-mix(in srgb, ${glowTertiary} 70%, transparent) 64%, transparent 78% 100%)`,
|
|
545
|
+
...(c.sliderFill ? { '--color-ak-slider-fill': c.sliderFill } : {}),
|
|
546
|
+
...(c.sliderRing ? { '--color-ak-slider-ring': c.sliderRing } : {}),
|
|
547
|
+
// Primary scale (generated from the primary color)
|
|
548
|
+
'--color-ak-primary-50': lighten(p, 0.92),
|
|
549
|
+
'--color-ak-primary-100': lighten(p, 0.84),
|
|
550
|
+
'--color-ak-primary-200': lighten(p, 0.68),
|
|
551
|
+
'--color-ak-primary-300': lighten(p, 0.48),
|
|
552
|
+
'--color-ak-primary-400': lighten(p, 0.28),
|
|
553
|
+
'--color-ak-primary-500': p,
|
|
554
|
+
'--color-ak-primary-600': darken(p, 0.12),
|
|
555
|
+
'--color-ak-primary-700': darken(p, 0.24),
|
|
556
|
+
'--color-ak-primary-800': darken(p, 0.36),
|
|
557
|
+
'--color-ak-primary-900': darken(p, 0.52),
|
|
558
|
+
'--color-ak-primary-950': darken(p, 0.68),
|
|
559
|
+
// Semantic text
|
|
560
|
+
'--color-ak-text': c.foreground,
|
|
561
|
+
'--color-ak-text-secondary': c.foregroundMuted,
|
|
562
|
+
'--color-ak-text-muted': mixColor(c.foregroundMuted, c.background, 0.3),
|
|
563
|
+
'--color-ak-text-disabled': mixColor(c.foregroundMuted, c.background, 0.6),
|
|
564
|
+
'--color-ak-text-link': linkColor,
|
|
565
|
+
'--color-ak-text-inverse': c.surface,
|
|
566
|
+
'--color-ak-on-surface': c.foreground,
|
|
567
|
+
'--color-ak-on-surface-variant': c.foregroundMuted,
|
|
568
|
+
// Text extras
|
|
569
|
+
'--color-ak-text-placeholder': mixColor(c.foregroundMuted, c.background, 0.4),
|
|
570
|
+
// Semantic surfaces
|
|
571
|
+
// Kit reality: ak-bg is PAPER (cards/inputs painted with it historically);
|
|
572
|
+
// ak-surface is the muted APP CANVAS (showcase background, banded sections).
|
|
573
|
+
'--color-ak-bg': c.surface,
|
|
574
|
+
'--color-ak-surface': c.background,
|
|
575
|
+
// Inset surfaces derive from the palette's *surface* color (seated slightly
|
|
576
|
+
// into the page) so tracks/panels keep the surface's definition — e.g. white
|
|
577
|
+
// panels on a bone page — instead of muddying toward the foreground.
|
|
578
|
+
'--color-ak-surface-2': surface2,
|
|
579
|
+
'--color-ak-surface-3': surface3,
|
|
580
|
+
'--color-ak-surface-4': surface4,
|
|
581
|
+
'--color-ak-surface-container-lowest': c.surface,
|
|
582
|
+
'--color-ak-surface-container-low': c.background,
|
|
583
|
+
'--color-ak-surface-container': surface2,
|
|
584
|
+
'--color-ak-surface-container-high': surface3,
|
|
585
|
+
'--color-ak-surface-container-highest': surface4,
|
|
586
|
+
'--color-ak-surface-raised-soft': mixColor(c.background, c.foreground, 0.02),
|
|
587
|
+
'--color-ak-hover': isDark ? 'rgba(255,255,255,0.04)' : mixColor(c.surface, c.foreground, 0.05),
|
|
588
|
+
'--color-ak-active': isDark ? 'rgba(255,255,255,0.08)' : mixColor(c.surface, c.foreground, 0.08),
|
|
589
|
+
'--color-ak-elevated': c.surface,
|
|
590
|
+
// Inverted panels (terminals, dark rails) — M3 inverse-surface
|
|
591
|
+
'--color-ak-inverse-surface': c.foreground,
|
|
592
|
+
'--color-ak-inverse-on-surface': onColor(c.foreground, c.background, c.surface),
|
|
593
|
+
// Semantic borders
|
|
594
|
+
'--color-ak-border': border,
|
|
595
|
+
'--color-ak-border-subtle': borderSubtle,
|
|
596
|
+
'--color-ak-border-strong': borderStrong,
|
|
597
|
+
'--color-ak-outline': borderStrong,
|
|
598
|
+
'--color-ak-outline-variant': borderSubtle,
|
|
599
|
+
// Focus follows the selection color when the palette splits primary/accent,
|
|
600
|
+
// and the explicit `ring` field wins over both.
|
|
601
|
+
'--color-ak-border-focus': c.ring && c.ring.toLowerCase() !== p.toLowerCase() ? c.ring : (c.accent && c.accent.toLowerCase() !== p.toLowerCase() ? c.accent : p),
|
|
602
|
+
// Neutral scale (derived from foreground/background mix)
|
|
603
|
+
'--color-ak-neutral-50': mixColor(c.background, c.foreground, 0.02),
|
|
604
|
+
'--color-ak-neutral-100': mixColor(c.background, c.foreground, 0.04),
|
|
605
|
+
'--color-ak-neutral-200': mixColor(c.background, c.foreground, 0.08),
|
|
606
|
+
'--color-ak-neutral-300': mixColor(c.background, c.foreground, 0.15),
|
|
607
|
+
'--color-ak-neutral-400': mixColor(c.background, c.foreground, 0.3),
|
|
608
|
+
'--color-ak-neutral-500': mixColor(c.background, c.foreground, 0.45),
|
|
609
|
+
'--color-ak-neutral-600': mixColor(c.background, c.foreground, 0.6),
|
|
610
|
+
'--color-ak-neutral-700': mixColor(c.background, c.foreground, 0.72),
|
|
611
|
+
'--color-ak-neutral-800': mixColor(c.background, c.foreground, 0.84),
|
|
612
|
+
'--color-ak-neutral-900': mixColor(c.background, c.foreground, 0.92),
|
|
613
|
+
'--color-ak-neutral-950': mixColor(c.background, c.foreground, 0.96),
|
|
614
|
+
// Primary as component tokens
|
|
615
|
+
'--color-ak-primary': p,
|
|
616
|
+
'--color-ak-primary-hover': isDark ? lighten(p, 0.15) : darken(p, 0.1),
|
|
617
|
+
'--color-ak-primary-active': isDark ? lighten(p, 0.25) : darken(p, 0.2),
|
|
618
|
+
'--color-ak-primary-outline': p,
|
|
619
|
+
'--color-ak-secondary-outline': secondary,
|
|
620
|
+
'--color-ak-primary-selection': p,
|
|
621
|
+
'--color-ak-on-primary-selection': onPrimary,
|
|
622
|
+
'--color-ak-on-primary': onPrimary,
|
|
623
|
+
'--color-ak-primary-subtle': primaryContainer,
|
|
624
|
+
'--color-ak-primary-container': primaryContainer,
|
|
625
|
+
'--color-ak-on-primary-container': onPrimaryContainer,
|
|
626
|
+
'--color-ak-selection-ink': selectionInk,
|
|
627
|
+
'--color-ak-primary-container-emphasis': selectionInk,
|
|
628
|
+
'--color-ak-secondary': secondary,
|
|
629
|
+
'--color-ak-on-secondary': onSecondary,
|
|
630
|
+
'--color-ak-secondary-container': secondaryContainer,
|
|
631
|
+
'--color-ak-on-secondary-container': onSecondaryContainer,
|
|
632
|
+
'--color-ak-tertiary': c.accent,
|
|
633
|
+
'--color-ak-on-tertiary': onAccent,
|
|
634
|
+
'--color-ak-tertiary-container': accentSoft,
|
|
635
|
+
'--color-ak-on-tertiary-container': onAccentSoft,
|
|
636
|
+
// Status colors
|
|
637
|
+
'--color-ak-success': '#22C55E',
|
|
638
|
+
'--color-ak-success-subtle': lighten('#22C55E', 0.88),
|
|
639
|
+
'--color-ak-success-text': darken('#22C55E', 0.2),
|
|
640
|
+
'--color-ak-warning': '#F59E0B',
|
|
641
|
+
'--color-ak-warning-subtle': lighten('#F59E0B', 0.88),
|
|
642
|
+
'--color-ak-warning-text': darken('#F59E0B', 0.2),
|
|
643
|
+
'--color-ak-danger': c.destructive,
|
|
644
|
+
'--color-ak-on-error': onColor(c.destructive, isDark ? c.background : c.foreground, isDark ? c.foreground : c.surface),
|
|
645
|
+
...(isDark ? { '--color-ak-pink': '#f472b6', '--color-ak-pink-subtle': 'rgba(244,114,182,.12)' } : {}),
|
|
646
|
+
'--color-ak-danger-subtle': errorContainer,
|
|
647
|
+
'--color-ak-danger-text': darken(c.destructive, 0.2),
|
|
648
|
+
'--color-ak-error': c.destructive,
|
|
649
|
+
'--color-ak-error-container': errorContainer,
|
|
650
|
+
'--color-ak-on-error-container': onErrorContainer,
|
|
651
|
+
'--color-ak-info': '#3B82F6',
|
|
652
|
+
'--color-ak-info-subtle': lighten('#3B82F6', 0.88),
|
|
653
|
+
'--color-ak-info-text': darken('#3B82F6', 0.2),
|
|
654
|
+
// Overlay
|
|
655
|
+
'--color-ak-overlay': isDark ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.5)',
|
|
656
|
+
// Dark mode: disable shadows (they don't read on dark surfaces)
|
|
657
|
+
...(isDark ? { '--shadow-ak-xs': 'none', '--shadow-ak-sm': 'none' } : {}),
|
|
658
|
+
// Neobrutalist tokens (derived from PRIMARY for visible color shifts)
|
|
659
|
+
'--color-ak-brutal-ink': isDark ? p : darken(p, 0.3),
|
|
660
|
+
'--color-ak-brutal-face': isDark ? c.background : c.surface,
|
|
661
|
+
'--color-ak-brutal-face-dark': isDark ? lighten(p, 0.15) : darken(p, 0.35),
|
|
662
|
+
'--color-ak-brutal-on-face': isDark ? p : darken(p, 0.3),
|
|
663
|
+
'--color-ak-brutal-on-face-dark': isDark ? c.background : c.background,
|
|
664
|
+
'--shadow-ak-brutal-xs': `2px 2px 0 0 ${isDark ? lighten(p, 0.2) : darken(p, 0.4)}`,
|
|
665
|
+
'--shadow-ak-brutal-sm': `3px 3px 0 0 ${isDark ? lighten(p, 0.2) : darken(p, 0.4)}`,
|
|
666
|
+
'--shadow-ak-brutal': `4px 4px 0 0 ${isDark ? lighten(p, 0.2) : darken(p, 0.4)}`,
|
|
667
|
+
'--shadow-ak-brutal-lg': `6px 6px 0 0 ${isDark ? lighten(p, 0.2) : darken(p, 0.4)}`,
|
|
668
|
+
'--shadow-ak-brutal-xl': `8px 8px 0 0 ${isDark ? lighten(p, 0.2) : darken(p, 0.4)}`,
|
|
669
|
+
// Status badge colors
|
|
670
|
+
'--color-ak-status-beta-bg': lighten('#8B5CF6', 0.88),
|
|
671
|
+
'--color-ak-status-beta-text': '#7C3AED',
|
|
672
|
+
'--color-ak-status-beta-border': lighten('#8B5CF6', 0.7),
|
|
673
|
+
'--color-ak-status-new-bg': lighten('#22C55E', 0.88),
|
|
674
|
+
'--color-ak-status-new-text': '#16A34A',
|
|
675
|
+
'--color-ak-status-new-border': lighten('#22C55E', 0.7),
|
|
676
|
+
'--color-ak-status-stable-bg': lighten('#3B82F6', 0.88),
|
|
677
|
+
'--color-ak-status-stable-text': '#2563EB',
|
|
678
|
+
'--color-ak-status-stable-border': lighten('#3B82F6', 0.7),
|
|
679
|
+
'--color-ak-status-deprecated-bg': lighten('#EF4444', 0.88),
|
|
680
|
+
'--color-ak-status-deprecated-text': '#DC2626',
|
|
681
|
+
'--color-ak-status-deprecated-border': lighten('#EF4444', 0.7),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Palette authorship lint — encodes the recipe learned from curation:
|
|
686
|
+
* keepers share a near-neutral foreground, neutral light surfaces, and a
|
|
687
|
+
* mid-luminance primary (or an explicit accent split for light primaries).
|
|
688
|
+
* Returns human-readable warnings; empty array = palette is well-formed.
|
|
689
|
+
*/
|
|
690
|
+
export function validatePalette(palette) {
|
|
691
|
+
const c = palette.colors;
|
|
692
|
+
const warnings = [];
|
|
693
|
+
const sat = (hex) => {
|
|
694
|
+
const h = hex.replace('#', '');
|
|
695
|
+
const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
|
|
696
|
+
return Math.max(r, g, b) - Math.min(r, g, b);
|
|
697
|
+
};
|
|
698
|
+
const isDark = palette.mode === 'dark';
|
|
699
|
+
if (sat(c.foreground) > 45)
|
|
700
|
+
warnings.push(`foreground ${c.foreground} is heavily tinted (sat ${sat(c.foreground)}) — text washes the whole page`);
|
|
701
|
+
if (!isDark && sat(c.surface) > 10)
|
|
702
|
+
warnings.push(`surface ${c.surface} is tinted (sat ${sat(c.surface)}) — paper should stay near-neutral in light mode`);
|
|
703
|
+
if (!isDark && luminance(c.primary) > 0.85)
|
|
704
|
+
warnings.push(`primary ${c.primary} is near-white (lum ${luminance(c.primary).toFixed(2)}) — CTAs will vanish on paper`);
|
|
705
|
+
if (c.background.toLowerCase() === c.surface.toLowerCase())
|
|
706
|
+
warnings.push('background == surface — no canvas/paper separation');
|
|
707
|
+
return warnings;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Generates CSS :root block from a palette using the ak variable names.
|
|
711
|
+
*/
|
|
712
|
+
export function paletteToCSS(palette) {
|
|
713
|
+
const vars = paletteToAkVariables(palette);
|
|
714
|
+
let css = `/* ${palette.name} — ${palette.description} */\n:root {\n`;
|
|
715
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
716
|
+
css += ` ${key}: ${value};\n`;
|
|
717
|
+
}
|
|
718
|
+
css += '}\n';
|
|
719
|
+
return css;
|
|
720
|
+
}
|