astralkit 0.6.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.
@@ -0,0 +1,330 @@
1
+ /**
2
+ * astralkit/presets — dials.
3
+ *
4
+ * BASE_COLORS — the neutral scale under surfaces/borders/text.
5
+ * ACCENT_THEMES — the brand accent hue over the full primary role family.
6
+ * TYPE_SCALES — authored size hierarchies (one dial, whole ramp).
7
+ * radiusToVariables — the full derived radius set (the :root substitution rule).
8
+ *
9
+ * Every color dial is MODE-AWARE: pass { mode: 'dark' } and you get the
10
+ * authored dark mapping, following the SDK's own dark language — near-black
11
+ * tinted surface ramps, WHITE-ALPHA hovers/borders (never gray hexes), text
12
+ * from the scale's light end, accents at 500 with darkened states. Built to
13
+ * match [data-theme="dark"] in astralkit-theme.css and the dark palettes.
14
+ */
15
+
16
+ // ── Base colors ──────────────────────────────────────────────────────
17
+
18
+ export const DEFAULT_BASE_COLOR_ID = 'neutral'
19
+
20
+ const lightBase = (s) => ({
21
+ '--color-ak-surface': s[50],
22
+ '--color-ak-surface-2': s[100],
23
+ '--color-ak-surface-3': s[200],
24
+ '--color-ak-surface-4': s[300],
25
+ '--color-ak-hover': s[100],
26
+ '--color-ak-active': s[200],
27
+ '--color-ak-border': s[200],
28
+ '--color-ak-border-subtle': s[100],
29
+ '--color-ak-border-strong': s[400],
30
+ '--color-ak-text': s[950],
31
+ '--color-ak-text-secondary': s[600],
32
+ '--color-ak-text-muted': s[400],
33
+ '--color-ak-on-surface': s[950],
34
+ '--color-ak-on-surface-variant': s[600],
35
+ })
36
+
37
+ // Dark: tinted near-black ramp from the scale's deep end; hovers/actives and
38
+ // borders stay white-alpha exactly like the SDK's own dark block.
39
+ const darkBase = (s) => ({
40
+ '--color-ak-bg': s[950],
41
+ '--color-ak-surface': s[900],
42
+ '--color-ak-surface-2': s[800],
43
+ '--color-ak-surface-3': s[800],
44
+ '--color-ak-surface-4': s[700],
45
+ '--color-ak-hover': 'rgba(255,255,255,0.04)',
46
+ '--color-ak-active': 'rgba(255,255,255,0.08)',
47
+ '--color-ak-border': 'rgba(255,255,255,0.10)',
48
+ '--color-ak-border-subtle': 'rgba(255,255,255,0.06)',
49
+ '--color-ak-border-strong': 'rgba(255,255,255,0.16)',
50
+ '--color-ak-text': s[50],
51
+ '--color-ak-text-secondary': s[400],
52
+ '--color-ak-text-muted': s[600],
53
+ '--color-ak-on-surface': s[50],
54
+ '--color-ak-on-surface-variant': s[400],
55
+ })
56
+
57
+ const base = (id, name, description, scale) => ({
58
+ id,
59
+ name,
60
+ description,
61
+ variables: lightBase(scale),
62
+ darkVariables: darkBase(scale),
63
+ swatch: scale[600],
64
+ })
65
+
66
+ export const BASE_COLORS = [
67
+ { id: 'neutral', name: 'Neutral', description: 'The SDK default neutral scale.', variables: {}, darkVariables: {}, swatch: '#a3a3a3' },
68
+ base('stone', 'Stone', 'Warm gray — earthy, editorial.', {
69
+ 50: '#fafaf9', 100: '#f5f5f4', 200: '#e7e5e4', 300: '#d6d3d1', 400: '#a8a29e', 600: '#57534e', 700: '#44403c', 800: '#292524', 900: '#1c1917', 950: '#0c0a09',
70
+ }),
71
+ base('zinc', 'Zinc', 'Cool gray — technical, precise.', {
72
+ 50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8', 400: '#a1a1aa', 600: '#52525b', 700: '#3f3f46', 800: '#27272a', 900: '#18181b', 950: '#09090b',
73
+ }),
74
+ base('mauve', 'Mauve', 'Violet-tinted neutral.', {
75
+ 50: 'oklch(98.5% 0 0)', 100: 'oklch(96% 0.003 325.6)', 200: 'oklch(92.2% 0.005 325.62)', 300: 'oklch(86.5% 0.012 325.68)', 400: 'oklch(71.1% 0.019 323.02)', 600: 'oklch(43.5% 0.029 321.78)', 700: 'oklch(36.4% 0.029 323.89)', 800: 'oklch(26.3% 0.024 320.12)', 900: 'oklch(21.2% 0.019 322.12)', 950: 'oklch(14.5% 0.008 326)',
76
+ }),
77
+ base('olive', 'Olive', 'Green-tinted neutral.', {
78
+ 50: 'oklch(98.8% 0.003 106.5)', 100: 'oklch(96.6% 0.005 106.5)', 200: 'oklch(93% 0.007 106.5)', 300: 'oklch(88% 0.011 106.6)', 400: 'oklch(73.7% 0.021 106.9)', 600: 'oklch(46.6% 0.025 107.3)', 700: 'oklch(39.4% 0.023 107.4)', 800: 'oklch(28.6% 0.016 107.4)', 900: 'oklch(22.8% 0.013 107.4)', 950: 'oklch(15.3% 0.006 107.1)',
79
+ }),
80
+ base('mist', 'Mist', 'Blue-tinted neutral.', {
81
+ 50: 'oklch(98.7% 0.002 197.1)', 100: 'oklch(96.3% 0.002 197.1)', 200: 'oklch(92.5% 0.005 214.3)', 300: 'oklch(87.2% 0.007 219.6)', 400: 'oklch(72.3% 0.014 214.4)', 600: 'oklch(45% 0.017 213.2)', 700: 'oklch(37.8% 0.015 216)', 800: 'oklch(27.5% 0.011 216.9)', 900: 'oklch(21.8% 0.008 223.9)', 950: 'oklch(14.8% 0.004 228.8)',
82
+ }),
83
+ base('taupe', 'Taupe', 'Brown-tinted neutral.', {
84
+ 50: 'oklch(98.6% 0.002 67.8)', 100: 'oklch(96% 0.002 17.2)', 200: 'oklch(92.2% 0.005 34.3)', 300: 'oklch(86.8% 0.007 39.5)', 400: 'oklch(71.4% 0.014 41.2)', 600: 'oklch(43.8% 0.017 39.3)', 700: 'oklch(36.7% 0.016 35.7)', 800: 'oklch(26.8% 0.011 36.5)', 900: 'oklch(21.4% 0.009 43.1)', 950: 'oklch(14.7% 0.004 49.3)',
85
+ }),
86
+ ]
87
+
88
+ export function baseColorToVariables(base, opts) {
89
+ const mode = (opts && opts.mode) || 'light'
90
+ return { ...(mode === 'dark' ? base.darkVariables : base.variables) }
91
+ }
92
+
93
+ // ── Accent themes ────────────────────────────────────────────────────
94
+
95
+ export const DEFAULT_ACCENT_THEME_ID = 'default'
96
+
97
+ // ink = on-primary for hues where white text fails contrast on the fill.
98
+ const lightAccent = (s, ink) => ({
99
+ '--color-ak-primary': s[600],
100
+ // Focus + glow follow the accent, mirroring the palette recipe
101
+ // (palettes: border-focus = ring||accent||primary, glow = primary).
102
+ // Without these the base theme's ink focus (#0b0b0b) and the
103
+ // utilities' indigo glow fallback (#818cf8) survive an accent swap.
104
+ '--color-ak-border-focus': s[600],
105
+ '--color-ak-glow-primary': s[600],
106
+ // Full hue parity with the palette layer. Accents apply ON TOP of a
107
+ // palette, so every primary-hue-derived token a palette sets must be
108
+ // overridden here or the palette's hue bleeds through (green gradients
109
+ // under an indigo accent). Neutral/surface tokens stay with the base
110
+ // dial on purpose.
111
+ '--color-ak-primary-50': s[50], '--color-ak-primary-100': s[100],
112
+ '--color-ak-primary-200': s[200], '--color-ak-primary-300': s[300],
113
+ '--color-ak-primary-400': s[400], '--color-ak-primary-500': s[500],
114
+ '--color-ak-primary-600': s[600], '--color-ak-primary-700': s[700],
115
+ '--color-ak-primary-800': s[800], '--color-ak-primary-900': s[900],
116
+ '--color-ak-primary-950': s[950],
117
+ '--color-ak-accent': s[600],
118
+ '--color-ak-on-accent': ink ? '#1c1917' : '#ffffff',
119
+ '--color-ak-accent-soft': s[100],
120
+ '--color-ak-primary-selection': s[600],
121
+ '--color-ak-on-primary-selection': ink ? '#1c1917' : '#ffffff',
122
+ '--color-ak-primary-container-emphasis': s[200],
123
+ '--color-ak-text-link': ink ? s[700] : s[600],
124
+ '--color-ak-hero-accent': s[600],
125
+ '--color-ak-gradient-start': s[400],
126
+ '--color-ak-gradient-mid': s[600],
127
+ '--color-ak-gradient-end': s[800],
128
+ '--gradient-ak-primary': `linear-gradient(135deg, ${s[400]} 0%, ${s[600]} 52%, ${s[800]} 100%)`,
129
+ '--gradient-ak-primary-hover': `linear-gradient(135deg, ${s[500]} 0%, ${s[700]} 48%, ${s[900]} 100%)`,
130
+ '--color-ak-primary-hover': s[700],
131
+ '--color-ak-primary-active': s[800],
132
+ '--color-ak-on-primary': ink ? '#1c1917' : '#ffffff',
133
+ '--color-ak-primary-subtle': s[50],
134
+ '--color-ak-primary-subtle-hover': s[100],
135
+ '--color-ak-primary-container': s[100],
136
+ '--color-ak-on-primary-container': s[900],
137
+ '--color-ak-primary-outline': s[400],
138
+ '--color-ak-selection-ink': s[700],
139
+ })
140
+
141
+ // Dark: brighter 500 fill, darkened hover/active states (contrast-stable
142
+ // with the same on-primary), hue-tinted dark containers, light readable inks.
143
+ const darkAccent = (s, ink) => ({
144
+ '--color-ak-primary': s[500],
145
+ '--color-ak-primary-hover': s[600],
146
+ '--color-ak-primary-active': s[700],
147
+ '--color-ak-border-focus': s[500],
148
+ '--color-ak-glow-primary': s[400],
149
+ // Dark parity — same token set, dark mappings (fills brighten, deeps
150
+ // carry containers). See the light builder for why parity matters.
151
+ '--color-ak-primary-50': s[950], '--color-ak-primary-100': s[900],
152
+ '--color-ak-primary-200': s[800], '--color-ak-primary-300': s[700],
153
+ '--color-ak-primary-400': s[600], '--color-ak-primary-500': s[500],
154
+ '--color-ak-primary-600': s[400], '--color-ak-primary-700': s[300],
155
+ '--color-ak-primary-800': s[200], '--color-ak-primary-900': s[100],
156
+ '--color-ak-primary-950': s[50],
157
+ '--color-ak-accent': s[500],
158
+ '--color-ak-on-accent': ink ? '#1c1917' : '#ffffff',
159
+ '--color-ak-accent-soft': s[900],
160
+ '--color-ak-primary-selection': s[500],
161
+ '--color-ak-on-primary-selection': ink ? '#1c1917' : '#ffffff',
162
+ '--color-ak-primary-container-emphasis': s[800],
163
+ '--color-ak-text-link': s[400],
164
+ '--color-ak-hero-accent': s[500],
165
+ '--color-ak-gradient-start': s[400],
166
+ '--color-ak-gradient-mid': s[500],
167
+ '--color-ak-gradient-end': s[700],
168
+ '--gradient-ak-primary': `linear-gradient(135deg, ${s[400]} 0%, ${s[500]} 52%, ${s[700]} 100%)`,
169
+ '--gradient-ak-primary-hover': `linear-gradient(135deg, ${s[300]} 0%, ${s[400]} 48%, ${s[600]} 100%)`,
170
+ '--color-ak-on-primary': ink ? '#1c1917' : '#ffffff',
171
+ '--color-ak-primary-subtle': s[900],
172
+ '--color-ak-primary-subtle-hover': s[800],
173
+ '--color-ak-primary-container': s[900],
174
+ '--color-ak-on-primary-container': s[100],
175
+ '--color-ak-primary-outline': s[700],
176
+ '--color-ak-selection-ink': s[400],
177
+ })
178
+
179
+ const acc = (id, name, scale, ink) => ({
180
+ id,
181
+ name,
182
+ description: '',
183
+ variables: lightAccent(scale, ink),
184
+ darkVariables: darkAccent(scale, ink),
185
+ swatch: scale[600],
186
+ })
187
+
188
+ export const ACCENT_THEMES = [
189
+ { id: 'default', name: 'Default', description: 'The active palette’s own accent.', variables: {}, darkVariables: {}, swatch: '#a3a3a3' },
190
+ acc('blue', 'Blue', { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' }),
191
+ acc('indigo', 'Indigo', { 50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc', 400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca', 800: '#3730a3', 900: '#312e81', 950: '#1e1b4b' }),
192
+ acc('violet', 'Violet', { 50: '#f5f3ff', 100: '#ede9fe', 200: '#ddd6fe', 300: '#c4b5fd', 400: '#a78bfa', 500: '#8b5cf6', 600: '#7c3aed', 700: '#6d28d9', 800: '#5b21b6', 900: '#4c1d95', 950: '#2e1065' }),
193
+ acc('purple', 'Purple', { 50: '#faf5ff', 100: '#f3e8ff', 200: '#e9d5ff', 300: '#d8b4fe', 400: '#c084fc', 500: '#a855f7', 600: '#9333ea', 700: '#7e22ce', 800: '#6b21a8', 900: '#581c87', 950: '#3b0764' }),
194
+ acc('pink', 'Pink', { 50: '#fdf2f8', 100: '#fce7f3', 200: '#fbcfe8', 300: '#f9a8d4', 400: '#f472b6', 500: '#ec4899', 600: '#db2777', 700: '#be185d', 800: '#9d174d', 900: '#831843', 950: '#500724' }),
195
+ acc('rose', 'Rose', { 50: '#fff1f2', 100: '#ffe4e6', 200: '#fecdd3', 300: '#fda4af', 400: '#fb7185', 500: '#f43f5e', 600: '#e11d48', 700: '#be123c', 800: '#9f1239', 900: '#881337', 950: '#4c0519' }),
196
+ acc('red', 'Red', { 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5', 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c', 800: '#991b1b', 900: '#7f1d1d', 950: '#450a0a' }),
197
+ acc('orange', 'Orange', { 50: '#fff7ed', 100: '#ffedd5', 200: '#fed7aa', 300: '#fdba74', 400: '#fb923c', 500: '#f97316', 600: '#ea580c', 700: '#c2410c', 800: '#9a3412', 900: '#7c2d12', 950: '#431407' }),
198
+ acc('amber', 'Amber', { 50: '#fffbeb', 100: '#fef3c7', 200: '#fde68a', 300: '#fcd34d', 400: '#fbbf24', 500: '#f59e0b', 600: '#d97706', 700: '#b45309', 800: '#92400e', 900: '#78350f', 950: '#451a03' }, true),
199
+ acc('emerald', 'Emerald', { 50: '#ecfdf5', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b9', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', 950: '#022c22' }),
200
+ acc('teal', 'Teal', { 50: '#f0fdfa', 100: '#ccfbf1', 200: '#99f6e4', 300: '#5eead4', 400: '#2dd4bf', 500: '#14b8a6', 600: '#0d9488', 700: '#0f766e', 800: '#115e59', 900: '#134e4a', 950: '#042f2e' }),
201
+ acc('cyan', 'Cyan', { 50: '#ecfeff', 100: '#cffafe', 200: '#a5f3fc', 300: '#67e8f9', 400: '#22d3ee', 500: '#06b6d4', 600: '#0891b2', 700: '#0e7490', 800: '#155e75', 900: '#164e63', 950: '#083344' }),
202
+ ]
203
+
204
+ export function accentThemeToVariables(theme, opts) {
205
+ const mode = (opts && opts.mode) || 'light'
206
+ return { ...(mode === 'dark' ? theme.darkVariables : theme.variables) }
207
+ }
208
+
209
+ // ── Radius (the corrected knob) ──────────────────────────────────────
210
+
211
+ /**
212
+ * Emit the FULL derived radius set for a knob value. Container-level
213
+ * overrides of --ak-radius alone do nothing: CSS substitutes the var()
214
+ * chains where they are DEFINED (:root), so descendants inherit resolved
215
+ * values. Spreading this record re-declares the whole chain at the
216
+ * container. Multipliers mirror the SDK theme exactly.
217
+ */
218
+ export function radiusToVariables(value) {
219
+ const rem = typeof value === 'number' ? value : parseFloat(value)
220
+ // PILL MODE (Extra Round): controls go full-pill via CSS's self-clamping
221
+ // 9999px (border-radius larger than the box clamps to a perfect pill),
222
+ // while CONTAINER-tier tokens cap at tasteful roundness so cards and
223
+ // panels never become blobs.
224
+ if (rem >= 62) {
225
+ return {
226
+ '--ak-radius': '9999px',
227
+ '--radius-ak-xs': '0.375rem',
228
+ '--radius-ak-sm': '0.5rem',
229
+ '--radius-ak-md': '9999px',
230
+ '--radius-ak-lg': '1.25rem',
231
+ '--radius-ak-xl': '1.5rem',
232
+ '--radius-ak-2xl': '2rem',
233
+ '--radius-ak-3xl': '2.5rem',
234
+ '--radius-ak-input': '9999px',
235
+ '--radius-ak-control-sm': '0.625rem',
236
+ }
237
+ }
238
+ const step = (m) => `${+(rem * m).toFixed(4)}rem`
239
+ return {
240
+ '--ak-radius': `${rem}rem`,
241
+ '--radius-ak-xs': step(0.25),
242
+ '--radius-ak-sm': step(0.5),
243
+ '--radius-ak-md': step(1),
244
+ '--radius-ak-lg': step(1.5),
245
+ '--radius-ak-xl': step(2),
246
+ '--radius-ak-2xl': step(3),
247
+ '--radius-ak-3xl': step(4),
248
+ '--radius-ak-input': step(1.25),
249
+ '--radius-ak-control-sm': `${Math.min(rem, 0.625)}rem`,
250
+ }
251
+ }
252
+
253
+ // ── Type scales ────────────────────────────────────────────────────
254
+
255
+ export const DEFAULT_TYPE_SCALE_ID = 'default'
256
+
257
+ /**
258
+ * TYPE_SCALES — true modular scales, named by their musical interval ratio.
259
+ * The base and everything below it are PINNED (16px body floor, UI-legible
260
+ * smalls); the ratio drives every step ABOVE the base via fixed exponents,
261
+ * so the dropdown is strictly monotonic: each option renders every heading
262
+ * step larger than the option before it. Unitless leadings adapt free.
263
+ */
264
+ // Default small sizes — they keep these PROPORTIONS to the base and lift
265
+ // with it (multiplying by the same gentle base lift keeps every token
266
+ // monotonic across the dropdown; dividing by the ratio made smalls shrink
267
+ // as scales grew).
268
+ const SMALL_DEFAULTS = {
269
+ '--text-ak-2xs': 0.6875,
270
+ '--text-ak-xs': 0.75,
271
+ '--text-ak-xs_5': 0.8125,
272
+ '--text-ak-sm': 0.875,
273
+ '--text-ak-sm_5': 0.9375,
274
+ }
275
+
276
+ // Exponent of the ratio applied per token above the base (anchored at 16px).
277
+ const STEP_EXPONENTS = {
278
+ '--text-ak-lg': 1, '--text-ak-xl': 1.5, '--text-ak-xl_5': 1.75,
279
+ '--text-ak-2xl': 2, '--text-ak-2xl_5': 2.25, '--text-ak-3xl': 2.75,
280
+ '--text-ak-4xl': 3.25, '--text-ak-4xl_5': 3.5, '--text-ak-5xl': 3.75,
281
+ '--text-ak-6xl': 4.25, '--text-ak-6xl_5': 4.5, '--text-ak-7xl': 4.75,
282
+ '--text-ak-7xl_5': 5, '--text-ak-8xl': 5.25, '--text-ak-9xl': 5.75,
283
+ '--text-ak-10xl': 6,
284
+ }
285
+
286
+ function modularRamp(ratio) {
287
+ // The base itself gets a GENTLE ratio lift (exp 0.25): 16px floor at
288
+ // Default, ~18px at Golden — so body copy, default buttons and links all
289
+ // breathe with the scale instead of only the display steps.
290
+ const lift = Math.pow(ratio, 0.25)
291
+ const out = { '--text-ak-base': `${+lift.toFixed(4)}rem` }
292
+ for (const token of Object.keys(SMALL_DEFAULTS)) {
293
+ out[token] = `${+(SMALL_DEFAULTS[token] * lift).toFixed(4)}rem`
294
+ }
295
+ for (const token of Object.keys(STEP_EXPONENTS)) {
296
+ out[token] = `${+(Math.pow(ratio, STEP_EXPONENTS[token])).toFixed(4)}rem`
297
+ }
298
+ return out
299
+ }
300
+
301
+ export const TYPE_SCALES = [
302
+ {
303
+ id: 'default', name: 'Default', description: 'The authored SDK ramp.',
304
+ variables: {},
305
+ },
306
+ {
307
+ id: 'major-third', name: 'Major Third', description: '1.250 — compact, businesslike hierarchy.',
308
+ variables: modularRamp(1.25),
309
+ },
310
+ {
311
+ id: 'perfect-fourth', name: 'Perfect Fourth', description: '1.333 — the classic editorial workhorse.',
312
+ variables: modularRamp(1.333),
313
+ },
314
+ {
315
+ id: 'augmented-fourth', name: 'Augmented Fourth', description: '1.414 — assertive, modern contrast.',
316
+ variables: modularRamp(1.414),
317
+ },
318
+ {
319
+ id: 'perfect-fifth', name: 'Perfect Fifth', description: '1.500 — bold, display-forward hierarchy.',
320
+ variables: modularRamp(1.5),
321
+ },
322
+ {
323
+ id: 'golden', name: 'Golden Ratio', description: '1.618 — maximum drama for heroes and landings.',
324
+ variables: modularRamp(1.618),
325
+ },
326
+ ]
327
+
328
+ export function typeScaleToVariables(scale) {
329
+ return { ...scale.variables }
330
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Curated font pairings — the AstralKit answer to shadcn's 26-font buffet.
3
+ *
4
+ * A pairing is not two font names: it is a MINIATURE TYPE SYSTEM — per-role
5
+ * family AND weight, balanced by hand. Swapping families without weight
6
+ * compensation always looks wrong (Space Grotesk 500 reads lighter than
7
+ * Inter 500), which is exactly why this is authored, never parametric.
8
+ *
9
+ * Roles (consumed via SDK classes ak-font-heading / -body / -ui / -label):
10
+ * heading — titles, section heads, KPIs
11
+ * body — descriptions, paragraphs, links
12
+ * ui — buttons, inputs, menus, tabs
13
+ * label — overlines, badges, table headers
14
+ * The typical split: the DISPLAY family carries heading + label, the BODY
15
+ * family carries body + ui — but each pairing decides. Links always follow
16
+ * body (a separate link font is almost never good typography).
17
+ */
18
+ export type FontPairingId = 'inter' | 'grotesk' | 'editorial' | 'humanist' | 'soft' | 'terminal';
19
+ type FontVariableName = '--font-ak-sans' | '--font-ak-heading' | '--font-ak-ui' | '--font-ak-label' | '--ak-weight-heading' | '--ak-weight-body' | '--ak-weight-ui' | '--ak-weight-label' | '--ak-tracking-label';
20
+ export interface FontPairing {
21
+ id: FontPairingId;
22
+ name: string;
23
+ description: string;
24
+ variables: Partial<Record<FontVariableName, string>>;
25
+ }
26
+ export declare const DEFAULT_FONT_PAIRING_ID: FontPairingId;
27
+ export declare const FONT_PAIRINGS: FontPairing[];
28
+ export declare function fontPairingToVariables(pairing: FontPairing): Record<string, string>;
29
+ export {};
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Curated font pairings — the AstralKit answer to shadcn's 26-font buffet.
3
+ *
4
+ * A pairing is not two font names: it is a MINIATURE TYPE SYSTEM — per-role
5
+ * family AND weight, balanced by hand. Swapping families without weight
6
+ * compensation always looks wrong (Space Grotesk 500 reads lighter than
7
+ * Inter 500), which is exactly why this is authored, never parametric.
8
+ *
9
+ * Roles (consumed via SDK classes ak-font-heading / -body / -ui / -label):
10
+ * heading — titles, section heads, KPIs
11
+ * body — descriptions, paragraphs, links
12
+ * ui — buttons, inputs, menus, tabs
13
+ * label — overlines, badges, table headers
14
+ * The typical split: the DISPLAY family carries heading + label, the BODY
15
+ * family carries body + ui — but each pairing decides. Links always follow
16
+ * body (a separate link font is almost never good typography).
17
+ */
18
+ export const DEFAULT_FONT_PAIRING_ID = 'inter';
19
+ const SANS = "system-ui, -apple-system, sans-serif";
20
+ const INTER = `'Inter', ${SANS}`;
21
+ const GROTESK = `'Space Grotesk', ${SANS}`;
22
+ const DM = `'DM Sans', ${SANS}`;
23
+ const OUTFIT = `'Outfit', ${SANS}`;
24
+ const PLAYFAIR = `'Playfair Display', Georgia, serif`;
25
+ const JBMONO = `'JetBrains Mono', ui-monospace, monospace`;
26
+ export const FONT_PAIRINGS = [
27
+ {
28
+ id: 'inter',
29
+ name: 'Inter',
30
+ description: 'The house default — one family, weight does the hierarchy.',
31
+ variables: {
32
+ '--font-ak-sans': INTER,
33
+ '--font-ak-heading': INTER,
34
+ '--font-ak-ui': INTER,
35
+ '--font-ak-label': INTER,
36
+ '--ak-weight-heading': '600',
37
+ '--ak-weight-body': '400',
38
+ '--ak-weight-ui': '500',
39
+ '--ak-weight-label': '600',
40
+ },
41
+ },
42
+ {
43
+ id: 'grotesk',
44
+ name: 'Grotesk',
45
+ description: 'Space Grotesk display + UI over Inter body — technical, product-led. Weights bumped to compensate its light curve.',
46
+ variables: {
47
+ '--font-ak-sans': INTER,
48
+ '--font-ak-heading': GROTESK,
49
+ '--font-ak-ui': GROTESK,
50
+ '--font-ak-label': GROTESK,
51
+ '--ak-weight-heading': '700',
52
+ '--ak-weight-body': '400',
53
+ '--ak-weight-ui': '500',
54
+ '--ak-weight-label': '500',
55
+ '--ak-tracking-label': '0.04em',
56
+ },
57
+ },
58
+ {
59
+ id: 'editorial',
60
+ name: 'Editorial',
61
+ description: 'Playfair Display headlines over DM Sans everything else — magazine warmth.',
62
+ variables: {
63
+ '--font-ak-sans': DM,
64
+ '--font-ak-heading': PLAYFAIR,
65
+ '--font-ak-ui': DM,
66
+ '--font-ak-label': DM,
67
+ '--ak-weight-heading': '600',
68
+ '--ak-weight-body': '400',
69
+ '--ak-weight-ui': '500',
70
+ '--ak-weight-label': '700',
71
+ '--ak-tracking-label': '0.06em',
72
+ },
73
+ },
74
+ {
75
+ id: 'humanist',
76
+ name: 'Humanist',
77
+ description: 'DM Sans throughout — rounded and friendly; headings carry extra weight.',
78
+ variables: {
79
+ '--font-ak-sans': DM,
80
+ '--font-ak-heading': DM,
81
+ '--font-ak-ui': DM,
82
+ '--font-ak-label': DM,
83
+ '--ak-weight-heading': '700',
84
+ '--ak-weight-body': '400',
85
+ '--ak-weight-ui': '500',
86
+ '--ak-weight-label': '600',
87
+ },
88
+ },
89
+ {
90
+ id: 'soft',
91
+ name: 'Soft Display',
92
+ description: 'Outfit throughout — geometric softness for consumer products.',
93
+ variables: {
94
+ '--font-ak-sans': OUTFIT,
95
+ '--font-ak-heading': OUTFIT,
96
+ '--font-ak-ui': OUTFIT,
97
+ '--font-ak-label': OUTFIT,
98
+ '--ak-weight-heading': '600',
99
+ '--ak-weight-body': '400',
100
+ '--ak-weight-ui': '500',
101
+ '--ak-weight-label': '600',
102
+ },
103
+ },
104
+ {
105
+ id: 'terminal',
106
+ name: 'Terminal',
107
+ description: 'JetBrains Mono display + labels over Inter body and UI — dev-tool energy.',
108
+ variables: {
109
+ '--font-ak-sans': INTER,
110
+ '--font-ak-heading': JBMONO,
111
+ '--font-ak-ui': INTER,
112
+ '--font-ak-label': JBMONO,
113
+ '--ak-weight-heading': '600',
114
+ '--ak-weight-body': '400',
115
+ '--ak-weight-ui': '500',
116
+ '--ak-weight-label': '500',
117
+ '--ak-tracking-label': '0',
118
+ },
119
+ },
120
+ ];
121
+ export function fontPairingToVariables(pairing) {
122
+ return { ...pairing.variables };
123
+ }
@@ -0,0 +1,16 @@
1
+ export type FontGroup = 'sans' | 'mono' | 'serif'
2
+
3
+ export interface LibraryFont {
4
+ id: string
5
+ name: string
6
+ group: FontGroup
7
+ minWeight: number
8
+ maxWeight: number
9
+ stack: string
10
+ }
11
+
12
+ export declare const FONT_LIBRARY: LibraryFont[]
13
+ export declare const FONT_GROUPS: FontGroup[]
14
+ export declare function getFont(id: string): LibraryFont | undefined
15
+ export declare function fontWeights(font: LibraryFont): number[]
16
+ export declare function clampWeight(font: LibraryFont, weight: number): number
@@ -0,0 +1,76 @@
1
+ /**
2
+ * astralkit/presets — the font library.
3
+ *
4
+ * The full shadcn font roster (all 26), grouped sans / mono / serif exactly
5
+ * as their pickers group them, plus per-font weight ranges so a weight
6
+ * dropdown can offer only weights the font actually ships — the control
7
+ * shadcn does not have.
8
+ */
9
+
10
+ const SANS_FALLBACK = 'system-ui, -apple-system, sans-serif'
11
+ const SERIF_FALLBACK = 'Georgia, serif'
12
+ const MONO_FALLBACK = 'ui-monospace, monospace'
13
+
14
+ const f = (id, name, group, minWeight, maxWeight, fallback) => ({
15
+ id,
16
+ name,
17
+ group,
18
+ minWeight,
19
+ maxWeight,
20
+ stack: `'${name}', ${fallback}`,
21
+ })
22
+
23
+ export const FONT_LIBRARY = [
24
+ // Sans
25
+ f('geist', 'Geist', 'sans', 100, 900, SANS_FALLBACK),
26
+ f('inter', 'Inter', 'sans', 100, 900, SANS_FALLBACK),
27
+ f('noto-sans', 'Noto Sans', 'sans', 100, 900, SANS_FALLBACK),
28
+ f('nunito-sans', 'Nunito Sans', 'sans', 200, 900, SANS_FALLBACK),
29
+ f('figtree', 'Figtree', 'sans', 300, 900, SANS_FALLBACK),
30
+ f('roboto', 'Roboto', 'sans', 100, 900, SANS_FALLBACK),
31
+ f('raleway', 'Raleway', 'sans', 100, 900, SANS_FALLBACK),
32
+ f('dm-sans', 'DM Sans', 'sans', 100, 900, SANS_FALLBACK),
33
+ f('public-sans', 'Public Sans', 'sans', 100, 900, SANS_FALLBACK),
34
+ f('outfit', 'Outfit', 'sans', 100, 900, SANS_FALLBACK),
35
+ f('oxanium', 'Oxanium', 'sans', 200, 800, SANS_FALLBACK),
36
+ f('manrope', 'Manrope', 'sans', 200, 800, SANS_FALLBACK),
37
+ f('space-grotesk', 'Space Grotesk', 'sans', 300, 700, SANS_FALLBACK),
38
+ f('montserrat', 'Montserrat', 'sans', 100, 900, SANS_FALLBACK),
39
+ f('ibm-plex-sans', 'IBM Plex Sans', 'sans', 300, 700, SANS_FALLBACK),
40
+ f('source-sans-3', 'Source Sans 3', 'sans', 200, 900, SANS_FALLBACK),
41
+ f('instrument-sans', 'Instrument Sans', 'sans', 400, 700, SANS_FALLBACK),
42
+ // Mono
43
+ f('geist-mono', 'Geist Mono', 'mono', 100, 900, MONO_FALLBACK),
44
+ f('jetbrains-mono', 'JetBrains Mono', 'mono', 100, 800, MONO_FALLBACK),
45
+ // Serif
46
+ f('noto-serif', 'Noto Serif', 'serif', 100, 900, SERIF_FALLBACK),
47
+ f('roboto-slab', 'Roboto Slab', 'serif', 100, 900, SERIF_FALLBACK),
48
+ f('merriweather', 'Merriweather', 'serif', 300, 900, SERIF_FALLBACK),
49
+ f('lora', 'Lora', 'serif', 400, 700, SERIF_FALLBACK),
50
+ f('playfair-display', 'Playfair Display', 'serif', 400, 900, SERIF_FALLBACK),
51
+ f('eb-garamond', 'EB Garamond', 'serif', 400, 800, SERIF_FALLBACK),
52
+ f('instrument-serif', 'Instrument Serif', 'serif', 400, 400, SERIF_FALLBACK),
53
+ ]
54
+
55
+ export const FONT_GROUPS = ['sans', 'mono', 'serif']
56
+
57
+ export function getFont(id) {
58
+ return FONT_LIBRARY.find((font) => font.id === id)
59
+ }
60
+
61
+ /** Weights the font actually ships, in 100 steps, clamped to the UI-sane 300–900 band
62
+ * (fonts whose whole range sits outside keep their own bounds). */
63
+ export function fontWeights(font) {
64
+ const lo = Math.max(font.minWeight, 300)
65
+ const hi = Math.min(font.maxWeight, 900)
66
+ if (lo > hi) return [font.minWeight]
67
+ const out = []
68
+ for (let w = lo; w <= hi; w += 100) out.push(w)
69
+ return out
70
+ }
71
+
72
+ /** Nearest available weight when switching fonts keeps the user's intent. */
73
+ export function clampWeight(font, weight) {
74
+ const weights = fontWeights(font)
75
+ return weights.reduce((best, w) => (Math.abs(w - weight) < Math.abs(best - weight) ? w : best), weights[0])
76
+ }
@@ -0,0 +1,7 @@
1
+ export * from './theme-palettes'
2
+ export * from './appearance-presets'
3
+ export * from './font-pairings'
4
+ export * from './chart-palettes'
5
+ export * from './dials'
6
+ export * from './fonts'
7
+ export * from './materials'
@@ -0,0 +1,24 @@
1
+ /**
2
+ * astralkit/presets — the single source of truth for every theming dial.
3
+ *
4
+ * One data model serves them all: the demo showcase bar, the Theme Builder,
5
+ * and (later) the MCP's list_themes / apply_theme. Every entry is
6
+ * { id, name, description, variables } where `variables` is a flat record of
7
+ * CSS custom properties — applying a preset is spreading those onto any
8
+ * container's style. Nothing here imports React or the DOM.
9
+ *
10
+ * Dials:
11
+ * THEME_PALETTES — 16 authored color palettes (+ dark handling)
12
+ * FONT_PAIRINGS — miniature type systems (per-role family + weight)
13
+ * CHART_PALETTES — authored 6-color series sets
14
+ * RADIUS_OPTIONS — the one-knob radius system's named stops
15
+ * APPEARANCE_PRESETS — atmosphere layers (mesh, grain, neon, backlight)
16
+ */
17
+
18
+ export * from './theme-palettes.js'
19
+ export * from './appearance-presets.js'
20
+ export * from './font-pairings.js'
21
+ export * from './chart-palettes.js'
22
+ export * from './dials.js'
23
+ export * from './fonts.js'
24
+ export * from './materials.js'
@@ -0,0 +1,15 @@
1
+ export type MaterialId = 'soft' | 'flat' | 'glass' | 'brutal'
2
+
3
+ export interface MaterialAppearance {
4
+ id: MaterialId
5
+ name: string
6
+ description: string
7
+ /** Suggested radius, applied only while the Radius dial is on Default. */
8
+ radius: string | null
9
+ variables: Record<string, string>
10
+ darkVariables: Record<string, string>
11
+ }
12
+
13
+ export declare const DEFAULT_MATERIAL_ID: MaterialId
14
+ export declare const MATERIAL_APPEARANCES: MaterialAppearance[]
15
+ export declare function materialToVariables(material: MaterialAppearance, opts?: { mode?: 'light' | 'dark' }): Record<string, string>