@ssgoi/react 7.4.0 → 7.5.0-beta.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 +166 -144
- package/dist/context.cjs +2 -0
- package/dist/context.d.ts +16 -0
- package/dist/context.js +18 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +8 -4
- package/dist/layout-boundary.cjs +2 -0
- package/dist/layout-boundary.d.ts +35 -0
- package/dist/layout-boundary.js +63 -0
- package/dist/legacy/nextjs/index.d.ts +9 -0
- package/dist/{routers → legacy}/nextjs/selected-segments-to-path.d.ts +3 -0
- package/dist/legacy/nextjs/ssgoi-route-boundary.cjs +2 -0
- package/dist/{routers → legacy}/nextjs/ssgoi-route-boundary.d.ts +14 -1
- package/dist/legacy/nextjs/ssgoi-route-boundary.js +14 -0
- package/dist/legacy/react-router.d.ts +20 -0
- package/dist/legacy/remix.d.ts +25 -0
- package/dist/legacy/route-boundary.cjs +2 -0
- package/dist/legacy/route-boundary.d.ts +33 -0
- package/dist/legacy/route-boundary.js +33 -0
- package/dist/{ssgoi-transition.d.ts → legacy/ssgoi-transition.d.ts} +1 -2
- package/dist/legacy/tanstack-router.d.ts +19 -0
- package/dist/legacy/unplugin/esbuild.d.ts +3 -0
- package/dist/legacy/unplugin/index.d.ts +17 -0
- package/dist/legacy/unplugin/rollup.d.ts +3 -0
- package/dist/legacy/unplugin/vite.d.ts +3 -0
- package/dist/legacy/unplugin/webpack.d.ts +3 -0
- package/dist/nextjs.cjs +1 -1
- package/dist/nextjs.d.ts +1 -1
- package/dist/nextjs.js +2 -2
- package/dist/react-router.cjs +1 -1
- package/dist/react-router.d.ts +1 -1
- package/dist/react-router.js +7 -10
- package/dist/remix.cjs +1 -1
- package/dist/remix.d.ts +1 -1
- package/dist/remix.js +7 -10
- package/dist/ssgoi.cjs +1 -1
- package/dist/ssgoi.d.ts +6 -0
- package/dist/ssgoi.js +23 -14
- package/dist/tanstack-router.cjs +1 -1
- package/dist/tanstack-router.d.ts +1 -1
- package/dist/tanstack-router.js +13 -12
- package/dist/unplugin/esbuild.d.ts +4 -2
- package/dist/unplugin/index.d.ts +6 -14
- package/dist/unplugin/rollup.d.ts +4 -2
- package/dist/unplugin/vite.d.ts +4 -2
- package/dist/unplugin/webpack.d.ts +4 -2
- package/package.json +34 -22
- package/dist/routers/nextjs/index.d.ts +0 -4
- package/dist/routers/nextjs/ssgoi-route-boundary.cjs +0 -2
- package/dist/routers/nextjs/ssgoi-route-boundary.js +0 -24
- package/dist/routers/react-router.d.ts +0 -7
- package/dist/routers/remix.d.ts +0 -8
- package/dist/routers/route-boundary.cjs +0 -2
- package/dist/routers/route-boundary.d.ts +0 -22
- package/dist/routers/route-boundary.js +0 -22
- package/dist/routers/tanstack-router.d.ts +0 -6
- /package/dist/{routers → legacy}/nextjs/selected-segments-to-path.cjs +0 -0
- /package/dist/{routers → legacy}/nextjs/selected-segments-to-path.js +0 -0
- /package/dist/{ssgoi-transition.cjs → legacy/ssgoi-transition.cjs} +0 -0
- /package/dist/{ssgoi-transition.js → legacy/ssgoi-transition.js} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ssgoi/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Native app-like page transitions for React and Next.js. Make a web app, PWA or WebView app (Capacitor, Tauri, Ionic) feel like a native app without replacing your router.
|
|
4
4
|
|
|
5
5
|
[](https://ssgoi.dev)
|
|
6
6
|
|
|
@@ -12,17 +12,28 @@ npm install @ssgoi/react
|
|
|
12
12
|
|
|
13
13
|
Agent setup guide: https://ssgoi.dev/llms.txt
|
|
14
14
|
|
|
15
|
-
## Router
|
|
15
|
+
## Router integration
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
Tell `<Ssgoi framework>` how to read your router once, describe your layout
|
|
18
|
+
tree in `config.layouts`, and put a prop-less `SsgoiLayoutBoundary` from
|
|
19
|
+
`@ssgoi/react` in each layout that owns routed content. The root entry imports
|
|
20
|
+
no router package: `framework.useRoute` is a hook you write with your router's
|
|
21
|
+
own hooks, returning `{ pathname, renderedPath? }` or `null` while the router
|
|
22
|
+
isn't ready. The first boundary calls it once for every boundary inside it.
|
|
19
23
|
|
|
20
|
-
| Router |
|
|
21
|
-
| ----------------------- |
|
|
22
|
-
| Next.js
|
|
23
|
-
| Remix 2
|
|
24
|
-
|
|
|
25
|
-
|
|
24
|
+
| Router | `useRoute` |
|
|
25
|
+
| ----------------------- | ---------------------------------------------------------------- |
|
|
26
|
+
| Next.js App Router | `usePathname` + `useSelectedLayoutSegments` ([Next.js](#nextjs)) |
|
|
27
|
+
| React Router / Remix 2 | `() => useLocation()` |
|
|
28
|
+
| TanStack Router / Start | the rendered leaf match ([TanStack Router](#tanstack-router)) |
|
|
29
|
+
|
|
30
|
+
Define the object at module scope so it stays stable. `SsgoiLayoutBoundary`
|
|
31
|
+
throws if the nearest `<Ssgoi>` has no `framework`.
|
|
32
|
+
|
|
33
|
+
The router subpaths `@ssgoi/react/nextjs`, `@ssgoi/react/remix`,
|
|
34
|
+
`@ssgoi/react/react-router` and `@ssgoi/react/tanstack-router` (and their
|
|
35
|
+
`SsgoiRouteBoundary`) still work but are deprecated and will be removed in
|
|
36
|
+
8.0.0.
|
|
26
37
|
|
|
27
38
|
See the [framework guide](https://ssgoi.dev/docs/frameworks/react) for wiring and limits.
|
|
28
39
|
|
|
@@ -55,10 +66,21 @@ Keep one config and one `<Ssgoi>` above route boundaries.
|
|
|
55
66
|
// app/ssgoi-provider.tsx
|
|
56
67
|
"use client";
|
|
57
68
|
|
|
58
|
-
import {
|
|
69
|
+
import { usePathname, useSelectedLayoutSegments } from "next/navigation";
|
|
70
|
+
import { Ssgoi, type SsgoiFramework } from "@ssgoi/react";
|
|
59
71
|
import { drill, slide } from "@ssgoi/react/view-transitions";
|
|
60
72
|
|
|
73
|
+
const TOP_LEVEL = ["/", "/search", "/profile"];
|
|
74
|
+
|
|
61
75
|
const config = {
|
|
76
|
+
layouts: [
|
|
77
|
+
{ paths: TOP_LEVEL },
|
|
78
|
+
{
|
|
79
|
+
paths: "/**",
|
|
80
|
+
except: TOP_LEVEL,
|
|
81
|
+
layouts: [{ paths: "/products/**" }],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
62
84
|
transitions: [
|
|
63
85
|
{ on: "/posts/**", except: "/posts", transition: drill() },
|
|
64
86
|
{
|
|
@@ -68,23 +90,49 @@ const config = {
|
|
|
68
90
|
],
|
|
69
91
|
};
|
|
70
92
|
|
|
93
|
+
const INTERCEPTION_PREFIX = /^(?:\(\.\.\.\)|\(\.\.\)|\(\.\))+/;
|
|
94
|
+
|
|
95
|
+
const framework: SsgoiFramework = {
|
|
96
|
+
useRoute: () => {
|
|
97
|
+
const pathname = usePathname();
|
|
98
|
+
// The page the children slot renders. During a soft intercept it is still
|
|
99
|
+
// the background page, so no background boundary moves.
|
|
100
|
+
const segments = useSelectedLayoutSegments();
|
|
101
|
+
if (pathname == null) return null;
|
|
102
|
+
const renderedPath = `/${segments
|
|
103
|
+
.filter((segment) => !(segment.startsWith("(") && segment.endsWith(")")))
|
|
104
|
+
.map((segment) => segment.replace(INTERCEPTION_PREFIX, ""))
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.join("/")}`;
|
|
107
|
+
return { pathname, renderedPath };
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
71
111
|
export function SsgoiProvider({ children }) {
|
|
72
|
-
return
|
|
112
|
+
return (
|
|
113
|
+
<Ssgoi config={config} framework={framework}>
|
|
114
|
+
{children}
|
|
115
|
+
</Ssgoi>
|
|
116
|
+
);
|
|
73
117
|
}
|
|
74
118
|
```
|
|
75
119
|
|
|
76
|
-
|
|
120
|
+
Render the provider in the root layout with the depth-0 boundary:
|
|
77
121
|
|
|
78
122
|
```tsx
|
|
79
123
|
// app/layout.tsx
|
|
124
|
+
import { SsgoiLayoutBoundary } from "@ssgoi/react";
|
|
80
125
|
import { SsgoiProvider } from "./ssgoi-provider";
|
|
81
126
|
|
|
82
|
-
export default function RootLayout({ children }) {
|
|
127
|
+
export default function RootLayout({ children, modal }) {
|
|
83
128
|
return (
|
|
84
129
|
<html>
|
|
85
130
|
<body>
|
|
86
131
|
<main className="relative z-0 min-h-dvh overflow-x-clip">
|
|
87
|
-
<SsgoiProvider>
|
|
132
|
+
<SsgoiProvider>
|
|
133
|
+
<SsgoiLayoutBoundary>{children}</SsgoiLayoutBoundary>
|
|
134
|
+
{modal}
|
|
135
|
+
</SsgoiProvider>
|
|
88
136
|
</main>
|
|
89
137
|
</body>
|
|
90
138
|
</html>
|
|
@@ -98,12 +146,18 @@ block, `z-0` creates the local stacking context, and `overflow-x-clip` prevents
|
|
|
98
146
|
horizontal transition flashes. These classes belong on the shell around
|
|
99
147
|
`<Ssgoi>`, not on route boundaries.
|
|
100
148
|
|
|
149
|
+
Put the first `SsgoiLayoutBoundary` in the same layout as `<Ssgoi>`:
|
|
150
|
+
`renderedPath` is computed relative to that layout. That boundary reads the
|
|
151
|
+
route for every boundary inside it, within its own `Suspense`, so Next.js Cache
|
|
152
|
+
Components can suspend on the URL. Until the route is known it renders nothing,
|
|
153
|
+
while content outside it (such as `{modal}`) renders as usual.
|
|
154
|
+
|
|
101
155
|
### Route boundary
|
|
102
156
|
|
|
103
157
|
A React route boundary is a keyed element marked with a route id:
|
|
104
158
|
|
|
105
159
|
```tsx
|
|
106
|
-
<div key={
|
|
160
|
+
<div key={key} data-ssgoi-transition={routePath}>
|
|
107
161
|
{children}
|
|
108
162
|
</div>
|
|
109
163
|
```
|
|
@@ -113,166 +167,131 @@ A React route boundary is a keyed element marked with a route id:
|
|
|
113
167
|
- `<Ssgoi>` observes that lifecycle and runs OUT/IN; it does not navigate or
|
|
114
168
|
change the key.
|
|
115
169
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
boundary.
|
|
119
|
-
|
|
120
|
-
Use `@ssgoi/react/nextjs` for the ready-made boundary. If the application needs
|
|
121
|
-
its own policy layer, the following manual example keeps route logic behind
|
|
122
|
-
semantic names so layouts cannot invent inconsistent keys:
|
|
123
|
-
|
|
124
|
-
```tsx
|
|
125
|
-
// app/ssgoi-route-boundary.tsx
|
|
126
|
-
"use client";
|
|
127
|
-
|
|
128
|
-
import { type ReactNode } from "react";
|
|
129
|
-
import { usePathname, useSelectedLayoutSegments } from "next/navigation";
|
|
170
|
+
`SsgoiLayoutBoundary` renders that element for you and takes no SSGOI props:
|
|
171
|
+
`as` picks the element (default `div`) and every other prop is forwarded.
|
|
130
172
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return segment.startsWith("(") && segment.endsWith(")");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function pathFromSegments(segments: string[]) {
|
|
141
|
-
const path = segments
|
|
142
|
-
.filter((segment) => !isRouteGroup(segment))
|
|
143
|
-
.map((segment) => segment.replace(INTERCEPTION_PREFIX, ""))
|
|
144
|
-
.filter(Boolean)
|
|
145
|
-
.join("/");
|
|
146
|
-
|
|
147
|
-
return path ? `/${path}` : null;
|
|
148
|
-
}
|
|
173
|
+
- Its depth is the number of boundaries above it: 0 in the root layout, 1 in a
|
|
174
|
+
layout under it, and so on.
|
|
175
|
+
- `routePath` is `renderedPath ?? pathname`; it is the id of every boundary.
|
|
176
|
+
- The key at depth N is the depth-N scope instance `config.layouts` resolves for
|
|
177
|
+
`routePath`, or `routePath` itself when there is none. Without
|
|
178
|
+
`config.layouts`, every boundary keys by the route.
|
|
149
179
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
segments: string[],
|
|
154
|
-
): BoundaryIdentity {
|
|
155
|
-
const ownedRoute = pathFromSegments(segments) ?? pathname;
|
|
156
|
-
|
|
157
|
-
switch (name) {
|
|
158
|
-
case "app-shell": {
|
|
159
|
-
const routeGroup = segments.find(isRouteGroup);
|
|
160
|
-
if (routeGroup === "(main)") {
|
|
161
|
-
return { id: ownedRoute, key: "main-shell" };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const project = ownedRoute.match(/^\/projects\/[^/]+/)?.[0];
|
|
165
|
-
return { id: ownedRoute, key: project ?? ownedRoute };
|
|
166
|
-
}
|
|
167
|
-
case "project-content": {
|
|
168
|
-
const project = pathname.match(/^\/projects\/[^/]+/)?.[0];
|
|
169
|
-
const child = pathFromSegments(segments);
|
|
170
|
-
const id = project
|
|
171
|
-
? child
|
|
172
|
-
? `${project}${child}`
|
|
173
|
-
: project
|
|
174
|
-
: ownedRoute;
|
|
175
|
-
return { id, key: id };
|
|
176
|
-
}
|
|
177
|
-
case "main-content":
|
|
178
|
-
return { id: ownedRoute, key: ownedRoute };
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function SsgoiRouteBoundary({
|
|
183
|
-
children,
|
|
184
|
-
name,
|
|
185
|
-
}: {
|
|
186
|
-
children: ReactNode;
|
|
187
|
-
name: BoundaryName;
|
|
188
|
-
}) {
|
|
189
|
-
const pathname = usePathname();
|
|
190
|
-
const segments = useSelectedLayoutSegments("children");
|
|
191
|
-
const boundary = resolveBoundary(name, pathname, segments);
|
|
192
|
-
|
|
193
|
-
return (
|
|
194
|
-
<div key={boundary.key} data-ssgoi-transition={boundary.id}>
|
|
195
|
-
{children}
|
|
196
|
-
</div>
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
```
|
|
180
|
+
The legacy `<SsgoiTransition>` wrapper only added this attribute and is
|
|
181
|
+
deprecated (removed in 8.0.0). Set `data-ssgoi-transition` directly on the keyed
|
|
182
|
+
application boundary.
|
|
200
183
|
|
|
201
184
|
## Persistent layouts
|
|
202
185
|
|
|
203
|
-
|
|
204
|
-
|
|
186
|
+
`config.layouts` mirrors the layouts that own boundaries. Siblings in one array
|
|
187
|
+
are tried in order and the first match wins (`except` excludes paths); a scope's
|
|
188
|
+
`layouts` is the next depth inside it. Patterns use the transition-rule syntax.
|
|
189
|
+
`*`, `:id` and `[id]` segments make one instance per value, so
|
|
190
|
+
`{ paths: "/projects/:id/**" }` keeps `/projects/1/files` and
|
|
191
|
+
`/projects/1/members` in one layout and gives `/projects/2` another; `**`
|
|
192
|
+
tails do not.
|
|
205
193
|
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
194
|
+
```text
|
|
195
|
+
app/
|
|
196
|
+
layout.tsx # <Ssgoi> + depth-0 boundary, {modal} outside it
|
|
197
|
+
(top-level)/layout.tsx # depth-1 boundary + bottom nav outside it
|
|
198
|
+
(top-level)/page.tsx
|
|
199
|
+
(top-level)/search/page.tsx
|
|
200
|
+
(detail)/layout.tsx # depth-1 boundary
|
|
201
|
+
(detail)/posts/[id]/page.tsx
|
|
202
|
+
(detail)/products/layout.tsx # depth-2 boundary + category tabs
|
|
203
|
+
@modal/(.)posts/[id]/page.tsx
|
|
211
204
|
```
|
|
212
205
|
|
|
213
206
|
```tsx
|
|
214
|
-
// app/(
|
|
215
|
-
|
|
207
|
+
// app/(top-level)/layout.tsx
|
|
208
|
+
import { SsgoiLayoutBoundary } from "@ssgoi/react";
|
|
209
|
+
|
|
210
|
+
export default function TopLevelLayout({ children }) {
|
|
216
211
|
return (
|
|
217
212
|
<>
|
|
218
|
-
<
|
|
213
|
+
<SsgoiLayoutBoundary>{children}</SsgoiLayoutBoundary>
|
|
219
214
|
<BottomNav />
|
|
220
215
|
</>
|
|
221
216
|
);
|
|
222
217
|
}
|
|
223
218
|
```
|
|
224
219
|
|
|
225
|
-
|
|
226
|
-
- Main → detail: the common app-shell key changes; the nav leaves with it.
|
|
227
|
-
- Project tab → project tab: the current project base-path key stays; a project
|
|
228
|
-
content boundary can change below its header and tabs.
|
|
229
|
-
|
|
230
|
-
Use Next.js route groups to describe layout ownership, not to duplicate the
|
|
231
|
-
outer boundary:
|
|
220
|
+
With the config above:
|
|
232
221
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
boundary’s id/key from `useSelectedLayoutSegments("children")`. In the layout
|
|
245
|
-
that owns `@modal`, `useSelectedLayoutSegment("modal") !== null` is an explicit
|
|
246
|
-
active-modal check. A soft intercept keeps the background key; direct entry to
|
|
247
|
-
the same URL resolves to the detail slot and receives a detail key.
|
|
222
|
+
- Tab → tab (`/` → `/search`): the depth-0 key stays, so the bottom nav stays;
|
|
223
|
+
only the depth-1 content changes.
|
|
224
|
+
- Top level → detail (`/search` → `/posts/1`): the depth-0 key changes; the
|
|
225
|
+
whole top level, bottom nav included, leaves.
|
|
226
|
+
- Detail → detail and category → category under `/products/**`: the enclosing
|
|
227
|
+
layouts stay; the boundary one level deeper changes, so `slide` moves only
|
|
228
|
+
the category content.
|
|
229
|
+
- A soft intercept (`/posts/1` opened over `/search`) keeps `renderedPath` at
|
|
230
|
+
`/search`, so nothing in the background moves; the `{modal}` slot sits
|
|
231
|
+
outside every boundary and animates by itself. Direct entry to `/posts/1`
|
|
232
|
+
renders the detail page and keys it as one.
|
|
248
233
|
|
|
249
234
|
Full pattern: https://ssgoi.dev/llms/bottom-nav.txt
|
|
250
235
|
|
|
251
236
|
## React Router
|
|
252
237
|
|
|
253
|
-
|
|
254
|
-
nested layout route:
|
|
238
|
+
Render `<Ssgoi>` inside the router and the boundary in a layout route:
|
|
255
239
|
|
|
256
240
|
```tsx
|
|
257
|
-
|
|
241
|
+
import { Outlet, useLocation } from "react-router"; // Remix 2: "@remix-run/react"
|
|
242
|
+
import { Ssgoi, SsgoiLayoutBoundary, type SsgoiFramework } from "@ssgoi/react";
|
|
258
243
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
244
|
+
const framework: SsgoiFramework = { useRoute: () => useLocation() };
|
|
245
|
+
|
|
246
|
+
export default function Layout() {
|
|
247
|
+
return (
|
|
248
|
+
<Ssgoi config={config} framework={framework}>
|
|
249
|
+
<SsgoiLayoutBoundary>
|
|
250
|
+
<Outlet />
|
|
251
|
+
</SsgoiLayoutBoundary>
|
|
252
|
+
</Ssgoi>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
264
255
|
```
|
|
265
256
|
|
|
257
|
+
Query-only navigation keeps the boundary. Nested layout routes add deeper
|
|
258
|
+
boundaries, keyed by `config.layouts` as above.
|
|
259
|
+
|
|
266
260
|
## TanStack Router
|
|
267
261
|
|
|
268
|
-
|
|
262
|
+
Key by the leaf match the outlet renders, not `location`: TanStack Router moves
|
|
263
|
+
`location` before pending matches render, so keying by it re-keys the boundary
|
|
264
|
+
while the outgoing page is still on screen. Index routes match as "/parent/",
|
|
265
|
+
so the location's own trailing-slash style wins.
|
|
269
266
|
|
|
270
267
|
```tsx
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
268
|
+
import { Outlet, useRouterState } from "@tanstack/react-router";
|
|
269
|
+
import { Ssgoi, SsgoiLayoutBoundary, type SsgoiFramework } from "@ssgoi/react";
|
|
270
|
+
|
|
271
|
+
const framework: SsgoiFramework = {
|
|
272
|
+
useRoute: () => ({
|
|
273
|
+
pathname: useRouterState({
|
|
274
|
+
select: (state) => {
|
|
275
|
+
const location = state.location.pathname;
|
|
276
|
+
const leaf = state.matches.at(-1)?.pathname;
|
|
277
|
+
if (!leaf) return location;
|
|
278
|
+
return leaf.length > 1 && leaf.endsWith("/") && !location.endsWith("/")
|
|
279
|
+
? leaf.slice(0, -1)
|
|
280
|
+
: leaf;
|
|
281
|
+
},
|
|
282
|
+
}),
|
|
283
|
+
}),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
<Ssgoi config={config} framework={framework}>
|
|
287
|
+
<SsgoiLayoutBoundary>
|
|
288
|
+
<Outlet />
|
|
289
|
+
</SsgoiLayoutBoundary>
|
|
290
|
+
</Ssgoi>;
|
|
274
291
|
```
|
|
275
292
|
|
|
293
|
+
Pathless `_layout` routes add deeper boundaries, keyed by `config.layouts`.
|
|
294
|
+
|
|
276
295
|
## Config
|
|
277
296
|
|
|
278
297
|
```ts
|
|
@@ -298,6 +317,8 @@ const config = {
|
|
|
298
317
|
};
|
|
299
318
|
```
|
|
300
319
|
|
|
320
|
+
- `layouts`: the layout tree for `SsgoiLayoutBoundary` keys (see
|
|
321
|
+
[Persistent layouts](#persistent-layouts)).
|
|
301
322
|
- `on`: route family.
|
|
302
323
|
- `from`/`to`: precise pair.
|
|
303
324
|
- `ordered`: directional sequence.
|
|
@@ -324,8 +345,9 @@ References: https://ssgoi.dev/llms.txt#7-transition-index
|
|
|
324
345
|
|
|
325
346
|
## Low-level APIs
|
|
326
347
|
|
|
327
|
-
`transition()`
|
|
328
|
-
|
|
348
|
+
`transition()` remains available for element-level mount/unmount animations;
|
|
349
|
+
it is separate from route boundaries. The `@ssgoi/react/unplugin` auto-key
|
|
350
|
+
plugins are deprecated and will be removed in 8.0.0.
|
|
329
351
|
|
|
330
352
|
## License
|
|
331
353
|
|
package/dist/context.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react"),n=require("@ssgoi/core/internal"),s=e.createContext(null),o=()=>n.IDLE_TRANSITION_STATE;function i(){const t=e.useContext(s);if(!t)throw new Error("useSsgoiTransition must be used within <Ssgoi>");return e.useSyncExternalStore(t.subscribe,t.getTransitionState,o)}exports.ssgoiContext=s;exports.useSsgoiTransition=i;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SsgoiContext } from '@ssgoi/core/internal';
|
|
2
|
+
import { SsgoiTransitionState } from '@ssgoi/core/types';
|
|
3
|
+
/** Provider-scoped dispatcher; `<Ssgoi>` owns the only value. */
|
|
4
|
+
export declare const ssgoiContext: import('react').Context<SsgoiContext | null>;
|
|
5
|
+
/**
|
|
6
|
+
* Reads the provider's transition lifecycle: whether a page transition is
|
|
7
|
+
* preparing or playing, and which routes it joins. Re-renders the caller on
|
|
8
|
+
* every change.
|
|
9
|
+
*
|
|
10
|
+
* Use it to defer work that would fight the animation, such as a
|
|
11
|
+
* programmatic scroll on the arriving page, until the status returns to
|
|
12
|
+
* `"idle"`.
|
|
13
|
+
*
|
|
14
|
+
* Must be called under `<Ssgoi>`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useSsgoiTransition(): SsgoiTransitionState;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext as o, useContext as e, useSyncExternalStore as s } from "react";
|
|
3
|
+
import { IDLE_TRANSITION_STATE as n } from "@ssgoi/core/internal";
|
|
4
|
+
const r = o(null), i = () => n;
|
|
5
|
+
function a() {
|
|
6
|
+
const t = e(r);
|
|
7
|
+
if (!t)
|
|
8
|
+
throw new Error("useSsgoiTransition must be used within <Ssgoi>");
|
|
9
|
+
return s(
|
|
10
|
+
t.subscribe,
|
|
11
|
+
t.getTransitionState,
|
|
12
|
+
i
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
r as ssgoiContext,
|
|
17
|
+
a as useSsgoiTransition
|
|
18
|
+
};
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("@ssgoi/core/types"),r=require("./ssgoi.cjs"),i=require("./context.cjs"),s=require("./legacy/ssgoi-transition.cjs"),n=require("./layout-boundary.cjs"),t=require("@ssgoi/core");exports.Ssgoi=r.Ssgoi;exports.useSsgoiTransition=i.useSsgoiTransition;exports.SsgoiTransition=s.SsgoiTransition;exports.SsgoiLayoutBoundary=n.SsgoiLayoutBoundary;Object.keys(o).forEach(e=>{e!=="default"&&!Object.prototype.hasOwnProperty.call(exports,e)&&Object.defineProperty(exports,e,{enumerable:!0,get:()=>o[e]})});Object.keys(t).forEach(e=>{e!=="default"&&!Object.prototype.hasOwnProperty.call(exports,e)&&Object.defineProperty(exports,e,{enumerable:!0,get:()=>t[e]})});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export * from './types';
|
|
2
2
|
export { Ssgoi } from './ssgoi';
|
|
3
|
-
export {
|
|
3
|
+
export { useSsgoiTransition } from './context';
|
|
4
|
+
export { SsgoiTransition } from './legacy/ssgoi-transition';
|
|
5
|
+
export { SsgoiLayoutBoundary } from './layout-boundary';
|
|
6
|
+
export type { SsgoiFramework, SsgoiLayoutBoundaryProps, } from './layout-boundary';
|
|
4
7
|
export * from '@ssgoi/core';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export * from "@ssgoi/core/types";
|
|
2
|
-
import { Ssgoi as
|
|
3
|
-
import {
|
|
2
|
+
import { Ssgoi as i } from "./ssgoi.js";
|
|
3
|
+
import { useSsgoiTransition as s } from "./context.js";
|
|
4
|
+
import { SsgoiTransition as m } from "./legacy/ssgoi-transition.js";
|
|
5
|
+
import { SsgoiLayoutBoundary as x } from "./layout-boundary.js";
|
|
4
6
|
export * from "@ssgoi/core";
|
|
5
7
|
export {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
i as Ssgoi,
|
|
9
|
+
x as SsgoiLayoutBoundary,
|
|
10
|
+
m as SsgoiTransition,
|
|
11
|
+
s as useSsgoiTransition
|
|
8
12
|
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("react/jsx-runtime"),n=require("react"),y=require("@ssgoi/core/internal"),a=n.createContext(null),c=n.createContext(null);function m({framework:e,layouts:t,children:o}){const r=n.useMemo(()=>e?{framework:e,layouts:t}:null,[e,t]);return s.jsx(a.Provider,{value:r,children:o})}function S(e){const t=n.useContext(a),o=n.useContext(c);if(o)return s.jsx(l,{...e,state:o});if(!t)throw new Error("SSGOI: SsgoiLayoutBoundary requires a framework. Pass it to <Ssgoi framework={{ useRoute }}>.");return s.jsx(n.Suspense,{fallback:null,children:s.jsx(f,{...e,setup:t})})}function f({setup:{framework:e,layouts:t},...o}){const r=e.useRoute(),u=r&&(r.renderedPath??r.pathname),i=n.useMemo(()=>u==null?null:{routePath:u,chain:y.resolveLayoutChain(t,u),depth:0},[t,u]);return i?s.jsx(l,{...o,state:i}):null}function l({state:e,as:t,children:o,...r}){const u=n.useMemo(()=>({...e,depth:e.depth+1}),[e]),{routePath:i,chain:d,depth:h}=e,x=t??"div";return n.createElement(x,{...r,key:d[h]??i,"data-ssgoi-transition":i},s.jsx(c.Provider,{value:u,children:o}))}exports.SsgoiFrameworkProvider=m;exports.SsgoiLayoutBoundary=S;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react';
|
|
2
|
+
import { SsgoiLayoutScope } from '@ssgoi/core/types';
|
|
3
|
+
/**
|
|
4
|
+
* How `<Ssgoi>` reads the router, passed to `<Ssgoi framework>`. Keep the
|
|
5
|
+
* object stable.
|
|
6
|
+
*/
|
|
7
|
+
export interface SsgoiFramework {
|
|
8
|
+
/**
|
|
9
|
+
* A hook returning the current route, or `null` while the router isn't
|
|
10
|
+
* ready. `renderedPath` is the path the routed region actually shows when it
|
|
11
|
+
* differs from the URL (a soft-intercepted Next.js route); boundaries key by
|
|
12
|
+
* it instead of `pathname`.
|
|
13
|
+
*/
|
|
14
|
+
useRoute: () => {
|
|
15
|
+
pathname: string;
|
|
16
|
+
renderedPath?: string;
|
|
17
|
+
} | null;
|
|
18
|
+
}
|
|
19
|
+
/** @internal `<Ssgoi>`'s framework and layout tree, for the first boundary. */
|
|
20
|
+
export declare function SsgoiFrameworkProvider({ framework, layouts, children, }: {
|
|
21
|
+
framework: SsgoiFramework | undefined;
|
|
22
|
+
layouts: readonly SsgoiLayoutScope[] | undefined;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export type SsgoiLayoutBoundaryProps<T extends ElementType = "div"> = {
|
|
26
|
+
children?: ReactNode;
|
|
27
|
+
as?: T;
|
|
28
|
+
} & Omit<ComponentPropsWithoutRef<T>, "as" | "children">;
|
|
29
|
+
/**
|
|
30
|
+
* A keyed route boundary for the layout that renders it. Its depth is the
|
|
31
|
+
* number of boundaries above it; it stays mounted while navigation stays in
|
|
32
|
+
* the same `config.layouts` scope at that depth, and otherwise keys by the
|
|
33
|
+
* route. The first boundary reads the route for the ones inside it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function SsgoiLayoutBoundary<T extends ElementType = "div">(props: SsgoiLayoutBoundaryProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as u } from "react/jsx-runtime";
|
|
3
|
+
import { createContext as c, useMemo as s, useContext as a, Suspense as f, createElement as y } from "react";
|
|
4
|
+
import { resolveLayoutChain as v } from "@ssgoi/core/internal";
|
|
5
|
+
const l = c(null), d = c(null);
|
|
6
|
+
function k({
|
|
7
|
+
framework: t,
|
|
8
|
+
layouts: e,
|
|
9
|
+
children: n
|
|
10
|
+
}) {
|
|
11
|
+
const o = s(
|
|
12
|
+
() => t ? { framework: t, layouts: e } : null,
|
|
13
|
+
[t, e]
|
|
14
|
+
);
|
|
15
|
+
return /* @__PURE__ */ u(l.Provider, { value: o, children: n });
|
|
16
|
+
}
|
|
17
|
+
function w(t) {
|
|
18
|
+
const e = a(l), n = a(d);
|
|
19
|
+
if (n) return /* @__PURE__ */ u(p, { ...t, state: n });
|
|
20
|
+
if (!e)
|
|
21
|
+
throw new Error(
|
|
22
|
+
"SSGOI: SsgoiLayoutBoundary requires a framework. Pass it to <Ssgoi framework={{ useRoute }}>."
|
|
23
|
+
);
|
|
24
|
+
return (
|
|
25
|
+
// Catches URL suspension (Next.js Cache Components) outside the hook.
|
|
26
|
+
/* @__PURE__ */ u(f, { fallback: null, children: /* @__PURE__ */ u(C, { ...t, setup: e }) })
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
function C({
|
|
30
|
+
setup: { framework: t, layouts: e },
|
|
31
|
+
...n
|
|
32
|
+
}) {
|
|
33
|
+
const o = t.useRoute(), r = o && (o.renderedPath ?? o.pathname), i = s(
|
|
34
|
+
() => r == null ? null : {
|
|
35
|
+
routePath: r,
|
|
36
|
+
chain: v(e, r),
|
|
37
|
+
depth: 0
|
|
38
|
+
},
|
|
39
|
+
[e, r]
|
|
40
|
+
);
|
|
41
|
+
return i ? /* @__PURE__ */ u(p, { ...n, state: i }) : null;
|
|
42
|
+
}
|
|
43
|
+
function p({
|
|
44
|
+
state: t,
|
|
45
|
+
as: e,
|
|
46
|
+
children: n,
|
|
47
|
+
...o
|
|
48
|
+
}) {
|
|
49
|
+
const r = s(() => ({ ...t, depth: t.depth + 1 }), [t]), { routePath: i, chain: h, depth: m } = t;
|
|
50
|
+
return /* @__PURE__ */ y(
|
|
51
|
+
e ?? "div",
|
|
52
|
+
{
|
|
53
|
+
...o,
|
|
54
|
+
key: h[m] ?? i,
|
|
55
|
+
"data-ssgoi-transition": i
|
|
56
|
+
},
|
|
57
|
+
/* @__PURE__ */ u(d.Provider, { value: r, children: n })
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
k as SsgoiFrameworkProvider,
|
|
62
|
+
w as SsgoiLayoutBoundary
|
|
63
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { RouteBoundaryState as RootRouteBoundaryState } from '../route-boundary';
|
|
2
|
+
export { SsgoiRouteBoundary } from './ssgoi-route-boundary';
|
|
3
|
+
export type { SsgoiRouteBoundaryProps, NextjsRouteLocation, } from './ssgoi-route-boundary';
|
|
4
|
+
export { selectedSegmentsToPath } from './selected-segments-to-path';
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated Will be removed in 8.0.0. Use `SsgoiLayoutBoundary` from
|
|
7
|
+
* "@ssgoi/react" with `<Ssgoi framework={...}>` instead.
|
|
8
|
+
*/
|
|
9
|
+
export type RouteBoundaryState = RootRouteBoundaryState;
|
|
@@ -2,5 +2,8 @@
|
|
|
2
2
|
* Turn a layout's selected slot into a route id. Pass the owning layout's
|
|
3
3
|
* absolute base path for nested layouts. Interception markers are stripped;
|
|
4
4
|
* this does not reconstruct an intercepted destination's relative ancestry.
|
|
5
|
+
*
|
|
6
|
+
* @deprecated Will be removed in 8.0.0. Use `SsgoiLayoutBoundary` from
|
|
7
|
+
* "@ssgoi/react" with `<Ssgoi framework={...}>` instead.
|
|
5
8
|
*/
|
|
6
9
|
export declare function selectedSegmentsToPath(segments: readonly string[], basePath?: string): string;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("react/jsx-runtime"),n=require("next/navigation"),r=require("../route-boundary.cjs"),s=({parallelRoutesKey:e="children"})=>{const t=n.usePathname(),u=n.useSelectedLayoutSegments(e)??[];return t==null?null:{pathname:t,selectedSegments:u}};function a(e){return o.jsx(r.SuspendedRouteBoundary,{...e,useRoute:s})}exports.SsgoiRouteBoundary=a;
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { ElementType, ReactNode } from 'react';
|
|
2
2
|
import { RouteBoundaryProps } from '../route-boundary';
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Will be removed in 8.0.0. Use `SsgoiLayoutBoundary` from
|
|
5
|
+
* "@ssgoi/react" with `<Ssgoi framework={...}>` instead.
|
|
6
|
+
*/
|
|
3
7
|
export interface NextjsRouteLocation {
|
|
4
8
|
pathname: string;
|
|
5
9
|
/** Segments below the layout that renders this boundary, including groups. */
|
|
6
10
|
selectedSegments: string[];
|
|
7
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated Will be removed in 8.0.0. Use `SsgoiLayoutBoundary` from
|
|
14
|
+
* "@ssgoi/react" with `<Ssgoi framework={...}>` instead.
|
|
15
|
+
*/
|
|
8
16
|
export type SsgoiRouteBoundaryProps<T extends ElementType = "div"> = RouteBoundaryProps<NextjsRouteLocation, T> & {
|
|
9
17
|
/** The parallel slot to read, without @. Defaults to children. */
|
|
10
18
|
parallelRoutesKey?: string;
|
|
11
19
|
/** Shown while Next.js resolves URL data (including Cache Components). */
|
|
12
20
|
fallback?: ReactNode;
|
|
13
21
|
};
|
|
14
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* App Router boundary. Import only from @ssgoi/react/nextjs.
|
|
24
|
+
*
|
|
25
|
+
* @deprecated Will be removed in 8.0.0. Use `SsgoiLayoutBoundary` from
|
|
26
|
+
* "@ssgoi/react" with `<Ssgoi framework={...}>` instead.
|
|
27
|
+
*/
|
|
15
28
|
export declare function SsgoiRouteBoundary<T extends ElementType = "div">(props: SsgoiRouteBoundaryProps<T>): import("react/jsx-runtime").JSX.Element;
|