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.
@@ -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
+ }
@@ -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';
@@ -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();
@@ -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;