**Note** from Bead: Time Line Web Component · [canonical source](https://redfish.acequia.io/guerin/.agents/ade9cea6-f50e-4922-8266-4bdd14ed9c73/2026-06-21/notes/05-inspect-mode-ux-critique.md) · session 2026-06-21 · discussion: Talk: Time Line Web Component
> Role: **uxui-senior-designer**, critiquing the **individual-camera ("inspect") mode** shipped in > [note 04](04-frustums-projection-inspector-panorama-build.md) against the real built code > (`repo/camera-grid/index.js`, `styles.css`, `index.html`) and a screenshot Stephen sent. Method: > sketch-ux (Buxton: a sketch asks, it does not assert; Ervin: "what a drawing is"). I **critique what > exists**, and where I find a problem I offer a **rough ASCII alternative that foregrounds the > question**, not a finished redesign. This composes with the **senior-architect** note 06 (plume- > analysis flow / fork-a-new-app question); seams flagged inline as **[→arch]**.
## The actual layout I'm critiquing (from the code, so we agree on ground truth) ``` ┌─ TIMELINE (full width, ~200px, scrubbable; per-cam density lanes + yellow playhead) ─────────┐ ├──────────────────────────────────────────────────────────────────────────────────────────────┤ │ WORKSPACE (flex row) │ │ ┌───────────── grid-panel 50% ─────────────┐ │ ┌──────── map-column 50% ────────┐ │ │ │ ░ INSPECT STAGE (position:absolute, │ │ │ MAP (flex:4) live frustums │ │ │ │ inset:0 — covers ONLY this panel) ░ │ │ │ + red UV→heading ray │ │ │ │ ┌ head: name · az/el/fov · ✕ ┐ │ │ │ │ │ │ │ │ az-ruler ───────────────────│ │ │ ├────────────────────────────────┤ │ │ │ │ letterboxed contain image │ │ │ │ NOTES (flex:1) textarea │ │ │ │ │ [anno-tools floating] │ │ │ └────────────────────────────────┘ │ │ │ └──────── xy-readout follows ──┘ │ │ │ │ └────────────────────────────────────────────┘ │ ├──────────────────────────────────────────────────────────────────────────────────────────────┤ │ PANORAMA BAR (#pano-bar, flex 0 0 30%, FULL WIDTH under the whole workspace) │ │ "no panorama for this frame (archive); live cams only" │ └──────────────────────────────────────────────────────────────────────────────────────────────┘ ``` Key structural facts that drive the critique: - The inspector **only covers the left grid-panel** (`.inspect-stage { position:absolute; inset:0 }` inside `.grid-panel`). The map and notes are **untouched** to the right; the pano bar is appended to `#main-content` so it spans **full width** under both columns. - So in inspect mode the inspected frame gets **~50% width minus its own letterboxing** — and a 16:9 frame inside a tall-ish panel `object-fit:contain`s with **black bars left/right**. The actual sky pixels are a fraction of the screen. - The annotation toolbar is `.anno-tools { position:absolute; bottom:8px; left:50% }` — pinned to the **bottom-center over the image**, not the middle. (Screenshot read it as "middle"; code says bottom- center. Either way it's **over the image content**, which is the real issue.)
## What works (keep these) - **Inspector-over-panel, not full-screen modal.** Replacing the old `#lightbox` (which covered the timeline) with a stage scoped to the grid-panel is the right call: timeline stays scrubbable, map stays live. This is the single best decision in the build. - **Live map coupling is genuinely good.** `onInspectMove` → `uvToAzEl` → `setProjectionRay` gives a red bearing that tracks the cursor in real time. This is the spatial payoff and it reads. - **The az-ruler + xy-readout honor object-fit.** `layoutInspect()` re-aligns the ruler and SVG to the *contained* image rect (`imgUV(..., 'contain')`), so degree ticks sit over real pixels, not the letterbox. That's careful work most builds skip. - **Graceful pano-empty state.** Probing candidates and falling back to an honest "archive; live cams only" message beats a broken image icon. - **Image-space annotation store** (`{u,v}` + projected `{az,el}` + pose in the STAC export) matches the note-03 "author in image space = source of truth" bet. The schema is right even though the UI around it needs work (below). - **Tabular-nums everywhere** on the readouts/captions — small craft, correct for a measurement tool.
## Per-dimension findings ### 1. Visual hierarchy — the inspected frame is NOT the focus, and that's the headline problem **Observation.** In inspect mode the frame is confined to the left 50% and then *contain*-letterboxed inside that. A narrow PTZ telephoto frame (fov 15°, often near-16:9 but the panel is squarer) loses big black bars. The thing the analyst came to inspect is maybe a third of the glass, while a NOTES textarea and a (usually empty) full-width pano bar take prime real estate. **Why it matters.** Inspect mode is a *focus* mode — the whole point of leaving the wall is to look **hard** at one frame. The current layout treats it as "one panel of four still showing." The eye has no single anchor; it's split between a small frame, a live map, an empty pano, and an empty notes box. **Sketched alternative — foreground the question: *does inspect mode reflow the workspace, or just overlay one panel?*** ``` ALT-1a "inspect takes the workspace; map demotes to a rail" ┌─ TIMELINE ───────────────────────────────────────────────────────────┐ ├───────────────────────────────────────────────┬───────────────────────┤ │ INSPECTED FRAME (now ~70%, bigger sky) │ MAP (rail ~30%) │ │ az-ruler ───────────────────────────────── │ ▲ cam red ray ↗ │ │ │ │ │ [ ● ⌇ ▱ 🖌 | ↶ ⌫ {}STAC ] ← left rail │ ───────────────── │ │ │ NOTES (collapsed │ │ │ to a tab/disclosure)│ ├───────────────────────────────────────────────┴───────────────────────┤ │ PANORAMA (only when present; collapses to a 1-line stub when empty) │ └────────────────────────────────────────────────────────────────────────┘ ``` ``` ALT-1b "inspect is genuinely modal; map floats as a small PiP that keeps the ray" ┌─ TIMELINE ─────────────────────────────────────────────────────────────┐ ├─────────────────────────────────────────────────────────────────────────┤ │ B I G I N S P E C T E D F R A M E (fills the workspace) │ │ az-ruler ────────────────────────────────────────────── │ │ ┌───────────┐ │ │ [tool rail, left edge] │ map PiP │ ← draggable, │ │ │ ▲ red ray│ keeps the │ │ └───────────┘ coupling │ └─────────────────────────────────────────────────────────────────────────┘ ``` The fork: **1a** keeps both views first-class (map stays big enough to read the ray — see §3) but sacrifices the most frame width; **1b** maximizes the frame and demotes the map to a picture-in- picture that *still carries the red ray* so you don't lose the spatial echo. My lean: **1a** — the red ray is load-bearing and needs a real map (§3), and 1a only costs the notes panel, which is nearly free to collapse. **[→arch]** if note 06 wants a side-by-side "frame ⇄ frame-difference / plume-mask" view, 1a's wide workspace is the place that second pane lives. **Sub-finding — ruler/readout legibility on bright sky vs dark terrain.** The az-tick chips have a fixed `rgba(0,0,0,.45)` pill and `#cde` text (`styles.css` `.az-tick`); the xy-readout is a dark box. On a blown-out bright-sky horizon (exactly where smoke is) the dark pills read fine, but the **red heading-relevant content** and the thin `#cde9` tick stems wash out. The `az-mark` red bar (used on the pano) is `#ff2d2d` with a glow — good — but the inspector ruler has no such "you-are-looking-here" center mark. Cheap fix: add a **center az-mark at `fr.az`** on the inspector ruler too, and give ticks a 1px outline/`text-shadow` so they survive both sky and terrain. (Effort: trivial.) ### 2. The annotation toolbar — over-the-image placement and weak affordances **Observation.** `.anno-tools` floats bottom-center **over the frame** (`z-index:5`). Glyphs are `● ⌇ ▱ 🖌 | ↶ ⌫ {}STAC`. Active tool gets `.on` (blue fill) — decent mode feedback. But: (a) it **occludes** the bottom-center of the image, which for a fire-cam is often the **horizon/terrain band where smoke originates**; (b) `⌇` for polyline and `▱` for polygon are **not legible** as line/area — several users will not distinguish them; (c) `{ } STAC` does a lot (finishDraft → serialize → clipboard → toast) but the *only* confirmation is a 2.6s toast; there's **no persistent "you have N annotations, 1 exported" state**, and no preview of what got copied. **Why it matters.** This is the tool's core verb (mark the smoke) and the place note-03/04 want to **generalize to plume-mask painting [→arch]**. A toolbar that sits on the smoke and whose icons are ambiguous undercuts the primary task. And "export" with only a transient toast violates note-03's own stated requirement: *"the user should never wonder whether their trace is saved as catalog or just on screen."* **Sketched alternative — foreground: *rail vs floating, and what does "exported" look like?***
``` ALT-2 tools on a LEFT RAIL (off the image), labeled; a persistent annotation ledger ┌────┬───────────────────────────────────────────────┐ │ ● │ az-ruler ────────────────────────── │ tools never cover the │pt │ │ image; labels on hover/ │────│ ___ │ wide-rail kill the │ ⌇ │ / smoke \ ← traced polygon │ ⌇-vs-▱ ambiguity │line│ │ │────│ │ │ ▱ │ │ │area│ │ │────│ │ │ 🖌 │ │ │mask│ ◀ "mask" not "paint" — names the [→arch] plume │ └────┴───────────────────────────────────────────────┘ ┌─ annotations (3) ──────────────── [{ } export ▸] ┐ ← persistent ledger, │ ▱ polygon image ✎ 🗑 ● solid = committed │ not a 2.6s toast │ ● point image ✎ 🗑 ⌇ dashed = draft │ │ ⌇ line image ✎ 🗑 "1 exported ✓ copied" │ └────────────────────────────────────────────────────┘ ``` The fork: **rail** (above) costs frame width but never occludes content and gives room for **labels + brush controls** (paint genuinely needs brush size/opacity — note-03 Sketch 1 said so). The cheaper alternative is to **keep it floating but move it off the image** — dock it into the `.inspect-head` strip (which today only holds title/cap/✕) so it overlays chrome, not sky. My lean: **dock the tool buttons into the header rail now** (trivial), and add a **persistent annotation ledger** with a draft/committed visual state (note-03 Sketch 4's "dashed→solid on export" — currently unbuilt; the code colors draft `#ffd24a` vs committed `#ff7847` in the SVG but there's no *list* the user can scan). For `{ } STAC`: keep the clipboard copy but **also** show a small inline "✓ copied · N features" chip that persists until the next edit, not a disappearing toast. **[→arch] generalization seam:** name the raster tool **"mask"** not "paint," and make its output the same `feature` envelope (just `kind:"mask"`, href to a PNG) so that a hand-painted plume mask and a **frame-difference-derived plume mask** are the *same annotation type* with different provenance (`createdBy:"analyst"` vs `createdBy:"frame-diff"`). That keeps the UX recommendation compatible with note 06's plume pipeline: the analyst paints, the algorithm proposes, both land in one ledger. ### 3. Map ↔ image coupling — the right idea, but the map is too small and the coupling is one-way **Observation.** The red ray (`setProjectionRay`) is the spatial heart of inspect mode. But in the built layout the map is the **top 80% of the right 50% column** (map-panel `flex:4`, sharing with notes `flex:1`) — and `RAY_M = 22 km` ray plus `WEDGE_M = 6 km` wedges have to read in that box. With 10 cameras spread over a burn the ray is often a short stub near the edge. Coupling is also **one-way**: hovering the image moves the ray, but hovering the **map** does nothing back to the image, and neither touches the pano. **Why it matters.** If the payoff of leaving the wall is "see where this pixel points," the map must be big enough that the bearing is legible against terrain, and ideally the loop should close both ways (this is the triangulation substrate **[→arch]**: two cameras' rays crossing = the fire location note 06 cares about). **Sketched alternative — foreground: *should the coupling be bidirectional, and across all three surfaces (image ⇄ map ⇄ pano)?*** ``` image hover ──draw ray──▶ MAP (built ✓) image hover ──move marker─▶ PANO (NOT built — should it?) MAP hover ──back-project▶ image (NOT built: hover a ground point → crosshair on the frame where it'd appear) PANO hover ──set az─────▶ MAP ray + image highlight (NOT built) ALT-3 give the map enough room (pairs with ALT-1a's 30% rail) AND add a second cursor on the pano so one mouse drives all three "hemispheres": ┌ inspected frame ─────────────┐ ┌ MAP (taller) ──────┐ │ • cursor here (az 236) │ │ ▲cam ──────▶ ray │ └──────────────────────────────┘ │ crosses 2nd │ ┌ PANORAMA ─────────────────────────────────┐ cam's ray │ │ ····│····│····│··▼··│···· ▼ = same az │ = FIRE ⊕ │ └────────────────────────────────────────────┘──────────┘ ``` The fork: **minimum** = just give the map more height in inspect mode (note demotes to a tab) so the ray reads. **Maximum** = make the cursor a single spatial pick that lights up frame + map ray + pano position simultaneously, and add **map→image back-projection** (hover terrain, get the frame crosshair). My lean: **bigger map now** (falls out of ALT-1a for free), **image→pano cursor next** (cheap: the pano already has an az-ruler; drop a moving `az-mark` at the hovered az), and reserve **map→image back- projection + ray-crossing triangulation** as the explicit **[→arch] seam to note 06** — that's where "inspect mode" stops being a viewer and becomes a geolocation tool. ### 4. Panorama bar — near-useless when empty, under-specified when present **Observation.** For the Sandy archive (the only wired dataset) the pano is **always 404** — so the full-width bar permanently shows "no panorama for this frame (archive); live cams only," consuming `flex:0 0 30%` of vertical space (`#pano-bar`) for a dead message. When present (live cams) it shows the `_p` wide image with a **±90° az-ruler and a single red `az-mark` at `fr.az`** — but ±90° is a guess (note 04 admits the extent is unknown), and there's **no "fov window" showing what slice of the pano the narrow inspected frame actually covers**.
**Why it matters.** The pano's entire value is *context*: "this 15° telephoto frame is **this sliver** of the 180°+ sweep." A single center tick under-delivers that; an empty 30%-tall bar actively wastes the focus mode's space. The pano is also the natural **azimuth scrubber** (drag along it to slew a PTZ preview / pick which frame to inspect). **Sketched alternative — foreground: *the pano should show the frame's fov WINDOW, and collapse when empty.*** ``` ALT-4 "you-are-here fov window on the pano; empty = 1-line stub, not a 30% void" PANORAMA College Peak · live CDN ┌──────────────────────────────────────────────────────────────────────────┐ │ 160° 190° 220° ┌════════┐ 250° 280° 310° │ │ ░░░░░░░░░░░░░░░░░░░░░░│ FRAME │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ← the box is │ │ ░░░░ wide sweep ░░░░│ az 236 │░░░░ the rest of the sweep ░ the narrow │ │ ░░░░░░░░░░░░░░░░░░░░░░└════════┘░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ frame's fov │ └──────────────────────────────────────────────────────────────────────────┘ • drag the box / the strip → slews azimuth (preview), helping pick the next frame • EMPTY state collapses to: "▸ Panorama — none for archive frames" (one line) ``` The fork: the **window** (drawing `[fr.az − fov/2, fr.az + fov/2]` as a box on the pano) is the high- value cheap win — you already have `fr.fov` and a ruler; it makes the wall↔pano↔frame relationship legible. **Empty-collapse** is pure layout (don't reserve 30% for a 404). Pano-as-azimuth-scrubber is the richer bet (note 04 already floated "its own scrubbable lane"). My lean: **fov-window + empty- collapse now**; azimuth-scrub later. **[→arch]** the pano is also the cheapest "coarse 3D" backdrop — a registered panosphere slice — so the fov-window is the same primitive note 06's panosphere/taos backdrop will reuse. ### 5. Scrub-while-inspecting — works, but the affordance is invisible **Observation.** `updateWall()` calls `refreshInspect()` every pump, so scrubbing the timeline **does** swap the inspected frame, ruler, pano, and ray live (verified in note 04). This is a genuinely nice behavior. But **nothing in the inspector tells you that.** The timeline is at the top, the inspector covers the left panel; a user focused on the frame has no cue that the clock above still drives it, and no temporal control *inside* the focus area. **Why it matters.** A discoverable temporal loop ("I'm looking at one camera *through time*") is the difference between "a still viewer" and "a per-camera playback." Right now it's a hidden feature. **Sketched alternative — foreground: *does inspect mode need its own mini temporal control + frame step?*** ``` ALT-5 a thin per-camera frame-strip inside the inspector head/foot ┌ College Peak · 2026-05-18 22:35:57Z · az 236.5 el -0.1 fov 15 ─────── ✕ ┐ │ ◀ ││││·│││││││·│▮│││·││││││ ▶ "frame 214 / 503" ⟲ this cam only │ │ ▲ this camera's own frames; ▮ = current; ◀▶ = step ±1 frame │ └──────────────────────────────────────────────────────────────────────────┘ ``` A per-camera frame ribbon (the inspected cam's frames, not all 10) with **◀ ▶ step** keys (and arrow- key binding) makes the temporal dimension *present in the focus mode*, lets you step frame-by-frame to watch smoke grow (the analyst's actual gesture), and visually signals "the clock still applies here." Cheap: the data is `inspectCam.frames`; reuse `drawAzRuler`'s tick approach. My lean: **add ◀ ▶ frame- step + a one-line "frame i/N" readout** at minimum; the full ribbon if §1 gives the height. ### 6. Entry / exit / multi-camera — entry is fine, exit is fine, but you're trapped on one camera **Observation.** Entry: click tile **or** map glyph → `openInspect` (good, two affordances). Exit: `✕` and `Esc` both wired (good). But once inspecting **camera A**, to look at **camera B** you must **close, find B's tile, reopen** — there's no "next camera" step, even though the wall is fundamentally a *set* you're comparing. The note-04 framing ("brought up over the two bottom panels") is also now slightly off: it covers the **left panel only**, and the pano is a **third** full-width region, so the "two bottom panels" mental model doesn't match the built three-region layout (grid-overlay + map/notes + pano). **Why it matters.** Firewatch is comparative: "is that smoke or cloud? check the neighbor camera that also sees that bearing." Forcing a full exit/re-enter per camera breaks the comparison rhythm. And the mismatched spatial model (note 04 vs built) will confuse the next reader. **Sketched alternative — foreground: *step between cameras without leaving inspect; and is the next camera chosen by index or by "who else sees this bearing"?*** ``` ALT-6 prev/next camera in the head, + a "cameras seeing this az" cluster ┌ ‹ A [ College Peak ▾ ] B › · az 236 ──────────────────────── ✕ ┐ │ │ │ ‹ › = prev/next camera (keeps timecode, re-points map+ray) │ │ ▾ = dropdown of all cams; ★ marks cams whose current frustum │ │ overlaps THIS camera's red bearing → the triangulation set │ └───────────────────────────────────────────────────────────────────────┘ ``` The fork: **plain prev/next** (index order) is trivial and solves the trap. The richer bet is **"who else sees this bearing"** — surface the cameras whose wedge crosses the current ray, because that's the *comparison the analyst actually wants* (and **[→arch]** it's the same ray-crossing set note 06 needs for triangulation/coarse-3D). My lean: **prev/next now**, "cameras-seeing-this-az" cluster as the [→arch] bridge. Also: **fix the note-04 "two bottom panels" wording** to match the built three regions. ### 7. Enjoyment / craft — what would make an analyst *want* this tool
- **The red ray should feel alive.** Today it's a static polyline that jumps to the cursor. A subtle ease (lerp the endpoint over ~80ms) + a soft glow (the `az-mark` already glows; the ray doesn't) makes scanning a bearing feel like sweeping a flashlight. Cheap, high delight. - **Frame transitions on scrub.** Swapping `sImg.src` hard-cuts. A 1-frame crossfade (or holding the old frame until the new `decode()`s — the wall already gates on decode) removes the flicker and makes per-camera playback feel like video. Pairs with §5's frame-step. - **Hover-to-measure.** The xy-readout shows az/el but not a *bearing back to the wall*. Adding a tiny compass rosette or "236°T" with a cardinal letter (NE) reads faster than a number under stress. - **Annotation "lands" feedback.** When a point/polygon commits, a quick pulse on the SVG vertex (note- 03's draft-dashed → committed-solid, currently only a color diff) gives the satisfying "it stuck." - **Empty-state honesty as character, not apology.** The pano's "archive; live cams only" is good; give the empty notes/annotation ledger the same voice ("no marks yet — click the smoke") so the tool feels guided, not blank. These are the difference between "a working inspector" and "a tool an analyst reaches for." All are post-§1–§4 polish (Tufte stage), not blockers.
## Top 5 changes (impact × effort) | # | Change | Impact | Effort | Why first | |---|--------|--------|--------|-----------| | 1 | **Inspect reflows the workspace** (frame ~70%, notes→tab, map stays a real rail) — ALT-1a | High | Med | Makes the frame the focus; unblocks §3 (readable map) and §4 (room to collapse pano). The headline fix. | | 2 | **Tools off the image** (dock into head rail) + **persistent annotation ledger** with draft/committed state + inline "✓ copied · N" — ALT-2 | High | Low–Med | Stops occluding the smoke; fixes the "did my export save?" gap note 03 itself flagged. | | 3 | **Pano fov-window + empty-collapse** — ALT-4 | Med–High | Low | Turns a permanent 30% dead bar into real context ("this frame is *this* sliver"); reclaims focus space. | | 4 | **Prev/next camera in-inspector** (+ ◀▶ frame-step, §5) — ALT-6 / ALT-5 | Med–High | Low | Unblocks the comparative + temporal gestures that are the actual firewatch job. | | 5 | **Ray/ruler legibility + alive ray** (center az-mark on inspector ruler, tick outlines, eased+glowing ray) — §1/§7 | Med | Low | Cheap craft that makes the load-bearing spatial feedback readable on bright sky and pleasant to use. |
## Open questions for Stephen 1. **Does inspect mode reflow, or just overlay one panel?** (§1.) Built = overlay left-50% only. My lean: **reflow** (ALT-1a) — frame to ~70%, notes demote, map stays a legible rail. Or go modal with a map PiP (ALT-1b) if maximal frame beats map size. 2. **Tools: rail off the image, or dock into the header?** (§2.) Both get them off the smoke; rail buys room for paint/mask brush controls. And: is the **export confirmation** a persistent ledger chip (my lean) or fine as the current toast? 3. **Should the cursor drive all three surfaces** (image → map ray **and** pano position), and should the loop go **backwards** (map hover → frame crosshair)? (§3.) The backward direction is the triangulation seam to note 06 — build it here or there? 4. **Pano: fov-window + azimuth-scrub?** (§4.) The window is cheap and high-value; is pano-as-azimuth- scrubber wanted now or a later lane? 5. **Per-camera temporal control in the inspector** — frame-step ◀▶ / mini-ribbon? (§5.) Or is the top timeline enough once we *signal* it still applies? 6. **Multi-camera stepping: index order, or "cameras seeing this bearing"?** (§6.) The latter is the comparative+triangulation gesture but couples to the wedge/ray geometry and note 06. 7. **[→arch] Does the annotation ledger generalize to algorithmic plume masks?** If note 06's frame- difference → plume-mask flow lands, should a derived mask be the **same feature type** with `createdBy:"frame-diff"` provenance, sharing this UI's ledger and STAC export? (My strong lean: yes — name the raster tool **"mask"** not "paint" now so the two compose.) > Cross-bead refs (absolute, per agent-agnostic policy): > - φ–θ panosphere / through-the-lens calibration (replaces level-mount `uvToAzEl`): > [astronomical-pano `9943ac1c`](https://redfish.acequia.io/guerin/.agents/9943ac1c-caa1-479d-b17e-89ef7ca326b6/about.md) > and image-pose-uxui `74c30681` (through-the-lens calibration sibling). > - Prior sketches this critiques against: [note 03](03-image-annotation-uv-vector-stac-sketches.md); > build under critique: [note 04](04-frustums-projection-inspector-panorama-build.md); > architect companion (plume-analysis flow / fork-app): note 06 (in progress).
## References (bead cross-links) - Bead: Astronomical Pano · [canonical](https://redfish.acequia.io/guerin/.agents/9943ac1c-caa1-479d-b17e-89ef7ca326b6/)