@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.
Files changed (61) hide show
  1. package/README.md +166 -144
  2. package/dist/context.cjs +2 -0
  3. package/dist/context.d.ts +16 -0
  4. package/dist/context.js +18 -0
  5. package/dist/index.cjs +1 -1
  6. package/dist/index.d.ts +4 -1
  7. package/dist/index.js +8 -4
  8. package/dist/layout-boundary.cjs +2 -0
  9. package/dist/layout-boundary.d.ts +35 -0
  10. package/dist/layout-boundary.js +63 -0
  11. package/dist/legacy/nextjs/index.d.ts +9 -0
  12. package/dist/{routers → legacy}/nextjs/selected-segments-to-path.d.ts +3 -0
  13. package/dist/legacy/nextjs/ssgoi-route-boundary.cjs +2 -0
  14. package/dist/{routers → legacy}/nextjs/ssgoi-route-boundary.d.ts +14 -1
  15. package/dist/legacy/nextjs/ssgoi-route-boundary.js +14 -0
  16. package/dist/legacy/react-router.d.ts +20 -0
  17. package/dist/legacy/remix.d.ts +25 -0
  18. package/dist/legacy/route-boundary.cjs +2 -0
  19. package/dist/legacy/route-boundary.d.ts +33 -0
  20. package/dist/legacy/route-boundary.js +33 -0
  21. package/dist/{ssgoi-transition.d.ts → legacy/ssgoi-transition.d.ts} +1 -2
  22. package/dist/legacy/tanstack-router.d.ts +19 -0
  23. package/dist/legacy/unplugin/esbuild.d.ts +3 -0
  24. package/dist/legacy/unplugin/index.d.ts +17 -0
  25. package/dist/legacy/unplugin/rollup.d.ts +3 -0
  26. package/dist/legacy/unplugin/vite.d.ts +3 -0
  27. package/dist/legacy/unplugin/webpack.d.ts +3 -0
  28. package/dist/nextjs.cjs +1 -1
  29. package/dist/nextjs.d.ts +1 -1
  30. package/dist/nextjs.js +2 -2
  31. package/dist/react-router.cjs +1 -1
  32. package/dist/react-router.d.ts +1 -1
  33. package/dist/react-router.js +7 -10
  34. package/dist/remix.cjs +1 -1
  35. package/dist/remix.d.ts +1 -1
  36. package/dist/remix.js +7 -10
  37. package/dist/ssgoi.cjs +1 -1
  38. package/dist/ssgoi.d.ts +6 -0
  39. package/dist/ssgoi.js +23 -14
  40. package/dist/tanstack-router.cjs +1 -1
  41. package/dist/tanstack-router.d.ts +1 -1
  42. package/dist/tanstack-router.js +13 -12
  43. package/dist/unplugin/esbuild.d.ts +4 -2
  44. package/dist/unplugin/index.d.ts +6 -14
  45. package/dist/unplugin/rollup.d.ts +4 -2
  46. package/dist/unplugin/vite.d.ts +4 -2
  47. package/dist/unplugin/webpack.d.ts +4 -2
  48. package/package.json +34 -22
  49. package/dist/routers/nextjs/index.d.ts +0 -4
  50. package/dist/routers/nextjs/ssgoi-route-boundary.cjs +0 -2
  51. package/dist/routers/nextjs/ssgoi-route-boundary.js +0 -24
  52. package/dist/routers/react-router.d.ts +0 -7
  53. package/dist/routers/remix.d.ts +0 -8
  54. package/dist/routers/route-boundary.cjs +0 -2
  55. package/dist/routers/route-boundary.d.ts +0 -22
  56. package/dist/routers/route-boundary.js +0 -22
  57. package/dist/routers/tanstack-router.d.ts +0 -6
  58. /package/dist/{routers → legacy}/nextjs/selected-segments-to-path.cjs +0 -0
  59. /package/dist/{routers → legacy}/nextjs/selected-segments-to-path.js +0 -0
  60. /package/dist/{ssgoi-transition.cjs → legacy/ssgoi-transition.cjs} +0 -0
  61. /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
- React bindings for SSGOI.
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
  [![SSGOI live showcase](https://ssgoi.dev/readme.png)](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 helpers
15
+ ## Router integration
16
16
 
17
- Install this framework package once; router helpers are optional subpaths.
18
- Next.js is the only helper not marked experimental. The other router helpers are experimental APIs.
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 | Import | API |
21
- | ----------------------- | ------------------------------ | -------------------- |
22
- | Next.js | `@ssgoi/react/nextjs` | `SsgoiRouteBoundary` |
23
- | Remix 2 | `@ssgoi/react/remix` | `SsgoiRouteBoundary` |
24
- | React Router | `@ssgoi/react/react-router` | `SsgoiRouteBoundary` |
25
- | TanStack Router / Start | `@ssgoi/react/tanstack-router` | `SsgoiRouteBoundary` |
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 { Ssgoi } from "@ssgoi/react";
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 <Ssgoi config={config}>{children}</Ssgoi>;
112
+ return (
113
+ <Ssgoi config={config} framework={framework}>
114
+ {children}
115
+ </Ssgoi>
116
+ );
73
117
  }
74
118
  ```
75
119
 
76
- Build the layout shell next:
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>{children}</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={boundary.key} data-ssgoi-transition={boundary.id}>
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
- The legacy `<SsgoiTransition>` wrapper only added this attribute and is
117
- deprecated. Set `data-ssgoi-transition` directly on the keyed application
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
- type BoundaryName = "app-shell" | "main-content" | "project-content";
132
- type BoundaryIdentity = { id: string; key: string };
133
-
134
- const INTERCEPTION_PREFIX = /^(?:\(\.\.\.\)|\(\.\.\)|\(\.\))+/;
135
-
136
- function isRouteGroup(segment: string) {
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
- function resolveBoundary(
151
- name: BoundaryName,
152
- pathname: string,
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
- Place one named boundary in the common app layout, then add child boundaries
204
- only around routed content that changes while its parent remains mounted.
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
- ```tsx
207
- // app/layout.tsx — replace the provider line from the shell example.
208
- <SsgoiProvider>
209
- <SsgoiRouteBoundary name="app-shell">{children}</SsgoiRouteBoundary>
210
- </SsgoiProvider>
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/(main)/layout.tsx
215
- export default function MainLayout({ children }) {
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
- <SsgoiRouteBoundary name="main-content">{children}</SsgoiRouteBoundary>
213
+ <SsgoiLayoutBoundary>{children}</SsgoiLayoutBoundary>
219
214
  <BottomNav />
220
215
  </>
221
216
  );
222
217
  }
223
218
  ```
224
219
 
225
- - Main → main: only the inner content boundary changes; the nav stays.
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
- ```text
234
- app/
235
- layout.tsx # one <Ssgoi> + one named app-shell boundary
236
- (main)/layout.tsx
237
- (main)/page.tsx
238
- (main)/search/page.tsx
239
- (detail)/post/[id]/page.tsx
240
- ```
241
-
242
- For Next.js parallel and intercepting routes, the browser pathname may point at
243
- a modal while the background `children` slot has not changed. Resolve that
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
- Replace `usePathname()` with `useLocation()` and render the boundary in a
254
- nested layout route:
238
+ Render `<Ssgoi>` inside the router and the boundary in a layout route:
255
239
 
256
240
  ```tsx
257
- const { pathname } = useLocation();
241
+ import { Outlet, useLocation } from "react-router"; // Remix 2: "@remix-run/react"
242
+ import { Ssgoi, SsgoiLayoutBoundary, type SsgoiFramework } from "@ssgoi/react";
258
243
 
259
- return (
260
- <div key={pathname} data-ssgoi-transition={pathname}>
261
- <Outlet />
262
- </div>
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
- Read the pathname from router state and place the boundary in a parent route:
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
- const pathname = useRouterState({
272
- select: (state) => state.location.pathname,
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()` and the auto-key plugins remain available for element-level
328
- mount/unmount animations; they are separate from route boundaries.
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
 
@@ -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;
@@ -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 t=require("@ssgoi/core/types"),o=require("./ssgoi.cjs"),i=require("./ssgoi-transition.cjs"),r=require("@ssgoi/core");exports.Ssgoi=o.Ssgoi;exports.SsgoiTransition=i.SsgoiTransition;Object.keys(t).forEach(e=>{e!=="default"&&!Object.prototype.hasOwnProperty.call(exports,e)&&Object.defineProperty(exports,e,{enumerable:!0,get:()=>t[e]})});Object.keys(r).forEach(e=>{e!=="default"&&!Object.prototype.hasOwnProperty.call(exports,e)&&Object.defineProperty(exports,e,{enumerable:!0,get:()=>r[e]})});
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 { SsgoiTransition } from './ssgoi-transition';
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 e } from "./ssgoi.js";
3
- import { SsgoiTransition as i } from "./ssgoi-transition.js";
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
- e as Ssgoi,
7
- i as SsgoiTransition
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
- /** App Router boundary. Import only from @ssgoi/react/nextjs. */
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;