Add URL hash view-state routing with Back/Forward and restore (M6, §01E)

The hash becomes the single serialized view (snippet / Datasets list / a dataset
/ new-dataset form / Chart Builder). url-hash is the sole reader/writer of
location+history (total, never-throwing parse; hash-only canonical URLs).
UrlStateSync grows from the modal↔URL seam into the full router: state→hash is
derived from the stores (so in-modal dataset sub-views reach the hash), hash→state
restores on load and on Back/Forward, validating ids and cleaning stale links.
startRouting() runs after store hydrate.
This commit is contained in:
2026-06-07 17:18:10 +03:00
parent 8c0a6b9239
commit f365258fcc
6 changed files with 597 additions and 16 deletions
+23 -1
View File
@@ -247,10 +247,32 @@ export const navigate = {
**Don't**
- Don't read or write `location.hash` from components — call `navigate.*`.
- Don't read or write `location.hash` from components.
- Don't `pushState` on load-restore (pollutes Back history).
- Don't throw on an unrecognized hash; degrade to the default view.
### Shipped (M6) — how the implementation refines this sketch
The routing mediator lives in **`modals/UrlStateSync.ts`**, not a new
`orchestration/UrlStateSync.ts`: that file was already the coordinator's modal↔URL
seam (`syncModalToUrl` / `clearModalFromUrl`), so M6 grew it into the whole router
rather than splitting routing across two modules. It must **not** import the
ModalCoordinator (cycle); restore drives `useAppStore.setActiveModal` directly, as
this sketch's `applyView` already does.
**state → hash is derived from the stores, not pushed by `navigate.*` calls.** A
`deriveViewState()` reads `activeModal` + `DatasetStore.view`/`selectedId` +
`ChartBuilderStore.datasetId` + `activeSnippetId`; a subscription on each of those
stores calls `pushView` when the derived view differs from the URL. Components never
call a navigate helper — they just mutate stores (select a snippet, open a dataset),
and the subscriber reflects it. This is **required**, not stylistic: the in-modal
dataset sub-views (`#datasets/new`, `#datasets/dataset-<id>`) are `DatasetStore` view
changes, not modal-open events, so only a derive-from-state writer captures them. Only
modals flagged `isUrlNavigable` in the registry own the hash; a non-navigable modal
(extract / about / donate) leaves the underlying snippet view in the URL. On load,
after restore, the active view is reflected with **`replaceView`** (not `pushView`) so
there's no dead Back step. `startRouting()` runs in `startup.ts` after store hydrate.
---
## 2. Global Event / Keyboard Routing