V1 Web PWA · Surface walkthrough

Shell: Overlays and panels

A per-surface walkthrough of the V1 Web PWA: layout, states, interactions, data, and cross-references.

unspecified
13sections5 minread

On this page

Source: apps/oshun/web/src/components/command-palette/CommandPalette.tsx (re-exported as components/CommandPalette.tsx), components/UniversalSearchPanel.tsx, components/assistant-dock/AssistantDock.tsx, components/assistant/AssistantPanel.tsx, components/WhatsNewDropdown.tsx, components/CrossDomainQuickActionsMenu.tsx, components/QuickActionsTrayPanel.tsx, components/WidgetSidebar.tsx, components/InProductHelp.tsx, components/DeepLinkBuilder.tsx, components/SmartAppBanner.tsx, components/ShellLayout.tsx, navigation/assistant-entry.ts (all under apps/oshun/web/src/)

Every cross-cutting overlay the customer can summon from any route. There is no shared Dialog / Modal / Sheet primitive — each overlay hand-rolls its own open-state, focus management, dismissal, and z-index. Walk this whenever a new overlay is added or the z-stack changes.

Command palette#

components/command-palette/CommandPalette.tsx. Mounted by ShellLayout on all non-admin shells: <CommandPalette activeRoute={active} />.

  • Trigger⌘K / Ctrl+K; / focuses shell-search input instead
  • z-index — backdrop 30, palette body 40
  • Semanticsrole="dialog", aria-label="Command palette", aria-modal="true"; input aria-label="Search commands"; list role="listbox" + aria-label="Command results", options role="option"
  • Categoriesrecent | navigation | domain | search | create | agent | jump | quick-action (CATEGORY_LABELS)
  • Recents persisted to localStorage['oshun-cmd-recent'] (queries, max 5) + oshun-cmd-recent-items (items, max 8)
  • Keyboard nav — Up/Down moves selection; Enter activates; Esc closes
  • Mobile — full-screen overlay; input focused via requestAnimationFrame so iOS keyboard pushes content
  • Recent-item actionsnavigate (href), assistant (prompt + source via dispatchOshunAssistantOpen), query (search text)

Universal search panel#

components/UniversalSearchPanel.tsx. Mounted inline by HomeWorkspace. It is a section, not a modal<section aria-label="Universal search">, input + clear aria-labels. Filters SEARCH_DOMAIN_FILTERS + all; recents at localStorage['oshun-search-recent'] (max 5). Cross-domain groups via SEARCH_CROSS_DOMAIN_CONNECTIONS shown when ≥2 active domains in results.

  • BFF callbuildOshunBffUrl(...) + resolveBffAuthToken()
  • Save to librarytoggleOshunWebLibraryItem + trackLibraryItemSaved / Unsaved; open via trackSearchResultOpened

Assistant dock + panel#

components/assistant-dock/AssistantDock.tsx + components/assistant/AssistantPanel.tsx. Opened from anywhere via dispatchOshunAssistantOpen() (custom event oshun:assistant-open); closed via dispatchOshunAssistantClose().

  • Dock mode persisted to localStorage[ASSISTANT_DOCK_MODE_KEY] = closed | collapsed | expanded; widths EXPANDED = 360, COLLAPSED = 128
  • Mount thresholdASSISTANT_DOCK_MIN_WIDTH = 1360; below, the assistant opens as an overlay panel not the right-rail dock
  • Layout<aside data-shell-assistant-dock> position: fixed, top: <header offset>, right: 20, bottom: 20, zIndex: 11
  • Collapsed view shows thread + stage + entry labels + Expand CTA; expanded mounts AssistantPanel presentation="dock"
  • Open event payload{ source, prompt, requestedModeId }; AssistantContextHandoff sent when a domain page transfers state

What's-new dropdown#

components/WhatsNewDropdown.tsx. Mounted by ShellLayout (top nav on desktop, mobile menu on mobile). Header trigger with aria-expanded + aria-haspopup; dropdown at position: absolute, width: 380, maxHeight: 480, zIndex: 20. Entry types feature | improvement | fix (+ optional domain, href); pulsing terracotta unread dot when entries newer than last-seen. Open animation oshun-fade-in-down; close oshun-scale-out; click-outside + Esc dismiss.

Cross-domain quick-actions menu#

components/CrossDomainQuickActionsMenu.tsx. Mounted by ShellLayout and by ExploreDashboard. Trigger variants dock (header) and panel (in-page). QUICK_ACTION_PRESETS define domainId + section ('center' | 'companions') + eyebrow + title + summary + path + icon. Unavailability handled by isDomainTemporarilyUnavailable + buildUnavailableDomainLaunchPath. Mounts via createPortal; onOpen + onNavigate telemetry callbacks.

Quick-actions tray panel#

components/QuickActionsTrayPanel.tsx. Defined, no current customer-surface mount (see Open questions). Surfaces home | explore; backdrop zIndex: 30, tray 40; actions carry domain (DomainName | null) for accent (falls back to L.accent); ripple from click origin.

Widget sidebar#

components/WidgetSidebar.tsx. Defined, no current mount (see Open questions). 280 px right rail. Widgets: streak | habits | timer | calendar | stats | quote (default visible: streak, habits, timer, stats). Controls via open + onOpenChange; per-widget visibility toggle in settings popover; GripVertical reorder.

In-product help#

components/InProductHelp.tsx. Mounted ad-hoc by routes that opt in (e.g. app/messages/page.tsx).

  • Trigger — Help button with aria-haspopup="dialog" + aria-expanded
  • Panel <aside role="dialog" aria-label="In-product help"> (non-modal); walkthrough overlay <div role="dialog" aria-modal="true" aria-label="Walkthrough">
  • Fetch /api/help/contextual?surface=<pathname>&audience=<…> (loading / error / loaded); audience customer | admin | creator | partner; sections cover tooltips, walkthroughs, videos, articles

components/DeepLinkBuilder.tsx. Defined; customer-surface mounts not yet wired. Builds https://oshun.app/<domain>/<contentType>/<id> URLs. Domain options tara | veritas | nyx | arete; contentType session | article | observation | habit | goal. buildUrl encodes contentId, returns empty when any part missing. Share via tryNativeShare() (navigator.share), falls back to clipboard with Check icon for 2000 ms.

Smart app banner#

components/SmartAppBanner.tsx. Mounted by welcome / download / domains public routes (app/welcome/*/page.tsx). Detects platform from UA (iphone|ipad|ipodios; androidandroid; else web), per-platform store URL + scheme oshun:// + action label. Dismiss persisted to localStorage['oshun_smart_banner_dismissed'] (7-day cooldown). Slide-down on entrance, slide-up on close; forcePlatform prop for tests. No explicit z-index — verify no collision on welcome routes.

Focus, dismissal, z-index#

Each overlay hand-rolls these — no central manager. z-stack has no central scale; observed: AssistantDock 11, WhatsNewDropdown 20, CommandPalette backdrop 30 / body 40, QuickActionsTrayPanel backdrop 30 / body 40. Modal-style overlays share 30/40 and are not designed to stack; the assistant dock sits below intentionally (persistent rail).

  • Open focusrequestAnimationFrame(() => inputRef.current?.focus())
  • Esc — each component wires its own document.addEventListener('keydown', …); closes topmost overlay
  • Backdrop click — palette + tray dismiss; dropdowns use click-outside on the popover
  • Focus trap — palette traps Tab in handleInputKeyDown; others mostly do not trap; most overlays also do NOT restore focus to opener
  • Native <dialog> picks up Lilith chrome from lilith.css, but most overlays predate and bypass the native element

Cross-references#

Open questions / known gaps#

  • QuickActionsTrayPanel, WidgetSidebar, DeepLinkBuilder have no customer-surface mount sites — pending integration or vestigial?
  • No shared Dialog / Modal / Sheet primitive — every overlay reimplements semantics, focus trap, Esc, backdrop click. Consolidate or document the policy by reference
  • Return-focus discipline missing on most overlays; walk each with screen-reader to catch regressions
  • Z-index scale (11, 20, 30, 40) hand-picked with no central contract; move to a token map or document explicitly
  • SmartAppBanner has no explicit z-index; verify stacking on welcome routes when other overlays are also mounted
  • UniversalSearchPanel vs CommandPalette overlap in mental space — document which is canonical for which entry point