Files
homelable/frontend/src/stores/walkthroughStore.ts
T
Pouzor 8c56921375 feat(walkthrough): add Getting Started tour with first-run invite
Bespoke spotlight walkthrough (no new deps) that guides new users through the
main features. A version-stamped invite bubble offers the tour to new users and
re-offers it once to existing users after an upgrade; restartable from Settings.

- Multi-hole SVG-mask overlay keeps a ringed control and any open modal lit
- Steps: scan, scan history, inventory, nodes, edit, text/zone, grouping,
  style, imports, and a closing recap with a GitHub link
- Scan/history/inventory/import steps inject demo data (no backend calls) and
  are filtered out in standalone, which ends on a full-mode recap
- Persisted invite state via localStorage (utils/walkthrough.ts), live tour
  state via zustand (stores/walkthroughStore.ts)

ha-relevant: maybe
2026-07-19 00:40:08 +02:00

49 lines
1.4 KiB
TypeScript

import { create } from 'zustand'
import { markWalkthroughSeen } from '@/utils/walkthrough'
/**
* Runtime state of an active Getting Started tour. Persistence of the "already
* seen" flag lives in utils/walkthrough.ts; this store only tracks the live run.
*
* `total` is set by the overlay from the mode-filtered step list, so next()/goTo
* can clamp and finish() fires when the user advances past the last step.
*/
interface WalkthroughStore {
isActive: boolean
stepIndex: number
total: number
setTotal: (n: number) => void
start: () => void
next: () => void
prev: () => void
goTo: (n: number) => void
skip: () => void
finish: () => void
}
export const useWalkthroughStore = create<WalkthroughStore>((set, get) => ({
isActive: false,
stepIndex: 0,
total: 0,
setTotal: (n) => set({ total: n }),
start: () => set({ isActive: true, stepIndex: 0 }),
next: () => {
const { stepIndex, total } = get()
if (stepIndex >= total - 1) {
get().finish()
return
}
set({ stepIndex: stepIndex + 1 })
},
prev: () => set((s) => ({ stepIndex: Math.max(0, s.stepIndex - 1) })),
goTo: (n) => set((s) => ({ stepIndex: Math.max(0, Math.min(n, Math.max(0, s.total - 1))) })),
skip: () => {
markWalkthroughSeen('skipped')
set({ isActive: false })
},
finish: () => {
markWalkthroughSeen('completed')
set({ isActive: false })
},
}))