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,