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,102 @@
1
+ export const DEFAULT_APPEARANCE_PRESET_ID = 'clean';
2
+ export const APPEARANCE_PRESETS = [
3
+ {
4
+ id: 'clean',
5
+ name: 'Clean',
6
+ description: 'No ambient atmosphere. Keeps components border-led and quiet.',
7
+ variables: {
8
+ '--ak-effect-mesh-opacity': '0',
9
+ '--ak-effect-grain-opacity': '0',
10
+ '--ak-effect-grid-opacity': '0',
11
+ '--ak-effect-backlight-opacity': '0',
12
+ '--ak-effect-vignette-opacity': '0',
13
+ '--ak-effect-border-opacity': '0.55',
14
+ '--ak-effect-glow-opacity': '0.22',
15
+ '--ak-effect-border-speed': '8s',
16
+ '--ak-effect-spark-size': '34%',
17
+ },
18
+ },
19
+ {
20
+ id: 'soft-mesh',
21
+ name: 'Soft Mesh',
22
+ description: 'A calm radial mesh with light grain for premium product surfaces.',
23
+ variables: {
24
+ '--ak-effect-mesh-opacity': '0.18',
25
+ '--ak-effect-grain-opacity': '0.018',
26
+ '--ak-effect-grid-opacity': '0',
27
+ '--ak-effect-backlight-opacity': '0.08',
28
+ '--ak-effect-vignette-opacity': '0.035',
29
+ '--ak-effect-border-opacity': '0.62',
30
+ '--ak-effect-glow-opacity': '0.32',
31
+ '--ak-effect-border-speed': '9s',
32
+ '--ak-effect-spark-size': '32%',
33
+ },
34
+ },
35
+ {
36
+ id: 'neon-edge',
37
+ name: 'Neon Edge',
38
+ description: 'Restrained glow and animated edge energy for dark premium demos.',
39
+ variables: {
40
+ '--ak-effect-mesh-opacity': '0.12',
41
+ '--ak-effect-grain-opacity': '0.014',
42
+ '--ak-effect-grid-opacity': '0',
43
+ '--ak-effect-backlight-opacity': '0.14',
44
+ '--ak-effect-vignette-opacity': '0.06',
45
+ '--ak-effect-border-opacity': '0.92',
46
+ '--ak-effect-glow-opacity': '0.44',
47
+ '--ak-effect-border-speed': '6s',
48
+ '--ak-effect-spark-size': '24%',
49
+ },
50
+ },
51
+ {
52
+ id: 'cinematic-backlight',
53
+ name: 'Cinematic',
54
+ description: 'Large soft backlights and a faint vignette for hero-style staging.',
55
+ variables: {
56
+ '--ak-effect-mesh-opacity': '0.24',
57
+ '--ak-effect-grain-opacity': '0.022',
58
+ '--ak-effect-grid-opacity': '0',
59
+ '--ak-effect-backlight-opacity': '0.28',
60
+ '--ak-effect-vignette-opacity': '0.12',
61
+ '--ak-effect-border-opacity': '0.7',
62
+ '--ak-effect-glow-opacity': '0.5',
63
+ '--ak-effect-border-speed': '10s',
64
+ '--ak-effect-spark-size': '36%',
65
+ },
66
+ },
67
+ {
68
+ id: 'circuit-spark',
69
+ name: 'Circuit Spark',
70
+ description: 'Fine grid texture with faster comet borders for systems UI.',
71
+ variables: {
72
+ '--ak-effect-mesh-opacity': '0.15',
73
+ '--ak-effect-grain-opacity': '0.016',
74
+ '--ak-effect-grid-opacity': '0.12',
75
+ '--ak-effect-backlight-opacity': '0.16',
76
+ '--ak-effect-vignette-opacity': '0.08',
77
+ '--ak-effect-border-opacity': '1',
78
+ '--ak-effect-glow-opacity': '0.48',
79
+ '--ak-effect-border-speed': '4.8s',
80
+ '--ak-effect-spark-size': '18%',
81
+ },
82
+ },
83
+ {
84
+ id: 'portfolio-grain',
85
+ name: 'Portfolio Grain',
86
+ description: 'Editorial grain, muted backlight, and quiet texture.',
87
+ variables: {
88
+ '--ak-effect-mesh-opacity': '0.1',
89
+ '--ak-effect-grain-opacity': '0.055',
90
+ '--ak-effect-grid-opacity': '0',
91
+ '--ak-effect-backlight-opacity': '0.07',
92
+ '--ak-effect-vignette-opacity': '0.1',
93
+ '--ak-effect-border-opacity': '0.5',
94
+ '--ak-effect-glow-opacity': '0.28',
95
+ '--ak-effect-border-speed': '12s',
96
+ '--ak-effect-spark-size': '40%',
97
+ },
98
+ },
99
+ ];
100
+ export function appearancePresetToVariables(preset) {
101
+ return preset.variables;
102
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Curated chart palettes — authored 6-color series sets driving the SDK's
3
+ * --color-ak-chart-1..6 slots at runtime. The default (Signal) is the SDK's
4
+ * Tableau-derived colorblind-safe set; the others are authored alternatives,
5
+ * sequenced so adjacent series stay distinguishable. Independent of the UI
6
+ * palette on purpose (the shadcn insight worth keeping: chart hues are a
7
+ * separate design decision from brand hues).
8
+ */
9
+ export type ChartPaletteId = 'signal' | 'oceanic' | 'ember' | 'prism' | 'ink';
10
+ type ChartVariableName = `--color-ak-chart-${1 | 2 | 3 | 4 | 5 | 6}`;
11
+ export interface ChartPalette {
12
+ id: ChartPaletteId;
13
+ name: string;
14
+ description: string;
15
+ variables: Record<ChartVariableName, string>;
16
+ darkVariables?: Record<ChartVariableName, string>;
17
+ }
18
+ export declare const DEFAULT_CHART_PALETTE_ID: ChartPaletteId;
19
+ export declare const CHART_PALETTES: ChartPalette[];
20
+ export declare function chartPaletteToVariables(palette: ChartPalette, opts?: { mode?: 'light' | 'dark' }): Record<string, string>;
21
+ export type RadiusOptionId = 'none' | 'small' | 'default' | 'large' | 'xl';
22
+ export interface RadiusOption {
23
+ id: RadiusOptionId;
24
+ name: string;
25
+ value: string;
26
+ }
27
+ export declare const DEFAULT_RADIUS_OPTION_ID: RadiusOptionId;
28
+ export declare const RADIUS_OPTIONS: RadiusOption[];
29
+ export {};
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Curated chart palettes — authored 6-color series sets driving the SDK's
3
+ * --color-ak-chart-1..6 slots at runtime. The default (Signal) is the SDK's
4
+ * Tableau-derived colorblind-safe set; the others are authored alternatives,
5
+ * sequenced so adjacent series stay distinguishable. Independent of the UI
6
+ * palette on purpose (the shadcn insight worth keeping: chart hues are a
7
+ * separate design decision from brand hues).
8
+ */
9
+ export const DEFAULT_CHART_PALETTE_ID = 'signal';
10
+ export const CHART_PALETTES = [
11
+ {
12
+ id: 'signal',
13
+ name: 'Default',
14
+ description: 'The SDK default — Tableau-derived, colorblind-safe.',
15
+ variables: {
16
+ '--color-ak-chart-1': '#4e79a7',
17
+ '--color-ak-chart-2': '#f28e2c',
18
+ '--color-ak-chart-3': '#e15759',
19
+ '--color-ak-chart-4': '#76b7b2',
20
+ '--color-ak-chart-5': '#59a14f',
21
+ '--color-ak-chart-6': '#edc949',
22
+ },
23
+ },
24
+ {
25
+ id: 'oceanic',
26
+ name: 'Blues',
27
+ description: 'Blues and teals — calm, financial, product dashboards.',
28
+ variables: {
29
+ '--color-ak-chart-1': '#2563eb',
30
+ '--color-ak-chart-2': '#0891b2',
31
+ '--color-ak-chart-3': '#14b8a6',
32
+ '--color-ak-chart-4': '#6366f1',
33
+ '--color-ak-chart-5': '#0ea5e9',
34
+ '--color-ak-chart-6': '#1e40af',
35
+ },
36
+ },
37
+ {
38
+ id: 'ember',
39
+ name: 'Warm',
40
+ description: 'Warm ambers and reds — energy, alerts, intensity scales.',
41
+ variables: {
42
+ '--color-ak-chart-1': '#dc2626',
43
+ '--color-ak-chart-2': '#f59e0b',
44
+ '--color-ak-chart-3': '#ea580c',
45
+ '--color-ak-chart-4': '#eab308',
46
+ '--color-ak-chart-5': '#9a3412',
47
+ '--color-ak-chart-6': '#fbbf24',
48
+ },
49
+ },
50
+ {
51
+ id: 'prism',
52
+ name: 'Vivid',
53
+ description: 'Saturated multi-hue — marketing analytics, category splits.',
54
+ variables: {
55
+ '--color-ak-chart-1': '#8b5cf6',
56
+ '--color-ak-chart-2': '#ec4899',
57
+ '--color-ak-chart-3': '#06b6d4',
58
+ '--color-ak-chart-4': '#22c55e',
59
+ '--color-ak-chart-5': '#f59e0b',
60
+ '--color-ak-chart-6': '#6366f1',
61
+ },
62
+ },
63
+ {
64
+ id: 'ink',
65
+ name: 'Mono',
66
+ description: 'Grayscale ramp — editorial, print-adjacent, monochrome brands.',
67
+ variables: {
68
+ '--color-ak-chart-1': '#171717',
69
+ '--color-ak-chart-2': '#737373',
70
+ '--color-ak-chart-3': '#a3a3a3',
71
+ '--color-ak-chart-4': '#404040',
72
+ '--color-ak-chart-5': '#d4d4d4',
73
+ '--color-ak-chart-6': '#525252',
74
+ },
75
+ // Grayscale must INVERT in dark mode or the ramp disappears into the bg.
76
+ darkVariables: {
77
+ '--color-ak-chart-1': '#e5e5e5',
78
+ '--color-ak-chart-2': '#8a8a8a',
79
+ '--color-ak-chart-3': '#525252',
80
+ '--color-ak-chart-4': '#bdbdbd',
81
+ '--color-ak-chart-5': '#404040',
82
+ '--color-ak-chart-6': '#a3a3a3',
83
+ },
84
+ },
85
+ ];
86
+ export function chartPaletteToVariables(palette, opts) {
87
+ const mode = (opts && opts.mode) || 'light';
88
+ if (mode === 'dark' && palette.darkVariables)
89
+ return { ...palette.darkVariables };
90
+ return { ...palette.variables };
91
+ }
92
+ export const DEFAULT_RADIUS_OPTION_ID = 'default';
93
+ export const RADIUS_OPTIONS = [
94
+ { id: 'none', name: 'Square', value: '0rem' },
95
+ { id: 'small', name: 'Small', value: '0.25rem' },
96
+ { id: 'default', name: 'Default', value: '0.5rem' },
97
+ { id: 'large', name: 'Large', value: '0.875rem' },
98
+ { id: 'xl', name: 'Extra Round', value: '9999rem' },
99
+ ];
@@ -0,0 +1,31 @@
1
+ export interface DialPreset<Id extends string = string> {
2
+ id: Id
3
+ name: string
4
+ description: string
5
+ variables: Record<string, string>
6
+ darkVariables?: Record<string, string>
7
+ swatch?: string
8
+ }
9
+
10
+ export interface DialModeOptions {
11
+ mode?: 'light' | 'dark'
12
+ }
13
+
14
+ export type BaseColorId = 'neutral' | 'stone' | 'zinc' | 'mauve' | 'olive' | 'mist' | 'taupe'
15
+ export declare const DEFAULT_BASE_COLOR_ID: BaseColorId
16
+ export declare const BASE_COLORS: DialPreset<BaseColorId>[]
17
+ export declare function baseColorToVariables(base: DialPreset<BaseColorId>, opts?: DialModeOptions): Record<string, string>
18
+
19
+ export type AccentThemeId =
20
+ | 'default' | 'blue' | 'indigo' | 'violet' | 'purple' | 'pink' | 'rose'
21
+ | 'red' | 'orange' | 'amber' | 'emerald' | 'teal' | 'cyan'
22
+ export declare const DEFAULT_ACCENT_THEME_ID: AccentThemeId
23
+ export declare const ACCENT_THEMES: DialPreset<AccentThemeId>[]
24
+ export declare function accentThemeToVariables(theme: DialPreset<AccentThemeId>, opts?: DialModeOptions): Record<string, string>
25
+
26
+ export declare function radiusToVariables(value: number | string): Record<string, string>
27
+ export type TypeScaleId = 'default' | 'major-third' | 'perfect-fourth' | 'augmented-fourth' | 'perfect-fifth' | 'golden'
28
+ export declare const DEFAULT_TYPE_SCALE_ID: TypeScaleId
29
+ export declare const TYPE_SCALES: DialPreset<TypeScaleId>[]
30
+ export declare function typeScaleToVariables(scale: DialPreset<TypeScaleId>): Record<string, string>
31
+
@@ -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 {};