diff --git a/CHANGELOG.md b/CHANGELOG.md index b94b5af..60d5ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,23 @@ # Changelog +## [0.2.0] - (2026-03-16) + +### Fixes + +- **Theme detection**: Fixed auto-theme not responding to host theme changes: removed narrow Marimo shadow DOM gate so the MutationObserver is always active +- **Marimo `dark-theme` class**: Added detection for Marimo 0.20.x `dark-theme` CSS class in addition to Tailwind `dark` + +### Improvements + +- **Universal theme observer**: Theme auto-detection now works in any host (Marimo, Jupyter, VS Code), observing both `` and `` for class/data-theme changes +- **OS-level theme listener**: Added `prefers-color-scheme` media query change listener for OS dark/light mode switches +- **Dark mode toggle always visible**: Manual toggle no longer hidden in auto-theme mode; clicking it disables auto-theme so the user's choice is respected + ## [0.1.4] - (2026-03-16) ### New Features -- **Automatic theme detection**: Auto-detects host dark mode (marimo Tailwind `class="dark"`, `data-theme`, or `prefers-color-scheme`) and syncs the widget theme automatically — dark mode toggle is hidden when the host controls the theme +- **Automatic theme detection**: Auto-detects host dark mode (marimo Tailwind `class="dark"`, `data-theme` or `prefers-color-scheme`) and syncs the widget theme automatically; dark mode toggle is hidden when the host controls the theme ### Improvements @@ -14,7 +27,7 @@ ### New Features -- **Upload button**: Toolbar button to load `.xml` or `.archimate` files directly from the browser — no Python code needed to switch models +- **Upload button**: Toolbar button to load `.xml` or `.archimate` files directly from the browser; no Python code needed to switch models - **Export to SVG**: Download the current diagram as a self-contained SVG file with inlined fonts - **Export to PNG**: Download the current diagram as a high-resolution (2x) PNG with correct light/dark background @@ -22,14 +35,14 @@ ### New Features -- **Demo module**: Built-in `demo_archimate()` function that creates a pre-populated ArchiMate widget with a sample enterprise architecture model (12 elements across Business, Application, and Technology layers) — no external files needed +- **Demo module**: Built-in `demo_archimate()` function that creates a pre-populated ArchiMate widget with a sample enterprise architecture model (12 elements across Business, Application and Technology layers); no external files needed ## [0.1.1] - (2026-03-15) ### New Features - **ArchiMate icons**: Standard SVG icons for all 13 element types (stick figure for BusinessActor, component tabs for ApplicationComponent, 3D box for Node, gear for BusinessFunction, etc.) -- **Filter sidebar**: Collapsible left panel with per-layer, per-element-type, and per-relationship-type checkboxes with counts and "all / none" toggles +- **Filter sidebar**: Collapsible left panel with per-layer, per-element-type and per-relationship-type checkboxes with counts and "all / none" toggles - **Nest elements**: Composition and Aggregation relationships rendered as visual nesting with a toggle checkbox in the filter panel - **Archi native format**: Parser now supports both Open Exchange Format XML and Archi tool `.archimate` files @@ -59,6 +72,6 @@ - **Element badges**: Two-letter type badges (AC, BA, TS, etc.) on each element - **Zoom and pan**: Mouse wheel zoom and drag-to-pan on the SVG diagram - **Dark mode**: Toggle between light and dark themes -- **Details panel**: Click any element to see its name, type, layer, and documentation -- **anywidget integration**: Works in Jupyter, Marimo, and VS Code notebooks +- **Details panel**: Click any element to see its name, type, layer and documentation +- **anywidget integration**: Works in Jupyter, Marimo and VS Code notebooks - **`from_xml` / `load_xml`**: Load ArchiMate models from file path or XML string diff --git a/pyproject.toml b/pyproject.toml index be6dabc..b9e5b12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "anywidget-archimate" -version = "0.1.4" +version = "0.2.0" description = "Interactive ArchiMate model viewer widget for Jupyter, Marimo, and VS Code notebooks" readme = "README.md" license = "Apache-2.0" diff --git a/src/anywidget_archimate/ui/index.js b/src/anywidget_archimate/ui/index.js index 9e46098..7df87ee 100644 --- a/src/anywidget_archimate/ui/index.js +++ b/src/anywidget_archimate/ui/index.js @@ -1054,28 +1054,45 @@ function render({ model, el }) { wrapper.className = "aam-wrapper"; el.appendChild(wrapper); - // Auto-detect host theme (marimo uses Tailwind class="dark" on ) + // Auto-detect host theme (Tailwind dark/dark-theme class, data-theme, or prefers-color-scheme) function detectHostDark() { const html = document.documentElement; - if (html.classList.contains("dark")) return true; + if (html.classList.contains("dark") || html.classList.contains("dark-theme")) return true; if (html.dataset.theme === "dark") return true; + if (document.body?.classList.contains("dark") || document.body?.classList.contains("dark-theme")) return true; + if (document.body?.dataset.theme === "dark") return true; if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return true; return false; } - const hasHostTheme = !!el.getRootNode()?.host?.tagName?.startsWith("MARIMO-"); - if (hasHostTheme) { - wrapper.classList.add("aam-auto-theme"); - model.set("dark_mode", detectHostDark()); - model.save_changes(); - const themeObserver = new MutationObserver(() => { + // Always auto-detect and observe theme changes + let autoTheme = true; + model.set("dark_mode", detectHostDark()); + model.save_changes(); + + const themeObserver = new MutationObserver(() => { + if (!autoTheme) return; + const dark = detectHostDark(); + if (model.get("dark_mode") !== dark) { + model.set("dark_mode", dark); + model.save_changes(); + } + }); + themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-theme", "style"] }); + if (document.body) { + themeObserver.observe(document.body, { attributes: true, attributeFilter: ["class", "data-theme", "style"] }); + } + + // Also listen for OS-level theme changes + if (window.matchMedia) { + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { + if (!autoTheme) return; const dark = detectHostDark(); if (model.get("dark_mode") !== dark) { model.set("dark_mode", dark); model.save_changes(); } }); - themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-theme"] }); } const toolbar = document.createElement("div"); @@ -1146,7 +1163,7 @@ function render({ model, el }) { const s = graphContainer.querySelector("svg"); if (s) { const v = s.getAttribute("viewBox").split(" ").map(Number); s.setAttribute("viewBox", v.join(" ")); } }); - toolbar.querySelector(".aam-btn-dark").addEventListener("click", () => { model.set("dark_mode", !model.get("dark_mode")); model.save_changes(); }); + toolbar.querySelector(".aam-btn-dark").addEventListener("click", () => { autoTheme = false; model.set("dark_mode", !model.get("dark_mode")); model.save_changes(); }); // Upload const fileInput = toolbar.querySelector(".aam-file-input"); diff --git a/src/anywidget_archimate/ui/styles.css b/src/anywidget_archimate/ui/styles.css index 56c5c04..5888899 100644 --- a/src/anywidget_archimate/ui/styles.css +++ b/src/anywidget_archimate/ui/styles.css @@ -399,7 +399,4 @@ font-size: 12px; } -/* Hide dark mode button when host provides theme */ -.aam-wrapper.aam-auto-theme .aam-btn-dark { - display: none; -} +/* Dark mode toggle always visible — manual toggle disables auto-theme */ diff --git a/tests/test_parser.py b/tests/test_parser.py index 84a43ca..30d1906 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -59,9 +59,19 @@ def test_parse_xml_from_file(): def test_layer_map_covers_all_types(): expected_types = { - "ApplicationComponent", "ApplicationInterface", "ApplicationService", "DataObject", - "BusinessActor", "BusinessProcess", "BusinessFunction", "BusinessEvent", "BusinessObject", - "Node", "Device", "SystemSoftware", "TechnologyService", + "ApplicationComponent", + "ApplicationInterface", + "ApplicationService", + "DataObject", + "BusinessActor", + "BusinessProcess", + "BusinessFunction", + "BusinessEvent", + "BusinessObject", + "Node", + "Device", + "SystemSoftware", + "TechnologyService", } assert set(LAYER_MAP.keys()) == expected_types diff --git a/uv.lock b/uv.lock index da43857..fe2c315 100644 --- a/uv.lock +++ b/uv.lock @@ -30,7 +30,7 @@ wheels = [ [[package]] name = "anywidget-archimate" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "anywidget" },