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

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

Palettes overhaul: neutral 'Klonkt' default + 7 real-colour themes

  • Default is now the clean neutral (white/black + gold accent), renamed 'Klonkt' (the old navy Klonkt is gone; former 'Paper' merged into it).
  • 7 real-colour palettes (tinted like forest/lilac, not navy-with-accent): Forest, Ocean (blue), Teal, Lilac, Sunset, Candy (red), Amber.
  • Dropped Sand/cream + Paper from the picker. style.css?v=46.

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