svelte-theme-picker 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/README.md +686 -0
- package/dist/ThemePicker.svelte +514 -0
- package/dist/ThemePicker.svelte.d.ts +19 -0
- package/dist/catalog.d.ts +47 -0
- package/dist/catalog.js +160 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +8 -0
- package/dist/store.d.ts +36 -0
- package/dist/store.js +118 -0
- package/dist/themes.d.ts +54 -0
- package/dist/themes.js +486 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.js +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
# svelte-theme-picker
|
|
2
|
+
|
|
3
|
+
A beautiful, customizable theme picker component for Svelte 5 applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 10 beautiful built-in themes (dark and light modes)
|
|
8
|
+
- Fully customizable - add your own themes
|
|
9
|
+
- Persists selection to localStorage
|
|
10
|
+
- Applies CSS variables to your document
|
|
11
|
+
- Svelte 5 runes compatible
|
|
12
|
+
- Full TypeScript support
|
|
13
|
+
- Floating button or inline mode (vertical or horizontal)
|
|
14
|
+
- **Preview mode** - temporarily apply themes without persisting
|
|
15
|
+
- **JSON-driven themes** - define themes in JSON with production/test filtering
|
|
16
|
+
- **Theme catalogs** - organize themes with metadata, tags, and filtering
|
|
17
|
+
- **Headless mode** - use the store without any UI for full programmatic control
|
|
18
|
+
- **Performance optimized** - GPU-accelerated animations, batched DOM updates
|
|
19
|
+
- **Accessible** - ARIA labels, keyboard navigation (Escape to close)
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install svelte-theme-picker
|
|
25
|
+
# or
|
|
26
|
+
pnpm add svelte-theme-picker
|
|
27
|
+
# or
|
|
28
|
+
yarn add svelte-theme-picker
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Basic Usage
|
|
32
|
+
|
|
33
|
+
```svelte
|
|
34
|
+
<script>
|
|
35
|
+
import { ThemePicker } from 'svelte-theme-picker';
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<ThemePicker />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This adds a floating theme picker button to the bottom-right of your page.
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
### Props
|
|
46
|
+
|
|
47
|
+
| Prop | Type | Default | Description |
|
|
48
|
+
|------|------|---------|-------------|
|
|
49
|
+
| `config` | `ThemePickerConfig` | `{}` | Configuration options |
|
|
50
|
+
| `store` | `ThemeStore` | `undefined` | Custom theme store |
|
|
51
|
+
| `position` | `'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left'` | `'bottom-right'` | Position of trigger button |
|
|
52
|
+
| `showTrigger` | `boolean` | `true` | Show floating button (false = inline mode) |
|
|
53
|
+
| `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction for inline mode |
|
|
54
|
+
| `onThemeChange` | `(id: string, theme: Theme) => void` | `undefined` | Callback when theme changes |
|
|
55
|
+
|
|
56
|
+
### Inline Layouts
|
|
57
|
+
|
|
58
|
+
```svelte
|
|
59
|
+
<!-- Vertical list with names and descriptions -->
|
|
60
|
+
<ThemePicker showTrigger={false} layout="vertical" />
|
|
61
|
+
|
|
62
|
+
<!-- Horizontal grid of color swatches (compact) -->
|
|
63
|
+
<ThemePicker showTrigger={false} layout="horizontal" />
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Configuration Options
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
interface ThemePickerConfig {
|
|
70
|
+
storageKey?: string; // localStorage key (default: 'svelte-theme-picker')
|
|
71
|
+
defaultTheme?: string; // Default theme ID (default: 'dreamy')
|
|
72
|
+
themes?: Record<string, Theme>; // Custom themes
|
|
73
|
+
cssVarPrefix?: string; // CSS variable prefix
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Headless Mode (No UI)
|
|
78
|
+
|
|
79
|
+
The `ThemePicker` component is completely optional. You can use just the store and utilities for full programmatic control without rendering any UI. This is useful when:
|
|
80
|
+
|
|
81
|
+
- You want to control themes from your own settings page
|
|
82
|
+
- Themes are set based on user preferences from a database
|
|
83
|
+
- You're building a custom theme switcher UI
|
|
84
|
+
- You want to disable user theme switching entirely
|
|
85
|
+
|
|
86
|
+
```svelte
|
|
87
|
+
<script>
|
|
88
|
+
// No ThemePicker component needed!
|
|
89
|
+
import { themeStore, applyTheme, defaultThemes } from 'svelte-theme-picker';
|
|
90
|
+
import { onMount } from 'svelte';
|
|
91
|
+
|
|
92
|
+
onMount(() => {
|
|
93
|
+
// Load theme from user preferences, database, API, etc.
|
|
94
|
+
const userTheme = getUserPreference() || 'dreamy';
|
|
95
|
+
themeStore.setTheme(userTheme);
|
|
96
|
+
applyTheme(defaultThemes[userTheme]);
|
|
97
|
+
|
|
98
|
+
// Optionally subscribe to persist changes
|
|
99
|
+
return themeStore.subscribe((themeId) => {
|
|
100
|
+
saveUserPreference(themeId);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<!-- Your app content - no picker UI rendered -->
|
|
106
|
+
<slot />
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
You get all the benefits (localStorage persistence, CSS variable application, TypeScript types) without any visible theme picker.
|
|
110
|
+
|
|
111
|
+
## Preview Mode (Temporary Themes)
|
|
112
|
+
|
|
113
|
+
Perfect for form-based theme selection where you want to preview themes without persisting them:
|
|
114
|
+
|
|
115
|
+
```svelte
|
|
116
|
+
<script>
|
|
117
|
+
import { themeStore, applyTheme, defaultThemes } from 'svelte-theme-picker';
|
|
118
|
+
import { onMount } from 'svelte';
|
|
119
|
+
|
|
120
|
+
let selectedTheme = 'dreamy';
|
|
121
|
+
|
|
122
|
+
onMount(() => {
|
|
123
|
+
// When the page unmounts, revert to persisted theme
|
|
124
|
+
return () => {
|
|
125
|
+
themeStore.revertPreview();
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
function previewTheme(themeId: string) {
|
|
130
|
+
selectedTheme = themeId;
|
|
131
|
+
// Preview without persisting to localStorage
|
|
132
|
+
themeStore.previewTheme(themeId);
|
|
133
|
+
applyTheme(defaultThemes[themeId]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function saveTheme() {
|
|
137
|
+
// Persist the selection
|
|
138
|
+
themeStore.setTheme(selectedTheme);
|
|
139
|
+
}
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<select onchange={(e) => previewTheme(e.target.value)}>
|
|
143
|
+
{#each Object.entries(defaultThemes) as [id, theme]}
|
|
144
|
+
<option value={id}>{theme.name}</option>
|
|
145
|
+
{/each}
|
|
146
|
+
</select>
|
|
147
|
+
|
|
148
|
+
<button onclick={saveTheme}>Save Theme</button>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Store Preview Methods
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
interface ThemeStore {
|
|
155
|
+
// ... standard methods ...
|
|
156
|
+
|
|
157
|
+
/** Preview a theme temporarily without persisting */
|
|
158
|
+
previewTheme: (themeId: string) => void;
|
|
159
|
+
|
|
160
|
+
/** Revert from preview back to the persisted theme */
|
|
161
|
+
revertPreview: () => void;
|
|
162
|
+
|
|
163
|
+
/** Check if currently in preview mode */
|
|
164
|
+
isPreviewMode: () => boolean;
|
|
165
|
+
|
|
166
|
+
/** Get the persisted theme ID (ignores preview) */
|
|
167
|
+
getPersistedThemeId: () => string;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Theme Catalogs
|
|
172
|
+
|
|
173
|
+
For larger applications, you may want to organize themes with metadata. Theme catalogs support:
|
|
174
|
+
- **Active/inactive** - Hide themes from production while keeping them documented
|
|
175
|
+
- **Tags** - Categorize themes (dark, light, seasonal, test, etc.)
|
|
176
|
+
- **Sorting** - Control display order
|
|
177
|
+
- **Filtering** - Show only relevant themes
|
|
178
|
+
|
|
179
|
+
### Catalog Structure
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import type { ThemeCatalog } from 'svelte-theme-picker';
|
|
183
|
+
|
|
184
|
+
const myCatalog: ThemeCatalog = {
|
|
185
|
+
'brand-dark': {
|
|
186
|
+
theme: { /* theme definition */ },
|
|
187
|
+
meta: {
|
|
188
|
+
active: true, // Show in picker
|
|
189
|
+
tags: ['dark', 'brand'],
|
|
190
|
+
order: 1, // Sort order
|
|
191
|
+
seasonal: false, // Or 'winter', 'spring', etc.
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
'christmas': {
|
|
195
|
+
theme: { /* theme definition */ },
|
|
196
|
+
meta: {
|
|
197
|
+
active: false, // Hidden from picker
|
|
198
|
+
tags: ['dark', 'seasonal'],
|
|
199
|
+
seasonal: 'winter',
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
'test-theme': {
|
|
203
|
+
theme: { /* theme definition */ },
|
|
204
|
+
meta: {
|
|
205
|
+
active: false, // Dev only
|
|
206
|
+
tags: ['test'],
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Filtering Themes
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import {
|
|
216
|
+
defaultThemeCatalog,
|
|
217
|
+
filterCatalog,
|
|
218
|
+
catalogToThemes,
|
|
219
|
+
getActiveThemes,
|
|
220
|
+
getThemesByTag,
|
|
221
|
+
getThemesByAnyTag,
|
|
222
|
+
} from 'svelte-theme-picker';
|
|
223
|
+
|
|
224
|
+
// Get only active themes
|
|
225
|
+
const activeThemes = getActiveThemes(defaultThemeCatalog);
|
|
226
|
+
|
|
227
|
+
// Get themes by tag
|
|
228
|
+
const darkThemes = getThemesByTag(defaultThemeCatalog, 'dark');
|
|
229
|
+
|
|
230
|
+
// Get themes by any of several tags
|
|
231
|
+
const accentThemes = getThemesByAnyTag(defaultThemeCatalog, ['neon', 'vibrant']);
|
|
232
|
+
|
|
233
|
+
// Advanced filtering
|
|
234
|
+
const productionThemes = catalogToThemes(
|
|
235
|
+
filterCatalog(defaultThemeCatalog, {
|
|
236
|
+
activeOnly: true,
|
|
237
|
+
excludeTags: ['test', 'seasonal'],
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Use with ThemePicker
|
|
242
|
+
<ThemePicker config={{ themes: productionThemes }} />
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### JSON-Driven Themes
|
|
246
|
+
|
|
247
|
+
Define your themes in a JSON file for easy management and version control:
|
|
248
|
+
|
|
249
|
+
**themes.json**
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"brand-dark": {
|
|
253
|
+
"theme": {
|
|
254
|
+
"name": "Brand Dark",
|
|
255
|
+
"description": "Official dark theme",
|
|
256
|
+
"colors": {
|
|
257
|
+
"bgDeep": "#0a0a12",
|
|
258
|
+
"bgMid": "#12121a",
|
|
259
|
+
"bgCard": "#1a1a24",
|
|
260
|
+
"bgGlow": "#2a2a3a",
|
|
261
|
+
"bgOverlay": "#0a0a12",
|
|
262
|
+
"primary1": "#6366f1",
|
|
263
|
+
"primary2": "#818cf8",
|
|
264
|
+
"primary3": "#a5b4fc",
|
|
265
|
+
"primary4": "#c7d2fe",
|
|
266
|
+
"primary5": "#e0e7ff",
|
|
267
|
+
"primary6": "#eef2ff",
|
|
268
|
+
"accent1": "#8b5cf6",
|
|
269
|
+
"accent2": "#a78bfa",
|
|
270
|
+
"accent3": "#c4b5fd",
|
|
271
|
+
"textPrimary": "#f8fafc",
|
|
272
|
+
"textSecondary": "#cbd5e1",
|
|
273
|
+
"textMuted": "#64748b"
|
|
274
|
+
},
|
|
275
|
+
"fonts": {
|
|
276
|
+
"heading": "'Inter', sans-serif",
|
|
277
|
+
"body": "'Inter', sans-serif",
|
|
278
|
+
"mono": "'JetBrains Mono', monospace"
|
|
279
|
+
},
|
|
280
|
+
"effects": {
|
|
281
|
+
"glowColor": "rgba(99, 102, 241, 0.15)",
|
|
282
|
+
"glowIntensity": 0.3,
|
|
283
|
+
"particleColors": ["#6366f1", "#8b5cf6"],
|
|
284
|
+
"useNoise": false,
|
|
285
|
+
"noiseOpacity": 0
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
"meta": {
|
|
289
|
+
"active": true,
|
|
290
|
+
"tags": ["dark", "brand", "production"],
|
|
291
|
+
"order": 1
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
"christmas-special": {
|
|
295
|
+
"theme": {
|
|
296
|
+
"name": "Christmas",
|
|
297
|
+
"description": "Festive holiday theme"
|
|
298
|
+
},
|
|
299
|
+
"meta": {
|
|
300
|
+
"active": false,
|
|
301
|
+
"tags": ["dark", "seasonal", "holiday"],
|
|
302
|
+
"seasonal": "winter"
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
"dev-test": {
|
|
306
|
+
"theme": {
|
|
307
|
+
"name": "Dev Test",
|
|
308
|
+
"description": "Testing theme - not for production"
|
|
309
|
+
},
|
|
310
|
+
"meta": {
|
|
311
|
+
"active": false,
|
|
312
|
+
"tags": ["test", "dev"]
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Loading and filtering for production:**
|
|
319
|
+
```typescript
|
|
320
|
+
import {
|
|
321
|
+
loadCatalogFromJSON,
|
|
322
|
+
getActiveThemes,
|
|
323
|
+
filterCatalog,
|
|
324
|
+
catalogToThemes,
|
|
325
|
+
} from 'svelte-theme-picker';
|
|
326
|
+
|
|
327
|
+
// Load themes from JSON (e.g., fetched or imported)
|
|
328
|
+
import themesJson from './themes.json';
|
|
329
|
+
const catalog = loadCatalogFromJSON(themesJson);
|
|
330
|
+
|
|
331
|
+
// Get only production-ready themes (active: true)
|
|
332
|
+
const productionThemes = getActiveThemes(catalog);
|
|
333
|
+
|
|
334
|
+
// Or with more advanced filtering
|
|
335
|
+
const productionThemes = catalogToThemes(
|
|
336
|
+
filterCatalog(catalog, {
|
|
337
|
+
activeOnly: true,
|
|
338
|
+
excludeTags: ['test', 'seasonal'],
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// Use with ThemePicker
|
|
343
|
+
<ThemePicker config={{ themes: productionThemes }} />
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
This pattern lets you:
|
|
347
|
+
- **Document all themes** in one place (production, test, seasonal)
|
|
348
|
+
- **Control which themes are active** in production via the `active` flag
|
|
349
|
+
- **Filter by tags** for specific use cases (e.g., only dark themes)
|
|
350
|
+
- **Version control** your theme definitions alongside your code
|
|
351
|
+
|
|
352
|
+
### Catalog Utilities
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// Convert simple themes to catalog
|
|
356
|
+
import { themesToCatalog } from 'svelte-theme-picker';
|
|
357
|
+
const catalog = themesToCatalog(myThemes, { active: true });
|
|
358
|
+
|
|
359
|
+
// Merge catalogs (combine default themes with your custom ones)
|
|
360
|
+
import { mergeCatalogs, defaultThemeCatalog } from 'svelte-theme-picker';
|
|
361
|
+
const combined = mergeCatalogs(defaultThemeCatalog, myCatalog);
|
|
362
|
+
|
|
363
|
+
// Get all tags in a catalog
|
|
364
|
+
import { getCatalogTags } from 'svelte-theme-picker';
|
|
365
|
+
const tags = getCatalogTags(myCatalog); // ['dark', 'brand', 'seasonal', ...]
|
|
366
|
+
|
|
367
|
+
// Sort catalog by order
|
|
368
|
+
import { sortCatalog } from 'svelte-theme-picker';
|
|
369
|
+
const sorted = sortCatalog(myCatalog);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Custom Theme Picker UI
|
|
373
|
+
|
|
374
|
+
For complete control over the theme picker UI (e.g., in forms), you can build your own using the exported utilities. This is useful when you need color swatches in a specific layout or want to match your app's design system.
|
|
375
|
+
|
|
376
|
+
### Color Swatch Grid (Form Integration)
|
|
377
|
+
|
|
378
|
+
```svelte
|
|
379
|
+
<script lang="ts">
|
|
380
|
+
import {
|
|
381
|
+
themeStore,
|
|
382
|
+
applyTheme,
|
|
383
|
+
defaultThemes,
|
|
384
|
+
type Theme
|
|
385
|
+
} from 'svelte-theme-picker';
|
|
386
|
+
import { onMount } from 'svelte';
|
|
387
|
+
|
|
388
|
+
let selectedTheme = $state('dreamy');
|
|
389
|
+
|
|
390
|
+
// Revert to saved theme when leaving the page
|
|
391
|
+
onMount(() => {
|
|
392
|
+
return () => themeStore.revertPreview();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
function previewTheme(themeId: string) {
|
|
396
|
+
selectedTheme = themeId;
|
|
397
|
+
themeStore.previewTheme(themeId);
|
|
398
|
+
applyTheme(defaultThemes[themeId]);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function saveSelection() {
|
|
402
|
+
// Persist the theme when form is submitted
|
|
403
|
+
themeStore.setTheme(selectedTheme);
|
|
404
|
+
}
|
|
405
|
+
</script>
|
|
406
|
+
|
|
407
|
+
<div class="theme-grid">
|
|
408
|
+
{#each Object.entries(defaultThemes) as [id, theme]}
|
|
409
|
+
<button
|
|
410
|
+
type="button"
|
|
411
|
+
class="theme-swatch"
|
|
412
|
+
class:selected={selectedTheme === id}
|
|
413
|
+
onclick={() => previewTheme(id)}
|
|
414
|
+
title={theme.name}
|
|
415
|
+
>
|
|
416
|
+
<div class="colors">
|
|
417
|
+
<span style="background: {theme.colors.primary1}"></span>
|
|
418
|
+
<span style="background: {theme.colors.primary3}"></span>
|
|
419
|
+
<span style="background: {theme.colors.primary5}"></span>
|
|
420
|
+
<span style="background: {theme.colors.accent1}"></span>
|
|
421
|
+
</div>
|
|
422
|
+
<span class="name">{theme.name}</span>
|
|
423
|
+
</button>
|
|
424
|
+
{/each}
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
<style>
|
|
428
|
+
.theme-grid {
|
|
429
|
+
display: flex;
|
|
430
|
+
flex-wrap: wrap;
|
|
431
|
+
gap: 8px;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.theme-swatch {
|
|
435
|
+
display: flex;
|
|
436
|
+
flex-direction: column;
|
|
437
|
+
align-items: center;
|
|
438
|
+
gap: 4px;
|
|
439
|
+
padding: 8px;
|
|
440
|
+
background: transparent;
|
|
441
|
+
border: 2px solid transparent;
|
|
442
|
+
border-radius: 8px;
|
|
443
|
+
cursor: pointer;
|
|
444
|
+
transition: all 0.2s;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.theme-swatch:hover {
|
|
448
|
+
background: rgba(255, 255, 255, 0.05);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.theme-swatch.selected {
|
|
452
|
+
border-color: var(--accent-1, #a855f7);
|
|
453
|
+
background: rgba(168, 85, 247, 0.1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.colors {
|
|
457
|
+
display: grid;
|
|
458
|
+
grid-template-columns: repeat(2, 1fr);
|
|
459
|
+
gap: 2px;
|
|
460
|
+
width: 32px;
|
|
461
|
+
height: 32px;
|
|
462
|
+
border-radius: 6px;
|
|
463
|
+
overflow: hidden;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.colors span {
|
|
467
|
+
width: 100%;
|
|
468
|
+
height: 100%;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.name {
|
|
472
|
+
font-size: 0.75rem;
|
|
473
|
+
color: var(--text-secondary, #a0a0a0);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.theme-swatch.selected .name {
|
|
477
|
+
color: var(--text-primary, #fff);
|
|
478
|
+
}
|
|
479
|
+
</style>
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Using with Forms
|
|
483
|
+
|
|
484
|
+
When integrating theme selection in a form (e.g., collection creation), use the preview pattern:
|
|
485
|
+
|
|
486
|
+
1. **On mount**: Save the current theme with `getPersistedThemeId()`
|
|
487
|
+
2. **On selection**: Use `previewTheme()` + `applyTheme()` for live preview
|
|
488
|
+
3. **On cancel/leave**: Call `revertPreview()` to restore the original theme
|
|
489
|
+
4. **On save**: Call `setTheme()` to persist the selection
|
|
490
|
+
|
|
491
|
+
```svelte
|
|
492
|
+
<script>
|
|
493
|
+
import { themeStore, applyTheme, defaultThemes } from 'svelte-theme-picker';
|
|
494
|
+
import { onMount } from 'svelte';
|
|
495
|
+
|
|
496
|
+
let selectedTheme = 'dreamy';
|
|
497
|
+
let formSubmitted = false;
|
|
498
|
+
|
|
499
|
+
onMount(() => {
|
|
500
|
+
// Restore original theme if user leaves without saving
|
|
501
|
+
return () => {
|
|
502
|
+
if (!formSubmitted) {
|
|
503
|
+
themeStore.revertPreview();
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
function selectTheme(themeId) {
|
|
509
|
+
selectedTheme = themeId;
|
|
510
|
+
themeStore.previewTheme(themeId);
|
|
511
|
+
applyTheme(defaultThemes[themeId]);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function handleSubmit() {
|
|
515
|
+
formSubmitted = true;
|
|
516
|
+
themeStore.setTheme(selectedTheme);
|
|
517
|
+
// ... save to database
|
|
518
|
+
}
|
|
519
|
+
</script>
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
## Custom Themes
|
|
523
|
+
|
|
524
|
+
You can provide your own themes:
|
|
525
|
+
|
|
526
|
+
```svelte
|
|
527
|
+
<script>
|
|
528
|
+
import { ThemePicker, type Theme } from 'svelte-theme-picker';
|
|
529
|
+
|
|
530
|
+
const myThemes: Record<string, Theme> = {
|
|
531
|
+
'my-theme': {
|
|
532
|
+
name: 'My Theme',
|
|
533
|
+
description: 'A custom theme',
|
|
534
|
+
colors: {
|
|
535
|
+
bgDeep: '#1a1a2e',
|
|
536
|
+
bgMid: '#232342',
|
|
537
|
+
bgCard: '#2a2a4a',
|
|
538
|
+
bgGlow: '#3d3d6b',
|
|
539
|
+
bgOverlay: '#1a1a2e',
|
|
540
|
+
primary1: '#c9a0dc',
|
|
541
|
+
primary2: '#b8a9d9',
|
|
542
|
+
primary3: '#e8a4c9',
|
|
543
|
+
primary4: '#7eb8da',
|
|
544
|
+
primary5: '#8ad4d4',
|
|
545
|
+
primary6: '#f0c4a8',
|
|
546
|
+
accent1: '#a855f7',
|
|
547
|
+
accent2: '#ff6b9d',
|
|
548
|
+
accent3: '#64c8ff',
|
|
549
|
+
textPrimary: '#e8e0f0',
|
|
550
|
+
textSecondary: '#c8c0d8',
|
|
551
|
+
textMuted: '#9090b0',
|
|
552
|
+
},
|
|
553
|
+
fonts: {
|
|
554
|
+
heading: "'Inter', sans-serif",
|
|
555
|
+
body: "'Inter', sans-serif",
|
|
556
|
+
mono: "'JetBrains Mono', monospace",
|
|
557
|
+
},
|
|
558
|
+
effects: {
|
|
559
|
+
glowColor: 'rgba(168, 85, 247, 0.15)',
|
|
560
|
+
glowIntensity: 0.3,
|
|
561
|
+
particleColors: ['#c9a0dc', '#a855f7'],
|
|
562
|
+
useNoise: false,
|
|
563
|
+
noiseOpacity: 0,
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
</script>
|
|
568
|
+
|
|
569
|
+
<ThemePicker config={{ themes: myThemes, defaultTheme: 'my-theme' }} />
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
## CSS Variables
|
|
573
|
+
|
|
574
|
+
The theme picker applies these CSS variables to your document:
|
|
575
|
+
|
|
576
|
+
### Background Colors
|
|
577
|
+
- `--bg-deep` - Deepest background
|
|
578
|
+
- `--bg-mid` - Mid-level background
|
|
579
|
+
- `--bg-card` - Card/surface background
|
|
580
|
+
- `--bg-glow` - Glow/highlight background
|
|
581
|
+
- `--bg-overlay` - Overlay background
|
|
582
|
+
|
|
583
|
+
### Primary Colors (1-6)
|
|
584
|
+
- `--primary-1` through `--primary-6`
|
|
585
|
+
|
|
586
|
+
### Accent Colors (1-3)
|
|
587
|
+
- `--accent-1` through `--accent-3`
|
|
588
|
+
|
|
589
|
+
### Text Colors
|
|
590
|
+
- `--text-primary`
|
|
591
|
+
- `--text-secondary`
|
|
592
|
+
- `--text-muted`
|
|
593
|
+
|
|
594
|
+
### Fonts
|
|
595
|
+
- `--font-heading`
|
|
596
|
+
- `--font-body`
|
|
597
|
+
- `--font-mono`
|
|
598
|
+
|
|
599
|
+
### Effects
|
|
600
|
+
- `--shadow-glow`
|
|
601
|
+
- `--glow-color`
|
|
602
|
+
- `--glow-intensity`
|
|
603
|
+
|
|
604
|
+
## Using the Store Directly
|
|
605
|
+
|
|
606
|
+
```svelte
|
|
607
|
+
<script>
|
|
608
|
+
import { themeStore, applyTheme, defaultThemes } from 'svelte-theme-picker';
|
|
609
|
+
|
|
610
|
+
// Subscribe to changes
|
|
611
|
+
$effect(() => {
|
|
612
|
+
const theme = defaultThemes[$themeStore];
|
|
613
|
+
if (theme) {
|
|
614
|
+
console.log('Theme changed to:', theme.name);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Change theme programmatically
|
|
619
|
+
function setDarkMode() {
|
|
620
|
+
themeStore.setTheme('mono');
|
|
621
|
+
}
|
|
622
|
+
</script>
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
## Built-in Themes
|
|
626
|
+
|
|
627
|
+
| ID | Name | Tags | Description |
|
|
628
|
+
|----|------|------|-------------|
|
|
629
|
+
| `dreamy` | Dreamy | dark, pastel | Soft pastels with dreamy atmosphere |
|
|
630
|
+
| `cyberpunk` | Cyberpunk | dark, neon | High contrast neons |
|
|
631
|
+
| `sunset` | Sunset | dark, warm | Warm oranges and purples |
|
|
632
|
+
| `ocean` | Ocean | dark, cool | Deep blues and teals |
|
|
633
|
+
| `mono` | Mono | dark, minimal | Clean monochromatic |
|
|
634
|
+
| `sakura` | Sakura | dark, pastel | Cherry blossom pinks |
|
|
635
|
+
| `aurora` | Aurora | dark, nature | Northern lights greens and purples |
|
|
636
|
+
| `galaxy` | Galaxy | dark, space | Deep space cosmic colors |
|
|
637
|
+
| `milk` | Milk | light, neutral | Clean, creamy light theme |
|
|
638
|
+
| `light` | Light | light, modern | Modern light theme with purple accents |
|
|
639
|
+
|
|
640
|
+
## Performance
|
|
641
|
+
|
|
642
|
+
The theme picker is optimized to minimize impact on your application:
|
|
643
|
+
|
|
644
|
+
- **CSS Containment**: Uses `contain: layout style` to isolate rendering
|
|
645
|
+
- **GPU Acceleration**: Animations use `transform` and `will-change` for smooth 60fps transitions
|
|
646
|
+
- **Batched DOM Updates**: Theme changes use `requestAnimationFrame` to batch all CSS variable updates into a single paint frame
|
|
647
|
+
- **Efficient Transitions**: Only animates specific properties instead of `transition: all`
|
|
648
|
+
- **Minimal Bundle**: ~75KB total including all 10 built-in themes
|
|
649
|
+
|
|
650
|
+
## Accessibility
|
|
651
|
+
|
|
652
|
+
The component follows accessibility best practices:
|
|
653
|
+
|
|
654
|
+
- **Keyboard Navigation**: Press `Escape` to close the theme panel
|
|
655
|
+
- **ARIA Labels**: All interactive elements have proper `aria-label` attributes
|
|
656
|
+
- **Role Attributes**: Theme list uses `role="listbox"` with `role="option"` items
|
|
657
|
+
- **Focus Management**: Proper `aria-expanded` and `aria-haspopup` on trigger button
|
|
658
|
+
- **Screen Reader Support**: Decorative elements marked with `aria-hidden="true"`
|
|
659
|
+
|
|
660
|
+
## Browser Support
|
|
661
|
+
|
|
662
|
+
- Modern browsers (Chrome, Firefox, Safari, Edge)
|
|
663
|
+
- Safari: Full support including `-webkit-backdrop-filter` for blur effects
|
|
664
|
+
- Graceful degradation: localStorage errors are handled silently (e.g., private browsing)
|
|
665
|
+
|
|
666
|
+
## TypeScript
|
|
667
|
+
|
|
668
|
+
Full TypeScript support with exported types:
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
import type {
|
|
672
|
+
Theme,
|
|
673
|
+
ThemeColors,
|
|
674
|
+
ThemeFonts,
|
|
675
|
+
ThemeEffects,
|
|
676
|
+
ThemePickerConfig,
|
|
677
|
+
ThemeCatalog,
|
|
678
|
+
ThemeCatalogEntry,
|
|
679
|
+
ThemeMeta,
|
|
680
|
+
ThemeFilterOptions,
|
|
681
|
+
} from 'svelte-theme-picker';
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
## License
|
|
685
|
+
|
|
686
|
+
MIT
|