sh3-core 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/Shell.svelte +283 -0
- package/dist/Shell.svelte.d.ts +5 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +50 -0
- package/dist/app/admin/ApiKeysView.svelte +169 -0
- package/dist/app/admin/ApiKeysView.svelte.d.ts +3 -0
- package/dist/app/admin/AuthSettingsView.svelte +105 -0
- package/dist/app/admin/AuthSettingsView.svelte.d.ts +3 -0
- package/dist/app/admin/SystemView.svelte +73 -0
- package/dist/app/admin/SystemView.svelte.d.ts +3 -0
- package/dist/app/admin/UsersView.svelte +188 -0
- package/dist/app/admin/UsersView.svelte.d.ts +3 -0
- package/dist/app/admin/adminApp.d.ts +7 -0
- package/dist/app/admin/adminApp.js +25 -0
- package/dist/app/admin/adminShard.svelte.d.ts +4 -0
- package/dist/app/admin/adminShard.svelte.js +62 -0
- package/dist/app/store/InstalledView.svelte +246 -0
- package/dist/app/store/InstalledView.svelte.d.ts +3 -0
- package/dist/app/store/StoreView.svelte +522 -0
- package/dist/app/store/StoreView.svelte.d.ts +3 -0
- package/dist/app/store/storeApp.d.ts +10 -0
- package/dist/app/store/storeApp.js +26 -0
- package/dist/app/store/storeShard.svelte.d.ts +38 -0
- package/dist/app/store/storeShard.svelte.js +218 -0
- package/dist/apps/lifecycle.d.ts +42 -0
- package/dist/apps/lifecycle.js +184 -0
- package/dist/apps/registry.svelte.d.ts +40 -0
- package/dist/apps/registry.svelte.js +59 -0
- package/dist/apps/terminal/manifest.d.ts +8 -0
- package/dist/apps/terminal/manifest.js +13 -0
- package/dist/apps/terminal/terminal-app.d.ts +7 -0
- package/dist/apps/terminal/terminal-app.js +14 -0
- package/dist/apps/types.d.ts +93 -0
- package/dist/apps/types.js +10 -0
- package/dist/artifact.d.ts +32 -0
- package/dist/artifact.js +1 -0
- package/dist/assets/SH3.png +0 -0
- package/dist/assets/icons.svg +1126 -0
- package/dist/assets.d.ts +13 -0
- package/dist/auth/GuestBanner.svelte +134 -0
- package/dist/auth/GuestBanner.svelte.d.ts +3 -0
- package/dist/auth/SignInWall.svelte +203 -0
- package/dist/auth/SignInWall.svelte.d.ts +7 -0
- package/dist/auth/auth.svelte.d.ts +69 -0
- package/dist/auth/auth.svelte.js +165 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/types.d.ts +41 -0
- package/dist/auth/types.js +6 -0
- package/dist/build.d.ts +49 -0
- package/dist/build.js +236 -0
- package/dist/contract.d.ts +20 -0
- package/dist/contract.js +28 -0
- package/dist/createShell.d.ts +24 -0
- package/dist/createShell.js +131 -0
- package/dist/documents/backends.d.ts +17 -0
- package/dist/documents/backends.js +156 -0
- package/dist/documents/config.d.ts +7 -0
- package/dist/documents/config.js +27 -0
- package/dist/documents/handle.d.ts +6 -0
- package/dist/documents/handle.js +154 -0
- package/dist/documents/http-backend.d.ts +22 -0
- package/dist/documents/http-backend.js +78 -0
- package/dist/documents/index.d.ts +6 -0
- package/dist/documents/index.js +8 -0
- package/dist/documents/notifications.d.ts +9 -0
- package/dist/documents/notifications.js +39 -0
- package/dist/documents/types.d.ts +97 -0
- package/dist/documents/types.js +12 -0
- package/dist/env/client.d.ts +44 -0
- package/dist/env/client.js +106 -0
- package/dist/env/index.d.ts +2 -0
- package/dist/env/index.js +1 -0
- package/dist/env/types.d.ts +12 -0
- package/dist/env/types.js +8 -0
- package/dist/host-entry.d.ts +13 -0
- package/dist/host-entry.js +17 -0
- package/dist/host.d.ts +15 -0
- package/dist/host.js +86 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +14 -0
- package/dist/layout/DragPreview.svelte +63 -0
- package/dist/layout/DragPreview.svelte.d.ts +3 -0
- package/dist/layout/LayoutRenderer.svelte +262 -0
- package/dist/layout/LayoutRenderer.svelte.d.ts +6 -0
- package/dist/layout/SlotContainer.svelte +140 -0
- package/dist/layout/SlotContainer.svelte.d.ts +8 -0
- package/dist/layout/SlotDropZone.svelte +122 -0
- package/dist/layout/SlotDropZone.svelte.d.ts +8 -0
- package/dist/layout/drag.svelte.d.ts +45 -0
- package/dist/layout/drag.svelte.js +200 -0
- package/dist/layout/inspection.d.ts +72 -0
- package/dist/layout/inspection.js +209 -0
- package/dist/layout/ops.d.ts +100 -0
- package/dist/layout/ops.js +310 -0
- package/dist/layout/slotHostPool.svelte.d.ts +36 -0
- package/dist/layout/slotHostPool.svelte.js +229 -0
- package/dist/layout/store.svelte.d.ts +39 -0
- package/dist/layout/store.svelte.js +153 -0
- package/dist/layout/tree-walk.d.ts +15 -0
- package/dist/layout/tree-walk.js +33 -0
- package/dist/layout/types.d.ts +108 -0
- package/dist/layout/types.js +25 -0
- package/dist/migrations/shell-rename.d.ts +16 -0
- package/dist/migrations/shell-rename.js +48 -0
- package/dist/overlays/ModalFrame.svelte +87 -0
- package/dist/overlays/ModalFrame.svelte.d.ts +10 -0
- package/dist/overlays/PopupFrame.svelte +85 -0
- package/dist/overlays/PopupFrame.svelte.d.ts +10 -0
- package/dist/overlays/ToastItem.svelte +77 -0
- package/dist/overlays/ToastItem.svelte.d.ts +9 -0
- package/dist/overlays/focusTrap.d.ts +1 -0
- package/dist/overlays/focusTrap.js +64 -0
- package/dist/overlays/modal.d.ts +9 -0
- package/dist/overlays/modal.js +141 -0
- package/dist/overlays/popup.d.ts +9 -0
- package/dist/overlays/popup.js +108 -0
- package/dist/overlays/roots.d.ts +4 -0
- package/dist/overlays/roots.js +31 -0
- package/dist/overlays/toast.d.ts +6 -0
- package/dist/overlays/toast.js +93 -0
- package/dist/overlays/types.d.ts +31 -0
- package/dist/overlays/types.js +15 -0
- package/dist/platform/index.d.ts +10 -0
- package/dist/platform/index.js +33 -0
- package/dist/platform/tauri-backend.d.ts +15 -0
- package/dist/platform/tauri-backend.js +58 -0
- package/dist/primitives/.gitkeep +0 -0
- package/dist/primitives/ResizableSplitter.svelte +333 -0
- package/dist/primitives/ResizableSplitter.svelte.d.ts +35 -0
- package/dist/primitives/TabbedPanel.svelte +305 -0
- package/dist/primitives/TabbedPanel.svelte.d.ts +50 -0
- package/dist/primitives/base.css +42 -0
- package/dist/registry/client.d.ts +74 -0
- package/dist/registry/client.js +117 -0
- package/dist/registry/index.d.ts +13 -0
- package/dist/registry/index.js +14 -0
- package/dist/registry/installer.d.ts +53 -0
- package/dist/registry/installer.js +168 -0
- package/dist/registry/integrity.d.ts +32 -0
- package/dist/registry/integrity.js +92 -0
- package/dist/registry/loader.d.ts +50 -0
- package/dist/registry/loader.js +145 -0
- package/dist/registry/schema.d.ts +47 -0
- package/dist/registry/schema.js +185 -0
- package/dist/registry/storage.d.ts +37 -0
- package/dist/registry/storage.js +101 -0
- package/dist/registry/types.d.ts +262 -0
- package/dist/registry/types.js +14 -0
- package/dist/server-shard/types.d.ts +67 -0
- package/dist/server-shard/types.js +13 -0
- package/dist/sh3core-shard/ShellHome.svelte +192 -0
- package/dist/sh3core-shard/ShellHome.svelte.d.ts +3 -0
- package/dist/sh3core-shard/ShellTitle.svelte +171 -0
- package/dist/sh3core-shard/ShellTitle.svelte.d.ts +3 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.d.ts +2 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +53 -0
- package/dist/shards/activate.svelte.d.ts +52 -0
- package/dist/shards/activate.svelte.js +186 -0
- package/dist/shards/registry.d.ts +4 -0
- package/dist/shards/registry.js +28 -0
- package/dist/shards/types.d.ts +207 -0
- package/dist/shards/types.js +20 -0
- package/dist/shell-shard/InputLine.svelte +133 -0
- package/dist/shell-shard/InputLine.svelte.d.ts +11 -0
- package/dist/shell-shard/ScrollbackView.svelte +47 -0
- package/dist/shell-shard/ScrollbackView.svelte.d.ts +7 -0
- package/dist/shell-shard/Terminal.svelte +122 -0
- package/dist/shell-shard/Terminal.svelte.d.ts +8 -0
- package/dist/shell-shard/entries/PromptEntry.svelte +25 -0
- package/dist/shell-shard/entries/PromptEntry.svelte.d.ts +7 -0
- package/dist/shell-shard/entries/RichEntry.svelte +19 -0
- package/dist/shell-shard/entries/RichEntry.svelte.d.ts +8 -0
- package/dist/shell-shard/entries/StatusEntry.svelte +22 -0
- package/dist/shell-shard/entries/StatusEntry.svelte.d.ts +7 -0
- package/dist/shell-shard/entries/TextEntry.svelte +25 -0
- package/dist/shell-shard/entries/TextEntry.svelte.d.ts +7 -0
- package/dist/shell-shard/manifest.d.ts +2 -0
- package/dist/shell-shard/manifest.js +11 -0
- package/dist/shell-shard/protocol.d.ts +90 -0
- package/dist/shell-shard/protocol.js +11 -0
- package/dist/shell-shard/registry.d.ts +69 -0
- package/dist/shell-shard/registry.js +47 -0
- package/dist/shell-shard/rich/AppCard.svelte +25 -0
- package/dist/shell-shard/rich/AppCard.svelte.d.ts +10 -0
- package/dist/shell-shard/rich/AppsTable.svelte +29 -0
- package/dist/shell-shard/rich/AppsTable.svelte.d.ts +12 -0
- package/dist/shell-shard/rich/EnvTable.svelte +27 -0
- package/dist/shell-shard/rich/EnvTable.svelte.d.ts +8 -0
- package/dist/shell-shard/rich/HelpTable.svelte +29 -0
- package/dist/shell-shard/rich/HelpTable.svelte.d.ts +12 -0
- package/dist/shell-shard/rich/HistoryList.svelte +37 -0
- package/dist/shell-shard/rich/HistoryList.svelte.d.ts +9 -0
- package/dist/shell-shard/rich/ShardsTable.svelte +28 -0
- package/dist/shell-shard/rich/ShardsTable.svelte.d.ts +12 -0
- package/dist/shell-shard/rich/ViewsTable.svelte +31 -0
- package/dist/shell-shard/rich/ViewsTable.svelte.d.ts +13 -0
- package/dist/shell-shard/rich/ZoneTree.svelte +19 -0
- package/dist/shell-shard/rich/ZoneTree.svelte.d.ts +8 -0
- package/dist/shell-shard/rich/ZonesTable.svelte +27 -0
- package/dist/shell-shard/rich/ZonesTable.svelte.d.ts +11 -0
- package/dist/shell-shard/scrollback.svelte.d.ts +36 -0
- package/dist/shell-shard/scrollback.svelte.js +43 -0
- package/dist/shell-shard/session-client.svelte.d.ts +23 -0
- package/dist/shell-shard/session-client.svelte.js +120 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +2 -0
- package/dist/shell-shard/shellShard.svelte.js +139 -0
- package/dist/shell-shard/verbs/apps.d.ts +3 -0
- package/dist/shell-shard/verbs/apps.js +50 -0
- package/dist/shell-shard/verbs/clear.d.ts +2 -0
- package/dist/shell-shard/verbs/clear.js +7 -0
- package/dist/shell-shard/verbs/help.d.ts +2 -0
- package/dist/shell-shard/verbs/help.js +21 -0
- package/dist/shell-shard/verbs/history.d.ts +2 -0
- package/dist/shell-shard/verbs/history.js +20 -0
- package/dist/shell-shard/verbs/index.d.ts +2 -0
- package/dist/shell-shard/verbs/index.js +29 -0
- package/dist/shell-shard/verbs/session.d.ts +5 -0
- package/dist/shell-shard/verbs/session.js +65 -0
- package/dist/shell-shard/verbs/shards.d.ts +2 -0
- package/dist/shell-shard/verbs/shards.js +14 -0
- package/dist/shell-shard/verbs/views.d.ts +4 -0
- package/dist/shell-shard/verbs/views.js +90 -0
- package/dist/shell-shard/verbs/zones.d.ts +3 -0
- package/dist/shell-shard/verbs/zones.js +38 -0
- package/dist/shellRuntime.svelte.d.ts +27 -0
- package/dist/shellRuntime.svelte.js +27 -0
- package/dist/state/backends.d.ts +26 -0
- package/dist/state/backends.js +99 -0
- package/dist/state/manage.d.ts +14 -0
- package/dist/state/manage.js +40 -0
- package/dist/state/types.d.ts +55 -0
- package/dist/state/types.js +17 -0
- package/dist/state/zones.svelte.d.ts +53 -0
- package/dist/state/zones.svelte.js +141 -0
- package/dist/theme.d.ts +28 -0
- package/dist/theme.js +92 -0
- package/dist/tokens.css +102 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +60 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LayoutNode } from './types';
|
|
2
|
+
import type { App } from '../apps/types';
|
|
3
|
+
/**
|
|
4
|
+
* Attach an app: create or hydrate its workspace-zone layout proxy,
|
|
5
|
+
* enforce the blueprint version gate, and take a refcount hold on all
|
|
6
|
+
* of the app's slot ids so root swaps don't destroy its pooled hosts.
|
|
7
|
+
* Does NOT switch the active root. Call switchToApp() separately.
|
|
8
|
+
*/
|
|
9
|
+
export declare function attachApp(app: App): void;
|
|
10
|
+
/**
|
|
11
|
+
* Detach the currently-attached app. Releases its refcount holds; the
|
|
12
|
+
* pool's microtask cleanup drops the pooled hosts if they also have no
|
|
13
|
+
* active renderer refs. Must be called before attaching a different app.
|
|
14
|
+
*/
|
|
15
|
+
export declare function detachApp(): void;
|
|
16
|
+
export declare function switchToHome(): void;
|
|
17
|
+
export declare function switchToApp(): void;
|
|
18
|
+
/**
|
|
19
|
+
* The currently-rendered root. LayoutRenderer reads this through the
|
|
20
|
+
* `layoutStore` export below. Home uses the framework constant;
|
|
21
|
+
* app uses the workspace-zone proxy's `root` (which is reactive, so
|
|
22
|
+
* mutations from splitter/drag/ops reach the renderer unchanged).
|
|
23
|
+
*/
|
|
24
|
+
export declare function activeLayout(): LayoutNode;
|
|
25
|
+
export declare function getActiveRoot(): 'home' | 'app';
|
|
26
|
+
export declare function getAttachedAppId(): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Preserved for callers that still read `layoutStore.root`. The getter
|
|
29
|
+
* delegates to `activeLayout()` so every read walks through the
|
|
30
|
+
* manager. Writes to `layoutStore.root` are disallowed (mutation is
|
|
31
|
+
* expected to happen on the returned tree's nodes in place, as in
|
|
32
|
+
* phase 7 — splitter drags mutate `sizes[i]`, tab clicks mutate
|
|
33
|
+
* `activeTab`, drag-commit calls `ops.ts` functions that mutate
|
|
34
|
+
* children arrays). Nothing in the codebase currently reassigns
|
|
35
|
+
* `layoutStore.root`, so this getter-only shape is sufficient.
|
|
36
|
+
*/
|
|
37
|
+
export declare const layoutStore: {
|
|
38
|
+
readonly root: LayoutNode;
|
|
39
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Layout manager — owns both the framework-constant shell home layout
|
|
3
|
+
* and the currently-active app's persisted layout, and swaps between
|
|
4
|
+
* them without tearing down the held tree.
|
|
5
|
+
*
|
|
6
|
+
* The manager is the sole owner of "which layout root is being rendered
|
|
7
|
+
* right now". LayoutRenderer reads `layoutStore.root` (a getter on the
|
|
8
|
+
* active tree); drag.svelte.ts and any other mutation site do the same.
|
|
9
|
+
* Neither needs to know whether the active tree is home or an app.
|
|
10
|
+
*
|
|
11
|
+
* Refcount-hold discipline:
|
|
12
|
+
* The slot host pool is refcount-based with a microtask-deferred
|
|
13
|
+
* destroy. A root swap (app → home) causes the app's SlotContainers
|
|
14
|
+
* to unmount and release their pool entries; home's slots have
|
|
15
|
+
* different ids, so nothing re-acquires the app's pool entries before
|
|
16
|
+
* the microtask fires, and the app's views would be destroyed. To
|
|
17
|
+
* prevent this, attaching an app calls `acquireSlotHost` for every
|
|
18
|
+
* slot id in the app's current layout tree once (in addition to what
|
|
19
|
+
* the renderer does when the tree is active). That hold keeps
|
|
20
|
+
* refcount ≥ 1 across swaps. Detaching an app releases the holds.
|
|
21
|
+
*
|
|
22
|
+
* Home does not need a hold — it is either rendered or unmounted
|
|
23
|
+
* entirely (there is no "held home while rendering app" state).
|
|
24
|
+
*
|
|
25
|
+
* Orphan cleanup:
|
|
26
|
+
* The pre-phase-8 shell wrote to `sh3:workspace:__shell__`. Phase 8
|
|
27
|
+
* switches to per-app keys; the old entry would otherwise sit as dead
|
|
28
|
+
* data. On first load after upgrade, the manager clears that orphan
|
|
29
|
+
* unconditionally (clearing a non-existent entry is a no-op).
|
|
30
|
+
*/
|
|
31
|
+
import { createStateZones, peekZone, clearZone } from '../state/zones.svelte';
|
|
32
|
+
import { acquireSlotHost, releaseSlotHost } from './slotHostPool.svelte';
|
|
33
|
+
import { collectSlotRefs } from './tree-walk';
|
|
34
|
+
// ---------- orphan cleanup of pre-phase-8 shell layout key ----------------
|
|
35
|
+
// Legacy pre-phase-8 orphan cleanup. The literal '__shell__' here is
|
|
36
|
+
// intentional — it clears data written under the old reserved id before
|
|
37
|
+
// this shard was restructured. Do not replace with '__sh3core__'.
|
|
38
|
+
clearZone('workspace', '__shell__');
|
|
39
|
+
// ---------- home layout (framework constant, in-memory only) --------------
|
|
40
|
+
/**
|
|
41
|
+
* The home layout is a single slot wrapping the sh3core:home view. The
|
|
42
|
+
* slot id is reserved (`sh3core.home`) and stable so the pool entry for
|
|
43
|
+
* home survives across boot/launch cycles.
|
|
44
|
+
*/
|
|
45
|
+
const HOME_LAYOUT = {
|
|
46
|
+
type: 'slot',
|
|
47
|
+
slotId: 'sh3core.home',
|
|
48
|
+
viewId: 'sh3core:home',
|
|
49
|
+
};
|
|
50
|
+
let appEntry = $state(null);
|
|
51
|
+
let activeRoot = $state('home');
|
|
52
|
+
// ---------- public (within-framework) API ---------------------------------
|
|
53
|
+
/**
|
|
54
|
+
* Attach an app: create or hydrate its workspace-zone layout proxy,
|
|
55
|
+
* enforce the blueprint version gate, and take a refcount hold on all
|
|
56
|
+
* of the app's slot ids so root swaps don't destroy its pooled hosts.
|
|
57
|
+
* Does NOT switch the active root. Call switchToApp() separately.
|
|
58
|
+
*/
|
|
59
|
+
export function attachApp(app) {
|
|
60
|
+
if (appEntry) {
|
|
61
|
+
throw new Error(`Layout manager cannot attach app "${app.manifest.id}": app "${appEntry.appId}" is still attached`);
|
|
62
|
+
}
|
|
63
|
+
const shardId = `__sh3core__:app:${app.manifest.id}`;
|
|
64
|
+
// Version gate: if a stored blob's layoutVersion doesn't match the
|
|
65
|
+
// app's current declaration, discard it so createStateZones falls
|
|
66
|
+
// back to the defaults (the app's initialLayout).
|
|
67
|
+
const stored = peekZone('workspace', shardId);
|
|
68
|
+
if (stored != null) {
|
|
69
|
+
const asBlob = stored;
|
|
70
|
+
if (asBlob.layoutVersion !== app.manifest.layoutVersion) {
|
|
71
|
+
clearZone('workspace', shardId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const state = createStateZones(shardId, {
|
|
75
|
+
workspace: {
|
|
76
|
+
layoutVersion: app.manifest.layoutVersion,
|
|
77
|
+
root: app.initialLayout,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const proxy = state.workspace;
|
|
81
|
+
// Take a refcount hold on every slot in the app's tree. These holds
|
|
82
|
+
// keep the pooled hosts alive across home⇄app swaps. They are
|
|
83
|
+
// acquired without attaching the returned host anywhere — the
|
|
84
|
+
// LayoutRenderer still acquires its own refs when it mounts the
|
|
85
|
+
// tree, so the active rendering doesn't double-hold harmfully (the
|
|
86
|
+
// pool's destroy logic just sees refcount 2, then 1 on release).
|
|
87
|
+
const refs = collectSlotRefs(proxy.root);
|
|
88
|
+
const heldSlotIds = [];
|
|
89
|
+
for (const { slotId, viewId, label } of refs) {
|
|
90
|
+
acquireSlotHost(slotId, viewId, label);
|
|
91
|
+
heldSlotIds.push(slotId);
|
|
92
|
+
}
|
|
93
|
+
appEntry = { appId: app.manifest.id, proxy, heldSlotIds };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Detach the currently-attached app. Releases its refcount holds; the
|
|
97
|
+
* pool's microtask cleanup drops the pooled hosts if they also have no
|
|
98
|
+
* active renderer refs. Must be called before attaching a different app.
|
|
99
|
+
*/
|
|
100
|
+
export function detachApp() {
|
|
101
|
+
if (!appEntry)
|
|
102
|
+
return;
|
|
103
|
+
for (const slotId of appEntry.heldSlotIds) {
|
|
104
|
+
releaseSlotHost(slotId);
|
|
105
|
+
}
|
|
106
|
+
appEntry = null;
|
|
107
|
+
// If we detach while the active root is 'app', the renderer now has
|
|
108
|
+
// nothing to show; callers must switchToHome() before or after
|
|
109
|
+
// detachApp. We don't auto-switch here so the ordering is explicit.
|
|
110
|
+
}
|
|
111
|
+
export function switchToHome() {
|
|
112
|
+
activeRoot = 'home';
|
|
113
|
+
}
|
|
114
|
+
export function switchToApp() {
|
|
115
|
+
if (!appEntry) {
|
|
116
|
+
throw new Error('Cannot switchToApp: no app is attached');
|
|
117
|
+
}
|
|
118
|
+
activeRoot = 'app';
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The currently-rendered root. LayoutRenderer reads this through the
|
|
122
|
+
* `layoutStore` export below. Home uses the framework constant;
|
|
123
|
+
* app uses the workspace-zone proxy's `root` (which is reactive, so
|
|
124
|
+
* mutations from splitter/drag/ops reach the renderer unchanged).
|
|
125
|
+
*/
|
|
126
|
+
export function activeLayout() {
|
|
127
|
+
if (activeRoot === 'app' && appEntry)
|
|
128
|
+
return appEntry.proxy.root;
|
|
129
|
+
return HOME_LAYOUT;
|
|
130
|
+
}
|
|
131
|
+
export function getActiveRoot() {
|
|
132
|
+
return activeRoot;
|
|
133
|
+
}
|
|
134
|
+
export function getAttachedAppId() {
|
|
135
|
+
var _a;
|
|
136
|
+
return (_a = appEntry === null || appEntry === void 0 ? void 0 : appEntry.appId) !== null && _a !== void 0 ? _a : null;
|
|
137
|
+
}
|
|
138
|
+
// ---------- `layoutStore` back-compat shim -------------------------------
|
|
139
|
+
/**
|
|
140
|
+
* Preserved for callers that still read `layoutStore.root`. The getter
|
|
141
|
+
* delegates to `activeLayout()` so every read walks through the
|
|
142
|
+
* manager. Writes to `layoutStore.root` are disallowed (mutation is
|
|
143
|
+
* expected to happen on the returned tree's nodes in place, as in
|
|
144
|
+
* phase 7 — splitter drags mutate `sizes[i]`, tab clicks mutate
|
|
145
|
+
* `activeTab`, drag-commit calls `ops.ts` functions that mutate
|
|
146
|
+
* children arrays). Nothing in the codebase currently reassigns
|
|
147
|
+
* `layoutStore.root`, so this getter-only shape is sufficient.
|
|
148
|
+
*/
|
|
149
|
+
export const layoutStore = {
|
|
150
|
+
get root() {
|
|
151
|
+
return activeLayout();
|
|
152
|
+
},
|
|
153
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LayoutNode } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Collect the slot id / view id pairs of every slot leaf (including the
|
|
4
|
+
* slots embedded inside tabs entries) in a layout tree. Used by the
|
|
5
|
+
* layout manager to hold pool refs for a non-rendered but still-resident
|
|
6
|
+
* app tree. Order is a pre-order walk; the returned list may contain
|
|
7
|
+
* the same slot id twice if the tree is malformed, but ops.ts maintains
|
|
8
|
+
* the invariant that slot ids are unique so that is not a concern in
|
|
9
|
+
* practice.
|
|
10
|
+
*/
|
|
11
|
+
export declare function collectSlotRefs(tree: LayoutNode): {
|
|
12
|
+
slotId: string;
|
|
13
|
+
viewId: string | null;
|
|
14
|
+
label: string;
|
|
15
|
+
}[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Layout tree traversal utilities shared between the layout manager
|
|
3
|
+
* and other consumers (refcount-hold, diagnostic inspection).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Collect the slot id / view id pairs of every slot leaf (including the
|
|
7
|
+
* slots embedded inside tabs entries) in a layout tree. Used by the
|
|
8
|
+
* layout manager to hold pool refs for a non-rendered but still-resident
|
|
9
|
+
* app tree. Order is a pre-order walk; the returned list may contain
|
|
10
|
+
* the same slot id twice if the tree is malformed, but ops.ts maintains
|
|
11
|
+
* the invariant that slot ids are unique so that is not a concern in
|
|
12
|
+
* practice.
|
|
13
|
+
*/
|
|
14
|
+
export function collectSlotRefs(tree) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const walk = (node) => {
|
|
17
|
+
if (node.type === 'slot') {
|
|
18
|
+
out.push({ slotId: node.slotId, viewId: node.viewId, label: node.viewId || node.slotId });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (node.type === 'tabs') {
|
|
22
|
+
for (const t of node.tabs) {
|
|
23
|
+
out.push({ slotId: t.slotId, viewId: t.viewId, label: t.label });
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// split
|
|
28
|
+
for (const c of node.children)
|
|
29
|
+
walk(c);
|
|
30
|
+
};
|
|
31
|
+
walk(tree);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Axis along which a split node divides its children. */
|
|
2
|
+
export type SplitDirection = 'horizontal' | 'vertical';
|
|
3
|
+
/** How a child of a split node is sized. */
|
|
4
|
+
export type SizeMode = 'fr' | 'px';
|
|
5
|
+
/**
|
|
6
|
+
* A layout node that divides its area into two or more children along an axis.
|
|
7
|
+
* Children are sized proportionally (`fr`) or pixel-pinned (`px`). Supports
|
|
8
|
+
* per-child collapsed state so panels can be hidden without removing them.
|
|
9
|
+
*/
|
|
10
|
+
export interface SplitNode {
|
|
11
|
+
type: 'split';
|
|
12
|
+
/** Axis along which children are arranged. */
|
|
13
|
+
direction: SplitDirection;
|
|
14
|
+
/**
|
|
15
|
+
* Per-child size. Interpretation depends on the parallel `pinned` entry:
|
|
16
|
+
* - 'fr' (default): proportional weight; reflows on resize.
|
|
17
|
+
* - 'px': pixel-pinned; held fixed while 'fr' siblings absorb deltas.
|
|
18
|
+
*/
|
|
19
|
+
sizes: number[];
|
|
20
|
+
/** Per-child sizing mode. Omitted entries default to 'fr'. */
|
|
21
|
+
pinned?: SizeMode[];
|
|
22
|
+
/** Per-child collapsed state. Omitted means all expanded. */
|
|
23
|
+
collapsed?: boolean[];
|
|
24
|
+
/** Ordered child nodes. Length must equal `sizes` length. */
|
|
25
|
+
children: LayoutNode[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A single tab descriptor inside a `TabsNode`. Each entry corresponds to one
|
|
29
|
+
* slot: `slotId` is the stable identifier the framework uses to key persistent
|
|
30
|
+
* state, `viewId` names the shard view to mount (null means the slot is empty).
|
|
31
|
+
*/
|
|
32
|
+
export interface TabEntry {
|
|
33
|
+
/** Stable identifier for the slot. Persisted with the layout. */
|
|
34
|
+
slotId: string;
|
|
35
|
+
/** View id to mount into this slot, or null for an empty slot. */
|
|
36
|
+
viewId: string | null;
|
|
37
|
+
/** Human-readable label shown in the tab strip. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** Optional icon hint (not yet rendered in phase 8). */
|
|
40
|
+
icon?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A layout node that groups one or more slots as tabs, showing one at a time.
|
|
44
|
+
* The active tab index drives which slot is visible; the others stay mounted
|
|
45
|
+
* in the background so they survive tab switches without re-initialization.
|
|
46
|
+
*/
|
|
47
|
+
export interface TabsNode {
|
|
48
|
+
type: 'tabs';
|
|
49
|
+
tabs: TabEntry[];
|
|
50
|
+
activeTab: number;
|
|
51
|
+
/**
|
|
52
|
+
* If true, the node is not pruned by cleanupTree when its last tab is
|
|
53
|
+
* closed. The layout renderer shows an empty-state placeholder instead.
|
|
54
|
+
* Typically set by app layout blueprints on structural tab groups.
|
|
55
|
+
*/
|
|
56
|
+
persistent?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Custom renderer for the empty state when all tabs are closed (only
|
|
59
|
+
* meaningful when `persistent` is true). Called with the container
|
|
60
|
+
* element; the app has full control over what's rendered.
|
|
61
|
+
* Runtime-only — not serialized with the layout tree.
|
|
62
|
+
*/
|
|
63
|
+
emptyRenderer?: (container: HTMLElement) => void;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A leaf layout node that holds a single mounted view. `slotId` is the stable
|
|
67
|
+
* identifier used to key view state; `viewId` is the shard-registered view to
|
|
68
|
+
* mount. A null `viewId` means the slot is present in the tree but empty.
|
|
69
|
+
*/
|
|
70
|
+
export interface SlotNode {
|
|
71
|
+
type: 'slot';
|
|
72
|
+
/** Stable identifier for this slot. Persisted with the layout. */
|
|
73
|
+
slotId: string;
|
|
74
|
+
/** View id to mount into this slot, or null for an empty slot. */
|
|
75
|
+
viewId: string | null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Union of all layout node kinds. The recursive tree is composed entirely of
|
|
79
|
+
* these three types: `split` → children, `tabs` → slots, `slot` → leaf.
|
|
80
|
+
*/
|
|
81
|
+
export type LayoutNode = SplitNode | TabsNode | SlotNode;
|
|
82
|
+
/**
|
|
83
|
+
* Schema version for persisted layouts. Bump this when the shape of
|
|
84
|
+
* `LayoutNode` (or anything reachable from it) changes incompatibly.
|
|
85
|
+
* A stored entry whose version does not match is discarded on boot and
|
|
86
|
+
* the default tree takes over — phase 7 deliberately does not ship a
|
|
87
|
+
* migration framework, only the hook for one.
|
|
88
|
+
*/
|
|
89
|
+
export declare const LAYOUT_SCHEMA_VERSION = 3;
|
|
90
|
+
/**
|
|
91
|
+
* The wire shape of a persisted layout in the workspace state zone.
|
|
92
|
+
* One blob per shell (or per program, once per-program layouts exist);
|
|
93
|
+
* the version field gates compatibility.
|
|
94
|
+
*/
|
|
95
|
+
export interface PersistedLayout {
|
|
96
|
+
version: typeof LAYOUT_SCHEMA_VERSION;
|
|
97
|
+
root: LayoutNode;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Per-app layout blob written to the workspace state zone under
|
|
101
|
+
* `__sh3core__:app:<appId>`. The `layoutVersion` is the app's own
|
|
102
|
+
* `AppManifest.layoutVersion`; on launch a mismatch discards the blob
|
|
103
|
+
* and the app's `initialLayout` is used.
|
|
104
|
+
*/
|
|
105
|
+
export interface AppLayoutBlob {
|
|
106
|
+
layoutVersion: number;
|
|
107
|
+
root: LayoutNode;
|
|
108
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Layout tree types — the single source of truth for docked layout topology.
|
|
3
|
+
*
|
|
4
|
+
* See docs/design/layout.md for rationale. The tree is recursive with three
|
|
5
|
+
* node kinds:
|
|
6
|
+
*
|
|
7
|
+
* - split: horizontal or vertical, with proportional sizes and optional
|
|
8
|
+
* per-child pixel pinning.
|
|
9
|
+
* - tabs: a tab group with one active tab at a time. Each tab owns a slot.
|
|
10
|
+
* - slot: leaf. Carries a stable slotId so state can be keyed to it, and
|
|
11
|
+
* a viewId that names the view a shard should mount into it.
|
|
12
|
+
* viewId is null for empty slots.
|
|
13
|
+
*
|
|
14
|
+
* Views are mounted into slots by the shard registry (phase 4). Trees are
|
|
15
|
+
* serialized to the workspace state zone for persistence (phase 7) — see
|
|
16
|
+
* `PersistedLayout` at the bottom of this file.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Schema version for persisted layouts. Bump this when the shape of
|
|
20
|
+
* `LayoutNode` (or anything reachable from it) changes incompatibly.
|
|
21
|
+
* A stored entry whose version does not match is discarded on boot and
|
|
22
|
+
* the default tree takes over — phase 7 deliberately does not ship a
|
|
23
|
+
* migration framework, only the hook for one.
|
|
24
|
+
*/
|
|
25
|
+
export const LAYOUT_SCHEMA_VERSION = 3;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface WorkspaceZoneStore {
|
|
2
|
+
keys(): string[];
|
|
3
|
+
read(key: string): unknown;
|
|
4
|
+
write(key: string, value: unknown): void;
|
|
5
|
+
delete(key: string): void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Run the `__shell__` → `__sh3core__` rename migration. Call once at boot,
|
|
9
|
+
* before any shard activates. If the flag is already set, returns immediately.
|
|
10
|
+
*
|
|
11
|
+
* @param zone An adapter around the workspace state-zone backend used for
|
|
12
|
+
* iterating and rewriting persisted shard-prefixed keys.
|
|
13
|
+
* @param storage The localStorage-like object (pass `globalThis.localStorage`
|
|
14
|
+
* in the browser; pass a mock in tests).
|
|
15
|
+
*/
|
|
16
|
+
export declare function runShellRenameMigration(zone: WorkspaceZoneStore, storage: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* One-time migration of persisted keys that were written under the former
|
|
3
|
+
* `__shell__` pseudo-shard id. Runs once per browser, gated by a
|
|
4
|
+
* localStorage flag. Idempotent — safe to call on every boot.
|
|
5
|
+
*
|
|
6
|
+
* Scope:
|
|
7
|
+
* 1. localStorage: sh3:user:__shell__:theme → sh3:user:__sh3core__:theme
|
|
8
|
+
* 2. workspace state zone: every key beginning with `__shell__:`
|
|
9
|
+
* (includes `__shell__:last-app` and `__shell__:app:<appId>` entries)
|
|
10
|
+
* is copied to `__sh3core__:<rest>` and the old key is deleted.
|
|
11
|
+
*
|
|
12
|
+
* See docs/superpowers/specs/2026-04-10-shell-shard-design.md § Step 0.
|
|
13
|
+
*/
|
|
14
|
+
const FLAG_KEY = 'sh3:migrations:shell-rename:done';
|
|
15
|
+
const OLD_PREFIX = '__shell__:';
|
|
16
|
+
const NEW_PREFIX = '__sh3core__:';
|
|
17
|
+
const OLD_THEME_KEY = 'sh3:user:__shell__:theme';
|
|
18
|
+
const NEW_THEME_KEY = 'sh3:user:__sh3core__:theme';
|
|
19
|
+
/**
|
|
20
|
+
* Run the `__shell__` → `__sh3core__` rename migration. Call once at boot,
|
|
21
|
+
* before any shard activates. If the flag is already set, returns immediately.
|
|
22
|
+
*
|
|
23
|
+
* @param zone An adapter around the workspace state-zone backend used for
|
|
24
|
+
* iterating and rewriting persisted shard-prefixed keys.
|
|
25
|
+
* @param storage The localStorage-like object (pass `globalThis.localStorage`
|
|
26
|
+
* in the browser; pass a mock in tests).
|
|
27
|
+
*/
|
|
28
|
+
export function runShellRenameMigration(zone, storage) {
|
|
29
|
+
if (storage.getItem(FLAG_KEY)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// 1. Migrate localStorage theme key
|
|
33
|
+
const theme = storage.getItem(OLD_THEME_KEY);
|
|
34
|
+
if (theme !== null && storage.getItem(NEW_THEME_KEY) === null) {
|
|
35
|
+
storage.setItem(NEW_THEME_KEY, theme);
|
|
36
|
+
storage.removeItem(OLD_THEME_KEY);
|
|
37
|
+
}
|
|
38
|
+
// 2. Migrate workspace state zone keys with the old prefix
|
|
39
|
+
for (const key of zone.keys()) {
|
|
40
|
+
if (!key.startsWith(OLD_PREFIX))
|
|
41
|
+
continue;
|
|
42
|
+
const newKey = NEW_PREFIX + key.slice(OLD_PREFIX.length);
|
|
43
|
+
const value = zone.read(key);
|
|
44
|
+
zone.write(newKey, value);
|
|
45
|
+
zone.delete(key);
|
|
46
|
+
}
|
|
47
|
+
storage.setItem(FLAG_KEY, '1');
|
|
48
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* ModalFrame — the internal wrapper that the modal manager mounts into
|
|
4
|
+
* a per-modal host div inside the layer-4 root.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* - Span the full layer as a transparent centering container so every
|
|
8
|
+
* modal box renders in the middle of the shell.
|
|
9
|
+
* - Catch pointer events on the area around the box so clicks outside
|
|
10
|
+
* the top modal do not fall through to modals beneath or to the
|
|
11
|
+
* underlying layout.
|
|
12
|
+
* - Dynamically render the caller's content component with its props.
|
|
13
|
+
* - Install a focus trap on the dialog box for as long as the frame
|
|
14
|
+
* lives, restoring previous focus on teardown.
|
|
15
|
+
*
|
|
16
|
+
* The frame does NOT render a backdrop. The modal manager owns a single
|
|
17
|
+
* shared backdrop element under the layer-4 root whose opacity scales
|
|
18
|
+
* (capped) with stack depth — otherwise per-frame backdrops compound
|
|
19
|
+
* into full black once a few modals are stacked, and every layer also
|
|
20
|
+
* pays its own composite cost.
|
|
21
|
+
*
|
|
22
|
+
* Escape-to-close and backdrop-click policy are decided by the manager,
|
|
23
|
+
* not the frame — the manager holds the stack and knows which modal is
|
|
24
|
+
* on top. The frame just receives a `close` callback and invokes it if
|
|
25
|
+
* the caller wants a built-in close button (there is none by default —
|
|
26
|
+
* content owns its chrome).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { Component } from 'svelte';
|
|
30
|
+
import { createFocusTrap } from './focusTrap';
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
Content,
|
|
34
|
+
contentProps,
|
|
35
|
+
close,
|
|
36
|
+
boxStyle,
|
|
37
|
+
}: {
|
|
38
|
+
Content: Component<Record<string, unknown>>;
|
|
39
|
+
contentProps: Record<string, unknown>;
|
|
40
|
+
close: () => void;
|
|
41
|
+
boxStyle?: string;
|
|
42
|
+
} = $props();
|
|
43
|
+
|
|
44
|
+
let box: HTMLDivElement;
|
|
45
|
+
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (!box) return;
|
|
48
|
+
return createFocusTrap(box);
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<div class="modal-frame" role="presentation">
|
|
53
|
+
<div
|
|
54
|
+
class="modal-box"
|
|
55
|
+
role="dialog"
|
|
56
|
+
aria-modal="true"
|
|
57
|
+
tabindex="-1"
|
|
58
|
+
bind:this={box}
|
|
59
|
+
style={boxStyle}
|
|
60
|
+
>
|
|
61
|
+
<Content {...contentProps} {close} />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
.modal-frame {
|
|
67
|
+
position: absolute;
|
|
68
|
+
inset: 0;
|
|
69
|
+
display: grid;
|
|
70
|
+
place-items: center;
|
|
71
|
+
/* Transparent but pointer-capturing so clicks around the box are
|
|
72
|
+
swallowed rather than falling through to modals beneath. */
|
|
73
|
+
pointer-events: auto;
|
|
74
|
+
}
|
|
75
|
+
.modal-box {
|
|
76
|
+
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
77
|
+
color: var(--shell-fg);
|
|
78
|
+
border: 1px solid var(--shell-border-strong);
|
|
79
|
+
border-radius: var(--shell-radius);
|
|
80
|
+
min-width: 320px;
|
|
81
|
+
max-width: min(640px, 90vw);
|
|
82
|
+
max-height: 90vh;
|
|
83
|
+
overflow: auto;
|
|
84
|
+
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.5);
|
|
85
|
+
outline: none;
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
Content: Component<Record<string, unknown>>;
|
|
4
|
+
contentProps: Record<string, unknown>;
|
|
5
|
+
close: () => void;
|
|
6
|
+
boxStyle?: string;
|
|
7
|
+
};
|
|
8
|
+
declare const ModalFrame: Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type ModalFrame = ReturnType<typeof ModalFrame>;
|
|
10
|
+
export default ModalFrame;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* PopupFrame — positioned wrapper for a popup's content component.
|
|
4
|
+
*
|
|
5
|
+
* Takes an anchor rect in viewport coordinates and places itself at
|
|
6
|
+
* bottom-start with a viewport overflow clamp. The measurement happens
|
|
7
|
+
* after mount via an $effect so we can read the real frame size; an
|
|
8
|
+
* initial render off-screen hides the flicker while we measure.
|
|
9
|
+
*
|
|
10
|
+
* Positioning policy for phase 5:
|
|
11
|
+
* - Preferred: bottom-start (top = anchor.bottom + 4, left = anchor.left)
|
|
12
|
+
* - If the frame's right edge would exit the viewport, shift left so it
|
|
13
|
+
* sits flush with the right edge minus a small margin.
|
|
14
|
+
* - If the frame's bottom edge would exit the viewport, flip to above
|
|
15
|
+
* the anchor (top = anchor.top - frameHeight - 4).
|
|
16
|
+
* - If it still doesn't fit, clamp to the viewport.
|
|
17
|
+
*
|
|
18
|
+
* Additional placements (right-start, left-start, etc.) arrive when a
|
|
19
|
+
* real shard needs them; phase 5 doesn't.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Component } from 'svelte';
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
Content,
|
|
26
|
+
contentProps,
|
|
27
|
+
anchorRect,
|
|
28
|
+
close,
|
|
29
|
+
}: {
|
|
30
|
+
Content: Component<Record<string, unknown>>;
|
|
31
|
+
contentProps: Record<string, unknown>;
|
|
32
|
+
anchorRect: DOMRect;
|
|
33
|
+
close: () => void;
|
|
34
|
+
} = $props();
|
|
35
|
+
|
|
36
|
+
let frame: HTMLDivElement;
|
|
37
|
+
let top = $state(-9999);
|
|
38
|
+
let left = $state(-9999);
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
if (!frame) return;
|
|
42
|
+
const rect = frame.getBoundingClientRect();
|
|
43
|
+
const margin = 4;
|
|
44
|
+
const vw = window.innerWidth;
|
|
45
|
+
const vh = window.innerHeight;
|
|
46
|
+
|
|
47
|
+
let t = anchorRect.bottom + margin;
|
|
48
|
+
let l = anchorRect.left;
|
|
49
|
+
|
|
50
|
+
if (l + rect.width > vw - margin) {
|
|
51
|
+
l = Math.max(margin, vw - rect.width - margin);
|
|
52
|
+
}
|
|
53
|
+
if (t + rect.height > vh - margin) {
|
|
54
|
+
const flipped = anchorRect.top - rect.height - margin;
|
|
55
|
+
t = flipped >= margin ? flipped : Math.max(margin, vh - rect.height - margin);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
top = t;
|
|
59
|
+
left = l;
|
|
60
|
+
});
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<div
|
|
64
|
+
class="popup-frame"
|
|
65
|
+
role="menu"
|
|
66
|
+
tabindex="-1"
|
|
67
|
+
style="top: {top}px; left: {left}px;"
|
|
68
|
+
bind:this={frame}
|
|
69
|
+
>
|
|
70
|
+
<Content {...contentProps} {close} />
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<style>
|
|
74
|
+
.popup-frame {
|
|
75
|
+
position: absolute;
|
|
76
|
+
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
77
|
+
color: var(--shell-fg);
|
|
78
|
+
border: 1px solid var(--shell-border-strong);
|
|
79
|
+
border-radius: var(--shell-radius-sm);
|
|
80
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
81
|
+
min-width: 120px;
|
|
82
|
+
outline: none;
|
|
83
|
+
pointer-events: auto;
|
|
84
|
+
}
|
|
85
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
Content: Component<Record<string, unknown>>;
|
|
4
|
+
contentProps: Record<string, unknown>;
|
|
5
|
+
anchorRect: DOMRect;
|
|
6
|
+
close: () => void;
|
|
7
|
+
};
|
|
8
|
+
declare const PopupFrame: Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type PopupFrame = ReturnType<typeof PopupFrame>;
|
|
10
|
+
export default PopupFrame;
|