@astrale-os/shell-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/dist/application/apps/app-host.d.ts +31 -0
- package/dist/application/apps/app-host.d.ts.map +1 -0
- package/dist/application/apps/app-host.js +59 -0
- package/dist/application/apps/app-logo.d.ts +19 -0
- package/dist/application/apps/app-logo.d.ts.map +1 -0
- package/dist/application/apps/app-logo.js +31 -0
- package/dist/application/apps/apps.hook.d.ts +12 -0
- package/dist/application/apps/apps.hook.d.ts.map +1 -0
- package/dist/application/apps/apps.hook.js +51 -0
- package/dist/application/apps/open-app.hook.d.ts +10 -0
- package/dist/application/apps/open-app.hook.d.ts.map +1 -0
- package/dist/application/apps/open-app.hook.js +7 -0
- package/dist/application/index.d.ts +18 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +12 -0
- package/dist/application/roles.hook.d.ts +7 -0
- package/dist/application/roles.hook.d.ts.map +1 -0
- package/dist/application/roles.hook.js +23 -0
- package/dist/application/spaces/current-space.hook.d.ts +7 -0
- package/dist/application/spaces/current-space.hook.d.ts.map +1 -0
- package/dist/application/spaces/current-space.hook.js +27 -0
- package/dist/application/spaces/current-space.store.d.ts +32 -0
- package/dist/application/spaces/current-space.store.d.ts.map +1 -0
- package/dist/application/spaces/current-space.store.js +79 -0
- package/dist/application/spaces/space.hook.d.ts +5 -0
- package/dist/application/spaces/space.hook.d.ts.map +1 -0
- package/dist/application/spaces/space.hook.js +25 -0
- package/dist/application/spaces/spaces.hook.d.ts +14 -0
- package/dist/application/spaces/spaces.hook.d.ts.map +1 -0
- package/dist/application/spaces/spaces.hook.js +82 -0
- package/dist/application/user/user.hook.d.ts +10 -0
- package/dist/application/user/user.hook.d.ts.map +1 -0
- package/dist/application/user/user.hook.js +19 -0
- package/dist/application/user/user.store.d.ts +35 -0
- package/dist/application/user/user.store.d.ts.map +1 -0
- package/dist/application/user/user.store.js +51 -0
- package/dist/auth/act-as.context.d.ts +40 -0
- package/dist/auth/act-as.context.d.ts.map +1 -0
- package/dist/auth/act-as.context.js +84 -0
- package/dist/auth/auth.hook.d.ts +3 -0
- package/dist/auth/auth.hook.d.ts.map +1 -0
- package/dist/auth/auth.hook.js +25 -0
- package/dist/auth/can.hook.d.ts +40 -0
- package/dist/auth/can.hook.d.ts.map +1 -0
- package/dist/auth/can.hook.js +119 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/self.hook.d.ts +3 -0
- package/dist/auth/self.hook.d.ts.map +1 -0
- package/dist/auth/self.hook.js +11 -0
- package/dist/auth/session-kernel.d.ts +4 -0
- package/dist/auth/session-kernel.d.ts.map +1 -0
- package/dist/auth/session-kernel.js +21 -0
- package/dist/graph/action.d.ts +22 -0
- package/dist/graph/action.d.ts.map +1 -0
- package/dist/graph/action.js +85 -0
- package/dist/graph/async.d.ts +11 -0
- package/dist/graph/async.d.ts.map +1 -0
- package/dist/graph/async.js +60 -0
- package/dist/graph/call/call.hook.d.ts +21 -0
- package/dist/graph/call/call.hook.d.ts.map +1 -0
- package/dist/graph/call/call.hook.js +23 -0
- package/dist/graph/call/stream.hook.d.ts +28 -0
- package/dist/graph/call/stream.hook.d.ts.map +1 -0
- package/dist/graph/call/stream.hook.js +137 -0
- package/dist/graph/graph.context.d.ts +4 -0
- package/dist/graph/graph.context.d.ts.map +1 -0
- package/dist/graph/graph.context.js +10 -0
- package/dist/graph/guards.error.d.ts +9 -0
- package/dist/graph/guards.error.d.ts.map +1 -0
- package/dist/graph/guards.error.js +7 -0
- package/dist/graph/index.d.ts +20 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +9 -0
- package/dist/graph/memory/base.d.ts +21 -0
- package/dist/graph/memory/base.d.ts.map +1 -0
- package/dist/graph/memory/base.js +33 -0
- package/dist/graph/memory/read/envelope.d.ts +71 -0
- package/dist/graph/memory/read/envelope.d.ts.map +1 -0
- package/dist/graph/memory/read/envelope.js +29 -0
- package/dist/graph/memory/read/idle.d.ts +18 -0
- package/dist/graph/memory/read/idle.d.ts.map +1 -0
- package/dist/graph/memory/read/idle.js +16 -0
- package/dist/graph/memory/read/read.d.ts +9 -0
- package/dist/graph/memory/read/read.d.ts.map +1 -0
- package/dist/graph/memory/read/read.js +87 -0
- package/dist/graph/memory/scope.d.ts +7 -0
- package/dist/graph/memory/scope.d.ts.map +1 -0
- package/dist/graph/memory/scope.js +25 -0
- package/dist/graph/mutate/move.hook.d.ts +7 -0
- package/dist/graph/mutate/move.hook.d.ts.map +1 -0
- package/dist/graph/mutate/move.hook.js +49 -0
- package/dist/graph/mutate/mutate.hook.d.ts +17 -0
- package/dist/graph/mutate/mutate.hook.d.ts.map +1 -0
- package/dist/graph/mutate/mutate.hook.js +45 -0
- package/dist/graph/query/children.hook.d.ts +29 -0
- package/dist/graph/query/children.hook.d.ts.map +1 -0
- package/dist/graph/query/children.hook.js +64 -0
- package/dist/graph/query/graph.hook.d.ts +9 -0
- package/dist/graph/query/graph.hook.d.ts.map +1 -0
- package/dist/graph/query/graph.hook.js +5 -0
- package/dist/graph/query/neighbors.hook.d.ts +7 -0
- package/dist/graph/query/neighbors.hook.d.ts.map +1 -0
- package/dist/graph/query/neighbors.hook.js +79 -0
- package/dist/graph/query/node.hook.d.ts +14 -0
- package/dist/graph/query/node.hook.d.ts.map +1 -0
- package/dist/graph/query/node.hook.js +43 -0
- package/dist/graph/query/query.hook.d.ts +14 -0
- package/dist/graph/query/query.hook.d.ts.map +1 -0
- package/dist/graph/query/query.hook.js +23 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/intents/envelope.d.ts +33 -0
- package/dist/intents/envelope.d.ts.map +1 -0
- package/dist/intents/envelope.js +19 -0
- package/dist/intents/index.d.ts +6 -0
- package/dist/intents/index.d.ts.map +1 -0
- package/dist/intents/index.js +3 -0
- package/dist/intents/listen-intent.hook.d.ts +16 -0
- package/dist/intents/listen-intent.hook.d.ts.map +1 -0
- package/dist/intents/listen-intent.hook.js +75 -0
- package/dist/intents/reply.d.ts +9 -0
- package/dist/intents/reply.d.ts.map +1 -0
- package/dist/intents/reply.js +14 -0
- package/dist/intents/send-intent.hook.d.ts +12 -0
- package/dist/intents/send-intent.hook.d.ts.map +1 -0
- package/dist/intents/send-intent.hook.js +86 -0
- package/dist/navigation/adapters/adapter.d.ts +15 -0
- package/dist/navigation/adapters/adapter.d.ts.map +1 -0
- package/dist/navigation/adapters/adapter.js +1 -0
- package/dist/navigation/adapters/memory.adapter.d.ts +9 -0
- package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -0
- package/dist/navigation/adapters/memory.adapter.js +15 -0
- package/dist/navigation/adapters/url.adapter.d.ts +6 -0
- package/dist/navigation/adapters/url.adapter.d.ts.map +1 -0
- package/dist/navigation/adapters/url.adapter.js +59 -0
- package/dist/navigation/history.d.ts +63 -0
- package/dist/navigation/history.d.ts.map +1 -0
- package/dist/navigation/history.js +144 -0
- package/dist/navigation/hooks/ancestors.hook.d.ts +13 -0
- package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/ancestors.hook.js +56 -0
- package/dist/navigation/hooks/location.hook.d.ts +4 -0
- package/dist/navigation/hooks/location.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/location.hook.js +17 -0
- package/dist/navigation/hooks/navigate.hook.d.ts +12 -0
- package/dist/navigation/hooks/navigate.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/navigate.hook.js +33 -0
- package/dist/navigation/hooks/scope.hook.d.ts +12 -0
- package/dist/navigation/hooks/scope.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/scope.hook.js +21 -0
- package/dist/navigation/index.d.ts +16 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +10 -0
- package/dist/navigation/link.d.ts +11 -0
- package/dist/navigation/link.d.ts.map +1 -0
- package/dist/navigation/link.js +62 -0
- package/dist/navigation/location.d.ts +48 -0
- package/dist/navigation/location.d.ts.map +1 -0
- package/dist/navigation/location.js +88 -0
- package/dist/navigation/scope.context.d.ts +64 -0
- package/dist/navigation/scope.context.d.ts.map +1 -0
- package/dist/navigation/scope.context.js +178 -0
- package/dist/navigation/snapshot.d.ts +22 -0
- package/dist/navigation/snapshot.d.ts.map +1 -0
- package/dist/navigation/snapshot.js +65 -0
- package/dist/schema/bind-read.d.ts +37 -0
- package/dist/schema/bind-read.d.ts.map +1 -0
- package/dist/schema/bind-read.js +205 -0
- package/dist/schema/binding.d.ts +21 -0
- package/dist/schema/binding.d.ts.map +1 -0
- package/dist/schema/binding.js +55 -0
- package/dist/schema/domain.hook.d.ts +10 -0
- package/dist/schema/domain.hook.d.ts.map +1 -0
- package/dist/schema/domain.hook.js +7 -0
- package/dist/schema/function.hook.d.ts +9 -0
- package/dist/schema/function.hook.d.ts.map +1 -0
- package/dist/schema/function.hook.js +40 -0
- package/dist/schema/index.d.ts +10 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +8 -0
- package/dist/schema/kernel.hook.d.ts +10 -0
- package/dist/schema/kernel.hook.d.ts.map +1 -0
- package/dist/schema/kernel.hook.js +8 -0
- package/dist/schema/method.hook.d.ts +14 -0
- package/dist/schema/method.hook.d.ts.map +1 -0
- package/dist/schema/method.hook.js +49 -0
- package/dist/schema/tag.d.ts +11 -0
- package/dist/schema/tag.d.ts.map +1 -0
- package/dist/schema/tag.js +11 -0
- package/dist/schema/types.d.ts +73 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +1 -0
- package/dist/schema/write.d.ts +15 -0
- package/dist/schema/write.d.ts.map +1 -0
- package/dist/schema/write.js +45 -0
- package/dist/selection/index.d.ts +4 -0
- package/dist/selection/index.d.ts.map +1 -0
- package/dist/selection/index.js +4 -0
- package/dist/selection/live.d.ts +21 -0
- package/dist/selection/live.d.ts.map +1 -0
- package/dist/selection/live.js +116 -0
- package/dist/selection/scope.context.d.ts +26 -0
- package/dist/selection/scope.context.d.ts.map +1 -0
- package/dist/selection/scope.context.js +100 -0
- package/dist/selection/selection.hook.d.ts +4 -0
- package/dist/selection/selection.hook.d.ts.map +1 -0
- package/dist/selection/selection.hook.js +14 -0
- package/dist/session/boot/boot.d.ts +19 -0
- package/dist/session/boot/boot.d.ts.map +1 -0
- package/dist/session/boot/boot.js +45 -0
- package/dist/session/boot/ownership.d.ts +18 -0
- package/dist/session/boot/ownership.d.ts.map +1 -0
- package/dist/session/boot/ownership.js +39 -0
- package/dist/session/boot/status.d.ts +24 -0
- package/dist/session/boot/status.d.ts.map +1 -0
- package/dist/session/boot/status.js +11 -0
- package/dist/session/connection/connection.hook.d.ts +3 -0
- package/dist/session/connection/connection.hook.d.ts.map +1 -0
- package/dist/session/connection/connection.hook.js +12 -0
- package/dist/session/connection/connection.store.d.ts +24 -0
- package/dist/session/connection/connection.store.d.ts.map +1 -0
- package/dist/session/connection/connection.store.js +31 -0
- package/dist/session/index.d.ts +8 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +6 -0
- package/dist/session/kernel-client.hook.d.ts +9 -0
- package/dist/session/kernel-client.hook.d.ts.map +1 -0
- package/dist/session/kernel-client.hook.js +5 -0
- package/dist/session/kernel-instance.hook.d.ts +9 -0
- package/dist/session/kernel-instance.hook.d.ts.map +1 -0
- package/dist/session/kernel-instance.hook.js +10 -0
- package/dist/session/provider.d.ts +31 -0
- package/dist/session/provider.d.ts.map +1 -0
- package/dist/session/provider.js +165 -0
- package/dist/session/session.context.d.ts +18 -0
- package/dist/session/session.context.d.ts.map +1 -0
- package/dist/session/session.context.js +8 -0
- package/dist/session/shell.hook.d.ts +4 -0
- package/dist/session/shell.hook.d.ts.map +1 -0
- package/dist/session/shell.hook.js +15 -0
- package/dist/window/adopt.d.ts +27 -0
- package/dist/window/adopt.d.ts.map +1 -0
- package/dist/window/adopt.js +27 -0
- package/dist/window/hooks/close-guard.hook.d.ts +6 -0
- package/dist/window/hooks/close-guard.hook.d.ts.map +1 -0
- package/dist/window/hooks/close-guard.hook.js +90 -0
- package/dist/window/hooks/target-node.hook.d.ts +7 -0
- package/dist/window/hooks/target-node.hook.d.ts.map +1 -0
- package/dist/window/hooks/target-node.hook.js +77 -0
- package/dist/window/hooks/views-for.hook.d.ts +8 -0
- package/dist/window/hooks/views-for.hook.d.ts.map +1 -0
- package/dist/window/hooks/views-for.hook.js +51 -0
- package/dist/window/hooks/window.hook.d.ts +4 -0
- package/dist/window/hooks/window.hook.d.ts.map +1 -0
- package/dist/window/hooks/window.hook.js +14 -0
- package/dist/window/hooks/windows.hook.d.ts +15 -0
- package/dist/window/hooks/windows.hook.d.ts.map +1 -0
- package/dist/window/hooks/windows.hook.js +25 -0
- package/dist/window/host.d.ts +31 -0
- package/dist/window/host.d.ts.map +1 -0
- package/dist/window/host.js +270 -0
- package/dist/window/hub.d.ts +41 -0
- package/dist/window/hub.d.ts.map +1 -0
- package/dist/window/hub.js +100 -0
- package/dist/window/index.d.ts +15 -0
- package/dist/window/index.d.ts.map +1 -0
- package/dist/window/index.js +10 -0
- package/dist/window/lifecycle/mount.d.ts +47 -0
- package/dist/window/lifecycle/mount.d.ts.map +1 -0
- package/dist/window/lifecycle/mount.js +92 -0
- package/dist/window/lifecycle/states.d.ts +33 -0
- package/dist/window/lifecycle/states.d.ts.map +1 -0
- package/dist/window/lifecycle/states.js +7 -0
- package/dist/window/placement.d.ts +33 -0
- package/dist/window/placement.d.ts.map +1 -0
- package/dist/window/placement.js +57 -0
- package/dist/window/stage.d.ts +3 -0
- package/dist/window/stage.d.ts.map +1 -0
- package/dist/window/stage.js +41 -0
- package/package.json +59 -0
- package/src/application/apps/app-host.tsx +113 -0
- package/src/application/apps/app-logo.tsx +56 -0
- package/src/application/apps/apps.hook.ts +81 -0
- package/src/application/apps/open-app.hook.ts +18 -0
- package/src/application/index.ts +22 -0
- package/src/application/roles.hook.ts +41 -0
- package/src/application/spaces/current-space.hook.ts +43 -0
- package/src/application/spaces/current-space.store.ts +110 -0
- package/src/application/spaces/space.hook.ts +41 -0
- package/src/application/spaces/spaces.hook.ts +113 -0
- package/src/application/user/user.hook.ts +27 -0
- package/src/application/user/user.store.ts +85 -0
- package/src/auth/act-as.context.tsx +124 -0
- package/src/auth/auth.hook.ts +36 -0
- package/src/auth/can.hook.ts +149 -0
- package/src/auth/index.ts +6 -0
- package/src/auth/self.hook.ts +14 -0
- package/src/auth/session-kernel.ts +24 -0
- package/src/graph/action.ts +127 -0
- package/src/graph/async.ts +87 -0
- package/src/graph/call/call.hook.ts +42 -0
- package/src/graph/call/stream.hook.ts +200 -0
- package/src/graph/graph.context.ts +14 -0
- package/src/graph/guards.error.ts +58 -0
- package/src/graph/index.ts +20 -0
- package/src/graph/memory/base.ts +50 -0
- package/src/graph/memory/read/envelope.ts +98 -0
- package/src/graph/memory/read/idle.ts +24 -0
- package/src/graph/memory/read/read.ts +103 -0
- package/src/graph/memory/scope.ts +29 -0
- package/src/graph/mutate/move.hook.ts +70 -0
- package/src/graph/mutate/mutate.hook.ts +69 -0
- package/src/graph/query/children.hook.ts +136 -0
- package/src/graph/query/graph.hook.ts +13 -0
- package/src/graph/query/neighbors.hook.ts +124 -0
- package/src/graph/query/node.hook.ts +83 -0
- package/src/graph/query/query.hook.ts +42 -0
- package/src/index.ts +202 -0
- package/src/intents/envelope.ts +52 -0
- package/src/intents/index.ts +6 -0
- package/src/intents/listen-intent.hook.ts +92 -0
- package/src/intents/reply.ts +24 -0
- package/src/intents/send-intent.hook.ts +111 -0
- package/src/navigation/adapters/adapter.ts +13 -0
- package/src/navigation/adapters/memory.adapter.ts +22 -0
- package/src/navigation/adapters/url.adapter.ts +61 -0
- package/src/navigation/history.ts +170 -0
- package/src/navigation/hooks/ancestors.hook.ts +71 -0
- package/src/navigation/hooks/location.hook.ts +21 -0
- package/src/navigation/hooks/navigate.hook.ts +43 -0
- package/src/navigation/hooks/scope.hook.ts +34 -0
- package/src/navigation/index.ts +17 -0
- package/src/navigation/link.tsx +110 -0
- package/src/navigation/location.ts +122 -0
- package/src/navigation/scope.context.tsx +267 -0
- package/src/navigation/snapshot.ts +79 -0
- package/src/schema/bind-read.ts +319 -0
- package/src/schema/binding.ts +91 -0
- package/src/schema/domain.hook.ts +16 -0
- package/src/schema/function.hook.ts +68 -0
- package/src/schema/index.ts +25 -0
- package/src/schema/kernel.hook.ts +17 -0
- package/src/schema/method.hook.ts +103 -0
- package/src/schema/tag.ts +19 -0
- package/src/schema/types.ts +150 -0
- package/src/schema/write.ts +80 -0
- package/src/selection/index.ts +5 -0
- package/src/selection/live.ts +142 -0
- package/src/selection/scope.context.tsx +151 -0
- package/src/selection/selection.hook.ts +18 -0
- package/src/session/boot/boot.ts +57 -0
- package/src/session/boot/ownership.ts +54 -0
- package/src/session/boot/status.ts +32 -0
- package/src/session/connection/connection.hook.ts +16 -0
- package/src/session/connection/connection.store.ts +58 -0
- package/src/session/index.ts +8 -0
- package/src/session/kernel-client.hook.ts +13 -0
- package/src/session/kernel-instance.hook.ts +22 -0
- package/src/session/provider.tsx +205 -0
- package/src/session/session.context.ts +27 -0
- package/src/session/shell.hook.ts +19 -0
- package/src/window/adopt.ts +57 -0
- package/src/window/hooks/close-guard.hook.ts +100 -0
- package/src/window/hooks/target-node.hook.ts +94 -0
- package/src/window/hooks/views-for.hook.ts +82 -0
- package/src/window/hooks/window.hook.ts +18 -0
- package/src/window/hooks/windows.hook.ts +40 -0
- package/src/window/host.tsx +347 -0
- package/src/window/hub.ts +156 -0
- package/src/window/index.ts +17 -0
- package/src/window/lifecycle/mount.ts +134 -0
- package/src/window/lifecycle/states.ts +23 -0
- package/src/window/placement.ts +84 -0
- package/src/window/stage.tsx +46 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { IntentMessage, IntentRegistry, Shell } from '@astrale-os/shell'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useSendIntent (API §11) — a stable typed sender. Each call attaches a
|
|
5
|
+
* `correlationId`, subscribes for the matching `intentReply` on the origin channel
|
|
6
|
+
* BEFORE sending, then resolves with the reply's result. The promise also resolves
|
|
7
|
+
* (`void`) if the target window closes or this hook unmounts — a pending reply
|
|
8
|
+
* never strands.
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
11
|
+
|
|
12
|
+
import type { IntentPayload, IntentReply } from './envelope'
|
|
13
|
+
|
|
14
|
+
import { useShell } from '../session/shell.hook'
|
|
15
|
+
import { INTENT_REPLY, NOOP, intentMessage } from './envelope'
|
|
16
|
+
|
|
17
|
+
/** The typed sender returned by {@link useSendIntent}. */
|
|
18
|
+
export interface SendIntent {
|
|
19
|
+
<K extends keyof IntentRegistry>(
|
|
20
|
+
name: K,
|
|
21
|
+
payload: IntentPayload<K>,
|
|
22
|
+
/** `to`: `'parent'` (reserved) or a child windowId. Default: the parent, if any. */
|
|
23
|
+
opts?: { to?: 'parent' | string },
|
|
24
|
+
): Promise<IntentReply<K> | void>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Target =
|
|
28
|
+
| { readonly kind: 'parent' }
|
|
29
|
+
| { readonly kind: 'child'; readonly windowId: string }
|
|
30
|
+
| { readonly kind: 'invalid'; readonly reason: string }
|
|
31
|
+
|
|
32
|
+
function resolveTarget(shell: Shell, to: string | undefined): Target {
|
|
33
|
+
if (to === undefined) {
|
|
34
|
+
return shell.parent !== null
|
|
35
|
+
? { kind: 'parent' }
|
|
36
|
+
: { kind: 'invalid', reason: 'no target — pass opts.to (a child windowId) from a root shell' }
|
|
37
|
+
}
|
|
38
|
+
if (to === 'parent') {
|
|
39
|
+
return shell.parent !== null
|
|
40
|
+
? { kind: 'parent' }
|
|
41
|
+
: { kind: 'invalid', reason: "opts.to 'parent' but this shell has no parent channel" }
|
|
42
|
+
}
|
|
43
|
+
return { kind: 'child', windowId: to }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useSendIntent(): SendIntent {
|
|
47
|
+
const shell = useShell()
|
|
48
|
+
// Unmount settlers for in-flight sends, so a pending reply resolves on teardown.
|
|
49
|
+
const pendingRef = useRef<Set<() => void>>(new Set())
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const pending = pendingRef.current
|
|
52
|
+
return () => {
|
|
53
|
+
for (const settle of pending) settle()
|
|
54
|
+
pending.clear()
|
|
55
|
+
}
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
return useCallback(
|
|
59
|
+
<K extends keyof IntentRegistry>(
|
|
60
|
+
name: K,
|
|
61
|
+
payload: IntentPayload<K>,
|
|
62
|
+
opts?: { to?: 'parent' | string },
|
|
63
|
+
): Promise<IntentReply<K> | void> => {
|
|
64
|
+
const target = resolveTarget(shell, opts?.to)
|
|
65
|
+
if (target.kind === 'invalid') {
|
|
66
|
+
return Promise.reject(new Error(`useSendIntent: ${target.reason}`))
|
|
67
|
+
}
|
|
68
|
+
const correlationId = crypto.randomUUID()
|
|
69
|
+
const message = intentMessage(name, payload, correlationId)
|
|
70
|
+
const pending = pendingRef.current
|
|
71
|
+
|
|
72
|
+
return new Promise<IntentReply<K> | void>((resolve) => {
|
|
73
|
+
let done = false
|
|
74
|
+
let offReply = NOOP
|
|
75
|
+
let offClose = NOOP
|
|
76
|
+
const onUnmount = (): void => settle(undefined)
|
|
77
|
+
function settle(value: IntentReply<K> | void): void {
|
|
78
|
+
if (done) return
|
|
79
|
+
done = true
|
|
80
|
+
offReply()
|
|
81
|
+
offClose()
|
|
82
|
+
pending.delete(onUnmount)
|
|
83
|
+
resolve(value)
|
|
84
|
+
}
|
|
85
|
+
const onReply = (m: IntentMessage): void => {
|
|
86
|
+
if (m.envelope.name !== INTENT_REPLY) return
|
|
87
|
+
const reply = m.envelope.payload as { correlationId: string; result: unknown }
|
|
88
|
+
if (reply.correlationId !== correlationId) return
|
|
89
|
+
settle(reply.result as IntentReply<K>)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pending.add(onUnmount)
|
|
93
|
+
if (target.kind === 'parent') {
|
|
94
|
+
const parent = shell.parent!
|
|
95
|
+
offReply = parent.on('intent', onReply)
|
|
96
|
+
parent.send(message)
|
|
97
|
+
} else {
|
|
98
|
+
const { windowId } = target
|
|
99
|
+
offReply = shell.children.on('intent', (wid, m) => {
|
|
100
|
+
if (wid === windowId) onReply(m)
|
|
101
|
+
})
|
|
102
|
+
offClose = shell.children.onClose((wid) => {
|
|
103
|
+
if (wid === windowId) settle(undefined)
|
|
104
|
+
})
|
|
105
|
+
shell.children.send(windowId, message)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
},
|
|
109
|
+
[shell],
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The nav persistence port (API §8.1). The pluggable seam a `NavScope` mirrors its
|
|
3
|
+
* location through. `href` is the URL-form probe `<Link>` needs: present → an
|
|
4
|
+
* `<a href>` (cmd/ctrl-click falls through); absent → `role="link"`.
|
|
5
|
+
*/
|
|
6
|
+
import type { SerializedLocation } from '../history'
|
|
7
|
+
|
|
8
|
+
export interface NavAdapter {
|
|
9
|
+
read(): SerializedLocation | null
|
|
10
|
+
write(loc: SerializedLocation, opts: { replace: boolean }): void
|
|
11
|
+
subscribe(onExternalChange: () => void): () => void
|
|
12
|
+
href?(loc: SerializedLocation): string
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memoryAdapter (API §8.1) — the default scope backing: holds one location in a
|
|
3
|
+
* closure (per-pane state, no URL form → Link renders `role="link"`).
|
|
4
|
+
*/
|
|
5
|
+
import type { SerializedLocation } from '../history'
|
|
6
|
+
import type { NavAdapter } from './adapter'
|
|
7
|
+
|
|
8
|
+
/** In-memory adapter — the default scope backing (per-pane state, no URL). */
|
|
9
|
+
export function memoryAdapter(initial?: SerializedLocation): NavAdapter {
|
|
10
|
+
let current: SerializedLocation | null = initial ?? null
|
|
11
|
+
const listeners = new Set<() => void>()
|
|
12
|
+
return {
|
|
13
|
+
read: () => current,
|
|
14
|
+
write: (loc) => {
|
|
15
|
+
current = loc
|
|
16
|
+
},
|
|
17
|
+
subscribe: (cb) => {
|
|
18
|
+
listeners.add(cb)
|
|
19
|
+
return () => listeners.delete(cb)
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* urlAdapter (API §8.1) — the History-API adapter: mirrors the location into the
|
|
3
|
+
* query string (URL form → `<a href>`), owning the nav query keys (`param`,
|
|
4
|
+
* `view`, the location's `params`).
|
|
5
|
+
*/
|
|
6
|
+
import type { SerializedLocation } from '../history'
|
|
7
|
+
import type { NavAdapter } from './adapter'
|
|
8
|
+
|
|
9
|
+
const VIEW_KEY = 'view'
|
|
10
|
+
|
|
11
|
+
/** History-API adapter — the location lives in the query string. */
|
|
12
|
+
export function urlAdapter(opts?: { param?: string }): NavAdapter {
|
|
13
|
+
const param = opts?.param ?? 'node'
|
|
14
|
+
const hasWindow = (): boolean => typeof window !== 'undefined'
|
|
15
|
+
|
|
16
|
+
const build = (loc: SerializedLocation): string => {
|
|
17
|
+
const url = new URL(window.location.href)
|
|
18
|
+
const sp = new URLSearchParams()
|
|
19
|
+
sp.set(param, loc.node)
|
|
20
|
+
if (loc.view !== undefined) sp.set(VIEW_KEY, loc.view)
|
|
21
|
+
for (const [k, v] of Object.entries(loc.params ?? {})) {
|
|
22
|
+
if (k !== param && k !== VIEW_KEY) sp.set(k, v)
|
|
23
|
+
}
|
|
24
|
+
url.search = sp.toString()
|
|
25
|
+
return `${url.pathname}${url.search}${url.hash}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
read() {
|
|
30
|
+
if (!hasWindow()) return null
|
|
31
|
+
const sp = new URL(window.location.href).searchParams
|
|
32
|
+
const node = sp.get(param)
|
|
33
|
+
if (node === null) return null
|
|
34
|
+
const view = sp.get(VIEW_KEY) ?? undefined
|
|
35
|
+
const params: Record<string, string> = {}
|
|
36
|
+
for (const [k, v] of sp) {
|
|
37
|
+
if (k !== param && k !== VIEW_KEY) params[k] = v
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
node,
|
|
41
|
+
...(view !== undefined ? { view } : {}),
|
|
42
|
+
...(Object.keys(params).length > 0 ? { params } : {}),
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
write(loc, { replace }) {
|
|
46
|
+
if (!hasWindow()) return
|
|
47
|
+
const href = build(loc)
|
|
48
|
+
if (replace) window.history.replaceState(null, '', href)
|
|
49
|
+
else window.history.pushState(null, '', href)
|
|
50
|
+
},
|
|
51
|
+
subscribe(cb) {
|
|
52
|
+
if (!hasWindow()) return () => undefined
|
|
53
|
+
const handler = (): void => cb()
|
|
54
|
+
window.addEventListener('popstate', handler)
|
|
55
|
+
return () => window.removeEventListener('popstate', handler)
|
|
56
|
+
},
|
|
57
|
+
href(loc) {
|
|
58
|
+
return hasWindow() ? build(loc) : loc.node
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-scope history model (API §8.2) — a framework-free entries+cursor stack
|
|
3
|
+
* with a version counter for `useSyncExternalStore`, its serialized entry shape
|
|
4
|
+
* (`SerializedLocation`/`NavSnapshot` + equality/clone), and the seed precedence.
|
|
5
|
+
* Source of truth for back/forward; the adapter is a projection sink around it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The serializable projection persisted in history / URLs / snapshots. */
|
|
9
|
+
export interface SerializedLocation {
|
|
10
|
+
node: string
|
|
11
|
+
view?: string
|
|
12
|
+
params?: Record<string, string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A versioned history snapshot — round-trips through `NavScope.restore`. */
|
|
16
|
+
export interface NavSnapshot {
|
|
17
|
+
v: 1
|
|
18
|
+
entries: SerializedLocation[]
|
|
19
|
+
cursor: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Structural equality of two serialized locations (node + view + params). */
|
|
23
|
+
export function sameLocation(a: SerializedLocation, b: SerializedLocation): boolean {
|
|
24
|
+
return a.node === b.node && a.view === b.view && sameParams(a.params, b.params)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sameParams(a?: Record<string, string>, b?: Record<string, string>): boolean {
|
|
28
|
+
if (a === b) return true
|
|
29
|
+
const ak = a ? Object.keys(a) : []
|
|
30
|
+
const bk = b ? Object.keys(b) : []
|
|
31
|
+
if (ak.length !== bk.length) return false
|
|
32
|
+
return ak.every((k) => a?.[k] === b?.[k])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A deep-enough clone for a serialized entry (params copied). */
|
|
36
|
+
export function cloneLocation(loc: SerializedLocation): SerializedLocation {
|
|
37
|
+
return {
|
|
38
|
+
node: loc.node,
|
|
39
|
+
...(loc.view !== undefined ? { view: loc.view } : {}),
|
|
40
|
+
...(loc.params !== undefined ? { params: { ...loc.params } } : {}),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class NavHistory {
|
|
45
|
+
private entries: SerializedLocation[]
|
|
46
|
+
private cursor: number
|
|
47
|
+
private version = 0
|
|
48
|
+
private readonly listeners = new Set<() => void>()
|
|
49
|
+
|
|
50
|
+
constructor(entries: SerializedLocation[], cursor: number) {
|
|
51
|
+
this.entries = entries
|
|
52
|
+
this.cursor = cursor
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
subscribe(listener: () => void): () => void {
|
|
56
|
+
this.listeners.add(listener)
|
|
57
|
+
return () => this.listeners.delete(listener)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getVersion(): number {
|
|
61
|
+
return this.version
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get current(): SerializedLocation | null {
|
|
65
|
+
return this.entries[this.cursor] ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get canBack(): boolean {
|
|
69
|
+
return this.cursor > 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get canForward(): boolean {
|
|
73
|
+
return this.cursor >= 0 && this.cursor < this.entries.length - 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
snapshot(): NavSnapshot {
|
|
77
|
+
return { v: 1, entries: this.entries.map(cloneLocation), cursor: this.cursor }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Push (or replace the current entry with) a location. No-op on same location. */
|
|
81
|
+
push(loc: SerializedLocation, replace: boolean): boolean {
|
|
82
|
+
const cur = this.current
|
|
83
|
+
if (cur !== null && sameLocation(cur, loc)) return false
|
|
84
|
+
if (replace && cur !== null) {
|
|
85
|
+
this.entries = [...this.entries]
|
|
86
|
+
this.entries[this.cursor] = loc
|
|
87
|
+
} else {
|
|
88
|
+
this.entries = [...this.entries.slice(0, this.cursor + 1), loc]
|
|
89
|
+
this.cursor = this.entries.length - 1
|
|
90
|
+
}
|
|
91
|
+
this.emit()
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
back(): boolean {
|
|
96
|
+
if (!this.canBack) return false
|
|
97
|
+
this.cursor--
|
|
98
|
+
this.emit()
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
forward(): boolean {
|
|
103
|
+
if (!this.canForward) return false
|
|
104
|
+
this.cursor++
|
|
105
|
+
this.emit()
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Reconcile an externally-observed location (browser back/forward, URL edit):
|
|
111
|
+
* step the cursor when it matches an adjacent entry, else push it fresh.
|
|
112
|
+
*/
|
|
113
|
+
syncExternal(loc: SerializedLocation): boolean {
|
|
114
|
+
const cur = this.current
|
|
115
|
+
if (cur !== null && sameLocation(cur, loc)) return false
|
|
116
|
+
const back = this.entries[this.cursor - 1]
|
|
117
|
+
if (back !== undefined && sameLocation(back, loc)) {
|
|
118
|
+
this.cursor--
|
|
119
|
+
this.emit()
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
const fwd = this.entries[this.cursor + 1]
|
|
123
|
+
if (fwd !== undefined && sameLocation(fwd, loc)) {
|
|
124
|
+
this.cursor++
|
|
125
|
+
this.emit()
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
return this.push(loc, false)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Replace the whole stack (restore revalidation prune). */
|
|
132
|
+
reset(entries: SerializedLocation[], cursor: number): void {
|
|
133
|
+
this.entries = entries
|
|
134
|
+
this.cursor = cursor
|
|
135
|
+
this.emit()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private emit(): void {
|
|
139
|
+
this.version++
|
|
140
|
+
for (const listener of this.listeners) listener()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Seed the initial entries+cursor from the three sources, in precedence order:
|
|
146
|
+
* a `restore` snapshot (revalidated later against the memory), then the
|
|
147
|
+
* adapter's current read, then `initial`; empty (idle) when none yields one.
|
|
148
|
+
* A live adapter read that disagrees with a restored cursor is appended as the
|
|
149
|
+
* current entry (a deep link overrides the saved position but keeps its back
|
|
150
|
+
* history).
|
|
151
|
+
*/
|
|
152
|
+
export function seedEntries(
|
|
153
|
+
fromAdapter: SerializedLocation | null,
|
|
154
|
+
restore: { entries: SerializedLocation[]; cursor: number } | undefined,
|
|
155
|
+
initialSerialized: SerializedLocation | null,
|
|
156
|
+
): { entries: SerializedLocation[]; cursor: number } {
|
|
157
|
+
if (restore !== undefined && restore.entries.length > 0) {
|
|
158
|
+
const entries = restore.entries.map(cloneLocation)
|
|
159
|
+
let cursor = Math.min(Math.max(restore.cursor, 0), entries.length - 1)
|
|
160
|
+
if (fromAdapter !== null && !sameLocation(fromAdapter, entries[cursor])) {
|
|
161
|
+
entries.splice(cursor + 1)
|
|
162
|
+
entries.push(cloneLocation(fromAdapter))
|
|
163
|
+
cursor = entries.length - 1
|
|
164
|
+
}
|
|
165
|
+
return { entries, cursor }
|
|
166
|
+
}
|
|
167
|
+
if (fromAdapter !== null) return { entries: [cloneLocation(fromAdapter)], cursor: 0 }
|
|
168
|
+
if (initialSerialized !== null) return { entries: [initialSerialized], cursor: 0 }
|
|
169
|
+
return { entries: [], cursor: -1 }
|
|
170
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { PathLike } from '@astrale-os/kernel-client/graph'
|
|
2
|
+
import type { LiveResult, ShapeInput } from '@astrale-os/kernel-client/store'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* useAncestors (API §8.2) — the live, inclusive root-first ancestor chain of a
|
|
6
|
+
* node (a move re-renders it). Defaults to the nearest scope's location node;
|
|
7
|
+
* passing a node/path makes it usable outside a scope. Drops any masked ancestor.
|
|
8
|
+
*/
|
|
9
|
+
import { useMemo } from 'react'
|
|
10
|
+
|
|
11
|
+
import type { ReadState } from '../../graph/memory/read/envelope'
|
|
12
|
+
import type { Node } from '../location'
|
|
13
|
+
|
|
14
|
+
import { idleReadState } from '../../graph/memory/read/idle'
|
|
15
|
+
import { useMemory } from '../../graph/memory/scope'
|
|
16
|
+
import { chainPaths, pathHint, pathOf } from '../location'
|
|
17
|
+
import { useHistoryVersion, useOptionalNavCtx, useMemoryShape } from '../scope.context'
|
|
18
|
+
import { serializeTarget } from '../snapshot'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The root-first ancestor chain (inclusive of the node itself), live: a move
|
|
22
|
+
* re-renders it. `node` defaults to the nearest scope's location node; passing a
|
|
23
|
+
* node/path makes it usable outside a scope. The chain drops any ancestor whose
|
|
24
|
+
* record is masked/absent.
|
|
25
|
+
*/
|
|
26
|
+
export function useAncestors(
|
|
27
|
+
node?: Node | PathLike,
|
|
28
|
+
): ReadState & { readonly chain: readonly Node[] } {
|
|
29
|
+
const memory = useMemory()
|
|
30
|
+
const ctx = useOptionalNavCtx()
|
|
31
|
+
useHistoryVersion(ctx?.history ?? null)
|
|
32
|
+
|
|
33
|
+
const baseRef =
|
|
34
|
+
node !== undefined ? serializeTarget(node).node : (ctx?.history.current?.node ?? null)
|
|
35
|
+
const baseResult = useMemoryShape(memory, baseRef)
|
|
36
|
+
const basePath = baseResult?.node ? pathOf(baseResult.node) : pathHint(baseRef ?? '')
|
|
37
|
+
|
|
38
|
+
const paths = useMemo(() => (basePath !== '' ? chainPaths(basePath) : EMPTY_PATHS), [basePath])
|
|
39
|
+
const shape = useMemo<ShapeInput | null>(
|
|
40
|
+
() => (basePath !== '' ? (q) => q.from(...chainPaths(basePath)) : null),
|
|
41
|
+
[basePath],
|
|
42
|
+
)
|
|
43
|
+
const chainResult = useMemoryShape(memory, shape)
|
|
44
|
+
const chain = useMemo<readonly Node[]>(() => {
|
|
45
|
+
if (chainResult === null) return EMPTY_CHAIN
|
|
46
|
+
const byPath = new Map<string, Node>()
|
|
47
|
+
for (const n of chainResult.roots) byPath.set(pathOf(n), n)
|
|
48
|
+
const out: Node[] = []
|
|
49
|
+
for (const p of paths) {
|
|
50
|
+
const hit = byPath.get(p)
|
|
51
|
+
if (hit !== undefined) out.push(hit)
|
|
52
|
+
}
|
|
53
|
+
return out
|
|
54
|
+
}, [paths, chainResult])
|
|
55
|
+
|
|
56
|
+
return { ...readStateOf(chainResult), chain }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const EMPTY_PATHS: readonly string[] = Object.freeze([])
|
|
60
|
+
const EMPTY_CHAIN: readonly Node[] = Object.freeze([])
|
|
61
|
+
|
|
62
|
+
function readStateOf(result: LiveResult | null): ReadState {
|
|
63
|
+
if (result === null) return idleReadState()
|
|
64
|
+
return {
|
|
65
|
+
pending: result.pending,
|
|
66
|
+
error: result.error,
|
|
67
|
+
live: result.live,
|
|
68
|
+
seq: result.seq,
|
|
69
|
+
refetch: result.refetch,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLocation (API §8.2) — the nearest scope's live, id-anchored location (live
|
|
3
|
+
* path/node, missing flag). Throws outside any {@link NavScope}.
|
|
4
|
+
*/
|
|
5
|
+
import { useMemo } from 'react'
|
|
6
|
+
|
|
7
|
+
import type { NavLocation } from '../location'
|
|
8
|
+
|
|
9
|
+
import { buildLocation, useSettled } from '../location'
|
|
10
|
+
import { useHistoryVersion, useNavCtx, useMemoryShape } from '../scope.context'
|
|
11
|
+
|
|
12
|
+
/** The nearest scope's live location. Throws outside any {@link NavScope}. */
|
|
13
|
+
export function useLocation(): NavLocation {
|
|
14
|
+
const ctx = useNavCtx()
|
|
15
|
+
useHistoryVersion(ctx.history)
|
|
16
|
+
const entry = ctx.history.current
|
|
17
|
+
const shape = entry?.node ?? null
|
|
18
|
+
const result = useMemoryShape(ctx.memory, shape)
|
|
19
|
+
const settled = useSettled(result, shape)
|
|
20
|
+
return useMemo(() => buildLocation(entry, result, settled), [entry, result, settled])
|
|
21
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useNavigate (API §8.2) — the nearest scope's imperative navigation surface
|
|
3
|
+
* (back/forward/up + live canBack/canForward, wrapped in transitions). A
|
|
4
|
+
* per-consumer handle that delegates to the scope's ONE stable callable.
|
|
5
|
+
*/
|
|
6
|
+
import { useMemo } from 'react'
|
|
7
|
+
|
|
8
|
+
import type { MutableNavigate, NavCtx, Navigate } from '../scope.context'
|
|
9
|
+
|
|
10
|
+
import { useHistoryVersion, useNavCtx } from '../scope.context'
|
|
11
|
+
|
|
12
|
+
/** The nearest scope's imperative navigation surface. Throws outside a scope. */
|
|
13
|
+
export function useNavigate(): Navigate {
|
|
14
|
+
return useNavigateHandle(useNavCtx())
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A per-consumer navigate handle with fresh `canBack`/`canForward`, delegating to
|
|
19
|
+
* the scope's ONE stable callable. Render-pure: it never mutates the shared
|
|
20
|
+
* callable during render (a concurrent render could otherwise read flags from a
|
|
21
|
+
* newer history version than its own snapshot). The wrapper's identity changes
|
|
22
|
+
* only when navigation availability changes.
|
|
23
|
+
*/
|
|
24
|
+
export function useNavigateHandle(ctx: NavCtx): Navigate {
|
|
25
|
+
useHistoryVersion(ctx.history)
|
|
26
|
+
const canBack = ctx.history.canBack
|
|
27
|
+
const canForward = ctx.history.canForward
|
|
28
|
+
return useMemo(
|
|
29
|
+
() => wrapNavigate(ctx.navigate, canBack, canForward),
|
|
30
|
+
[ctx.navigate, canBack, canForward],
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function wrapNavigate(base: MutableNavigate, canBack: boolean, canForward: boolean): Navigate {
|
|
35
|
+
const fn = ((to: Parameters<Navigate>[0], opts?: Parameters<Navigate>[1]) =>
|
|
36
|
+
base(to, opts)) as MutableNavigate
|
|
37
|
+
fn.back = base.back
|
|
38
|
+
fn.forward = base.forward
|
|
39
|
+
fn.up = base.up
|
|
40
|
+
fn.canBack = canBack
|
|
41
|
+
fn.canForward = canForward
|
|
42
|
+
return fn
|
|
43
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useNavScope (API §8.2) — the nearest scope, for chrome/persistence: the live
|
|
3
|
+
* location, a `snapshot()` handle, and the imperative navigate. Throws outside a
|
|
4
|
+
* scope.
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useMemo } from 'react'
|
|
7
|
+
|
|
8
|
+
import type { NavSnapshot } from '../history'
|
|
9
|
+
import type { NavLocation } from '../location'
|
|
10
|
+
import type { Navigate } from '../scope.context'
|
|
11
|
+
|
|
12
|
+
import { buildLocation, useSettled } from '../location'
|
|
13
|
+
import { useNavCtx, useMemoryShape } from '../scope.context'
|
|
14
|
+
import { useNavigateHandle } from './navigate.hook'
|
|
15
|
+
|
|
16
|
+
/** The nearest scope, for chrome/persistence (API §8.2). */
|
|
17
|
+
export interface NavScopeHandle {
|
|
18
|
+
readonly location: NavLocation
|
|
19
|
+
snapshot(): NavSnapshot
|
|
20
|
+
navigate: Navigate
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The nearest scope, for chrome/persistence (API §8.2). Throws outside a scope. */
|
|
24
|
+
export function useNavScope(): NavScopeHandle {
|
|
25
|
+
const ctx = useNavCtx()
|
|
26
|
+
const navigate = useNavigateHandle(ctx)
|
|
27
|
+
const entry = ctx.history.current
|
|
28
|
+
const shape = entry?.node ?? null
|
|
29
|
+
const result = useMemoryShape(ctx.memory, shape)
|
|
30
|
+
const settled = useSettled(result, shape)
|
|
31
|
+
const location = useMemo(() => buildLocation(entry, result, settled), [entry, result, settled])
|
|
32
|
+
const snapshot = useCallback(() => ctx.history.snapshot(), [ctx.history])
|
|
33
|
+
return { location, snapshot, navigate }
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// navigation — API §8 (NavScope, useLocation, useNavigate, Link, useAncestors,
|
|
2
|
+
// useNavScope, adapters). See SHELL_REACT_API.md.
|
|
3
|
+
export { NavScope } from './scope.context'
|
|
4
|
+
export type { MissingPolicy, NavScopeProps, Navigate } from './scope.context'
|
|
5
|
+
export { useLocation } from './hooks/location.hook'
|
|
6
|
+
export { useNavigate } from './hooks/navigate.hook'
|
|
7
|
+
export { useAncestors } from './hooks/ancestors.hook'
|
|
8
|
+
export { useNavScope } from './hooks/scope.hook'
|
|
9
|
+
export type { NavScopeHandle } from './hooks/scope.hook'
|
|
10
|
+
export { Link } from './link'
|
|
11
|
+
export type { LinkProps } from './link'
|
|
12
|
+
export { memoryAdapter } from './adapters/memory.adapter'
|
|
13
|
+
export { urlAdapter } from './adapters/url.adapter'
|
|
14
|
+
export type { NavAdapter } from './adapters/adapter'
|
|
15
|
+
export type { NavLocation } from './location'
|
|
16
|
+
export type { NavSnapshot, SerializedLocation } from './history'
|
|
17
|
+
export type { NavTarget } from './snapshot'
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnchorHTMLAttributes,
|
|
3
|
+
FocusEvent,
|
|
4
|
+
KeyboardEvent,
|
|
5
|
+
MouseEvent,
|
|
6
|
+
PointerEvent,
|
|
7
|
+
ReactElement,
|
|
8
|
+
ReactNode,
|
|
9
|
+
} from 'react'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Link (API §8.2) — navigates the nearest scope. Renders `<a href>` when the
|
|
13
|
+
* scope's adapter exposes a URL form (cmd/ctrl/middle-click then falls through
|
|
14
|
+
* to the browser), else an href-less `<a role="link">` with identical behavior.
|
|
15
|
+
* Default prefetch `'intent'` warms the target shape on hover/pointer-down/focus.
|
|
16
|
+
*/
|
|
17
|
+
import { useCallback, useEffect } from 'react'
|
|
18
|
+
|
|
19
|
+
import type { NavTarget } from './snapshot'
|
|
20
|
+
|
|
21
|
+
import { useNavCtx } from './scope.context'
|
|
22
|
+
import { serializeTarget } from './snapshot'
|
|
23
|
+
|
|
24
|
+
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
25
|
+
to: NavTarget
|
|
26
|
+
replace?: boolean
|
|
27
|
+
/** Warm the target shape ahead of navigation. Default `'intent'`. */
|
|
28
|
+
prefetch?: 'intent' | 'render' | false
|
|
29
|
+
children: ReactNode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function Link(props: LinkProps): ReactElement {
|
|
33
|
+
const {
|
|
34
|
+
to,
|
|
35
|
+
replace,
|
|
36
|
+
prefetch = 'intent',
|
|
37
|
+
children,
|
|
38
|
+
onClick,
|
|
39
|
+
onPointerEnter,
|
|
40
|
+
onPointerDown,
|
|
41
|
+
onFocus,
|
|
42
|
+
onKeyDown,
|
|
43
|
+
...rest
|
|
44
|
+
} = props
|
|
45
|
+
const ctx = useNavCtx()
|
|
46
|
+
const target = serializeTarget(to)
|
|
47
|
+
const href = ctx.adapter.href?.(target)
|
|
48
|
+
const nodeRef = target.node
|
|
49
|
+
|
|
50
|
+
const preload = useCallback(() => {
|
|
51
|
+
void ctx.memory?.preload(nodeRef)
|
|
52
|
+
}, [ctx.memory, nodeRef])
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (prefetch === 'render') preload()
|
|
56
|
+
}, [prefetch, preload])
|
|
57
|
+
|
|
58
|
+
const intent = useCallback(() => {
|
|
59
|
+
if (prefetch === 'intent') preload()
|
|
60
|
+
}, [prefetch, preload])
|
|
61
|
+
|
|
62
|
+
const go = useCallback(() => ctx.navigate(to, { replace }), [ctx, to, replace])
|
|
63
|
+
|
|
64
|
+
const handleClick = (e: MouseEvent<HTMLAnchorElement>): void => {
|
|
65
|
+
onClick?.(e)
|
|
66
|
+
if (e.defaultPrevented) return
|
|
67
|
+
const modified = e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0
|
|
68
|
+
if (href !== undefined && modified) return // let the browser open it (new tab / window)
|
|
69
|
+
e.preventDefault()
|
|
70
|
+
go()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLAnchorElement>): void => {
|
|
74
|
+
onKeyDown?.(e)
|
|
75
|
+
if (e.defaultPrevented || href !== undefined) return // a real anchor handles Enter itself
|
|
76
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
77
|
+
e.preventDefault()
|
|
78
|
+
go()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const handlePointerEnter = (e: PointerEvent<HTMLAnchorElement>): void => {
|
|
83
|
+
onPointerEnter?.(e)
|
|
84
|
+
intent()
|
|
85
|
+
}
|
|
86
|
+
const handlePointerDown = (e: PointerEvent<HTMLAnchorElement>): void => {
|
|
87
|
+
onPointerDown?.(e)
|
|
88
|
+
intent()
|
|
89
|
+
}
|
|
90
|
+
const handleFocus = (e: FocusEvent<HTMLAnchorElement>): void => {
|
|
91
|
+
onFocus?.(e)
|
|
92
|
+
intent()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const linkRole = href !== undefined ? { href } : { role: 'link', tabIndex: rest.tabIndex ?? 0 }
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<a
|
|
99
|
+
{...rest}
|
|
100
|
+
{...linkRole}
|
|
101
|
+
onClick={handleClick}
|
|
102
|
+
onKeyDown={handleKeyDown}
|
|
103
|
+
onPointerEnter={handlePointerEnter}
|
|
104
|
+
onPointerDown={handlePointerDown}
|
|
105
|
+
onFocus={handleFocus}
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</a>
|
|
109
|
+
)
|
|
110
|
+
}
|