Skip to main content

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-id tooltip engine. The tooltip engine has a tuned token contract (--tip-* CSS custom properties, the useTooltipEngine hook, the TooltipHost and TooltipPanel components) 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 LocationContextPanel on the strategic map.
  • A blocking modal surface for branching choice, dangerous confirmation, and major outcome flows. The existing PendingChoiceModal and CombatDetailModal cover the prior baseline; Wave C added ActionConfirmModal and FeatureDiscoveryModal.

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-id registry through useTooltipEngine, TooltipHost, and TooltipPanel. 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) and FeatureDiscoveryModal (major discovery), coordinated by the new uxModalStore. PendingChoiceModal and CombatDetailModal continue as before. Modal usage follows the allowlist in gd-shell-screen-model.md § Modal Taxonomy.

Consequences

  • A new React component pattern lives in the client tree (LocationContextPanel in 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 openCanvasLocationTooltip for 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 Layering reflects 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.tsx and FeatureDiscoveryModal.tsx — new FloatingSurface modal consumers.
  • examples/valenar/Client/src/components/tooltip/ — untouchable tooltip engine core.
  • examples/valenar/Client/src/components/tooltip/resolvers/index.ts — additive openCanvasLocationTooltip extension.
  • examples/valenar/Client/src/store/uxModalStore.ts — Zustand store coordinating the two new modals.