@funstack/router 0.0.6 → 0.0.7-alpha.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.
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,13 @@
1
+ #! /usr/bin/env node
2
+ import { install } from "@funstack/skill-installer";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, resolve } from "node:path";
5
+
6
+ //#region src/bin/skill-installer.ts
7
+ const skillDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../skills/funstack-router-knowledge");
8
+ console.log("Installing skill from:", skillDir);
9
+ await install(skillDir);
10
+
11
+ //#endregion
12
+ export { };
13
+ //# sourceMappingURL=skill-installer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-installer.mjs","names":[],"sources":["../../src/bin/skill-installer.ts"],"sourcesContent":["#! /usr/bin/env node\n\nimport { install } from \"@funstack/skill-installer\";\nimport { fileURLToPath } from \"node:url\";\nimport { resolve, dirname } from \"node:path\";\n\n// Resolve the skill directory relative to this script's location.\n// This script is at dist/bin/skill-installer.mjs, so go up two levels\n// to reach the package root, then into skills/.\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst skillDir = resolve(__dirname, \"../../skills/funstack-router-knowledge\");\n\nconsole.log(\"Installing skill from:\", skillDir);\n\nawait install(skillDir);\n"],"mappings":";;;;;;AAUA,MAAM,WAAW,QADC,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EACrB,yCAAyC;AAE7E,QAAQ,IAAI,0BAA0B,SAAS;AAE/C,MAAM,QAAQ,SAAS"}
@@ -0,0 +1,85 @@
1
+ import { CodeBlock } from "../components/CodeBlock.js";
2
+
3
+ export function ApiComponentsPage() {
4
+ return (
5
+ <div className="page docs-page api-page">
6
+ <h1>Components</h1>
7
+ <p className="page-intro">
8
+ Core components for building routing in your React application.
9
+ </p>
10
+
11
+ <article className="api-item">
12
+ <h3>
13
+ <code>{"<Router>"}</code>
14
+ </h3>
15
+ <p>The main router component that provides routing context.</p>
16
+ <CodeBlock language="tsx">{`import { Router } from "@funstack/router";
17
+
18
+ <Router
19
+ routes={routes}
20
+ onNavigate={(event, info) => {
21
+ console.log("Navigating to:", event.destination.url);
22
+ console.log("Matched routes:", info.matches);
23
+ console.log("Will intercept:", info.intercepting);
24
+ }}
25
+ />`}</CodeBlock>
26
+ <h4>Props</h4>
27
+ <table className="props-table">
28
+ <thead>
29
+ <tr>
30
+ <th>Prop</th>
31
+ <th>Type</th>
32
+ <th>Description</th>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <tr>
37
+ <td>
38
+ <code>routes</code>
39
+ </td>
40
+ <td>
41
+ <code>RouteDefinition[]</code>
42
+ </td>
43
+ <td>Array of route definitions</td>
44
+ </tr>
45
+ <tr>
46
+ <td>
47
+ <code>onNavigate</code>
48
+ </td>
49
+ <td>
50
+ <code>OnNavigateCallback</code>
51
+ </td>
52
+ <td>
53
+ Callback fired before navigation is intercepted. Receives the
54
+ NavigateEvent and an info object with matched routes and whether
55
+ the router will intercept the navigation.
56
+ </td>
57
+ </tr>
58
+ </tbody>
59
+ </table>
60
+ </article>
61
+
62
+ <article className="api-item">
63
+ <h3>
64
+ <code>{"<Outlet>"}</code>
65
+ </h3>
66
+ <p>
67
+ Renders the child route's component. Used in parent routes for nested
68
+ layouts.
69
+ </p>
70
+ <CodeBlock language="tsx">{`import { Outlet } from "@funstack/router";
71
+
72
+ function Layout() {
73
+ return (
74
+ <div>
75
+ <header>My App</header>
76
+ <main>
77
+ <Outlet />
78
+ </main>
79
+ </div>
80
+ );
81
+ }`}</CodeBlock>
82
+ </article>
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,323 @@
1
+ import { CodeBlock } from "../components/CodeBlock.js";
2
+
3
+ export function ApiHooksPage() {
4
+ return (
5
+ <div className="page docs-page api-page">
6
+ <h1>Hooks</h1>
7
+ <p className="page-intro">
8
+ React hooks for accessing router state and navigation.
9
+ </p>
10
+
11
+ <article className="api-item">
12
+ <h3>
13
+ <code>useNavigate()</code>
14
+ </h3>
15
+ <p>Returns a function to programmatically navigate.</p>
16
+ <CodeBlock language="tsx">{`import { useNavigate } from "@funstack/router";
17
+
18
+ function MyComponent() {
19
+ const navigate = useNavigate();
20
+
21
+ // Navigate to a path
22
+ navigate("/about");
23
+
24
+ // Navigate with options
25
+ navigate("/users/123", {
26
+ replace: true, // Replace current history entry
27
+ state: { from: "home" }, // Persistent state (survives back/forward)
28
+ info: { referrer: "home" }, // Ephemeral info (only for this navigation)
29
+ });
30
+ }`}</CodeBlock>
31
+ </article>
32
+
33
+ <article className="api-item">
34
+ <h3>
35
+ <code>useLocation()</code>
36
+ </h3>
37
+ <p>Returns the current location object.</p>
38
+ <CodeBlock language="tsx">{`import { useLocation } from "@funstack/router";
39
+
40
+ function MyComponent() {
41
+ const location = useLocation();
42
+
43
+ console.log(location.pathname); // "/users/123"
44
+ console.log(location.search); // "?tab=profile"
45
+ console.log(location.hash); // "#section"
46
+ }`}</CodeBlock>
47
+ </article>
48
+
49
+ <article className="api-item">
50
+ <h3>
51
+ <code>useSearchParams()</code>
52
+ </h3>
53
+ <p>
54
+ Returns a tuple of the current search params and a setter function.
55
+ </p>
56
+ <CodeBlock language="tsx">{`import { useSearchParams } from "@funstack/router";
57
+
58
+ function SearchPage() {
59
+ const [searchParams, setSearchParams] = useSearchParams();
60
+
61
+ const query = searchParams.get("q");
62
+
63
+ const handleSearch = (newQuery: string) => {
64
+ setSearchParams({ q: newQuery });
65
+ };
66
+ }`}</CodeBlock>
67
+ </article>
68
+
69
+ <article className="api-item">
70
+ <h3>
71
+ <code>useBlocker(options)</code>
72
+ </h3>
73
+ <p>
74
+ Prevents navigation away from the current route. Useful for scenarios
75
+ like unsaved form data, ongoing file uploads, or any state that would
76
+ be lost on navigation.
77
+ </p>
78
+ <CodeBlock language="tsx">{`import { useBlocker } from "@funstack/router";
79
+ import { useState, useCallback } from "react";
80
+
81
+ function EditForm() {
82
+ const [isDirty, setIsDirty] = useState(false);
83
+
84
+ useBlocker({
85
+ shouldBlock: useCallback(() => {
86
+ if (isDirty) {
87
+ return !confirm("You have unsaved changes. Leave anyway?");
88
+ }
89
+ return false;
90
+ }, [isDirty]),
91
+ });
92
+
93
+ const handleSave = () => {
94
+ // Save logic...
95
+ setIsDirty(false);
96
+ };
97
+
98
+ return (
99
+ <form>
100
+ <input onChange={() => setIsDirty(true)} />
101
+ <button type="button" onClick={handleSave}>
102
+ Save
103
+ </button>
104
+ </form>
105
+ );
106
+ }`}</CodeBlock>
107
+ <h4>Options</h4>
108
+ <ul>
109
+ <li>
110
+ <code>shouldBlock</code>: A function that returns <code>true</code>{" "}
111
+ to block navigation, or <code>false</code> to allow it. You can call{" "}
112
+ <code>confirm()</code> inside this function to show a confirmation
113
+ dialog. Wrap with <code>useCallback</code> when the function depends
114
+ on state.
115
+ </li>
116
+ </ul>
117
+ <h4>Notes</h4>
118
+ <ul>
119
+ <li>
120
+ Multiple blockers can coexist in the component tree. If any blocker
121
+ returns <code>true</code>, navigation is blocked.
122
+ </li>
123
+ <li>
124
+ This hook only handles SPA navigations (links, programmatic
125
+ navigation). For hard navigations (tab close, refresh), handle{" "}
126
+ <code>beforeunload</code> separately.
127
+ </li>
128
+ </ul>
129
+ </article>
130
+
131
+ <article className="api-item">
132
+ <h3>
133
+ <code>useIsPending()</code>
134
+ </h3>
135
+ <p>
136
+ Returns whether a navigation transition is currently pending. When
137
+ navigating to a route that suspends (e.g., using{" "}
138
+ <code>React.lazy</code>), <code>isPending</code> becomes{" "}
139
+ <code>true</code> while React keeps the previous UI visible, and
140
+ returns to <code>false</code> once the new route is ready.
141
+ </p>
142
+ <CodeBlock language="tsx">{`import { useIsPending } from "@funstack/router";
143
+
144
+ function MyComponent() {
145
+ const isPending = useIsPending();
146
+
147
+ return (
148
+ <div style={{ opacity: isPending ? 0.7 : 1 }}>
149
+ {isPending && <span>Navigating...</span>}
150
+ {/* page content */}
151
+ </div>
152
+ );
153
+ }`}</CodeBlock>
154
+ <h4>Return Value</h4>
155
+ <ul>
156
+ <li>
157
+ <code>boolean</code> — <code>true</code> when a navigation
158
+ transition is in progress (the destination route is suspending),{" "}
159
+ <code>false</code> otherwise.
160
+ </li>
161
+ </ul>
162
+ <h4>Notes</h4>
163
+ <ul>
164
+ <li>
165
+ This hook is powered by React's <code>useTransition</code>. The
166
+ router wraps navigation state updates in{" "}
167
+ <code>startTransition</code>, so React defers rendering suspended
168
+ routes and keeps the current UI visible.
169
+ </li>
170
+ <li>
171
+ The same <code>isPending</code> value is also available as a prop on
172
+ route components.
173
+ </li>
174
+ </ul>
175
+ </article>
176
+
177
+ <h2>Type-Safe Hooks</h2>
178
+ <p>
179
+ These hooks provide type-safe access to route data when using routes
180
+ defined with an <code>id</code>. They extract type information from{" "}
181
+ <code>TypefulOpaqueRouteDefinition</code> and validate at runtime that
182
+ the specified route exists in the current route hierarchy.
183
+ </p>
184
+ <p>
185
+ In nested routes, these hooks can access data from any ancestor route in
186
+ the hierarchy. For example, a child route component can use{" "}
187
+ <code>useRouteParams(parentRoute)</code> to access the parent route's
188
+ parameters.
189
+ </p>
190
+
191
+ <article className="api-item">
192
+ <h3>
193
+ <code>useRouteParams(route)</code>
194
+ </h3>
195
+ <p>
196
+ Returns typed route parameters for the given route definition. The
197
+ parameter types are automatically inferred from the route's path
198
+ pattern.
199
+ </p>
200
+ <CodeBlock language="tsx">{`import { route, useRouteParams } from "@funstack/router";
201
+
202
+ // Define route with id for type-safe access
203
+ const userRoute = route({
204
+ id: "user",
205
+ path: "/users/:userId",
206
+ component: UserPage,
207
+ });
208
+
209
+ function UserPage() {
210
+ // params is typed as { userId: string }
211
+ const params = useRouteParams(userRoute);
212
+
213
+ return <div>User ID: {params.userId}</div>;
214
+ }
215
+
216
+ // In nested routes, access parent route params:
217
+ const orgRoute = route({
218
+ id: "org",
219
+ path: "/org/:orgId",
220
+ component: OrgLayout,
221
+ children: [teamRoute],
222
+ });
223
+
224
+ function TeamPage() {
225
+ // Access parent route's params
226
+ const { orgId } = useRouteParams(orgRoute);
227
+ return <div>Org: {orgId}</div>;
228
+ }`}</CodeBlock>
229
+ <h4>Errors</h4>
230
+ <ul>
231
+ <li>Throws if called outside a route component (no RouteContext).</li>
232
+ <li>
233
+ Throws if the specified route's <code>id</code> is not found in the
234
+ current route hierarchy (neither the current route nor any
235
+ ancestor).
236
+ </li>
237
+ </ul>
238
+ </article>
239
+
240
+ <article className="api-item">
241
+ <h3>
242
+ <code>useRouteState(route)</code>
243
+ </h3>
244
+ <p>
245
+ Returns typed navigation state for the given route definition. Use
246
+ this with routes defined via <code>routeState&lt;T&gt;()</code> to get
247
+ properly typed state.
248
+ </p>
249
+ <CodeBlock language="tsx">{`import { routeState, useRouteState } from "@funstack/router";
250
+
251
+ type ScrollState = { scrollPos: number };
252
+
253
+ const scrollRoute = routeState<ScrollState>()({
254
+ id: "scroll",
255
+ path: "/page",
256
+ component: ScrollPage,
257
+ });
258
+
259
+ function ScrollPage() {
260
+ // state is typed as ScrollState | undefined
261
+ const state = useRouteState(scrollRoute);
262
+
263
+ return <div>Scroll position: {state?.scrollPos ?? 0}</div>;
264
+ }`}</CodeBlock>
265
+ <h4>Return Value</h4>
266
+ <p>
267
+ Returns <code>State | undefined</code>. State is{" "}
268
+ <code>undefined</code> on initial visit and when navigating to a new
269
+ entry without state.
270
+ </p>
271
+ <h4>Errors</h4>
272
+ <ul>
273
+ <li>Throws if called outside a route component (no RouteContext).</li>
274
+ <li>
275
+ Throws if the specified route's <code>id</code> is not found in the
276
+ current route hierarchy (neither the current route nor any
277
+ ancestor).
278
+ </li>
279
+ </ul>
280
+ </article>
281
+
282
+ <article className="api-item">
283
+ <h3>
284
+ <code>useRouteData(route)</code>
285
+ </h3>
286
+ <p>
287
+ Returns typed loader data for the given route definition. The data
288
+ type is automatically inferred from the route's <code>loader</code>{" "}
289
+ function.
290
+ </p>
291
+ <CodeBlock language="tsx">{`import { route, useRouteData } from "@funstack/router";
292
+
293
+ const userRoute = route({
294
+ id: "user",
295
+ path: "/users/:userId",
296
+ loader: async ({ params }) => {
297
+ const res = await fetch(\`/api/users/\${params.userId}\`);
298
+ return res.json() as Promise<{ name: string; age: number }>;
299
+ },
300
+ component: UserPage,
301
+ });
302
+
303
+ function UserPage() {
304
+ // data is typed as Promise<{ name: string; age: number }>
305
+ const data = useRouteData(userRoute);
306
+
307
+ // Use with React.use() or Suspense
308
+ const user = use(data);
309
+ return <div>User: {user.name}</div>;
310
+ }`}</CodeBlock>
311
+ <h4>Errors</h4>
312
+ <ul>
313
+ <li>Throws if called outside a route component (no RouteContext).</li>
314
+ <li>
315
+ Throws if the specified route's <code>id</code> is not found in the
316
+ current route hierarchy (neither the current route nor any
317
+ ancestor).
318
+ </li>
319
+ </ul>
320
+ </article>
321
+ </div>
322
+ );
323
+ }