From 95dd5397cd3e7dac2d28a69747b04700cea1a0a0 Mon Sep 17 00:00:00 2001 From: Mahaboob pasha Date: Sat, 10 Oct 2026 22:05:39 +0530 Subject: [PATCH] feat(html): add a light/dark/system theme toggle to graph.html (#3894) Co-Authored-By: Grok Bot --- graphify/exporters/html.py | 110 ++++++++++++++++++++++++++++++------- tests/test_html_theme.py | 84 ++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 tests/test_html_theme.py diff --git a/graphify/exporters/html.py b/graphify/exporters/html.py index 14f781249..bd113d7a4 100644 --- a/graphify/exporters/html.py +++ b/graphify/exporters/html.py @@ -31,42 +31,56 @@ def _viz_node_limit() -> int: def _html_styles() -> str: return """""" def _hyperedge_script(hyperedges_json: str) -> str: @@ -198,6 +212,43 @@ const network = new vis.Network(container, {{ nodes: nodesDS, edges: edgesDS }}, edges: {{ smooth: {{ type: 'continuous', roundness: 0.2 }}, selectionWidth: 3 }}, }}); +// Theme toggle (#3894): Light / Dark / System, persisted in localStorage. +const THEME_KEY = 'graphify-theme'; +const themeMQ = window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null; +let themeLabelColor = '#ffffff'; // matches the per-node font color set in Python +function themePref() {{ + let p = null; + try {{ p = localStorage.getItem(THEME_KEY); }} catch (e) {{}} + return (p === 'light' || p === 'dark' || p === 'system') ? p : null; +}} +function applyTheme(pref) {{ + const t = pref === 'light' ? 'light' : (pref === 'system' && themeMQ && themeMQ.matches) ? 'light' : 'dark'; + const root = document.documentElement; + if (!root) return; + root.setAttribute('data-theme', t); + document.querySelectorAll('[data-theme-choice]').forEach(b => {{ + b.setAttribute('aria-pressed', String(b.dataset.themeChoice === (pref || 'dark'))); + }}); + const labelColor = getComputedStyle(root).getPropertyValue('--node-label').trim(); + if (labelColor && labelColor !== themeLabelColor) {{ + themeLabelColor = labelColor; + nodesDS.update(nodesDS.get().map(n => ({{ id: n.id, font: Object.assign({{}}, n.font, {{ color: labelColor }}) }}))); + }} +}} +document.querySelectorAll('[data-theme-choice]').forEach(b => {{ + b.addEventListener('click', () => {{ + const pref = b.dataset.themeChoice; + try {{ localStorage.setItem(THEME_KEY, pref); }} catch (e) {{}} + applyTheme(pref); + }}); +}}); +if (themeMQ) {{ + const onSchemeChange = () => {{ if (themePref() === 'system') applyTheme('system'); }}; + if (themeMQ.addEventListener) themeMQ.addEventListener('change', onSchemeChange); + else if (themeMQ.addListener) themeMQ.addListener(onSchemeChange); +}} +applyTheme(themePref()); + network.once('stabilizationIterationsDone', () => {{ network.setOptions({{ physics: {{ enabled: false }} }}); }}); @@ -224,7 +275,7 @@ function showInfo(nodeId) {{ }} html += `
Degree: ${{n.degree}}
`; if (neighborIds.length) {{ - html += `
Neighbors (${{neighborIds.length}})
${{neighborItems}}
`; + html += `
Neighbors (${{neighborIds.length}})
${{neighborItems}}
`; }} document.getElementById('info-content').innerHTML = html; }} @@ -633,10 +684,27 @@ def to_html( integrity="sha384-Ux6phic9PEHJ38YtrijhkzyJ8yQlH8i/+buBR8s3mAZOJrP1gwyvAcIYl3GWtpX1" crossorigin="anonymous"> {_html_styles()} +