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,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,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;
|