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/dist/catalog.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a simple themes record to a catalog with default metadata
|
|
3
|
+
*/
|
|
4
|
+
export function themesToCatalog(themes, defaultMeta = {}) {
|
|
5
|
+
const catalog = {};
|
|
6
|
+
for (const [id, theme] of Object.entries(themes)) {
|
|
7
|
+
catalog[id] = {
|
|
8
|
+
theme,
|
|
9
|
+
meta: { active: true, ...defaultMeta },
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
return catalog;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Extract just the themes from a catalog (for use with ThemePicker)
|
|
16
|
+
*/
|
|
17
|
+
export function catalogToThemes(catalog) {
|
|
18
|
+
const themes = {};
|
|
19
|
+
for (const [id, entry] of Object.entries(catalog)) {
|
|
20
|
+
themes[id] = entry.theme;
|
|
21
|
+
}
|
|
22
|
+
return themes;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Filter themes from a catalog based on options
|
|
26
|
+
*/
|
|
27
|
+
export function filterCatalog(catalog, options = {}) {
|
|
28
|
+
const { activeOnly = true, tags, anyTags, excludeTags, } = options;
|
|
29
|
+
const filtered = {};
|
|
30
|
+
for (const [id, entry] of Object.entries(catalog)) {
|
|
31
|
+
const meta = entry.meta || {};
|
|
32
|
+
const themeTags = meta.tags || [];
|
|
33
|
+
// Filter by active status
|
|
34
|
+
if (activeOnly && meta.active === false) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Filter by required tags (ALL must match)
|
|
38
|
+
if (tags && tags.length > 0) {
|
|
39
|
+
const hasAllTags = tags.every((tag) => themeTags.includes(tag));
|
|
40
|
+
if (!hasAllTags)
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Filter by any tags (ANY must match)
|
|
44
|
+
if (anyTags && anyTags.length > 0) {
|
|
45
|
+
const hasAnyTag = anyTags.some((tag) => themeTags.includes(tag));
|
|
46
|
+
if (!hasAnyTag)
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Exclude by tags
|
|
50
|
+
if (excludeTags && excludeTags.length > 0) {
|
|
51
|
+
const hasExcludedTag = excludeTags.some((tag) => themeTags.includes(tag));
|
|
52
|
+
if (hasExcludedTag)
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
filtered[id] = entry;
|
|
56
|
+
}
|
|
57
|
+
return filtered;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get only active themes from a catalog as a simple themes record
|
|
61
|
+
*/
|
|
62
|
+
export function getActiveThemes(catalog) {
|
|
63
|
+
return catalogToThemes(filterCatalog(catalog, { activeOnly: true }));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get themes by tag from a catalog
|
|
67
|
+
*/
|
|
68
|
+
export function getThemesByTag(catalog, tag) {
|
|
69
|
+
return catalogToThemes(filterCatalog(catalog, { tags: [tag] }));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get themes by any of the given tags
|
|
73
|
+
*/
|
|
74
|
+
export function getThemesByAnyTag(catalog, tags) {
|
|
75
|
+
return catalogToThemes(filterCatalog(catalog, { anyTags: tags }));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Sort catalog entries by their order property
|
|
79
|
+
*/
|
|
80
|
+
export function sortCatalog(catalog) {
|
|
81
|
+
const entries = Object.entries(catalog);
|
|
82
|
+
entries.sort((a, b) => {
|
|
83
|
+
const orderA = a[1].meta?.order ?? 999;
|
|
84
|
+
const orderB = b[1].meta?.order ?? 999;
|
|
85
|
+
return orderA - orderB;
|
|
86
|
+
});
|
|
87
|
+
return Object.fromEntries(entries);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get all unique tags from a catalog
|
|
91
|
+
*/
|
|
92
|
+
export function getCatalogTags(catalog) {
|
|
93
|
+
const tagSet = new Set();
|
|
94
|
+
for (const entry of Object.values(catalog)) {
|
|
95
|
+
const tags = entry.meta?.tags || [];
|
|
96
|
+
for (const tag of tags) {
|
|
97
|
+
tagSet.add(tag);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return Array.from(tagSet).sort();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Create a catalog entry with defaults
|
|
104
|
+
*/
|
|
105
|
+
export function createCatalogEntry(theme, meta = {}) {
|
|
106
|
+
return {
|
|
107
|
+
theme,
|
|
108
|
+
meta: {
|
|
109
|
+
active: true,
|
|
110
|
+
tags: [],
|
|
111
|
+
...meta,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Merge multiple catalogs (later catalogs override earlier ones)
|
|
117
|
+
*/
|
|
118
|
+
export function mergeCatalogs(...catalogs) {
|
|
119
|
+
return Object.assign({}, ...catalogs);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Type guard to check if a value is a valid theme entry
|
|
123
|
+
*/
|
|
124
|
+
function isValidThemeEntry(entry) {
|
|
125
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const obj = entry;
|
|
129
|
+
if (typeof obj.theme !== 'object' || obj.theme === null) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const theme = obj.theme;
|
|
133
|
+
return (typeof theme.name === 'string' &&
|
|
134
|
+
typeof theme.description === 'string' &&
|
|
135
|
+
typeof theme.colors === 'object' &&
|
|
136
|
+
theme.colors !== null &&
|
|
137
|
+
typeof theme.fonts === 'object' &&
|
|
138
|
+
theme.fonts !== null &&
|
|
139
|
+
typeof theme.effects === 'object' &&
|
|
140
|
+
theme.effects !== null);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Load themes from a JSON object (useful for loading from files)
|
|
144
|
+
* The JSON should match the ThemeCatalog structure
|
|
145
|
+
* @throws {Error} If the JSON is not a valid ThemeCatalog
|
|
146
|
+
*/
|
|
147
|
+
export function loadCatalogFromJSON(json) {
|
|
148
|
+
if (typeof json !== 'object' || json === null) {
|
|
149
|
+
throw new Error('Invalid catalog JSON: expected an object');
|
|
150
|
+
}
|
|
151
|
+
const catalog = {};
|
|
152
|
+
const entries = Object.entries(json);
|
|
153
|
+
for (const [id, entry] of entries) {
|
|
154
|
+
if (!isValidThemeEntry(entry)) {
|
|
155
|
+
throw new Error(`Invalid theme entry for "${id}": missing required fields (theme.name, theme.description, theme.colors, theme.fonts, theme.effects)`);
|
|
156
|
+
}
|
|
157
|
+
catalog[id] = entry;
|
|
158
|
+
}
|
|
159
|
+
return catalog;
|
|
160
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as ThemePicker } from './ThemePicker.svelte';
|
|
2
|
+
export type { Theme, ThemeColors, ThemeFonts, ThemeEffects, ThemePickerConfig, ThemeMeta, ThemeCatalogEntry, ThemeCatalog, ThemeFilterOptions, } from './types.js';
|
|
3
|
+
export { createThemeStore, applyTheme, themeStore, type ThemeStore, } from './store.js';
|
|
4
|
+
export { defaultThemes, defaultThemeCatalog, DEFAULT_THEME_ID, dreamy, cyberpunk, sunset, ocean, mono, sakura, aurora, galaxy, milk, light, } from './themes.js';
|
|
5
|
+
export { themesToCatalog, catalogToThemes, filterCatalog, getActiveThemes, getThemesByTag, getThemesByAnyTag, sortCatalog, getCatalogTags, createCatalogEntry, mergeCatalogs, loadCatalogFromJSON, } from './catalog.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export { default as ThemePicker } from './ThemePicker.svelte';
|
|
3
|
+
// Store
|
|
4
|
+
export { createThemeStore, applyTheme, themeStore, } from './store.js';
|
|
5
|
+
// Default themes
|
|
6
|
+
export { defaultThemes, defaultThemeCatalog, DEFAULT_THEME_ID, dreamy, cyberpunk, sunset, ocean, mono, sakura, aurora, galaxy, milk, light, } from './themes.js';
|
|
7
|
+
// Catalog utilities
|
|
8
|
+
export { themesToCatalog, catalogToThemes, filterCatalog, getActiveThemes, getThemesByTag, getThemesByAnyTag, sortCatalog, getCatalogTags, createCatalogEntry, mergeCatalogs, loadCatalogFromJSON, } from './catalog.js';
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Writable } from 'svelte/store';
|
|
2
|
+
import type { Theme, ThemePickerConfig } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Theme store interface with preview support
|
|
5
|
+
*/
|
|
6
|
+
export interface ThemeStore extends Writable<string> {
|
|
7
|
+
/** Set the current theme by ID (persists to localStorage) */
|
|
8
|
+
setTheme: (themeId: string) => void;
|
|
9
|
+
/** Get a theme by ID */
|
|
10
|
+
getTheme: (themeId: string) => Theme | undefined;
|
|
11
|
+
/** Get all available themes */
|
|
12
|
+
getAllThemes: () => Record<string, Theme>;
|
|
13
|
+
/** Get the current theme ID (may be preview theme) */
|
|
14
|
+
getCurrentThemeId: () => string;
|
|
15
|
+
/** Get the persisted theme ID (ignores preview) */
|
|
16
|
+
getPersistedThemeId: () => string;
|
|
17
|
+
/** Preview a theme temporarily without persisting */
|
|
18
|
+
previewTheme: (themeId: string) => void;
|
|
19
|
+
/** Revert from preview back to the persisted theme */
|
|
20
|
+
revertPreview: () => void;
|
|
21
|
+
/** Check if currently in preview mode */
|
|
22
|
+
isPreviewMode: () => boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a theme store with the given configuration
|
|
26
|
+
*/
|
|
27
|
+
export declare function createThemeStore(config?: ThemePickerConfig): ThemeStore;
|
|
28
|
+
/**
|
|
29
|
+
* Apply a theme's CSS variables to the document.
|
|
30
|
+
* Uses requestAnimationFrame to batch DOM updates for better performance.
|
|
31
|
+
*/
|
|
32
|
+
export declare function applyTheme(theme: Theme, prefix?: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Default theme store instance
|
|
35
|
+
*/
|
|
36
|
+
export declare const themeStore: ThemeStore;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
import { defaultThemes, DEFAULT_THEME_ID } from './themes.js';
|
|
3
|
+
const isBrowser = typeof window !== 'undefined';
|
|
4
|
+
/**
|
|
5
|
+
* Create a theme store with the given configuration
|
|
6
|
+
*/
|
|
7
|
+
export function createThemeStore(config = {}) {
|
|
8
|
+
const { storageKey = 'svelte-theme-picker', defaultTheme = DEFAULT_THEME_ID, themes = defaultThemes, } = config;
|
|
9
|
+
function getInitialTheme() {
|
|
10
|
+
if (isBrowser) {
|
|
11
|
+
try {
|
|
12
|
+
const stored = localStorage.getItem(storageKey);
|
|
13
|
+
if (stored && themes[stored]) {
|
|
14
|
+
return stored;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (_error) {
|
|
18
|
+
// localStorage may be unavailable (private browsing, storage quota exceeded, etc.)
|
|
19
|
+
// Fall through to default theme
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return themes[defaultTheme] ? defaultTheme : Object.keys(themes)[0] || DEFAULT_THEME_ID;
|
|
23
|
+
}
|
|
24
|
+
let persistedThemeId = getInitialTheme();
|
|
25
|
+
let currentThemeId = persistedThemeId;
|
|
26
|
+
let previewMode = false;
|
|
27
|
+
const { subscribe, set, update } = writable(currentThemeId);
|
|
28
|
+
const store = {
|
|
29
|
+
subscribe,
|
|
30
|
+
set,
|
|
31
|
+
update,
|
|
32
|
+
setTheme: (themeId) => {
|
|
33
|
+
if (themes[themeId]) {
|
|
34
|
+
persistedThemeId = themeId;
|
|
35
|
+
currentThemeId = themeId;
|
|
36
|
+
previewMode = false;
|
|
37
|
+
set(themeId);
|
|
38
|
+
if (isBrowser) {
|
|
39
|
+
try {
|
|
40
|
+
localStorage.setItem(storageKey, themeId);
|
|
41
|
+
}
|
|
42
|
+
catch (_error) {
|
|
43
|
+
// localStorage may be unavailable (private browsing, storage quota exceeded, etc.)
|
|
44
|
+
// Theme is still applied in memory, just won't persist across sessions
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
getTheme: (themeId) => themes[themeId],
|
|
50
|
+
getAllThemes: () => themes,
|
|
51
|
+
getCurrentThemeId: () => currentThemeId,
|
|
52
|
+
getPersistedThemeId: () => persistedThemeId,
|
|
53
|
+
previewTheme: (themeId) => {
|
|
54
|
+
if (themes[themeId]) {
|
|
55
|
+
currentThemeId = themeId;
|
|
56
|
+
previewMode = true;
|
|
57
|
+
set(themeId);
|
|
58
|
+
// Note: Does NOT persist to localStorage
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
revertPreview: () => {
|
|
62
|
+
if (previewMode) {
|
|
63
|
+
currentThemeId = persistedThemeId;
|
|
64
|
+
previewMode = false;
|
|
65
|
+
set(persistedThemeId);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
isPreviewMode: () => previewMode,
|
|
69
|
+
};
|
|
70
|
+
return store;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Apply a theme's CSS variables to the document.
|
|
74
|
+
* Uses requestAnimationFrame to batch DOM updates for better performance.
|
|
75
|
+
*/
|
|
76
|
+
export function applyTheme(theme, prefix = '') {
|
|
77
|
+
if (!isBrowser)
|
|
78
|
+
return;
|
|
79
|
+
// Use requestAnimationFrame to batch all CSS variable updates into a single frame
|
|
80
|
+
// This prevents multiple repaints/reflows during theme transitions
|
|
81
|
+
requestAnimationFrame(() => {
|
|
82
|
+
const root = document.documentElement;
|
|
83
|
+
const p = prefix ? `${prefix}-` : '';
|
|
84
|
+
// Background colors
|
|
85
|
+
root.style.setProperty(`--${p}bg-deep`, theme.colors.bgDeep);
|
|
86
|
+
root.style.setProperty(`--${p}bg-mid`, theme.colors.bgMid);
|
|
87
|
+
root.style.setProperty(`--${p}bg-card`, theme.colors.bgCard);
|
|
88
|
+
root.style.setProperty(`--${p}bg-glow`, theme.colors.bgGlow);
|
|
89
|
+
root.style.setProperty(`--${p}bg-overlay`, theme.colors.bgOverlay);
|
|
90
|
+
// Primary palette
|
|
91
|
+
root.style.setProperty(`--${p}primary-1`, theme.colors.primary1);
|
|
92
|
+
root.style.setProperty(`--${p}primary-2`, theme.colors.primary2);
|
|
93
|
+
root.style.setProperty(`--${p}primary-3`, theme.colors.primary3);
|
|
94
|
+
root.style.setProperty(`--${p}primary-4`, theme.colors.primary4);
|
|
95
|
+
root.style.setProperty(`--${p}primary-5`, theme.colors.primary5);
|
|
96
|
+
root.style.setProperty(`--${p}primary-6`, theme.colors.primary6);
|
|
97
|
+
// Accent colors
|
|
98
|
+
root.style.setProperty(`--${p}accent-1`, theme.colors.accent1);
|
|
99
|
+
root.style.setProperty(`--${p}accent-2`, theme.colors.accent2);
|
|
100
|
+
root.style.setProperty(`--${p}accent-3`, theme.colors.accent3);
|
|
101
|
+
// Text colors
|
|
102
|
+
root.style.setProperty(`--${p}text-primary`, theme.colors.textPrimary);
|
|
103
|
+
root.style.setProperty(`--${p}text-secondary`, theme.colors.textSecondary);
|
|
104
|
+
root.style.setProperty(`--${p}text-muted`, theme.colors.textMuted);
|
|
105
|
+
// Fonts
|
|
106
|
+
root.style.setProperty(`--${p}font-heading`, theme.fonts.heading);
|
|
107
|
+
root.style.setProperty(`--${p}font-body`, theme.fonts.body);
|
|
108
|
+
root.style.setProperty(`--${p}font-mono`, theme.fonts.mono);
|
|
109
|
+
// Effects
|
|
110
|
+
root.style.setProperty(`--${p}shadow-glow`, `0 0 40px ${theme.effects.glowColor}`);
|
|
111
|
+
root.style.setProperty(`--${p}glow-color`, theme.effects.glowColor);
|
|
112
|
+
root.style.setProperty(`--${p}glow-intensity`, theme.effects.glowIntensity.toString());
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Default theme store instance
|
|
117
|
+
*/
|
|
118
|
+
export const themeStore = createThemeStore();
|
package/dist/themes.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Theme, ThemeCatalog } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Dreamy pastel theme with soft purples and pinks
|
|
4
|
+
*/
|
|
5
|
+
export declare const dreamy: Theme;
|
|
6
|
+
/**
|
|
7
|
+
* High contrast cyberpunk neon theme
|
|
8
|
+
*/
|
|
9
|
+
export declare const cyberpunk: Theme;
|
|
10
|
+
/**
|
|
11
|
+
* Warm sunset oranges and purples
|
|
12
|
+
*/
|
|
13
|
+
export declare const sunset: Theme;
|
|
14
|
+
/**
|
|
15
|
+
* Deep ocean blues and teals
|
|
16
|
+
*/
|
|
17
|
+
export declare const ocean: Theme;
|
|
18
|
+
/**
|
|
19
|
+
* Clean monochromatic with purple accents
|
|
20
|
+
*/
|
|
21
|
+
export declare const mono: Theme;
|
|
22
|
+
/**
|
|
23
|
+
* Cherry blossom inspired pinks
|
|
24
|
+
*/
|
|
25
|
+
export declare const sakura: Theme;
|
|
26
|
+
/**
|
|
27
|
+
* Aurora borealis greens and purples
|
|
28
|
+
*/
|
|
29
|
+
export declare const aurora: Theme;
|
|
30
|
+
/**
|
|
31
|
+
* Deep space with cosmic nebulae
|
|
32
|
+
*/
|
|
33
|
+
export declare const galaxy: Theme;
|
|
34
|
+
/**
|
|
35
|
+
* Clean, creamy light theme
|
|
36
|
+
*/
|
|
37
|
+
export declare const milk: Theme;
|
|
38
|
+
/**
|
|
39
|
+
* Modern light theme with purple accents
|
|
40
|
+
*/
|
|
41
|
+
export declare const light: Theme;
|
|
42
|
+
/**
|
|
43
|
+
* Default themes included with the package
|
|
44
|
+
*/
|
|
45
|
+
export declare const defaultThemes: Record<string, Theme>;
|
|
46
|
+
/**
|
|
47
|
+
* Default theme ID
|
|
48
|
+
*/
|
|
49
|
+
export declare const DEFAULT_THEME_ID: "dreamy";
|
|
50
|
+
/**
|
|
51
|
+
* Default themes as a catalog with metadata
|
|
52
|
+
* This format supports filtering by tags, active status, etc.
|
|
53
|
+
*/
|
|
54
|
+
export declare const defaultThemeCatalog: ThemeCatalog;
|