Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

.nornir/theming-and-facett-migration.md

Theming everywhere (C8) + the facett-UI migration plan (D3)

Design + guide for the app-wide facett palette across every viz pane, and the plan to re-home the generic viz widgets into the upstream facett crate. Closes THE_BIG_PLAN C8 ("viz uses the facett themable/color schema everywhere") and D3 ("ALL generic viz UI migrated into facett; viz consumes facett themes"). Laws honoured: #1 inject-assert tests · #2 this doc · #3 facett components/theme · #4 screenshot proof · #6 "see what the user sees" as data.

TL;DR

Why a vendored palette, not a facett dependency (the egui-version blocker)

nornir's viz is pinned to egui 0.33 — the highest version egui-snarl 0.9 (the Funnel node-graph) supports, so the whole viz unifies on one egui (see Cargo.toml's egui block). The upstream facett crate is on egui 0.34. egui's Color32 / Painter / Ui / Visuals types are version-specific and do not interoperate, so a direct dep:facett-grid / dep:facett-table would link two egui versions into one binary — a hard build/ABI break.

This is the exact skew the L5 agent hit, which is why facett_theme.rs is a verbatim vendored copy of facett_core::theme::Theme (same names, same colours, same ALL order) rather than an import. C8/D3 follow that precedent.

Migration plan (when the move becomes a re-home, not a rewrite)

When What happens
today facett_theme (palette) + facett_ui (widgets) live in the viz crate, both fully palette-driven (every widget takes a Theme).
egui-snarl ships on egui 0.34 (or the Funnel moves off snarl) bump the viz to egui 0.34.
after the bump facett_theme collapses to pub use facett::Theme;, facett_ui collapses to pub use facett::widgets::*;, and the panes import the upstream components directly. Because every widget already takes a Theme, the diff is a re-home, not a re-write.

Tracking dep: egui-snarl 0.34 support (or de-snarl the Funnel). Until then this doc + the module headers are the "facett-bound when egui aligns" marker.

Architecture — one palette, broadcast to all

                 🎨 top-bar picker  ──┐         viz.click { "palette": "…" }
                                      ▼                    │ (robot / MCP parity)
                          UrdrThreadsApp.set_palette(Theme) ◄┘
                                      │
              ┌───────────────────────┼───────────────────────────┐
              ▼                       ▼                            ▼
   ctx.set_visuals(theme.visuals())   broadcast_palette()    state_json["palette"]
   (re-skins egui widgets)            pane.set_palette(t) ×N  (+ each pane's own
                                      (test, release, live,    "palette" key — LAW 6)
                                       knowledge, warehouse,
                                       mcp, callgraph, funnel,
                                       security, search, gates,
                                       bench, timetravel, dep_view, leaderboard)

The semantic ramp (what panes call instead of raw rgb)

All in src/viz/facett_theme.rs on Theme (+ module consts):

call meaning
theme.bg / node_fill / node_stroke / edge / text / text_dim / accent / point / panel_bg / panel_stroke / glow the base palette fields
theme.status_color(s) release/run/op verdict → green/red/amber (succeeded*/ok/pass/done→green, failed*/error/stalled→red, running/dry_run/warn→amber, failed_bench→amber-red, else text_dim)
theme.status_fill(s) test-matrix cell status (green/red/amber/dim)
theme.health_color(0..100) red→amber→green health ramp
theme.heat(t∈0..1) cool→hot ramp (edge→accent) for fan-out / bubbles
theme.zebra(odd) · hover() · selection() · gridline() · info() chrome derived from the palette so it drifts with the theme
facett_theme::{GREEN, RED, AMBER} the canonical verdict colours (stable across palettes so status reads the same everywhere)

Rule of thumb: verdict colours (pass/fail/warn) are the canonical GREEN/RED/AMBER consts — a green test is green in every palette. Chrome colours (background, gridlines, zebra, hover, selection, accents, dim text) come from the active Theme so a switch re-skins the surface.

facett_ui — the generic widgets (D3)

src/viz/facett_ui.rs — non-nornir-specific, palette-driven, facett-bound:

These are the components that should live in facett (korp / Njord / any egui surface want them). They don't yet, for the egui-skew reason above; the module header documents the re-home. New panes should paint via these, not re-roll.

C8 follow-up — the last hand-rolled colours (2026-06-13)

The first C8 pass themed most panes but left a few hard-coded Color32::from_rgb islands. This follow-up routed them all through the palette:

Computed-ramp exceptions (deliberately left)

A few colours are computed ramps, not flat chrome, and stay as math: callgraph.rs heat(deg) (glacier→cyan by node degree), fog_blend (depth extinction toward a fog tint), and the per-node perspective brightness. These are ramp internals; the visible chrome around them is themed. (Their Color32 literals are the ramp endpoints + #[cfg(test)] inputs, not surface chrome.)

CLI / robot parity (LAW: never GUI-only)

The palette is drivable headlessly, matching every other viz control:

Tests (LAW #1 inject-assert, LAW #6 data, LAW #4 screenshot)

Files