AD-0002: UI Surface Taxonomy
Status
Accepted (2026-05-13).
Context
The Valenar client shell exposes three potentially-overlapping interactive surfaces:
- Tooltip — read-only hover-anchored detail, driven by the existing
data-tip-idtooltip engine. The tooltip engine has a tuned token contract (--tip-*CSS custom properties, theuseTooltipEnginehook, theTooltipHostandTooltipPanelcomponents) and is considered untouchable except via additive extensions. - An anchored context menu style popover at a pointer or element that
exposes grouped quick activities without blocking the rest of the map. The
canonical instance is the right-click
LocationContextPanelon the strategic map. - A blocking modal surface for branching choice, dangerous confirmation,
and major outcome flows. The existing
PendingChoiceModalandCombatDetailModalcover the prior baseline; Wave C addedActionConfirmModalandFeatureDiscoveryModal.
Earlier wave drafts considered collapsing two of these into one surface — for example, extending the tooltip with inline buttons, or extending the modal abstraction with an "anchored mode" that would behave like a context menu. Both alternatives were rejected for the reasons below.
Decision
The Valenar shell keeps three distinct surface abstractions. They are documented in examples/valenar/docs/ux/gd-shell-screen-model.md § UI Surface Taxonomy and enforced in code by the separate component trees:
- Tooltip is read-only. It must not gain inline buttons, forms, or
activity dispatch. Tooltip lookups remain driven by the
data-tip-idregistry throughuseTooltipEngine,TooltipHost, andTooltipPanel. Tooltip core files (useTooltipEngine.ts,TooltipHost.tsx,TooltipPanel.tsx,tooltip.module.css) are untouchable; the--tip-*CSS custom properties remain the contract. The only allowed extension is additive: new resolver exports (e.g.openCanvasLocationTooltip) attached to the existing engine, never structural changes to it. - ContextPanel is a new, separate component pattern. Its first
implementation is
LocationContextPanel, an anchored popover that opens at the cursor on a right-click on a Location, viewport-clamped, non-blocking, dismissed on outside click, Escape, activity selection, or selection change. ContextPanel groups its entries into category labels (Travel, Survival, Explore, Planning for the Location use case). Selections route through the canonical queue / overlay path; there is no parallel command path. - FloatingSurface remains the blocking modal pattern. Wave C extended it
with
ActionConfirmModal(dangerous-activity confirmation) andFeatureDiscoveryModal(major discovery), coordinated by the newuxModalStore.PendingChoiceModalandCombatDetailModalcontinue as before. Modal usage follows the allowlist ingd-shell-screen-model.md § Modal Taxonomy.
Consequences
- A new React component pattern lives in the client tree (
LocationContextPanelin Wave C). Future ContextPanel instances should follow the same anchoring, dismissal, and routing rules without re-deriving them. - The tooltip engine's untouchable rule is preserved. Wave C extended the
resolvers index with
openCanvasLocationTooltipfor canvas-driven anchors, but the engine core and the--tip-*token contract were not modified. - FloatingSurface is the only blocking surface. ContextPanel never blocks; Tooltip never blocks; both must dismiss as the player continues interacting with the map.
- Mixing the three is a regression signal. A "context menu that prevents map scrolling" or a "tooltip with a confirm button" indicates a design drift back toward a single overloaded surface and should be corrected to the appropriate abstraction.
- The shell layering rule in
gd-shell-screen-model.md § Overlay Layeringreflects the three surfaces explicitly (Tooltip layer, ContextPanel layer, FloatingSurface modal layer) so renderers do not accidentally re-order them.
Alternatives Considered
- Extend FloatingSurface with an anchored mode. Rejected. A modal's contract is "block input outside the modal until the player confirms or cancels". Adding a non-blocking anchored mode strains that contract; the modal stops being a modal. A separate non-blocking ContextPanel pattern is cleaner.
- Extend TooltipHost as the anchor for an interactive popover. Rejected.
The tooltip engine is tuned for read-only hover. Embedding interactive
buttons inside the tooltip surface breaks its dismissal model (tooltips
close on pointer leave) and conflicts with the
--tip-*token contract. Interactive popovers belong to ContextPanel. - Collapse Tooltip and ContextPanel into one engine. Rejected. Read-only hover and interactive right-click have different lifetimes (pointer-leave vs explicit dismissal), different accessibility requirements (focus management for interactive surfaces only), and different content shapes (read-only key-value vs grouped activity list). Sharing an implementation would force one to inherit the other's constraints.
References
examples/valenar/docs/ux/gd-shell-screen-model.md— UI Surface Taxonomy and Overlay Layering sections.examples/valenar/Client/src/components/map/LocationContextPanel.tsx— ContextPanel implementation.examples/valenar/Client/src/components/notifications/ActionConfirmModal.tsxandFeatureDiscoveryModal.tsx— new FloatingSurface modal consumers.examples/valenar/Client/src/components/tooltip/— untouchable tooltip engine core.examples/valenar/Client/src/components/tooltip/resolvers/index.ts— additiveopenCanvasLocationTooltipextension.examples/valenar/Client/src/store/uxModalStore.ts— Zustand store coordinating the two new modals.