diff --git a/TODO.md b/TODO.md index 796b57e..cc29883 100644 --- a/TODO.md +++ b/TODO.md @@ -262,6 +262,10 @@ note only says where to start. `myevent:`/`mytask:`, or with no resolvable game) stay out of v2 account data, as nothing can show them; they survive in `legacyRaw`, `v2:migration-recovery` and the v1 keys. After a rollback, anything done in the new build is in v2 only: stranded for the old app, not lost. +- [ ] Find why the tour's look switches snapped back and the tour restarted after Finish (owner, + 2026-10-18). Not reproduced; paused by the owner. Guards are in, and the investigation so far is in + the [handoff](docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md) → "Owner feedback on + the forced tour". - [ ] Decide where an imported v1 file's marks go when its region differs from the reader's account. Today they land in a new `account:import-N`, kept but out of sight (`state/reader/import.ts`, `export.ts`). Options: merge into the visible account for that game, or show the imported account diff --git a/docs/DESIGN.md b/docs/DESIGN.md index af576b6..1f8a83a 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -634,13 +634,21 @@ legend on the same baseline as "Next: …". - **The tour (2026-10-18, owner).** Every reader takes it once before using the app: new readers straight after setup, returning readers on their first visit, and readers who saw the earlier tour once more. Routines → Checklist → Pick a layout → Timeline → Redeem codes → Archive → Settings. - - It floats over the page it describes. While forced, the page behind is `inert` under a light - scrim. It can be scrolled and seen, but not clicked. There is no Skip, and a reload restarts at - step 1. + - It floats over the page it describes. While forced, the page behind is `inert`. It can be + scrolled and seen, but not clicked. There is no Skip, and a reload restarts at step 1. + - Each step rings what it is about (owner, 2026-10-18: greying the page "makes sense, but it should + highlight the option"): the nav tab for Routines, Checklist, Archive and Settings, the Checklist + or the Timeline board for the two comparing steps, the codes list for codes. While forced, the + rest of the page is dimmed around it. The ring is a fixed layer drawn over the target's box + (`TourSpotlight`), not the target raised above a scrim, so no stacking context can trap it. - "Pick a layout" (on the Checklist) and the Timeline step each hold a two-way switch, Relaxed / Structured and New / Classic. The switch changes the page behind in place, and the selected one is kept. Readers are shown the difference rather than told about it, because most never open - Appearance. + Appearance. Continue waits until both options have been on screen (owner: it "should *require* + you to have tested both"); the look showing when the step opens counts as one. These two steps + move the card to the right edge on wide screens (≥1100px) and scroll their subject to the top of + the window, past the code and release banners, so the page has room to show the difference. On a + phone the card stays at the foot, shortened, with the options side by side. - Returning readers are moved to Relaxed as the tour opens. New readers keep the look they chose in setup. - Continue stays in a fixed place and Back sits beside it. Only a replay ("Replay the diff --git a/docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md b/docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md index 9f53621..34f1397 100644 --- a/docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md +++ b/docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md @@ -950,3 +950,21 @@ Schema review: no data-loss or stuck-state path. Accepted, by the owner's rule: had picked Structured on purpose is moved to Relaxed once, and the previous value isn't kept. The tour's layout step puts it back in one click. Left as is (low): a stale second tab can re-force the tour once, and a save-error banner shown during a forced tour can be read but not clicked. + +### Owner feedback on the forced tour (2026-10-18) + +- **Highlight, try both, room for the Timeline.** Done; see `docs/DESIGN.md` → "The tour". Each + step rings its subject, the comparing steps hold Continue until both options have been on screen, + and they dock the card to the side and scroll their subject to the top. +- **Not reproduced, paused by the owner's call:** the owner saw the layout and Timeline switches snap + back ("you have to click like 5 times"), and the tour start again right after Finish tour, without + end. It did not reproduce in Chrome with one, two or three tabs, locally or on the live site, from + a fresh setup or a returning reader (a save logger showed each click written once, and nothing + writing it back). The only code that writes `interface` and `redesignTourSeen` on its own is the + "unseen" effect in `ReaderApp.tsx` (Relaxed + `"returning"`), and it would cause both symptoms if + it fired in some tab after the tour was finished; it now fires at most once per page load and + never after that tab saw the tour done. Setup now saves only the prefs it changed, not the whole + object, so a setup finished in a stale second tab leaves every other setting alone (the ones setup + sets, `redesignTourSeen` among them, it still writes: at worst that tab shows the tour once more). If it recurs: + log every `gacha-tracker:v2:reader` write with a stack (wrap `Storage.prototype.setItem`) in the + owner's own browser, with their data, and note which tabs are open and which build each runs. diff --git a/src/client/ReaderApp.tsx b/src/client/ReaderApp.tsx index d07b414..6ca1e97 100644 --- a/src/client/ReaderApp.tsx +++ b/src/client/ReaderApp.tsx @@ -270,8 +270,14 @@ function ReaderContent({ const tour = prefs.onboarded ? tourMode(state.prefs.redesignTourSeen) : "done"; + // At most once per page load, and never after this tab saw the tour + // finished: if another tab or a stale write ever put an old marker back, + // the reader is not moved to Relaxed and sent round the tour again. + const tourSettled = useRef(false); + if (tour === "done" && prefs.onboarded) tourSettled.current = true; useEffect(() => { - if (tour !== "unseen") return; + if (tour !== "unseen" || tourSettled.current) return; + tourSettled.current = true; void dispatch({ type: "prefs", patch: { interface: "relaxed", redesignTourSeen: "returning" }, @@ -752,9 +758,16 @@ function ReaderContent({ } // No navigation here: the tour opens next and leads the way, // ending on the chosen view. + // Only what setup changed, so a setup finished in a second, + // stale tab leaves alone every setting setup does not set. await dispatch({ type: "prefs", - patch: next.prefs, + patch: Object.fromEntries( + Object.entries(next.prefs).filter( + ([k, v]) => + JSON.stringify(v) !== JSON.stringify(state.prefs[k]), + ), + ), ...actionMeta(), }); }} @@ -1123,7 +1136,6 @@ function ReaderContent({ {touring && ( <> - {tourForced &&