@astrale-os/shell-react 0.2.9 → 0.2.10-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/apps/app-host.d.ts +4 -15
- package/dist/application/apps/app-host.d.ts.map +1 -1
- package/dist/application/apps/app-host.js +1 -1
- package/dist/application/apps/apps.hook.d.ts +7 -4
- package/dist/application/apps/apps.hook.d.ts.map +1 -1
- package/dist/application/apps/apps.hook.js +10 -2
- package/dist/application/apps/domains.hook.d.ts +1 -1
- package/dist/application/apps/domains.hook.d.ts.map +1 -1
- package/dist/application/apps/domains.hook.js +1 -1
- package/dist/application/apps/open-app.hook.d.ts +2 -3
- package/dist/application/apps/open-app.hook.d.ts.map +1 -1
- package/dist/application/index.d.ts +2 -4
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +2 -3
- package/dist/application/spaces/space.hook.d.ts +2 -2
- package/dist/application/spaces/space.hook.d.ts.map +1 -1
- package/dist/application/spaces/space.hook.js +2 -2
- package/dist/application/spaces/spaces.hook.d.ts +2 -3
- package/dist/application/spaces/spaces.hook.d.ts.map +1 -1
- package/dist/application/spaces/spaces.hook.js +10 -3
- package/dist/auth/act-as.context.d.ts +15 -32
- package/dist/auth/act-as.context.d.ts.map +1 -1
- package/dist/auth/act-as.context.js +22 -66
- package/dist/auth/auth.hook.d.ts +1 -0
- package/dist/auth/auth.hook.d.ts.map +1 -1
- package/dist/auth/auth.hook.js +2 -22
- package/dist/auth/can.hook.d.ts +10 -38
- package/dist/auth/can.hook.d.ts.map +1 -1
- package/dist/auth/can.hook.js +35 -110
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/self.hook.d.ts +2 -2
- package/dist/auth/self.hook.d.ts.map +1 -1
- package/dist/auth/self.hook.js +0 -7
- package/dist/auth/session-kernel.d.ts +2 -3
- package/dist/auth/session-kernel.d.ts.map +1 -1
- package/dist/auth/session-kernel.js +4 -13
- package/dist/components/astrale.d.ts +14 -0
- package/dist/components/astrale.d.ts.map +1 -0
- package/dist/components/astrale.js +12 -0
- package/dist/components/boundaries.d.ts +47 -0
- package/dist/components/boundaries.d.ts.map +1 -0
- package/dist/components/boundaries.js +55 -0
- package/dist/components/can.d.ts +11 -0
- package/dist/components/can.d.ts.map +1 -0
- package/dist/components/can.js +5 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +3 -0
- package/dist/graph/call/call.hook.d.ts +8 -18
- package/dist/graph/call/call.hook.d.ts.map +1 -1
- package/dist/graph/call/call.hook.js +7 -13
- package/dist/graph/call/stream.hook.d.ts +12 -20
- package/dist/graph/call/stream.hook.d.ts.map +1 -1
- package/dist/graph/call/stream.hook.js +55 -111
- package/dist/graph/graph.context.d.ts +4 -3
- package/dist/graph/graph.context.d.ts.map +1 -1
- package/dist/graph/graph.context.js +2 -8
- package/dist/graph/guards.error.d.ts +7 -8
- package/dist/graph/guards.error.d.ts.map +1 -1
- package/dist/graph/guards.error.js +11 -7
- package/dist/graph/index.d.ts +8 -16
- package/dist/graph/index.d.ts.map +1 -1
- package/dist/graph/index.js +3 -7
- package/dist/graph/memory/read/envelope.d.ts +22 -54
- package/dist/graph/memory/read/envelope.d.ts.map +1 -1
- package/dist/graph/memory/read/envelope.js +26 -20
- package/dist/graph/memory/read/idle.d.ts +1 -15
- package/dist/graph/memory/read/idle.d.ts.map +1 -1
- package/dist/graph/memory/read/idle.js +4 -13
- package/dist/graph/query-definition.hook.d.ts +11 -0
- package/dist/graph/query-definition.hook.d.ts.map +1 -0
- package/dist/graph/query-definition.hook.js +205 -0
- package/dist/graph/resource.d.ts +34 -0
- package/dist/graph/resource.d.ts.map +1 -0
- package/dist/graph/resource.js +244 -0
- package/dist/graph/store.d.ts +25 -0
- package/dist/graph/store.d.ts.map +1 -0
- package/dist/graph/store.js +69 -0
- package/dist/host/actions.hook.d.ts +34 -0
- package/dist/host/actions.hook.d.ts.map +1 -0
- package/dist/host/actions.hook.js +125 -0
- package/dist/host/components.d.ts +15 -0
- package/dist/host/components.d.ts.map +1 -0
- package/dist/host/components.js +14 -0
- package/dist/host/index.d.ts +5 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +3 -0
- package/dist/index.d.ts +26 -37
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -31
- package/dist/intents/envelope.d.ts +7 -19
- package/dist/intents/envelope.d.ts.map +1 -1
- package/dist/intents/envelope.js +2 -3
- package/dist/intents/send-intent.hook.d.ts +5 -7
- package/dist/intents/send-intent.hook.d.ts.map +1 -1
- package/dist/intents/send-intent.hook.js +17 -75
- package/dist/navigation/adapters/adapter.d.ts +5 -0
- package/dist/navigation/adapters/adapter.d.ts.map +1 -1
- package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -1
- package/dist/navigation/adapters/memory.adapter.js +4 -0
- package/dist/navigation/adapters/url.adapter.d.ts.map +1 -1
- package/dist/navigation/adapters/url.adapter.js +57 -10
- package/dist/navigation/codec.d.ts +60 -0
- package/dist/navigation/codec.d.ts.map +1 -0
- package/dist/navigation/codec.js +121 -0
- package/dist/navigation/history.d.ts +11 -9
- package/dist/navigation/history.d.ts.map +1 -1
- package/dist/navigation/history.js +6 -8
- package/dist/navigation/hooks/ancestors.hook.d.ts +5 -7
- package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -1
- package/dist/navigation/hooks/ancestors.hook.js +7 -51
- package/dist/navigation/hooks/location.hook.d.ts.map +1 -1
- package/dist/navigation/hooks/location.hook.js +4 -6
- package/dist/navigation/hooks/scope.hook.d.ts.map +1 -1
- package/dist/navigation/hooks/scope.hook.js +4 -6
- package/dist/navigation/index.d.ts +4 -2
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +2 -1
- package/dist/navigation/link.d.ts +12 -0
- package/dist/navigation/link.d.ts.map +1 -1
- package/dist/navigation/link.js +13 -2
- package/dist/navigation/location.d.ts +13 -40
- package/dist/navigation/location.d.ts.map +1 -1
- package/dist/navigation/location.js +14 -76
- package/dist/navigation/scope.context.d.ts +17 -26
- package/dist/navigation/scope.context.d.ts.map +1 -1
- package/dist/navigation/scope.context.js +140 -108
- package/dist/navigation/snapshot.d.ts +14 -19
- package/dist/navigation/snapshot.d.ts.map +1 -1
- package/dist/navigation/snapshot.js +48 -44
- package/dist/schema/action.hook.d.ts +44 -0
- package/dist/schema/action.hook.d.ts.map +1 -0
- package/dist/schema/action.hook.js +72 -0
- package/dist/schema/domain.hook.d.ts +12 -7
- package/dist/schema/domain.hook.d.ts.map +1 -1
- package/dist/schema/domain.hook.js +101 -4
- package/dist/schema/index.d.ts +9 -8
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +5 -7
- package/dist/schema/kernel.hook.d.ts +4 -9
- package/dist/schema/kernel.hook.d.ts.map +1 -1
- package/dist/schema/kernel.hook.js +4 -6
- package/dist/schema/mutation.hook.d.ts +9 -0
- package/dist/schema/mutation.hook.d.ts.map +1 -0
- package/dist/schema/mutation.hook.js +41 -0
- package/dist/schema/object.hook.d.ts +16 -0
- package/dist/schema/object.hook.d.ts.map +1 -0
- package/dist/schema/object.hook.js +46 -0
- package/dist/schema/relation.hook.d.ts +46 -0
- package/dist/schema/relation.hook.d.ts.map +1 -0
- package/dist/schema/relation.hook.js +136 -0
- package/dist/selection/live.d.ts +9 -17
- package/dist/selection/live.d.ts.map +1 -1
- package/dist/selection/live.js +65 -112
- package/dist/selection/scope.context.js +5 -4
- package/dist/session/boot/boot.d.ts +1 -13
- package/dist/session/boot/boot.d.ts.map +1 -1
- package/dist/session/boot/boot.js +9 -27
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/kernel-instance.hook.d.ts +1 -4
- package/dist/session/kernel-instance.hook.d.ts.map +1 -1
- package/dist/session/kernel-instance.hook.js +1 -5
- package/dist/session/provider.d.ts +9 -23
- package/dist/session/provider.d.ts.map +1 -1
- package/dist/session/provider.js +29 -75
- package/dist/session/session.context.d.ts +5 -0
- package/dist/session/session.context.d.ts.map +1 -1
- package/dist/window/adopt.d.ts +0 -1
- package/dist/window/adopt.d.ts.map +1 -1
- package/dist/window/hooks/target-node.hook.d.ts +10 -6
- package/dist/window/hooks/target-node.hook.d.ts.map +1 -1
- package/dist/window/hooks/target-node.hook.js +14 -72
- package/dist/window/hooks/views-for.hook.d.ts +4 -3
- package/dist/window/hooks/views-for.hook.d.ts.map +1 -1
- package/dist/window/hooks/views-for.hook.js +6 -22
- package/dist/window/hooks/windows.hook.d.ts.map +1 -1
- package/dist/window/hooks/windows.hook.js +3 -4
- package/dist/window/host.d.ts +9 -11
- package/dist/window/host.d.ts.map +1 -1
- package/dist/window/host.js +26 -62
- package/dist/window/hub.d.ts +2 -0
- package/dist/window/hub.d.ts.map +1 -1
- package/dist/window/hub.js +33 -0
- package/dist/window/index.d.ts +2 -2
- package/dist/window/index.d.ts.map +1 -1
- package/dist/window/index.js +2 -2
- package/dist/window/lifecycle/mount.d.ts +5 -44
- package/dist/window/lifecycle/mount.d.ts.map +1 -1
- package/dist/window/lifecycle/mount.js +14 -120
- package/dist/window/lifecycle/states.d.ts +0 -5
- package/dist/window/lifecycle/states.d.ts.map +1 -1
- package/package.json +20 -8
- package/src/application/apps/app-host.tsx +11 -15
- package/src/application/apps/apps.hook.ts +13 -6
- package/src/application/apps/domains.hook.ts +1 -1
- package/src/application/apps/open-app.hook.ts +3 -4
- package/src/application/index.ts +4 -6
- package/src/application/spaces/space.hook.ts +5 -6
- package/src/application/spaces/spaces.hook.ts +14 -9
- package/src/auth/act-as.context.tsx +37 -91
- package/src/auth/auth.hook.ts +2 -30
- package/src/auth/can.hook.ts +44 -135
- package/src/auth/index.ts +1 -0
- package/src/auth/self.hook.ts +2 -9
- package/src/auth/session-kernel.ts +6 -16
- package/src/components/astrale.tsx +40 -0
- package/src/components/boundaries.tsx +113 -0
- package/src/components/can.tsx +16 -0
- package/src/components/index.ts +13 -0
- package/src/graph/call/call.hook.ts +22 -27
- package/src/graph/call/stream.hook.ts +66 -143
- package/src/graph/graph.context.ts +6 -11
- package/src/graph/guards.error.ts +17 -55
- package/src/graph/index.ts +23 -17
- package/src/graph/memory/read/envelope.ts +40 -65
- package/src/graph/memory/read/idle.ts +5 -18
- package/src/graph/query-definition.hook.ts +270 -0
- package/src/graph/resource.ts +358 -0
- package/src/graph/store.ts +97 -0
- package/src/host/actions.hook.ts +165 -0
- package/src/host/components.tsx +29 -0
- package/src/host/index.ts +17 -0
- package/src/index.ts +131 -124
- package/src/intents/envelope.ts +7 -23
- package/src/intents/send-intent.hook.ts +24 -79
- package/src/navigation/adapters/adapter.ts +3 -0
- package/src/navigation/adapters/memory.adapter.ts +4 -0
- package/src/navigation/adapters/url.adapter.ts +48 -8
- package/src/navigation/codec.ts +190 -0
- package/src/navigation/history.ts +20 -11
- package/src/navigation/hooks/ancestors.hook.ts +10 -60
- package/src/navigation/hooks/location.hook.ts +4 -6
- package/src/navigation/hooks/scope.hook.ts +4 -6
- package/src/navigation/index.ts +18 -2
- package/src/navigation/link.tsx +27 -2
- package/src/navigation/location.ts +26 -93
- package/src/navigation/scope.context.tsx +151 -136
- package/src/navigation/snapshot.ts +69 -50
- package/src/schema/action.hook.ts +146 -0
- package/src/schema/domain.hook.ts +127 -11
- package/src/schema/index.ts +9 -30
- package/src/schema/kernel.hook.ts +6 -13
- package/src/schema/mutation.hook.ts +63 -0
- package/src/schema/object.hook.ts +85 -0
- package/src/schema/relation.hook.ts +253 -0
- package/src/selection/live.ts +75 -125
- package/src/selection/scope.context.tsx +4 -4
- package/src/session/boot/boot.ts +8 -33
- package/src/session/index.ts +1 -0
- package/src/session/kernel-instance.hook.ts +2 -11
- package/src/session/provider.tsx +34 -103
- package/src/session/session.context.ts +6 -0
- package/src/window/adopt.ts +0 -1
- package/src/window/hooks/target-node.hook.ts +20 -85
- package/src/window/hooks/views-for.hook.ts +12 -25
- package/src/window/hooks/windows.hook.ts +3 -4
- package/src/window/host.tsx +35 -77
- package/src/window/hub.ts +33 -0
- package/src/window/index.ts +3 -3
- package/src/window/lifecycle/mount.ts +15 -160
- package/src/window/lifecycle/states.ts +1 -5
- package/dist/application/roles.hook.d.ts +0 -7
- package/dist/application/roles.hook.d.ts.map +0 -1
- package/dist/application/roles.hook.js +0 -23
- package/dist/graph/memory/base.d.ts +0 -21
- package/dist/graph/memory/base.d.ts.map +0 -1
- package/dist/graph/memory/base.js +0 -33
- package/dist/graph/memory/read/read.d.ts +0 -9
- package/dist/graph/memory/read/read.d.ts.map +0 -1
- package/dist/graph/memory/read/read.js +0 -87
- package/dist/graph/memory/scope.d.ts +0 -7
- package/dist/graph/memory/scope.d.ts.map +0 -1
- package/dist/graph/memory/scope.js +0 -25
- package/dist/graph/mutate/move.hook.d.ts +0 -7
- package/dist/graph/mutate/move.hook.d.ts.map +0 -1
- package/dist/graph/mutate/move.hook.js +0 -49
- package/dist/graph/mutate/mutate.hook.d.ts +0 -17
- package/dist/graph/mutate/mutate.hook.d.ts.map +0 -1
- package/dist/graph/mutate/mutate.hook.js +0 -45
- package/dist/graph/query/children.hook.d.ts +0 -29
- package/dist/graph/query/children.hook.d.ts.map +0 -1
- package/dist/graph/query/children.hook.js +0 -64
- package/dist/graph/query/graph.hook.d.ts +0 -9
- package/dist/graph/query/graph.hook.d.ts.map +0 -1
- package/dist/graph/query/graph.hook.js +0 -5
- package/dist/graph/query/neighbors.hook.d.ts +0 -7
- package/dist/graph/query/neighbors.hook.d.ts.map +0 -1
- package/dist/graph/query/neighbors.hook.js +0 -104
- package/dist/graph/query/node.hook.d.ts +0 -14
- package/dist/graph/query/node.hook.d.ts.map +0 -1
- package/dist/graph/query/node.hook.js +0 -43
- package/dist/graph/query/query.hook.d.ts +0 -14
- package/dist/graph/query/query.hook.d.ts.map +0 -1
- package/dist/graph/query/query.hook.js +0 -23
- package/dist/schema/bind-read.d.ts +0 -37
- package/dist/schema/bind-read.d.ts.map +0 -1
- package/dist/schema/bind-read.js +0 -205
- package/dist/schema/binding.d.ts +0 -21
- package/dist/schema/binding.d.ts.map +0 -1
- package/dist/schema/binding.js +0 -99
- package/dist/schema/function.hook.d.ts +0 -9
- package/dist/schema/function.hook.d.ts.map +0 -1
- package/dist/schema/function.hook.js +0 -40
- package/dist/schema/method.hook.d.ts +0 -14
- package/dist/schema/method.hook.d.ts.map +0 -1
- package/dist/schema/method.hook.js +0 -49
- package/dist/schema/tag.d.ts +0 -11
- package/dist/schema/tag.d.ts.map +0 -1
- package/dist/schema/tag.js +0 -11
- package/dist/schema/types.d.ts +0 -73
- package/dist/schema/types.d.ts.map +0 -1
- package/dist/schema/types.js +0 -1
- package/dist/schema/write.d.ts +0 -16
- package/dist/schema/write.d.ts.map +0 -1
- package/dist/schema/write.js +0 -56
- package/src/application/roles.hook.ts +0 -41
- package/src/graph/memory/base.ts +0 -50
- package/src/graph/memory/read/read.ts +0 -103
- package/src/graph/memory/scope.ts +0 -29
- package/src/graph/mutate/move.hook.ts +0 -70
- package/src/graph/mutate/mutate.hook.ts +0 -69
- package/src/graph/query/children.hook.ts +0 -136
- package/src/graph/query/graph.hook.ts +0 -13
- package/src/graph/query/neighbors.hook.ts +0 -159
- package/src/graph/query/node.hook.ts +0 -83
- package/src/graph/query/query.hook.ts +0 -42
- package/src/schema/bind-read.ts +0 -319
- package/src/schema/binding.ts +0 -146
- package/src/schema/function.hook.ts +0 -68
- package/src/schema/method.hook.ts +0 -103
- package/src/schema/tag.ts +0 -19
- package/src/schema/types.ts +0 -150
- package/src/schema/write.ts +0 -96
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { Key } from '@astrale-os/sdk/schema'
|
|
2
|
+
import type { ReactElement, ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Path } from '@astrale-os/sdk/graph/path'
|
|
5
|
+
import { Key as admitKey } from '@astrale-os/sdk/schema'
|
|
6
|
+
import { createElement } from 'react'
|
|
7
|
+
|
|
8
|
+
import type { NavAdapter } from './adapters/adapter.js'
|
|
9
|
+
import type { NavSnapshot, SerializedLocation } from './history.js'
|
|
10
|
+
|
|
11
|
+
import { NavScope } from './scope.context.js'
|
|
12
|
+
import { useNavCtx } from './scope.context.js'
|
|
13
|
+
import { useHistoryVersion } from './scope.context.js'
|
|
14
|
+
|
|
15
|
+
export interface NavigationParam<Value> {
|
|
16
|
+
encode(value: Value): string
|
|
17
|
+
decode(value: string | undefined): Value
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function pathParam(_name: string): NavigationParam<Path> {
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
encode: (value: Path) => Path.from(value).raw,
|
|
23
|
+
decode(value: string | undefined) {
|
|
24
|
+
if (value === undefined) throw new TypeError('Navigation target is missing.')
|
|
25
|
+
return Path.parse(value)
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function enumParam<const Values extends readonly [string, ...string[]]>(
|
|
31
|
+
values: Values,
|
|
32
|
+
fallback: Values[number],
|
|
33
|
+
): NavigationParam<Values[number]> {
|
|
34
|
+
if (!values.includes(fallback)) throw new TypeError('Navigation enum fallback is not admitted.')
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
encode(value: Values[number]) {
|
|
37
|
+
if (!values.includes(value)) throw new TypeError(`Invalid navigation enum value '${value}'.`)
|
|
38
|
+
return value
|
|
39
|
+
},
|
|
40
|
+
decode(value: string | undefined) {
|
|
41
|
+
if (value === undefined) return fallback
|
|
42
|
+
if (!values.includes(value)) throw new TypeError(`Invalid navigation enum value '${value}'.`)
|
|
43
|
+
return value as Values[number]
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type NavigationParams = Readonly<Record<string, NavigationParam<unknown>>>
|
|
49
|
+
|
|
50
|
+
export interface LocationDefinition<Target, Params extends NavigationParams> {
|
|
51
|
+
readonly view: Key.View
|
|
52
|
+
readonly target: NavigationParam<Target>
|
|
53
|
+
readonly params: Params
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function location<
|
|
57
|
+
Target,
|
|
58
|
+
const Params extends NavigationParams = Readonly<Record<never, never>>,
|
|
59
|
+
>(input: {
|
|
60
|
+
readonly view: Key.View | { readonly key: Key.View }
|
|
61
|
+
readonly target: NavigationParam<Target>
|
|
62
|
+
readonly params?: Params
|
|
63
|
+
}): LocationDefinition<Target, Params> {
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
view: viewKey(typeof input.view === 'string' ? input.view : input.view.key),
|
|
66
|
+
target: input.target,
|
|
67
|
+
params: Object.freeze({ ...input.params }) as Params,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type ValueOf<Codec> = Codec extends NavigationParam<infer Value> ? Value : never
|
|
72
|
+
type RouteTarget<
|
|
73
|
+
Routes extends Readonly<Record<string, LocationDefinition<unknown, NavigationParams>>>,
|
|
74
|
+
> = {
|
|
75
|
+
[Name in keyof Routes]: Readonly<{
|
|
76
|
+
name: Name
|
|
77
|
+
target: ValueOf<Routes[Name]['target']>
|
|
78
|
+
params: Readonly<{
|
|
79
|
+
[Param in keyof Routes[Name]['params']]: ValueOf<Routes[Name]['params'][Param]>
|
|
80
|
+
}>
|
|
81
|
+
}>
|
|
82
|
+
}[keyof Routes]
|
|
83
|
+
|
|
84
|
+
export type NavigationRoutes = Readonly<
|
|
85
|
+
Record<string, LocationDefinition<unknown, NavigationParams>>
|
|
86
|
+
>
|
|
87
|
+
|
|
88
|
+
export interface NavigationDefinition<Routes extends NavigationRoutes = NavigationRoutes> {
|
|
89
|
+
readonly routes: Routes
|
|
90
|
+
encode(target: RouteTarget<Routes>): SerializedLocation
|
|
91
|
+
decode(location: SerializedLocation): RouteTarget<Routes> | null
|
|
92
|
+
admit(location: SerializedLocation): SerializedLocation | null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function defineNavigation<const Routes extends NavigationRoutes>(
|
|
96
|
+
routes: Routes,
|
|
97
|
+
): NavigationDefinition<Routes> {
|
|
98
|
+
const names = Object.keys(routes).sort() as (keyof Routes)[]
|
|
99
|
+
const byView = new Map(names.map((name) => [routes[name]!.view, name] as const))
|
|
100
|
+
if (byView.size !== names.length) throw new TypeError('Navigation Views must be unique.')
|
|
101
|
+
const definition: NavigationDefinition<Routes> = {
|
|
102
|
+
routes: Object.freeze({ ...routes }),
|
|
103
|
+
encode(target) {
|
|
104
|
+
const route = routes[target.name]
|
|
105
|
+
if (route === undefined)
|
|
106
|
+
throw new TypeError(`Unknown navigation location '${String(target.name)}'.`)
|
|
107
|
+
const params = Object.fromEntries(
|
|
108
|
+
Object.entries(route.params).map(([name, codec]) => [
|
|
109
|
+
name,
|
|
110
|
+
codec.encode((target.params as Record<string, unknown>)[name]),
|
|
111
|
+
]),
|
|
112
|
+
)
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
node: Path.parse(route.target.encode(target.target)).raw,
|
|
115
|
+
view: route.view,
|
|
116
|
+
...(Object.keys(params).length === 0 ? {} : { params: Object.freeze(params) }),
|
|
117
|
+
})
|
|
118
|
+
},
|
|
119
|
+
decode(serialized) {
|
|
120
|
+
if (serialized.view === undefined) return null
|
|
121
|
+
const name = byView.get(serialized.view)
|
|
122
|
+
if (name === undefined) return null
|
|
123
|
+
const route = routes[name]!
|
|
124
|
+
try {
|
|
125
|
+
const params = Object.fromEntries(
|
|
126
|
+
Object.entries(route.params).map(([param, codec]) => [
|
|
127
|
+
param,
|
|
128
|
+
codec.decode(serialized.params?.[param]),
|
|
129
|
+
]),
|
|
130
|
+
)
|
|
131
|
+
if (Object.keys(serialized.params ?? {}).some((param) => !(param in route.params)))
|
|
132
|
+
return null
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
name,
|
|
135
|
+
target: route.target.decode(serialized.node),
|
|
136
|
+
params: Object.freeze(params),
|
|
137
|
+
}) as RouteTarget<Routes>
|
|
138
|
+
} catch {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
admit(serialized) {
|
|
143
|
+
const decoded = definition.decode(serialized)
|
|
144
|
+
return decoded === null ? null : definition.encode(decoded)
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
return Object.freeze(definition)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function NavigationScope<Routes extends NavigationRoutes>(props: {
|
|
151
|
+
readonly navigation: NavigationDefinition<Routes>
|
|
152
|
+
readonly adapter?: NavAdapter
|
|
153
|
+
readonly restore?: NavSnapshot
|
|
154
|
+
readonly initial?: RouteTarget<Routes>
|
|
155
|
+
readonly children: ReactNode
|
|
156
|
+
}): ReactElement {
|
|
157
|
+
return createElement(NavScope, {
|
|
158
|
+
...(props.adapter === undefined ? {} : { adapter: props.adapter }),
|
|
159
|
+
...(props.restore === undefined ? {} : { restore: props.restore }),
|
|
160
|
+
...(props.initial === undefined ? {} : { initial: props.navigation.encode(props.initial) }),
|
|
161
|
+
admit: props.navigation.admit,
|
|
162
|
+
children: props.children,
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function useNavigation<Routes extends NavigationRoutes>(
|
|
167
|
+
navigation: NavigationDefinition<Routes>,
|
|
168
|
+
) {
|
|
169
|
+
const context = useNavCtx()
|
|
170
|
+
useHistoryVersion(context.history)
|
|
171
|
+
const current = context.history.current
|
|
172
|
+
return Object.freeze({
|
|
173
|
+
location: current === null ? null : navigation.decode(current),
|
|
174
|
+
push: (target: RouteTarget<Routes>) => context.navigate(navigation.encode(target)),
|
|
175
|
+
replace: (target: RouteTarget<Routes>) =>
|
|
176
|
+
context.navigate(navigation.encode(target), { replace: true }),
|
|
177
|
+
back: context.navigate.back,
|
|
178
|
+
forward: context.navigate.forward,
|
|
179
|
+
snapshot: () => context.history.snapshot(),
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type NavigationTarget<Definition> =
|
|
184
|
+
Definition extends NavigationDefinition<infer Routes> ? RouteTarget<Routes> : never
|
|
185
|
+
|
|
186
|
+
function viewKey(input: string): Key.View {
|
|
187
|
+
const key = admitKey(input)
|
|
188
|
+
if (!admitKey.ref(key).startsWith('view.')) throw new TypeError(`Invalid View key '${input}'.`)
|
|
189
|
+
return key as Key.View
|
|
190
|
+
}
|
|
@@ -4,19 +4,21 @@
|
|
|
4
4
|
* (`SerializedLocation`/`NavSnapshot` + equality/clone), and the seed precedence.
|
|
5
5
|
* Source of truth for back/forward; the adapter is a projection sink around it.
|
|
6
6
|
*/
|
|
7
|
+
import type { NodePath } from '@astrale-os/sdk/graph/path'
|
|
8
|
+
import type { Key } from '@astrale-os/sdk/schema'
|
|
7
9
|
|
|
8
10
|
/** The serializable projection persisted in history / URLs / snapshots. */
|
|
9
11
|
export interface SerializedLocation {
|
|
10
|
-
node:
|
|
11
|
-
view?:
|
|
12
|
-
params?: Record<string, string
|
|
12
|
+
readonly node: NodePath
|
|
13
|
+
readonly view?: Key.View
|
|
14
|
+
readonly params?: Readonly<Record<string, string>>
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/** A versioned history snapshot — round-trips through `NavScope.restore`. */
|
|
16
18
|
export interface NavSnapshot {
|
|
17
|
-
v: 1
|
|
18
|
-
entries: SerializedLocation[]
|
|
19
|
-
cursor: number
|
|
19
|
+
readonly v: 1
|
|
20
|
+
readonly entries: readonly SerializedLocation[]
|
|
21
|
+
readonly cursor: number
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** Structural equality of two serialized locations (node + view + params). */
|
|
@@ -24,7 +26,10 @@ export function sameLocation(a: SerializedLocation, b: SerializedLocation): bool
|
|
|
24
26
|
return a.node === b.node && a.view === b.view && sameParams(a.params, b.params)
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
function sameParams(
|
|
29
|
+
function sameParams(
|
|
30
|
+
a?: Readonly<Record<string, string>>,
|
|
31
|
+
b?: Readonly<Record<string, string>>,
|
|
32
|
+
): boolean {
|
|
28
33
|
if (a === b) return true
|
|
29
34
|
const ak = a ? Object.keys(a) : []
|
|
30
35
|
const bk = b ? Object.keys(b) : []
|
|
@@ -74,7 +79,11 @@ export class NavHistory {
|
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
snapshot(): NavSnapshot {
|
|
77
|
-
return
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
v: 1 as const,
|
|
84
|
+
entries: Object.freeze(this.entries.map(cloneLocation)),
|
|
85
|
+
cursor: this.cursor,
|
|
86
|
+
})
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
/** Push (or replace the current entry with) a location. No-op on same location. */
|
|
@@ -129,8 +138,8 @@ export class NavHistory {
|
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
/** Replace the whole stack (restore revalidation prune). */
|
|
132
|
-
reset(entries: SerializedLocation[], cursor: number): void {
|
|
133
|
-
this.entries = entries
|
|
141
|
+
reset(entries: readonly SerializedLocation[], cursor: number): void {
|
|
142
|
+
this.entries = [...entries]
|
|
134
143
|
this.cursor = cursor
|
|
135
144
|
this.emit()
|
|
136
145
|
}
|
|
@@ -151,7 +160,7 @@ export class NavHistory {
|
|
|
151
160
|
*/
|
|
152
161
|
export function seedEntries(
|
|
153
162
|
fromAdapter: SerializedLocation | null,
|
|
154
|
-
restore: { entries: SerializedLocation[]; cursor: number } | undefined,
|
|
163
|
+
restore: { readonly entries: readonly SerializedLocation[]; readonly cursor: number } | undefined,
|
|
155
164
|
initialSerialized: SerializedLocation | null,
|
|
156
165
|
): { entries: SerializedLocation[]; cursor: number } {
|
|
157
166
|
if (restore !== undefined && restore.entries.length > 0) {
|
|
@@ -1,71 +1,21 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
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'
|
|
1
|
+
import type { Node } from '@astrale-os/sdk/graph/node'
|
|
2
|
+
import type { PathLike } from '@astrale-os/sdk/graph/path'
|
|
10
3
|
|
|
11
4
|
import type { ReadState } from '../../graph/memory/read/envelope.js'
|
|
12
|
-
import type { Node } from '../location.js'
|
|
13
5
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import { useHistoryVersion, useOptionalNavCtx, useMemoryShape } from '../scope.context.js'
|
|
18
|
-
import { serializeTarget } from '../snapshot.js'
|
|
6
|
+
import { unsupportedReadState } from '../../graph/memory/read/envelope.js'
|
|
7
|
+
|
|
8
|
+
const EMPTY_CHAIN: readonly Node[] = Object.freeze([])
|
|
19
9
|
|
|
20
10
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* node/path makes it usable outside a scope. The chain drops any ancestor whose
|
|
24
|
-
* record is masked/absent.
|
|
11
|
+
* Retained public concern with an explicit failed state: V2 has no implicit
|
|
12
|
+
* slash-tree ancestry. Callers must query a declared Edge Class instead.
|
|
25
13
|
*/
|
|
26
14
|
export function useAncestors(
|
|
27
|
-
|
|
15
|
+
_node?: Node | PathLike,
|
|
28
16
|
): 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
17
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
live: result.live,
|
|
68
|
-
seq: result.seq,
|
|
69
|
-
refetch: result.refetch,
|
|
18
|
+
...unsupportedReadState('navigation.ancestors requires an explicit parent Edge Class'),
|
|
19
|
+
chain: EMPTY_CHAIN,
|
|
70
20
|
}
|
|
71
21
|
}
|
|
@@ -6,16 +6,14 @@ import { useMemo } from 'react'
|
|
|
6
6
|
|
|
7
7
|
import type { NavLocation } from '../location.js'
|
|
8
8
|
|
|
9
|
-
import { buildLocation
|
|
10
|
-
import { useHistoryVersion, useNavCtx
|
|
9
|
+
import { buildLocation } from '../location.js'
|
|
10
|
+
import { useGraphPoint, useHistoryVersion, useNavCtx } from '../scope.context.js'
|
|
11
11
|
|
|
12
12
|
/** The nearest scope's live location. Throws outside any {@link NavScope}. */
|
|
13
13
|
export function useLocation(): NavLocation {
|
|
14
14
|
const ctx = useNavCtx()
|
|
15
15
|
useHistoryVersion(ctx.history)
|
|
16
16
|
const entry = ctx.history.current
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const settled = useSettled(result, shape)
|
|
20
|
-
return useMemo(() => buildLocation(entry, result, settled), [entry, result, settled])
|
|
17
|
+
const result = useGraphPoint(entry?.node ?? null)
|
|
18
|
+
return useMemo(() => buildLocation(entry, result), [entry, result])
|
|
21
19
|
}
|
|
@@ -9,8 +9,8 @@ import type { NavSnapshot } from '../history.js'
|
|
|
9
9
|
import type { NavLocation } from '../location.js'
|
|
10
10
|
import type { Navigate } from '../scope.context.js'
|
|
11
11
|
|
|
12
|
-
import { buildLocation
|
|
13
|
-
import {
|
|
12
|
+
import { buildLocation } from '../location.js'
|
|
13
|
+
import { useGraphPoint, useNavCtx } from '../scope.context.js'
|
|
14
14
|
import { useNavigateHandle } from './navigate.hook.js'
|
|
15
15
|
|
|
16
16
|
/** The nearest scope, for chrome/persistence (API §8.2). */
|
|
@@ -25,10 +25,8 @@ export function useNavScope(): NavScopeHandle {
|
|
|
25
25
|
const ctx = useNavCtx()
|
|
26
26
|
const navigate = useNavigateHandle(ctx)
|
|
27
27
|
const entry = ctx.history.current
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const settled = useSettled(result, shape)
|
|
31
|
-
const location = useMemo(() => buildLocation(entry, result, settled), [entry, result, settled])
|
|
28
|
+
const result = useGraphPoint(entry?.node ?? null)
|
|
29
|
+
const location = useMemo(() => buildLocation(entry, result), [entry, result])
|
|
32
30
|
const snapshot = useCallback(() => ctx.history.snapshot(), [ctx.history])
|
|
33
31
|
return { location, snapshot, navigate }
|
|
34
32
|
}
|
package/src/navigation/index.ts
CHANGED
|
@@ -7,8 +7,24 @@ export { useNavigate } from './hooks/navigate.hook.js'
|
|
|
7
7
|
export { useAncestors } from './hooks/ancestors.hook.js'
|
|
8
8
|
export { useNavScope } from './hooks/scope.hook.js'
|
|
9
9
|
export type { NavScopeHandle } from './hooks/scope.hook.js'
|
|
10
|
-
export { Link } from './link.js'
|
|
11
|
-
export type { LinkProps } from './link.js'
|
|
10
|
+
export { Link, NodeLink, ViewLink } from './link.js'
|
|
11
|
+
export type { LinkProps, NodeLinkProps, ViewLinkProps } from './link.js'
|
|
12
|
+
export {
|
|
13
|
+
NavigationScope,
|
|
14
|
+
defineNavigation,
|
|
15
|
+
enumParam,
|
|
16
|
+
location,
|
|
17
|
+
pathParam,
|
|
18
|
+
useNavigation,
|
|
19
|
+
} from './codec.js'
|
|
20
|
+
export type {
|
|
21
|
+
LocationDefinition,
|
|
22
|
+
NavigationDefinition,
|
|
23
|
+
NavigationParam,
|
|
24
|
+
NavigationParams,
|
|
25
|
+
NavigationRoutes,
|
|
26
|
+
NavigationTarget,
|
|
27
|
+
} from './codec.js'
|
|
12
28
|
export { memoryAdapter } from './adapters/memory.adapter.js'
|
|
13
29
|
export { urlAdapter } from './adapters/url.adapter.js'
|
|
14
30
|
export type { NavAdapter } from './adapters/adapter.js'
|
package/src/navigation/link.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ReactNode,
|
|
9
9
|
} from 'react'
|
|
10
10
|
|
|
11
|
+
import { Query } from '@astrale-os/sdk/query'
|
|
11
12
|
/**
|
|
12
13
|
* Link (API §8.2) — navigates the nearest scope. Renders `<a href>` when the
|
|
13
14
|
* scope's adapter exposes a URL form (cmd/ctrl/middle-click then falls through
|
|
@@ -16,6 +17,7 @@ import type {
|
|
|
16
17
|
*/
|
|
17
18
|
import { useCallback, useEffect } from 'react'
|
|
18
19
|
|
|
20
|
+
import type { NavigationDefinition, NavigationTarget } from './codec.js'
|
|
19
21
|
import type { NavTarget } from './snapshot.js'
|
|
20
22
|
|
|
21
23
|
import { useNavCtx } from './scope.context.js'
|
|
@@ -48,8 +50,9 @@ export function Link(props: LinkProps): ReactElement {
|
|
|
48
50
|
const nodeRef = target.node
|
|
49
51
|
|
|
50
52
|
const preload = useCallback(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const query = Query.from({ kind: 'node', paths: [nodeRef] }).select({ kind: 'graph' })
|
|
54
|
+
void ctx.store.preload(query, { page: { size: 1 } }).catch(() => {})
|
|
55
|
+
}, [ctx.store, nodeRef])
|
|
53
56
|
|
|
54
57
|
useEffect(() => {
|
|
55
58
|
if (prefetch === 'render') preload()
|
|
@@ -108,3 +111,25 @@ export function Link(props: LinkProps): ReactElement {
|
|
|
108
111
|
</a>
|
|
109
112
|
)
|
|
110
113
|
}
|
|
114
|
+
|
|
115
|
+
export type ViewLinkProps<Definition extends NavigationDefinition> = Omit<LinkProps, 'to'> & {
|
|
116
|
+
readonly navigation: Definition
|
|
117
|
+
readonly to: NavigationTarget<Definition>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Codec-admitted link for one declared View location. */
|
|
121
|
+
export function ViewLink<Definition extends NavigationDefinition>(
|
|
122
|
+
props: ViewLinkProps<Definition>,
|
|
123
|
+
): ReactElement {
|
|
124
|
+
const { navigation, to, ...link } = props
|
|
125
|
+
return <Link {...link} to={navigation.encode(to)} />
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type NodeLinkProps = Omit<LinkProps, 'to'> & {
|
|
129
|
+
readonly to: Extract<NavTarget, string | object>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Canonical node-only link; use ViewLink when a View/params contract exists. */
|
|
133
|
+
export function NodeLink(props: NodeLinkProps): ReactElement {
|
|
134
|
+
return <Link {...props} />
|
|
135
|
+
}
|
|
@@ -1,122 +1,55 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Node, NodeId } from '@astrale-os/sdk/graph/node'
|
|
2
|
+
import type { NodePath } from '@astrale-os/sdk/graph/path'
|
|
3
|
+
import type { Key } from '@astrale-os/sdk/schema'
|
|
2
4
|
|
|
3
|
-
import {
|
|
4
|
-
/**
|
|
5
|
-
* NavLocation projection (API §8.1) — the live, id-anchored location a scope
|
|
6
|
-
* points at (live path, missing flag), its `buildLocation` assembly, the
|
|
7
|
-
* `useSettled` guard against a one-render false-empty, and the framework-free
|
|
8
|
-
* path math (parent/chain) the scope and hooks share.
|
|
9
|
-
*/
|
|
10
|
-
import { useEffect, useRef } from 'react'
|
|
5
|
+
import { Path } from '@astrale-os/sdk/graph/path'
|
|
11
6
|
|
|
7
|
+
import type { ReadState } from '../graph/memory/read/envelope.js'
|
|
12
8
|
import type { SerializedLocation } from './history.js'
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* off `LiveResult` rather than taking a direct kernel-core dependency.
|
|
18
|
-
*/
|
|
19
|
-
export type Node = NonNullable<LiveResult['node']>
|
|
20
|
-
export type NodeId = Node['id']
|
|
10
|
+
export interface PointResult extends ReadState {
|
|
11
|
+
readonly node: Node | null
|
|
12
|
+
}
|
|
21
13
|
|
|
22
|
-
/**
|
|
14
|
+
/** One canonical location and its current caller-visible point projection. */
|
|
23
15
|
export interface NavLocation {
|
|
24
|
-
|
|
25
|
-
readonly id: NodeId
|
|
26
|
-
/** LIVE projection of the node's current path (empty until first resolve). */
|
|
27
|
-
readonly path: string
|
|
28
|
-
/** Live memory node — null while cold, or after the anchor stops resolving. */
|
|
16
|
+
readonly target: NodePath | null
|
|
17
|
+
readonly id: NodeId | null
|
|
29
18
|
readonly node: Node | null
|
|
30
|
-
|
|
31
|
-
readonly view?: string
|
|
19
|
+
readonly view?: Key.View
|
|
32
20
|
readonly params: Readonly<Record<string, string>>
|
|
33
|
-
/** The anchor no longer resolves (deleted / masked). */
|
|
34
21
|
readonly missing: boolean
|
|
35
22
|
}
|
|
36
23
|
|
|
37
24
|
const EMPTY_PARAMS: Readonly<Record<string, string>> = Object.freeze({})
|
|
38
25
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
id:
|
|
42
|
-
path: '',
|
|
26
|
+
export const IDLE_LOCATION: NavLocation = Object.freeze({
|
|
27
|
+
target: null,
|
|
28
|
+
id: null,
|
|
43
29
|
node: null,
|
|
44
30
|
params: EMPTY_PARAMS,
|
|
45
31
|
missing: false,
|
|
46
|
-
}
|
|
32
|
+
})
|
|
47
33
|
|
|
48
|
-
/** Project a serialized entry + its live read into the public {@link NavLocation}. */
|
|
49
34
|
export function buildLocation(
|
|
50
35
|
entry: SerializedLocation | null,
|
|
51
|
-
result:
|
|
52
|
-
settled: boolean,
|
|
36
|
+
result: PointResult | null,
|
|
53
37
|
): NavLocation {
|
|
54
38
|
if (entry === null) return IDLE_LOCATION
|
|
55
39
|
const node = result?.node ?? null
|
|
56
40
|
return {
|
|
57
|
-
|
|
58
|
-
|
|
41
|
+
target: entry.node,
|
|
42
|
+
id: node?.id ?? idAnchor(entry.node),
|
|
59
43
|
node,
|
|
60
|
-
...(entry.view
|
|
61
|
-
params: entry.params ??
|
|
62
|
-
missing:
|
|
44
|
+
...(entry.view === undefined ? {} : { view: entry.view }),
|
|
45
|
+
params: entry.params ?? EMPTY_PARAMS,
|
|
46
|
+
missing: result !== null && !result.pending && node === null,
|
|
63
47
|
}
|
|
64
48
|
}
|
|
65
49
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* one-render false-empty before a cold shape's first fetch is even in flight.
|
|
70
|
-
*/
|
|
71
|
-
export function useSettled(result: LiveResult | null, key: string | null): boolean {
|
|
72
|
-
const sawPending = useRef(new Map<string, boolean>())
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
if (key !== null && result?.pending === true) sawPending.current.set(key, true)
|
|
75
|
-
}, [key, result?.pending])
|
|
76
|
-
if (key === null || result === null) return false
|
|
77
|
-
if (result.node !== null) return true
|
|
78
|
-
if (result.error !== undefined) return true
|
|
79
|
-
return !result.pending && sawPending.current.get(key) === true
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ── path math ──────────────────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
/** The raw path of a live node (empty when cold). */
|
|
85
|
-
export function pathOf(node: Node | null): string {
|
|
86
|
-
return node ? rawOf(node.path) : ''
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Best-effort anchor id from a stored ref (`@<id>` → id, else the raw ref). */
|
|
90
|
-
export function idHint(ref: string): string {
|
|
91
|
-
return ref.startsWith('@') ? ref.slice(1) : ref
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Best-effort path from a stored ref (a `/…` ref is already a path). */
|
|
95
|
-
export function pathHint(ref: string): string {
|
|
96
|
-
return ref.startsWith('/') ? ref : ''
|
|
50
|
+
function idAnchor(target: NodePath): NodeId | null {
|
|
51
|
+
const path = Path.from(target)
|
|
52
|
+
return path.ast.anchor.kind === 'id' && path.ast.steps.length === 0 ? path.ast.anchor.id : null
|
|
97
53
|
}
|
|
98
54
|
|
|
99
|
-
|
|
100
|
-
export function parentPath(raw: string): string | null {
|
|
101
|
-
const norm = raw.replace(/\/+$/, '')
|
|
102
|
-
if (norm === '' || norm === '/') return null
|
|
103
|
-
const idx = norm.lastIndexOf('/')
|
|
104
|
-
return idx <= 0 ? '/' : norm.slice(0, idx)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* The inclusive, root-first path chain for an absolute path — the ancestry the
|
|
109
|
-
* kernel tree encodes in the slug path (`/a/b/c` → `/`, `/a`, `/a/b`, `/a/b/c`).
|
|
110
|
-
* The node itself is the last element, so breadcrumbs map the chain directly.
|
|
111
|
-
*/
|
|
112
|
-
export function chainPaths(raw: string): string[] {
|
|
113
|
-
const norm = raw.replace(/\/+$/, '')
|
|
114
|
-
if (norm === '' || norm === '/') return ['/']
|
|
115
|
-
const out = ['/']
|
|
116
|
-
let acc = ''
|
|
117
|
-
for (const seg of norm.split('/').filter(Boolean)) {
|
|
118
|
-
acc += `/${seg}`
|
|
119
|
-
out.push(acc)
|
|
120
|
-
}
|
|
121
|
-
return out
|
|
122
|
-
}
|
|
55
|
+
export type { Node, NodeId }
|