@real-router/solid 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/dist/cjs/index.d.ts +60 -0
- package/dist/cjs/index.js +0 -0
- package/dist/esm/index.d.mts +60 -0
- package/dist/esm/index.mjs +0 -0
- package/dist/types/RouterProvider.d.ts +2 -1
- package/dist/types/RouterProvider.d.ts.map +1 -1
- package/dist/types/dom-utils/index.d.ts +2 -0
- package/dist/types/dom-utils/index.d.ts.map +1 -1
- package/dist/types/dom-utils/scroll-restore.d.ts.map +1 -1
- package/dist/types/dom-utils/scroll-spy.d.ts +65 -0
- package/dist/types/dom-utils/scroll-spy.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/RouterProvider.tsx +16 -1
package/README.md
CHANGED
|
@@ -242,7 +242,7 @@ Navigation link with automatic active state detection. Uses `classList` for acti
|
|
|
242
242
|
<Link routeName="settings" hash="account">Account</Link>
|
|
243
243
|
```
|
|
244
244
|
|
|
245
|
-
Tri-state: `undefined` preserves the current hash, `""` clears it, a value sets it. Active class is hash-aware — only the matching tab lights up. Setting `hash` forces the slow path (the fast-path `routeSelector` is hash-agnostic). Live demo: [`examples/web/react/link-hash/`](../../examples/web/react/link-hash/) — behavior is identical across adapters, only template syntax differs. See the [Hash Fragment Support](https://github.com/greydragon888/real-router/wiki/Hash) wiki page for the full surface.
|
|
245
|
+
Tri-state: `undefined` preserves the current hash, `""` clears it, a value sets it. Active class is hash-aware — only the matching tab lights up. Setting `hash` forces the slow path (the fast-path `routeSelector` is hash-agnostic). Live demo: [`examples/web/react/hash-examples/link-hash/`](../../examples/web/react/hash-examples/link-hash/) — behavior is identical across adapters, only template syntax differs. See the [Hash Fragment Support](https://github.com/greydragon888/real-router/wiki/Hash) wiki page for the full surface.
|
|
246
246
|
|
|
247
247
|
### `<RouteView>`
|
|
248
248
|
|
|
@@ -524,7 +524,26 @@ Opt-in preservation of scroll position across navigations:
|
|
|
524
524
|
</RouterProvider>
|
|
525
525
|
```
|
|
526
526
|
|
|
527
|
-
Restores scroll on back/forward, scrolls to top (or `#hash`) on push. Three modes: `"restore"` (default), `"top"`, `"native"`. Custom containers via `scrollContainer: () => HTMLElement | null`. Options are read once on mount — changing the prop at runtime does not reconfigure the utility (Solid `onMount` is non-reactive). See [Scroll Restoration guide](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration) for
|
|
527
|
+
Restores scroll on back/forward, scrolls to top (or `#hash`) on push. Three modes: `"restore"` (default), `"top"`, `"native"`. Custom containers via `scrollContainer: () => HTMLElement | null`. Options are read once on mount — changing the prop at runtime does not reconfigure the utility (Solid `onMount` is non-reactive). Under `@real-router/browser-plugin`, replace transitions now preserve scroll position and programmatic reloads restore from `sessionStorage` (portable via `state.transition.replace` / `state.transition.reload`). See [Scroll Restoration guide](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration) for the full behaviour matrix.
|
|
528
|
+
|
|
529
|
+
## Scroll Spy
|
|
530
|
+
|
|
531
|
+
Opt-in router-coordinated `IntersectionObserver` scroll spy — the URL hash tracks the topmost visible anchor as the user scrolls, syncing `state.context.url.hash` so sibling `<Link hash>` highlights stay current:
|
|
532
|
+
|
|
533
|
+
```tsx
|
|
534
|
+
<RouterProvider
|
|
535
|
+
router={router}
|
|
536
|
+
scrollSpy={{ selector: "[id]:is(h2,h3)" }}
|
|
537
|
+
>
|
|
538
|
+
{/* Your app */}
|
|
539
|
+
</RouterProvider>
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Emits a forced same-route transition with `{ hash, replace: true, force: true, hashChange: true }` — same write API as `<Link hash>` (#532), `replace: true` so spy doesn't pollute history. Anti-flicker via `isTransitioning` + `coolingDown` gates with `selfEmitting` guard. Hardcoded internals: rAF + 150 ms debounce, MutationObserver 250 ms.
|
|
543
|
+
|
|
544
|
+
Options: `{ selector: string, rootMargin?: string, scrollContainer?: () => HTMLElement | null }`. Empty `selector` / `undefined` = off. Read once on mount (Solid `onMount` is non-reactive). SSR / browsers without `IntersectionObserver` = NOOP. Requires `browser-plugin` or `navigation-plugin` (hash-plugin / memory-plugin → warn-once + NOOP).
|
|
545
|
+
|
|
546
|
+
Behaviour is identical to the React adapter — see the [React Scroll Spy demo](../../examples/web/react/hash-examples/scroll-spy/) (12 sections, TOC sidebar, 10 e2e scenarios) and the [Scroll Spy guide](https://github.com/greydragon888/real-router/wiki/Scroll-Spy).
|
|
528
547
|
|
|
529
548
|
## View Transitions
|
|
530
549
|
|
|
@@ -542,7 +561,7 @@ No-op on unsupported browsers (Firefox as of 2026-04, SSR). Prop is read once on
|
|
|
542
561
|
|
|
543
562
|
Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki)
|
|
544
563
|
|
|
545
|
-
- [RouterProvider](https://github.com/greydragon888/real-router/wiki/RouterProvider) · [RouteView](https://github.com/greydragon888/real-router/wiki/RouteView) · [RouterErrorBoundary](https://github.com/greydragon888/real-router/wiki/RouterErrorBoundary) · [Link](https://github.com/greydragon888/real-router/wiki/Link) · [Scroll Restoration](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration) · [View Transitions](https://github.com/greydragon888/real-router/wiki/View-Transitions)
|
|
564
|
+
- [RouterProvider](https://github.com/greydragon888/real-router/wiki/RouterProvider) · [RouteView](https://github.com/greydragon888/real-router/wiki/RouteView) · [RouterErrorBoundary](https://github.com/greydragon888/real-router/wiki/RouterErrorBoundary) · [Link](https://github.com/greydragon888/real-router/wiki/Link) · [Scroll Restoration](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration) · [Scroll Spy](https://github.com/greydragon888/real-router/wiki/Scroll-Spy) · [View Transitions](https://github.com/greydragon888/real-router/wiki/View-Transitions)
|
|
546
565
|
- [useRouter](https://github.com/greydragon888/real-router/wiki/useRouter) · [useRoute](https://github.com/greydragon888/real-router/wiki/useRoute) · [useRouteNode](https://github.com/greydragon888/real-router/wiki/useRouteNode) · [useNavigator](https://github.com/greydragon888/real-router/wiki/useNavigator) · [useRouteUtils](https://github.com/greydragon888/real-router/wiki/useRouteUtils) · [useRouterTransition](https://github.com/greydragon888/real-router/wiki/useRouterTransition) · [useRouteExit](https://github.com/greydragon888/real-router/wiki/useRouteExit) · [useRouteEnter](https://github.com/greydragon888/real-router/wiki/useRouteEnter)
|
|
547
566
|
- Solid-specific store variants: [useRouteStore / useRouteNodeStore](https://github.com/greydragon888/real-router/wiki/Solid-Integration#store-based-granular-route-state) — `createStore` + `reconcile`, property-level reactivity
|
|
548
567
|
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -301,10 +301,70 @@ interface ScrollRestorationOptions {
|
|
|
301
301
|
storageKey?: string | undefined;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Router-coordinated scroll spy (#575).
|
|
306
|
+
*
|
|
307
|
+
* On `IntersectionObserver` notifications the utility picks the topmost
|
|
308
|
+
* visible anchor inside the configured scroll container and emits a forced
|
|
309
|
+
* same-route transition with `{ hash, replace: true, force: true, hashChange:
|
|
310
|
+
* true }` through `router.navigate(...)`. The URL plugin
|
|
311
|
+
* (`@real-router/browser-plugin` or `@real-router/navigation-plugin`) updates
|
|
312
|
+
* `state.context.url.hash` so sibling hash-aware `<Link hash>` re-highlights
|
|
313
|
+
* via the standard `createActiveRouteSource` pipeline.
|
|
314
|
+
*
|
|
315
|
+
* **Anti-flicker gates** (RFC §5.2):
|
|
316
|
+
* 1. `getTransitionSource(router).getSnapshot().isTransitioning` — skip emits
|
|
317
|
+
* while a transition is in-flight (re-entrant lock).
|
|
318
|
+
* 2. `coolingDown` — set on a user-driven hash transition (e.g. `<Link hash>`
|
|
319
|
+
* click + smooth `scrollIntoView`). Cleared on `scrollend` or after a
|
|
320
|
+
* 500ms safety timeout. Spy's own emits are excluded via the synchronous
|
|
321
|
+
* `selfEmitting` flag — required so the spy doesn't rate-limit itself.
|
|
322
|
+
*
|
|
323
|
+
* **Self-healing** (RFC §7.3): if the initial URL contains a hash without a
|
|
324
|
+
* matching `id` (e.g. `/page#nonexistent`), the first IO event emitted right
|
|
325
|
+
* after observe()-ing picks the topmost real anchor and corrects the URL.
|
|
326
|
+
*
|
|
327
|
+
* **Hash-only transition pipeline cost** (RFC §5.3): for same-route same-
|
|
328
|
+
* params hash-only navigations, `getTransitionPath` returns empty
|
|
329
|
+
* `toDeactivate` / `toActivate` arrays, so `runGuards` is a no-op. The only
|
|
330
|
+
* work is the URL plugin's `onTransitionSuccess` write and the
|
|
331
|
+
* `getTransitionSource` flip — cheap.
|
|
332
|
+
*
|
|
333
|
+
* **Architecture**: decomposed into 4 private subsystem closure factories
|
|
334
|
+
* (`createUrlPluginDetector`, `createCooldown`, `createDebouncer`,
|
|
335
|
+
* `createObserverPair`). The main `createScrollSpy` wires them together
|
|
336
|
+
* around the shared `silenced` / `destroyed` / `selfEmitting` flags and the
|
|
337
|
+
* `flush()` emit logic. Each subsystem owns its state + cleanup; `destroy()`
|
|
338
|
+
* delegates to each. See section banners below.
|
|
339
|
+
*
|
|
340
|
+
* @returns A `ScrollSpy` handle whose `destroy()` is idempotent.
|
|
341
|
+
*/
|
|
342
|
+
interface ScrollSpyOptions {
|
|
343
|
+
/**
|
|
344
|
+
* CSS selector for anchor candidates. Empty string `""` or `undefined`
|
|
345
|
+
* disables the spy (returns a NOOP handle). Common values:
|
|
346
|
+
* `"[id]"`, `"[id]:is(h1,h2,h3)"`, `"section[id]"`.
|
|
347
|
+
*/
|
|
348
|
+
selector: string;
|
|
349
|
+
/**
|
|
350
|
+
* `IntersectionObserver` `rootMargin`. Default
|
|
351
|
+
* `"-20% 0px -60% 0px"` — an anchor is considered "active" once it crosses
|
|
352
|
+
* into the top 20 % of the viewport (or scroll container).
|
|
353
|
+
*/
|
|
354
|
+
rootMargin?: string | undefined;
|
|
355
|
+
/**
|
|
356
|
+
* Lazy getter for the scrollable container. Resolved on every event.
|
|
357
|
+
* `null` (or missing getter) falls back to the window viewport
|
|
358
|
+
* (`root: null` on the `IntersectionObserver`).
|
|
359
|
+
*/
|
|
360
|
+
scrollContainer?: (() => HTMLElement | null) | undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
304
363
|
interface RouteProviderProps {
|
|
305
364
|
router: Router;
|
|
306
365
|
announceNavigation?: boolean;
|
|
307
366
|
scrollRestoration?: ScrollRestorationOptions;
|
|
367
|
+
scrollSpy?: ScrollSpyOptions;
|
|
308
368
|
viewTransitions?: boolean;
|
|
309
369
|
}
|
|
310
370
|
declare function RouterProvider(props: ParentProps<RouteProviderProps>): JSX.Element;
|
package/dist/cjs/index.js
CHANGED
|
Binary file
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -301,10 +301,70 @@ interface ScrollRestorationOptions {
|
|
|
301
301
|
storageKey?: string | undefined;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Router-coordinated scroll spy (#575).
|
|
306
|
+
*
|
|
307
|
+
* On `IntersectionObserver` notifications the utility picks the topmost
|
|
308
|
+
* visible anchor inside the configured scroll container and emits a forced
|
|
309
|
+
* same-route transition with `{ hash, replace: true, force: true, hashChange:
|
|
310
|
+
* true }` through `router.navigate(...)`. The URL plugin
|
|
311
|
+
* (`@real-router/browser-plugin` or `@real-router/navigation-plugin`) updates
|
|
312
|
+
* `state.context.url.hash` so sibling hash-aware `<Link hash>` re-highlights
|
|
313
|
+
* via the standard `createActiveRouteSource` pipeline.
|
|
314
|
+
*
|
|
315
|
+
* **Anti-flicker gates** (RFC §5.2):
|
|
316
|
+
* 1. `getTransitionSource(router).getSnapshot().isTransitioning` — skip emits
|
|
317
|
+
* while a transition is in-flight (re-entrant lock).
|
|
318
|
+
* 2. `coolingDown` — set on a user-driven hash transition (e.g. `<Link hash>`
|
|
319
|
+
* click + smooth `scrollIntoView`). Cleared on `scrollend` or after a
|
|
320
|
+
* 500ms safety timeout. Spy's own emits are excluded via the synchronous
|
|
321
|
+
* `selfEmitting` flag — required so the spy doesn't rate-limit itself.
|
|
322
|
+
*
|
|
323
|
+
* **Self-healing** (RFC §7.3): if the initial URL contains a hash without a
|
|
324
|
+
* matching `id` (e.g. `/page#nonexistent`), the first IO event emitted right
|
|
325
|
+
* after observe()-ing picks the topmost real anchor and corrects the URL.
|
|
326
|
+
*
|
|
327
|
+
* **Hash-only transition pipeline cost** (RFC §5.3): for same-route same-
|
|
328
|
+
* params hash-only navigations, `getTransitionPath` returns empty
|
|
329
|
+
* `toDeactivate` / `toActivate` arrays, so `runGuards` is a no-op. The only
|
|
330
|
+
* work is the URL plugin's `onTransitionSuccess` write and the
|
|
331
|
+
* `getTransitionSource` flip — cheap.
|
|
332
|
+
*
|
|
333
|
+
* **Architecture**: decomposed into 4 private subsystem closure factories
|
|
334
|
+
* (`createUrlPluginDetector`, `createCooldown`, `createDebouncer`,
|
|
335
|
+
* `createObserverPair`). The main `createScrollSpy` wires them together
|
|
336
|
+
* around the shared `silenced` / `destroyed` / `selfEmitting` flags and the
|
|
337
|
+
* `flush()` emit logic. Each subsystem owns its state + cleanup; `destroy()`
|
|
338
|
+
* delegates to each. See section banners below.
|
|
339
|
+
*
|
|
340
|
+
* @returns A `ScrollSpy` handle whose `destroy()` is idempotent.
|
|
341
|
+
*/
|
|
342
|
+
interface ScrollSpyOptions {
|
|
343
|
+
/**
|
|
344
|
+
* CSS selector for anchor candidates. Empty string `""` or `undefined`
|
|
345
|
+
* disables the spy (returns a NOOP handle). Common values:
|
|
346
|
+
* `"[id]"`, `"[id]:is(h1,h2,h3)"`, `"section[id]"`.
|
|
347
|
+
*/
|
|
348
|
+
selector: string;
|
|
349
|
+
/**
|
|
350
|
+
* `IntersectionObserver` `rootMargin`. Default
|
|
351
|
+
* `"-20% 0px -60% 0px"` — an anchor is considered "active" once it crosses
|
|
352
|
+
* into the top 20 % of the viewport (or scroll container).
|
|
353
|
+
*/
|
|
354
|
+
rootMargin?: string | undefined;
|
|
355
|
+
/**
|
|
356
|
+
* Lazy getter for the scrollable container. Resolved on every event.
|
|
357
|
+
* `null` (or missing getter) falls back to the window viewport
|
|
358
|
+
* (`root: null` on the `IntersectionObserver`).
|
|
359
|
+
*/
|
|
360
|
+
scrollContainer?: (() => HTMLElement | null) | undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
304
363
|
interface RouteProviderProps {
|
|
305
364
|
router: Router;
|
|
306
365
|
announceNavigation?: boolean;
|
|
307
366
|
scrollRestoration?: ScrollRestorationOptions;
|
|
367
|
+
scrollSpy?: ScrollSpyOptions;
|
|
308
368
|
viewTransitions?: boolean;
|
|
309
369
|
}
|
|
310
370
|
declare function RouterProvider(props: ParentProps<RouteProviderProps>): JSX.Element;
|
package/dist/esm/index.mjs
CHANGED
|
Binary file
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { ScrollRestorationOptions } from "./dom-utils";
|
|
1
|
+
import type { ScrollRestorationOptions, ScrollSpyOptions } from "./dom-utils";
|
|
2
2
|
import type { Router } from "@real-router/core";
|
|
3
3
|
import type { ParentProps, JSX } from "solid-js";
|
|
4
4
|
export interface RouteProviderProps {
|
|
5
5
|
router: Router;
|
|
6
6
|
announceNavigation?: boolean;
|
|
7
7
|
scrollRestoration?: ScrollRestorationOptions;
|
|
8
|
+
scrollSpy?: ScrollSpyOptions;
|
|
8
9
|
viewTransitions?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export declare function isRouteActive(linkRouteName: string, currentRouteName: string): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouterProvider.d.ts","sourceRoot":"","sources":["../../src/RouterProvider.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RouterProvider.d.ts","sourceRoot":"","sources":["../../src/RouterProvider.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEjD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;IAC7C,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAgB,aAAa,CAC3B,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAKT;AAwBD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,WAAW,CAAC,kBAAkB,CAAC,GACrC,GAAG,CAAC,OAAO,CAoDb"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export { createDirectionTracker } from "./direction-tracker.js";
|
|
2
2
|
export { createRouteAnnouncer } from "./route-announcer.js";
|
|
3
3
|
export { createScrollRestoration } from "./scroll-restore.js";
|
|
4
|
+
export { createScrollSpy } from "./scroll-spy.js";
|
|
4
5
|
export { createViewTransitions } from "./view-transitions.js";
|
|
5
6
|
export { shouldNavigate, buildHref, buildActiveClassName, navigateWithHash, shallowEqual, applyLinkA11y, } from "./link-utils.js";
|
|
6
7
|
export type { RouteAnnouncerOptions } from "./route-announcer.js";
|
|
7
8
|
export type { ScrollRestorationOptions, ScrollRestorationMode, } from "./scroll-restore.js";
|
|
9
|
+
export type { ScrollSpy, ScrollSpyOptions } from "./scroll-spy.js";
|
|
8
10
|
export type { DirectionTracker } from "./direction-tracker.js";
|
|
9
11
|
export type { ViewTransitions } from "./view-transitions.js";
|
|
10
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dom-utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,OAAO,EACL,cAAc,EACd,SAAS,EACT,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,YAAY,EACV,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dom-utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,OAAO,EACL,cAAc,EACd,SAAS,EACT,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,YAAY,EACV,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnE,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-restore.d.ts","sourceRoot":"","sources":["../../../src/dom-utils/scroll-restore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"scroll-restore.d.ts","sourceRoot":"","sources":["../../../src/dom-utils/scroll-restore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAkBvD,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEjE,MAAM,WAAW,wBAAwB;IACvC,IAAI,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC;IACzC,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,eAAe,CAAC,EAAE,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC;IACzD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACtC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAOD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,wBAAwB,GACjC;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CA+UzB;AA2BD,wBAAgB,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAY1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Router } from "@real-router/core";
|
|
2
|
+
/**
|
|
3
|
+
* Router-coordinated scroll spy (#575).
|
|
4
|
+
*
|
|
5
|
+
* On `IntersectionObserver` notifications the utility picks the topmost
|
|
6
|
+
* visible anchor inside the configured scroll container and emits a forced
|
|
7
|
+
* same-route transition with `{ hash, replace: true, force: true, hashChange:
|
|
8
|
+
* true }` through `router.navigate(...)`. The URL plugin
|
|
9
|
+
* (`@real-router/browser-plugin` or `@real-router/navigation-plugin`) updates
|
|
10
|
+
* `state.context.url.hash` so sibling hash-aware `<Link hash>` re-highlights
|
|
11
|
+
* via the standard `createActiveRouteSource` pipeline.
|
|
12
|
+
*
|
|
13
|
+
* **Anti-flicker gates** (RFC §5.2):
|
|
14
|
+
* 1. `getTransitionSource(router).getSnapshot().isTransitioning` — skip emits
|
|
15
|
+
* while a transition is in-flight (re-entrant lock).
|
|
16
|
+
* 2. `coolingDown` — set on a user-driven hash transition (e.g. `<Link hash>`
|
|
17
|
+
* click + smooth `scrollIntoView`). Cleared on `scrollend` or after a
|
|
18
|
+
* 500ms safety timeout. Spy's own emits are excluded via the synchronous
|
|
19
|
+
* `selfEmitting` flag — required so the spy doesn't rate-limit itself.
|
|
20
|
+
*
|
|
21
|
+
* **Self-healing** (RFC §7.3): if the initial URL contains a hash without a
|
|
22
|
+
* matching `id` (e.g. `/page#nonexistent`), the first IO event emitted right
|
|
23
|
+
* after observe()-ing picks the topmost real anchor and corrects the URL.
|
|
24
|
+
*
|
|
25
|
+
* **Hash-only transition pipeline cost** (RFC §5.3): for same-route same-
|
|
26
|
+
* params hash-only navigations, `getTransitionPath` returns empty
|
|
27
|
+
* `toDeactivate` / `toActivate` arrays, so `runGuards` is a no-op. The only
|
|
28
|
+
* work is the URL plugin's `onTransitionSuccess` write and the
|
|
29
|
+
* `getTransitionSource` flip — cheap.
|
|
30
|
+
*
|
|
31
|
+
* **Architecture**: decomposed into 4 private subsystem closure factories
|
|
32
|
+
* (`createUrlPluginDetector`, `createCooldown`, `createDebouncer`,
|
|
33
|
+
* `createObserverPair`). The main `createScrollSpy` wires them together
|
|
34
|
+
* around the shared `silenced` / `destroyed` / `selfEmitting` flags and the
|
|
35
|
+
* `flush()` emit logic. Each subsystem owns its state + cleanup; `destroy()`
|
|
36
|
+
* delegates to each. See section banners below.
|
|
37
|
+
*
|
|
38
|
+
* @returns A `ScrollSpy` handle whose `destroy()` is idempotent.
|
|
39
|
+
*/
|
|
40
|
+
export interface ScrollSpyOptions {
|
|
41
|
+
/**
|
|
42
|
+
* CSS selector for anchor candidates. Empty string `""` or `undefined`
|
|
43
|
+
* disables the spy (returns a NOOP handle). Common values:
|
|
44
|
+
* `"[id]"`, `"[id]:is(h1,h2,h3)"`, `"section[id]"`.
|
|
45
|
+
*/
|
|
46
|
+
selector: string;
|
|
47
|
+
/**
|
|
48
|
+
* `IntersectionObserver` `rootMargin`. Default
|
|
49
|
+
* `"-20% 0px -60% 0px"` — an anchor is considered "active" once it crosses
|
|
50
|
+
* into the top 20 % of the viewport (or scroll container).
|
|
51
|
+
*/
|
|
52
|
+
rootMargin?: string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Lazy getter for the scrollable container. Resolved on every event.
|
|
55
|
+
* `null` (or missing getter) falls back to the window viewport
|
|
56
|
+
* (`root: null` on the `IntersectionObserver`).
|
|
57
|
+
*/
|
|
58
|
+
scrollContainer?: (() => HTMLElement | null) | undefined;
|
|
59
|
+
}
|
|
60
|
+
export interface ScrollSpy {
|
|
61
|
+
/** Tear down observer + listeners. Idempotent. */
|
|
62
|
+
destroy: () => void;
|
|
63
|
+
}
|
|
64
|
+
export declare function createScrollSpy(router: Router, options: ScrollSpyOptions): ScrollSpy;
|
|
65
|
+
//# sourceMappingURL=scroll-spy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll-spy.d.ts","sourceRoot":"","sources":["../../../src/dom-utils/scroll-spy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAqB,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEhC;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC;CAC1D;AAED,MAAM,WAAW,SAAS;IACxB,kDAAkD;IAClD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAuaD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,gBAAgB,GACxB,SAAS,CAiMX"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/solid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "Solid.js integration for Real-Router",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"license": "MIT",
|
|
68
68
|
"sideEffects": false,
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@real-router/core": "^0.
|
|
70
|
+
"@real-router/core": "^0.54.6",
|
|
71
71
|
"@real-router/route-utils": "^0.2.2",
|
|
72
|
-
"@real-router/sources": "^0.8.
|
|
72
|
+
"@real-router/sources": "^0.8.3"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@babel/core": "7.29.0",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"rollup-plugin-dts": "6.4.1",
|
|
87
87
|
"solid-js": "1.9.12",
|
|
88
88
|
"vite-plugin-solid": "2.11.11",
|
|
89
|
-
"vitest": "4.1.
|
|
90
|
-
"@real-router/browser-plugin": "^0.17.
|
|
89
|
+
"vitest": "4.1.6",
|
|
90
|
+
"@real-router/browser-plugin": "^0.17.4"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
93
|
"solid-js": ">=1.7.0"
|
package/src/RouterProvider.tsx
CHANGED
|
@@ -7,10 +7,11 @@ import { createSignalFromSource } from "./createSignalFromSource";
|
|
|
7
7
|
import {
|
|
8
8
|
createRouteAnnouncer,
|
|
9
9
|
createScrollRestoration,
|
|
10
|
+
createScrollSpy,
|
|
10
11
|
createViewTransitions,
|
|
11
12
|
} from "./dom-utils";
|
|
12
13
|
|
|
13
|
-
import type { ScrollRestorationOptions } from "./dom-utils";
|
|
14
|
+
import type { ScrollRestorationOptions, ScrollSpyOptions } from "./dom-utils";
|
|
14
15
|
import type { Router } from "@real-router/core";
|
|
15
16
|
import type { ParentProps, JSX } from "solid-js";
|
|
16
17
|
|
|
@@ -18,6 +19,7 @@ export interface RouteProviderProps {
|
|
|
18
19
|
router: Router;
|
|
19
20
|
announceNavigation?: boolean;
|
|
20
21
|
scrollRestoration?: ScrollRestorationOptions;
|
|
22
|
+
scrollSpy?: ScrollSpyOptions;
|
|
21
23
|
viewTransitions?: boolean;
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -81,6 +83,19 @@ export function RouterProvider(
|
|
|
81
83
|
mountFeature(props.scrollRestoration, () =>
|
|
82
84
|
createScrollRestoration(props.router, props.scrollRestoration),
|
|
83
85
|
);
|
|
86
|
+
onMount(() => {
|
|
87
|
+
const spyOpts = props.scrollSpy;
|
|
88
|
+
|
|
89
|
+
if (spyOpts === undefined || spyOpts.selector === "") {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const spy = createScrollSpy(props.router, spyOpts);
|
|
94
|
+
|
|
95
|
+
onCleanup(() => {
|
|
96
|
+
spy.destroy();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
84
99
|
mountFeature(props.viewTransitions, () =>
|
|
85
100
|
createViewTransitions(props.router),
|
|
86
101
|
);
|