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.
@@ -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>
@@ -0,0 +1,245 @@
1
+ /**
2
+ * astralkit/presets — material appearances.
3
+ *
4
+ * An APPEARANCE is a design language: the physical properties of the UI —
5
+ * surface fill/blur, border weight, shadow character, press behavior, type
6
+ * weight — expressed as a pack of --ak-material slots. Components consume
7
+ * the slots via the SDK recipes (.ak-material-control / .ak-material-surface
8
+ * / .ak-press); switching packs re-materializes a RUNNING app.
9
+ *
10
+ * Composability is the point: Appearance × Palette × Radius × Type Scale,
11
+ * all runtime. shadcn's styles are frozen skins baked at scaffold time —
12
+ * this dial is the thing that architecture cannot do.
13
+ *
14
+ * Each pack: light `variables`, authored `darkVariables`, and a suggested
15
+ * `radius` (applied only while the Radius dial sits on Default — an explicit
16
+ * radius pick always wins).
17
+ */
18
+
19
+ export const DEFAULT_MATERIAL_ID = 'soft'
20
+
21
+ const INK = '#0a0a0a'
22
+ const PAPER = '#f4f4f5'
23
+
24
+ export const MATERIAL_APPEARANCES = [
25
+ {
26
+ id: 'soft',
27
+ name: 'Soft',
28
+ description: 'The house default — quiet borders, calm surfaces.',
29
+ radius: null,
30
+ variables: {},
31
+ darkVariables: {},
32
+ },
33
+ {
34
+ id: 'flat',
35
+ name: 'Flat',
36
+ description: 'Zero elevation, crisp hairlines, sharp corners.',
37
+ radius: '0.375rem',
38
+ variables: {
39
+ '--ak-control-shadow': 'none',
40
+ '--ak-surface-shadow': 'none',
41
+ '--ak-surface-border-color': 'var(--color-ak-border-strong)',
42
+ '--ak-press-transform': 'scale(0.99)',
43
+ // Zero elevation extends to the small parts: crisp bordered thumb,
44
+ // shadowless knob, flat tracks, no hover theatrics.
45
+ '--ak-knob-shadow': 'none',
46
+ '--ak-thumb-shadow': 'none',
47
+ '--ak-thumb-border-w': '1px',
48
+ '--ak-thumb-border-color': 'var(--color-ak-border-strong)',
49
+ '--ak-thumb-hover-transform': 'none',
50
+ '--ak-track-shadow': '0 0 0 0 transparent',
51
+ '--ak-effect-mesh-opacity': '0',
52
+ '--ak-effect-grain-opacity': '0',
53
+ '--ak-effect-backlight-opacity': '0',
54
+ },
55
+ darkVariables: {
56
+ '--ak-control-shadow': 'none',
57
+ '--ak-surface-shadow': 'none',
58
+ '--ak-surface-border-color': 'rgba(255,255,255,0.22)',
59
+ '--ak-press-transform': 'scale(0.99)',
60
+ '--ak-knob-shadow': 'none',
61
+ '--ak-thumb-shadow': 'none',
62
+ '--ak-thumb-border-w': '1px',
63
+ '--ak-thumb-border-color': 'rgba(255,255,255,0.22)',
64
+ '--ak-thumb-hover-transform': 'none',
65
+ '--ak-track-shadow': '0 0 0 0 transparent',
66
+ '--ak-effect-mesh-opacity': '0',
67
+ '--ak-effect-grain-opacity': '0',
68
+ '--ak-effect-backlight-opacity': '0',
69
+ },
70
+ },
71
+ {
72
+ id: 'glass',
73
+ name: 'Glassmorphic',
74
+ description: 'White glass — one top-left light source; sheen, rim and speculars all agree on it.',
75
+ radius: '0.875rem',
76
+ variables: {
77
+ // Fred's Glass v2 reference: sheen falls off from the light (125deg),
78
+ // rim burns hot near it and dies opposite, specular inset on the lit
79
+ // edge + faint bounce on the shadow side. Standard 18px thickness.
80
+ '--ak-surface-fill': 'rgba(255,255,255,0.11)',
81
+ '--ak-surface-fill-2': 'rgba(255,255,255,0.18)',
82
+ '--ak-surface-sheen': 'linear-gradient(125deg, rgba(255,255,255,0.20), rgba(255,255,255,0.04) 38%, rgba(255,255,255,0) 62%)',
83
+ '--ak-surface-blur': 'blur(18px) saturate(1.6)',
84
+ '--ak-surface-border-w': '0px',
85
+ '--ak-surface-rim': 'linear-gradient(125deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.25) 30%, rgba(255,255,255,0.08) 55%, rgba(255,255,255,0.18) 100%)',
86
+ '--ak-surface-shadow': '0 24px 48px -16px rgba(0,0,0,0.28), inset 1px 1px 1px rgba(255,255,255,0.45), inset -1px -1px 1px rgba(255,255,255,0.08)',
87
+ '--ak-surface-shadow-2': '0 10px 22px -10px rgba(0,0,0,0.28), inset 1px 1px 1px rgba(255,255,255,0.4)',
88
+ '--ak-control-shadow': '0 4px 14px rgba(15,15,20,0.10)',
89
+ '--ak-press-transform': 'scale(0.97)',
90
+ // Law 4: glass is for surfaces — small-control active parts stay FLAT
91
+ // SOLID. Knob/thumb = plain white with a real drop; slider track = a
92
+ // carved groove (dark inset + light bottom edge). Spring per the ref.
93
+ '--ak-knob-fill': '#ffffff',
94
+ '--ak-knob-fill-checked': '#ffffff',
95
+ '--ak-knob-shadow': '0 2px 5px rgba(0,0,0,0.4)',
96
+ '--ak-control-spring': 'cubic-bezier(0.34, 1.4, 0.5, 1)',
97
+ '--ak-thumb-fill': '#ffffff',
98
+ '--ak-thumb-shadow': '0 2px 5px rgba(0,0,0,0.4)',
99
+ '--ak-thumb-hover-transform': 'scale(1.1)',
100
+ '--ak-thumb-active-transform': 'scale(0.96)',
101
+ '--ak-track-shadow': 'inset 0 2px 3px rgba(0,0,0,0.35), inset 0 -1px 1px rgba(255,255,255,0.25)',
102
+ '--ak-effect-mesh-opacity': '0.18',
103
+ '--ak-effect-grain-opacity': '0.015',
104
+ '--ak-effect-backlight-opacity': '0.08',
105
+ },
106
+ darkVariables: {
107
+ // Smoked glass — dark material, locks in text contrast regardless of
108
+ // the backdrop. Dimmer sheen and rim, calmer saturation.
109
+ '--ak-surface-fill': 'rgba(16,17,22,0.45)',
110
+ '--ak-surface-fill-2': 'rgba(22,24,30,0.55)',
111
+ '--ak-surface-sheen': 'linear-gradient(125deg, rgba(255,255,255,0.09), rgba(255,255,255,0.02) 38%, rgba(255,255,255,0) 62%)',
112
+ '--ak-surface-blur': 'blur(18px) saturate(1.2)',
113
+ '--ak-surface-border-w': '0px',
114
+ '--ak-surface-rim': 'linear-gradient(125deg, rgba(255,255,255,0.35) 0%, rgba(255,255,255,0.12) 30%, rgba(255,255,255,0.04) 55%, rgba(255,255,255,0.10) 100%)',
115
+ '--ak-surface-shadow': '0 24px 48px -16px rgba(0,0,0,0.5), inset 1px 1px 1px rgba(255,255,255,0.18), inset -1px -1px 1px rgba(255,255,255,0.04)',
116
+ '--ak-surface-shadow-2': '0 10px 22px -10px rgba(0,0,0,0.5), inset 1px 1px 1px rgba(255,255,255,0.14)',
117
+ '--ak-control-shadow': '0 4px 14px rgba(0,0,0,0.35)',
118
+ '--ak-press-transform': 'scale(0.97)',
119
+ '--ak-knob-fill': '#ffffff',
120
+ '--ak-knob-fill-checked': '#ffffff',
121
+ '--ak-knob-shadow': '0 2px 5px rgba(0,0,0,0.55)',
122
+ '--ak-control-spring': 'cubic-bezier(0.34, 1.4, 0.5, 1)',
123
+ '--ak-thumb-fill': '#ffffff',
124
+ '--ak-thumb-shadow': '0 2px 5px rgba(0,0,0,0.55)',
125
+ '--ak-thumb-hover-transform': 'scale(1.1)',
126
+ '--ak-thumb-active-transform': 'scale(0.96)',
127
+ '--ak-track-shadow': 'inset 0 2px 3px rgba(0,0,0,0.5), inset 0 -1px 1px rgba(255,255,255,0.12)',
128
+ '--ak-effect-mesh-opacity': '0.14',
129
+ '--ak-effect-grain-opacity': '0.015',
130
+ '--ak-effect-backlight-opacity': '0.10',
131
+ },
132
+ },
133
+ {
134
+ id: 'brutal',
135
+ name: 'Neo-Brutal',
136
+ description: 'The house brutalism — ink borders, hard offsets, the push-into-shadow press.',
137
+ // House brutal keeps FRIENDLY corners (--radius-ak-brutal = 0.5rem) —
138
+ // hard geometry, soft radius. That contrast IS the AstralKit brutal look.
139
+ radius: '0.5rem',
140
+ variables: {
141
+ // The authored ak-brutal token ladder (ink #000, 4/3/6px offsets) and
142
+ // the three-act ak-brutal-press choreography, expressed as slots:
143
+ // rest 4px → hover: slide 2px into a 3px shadow → active: fully
144
+ // pressed, shadow collapses to zero.
145
+ // Control shadows use SOFTENED ink (mixed toward the page color) so a
146
+ // fill that matches the ink — the B&W palette's primary — still reads
147
+ // against its shadow (Fred's call: tone separation, not rings). Card
148
+ // shadows stay pure ink; faces are always lighter there.
149
+ '--ak-control-shadow': '4px 4px 0 0 color-mix(in srgb, #000000 72%, var(--color-ak-bg))',
150
+ '--ak-control-hover-transform': 'translate(2px, 2px)',
151
+ '--ak-control-shadow-hover': '3px 3px 0 0 color-mix(in srgb, #000000 72%, var(--color-ak-bg))',
152
+ '--ak-press-transform': 'translate(4px, 4px)',
153
+ '--ak-control-shadow-active': '0 0 0 0 #000000',
154
+ '--ak-surface-border-w': 'var(--border-ak-brutal)',
155
+ '--ak-surface-border-color': '#000000',
156
+ '--ak-surface-shadow': '6px 6px 0 0 #000000',
157
+ '--ak-weight-ui': '700',
158
+ '--ak-weight-ui-strong': '800',
159
+ '--ak-weight-heading': '800',
160
+ '--ak-weight-label': '700',
161
+ '--ak-tracking-label': '0.05em',
162
+ // Quiet hover = the brutal backface, bottom edge only: a THICK ink
163
+ // underline pushed away from the text (not a hairline text-decoration).
164
+ '--ak-quiet-hover-line': 'underline',
165
+ '--ak-quiet-hover-thickness': '3px',
166
+ '--ak-quiet-hover-offset': '2px',
167
+ '--ak-quiet-hover-bg': 'transparent',
168
+ '--ak-quiet-hover-color': '#000000',
169
+ // Brutal focus is dashed ink; nested brutal surfaces go FLAT (border
170
+ // only) — shadow lives at the top level, per the reference doctrine.
171
+ '--ak-focus-outline': '3px dashed #000000',
172
+ '--ak-focus-offset': '4px',
173
+ '--ak-surface-shadow-2': 'none',
174
+ // Batch-2 control slots — the squish is the juice: solid ink knob that
175
+ // stretches on press, square-ish thumb that tilts on hover, loud
176
+ // bordered tracks. Thumb shadow uses the same softened ink as controls.
177
+ '--ak-knob-fill': '#000000',
178
+ '--ak-knob-fill-checked': 'var(--color-ak-bg)',
179
+ '--ak-knob-shadow': 'none',
180
+ '--ak-knob-press': 'scaleX(1.25)',
181
+ '--ak-control-spring': 'cubic-bezier(0.6, 1.6, 0.4, 1)',
182
+ '--ak-thumb-fill': 'var(--color-ak-bg)',
183
+ '--ak-thumb-radius': '8px',
184
+ '--ak-thumb-border-w': '3px',
185
+ '--ak-thumb-border-color': '#000000',
186
+ '--ak-thumb-shadow': '2px 2px 0 0 color-mix(in srgb, #000000 72%, var(--color-ak-bg))',
187
+ '--ak-thumb-hover-transform': 'rotate(8deg) scale(1.08)',
188
+ '--ak-thumb-active-transform': 'scale(0.94)',
189
+ '--ak-track-shadow': '0 0 0 0 transparent',
190
+ '--ak-track-border-w': '2px',
191
+ '--ak-track-border-color': '#000000',
192
+ '--ak-effect-mesh-opacity': '0',
193
+ '--ak-effect-grain-opacity': '0',
194
+ '--ak-effect-backlight-opacity': '0',
195
+ },
196
+ darkVariables: {
197
+ // Explicit paper values — the :root substitution rule means overriding
198
+ // --color-ak-brutal-ink here would never reach the shadow tokens.
199
+ '--ak-control-shadow': '4px 4px 0 0 color-mix(in srgb, #f4f4f5 68%, var(--color-ak-bg))',
200
+ '--ak-control-hover-transform': 'translate(2px, 2px)',
201
+ '--ak-control-shadow-hover': '3px 3px 0 0 color-mix(in srgb, #f4f4f5 68%, var(--color-ak-bg))',
202
+ '--ak-press-transform': 'translate(4px, 4px)',
203
+ '--ak-control-shadow-active': '0 0 0 0 #f4f4f5',
204
+ '--ak-surface-border-w': 'var(--border-ak-brutal)',
205
+ '--ak-surface-border-color': '#f4f4f5',
206
+ '--ak-surface-shadow': '6px 6px 0 0 #f4f4f5',
207
+ '--ak-weight-ui': '700',
208
+ '--ak-weight-ui-strong': '800',
209
+ '--ak-weight-heading': '800',
210
+ '--ak-weight-label': '700',
211
+ '--ak-tracking-label': '0.05em',
212
+ '--ak-quiet-hover-line': 'underline',
213
+ '--ak-quiet-hover-thickness': '3px',
214
+ '--ak-quiet-hover-offset': '2px',
215
+ '--ak-quiet-hover-bg': 'transparent',
216
+ '--ak-quiet-hover-color': '#f4f4f5',
217
+ '--ak-focus-outline': '3px dashed #f4f4f5',
218
+ '--ak-focus-offset': '4px',
219
+ '--ak-surface-shadow-2': 'none',
220
+ '--ak-knob-fill': '#f4f4f5',
221
+ '--ak-knob-fill-checked': 'var(--color-ak-bg)',
222
+ '--ak-knob-shadow': 'none',
223
+ '--ak-knob-press': 'scaleX(1.25)',
224
+ '--ak-control-spring': 'cubic-bezier(0.6, 1.6, 0.4, 1)',
225
+ '--ak-thumb-fill': 'var(--color-ak-bg)',
226
+ '--ak-thumb-radius': '8px',
227
+ '--ak-thumb-border-w': '3px',
228
+ '--ak-thumb-border-color': '#f4f4f5',
229
+ '--ak-thumb-shadow': '2px 2px 0 0 color-mix(in srgb, #f4f4f5 68%, var(--color-ak-bg))',
230
+ '--ak-thumb-hover-transform': 'rotate(8deg) scale(1.08)',
231
+ '--ak-thumb-active-transform': 'scale(0.94)',
232
+ '--ak-track-shadow': '0 0 0 0 transparent',
233
+ '--ak-track-border-w': '2px',
234
+ '--ak-track-border-color': '#f4f4f5',
235
+ '--ak-effect-mesh-opacity': '0',
236
+ '--ak-effect-grain-opacity': '0',
237
+ '--ak-effect-backlight-opacity': '0',
238
+ },
239
+ },
240
+ ]
241
+
242
+ export function materialToVariables(material, opts) {
243
+ const mode = (opts && opts.mode) || 'light'
244
+ return { ...(mode === 'dark' ? material.darkVariables : material.variables) }
245
+ }
@@ -0,0 +1,90 @@
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
+ export interface ThemePalette {
23
+ id: string;
24
+ name: string;
25
+ description: string;
26
+ mode: 'light' | 'dark';
27
+ colors: {
28
+ background: string;
29
+ surface: string;
30
+ foreground: string;
31
+ foregroundMuted: string;
32
+ primary: string;
33
+ secondary: string;
34
+ accent: string;
35
+ /** Optional text color for filled accent elements. Defaults to computed contrast. */
36
+ onAccent?: string;
37
+ border: string;
38
+ ring: string;
39
+ muted: string;
40
+ destructive: string;
41
+ /** Optional: slider/scrubber filled-track color. Defaults to primary when omitted. */
42
+ sliderFill?: string;
43
+ /** Optional: slider thumb ring color. Defaults to paper (bg) when omitted. */
44
+ sliderRing?: string;
45
+ /** Optional: branded CTA gradient stops. Defaults are derived from primary. */
46
+ brandGradient?: {
47
+ from: string;
48
+ via: string;
49
+ to: string;
50
+ hoverFrom?: string;
51
+ hoverVia?: string;
52
+ hoverTo?: string;
53
+ };
54
+ };
55
+ effects?: {
56
+ glowPrimary?: string;
57
+ glowSecondary?: string;
58
+ glowTertiary?: string;
59
+ glowWarm?: string;
60
+ meshBase?: string;
61
+ };
62
+ }
63
+ export type ThemeMode = ThemePalette['mode'];
64
+ export declare const THEME_PALETTES: ThemePalette[];
65
+ /**
66
+ * Keeps the chosen brand colors while swapping surface, text, border, and muted
67
+ * scaffolding to the current UI mode.
68
+ */
69
+ export declare function resolvePaletteForMode(palette: ThemePalette, mode: ThemeMode): ThemePalette;
70
+ /**
71
+ * Maps a ThemePalette to the actual --color-ak-* CSS variables
72
+ * that AstralKit components consume.
73
+ *
74
+ * This is the critical bridge between our 60-30-10 palette
75
+ * and the component design token system.
76
+ */
77
+ export declare function paletteToAkVariables(palette: ThemePalette, options?: {
78
+ mode?: ThemeMode;
79
+ }): Record<string, string>;
80
+ /**
81
+ * Palette authorship lint — encodes the recipe learned from curation:
82
+ * keepers share a near-neutral foreground, neutral light surfaces, and a
83
+ * mid-luminance primary (or an explicit accent split for light primaries).
84
+ * Returns human-readable warnings; empty array = palette is well-formed.
85
+ */
86
+ export declare function validatePalette(palette: ThemePalette): string[];
87
+ /**
88
+ * Generates CSS :root block from a palette using the ak variable names.
89
+ */
90
+ export declare function paletteToCSS(palette: ThemePalette): string;