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.
Files changed (141) hide show
  1. package/dist/actions/ctx-actions.svelte.test.js +111 -0
  2. package/dist/actions/dispatcher.svelte.js +23 -2
  3. package/dist/actions/dispatcher.test.js +33 -0
  4. package/dist/actions/listActionsFromEntries.test.js +78 -0
  5. package/dist/actions/listActive.d.ts +2 -1
  6. package/dist/actions/listActive.js +43 -17
  7. package/dist/actions/listeners.d.ts +16 -0
  8. package/dist/actions/listeners.js +68 -14
  9. package/dist/actions/programmatic-dispatch.svelte.test.d.ts +1 -0
  10. package/dist/actions/programmatic-dispatch.svelte.test.js +98 -0
  11. package/dist/actions/types.d.ts +37 -0
  12. package/dist/api.d.ts +1 -1
  13. package/dist/app/store/verbs.js +4 -0
  14. package/dist/app-appearance/appearanceShard.svelte.js +19 -6
  15. package/dist/app-appearance/appearanceState.svelte.js +3 -3
  16. package/dist/host.js +2 -1
  17. package/dist/layouts-shard/LayoutSaveModal.svelte +145 -0
  18. package/dist/layouts-shard/LayoutSaveModal.svelte.d.ts +12 -0
  19. package/dist/layouts-shard/LayoutsSection.svelte +142 -0
  20. package/dist/layouts-shard/LayoutsSection.svelte.d.ts +3 -0
  21. package/dist/layouts-shard/filter.d.ts +3 -0
  22. package/dist/layouts-shard/filter.js +66 -0
  23. package/dist/layouts-shard/filter.test.d.ts +1 -0
  24. package/dist/layouts-shard/filter.test.js +123 -0
  25. package/dist/layouts-shard/index.d.ts +1 -0
  26. package/dist/layouts-shard/index.js +1 -0
  27. package/dist/layouts-shard/layoutsApi.d.ts +12 -0
  28. package/dist/layouts-shard/layoutsApi.js +41 -0
  29. package/dist/layouts-shard/layoutsApi.test.d.ts +1 -0
  30. package/dist/layouts-shard/layoutsApi.test.js +74 -0
  31. package/dist/layouts-shard/layoutsShard.svelte.d.ts +11 -0
  32. package/dist/layouts-shard/layoutsShard.svelte.js +231 -0
  33. package/dist/layouts-shard/layoutsShard.svelte.test.d.ts +1 -0
  34. package/dist/layouts-shard/layoutsShard.svelte.test.js +215 -0
  35. package/dist/layouts-shard/layoutsState.svelte.d.ts +9 -0
  36. package/dist/layouts-shard/layoutsState.svelte.js +50 -0
  37. package/dist/layouts-shard/layoutsState.test.d.ts +1 -0
  38. package/dist/layouts-shard/layoutsState.test.js +43 -0
  39. package/dist/layouts-shard/types.d.ts +21 -0
  40. package/dist/layouts-shard/types.js +6 -0
  41. package/dist/{app-appearance/AppAppearanceModal.svelte → overlays/EntityAppearanceModal.svelte} +36 -31
  42. package/dist/overlays/EntityAppearanceModal.svelte.d.ts +19 -0
  43. package/dist/overlays/EntityAppearanceModal.test.d.ts +1 -0
  44. package/dist/overlays/EntityAppearanceModal.test.js +57 -0
  45. package/dist/overlays/FloatFrame.svelte +149 -8
  46. package/dist/overlays/FloatFrame.svelte.d.ts +1 -1
  47. package/dist/overlays/FloatLayer.svelte +2 -2
  48. package/dist/overlays/float.d.ts +38 -1
  49. package/dist/overlays/float.js +82 -0
  50. package/dist/overlays/float.test.js +394 -0
  51. package/dist/overlays/floatMaximized.svelte.d.ts +4 -0
  52. package/dist/overlays/floatMaximized.svelte.js +30 -0
  53. package/dist/runtime/runVerb-shell.test.d.ts +1 -0
  54. package/dist/runtime/runVerb-shell.test.js +231 -0
  55. package/dist/sh3core-shard/ShellHome.svelte +3 -0
  56. package/dist/sh3core-shard/sh3coreShard.svelte.d.ts +7 -0
  57. package/dist/sh3core-shard/sh3coreShard.svelte.js +23 -0
  58. package/dist/shards/activate-runtime.test.js +24 -2
  59. package/dist/shards/activate.svelte.js +18 -4
  60. package/dist/shards/types.d.ts +44 -4
  61. package/dist/shell-shard/CommandLine.svelte +143 -0
  62. package/dist/shell-shard/CommandLine.svelte.d.ts +26 -0
  63. package/dist/shell-shard/CommandLine.svelte.test.d.ts +1 -0
  64. package/dist/shell-shard/CommandLine.svelte.test.js +43 -0
  65. package/dist/shell-shard/InputLine.svelte +17 -40
  66. package/dist/shell-shard/InputLine.svelte.d.ts +2 -0
  67. package/dist/shell-shard/ScrollbackView.svelte +10 -3
  68. package/dist/shell-shard/ScrollbackView.svelte.d.ts +1 -0
  69. package/dist/shell-shard/Terminal.svelte +94 -22
  70. package/dist/shell-shard/buffer-store.d.ts +15 -0
  71. package/dist/shell-shard/buffer-store.js +124 -0
  72. package/dist/shell-shard/buffer-store.svelte.test.d.ts +1 -0
  73. package/dist/shell-shard/buffer-store.svelte.test.js +107 -0
  74. package/dist/shell-shard/buffer-zone-state.svelte.d.ts +38 -0
  75. package/dist/shell-shard/buffer-zone-state.svelte.js +31 -0
  76. package/dist/shell-shard/contract.d.ts +7 -0
  77. package/dist/shell-shard/dispatch-custom.test.js +3 -1
  78. package/dist/shell-shard/dispatch-gating.test.js +6 -2
  79. package/dist/shell-shard/dispatch-invoke.test.js +10 -8
  80. package/dist/shell-shard/dispatch.d.ts +7 -2
  81. package/dist/shell-shard/dispatch.js +23 -27
  82. package/dist/shell-shard/display-cwd.d.ts +1 -0
  83. package/dist/shell-shard/display-cwd.js +27 -0
  84. package/dist/shell-shard/display-cwd.test.d.ts +1 -0
  85. package/dist/shell-shard/display-cwd.test.js +29 -0
  86. package/dist/shell-shard/entries/StatusEntry.svelte +2 -0
  87. package/dist/shell-shard/manifest.js +2 -1
  88. package/dist/shell-shard/manifest.test.d.ts +1 -0
  89. package/dist/shell-shard/manifest.test.js +8 -0
  90. package/dist/shell-shard/mode-buffer.svelte.d.ts +8 -0
  91. package/dist/shell-shard/mode-buffer.svelte.js +19 -0
  92. package/dist/shell-shard/mode-buffer.svelte.test.d.ts +1 -0
  93. package/dist/shell-shard/mode-buffer.svelte.test.js +25 -0
  94. package/dist/shell-shard/modes/builtin.js +2 -0
  95. package/dist/shell-shard/modes/types.d.ts +8 -0
  96. package/dist/shell-shard/protocol.d.ts +12 -6
  97. package/dist/shell-shard/replay.d.ts +3 -0
  98. package/dist/shell-shard/replay.js +44 -0
  99. package/dist/shell-shard/replay.svelte.test.d.ts +1 -0
  100. package/dist/shell-shard/replay.svelte.test.js +47 -0
  101. package/dist/shell-shard/rich-registry.d.ts +5 -0
  102. package/dist/shell-shard/rich-registry.js +25 -0
  103. package/dist/shell-shard/rich-registry.test.d.ts +1 -0
  104. package/dist/shell-shard/rich-registry.test.js +31 -0
  105. package/dist/shell-shard/scrollback.svelte.d.ts +2 -0
  106. package/dist/shell-shard/scrollback.svelte.js +23 -0
  107. package/dist/shell-shard/scrollback.svelte.test.d.ts +1 -0
  108. package/dist/shell-shard/scrollback.svelte.test.js +51 -0
  109. package/dist/shell-shard/session-client.svelte.d.ts +18 -2
  110. package/dist/shell-shard/session-client.svelte.js +21 -4
  111. package/dist/shell-shard/shellApi.d.ts +2 -1
  112. package/dist/shell-shard/shellApi.js +32 -3
  113. package/dist/shell-shard/shellApi.svelte.test.d.ts +1 -0
  114. package/dist/shell-shard/shellApi.svelte.test.js +59 -0
  115. package/dist/shell-shard/shellShard.svelte.js +11 -1
  116. package/dist/shell-shard/terminal-dispatch.test.js +3 -1
  117. package/dist/shell-shard/verbs/apps.js +9 -0
  118. package/dist/shell-shard/verbs/env.js +4 -0
  119. package/dist/shell-shard/verbs/help.js +9 -1
  120. package/dist/shell-shard/verbs/help.svelte.test.d.ts +1 -0
  121. package/dist/shell-shard/verbs/help.svelte.test.js +53 -0
  122. package/dist/shell-shard/verbs/history.js +8 -1
  123. package/dist/shell-shard/verbs/index.js +0 -8
  124. package/dist/shell-shard/verbs/shards.js +5 -0
  125. package/dist/shell-shard/verbs/views.js +9 -0
  126. package/dist/shell-shard/verbs/zones.js +9 -0
  127. package/dist/verbs/types.d.ts +9 -0
  128. package/dist/version.d.ts +1 -1
  129. package/dist/version.js +1 -1
  130. package/package.json +1 -1
  131. package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +0 -8
  132. package/dist/shell-shard/verbs/cat.d.ts +0 -2
  133. package/dist/shell-shard/verbs/cat.js +0 -34
  134. package/dist/shell-shard/verbs/cd.test.js +0 -56
  135. package/dist/shell-shard/verbs/ls.d.ts +0 -2
  136. package/dist/shell-shard/verbs/ls.js +0 -29
  137. package/dist/shell-shard/verbs/ls.test.js +0 -49
  138. package/dist/shell-shard/verbs/session.d.ts +0 -4
  139. package/dist/shell-shard/verbs/session.js +0 -97
  140. /package/dist/{shell-shard/verbs/cd.test.d.ts → actions/ctx-actions.svelte.test.d.ts} +0 -0
  141. /package/dist/{shell-shard/verbs/ls.test.d.ts → actions/listActionsFromEntries.test.d.ts} +0 -0
@@ -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 AppAppearanceModal from './AppAppearanceModal.svelte';
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
- appId: ref.appId,
28
- appLabel: (_a = m === null || m === void 0 ? void 0 : m.label) !== null && _a !== void 0 ? _a : ref.appId,
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(AppAppearanceModal, props);
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 AppAppearanceModal can import
5
- * get/set without creating an import cycle through the shard's modal
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,3 @@
1
+ declare const LayoutsSection: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type LayoutsSection = ReturnType<typeof LayoutsSection>;
3
+ export default LayoutsSection;
@@ -0,0 +1,3 @@
1
+ import type { LayoutNode } from '../layout/types';
2
+ export type IsStandalone = (viewId: string) => boolean;
3
+ export declare function filterToStandalone(node: LayoutNode, isStandalone: IsStandalone): LayoutNode | null;
@@ -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 {};