@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.
@@ -0,0 +1,16 @@
1
+ import type { Component } from 'svelte';
2
+ import type { FontSizeSettings } from '../stores/font-size.svelte.js';
3
+
4
+ export interface FontSizeControlProps {
5
+ /** Current content area font size in pixels. */
6
+ contentSize: number;
7
+ /** Current interface chrome font size in pixels. */
8
+ interfaceSize: number;
9
+ /** Called when the user selects a new preset size. */
10
+ onchange: (settings: FontSizeSettings) => void;
11
+ /** Called when the user clicks Reset. */
12
+ onreset?: () => void;
13
+ }
14
+
15
+ declare const FontSizeControl: Component<FontSizeControlProps>;
16
+ export default FontSizeControl;
@@ -0,0 +1,359 @@
1
+ <!--
2
+ @component ThemeCustomizer
3
+ @description Full-color editor for creating and tweaking custom themes.
4
+
5
+ Renders a two-column layout: a scrollable color editor on the left and a live
6
+ preview panel on the right. The editor shows ColorInput sliders organized into
7
+ six labeled sections (Backgrounds, Text, Primary, Accent, Glass, Status) plus
8
+ text inputs for font names.
9
+
10
+ Maintains an internal draft copy of the theme so the user can edit freely
11
+ before applying. Changes are shown live in the preview mini-panel. The draft
12
+ resets whenever the parent passes a new `theme` prop.
13
+
14
+ @props
15
+ theme - The base ThemeDefinition to edit. Changes to this prop reset the draft.
16
+ onapply - Called with the complete updated ThemeDefinition when Apply is clicked.
17
+ oncancel - Optional. Called when the Cancel button is clicked.
18
+ -->
19
+ <script lang="ts">
20
+ import type { ThemeDefinition, ThemeColors, ThemeFonts } from '../types.js';
21
+ import ColorInput from './ColorInput.svelte';
22
+
23
+ interface Props {
24
+ theme: ThemeDefinition;
25
+ onapply: (theme: ThemeDefinition) => void;
26
+ oncancel?: () => void;
27
+ }
28
+
29
+ let { theme, onapply, oncancel }: Props = $props();
30
+
31
+ // ── Draft state ───────────────────────────────────────────────────────────────
32
+
33
+ /** Mutable draft copy of the theme colors. Edited before Apply is clicked. */
34
+ let draftColors = $state<ThemeColors>({ ...theme.colors });
35
+ /** Mutable draft copy of the font settings. */
36
+ let draftFonts = $state<ThemeFonts>({ ...theme.fonts });
37
+
38
+ // Reset draft whenever the parent passes a different theme.
39
+ // The effect reads theme.colors / theme.fonts; writing to draftColors does not
40
+ // create a circular dependency because the effect does not read draftColors.
41
+ $effect(() => {
42
+ draftColors = { ...theme.colors };
43
+ draftFonts = { ...theme.fonts };
44
+ });
45
+
46
+ // ── Helpers ───────────────────────────────────────────────────────────────────
47
+
48
+ function updateColor(key: keyof ThemeColors, value: string) {
49
+ draftColors = { ...draftColors, [key]: value };
50
+ }
51
+
52
+ function handleApply() {
53
+ onapply({
54
+ ...theme,
55
+ colors: { ...draftColors },
56
+ fonts: { ...draftFonts },
57
+ });
58
+ }
59
+
60
+ // ── Color section definitions ─────────────────────────────────────────────────
61
+
62
+ /** Sections group related color slots in the editor for readability. */
63
+ const COLOR_SECTIONS: { label: string; keys: (keyof ThemeColors)[] }[] = [
64
+ { label: 'Backgrounds', keys: ['bgBase', 'bgSurface', 'bgElevated'] },
65
+ { label: 'Text', keys: ['textPrimary', 'textSecondary'] },
66
+ { label: 'Primary', keys: ['primary', 'primaryLight', 'primaryDark'] },
67
+ { label: 'Accent', keys: ['accent', 'accentLight'] },
68
+ { label: 'Glass', keys: ['glass', 'glassBorder'] },
69
+ { label: 'Status', keys: ['success', 'warning', 'error'] },
70
+ ];
71
+
72
+ const COLOR_LABELS: Record<keyof ThemeColors, string> = {
73
+ bgBase: 'Base',
74
+ bgSurface: 'Surface',
75
+ bgElevated: 'Elevated',
76
+ textPrimary: 'Primary',
77
+ textSecondary: 'Secondary',
78
+ primary: 'Primary',
79
+ primaryLight: 'Light',
80
+ primaryDark: 'Dark',
81
+ accent: 'Accent',
82
+ accentLight: 'Light',
83
+ glass: 'Glass',
84
+ glassBorder: 'Border',
85
+ success: 'Success',
86
+ warning: 'Warning',
87
+ error: 'Error',
88
+ };
89
+ </script>
90
+
91
+ <div class="customizer">
92
+ <!-- ── Left: color editor ─────────────────────────────────────────────── -->
93
+ <div class="editor">
94
+ {#each COLOR_SECTIONS as section}
95
+ <div class="section">
96
+ <div class="section-label">{section.label}</div>
97
+ <div class="color-grid">
98
+ {#each section.keys as key}
99
+ <ColorInput
100
+ label={COLOR_LABELS[key]}
101
+ value={draftColors[key]}
102
+ onchange={(v) => updateColor(key, v)}
103
+ />
104
+ {/each}
105
+ </div>
106
+ </div>
107
+ {/each}
108
+
109
+ <!-- Font inputs -->
110
+ <div class="section">
111
+ <div class="section-label">Typography</div>
112
+ <div class="font-fields">
113
+ <label class="font-field">
114
+ <span class="font-label">Display Font</span>
115
+ <input
116
+ type="text"
117
+ class="font-input"
118
+ value={draftFonts.displayFont}
119
+ oninput={(e) => {
120
+ draftFonts = { ...draftFonts, displayFont: (e.target as HTMLInputElement).value };
121
+ }}
122
+ placeholder="system-ui"
123
+ />
124
+ </label>
125
+ <label class="font-field">
126
+ <span class="font-label">Body Font</span>
127
+ <input
128
+ type="text"
129
+ class="font-input"
130
+ value={draftFonts.bodyFont}
131
+ oninput={(e) => {
132
+ draftFonts = { ...draftFonts, bodyFont: (e.target as HTMLInputElement).value };
133
+ }}
134
+ placeholder="system-ui"
135
+ />
136
+ </label>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <!-- ── Right: live preview + actions ──────────────────────────────────── -->
142
+ <div class="sidebar">
143
+ <div class="preview-label">Preview</div>
144
+ <div
145
+ class="preview-card"
146
+ style="background: linear-gradient(135deg, {draftColors.bgBase} 0%, {draftColors.bgSurface} 100%)"
147
+ >
148
+ <div class="preview-bar" style="background: {draftColors.primary}"></div>
149
+ <div class="preview-body">
150
+ <div class="preview-heading" style="color: {draftColors.textPrimary}; font-family: {draftFonts.displayFont}, system-ui">
151
+ Heading Text
152
+ </div>
153
+ <div class="preview-muted" style="color: {draftColors.textSecondary}">
154
+ Secondary text
155
+ </div>
156
+ <div class="preview-dots">
157
+ <div class="dot" style="background: {draftColors.primary}"></div>
158
+ <div class="dot" style="background: {draftColors.accent}"></div>
159
+ <div class="dot" style="background: {draftColors.success}"></div>
160
+ <div class="dot" style="background: {draftColors.warning}"></div>
161
+ <div class="dot" style="background: {draftColors.error}"></div>
162
+ </div>
163
+ <div class="preview-glass" style="background: {draftColors.glass}; border-color: {draftColors.glassBorder}">
164
+ <span style="color: {draftColors.textPrimary}; font-size: 9px">Glass panel</span>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <div class="actions">
170
+ {#if oncancel}
171
+ <button class="btn-cancel" onclick={oncancel}>Cancel</button>
172
+ {/if}
173
+ <button class="btn-apply" onclick={handleApply}>Apply</button>
174
+ </div>
175
+ </div>
176
+ </div>
177
+
178
+ <style>
179
+ .customizer {
180
+ display: flex;
181
+ gap: 20px;
182
+ min-width: 0;
183
+ }
184
+
185
+ .editor {
186
+ flex: 1;
187
+ display: flex;
188
+ flex-direction: column;
189
+ gap: 16px;
190
+ overflow-y: auto;
191
+ min-width: 0;
192
+ }
193
+
194
+ .section {
195
+ display: flex;
196
+ flex-direction: column;
197
+ gap: 8px;
198
+ }
199
+
200
+ .section-label {
201
+ font-size: 10px;
202
+ font-weight: 600;
203
+ text-transform: uppercase;
204
+ letter-spacing: 0.08em;
205
+ color: var(--theme-text-secondary, #a0aec0);
206
+ padding-bottom: 2px;
207
+ border-bottom: 1px solid var(--theme-glass-border, rgba(255,255,255,0.1));
208
+ }
209
+
210
+ .color-grid {
211
+ display: flex;
212
+ flex-direction: column;
213
+ gap: 8px;
214
+ }
215
+
216
+ .font-fields {
217
+ display: flex;
218
+ flex-direction: column;
219
+ gap: 8px;
220
+ }
221
+
222
+ .font-field {
223
+ display: flex;
224
+ flex-direction: column;
225
+ gap: 3px;
226
+ }
227
+
228
+ .font-label {
229
+ font-size: 10px;
230
+ font-weight: 600;
231
+ text-transform: uppercase;
232
+ letter-spacing: 0.06em;
233
+ color: var(--theme-text-secondary, #a0aec0);
234
+ }
235
+
236
+ .font-input {
237
+ padding: 5px 8px;
238
+ border-radius: 5px;
239
+ border: 1px solid var(--theme-glass-border, rgba(255,255,255,0.1));
240
+ background: var(--theme-glass, rgba(255,255,255,0.05));
241
+ color: var(--theme-text-primary, #e8eaf0);
242
+ font-size: 12px;
243
+ font-family: monospace;
244
+ outline: none;
245
+ }
246
+
247
+ .font-input:focus {
248
+ border-color: var(--theme-primary, #2a9d8f);
249
+ }
250
+
251
+ /* ── Sidebar ──────────────────────────────────────────────────────────── */
252
+
253
+ .sidebar {
254
+ width: 180px;
255
+ flex-shrink: 0;
256
+ display: flex;
257
+ flex-direction: column;
258
+ gap: 12px;
259
+ }
260
+
261
+ .preview-label {
262
+ font-size: 10px;
263
+ font-weight: 600;
264
+ text-transform: uppercase;
265
+ letter-spacing: 0.08em;
266
+ color: var(--theme-text-secondary, #a0aec0);
267
+ }
268
+
269
+ .preview-card {
270
+ border-radius: 8px;
271
+ overflow: hidden;
272
+ border: 1px solid var(--theme-glass-border, rgba(255,255,255,0.1));
273
+ }
274
+
275
+ .preview-bar {
276
+ height: 4px;
277
+ width: 100%;
278
+ }
279
+
280
+ .preview-body {
281
+ padding: 10px;
282
+ display: flex;
283
+ flex-direction: column;
284
+ gap: 6px;
285
+ }
286
+
287
+ .preview-heading {
288
+ font-size: 13px;
289
+ font-weight: 600;
290
+ line-height: 1.2;
291
+ }
292
+
293
+ .preview-muted {
294
+ font-size: 10px;
295
+ }
296
+
297
+ .preview-dots {
298
+ display: flex;
299
+ gap: 4px;
300
+ margin-top: 2px;
301
+ }
302
+
303
+ .dot {
304
+ width: 10px;
305
+ height: 10px;
306
+ border-radius: 50%;
307
+ }
308
+
309
+ .preview-glass {
310
+ margin-top: 4px;
311
+ padding: 6px 8px;
312
+ border-radius: 5px;
313
+ border: 1px solid;
314
+ }
315
+
316
+ /* ── Actions ──────────────────────────────────────────────────────────── */
317
+
318
+ .actions {
319
+ display: flex;
320
+ gap: 6px;
321
+ flex-direction: column;
322
+ margin-top: auto;
323
+ }
324
+
325
+ .btn-apply,
326
+ .btn-cancel {
327
+ padding: 7px 14px;
328
+ border-radius: 6px;
329
+ font-size: 12px;
330
+ font-family: inherit;
331
+ cursor: pointer;
332
+ transition: all 0.12s ease;
333
+ border: 1px solid;
334
+ width: 100%;
335
+ }
336
+
337
+ .btn-apply {
338
+ background: var(--theme-primary, #2a9d8f);
339
+ border-color: var(--theme-primary, #2a9d8f);
340
+ color: var(--theme-bg-base, #0a0e1a);
341
+ font-weight: 600;
342
+ }
343
+
344
+ .btn-apply:hover {
345
+ background: var(--theme-primary-light, #3ab8a8);
346
+ border-color: var(--theme-primary-light, #3ab8a8);
347
+ }
348
+
349
+ .btn-cancel {
350
+ background: transparent;
351
+ border-color: var(--theme-glass-border, rgba(255,255,255,0.1));
352
+ color: var(--theme-text-secondary, #a0aec0);
353
+ }
354
+
355
+ .btn-cancel:hover {
356
+ border-color: var(--theme-text-secondary, #a0aec0);
357
+ color: var(--theme-text-primary, #e8eaf0);
358
+ }
359
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Component } from 'svelte';
2
+ import type { ThemeDefinition } from '../types.js';
3
+
4
+ export interface ThemeCustomizerProps {
5
+ /** The theme being customized. */
6
+ theme: ThemeDefinition;
7
+ /** Called when the user applies their edits. */
8
+ onapply: (theme: ThemeDefinition) => void;
9
+ /** Called when the user cancels without applying. */
10
+ oncancel?: () => void;
11
+ }
12
+
13
+ declare const ThemeCustomizer: Component<ThemeCustomizerProps>;
14
+ export default ThemeCustomizer;
@@ -0,0 +1,129 @@
1
+ <!--
2
+ @component ThemePicker
3
+ @description Swatch grid for selecting a theme, grouped by category.
4
+
5
+ Renders three labeled sections (Soulcraft, Dark Themes, Light Themes), each
6
+ containing a responsive grid of ThemeSwatch cards. Optionally shows a
7
+ "Customize..." button at the bottom to open the ThemeCustomizer.
8
+
9
+ Purely presentational — delegates all state management to the parent via
10
+ `onselect` and `oncustomize` callbacks.
11
+
12
+ @props
13
+ currentThemeId - The ID of the currently active theme.
14
+ onselect - Called with the chosen theme ID when a swatch is clicked.
15
+ oncustomize - Optional. Called when the "Customize..." button is clicked.
16
+ -->
17
+ <script lang="ts">
18
+ import { THEME_CATALOG, getThemesGrouped } from '../catalog.js';
19
+ import type { ThemeDefinition } from '../types.js';
20
+ import ThemeSwatch from './ThemeSwatch.svelte';
21
+
22
+ interface Props {
23
+ currentThemeId: string;
24
+ onselect: (themeId: string) => void;
25
+ oncustomize?: () => void;
26
+ }
27
+
28
+ let { currentThemeId, onselect, oncustomize }: Props = $props();
29
+
30
+ const groups = getThemesGrouped();
31
+ </script>
32
+
33
+ <div class="theme-picker">
34
+ <!-- Soulcraft section -->
35
+ <div class="group">
36
+ <div class="group-label">Soulcraft</div>
37
+ <div class="swatches">
38
+ {#each groups.soulcraft as theme}
39
+ <ThemeSwatch
40
+ {theme}
41
+ selected={currentThemeId === theme.meta.id}
42
+ {onselect}
43
+ />
44
+ {/each}
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Dark themes section -->
49
+ <div class="group">
50
+ <div class="group-label">Dark Themes</div>
51
+ <div class="swatches">
52
+ {#each groups.dark as theme}
53
+ <ThemeSwatch
54
+ {theme}
55
+ selected={currentThemeId === theme.meta.id}
56
+ {onselect}
57
+ />
58
+ {/each}
59
+ </div>
60
+ </div>
61
+
62
+ <!-- Light themes section -->
63
+ <div class="group">
64
+ <div class="group-label">Light Themes</div>
65
+ <div class="swatches">
66
+ {#each groups.light as theme}
67
+ <ThemeSwatch
68
+ {theme}
69
+ selected={currentThemeId === theme.meta.id}
70
+ {onselect}
71
+ />
72
+ {/each}
73
+ </div>
74
+ </div>
75
+
76
+ {#if oncustomize}
77
+ <button class="customize-btn" onclick={oncustomize}>
78
+ Customize...
79
+ </button>
80
+ {/if}
81
+ </div>
82
+
83
+ <style>
84
+ .theme-picker {
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: 16px;
88
+ }
89
+
90
+ .group {
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: 8px;
94
+ }
95
+
96
+ .group-label {
97
+ font-size: 10px;
98
+ font-weight: 600;
99
+ text-transform: uppercase;
100
+ letter-spacing: 0.08em;
101
+ color: var(--theme-text-secondary, #a0aec0);
102
+ padding-bottom: 2px;
103
+ border-bottom: 1px solid var(--theme-glass-border, rgba(255,255,255,0.1));
104
+ }
105
+
106
+ .swatches {
107
+ display: grid;
108
+ grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
109
+ gap: 6px;
110
+ }
111
+
112
+ .customize-btn {
113
+ align-self: flex-start;
114
+ padding: 6px 14px;
115
+ border-radius: 6px;
116
+ border: 1px solid var(--theme-glass-border, rgba(255,255,255,0.1));
117
+ background: var(--theme-glass, rgba(255,255,255,0.05));
118
+ color: var(--theme-text-secondary, #a0aec0);
119
+ font-size: 12px;
120
+ font-family: inherit;
121
+ cursor: pointer;
122
+ transition: all 0.15s ease;
123
+ }
124
+
125
+ .customize-btn:hover {
126
+ border-color: var(--theme-primary, #2a9d8f);
127
+ color: var(--theme-primary, #2a9d8f);
128
+ }
129
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Component } from 'svelte';
2
+
3
+ export interface ThemePickerProps {
4
+ /** Currently active theme ID. */
5
+ currentThemeId: string;
6
+ /** Called when the user selects a theme. */
7
+ onselect: (themeId: string) => void;
8
+ /** Called when the user clicks "Customize". */
9
+ oncustomize?: () => void;
10
+ }
11
+
12
+ declare const ThemePicker: Component<ThemePickerProps>;
13
+ export default ThemePicker;
@@ -0,0 +1,136 @@
1
+ <!--
2
+ @component ThemeSwatch
3
+ @description Mini preview card for a single theme in the ThemePicker grid.
4
+
5
+ Renders a compact visual preview of the theme's color palette: background gradient,
6
+ primary color bar, accent dot, and the theme name. Highlights the currently
7
+ selected theme with a border ring.
8
+
9
+ @props
10
+ theme - The ThemeDefinition to preview.
11
+ selected - Whether this theme is currently selected.
12
+ onselect - Callback invoked when the user clicks this swatch.
13
+ -->
14
+ <script lang="ts">
15
+ import type { ThemeDefinition } from '../types.js';
16
+
17
+ interface Props {
18
+ theme: ThemeDefinition;
19
+ selected: boolean;
20
+ onselect: (id: string) => void;
21
+ }
22
+
23
+ let { theme, selected, onselect }: Props = $props();
24
+
25
+ const { colors } = theme;
26
+
27
+ // Inline style for the swatch background — uses the theme's actual colors
28
+ const bgStyle = `background: linear-gradient(135deg, ${colors.bgBase} 0%, ${colors.bgSurface} 100%)`;
29
+ const primaryStyle = `background: ${colors.primary}`;
30
+ const accentStyle = `background: ${colors.accent}`;
31
+ const textStyle = `color: ${colors.textPrimary}`;
32
+ const mutedStyle = `color: ${colors.textSecondary}`;
33
+ </script>
34
+
35
+ <button
36
+ class="swatch"
37
+ class:selected
38
+ onclick={() => onselect(theme.meta.id)}
39
+ title={theme.meta.name}
40
+ aria-pressed={selected}
41
+ >
42
+ <div class="preview" style={bgStyle}>
43
+ <div class="color-bar" style={primaryStyle}></div>
44
+ <div class="dots">
45
+ <div class="dot" style={accentStyle}></div>
46
+ <div class="dot secondary" style={`background: ${colors.textSecondary}`}></div>
47
+ </div>
48
+ <div class="lines">
49
+ <div class="line" style={`background: ${colors.textPrimary}; opacity: 0.6; width: 65%`}></div>
50
+ <div class="line" style={`background: ${colors.textSecondary}; opacity: 0.4; width: 45%`}></div>
51
+ </div>
52
+ </div>
53
+ <div class="label" style={textStyle}>
54
+ <span class="name">{theme.meta.name}</span>
55
+ </div>
56
+ </button>
57
+
58
+ <style>
59
+ .swatch {
60
+ display: flex;
61
+ flex-direction: column;
62
+ border: 2px solid transparent;
63
+ border-radius: 8px;
64
+ overflow: hidden;
65
+ cursor: pointer;
66
+ padding: 0;
67
+ background: none;
68
+ font-family: inherit;
69
+ transition: border-color 0.15s ease, transform 0.1s ease;
70
+ }
71
+
72
+ .swatch:hover {
73
+ border-color: var(--theme-primary, #2a9d8f);
74
+ transform: scale(1.02);
75
+ }
76
+
77
+ .swatch.selected {
78
+ border-color: var(--theme-primary, #2a9d8f);
79
+ box-shadow: 0 0 0 1px var(--theme-primary, #2a9d8f);
80
+ }
81
+
82
+ .preview {
83
+ height: 52px;
84
+ padding: 6px 8px;
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: 4px;
88
+ position: relative;
89
+ overflow: hidden;
90
+ }
91
+
92
+ .color-bar {
93
+ height: 3px;
94
+ border-radius: 2px;
95
+ width: 100%;
96
+ }
97
+
98
+ .dots {
99
+ display: flex;
100
+ gap: 3px;
101
+ margin-top: 2px;
102
+ }
103
+
104
+ .dot {
105
+ width: 7px;
106
+ height: 7px;
107
+ border-radius: 50%;
108
+ }
109
+
110
+ .lines {
111
+ display: flex;
112
+ flex-direction: column;
113
+ gap: 3px;
114
+ margin-top: 2px;
115
+ }
116
+
117
+ .line {
118
+ height: 2px;
119
+ border-radius: 1px;
120
+ }
121
+
122
+ .label {
123
+ padding: 4px 6px 5px;
124
+ background: var(--theme-bg-elevated, #1f2937);
125
+ text-align: left;
126
+ }
127
+
128
+ .name {
129
+ font-size: 10px;
130
+ font-weight: 500;
131
+ white-space: nowrap;
132
+ overflow: hidden;
133
+ text-overflow: ellipsis;
134
+ display: block;
135
+ }
136
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Component } from 'svelte';
2
+ import type { ThemeDefinition } from '../types.js';
3
+
4
+ export interface ThemeSwatchProps {
5
+ /** The theme to render as a preview swatch. */
6
+ theme: ThemeDefinition;
7
+ /** Whether this swatch is currently selected. */
8
+ selected: boolean;
9
+ /** Called when the user clicks this swatch. */
10
+ onselect: (id: string) => void;
11
+ }
12
+
13
+ declare const ThemeSwatch: Component<ThemeSwatchProps>;
14
+ export default ThemeSwatch;