sh3-core 0.22.5 → 0.23.2
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/api.d.ts +1 -1
- package/dist/api.js +1 -1
- package/dist/app/admin/adminApp.js +2 -0
- package/dist/app/admin/adminShard.svelte.js +1 -0
- package/dist/app/store/storeApp.js +3 -1
- package/dist/app/store/storeShard.svelte.js +1 -0
- package/dist/app-appearance/appearanceShard.svelte.js +1 -0
- package/dist/apps/lifecycle.js +22 -10
- package/dist/apps/lifecycle.test.js +53 -1
- package/dist/apps/types.d.ts +9 -0
- package/dist/chrome/CompactChrome.svelte +11 -7
- package/dist/createShell.js +40 -0
- package/dist/documents/picker-api.test.js +40 -0
- package/dist/documents/picker-primitive.d.ts +39 -1
- package/dist/documents/picker-primitive.js +5 -4
- package/dist/host.js +30 -7
- package/dist/layout/slotHostPool.svelte.d.ts +11 -0
- package/dist/layout/slotHostPool.svelte.js +41 -17
- package/dist/layout/slotHostPool.test.js +45 -1
- package/dist/layouts-shard/layoutsShard.svelte.js +1 -0
- package/dist/overlays/OverlayRoots.svelte +15 -4
- package/dist/overlays/__test__/OverlayBindHarness.svelte +20 -0
- package/dist/overlays/__test__/OverlayBindHarness.svelte.d.ts +3 -0
- package/dist/overlays/float-compact-bind.svelte.test.d.ts +1 -0
- package/dist/overlays/float-compact-bind.svelte.test.js +51 -0
- package/dist/overlays/modal.js +3 -0
- package/dist/overlays/modal.test.js +45 -0
- package/dist/overlays/types.d.ts +9 -0
- package/dist/primitives/widgets/ShardPicker.svelte +38 -0
- package/dist/primitives/widgets/ShardPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +11 -3
- package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +2 -0
- package/dist/projects/scope-gate.d.ts +4 -0
- package/dist/projects/scope-gate.js +51 -0
- package/dist/projects/scope-gate.test.d.ts +1 -0
- package/dist/projects/scope-gate.test.js +92 -0
- package/dist/projects-shard/ProjectManage.svelte +42 -2
- package/dist/projects-shard/ProjectManage.svelte.test.js +10 -9
- package/dist/projects-shard/projectsApi.d.ts +3 -2
- package/dist/projects-shard/projectsApi.test.js +1 -1
- package/dist/projects-shard/projectsShard.svelte.js +1 -0
- package/dist/runtime/runVerb.d.ts +9 -0
- package/dist/runtime/runVerb.js +4 -4
- package/dist/runtime/runVerb.test.js +29 -0
- package/dist/sh3Api/headless.d.ts +7 -0
- package/dist/sh3Api/headless.js +3 -1
- package/dist/sh3Api/headless.svelte.test.js +42 -0
- package/dist/sh3core-shard/Sh3Home.svelte +3 -3
- package/dist/sh3core-shard/sh3coreShard.svelte.js +1 -0
- package/dist/shards/lifecycle.svelte.d.ts +8 -2
- package/dist/shards/lifecycle.svelte.js +65 -7
- package/dist/shards/lifecycle.test.js +110 -1
- package/dist/shards/types.d.ts +13 -0
- package/dist/shell-shard/Terminal.svelte +1 -4
- package/dist/shell-shard/Terminal.svelte.d.ts +0 -2
- package/dist/shell-shard/dispatch.d.ts +0 -2
- package/dist/shell-shard/dispatch.js +0 -2
- package/dist/shell-shard/display-cwd.test.js +4 -4
- package/dist/shell-shard/manifest.js +1 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +1 -1
- package/dist/shell-shard/shellShard.svelte.js +9 -4
- package/dist/shell-shard/verbs/cat.js +3 -3
- package/dist/shell-shard/verbs/cat.test.js +1 -2
- package/dist/shell-shard/verbs/ls.js +2 -2
- package/dist/shell-shard/verbs/ls.test.js +1 -2
- package/dist/shell-shard/verbs/mkdir.js +3 -3
- package/dist/shell-shard/verbs/mkdir.test.js +1 -2
- package/dist/shell-shard/verbs/mv.js +3 -3
- package/dist/shell-shard/verbs/mv.test.js +1 -2
- package/dist/shell-shard/verbs/rm.js +3 -3
- package/dist/shell-shard/verbs/rm.test.js +1 -2
- package/dist/shell-shard/verbs/xfer.js +5 -5
- package/dist/shell-shard/verbs/xfer.test.js +2 -2
- package/dist/verbs/types.d.ts +10 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type { ConflictItem, ConflictBranch as ConflictManagerBranch, ResolveOpti
|
|
|
38
38
|
export { CONFLICT_RENDERER_POINT, ConflictPermissionError, ConflictSessionOrphanedError, } from './conflicts/api';
|
|
39
39
|
export type { ColorPickOptions, ColorContribution, ColorApi, } from './color/api';
|
|
40
40
|
export { COLOR_PICKER_POINT } from './color/api';
|
|
41
|
-
export { registeredShards, activeShards, erroredShards } from './shards/lifecycle.svelte';
|
|
41
|
+
export { registeredShards, activeShards, erroredShards, listRegisteredShards } from './shards/lifecycle.svelte';
|
|
42
42
|
export type { ShardErrorEntry } from './shards/lifecycle.svelte';
|
|
43
43
|
export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, InstalledPackage, InstallResult, PackageMeta, RemoteInstallRequest, } from './registry/types';
|
|
44
44
|
export type { ResolvedPackage } from './registry/client';
|
package/dist/api.js
CHANGED
|
@@ -42,7 +42,7 @@ export { COLOR_PICKER_POINT } from './color/api';
|
|
|
42
42
|
// and tooling shards that need to visualize framework state. Phase 9
|
|
43
43
|
// addition: diagnostic used to reach `activate.svelte` directly via $lib;
|
|
44
44
|
// the package boundary requires routing through the public surface.
|
|
45
|
-
export { registeredShards, activeShards, erroredShards } from './shards/lifecycle.svelte';
|
|
45
|
+
export { registeredShards, activeShards, erroredShards, listRegisteredShards } from './shards/lifecycle.svelte';
|
|
46
46
|
export { fetchRegistries, fetchArchive, buildPackageMeta } from './registry/client';
|
|
47
47
|
export { validateRegistryIndex } from './registry/schema';
|
|
48
48
|
// Key mint/revoke types — client shards that declare `keys:mint` get ctx.keys.
|
|
@@ -10,7 +10,9 @@ import { VERSION } from '../../version';
|
|
|
10
10
|
export const storeApp = {
|
|
11
11
|
manifest: {
|
|
12
12
|
id: 'sh3-store-app',
|
|
13
|
-
label: 'Package
|
|
13
|
+
label: 'Package Manager',
|
|
14
|
+
icon: 'file-archive',
|
|
15
|
+
color: '#D16F19',
|
|
14
16
|
version: VERSION,
|
|
15
17
|
requiredShards: ['sh3-store'],
|
|
16
18
|
layoutVersion: 1,
|
package/dist/apps/lifecycle.js
CHANGED
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
* `{ id: string | null }`. Writing happens on launch (id) and on
|
|
12
12
|
* return-to-home (null). Boot reads it to decide whether to auto-launch.
|
|
13
13
|
*/
|
|
14
|
+
import { flushSync } from 'svelte';
|
|
14
15
|
import { createStateZones } from '../state/zones.svelte';
|
|
15
16
|
import { createGestureRegistry } from '../gestures';
|
|
16
17
|
import { registeredShards, } from '../shards/lifecycle.svelte';
|
|
17
18
|
import { shardEntries, runAppActivate, runAppDeactivate, registerAllShards, erroredShards } from '../shards/lifecycle.svelte';
|
|
18
19
|
import { attachApp, acquireAppSlotHolds, detachApp, switchToApp, switchToHome, getActiveAppTree, } from '../layout/store.svelte';
|
|
20
|
+
import { flushPendingDestroys } from '../layout/slotHostPool.svelte';
|
|
19
21
|
import { activeApp, breadcrumbApp, getRegisteredApp, registeredApps } from './registry.svelte';
|
|
20
22
|
import { createZoneManager } from '../state/manage';
|
|
21
23
|
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
@@ -227,19 +229,29 @@ export function unloadApp(id, skipSwitchToHome = false) {
|
|
|
227
229
|
if (!app)
|
|
228
230
|
return;
|
|
229
231
|
void ((_a = app.deactivate) === null || _a === void 0 ? void 0 : _a.call(app));
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
232
|
+
// Teardown order is load-bearing. Views must be fully unmounted before
|
|
233
|
+
// any shard's onAppDeactivate runs, so shards can safely close stores /
|
|
234
|
+
// nullify reactive state in onAppDeactivate without tripping leaked
|
|
235
|
+
// $derived references in still-mounted views (the leaked-pool bug).
|
|
236
|
+
//
|
|
237
|
+
// 1) switchToHome flips the rendered root so LayoutRenderer stops
|
|
238
|
+
// reading the app's tree. SlotContainer cleanups are scheduled.
|
|
239
|
+
// 2) flushSync forces Svelte to run those cleanups now (drops the
|
|
240
|
+
// renderer's refcount on every app slot).
|
|
241
|
+
// 3) detachApp releases the per-app refcount holds. Pool entries for
|
|
242
|
+
// the app's slots are now at refcount 0 and queued in pendingDestroy.
|
|
243
|
+
// 4) flushPendingDestroys synchronously runs the destroy body for
|
|
244
|
+
// those entries — the Svelte components for the app's views are
|
|
245
|
+
// unmounted now, not on a deferred microtask.
|
|
246
|
+
// 5) Only now do we call runAppDeactivate on each required shard.
|
|
240
247
|
if (!skipSwitchToHome)
|
|
241
248
|
switchToHome();
|
|
249
|
+
flushSync();
|
|
242
250
|
detachApp();
|
|
251
|
+
flushPendingDestroys();
|
|
252
|
+
for (const shardId of app.manifest.requiredShards) {
|
|
253
|
+
void runAppDeactivate(shardId, id);
|
|
254
|
+
}
|
|
243
255
|
activeApp.id = null;
|
|
244
256
|
setActiveApp(null, new Set());
|
|
245
257
|
clearSelectionUnconditional();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { resetFramework } from '../__test__/reset';
|
|
3
3
|
import { makeApp, makeShard, makeAppManifest, makeShardManifest, makeTabsNode, makeTabEntry, makeSlotNode, makeTree, } from '../__test__/fixtures';
|
|
4
|
-
import { launchApp, returnToHome, unregisterApp } from './lifecycle';
|
|
4
|
+
import { launchApp, returnToHome, unloadApp, unregisterApp } from './lifecycle';
|
|
5
5
|
import { registerApp } from './registry.svelte';
|
|
6
6
|
import { registerShard } from '../shards/lifecycle.svelte';
|
|
7
7
|
import { presetManager } from '../overlays/presets';
|
|
@@ -703,6 +703,58 @@ describe('unloadApp — onAppDeactivate hook', () => {
|
|
|
703
703
|
expect(deactivated).toEqual(['deact-app']);
|
|
704
704
|
});
|
|
705
705
|
});
|
|
706
|
+
describe('unloadApp — view unmount happens before onAppDeactivate', () => {
|
|
707
|
+
beforeEach(resetFramework);
|
|
708
|
+
it('unmounts all of the app\'s pooled views before any shard\'s onAppDeactivate fires', async () => {
|
|
709
|
+
const order = [];
|
|
710
|
+
const shard = makeShard({
|
|
711
|
+
manifest: makeShardManifest({ id: 'order-shard' }),
|
|
712
|
+
register() { },
|
|
713
|
+
onAppDeactivate() { order.push('onAppDeactivate'); },
|
|
714
|
+
});
|
|
715
|
+
registerShard(shard);
|
|
716
|
+
// Register two views so the assertion covers an active slot AND an
|
|
717
|
+
// inactive-but-held slot. Both must unmount before deactivate.
|
|
718
|
+
registerView('order:view-a', {
|
|
719
|
+
mount: () => ({ unmount: () => order.push('view-a:unmount') }),
|
|
720
|
+
});
|
|
721
|
+
registerView('order:view-b', {
|
|
722
|
+
mount: () => ({ unmount: () => order.push('view-b:unmount') }),
|
|
723
|
+
});
|
|
724
|
+
const app = makeApp({
|
|
725
|
+
manifest: makeAppManifest({ id: 'order-app', requiredShards: ['order-shard'] }),
|
|
726
|
+
initialLayout: [
|
|
727
|
+
{
|
|
728
|
+
name: 'default',
|
|
729
|
+
tree: makeTree({
|
|
730
|
+
type: 'split',
|
|
731
|
+
direction: 'horizontal',
|
|
732
|
+
children: [
|
|
733
|
+
makeSlotNode('a-slot', 'order:view-a'),
|
|
734
|
+
makeSlotNode('b-slot', 'order:view-b'),
|
|
735
|
+
],
|
|
736
|
+
sizes: [0.5, 0.5],
|
|
737
|
+
}),
|
|
738
|
+
},
|
|
739
|
+
],
|
|
740
|
+
});
|
|
741
|
+
registerApp(app);
|
|
742
|
+
await launchApp('order-app');
|
|
743
|
+
// Let the mount microtasks settle so factories have run.
|
|
744
|
+
await Promise.resolve();
|
|
745
|
+
await Promise.resolve();
|
|
746
|
+
unloadApp('order-app');
|
|
747
|
+
// Both views must have unmounted BEFORE onAppDeactivate ran.
|
|
748
|
+
const deactivateIdx = order.indexOf('onAppDeactivate');
|
|
749
|
+
const unmountAIdx = order.indexOf('view-a:unmount');
|
|
750
|
+
const unmountBIdx = order.indexOf('view-b:unmount');
|
|
751
|
+
expect(deactivateIdx).toBeGreaterThanOrEqual(0);
|
|
752
|
+
expect(unmountAIdx).toBeGreaterThanOrEqual(0);
|
|
753
|
+
expect(unmountBIdx).toBeGreaterThanOrEqual(0);
|
|
754
|
+
expect(unmountAIdx).toBeLessThan(deactivateIdx);
|
|
755
|
+
expect(unmountBIdx).toBeLessThan(deactivateIdx);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
706
758
|
describe('launchApp — onLayoutWillRestore / onLayoutRestored hooks', () => {
|
|
707
759
|
beforeEach(resetFramework);
|
|
708
760
|
it('calls onLayoutWillRestore before slot acquisition and onLayoutRestored after switchToApp', async () => {
|
package/dist/apps/types.d.ts
CHANGED
|
@@ -46,6 +46,15 @@ export interface AppManifest {
|
|
|
46
46
|
* starting shards (diagnostic, __sh3core__) stay running.
|
|
47
47
|
*/
|
|
48
48
|
requiredShards: string[];
|
|
49
|
+
/**
|
|
50
|
+
* Optional list of shard ids that ship in the same bundle as this app
|
|
51
|
+
* (combo packages). Mirrors the server-side `project-allowlist`
|
|
52
|
+
* middleware's resolution rule — these are part of the project-scope
|
|
53
|
+
* closure when the app is allowlisted, but they are NOT activated
|
|
54
|
+
* automatically (use `requiredShards` for that). Defaults to empty
|
|
55
|
+
* when omitted.
|
|
56
|
+
*/
|
|
57
|
+
bundledShards?: string[];
|
|
49
58
|
/**
|
|
50
59
|
* Bump to invalidate persisted layouts. On launch, a persisted blob
|
|
51
60
|
* whose version doesn't match this is discarded and `initialLayout`
|
|
@@ -89,16 +89,20 @@
|
|
|
89
89
|
function toggleFloatsSheet() {
|
|
90
90
|
if (floatsOpen) return;
|
|
91
91
|
floatsOpen = true;
|
|
92
|
-
|
|
92
|
+
sh3.modal.open(
|
|
93
93
|
FloatsSheet,
|
|
94
94
|
{},
|
|
95
|
-
{
|
|
95
|
+
{
|
|
96
|
+
dismissOnBackdrop: true,
|
|
97
|
+
boxStyle: 'max-width: 320px;',
|
|
98
|
+
// Reset on every dismissal path. Wrapping handle.close after
|
|
99
|
+
// open() returns doesn't work — ModalFrame captured the close
|
|
100
|
+
// reference at mount and bypasses any later monkey-patch.
|
|
101
|
+
onClose: () => {
|
|
102
|
+
floatsOpen = false;
|
|
103
|
+
},
|
|
104
|
+
},
|
|
96
105
|
);
|
|
97
|
-
const origClose = handle.close;
|
|
98
|
-
handle.close = () => {
|
|
99
|
-
origClose();
|
|
100
|
-
floatsOpen = false;
|
|
101
|
-
};
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
function toggleDrawer(anchor: DrawerAnchor) {
|
package/dist/createShell.js
CHANGED
|
@@ -23,6 +23,9 @@ import { attachGlobalListeners } from './actions/listeners';
|
|
|
23
23
|
import { detectSatelliteMode } from './boot/satelliteMode';
|
|
24
24
|
import { MemoryBackend } from './state/backends';
|
|
25
25
|
import { sessionState, readPendingScope, PENDING_SCOPE_KEY } from './projects/session-state.svelte';
|
|
26
|
+
import { projectsApi } from './projects-shard/projectsApi';
|
|
27
|
+
import { projectsState } from './projects-shard/projectsShard.svelte';
|
|
28
|
+
import { toastManager } from './overlays/toast';
|
|
26
29
|
import SatelliteShell from './satellite/SatelliteShell.svelte';
|
|
27
30
|
export async function createShell(config) {
|
|
28
31
|
var _a, _b;
|
|
@@ -87,6 +90,19 @@ export async function createShell(config) {
|
|
|
87
90
|
if (satellite.payload.projectId) {
|
|
88
91
|
sessionState.activeProjectId = satellite.payload.projectId;
|
|
89
92
|
}
|
|
93
|
+
// Mirror main-mode: eager-fetch the active project so the register
|
|
94
|
+
// gate computed in bootstrapSatellite has the allowlist. Fail-closed
|
|
95
|
+
// identically — drop to personal scope on error.
|
|
96
|
+
if (sessionState.activeProjectId !== null) {
|
|
97
|
+
try {
|
|
98
|
+
const record = await projectsApi.get(sessionState.activeProjectId);
|
|
99
|
+
projectsState.projects = [record];
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.warn(`[sh3] Satellite: failed to load project "${sessionState.activeProjectId}"; dropping to personal scope:`, err instanceof Error ? err.message : err);
|
|
103
|
+
sessionState.activeProjectId = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
90
106
|
__setScopeResolver(() => sessionState.activeProjectId);
|
|
91
107
|
__setShardScopeResolver(() => sessionState.activeProjectId ? 'project' : 'tenant');
|
|
92
108
|
if (config === null || config === void 0 ? void 0 : config.shards)
|
|
@@ -154,6 +170,30 @@ export async function createShell(config) {
|
|
|
154
170
|
}
|
|
155
171
|
}
|
|
156
172
|
}
|
|
173
|
+
// 4b. Eager-fetch the active project record so the register gate
|
|
174
|
+
// (computed in bootstrap()) has the appAllowlist available.
|
|
175
|
+
// Fail-closed: a failed fetch drops us to personal scope rather
|
|
176
|
+
// than booting with no gate (which would leak shards into the
|
|
177
|
+
// project). The user sees a toast on next paint.
|
|
178
|
+
if (sessionState.activeProjectId !== null) {
|
|
179
|
+
try {
|
|
180
|
+
const record = await projectsApi.get(sessionState.activeProjectId);
|
|
181
|
+
projectsState.projects = [record];
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
console.warn(`[sh3] Failed to load project "${sessionState.activeProjectId}"; dropping to personal scope:`, err instanceof Error ? err.message : err);
|
|
185
|
+
sessionState.activeProjectId = null;
|
|
186
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
187
|
+
sessionStorage.removeItem(PENDING_SCOPE_KEY);
|
|
188
|
+
}
|
|
189
|
+
queueMicrotask(() => {
|
|
190
|
+
try {
|
|
191
|
+
toastManager.notify('Could not load that project; dropped to personal scope.', { level: 'error', duration: 6000 });
|
|
192
|
+
}
|
|
193
|
+
catch ( /* overlay not ready; swallow */_a) { /* overlay not ready; swallow */ }
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
157
197
|
// 5. Load server-discovered packages
|
|
158
198
|
await loadDiscoveredPackages(config === null || config === void 0 ? void 0 : config.discoveredPackages);
|
|
159
199
|
// 6. Register consumer-provided shards and apps
|
|
@@ -44,6 +44,46 @@ beforeEach(() => {
|
|
|
44
44
|
});
|
|
45
45
|
describe('createDocumentPicker', () => {
|
|
46
46
|
const sampleDoc = { shardId: 'my-shard', path: 'readme.md', kind: 'file' };
|
|
47
|
+
describe('options forwarding to browser modal', () => {
|
|
48
|
+
it('threads listFolders/handle/readOnlyShard/initialShardId into the modal props', async () => {
|
|
49
|
+
const listFn = async () => [];
|
|
50
|
+
const listFolders = vi.fn(async () => ['empty']);
|
|
51
|
+
const handle = {
|
|
52
|
+
mkdir: vi.fn(async () => { }),
|
|
53
|
+
rmdir: vi.fn(async () => { }),
|
|
54
|
+
renameFolder: vi.fn(async () => { }),
|
|
55
|
+
rename: vi.fn(async () => { }),
|
|
56
|
+
delete: vi.fn(async () => { }),
|
|
57
|
+
};
|
|
58
|
+
const readOnlyShard = vi.fn(() => false);
|
|
59
|
+
const picker = createDocumentPicker(listFn, {
|
|
60
|
+
listFolders, handle, readOnlyShard,
|
|
61
|
+
initialShardId: 'svg-designer', lockToShard: true,
|
|
62
|
+
});
|
|
63
|
+
mockModal();
|
|
64
|
+
picker.save();
|
|
65
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
66
|
+
const props = mockModalOpen.mock.calls[0][1];
|
|
67
|
+
expect(props.listFolders).toBe(listFolders);
|
|
68
|
+
expect(props.handle).toBe(handle);
|
|
69
|
+
expect(props.readOnlyShard).toBe(readOnlyShard);
|
|
70
|
+
expect(props.initialShardId).toBe('svg-designer');
|
|
71
|
+
expect(props.lockToShard).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('omits options when none provided (backward-compatible single-arg call)', async () => {
|
|
74
|
+
const listFn = async () => [];
|
|
75
|
+
const picker = createDocumentPicker(listFn);
|
|
76
|
+
mockModal();
|
|
77
|
+
picker.open();
|
|
78
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
79
|
+
const props = mockModalOpen.mock.calls[0][1];
|
|
80
|
+
expect(props.listFolders).toBeUndefined();
|
|
81
|
+
expect(props.handle).toBeUndefined();
|
|
82
|
+
expect(props.readOnlyShard).toBeUndefined();
|
|
83
|
+
expect(props.initialShardId).toBeUndefined();
|
|
84
|
+
expect(props.lockToShard).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
47
87
|
describe('open() — modal (no anchor)', () => {
|
|
48
88
|
it('resolves with OpenerValue when user commits', async () => {
|
|
49
89
|
const listFn = async () => [{ shardId: 'my-shard', path: 'readme.md', size: 100, lastModified: 0 }];
|
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
import type { DocumentPickerApi, DocListFn } from './picker-api';
|
|
2
|
+
/**
|
|
3
|
+
* Folder mutation surface forwarded to the document browser modal.
|
|
4
|
+
* Mirrors the `handle` prop of `_DocumentBrowser.svelte`.
|
|
5
|
+
*/
|
|
6
|
+
export interface PickerHandleOps {
|
|
7
|
+
mkdir: (shardId: string, path: string) => Promise<void>;
|
|
8
|
+
rmdir: (shardId: string, path: string, opts: {
|
|
9
|
+
recursive: boolean;
|
|
10
|
+
}) => Promise<void>;
|
|
11
|
+
renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
12
|
+
rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
13
|
+
delete: (shardId: string, path: string) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Construction-time options for `createDocumentPicker`. All optional —
|
|
17
|
+
* absent values fall back to a read-only listing of `listFn`.
|
|
18
|
+
*/
|
|
19
|
+
export interface DocumentPickerOptions {
|
|
20
|
+
/** Enumerates immediate child folders of `prefix` in `shardId`. When set,
|
|
21
|
+
* empty folders (with no documents underneath) appear in the browser. */
|
|
22
|
+
listFolders?: (shardId: string, prefix: string) => Promise<string[]>;
|
|
23
|
+
/** Folder/file mutation surface. When set, the browser exposes a toolbar
|
|
24
|
+
* with new-folder / rename / delete actions (gated per shard by
|
|
25
|
+
* `readOnlyShard`). */
|
|
26
|
+
handle?: PickerHandleOps;
|
|
27
|
+
/** Returns true for shards the caller cannot mutate. The browser hides
|
|
28
|
+
* the edit toolbar while navigated into such shards. */
|
|
29
|
+
readOnlyShard?: (shardId: string) => boolean;
|
|
30
|
+
/** When set, the browser opens pre-navigated into this shard's namespace
|
|
31
|
+
* instead of the (often empty) tenant-wide shard list. The "SH3" crumb
|
|
32
|
+
* still lets the user back out — useful in browse mode. */
|
|
33
|
+
initialShardId?: string;
|
|
34
|
+
/** When true, the browser is hard-scoped to `initialShardId`: the "SH3"
|
|
35
|
+
* breadcrumb is hidden and Backspace at the shard root is a no-op.
|
|
36
|
+
* Use this for non-browse shards that can only ever work within their
|
|
37
|
+
* own namespace, so the user can't navigate into a dead-end root. */
|
|
38
|
+
lockToShard?: boolean;
|
|
39
|
+
}
|
|
2
40
|
/**
|
|
3
41
|
* Create a document picker API bound to a document listing function.
|
|
4
42
|
* The listFn is derived from the shard's document zone + browse permission
|
|
@@ -8,4 +46,4 @@ import type { DocumentPickerApi, DocListFn } from './picker-api';
|
|
|
8
46
|
* (anchored near the element). Without an anchor it opens as a centered
|
|
9
47
|
* modal (the expected default for file-browser dialogs).
|
|
10
48
|
*/
|
|
11
|
-
export declare function createDocumentPicker(listFn: DocListFn): DocumentPickerApi;
|
|
49
|
+
export declare function createDocumentPicker(listFn: DocListFn, options?: DocumentPickerOptions): DocumentPickerApi;
|
|
@@ -11,14 +11,15 @@ const MODAL_OPTS = { dismissOnBackdrop: true, boxStyle: BOX_STYLE };
|
|
|
11
11
|
* (anchored near the element). Without an anchor it opens as a centered
|
|
12
12
|
* modal (the expected default for file-browser dialogs).
|
|
13
13
|
*/
|
|
14
|
-
export function createDocumentPicker(listFn) {
|
|
15
|
-
|
|
14
|
+
export function createDocumentPicker(listFn, options = {}) {
|
|
15
|
+
const { listFolders, handle, readOnlyShard, initialShardId, lockToShard } = options;
|
|
16
16
|
function openBrowser(browserProps, anchor) {
|
|
17
|
+
const props = Object.assign(Object.assign({}, browserProps), { listFolders, handle, readOnlyShard, initialShardId, lockToShard });
|
|
17
18
|
if (anchor) {
|
|
18
19
|
const rect = anchor.getBoundingClientRect();
|
|
19
|
-
return sh3.popup.show(DocumentBrowser, { anchor: { x: rect.left + rect.width / 2, y: rect.top } },
|
|
20
|
+
return sh3.popup.show(DocumentBrowser, { anchor: { x: rect.left + rect.width / 2, y: rect.top } }, props);
|
|
20
21
|
}
|
|
21
|
-
return sh3.modal.open(DocumentBrowser,
|
|
22
|
+
return sh3.modal.open(DocumentBrowser, props, MODAL_OPTS);
|
|
22
23
|
}
|
|
23
24
|
function wrapHandle(handle, resolve) {
|
|
24
25
|
const origClose = handle.close;
|
package/dist/host.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* import-hygiene rule is: shards and apps import from `api.ts`, the host
|
|
16
16
|
* imports from `host.ts`.
|
|
17
17
|
*/
|
|
18
|
-
import { registerShard as registerShardInternal, registerAllShards, } from './shards/lifecycle.svelte';
|
|
18
|
+
import { registerShard as registerShardInternal, registerAllShards, listRegisteredShards, } from './shards/lifecycle.svelte';
|
|
19
19
|
import { registerApp, registeredApps } from './apps/registry.svelte';
|
|
20
20
|
import { launchApp, readLastApp, clearLastApp } from './apps/lifecycle';
|
|
21
21
|
import { sh3coreShard } from './sh3core-shard/sh3coreShard.svelte';
|
|
@@ -35,6 +35,9 @@ import { runModeIdRenameMigration } from './migrations/mode-id-rename';
|
|
|
35
35
|
import { setLifecycleHandlers } from './navigation/back-stack';
|
|
36
36
|
import { installWebEmitter } from './navigation/platform-web';
|
|
37
37
|
import { returnToHome } from './apps/lifecycle';
|
|
38
|
+
import { resolveAllowedShardIds } from './projects/scope-gate';
|
|
39
|
+
import { sessionState } from './projects/session-state.svelte';
|
|
40
|
+
import { projectsState } from './projects-shard/projectsShard.svelte';
|
|
38
41
|
export { __setBackend };
|
|
39
42
|
export { setLocalOwner };
|
|
40
43
|
export { __setActiveScope, __setDocumentBackend } from './documents/config';
|
|
@@ -58,6 +61,7 @@ function createWorkspaceZoneAdapter() {
|
|
|
58
61
|
};
|
|
59
62
|
}
|
|
60
63
|
export async function bootstrap(config) {
|
|
64
|
+
var _a;
|
|
61
65
|
// Run before anything touches the workspace zone so renamed keys are
|
|
62
66
|
// already in place when shards activate.
|
|
63
67
|
if (typeof globalThis.localStorage !== 'undefined') {
|
|
@@ -83,10 +87,22 @@ export async function bootstrap(config) {
|
|
|
83
87
|
}
|
|
84
88
|
// 3. Load any packages installed in a previous session from IndexedDB
|
|
85
89
|
await loadInstalledPackages();
|
|
86
|
-
// 4.
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
+
// 4. Compute the project-scope register gate. Null when in personal
|
|
91
|
+
// scope or when the active project has an empty allowlist; otherwise
|
|
92
|
+
// a Set of shard ids whose register() may run. Shards not in the
|
|
93
|
+
// set are skipped — they remain in `registeredShards` so the apps
|
|
94
|
+
// registry can still display their manifests, but their boot
|
|
95
|
+
// side-effects (verbs, contributions, gestures, network calls)
|
|
96
|
+
// are suppressed.
|
|
97
|
+
const activeProject = sessionState.activeProjectId
|
|
98
|
+
? (_a = projectsState.projects.find((p) => p.id === sessionState.activeProjectId)) !== null && _a !== void 0 ? _a : null
|
|
99
|
+
: null;
|
|
100
|
+
const allowed = resolveAllowedShardIds(activeProject, registeredApps, listRegisteredShards());
|
|
101
|
+
// 5. v3: run register(ctx) on every registered shard (gated by allowed).
|
|
102
|
+
// Lifecycle module handles error isolation; one failing shard does
|
|
103
|
+
// not block boot.
|
|
104
|
+
await registerAllShards(allowed);
|
|
105
|
+
// 6. Read the last-active app from the user zone. If auto-launch fails,
|
|
90
106
|
// clear the slot so the next reload lands on home instead of looping
|
|
91
107
|
// into the same failure. No toast — the user did not initiate this.
|
|
92
108
|
const lastId = readLastApp();
|
|
@@ -99,7 +115,7 @@ export async function bootstrap(config) {
|
|
|
99
115
|
clearLastApp();
|
|
100
116
|
}
|
|
101
117
|
}
|
|
102
|
-
//
|
|
118
|
+
// 7. Wire navigation lifecycle handlers and install the web back/forward
|
|
103
119
|
// emitter. Order: after autostart shards and the optional last-app
|
|
104
120
|
// launch, so the emitter's synthetic history entries don't interleave
|
|
105
121
|
// with boot-time launches. The window/history guard makes bootstrap
|
|
@@ -118,6 +134,7 @@ export async function bootstrap(config) {
|
|
|
118
134
|
* - lastApp auto-launch (satellites have no last-app concept)
|
|
119
135
|
*/
|
|
120
136
|
export async function bootstrapSatellite(config) {
|
|
137
|
+
var _a;
|
|
121
138
|
// 1. Framework-owned shards (same list as bootstrap, no excludes for satellites)
|
|
122
139
|
const frameworkShards = [sh3coreShard, shellShard, storeShard, adminShard, projectsShard, appearanceShard, layoutsShard];
|
|
123
140
|
for (const shard of frameworkShards) {
|
|
@@ -133,7 +150,13 @@ export async function bootstrapSatellite(config) {
|
|
|
133
150
|
// 4. v3: one-pass register sweep replaces the old autostart + satellite
|
|
134
151
|
// passes. `config.activateShardIds` is now a no-op (the field is
|
|
135
152
|
// retained on the interface for back-compat; Phase 6 deletes it).
|
|
153
|
+
// Satellites inherit the host's project scope, so apply the same
|
|
154
|
+
// register gate.
|
|
136
155
|
void config;
|
|
137
|
-
|
|
156
|
+
const activeProject = sessionState.activeProjectId
|
|
157
|
+
? (_a = projectsState.projects.find((p) => p.id === sessionState.activeProjectId)) !== null && _a !== void 0 ? _a : null
|
|
158
|
+
: null;
|
|
159
|
+
const allowed = resolveAllowedShardIds(activeProject, registeredApps, listRegisteredShards());
|
|
160
|
+
await registerAllShards(allowed);
|
|
138
161
|
}
|
|
139
162
|
export { installPackage, listInstalledPackages } from './registry/installer';
|
|
@@ -19,6 +19,17 @@ export declare function acquireSlotHost(slotId: string, viewId: string | null, l
|
|
|
19
19
|
* unconditional detach would yank the host out of its new home.
|
|
20
20
|
*/
|
|
21
21
|
export declare function releaseSlotHost(slotId: string, fromWrapper?: HTMLElement): void;
|
|
22
|
+
/**
|
|
23
|
+
* Synchronously drain `pendingDestroy`: for every slot id queued for
|
|
24
|
+
* deferred destruction, run the destroy body immediately if refcount is
|
|
25
|
+
* 0. Called by `unloadApp` so that all of the unloading app's views are
|
|
26
|
+
* unmounted before `onAppDeactivate` runs — preventing leaked `$derived`s
|
|
27
|
+
* in still-mounted views from tripping on state the shard nullifies.
|
|
28
|
+
*
|
|
29
|
+
* Safe to call when `pendingDestroy` is empty (no-op). Safe to call
|
|
30
|
+
* multiple times in succession.
|
|
31
|
+
*/
|
|
32
|
+
export declare function flushPendingDestroys(): void;
|
|
22
33
|
/**
|
|
23
34
|
* Test / teardown helper — destroys every pooled host immediately. Used
|
|
24
35
|
* by HMR boundaries and tests; not part of normal runtime flow.
|
|
@@ -279,23 +279,47 @@ export function releaseSlotHost(slotId, fromWrapper) {
|
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
281
281
|
pendingDestroy.add(slotId);
|
|
282
|
-
queueMicrotask(() =>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
282
|
+
queueMicrotask(() => destroyIfPending(slotId));
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Internal: run the destroy body for one slotId if it is still pending
|
|
286
|
+
* and its refcount is 0. Shared between the microtask deferred destroy
|
|
287
|
+
* and the synchronous `flushPendingDestroys`.
|
|
288
|
+
*/
|
|
289
|
+
function destroyIfPending(slotId) {
|
|
290
|
+
var _a, _b;
|
|
291
|
+
if (!pendingDestroy.has(slotId))
|
|
292
|
+
return;
|
|
293
|
+
pendingDestroy.delete(slotId);
|
|
294
|
+
const current = pool.get(slotId);
|
|
295
|
+
if (!current || current.refcount > 0)
|
|
296
|
+
return; // re-acquired, keep
|
|
297
|
+
(_a = current.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
298
|
+
__disposeSlotContributions(slotId);
|
|
299
|
+
(_b = current.handle) === null || _b === void 0 ? void 0 : _b.unmount();
|
|
300
|
+
current.cancelPendingMount();
|
|
301
|
+
current.host.remove();
|
|
302
|
+
pool.delete(slotId);
|
|
303
|
+
delete dirtyState[slotId];
|
|
304
|
+
delete closableState[slotId];
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Synchronously drain `pendingDestroy`: for every slot id queued for
|
|
308
|
+
* deferred destruction, run the destroy body immediately if refcount is
|
|
309
|
+
* 0. Called by `unloadApp` so that all of the unloading app's views are
|
|
310
|
+
* unmounted before `onAppDeactivate` runs — preventing leaked `$derived`s
|
|
311
|
+
* in still-mounted views from tripping on state the shard nullifies.
|
|
312
|
+
*
|
|
313
|
+
* Safe to call when `pendingDestroy` is empty (no-op). Safe to call
|
|
314
|
+
* multiple times in succession.
|
|
315
|
+
*/
|
|
316
|
+
export function flushPendingDestroys() {
|
|
317
|
+
// Snapshot the set so destroyIfPending's `pendingDestroy.delete(slotId)`
|
|
318
|
+
// doesn't mutate the iteration source.
|
|
319
|
+
const ids = [...pendingDestroy];
|
|
320
|
+
for (const slotId of ids) {
|
|
321
|
+
destroyIfPending(slotId);
|
|
322
|
+
}
|
|
299
323
|
}
|
|
300
324
|
/**
|
|
301
325
|
* Test / teardown helper — destroys every pooled host immediately. Used
|