source: Klonkt/src/services/ThemeService.js@ 52215bc

main
Last change on this file since 52215bc was 7bc636b, checked in by Robin <robin@…>, 4 months ago

Initial commit — PrutFolio v1 source (pulled from Hetzner /srv/prutfolio)

  • Property mode set to 100644
File size: 6.2 KB
RevLine 
[7bc636b]1/**
2 * ThemeService — Palette and theme management
3 *
4 * 8 Built-in Palettes (from v9):
5 * - Sage (default, warm crème)
6 * - Paper (minimalist white)
7 * - Ocean (cool blues)
8 * - Forest (greens)
9 * - Stone (grays)
10 * - Midnight (dark blue)
11 * - Sunset (warm oranges)
12 * - Cream (light beige)
13 *
14 * Dark/Light mode toggle stored per user
15 */
16
17class ThemeService {
18 static PALETTES = {
19 sage: {
20 name: 'Sage',
21 light: { paper: '#faf8f3', ink: '#1a1a1a', accent: '#c2410c' },
22 dark: { paper: '#1c1a17', ink: '#f4ede0', accent: '#c2410c' }
23 },
24 paper: {
25 name: 'Paper',
26 light: { paper: '#ffffff', ink: '#09090b', accent: '#000000' },
27 dark: { paper: '#09090b', ink: '#fafafa', accent: '#ffffff' }
28 },
29 ocean: {
30 name: 'Ocean',
31 light: { paper: '#f0f9ff', ink: '#0c2d48', accent: '#0369a1' },
32 dark: { paper: '#001f3f', ink: '#e0f2fe', accent: '#06b6d4' }
33 },
34 forest: {
35 name: 'Forest',
36 light: { paper: '#f0fdf4', ink: '#15803d', accent: '#16a34a' },
37 dark: { paper: '#052e16', ink: '#dcfce7', accent: '#22c55e' }
38 },
39 stone: {
40 name: 'Stone',
41 light: { paper: '#f5f5f5', ink: '#262626', accent: '#737373' },
42 dark: { paper: '#1f1f1f', ink: '#e5e5e5', accent: '#a3a3a3' }
43 },
44 midnight: {
45 name: 'Midnight',
46 light: { paper: '#f8fafc', ink: '#1e293b', accent: '#3b82f6' },
47 dark: { paper: '#0f172a', ink: '#f1f5f9', accent: '#60a5fa' }
48 },
49 sunset: {
50 name: 'Sunset',
51 light: { paper: '#fef3c7', ink: '#92400e', accent: '#f97316' },
52 dark: { paper: '#5a1f08', ink: '#fef3c7', accent: '#fb923c' }
53 },
54 cream: {
55 name: 'Cream',
56 light: { paper: '#fffbf0', ink: '#78350f', accent: '#d97706' },
57 dark: { paper: '#3f2305', ink: '#fffbf0', accent: '#f59e0b' }
58 }
59 };
60
61 /**
62 * Curated accent palette — admins pick one of these instead of a free-form
63 * hex picker. Keeps the brand consistent and avoids unreadable combinations.
64 * Each color works against both light and dark themes.
65 */
66 static ACCENTS = [
67 { key: 'orange', name: 'Oranje', color: '#c2410c' },
68 { key: 'sage', name: 'Salie', color: '#5a8a5a' },
69 { key: 'ocean', name: 'Oceaan', color: '#0369a1' },
70 { key: 'forest', name: 'Bos', color: '#16a34a' },
71 { key: 'plum', name: 'Pruim', color: '#9d3a78' },
72 { key: 'gold', name: 'Goud', color: '#d97706' },
73 { key: 'crimson', name: 'Karmijn', color: '#ef2840' },
74 { key: 'indigo', name: 'Indigo', color: '#6366f1' },
75 ];
76
77 static listAccents() {
78 return this.ACCENTS;
79 }
80
81 /**
82 * Validate an accent color. Returns the canonical hex if it's in the curated
83 * set (case-insensitive match), or null if it's not. Server-side validation
84 * uses this so we never persist arbitrary hex from the form.
85 */
86 static validateAccent(hex) {
87 if (!hex || typeof hex !== 'string') return null;
88 const target = hex.trim().toLowerCase();
89 const found = this.ACCENTS.find(a => a.color.toLowerCase() === target);
90 return found ? found.color : null;
91 }
92
93 /**
94 * Get palette data
95 */
96 static getPalette(paletteKey) {
97 return this.PALETTES[paletteKey] || this.PALETTES.sage;
98 }
99
100 /**
101 * Get all available palettes (with full color data so a picker UI can
102 * render true previews of paper/ink/accent for both light and dark).
103 */
104 static listPalettes() {
105 return Object.entries(this.PALETTES).map(([key, data]) => ({
106 key,
107 name: data.name,
108 light: data.light,
109 dark: data.dark,
110 }));
111 }
112
113 /**
114 * Generate CSS variables for palette + theme
115 */
116 static generateCSSVars(paletteKey, theme = 'dark', accentColor = null) {
117 const palette = this.getPalette(paletteKey);
118 const colors = theme === 'dark' ? palette.dark : palette.light;
119 const accent = accentColor || colors.accent;
120
121 return `
122 :root {
123 --palette: ${paletteKey};
124 --theme: ${theme};
125 --paper: ${colors.paper};
126 --ink: ${colors.ink};
127 --accent: ${accent};
128 }
129 `.trim();
130 }
131
132 /**
133 * Update user's theme preference
134 */
135 static updateUserTheme(db, userId, theme, palette) {
136 if (!['dark', 'light'].includes(theme)) {
137 throw new Error('Invalid theme. Must be "dark" or "light"');
138 }
139 if (!this.PALETTES[palette]) {
140 throw new Error('Invalid palette');
141 }
142
143 db.prepare(`
144 UPDATE users SET theme = ?, palette = ? WHERE id = ?
145 `).run(theme, palette, userId);
146 }
147
148 /**
149 * Update site's palette + accent
150 */
151 static updateSitePalette(db, siteId, paletteKey, accentColor) {
152 if (!this.PALETTES[paletteKey]) {
153 throw new Error('Invalid palette');
154 }
155 if (!/^#[0-9a-f]{6}$/i.test(accentColor)) {
156 throw new Error('Invalid accent color. Must be hex #RRGGBB');
157 }
158
159 db.prepare(`
160 UPDATE sites SET palette = ?, accent = ? WHERE id = ?
161 `).run(paletteKey, accentColor, siteId);
162 }
163
164 /**
165 * Generate full HTML theme meta tags
166 */
167 static generateThemeMeta(userTheme, userPalette, siteTheme, siteAccent) {
168 const theme = userTheme || siteTheme || 'dark';
169 const palette = userPalette || 'sage';
170 const accent = siteAccent || '#c2410c';
171 const paletteData = this.getPalette(palette);
172 const colors = theme === 'dark' ? paletteData.dark : paletteData.light;
173
174 return {
175 colorScheme: 'dark light',
176 themeColor: accent,
177 appleMobileWebAppStatusBarStyle: 'black-translucent',
178 cssVars: this.generateCSSVars(palette, theme, accent),
179 inline: `
180 <style>
181 html {
182 color-scheme: ${theme === 'dark' ? 'dark light' : 'light dark'};
183 }
184 :root {
185 --paper: ${colors.paper};
186 --ink: ${colors.ink};
187 --accent: ${accent};
188 }
189 </style>
190 <script>
191 // Apply theme ASAP (before paint) to avoid flash
192 (function() {
193 try {
194 const t = localStorage.getItem('pcms-theme') || '${theme}';
195 const p = localStorage.getItem('pcms-palette') || '${palette}';
196 document.documentElement.setAttribute('data-theme', t);
197 document.documentElement.setAttribute('data-palette', p);
198 } catch(e) {}
199 })();
200 </script>
201 `.trim()
202 };
203 }
204}
205
206export default ThemeService;
Note: See TracBrowser for help on using the repository browser.