Initial Commit
This commit is contained in:
166
openspec/changes/enterprise-browser-react-tour/proposal.md
Normal file
166
openspec/changes/enterprise-browser-react-tour/proposal.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# OpenSpec Change: enterprise-browser-react-tour
|
||||
|
||||
- **Change ID**: enterprise-browser-react-tour
|
||||
- **Package**: `@suaveplan/react-tour`
|
||||
- **Category**: browser
|
||||
- **Target path**: `browser/react/react-tour`
|
||||
- **Status**: IN PROGRESS
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/react-tour.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/react-tour` on the genesis branch at enterprise quality. The package provides a headless `useTour` hook for guided product tours with step navigation, spotlight targeting, scroll-into-view, and completion persistence. Genesis adds Zod-validated options, live `targetRect` tracking via `ResizeObserver`/`IntersectionObserver`, a `PersistenceAdapter` interface for injectable storage, `@suaveplan/events` lifecycle events, structured logging via `@suaveplan/logger`, SSR safety guards, Storybook stories, and co-located `.md` docs.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
Fresh build — does not exist on genesis yet.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | `TourConfigError`, `TOUR_STEP_NOT_FOUND` |
|
||||
| `@suaveplan/events` (`workspace:^`) | `tour:start`, `tour:step-change`, `tour:complete`, `tour:skip`, `tour:error` |
|
||||
| `@suaveplan/logger` (`workspace:^`) | Structured logging for storage failures |
|
||||
| `@suaveplan/schema-registry` (`workspace:^`) | Register `TourStep` schema |
|
||||
| `zod` (`catalog:forms`) | `TourConfig` + `TourStep` Zod schemas |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Step-view / completion metrics |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
Dev: `vitest`, `@vitest/coverage-istanbul`, `happy-dom`, `@testing-library/react`, `@suaveplan/testing`, `@suaveplan/testing-dom`.
|
||||
|
||||
---
|
||||
|
||||
## Preserved public API surface
|
||||
|
||||
```ts
|
||||
function useTour(config: TourConfig): [TourState, TourActions]
|
||||
|
||||
// Utils
|
||||
function calculateSpotlightInset(rect: DOMRect, padding?: number): SpotlightInset
|
||||
function calculateTooltipPosition(rect: DOMRect, placement: TooltipPlacement): TooltipPosition
|
||||
function getTargetRect(selector: string): DOMRect | null
|
||||
function scrollTargetIntoView(selector: string): Promise<void>
|
||||
|
||||
// Persistence (new injectable interface)
|
||||
interface PersistenceAdapter { isCompleted(key: string): boolean; markCompleted(key: string): void; clear(key: string): void }
|
||||
|
||||
// Types
|
||||
interface TourConfig { steps: TourStep[]; storageKey?: string; persistence?: PersistenceAdapter; onComplete?(): void; onSkip?(): void }
|
||||
interface TourStep { id: string; target?: string; title: string; content: string; placement?: TooltipPlacement; shouldSkip?(ctx: TourState): boolean }
|
||||
interface TourState { isActive: boolean; currentStep: number; totalSteps: number; isFirst: boolean; isLast: boolean; progress: number; targetRect: DOMRect | null }
|
||||
interface TourActions { next(): void; prev(): void; goTo(n: number): void; skip(): void; reset(): void }
|
||||
```
|
||||
|
||||
Subpath exports: `"."`, `"./core/tour/tour"`, `"./utils/positioning"`, `"./persistence"`.
|
||||
|
||||
---
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
### 1. Zod-validated options
|
||||
|
||||
`TourConfigSchema` and `TourStepSchema` validated at hook entry; throws `TourConfigError` (extends `@suaveplan/error`) on invalid input. Registered in `@suaveplan/schema-registry`.
|
||||
|
||||
### 2. Live `targetRect` tracking
|
||||
|
||||
Replace the 100 ms `setTimeout` with a composite observer (`ResizeObserver` + `IntersectionObserver` + scroll listener) in `src/utils/observe-target.ts` so `targetRect` stays accurate during layout shifts.
|
||||
|
||||
### 3. `PersistenceAdapter` interface
|
||||
|
||||
`LocalStoragePersistence` (default), `SessionStoragePersistence`, `MemoryPersistence` (for tests), injectable via `TourConfig.persistence`. SSR-safe: `typeof window` guards everywhere.
|
||||
|
||||
### 4. Event lifecycle via `@suaveplan/events`
|
||||
|
||||
Emits `tour:start`, `tour:step-change`, `tour:complete`, `tour:skip`, `tour:error` so analytics and observability consumers can subscribe.
|
||||
|
||||
### 5. Structured logging
|
||||
|
||||
`@suaveplan/logger` replaces the three bare `catch {}` blocks in the original for storage and scroll errors.
|
||||
|
||||
### 6. Storybook stories
|
||||
|
||||
`useTour.stories.tsx` — basic tour, spotlight overlay, scroll-into-view, completion persistence, accessibility-compliant tooltip overlay.
|
||||
|
||||
### 7. Error codes
|
||||
|
||||
`TOUR_CONFIG_INVALID`, `TOUR_STEP_NOT_FOUND`.
|
||||
|
||||
### 8. OTel metrics
|
||||
|
||||
Counter `tour.step_view_total`, counter `tour.complete_total`, counter `tour.skip_total`, histogram `tour.step_duration_ms`.
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
browser/react/react-tour/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
error-codes.ts
|
||||
core/
|
||||
tour/
|
||||
tour.ts (useTour hook, ≤250 lines)
|
||||
tour-state.ts (state machine helpers)
|
||||
tour-effects.ts (observer wiring + events)
|
||||
tour-actions.ts (next/prev/goTo/skip/reset)
|
||||
tour.test.ts
|
||||
tour.md
|
||||
tour.types.ts
|
||||
index.ts
|
||||
utils/
|
||||
positioning/
|
||||
positioning.ts (calculateSpotlightInset, calculateTooltipPosition, getTargetRect, scrollTargetIntoView)
|
||||
positioning.test.ts
|
||||
positioning.md
|
||||
positioning.types.ts
|
||||
index.ts
|
||||
observe-target/
|
||||
observe-target.ts (ResizeObserver + IntersectionObserver + scroll)
|
||||
observe-target.test.ts
|
||||
observe-target.md
|
||||
index.ts
|
||||
persistence/
|
||||
persistence/
|
||||
persistence.ts (PersistenceAdapter interface + implementations)
|
||||
persistence.test.ts
|
||||
persistence.md
|
||||
index.ts
|
||||
schema/
|
||||
tour-schema/
|
||||
tour-schema.ts (TourConfigSchema, TourStepSchema)
|
||||
tour-schema.md
|
||||
index.ts
|
||||
stories/
|
||||
use-tour.stories.tsx (basic, spotlight, scroll, persistence, a11y)
|
||||
types/
|
||||
common.ts
|
||||
common.md
|
||||
constants/
|
||||
index.ts
|
||||
tests/
|
||||
integration.test.ts
|
||||
e2e/
|
||||
react-tour.spec.ts (targetRect tracks resize/scroll, completion persistence survives reload)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Downstream**: any Suaveplan browser app using guided tours.
|
||||
|
||||
**Breaking changes**: None — all additions are backward-compatible; `PersistenceAdapter` is opt-in.
|
||||
|
||||
**Prerequisites**: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/logger` on Verdaccio.
|
||||
136
openspec/changes/enterprise-browser-react-tour/tasks.md
Normal file
136
openspec/changes/enterprise-browser-react-tour/tasks.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Tasks: enterprise-browser-react-tour
|
||||
|
||||
**Change ID**: enterprise-browser-react-tour
|
||||
**Package**: `@suaveplan/react-tour`
|
||||
**Target path**: `browser/react/react-tour`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/logger`, `@suaveplan/schema-registry`
|
||||
|
||||
---
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [ ] Create directory tree per `proposal.md`.
|
||||
- [ ] `package.json` v0.0.0, `"sideEffects": false`.
|
||||
- `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/events workspace:^`, `@suaveplan/logger workspace:^`, `@suaveplan/schema-registry workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`, `react catalog:react`
|
||||
- `peerDependencies`: `react`
|
||||
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `happy-dom catalog:testing`, `@testing-library/react catalog:testing`, `@suaveplan/testing workspace:^`, `@suaveplan/testing-dom workspace:^`
|
||||
- Subpath exports: `"."`, `"./core/tour/tour"`, `"./utils/positioning"`, `"./persistence"`
|
||||
- [ ] `tsconfig.json`, `vitest.config.ts` (pool: forks, happy-dom environment).
|
||||
|
||||
---
|
||||
|
||||
## 2. Error codes
|
||||
|
||||
- [ ] `TOUR_CONFIG_INVALID`, `TOUR_STEP_NOT_FOUND`.
|
||||
- [ ] `registerTourErrorCodes()` — call eagerly.
|
||||
|
||||
---
|
||||
|
||||
## 3. Types (`src/types/common.ts`)
|
||||
|
||||
- [ ] `TourConfig`, `TourStep`, `TourState`, `TourActions`, `TooltipPlacement`, `TooltipPosition`, `SpotlightInset`.
|
||||
- [ ] Write `common.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. `TourSchema` (`src/schema/tour-schema/`)
|
||||
|
||||
- [ ] `tour-schema.ts` — `TourConfigSchema`, `TourStepSchema` (Zod); `registerTourSchemas()`.
|
||||
- [ ] Write `tour-schema.md`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Positioning utilities (`src/utils/positioning/`)
|
||||
|
||||
- [ ] `positioning.ts` — `calculateSpotlightInset`, `calculateTooltipPosition`, `getTargetRect`, `scrollTargetIntoView`.
|
||||
- [ ] All functions SSR-safe (`typeof document` guard).
|
||||
- [ ] Write `positioning.test.ts` (happy-dom geometry, SSR import smoke test). Write `positioning.md`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Observer utility (`src/utils/observe-target/`)
|
||||
|
||||
- [ ] `observe-target.ts` — composite `observeTarget(selector, callback)` using `ResizeObserver` + `IntersectionObserver` + scroll listener; returns cleanup.
|
||||
- [ ] Write `observe-target.test.ts` (observer fires on resize, fires on scroll). Write `observe-target.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Persistence adapters (`src/persistence/persistence/`)
|
||||
|
||||
- [ ] `persistence.ts` — `PersistenceAdapter` interface; `LocalStoragePersistence` (SSR-safe), `SessionStoragePersistence`, `MemoryPersistence`.
|
||||
- [ ] Write `persistence.test.ts` (each adapter: isCompleted, markCompleted, clear, SSR no-throw). Write `persistence.md`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tour state helpers (`src/core/tour/tour-state.ts`)
|
||||
|
||||
- [ ] `computeProgress(step, total): number`, boundary guards, `shouldSkip` evaluation.
|
||||
|
||||
---
|
||||
|
||||
## 9. Tour effects (`src/core/tour/tour-effects.ts`)
|
||||
|
||||
- [ ] Observer wiring: call `observeTarget` on step change, update `targetRect` on callback.
|
||||
- [ ] Emit `@suaveplan/events` lifecycle events (`tour:start`, `tour:step-change`, `tour:complete`, `tour:skip`, `tour:error`).
|
||||
- [ ] Wire OTel metrics.
|
||||
|
||||
---
|
||||
|
||||
## 10. Tour actions (`src/core/tour/tour-actions.ts`)
|
||||
|
||||
- [ ] `next()`, `prev()`, `goTo(n)`, `skip()`, `reset()` — with boundary checks and `shouldSkip` evaluation.
|
||||
|
||||
---
|
||||
|
||||
## 11. `useTour` hook (`src/core/tour/tour.ts`)
|
||||
|
||||
- [ ] Compose state + effects + actions.
|
||||
- [ ] Validate `TourConfig` via `TourConfigSchema` on entry; throw `TOUR_CONFIG_INVALID`.
|
||||
- [ ] Log storage errors via `@suaveplan/logger` instead of swallowing.
|
||||
- [ ] Write `tour.test.ts` (config validation, next/prev/goTo/skip, persistence, SSR no-throw). Write `tour.md`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Storybook stories (`src/stories/use-tour.stories.tsx`)
|
||||
|
||||
- [ ] Default story: basic 3-step tour.
|
||||
- [ ] Spotlight overlay: CSS spotlight with `calculateSpotlightInset`.
|
||||
- [ ] Scroll-into-view: target below the fold.
|
||||
- [ ] Completion persistence: reload and confirm tour does not restart.
|
||||
- [ ] Accessibility-compliant tooltip: `role="dialog"`, focus trap, `aria-live`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Integration test (`tests/integration.test.ts`)
|
||||
|
||||
- [ ] Full tour flow: start, navigate, complete, verify persistence adapter called.
|
||||
- [ ] `MemoryPersistence` swap: no `localStorage` touched.
|
||||
- [ ] `shouldSkip` step evaluated; step skipped automatically.
|
||||
|
||||
---
|
||||
|
||||
## 14. Playwright E2E (`e2e/react-tour.spec.ts`)
|
||||
|
||||
- [ ] `targetRect` stays aligned after element resizes (Playwright + Chromium).
|
||||
- [ ] Completion persists across reload (Playwright localStorage).
|
||||
- [ ] SSR smoke: import from Node without throwing.
|
||||
|
||||
---
|
||||
|
||||
## 15. Docs + verify
|
||||
|
||||
- [ ] Write `.md` per module (≥200 words, API reference, examples).
|
||||
- [ ] Write `README.md` with subpath table, persistence-adapter guide, SSR guide, a11y notes.
|
||||
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/react-tour` — green.
|
||||
- [ ] `bunx turbo test:coverage` — 100%.
|
||||
- [ ] No `@ts-expect-error` in `src/` or tests. No `vi.stubGlobal`.
|
||||
- [ ] Storybook stories render and pass axe-core.
|
||||
- [ ] Playwright E2E green on Chromium.
|
||||
- [ ] `bun scripts/publish.ts --package @suaveplan/react-tour`.
|
||||
- [ ] Archive to `openspec/archive/2026-04-24-enterprise-browser-react-tour/`.
|
||||
Reference in New Issue
Block a user