Viz theme system architecture¶
How the viewer's UI look & feel is themed — the CSS layout under
py/pytanga/viz/templates/themes/, the JSON registry that resolves a theme id to
an ordered CSS file list, backend theme selection + runtime switching, and how
the active theme is packed into self-contained HTML exports.
Layers (resolved order)¶
A theme is an ordered list of CSS files resolved from
templates/themes/registry.json. Later files win at equal specificity:
base—base.css: design tokens (:root { --tanga-*: … }), global reset / shell, the borderless-icon contract, and shared control chrome.- default
tokens—tokens.css: the shared default token layer. - theme
tokens—<theme>/tokens.css: overrides token values per theme (e.g.light/tokens.cssre-themes the palette). components—controls/*.css+views/*.css: one sheet per control/view, referencingvar(--tanga-…)instead of hardcoded colors.- theme
overrides—<theme>/overrides/*.css: an optional full re-definition of a single element (e.g. a pill button or a switch-style checkbox).
registry.json is the single source of truth; the Python loader
(py/pytanga/viz/_themes.py) reads and validates it and exposes
list_themes(), theme_label(), theme_css_files() (the resolved order), and
default_theme(). The browser never parses the JSON — it receives the resolved
css list over the wire.
Stable class-name contract¶
The JS assigns stable semantic classes (.tanga-action-button,
.tanga-range-input, .tanga-checkbox, .tanga-banner, .tanga-dialog,
.tanga-menu-trigger, .tanga-warning-banner, …) and never inlines
appearance. Only computed geometry (overlay anchors, banner/dialog
transform: translate(-x%,-y%), drag left/top) stays inline. Overrides and
themes target those stable classes.
Wire message¶
{ "type": "theme_define", "theme": "light", "label": "Light",
"css": ["base.css", "tokens.css", "light/tokens.css", "controls/button.css", "…"] }
Live viewer¶
Visualizer.theme/set_theme(theme_id)store the active theme id (viewer-global, default"dark").set_themevalidates viatheme_css_filesand pushestheme_defineto all connected clients (run_coroutine_threadsafe(push_raw, loop));set_theme_asyncis the loop-safe variant.- On page load,
VizServercalls an optionaltheme_callback(wired toVisualizer._theme_define_payload) and injects one<link rel="stylesheet" data-tanga-theme href="themes/…">per resolved file intoviewer.html's<head>(after the page-token injection).base.cssis also linked statically inviewer.htmlfor no-FOUC. templates/themes.js::handleThemeDefine(msg)swaps the[data-tanga-theme]links (idempotent per theme) and marksdata-tanga-theme-nameon<html>;viewer.jsroutestheme_defineto it.
Export packing¶
generate_theme_css(theme_id, *, include_components=True, include_overrides=True)
(in export/_bootstrap/_html.py) reads the resolved CSS files and returns one
inlined <style> block — symmetric to how generate_bootstrap_js packs the
renderer modules. theme_css_for_delivery(theme_id, delivery, delivery_ref, *,
include_components=…, include_overrides=…) adapts that per delivery mode:
"cdn" returns one jsDelivr <link> per bundled theme file (pinned to the same
ref as the viewer bundle), "inline"/"offline" inline, and runtime-registered
external themes always inline (they are not on the CDN).
Standalone exports render no themed controls, so render_snapshot,
render_figure, and the animated renderers pass include_components=False,
include_overrides=False to pack only the base + token shell — dropping
controls/*.css, views/*.css, and per-theme overrides. The theme: str =
"dark" parameter (and __THEME_CSS__ in export_viewer.html) still selects
the active theme, and Visualizer.export_snapshot / export_figure accept
theme= (defaulting to self.theme).
Delivery modes¶
export_snapshot / export_figure / display_snapshot accept delivery=
("cdn" default, "inline", "offline") and delivery_ref= to override the
jsDelivr ref. The export bootstrap is split into a scene-independent
library (generate_library_js — the stripped renderer + shared modules,
the Three.js/addons imports, the SDF shaders, and a window.__tanga bridge)
and a scene-specific adapter that destructures from that bridge.
"cdn" serves the committed library from
https://cdn.jsdelivr.net/gh/dodeka12/tanga@<ref>/js/tanga-viewer.js;
"inline" inlines it; "offline" downloads pinned three.js/marked/KaTeX/
html2canvas to a user cache and bundles three.js + the library with esbuild at
export time (requiring Node.js + esbuild; see export/_offline.py), then
inlines everything.
Non-goals¶
- 3D geometry/entity renderer colors remain style-driven via the Python style system — this system themes UI controls/views only.
- No user-facing theme picker widget (theme is selected from the backend).
- No CSS preprocessors/bundlers — plain CSS files +
var(), consistent with the zero-build-step frontend.