sh3-core 0.13.3 → 0.14.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/dist/api.d.ts +3 -0
- package/dist/api.js +3 -0
- package/dist/app/store/StoreView.svelte +15 -4
- package/dist/app/store/permissionConfirm.js +1 -2
- package/dist/app/store/storeApp.js +0 -1
- package/dist/app/store/storeShard.svelte.js +9 -18
- package/dist/app/store/storeTypes.d.ts +21 -0
- package/dist/app/store/storeTypes.js +33 -0
- package/dist/app/store/storeTypes.test.d.ts +1 -0
- package/dist/app/store/storeTypes.test.js +41 -0
- package/dist/app/store/updatePackage.test.js +1 -1
- package/dist/app/store/verbs.test.js +20 -17
- package/dist/host.js +2 -0
- package/dist/migrations/mode-id-rename.d.ts +9 -0
- package/dist/migrations/mode-id-rename.js +39 -0
- package/dist/migrations/mode-id-rename.test.d.ts +1 -0
- package/dist/migrations/mode-id-rename.test.js +52 -0
- package/dist/overlays/FloatFrame.svelte +18 -1
- package/dist/overlays/float.d.ts +12 -0
- package/dist/overlays/float.js +16 -0
- package/dist/overlays/float.test.js +97 -2
- package/dist/overlays/modal.js +1 -0
- package/dist/overlays/modal.test.js +17 -0
- package/dist/overlays/parentHost.d.ts +1 -0
- package/dist/overlays/parentHost.js +15 -0
- package/dist/overlays/parentHost.test.d.ts +1 -0
- package/dist/overlays/parentHost.test.js +39 -0
- package/dist/overlays/popup.js +1 -0
- package/dist/overlays/popup.test.js +19 -0
- package/dist/shell-shard/Terminal.svelte +85 -8
- package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
- package/dist/shell-shard/contract.d.ts +65 -0
- package/dist/shell-shard/contract.js +11 -0
- package/dist/shell-shard/dispatch-custom.test.d.ts +1 -0
- package/dist/shell-shard/dispatch-custom.test.js +104 -0
- package/dist/shell-shard/dispatch.d.ts +14 -1
- package/dist/shell-shard/dispatch.js +58 -5
- package/dist/shell-shard/modes/builtin.d.ts +2 -2
- package/dist/shell-shard/modes/builtin.js +8 -8
- package/dist/shell-shard/modes/prefs.js +1 -1
- package/dist/shell-shard/modes/prefs.test.js +13 -13
- package/dist/shell-shard/modes/registry.test.js +13 -13
- package/dist/shell-shard/output.d.ts +3 -0
- package/dist/shell-shard/output.js +75 -0
- package/dist/shell-shard/output.test.d.ts +1 -0
- package/dist/shell-shard/output.test.js +54 -0
- package/dist/shell-shard/registerShellMode.d.ts +13 -0
- package/dist/shell-shard/registerShellMode.js +14 -0
- package/dist/shell-shard/registerShellMode.test.d.ts +1 -0
- package/dist/shell-shard/registerShellMode.test.js +19 -0
- package/dist/shell-shard/shellShard.svelte.js +8 -1
- package/dist/shell-shard/terminal-dispatch.test.js +9 -9
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte +11 -51
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte.d.ts +2 -4
- package/dist/shell-shard/toolbar/slots.test.js +6 -6
- package/dist/shell-shard/verbs/index.js +2 -0
- package/dist/shell-shard/verbs/mode.d.ts +2 -0
- package/dist/shell-shard/verbs/mode.js +28 -0
- package/dist/shell-shard/verbs/mode.test.d.ts +1 -0
- package/dist/shell-shard/verbs/mode.test.js +43 -0
- package/dist/verbs/types.d.ts +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/app/store/InstalledView.svelte +0 -255
- package/dist/app/store/InstalledView.svelte.d.ts +0 -3
package/dist/api.d.ts
CHANGED
|
@@ -50,6 +50,9 @@ export type { Verb, VerbContext, ShellApi } from './verbs/types';
|
|
|
50
50
|
export type { Scrollback } from './shell-shard/scrollback.svelte';
|
|
51
51
|
export type { SessionClient } from './shell-shard/session-client.svelte';
|
|
52
52
|
export { listVerbs } from './shards/registry';
|
|
53
|
+
export { registerShellMode } from './shell-shard/registerShellMode';
|
|
54
|
+
export type { ShellModeDescriptor, ShellModeOutput, ShellModeDispatchHandler, ShellModeDispatchInput, ShellModeRunsOn, RichEntryHandle, StreamHandle, } from './shell-shard/contract';
|
|
55
|
+
export { SHELL_MODE_CONTRIBUTION_POINT } from './shell-shard/contract';
|
|
53
56
|
export { VERSION } from './version';
|
|
54
57
|
export declare const FRAMEWORK_SHARD_IDS: readonly string[];
|
|
55
58
|
export { setTokenOverrides, clearTokenOverrides, getTokenOverrides, } from './theme';
|
package/dist/api.js
CHANGED
|
@@ -56,6 +56,9 @@ export const capabilities = {
|
|
|
56
56
|
hotInstall: typeof Blob !== 'undefined' && typeof URL.createObjectURL === 'function',
|
|
57
57
|
};
|
|
58
58
|
export { listVerbs } from './shards/registry';
|
|
59
|
+
// Shell mode contributions (external shards extend the shell with new modes).
|
|
60
|
+
export { registerShellMode } from './shell-shard/registerShellMode';
|
|
61
|
+
export { SHELL_MODE_CONTRIBUTION_POINT } from './shell-shard/contract';
|
|
59
62
|
// Package version.
|
|
60
63
|
export { VERSION } from './version';
|
|
61
64
|
// Framework shard IDs — shards that are always present (built-in to sh3-core).
|
|
@@ -17,9 +17,15 @@
|
|
|
17
17
|
import type { InstalledPackage } from '../../registry/types';
|
|
18
18
|
import { FRAMEWORK_SHARD_IDS } from '../../api';
|
|
19
19
|
import PermissionConfirmModal from './PermissionConfirmModal.svelte';
|
|
20
|
+
import {
|
|
21
|
+
displayPackageType,
|
|
22
|
+
displayPackageTypeLabel,
|
|
23
|
+
packageMatchesTypeFilter,
|
|
24
|
+
type PackageTypeFilter,
|
|
25
|
+
} from './storeTypes';
|
|
20
26
|
|
|
21
27
|
let search = $state('');
|
|
22
|
-
let typeFilter = $state<
|
|
28
|
+
let typeFilter = $state<PackageTypeFilter>('all');
|
|
23
29
|
let installingIds = $state<Set<string>>(new Set());
|
|
24
30
|
let updatingIds = $state<Set<string>>(new Set());
|
|
25
31
|
let installError = $state<string | null>(null);
|
|
@@ -70,7 +76,7 @@
|
|
|
70
76
|
const filtered = $derived.by(() => {
|
|
71
77
|
const q = search.toLowerCase().trim();
|
|
72
78
|
return ctx.state.ephemeral.catalog.filter((pkg: ResolvedPackage) => {
|
|
73
|
-
if (
|
|
79
|
+
if (!packageMatchesTypeFilter(pkg.entry.type, typeFilter)) return false;
|
|
74
80
|
if (!q) return true;
|
|
75
81
|
return (
|
|
76
82
|
pkg.entry.id.toLowerCase().includes(q) ||
|
|
@@ -312,6 +318,7 @@
|
|
|
312
318
|
{@const updatable = hasUpdate(pkg.entry.id)}
|
|
313
319
|
{@const updating = updatingIds.has(pkg.entry.id)}
|
|
314
320
|
{@const missing = missingShards(pkg, ctx.state.ephemeral.installed)}
|
|
321
|
+
{@const displayType = displayPackageType(pkg.entry.type)}
|
|
315
322
|
<div class="store-card">
|
|
316
323
|
<div class="store-card-header">
|
|
317
324
|
<div class="store-card-icon">
|
|
@@ -325,8 +332,12 @@
|
|
|
325
332
|
</div>
|
|
326
333
|
<div class="store-card-title">
|
|
327
334
|
<span class="store-card-label">{pkg.entry.label}</span>
|
|
328
|
-
<span
|
|
329
|
-
|
|
335
|
+
<span
|
|
336
|
+
class="store-card-badge"
|
|
337
|
+
class:badge-shard={displayType === 'shard'}
|
|
338
|
+
class:badge-app={displayType === 'app'}
|
|
339
|
+
>
|
|
340
|
+
{displayPackageTypeLabel(pkg.entry.type)}
|
|
330
341
|
</span>
|
|
331
342
|
<span class="store-card-version">{pkg.latest.version}</span>
|
|
332
343
|
</div>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Shared permission-diff confirmation flow for the `update` path. Opens
|
|
3
3
|
* PermissionConfirmModal via modalManager and resolves to the user's choice.
|
|
4
|
-
* Used by
|
|
5
|
-
* updates" context-menu action.
|
|
4
|
+
* Used by the home-card "Check for updates" context-menu action.
|
|
6
5
|
*/
|
|
7
6
|
import { modalManager } from '../../overlays/modal';
|
|
8
7
|
import PermissionConfirmModal from './PermissionConfirmModal.svelte';
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Store shard — framework-shipped shard for browsing and
|
|
3
|
-
*
|
|
2
|
+
* Store shard — framework-shipped shard for browsing and installing
|
|
3
|
+
* packages.
|
|
4
4
|
*
|
|
5
|
-
* Contributes
|
|
6
|
-
* - `sh3-store:browse`
|
|
7
|
-
*
|
|
5
|
+
* Contributes a single view:
|
|
6
|
+
* - `sh3-store:browse` — searchable/filterable catalog of available packages
|
|
7
|
+
*
|
|
8
|
+
* Uninstall and update flows for already-installed packages live on the
|
|
9
|
+
* shell home card's context menu (see `sh3core-shard/appActions.ts`); this
|
|
10
|
+
* shard exposes the underlying operations as verbs (`installVerb`,
|
|
11
|
+
* `uninstallVerb`, `updateVerb`, `appinfoVerb`) and via `storeContext`.
|
|
8
12
|
*
|
|
9
13
|
* Uses env state for registries (server-authoritative, admin-writable) and
|
|
10
14
|
* an ephemeral zone for the live catalog / installed list / loading / error state.
|
|
@@ -13,7 +17,6 @@
|
|
|
13
17
|
*/
|
|
14
18
|
import { mount, unmount } from 'svelte';
|
|
15
19
|
import StoreView from './StoreView.svelte';
|
|
16
|
-
import InstalledView from './InstalledView.svelte';
|
|
17
20
|
import { fetchRegistries, fetchBundle, fetchServerBundle, buildPackageMeta } from '../../registry/client';
|
|
18
21
|
import { installPackage, listInstalledPackages } from '../../registry/installer';
|
|
19
22
|
import { uninstallPackage as installerUninstallPackage } from '../../registry/installer';
|
|
@@ -83,7 +86,6 @@ export const storeShard = {
|
|
|
83
86
|
version: VERSION,
|
|
84
87
|
views: [
|
|
85
88
|
{ id: 'sh3-store:browse', label: 'Store' },
|
|
86
|
-
{ id: 'sh3-store:installed', label: 'Installed' },
|
|
87
89
|
],
|
|
88
90
|
},
|
|
89
91
|
activate(ctx) {
|
|
@@ -284,18 +286,7 @@ export const storeShard = {
|
|
|
284
286
|
};
|
|
285
287
|
},
|
|
286
288
|
};
|
|
287
|
-
const installedFactory = {
|
|
288
|
-
mount(container, _context) {
|
|
289
|
-
const instance = mount(InstalledView, { target: container });
|
|
290
|
-
return {
|
|
291
|
-
unmount() {
|
|
292
|
-
unmount(instance);
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
289
|
ctx.registerView('sh3-store:browse', browseFactory);
|
|
298
|
-
ctx.registerView('sh3-store:installed', installedFactory);
|
|
299
290
|
// Store verbs — registered as sh3-store:install, sh3-store:uninstall, sh3-store:appinfo
|
|
300
291
|
ctx.registerVerb(installVerb);
|
|
301
292
|
ctx.registerVerb(uninstallVerb);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Internal package types as carried by the registry index. */
|
|
2
|
+
export type PackageType = 'shard' | 'app' | 'combo';
|
|
3
|
+
/** User-visible package types — the registry triple collapsed to a pair. */
|
|
4
|
+
export type DisplayPackageType = 'shard' | 'app';
|
|
5
|
+
/** Type-filter values exposed by the browse-view dropdown. */
|
|
6
|
+
export type PackageTypeFilter = 'all' | DisplayPackageType;
|
|
7
|
+
/**
|
|
8
|
+
* Collapse the internal triple to the user-visible pair. Combo packages
|
|
9
|
+
* fold into `app`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function displayPackageType(type: PackageType): DisplayPackageType;
|
|
12
|
+
/**
|
|
13
|
+
* Title-cased label for the type chip. Returns `"Shard"` or `"App"` —
|
|
14
|
+
* never `"Combo"`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function displayPackageTypeLabel(type: PackageType): 'Shard' | 'App';
|
|
17
|
+
/**
|
|
18
|
+
* True if a package of `type` should be visible under the chosen filter.
|
|
19
|
+
* Combos pass under both `"all"` and `"app"` (never under `"shard"`).
|
|
20
|
+
*/
|
|
21
|
+
export declare function packageMatchesTypeFilter(type: PackageType, filter: PackageTypeFilter): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pure helpers that translate the registry's internal package-type triple
|
|
3
|
+
* (`shard | app | combo`) into the two-value vocabulary the store UI shows
|
|
4
|
+
* the user (`Shard | App`). `combo` is an internal distinction — a package
|
|
5
|
+
* that ships both a shard and an app surface — and from a user's standpoint
|
|
6
|
+
* a combo *is* an app, so it collapses to "App" everywhere user-facing.
|
|
7
|
+
*
|
|
8
|
+
* Kept as a separate module (rather than inline in StoreView) so the mapping
|
|
9
|
+
* is unit-testable without mounting Svelte components.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Collapse the internal triple to the user-visible pair. Combo packages
|
|
13
|
+
* fold into `app`.
|
|
14
|
+
*/
|
|
15
|
+
export function displayPackageType(type) {
|
|
16
|
+
return type === 'shard' ? 'shard' : 'app';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Title-cased label for the type chip. Returns `"Shard"` or `"App"` —
|
|
20
|
+
* never `"Combo"`.
|
|
21
|
+
*/
|
|
22
|
+
export function displayPackageTypeLabel(type) {
|
|
23
|
+
return displayPackageType(type) === 'shard' ? 'Shard' : 'App';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* True if a package of `type` should be visible under the chosen filter.
|
|
27
|
+
* Combos pass under both `"all"` and `"app"` (never under `"shard"`).
|
|
28
|
+
*/
|
|
29
|
+
export function packageMatchesTypeFilter(type, filter) {
|
|
30
|
+
if (filter === 'all')
|
|
31
|
+
return true;
|
|
32
|
+
return displayPackageType(type) === filter;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { displayPackageType, displayPackageTypeLabel, packageMatchesTypeFilter, } from './storeTypes';
|
|
3
|
+
describe('displayPackageType', () => {
|
|
4
|
+
it('returns "shard" for shard packages', () => {
|
|
5
|
+
expect(displayPackageType('shard')).toBe('shard');
|
|
6
|
+
});
|
|
7
|
+
it('returns "app" for app packages', () => {
|
|
8
|
+
expect(displayPackageType('app')).toBe('app');
|
|
9
|
+
});
|
|
10
|
+
it('collapses combo packages to "app"', () => {
|
|
11
|
+
expect(displayPackageType('combo')).toBe('app');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
describe('displayPackageTypeLabel', () => {
|
|
15
|
+
it('returns "Shard" for shard packages', () => {
|
|
16
|
+
expect(displayPackageTypeLabel('shard')).toBe('Shard');
|
|
17
|
+
});
|
|
18
|
+
it('returns "App" for app packages', () => {
|
|
19
|
+
expect(displayPackageTypeLabel('app')).toBe('App');
|
|
20
|
+
});
|
|
21
|
+
it('returns "App" for combo packages — the user-visible vocabulary never includes "combo"', () => {
|
|
22
|
+
expect(displayPackageTypeLabel('combo')).toBe('App');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('packageMatchesTypeFilter', () => {
|
|
26
|
+
it('matches every type when filter is "all"', () => {
|
|
27
|
+
expect(packageMatchesTypeFilter('shard', 'all')).toBe(true);
|
|
28
|
+
expect(packageMatchesTypeFilter('app', 'all')).toBe(true);
|
|
29
|
+
expect(packageMatchesTypeFilter('combo', 'all')).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
it('matches only shards when filter is "shard"', () => {
|
|
32
|
+
expect(packageMatchesTypeFilter('shard', 'shard')).toBe(true);
|
|
33
|
+
expect(packageMatchesTypeFilter('app', 'shard')).toBe(false);
|
|
34
|
+
expect(packageMatchesTypeFilter('combo', 'shard')).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it('matches both apps and combos when filter is "app"', () => {
|
|
37
|
+
expect(packageMatchesTypeFilter('shard', 'app')).toBe(false);
|
|
38
|
+
expect(packageMatchesTypeFilter('app', 'app')).toBe(true);
|
|
39
|
+
expect(packageMatchesTypeFilter('combo', 'app')).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* updatePackage() — version-aware unit tests. We exercise the version
|
|
3
3
|
* resolution branch only; the full flow (server install + permission diff)
|
|
4
|
-
* is covered indirectly through
|
|
4
|
+
* is covered indirectly through the home-card update-action tests.
|
|
5
5
|
*/
|
|
6
6
|
import { describe, it, expect } from 'vitest';
|
|
7
7
|
import { pickVersion } from './storeShard.svelte';
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
const { mockStoreContext } = vi.hoisted(() => ({
|
|
3
|
+
mockStoreContext: {
|
|
4
|
+
state: { ephemeral: { installed: [] } },
|
|
5
|
+
updatePackage: vi.fn(),
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('./storeShard.svelte', () => ({
|
|
9
|
+
storeContext: mockStoreContext,
|
|
10
|
+
}));
|
|
2
11
|
import { updateVerb } from './verbs';
|
|
3
|
-
import * as shard from './storeShard.svelte';
|
|
4
12
|
function mkCtx() {
|
|
5
13
|
const lines = [];
|
|
6
14
|
return {
|
|
@@ -11,6 +19,10 @@ function mkCtx() {
|
|
|
11
19
|
};
|
|
12
20
|
}
|
|
13
21
|
describe('updateVerb', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mockStoreContext.state = { ephemeral: { installed: [] } };
|
|
24
|
+
mockStoreContext.updatePackage = vi.fn();
|
|
25
|
+
});
|
|
14
26
|
it('warns when no id is given', async () => {
|
|
15
27
|
var _a;
|
|
16
28
|
const { ctx, lines } = mkCtx();
|
|
@@ -19,11 +31,8 @@ describe('updateVerb', () => {
|
|
|
19
31
|
});
|
|
20
32
|
it('delegates to storeContext.updatePackage when version omitted', async () => {
|
|
21
33
|
const fake = vi.fn().mockResolvedValue(undefined);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
state: { ephemeral: { installed: [{ id: 'foo', version: '1.0.0' }] } },
|
|
25
|
-
updatePackage: fake,
|
|
26
|
-
};
|
|
34
|
+
mockStoreContext.state = { ephemeral: { installed: [{ id: 'foo', version: '1.0.0' }] } };
|
|
35
|
+
mockStoreContext.updatePackage = fake;
|
|
27
36
|
const { ctx, lines } = mkCtx();
|
|
28
37
|
await updateVerb.run(ctx, ['foo']);
|
|
29
38
|
expect(fake).toHaveBeenCalledWith('foo', expect.any(Function), undefined);
|
|
@@ -31,22 +40,16 @@ describe('updateVerb', () => {
|
|
|
31
40
|
});
|
|
32
41
|
it('passes version through when provided', async () => {
|
|
33
42
|
const fake = vi.fn().mockResolvedValue(undefined);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
state: { ephemeral: { installed: [{ id: 'foo', version: '2.0.0' }] } },
|
|
37
|
-
updatePackage: fake,
|
|
38
|
-
};
|
|
43
|
+
mockStoreContext.state = { ephemeral: { installed: [{ id: 'foo', version: '2.0.0' }] } };
|
|
44
|
+
mockStoreContext.updatePackage = fake;
|
|
39
45
|
const { ctx } = mkCtx();
|
|
40
46
|
await updateVerb.run(ctx, ['foo', '1.0.0']);
|
|
41
47
|
expect(fake).toHaveBeenCalledWith('foo', expect.any(Function), '1.0.0');
|
|
42
48
|
});
|
|
43
49
|
it('reports failure as error scrollback line', async () => {
|
|
44
50
|
const fake = vi.fn().mockRejectedValue(new Error('boom'));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
state: { ephemeral: { installed: [{ id: 'foo', version: '1.0.0' }] } },
|
|
48
|
-
updatePackage: fake,
|
|
49
|
-
};
|
|
51
|
+
mockStoreContext.state = { ephemeral: { installed: [{ id: 'foo', version: '1.0.0' }] } };
|
|
52
|
+
mockStoreContext.updatePackage = fake;
|
|
50
53
|
const { ctx, lines } = mkCtx();
|
|
51
54
|
await updateVerb.run(ctx, ['foo']);
|
|
52
55
|
const last = lines.at(-1);
|
package/dist/host.js
CHANGED
|
@@ -31,6 +31,7 @@ import { storeApp } from './app/store/storeApp';
|
|
|
31
31
|
import { adminShard } from './app/admin/adminShard.svelte';
|
|
32
32
|
import { adminApp } from './app/admin/adminApp';
|
|
33
33
|
import { runShellRenameMigration, } from './migrations/shell-rename';
|
|
34
|
+
import { runModeIdRenameMigration } from './migrations/mode-id-rename';
|
|
34
35
|
import { setLifecycleHandlers } from './navigation/back-stack';
|
|
35
36
|
import { installWebEmitter } from './navigation/platform-web';
|
|
36
37
|
import { returnToHome } from './apps/lifecycle';
|
|
@@ -60,6 +61,7 @@ export async function bootstrap(config) {
|
|
|
60
61
|
// already in place when shards activate.
|
|
61
62
|
if (typeof globalThis.localStorage !== 'undefined') {
|
|
62
63
|
runShellRenameMigration(createWorkspaceZoneAdapter(), globalThis.localStorage);
|
|
64
|
+
runModeIdRenameMigration(globalThis.localStorage);
|
|
63
65
|
// Per ADR-002 amendment, app workspace state is keyed by (scopeId, appId).
|
|
64
66
|
// Rewrite legacy unkeyed entries to the personal scope namespace.
|
|
65
67
|
const { migrateLegacyWorkspaceKeys } = await import('./apps/workspace-rekey');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface MinimalStorage {
|
|
2
|
+
getItem(key: string): string | null;
|
|
3
|
+
setItem(key: string, value: string): void;
|
|
4
|
+
removeItem(key: string): void;
|
|
5
|
+
}
|
|
6
|
+
export declare function runModeIdRenameMigration(storage: MinimalStorage & {
|
|
7
|
+
_keys?: () => string[];
|
|
8
|
+
}): void;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* One-shot migration: rewrites persisted shell-mode preferences from the
|
|
3
|
+
* pre-rename ids (`dev`, `user`) to the new ids (`bash`, `sh3`). Idempotent —
|
|
4
|
+
* gated by a localStorage flag, safe to call on every boot.
|
|
5
|
+
*
|
|
6
|
+
* Persistence shape: localStorage keys of the form `sh3.shell.lastMode.<userId>`
|
|
7
|
+
* (see packages/sh3-core/src/shell-shard/modes/prefs.ts).
|
|
8
|
+
*/
|
|
9
|
+
const FLAG_KEY = 'sh3:migrations:mode-id-rename:done';
|
|
10
|
+
const KEY_PREFIX = 'sh3.shell.lastMode.';
|
|
11
|
+
const REWRITES = { dev: 'bash', user: 'sh3' };
|
|
12
|
+
function listKeys(storage) {
|
|
13
|
+
if (typeof storage._keys === 'function')
|
|
14
|
+
return storage._keys();
|
|
15
|
+
const ls = storage;
|
|
16
|
+
if (typeof ls.length === 'number' && typeof ls.key === 'function') {
|
|
17
|
+
const out = [];
|
|
18
|
+
for (let i = 0; i < ls.length; i++) {
|
|
19
|
+
const k = ls.key(i);
|
|
20
|
+
if (k !== null)
|
|
21
|
+
out.push(k);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
export function runModeIdRenameMigration(storage) {
|
|
28
|
+
if (storage.getItem(FLAG_KEY))
|
|
29
|
+
return;
|
|
30
|
+
for (const key of listKeys(storage)) {
|
|
31
|
+
if (!key.startsWith(KEY_PREFIX))
|
|
32
|
+
continue;
|
|
33
|
+
const value = storage.getItem(key);
|
|
34
|
+
if (value && Object.prototype.hasOwnProperty.call(REWRITES, value)) {
|
|
35
|
+
storage.setItem(key, REWRITES[value]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
storage.setItem(FLAG_KEY, '1');
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { runModeIdRenameMigration } from './mode-id-rename';
|
|
3
|
+
function makeStorage(initial = {}) {
|
|
4
|
+
const map = new Map(Object.entries(initial));
|
|
5
|
+
return {
|
|
6
|
+
getItem: (k) => (map.has(k) ? map.get(k) : null),
|
|
7
|
+
setItem: (k, v) => { map.set(k, v); },
|
|
8
|
+
removeItem: (k) => { map.delete(k); },
|
|
9
|
+
_dump: () => Object.fromEntries(map),
|
|
10
|
+
_keys: () => [...map.keys()],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
describe('runModeIdRenameMigration', () => {
|
|
14
|
+
it('rewrites dev → bash for every user-keyed pref', () => {
|
|
15
|
+
const s = makeStorage({
|
|
16
|
+
'sh3.shell.lastMode.alice': 'dev',
|
|
17
|
+
'sh3.shell.lastMode.bob': 'dev',
|
|
18
|
+
});
|
|
19
|
+
runModeIdRenameMigration(s);
|
|
20
|
+
expect(s._dump()).toMatchObject({
|
|
21
|
+
'sh3.shell.lastMode.alice': 'bash',
|
|
22
|
+
'sh3.shell.lastMode.bob': 'bash',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
it('rewrites user → sh3 for every user-keyed pref', () => {
|
|
26
|
+
const s = makeStorage({ 'sh3.shell.lastMode.alice': 'user' });
|
|
27
|
+
runModeIdRenameMigration(s);
|
|
28
|
+
expect(s.getItem('sh3.shell.lastMode.alice')).toBe('sh3');
|
|
29
|
+
});
|
|
30
|
+
it('leaves unknown values untouched', () => {
|
|
31
|
+
const s = makeStorage({ 'sh3.shell.lastMode.alice': 'gemini' });
|
|
32
|
+
runModeIdRenameMigration(s);
|
|
33
|
+
expect(s.getItem('sh3.shell.lastMode.alice')).toBe('gemini');
|
|
34
|
+
});
|
|
35
|
+
it('is idempotent (gated by a done flag)', () => {
|
|
36
|
+
const s = makeStorage({ 'sh3.shell.lastMode.alice': 'dev' });
|
|
37
|
+
runModeIdRenameMigration(s);
|
|
38
|
+
s.setItem('sh3.shell.lastMode.alice', 'dev');
|
|
39
|
+
runModeIdRenameMigration(s);
|
|
40
|
+
expect(s.getItem('sh3.shell.lastMode.alice')).toBe('dev');
|
|
41
|
+
});
|
|
42
|
+
it('ignores unrelated keys', () => {
|
|
43
|
+
const s = makeStorage({
|
|
44
|
+
'sh3.shell.lastMode.alice': 'dev',
|
|
45
|
+
'sh3.unrelated': 'dev',
|
|
46
|
+
'random': 'user',
|
|
47
|
+
});
|
|
48
|
+
runModeIdRenameMigration(s);
|
|
49
|
+
expect(s.getItem('sh3.unrelated')).toBe('dev');
|
|
50
|
+
expect(s.getItem('random')).toBe('user');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
-->
|
|
18
18
|
<script lang="ts">
|
|
19
19
|
import LayoutRenderer from '../layout/LayoutRenderer.svelte';
|
|
20
|
-
import { floatManager } from './float';
|
|
20
|
+
import { floatManager, getFloatParentHost } from './float';
|
|
21
21
|
import { registerDismissableFrame, unregisterDismissableFrame } from './floatDismiss';
|
|
22
22
|
import type { FloatEntry } from '../layout/types';
|
|
23
23
|
|
|
@@ -37,6 +37,22 @@
|
|
|
37
37
|
return () => unregisterDismissableFrame(entry.id);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
// Portal the frame into the anchor's enclosing overlay host when one was
|
|
41
|
+
// resolved at open() time. This puts the frame inside the opener's
|
|
42
|
+
// stacking context — so a picker opened from inside a modal stacks above
|
|
43
|
+
// that modal without writing any z-index. The Svelte component lifecycle
|
|
44
|
+
// is unaffected; we're only relocating the rendered DOM node.
|
|
45
|
+
$effect(() => {
|
|
46
|
+
if (!frameEl) return;
|
|
47
|
+
const host = getFloatParentHost(entry.id);
|
|
48
|
+
if (!host) return;
|
|
49
|
+
const original = frameEl.parentNode;
|
|
50
|
+
host.appendChild(frameEl);
|
|
51
|
+
return () => {
|
|
52
|
+
if (frameEl?.parentNode === host && original) original.appendChild(frameEl);
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
40
56
|
function onHeaderPointerDown(e: PointerEvent): void {
|
|
41
57
|
if (e.button !== 0) return;
|
|
42
58
|
if ((e.target as HTMLElement).closest('.sh3-float-close')) return;
|
|
@@ -76,6 +92,7 @@
|
|
|
76
92
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
77
93
|
<div
|
|
78
94
|
class="sh3-float-frame"
|
|
95
|
+
data-shell-overlay-host="float"
|
|
79
96
|
bind:this={frameEl}
|
|
80
97
|
style:left="{entry.position.x}px"
|
|
81
98
|
style:top="{entry.position.y}px"
|
package/dist/overlays/float.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ export interface FloatOptions {
|
|
|
15
15
|
* See docs/superpowers/specs/2026-04-21-dismissable-float-design.md.
|
|
16
16
|
*/
|
|
17
17
|
dismissable?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* For `dismissable` floats only: anchor element used to determine the
|
|
20
|
+
* mount host. When the anchor is inside another overlay (modal, popup,
|
|
21
|
+
* float frame), the float frame is portaled into that host so it stacks
|
|
22
|
+
* above its opener instead of sitting at layer 1. Without an anchor —
|
|
23
|
+
* or for non-dismissable floats — the frame renders at the FloatLayer
|
|
24
|
+
* root as usual. The anchor isn't stored on FloatEntry (HTMLElement
|
|
25
|
+
* isn't serializable through the workspace-zone proxy); only the
|
|
26
|
+
* resolved parent host is, in a sidecar map keyed by float id.
|
|
27
|
+
*/
|
|
28
|
+
anchor?: HTMLElement;
|
|
18
29
|
}
|
|
19
30
|
export interface FloatManager {
|
|
20
31
|
open(viewId: string, options?: FloatOptions): string;
|
|
@@ -34,4 +45,5 @@ export declare function bindFloatStore(floats: FloatEntry[], getBounds: () => {
|
|
|
34
45
|
export declare function unbindFloatStore(): void;
|
|
35
46
|
/** Test-only reset. Clears in-memory fallback and unbinds any store. */
|
|
36
47
|
export declare function __resetFloatManagerForTest(): void;
|
|
48
|
+
export declare function getFloatParentHost(id: string): HTMLElement | undefined;
|
|
37
49
|
export declare const floatManager: FloatManager;
|
package/dist/overlays/float.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* and the pre-boot state.
|
|
28
28
|
*/
|
|
29
29
|
import { computeMinSize, cascadePosition, generateFloatId } from '../layout/floats';
|
|
30
|
+
import { findEnclosingOverlayHost } from './parentHost';
|
|
30
31
|
// ----- storage binding ---------------------------------------------------
|
|
31
32
|
let fallbackFloats = [];
|
|
32
33
|
let boundFloats = null;
|
|
@@ -49,10 +50,19 @@ export function __resetFloatManagerForTest() {
|
|
|
49
50
|
fallbackFloats = [];
|
|
50
51
|
boundFloats = null;
|
|
51
52
|
getTreeBounds = () => ({ w: 1600, h: 900 });
|
|
53
|
+
parentHosts.clear();
|
|
52
54
|
}
|
|
53
55
|
function activeStore() {
|
|
54
56
|
return boundFloats !== null && boundFloats !== void 0 ? boundFloats : fallbackFloats;
|
|
55
57
|
}
|
|
58
|
+
// ----- parent host sidecar ------------------------------------------------
|
|
59
|
+
// HTMLElement can't live on FloatEntry (workspace-zone proxy state), so the
|
|
60
|
+
// resolved parent host is stored here keyed by float id and consumed by
|
|
61
|
+
// FloatFrame to portal the rendered DOM into the opener's stacking context.
|
|
62
|
+
const parentHosts = new Map();
|
|
63
|
+
export function getFloatParentHost(id) {
|
|
64
|
+
return parentHosts.get(id);
|
|
65
|
+
}
|
|
56
66
|
// ----- slot id minting ---------------------------------------------------
|
|
57
67
|
let floatSlotCounter = 0;
|
|
58
68
|
function mintFloatSlotId(viewId) {
|
|
@@ -107,6 +117,11 @@ function openFloat(viewId, options = {}) {
|
|
|
107
117
|
};
|
|
108
118
|
if (options.dismissable)
|
|
109
119
|
entry.dismissable = true;
|
|
120
|
+
if (options.dismissable && options.anchor) {
|
|
121
|
+
const host = findEnclosingOverlayHost(options.anchor);
|
|
122
|
+
if (host)
|
|
123
|
+
parentHosts.set(id, host);
|
|
124
|
+
}
|
|
110
125
|
store.push(entry);
|
|
111
126
|
return id;
|
|
112
127
|
}
|
|
@@ -116,6 +131,7 @@ function closeFloat(floatId) {
|
|
|
116
131
|
if (idx < 0)
|
|
117
132
|
return;
|
|
118
133
|
store.splice(idx, 1);
|
|
134
|
+
parentHosts.delete(floatId);
|
|
119
135
|
}
|
|
120
136
|
function listFloats() {
|
|
121
137
|
// Return a snapshot so callers can iterate without racing mutations.
|