evolit 0.1.0-alpha.22 → 0.1.0-alpha.24

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 CHANGED
@@ -105,8 +105,10 @@ export default function CollectionControls() {
105
105
 
106
106
  The hook returns `{ status, url, pendingUrl, error, push, replace, refresh, createHref }`:
107
107
 
108
- - `push(target)` adds a browser-history entry.
109
- - `replace(target)` updates the current entry, useful for visual-only query state.
108
+ - `push(target, { scroll: false })` adds a browser-history entry. Pass
109
+ `scroll: false` to keep the current viewport position.
110
+ - `replace(target, { scroll: false })` updates the current entry, useful for
111
+ visual-only query state. It accepts the same scroll option.
110
112
  - `refresh()` bypasses the client delta cache for the current URL.
111
113
  - `createHref(pathname, searchParams)` creates a relative internal URL. It accepts standard
112
114
  `URLSearchParams`, preserving repeated keys such as `facet=brand&facet=material`.
@@ -115,6 +117,20 @@ The hook returns `{ status, url, pendingUrl, error, push, replace, refresh, crea
115
117
  share with server-evaluated route code. `useNavigation()` itself is browser-only and must only run
116
118
  from a connected client component.
117
119
 
120
+ Client components can also read the active route state with browser-only hooks:
121
+
122
+ ```jsx
123
+ import { useParams, useSearchParams } from "evolit/navigation";
124
+
125
+ const { slug = [] } = useParams();
126
+ const searchParams = useSearchParams();
127
+ const selectedFacets = searchParams.getAll("facet");
128
+ ```
129
+
130
+ Both hooks update after client navigation. `useParams()` returns a read-only snapshot; the
131
+ `URLSearchParams` from `useSearchParams()` is a local snapshot, so build a new href and navigate to
132
+ it to update the URL.
133
+
118
134
  ### Progressive links and forms
119
135
 
120
136
  Links work without JavaScript. With JavaScript, Evolit intercepts ordinary same-origin left-clicks.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolit",
3
- "version": "0.1.0-alpha.22",
3
+ "version": "0.1.0-alpha.24",
4
4
  "description": "A convention-driven application framework for LitSX and web components.",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.10.3",
@@ -18,6 +18,20 @@ function readRoute(documentRef) {
18
18
  try { return source ? JSON.parse(source) : null; } catch { return null; }
19
19
  }
20
20
 
21
+ function readRouteParams(documentRef) {
22
+ const params = readRoute(documentRef)?.params;
23
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
24
+ return Object.freeze({});
25
+ }
26
+
27
+ return Object.freeze(Object.fromEntries(
28
+ Object.entries(params).map(([key, value]) => [
29
+ key,
30
+ Array.isArray(value) ? Object.freeze([...value]) : value,
31
+ ]),
32
+ ));
33
+ }
34
+
21
35
  function findMarkers(root, segmentId, matches = []) {
22
36
  const documentRef = root.ownerDocument ?? root;
23
37
  const walker = documentRef.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
@@ -309,14 +323,16 @@ function findHashTarget(documentRef, href) {
309
323
  }
310
324
  }
311
325
 
312
- function restoreScrollAndFocus(windowRef, href, position) {
313
- const target = findHashTarget(windowRef.document, href);
314
- if (target?.scrollIntoView) {
315
- target.scrollIntoView();
316
- } else if (position) {
317
- windowRef.scrollTo?.(position.x, position.y);
318
- } else {
319
- windowRef.scrollTo?.(0, 0);
326
+ function restoreScrollAndFocus(windowRef, href, position, preserveScroll = false) {
327
+ if (!preserveScroll) {
328
+ const target = findHashTarget(windowRef.document, href);
329
+ if (target?.scrollIntoView) {
330
+ target.scrollIntoView();
331
+ } else if (position) {
332
+ windowRef.scrollTo?.(position.x, position.y);
333
+ } else {
334
+ windowRef.scrollTo?.(0, 0);
335
+ }
320
336
  }
321
337
 
322
338
  const main = windowRef.document.querySelector?.("main");
@@ -410,6 +426,9 @@ export function createBrowserNavigation(options = {}) {
410
426
  async function navigate(target, mode = "push", fromPopState = false, options = {}) {
411
427
  const href = toHref(target, windowRef.location);
412
428
  const cacheKey = toCacheKey(href);
429
+ const scrollPosition = options.scroll === false
430
+ ? currentScrollPosition(windowRef)
431
+ : options.scrollPosition;
413
432
  controller?.abort();
414
433
  const navigationController = new AbortController();
415
434
  controller = navigationController;
@@ -475,14 +494,19 @@ export function createBrowserNavigation(options = {}) {
475
494
  {
476
495
  ...(mode === "replace" ? windowRef.history.state : {}),
477
496
  __evolitNavigationEntry: entryId,
478
- __evolitScroll: { x: 0, y: 0 },
497
+ __evolitScroll: scrollPosition ?? { x: 0, y: 0 },
479
498
  },
480
499
  "",
481
500
  canonicalHref,
482
501
  );
483
502
  }
484
503
  cacheDelta(entryId, toCacheKey(canonicalHref), delta);
485
- restoreScrollAndFocus(windowRef, canonicalHref, options.scrollPosition);
504
+ restoreScrollAndFocus(
505
+ windowRef,
506
+ canonicalHref,
507
+ scrollPosition,
508
+ options.scroll === false,
509
+ );
486
510
  state = { status: "idle", url: canonicalHref, pendingUrl: null, error: null };
487
511
  emit();
488
512
  return delta;
@@ -519,9 +543,9 @@ export function createBrowserNavigation(options = {}) {
519
543
  return {
520
544
  getState: () => state,
521
545
  subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
522
- push: (target) => navigate(target, "push"),
523
- replace: (target) => navigate(target, "replace"),
524
- refresh: () => navigate(windowRef.location.href, "replace", false, { force: true }),
546
+ push: (target, options) => navigate(target, "push", false, options),
547
+ replace: (target, options) => navigate(target, "replace", false, options),
548
+ refresh: (options) => navigate(windowRef.location.href, "replace", false, { ...options, force: true }),
525
549
  createHref,
526
550
  };
527
551
  }
@@ -544,3 +568,22 @@ export function useNavigation() {
544
568
  createHref: navigation.createHref,
545
569
  };
546
570
  }
571
+
572
+ /**
573
+ * Returns the dynamic route parameters for the active client route.
574
+ * The returned object is a read-only snapshot and updates after navigation.
575
+ */
576
+ export function useParams() {
577
+ useNavigation();
578
+ return readRouteParams(globalThis.document);
579
+ }
580
+
581
+ /**
582
+ * Returns a URLSearchParams snapshot for the active client route.
583
+ * Mutating it is local only; use createHref() and navigation.push()/replace()
584
+ * to update the URL.
585
+ */
586
+ export function useSearchParams() {
587
+ const navigation = useNavigation();
588
+ return new URL(navigation.url).searchParams;
589
+ }
@@ -1,3 +1,5 @@
1
1
  export function getNavigation() { throw new Error("Navigation is only available in a browser context."); }
2
2
  export { createHref } from "./navigation-url.js";
3
3
  export function useNavigation() { throw new Error("useNavigation() is only available in a browser component."); }
4
+ export function useParams() { throw new Error("useParams() is only available in a browser component."); }
5
+ export function useSearchParams() { throw new Error("useSearchParams() is only available in a browser component."); }
@@ -73,6 +73,9 @@ export function createRouteSegmentPayload(routeResult) {
73
73
  version: 1,
74
74
  url: routeResult.cacheKey ?? routeResult.route?.pathname ?? null,
75
75
  pathname: routeResult.cacheKey?.split("?")[0] ?? routeResult.route?.pathname ?? null,
76
+ params: routeResult.params && typeof routeResult.params === "object"
77
+ ? routeResult.params
78
+ : {},
76
79
  cachePolicy: routeResult.cachePolicy
77
80
  ? {
78
81
  mode: routeResult.cachePolicy.mode,