diff --git a/app/app.py b/app/app.py
index 11f84f2..0509151 100644
--- a/app/app.py
+++ b/app/app.py
@@ -3,6 +3,7 @@
from app.export import export_site
app = rx.App(
+ enable_state=False,
head_components=[
rx.el.script(src="/fuse/fuse-init.js", type="module"),
rx.el.script(src="/fuse/searchFunction.js"),
diff --git a/app/engine/actions.py b/app/engine/actions.py
index e017180..c1e467d 100644
--- a/app/engine/actions.py
+++ b/app/engine/actions.py
@@ -6,221 +6,178 @@
from app.registry.styles import STYLE_REGISTRY
from app.registry.themes import BASE_THEMES
+# ── shared engine (inlined once per action via _engine_js()) ──────────────────
+# NOTE: We intentionally inline the engine into each action rather than
+# relying on window.__ globals surviving re-renders. Each action is
+# self-contained and safe to call at any time.
-def _engine_js() -> str:
- """
- Inline the full theme engine (registries + rebuildTheme + helpers).
- Used in every action so nothing depends on window.__ globals surviving re-renders.
- """
- style_registry_js = json.dumps(STYLE_REGISTRY)
- base_themes_js = json.dumps(BASE_THEMES)
- color_themes_js = json.dumps(COLOR_THEMES)
- font_registry_js = json.dumps(FONT_REGISTRY)
- radius_options_js = json.dumps(RADIUS_OPTIONS)
+def _engine_js() -> str:
return f"""
- const _STYLE_REGISTRY = {style_registry_js};
- const _BASE_THEMES = {base_themes_js};
- const _COLOR_THEMES = {color_themes_js};
- const _FONT_REGISTRY = {font_registry_js};
- const _RADIUS_OPTIONS = {radius_options_js};
- const _CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
-
- function _flattenVars(obj) {{
+ const _SR = {json.dumps(STYLE_REGISTRY)};
+ const _BT = {json.dumps(BASE_THEMES)};
+ const _CT = {json.dumps(COLOR_THEMES)};
+ const _FR = {json.dumps(FONT_REGISTRY)};
+ const _RO = {json.dumps(RADIUS_OPTIONS)};
+ const _CH = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+
+ function _flat(obj) {{
const out = {{}};
- for (const [k, v] of Object.entries(obj)) {{
+ for (const [k, v] of Object.entries(obj))
out[k === 'radius' ? '--radius' : '--' + k] = v;
- }}
return out;
}}
- function _toBase62(n, len) {{
- let res = "";
- for (let i = 0; i < len; i++) {{
- res += _CHARS[n % 62];
- n = Math.floor(n / 62);
- }}
- return res;
+ function _b62e(n, len) {{
+ let r = '';
+ for (let i = 0; i < len; i++) {{ r += _CH[n % 62]; n = Math.floor(n / 62); }}
+ return r;
}}
- function _fromBase62(s) {{
+ function _b62d(s) {{
let n = 0;
- for (let i = s.length - 1; i >= 0; i--) {{
- n = n * 62 + _CHARS.indexOf(s[i]);
- }}
+ for (let i = s.length - 1; i >= 0; i--) n = n * 62 + _CH.indexOf(s[i]);
return n;
}}
- function _rebuildTheme(config) {{
- const {{ baseId, colorId, chartId, styleId, fontId, radius, darkMode }} = config;
- const base = _BASE_THEMES.find(b => b.id === baseId);
- if (!base) return {{}};
+ function _decode(seed) {{
+ if (!seed) return null;
+ if (seed === 'b0') return {{
+ baseId: _BT[0].id, colorId: null, chartId: null,
+ styleId: _SR[0].id, fontId: _FR[0].id, radius: _RO[2][1]
+ }};
+ if (seed.length !== 9) return null;
+ const n = _b62d(seed.substring(0, 4));
+ const cs = _b62d(seed.substring(4));
+ if (cs !== (n * 12345) % 916132832 || n >= 72600) return null;
+ let t = n;
+ const rI = t % 4; t = Math.floor(t / 4);
+ const fI = t % 5; t = Math.floor(t / 5);
+ const sI = t % 5; t = Math.floor(t / 5);
+ const chI = t % 11; t = Math.floor(t / 11);
+ const cI = t % 11; t = Math.floor(t / 11);
+ const bI = t % 6;
+ return {{
+ baseId: _BT[bI].id,
+ colorId: cI > 0 ? _CT[cI - 1].id : null,
+ chartId: chI > 0 ? _CT[chI - 1].id : null,
+ styleId: _SR[sI].id,
+ fontId: _FR[fI].id,
+ radius: _RO[rI][1],
+ }};
+ }}
- const baseVars = _flattenVars(darkMode ? base.dark : base.light);
- let theme = {{ ...baseVars, '__base_id': baseId, '__base_label': base.label }};
+ function _encode(cfg) {{
+ const bI = _BT.findIndex(b => b.id === cfg['__base_id']);
+ const cI = cfg['__color_id'] ? _CT.findIndex(c => c.id === cfg['__color_id']) + 1 : 0;
+ const chI = cfg['__chart_id'] ? _CT.findIndex(c => c.id === cfg['__chart_id']) + 1 : 0;
+ const sI = _SR.findIndex(s => s.id === cfg['__style_id']);
+ const fI = _FR.findIndex(f => f.id === cfg['__font_id']);
+ const rI = _RO.findIndex(r => r[1] === cfg['--radius']);
+ if (bI < 0 || sI < 0 || fI < 0 || rI < 0) return null;
+ if (bI === 0 && cI === 0 && chI === 0 && sI === 0 && fI === 0 && rI === 2) return 'b0';
+ const n = (((((bI * 11 + cI) * 11 + chI) * 5 + sI) * 5 + fI) * 4 + rI);
+ return _b62e(n, 4) + _b62e((n * 12345) % 916132832, 5);
+ }}
+ function _rebuild(cfg) {{
+ const {{ baseId, colorId, chartId, styleId, fontId, radius, darkMode }} = cfg;
+ const base = _BT.find(b => b.id === baseId);
+ if (!base) return {{}};
+ let th = {{ ..._flat(darkMode ? base.dark : base.light), '__base_id': baseId, '__base_label': base.label }};
if (colorId) {{
- const color = _COLOR_THEMES.find(c => c.id === colorId);
- if (color) {{
- const cvars = _flattenVars(darkMode ? color.dark : color.light);
- for (const [k, v] of Object.entries(cvars)) {{
- if (!k.startsWith('--chart-')) theme[k] = v;
- }}
- theme['__color_id'] = colorId;
- theme['__color_label'] = color.label;
+ const col = _CT.find(c => c.id === colorId);
+ if (col) {{
+ for (const [k, v] of Object.entries(_flat(darkMode ? col.dark : col.light)))
+ if (!k.startsWith('--chart-')) th[k] = v;
+ th['__color_id'] = colorId; th['__color_label'] = col.label;
}}
- }} else {{
- theme['__color_id'] = null;
- theme['__color_label'] = null;
- }}
-
+ }} else {{ th['__color_id'] = null; th['__color_label'] = null; }}
if (chartId) {{
- const chart = _COLOR_THEMES.find(c => c.id === chartId);
- if (chart) {{
- const cvars = _flattenVars(darkMode ? chart.dark : chart.light);
- for (const [k, v] of Object.entries(cvars)) {{
- if (k.startsWith('--chart-')) theme[k] = v;
- }}
- theme['__chart_id'] = chartId;
- theme['__chart_label'] = chart.label;
+ const ch = _CT.find(c => c.id === chartId);
+ if (ch) {{
+ for (const [k, v] of Object.entries(_flat(darkMode ? ch.dark : ch.light)))
+ if (k.startsWith('--chart-')) th[k] = v;
+ th['__chart_id'] = chartId; th['__chart_label'] = ch.label;
}}
- }} else {{
- theme['__chart_id'] = null;
- theme['__chart_label'] = null;
- }}
-
+ }} else {{ th['__chart_id'] = null; th['__chart_label'] = null; }}
if (styleId) {{
- const style = _STYLE_REGISTRY.find(s => s.id === styleId);
- if (style) {{
- Object.assign(theme, style.vars);
- theme['__style_id'] = styleId;
- theme['__style_label'] = style.label;
- }}
+ const st = _SR.find(s => s.id === styleId);
+ if (st) {{ Object.assign(th, st.vars); th['__style_id'] = styleId; th['__style_label'] = st.label; }}
}}
-
if (fontId) {{
- const font = _FONT_REGISTRY.find(f => f.id === fontId);
- if (font) {{
- Object.assign(theme, font.vars);
- theme['__font_id'] = fontId;
- theme['__font_label'] = font.label;
- }}
+ const fn = _FR.find(f => f.id === fontId);
+ if (fn) {{ Object.assign(th, fn.vars); th['__font_id'] = fontId; th['__font_label'] = fn.label; }}
}}
-
- if (radius) {{
- theme['--radius'] = radius;
- }}
-
- return theme;
+ if (radius) th['--radius'] = radius;
+ return th;
}}
- function _encodeConfig(theme) {{
- const bIdx = _BASE_THEMES.findIndex(b => b.id === theme['__base_id']);
- const cIdx = theme['__color_id'] ? _COLOR_THEMES.findIndex(c => c.id === theme['__color_id']) + 1 : 0;
- const chIdx = theme['__chart_id'] ? _COLOR_THEMES.findIndex(c => c.id === theme['__chart_id']) + 1 : 0;
- const sIdx = _STYLE_REGISTRY.findIndex(s => s.id === theme['__style_id']);
- const fIdx = _FONT_REGISTRY.findIndex(f => f.id === theme['__font_id']);
- const rIdx = _RADIUS_OPTIONS.findIndex(r => r[1] === theme['--radius']);
-
- if (bIdx === -1 || sIdx === -1 || fIdx === -1 || rIdx === -1) return null;
-
- // Default state special case
- if (bIdx === 0 && cIdx === 0 && chIdx === 0 && sIdx === 0 && fIdx === 0 && rIdx === 2) return "b0";
-
- // State space: 6 * 11 * 11 * 5 * 5 * 4 = 72600
- let n = (((((bIdx * 11 + cIdx) * 11 + chIdx) * 5 + sIdx) * 5 + fIdx) * 4 + rIdx);
-
- // Encode as 9 chars: [Base62(N, 4)] + [Base62(Checksum(N), 5)]
- const checksum = (n * 12345) % 916132832; // 62^5
- return _toBase62(n, 4) + _toBase62(checksum, 5);
- }}
-
- function _decodeSeed(seed) {{
- if (!seed) return null;
- if (seed === "b0") {{
- return {{ baseId: _BASE_THEMES[0].id, colorId: null, chartId: null, styleId: _STYLE_REGISTRY[0].id, fontId: _FONT_REGISTRY[0].id, radius: _RADIUS_OPTIONS[2][1] }};
- }}
- if (seed.length !== 9) return null;
-
- const n = _fromBase62(seed.substring(0, 4));
- const checksum = _fromBase62(seed.substring(4));
-
- if (checksum === (n * 12345) % 916132832 && n < 72600) {{
- let temp = n;
- const rIdx = temp % 4; temp = Math.floor(temp / 4);
- const fIdx = temp % 5; temp = Math.floor(temp / 5);
- const sIdx = temp % 5; temp = Math.floor(temp / 5);
- const chIdx = temp % 11; temp = Math.floor(temp / 11);
- const cIdx = temp % 11; temp = Math.floor(temp / 11);
- const bIdx = temp % 6;
-
- return {{
- baseId: _BASE_THEMES[bIdx].id,
- colorId: cIdx > 0 ? _COLOR_THEMES[cIdx - 1].id : null,
- chartId: chIdx > 0 ? _COLOR_THEMES[chIdx - 1].id : null,
- styleId: _STYLE_REGISTRY[sIdx].id,
- fontId: _FONT_REGISTRY[fIdx].id,
- radius: _RADIUS_OPTIONS[rIdx][1]
+ function _fromSeed(s, dark) {{
+ let cfg = _decode(s);
+ if (!cfg) {{
+ // legacy random seed fallback
+ function _hash(str) {{
+ let h = 0;
+ for (let i = 0; i < str.length; i++) {{ h = (h << 5) - h + str.charCodeAt(i); h |= 0; }}
+ return h >>> 0;
+ }}
+ function _mb32(seed) {{
+ return function() {{
+ let t = (seed += 0x6D2B79F5);
+ t = Math.imul(t ^ (t >>> 15), t | 1);
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+ }};
+ }}
+ const rand = _mb32(_hash(s));
+ cfg = {{
+ baseId: _BT[Math.floor(rand() * _BT.length)].id,
+ colorId: (function(r) {{ const i = Math.floor(r * 11); return i === 0 ? null : _CT[i-1].id; }})(rand()),
+ chartId: (function(r) {{ const i = Math.floor(r * 11); return i === 0 ? null : _CT[i-1].id; }})(rand()),
+ styleId: _SR[Math.floor(rand() * _SR.length)].id,
+ fontId: _FR[Math.floor(rand() * _FR.length)].id,
+ radius: _RO[Math.floor(rand() * _RO.length)][1],
}};
}}
- return null;
+ const th = _rebuild({{ ...cfg, darkMode: dark }});
+ return {{ ...th, '__seed': s, '__dark': dark }};
}}
- function _hashStringToInt(str) {{
- let hash = 0;
- for (let i = 0; i < str.length; i++) {{
- hash = (hash << 5) - hash + str.charCodeAt(i);
- hash |= 0;
- }}
- return hash >>> 0;
+ function _randomSeed() {{
+ const n = Math.floor(Math.random() * 72600);
+ return _b62e(n, 4) + _b62e((n * 12345) % 916132832, 5);
}}
- function _mulberry32(seed) {{
- return function() {{
- let t = (seed += 0x6D2B79F5);
- t = Math.imul(t ^ (t >>> 15), t | 1);
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
- }};
+ // ── KEY FUNCTION: apply CSS vars to preview container only ──────────
+ // Targets #theme-preview-container so site styles are unaffected.
+ // Falls back to :root if container not found (e.g. other pages).
+ function _applyToRoot(config) {{
+ const preview = document.getElementById('theme-preview-container');
+ const target = preview || document.documentElement;
+ for (const [k, v] of Object.entries(config))
+ if (k.startsWith('--')) target.style.setProperty(k, v);
}}
+ """
- function _generateFromSeed(seedString, darkMode) {{
- let config = _decodeSeed(seedString);
- if (!config) {{
- // Legacy support for non-encoded random strings
- const rand = _mulberry32(_hashStringToInt(seedString));
- config = {{
- baseId: _BASE_THEMES[Math.floor(rand() * _BASE_THEMES.length)].id,
- colorId: (function(r){{ let i = Math.floor(r * 11); return i === 0 ? null : _COLOR_THEMES[i-1].id; }})(rand()),
- chartId: (function(r){{ let i = Math.floor(r * 11); return i === 0 ? null : _COLOR_THEMES[i-1].id; }})(rand()),
- styleId: _STYLE_REGISTRY[Math.floor(rand() * _STYLE_REGISTRY.length)].id,
- fontId: _FONT_REGISTRY[Math.floor(rand() * _FONT_REGISTRY.length)].id,
- radius: _RADIUS_OPTIONS[Math.floor(rand() * _RADIUS_OPTIONS.length)][1]
- }};
- }}
- const theme = _rebuildTheme({{ ...config, darkMode }});
- return {{ ...theme, '__seed': seedString, '__dark': darkMode }};
- }}
+def _sync_sidebar_js() -> str:
+ return "if (window.__syncSidebar) window.__syncSidebar(config);"
- function _randomSeed() {{
- const n = Math.floor(Math.random() * 72600);
- const checksum = (n * 12345) % 916132832;
- return _toBase62(n, 4) + _toBase62(checksum, 5);
- }}
- """
+# ── helpers used by multiple actions ─────────────────────────────────────────
-def _sync_sidebar_js() -> str:
- """
- After applying a theme config, sync the sidebar ClientStateVars
- so the dropdowns reflect the current state.
- """
- return """
- if (window.__syncSidebar) window.__syncSidebar(config);
+
+def _set_refs(seed_val: str = "newSeed") -> str:
+ """Sync ClientStateVars that are still needed for reactive UI."""
+ return f"""
+ // Still needed: seed display, copy button, css_output rendering
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed']({seed_val});
"""
+# ── actions ───────────────────────────────────────────────────────────────────
+
SHUFFLE_JS = f"""
(function() {{
{_engine_js()}
@@ -228,13 +185,16 @@ def _sync_sidebar_js() -> str:
const dark = refs['_client_state_darkmode'] || false;
const s = _randomSeed();
- // update seed input if present
const el = document.getElementById('seed-input-el');
if (el) el.value = s;
- const config = _generateFromSeed(s, dark);
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](s);
+ const config = _fromSeed(s, dark);
+
+ // Apply CSS vars directly — no WebSocket needed
+ _applyToRoot(config);
+
+ // Sync remaining refs
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](s);
{_sync_sidebar_js()}
if (window.__updatePresetURL) window.__updatePresetURL(s);
@@ -250,9 +210,11 @@ def _sync_sidebar_js() -> str:
const s = el ? el.value.trim() : '';
if (!s) return;
- const config = _generateFromSeed(s, dark);
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](s);
+ const config = _fromSeed(s, dark);
+
+ _applyToRoot(config);
+
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](s);
{_sync_sidebar_js()}
if (window.__updatePresetURL) window.__updatePresetURL(s);
@@ -264,73 +226,46 @@ def _sync_sidebar_js() -> str:
{_engine_js()}
window.refs = refs;
- // 1. Helper: Robust URL Parsing
- const getPresetFromURL = () => {{
- try {{
- const params = new URLSearchParams(window.location.search);
- return params.get('preset');
- }} catch (e) {{
- return null;
- }}
- }};
-
- // 2. Determine Dark Mode
- const savedTheme = localStorage.getItem('theme');
- let isDark = false;
- if (savedTheme === 'dark') {{
- isDark = true;
- }} else if (savedTheme === 'light') {{
- isDark = false;
- }} else {{
- isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
- }}
+ // 1. Determine dark mode
+ const saved = localStorage.getItem('theme');
+ let isDark = saved === 'dark' ? true
+ : saved === 'light' ? false
+ : window.matchMedia('(prefers-color-scheme: dark)').matches;
- if (isDark) {{
- document.documentElement.classList.add('dark');
- }} else {{
- document.documentElement.classList.remove('dark');
- }}
+ if (isDark) document.documentElement.classList.add('dark');
+ else document.documentElement.classList.remove('dark');
if (refs['_client_state_setDarkmode']) refs['_client_state_setDarkmode'](isDark);
- // 2. Check for Welcome Dialog
- const hasSeenWelcome = localStorage.getItem('has_seen_welcome');
- if (!hasSeenWelcome) {{
- if (refs['_client_state_setWelcome_open']) refs['_client_state_setWelcome_open'](true);
- }}
+ // 2. Welcome dialog
+ if (!localStorage.getItem('has_seen_welcome'))
+ if (refs['_client_state_setWelcome_open']) refs['_client_state_setWelcome_open'](true);
- // 3. Determine Seed (Prioritize URL > current state > default)
- const urlSeed = getPresetFromURL();
- const currentSeed = refs['_client_state_seed'];
- const s = urlSeed || currentSeed || "b0";
+ // 3. Determine seed
+ let urlSeed = null;
+ try {{ urlSeed = new URLSearchParams(window.location.search).get('preset'); }} catch(e) {{}}
+ const s = urlSeed || refs['_client_state_seed'] || 'b0';
- // 4. Generate and Apply Theme
- const config = _generateFromSeed(s, isDark);
+ // 4. Build config and apply CSS vars immediately — before WebSocket matters
+ const config = _fromSeed(s, isDark);
- const applyConfig = () => {{
- if (refs['_client_state_setTheme']) refs['_client_state_setTheme'](config);
- if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](s);
+ function apply() {{
+ // Apply CSS vars to :root — instant, no WebSocket needed
+ _applyToRoot(config);
- // Sync sidebar if the helper exists, otherwise retry soon
- if (window.__syncSidebar) {{
- window.__syncSidebar(config);
- }}
+ // Sync remaining reactive refs
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](s);
- // Ensure URL stays in sync
- if (window.__updatePresetURL) {{
- window.__updatePresetURL(s, false);
- }}
+ if (window.__syncSidebar) window.__syncSidebar(config);
+ if (window.__updatePresetURL) window.__updatePresetURL(s, false);
const el = document.getElementById('seed-input-el');
if (el) el.value = s;
- }};
-
- // Execute sequence
- applyConfig();
+ }}
- // Retries to catch late-binding sidebar/url scripts
- setTimeout(applyConfig, 50);
- setTimeout(applyConfig, 200);
+ apply();
+ setTimeout(apply, 50);
+ setTimeout(apply, 200);
}})();
"""
@@ -338,11 +273,8 @@ def _sync_sidebar_js() -> str:
(function() {{
{_engine_js()}
- const current = refs['_client_state_theme'] || {{}};
- const nowDark = !(current['__dark'] || false);
- refs['_client_state_setDarkmode'](nowDark);
+ const nowDark = !document.documentElement.classList.contains('dark');
- // Toggle root class for Tailwind
if (nowDark) {{
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
@@ -351,40 +283,29 @@ def _sync_sidebar_js() -> str:
localStorage.setItem('theme', 'light');
}}
- let config;
- const s = current['__seed'] || refs['_client_state_seed'] || '';
- if (s) {{
- config = _generateFromSeed(s, nowDark);
- }} else {{
- config = _rebuildTheme({{
- baseId: current['__base_id'] || 'neutral',
- colorId: current['__color_id'] || null,
- chartId: current['__chart_id'] || null,
- styleId: current['__style_id'] || null,
- fontId: current['__font_id'] || null,
- radius: current['--radius'] || null,
- darkMode: nowDark,
- }});
- config = {{ ...current, ...config, '__dark': nowDark }};
- }}
+ if (refs['_client_state_setDarkmode']) refs['_client_state_setDarkmode'](nowDark);
+
+ const s = refs['_client_state_seed'] || 'b0';
+ const config = _fromSeed(s, nowDark);
+
+ _applyToRoot(config);
- refs['_client_state_setTheme'](config);
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](s);
{_sync_sidebar_js()}
}})();
"""
-
RESET_JS = f"""
(function() {{
{_engine_js()}
- const dark = refs['_client_state_darkmode'] || false;
- const s = "b0";
+ const dark = document.documentElement.classList.contains('dark');
+ const s = 'b0';
+ const config = _fromSeed(s, dark);
- const config = _generateFromSeed(s, dark);
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](s);
+ _applyToRoot(config);
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](s);
{_sync_sidebar_js()}
if (window.__updatePresetURL) window.__updatePresetURL(s);
}})();
@@ -395,22 +316,22 @@ def _apply_base_theme_js(base_id: str) -> str:
return f"""
(function() {{
{_engine_js()}
- const current = refs['_client_state_theme'] || {{}};
- const dark = refs['_client_state_darkmode'] || false;
- let config = _rebuildTheme({{
+ const dark = document.documentElement.classList.contains('dark');
+ const cur = refs['_client_state_seed'] || 'b0';
+ const prev = _fromSeed(cur, dark);
+ const config = _rebuild({{
baseId: '{base_id}',
- colorId: current['__color_id'] || null,
- chartId: current['__chart_id'] || null,
- styleId: current['__style_id'] || null,
- fontId: current['__font_id'] || null,
- radius: current['--radius'] || null,
+ colorId: prev['__color_id'] || null,
+ chartId: prev['__chart_id'] || null,
+ styleId: prev['__style_id'] || null,
+ fontId: prev['__font_id'] || null,
+ radius: prev['--radius'] || null,
darkMode: dark,
}});
- const newSeed = _encodeConfig(config);
- config['__seed'] = newSeed;
- config['__dark'] = dark;
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](newSeed);
+ const newSeed = _encode(config) || cur;
+ config['__seed'] = newSeed; config['__dark'] = dark;
+ _applyToRoot(config);
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](newSeed);
{_sync_sidebar_js()}
if (window.__updatePresetURL) window.__updatePresetURL(newSeed);
}})();
@@ -421,22 +342,22 @@ def _apply_color_theme_js(color_id: str) -> str:
return f"""
(function() {{
{_engine_js()}
- const current = refs['_client_state_theme'] || {{}};
- const dark = refs['_client_state_darkmode'] || false;
- let config = _rebuildTheme({{
- baseId: current['__base_id'] || 'neutral',
+ const dark = document.documentElement.classList.contains('dark');
+ const cur = refs['_client_state_seed'] || 'b0';
+ const prev = _fromSeed(cur, dark);
+ const config = _rebuild({{
+ baseId: prev['__base_id'] || 'neutral',
colorId: '{color_id}',
- chartId: current['__chart_id'] || null,
- styleId: current['__style_id'] || null,
- fontId: current['__font_id'] || null,
- radius: current['--radius'] || null,
+ chartId: prev['__chart_id'] || null,
+ styleId: prev['__style_id'] || null,
+ fontId: prev['__font_id'] || null,
+ radius: prev['--radius'] || null,
darkMode: dark,
}});
- const newSeed = _encodeConfig(config);
- config['__seed'] = newSeed;
- config['__dark'] = dark;
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](newSeed);
+ const newSeed = _encode(config) || cur;
+ config['__seed'] = newSeed; config['__dark'] = dark;
+ _applyToRoot(config);
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](newSeed);
{_sync_sidebar_js()}
if (window.__updatePresetURL) window.__updatePresetURL(newSeed);
}})();
@@ -447,22 +368,22 @@ def _apply_chart_color_js(color_id: str) -> str:
return f"""
(function() {{
{_engine_js()}
- const current = refs['_client_state_theme'] || {{}};
- const dark = refs['_client_state_darkmode'] || false;
- let config = _rebuildTheme({{
- baseId: current['__base_id'] || 'neutral',
- colorId: current['__color_id'] || null,
+ const dark = document.documentElement.classList.contains('dark');
+ const cur = refs['_client_state_seed'] || 'b0';
+ const prev = _fromSeed(cur, dark);
+ const config = _rebuild({{
+ baseId: prev['__base_id'] || 'neutral',
+ colorId: prev['__color_id'] || null,
chartId: '{color_id}',
- styleId: current['__style_id'] || null,
- fontId: current['__font_id'] || null,
- radius: current['--radius'] || null,
+ styleId: prev['__style_id'] || null,
+ fontId: prev['__font_id'] || null,
+ radius: prev['--radius'] || null,
darkMode: dark,
}});
- const newSeed = _encodeConfig(config);
- config['__seed'] = newSeed;
- config['__dark'] = dark;
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](newSeed);
+ const newSeed = _encode(config) || cur;
+ config['__seed'] = newSeed; config['__dark'] = dark;
+ _applyToRoot(config);
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](newSeed);
{_sync_sidebar_js()}
if (window.__updatePresetURL) window.__updatePresetURL(newSeed);
}})();
@@ -473,21 +394,22 @@ def _apply_style_js(style_id: str) -> str:
return f"""
(function() {{
{_engine_js()}
- const current = refs['_client_state_theme'] || {{}};
- const dark = refs['_client_state_darkmode'] || false;
- let config = _rebuildTheme({{
- baseId: current['__base_id'] || 'neutral',
- colorId: current['__color_id'] || null,
- chartId: current['__chart_id'] || null,
+ const dark = document.documentElement.classList.contains('dark');
+ const cur = refs['_client_state_seed'] || 'b0';
+ const prev = _fromSeed(cur, dark);
+ const config = _rebuild({{
+ baseId: prev['__base_id'] || 'neutral',
+ colorId: prev['__color_id'] || null,
+ chartId: prev['__chart_id'] || null,
styleId: '{style_id}',
- fontId: current['__font_id'] || null,
+ fontId: prev['__font_id'] || null,
+ radius: prev['--radius'] || null,
darkMode: dark,
}});
- const newSeed = _encodeConfig(config);
- config['__seed'] = newSeed;
- config['__dark'] = dark;
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](newSeed);
+ const newSeed = _encode(config) || cur;
+ config['__seed'] = newSeed; config['__dark'] = dark;
+ _applyToRoot(config);
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](newSeed);
{_sync_sidebar_js()}
if (window.__updatePresetURL) window.__updatePresetURL(newSeed);
}})();
@@ -498,22 +420,48 @@ def _apply_font_js(font_id: str) -> str:
return f"""
(function() {{
{_engine_js()}
- const current = refs['_client_state_theme'] || {{}};
- const dark = refs['_client_state_darkmode'] || false;
- let config = _rebuildTheme({{
- baseId: current['__base_id'] || 'neutral',
- colorId: current['__color_id'] || null,
- chartId: current['__chart_id'] || null,
- styleId: current['__style_id'] || null,
+ const dark = document.documentElement.classList.contains('dark');
+ const cur = refs['_client_state_seed'] || 'b0';
+ const prev = _fromSeed(cur, dark);
+ const config = _rebuild({{
+ baseId: prev['__base_id'] || 'neutral',
+ colorId: prev['__color_id'] || null,
+ chartId: prev['__chart_id'] || null,
+ styleId: prev['__style_id'] || null,
fontId: '{font_id}',
- radius: current['--radius'] || null,
+ radius: prev['--radius'] || null,
+ darkMode: dark,
+ }});
+ const newSeed = _encode(config) || cur;
+ config['__seed'] = newSeed; config['__dark'] = dark;
+ _applyToRoot(config);
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](newSeed);
+ {_sync_sidebar_js()}
+ if (window.__updatePresetURL) window.__updatePresetURL(newSeed);
+ }})();
+ """
+
+
+def _patch_radius_js(value: str) -> str:
+ return f"""
+ (function() {{
+ {_engine_js()}
+ const dark = document.documentElement.classList.contains('dark');
+ const cur = refs['_client_state_seed'] || 'b0';
+ const prev = _fromSeed(cur, dark);
+ const config = _rebuild({{
+ baseId: prev['__base_id'] || 'neutral',
+ colorId: prev['__color_id'] || null,
+ chartId: prev['__chart_id'] || null,
+ styleId: prev['__style_id'] || null,
+ fontId: prev['__font_id'] || null,
+ radius: '{value}',
darkMode: dark,
}});
- const newSeed = _encodeConfig(config);
- config['__seed'] = newSeed;
- config['__dark'] = dark;
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](newSeed);
+ const newSeed = _encode(config) || cur;
+ config['__seed'] = newSeed; config['__dark'] = dark;
+ _applyToRoot(config);
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](newSeed);
{_sync_sidebar_js()}
if (window.__updatePresetURL) window.__updatePresetURL(newSeed);
}})();
@@ -523,21 +471,22 @@ def _apply_font_js(font_id: str) -> str:
APPLY_BASE_PRIMARY_JS = f"""
(function() {{
{_engine_js()}
- const current = refs['_client_state_theme'] || {{}};
- const dark = refs['_client_state_darkmode'] || false;
- let config = _rebuildTheme({{
- baseId: current['__base_id'] || 'neutral',
+ const dark = document.documentElement.classList.contains('dark');
+ const cur = refs['_client_state_seed'] || 'b0';
+ const prev = _fromSeed(cur, dark);
+ const config = _rebuild({{
+ baseId: prev['__base_id'] || 'neutral',
colorId: null,
- chartId: current['__chart_id'] || null,
- styleId: current['__style_id'] || null,
- fontId: current['__font_id'] || null,
+ chartId: prev['__chart_id'] || null,
+ styleId: prev['__style_id'] || null,
+ fontId: prev['__font_id'] || null,
+ radius: prev['--radius'] || null,
darkMode: dark,
}});
- const newSeed = _encodeConfig(config);
- config['__seed'] = newSeed;
- config['__dark'] = dark;
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](newSeed);
+ const newSeed = _encode(config) || cur;
+ config['__seed'] = newSeed; config['__dark'] = dark;
+ _applyToRoot(config);
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](newSeed);
{_sync_sidebar_js()}
if (window.__updatePresetURL) window.__updatePresetURL(newSeed);
}})();
@@ -546,21 +495,22 @@ def _apply_font_js(font_id: str) -> str:
APPLY_BASE_CHARTS_JS = f"""
(function() {{
{_engine_js()}
- const current = refs['_client_state_theme'] || {{}};
- const dark = refs['_client_state_darkmode'] || false;
- let config = _rebuildTheme({{
- baseId: current['__base_id'] || 'neutral',
- colorId: current['__color_id'] || null,
+ const dark = document.documentElement.classList.contains('dark');
+ const cur = refs['_client_state_seed'] || 'b0';
+ const prev = _fromSeed(cur, dark);
+ const config = _rebuild({{
+ baseId: prev['__base_id'] || 'neutral',
+ colorId: prev['__color_id'] || null,
chartId: null,
- styleId: current['__style_id'] || null,
- fontId: current['__font_id'] || null,
+ styleId: prev['__style_id'] || null,
+ fontId: prev['__font_id'] || null,
+ radius: prev['--radius'] || null,
darkMode: dark,
}});
- const newSeed = _encodeConfig(config);
- config['__seed'] = newSeed;
- config['__dark'] = dark;
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](newSeed);
+ const newSeed = _encode(config) || cur;
+ config['__seed'] = newSeed; config['__dark'] = dark;
+ _applyToRoot(config);
+ if (refs['_client_state_setSeed']) refs['_client_state_setSeed'](newSeed);
{_sync_sidebar_js()}
if (window.__updatePresetURL) window.__updatePresetURL(newSeed);
}})();
@@ -571,133 +521,44 @@ def _apply_font_js(font_id: str) -> str:
function injectSwatches() {
const block = document.getElementById("css-export-block");
if (!block) return;
-
- // Ensure we are targeting the inner block text wrapper
- const codeElement = block.querySelector("code") || block;
-
- // Prevent infinite loops if already formatted
- if (codeElement.dataset.swatchesDone === "true") return;
-
- let rawText = codeElement.textContent;
-
- // Match all variations of oklch(...) globally
- const oklchRegex = /oklch\([^)]+\)/g;
-
- // Map every text match to an HTML template containing the swatch + the string itself
- const highLightedHtml = rawText.replace(oklchRegex, (match) => {
- const swatchHtml = ``;
- return swatchHtml + match;
- });
-
- codeElement.innerHTML = highLightedHtml;
- codeElement.dataset.swatchesDone = "true";
+ const code = block.querySelector("code") || block;
+ if (code.dataset.swatchesDone === "true") return;
+ code.innerHTML = code.textContent.replace(/oklch\([^)]+\)/g, (m) =>
+ `` + m
+ );
+ code.dataset.swatchesDone = "true";
}
-
- // Run quickly across standard rendering cycles
setTimeout(injectSwatches, 40);
setTimeout(injectSwatches, 120);
setTimeout(injectSwatches, 300);
})();
"""
-
FORMAT_CSS_JS = f"""
(function() {{
{_engine_js()}
- const theme = refs['_client_state_theme'] || {{}};
- const seed = theme['__seed'] || 'b0';
-
- const lightConfig = _generateFromSeed(seed, false);
- const darkConfig = _generateFromSeed(seed, true);
+ const s = refs['_client_state_seed'] || 'b0';
+ const light = _fromSeed(s, false);
+ const dark = _fromSeed(s, true);
- const format = (config) =>
- Object.entries(config)
- .filter(([k]) => k.startsWith('--'))
- .map(([k, v]) => ` ${{k}}: ${{v}};`)
- .join('\\n');
+ const fmt = (cfg) => Object.entries(cfg)
+ .filter(([k]) => k.startsWith('--'))
+ .map(([k, v]) => ` ${{k}}: ${{v}};`)
+ .join('\\n');
- const css = `:root {{\\n${{format(lightConfig)}}\\n}}\\n\\n.dark {{\\n${{format(darkConfig)}}\\n}}`;
+ const css = `:root {{\\n${{fmt(light)}}\\n}}\\n\\n.dark {{\\n${{fmt(dark)}}\\n}}`;
- // 1. Send the clean raw string text back to state
- refs['_client_state_setCssoutput'](css);
+ if (refs['_client_state_setCssoutput']) refs['_client_state_setCssoutput'](css);
- // 2. Wait for React render state update, then cleanly map HTML templates
setTimeout(() => {{
const block = document.getElementById("css-export-block");
if (!block) return;
-
- const codeElement = block.querySelector("code") || block;
- let rawText = codeElement.textContent;
-
- const oklchRegex = /oklch\\([^)]+\\)/g;
- const highLightedHtml = rawText.replace(oklchRegex, (match) => {{
- return `` + match;
- }}); // <-- FIXED: Doubled the function body closing brace '}}'
-
- codeElement.innerHTML = highLightedHtml;
- codeElement.dataset.swatchesDone = "true";
+ const code = block.querySelector("code") || block;
+ code.innerHTML = code.textContent.replace(/oklch\([^)]+\)/g, (m) =>
+ `` + m
+ );
+ code.dataset.swatchesDone = "true";
}}, 60);
}})();
"""
-
-
-def _patch_js(updates: dict) -> str:
- updates_js = json.dumps(updates)
- return f"""
- (function() {{
- {_engine_js()}
- const current = refs['_client_state_theme'] || {{}};
- const dark = refs['_client_state_darkmode'] || false;
- let config = {{...current, ...{updates_js}}};
-
- // Ensure rebuild to get IDs for encoding
- config = _rebuildTheme({{
- baseId: config['__base_id'] || 'neutral',
- colorId: config['__color_id'] || null,
- chartId: config['__chart_id'] || null,
- styleId: config['__style_id'] || null,
- fontId: config['__font_id'] || null,
- radius: config['--radius'] || null,
- darkMode: dark
- }});
-
- const newSeed = _encodeConfig(config);
- config['__seed'] = newSeed;
- config['__dark'] = dark;
-
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](newSeed);
- {_sync_sidebar_js()}
- if (window.__updatePresetURL) window.__updatePresetURL(newSeed);
- }})();
- """
-
-
-def _patch_radius_js(value: str) -> str:
- return f"""
- (function() {{
- {_engine_js()}
- const current = refs['_client_state_theme'] || {{}};
- const dark = refs['_client_state_darkmode'] || false;
-
- let config = _rebuildTheme({{
- baseId: current['__base_id'] || 'neutral',
- colorId: current['__color_id'] || null,
- chartId: current['__chart_id'] || null,
- styleId: current['__style_id'] || 'vega',
- fontId: current['__font_id'] || 'inter',
- radius: '{value}',
- darkMode: dark,
- }});
-
- const newSeed = _encodeConfig(config);
- config['__seed'] = newSeed;
- config['__dark'] = dark;
-
- refs['_client_state_setTheme'](config);
- refs['_client_state_setSeed'](newSeed);
- {_sync_sidebar_js()}
- if (window.__updatePresetURL) window.__updatePresetURL(newSeed);
- }})();
- """
diff --git a/app/hooks.py b/app/hooks.py
index fd692e5..609c2cd 100644
--- a/app/hooks.py
+++ b/app/hooks.py
@@ -20,7 +20,6 @@
selected_radius_cs = ClientStateVar.create("selected_radius_cs", "Medium")
-theme = ClientStateVar.create("theme", {})
seed = ClientStateVar.create("seed", "")
diff --git a/app/templates/docsidebar.py b/app/templates/docsidebar.py
index 60ba8c3..a23e1a2 100644
--- a/app/templates/docsidebar.py
+++ b/app/templates/docsidebar.py
@@ -8,6 +8,26 @@
from components.ui.button import button
from components.ui.select import select
+HIGHLIGHT_SCRIPT = """
+ const currentPath = window.location.pathname.substring(1);
+ const activeElement = document.getElementById(currentPath);
+ if (activeElement) {
+ const sidebar = document.getElementById('sidebar');
+ if (sidebar) {
+ sidebar.querySelectorAll('.bg-secondary').forEach(el => {
+ el.classList.remove('bg-secondary');
+ });
+ }
+ // 2. Add highlight to the current one
+ activeElement.classList.add('bg-secondary');
+ // 3. Scroll into view
+ activeElement.scrollIntoView({
+ behavior: 'instant',
+ block: 'center'
+ });
+ }
+"""
+
@dataclass
class SidebarSection:
@@ -107,28 +127,7 @@ def sidebar():
"sm:mask-size-[100%_100%] "
"sm:mask-repeat-no-repeat "
),
- on_mount=rx.call_script("""
- const currentPath = window.location.pathname.substring(1);
- const activeElement = document.getElementById(currentPath);
-
- if (activeElement) {
- const sidebar = document.getElementById('sidebar');
- if (sidebar) {
- sidebar.querySelectorAll('.bg-secondary').forEach(el => {
- el.classList.remove('bg-secondary');
- });
- }
-
- // 2. Add highlight to the current one
- activeElement.classList.add('bg-secondary');
-
- // 3. Scroll into view
- activeElement.scrollIntoView({
- behavior: 'instant',
- block: 'center'
- });
- }
- """),
+ on_mount=rx.call_script(HIGHLIGHT_SCRIPT),
)
diff --git a/app/templates/preview.py b/app/templates/preview.py
index 37f4de4..76da763 100644
--- a/app/templates/preview.py
+++ b/app/templates/preview.py
@@ -32,7 +32,6 @@
card_two,
)
from app.examples.utils import masonry_card
-from app.hooks import theme
from app.www.library.charts.bar.v1 import barchart_v1
from app.www.library.charts.doughnut.v1 import doughnutchart_v1
from app.www.library.charts.line.v8 import linechart_v8
@@ -109,6 +108,6 @@ def preview() -> rx.Component:
),
class_name="w-full h-full",
),
+ id="theme-preview-container",
class_name="w-full flex-[2] min-h-0 order-first lg:order-none lg:flex-1 lg:min-w-0 lg:h-full",
- style=theme.value.to(dict),
)
diff --git a/app/templates/sidebar.py b/app/templates/sidebar.py
index 98196ad..578dd6a 100644
--- a/app/templates/sidebar.py
+++ b/app/templates/sidebar.py
@@ -13,7 +13,6 @@
selected_radius_cs,
selected_style_cs,
selected_theme_cs,
- theme,
theme_color,
theme_export_method,
theme_preset_option,
@@ -102,7 +101,6 @@ def sidebar() -> rx.Component:
selected_radius_cs,
selected_style_cs,
selected_theme_cs,
- theme,
theme_color,
theme_export_method,
theme_preset_option,