import * as React from 'react'; /** Where "this person has seen the intro" is remembered between visits. */ const TOUR_KEY = 'cbar-showcase:tour'; /** * What counts as having seen it. * * A version rather than a boolean, so that a genuinely reworked tour can be * shown once more to people who already dismissed the old one — bump this and * every stored `'1'` stops matching. It is deliberately not the package * version: most releases change nothing about the intro, and re-opening a modal * at everybody on every patch is how a welcome screen becomes an annoyance. */ const TOUR_VERSION = '1'; export interface TourControls { open: boolean; /** * Has it been open at least once in this session? * * The dialog is a lazy chunk — it pulls six demos, the token table and the * lockup — and a returning visitor never opens it, so it should not be in * their download at all. But unmounting the moment `open` goes false would * also cut the closing animation off, so the mount outlives the close. */ mounted: boolean; /** Reopen it from the header, at the first slide. */ start: () => void; close: () => void; } /** * Should the intro be on screen, and how to change that. * * The storage half is `useRailVisible` in `sidebar.tsx` almost line for line — * read in the initialiser, write in an effect, every access wrapped — and that * file carries the reasoning. Three things are specific to this one: * * 1. **No storage means no auto-open.** `useRailVisible` falls back to `true` * because a visible rail is the better default; here the equivalent * fallback would open a modal on every single load in a Safari private * window, which is worse than never showing it at all. * * 2. **The mark is written when it opens, not when it closes.** Someone who * reloads halfway through the slides has seen it, and should not be handed * it again — that is the literal ask. Writing on close would also lose the * mark for anyone who navigates away instead of dismissing. * * 3. **`enabled`** exists for `/embed/*`. That route renders no shell, so the * tour would not be visible there; without the flag, opening an embed link * would still run the effect and silently spend the one automatic showing * on a dialog nobody saw. */ export function useTour(enabled: boolean): TourControls { /* * One state, not two. * * `mounted` is "has `open` ever been true", which looks like a job for an * effect watching `open` — and that is exactly the cascading render * `react-hooks/set-state-in-effect` exists to stop. It never needed one: * `open` only ever changes through the two callbacks below, so the latch can * simply be part of the same transition. */ const [state, setState] = React.useState(() => { const open = enabled && unseen(); return { open, mounted: open }; }); /* An effect, and legitimately so — this one writes to an external system rather than back into React. In an effect rather than inside the updater for the reason `sidebar.tsx` gives: React runs an updater twice under StrictMode, and a write is not the sort of thing to do twice. */ React.useEffect(() => { if (!state.open) return; try { window.localStorage.setItem(TOUR_KEY, TOUR_VERSION); } catch { /* No storage — the tour still works, it just returns on the next load. */ } }, [state.open]); const start = React.useCallback(() => setState({ open: true, mounted: true }), []); const close = React.useCallback( () => setState((current) => ({ ...current, open: false })), [] ); return { open: state.open, mounted: state.mounted, start, close }; } /** Has this browser not been shown the current tour yet? */ function unseen(): boolean { try { return window.localStorage.getItem(TOUR_KEY) !== TOUR_VERSION; } catch { return false; } }