From b7990ea557c24ff6c1c8a498b08ca5890ee3be37 Mon Sep 17 00:00:00 2001 From: mintaka Date: Wed, 26 Aug 2026 21:58:16 -0400 Subject: [PATCH] docs(product): native first-run product tour design record (RIG-2797) Design record for the native first-run product tour: a store-gated App-root SolidJS overlay whose steps anchor to real chrome via data-tour attributes and drive the app's own router/store, entering on the chase-light motion primitive. No PostHog-rendered UI ships in-app; PostHog is measurement-only through a no-op-safe captureTourEvent indirection over the #656 T6 embed, and tour UI has zero dependency on that embed landing. Drafted by the design subagent, red-teamed by design-critic; both blocking findings (async anchor-resolve race, unreachable agent-workspace nav) and all should-fix findings folded. Ledger delta DL-272..275 applied to docs/designs/DECISIONS.md in the same PR (UX foundation section). Three load-bearing open questions (remote content, seen-state grain, step-arc ratification) go to Matt for ruling before the freeze. Co-authored-by: Matt Wilkinson --- docs/designs/DECISIONS.md | 4 + .../product/compass-first-run-tour/design.md | 660 ++++++++++++++++++ 2 files changed, 664 insertions(+) create mode 100644 docs/designs/product/compass-first-run-tour/design.md diff --git a/docs/designs/DECISIONS.md b/docs/designs/DECISIONS.md index dfd5ec020..a7e4f217a 100644 --- a/docs/designs/DECISIONS.md +++ b/docs/designs/DECISIONS.md @@ -323,6 +323,10 @@ check enforces the mechanical half. Full rationale: | DL-250 | The leader runtime is pending-leader closure state INSIDE the one `installKeymap` keydown handler (never a second listener, honoring DL-223): guard-FIRST ordering (a modifier-less key on an editable/interactive target types, never arms — the guard moves ahead of the `matching.length===0` early return at `dispatch.ts:115`), arm on a leader-set hit (`!event.repeat`) with a `LEADER_TIMEOUT_MS` disarm timeout, completion resolves the two-segment chord through the UNCHANGED three tiers (DL-222), dead sequences fall through RE-ENTERING arming (a re-pressed leader re-arms), Escape disarms, Mod-chords disarm-and-run, the uninstaller clears the timer. A separate leader-mode dispatcher was rejected (duplicate guard + tier logic). | Active (Matt, 2026-08-23) | [leader chords §A3](product/compass-leader-chords/design.md#a3--the-leader-runtime-pending-state-inside-the-one-keydown-handler) | | DL-251 | A leader sequence is NEVER written into `aria-keyshortcuts` (WAI-ARIA has no sequential-press grammar — `"G B"` reads as "G OR B"): the shipped scanners harden ASYMMETRICALLY (refining DL-234's `shortcutFor` contract) — `shortcutForAria` (`keymap.ts:70-76`) SKIPS sequence rows so a sequence-only command emits no attribute, while `shortcutFor` (`keymap.ts:57-63`) FORMATS them via one pure `formatChordForDisplay` (`"G then B"`) so point-of-use display (sidebar `title`, palette chip) is preserved. A dual-bound command keeps its modifier chord in both. Minting parallel replacement helpers was rejected (leaves the shipped ones emitting the bug). | Active (Matt, 2026-08-23) | [leader chords §A5](product/compass-leader-chords/design.md#a5--display-and-aria-keyshortcuts-for-sequences) | | DL-252 | Wave-1 leader allocation is `G B`/`G L`/`G D`/`G S` → view.bridge/backlog/done/settings — all four commands ALREADY registered by the shipped spine (`spine.ts:79-126`) and wired (`store.ts:1973-1980`), so wave 1 adds ONLY keymap rows (no spine/store change); `view.agentWorkspace` gets no `G A` (spine registers no such command). Zero bare letters in wave 1 (the board's verbs are group-relative; Linear's a/s/p act on issue fields Compass's board doesn't yet edit). Coexists additively with all existing `Mod+*` bindings (DL-152); deprecating overlapping modifier chords is a deferred later call (record OQ5). | Active (Matt, 2026-08-23) | [leader chords §A4](product/compass-leader-chords/design.md#a4--bare-letter-and-sequence-allocation-wave-1) | +| DL-272 | The first-run product tour (RIG-2797) is built natively in SolidJS v2 as a store-gated App-root overlay (a third sibling of the shortcuts overlay + palette) whose steps anchor to real chrome via `data-tour` attributes and navigate the real router through the store's closures; PostHog-rendered UI (Product Tours/surveys/banners or any `posthog-js` widget) NEVER ships in-app — PostHog is measurement/data-only (Matt's RIG-2793 ruling, restated by #656 T6) | Active (Matt, 2026-08-26) | [first-run tour §A1](product/compass-first-run-tour/design.md#a1--host-shape-an-app-root-overlay-layer-store-gated) | +| DL-273 | Tour callout substrate is the Kobalte v2-alpha `Popover` (external `anchorRef` + controlled `open` — DL-150 a11y-hard scope); the welcome/finale modal steps stay hand-rolled on the `.cx-dialog` convention (DL-230), reusing the ShortcutsOverlay focus trap/restore; a missing `data-tour` anchor skips the step only after a bounded reactive resolve, never errors | Active (Matt, 2026-08-26) | [first-run tour §A3](product/compass-first-run-tour/design.md#a3--callout-substrate-kobalte-popover-with-an-external-anchorref) | +| DL-274 | Tour "seen"/resume state persists as best-effort localStorage via the shipped `safeLocalStorage` + write-through pin-set pattern, self-healing on hydrate, auto-opening only when storage is present and empty (locked storage → replay-only); the namespacing grain (client-global vs per-workspace vs global-seen + per-workspace resume) is set by OQ-2's ruling, and server-side preference sync is a named future, not day-1 | Active (Matt, 2026-08-26) | [first-run tour §A5](product/compass-first-run-tour/design.md#a5--first-run-detection--persistence-the-safelocalstorage-pattern) | +| DL-275 | Tour analytics ride a thin call-time indirection (`captureTourEvent`) over the #656 T6 PostHog embed: flag off → silent no-op, no static `posthog-js` import in tour code; only the instrumentation task sequences after T6 — the tour UI has zero dependency on it. Step content ships static in-code; the headless flag/EAF remote-content path is a deferred additive behind the same `TourStep[]` type | Active (Matt, 2026-08-26) | [first-run tour §A7](product/compass-first-run-tour/design.md#a7--analytics-a-thin-no-op-safe-indirection-over-the-t6-embed) | ## Bridge spawn control diff --git a/docs/designs/product/compass-first-run-tour/design.md b/docs/designs/product/compass-first-run-tour/design.md new file mode 100644 index 000000000..a473d7858 --- /dev/null +++ b/docs/designs/product/compass-first-run-tour/design.md @@ -0,0 +1,660 @@ +# Compass native first-run product tour (RIG-2797) + +Status: Draft + +Parent: the Compass onboarding/discoverability net. The coaching-tooltips +record explicitly deferred "a first-run coach" to a future record +(`compass-coaching-tooltips/design.md` §Deferred — "The wider onboarding +affordances — empty-state keyboard nudges, a persistent key-hint footer, a +first-run coach — are explicitly deferred by Matt"); this is that record. +Consumes (does not build) the PostHog embed seam of the observability record +(PR #656 T6, `docs/designs/platform/compass-observability-architecture/`). + +## Problem / Intent + +A first launch of Compass drops the user cold onto the Bridge with zero +orientation: nothing introduces the board, the agent tree, the comms surfaces, +or the keyboard-first posture the whole UX is built around. Ship a first-run +product tour built **natively in SolidJS v2** — real components anchored to the +app's real chrome, driven by the app's own router and store, entering on the +brand chase-light motion — that wows on first boot, is skippable and +replayable, and (once the analytics embed exists) reports its funnel through +headless PostHog capture. No PostHog-rendered UI ships in-app, ever +(Matt's RIG-2793 ruling — the tour split off that thread — restated by +PR #656 T6: "no PostHog-rendered UI ships in the product"). + +## Approach + +One store-owned tour controller plus one App-root overlay component: the tour +is a sequence of **steps**, each either a centered dialog (welcome / finale) or +a **callout anchored to a real UI element**, advancing through the app's real +navigation so the user watches the actual product move — the tour IS the app, +never a screenshot-overlay fighting it. + +### A1 — Host shape: an App-root overlay layer, store-gated + +The tour mounts exactly where the app's other transient layers do. `App` is the +router's always-mounted root layout (`mount.tsx:44-50` — `createRouter({ +routes: appRoutes, history: hashHistory() })` with `{(props) => }`), and it already hosts the shortcuts overlay and the +palette behind store signals (`App.tsx:185-191`): + +> ```tsx +> +> +> +> +> +> +> +> ``` + +`TourOverlay` is a third sibling behind `store.tour.open()`. Because App wraps +every route, the overlay survives the route changes the tour itself performs — +a step can navigate to `/backlog` and keep narrating. The controller state +(current step index, open flag) lives in the store beside the sibling overlay +signals (`store.ts:268-273` — `shortcutsOpen` / `hideShortcuts` / +`toggleShortcuts` is the established shape), built in `createAppStore` where +the navigation closures it drives already exist (`store.ts:1950-1965` — +`showBridge`/`showBacklog`/`showDone`/`showSettings` each `hideShortcuts()` +then `navigateTo(...)`). + +### A2 — Step model: declarative steps over real anchors + +A step is data, not a component: + +```ts +interface TourStep { + /** Stable id — the analytics event dimension and the resume cursor. */ + readonly id: string; + /** Centered dialog (welcome/finale) or anchored callout. */ + readonly kind: "dialog" | "callout"; + /** For callouts: the `data-tour` anchor value to attach to. */ + readonly anchor?: string; + /** View the step needs; controller navigates via the store closure on + * entry. Only the four closure-backed static views (A1); parameterized + * surfaces (agent workspace) are reached by anchor, never navigated. */ + readonly route?: "/" | "/backlog" | "/done" | "/settings"; + readonly title: string; + readonly body: string; +} +``` + +Anchoring is by **`data-tour=""` attributes on the real elements** — +e.g. the LeftSidebar Bridge button (`LeftSidebar.tsx:442-454`, the +`CoachTipTrigger as="button" class={["bridge-link", …]}` view buttons), the +topbar view-tabs nav (`App.tsx:86` — `