@rangojs/router 0.0.0-experimental.30 → 0.0.0-experimental.31
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/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +206 -0
- package/skills/hooks/SKILL.md +2 -4
- package/skills/route/SKILL.md +1 -2
- package/skills/typesafety/SKILL.md +23 -21
- package/src/client.rsc.tsx +2 -0
- package/src/client.tsx +1 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +3 -0
package/dist/vite/index.js
CHANGED
|
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
|
|
|
1745
1745
|
// package.json
|
|
1746
1746
|
var package_default = {
|
|
1747
1747
|
name: "@rangojs/router",
|
|
1748
|
-
version: "0.0.0-experimental.
|
|
1748
|
+
version: "0.0.0-experimental.31",
|
|
1749
1749
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1750
1750
|
keywords: [
|
|
1751
1751
|
"react",
|
package/package.json
CHANGED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: breadcrumbs
|
|
3
|
+
description: Built-in Breadcrumbs handle for accumulating breadcrumb navigation across route segments
|
|
4
|
+
argument-hint: [setup]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Breadcrumbs
|
|
8
|
+
|
|
9
|
+
Built-in handle for accumulating breadcrumb items across route segments.
|
|
10
|
+
Each layout/route pushes items via `ctx.use(Breadcrumbs)`, and they are
|
|
11
|
+
collected in parent-to-child order with automatic deduplication by `href`.
|
|
12
|
+
|
|
13
|
+
## BreadcrumbItem Type
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
interface BreadcrumbItem {
|
|
17
|
+
label: string; // Display text
|
|
18
|
+
href: string; // URL the breadcrumb links to
|
|
19
|
+
content?: ReactNode | Promise<ReactNode>; // Optional extra content (sync or async)
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Pushing Breadcrumbs (Server)
|
|
24
|
+
|
|
25
|
+
Import `Breadcrumbs` from `@rangojs/router` in RSC/server context:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { urls, Breadcrumbs } from "@rangojs/router";
|
|
29
|
+
import { Outlet } from "@rangojs/router/client";
|
|
30
|
+
|
|
31
|
+
export const urlpatterns = urls(({ path, layout }) => [
|
|
32
|
+
// Root layout pushes "Home"
|
|
33
|
+
layout((ctx) => {
|
|
34
|
+
const breadcrumb = ctx.use(Breadcrumbs);
|
|
35
|
+
breadcrumb({ label: "Home", href: "/" });
|
|
36
|
+
return <RootLayout />;
|
|
37
|
+
}, () => [
|
|
38
|
+
path("/", HomePage, { name: "home" }),
|
|
39
|
+
|
|
40
|
+
// Nested layout pushes "Blog"
|
|
41
|
+
layout((ctx) => {
|
|
42
|
+
const breadcrumb = ctx.use(Breadcrumbs);
|
|
43
|
+
breadcrumb({ label: "Blog", href: "/blog" });
|
|
44
|
+
return <BlogLayout />;
|
|
45
|
+
}, () => [
|
|
46
|
+
path("/blog", BlogIndex, { name: "blog.index" }),
|
|
47
|
+
|
|
48
|
+
// Route handler pushes post title
|
|
49
|
+
path("/blog/:slug", (ctx) => {
|
|
50
|
+
const breadcrumb = ctx.use(Breadcrumbs);
|
|
51
|
+
breadcrumb({ label: ctx.params.slug, href: `/blog/${ctx.params.slug}` });
|
|
52
|
+
return <BlogPost slug={ctx.params.slug} />;
|
|
53
|
+
}, { name: "blog.post" }),
|
|
54
|
+
]),
|
|
55
|
+
]),
|
|
56
|
+
]);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
On `/blog/my-post`, breadcrumbs accumulate: `Home > Blog > my-post`.
|
|
60
|
+
|
|
61
|
+
## Async Content
|
|
62
|
+
|
|
63
|
+
The `content` field supports `Promise<ReactNode>` for streaming:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
path("/product/:id", async (ctx) => {
|
|
67
|
+
const breadcrumb = ctx.use(Breadcrumbs);
|
|
68
|
+
const productPromise = fetchProduct(ctx.params.id);
|
|
69
|
+
|
|
70
|
+
breadcrumb({
|
|
71
|
+
label: "Product",
|
|
72
|
+
href: `/product/${ctx.params.id}`,
|
|
73
|
+
content: productPromise.then((p) => <span>({p.category})</span>),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const product = await productPromise;
|
|
77
|
+
return <ProductPage product={product} />;
|
|
78
|
+
}, { name: "product" })
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Async content is a `Promise<ReactNode>`. Resolve it in your component
|
|
82
|
+
with React's `use()` hook wrapped in `<Suspense>`.
|
|
83
|
+
|
|
84
|
+
## Consuming Breadcrumbs (Client)
|
|
85
|
+
|
|
86
|
+
Use `useHandle(Breadcrumbs)` in a client component to read the accumulated items:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
"use client";
|
|
90
|
+
import { useHandle, Breadcrumbs, Link } from "@rangojs/router/client";
|
|
91
|
+
|
|
92
|
+
function BreadcrumbNav() {
|
|
93
|
+
const breadcrumbs = useHandle(Breadcrumbs);
|
|
94
|
+
|
|
95
|
+
if (!breadcrumbs.length) return null;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<nav aria-label="Breadcrumb">
|
|
99
|
+
<ol>
|
|
100
|
+
{breadcrumbs.map((crumb, i) => (
|
|
101
|
+
<li key={crumb.href}>
|
|
102
|
+
{i === breadcrumbs.length - 1 ? (
|
|
103
|
+
<span aria-current="page">{crumb.label}</span>
|
|
104
|
+
) : (
|
|
105
|
+
<Link to={crumb.href}>{crumb.label}</Link>
|
|
106
|
+
)}
|
|
107
|
+
</li>
|
|
108
|
+
))}
|
|
109
|
+
</ol>
|
|
110
|
+
</nav>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### With Selector
|
|
116
|
+
|
|
117
|
+
Re-render only when the selected value changes:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// Only the last breadcrumb
|
|
121
|
+
const current = useHandle(Breadcrumbs, (data) => data.at(-1));
|
|
122
|
+
|
|
123
|
+
// Breadcrumb count
|
|
124
|
+
const count = useHandle(Breadcrumbs, (data) => data.length);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Deduplication
|
|
128
|
+
|
|
129
|
+
The built-in collect function deduplicates by `href`. If multiple segments
|
|
130
|
+
push the same `href`, the last one wins. This prevents duplicates when
|
|
131
|
+
navigating between sibling routes that share a common breadcrumb.
|
|
132
|
+
|
|
133
|
+
## Passing as Props
|
|
134
|
+
|
|
135
|
+
Breadcrumbs handle can be passed from server to client components:
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
// Server component
|
|
139
|
+
path("/dashboard", (ctx) => {
|
|
140
|
+
const breadcrumb = ctx.use(Breadcrumbs);
|
|
141
|
+
breadcrumb({ label: "Dashboard", href: "/dashboard" });
|
|
142
|
+
return <DashboardNav handle={Breadcrumbs} />;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Client component
|
|
146
|
+
("use client");
|
|
147
|
+
import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
148
|
+
|
|
149
|
+
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
150
|
+
const crumbs = useHandle(handle);
|
|
151
|
+
return (
|
|
152
|
+
<nav>
|
|
153
|
+
{crumbs.map((c) => (
|
|
154
|
+
<a href={c.href}>{c.label}</a>
|
|
155
|
+
))}
|
|
156
|
+
</nav>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Complete Example
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// urls.tsx
|
|
165
|
+
import { urls, Breadcrumbs, Meta } from "@rangojs/router";
|
|
166
|
+
import { Outlet, MetaTags } from "@rangojs/router/client";
|
|
167
|
+
import { BreadcrumbNav } from "./components/BreadcrumbNav";
|
|
168
|
+
|
|
169
|
+
function RootLayout() {
|
|
170
|
+
return (
|
|
171
|
+
<html lang="en">
|
|
172
|
+
<head><MetaTags /></head>
|
|
173
|
+
<body>
|
|
174
|
+
<BreadcrumbNav />
|
|
175
|
+
<main><Outlet /></main>
|
|
176
|
+
</body>
|
|
177
|
+
</html>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const urlpatterns = urls(({ path, layout }) => [
|
|
182
|
+
layout((ctx) => {
|
|
183
|
+
ctx.use(Breadcrumbs)({ label: "Home", href: "/" });
|
|
184
|
+
ctx.use(Meta)({ title: "My App" });
|
|
185
|
+
return <RootLayout />;
|
|
186
|
+
}, () => [
|
|
187
|
+
path("/", () => <h1>Welcome</h1>, { name: "home" }),
|
|
188
|
+
|
|
189
|
+
layout((ctx) => {
|
|
190
|
+
ctx.use(Breadcrumbs)({ label: "Shop", href: "/shop" });
|
|
191
|
+
return <Outlet />;
|
|
192
|
+
}, () => [
|
|
193
|
+
path("/shop", () => <h1>Shop</h1>, { name: "shop" }),
|
|
194
|
+
path("/shop/:slug", (ctx) => {
|
|
195
|
+
ctx.use(Breadcrumbs)({
|
|
196
|
+
label: ctx.params.slug,
|
|
197
|
+
href: `/shop/${ctx.params.slug}`,
|
|
198
|
+
});
|
|
199
|
+
return <h1>Product: {ctx.params.slug}</h1>;
|
|
200
|
+
}, { name: "shop.product" }),
|
|
201
|
+
]),
|
|
202
|
+
]),
|
|
203
|
+
]);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Navigating to `/shop/widget` produces: `Home / Shop / widget`
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -247,8 +247,7 @@ Access accumulated handle data from route segments:
|
|
|
247
247
|
|
|
248
248
|
```tsx
|
|
249
249
|
"use client";
|
|
250
|
-
import { useHandle } from "@rangojs/router/client";
|
|
251
|
-
import { Breadcrumbs } from "../handles/breadcrumbs";
|
|
250
|
+
import { useHandle, Breadcrumbs } from "@rangojs/router/client";
|
|
252
251
|
|
|
253
252
|
function BreadcrumbNav() {
|
|
254
253
|
const crumbs = useHandle(Breadcrumbs);
|
|
@@ -282,8 +281,7 @@ path("/dashboard", (ctx) => {
|
|
|
282
281
|
|
|
283
282
|
// Client component — typeof infers the full Handle<T> type
|
|
284
283
|
("use client");
|
|
285
|
-
import { useHandle } from "@rangojs/router/client";
|
|
286
|
-
import type { Breadcrumbs } from "../handles";
|
|
284
|
+
import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
287
285
|
|
|
288
286
|
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
289
287
|
const crumbs = useHandle(handle);
|
package/skills/route/SKILL.md
CHANGED
|
@@ -355,8 +355,7 @@ urls(({ path, layout }) => [
|
|
|
355
355
|
## Complete Example
|
|
356
356
|
|
|
357
357
|
```typescript
|
|
358
|
-
import { urls } from "@rangojs/router";
|
|
359
|
-
import { Breadcrumbs } from "./handles/breadcrumbs";
|
|
358
|
+
import { urls, Breadcrumbs } from "@rangojs/router";
|
|
360
359
|
|
|
361
360
|
export const urlpatterns = urls(({ path, layout, loader, loading }) => [
|
|
362
361
|
// Simple route
|
|
@@ -414,26 +414,30 @@ Both approaches coexist: `ctx.get("user")` (global via Vars) and
|
|
|
414
414
|
Handles have typed data:
|
|
415
415
|
|
|
416
416
|
```typescript
|
|
417
|
-
//
|
|
418
|
-
import {
|
|
419
|
-
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
// In client - typed array
|
|
417
|
+
// Built-in Breadcrumbs handle — import from "@rangojs/router"
|
|
418
|
+
import { Breadcrumbs } from "@rangojs/router";
|
|
419
|
+
// Type: Handle<BreadcrumbItem, BreadcrumbItem[]>
|
|
420
|
+
// BreadcrumbItem: { label: string; href: string; content?: ReactNode | Promise<ReactNode> }
|
|
421
|
+
|
|
422
|
+
// In route handler — push is fully typed
|
|
423
|
+
path("/shop/product/:slug", (ctx) => {
|
|
424
|
+
const breadcrumb = ctx.use(Breadcrumbs);
|
|
425
|
+
breadcrumb({ label: "Products", href: "/shop/products" });
|
|
426
|
+
return <ProductPage />;
|
|
427
|
+
}, { name: "product" });
|
|
428
|
+
|
|
429
|
+
// In client — typed array
|
|
430
|
+
import { useHandle, Breadcrumbs } from "@rangojs/router/client";
|
|
433
431
|
function BreadcrumbNav() {
|
|
434
432
|
const crumbs = useHandle(Breadcrumbs);
|
|
435
|
-
// crumbs:
|
|
433
|
+
// crumbs: BreadcrumbItem[]
|
|
436
434
|
}
|
|
435
|
+
|
|
436
|
+
// Custom handles also work the same way
|
|
437
|
+
import { createHandle } from "@rangojs/router";
|
|
438
|
+
export const PageTitle = createHandle<string, string>(
|
|
439
|
+
(segments) => segments.flat().at(-1) ?? "Default Title"
|
|
440
|
+
);
|
|
437
441
|
```
|
|
438
442
|
|
|
439
443
|
## Ref Prop Type Safety (Loaders & Handles)
|
|
@@ -447,14 +451,12 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
447
451
|
return { product: await fetchProduct(ctx.params.slug) };
|
|
448
452
|
});
|
|
449
453
|
|
|
450
|
-
//
|
|
451
|
-
export const Breadcrumbs = createHandle<{ label: string; href: string }>();
|
|
454
|
+
// Built-in Breadcrumbs — or any custom handle created with createHandle()
|
|
452
455
|
|
|
453
456
|
// Client component — typeof infers all generics
|
|
454
457
|
("use client");
|
|
455
|
-
import { useLoader, useHandle } from "@rangojs/router/client";
|
|
458
|
+
import { useLoader, useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
456
459
|
import type { ProductLoader } from "../loaders";
|
|
457
|
-
import type { Breadcrumbs } from "../handles";
|
|
458
460
|
|
|
459
461
|
function MyComponent({
|
|
460
462
|
loader,
|
package/src/client.rsc.tsx
CHANGED
|
@@ -63,6 +63,8 @@ export { Meta } from "./handles/meta.js";
|
|
|
63
63
|
// MetaTags is a "use client" component that can be imported from RSC
|
|
64
64
|
export { MetaTags } from "./handles/MetaTags.js";
|
|
65
65
|
export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
|
|
66
|
+
// Breadcrumbs handle works in RSC context
|
|
67
|
+
export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
|
|
66
68
|
|
|
67
69
|
// Location state - createLocationState works in RSC (just creates definition)
|
|
68
70
|
// useLocationState is NOT exported here as it uses client hooks
|
package/src/client.tsx
CHANGED
|
@@ -544,6 +544,7 @@ export { useHandle } from "./browser/react/use-handle.js";
|
|
|
544
544
|
export { Meta } from "./handles/meta.js";
|
|
545
545
|
export { MetaTags } from "./handles/MetaTags.js";
|
|
546
546
|
export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
|
|
547
|
+
export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
|
|
547
548
|
|
|
548
549
|
// Location state - type-safe navigation state
|
|
549
550
|
export {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Breadcrumbs handle for accumulating breadcrumb items across route segments.
|
|
3
|
+
*
|
|
4
|
+
* Each layout/route pushes breadcrumb items via `ctx.use(Breadcrumbs)`.
|
|
5
|
+
* Items are collected in parent-to-child order with automatic deduplication
|
|
6
|
+
* by `href` (last item for each href wins).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* // In route handler
|
|
11
|
+
* route("/blog/:slug", (ctx) => {
|
|
12
|
+
* const breadcrumb = ctx.use(Breadcrumbs);
|
|
13
|
+
* breadcrumb({ label: "Blog", href: "/blog" });
|
|
14
|
+
* breadcrumb({ label: post.title, href: `/blog/${ctx.params.slug}` });
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // In client component (consume with useHandle)
|
|
18
|
+
* const crumbs = useHandle(Breadcrumbs);
|
|
19
|
+
* crumbs.map((c) => <a href={c.href}>{c.label}</a>);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ReactNode } from "react";
|
|
24
|
+
import { createHandle, type Handle } from "../handle.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A single breadcrumb item.
|
|
28
|
+
*
|
|
29
|
+
* @property label - Display text for the breadcrumb
|
|
30
|
+
* @property href - URL the breadcrumb links to
|
|
31
|
+
* @property content - Optional extra content (sync or async) rendered alongside the label
|
|
32
|
+
*/
|
|
33
|
+
export interface BreadcrumbItem {
|
|
34
|
+
label: string;
|
|
35
|
+
href: string;
|
|
36
|
+
content?: ReactNode | Promise<ReactNode>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Collect function for Breadcrumbs handle.
|
|
41
|
+
* Flattens segments in parent-to-child order with deduplication by href
|
|
42
|
+
* (last item for each href wins).
|
|
43
|
+
*/
|
|
44
|
+
function collectBreadcrumbs(segments: BreadcrumbItem[][]): BreadcrumbItem[] {
|
|
45
|
+
const all = segments.flat();
|
|
46
|
+
const seen = new Map<string, number>();
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < all.length; i++) {
|
|
49
|
+
seen.set(all[i].href, i);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Return items in order, keeping only the last occurrence per href
|
|
53
|
+
return all.filter((item, index) => seen.get(item.href) === index);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Built-in handle for accumulating breadcrumb navigation items.
|
|
58
|
+
*
|
|
59
|
+
* Use `ctx.use(Breadcrumbs)` in route handlers to push breadcrumb items.
|
|
60
|
+
* Use `useHandle(Breadcrumbs)` in client components to consume them.
|
|
61
|
+
*/
|
|
62
|
+
export const Breadcrumbs: Handle<BreadcrumbItem, BreadcrumbItem[]> =
|
|
63
|
+
createHandle<BreadcrumbItem, BreadcrumbItem[]>(
|
|
64
|
+
collectBreadcrumbs,
|
|
65
|
+
"__rsc_router_breadcrumbs__",
|
|
66
|
+
);
|
package/src/handles/index.ts
CHANGED
package/src/index.rsc.ts
CHANGED
|
@@ -171,6 +171,7 @@ export type { HandlerCacheConfig } from "./rsc/types.js";
|
|
|
171
171
|
|
|
172
172
|
// Built-in handles (server-side)
|
|
173
173
|
export { Meta } from "./handles/meta.js";
|
|
174
|
+
export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
|
|
174
175
|
|
|
175
176
|
// Request context (for accessing request data in server actions/components).
|
|
176
177
|
// Re-exported with a narrowed return type so that public consumers only see
|
package/src/index.ts
CHANGED
|
@@ -259,6 +259,9 @@ export type {
|
|
|
259
259
|
// Meta types
|
|
260
260
|
export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
|
|
261
261
|
|
|
262
|
+
// Breadcrumb types
|
|
263
|
+
export type { BreadcrumbItem } from "./handles/breadcrumbs.js";
|
|
264
|
+
|
|
262
265
|
// Reverse type utilities for type-safe URL generation (Django-style URL reversal)
|
|
263
266
|
export type {
|
|
264
267
|
ScopedReverseFunction,
|