source: Klonkt/src/services/ThemeService.js@ dd7e2a2

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

feat(theme): default = Klonkt brand (dark navy + yellow, matches klonkt.com)

New 'klonkt' palette (navy #0b0d12 / off-white #f3f1ea) + accent Klonkt yellow
#e8b04b, set as default for new sites + all fallbacks (ThemeService,
admin-sites, site.js, render, auth, server theme_color). site.js palette list
was stale (8) -> now complete (13). style.css [data-palette=klonkt] + buster v31.

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

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