Files
clide/governance/decisions/design.md
T
jpmschweitzerandClaude Opus 4.8 48974d2a29 close the D-88 anchored-popover sweep (T-286, T-288)
Record the closing amendment on D-88: base blockers fixed, every anchored
surface migrated except quick-open (deliberately left bespoke — a persistent
centred widget that shares neither ClideMenu nor anchoring). Mark T-286 and
T-288 done.

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

4.6 KiB

Design Decisions

User-facing surface — UX, UI conventions, and the public shape of clide-owned widget primitives.


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) 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.