sh3-core 0.15.0 → 0.15.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/actions/ctx-actions.svelte.test.js +111 -0
- package/dist/actions/dispatcher.svelte.js +23 -2
- package/dist/actions/dispatcher.test.js +33 -0
- package/dist/actions/listActionsFromEntries.test.js +78 -0
- package/dist/actions/listActive.d.ts +2 -1
- package/dist/actions/listActive.js +43 -17
- package/dist/actions/listeners.d.ts +16 -0
- package/dist/actions/listeners.js +68 -14
- package/dist/actions/programmatic-dispatch.svelte.test.d.ts +1 -0
- package/dist/actions/programmatic-dispatch.svelte.test.js +98 -0
- package/dist/actions/types.d.ts +37 -0
- package/dist/api.d.ts +1 -1
- package/dist/app/store/verbs.js +4 -0
- package/dist/app-appearance/appearanceShard.svelte.js +19 -6
- package/dist/app-appearance/appearanceState.svelte.js +3 -3
- package/dist/host.js +2 -1
- package/dist/layouts-shard/LayoutSaveModal.svelte +145 -0
- package/dist/layouts-shard/LayoutSaveModal.svelte.d.ts +12 -0
- package/dist/layouts-shard/LayoutsSection.svelte +142 -0
- package/dist/layouts-shard/LayoutsSection.svelte.d.ts +3 -0
- package/dist/layouts-shard/filter.d.ts +3 -0
- package/dist/layouts-shard/filter.js +66 -0
- package/dist/layouts-shard/filter.test.d.ts +1 -0
- package/dist/layouts-shard/filter.test.js +123 -0
- package/dist/layouts-shard/index.d.ts +1 -0
- package/dist/layouts-shard/index.js +1 -0
- package/dist/layouts-shard/layoutsApi.d.ts +12 -0
- package/dist/layouts-shard/layoutsApi.js +41 -0
- package/dist/layouts-shard/layoutsApi.test.d.ts +1 -0
- package/dist/layouts-shard/layoutsApi.test.js +74 -0
- package/dist/layouts-shard/layoutsShard.svelte.d.ts +11 -0
- package/dist/layouts-shard/layoutsShard.svelte.js +231 -0
- package/dist/layouts-shard/layoutsShard.svelte.test.d.ts +1 -0
- package/dist/layouts-shard/layoutsShard.svelte.test.js +215 -0
- package/dist/layouts-shard/layoutsState.svelte.d.ts +9 -0
- package/dist/layouts-shard/layoutsState.svelte.js +50 -0
- package/dist/layouts-shard/layoutsState.test.d.ts +1 -0
- package/dist/layouts-shard/layoutsState.test.js +43 -0
- package/dist/layouts-shard/types.d.ts +21 -0
- package/dist/layouts-shard/types.js +6 -0
- package/dist/{app-appearance/AppAppearanceModal.svelte → overlays/EntityAppearanceModal.svelte} +36 -31
- package/dist/overlays/EntityAppearanceModal.svelte.d.ts +19 -0
- package/dist/overlays/EntityAppearanceModal.test.d.ts +1 -0
- package/dist/overlays/EntityAppearanceModal.test.js +57 -0
- package/dist/overlays/FloatFrame.svelte +149 -8
- package/dist/overlays/FloatFrame.svelte.d.ts +1 -1
- package/dist/overlays/FloatLayer.svelte +2 -2
- package/dist/overlays/float.d.ts +38 -1
- package/dist/overlays/float.js +82 -0
- package/dist/overlays/float.test.js +394 -0
- package/dist/overlays/floatMaximized.svelte.d.ts +4 -0
- package/dist/overlays/floatMaximized.svelte.js +30 -0
- package/dist/runtime/runVerb-shell.test.d.ts +1 -0
- package/dist/runtime/runVerb-shell.test.js +231 -0
- package/dist/sh3core-shard/ShellHome.svelte +3 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.d.ts +7 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +23 -0
- package/dist/shards/activate-runtime.test.js +24 -2
- package/dist/shards/activate.svelte.js +18 -4
- package/dist/shards/types.d.ts +44 -4
- package/dist/shell-shard/CommandLine.svelte +143 -0
- package/dist/shell-shard/CommandLine.svelte.d.ts +26 -0
- package/dist/shell-shard/CommandLine.svelte.test.d.ts +1 -0
- package/dist/shell-shard/CommandLine.svelte.test.js +43 -0
- package/dist/shell-shard/InputLine.svelte +17 -40
- package/dist/shell-shard/InputLine.svelte.d.ts +2 -0
- package/dist/shell-shard/ScrollbackView.svelte +10 -3
- package/dist/shell-shard/ScrollbackView.svelte.d.ts +1 -0
- package/dist/shell-shard/Terminal.svelte +94 -22
- package/dist/shell-shard/buffer-store.d.ts +15 -0
- package/dist/shell-shard/buffer-store.js +124 -0
- package/dist/shell-shard/buffer-store.svelte.test.d.ts +1 -0
- package/dist/shell-shard/buffer-store.svelte.test.js +107 -0
- package/dist/shell-shard/buffer-zone-state.svelte.d.ts +38 -0
- package/dist/shell-shard/buffer-zone-state.svelte.js +31 -0
- package/dist/shell-shard/contract.d.ts +7 -0
- package/dist/shell-shard/dispatch-custom.test.js +3 -1
- package/dist/shell-shard/dispatch-gating.test.js +6 -2
- package/dist/shell-shard/dispatch-invoke.test.js +10 -8
- package/dist/shell-shard/dispatch.d.ts +7 -2
- package/dist/shell-shard/dispatch.js +23 -27
- package/dist/shell-shard/display-cwd.d.ts +1 -0
- package/dist/shell-shard/display-cwd.js +27 -0
- package/dist/shell-shard/display-cwd.test.d.ts +1 -0
- package/dist/shell-shard/display-cwd.test.js +29 -0
- package/dist/shell-shard/entries/StatusEntry.svelte +2 -0
- package/dist/shell-shard/manifest.js +2 -1
- package/dist/shell-shard/manifest.test.d.ts +1 -0
- package/dist/shell-shard/manifest.test.js +8 -0
- package/dist/shell-shard/mode-buffer.svelte.d.ts +8 -0
- package/dist/shell-shard/mode-buffer.svelte.js +19 -0
- package/dist/shell-shard/mode-buffer.svelte.test.d.ts +1 -0
- package/dist/shell-shard/mode-buffer.svelte.test.js +25 -0
- package/dist/shell-shard/modes/builtin.js +2 -0
- package/dist/shell-shard/modes/types.d.ts +8 -0
- package/dist/shell-shard/protocol.d.ts +12 -6
- package/dist/shell-shard/replay.d.ts +3 -0
- package/dist/shell-shard/replay.js +44 -0
- package/dist/shell-shard/replay.svelte.test.d.ts +1 -0
- package/dist/shell-shard/replay.svelte.test.js +47 -0
- package/dist/shell-shard/rich-registry.d.ts +5 -0
- package/dist/shell-shard/rich-registry.js +25 -0
- package/dist/shell-shard/rich-registry.test.d.ts +1 -0
- package/dist/shell-shard/rich-registry.test.js +31 -0
- package/dist/shell-shard/scrollback.svelte.d.ts +2 -0
- package/dist/shell-shard/scrollback.svelte.js +23 -0
- package/dist/shell-shard/scrollback.svelte.test.d.ts +1 -0
- package/dist/shell-shard/scrollback.svelte.test.js +51 -0
- package/dist/shell-shard/session-client.svelte.d.ts +18 -2
- package/dist/shell-shard/session-client.svelte.js +21 -4
- package/dist/shell-shard/shellApi.d.ts +2 -1
- package/dist/shell-shard/shellApi.js +32 -3
- package/dist/shell-shard/shellApi.svelte.test.d.ts +1 -0
- package/dist/shell-shard/shellApi.svelte.test.js +59 -0
- package/dist/shell-shard/shellShard.svelte.js +11 -1
- package/dist/shell-shard/terminal-dispatch.test.js +3 -1
- package/dist/shell-shard/verbs/apps.js +9 -0
- package/dist/shell-shard/verbs/env.js +4 -0
- package/dist/shell-shard/verbs/help.js +9 -1
- package/dist/shell-shard/verbs/help.svelte.test.d.ts +1 -0
- package/dist/shell-shard/verbs/help.svelte.test.js +53 -0
- package/dist/shell-shard/verbs/history.js +8 -1
- package/dist/shell-shard/verbs/index.js +0 -8
- package/dist/shell-shard/verbs/shards.js +5 -0
- package/dist/shell-shard/verbs/views.js +9 -0
- package/dist/shell-shard/verbs/zones.js +9 -0
- package/dist/verbs/types.d.ts +9 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +0 -8
- package/dist/shell-shard/verbs/cat.d.ts +0 -2
- package/dist/shell-shard/verbs/cat.js +0 -34
- package/dist/shell-shard/verbs/cd.test.js +0 -56
- package/dist/shell-shard/verbs/ls.d.ts +0 -2
- package/dist/shell-shard/verbs/ls.js +0 -29
- package/dist/shell-shard/verbs/ls.test.js +0 -49
- package/dist/shell-shard/verbs/session.d.ts +0 -4
- package/dist/shell-shard/verbs/session.js +0 -97
- /package/dist/{shell-shard/verbs/cd.test.d.ts → actions/ctx-actions.svelte.test.d.ts} +0 -0
- /package/dist/{shell-shard/verbs/ls.test.d.ts → actions/listActionsFromEntries.test.d.ts} +0 -0
package/dist/app/store/verbs.js
CHANGED
|
@@ -17,6 +17,7 @@ function findInstalled(id) {
|
|
|
17
17
|
export const installVerb = {
|
|
18
18
|
name: 'install',
|
|
19
19
|
summary: 'Install a package by id from the catalog.',
|
|
20
|
+
programmatic: true,
|
|
20
21
|
async run(ctx, args) {
|
|
21
22
|
var _a, _b, _c;
|
|
22
23
|
const id = args[0];
|
|
@@ -117,6 +118,7 @@ export const installVerb = {
|
|
|
117
118
|
export const uninstallVerb = {
|
|
118
119
|
name: 'uninstall',
|
|
119
120
|
summary: 'Uninstall an installed package by id.',
|
|
121
|
+
programmatic: true,
|
|
120
122
|
async run(ctx, args) {
|
|
121
123
|
const id = args[0];
|
|
122
124
|
if (!id) {
|
|
@@ -168,6 +170,7 @@ export const updateVerb = {
|
|
|
168
170
|
summary: 'Update an installed package (sh3-store:update <id> [version]). When ' +
|
|
169
171
|
'version is omitted, bumps to latest; with a version, installs that ' +
|
|
170
172
|
'exact version (downgrade or same-version reinstall allowed).',
|
|
173
|
+
programmatic: true,
|
|
171
174
|
async run(ctx, args) {
|
|
172
175
|
var _a, _b;
|
|
173
176
|
const id = args[0];
|
|
@@ -243,6 +246,7 @@ export const updateVerb = {
|
|
|
243
246
|
export const appinfoVerb = {
|
|
244
247
|
name: 'appinfo',
|
|
245
248
|
summary: 'Show info about a package (installed status, version, catalog details).',
|
|
249
|
+
programmatic: true,
|
|
246
250
|
async run(ctx, args) {
|
|
247
251
|
const id = args[0];
|
|
248
252
|
if (!id) {
|
|
@@ -9,8 +9,8 @@ import { VERSION } from '../version';
|
|
|
9
9
|
import { listRegisteredApps } from '../api';
|
|
10
10
|
import { getSelection } from '../actions/selection.svelte';
|
|
11
11
|
import { modalManager } from '../overlays/modal';
|
|
12
|
-
import
|
|
13
|
-
import { __bindZone, __unbindZone, } from './appearanceState.svelte';
|
|
12
|
+
import EntityAppearanceModal from '../overlays/EntityAppearanceModal.svelte';
|
|
13
|
+
import { __bindZone, __unbindZone, getAppearance, setAppearance, } from './appearanceState.svelte';
|
|
14
14
|
function readSelection() {
|
|
15
15
|
const sel = getSelection();
|
|
16
16
|
if (!sel || sel.type !== 'app')
|
|
@@ -18,16 +18,29 @@ function readSelection() {
|
|
|
18
18
|
return sel.ref;
|
|
19
19
|
}
|
|
20
20
|
function runCustomize(_ctx) {
|
|
21
|
-
var _a;
|
|
21
|
+
var _a, _b, _c;
|
|
22
22
|
const ref = readSelection();
|
|
23
23
|
if (!ref)
|
|
24
24
|
return;
|
|
25
25
|
const m = listRegisteredApps().find((x) => x.id === ref.appId);
|
|
26
|
+
const appLabel = (_a = m === null || m === void 0 ? void 0 : m.label) !== null && _a !== void 0 ? _a : ref.appId;
|
|
27
|
+
const manifestIcon = (_b = m === null || m === void 0 ? void 0 : m.icon) !== null && _b !== void 0 ? _b : 'box';
|
|
28
|
+
const initial = getAppearance(ref.appId);
|
|
26
29
|
const props = {
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
entityLabel: (_c = initial === null || initial === void 0 ? void 0 : initial.label) !== null && _c !== void 0 ? _c : appLabel,
|
|
31
|
+
initialAppearance: initial,
|
|
32
|
+
defaultIcon: manifestIcon,
|
|
33
|
+
requireLabel: false,
|
|
34
|
+
onSave: (next) => {
|
|
35
|
+
setAppearance(ref.appId, {
|
|
36
|
+
label: next.label === '' ? undefined : next.label,
|
|
37
|
+
icon: next.icon,
|
|
38
|
+
color: next.color,
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
onReset: () => setAppearance(ref.appId, undefined),
|
|
29
42
|
};
|
|
30
|
-
modalManager.open(
|
|
43
|
+
modalManager.open(EntityAppearanceModal, props);
|
|
31
44
|
}
|
|
32
45
|
export const appearanceShard = {
|
|
33
46
|
manifest: {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Per-user-per-browser visual overrides for apps. The store + helpers
|
|
3
3
|
* live separately from the shard so the state can be unit-tested without
|
|
4
|
-
* booting the shard system, and so the
|
|
5
|
-
* get/set without creating an import cycle through the
|
|
6
|
-
* import.
|
|
4
|
+
* booting the shard system, and so the appearanceShard's runCustomize
|
|
5
|
+
* can import get/set without creating an import cycle through the
|
|
6
|
+
* shard's modal import.
|
|
7
7
|
*
|
|
8
8
|
* EXPLICITLY TEMPORARY. A future ADR is expected to add icon/color
|
|
9
9
|
* fields to the app manifest itself.
|
package/dist/host.js
CHANGED
|
@@ -24,6 +24,7 @@ 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
26
|
import { appearanceShard } from './app-appearance';
|
|
27
|
+
import { layoutsShard } from './layouts-shard';
|
|
27
28
|
import { __setBackend, backends } from './state/zones.svelte';
|
|
28
29
|
import { loadInstalledPackages } from './registry/installer';
|
|
29
30
|
import { setLocalOwner } from './auth/index';
|
|
@@ -70,7 +71,7 @@ export async function bootstrap(config) {
|
|
|
70
71
|
}
|
|
71
72
|
const exShards = new Set(config === null || config === void 0 ? void 0 : config.excludeShards);
|
|
72
73
|
// 1. Framework-owned shards
|
|
73
|
-
const frameworkShards = [sh3coreShard, shellShard, storeShard, adminShard, projectsShard, appearanceShard];
|
|
74
|
+
const frameworkShards = [sh3coreShard, shellShard, storeShard, adminShard, projectsShard, appearanceShard, layoutsShard];
|
|
74
75
|
for (const shard of frameworkShards) {
|
|
75
76
|
if (!exShards.has(shard.manifest.id)) {
|
|
76
77
|
registerShardInternal(shard);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* LayoutSaveModal — Save layout as… dialog.
|
|
4
|
+
*
|
|
5
|
+
* Receives the floatId of the source float. On mount it reads the live
|
|
6
|
+
* float entry, runs the standalone filter once, and shows:
|
|
7
|
+
* - a name input (default = float title or "Layout {N}")
|
|
8
|
+
* - a preview list of view ids being captured
|
|
9
|
+
* - Save / Cancel
|
|
10
|
+
*
|
|
11
|
+
* If the source float has closed by the time the modal is open, or the
|
|
12
|
+
* filter drops everything, Save is disabled with an inline message.
|
|
13
|
+
*
|
|
14
|
+
* On Save the modal calls the supplied onConfirm(name) callback —
|
|
15
|
+
* the shard does the actual capture via captureFromFloat and addLayout.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { untrack } from 'svelte';
|
|
19
|
+
import type { LayoutNode } from '../layout/types';
|
|
20
|
+
import { floatManager } from '../overlays/float';
|
|
21
|
+
import { filterToStandalone, type IsStandalone } from './filter';
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
floatId: string;
|
|
25
|
+
isStandalone: IsStandalone;
|
|
26
|
+
resolveLabel: (viewId: string) => string;
|
|
27
|
+
defaultName: string;
|
|
28
|
+
onConfirm(name: string): void;
|
|
29
|
+
close: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let { floatId, isStandalone, resolveLabel, defaultName, onConfirm, close }: Props = $props();
|
|
33
|
+
|
|
34
|
+
const sourceContent = untrack(() =>
|
|
35
|
+
floatManager.list().find((f) => f.id === floatId)?.content ?? null,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const filtered = $derived<LayoutNode | null>(
|
|
39
|
+
sourceContent ? filterToStandalone(sourceContent, isStandalone) : null,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const previewIds = $derived<string[]>(filtered ? collectViewIds(filtered) : []);
|
|
43
|
+
const sourceMissing = $derived(sourceContent === null);
|
|
44
|
+
const filterEmpty = $derived(!sourceMissing && filtered === null);
|
|
45
|
+
|
|
46
|
+
let name = $state<string>(untrack(() => defaultName));
|
|
47
|
+
const saveDisabled = $derived(name.trim() === '' || sourceMissing || filterEmpty);
|
|
48
|
+
|
|
49
|
+
function collectViewIds(node: LayoutNode): string[] {
|
|
50
|
+
if (node.type === 'slot') return node.viewId ? [node.viewId] : [];
|
|
51
|
+
if (node.type === 'tabs') {
|
|
52
|
+
const out: string[] = [];
|
|
53
|
+
for (const t of node.tabs) if (t.viewId) out.push(t.viewId);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
return node.children.flatMap(collectViewIds);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function save() {
|
|
60
|
+
if (saveDisabled) return;
|
|
61
|
+
onConfirm(name.trim());
|
|
62
|
+
close();
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<div class="layout-save">
|
|
67
|
+
<h2>Save layout as…</h2>
|
|
68
|
+
|
|
69
|
+
<label class="row"><span>Name</span>
|
|
70
|
+
<input
|
|
71
|
+
type="text"
|
|
72
|
+
bind:value={name}
|
|
73
|
+
class="name-input"
|
|
74
|
+
placeholder="Layout name"
|
|
75
|
+
/>
|
|
76
|
+
</label>
|
|
77
|
+
|
|
78
|
+
{#if sourceMissing}
|
|
79
|
+
<p class="error">The source float was closed.</p>
|
|
80
|
+
{:else if filterEmpty}
|
|
81
|
+
<p class="error">No standalone views in this float.</p>
|
|
82
|
+
{:else}
|
|
83
|
+
<div class="preview">
|
|
84
|
+
<div class="preview-label">Captured views ({previewIds.length}):</div>
|
|
85
|
+
<ul class="preview-list">
|
|
86
|
+
{#each previewIds as viewId}
|
|
87
|
+
<li>{resolveLabel(viewId)} <span class="muted">({viewId})</span></li>
|
|
88
|
+
{/each}
|
|
89
|
+
</ul>
|
|
90
|
+
</div>
|
|
91
|
+
{/if}
|
|
92
|
+
|
|
93
|
+
<div class="actions">
|
|
94
|
+
<button type="button" class="primary" onclick={save} disabled={saveDisabled}>Save</button>
|
|
95
|
+
<button type="button" onclick={() => close()}>Cancel</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<style>
|
|
100
|
+
.layout-save {
|
|
101
|
+
padding: 16px 20px;
|
|
102
|
+
max-width: 460px;
|
|
103
|
+
color: var(--shell-fg);
|
|
104
|
+
background: var(--shell-bg);
|
|
105
|
+
font: inherit;
|
|
106
|
+
}
|
|
107
|
+
h2 { margin: 0 0 12px; font-size: 16px; }
|
|
108
|
+
.row { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; font-size: 13px; }
|
|
109
|
+
.row span { color: var(--shell-fg-muted); }
|
|
110
|
+
.name-input {
|
|
111
|
+
background: var(--shell-bg-elevated);
|
|
112
|
+
color: var(--shell-fg);
|
|
113
|
+
border: 1px solid var(--shell-border);
|
|
114
|
+
border-radius: var(--shell-radius-sm, 3px);
|
|
115
|
+
padding: 6px 8px; font: inherit; font-size: 13px;
|
|
116
|
+
}
|
|
117
|
+
.preview { margin: 4px 0 12px; }
|
|
118
|
+
.preview-label { font-size: 12px; color: var(--shell-fg-muted); margin-bottom: 4px; }
|
|
119
|
+
.preview-list {
|
|
120
|
+
list-style: none; padding: 0; margin: 0;
|
|
121
|
+
font-size: 12px; max-height: 160px; overflow: auto;
|
|
122
|
+
border: 1px solid var(--shell-border);
|
|
123
|
+
border-radius: var(--shell-radius-sm, 3px);
|
|
124
|
+
background: var(--shell-bg-elevated);
|
|
125
|
+
}
|
|
126
|
+
.preview-list li { padding: 4px 8px; border-bottom: 1px solid var(--shell-border); }
|
|
127
|
+
.preview-list li:last-child { border-bottom: 0; }
|
|
128
|
+
.muted { color: var(--shell-fg-muted); }
|
|
129
|
+
.error {
|
|
130
|
+
color: var(--shell-fg-muted);
|
|
131
|
+
font-style: italic;
|
|
132
|
+
margin: 4px 0 12px;
|
|
133
|
+
}
|
|
134
|
+
.actions { display: flex; gap: 8px; margin-top: 8px; }
|
|
135
|
+
.actions button {
|
|
136
|
+
background: var(--shell-bg-elevated);
|
|
137
|
+
color: var(--shell-fg);
|
|
138
|
+
border: 1px solid var(--shell-border);
|
|
139
|
+
border-radius: var(--shell-radius-sm, 3px);
|
|
140
|
+
padding: 6px 14px; font: inherit; cursor: pointer;
|
|
141
|
+
}
|
|
142
|
+
.actions button.primary { background: var(--shell-accent); color: #fff; border-color: var(--shell-accent); }
|
|
143
|
+
.actions button:hover { border-color: var(--shell-accent); }
|
|
144
|
+
.actions button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
145
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type IsStandalone } from './filter';
|
|
2
|
+
interface Props {
|
|
3
|
+
floatId: string;
|
|
4
|
+
isStandalone: IsStandalone;
|
|
5
|
+
resolveLabel: (viewId: string) => string;
|
|
6
|
+
defaultName: string;
|
|
7
|
+
onConfirm(name: string): void;
|
|
8
|
+
close: () => void;
|
|
9
|
+
}
|
|
10
|
+
declare const LayoutSaveModal: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type LayoutSaveModal = ReturnType<typeof LayoutSaveModal>;
|
|
12
|
+
export default LayoutSaveModal;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* Saved-layouts section for ShellHome. Renders one card per SavedLayout.
|
|
4
|
+
* Click → restoreToFloat. Right-click sets a typed selection so card
|
|
5
|
+
* actions (Customize, Delete) can recover the layoutId.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getLayouts } from './layoutsState.svelte';
|
|
9
|
+
import { restoreToFloat } from './layoutsApi';
|
|
10
|
+
import { listStandaloneViews } from '../shards/activate.svelte';
|
|
11
|
+
import { toastManager } from '../overlays/toast';
|
|
12
|
+
import { shell } from '../shellRuntime.svelte';
|
|
13
|
+
import { makeSelectionApi } from '../actions/selection.svelte';
|
|
14
|
+
import iconsUrl from '../assets/icons.svg';
|
|
15
|
+
|
|
16
|
+
const layouts = $derived(getLayouts());
|
|
17
|
+
const selection = makeSelectionApi('__layouts__');
|
|
18
|
+
|
|
19
|
+
function isStandalone(viewId: string): boolean {
|
|
20
|
+
return listStandaloneViews().some((v) => v.viewId === viewId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function emitToast(message: string, level: 'info' | 'warn' | 'error'): void {
|
|
24
|
+
toastManager.notify(message, { level });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function open(id: string): void {
|
|
28
|
+
const live = getLayouts().find((l) => l.id === id);
|
|
29
|
+
if (live) restoreToFloat(live, isStandalone, emitToast);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function openCardContextMenu(event: MouseEvent, layoutId: string): void {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
selection.set({ type: 'saved-layout', ref: { layoutId } });
|
|
35
|
+
shell.actions.openContextMenu({
|
|
36
|
+
x: event.clientX,
|
|
37
|
+
y: event.clientY,
|
|
38
|
+
scope: { element: 'saved-layout' },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
{#if layouts.length > 0}
|
|
44
|
+
<section class="saved-layouts-section">
|
|
45
|
+
<h2 class="saved-layouts-heading">Saved Layouts</h2>
|
|
46
|
+
<div class="saved-layouts-grid">
|
|
47
|
+
{#each layouts as layout (layout.id)}
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
class="saved-layout-card"
|
|
51
|
+
class:saved-layout-card--tinted={layout.appearance?.color}
|
|
52
|
+
style:--card-color={layout.appearance?.color ?? 'transparent'}
|
|
53
|
+
data-sh3-scope="element:saved-layout"
|
|
54
|
+
data-saved-layout-id={layout.id}
|
|
55
|
+
onclick={() => open(layout.id)}
|
|
56
|
+
oncontextmenu={(e) => openCardContextMenu(e, layout.id)}
|
|
57
|
+
>
|
|
58
|
+
<span class="saved-layout-card-square">
|
|
59
|
+
<svg class="saved-layout-card-icon">
|
|
60
|
+
<use href="{iconsUrl}#{layout.appearance?.icon ?? 'eye'}" />
|
|
61
|
+
</svg>
|
|
62
|
+
</span>
|
|
63
|
+
<span class="saved-layout-card-label">{layout.name}</span>
|
|
64
|
+
</button>
|
|
65
|
+
{/each}
|
|
66
|
+
</div>
|
|
67
|
+
</section>
|
|
68
|
+
{/if}
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.saved-layouts-section {
|
|
72
|
+
width: 100%;
|
|
73
|
+
max-width: 720px;
|
|
74
|
+
margin-bottom: 28px;
|
|
75
|
+
}
|
|
76
|
+
.saved-layouts-heading {
|
|
77
|
+
font-size: 13px;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
text-transform: uppercase;
|
|
80
|
+
letter-spacing: 0.06em;
|
|
81
|
+
color: var(--shell-fg-subtle);
|
|
82
|
+
margin: 0 0 12px;
|
|
83
|
+
}
|
|
84
|
+
.saved-layouts-grid {
|
|
85
|
+
display: grid;
|
|
86
|
+
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
|
|
87
|
+
gap: 18px 14px;
|
|
88
|
+
}
|
|
89
|
+
.saved-layout-card {
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 6px;
|
|
94
|
+
padding: 0;
|
|
95
|
+
background: transparent;
|
|
96
|
+
border: none;
|
|
97
|
+
color: inherit;
|
|
98
|
+
font: inherit;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
}
|
|
101
|
+
.saved-layout-card-square {
|
|
102
|
+
width: 64px;
|
|
103
|
+
height: 64px;
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
background: var(--saved-layout-color, var(--shell-grad-bg-elevated, var(--shell-bg-elevated)));
|
|
108
|
+
border: 1px dashed var(--shell-border);
|
|
109
|
+
border-radius: var(--shell-radius-md);
|
|
110
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.15);
|
|
111
|
+
transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
|
112
|
+
}
|
|
113
|
+
.saved-layout-card:hover .saved-layout-card-square {
|
|
114
|
+
border-color: var(--shell-accent);
|
|
115
|
+
transform: translateY(-1px);
|
|
116
|
+
box-shadow:
|
|
117
|
+
0 6px 14px rgba(0, 0, 0, 0.3),
|
|
118
|
+
0 0 0 1px color-mix(in srgb, var(--shell-accent) 35%, transparent),
|
|
119
|
+
0 4px 12px color-mix(in srgb, var(--shell-accent) 18%, transparent);
|
|
120
|
+
}
|
|
121
|
+
.saved-layout-card-icon {
|
|
122
|
+
width: 28px;
|
|
123
|
+
height: 28px;
|
|
124
|
+
color: var(--shell-fg);
|
|
125
|
+
}
|
|
126
|
+
.saved-layout-card-label {
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
font-size: 11px;
|
|
129
|
+
line-height: 1.2;
|
|
130
|
+
text-align: center;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
display: -webkit-box;
|
|
133
|
+
-webkit-box-orient: vertical;
|
|
134
|
+
-webkit-line-clamp: 2;
|
|
135
|
+
line-clamp: 2;
|
|
136
|
+
word-break: break-word;
|
|
137
|
+
}
|
|
138
|
+
.saved-layout-card--tinted .saved-layout-card-square {
|
|
139
|
+
background: var(--card-color);
|
|
140
|
+
border-style: solid;
|
|
141
|
+
}
|
|
142
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* filterToStandalone — pure tree-walk that drops every tab/slot whose
|
|
3
|
+
* viewId is not a standalone view, prunes empty branches, and flattens
|
|
4
|
+
* 1-child splits. Returns null when the entire tree filters out.
|
|
5
|
+
*
|
|
6
|
+
* Used at save time (against the live float content) and at restore time
|
|
7
|
+
* (defensive re-pass against shard uninstalls / view-id renames between
|
|
8
|
+
* save and restore). Idempotent on already-filtered trees.
|
|
9
|
+
*
|
|
10
|
+
* The standalone-ness predicate is injected so this module stays pure —
|
|
11
|
+
* the actual list comes from `listStandaloneViews()` at the call site.
|
|
12
|
+
*/
|
|
13
|
+
export function filterToStandalone(node, isStandalone) {
|
|
14
|
+
var _a;
|
|
15
|
+
if (node.type === 'slot') {
|
|
16
|
+
if (node.viewId === null)
|
|
17
|
+
return null;
|
|
18
|
+
if (!isStandalone(node.viewId))
|
|
19
|
+
return null;
|
|
20
|
+
return node;
|
|
21
|
+
}
|
|
22
|
+
if (node.type === 'tabs') {
|
|
23
|
+
const keptTabs = [];
|
|
24
|
+
let originalActiveSurvived = false;
|
|
25
|
+
let originalActiveNewIndex = 0;
|
|
26
|
+
for (let i = 0; i < node.tabs.length; i++) {
|
|
27
|
+
const tab = node.tabs[i];
|
|
28
|
+
if (tab.viewId === null)
|
|
29
|
+
continue;
|
|
30
|
+
if (!isStandalone(tab.viewId))
|
|
31
|
+
continue;
|
|
32
|
+
if (i === node.activeTab) {
|
|
33
|
+
originalActiveSurvived = true;
|
|
34
|
+
originalActiveNewIndex = keptTabs.length;
|
|
35
|
+
}
|
|
36
|
+
keptTabs.push(tab);
|
|
37
|
+
}
|
|
38
|
+
if (keptTabs.length === 0)
|
|
39
|
+
return null;
|
|
40
|
+
return {
|
|
41
|
+
type: 'tabs',
|
|
42
|
+
tabs: keptTabs,
|
|
43
|
+
activeTab: originalActiveSurvived ? originalActiveNewIndex : 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// split
|
|
47
|
+
const keptChildren = [];
|
|
48
|
+
const keptSizes = [];
|
|
49
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
50
|
+
const child = filterToStandalone(node.children[i], isStandalone);
|
|
51
|
+
if (child === null)
|
|
52
|
+
continue;
|
|
53
|
+
keptChildren.push(child);
|
|
54
|
+
keptSizes.push((_a = node.sizes[i]) !== null && _a !== void 0 ? _a : 1);
|
|
55
|
+
}
|
|
56
|
+
if (keptChildren.length === 0)
|
|
57
|
+
return null;
|
|
58
|
+
if (keptChildren.length === 1)
|
|
59
|
+
return keptChildren[0];
|
|
60
|
+
return {
|
|
61
|
+
type: 'split',
|
|
62
|
+
direction: node.direction,
|
|
63
|
+
sizes: keptSizes,
|
|
64
|
+
children: keptChildren,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { filterToStandalone } from './filter';
|
|
3
|
+
const STANDALONE = new Set(['shell:terminal', 'graphlive:hierarchy']);
|
|
4
|
+
const isStandalone = (viewId) => STANDALONE.has(viewId);
|
|
5
|
+
describe('filterToStandalone', () => {
|
|
6
|
+
it('returns null for an empty slot', () => {
|
|
7
|
+
const tree = { type: 'slot', slotId: 's', viewId: null };
|
|
8
|
+
expect(filterToStandalone(tree, isStandalone)).toBeNull();
|
|
9
|
+
});
|
|
10
|
+
it('returns null for a slot whose view is non-standalone', () => {
|
|
11
|
+
const tree = { type: 'slot', slotId: 's', viewId: 'app-only:view' };
|
|
12
|
+
expect(filterToStandalone(tree, isStandalone)).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
it('passes through a slot with a standalone view', () => {
|
|
15
|
+
const tree = { type: 'slot', slotId: 's', viewId: 'shell:terminal' };
|
|
16
|
+
expect(filterToStandalone(tree, isStandalone)).toEqual(tree);
|
|
17
|
+
});
|
|
18
|
+
it('drops non-standalone tabs from a tabs node', () => {
|
|
19
|
+
const tree = {
|
|
20
|
+
type: 'tabs',
|
|
21
|
+
activeTab: 0,
|
|
22
|
+
tabs: [
|
|
23
|
+
{ slotId: 'a', viewId: 'shell:terminal', label: 'Shell' },
|
|
24
|
+
{ slotId: 'b', viewId: 'app-only:view', label: 'App' },
|
|
25
|
+
{ slotId: 'c', viewId: 'graphlive:hierarchy', label: 'Graph' },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
const out = filterToStandalone(tree, isStandalone);
|
|
29
|
+
expect(out).toEqual({
|
|
30
|
+
type: 'tabs',
|
|
31
|
+
activeTab: 0,
|
|
32
|
+
tabs: [
|
|
33
|
+
{ slotId: 'a', viewId: 'shell:terminal', label: 'Shell' },
|
|
34
|
+
{ slotId: 'c', viewId: 'graphlive:hierarchy', label: 'Graph' },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it('clamps activeTab when the original active was filtered out', () => {
|
|
39
|
+
const tree = {
|
|
40
|
+
type: 'tabs',
|
|
41
|
+
activeTab: 1,
|
|
42
|
+
tabs: [
|
|
43
|
+
{ slotId: 'a', viewId: 'shell:terminal', label: 'Shell' },
|
|
44
|
+
{ slotId: 'b', viewId: 'app-only:view', label: 'App' },
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
const out = filterToStandalone(tree, isStandalone);
|
|
48
|
+
expect(out).not.toBeNull();
|
|
49
|
+
if (out && out.type === 'tabs')
|
|
50
|
+
expect(out.activeTab).toBe(0);
|
|
51
|
+
});
|
|
52
|
+
it('returns null for a tabs node whose tabs all filter out', () => {
|
|
53
|
+
const tree = {
|
|
54
|
+
type: 'tabs',
|
|
55
|
+
activeTab: 0,
|
|
56
|
+
tabs: [
|
|
57
|
+
{ slotId: 'a', viewId: 'app-only:view', label: 'App' },
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
expect(filterToStandalone(tree, isStandalone)).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
it('flattens a 1-child split to that child', () => {
|
|
63
|
+
const tree = {
|
|
64
|
+
type: 'split',
|
|
65
|
+
direction: 'horizontal',
|
|
66
|
+
sizes: [1, 1],
|
|
67
|
+
children: [
|
|
68
|
+
{ type: 'slot', slotId: 'a', viewId: 'shell:terminal' },
|
|
69
|
+
{ type: 'slot', slotId: 'b', viewId: 'app-only:view' },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
const out = filterToStandalone(tree, isStandalone);
|
|
73
|
+
expect(out).toEqual({ type: 'slot', slotId: 'a', viewId: 'shell:terminal' });
|
|
74
|
+
});
|
|
75
|
+
it('returns null for a split whose children all filter out', () => {
|
|
76
|
+
const tree = {
|
|
77
|
+
type: 'split',
|
|
78
|
+
direction: 'horizontal',
|
|
79
|
+
sizes: [1, 1],
|
|
80
|
+
children: [
|
|
81
|
+
{ type: 'slot', slotId: 'a', viewId: 'app-only:view' },
|
|
82
|
+
{ type: 'slot', slotId: 'b', viewId: null },
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
expect(filterToStandalone(tree, isStandalone)).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
it('preserves split sizes for surviving children', () => {
|
|
88
|
+
const tree = {
|
|
89
|
+
type: 'split',
|
|
90
|
+
direction: 'vertical',
|
|
91
|
+
sizes: [1, 2, 3],
|
|
92
|
+
children: [
|
|
93
|
+
{ type: 'slot', slotId: 'a', viewId: 'shell:terminal' },
|
|
94
|
+
{ type: 'slot', slotId: 'b', viewId: 'app-only:view' },
|
|
95
|
+
{ type: 'slot', slotId: 'c', viewId: 'graphlive:hierarchy' },
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
const out = filterToStandalone(tree, isStandalone);
|
|
99
|
+
expect(out).toEqual({
|
|
100
|
+
type: 'split',
|
|
101
|
+
direction: 'vertical',
|
|
102
|
+
sizes: [1, 3],
|
|
103
|
+
children: [
|
|
104
|
+
{ type: 'slot', slotId: 'a', viewId: 'shell:terminal' },
|
|
105
|
+
{ type: 'slot', slotId: 'c', viewId: 'graphlive:hierarchy' },
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
it('is idempotent on already-filtered trees', () => {
|
|
110
|
+
const tree = {
|
|
111
|
+
type: 'split',
|
|
112
|
+
direction: 'horizontal',
|
|
113
|
+
sizes: [1, 1],
|
|
114
|
+
children: [
|
|
115
|
+
{ type: 'slot', slotId: 'a', viewId: 'shell:terminal' },
|
|
116
|
+
{ type: 'slot', slotId: 'b', viewId: 'graphlive:hierarchy' },
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
const once = filterToStandalone(tree, isStandalone);
|
|
120
|
+
const twice = filterToStandalone(once, isStandalone);
|
|
121
|
+
expect(twice).toEqual(once);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { layoutsShard } from './layoutsShard.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { layoutsShard } from './layoutsShard.svelte';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LayoutNode } from '../layout/types';
|
|
2
|
+
import type { SavedLayout } from './types';
|
|
3
|
+
import { type IsStandalone } from './filter';
|
|
4
|
+
export type ToastEmit = (message: string, level: 'info' | 'warn' | 'error') => void;
|
|
5
|
+
export declare function captureFromFloat(floatId: string, isStandalone: IsStandalone): {
|
|
6
|
+
size: {
|
|
7
|
+
w: number;
|
|
8
|
+
h: number;
|
|
9
|
+
};
|
|
10
|
+
content: LayoutNode;
|
|
11
|
+
} | null;
|
|
12
|
+
export declare function restoreToFloat(layout: SavedLayout, isStandalone: IsStandalone, toast: ToastEmit): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* captureFromFloat / restoreToFloat — orchestration glue between the
|
|
3
|
+
* live float manager, the standalone-view filter, and the floats overlay.
|
|
4
|
+
*
|
|
5
|
+
* Pure orchestration: the standalone predicate and the toast emitter are
|
|
6
|
+
* injected so this file is testable in the dom project without booting
|
|
7
|
+
* the full shard system. The shard wires the real lookups in.
|
|
8
|
+
*/
|
|
9
|
+
import { floatManager } from '../overlays/float';
|
|
10
|
+
import { filterToStandalone } from './filter';
|
|
11
|
+
// JSON round-trip clone. structuredClone can't traverse Svelte's $state
|
|
12
|
+
// proxies (saved layouts live in the user-zone proxy), so we route
|
|
13
|
+
// through JSON. LayoutNode is intentionally JSON-safe — TabEntry.meta is
|
|
14
|
+
// ephemeral and not part of a saved/restored layout.
|
|
15
|
+
function deepClone(value) {
|
|
16
|
+
return JSON.parse(JSON.stringify(value));
|
|
17
|
+
}
|
|
18
|
+
export function captureFromFloat(floatId, isStandalone) {
|
|
19
|
+
const entry = floatManager.list().find((f) => f.id === floatId);
|
|
20
|
+
if (!entry)
|
|
21
|
+
return null;
|
|
22
|
+
const filtered = filterToStandalone(entry.content, isStandalone);
|
|
23
|
+
if (filtered === null)
|
|
24
|
+
return null;
|
|
25
|
+
return {
|
|
26
|
+
size: { w: entry.size.w, h: entry.size.h },
|
|
27
|
+
content: deepClone(filtered),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function restoreToFloat(layout, isStandalone, toast) {
|
|
31
|
+
const filtered = filterToStandalone(layout.content, isStandalone);
|
|
32
|
+
if (filtered === null) {
|
|
33
|
+
toast(`Saved layout "${layout.name}" has no available standalone views`, 'warn');
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
return floatManager.openWithContent({
|
|
37
|
+
content: deepClone(filtered),
|
|
38
|
+
size: { w: layout.size.w, h: layout.size.h },
|
|
39
|
+
title: layout.name,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|