source: Klonkt/src/services/ThemeService.js@ 0251bae

main
Last change on this file since 0251bae was 0251bae, checked in by roboburr <roboburr@…>, 3 months ago

feat(themes): more colour variety — fewer greens/blues in accents + palettes

Accents: 12 redistributed across the colour wheel (red/orange/amber/gold/green/
teal/blue/indigo/violet/magenta/pink/brown) — green+blue reduced from 6 to 4.
Palettes: 'slate' (2nd blue) -> warm Gold, 'mint' (2nd green) -> warm Terracotta;
keys unchanged so existing sites don't break. style.css blocks + buster v30.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 7.7 KB
Line 
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 // Paper/ink komen 1-op-1 uit de [data-palette]-CSS in style.css (= wat ECHT
19 // wordt toegepast); de accent-stip is een representatieve kleur per palet.
20 static PALETTES = {
21 sage: {
22 name: 'Sage',
23 light: { paper: '#faf8f3', ink: '#1a1a1a', accent: '#c2410c' },
24 dark: { paper: '#1c1a17', ink: '#f4ede0', accent: '#c2410c' }
25 },
26 paper: {
27 name: 'Paper',
28 light: { paper: '#ffffff', ink: '#09090b', accent: '#000000' },
29 dark: { paper: '#0a0a0a', ink: '#fafafa', accent: '#ffffff' }
30 },
31 ocean: {
32 name: 'Ocean',
33 light: { paper: '#f0f6fb', ink: '#0f1e2e', accent: '#0369a1' },
34 dark: { paper: '#0a1929', ink: '#e0f2fe', accent: '#38bdf8' }
35 },
36 forest: {
37 name: 'Forest',
38 light: { paper: '#f2f6ed', ink: '#1a2e15', accent: '#4d7c2a' },
39 dark: { paper: '#0d1f12', ink: '#dcf2d0', accent: '#6fae3f' }
40 },
41 stone: {
42 name: 'Stone',
43 light: { paper: '#f5f0e8', ink: '#2b2218', accent: '#8a6a45' },
44 dark: { paper: '#1a130b', ink: '#f5e9d5', accent: '#b89366' }
45 },
46 midnight: {
47 name: 'Midnight',
48 light: { paper: '#f3f1f8', ink: '#1e1a2e', accent: '#7c5cbf' },
49 dark: { paper: '#0f0a1f', ink: '#e9def7', accent: '#9d88e0' }
50 },
51 sunset: {
52 name: 'Sunset',
53 light: { paper: '#fdf4f3', ink: '#2e1618', accent: '#d6477f' },
54 dark: { paper: '#1f0a14', ink: '#fce7f3', accent: '#f06fa3' }
55 },
56 cream: {
57 name: 'Cream',
58 light: { paper: '#fefaf0', ink: '#2a1f0f', accent: '#d97706' },
59 dark: { paper: '#1a1208', ink: '#fef3d6', accent: '#f0a93a' }
60 },
61 rose: {
62 name: 'Rose',
63 light: { paper: '#fdf2f4', ink: '#2e1419', accent: '#e11d6b' },
64 dark: { paper: '#1f0a0f', ink: '#fce4ea', accent: '#f06b9a' }
65 },
66 // key blijft 'slate' (DB-veilig), maar omgekleurd naar warm Goud — minder blauw.
67 slate: {
68 name: 'Goud',
69 light: { paper: '#fdf8ea', ink: '#2a2410', accent: '#b8860b' },
70 dark: { paper: '#1a1608', ink: '#f7eecd', accent: '#d4a72c' }
71 },
72 // key blijft 'mint' (DB-veilig), maar omgekleurd naar warm Terracotta — minder groen.
73 mint: {
74 name: 'Terracotta',
75 light: { paper: '#faf2ee', ink: '#2e1a12', accent: '#c2410c' },
76 dark: { paper: '#1f120c', ink: '#f7e6da', accent: '#e8783f' }
77 },
78 lilac: {
79 name: 'Lilac',
80 light: { paper: '#faf4fb', ink: '#2a1830', accent: '#a855f7' },
81 dark: { paper: '#170a1c', ink: '#f3e2f7', accent: '#c084fc' }
82 }
83 };
84
85 /**
86 * Curated accent palette — admins pick one of these instead of a free-form
87 * hex picker. Keeps the brand consistent and avoids unreadable combinations.
88 * Each color works against both light and dark themes.
89 */
90 // Evenwichtig over het kleurenwiel — minder groen/blauw (4 van de 12),
91 // meer warme + paars/roze variatie. Allemaal leesbaar op licht én donker.
92 static ACCENTS = [
93 { key: 'red', name: 'Rood', color: '#dc2626' },
94 { key: 'orange', name: 'Oranje', color: '#ea580c' },
95 { key: 'amber', name: 'Amber', color: '#d97706' },
96 { key: 'gold', name: 'Goud', color: '#ca8a04' },
97 { key: 'forest', name: 'Groen', color: '#16a34a' },
98 { key: 'teal', name: 'Turquoise', color: '#0d9488' },
99 { key: 'ocean', name: 'Blauw', color: '#2563eb' },
100 { key: 'indigo', name: 'Indigo', color: '#4f46e5' },
101 { key: 'violet', name: 'Violet', color: '#7c3aed' },
102 { key: 'plum', name: 'Magenta', color: '#c026d3' },
103 { key: 'pink', name: 'Roze', color: '#db2777' },
104 { key: 'brown', name: 'Bruin', color: '#9a3412' },
105 ];
106
107 static listAccents() {
108 return this.ACCENTS;
109 }
110
111 /**
112 * Validate an accent color. Returns the canonical hex if it's in the curated
113 * set (case-insensitive match), or null if it's not. Server-side validation
114 * uses this so we never persist arbitrary hex from the form.
115 */
116 static validateAccent(hex) {
117 if (!hex || typeof hex !== 'string') return null;
118 const target = hex.trim().toLowerCase();
119 const found = this.ACCENTS.find(a => a.color.toLowerCase() === target);
120 return found ? found.color : null;
121 }
122
123 /**
124 * Get palette data
125 */
126 static getPalette(paletteKey) {
127 return this.PALETTES[paletteKey] || this.PALETTES.sage;
128 }
129
130 /**
131 * Get all available palettes (with full color data so a picker UI can
132 * render true previews of paper/ink/accent for both light and dark).
133 */
134 static listPalettes() {
135 return Object.entries(this.PALETTES).map(([key, data]) => ({
136 key,
137 name: data.name,
138 light: data.light,
139 dark: data.dark,
140 }));
141 }
142
143 /**
144 * Generate CSS variables for palette + theme
145 */
146 static generateCSSVars(paletteKey, theme = 'dark', accentColor = null) {
147 const palette = this.getPalette(paletteKey);
148 const colors = theme === 'dark' ? palette.dark : palette.light;
149 const accent = accentColor || colors.accent;
150
151 return `
152 :root {
153 --palette: ${paletteKey};
154 --theme: ${theme};
155 --paper: ${colors.paper};
156 --ink: ${colors.ink};
157 --accent: ${accent};
158 }
159 `.trim();
160 }
161
162 /**
163 * Update user's theme preference
164 */
165 static updateUserTheme(db, userId, theme, palette) {
166 if (!['dark', 'light'].includes(theme)) {
167 throw new Error('Invalid theme. Must be "dark" or "light"');
168 }
169 if (!this.PALETTES[palette]) {
170 throw new Error('Invalid palette');
171 }
172
173 db.prepare(`
174 UPDATE users SET theme = ?, palette = ? WHERE id = ?
175 `).run(theme, palette, userId);
176 }
177
178 /**
179 * Update site's palette + accent
180 */
181 static updateSitePalette(db, siteId, paletteKey, accentColor) {
182 if (!this.PALETTES[paletteKey]) {
183 throw new Error('Invalid palette');
184 }
185 if (!/^#[0-9a-f]{6}$/i.test(accentColor)) {
186 throw new Error('Invalid accent color. Must be hex #RRGGBB');
187 }
188
189 db.prepare(`
190 UPDATE sites SET palette = ?, accent = ? WHERE id = ?
191 `).run(paletteKey, accentColor, siteId);
192 }
193
194 /**
195 * Generate full HTML theme meta tags
196 */
197 static generateThemeMeta(userTheme, userPalette, siteTheme, siteAccent) {
198 const theme = userTheme || siteTheme || 'dark';
199 const palette = userPalette || 'sage';
200 const accent = siteAccent || '#c2410c';
201 const paletteData = this.getPalette(palette);
202 const colors = theme === 'dark' ? paletteData.dark : paletteData.light;
203
204 return {
205 colorScheme: 'dark light',
206 themeColor: accent,
207 appleMobileWebAppStatusBarStyle: 'black-translucent',
208 cssVars: this.generateCSSVars(palette, theme, accent),
209 inline: `
210 <style>
211 html {
212 color-scheme: ${theme === 'dark' ? 'dark light' : 'light dark'};
213 }
214 :root {
215 --paper: ${colors.paper};
216 --ink: ${colors.ink};
217 --accent: ${accent};
218 }
219 </style>
220 <script>
221 // Apply theme ASAP (before paint) to avoid flash
222 (function() {
223 try {
224 const t = localStorage.getItem('pcms-theme') || '${theme}';
225 const p = localStorage.getItem('pcms-palette') || '${palette}';
226 document.documentElement.setAttribute('data-theme', t);
227 document.documentElement.setAttribute('data-palette', p);
228 } catch(e) {}
229 })();
230 </script>
231 `.trim()
232 };
233 }
234}
235
236export default ThemeService;
Note: See TracBrowser for help on using the repository browser.