@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,92 @@
|
|
|
1
|
+
function isViewDescriptor(view) {
|
|
2
|
+
return typeof view === 'object' && view !== null && 'id' in view && 'path' in view;
|
|
3
|
+
}
|
|
4
|
+
/** Last path segment or host, alnum-only — a stable-ish functionId from a url. */
|
|
5
|
+
function slugOfUrl(url) {
|
|
6
|
+
const trimmed = url.replace(/[#?].*$/, '').replace(/\/+$/, '');
|
|
7
|
+
const tail = trimmed.split('/').filter(Boolean).pop() ?? 'view';
|
|
8
|
+
return tail.replace(/[^a-z0-9]/gi, '').slice(-32) || 'view';
|
|
9
|
+
}
|
|
10
|
+
/** A ref addressable by the kernel read surface (`get`). `@<id>` for bare ids. */
|
|
11
|
+
function toPath(ref) {
|
|
12
|
+
if (typeof ref === 'string') {
|
|
13
|
+
return ref.startsWith('/') || ref.startsWith('@') ? ref : `@${ref}`;
|
|
14
|
+
}
|
|
15
|
+
const obj = ref;
|
|
16
|
+
if (obj.path?.raw !== undefined)
|
|
17
|
+
return obj.path.raw;
|
|
18
|
+
if (obj.id !== undefined)
|
|
19
|
+
return `@${obj.id}`;
|
|
20
|
+
throw new Error('ViewHost: unresolvable view ref');
|
|
21
|
+
}
|
|
22
|
+
async function resolveViewRef(kernel, ref, handshakeHint) {
|
|
23
|
+
const node = await kernel.get(toPath(ref));
|
|
24
|
+
if (node === null)
|
|
25
|
+
throw new Error(`ViewHost: view not found (${toPath(ref)})`);
|
|
26
|
+
const props = node.props ?? {};
|
|
27
|
+
const url = props.url;
|
|
28
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
29
|
+
throw new Error('ViewHost: resolved view has no url');
|
|
30
|
+
}
|
|
31
|
+
const handshake = normalizeHandshake(handshakeHint ?? props.handshake);
|
|
32
|
+
const id = node.id ?? slugOfUrl(url);
|
|
33
|
+
return { url, handshake, functionId: id };
|
|
34
|
+
}
|
|
35
|
+
function normalizeHandshake(value) {
|
|
36
|
+
return value === 'none' ? 'none' : 'shell';
|
|
37
|
+
}
|
|
38
|
+
/** Resolve a source; may hit the kernel for a bare/urless View ref (G5). */
|
|
39
|
+
export async function resolveSource(kernel, source) {
|
|
40
|
+
if ('url' in source) {
|
|
41
|
+
return { url: source.url, handshake: 'none', functionId: slugOfUrl(source.url) };
|
|
42
|
+
}
|
|
43
|
+
const view = source.view;
|
|
44
|
+
if (isViewDescriptor(view)) {
|
|
45
|
+
if (view.url !== undefined && view.url.length > 0) {
|
|
46
|
+
return {
|
|
47
|
+
url: view.url,
|
|
48
|
+
handshake: normalizeHandshake(view.handshake),
|
|
49
|
+
functionId: view.id,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return resolveViewRef(kernel, view.id, view.handshake);
|
|
53
|
+
}
|
|
54
|
+
return resolveViewRef(kernel, view);
|
|
55
|
+
}
|
|
56
|
+
/** A stable remount key: the same source identity must not remount. */
|
|
57
|
+
export function sourceKey(source) {
|
|
58
|
+
if ('url' in source)
|
|
59
|
+
return `url:${source.url}`;
|
|
60
|
+
const view = source.view;
|
|
61
|
+
if (isViewDescriptor(view))
|
|
62
|
+
return `view:${view.id}:${view.url ?? ''}:${view.handshake ?? ''}`;
|
|
63
|
+
if (typeof view === 'string')
|
|
64
|
+
return `ref:${view}`;
|
|
65
|
+
const obj = view;
|
|
66
|
+
return `ref:${obj.id ?? obj.path?.raw ?? '?'}`;
|
|
67
|
+
}
|
|
68
|
+
/** The bare node id delivered as `targetNodeId` at mount / retarget. */
|
|
69
|
+
export function refToId(ref) {
|
|
70
|
+
if (ref === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
if (typeof ref === 'string')
|
|
73
|
+
return ref.startsWith('@') ? ref.slice(1) : ref;
|
|
74
|
+
const obj = ref;
|
|
75
|
+
return obj.id;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Fallbackable = the view didn't speak the shell protocol (timeout/failed), not a
|
|
79
|
+
* security refusal (origin mismatch) — only the former downgrades to plain.
|
|
80
|
+
*/
|
|
81
|
+
export function isFallbackableHandshakeError(err) {
|
|
82
|
+
const code = err?.code;
|
|
83
|
+
return code === 'HANDSHAKE_TIMEOUT' || code === 'HANDSHAKE_FAILED';
|
|
84
|
+
}
|
|
85
|
+
/** Parent → child hot-swap of the target node (no remount) — API §10 setTarget. */
|
|
86
|
+
export function sendSetTarget(shell, windowId, nodeId) {
|
|
87
|
+
shell.children.send(windowId, {
|
|
88
|
+
type: 'intent',
|
|
89
|
+
version: 1,
|
|
90
|
+
envelope: { name: 'setTarget', payload: { nodeId }, sender: { windowId: '<parent>' } },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WindowState phase machine (API §10) — the lifecycle of a hosted view as the
|
|
3
|
+
* React face observes it. Distinct from the shell's own `WindowState`
|
|
4
|
+
* (`active|hidden|closing|closed`): this is the phase a `ViewHost`/`AppHost`
|
|
5
|
+
* renders around.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Lifecycle of a hosted view as the React face observes it (API §10).
|
|
9
|
+
*
|
|
10
|
+
* `ready.downgraded` marks a mount that fell back to a plain iframe after a
|
|
11
|
+
* fallbackable handshake error. It rides here (not on the shell `Window`) until
|
|
12
|
+
* the shell `Window` grows a first-class flag — flagged in the module report.
|
|
13
|
+
*
|
|
14
|
+
* `crashed.windowId` is OPTIONAL: a crash before a window exists (a session-open
|
|
15
|
+
* failure, a pre-mount source-resolution failure) has no id to report — the type
|
|
16
|
+
* says so rather than carrying a placeholder `''`.
|
|
17
|
+
*/
|
|
18
|
+
export type WindowState = {
|
|
19
|
+
readonly phase: 'mounting';
|
|
20
|
+
} | {
|
|
21
|
+
readonly phase: 'ready';
|
|
22
|
+
readonly windowId: string;
|
|
23
|
+
readonly downgraded?: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
readonly phase: 'crashed';
|
|
26
|
+
readonly windowId?: string;
|
|
27
|
+
readonly error?: unknown;
|
|
28
|
+
} | {
|
|
29
|
+
readonly phase: 'closed';
|
|
30
|
+
readonly windowId: string;
|
|
31
|
+
readonly refusal?: string;
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=states.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"states.d.ts","sourceRoot":"","sources":["../../../src/window/lifecycle/states.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GACrF;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACnF;IAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WindowState phase machine (API §10) — the lifecycle of a hosted view as the
|
|
3
|
+
* React face observes it. Distinct from the shell's own `WindowState`
|
|
4
|
+
* (`active|hidden|closing|closed`): this is the phase a `ViewHost`/`AppHost`
|
|
5
|
+
* renders around.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot geometry (API §10) — the physical iframe-slot layer over a stage container
|
|
3
|
+
* (measure + project). Slots live in the stage (imperative DOM), insulated from
|
|
4
|
+
* React reconciliation of the hosts; only a slot's box is repositioned, so a
|
|
5
|
+
* healthy iframe never reloads on reorder/re-key/re-parent.
|
|
6
|
+
*/
|
|
7
|
+
/** Viewport-space box a host projects onto its stage slot. */
|
|
8
|
+
export interface SlotRect {
|
|
9
|
+
readonly top: number;
|
|
10
|
+
readonly left: number;
|
|
11
|
+
readonly width: number;
|
|
12
|
+
readonly height: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One persistent slot inside the stage. Its `element` is the mount host handed
|
|
16
|
+
* to `shell.mount`; the element NEVER moves in the DOM while the owning host
|
|
17
|
+
* reorders/re-keys/re-parents — only its box is repositioned via `place`.
|
|
18
|
+
*/
|
|
19
|
+
export interface StageSlot {
|
|
20
|
+
readonly element: HTMLElement;
|
|
21
|
+
place(rect: SlotRect): void;
|
|
22
|
+
release(): void;
|
|
23
|
+
}
|
|
24
|
+
export interface StageController {
|
|
25
|
+
acquire(): StageSlot;
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
28
|
+
/** Build the slot layer over a stage container element (imperative, no React). */
|
|
29
|
+
export declare function createStageController(container: HTMLElement): StageController;
|
|
30
|
+
/** Measure an anchor element's viewport box. */
|
|
31
|
+
export declare function rectOf(el: HTMLElement): SlotRect;
|
|
32
|
+
export declare function sameRect(a: SlotRect, b: SlotRect): boolean;
|
|
33
|
+
//# sourceMappingURL=placement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../../src/window/placement.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,8DAA8D;AAC9D,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,KAAK,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAA;IAC3B,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,IAAI,SAAS,CAAA;IACpB,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,kFAAkF;AAClF,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,WAAW,GAAG,eAAe,CAyC7E;AAED,gDAAgD;AAChD,wBAAgB,MAAM,CAAC,EAAE,EAAE,WAAW,GAAG,QAAQ,CAGhD;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAE1D"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot geometry (API §10) — the physical iframe-slot layer over a stage container
|
|
3
|
+
* (measure + project). Slots live in the stage (imperative DOM), insulated from
|
|
4
|
+
* React reconciliation of the hosts; only a slot's box is repositioned, so a
|
|
5
|
+
* healthy iframe never reloads on reorder/re-key/re-parent.
|
|
6
|
+
*/
|
|
7
|
+
/** Build the slot layer over a stage container element (imperative, no React). */
|
|
8
|
+
export function createStageController(container) {
|
|
9
|
+
const slots = new Set();
|
|
10
|
+
let disposed = false;
|
|
11
|
+
return {
|
|
12
|
+
acquire() {
|
|
13
|
+
const element = container.ownerDocument.createElement('div');
|
|
14
|
+
element.dataset.astraleViewSlot = '';
|
|
15
|
+
element.style.position = 'absolute';
|
|
16
|
+
element.style.top = '0';
|
|
17
|
+
element.style.left = '0';
|
|
18
|
+
element.style.pointerEvents = 'auto';
|
|
19
|
+
container.appendChild(element);
|
|
20
|
+
slots.add(element);
|
|
21
|
+
let released = false;
|
|
22
|
+
return {
|
|
23
|
+
element,
|
|
24
|
+
place(rect) {
|
|
25
|
+
// translate + explicit box, so a reflow of the container never drifts
|
|
26
|
+
// the slot off its anchor.
|
|
27
|
+
element.style.transform = `translate(${rect.left}px, ${rect.top}px)`;
|
|
28
|
+
element.style.width = `${rect.width}px`;
|
|
29
|
+
element.style.height = `${rect.height}px`;
|
|
30
|
+
},
|
|
31
|
+
release() {
|
|
32
|
+
if (released)
|
|
33
|
+
return;
|
|
34
|
+
released = true;
|
|
35
|
+
slots.delete(element);
|
|
36
|
+
element.remove();
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
dispose() {
|
|
41
|
+
if (disposed)
|
|
42
|
+
return;
|
|
43
|
+
disposed = true;
|
|
44
|
+
for (const el of slots)
|
|
45
|
+
el.remove();
|
|
46
|
+
slots.clear();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Measure an anchor element's viewport box. */
|
|
51
|
+
export function rectOf(el) {
|
|
52
|
+
const r = el.getBoundingClientRect();
|
|
53
|
+
return { top: r.top, left: r.left, width: r.width, height: r.height };
|
|
54
|
+
}
|
|
55
|
+
export function sameRect(a, b) {
|
|
56
|
+
return a.top === b.top && a.left === b.left && a.width === b.width && a.height === b.height;
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage.d.ts","sourceRoot":"","sources":["../../src/window/stage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,OAAO,CAAA;AA4BxD,wBAAgB,SAAS,IAAI,YAAY,CAiBxC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ViewStage (API §10) — the fixed-positioning layer that physically owns every
|
|
4
|
+
* iframe for the `ViewHost`/`AppHost`s in the tree. Mount it ONCE near the root.
|
|
5
|
+
* Reordering / re-parenting / re-keying hosts never reloads a healthy iframe:
|
|
6
|
+
* each iframe lives in a stage slot (imperative DOM), insulated from React
|
|
7
|
+
* reconciliation of the hosts; only the slot's box is repositioned.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
10
|
+
import { useShell } from '../session/shell.hook';
|
|
11
|
+
import { getStageHub } from './hub';
|
|
12
|
+
import { createStageController } from './placement';
|
|
13
|
+
/**
|
|
14
|
+
* A full-viewport overlay that does not intercept pointer events — individual
|
|
15
|
+
* slots re-enable them, so only the iframes are interactive, not the gaps.
|
|
16
|
+
*/
|
|
17
|
+
const STAGE_STYLE = {
|
|
18
|
+
position: 'fixed',
|
|
19
|
+
top: 0,
|
|
20
|
+
left: 0,
|
|
21
|
+
right: 0,
|
|
22
|
+
bottom: 0,
|
|
23
|
+
pointerEvents: 'none',
|
|
24
|
+
};
|
|
25
|
+
export function ViewStage() {
|
|
26
|
+
const shell = useShell();
|
|
27
|
+
const hub = useMemo(() => getStageHub(shell), [shell]);
|
|
28
|
+
const containerRef = useRef(null);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const container = containerRef.current;
|
|
31
|
+
if (container === null)
|
|
32
|
+
return;
|
|
33
|
+
const controller = createStageController(container);
|
|
34
|
+
hub.setController(controller);
|
|
35
|
+
return () => {
|
|
36
|
+
hub.setController(null);
|
|
37
|
+
controller.dispose();
|
|
38
|
+
};
|
|
39
|
+
}, [hub]);
|
|
40
|
+
return _jsx("div", { ref: containerRef, "data-astrale-view-stage": "", style: STAGE_STYLE });
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@astrale-os/shell-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Astrale shell-react — the React face of the shell (provider, hooks, view components)",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/astrale-os/shell.git",
|
|
9
|
+
"directory": "packages/shell-react"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@astrale-domains/workspace-schema": ">=0.2.0 <1.0.0",
|
|
29
|
+
"@astrale-os/kernel-client": ">=0.3.0 <1.0.0",
|
|
30
|
+
"@astrale-os/kernel-core": ">=0.6.0 <1.0.0",
|
|
31
|
+
"@astrale-os/kernel-dsl": ">=0.1.0 <1.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@testing-library/dom": "^10.4.0",
|
|
35
|
+
"@testing-library/react": "^16.3.0",
|
|
36
|
+
"@types/react": "^19.2.0",
|
|
37
|
+
"@types/react-dom": "^19.2.0",
|
|
38
|
+
"@typescript/native-preview": "latest",
|
|
39
|
+
"@vitejs/plugin-react": "^5.0.4",
|
|
40
|
+
"happy-dom": "^20.0.0",
|
|
41
|
+
"react": "^19.2.0",
|
|
42
|
+
"react-dom": "^19.2.0",
|
|
43
|
+
"typescript": "~6.0.1-rc",
|
|
44
|
+
"vitest": "^3.2.4",
|
|
45
|
+
"@astrale-os/shell": "0.2.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@astrale-os/shell": ">=0.2.0 <1.0.0",
|
|
49
|
+
"react": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsgo",
|
|
53
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
54
|
+
"typecheck": "tsgo --noEmit && tsgo --noEmit -p tsconfig.test.json",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"preinstall": "node .check-workspace.cjs"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { NodeRefInput } from '@astrale-os/kernel-client'
|
|
2
|
+
import type { ApplicationNode, AppSession, IntentRegistry, SandboxProfile } from '@astrale-os/shell'
|
|
3
|
+
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AppHost (API §12.2) = useOpenApp + ViewHost. Opens the app's `AppSession`,
|
|
7
|
+
* mounts its entry view with the session credential, and DISPOSES the session
|
|
8
|
+
* with the window (unmount, or an app change). `opening` renders while the
|
|
9
|
+
* session opens; `no-view` when the app pins no entrypoint; every mounted-frame
|
|
10
|
+
* phase is the window module's `WindowState`.
|
|
11
|
+
*/
|
|
12
|
+
import { forwardRef, useEffect, useRef, useState } from 'react'
|
|
13
|
+
|
|
14
|
+
import type { ViewHostHandle } from '../../window/host'
|
|
15
|
+
import type { ViewDescriptor } from '../../window/lifecycle/mount'
|
|
16
|
+
import type { WindowState } from '../../window/lifecycle/states'
|
|
17
|
+
|
|
18
|
+
import { ViewHost } from '../../window/host'
|
|
19
|
+
import { toRef } from './apps.hook'
|
|
20
|
+
import { useOpenApp } from './open-app.hook'
|
|
21
|
+
|
|
22
|
+
/** The states `AppHost`'s fallback answers — the frame's, plus the two OS phases. */
|
|
23
|
+
export type AppHostFallbackState =
|
|
24
|
+
| WindowState
|
|
25
|
+
| { readonly phase: 'opening' }
|
|
26
|
+
| { readonly phase: 'no-view' }
|
|
27
|
+
|
|
28
|
+
export interface AppHostProps {
|
|
29
|
+
app: ApplicationNode | NodeRefInput
|
|
30
|
+
/**
|
|
31
|
+
* Default target node delivered to the app's view as `targetNodeId` (API §12.2).
|
|
32
|
+
* Container-anchored apps need one — the dock passes the current space. A deeper
|
|
33
|
+
* fix (an install-time app data container + `app::open` returning a default
|
|
34
|
+
* target) is a tracked domain-modeling question; until then the host supplies it.
|
|
35
|
+
*/
|
|
36
|
+
node?: NodeRefInput
|
|
37
|
+
capabilities?: { intents: readonly (keyof IntentRegistry)[] }
|
|
38
|
+
sandbox?: SandboxProfile | null
|
|
39
|
+
fallback?: (state: AppHostFallbackState) => ReactNode
|
|
40
|
+
onSession?: (session: AppSession) => void
|
|
41
|
+
className?: string
|
|
42
|
+
style?: CSSProperties
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const AppHost = forwardRef<ViewHostHandle, AppHostProps>(
|
|
46
|
+
function AppHost(props, ref): ReactElement {
|
|
47
|
+
const open = useOpenApp()
|
|
48
|
+
const [session, setSession] = useState<AppSession | null>(null)
|
|
49
|
+
const [error, setError] = useState<unknown>(undefined)
|
|
50
|
+
|
|
51
|
+
const appKey = toRef(props.app)
|
|
52
|
+
const openRef = useRef(open)
|
|
53
|
+
openRef.current = open
|
|
54
|
+
const appRef = useRef(props.app)
|
|
55
|
+
appRef.current = props.app
|
|
56
|
+
const onSessionRef = useRef(props.onSession)
|
|
57
|
+
onSessionRef.current = props.onSession
|
|
58
|
+
|
|
59
|
+
// Open (re)opens the session per app; the cleanup disposes it — so the session
|
|
60
|
+
// is torn down with the window (unmount) and swapped on an app change.
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
let cancelled = false
|
|
63
|
+
let opened: AppSession | null = null
|
|
64
|
+
setSession(null)
|
|
65
|
+
setError(undefined)
|
|
66
|
+
void openRef.current(appRef.current).then(
|
|
67
|
+
(s) => {
|
|
68
|
+
if (cancelled) {
|
|
69
|
+
s.dispose()
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
opened = s
|
|
73
|
+
setSession(s)
|
|
74
|
+
onSessionRef.current?.(s)
|
|
75
|
+
},
|
|
76
|
+
(e: unknown) => {
|
|
77
|
+
if (!cancelled) setError(e)
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
return () => {
|
|
81
|
+
cancelled = true
|
|
82
|
+
opened?.dispose()
|
|
83
|
+
}
|
|
84
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
85
|
+
}, [appKey])
|
|
86
|
+
|
|
87
|
+
const { fallback, className, style } = props
|
|
88
|
+
const box = (children: ReactNode): ReactElement => (
|
|
89
|
+
<div className={className} style={style}>
|
|
90
|
+
{children}
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
// A session-open failure has no window yet — `windowId` is honestly absent (§10).
|
|
95
|
+
if (error !== undefined) return box(fallback?.({ phase: 'crashed', error }) ?? null)
|
|
96
|
+
if (session === null) return box(fallback?.({ phase: 'opening' }) ?? null)
|
|
97
|
+
if (session.view === null) return box(fallback?.({ phase: 'no-view' }) ?? null)
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<ViewHost
|
|
101
|
+
ref={ref}
|
|
102
|
+
view={session.view as ViewDescriptor}
|
|
103
|
+
node={props.node}
|
|
104
|
+
credential={session.credential}
|
|
105
|
+
capabilities={props.capabilities ?? { intents: [] }}
|
|
106
|
+
sandbox={props.sandbox}
|
|
107
|
+
fallback={fallback ? (s: WindowState) => fallback(s) : undefined}
|
|
108
|
+
className={className}
|
|
109
|
+
style={style}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
},
|
|
113
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppLogo (API §12.2) — render a node's logo prop: a url/`data:` URI as `<img>`,
|
|
3
|
+
* or an inline SVG string (sanitized + inlined), else `fallback`. The SVG path is
|
|
4
|
+
* a MINIMAL dependency-free strip (drops `<script>`/`<foreignObject>`, `on*`
|
|
5
|
+
* handlers, `javascript:` URLs); pass pre-sanitized markup for a hardened guard.
|
|
6
|
+
*/
|
|
7
|
+
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
|
8
|
+
|
|
9
|
+
const URL_LOGO = /^(?:data:|https?:|\/)/i
|
|
10
|
+
|
|
11
|
+
export interface AppLogoProps {
|
|
12
|
+
/** The logo string — a url/`data:` URI or an inline `<svg>` string. */
|
|
13
|
+
logo?: string
|
|
14
|
+
/** `<img>` alt text (url logos only). Default: `''` (decorative). */
|
|
15
|
+
alt?: string
|
|
16
|
+
className?: string
|
|
17
|
+
style?: CSSProperties
|
|
18
|
+
/** Rendered when `logo` is absent or unusable (e.g. a letter avatar). */
|
|
19
|
+
fallback?: ReactNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function AppLogo({ logo, alt, className, style, fallback }: AppLogoProps): ReactElement {
|
|
23
|
+
if (logo !== undefined && URL_LOGO.test(logo)) {
|
|
24
|
+
return <img src={logo} alt={alt ?? ''} className={className} style={style} />
|
|
25
|
+
}
|
|
26
|
+
const svg = logo !== undefined ? sanitizeInlineSvg(logo) : null
|
|
27
|
+
if (svg !== null) {
|
|
28
|
+
return (
|
|
29
|
+
<span
|
|
30
|
+
aria-hidden
|
|
31
|
+
className={className}
|
|
32
|
+
style={style}
|
|
33
|
+
// Minimally sanitized inline SVG — see the module header for the trust boundary.
|
|
34
|
+
dangerouslySetInnerHTML={{ __html: svg }}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
return <>{fallback ?? null}</>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Accept only a string that opens with `<svg`, and strip the obvious script
|
|
43
|
+
* vectors: `<script>`/`<foreignObject>` blocks, `on*=` handlers, `javascript:`
|
|
44
|
+
* URLs. Intentionally minimal (no heavy dep) — not a general HTML sanitizer.
|
|
45
|
+
*/
|
|
46
|
+
function sanitizeInlineSvg(raw: string): string | null {
|
|
47
|
+
const s = raw.trimStart()
|
|
48
|
+
if (!s.toLowerCase().startsWith('<svg')) return null
|
|
49
|
+
return s
|
|
50
|
+
.replace(/<\s*script[\s\S]*?<\s*\/\s*script\s*>/gi, '')
|
|
51
|
+
.replace(/<\s*foreignObject[\s\S]*?<\s*\/\s*foreignObject\s*>/gi, '')
|
|
52
|
+
.replace(/\son[a-z0-9_-]+\s*=\s*"[^"]*"/gi, '')
|
|
53
|
+
.replace(/\son[a-z0-9_-]+\s*=\s*'[^']*'/gi, '')
|
|
54
|
+
.replace(/\son[a-z0-9_-]+\s*=\s*[^\s>]+/gi, '')
|
|
55
|
+
.replace(/(href|xlink:href|src)\s*=\s*(["'])\s*javascript:[^"']*\2/gi, '')
|
|
56
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { NodeRefInput } from '@astrale-os/kernel-client'
|
|
2
|
+
import type { ApplicationNode, SpaceNode } from '@astrale-os/shell'
|
|
3
|
+
|
|
4
|
+
import { normalizeRef } from '@astrale-os/kernel-client'
|
|
5
|
+
/**
|
|
6
|
+
* useApps(space?) (API §12.2) — the apps installed in a space (`installed_in`
|
|
7
|
+
* in-edges). No arg targets the CURRENT space (shared store, so a switch anywhere
|
|
8
|
+
* swaps the dock) and is IDLE while none is selected (§1.3); an explicit `null`
|
|
9
|
+
* stays idle; OS plane unavailable → fully idle. Also `toRef`, the app/space
|
|
10
|
+
* subject normalizer the apps hooks share.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'
|
|
13
|
+
|
|
14
|
+
import type { ReadState } from '../../graph/memory/read/envelope'
|
|
15
|
+
import type { CurrentSpaceSnapshot } from '../spaces/current-space.store'
|
|
16
|
+
|
|
17
|
+
import { useAsyncResource } from '../../graph/async'
|
|
18
|
+
import { useShell } from '../../session/shell.hook'
|
|
19
|
+
import { currentSpaceStore } from '../spaces/current-space.store'
|
|
20
|
+
import { isOsPlaneAvailable } from '../user/user.store'
|
|
21
|
+
|
|
22
|
+
const NO_APPS: readonly ApplicationNode[] = []
|
|
23
|
+
const IDLE_CURRENT: CurrentSpaceSnapshot = { space: null, pending: false, error: undefined }
|
|
24
|
+
|
|
25
|
+
export interface AppsResult extends ReadState {
|
|
26
|
+
readonly apps: readonly ApplicationNode[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useApps(space?: SpaceNode | NodeRefInput | null): AppsResult {
|
|
30
|
+
const shell = useShell()
|
|
31
|
+
const available = isOsPlaneAvailable(shell)
|
|
32
|
+
|
|
33
|
+
// No arg → the CURRENT space, read through the shared store so a switch in any
|
|
34
|
+
// window swaps the dock here (§12.2). An explicit `null` stays idle; an explicit
|
|
35
|
+
// space bypasses the store entirely (no needless currentSpace fetch). OS plane
|
|
36
|
+
// unavailable (§12.2) → fully idle.
|
|
37
|
+
const useCurrent = space === undefined
|
|
38
|
+
const store = currentSpaceStore(shell)
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (useCurrent && available) store.ensure()
|
|
41
|
+
}, [store, useCurrent, available])
|
|
42
|
+
const subscribe = useCallback(
|
|
43
|
+
(cb: () => void) => (useCurrent && available ? store.subscribe(cb) : () => {}),
|
|
44
|
+
[store, useCurrent, available],
|
|
45
|
+
)
|
|
46
|
+
const getSnapshot = useCallback(
|
|
47
|
+
(): CurrentSpaceSnapshot => (useCurrent && available ? store.getSnapshot() : IDLE_CURRENT),
|
|
48
|
+
[store, useCurrent, available],
|
|
49
|
+
)
|
|
50
|
+
const currentSnap = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
51
|
+
|
|
52
|
+
const effective = useCurrent ? currentSnap.space : (space ?? null)
|
|
53
|
+
const effRef = effective === null ? null : toRef(effective)
|
|
54
|
+
const appsRes = useAsyncResource<readonly ApplicationNode[]>(
|
|
55
|
+
!available || effRef === null ? null : () => shell.apps.in(effRef),
|
|
56
|
+
NO_APPS,
|
|
57
|
+
[shell, effRef, available],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return useMemo<AppsResult>(
|
|
61
|
+
() => ({
|
|
62
|
+
apps: appsRes.data,
|
|
63
|
+
pending: (useCurrent && currentSnap.pending) || appsRes.pending,
|
|
64
|
+
error: (useCurrent ? currentSnap.error : undefined) ?? appsRes.error,
|
|
65
|
+
live: false,
|
|
66
|
+
seq: 0,
|
|
67
|
+
refetch: appsRes.refetch,
|
|
68
|
+
}),
|
|
69
|
+
[appsRes.data, appsRes.pending, appsRes.error, appsRes.refetch, useCurrent, currentSnap],
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A bound-node-ish subject carries a graph id. */
|
|
74
|
+
function hasId(x: unknown): x is { readonly id: string } {
|
|
75
|
+
return typeof x === 'object' && x !== null && 'id' in x
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** An `@id` ref for a bound node, else the normalized ref of a raw subject. */
|
|
79
|
+
export function toRef(x: NodeRefInput | { readonly id: string }): string {
|
|
80
|
+
return hasId(x) ? `@${x.id}` : normalizeRef(x)
|
|
81
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOpenApp (API §12.2) — the Action that opens an `AppSession` (the three-call
|
|
3
|
+
* app-open protocol lives in the shell package). Resolves to the session; disposal
|
|
4
|
+
* is the caller's (AppHost disposes it with the window).
|
|
5
|
+
*/
|
|
6
|
+
import type { NodeRefInput } from '@astrale-os/kernel-client'
|
|
7
|
+
import type { ApplicationNode, AppSession } from '@astrale-os/shell'
|
|
8
|
+
|
|
9
|
+
import type { Action } from '../../graph/action'
|
|
10
|
+
|
|
11
|
+
import { useAction } from '../../graph/action'
|
|
12
|
+
import { useShell } from '../../session/shell.hook'
|
|
13
|
+
import { toRef } from './apps.hook'
|
|
14
|
+
|
|
15
|
+
export function useOpenApp(): Action<[ApplicationNode | NodeRefInput], AppSession> {
|
|
16
|
+
const shell = useShell()
|
|
17
|
+
return useAction((app: ApplicationNode | NodeRefInput) => shell.apps.open(toRef(app)))
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// application — API §12.2. The OS-plane hooks over the shell package's
|
|
2
|
+
// `shell.user/spaces/apps` namespaces + the window module: useUser, useSpaces,
|
|
3
|
+
// useCurrentSpace, useSpace, useApps, useRoles, useOpenApp, AppHost, AppLogo.
|
|
4
|
+
export { useUser } from './user/user.hook'
|
|
5
|
+
export { useSpaces } from './spaces/spaces.hook'
|
|
6
|
+
export type { SpacesResult } from './spaces/spaces.hook'
|
|
7
|
+
export { useCurrentSpace } from './spaces/current-space.hook'
|
|
8
|
+
export type { CurrentSpaceResult } from './spaces/current-space.hook'
|
|
9
|
+
export { useSpace } from './spaces/space.hook'
|
|
10
|
+
export type { SpaceResult } from './spaces/space.hook'
|
|
11
|
+
export { useApps } from './apps/apps.hook'
|
|
12
|
+
export type { AppsResult } from './apps/apps.hook'
|
|
13
|
+
export { useOpenApp } from './apps/open-app.hook'
|
|
14
|
+
export { AppHost } from './apps/app-host'
|
|
15
|
+
export type { AppHostFallbackState, AppHostProps } from './apps/app-host'
|
|
16
|
+
export { AppLogo } from './apps/app-logo'
|
|
17
|
+
export type { AppLogoProps } from './apps/app-logo'
|
|
18
|
+
export { useRoles } from './roles.hook'
|
|
19
|
+
export type { RolesResult } from './roles.hook'
|
|
20
|
+
|
|
21
|
+
// The OS node/session types (from the shell package, single import surface).
|
|
22
|
+
export type { ApplicationNode, AppSession, RoleNode, SpaceNode, UserNode } from '@astrale-os/shell'
|