panicOS

BRIEFS

Circuit Subsystem Spec

REFERENCE / Circuit Subsystem Spec

2026-06-29

The “circuit” is the marching-dash visual language that frames the whole site and wires the OS homepage together. This doc specs how it works and lays groundwork for a future admin circuit editor (draw/edit traces from the dontpanic login, persisted + replayed live).

Companions: docs/DESIGN-SYSTEM.md (UI catalog) · docs/STYLE-MAP.md (token tables — see its CIRCUITS + OS HOME SURFACES sections for every var).


1. Visual language

  • Marching ants: dashed strokes that animate (the dash-offset loops by exactly ±(length + gap) so the march is seamless). Precomputed as --dash-march in lib/theme-css.ts.
  • Two readings: (a) a framed box (every card/panel/window has a dashed “circuit” border); (b) the OS bus — a PCB-style tree of orthogonal traces on the / homepage that visually wires the panels to a central hub, as if the UI is a circuit board.
  • Underlay + dash: each border is actually two strokes — a faint solid --c-circuit underlay beneath the animated --c-border-dashed dashes — so even when motion is off the trace still reads.

Global toggle: effects.toggles.marchingAnts:false → sets html[data-marching-ants] and freezes every .dash-animated march (the underlay stays).


2. Two subsystems

2a. Per-element marching borders — DashedBorderFull.astro

src/components/DashedBorderFull.astro. Drops an absolutely-positioned SVG (two <rect>s: solid --c-circuit underlay + dashed --c-border-dashed) into any position:relative box.

  • Animate it: pass animated when the box is inside a .group (desktop hover) or a .t-line (mobile scroll-in) → adds .dash-animated.
  • Geometry comes from --dash-length / --dash-gap / --dash-width / --dash-linecap / --circuit-width (defaults = the main circuit from circuits.md). Props can override.
  • Own circuit per region: a box can remap those vars via scoped custom properties to get its own dash colour/geometry — LinkCard (link circuit) and the OS Action Center / Terminal / MOTD each do this.

circuits.md configs (→ STYLE-MAP CIRCUITS table): main (default border), card (quick links), osBus (the homepage tree), osActionCenter / osTerminal / osMotd (panel borders).

Rule.astro is the non-animated cousin — a single solid divider (--c-rule), never marched.

2b. Runtime trace router — os-circuit.ts + CircuitTraces.astro

src/scripts/os-circuit.ts draws the OS bus (the homepage tree) at runtime into the shared SVG host CircuitTraces.astro ([data-os-traces]). Because the panels are placed responsively (clamp/vw/vh), the geometry cannot be hard-coded — it measures the live boxes and redraws.


3. Router spec (os-circuit.ts)

3a. The opt-in contract (HTML hooks)

AttributeOnMeaning
data-os-hubthe hub ring (HubNode)the tree root; its rect → ring centre + radius
data-os-node="<id>"a target boxa node other wires can attach to (e.g. action-center, spotlight)
data-os-wire="<targetId>"a source box”wire me to <targetId>” — hub (trunk) or action-center (folder-tree branch)
data-os-terminalthe consolemarks the tall console so its trunk anchors to the hub centre

The SVG host CircuitTraces.astro contains three <g>s: .os-trace-track (static underlay), .os-trace-line (the marching dashes), .os-trace-nodes (junction dots, radius NODE_R).

3b. What gets drawn

  1. Central spine — a single vertical bus through the hub centre. Desktop: full scene height behind the cards. Mobile: links the stacked console → hub → buttons.
  2. Trunks (desktop) — each data-os-wire="hub" panel routes an orthogonal elbow from its inner edge to a ring-entry point (NAVIGATION CENTER, console, spotlight).
  3. Folder-tree (desktop) — the launcher buttons (data-os-wire="action-center") branch off a vertical spine in the gutter (TREE_GUTTER px) just left of the button stack, each tapping it with a short stub that runs a few px into the button (TAP_INTO).

3c. Anchor logic (important for editing)

For each trunk the tap point (where the trace leaves the panel) is chosen for stability so it doesn’t slide as the window scales:

  • tapX = the panel’s inner edge (right edge if left of the hub, else left edge).
  • anchorY = the panel’s own vertical centre (b.cy) for short side panels (nav centre, spotlight) — glued to the box; the hub centre (hub.cy) for the console (data-os-terminal) so its trunk leaves the hub centre and runs straight across.
  • tapY is anchorY clamped into the panel (with TAP_PAD).
  • Ring entry: left panels enter the ring’s left point, or the upper/lower-left diagonal (hub.cx ± diag, diag = hubR·0.7071) when the tap is clearly above/below the hub centre.

3d. Redraw triggers

draw() is rAF-debounced via schedule() and re-run on: window resize, ResizeObserver on main.os-desktop, document.fonts.ready (DOS-V load changes widths), and astro:page-load (SPA nav). The SVG viewBox is set to pixel space (1 unit = 1px) so strokes stay crisp. Below DESKTOP_MIN (768px) only the spine is drawn.

3e. Constants

DESKTOP_MIN=768, NODE_R=6 (junction dot), TREE_GUTTER=64 (spine offset left of buttons), TAP_PAD=14 (keep tap inside corners), TAP_INTO=6 (stub into button edge).


4. Tokens

Circuit colours/geometry are config-driven — see docs/STYLE-MAP.md:

  • CIRCUITS section: main / card / osBus / osActionCenter / osTerminal / osMotd → their --c-circuit / --c-border-dashed / --dash-* / --circuit-width families.
  • OS HOME SURFACES section: os.hub.{ring,logo,node,spin}--os-hub-*.

5. 🌌 Future: admin circuit-path editor (groundwork)

The user’s stretch idea: an in-page edit mode (admin dontpanic only) to draw/edit the homepage traces, persisted and replayed live. This section is the concept — not built.

5a. Where it hooks in

os-circuit.ts currently generates paths (SVG path d strings) + nodes ([x,y]) and renders them into .os-trace-track / .os-trace-line / .os-trace-nodes. An editor would supply or override that paths/nodes set — replacing or overlaying the auto-router.

5b. Authored-path data model

A PocketBase collection circuits (admin-gated, public read):

fieldtypenotes
nametextlayout name / breakpoint label
segmentsjsonordered list of orthogonal segments, each as anchors, not raw px
min_widthnumberwhich breakpoint this layout applies at
enabledbooltoggle without deleting

Anchors, not pixels. An authored segment stores endpoints relative to nodes, e.g. { from: { node: "action-center", edge: "right", t: 0.5 }, to: { node: "hub", point: "ring-left" }, route: "elbow" }. At draw time the renderer resolves each anchor against the live rel(el) rect. Invariant: re-anchor to live node rects on every redraw — never persist absolute coordinates.

5c. Editor UX (concept)

  • Toggle “wire mode” (admin only) → overlay a grid; click a node edge, click a target → an orthogonal segment is proposed (snap to the same elbow logic as §3c).
  • Drag to adjust the mid-run; delete to remove. Save → POST to circuits.
  • Non-admins just get the rendered result (read the collection at load, fall back to the auto-router if empty).

5d. Open questions

  • Per-breakpoint authoring vs one responsive model.
  • Editor vs auto-router precedence (full replace, or author only specific trunks).
  • How to author the central spine + folder-tree (or leave those auto).
  • Undo/history + validation (no dangling anchors when a node is removed).
DISCUSSION
/ commands