sh3-core 0.13.2 → 0.13.4
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/actions/MenuButton.svelte +2 -1
- package/dist/actions/contextMenuModel.d.ts +1 -1
- package/dist/actions/contextMenuModel.js +2 -1
- package/dist/actions/dispatcher.svelte.d.ts +1 -1
- package/dist/actions/dispatcher.svelte.js +2 -1
- package/dist/actions/listActive.d.ts +1 -1
- package/dist/actions/listActive.js +2 -1
- package/dist/actions/listeners.d.ts +1 -1
- package/dist/actions/listeners.js +6 -5
- package/dist/actions/menuBarModel.js +3 -2
- package/dist/actions/paletteModel.js +2 -1
- package/dist/actions/resolveLabel.test.d.ts +1 -0
- package/dist/actions/resolveLabel.test.js +14 -0
- package/dist/actions/types.d.ts +12 -1
- package/dist/actions/types.js +7 -1
- package/dist/app/store/AppUpdateAvailableModal.svelte +87 -0
- package/dist/app/store/AppUpdateAvailableModal.svelte.d.ts +11 -0
- package/dist/app/store/StoreView.svelte +15 -4
- package/dist/app/store/UninstallAppDialog.svelte +86 -0
- package/dist/app/store/UninstallAppDialog.svelte.d.ts +10 -0
- package/dist/app/store/permissionConfirm.d.ts +4 -0
- package/dist/app/store/permissionConfirm.js +27 -0
- package/dist/app/store/storeApp.js +0 -1
- package/dist/app/store/storeShard.svelte.d.ts +8 -1
- package/dist/app/store/storeShard.svelte.js +51 -27
- 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.d.ts +1 -0
- package/dist/app/store/updatePackage.test.js +34 -0
- package/dist/app/store/verbs.d.ts +1 -0
- package/dist/app/store/verbs.js +79 -5
- package/dist/app/store/verbs.test.d.ts +1 -0
- package/dist/app/store/verbs.test.js +59 -0
- package/dist/app-appearance/AppAppearanceModal.svelte +174 -0
- package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +8 -0
- package/dist/app-appearance/appearanceShard.svelte.d.ts +2 -0
- package/dist/app-appearance/appearanceShard.svelte.js +61 -0
- package/dist/app-appearance/appearanceState.svelte.d.ts +15 -0
- package/dist/app-appearance/appearanceState.svelte.js +59 -0
- package/dist/app-appearance/appearanceState.test.d.ts +1 -0
- package/dist/app-appearance/appearanceState.test.js +30 -0
- package/dist/app-appearance/index.d.ts +3 -0
- package/dist/app-appearance/index.js +2 -0
- package/dist/app-appearance/types.d.ts +11 -0
- package/dist/app-appearance/types.js +1 -0
- package/dist/apps/types.d.ts +7 -0
- package/dist/assets/iconIds.generated.d.ts +2 -0
- package/dist/assets/iconIds.generated.js +154 -0
- package/dist/host.js +2 -1
- 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/primitives/widgets/IconPicker.svelte +115 -0
- package/dist/primitives/widgets/IconPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/IconPicker.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/IconPicker.svelte.test.js +43 -0
- package/dist/projects-shard/ProjectManage.svelte +14 -4
- package/dist/sh3core-shard/ShellHome.svelte +64 -38
- package/dist/sh3core-shard/appActions.d.ts +13 -0
- package/dist/sh3core-shard/appActions.js +181 -0
- package/dist/sh3core-shard/appActions.test.d.ts +1 -0
- package/dist/sh3core-shard/appActions.test.js +25 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/app/store/InstalledView.svelte +0 -301
- package/dist/app/store/InstalledView.svelte.d.ts +0 -3
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `__app-appearance__` shard — owns the user-zone for per-app overrides
|
|
3
|
+
* and registers the `app.customize` element-scope action. The state
|
|
4
|
+
* itself lives in appearanceState.svelte.ts (so unit tests don't have
|
|
5
|
+
* to boot a real ShardContext). This file binds the zone on activate
|
|
6
|
+
* and unbinds on deactivate, and contributes the action.
|
|
7
|
+
*/
|
|
8
|
+
import { VERSION } from '../version';
|
|
9
|
+
import { listRegisteredApps } from '../api';
|
|
10
|
+
import { getSelection } from '../actions/selection.svelte';
|
|
11
|
+
import { modalManager } from '../overlays/modal';
|
|
12
|
+
import AppAppearanceModal from './AppAppearanceModal.svelte';
|
|
13
|
+
import { __bindZone, __unbindZone, } from './appearanceState.svelte';
|
|
14
|
+
function readSelection() {
|
|
15
|
+
const sel = getSelection();
|
|
16
|
+
if (!sel || sel.type !== 'app')
|
|
17
|
+
return null;
|
|
18
|
+
return sel.ref;
|
|
19
|
+
}
|
|
20
|
+
function runCustomize(_ctx) {
|
|
21
|
+
var _a;
|
|
22
|
+
const ref = readSelection();
|
|
23
|
+
if (!ref)
|
|
24
|
+
return;
|
|
25
|
+
const m = listRegisteredApps().find((x) => x.id === ref.appId);
|
|
26
|
+
const props = {
|
|
27
|
+
appId: ref.appId,
|
|
28
|
+
appLabel: (_a = m === null || m === void 0 ? void 0 : m.label) !== null && _a !== void 0 ? _a : ref.appId,
|
|
29
|
+
};
|
|
30
|
+
modalManager.open(AppAppearanceModal, props);
|
|
31
|
+
}
|
|
32
|
+
export const appearanceShard = {
|
|
33
|
+
manifest: {
|
|
34
|
+
id: '__app-appearance__',
|
|
35
|
+
label: 'App Appearance',
|
|
36
|
+
version: VERSION,
|
|
37
|
+
views: [],
|
|
38
|
+
},
|
|
39
|
+
activate(ctx) {
|
|
40
|
+
const zone = ctx.state({
|
|
41
|
+
user: { overrides: {} },
|
|
42
|
+
});
|
|
43
|
+
__bindZone(zone);
|
|
44
|
+
const customize = {
|
|
45
|
+
id: 'app.customize',
|
|
46
|
+
label: 'Customize…',
|
|
47
|
+
scope: { element: 'app' },
|
|
48
|
+
contextItem: true,
|
|
49
|
+
group: 'appearance',
|
|
50
|
+
run: runCustomize,
|
|
51
|
+
};
|
|
52
|
+
ctx.actions.register(customize);
|
|
53
|
+
},
|
|
54
|
+
autostart() {
|
|
55
|
+
// Self-start so the `app.customize` action is registered before the
|
|
56
|
+
// user right-clicks a home card. No imperative work required.
|
|
57
|
+
},
|
|
58
|
+
deactivate() {
|
|
59
|
+
__unbindZone();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StateZones } from '../state/zones.svelte';
|
|
2
|
+
import type { AppAppearance } from './types';
|
|
3
|
+
export interface AppearanceZoneSchema {
|
|
4
|
+
user: {
|
|
5
|
+
overrides: Record<string, AppAppearance>;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** Bind the shard's zone to this module. Called by the shard's activate. */
|
|
9
|
+
export declare function __bindZone(s: StateZones<AppearanceZoneSchema>): void;
|
|
10
|
+
/** Unbind the zone (deactivate). */
|
|
11
|
+
export declare function __unbindZone(): void;
|
|
12
|
+
export declare function getAppearance(appId: string): AppAppearance | undefined;
|
|
13
|
+
export declare function setAppearance(appId: string, value: AppAppearance | undefined): void;
|
|
14
|
+
/** Test-only: replace the bound zone with a memory shim. */
|
|
15
|
+
export declare function __resetForTests(): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Per-user-per-browser visual overrides for apps. The store + helpers
|
|
3
|
+
* live separately from the shard so the state can be unit-tested without
|
|
4
|
+
* booting the shard system, and so the AppAppearanceModal can import
|
|
5
|
+
* get/set without creating an import cycle through the shard's modal
|
|
6
|
+
* import.
|
|
7
|
+
*
|
|
8
|
+
* EXPLICITLY TEMPORARY. A future ADR is expected to add icon/color
|
|
9
|
+
* fields to the app manifest itself.
|
|
10
|
+
*/
|
|
11
|
+
let zoneState = null;
|
|
12
|
+
/** Bind the shard's zone to this module. Called by the shard's activate. */
|
|
13
|
+
export function __bindZone(s) {
|
|
14
|
+
zoneState = s;
|
|
15
|
+
}
|
|
16
|
+
/** Unbind the zone (deactivate). */
|
|
17
|
+
export function __unbindZone() {
|
|
18
|
+
zoneState = null;
|
|
19
|
+
}
|
|
20
|
+
function isEmpty(v) {
|
|
21
|
+
return v.icon === undefined && v.color === undefined && v.label === undefined;
|
|
22
|
+
}
|
|
23
|
+
export function getAppearance(appId) {
|
|
24
|
+
const map = zoneState === null || zoneState === void 0 ? void 0 : zoneState.user.overrides;
|
|
25
|
+
if (!map)
|
|
26
|
+
return undefined;
|
|
27
|
+
const v = map[appId];
|
|
28
|
+
if (!v)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (isEmpty(v))
|
|
31
|
+
return undefined;
|
|
32
|
+
return v;
|
|
33
|
+
}
|
|
34
|
+
export function setAppearance(appId, value) {
|
|
35
|
+
if (!zoneState)
|
|
36
|
+
return;
|
|
37
|
+
const map = zoneState.user.overrides;
|
|
38
|
+
const next = Object.assign({}, map);
|
|
39
|
+
if (value === undefined || isEmpty(value)) {
|
|
40
|
+
delete next[appId];
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
next[appId] = value;
|
|
44
|
+
}
|
|
45
|
+
zoneState.user.overrides = next;
|
|
46
|
+
}
|
|
47
|
+
/** Test-only: replace the bound zone with a memory shim. */
|
|
48
|
+
export function __resetForTests() {
|
|
49
|
+
zoneState = {
|
|
50
|
+
ephemeral: {},
|
|
51
|
+
session: {},
|
|
52
|
+
workspace: {},
|
|
53
|
+
user: { overrides: {} },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Initialise the memory shim at module load so tests can call set/get
|
|
57
|
+
// without first invoking activate. Production replaces this via
|
|
58
|
+
// __bindZone() inside appearanceShard.activate().
|
|
59
|
+
__resetForTests();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { setAppearance, getAppearance, __resetForTests } from './appearanceState.svelte';
|
|
3
|
+
describe('appearanceShard get/set', () => {
|
|
4
|
+
beforeEach(() => __resetForTests());
|
|
5
|
+
it('returns undefined for an unset app', () => {
|
|
6
|
+
expect(getAppearance('foo')).toBeUndefined();
|
|
7
|
+
});
|
|
8
|
+
it('round-trips an icon override', () => {
|
|
9
|
+
setAppearance('foo', { icon: 'house' });
|
|
10
|
+
expect(getAppearance('foo')).toEqual({ icon: 'house' });
|
|
11
|
+
});
|
|
12
|
+
it('round-trips a color override', () => {
|
|
13
|
+
setAppearance('foo', { color: '#ff0000' });
|
|
14
|
+
expect(getAppearance('foo')).toEqual({ color: '#ff0000' });
|
|
15
|
+
});
|
|
16
|
+
it('round-trips a label override', () => {
|
|
17
|
+
setAppearance('foo', { label: 'My Label' });
|
|
18
|
+
expect(getAppearance('foo')).toEqual({ label: 'My Label' });
|
|
19
|
+
});
|
|
20
|
+
it('clears the entry when all fields are undefined', () => {
|
|
21
|
+
setAppearance('foo', { icon: 'house', color: '#ff0000', label: 'X' });
|
|
22
|
+
setAppearance('foo', { icon: undefined, color: undefined, label: undefined });
|
|
23
|
+
expect(getAppearance('foo')).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
it('clears the entry when given undefined directly', () => {
|
|
26
|
+
setAppearance('foo', { icon: 'house' });
|
|
27
|
+
setAppearance('foo', undefined);
|
|
28
|
+
expect(getAppearance('foo')).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-app per-user-per-browser visual override. Stored in the user zone
|
|
3
|
+
* of the __app-appearance__ shard. Every field is optional — an empty
|
|
4
|
+
* AppAppearance object is treated as "no override" and removed from the
|
|
5
|
+
* map by setAppearance().
|
|
6
|
+
*/
|
|
7
|
+
export interface AppAppearance {
|
|
8
|
+
icon?: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/apps/types.d.ts
CHANGED
|
@@ -74,6 +74,13 @@ export interface AppManifest {
|
|
|
74
74
|
* When absent, the canonical fallback is used. See MenuContainer.
|
|
75
75
|
*/
|
|
76
76
|
menus?: MenuContainer[];
|
|
77
|
+
/**
|
|
78
|
+
* Optional default home-card icon — a lucide icon id from the bundled
|
|
79
|
+
* sprite (see `src/assets/icons.svg`). User-set per-browser overrides
|
|
80
|
+
* (via the home-card Customize action) take precedence; if neither is
|
|
81
|
+
* set the framework falls back to `box`.
|
|
82
|
+
*/
|
|
83
|
+
icon?: string;
|
|
77
84
|
}
|
|
78
85
|
/**
|
|
79
86
|
* Context object passed to `App.activate`. Provides app-scoped state zones
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const ICON_IDS: readonly ["activity", "align-horizontal-justify-center", "align-horizontal-justify-end", "align-horizontal-justify-start", "app-window", "archive", "archive-restore", "axis-3d", "box", "brick-wall", "bug", "building-2", "cable", "calendar", "camera", "check", "chevron-down", "chevron-right", "circle-check", "circle-dot", "circle-minus", "circle-x", "clipboard", "clipboard-paste", "clock", "compass", "component", "copy", "cpu", "crop", "crosshair", "crown", "dollar-sign", "download", "droplet", "eraser", "euro", "external-link", "eye", "eye-off", "file", "file-archive", "file-diff", "file-plus", "file-text", "flame", "flip-horizontal-2", "flip-vertical-2", "folder", "folder-open", "folder-plus", "folder-tree", "gallery-vertical-end", "gamepad-2", "gauge", "gem", "git-branch", "git-commit-horizontal", "git-merge", "globe", "grid-2x2", "grid-3x3", "group", "hard-drive", "heart", "history", "house", "image", "info", "joystick", "key", "layers", "layout-dashboard", "layout-grid", "layout-list", "layout-panel-left", "layout-panel-top", "layout-template", "lightbulb", "link", "list-ordered", "list-tree", "lock", "log-out", "magnet", "mail", "map", "maximize", "minimize", "moon", "mouse-pointer", "move", "move-3d", "music", "navigation", "network", "notebook-pen", "palette", "pause", "pencil", "pipette", "play", "plus", "pointer", "pound-sterling", "receipt", "redo-2", "refresh-cw", "rocket", "rotate-3d", "rotate-ccw", "rotate-cw", "ruler", "save", "scissors", "scroll-text", "search", "send", "server", "settings", "shield", "skull", "sliders-horizontal", "snowflake", "sparkles", "square", "square-terminal", "star", "sun", "sword", "table-properties", "target", "texture", "timer", "trash-2", "triangle-alert", "type", "undo-2", "ungroup", "unity", "upload", "user", "users", "video", "volume-2", "wand-sparkles", "wind", "x", "zap", "zoom-in", "zoom-out"];
|
|
2
|
+
export type IconId = (typeof ICON_IDS)[number];
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// GENERATED — do not edit. See scripts/sync-icon-ids.ts
|
|
2
|
+
export const ICON_IDS = [
|
|
3
|
+
'activity',
|
|
4
|
+
'align-horizontal-justify-center',
|
|
5
|
+
'align-horizontal-justify-end',
|
|
6
|
+
'align-horizontal-justify-start',
|
|
7
|
+
'app-window',
|
|
8
|
+
'archive',
|
|
9
|
+
'archive-restore',
|
|
10
|
+
'axis-3d',
|
|
11
|
+
'box',
|
|
12
|
+
'brick-wall',
|
|
13
|
+
'bug',
|
|
14
|
+
'building-2',
|
|
15
|
+
'cable',
|
|
16
|
+
'calendar',
|
|
17
|
+
'camera',
|
|
18
|
+
'check',
|
|
19
|
+
'chevron-down',
|
|
20
|
+
'chevron-right',
|
|
21
|
+
'circle-check',
|
|
22
|
+
'circle-dot',
|
|
23
|
+
'circle-minus',
|
|
24
|
+
'circle-x',
|
|
25
|
+
'clipboard',
|
|
26
|
+
'clipboard-paste',
|
|
27
|
+
'clock',
|
|
28
|
+
'compass',
|
|
29
|
+
'component',
|
|
30
|
+
'copy',
|
|
31
|
+
'cpu',
|
|
32
|
+
'crop',
|
|
33
|
+
'crosshair',
|
|
34
|
+
'crown',
|
|
35
|
+
'dollar-sign',
|
|
36
|
+
'download',
|
|
37
|
+
'droplet',
|
|
38
|
+
'eraser',
|
|
39
|
+
'euro',
|
|
40
|
+
'external-link',
|
|
41
|
+
'eye',
|
|
42
|
+
'eye-off',
|
|
43
|
+
'file',
|
|
44
|
+
'file-archive',
|
|
45
|
+
'file-diff',
|
|
46
|
+
'file-plus',
|
|
47
|
+
'file-text',
|
|
48
|
+
'flame',
|
|
49
|
+
'flip-horizontal-2',
|
|
50
|
+
'flip-vertical-2',
|
|
51
|
+
'folder',
|
|
52
|
+
'folder-open',
|
|
53
|
+
'folder-plus',
|
|
54
|
+
'folder-tree',
|
|
55
|
+
'gallery-vertical-end',
|
|
56
|
+
'gamepad-2',
|
|
57
|
+
'gauge',
|
|
58
|
+
'gem',
|
|
59
|
+
'git-branch',
|
|
60
|
+
'git-commit-horizontal',
|
|
61
|
+
'git-merge',
|
|
62
|
+
'globe',
|
|
63
|
+
'grid-2x2',
|
|
64
|
+
'grid-3x3',
|
|
65
|
+
'group',
|
|
66
|
+
'hard-drive',
|
|
67
|
+
'heart',
|
|
68
|
+
'history',
|
|
69
|
+
'house',
|
|
70
|
+
'image',
|
|
71
|
+
'info',
|
|
72
|
+
'joystick',
|
|
73
|
+
'key',
|
|
74
|
+
'layers',
|
|
75
|
+
'layout-dashboard',
|
|
76
|
+
'layout-grid',
|
|
77
|
+
'layout-list',
|
|
78
|
+
'layout-panel-left',
|
|
79
|
+
'layout-panel-top',
|
|
80
|
+
'layout-template',
|
|
81
|
+
'lightbulb',
|
|
82
|
+
'link',
|
|
83
|
+
'list-ordered',
|
|
84
|
+
'list-tree',
|
|
85
|
+
'lock',
|
|
86
|
+
'log-out',
|
|
87
|
+
'magnet',
|
|
88
|
+
'mail',
|
|
89
|
+
'map',
|
|
90
|
+
'maximize',
|
|
91
|
+
'minimize',
|
|
92
|
+
'moon',
|
|
93
|
+
'mouse-pointer',
|
|
94
|
+
'move',
|
|
95
|
+
'move-3d',
|
|
96
|
+
'music',
|
|
97
|
+
'navigation',
|
|
98
|
+
'network',
|
|
99
|
+
'notebook-pen',
|
|
100
|
+
'palette',
|
|
101
|
+
'pause',
|
|
102
|
+
'pencil',
|
|
103
|
+
'pipette',
|
|
104
|
+
'play',
|
|
105
|
+
'plus',
|
|
106
|
+
'pointer',
|
|
107
|
+
'pound-sterling',
|
|
108
|
+
'receipt',
|
|
109
|
+
'redo-2',
|
|
110
|
+
'refresh-cw',
|
|
111
|
+
'rocket',
|
|
112
|
+
'rotate-3d',
|
|
113
|
+
'rotate-ccw',
|
|
114
|
+
'rotate-cw',
|
|
115
|
+
'ruler',
|
|
116
|
+
'save',
|
|
117
|
+
'scissors',
|
|
118
|
+
'scroll-text',
|
|
119
|
+
'search',
|
|
120
|
+
'send',
|
|
121
|
+
'server',
|
|
122
|
+
'settings',
|
|
123
|
+
'shield',
|
|
124
|
+
'skull',
|
|
125
|
+
'sliders-horizontal',
|
|
126
|
+
'snowflake',
|
|
127
|
+
'sparkles',
|
|
128
|
+
'square',
|
|
129
|
+
'square-terminal',
|
|
130
|
+
'star',
|
|
131
|
+
'sun',
|
|
132
|
+
'sword',
|
|
133
|
+
'table-properties',
|
|
134
|
+
'target',
|
|
135
|
+
'texture',
|
|
136
|
+
'timer',
|
|
137
|
+
'trash-2',
|
|
138
|
+
'triangle-alert',
|
|
139
|
+
'type',
|
|
140
|
+
'undo-2',
|
|
141
|
+
'ungroup',
|
|
142
|
+
'unity',
|
|
143
|
+
'upload',
|
|
144
|
+
'user',
|
|
145
|
+
'users',
|
|
146
|
+
'video',
|
|
147
|
+
'volume-2',
|
|
148
|
+
'wand-sparkles',
|
|
149
|
+
'wind',
|
|
150
|
+
'x',
|
|
151
|
+
'zap',
|
|
152
|
+
'zoom-in',
|
|
153
|
+
'zoom-out',
|
|
154
|
+
];
|
package/dist/host.js
CHANGED
|
@@ -23,6 +23,7 @@ import { sh3coreShard } from './sh3core-shard/sh3coreShard.svelte';
|
|
|
23
23
|
import { shellShard } from './shell-shard/shellShard.svelte';
|
|
24
24
|
import { storeShard } from './app/store/storeShard.svelte';
|
|
25
25
|
import { projectsShard } from './projects-shard/projectsShard.svelte';
|
|
26
|
+
import { appearanceShard } from './app-appearance';
|
|
26
27
|
import { __setBackend, backends } from './state/zones.svelte';
|
|
27
28
|
import { loadInstalledPackages } from './registry/installer';
|
|
28
29
|
import { setLocalOwner } from './auth/index';
|
|
@@ -67,7 +68,7 @@ export async function bootstrap(config) {
|
|
|
67
68
|
}
|
|
68
69
|
const exShards = new Set(config === null || config === void 0 ? void 0 : config.excludeShards);
|
|
69
70
|
// 1. Framework-owned shards
|
|
70
|
-
const frameworkShards = [sh3coreShard, shellShard, storeShard, adminShard, projectsShard];
|
|
71
|
+
const frameworkShards = [sh3coreShard, shellShard, storeShard, adminShard, projectsShard, appearanceShard];
|
|
71
72
|
for (const shard of frameworkShards) {
|
|
72
73
|
if (!exShards.has(shard.manifest.id)) {
|
|
73
74
|
registerShardInternal(shard);
|
|
@@ -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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { floatManager, __resetFloatManagerForTest, bindFloatStore } from './float';
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { floatManager, __resetFloatManagerForTest, bindFloatStore, getFloatParentHost, } from './float';
|
|
3
3
|
import { layoutStore } from '../layout/store.svelte';
|
|
4
4
|
describe('floatManager', () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -80,6 +80,49 @@ describe('floatManager', () => {
|
|
|
80
80
|
expect(f.content.type).toBe('tabs');
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
|
+
describe('floatManager — anchor-aware parent host', () => {
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
__resetFloatManagerForTest();
|
|
86
|
+
});
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
document.body.innerHTML = '';
|
|
89
|
+
});
|
|
90
|
+
function makeOverlayHost(kind) {
|
|
91
|
+
const host = document.createElement('div');
|
|
92
|
+
host.dataset.shellOverlayHost = kind;
|
|
93
|
+
const anchor = document.createElement('button');
|
|
94
|
+
host.appendChild(anchor);
|
|
95
|
+
document.body.appendChild(host);
|
|
96
|
+
return { host, anchor };
|
|
97
|
+
}
|
|
98
|
+
it('getFloatParentHost is undefined when no anchor was passed', () => {
|
|
99
|
+
const id = floatManager.open('test:view', { dismissable: true });
|
|
100
|
+
expect(getFloatParentHost(id)).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
it('getFloatParentHost is undefined when the anchor lives outside any overlay host', () => {
|
|
103
|
+
const anchor = document.createElement('button');
|
|
104
|
+
document.body.appendChild(anchor);
|
|
105
|
+
const id = floatManager.open('test:view', { dismissable: true, anchor });
|
|
106
|
+
expect(getFloatParentHost(id)).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
it('getFloatParentHost returns the enclosing host for a dismissable+anchored float', () => {
|
|
109
|
+
const { host, anchor } = makeOverlayHost('modal');
|
|
110
|
+
const id = floatManager.open('test:view', { dismissable: true, anchor });
|
|
111
|
+
expect(getFloatParentHost(id)).toBe(host);
|
|
112
|
+
});
|
|
113
|
+
it('getFloatParentHost is undefined for non-dismissable floats even with an anchor', () => {
|
|
114
|
+
const { anchor } = makeOverlayHost('modal');
|
|
115
|
+
const id = floatManager.open('test:view', { anchor });
|
|
116
|
+
expect(getFloatParentHost(id)).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
it('getFloatParentHost is cleared when the float is closed', () => {
|
|
119
|
+
const { anchor } = makeOverlayHost('modal');
|
|
120
|
+
const id = floatManager.open('test:view', { dismissable: true, anchor });
|
|
121
|
+
expect(getFloatParentHost(id)).toBeDefined();
|
|
122
|
+
floatManager.close(id);
|
|
123
|
+
expect(getFloatParentHost(id)).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
83
126
|
// ---------------------------------------------------------------------------
|
|
84
127
|
// DOM tests — floatManager + FloatLayer.svelte in happy-dom
|
|
85
128
|
// ---------------------------------------------------------------------------
|
|
@@ -311,3 +354,55 @@ describe('floats — F.6 multi-picker interaction', () => {
|
|
|
311
354
|
expect(floatManager.list().some((f) => f.id === id)).toBe(false);
|
|
312
355
|
});
|
|
313
356
|
});
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// F.7 — anchor portals dismissable float into the enclosing overlay host
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
describe('floats — F.7 anchor portals to enclosing overlay host', () => {
|
|
361
|
+
beforeEach(() => {
|
|
362
|
+
resetFramework();
|
|
363
|
+
bindManagerToStore();
|
|
364
|
+
});
|
|
365
|
+
it('reparents the FloatFrame into the anchor’s enclosing overlay host', async () => {
|
|
366
|
+
const { container } = renderWithShell(FloatLayer, {});
|
|
367
|
+
const fakeModalHost = document.createElement('div');
|
|
368
|
+
fakeModalHost.className = 'fake-modal-host';
|
|
369
|
+
fakeModalHost.dataset.shellOverlayHost = 'modal';
|
|
370
|
+
const anchor = document.createElement('button');
|
|
371
|
+
fakeModalHost.appendChild(anchor);
|
|
372
|
+
document.body.appendChild(fakeModalHost);
|
|
373
|
+
floatManager.open('test:view', {
|
|
374
|
+
dismissable: true,
|
|
375
|
+
anchor,
|
|
376
|
+
title: 'Picker',
|
|
377
|
+
});
|
|
378
|
+
await tick();
|
|
379
|
+
const frame = document.querySelector('[role="dialog"][aria-label="Picker"]');
|
|
380
|
+
expect(frame).toBeTruthy();
|
|
381
|
+
expect(fakeModalHost.contains(frame)).toBe(true);
|
|
382
|
+
expect(container.contains(frame)).toBe(false);
|
|
383
|
+
});
|
|
384
|
+
it('renders inside FloatLayer when no anchor is provided', async () => {
|
|
385
|
+
const { container } = renderWithShell(FloatLayer, {});
|
|
386
|
+
floatManager.open('test:view', { dismissable: true, title: 'NoAnchor' });
|
|
387
|
+
await tick();
|
|
388
|
+
const frame = container.querySelector('[role="dialog"][aria-label="NoAnchor"]');
|
|
389
|
+
expect(frame).toBeTruthy();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// F.8 — overlay host marker on FloatFrame
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
describe('floats — F.8 overlay host marker', () => {
|
|
396
|
+
beforeEach(() => {
|
|
397
|
+
resetFramework();
|
|
398
|
+
bindManagerToStore();
|
|
399
|
+
});
|
|
400
|
+
it('marks each FloatFrame with data-shell-overlay-host="float"', async () => {
|
|
401
|
+
const { container } = renderWithShell(FloatLayer, {});
|
|
402
|
+
floatManager.open('test:view', { title: 'Marked' });
|
|
403
|
+
await tick();
|
|
404
|
+
const frame = container.querySelector('[role="dialog"][aria-label="Marked"]');
|
|
405
|
+
expect(frame).toBeTruthy();
|
|
406
|
+
expect(frame.dataset.shellOverlayHost).toBe('float');
|
|
407
|
+
});
|
|
408
|
+
});
|
package/dist/overlays/modal.js
CHANGED
|
@@ -109,6 +109,7 @@ function openModal(Content, props, options) {
|
|
|
109
109
|
const root = getLayerRoot('modal');
|
|
110
110
|
const host = document.createElement('div');
|
|
111
111
|
host.className = 'sh3-modal-host';
|
|
112
|
+
host.dataset.shellOverlayHost = 'modal';
|
|
112
113
|
host.style.position = 'absolute';
|
|
113
114
|
host.style.inset = '0';
|
|
114
115
|
host.style.pointerEvents = 'auto';
|
|
@@ -88,3 +88,20 @@ describe('modal — back-cascade integration', () => {
|
|
|
88
88
|
expect(layerRoot.querySelectorAll('.sh3-modal-host').length).toBe(0);
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
|
+
describe('modal — overlay host marker', () => {
|
|
92
|
+
let layerRoot;
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
layerRoot = makeLayerRoot();
|
|
95
|
+
});
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
modalManager.closeAll();
|
|
98
|
+
teardownLayerRoot(layerRoot);
|
|
99
|
+
});
|
|
100
|
+
it('marks the modal host with data-shell-overlay-host="modal"', async () => {
|
|
101
|
+
modalManager.open(DummyFrame, {});
|
|
102
|
+
await tick();
|
|
103
|
+
const host = layerRoot.querySelector('.sh3-modal-host');
|
|
104
|
+
expect(host).not.toBeNull();
|
|
105
|
+
expect(host.dataset.shellOverlayHost).toBe('modal');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findEnclosingOverlayHost(anchor: HTMLElement): HTMLElement | null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Walks up from `anchor` looking for an element marked as an overlay host
|
|
3
|
+
* via `data-shell-overlay-host`. Modal hosts, popup hosts, and float frames
|
|
4
|
+
* tag themselves so anchored overlays (popups, dismissable picker floats)
|
|
5
|
+
* can mount inside their opener's stacking context instead of at a global
|
|
6
|
+
* layer root — which is what the layer-z-index invariant gives us when a
|
|
7
|
+
* popover is logically "inside" a modal.
|
|
8
|
+
*
|
|
9
|
+
* Returns null when the anchor lives in the docked tree; callers fall back
|
|
10
|
+
* to their configured layer root in that case. The marker is read via
|
|
11
|
+
* `Element.closest`, so a marker on the anchor itself counts.
|
|
12
|
+
*/
|
|
13
|
+
export function findEnclosingOverlayHost(anchor) {
|
|
14
|
+
return anchor.closest('[data-shell-overlay-host]');
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|