@soulcraft/theme 1.0.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/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@soulcraft/theme",
3
+ "version": "1.0.0",
4
+ "description": "Unified design system and theme engine for all Soulcraft products",
5
+ "license": "UNLICENSED",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "svelte": "./src/index.ts",
11
+ "exports": {
12
+ ".": "./src/index.ts",
13
+ "./types": "./src/types.ts",
14
+ "./oklch": "./src/oklch.ts",
15
+ "./catalog": "./src/catalog.ts",
16
+ "./resolve": "./src/resolve.ts",
17
+ "./css": "./src/css.ts",
18
+ "./derive": "./src/derive.ts",
19
+ "./tailwind/tokens.css": "./src/tailwind/tokens.css",
20
+ "./stores/theme.svelte": "./src/stores/theme.svelte.ts",
21
+ "./stores/font-size.svelte": "./src/stores/font-size.svelte.ts",
22
+ "./components/ThemePicker": {
23
+ "types": "./src/components/ThemePicker.svelte.d.ts",
24
+ "svelte": "./src/components/ThemePicker.svelte",
25
+ "default": "./src/components/ThemePicker.svelte"
26
+ },
27
+ "./components/ThemeCustomizer": {
28
+ "types": "./src/components/ThemeCustomizer.svelte.d.ts",
29
+ "svelte": "./src/components/ThemeCustomizer.svelte",
30
+ "default": "./src/components/ThemeCustomizer.svelte"
31
+ },
32
+ "./components/ThemeSwatch": {
33
+ "types": "./src/components/ThemeSwatch.svelte.d.ts",
34
+ "svelte": "./src/components/ThemeSwatch.svelte",
35
+ "default": "./src/components/ThemeSwatch.svelte"
36
+ },
37
+ "./components/ColorInput": {
38
+ "types": "./src/components/ColorInput.svelte.d.ts",
39
+ "svelte": "./src/components/ColorInput.svelte",
40
+ "default": "./src/components/ColorInput.svelte"
41
+ },
42
+ "./components/FontSizeControl": {
43
+ "types": "./src/components/FontSizeControl.svelte.d.ts",
44
+ "svelte": "./src/components/FontSizeControl.svelte",
45
+ "default": "./src/components/FontSizeControl.svelte"
46
+ }
47
+ },
48
+ "scripts": {
49
+ "test": "vitest run",
50
+ "typecheck": "tsc --noEmit"
51
+ },
52
+ "peerDependencies": {
53
+ "svelte": "^5.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "svelte": "^5.51.0",
57
+ "typescript": "^5.7.3",
58
+ "vitest": "^3.0.5"
59
+ }
60
+ }
package/src/catalog.ts ADDED
@@ -0,0 +1,313 @@
1
+ /**
2
+ * @module catalog
3
+ * @description Predefined theme catalog for the Soulcraft design system.
4
+ *
5
+ * Provides 23 themes sourced from Workshop's theme registry, converted from
6
+ * hex/rgba to OKLCH at module initialization time. All themes have exact
7
+ * Shiki syntax-highlighting equivalents where Workshop had them.
8
+ *
9
+ * Themes are grouped into three categories:
10
+ * - `soulcraft` — 2 custom themes (dark + light)
11
+ * - `dark` — 15 popular dark IDE themes
12
+ * - `light` — 6 popular light IDE themes
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { THEME_CATALOG, CATALOG_BY_ID, getThemesByCategory } from '@soulcraft/theme/catalog';
17
+ *
18
+ * const dark = getThemesByCategory('dark');
19
+ * const tokyoNight = CATALOG_BY_ID.get('tokyo-night');
20
+ * ```
21
+ */
22
+
23
+ import { hexToOklch, rgbaToOklch } from './oklch.js';
24
+ import type { ThemeDefinition, ThemeColors, ThemeFonts, ThemeCategory } from './types.js';
25
+
26
+ /** Shorthand for use in the static catalog definitions below. */
27
+ const h = hexToOklch;
28
+ const r = rgbaToOklch;
29
+
30
+ // ─── Internal: hex palette definitions ──────────────────────────────────────
31
+
32
+ /** Workshop hex theme definition shape (internal, for catalog construction). */
33
+ interface HexTheme {
34
+ bgDark: string; bgMedium: string; bgLight: string;
35
+ textPrimary: string; textSecondary: string;
36
+ primary: string; primaryLight: string; primaryDark: string;
37
+ accent: string; accentLight: string;
38
+ glass: string; glassBorder: string;
39
+ success: string; warning: string; error: string;
40
+ }
41
+
42
+ /** Convert a hex-based workshop theme to unified ThemeColors (oklch). */
43
+ function toColors(t: HexTheme): ThemeColors {
44
+ return {
45
+ bgBase: h(t.bgDark),
46
+ bgSurface: h(t.bgMedium),
47
+ bgElevated: h(t.bgLight),
48
+ textPrimary: h(t.textPrimary),
49
+ textSecondary: h(t.textSecondary),
50
+ primary: h(t.primary),
51
+ primaryLight: h(t.primaryLight),
52
+ primaryDark: h(t.primaryDark),
53
+ accent: h(t.accent),
54
+ accentLight: h(t.accentLight),
55
+ glass: t.glass.startsWith('rgba') ? r(t.glass) : h(t.glass),
56
+ glassBorder: t.glassBorder.startsWith('rgba') ? r(t.glassBorder) : h(t.glassBorder),
57
+ success: h(t.success),
58
+ warning: h(t.warning),
59
+ error: h(t.error),
60
+ };
61
+ }
62
+
63
+ const WORKSHOP_FONTS: ThemeFonts = { displayFont: 'system-ui', bodyFont: 'Inter' };
64
+
65
+ /** Build a catalog entry from raw hex theme data. */
66
+ function entry(
67
+ id: string,
68
+ name: string,
69
+ category: ThemeCategory,
70
+ isDark: boolean,
71
+ hex: HexTheme,
72
+ fonts: ThemeFonts = WORKSHOP_FONTS
73
+ ): ThemeDefinition {
74
+ return { meta: { id, name, category, isDark }, colors: toColors(hex), fonts };
75
+ }
76
+
77
+ // ─── Catalog entries ─────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * The complete 23-theme catalog as an ordered array.
81
+ * Order matches Workshop's getThemesByCategory() groupings.
82
+ */
83
+ export const THEME_CATALOG: ThemeDefinition[] = [
84
+ // ── Soulcraft Custom ─────────────────────────────────────────────────────
85
+ entry('soulcraft-dark', 'Soulcraft Dark', 'soulcraft', true, {
86
+ bgDark: '#0a0e1a', bgMedium: '#141b2d', bgLight: '#1f2937',
87
+ textPrimary: '#f0f4f8', textSecondary: '#a0aec0',
88
+ primary: '#2a9d8f', primaryLight: '#52c7b8', primaryDark: '#264653',
89
+ accent: '#e76f51', accentLight: '#f4a261',
90
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(255, 255, 255, 0.1)',
91
+ success: '#10b981', warning: '#f59e0b', error: '#ef4444',
92
+ }),
93
+ entry('soulcraft-light', 'Soulcraft Light', 'soulcraft', false, {
94
+ bgDark: '#f8fafc', bgMedium: '#f1f5f9', bgLight: '#e2e8f0',
95
+ textPrimary: '#1e293b', textSecondary: '#64748b',
96
+ primary: '#0d9488', primaryLight: '#14b8a6', primaryDark: '#0f766e',
97
+ accent: '#dc2626', accentLight: '#ef4444',
98
+ glass: 'rgba(0, 0, 0, 0.02)', glassBorder: 'rgba(0, 0, 0, 0.1)',
99
+ success: '#059669', warning: '#d97706', error: '#dc2626',
100
+ }),
101
+
102
+ // ── Dark Themes ───────────────────────────────────────────────────────────
103
+ entry('tokyo-night', 'Tokyo Night', 'dark', true, {
104
+ bgDark: '#16161e', bgMedium: '#1a1b26', bgLight: '#24283b',
105
+ textPrimary: '#c0caf5', textSecondary: '#565f89',
106
+ primary: '#7aa2f7', primaryLight: '#89b4fa', primaryDark: '#3d59a1',
107
+ accent: '#f7768e', accentLight: '#ff9e64',
108
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(122, 162, 247, 0.2)',
109
+ success: '#9ece6a', warning: '#e0af68', error: '#f7768e',
110
+ }),
111
+ entry('catppuccin-mocha', 'Catppuccin Mocha', 'dark', true, {
112
+ bgDark: '#11111b', bgMedium: '#1e1e2e', bgLight: '#313244',
113
+ textPrimary: '#cdd6f4', textSecondary: '#6c7086',
114
+ primary: '#89b4fa', primaryLight: '#a6d4fa', primaryDark: '#5e8dd4',
115
+ accent: '#f38ba8', accentLight: '#f5c2e7',
116
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(137, 180, 250, 0.2)',
117
+ success: '#a6e3a1', warning: '#f9e2af', error: '#f38ba8',
118
+ }),
119
+ entry('gruvbox-dark', 'Gruvbox Dark', 'dark', true, {
120
+ bgDark: '#1d2021', bgMedium: '#282828', bgLight: '#3c3836',
121
+ textPrimary: '#ebdbb2', textSecondary: '#a89984',
122
+ primary: '#83a598', primaryLight: '#8ec07c', primaryDark: '#458588',
123
+ accent: '#fb4934', accentLight: '#fe8019',
124
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(131, 165, 152, 0.2)',
125
+ success: '#b8bb26', warning: '#fabd2f', error: '#fb4934',
126
+ }),
127
+ entry('material-theme-darker', 'Material Darker', 'dark', true, {
128
+ bgDark: '#0f111a', bgMedium: '#212121', bgLight: '#292D3E',
129
+ textPrimary: '#eeffff', textSecondary: '#676E95',
130
+ primary: '#82aaff', primaryLight: '#a6c4ff', primaryDark: '#5e8dd4',
131
+ accent: '#f07178', accentLight: '#ff5370',
132
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(130, 170, 255, 0.2)',
133
+ success: '#c3e88d', warning: '#ffcb6b', error: '#f07178',
134
+ }),
135
+ entry('material-theme-palenight', 'Material Palenight', 'dark', true, {
136
+ bgDark: '#232634', bgMedium: '#292D3E', bgLight: '#32364a',
137
+ textPrimary: '#bfc7d5', textSecondary: '#676E95',
138
+ primary: '#82aaff', primaryLight: '#a6c4ff', primaryDark: '#5e8dd4',
139
+ accent: '#c792ea', accentLight: '#d4a6f5',
140
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(130, 170, 255, 0.2)',
141
+ success: '#c3e88d', warning: '#ffcb6b', error: '#f07178',
142
+ }),
143
+ entry('ayu-dark', 'Ayu Dark', 'dark', true, {
144
+ bgDark: '#0a0e14', bgMedium: '#0f1419', bgLight: '#1f2430',
145
+ textPrimary: '#b3b1ad', textSecondary: '#626a73',
146
+ primary: '#59c2ff', primaryLight: '#73ccff', primaryDark: '#3fa1d9',
147
+ accent: '#ffaa33', accentLight: '#ffb454',
148
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(89, 194, 255, 0.2)',
149
+ success: '#91b362', warning: '#ffaa33', error: '#d95757',
150
+ }),
151
+ entry('synthwave-84', 'Synthwave \'84', 'dark', true, {
152
+ bgDark: '#241b2f', bgMedium: '#262335', bgLight: '#2b2a3e',
153
+ textPrimary: '#f92aad', textSecondary: '#848bbd',
154
+ primary: '#fede5d', primaryLight: '#fdff6a', primaryDark: '#e0c848',
155
+ accent: '#f92aad', accentLight: '#ff4081',
156
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(254, 222, 93, 0.3)',
157
+ success: '#72f1b8', warning: '#fede5d', error: '#ff7edb',
158
+ }),
159
+ entry('one-dark-pro', 'One Dark Pro', 'dark', true, {
160
+ bgDark: '#1e2127', bgMedium: '#282c34', bgLight: '#2c313c',
161
+ textPrimary: '#abb2bf', textSecondary: '#5c6370',
162
+ primary: '#61afef', primaryLight: '#84bcf5', primaryDark: '#4fa3e6',
163
+ accent: '#e06c75', accentLight: '#e78991',
164
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(97, 175, 239, 0.2)',
165
+ success: '#98c379', warning: '#e5c07b', error: '#e06c75',
166
+ }),
167
+ entry('dracula', 'Dracula', 'dark', true, {
168
+ bgDark: '#21222c', bgMedium: '#282a36', bgLight: '#44475a',
169
+ textPrimary: '#f8f8f2', textSecondary: '#6272a4',
170
+ primary: '#bd93f9', primaryLight: '#d4aeff', primaryDark: '#9969e6',
171
+ accent: '#ff79c6', accentLight: '#ff92d0',
172
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(189, 147, 249, 0.2)',
173
+ success: '#50fa7b', warning: '#f1fa8c', error: '#ff5555',
174
+ }),
175
+ entry('nord', 'Nord', 'dark', true, {
176
+ bgDark: '#2e3440', bgMedium: '#3b4252', bgLight: '#434c5e',
177
+ textPrimary: '#eceff4', textSecondary: '#d8dee9',
178
+ primary: '#88c0d0', primaryLight: '#a3d0da', primaryDark: '#6fb3c4',
179
+ accent: '#81a1c1', accentLight: '#9ab6ca',
180
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(136, 192, 208, 0.2)',
181
+ success: '#a3be8c', warning: '#ebcb8b', error: '#bf616a',
182
+ }),
183
+ entry('monokai', 'Monokai', 'dark', true, {
184
+ bgDark: '#1e1f1c', bgMedium: '#272822', bgLight: '#3e3d32',
185
+ textPrimary: '#f8f8f2', textSecondary: '#75715e',
186
+ primary: '#66d9ef', primaryLight: '#92e7f5', primaryDark: '#48c8e3',
187
+ accent: '#fd971f', accentLight: '#fdb04f',
188
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(102, 217, 239, 0.2)',
189
+ success: '#a6e22e', warning: '#e6db74', error: '#f92672',
190
+ }),
191
+ entry('night-owl', 'Night Owl', 'dark', true, {
192
+ bgDark: '#001424', bgMedium: '#011627', bgLight: '#0b2942',
193
+ textPrimary: '#d6deeb', textSecondary: '#5f7e97',
194
+ primary: '#82aaff', primaryLight: '#a3bdff', primaryDark: '#5f8eef',
195
+ accent: '#c792ea', accentLight: '#d9b0f0',
196
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(130, 170, 255, 0.2)',
197
+ success: '#addb67', warning: '#ecc48d', error: '#ef5350',
198
+ }),
199
+ entry('solarized-dark', 'Solarized Dark', 'dark', true, {
200
+ bgDark: '#002b36', bgMedium: '#073642', bgLight: '#0f4757',
201
+ textPrimary: '#fdf6e3', textSecondary: '#93a1a1',
202
+ primary: '#268bd2', primaryLight: '#5da5db', primaryDark: '#1d6fa8',
203
+ accent: '#d33682', accentLight: '#dc609c',
204
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(38, 139, 210, 0.2)',
205
+ success: '#859900', warning: '#b58900', error: '#dc322f',
206
+ }),
207
+ entry('github-dark', 'GitHub Dark', 'dark', true, {
208
+ bgDark: '#0d1117', bgMedium: '#161b22', bgLight: '#21262d',
209
+ textPrimary: '#c9d1d9', textSecondary: '#8b949e',
210
+ primary: '#58a6ff', primaryLight: '#79b8ff', primaryDark: '#388bfd',
211
+ accent: '#f778ba', accentLight: '#f892c9',
212
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(88, 166, 255, 0.2)',
213
+ success: '#3fb950', warning: '#d29922', error: '#f85149',
214
+ }),
215
+ entry('github-dark-dimmed', 'GitHub Dark Dimmed', 'dark', true, {
216
+ bgDark: '#1c2128', bgMedium: '#22272e', bgLight: '#2d333b',
217
+ textPrimary: '#adbac7', textSecondary: '#768390',
218
+ primary: '#539bf5', primaryLight: '#6cb6ff', primaryDark: '#368cf9',
219
+ accent: '#e275ad', accentLight: '#f692ce',
220
+ glass: 'rgba(255, 255, 255, 0.05)', glassBorder: 'rgba(83, 155, 245, 0.2)',
221
+ success: '#57ab5a', warning: '#c69026', error: '#e5534b',
222
+ }),
223
+
224
+ // ── Light Themes ──────────────────────────────────────────────────────────
225
+ entry('catppuccin-latte', 'Catppuccin Latte', 'light', false, {
226
+ bgDark: '#eff1f5', bgMedium: '#e6e9ef', bgLight: '#dce0e8',
227
+ textPrimary: '#4c4f69', textSecondary: '#6c6f85',
228
+ primary: '#1e66f5', primaryLight: '#3b7dff', primaryDark: '#0f52d9',
229
+ accent: '#d20f39', accentLight: '#dd7878',
230
+ glass: 'rgba(0, 0, 0, 0.02)', glassBorder: 'rgba(30, 102, 245, 0.2)',
231
+ success: '#40a02b', warning: '#df8e1d', error: '#d20f39',
232
+ }),
233
+ entry('gruvbox-light', 'Gruvbox Light', 'light', false, {
234
+ bgDark: '#f9f5d7', bgMedium: '#fbf1c7', bgLight: '#ebdbb2',
235
+ textPrimary: '#3c3836', textSecondary: '#7c6f64',
236
+ primary: '#076678', primaryLight: '#458588', primaryDark: '#055765',
237
+ accent: '#9d0006', accentLight: '#cc241d',
238
+ glass: 'rgba(0, 0, 0, 0.02)', glassBorder: 'rgba(7, 102, 120, 0.2)',
239
+ success: '#79740e', warning: '#b57614', error: '#9d0006',
240
+ }),
241
+ entry('solarized-light', 'Solarized Light', 'light', false, {
242
+ bgDark: '#fdf6e3', bgMedium: '#eee8d5', bgLight: '#d3cbb7',
243
+ textPrimary: '#657b83', textSecondary: '#93a1a1',
244
+ primary: '#268bd2', primaryLight: '#5da5db', primaryDark: '#1d6fa8',
245
+ accent: '#d33682', accentLight: '#dc609c',
246
+ glass: 'rgba(0, 0, 0, 0.02)', glassBorder: 'rgba(38, 139, 210, 0.2)',
247
+ success: '#859900', warning: '#b58900', error: '#dc322f',
248
+ }),
249
+ entry('github-light', 'GitHub Light', 'light', false, {
250
+ bgDark: '#ffffff', bgMedium: '#f6f8fa', bgLight: '#e1e4e8',
251
+ textPrimary: '#24292f', textSecondary: '#57606a',
252
+ primary: '#0969da', primaryLight: '#218bff', primaryDark: '#0550ae',
253
+ accent: '#cf222e', accentLight: '#e5534b',
254
+ glass: 'rgba(0, 0, 0, 0.02)', glassBorder: 'rgba(9, 105, 218, 0.2)',
255
+ success: '#1a7f37', warning: '#9a6700', error: '#cf222e',
256
+ }),
257
+ entry('one-light', 'One Light', 'light', false, {
258
+ bgDark: '#fafafa', bgMedium: '#f0f0f0', bgLight: '#e0e0e0',
259
+ textPrimary: '#383a42', textSecondary: '#a0a1a7',
260
+ primary: '#4078f2', primaryLight: '#5e8df7', primaryDark: '#2966e8',
261
+ accent: '#e45649', accentLight: '#ee6559',
262
+ glass: 'rgba(0, 0, 0, 0.02)', glassBorder: 'rgba(64, 120, 242, 0.2)',
263
+ success: '#50a14f', warning: '#c18401', error: '#e45649',
264
+ }),
265
+ entry('material-theme-lighter', 'Material Lighter', 'light', false, {
266
+ bgDark: '#fafafa', bgMedium: '#f4f4f4', bgLight: '#eeeeee',
267
+ textPrimary: '#272727', textSecondary: '#8e8e8e',
268
+ primary: '#6182b8', primaryLight: '#7f9ec8', primaryDark: '#4566a8',
269
+ accent: '#ff5370', accentLight: '#ff6d82',
270
+ glass: 'rgba(0, 0, 0, 0.02)', glassBorder: 'rgba(97, 130, 184, 0.2)',
271
+ success: '#91b859', warning: '#ffb62c', error: '#e53935',
272
+ }),
273
+ ];
274
+
275
+ /**
276
+ * Lookup map for O(1) theme retrieval by ID.
277
+ *
278
+ * @example
279
+ * const theme = CATALOG_BY_ID.get('tokyo-night');
280
+ */
281
+ export const CATALOG_BY_ID: Map<string, ThemeDefinition> = new Map(
282
+ THEME_CATALOG.map(t => [t.meta.id, t])
283
+ );
284
+
285
+ /**
286
+ * Return all themes in a given category, in catalog order.
287
+ *
288
+ * @param category - Category to filter by.
289
+ * @returns Themes matching the given category.
290
+ *
291
+ * @example
292
+ * const darkThemes = getThemesByCategory('dark');
293
+ */
294
+ export function getThemesByCategory(category: ThemeCategory): ThemeDefinition[] {
295
+ return THEME_CATALOG.filter(t => t.meta.category === category);
296
+ }
297
+
298
+ /**
299
+ * Return themes grouped by category, in catalog order within each group.
300
+ * Equivalent to Workshop's `themeStore.getThemesByCategory()`.
301
+ *
302
+ * @returns Record mapping category name to array of ThemeDefinition.
303
+ *
304
+ * @example
305
+ * const { soulcraft, dark, light } = getThemesGrouped();
306
+ */
307
+ export function getThemesGrouped(): Record<ThemeCategory, ThemeDefinition[]> {
308
+ return {
309
+ soulcraft: getThemesByCategory('soulcraft'),
310
+ dark: getThemesByCategory('dark'),
311
+ light: getThemesByCategory('light'),
312
+ };
313
+ }
@@ -0,0 +1,150 @@
1
+ <!--
2
+ @component ColorInput
3
+ @description OKLCH color picker with Lightness, Chroma, and Hue sliders.
4
+
5
+ Renders three labeled range sliders for the L (0–1), C (0–0.4), and H (0–360°)
6
+ components of an OKLCH color string. Includes a live preview swatch showing the
7
+ current color and its hex equivalent for reference.
8
+
9
+ Purely presentational — calls `onchange` with the new OKLCH string on every
10
+ slider interaction. The parent is responsible for persisting state.
11
+
12
+ @props
13
+ value - The current OKLCH color string to edit.
14
+ label - Optional label shown above the control group.
15
+ onchange - Called with the updated OKLCH string on any slider movement.
16
+ -->
17
+ <script lang="ts">
18
+ import { parseOklch, formatOklch, oklchToHex } from '../oklch.js';
19
+ import type { OklchColor } from '../types.js';
20
+
21
+ interface Props {
22
+ value: OklchColor;
23
+ label?: string;
24
+ onchange: (value: OklchColor) => void;
25
+ }
26
+
27
+ let { value, label, onchange }: Props = $props();
28
+
29
+ // Parse the current oklch value into components for slider positioning.
30
+ // The fallback tuple ensures sliders always have valid positions.
31
+ let _parsed = $derived(parseOklch(value));
32
+ let L = $derived(_parsed ? _parsed[0] : 0.5);
33
+ let C = $derived(_parsed ? _parsed[1] : 0);
34
+ let H = $derived(_parsed ? _parsed[2] : 0);
35
+ let alpha = $derived(_parsed ? _parsed[3] : undefined);
36
+ let hexValue = $derived(oklchToHex(value));
37
+
38
+ function handleL(e: Event) {
39
+ onchange(formatOklch(Number((e.target as HTMLInputElement).value), C, H, alpha));
40
+ }
41
+ function handleC(e: Event) {
42
+ onchange(formatOklch(L, Number((e.target as HTMLInputElement).value), H, alpha));
43
+ }
44
+ function handleH(e: Event) {
45
+ onchange(formatOklch(L, C, Number((e.target as HTMLInputElement).value), alpha));
46
+ }
47
+ </script>
48
+
49
+ <div class="color-input">
50
+ {#if label}
51
+ <div class="field-label">{label}</div>
52
+ {/if}
53
+ <div class="controls">
54
+ <div class="swatch" style="background: {value}" title={hexValue}></div>
55
+ <div class="sliders">
56
+ <label class="slider-row">
57
+ <span class="axis">L</span>
58
+ <input type="range" min="0" max="1" step="0.001" value={L} oninput={handleL} />
59
+ <span class="val">{L.toFixed(3)}</span>
60
+ </label>
61
+ <label class="slider-row">
62
+ <span class="axis">C</span>
63
+ <input type="range" min="0" max="0.4" step="0.001" value={C} oninput={handleC} />
64
+ <span class="val">{C.toFixed(3)}</span>
65
+ </label>
66
+ <label class="slider-row">
67
+ <span class="axis">H</span>
68
+ <input type="range" min="0" max="360" step="0.1" value={H} oninput={handleH} />
69
+ <span class="val">{H.toFixed(1)}°</span>
70
+ </label>
71
+ </div>
72
+ </div>
73
+ <div class="hex-value">{hexValue}</div>
74
+ </div>
75
+
76
+ <style>
77
+ .color-input {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 4px;
81
+ }
82
+
83
+ .field-label {
84
+ font-size: 10px;
85
+ font-weight: 600;
86
+ text-transform: uppercase;
87
+ letter-spacing: 0.06em;
88
+ color: var(--theme-text-secondary, #a0aec0);
89
+ }
90
+
91
+ .controls {
92
+ display: flex;
93
+ gap: 8px;
94
+ align-items: flex-start;
95
+ }
96
+
97
+ .swatch {
98
+ width: 28px;
99
+ height: 28px;
100
+ border-radius: 4px;
101
+ flex-shrink: 0;
102
+ border: 1px solid var(--theme-glass-border, rgba(255,255,255,0.12));
103
+ margin-top: 2px;
104
+ }
105
+
106
+ .sliders {
107
+ flex: 1;
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 3px;
111
+ }
112
+
113
+ .slider-row {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 5px;
117
+ }
118
+
119
+ .axis {
120
+ font-size: 9px;
121
+ font-weight: 700;
122
+ color: var(--theme-text-secondary, #a0aec0);
123
+ width: 10px;
124
+ text-align: center;
125
+ flex-shrink: 0;
126
+ }
127
+
128
+ .slider-row input[type="range"] {
129
+ flex: 1;
130
+ height: 3px;
131
+ accent-color: var(--theme-primary, #2a9d8f);
132
+ cursor: pointer;
133
+ }
134
+
135
+ .val {
136
+ font-size: 9px;
137
+ font-variant-numeric: tabular-nums;
138
+ color: var(--theme-text-secondary, #a0aec0);
139
+ width: 38px;
140
+ text-align: right;
141
+ flex-shrink: 0;
142
+ }
143
+
144
+ .hex-value {
145
+ font-size: 9px;
146
+ font-family: monospace;
147
+ color: var(--theme-text-secondary, #a0aec0);
148
+ padding-left: 36px;
149
+ }
150
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Component } from 'svelte';
2
+ import type { OklchColor } from '../types.js';
3
+
4
+ export interface ColorInputProps {
5
+ /** Current OKLCH color value. */
6
+ value: OklchColor;
7
+ /** Optional label shown above the sliders. */
8
+ label?: string;
9
+ /** Called when any L/C/H/alpha slider changes. */
10
+ onchange: (value: OklchColor) => void;
11
+ }
12
+
13
+ declare const ColorInput: Component<ColorInputProps>;
14
+ export default ColorInput;
@@ -0,0 +1,138 @@
1
+ <!--
2
+ @component FontSizeControl
3
+ @description Two-row preset picker for content and interface font sizes.
4
+
5
+ Shows labeled preset buttons for content areas (editor text, chat, preview)
6
+ and interface chrome (dock labels, explorer items, UI labels). The active
7
+ preset for each axis is highlighted with the primary brand color.
8
+
9
+ Purely presentational — calls `onchange` with `FontSizeSettings` when any
10
+ preset button is clicked. Optionally shows a "Reset to defaults" link if
11
+ `onreset` is provided.
12
+
13
+ @props
14
+ contentSize - Current content area font size in px.
15
+ interfaceSize - Current interface chrome font size in px.
16
+ onchange - Called with the new { contentSize, interfaceSize } on preset click.
17
+ onreset - Optional. Called when the Reset to defaults button is clicked.
18
+ -->
19
+ <script lang="ts">
20
+ import { FONT_SIZE_PRESETS, type FontSizeSettings } from '../stores/font-size.svelte.js';
21
+
22
+ interface Props {
23
+ contentSize: number;
24
+ interfaceSize: number;
25
+ onchange: (settings: FontSizeSettings) => void;
26
+ onreset?: () => void;
27
+ }
28
+
29
+ let { contentSize, interfaceSize, onchange, onreset }: Props = $props();
30
+ </script>
31
+
32
+ <div class="font-size-control">
33
+ <div class="row">
34
+ <div class="row-label">Content</div>
35
+ <div class="presets">
36
+ {#each FONT_SIZE_PRESETS.content as preset}
37
+ <button
38
+ class="preset-btn"
39
+ class:active={contentSize === preset.value}
40
+ onclick={() => onchange({ contentSize: preset.value, interfaceSize })}
41
+ title="{preset.value}px"
42
+ >
43
+ {preset.label}
44
+ </button>
45
+ {/each}
46
+ </div>
47
+ </div>
48
+
49
+ <div class="row">
50
+ <div class="row-label">Interface</div>
51
+ <div class="presets">
52
+ {#each FONT_SIZE_PRESETS.interface as preset}
53
+ <button
54
+ class="preset-btn"
55
+ class:active={interfaceSize === preset.value}
56
+ onclick={() => onchange({ contentSize, interfaceSize: preset.value })}
57
+ title="{preset.value}px"
58
+ >
59
+ {preset.label}
60
+ </button>
61
+ {/each}
62
+ </div>
63
+ </div>
64
+
65
+ {#if onreset}
66
+ <button class="reset-btn" onclick={onreset}>Reset to defaults</button>
67
+ {/if}
68
+ </div>
69
+
70
+ <style>
71
+ .font-size-control {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 10px;
75
+ }
76
+
77
+ .row {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 5px;
81
+ }
82
+
83
+ .row-label {
84
+ font-size: 10px;
85
+ font-weight: 600;
86
+ text-transform: uppercase;
87
+ letter-spacing: 0.06em;
88
+ color: var(--theme-text-secondary, #a0aec0);
89
+ }
90
+
91
+ .presets {
92
+ display: flex;
93
+ gap: 4px;
94
+ flex-wrap: wrap;
95
+ }
96
+
97
+ .preset-btn {
98
+ padding: 4px 10px;
99
+ border-radius: 5px;
100
+ border: 1px solid var(--theme-glass-border, rgba(255,255,255,0.1));
101
+ background: var(--theme-glass, rgba(255,255,255,0.05));
102
+ color: var(--theme-text-secondary, #a0aec0);
103
+ font-size: 11px;
104
+ font-family: inherit;
105
+ cursor: pointer;
106
+ transition: all 0.12s ease;
107
+ }
108
+
109
+ .preset-btn:hover {
110
+ border-color: var(--theme-primary, #2a9d8f);
111
+ color: var(--theme-text-primary, #e8eaf0);
112
+ }
113
+
114
+ .preset-btn.active {
115
+ border-color: var(--theme-primary, #2a9d8f);
116
+ background: var(--theme-primary, #2a9d8f);
117
+ color: var(--theme-bg-base, #0a0e1a);
118
+ font-weight: 600;
119
+ }
120
+
121
+ .reset-btn {
122
+ align-self: flex-start;
123
+ padding: 0;
124
+ border: none;
125
+ background: none;
126
+ color: var(--theme-text-secondary, #a0aec0);
127
+ font-size: 11px;
128
+ font-family: inherit;
129
+ cursor: pointer;
130
+ text-decoration: underline;
131
+ text-underline-offset: 2px;
132
+ transition: color 0.12s ease;
133
+ }
134
+
135
+ .reset-btn:hover {
136
+ color: var(--theme-primary, #2a9d8f);
137
+ }
138
+ </style>