@moku-labs/web 2.0.1 → 2.1.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.
@@ -1,12 +1,32 @@
1
+ //#region src/plugins/router/types.d.ts
2
+ /**
3
+ * Scroll behaviour for a navigation TO a route — a closed, typed vocabulary the SPA
4
+ * kernel interprets (a first-class `.scroll()` method, not a `.meta()` key):
5
+ * - `"top"` — reset to the top of the page on the swap (the default for forward navs);
6
+ * - `"preserve"` — keep the current scroll position (e.g. opening an overlay/issue route
7
+ * over a board that must stay still). A route's `.scroll()` OVERRIDES `spa.scrollRestoration`.
8
+ */
9
+ type ScrollMode = "top" | "preserve";
10
+ //#endregion
1
11
  //#region src/plugins/spa/types.d.ts
12
+ /** Per-navigation options for the programmatic navigators (`ctx.navigate` / `app.spa.navigate`). */
13
+ interface NavigateOptions {
14
+ /** Override the scroll behaviour for this single navigation (else the route/app default). */
15
+ scroll?: ScrollMode;
16
+ }
2
17
  /**
3
18
  * What a island's `render` may return:
4
19
  * - a Preact `VNode` — committed into the host through the lazy Preact gate (`commitVNode`);
5
20
  * - a `Node` — replaces the host's children;
6
21
  * - a `string` — set as the host's `innerHTML`;
22
+ * - `null` — render NOTHING but stay mountable: the host's Preact tree is unmounted via
23
+ * `render(null, host)` (NOT `innerHTML = ""`), so a later non-empty render re-commits
24
+ * cleanly. This is the correct "empty" for a persistent render-island (e.g. a closed
25
+ * overlay panel) — returning `""` instead corrupts Preact's retained vdom and the panel
26
+ * never re-opens;
7
27
  * - `void`/`undefined` — the render mutated the DOM itself (DOM-only islands → no Preact loaded).
8
28
  */
9
- type RenderResult = AnyVNode | Node | string | void;
29
+ type RenderResult = AnyVNode | Node | string | null | void;
10
30
  /**
11
31
  * A Preact `VNode` of ANY props shape. A render returns `h(Component, props)`, i.e. a
12
32
  * `VNode<SomeProps>`; the props generic is invariant under `exactOptionalPropertyTypes`,
@@ -92,6 +112,21 @@ interface IslandContext<S = undefined> {
92
112
  readonly locale: string;
93
113
  /** Build a link to a named route by pattern substitution (same output as `app.router.toUrl`). */
94
114
  readonly url: (name: string, params?: Record<string, string>) => string;
115
+ /**
116
+ * Programmatically navigate the SPA to an internal path (same swap pipeline as a
117
+ * link click; no-op without a DOM). Build `path` from the route map's `urls`/`ctx.url`,
118
+ * never a literal. Pass `{ scroll: "preserve" }` to keep the current scroll for this nav.
119
+ *
120
+ * An ALWAYS-PRESENT member (like `set`/`flush`/`cleanup`) so every island can navigate
121
+ * without an `app` handle — no synthesized anchor click.
122
+ *
123
+ * @param path - The internal destination path (e.g. `ctx.url("board", { id })`).
124
+ * @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
125
+ * @returns void
126
+ * @example
127
+ * ctx.navigate(ctx.url("issue", { id, issueId }));
128
+ */
129
+ readonly navigate: (path: string, options?: NavigateOptions) => void;
95
130
  /** The live per-instance state (the object returned by `spec.state`). `undefined` for legacy hooks-only islands. */
96
131
  readonly state: S;
97
132
  /**
package/dist/testing.mjs CHANGED
@@ -62,13 +62,15 @@ let commitVNodeFunction;
62
62
  * await loadRenderChunk();
63
63
  */
64
64
  async function loadRenderChunk() {
65
- renderChunk ??= import("./render-yXHc9BWI.mjs");
65
+ renderChunk ??= import("./render-CkdqOmsm.mjs");
66
66
  commitVNodeFunction = (await renderChunk).commitVNode;
67
67
  }
68
68
  /**
69
69
  * Commit a {@link RenderResult} into a host: `string` → `innerHTML`, `Node` →
70
- * `replaceChildren`, `void`/`undefined` → no-op (the render mutated the DOM itself), and
71
- * a Preact `VNode` → committed through the lazy gate (loading it on demand if needed).
70
+ * `replaceChildren`, `null` → Preact unmount via the lazy gate (`render(null, host)` —
71
+ * stays re-mountable, the correct "empty" for a persistent render-island), `void`/`undefined`
72
+ * → no-op (the render mutated the DOM itself), and a Preact `VNode` → committed through the
73
+ * lazy gate (loading it on demand if needed).
72
74
  *
73
75
  * @param host - The island host element to render into.
74
76
  * @param result - The value returned by the island's `render`.
@@ -195,6 +197,18 @@ function buildContext(state, instance) {
195
197
  return instance.route.url;
196
198
  },
197
199
  /**
200
+ * Programmatically navigate the SPA (delegates to the kernel's navigator through
201
+ * `state.navigate` — the same decoupled seam as `island`). No-op before the kernel binds.
202
+ *
203
+ * @param path - The internal destination path.
204
+ * @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
205
+ * @example
206
+ * ctx.navigate(ctx.url("issue", { id, issueId }));
207
+ */
208
+ navigate(path, options) {
209
+ state.navigate?.(path, options);
210
+ },
211
+ /**
198
212
  * The live per-instance state (`undefined` for legacy hooks-only islands).
199
213
  *
200
214
  * @returns The current state.
@@ -552,6 +566,7 @@ function createState(_ctx) {
552
566
  currentUrl: "",
553
567
  destroyRouter: null,
554
568
  started: false,
569
+ navigate: null,
555
570
  kernel: null
556
571
  };
557
572
  }
package/package.json CHANGED
@@ -132,5 +132,5 @@
132
132
  "test:build-e2e": "bun test src/plugins/build/__tests__/e2e/",
133
133
  "test:coverage": "vitest run --project unit --project integration --coverage"
134
134
  },
135
- "version": "2.0.1"
135
+ "version": "2.1.0"
136
136
  }