source: Klonkt/src/services/ThemeService.js@ 434c2e9

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

Trim palettes to 8 (drop Sage/Rose/Midnight near-duplicates)

Sage shared Terracotta's exact accent (#c2410c); Rose ~ Sunset; Midnight ~ Lilac.
Keep 8 distinct: Klonkt, Paper, Forest, Stone, Sunset, Cream, Terracotta, Lilac.
Sites on a removed palette are migrated to the kept twin (sage->mint, rose->sunset,
midnight->lilac) so nothing visibly changes.

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

  • Property mode set to 100644
File size: 7.0 KB
Line 
1/**
2 * ThemeService — Palette and theme management
3 *
4 * 8 Built-in Palettes (curated, no near-duplicates):
5 * - Klonkt (default, navy + gold brand)
6 * - Paper (minimalist white)
7 * - Forest (greens)
8 * - Stone (warm grays)
9 * - Sunset (pink)
10 * - Cream (amber beige)
11 * - Terracotta (burnt orange, key 'mint')
12 * - Lilac (purple)
13 *
14 * Dark/Light mode toggle stored per user
15 */
16
17class ThemeService {
18 // Paper/ink values map 1-to-1 from the [data-palette] CSS in style.css (= what
19 // is ACTUALLY applied); the accent dot is a representative color per palette.
20 static PALETTES = {
21 // Brand default — matches klonkt.com (dark blue + gold).
22 klonkt: {
23 name: 'Klonkt',
24 light: { paper: '#f3f1ea', ink: '#11141c', accent: '#c98a2a' },
25 dark: { paper: '#0b0d12', ink: '#f3f1ea', accent: '#e8b04b' }
26 },
27 paper: {
28 name: 'Paper',
29 light: { paper: '#ffffff', ink: '#09090b', accent: '#000000' },
30 dark: { paper: '#0a0a0a', ink: '#fafafa', accent: '#ffffff' }
31 },
32 forest: {
33 name: 'Forest',
34 light: { paper: '#f2f6ed', ink: '#1a2e15', accent: '#4d7c2a' },
35 dark: { paper: '#0d1f12', ink: '#dcf2d0', accent: '#6fae3f' }
36 },
37 stone: {
38 name: 'Stone',
39 light: { paper: '#f5f0e8', ink: '#2b2218', accent: '#8a6a45' },
40 dark: { paper: '#1a130b', ink: '#f5e9d5', accent: '#b89366' }
41 },
42 sunset: {
43 name: 'Sunset',
44 light: { paper: '#fdf4f3', ink: '#2e1618', accent: '#d6477f' },
45 dark: { paper: '#1f0a14', ink: '#fce7f3', accent: '#f06fa3' }
46 },
47 cream: {
48 name: 'Cream',
49 light: { paper: '#fefaf0', ink: '#2a1f0f', accent: '#d97706' },
50 dark: { paper: '#1a1208', ink: '#fef3d6', accent: '#f0a93a' }
51 },
52 // key stays 'mint' (DB-safe), but recolored to warm Terracotta — less green.
53 mint: {
54 name: 'Terracotta',
55 light: { paper: '#faf2ee', ink: '#2e1a12', accent: '#c2410c' },
56 dark: { paper: '#1f120c', ink: '#f7e6da', accent: '#e8783f' }
57 },
58 lilac: {
59 name: 'Lilac',
60 light: { paper: '#faf4fb', ink: '#2a1830', accent: '#a855f7' },
61 dark: { paper: '#170a1c', ink: '#f3e2f7', accent: '#c084fc' }
62 }
63 };
64
65 /**
66 * Curated accent palette — admins pick one of these instead of a free-form
67 * hex picker. Keeps the brand consistent and avoids unreadable combinations.
68 * Each color works against both light and dark themes.
69 */
70 // Balanced across the color wheel — fewer greens/blues (4 of 12),
71 // more warm + purple/pink variation. All readable on both light and dark.
72 static ACCENTS = [
73 { key: 'klonkt', name: 'Klonkt-geel', color: '#e8b04b' },
74 { key: 'red', name: 'Rood', color: '#dc2626' },
75 { key: 'orange', name: 'Oranje', color: '#ea580c' },
76 { key: 'amber', name: 'Amber', color: '#d97706' },
77 { key: 'forest', name: 'Groen', color: '#16a34a' },
78 { key: 'teal', name: 'Turquoise', color: '#0d9488' },
79 { key: 'ocean', name: 'Blauw', color: '#2563eb' },
80 { key: 'indigo', name: 'Indigo', color: '#4f46e5' },
81 { key: 'violet', name: 'Violet', color: '#7c3aed' },
82 { key: 'plum', name: 'Magenta', color: '#c026d3' },
83 { key: 'pink', name: 'Roze', color: '#db2777' },
84 { key: 'brown', name: 'Bruin', color: '#9a3412' },
85 ];
86
87 static listAccents() {
88 return this.ACCENTS;
89 }
90
91 /**
92 * Validate an accent color. Returns the canonical hex if it's in the curated
93 * set (case-insensitive match), or null if it's not. Server-side validation
94 * uses this so we never persist arbitrary hex from the form.
95 */
96 static validateAccent(hex) {
97 if (!hex || typeof hex !== 'string') return null;
98 const target = hex.trim().toLowerCase();
99 const found = this.ACCENTS.find(a => a.color.toLowerCase() === target);
100 return found ? found.color : null;
101 }
102
103 /**
104 * Get palette data
105 */
106 static getPalette(paletteKey) {
107 return this.PALETTES[paletteKey] || this.PALETTES.klonkt;
108 }
109
110 /**
111 * Get all available palettes (with full color data so a picker UI can
112 * render true previews of paper/ink/accent for both light and dark).
113 */
114 static listPalettes() {
115 return Object.entries(this.PALETTES).map(([key, data]) => ({
116 key,
117 name: data.name,
118 light: data.light,
119 dark: data.dark,
120 }));
121 }
122
123 /**
124 * Generate CSS variables for palette + theme
125 */
126 static generateCSSVars(paletteKey, theme = 'dark', accentColor = null) {
127 const palette = this.getPalette(paletteKey);
128 const colors = theme === 'dark' ? palette.dark : palette.light;
129 const accent = accentColor || colors.accent;
130
131 return `
132 :root {
133 --palette: ${paletteKey};
134 --theme: ${theme};
135 --paper: ${colors.paper};
136 --ink: ${colors.ink};
137 --accent: ${accent};
138 }
139 `.trim();
140 }
141
142 /**
143 * Update user's theme preference
144 */
145 static updateUserTheme(db, userId, theme, palette) {
146 if (!['dark', 'light'].includes(theme)) {
147 throw new Error('Invalid theme. Must be "dark" or "light"');
148 }
149 if (!this.PALETTES[palette]) {
150 throw new Error('Invalid palette');
151 }
152
153 db.prepare(`
154 UPDATE users SET theme = ?, palette = ? WHERE id = ?
155 `).run(theme, palette, userId);
156 }
157
158 /**
159 * Update site's palette + accent
160 */
161 static updateSitePalette(db, siteId, paletteKey, accentColor) {
162 if (!this.PALETTES[paletteKey]) {
163 throw new Error('Invalid palette');
164 }
165 if (!/^#[0-9a-f]{6}$/i.test(accentColor)) {
166 throw new Error('Invalid accent color. Must be hex #RRGGBB');
167 }
168
169 db.prepare(`
170 UPDATE sites SET palette = ?, accent = ? WHERE id = ?
171 `).run(paletteKey, accentColor, siteId);
172 }
173
174 /**
175 * Generate full HTML theme meta tags
176 */
177 static generateThemeMeta(userTheme, userPalette, siteTheme, siteAccent) {
178 const theme = userTheme || siteTheme || 'dark';
179 const palette = userPalette || 'klonkt';
180 const accent = siteAccent || '#e8b04b';
181 const paletteData = this.getPalette(palette);
182 const colors = theme === 'dark' ? paletteData.dark : paletteData.light;
183
184 return {
185 colorScheme: 'dark light',
186 themeColor: accent,
187 appleMobileWebAppStatusBarStyle: 'black-translucent',
188 cssVars: this.generateCSSVars(palette, theme, accent),
189 inline: `
190 <style>
191 html {
192 color-scheme: ${theme === 'dark' ? 'dark light' : 'light dark'};
193 }
194 :root {
195 --paper: ${colors.paper};
196 --ink: ${colors.ink};
197 --accent: ${accent};
198 }
199 </style>
200 <script>
201 // Apply theme ASAP (before paint) to avoid flash
202 (function() {
203 try {
204 const t = localStorage.getItem('pcms-theme') || '${theme}';
205 const p = localStorage.getItem('pcms-palette') || '${palette}';
206 document.documentElement.setAttribute('data-theme', t);
207 document.documentElement.setAttribute('data-palette', p);
208 } catch(e) {}
209 })();
210 </script>
211 `.trim()
212 };
213 }
214}
215
216export default ThemeService;
Note: See TracBrowser for help on using the repository browser.