source: Klonkt/src/services/ThemeService.js@ 19fbe03

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

Palettes: drop Stone+Terracotta, add Ocean+Candy, rework warm to 'Sand'

  • 8 palettes: klonkt, paper, forest, sunset, Sand (warm paper, clay accent, not yellow; key 'cream'), lilac, Ocean (klonkt navy + blue), Candy (klonkt navy + red).
  • Ocean/Candy reuse the klonkt structure with their own accent (Robin: klonkt-based looks best). CSS blocks added; cream block reworked to Sand.
  • Accents: drop Oranje + Bruin; Rood -> Candy-rood (#e11d48). style.css?v=45.

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 * - Sunset (pink)
9 * - Sand (warm paper, clay accent — key 'cream')
10 * - Lilac (purple)
11 * - Ocean (klonkt navy + blue accent)
12 * - Candy (klonkt navy + candy-red accent)
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 sunset: {
38 name: 'Sunset',
39 light: { paper: '#fdf4f3', ink: '#2e1618', accent: '#d6477f' },
40 dark: { paper: '#1f0a14', ink: '#fce7f3', accent: '#f06fa3' }
41 },
42 // Warm paper — soft off-white + clay accent (deliberately NOT bright yellow).
43 // key stays 'cream' (DB-safe).
44 cream: {
45 name: 'Sand',
46 light: { paper: '#f7f3ec', ink: '#232019', accent: '#bf6a45' },
47 dark: { paper: '#14120d', ink: '#efe9dd', accent: '#d6845f' }
48 },
49 // Ocean & Candy = the klonkt palette (navy + off-white) with a different accent.
50 ocean: {
51 name: 'Ocean',
52 light: { paper: '#f3f1ea', ink: '#11141c', accent: '#1d6fe0' },
53 dark: { paper: '#0b0d12', ink: '#f3f1ea', accent: '#4f9bff' }
54 },
55 candy: {
56 name: 'Candy',
57 light: { paper: '#f3f1ea', ink: '#11141c', accent: '#e11d48' },
58 dark: { paper: '#0b0d12', ink: '#f3f1ea', accent: '#fb5c7d' }
59 },
60 lilac: {
61 name: 'Lilac',
62 light: { paper: '#faf4fb', ink: '#2a1830', accent: '#a855f7' },
63 dark: { paper: '#170a1c', ink: '#f3e2f7', accent: '#c084fc' }
64 }
65 };
66
67 /**
68 * Curated accent palette — admins pick one of these instead of a free-form
69 * hex picker. Keeps the brand consistent and avoids unreadable combinations.
70 * Each color works against both light and dark themes.
71 */
72 // Balanced across the color wheel — fewer greens/blues (4 of 12),
73 // more warm + purple/pink variation. All readable on both light and dark.
74 static ACCENTS = [
75 { key: 'klonkt', name: 'Klonkt-geel', color: '#e8b04b' },
76 { key: 'red', name: 'Candy-rood', color: '#e11d48' },
77 { key: 'amber', name: 'Amber', color: '#d97706' },
78 { key: 'forest', name: 'Groen', color: '#16a34a' },
79 { key: 'teal', name: 'Turquoise', color: '#0d9488' },
80 { key: 'ocean', name: 'Blauw', color: '#2563eb' },
81 { key: 'indigo', name: 'Indigo', color: '#4f46e5' },
82 { key: 'violet', name: 'Violet', color: '#7c3aed' },
83 { key: 'plum', name: 'Magenta', color: '#c026d3' },
84 { key: 'pink', name: 'Roze', color: '#db2777' },
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.