Files
clide/governance/decisions/design.md
T
jpmschweitzerandClaude Opus 4.8 529f40996d render pasted images inline with lightbox expansion (T-236, T-254, D-89)
A pasted-image @<path> token now renders as an inline, keyboard-activatable
thumbnail in the Claude conversation that opens the full image in the lightbox;
the composer's attachment chips use the same (larger, 44px) thumbnail. New
ImageThumbnail + openImageLightbox in the Claude layer; ClideMarkdown gains an
onImageToken builder seam (mirroring onRecordTap) that drops a WidgetSpan into
the text flow — it owns no Image.file/lightbox, staying generic. Missing files
degrade to a placeholder; render-only (the sent text + copyText are unchanged).

Resolves the conflicting T-236 (inline thumbnail) / T-254 (image card) designs
into the hybrid the user chose; recorded as D-89.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:54:55 +02:00

30 lines
6.3 KiB
Markdown

# Design Decisions
User-facing surface — UX, UI conventions, and the public shape of clide-owned
widget primitives.
---
### D-89: inline pasted-image thumbnails that expand to the lightbox
- **Date:** 2026-06-09
- **Decision:** A pasted-image reference (the composer's `@<path>` token) renders **inline** in the message prose as a bounded, keyboard-activatable thumbnail; activating it opens the full image in the shared lightbox (T-252). The same `ImageThumbnail` is used in the composer's attachment chips. The renderer (`ClideMarkdown`) only locates `@<path>` image tokens and drops a caller-built widget into the text flow via a `WidgetSpan` — it owns no `Image.file`/lightbox. Display-only: the text sent to Claude and the card's `copyText` are unchanged.
- **Rationale:** Two conflicting designs were filed — T-236 (inline thumbnail, in place of the token) and T-254 (a separate image-viewer **card** with a path caption). The user resolved the conflict toward a hybrid: *inline* (a separate card "breaks context") **plus** the lightbox expansion. Reusing the existing lightbox keeps presentation consistent and avoids a second image surface; keeping `Image.file`/lightbox out of `ClideMarkdown` keeps the generic renderer decoupled (it gained only an `onImageToken` builder, mirroring the existing `onRecordTap` seam).
- **Cost:** `ClideMarkdown` threads one optional builder through its inline chain; the Claude layer owns `ImageThumbnail` + the token→widget wiring. Mid-sentence tokens render the thumbnail at the token's position; in practice the composer appends tokens, so they trail the prose.
- **Supersedes:** the either/or framing of **T-236** vs **T-254** — both are satisfied by this one design (neither rejected). Raised by the user while triaging the two as "conflicting designs."
---
### D-88: clide-owned anchored popover + menu primitive
- **Date:** 2026-06-08
- **Decision:** Anchored, non-modal popovers (dropdowns, pickers, typeaheads, the command palette) build on two clide-owned primitives in `lib/widgets/`: `ClideAnchoredOverlay` (positioning + lifecycle — a `LayerLink`/`CompositedTransformFollower` or centred `Positioned`, a full-screen tap-away barrier, `OverlayEntry` bookkeeping, focus capture, Esc-to-close, and auto-flip on viewport bounds) and `ClideMenu` + `ClideMenuListController` (a dropdown-token row surface with arrow/enter/escape nav, skip-disabled/separator, active mark, and a reusable nav controller for surfaces that keep bespoke rows). No Material/Cupertino. Modal, centred dialogs (session / project / branch pickers) stay on the kernel `DialogRouter` — a separate concern.
- **Rationale:** Nine surfaces had hand-rolled the same anchored-overlay + row-list + barrier + keyboard-nav pattern (menu-bar dropdowns, theme picker, slash + @ typeaheads, quick-open, three modal pickers), each re-deriving positioning, dismissal, and nav — divergent a11y, inconsistent dismissal, and a `pumpAndSettle`-hostile spread of ad-hoc overlays. Owning one primitive (per "own the rendering stack", [D-5](architecture.md#d-5-dart-core-sidecar-dissolved-ptyc-as-pql-peer)) makes the behaviour uniform and testable once, and turns the tenth surface (the T-275 permission-mode picker) into a few lines instead of another hand-roll.
- **Cost:** A migration sweep across the existing surfaces (menu bar, theme picker, typeaheads, quick-open); the typeaheads keep their text-completion/key pipeline and only delegate anchoring + body, so the primitive must stay composable (a bare lifecycle wrapper + an optional turnkey menu), not a monolith. New UI authors must reach for the primitive rather than rolling another overlay.
- **Raised by:** 2026-06-08 — user, while building the T-275 permission-mode picker: "since we don't do Material doesn't mean we can't make components of our own." Realises the "own the rendering stack" guardrail at the component level. Tracked by epic T-286.
- **Amendment (2026-06-08):** The shared piece is `ClideAnchoredOverlay` (positioning + lifecycle) — that is what every anchored surface adopts. Content is **NOT** universally `ClideMenu`; it matches the surface's shape, to avoid bending divergent surfaces into a menu (the theme-picker migration was reverted for exactly this friction):
- **`ClideMenu`** — selectable-list menus only (menu bar, permission picker).
- **`ClideTypeahead`** (a second shared content component, to add) — the slash and @ typeaheads are near-duplicate caret-anchored completion surfaces; they share *this*, not `ClideMenu`.
- **Quick-open / command palette** — bespoke content (centred filter + fuzzy + two-column rows); uses only the `ClideAnchoredOverlay` base.
- **Theme picker** — toggle + live-apply list; its own small content (or `ClideMenu` if it ever fits cleanly), on the base.
Revised goal: *everything anchored shares `ClideAnchoredOverlay`; content components match the surface.* The blockers to clear first (focus capture racing content autofocus; follower content untappable in the `canSizeOverlay` test harness) live in the base, not in any content component. Tracked by T-288. Raised by the user: "should those then not just be a different shared component from the others?"
- **Amendment (2026-06-08) — sweep complete:** The base blockers were fixed (auto-flip reads the real `View` size, not the zeroable `MediaQuery`; the follower drops its `Align` wrapper so non-top-left anchors hit-test; tests use a sized `anchoredHarness`). Migrated: menu bar, permission picker (T-275), theme picker (onto `ClideMenu` — the HC toggle is a `keepOpenOnSelect` item; it fit cleanly), the @-mention and slash typeaheads (onto `ClideTypeahead`, which bridges its live suggestions through a `ValueNotifier` so the popover narrows as you type — the `OverlayEntry` is a separate subtree that would otherwise show the list it opened with). **Quick-open is deliberately NOT migrated:** it is a *persistent* widget that conditionally renders a centred `Positioned` (no `OverlayEntry` lifecycle, no barrier, no anchor) with bespoke two-column rows and intricate keymap-scope/focus/file-load logic — it shares neither `ClideMenu` nor any real anchoring, so wrapping it in `ClideAnchoredOverlay(centered)` would add insert/remove churn to a delicate widget for ~6 lines of trivial centring and no code reuse. The base primitive stands ready if quick-open's shape ever converges. T-286 / T-288 close here.