openvibers/openvibe.shared theme-sync.js

theme-sync.js

main 4.7 KB · 130 lines · javascript

1'use strict';
2
3const { BUILTIN_THEMES, DEFAULT_VARS, deriveTokens } = require('./builtin-themes');
4
5// ═══════════════════════════════════════════════════════════════
6// OpenVibe — Theme Sync Engine
7// Isomorphic theme system shared across all OpenVibe services.
8// Works in both Node.js (SSR) and browser environments.
9// ═══════════════════════════════════════════════════════════════
10
11const CSS_VARIABLES = [
12 '--bg-primary', '--bg-secondary', '--bg-tertiary', '--bg-card',
13 '--bg-hover', '--bg-input',
14 '--text-primary', '--text-secondary', '--text-muted',
15 '--accent', '--accent-light', '--accent-dark',
16 '--live-red', '--success', '--warning', '--danger', '--info',
17 '--border', '--border-light', '--shadow', '--shadow-lg',
18 // Derived on every theme (builtin-themes.js deriveTokens); community themes get them too.
19 '--on-accent', '--accent-rgb', '--accent-glow', '--color-scheme', '--accent-strong', '--on-accent-strong',
20];
21
22const NORMALIZED_DEFAULT_VARS = Object.freeze({ ...DEFAULT_VARS });
23
24// ── Dangerous CSS patterns (XSS prevention) ─────────────────
25const DANGEROUS_PATTERNS = [
26 /url\s*\(/i, /expression\s*\(/i, /javascript:/i,
27 /@import/i, /behavior\s*:/i, /var\s*\(/i, /-moz-binding/i,
28];
29
30function sanitizeCssValue(value) {
31 if (typeof value !== 'string') return null;
32 const trimmed = value.trim();
33 if (trimmed.length > 200) return null;
34 for (const pat of DANGEROUS_PATTERNS) {
35 if (pat.test(trimmed)) return null;
36 }
37 return trimmed;
38}
39
40// ── Theme Application ────────────────────────────────────────
41
42/**
43 * Apply a theme's variables to a target (DOM element or plain object).
44 * In browser: pass `document.documentElement` as target.
45 * In Node: pass an empty object to collect computed values.
46 */
47function applyTheme(theme, target) {
48 if (!theme || !theme.variables) return;
49 const vars = deriveTokens({ ...NORMALIZED_DEFAULT_VARS, ...theme.variables }, theme.mode);
50 for (const [key, value] of Object.entries(vars)) {
51 if (!CSS_VARIABLES.includes(key)) continue;
52 const safe = sanitizeCssValue(value);
53 if (!safe) continue;
54 if (target && typeof target.style !== 'undefined' && target.style.setProperty) {
55 // DOM element (browser)
56 target.style.setProperty(key, safe);
57 } else if (typeof target === 'object') {
58 // Plain object (Node SSR)
59 target[key] = safe;
60 }
61 }
62}
63
64/**
65 * Resolve a theme by ID from the built-in catalog.
66 * Falls back to 'vibe' if not found.
67 */
68function resolveBuiltinTheme(themeId) {
69 return BUILTIN_THEMES.find(t => t.id === themeId || t.slug === themeId)
70 || BUILTIN_THEMES[0];
71}
72
73// ── Browser-Only Helpers ─────────────────────────────────────
74// These are safe to call in Node (they no-op if `window` is undefined).
75
76const STORAGE_KEY = 'ov_theme';
77const THEME_API_BASE = 'https://openvibe.network/api/themes';
78
79function loadFromStorage() {
80 if (typeof localStorage === 'undefined') return null;
81 try {
82 const raw = localStorage.getItem(STORAGE_KEY);
83 return raw ? JSON.parse(raw) : null;
84 } catch { return null; }
85}
86
87function saveToStorage(theme) {
88 if (typeof localStorage === 'undefined') return;
89 try {
90 localStorage.setItem(STORAGE_KEY, JSON.stringify({
91 id: theme.id,
92 slug: theme.slug,
93 name: theme.name,
94 mode: theme.mode,
95 variables: theme.variables,
96 }));
97 } catch { /* quota exceeded, etc. */ }
98}
99
100/**
101 * Sync theme choice to the central OpenVibe.Network server.
102 * Requires an auth token. Fails silently if offline.
103 */
104async function syncThemeToServer(themeId, customVars, token) {
105 if (typeof fetch === 'undefined' || !token) return;
106 try {
107 await fetch(`${THEME_API_BASE}/me`, {
108 method: 'PUT',
109 headers: {
110 'Content-Type': 'application/json',
111 'Authorization': `Bearer ${token}`,
112 },
113 body: JSON.stringify({ theme_id: themeId, custom_variables: customVars || null }),
114 });
115 } catch { /* offline, CORS, etc. */ }
116}
117
118module.exports = {
119 CSS_VARIABLES,
120 DEFAULT_VARS: NORMALIZED_DEFAULT_VARS,
121 BUILTIN_THEMES,
122 sanitizeCssValue,
123 applyTheme,
124 resolveBuiltinTheme,
125 loadFromStorage,
126 saveToStorage,
127 syncThemeToServer,
128 STORAGE_KEY,
129};
130