sh3-core 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__test__/fixtures.d.ts +12 -0
- package/dist/__test__/fixtures.js +62 -0
- package/dist/__test__/render.d.ts +3 -0
- package/dist/__test__/render.js +11 -0
- package/dist/__test__/reset.d.ts +14 -0
- package/dist/__test__/reset.js +34 -0
- package/dist/__test__/setup-dom.d.ts +1 -0
- package/dist/__test__/setup-dom.js +26 -0
- package/dist/__test__/smoke.test.d.ts +1 -0
- package/dist/__test__/smoke.test.js +28 -0
- package/dist/api.d.ts +15 -2
- package/dist/api.js +13 -1
- package/dist/app/store/StoreView.svelte +36 -7
- package/dist/app/store/storeShard.svelte.js +9 -3
- package/dist/app/store/verbs.js +8 -2
- package/dist/apps/lifecycle.d.ts +11 -0
- package/dist/apps/lifecycle.js +48 -11
- package/dist/apps/lifecycle.test.d.ts +1 -0
- package/dist/apps/lifecycle.test.js +309 -0
- package/dist/apps/registry.svelte.d.ts +2 -0
- package/dist/apps/registry.svelte.js +5 -0
- package/dist/apps/types.d.ts +24 -2
- package/dist/createShell.d.ts +2 -0
- package/dist/createShell.js +9 -7
- package/dist/documents/handle.js +5 -0
- package/dist/documents/index.d.ts +1 -0
- package/dist/documents/index.js +1 -0
- package/dist/documents/journal-hook.d.ts +6 -0
- package/dist/documents/journal-hook.js +16 -0
- package/dist/documents/sync/activate-integration.test.d.ts +1 -0
- package/dist/documents/sync/activate-integration.test.js +37 -0
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte +99 -0
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte.d.ts +15 -0
- package/dist/documents/sync/components/SyncGrantPicker.svelte +70 -0
- package/dist/documents/sync/components/SyncGrantPicker.svelte.d.ts +12 -0
- package/dist/documents/sync/conflicts.d.ts +30 -0
- package/dist/documents/sync/conflicts.js +77 -0
- package/dist/documents/sync/conflicts.test.d.ts +1 -0
- package/dist/documents/sync/conflicts.test.js +71 -0
- package/dist/documents/sync/engine.d.ts +19 -0
- package/dist/documents/sync/engine.js +188 -0
- package/dist/documents/sync/engine.test.d.ts +1 -0
- package/dist/documents/sync/engine.test.js +169 -0
- package/dist/documents/sync/handle.d.ts +11 -0
- package/dist/documents/sync/handle.js +79 -0
- package/dist/documents/sync/handle.test.d.ts +1 -0
- package/dist/documents/sync/handle.test.js +56 -0
- package/dist/documents/sync/hash.d.ts +1 -0
- package/dist/documents/sync/hash.js +13 -0
- package/dist/documents/sync/hash.test.d.ts +1 -0
- package/dist/documents/sync/hash.test.js +20 -0
- package/dist/documents/sync/index.d.ts +6 -0
- package/dist/documents/sync/index.js +12 -0
- package/dist/documents/sync/journal.d.ts +30 -0
- package/dist/documents/sync/journal.js +179 -0
- package/dist/documents/sync/journal.test.d.ts +1 -0
- package/dist/documents/sync/journal.test.js +87 -0
- package/dist/documents/sync/registry.d.ts +10 -0
- package/dist/documents/sync/registry.js +66 -0
- package/dist/documents/sync/registry.test.d.ts +1 -0
- package/dist/documents/sync/registry.test.js +42 -0
- package/dist/documents/sync/serialization.d.ts +5 -0
- package/dist/documents/sync/serialization.js +24 -0
- package/dist/documents/sync/serialization.test.d.ts +1 -0
- package/dist/documents/sync/serialization.test.js +26 -0
- package/dist/documents/sync/singleton.d.ts +11 -0
- package/dist/documents/sync/singleton.js +26 -0
- package/dist/documents/sync/tombstones.d.ts +19 -0
- package/dist/documents/sync/tombstones.js +58 -0
- package/dist/documents/sync/tombstones.test.d.ts +1 -0
- package/dist/documents/sync/tombstones.test.js +37 -0
- package/dist/documents/sync/types.d.ts +116 -0
- package/dist/documents/sync/types.js +27 -0
- package/dist/documents/sync/write-hook.test.d.ts +1 -0
- package/dist/documents/sync/write-hook.test.js +36 -0
- package/dist/env/client.d.ts +10 -5
- package/dist/env/client.js +12 -4
- package/dist/layout/LayoutRenderer.browser.test.d.ts +1 -0
- package/dist/layout/LayoutRenderer.browser.test.js +274 -0
- package/dist/layout/LayoutRenderer.svelte +2 -1
- package/dist/layout/LayoutRenderer.test.d.ts +1 -0
- package/dist/layout/LayoutRenderer.test.js +143 -0
- package/dist/layout/SlotContainer.svelte +8 -2
- package/dist/layout/SlotDropZone.svelte +19 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-1-drag-tab-between-groups-moves-a-tab-from-one-tabs-group-to-another-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-2-drag-tab-to-quadrant-creates-a-split-when-dropping-a-tab-on-a-quadrant-drop-zone-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-3-splitter-drag-updates-split-sizes-when-the-splitter-handle-is-dragged-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-4-close-policy-removes-closable-tabs--keeps-non-closable--and-awaits-canClose-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-5-splitter-collapse-toggle-toggles-collapsed-i--on-double-click-1.png +0 -0
- package/dist/layout/drag.svelte.d.ts +5 -0
- package/dist/layout/drag.svelte.js +15 -0
- package/dist/layout/slotHostPool.svelte.d.ts +16 -1
- package/dist/layout/slotHostPool.svelte.js +123 -5
- package/dist/layout/slotHostPool.test.d.ts +1 -0
- package/dist/layout/slotHostPool.test.js +104 -0
- package/dist/layout/store.svelte.d.ts +22 -0
- package/dist/layout/store.svelte.js +78 -16
- package/dist/layout/tree-walk.d.ts +2 -0
- package/dist/layout/tree-walk.js +1 -1
- package/dist/layout/types.d.ts +5 -0
- package/dist/overlays/float.d.ts +2 -0
- package/dist/overlays/float.js +4 -1
- package/dist/overlays/float.test.js +102 -1
- package/dist/primitives/ResizableSplitter.svelte +2 -0
- package/dist/primitives/TabbedPanel.svelte +4 -0
- package/dist/primitives/TabbedPanel.svelte.d.ts +2 -0
- package/dist/registry/installer.d.ts +10 -7
- package/dist/registry/installer.js +39 -35
- package/dist/registry/register.d.ts +17 -0
- package/dist/registry/register.js +22 -0
- package/dist/registry/register.test.d.ts +1 -0
- package/dist/registry/register.test.js +28 -0
- package/dist/shards/activate.svelte.d.ts +6 -0
- package/dist/shards/activate.svelte.js +33 -2
- package/dist/shards/registry.d.ts +4 -0
- package/dist/shards/registry.js +18 -0
- package/dist/shards/types.d.ts +16 -1
- package/dist/shell-shard/Terminal.svelte +140 -33
- package/dist/shell-shard/Terminal.svelte.d.ts +3 -0
- package/dist/shell-shard/auto-relocate.d.ts +12 -0
- package/dist/shell-shard/auto-relocate.js +20 -0
- package/dist/shell-shard/auto-relocate.test.d.ts +1 -0
- package/dist/shell-shard/auto-relocate.test.js +35 -0
- package/dist/shell-shard/dispatch.d.ts +15 -0
- package/dist/shell-shard/dispatch.js +56 -0
- package/dist/shell-shard/modes/builtin.d.ts +5 -0
- package/dist/shell-shard/modes/builtin.js +18 -0
- package/dist/shell-shard/modes/prefs.d.ts +5 -0
- package/dist/shell-shard/modes/prefs.js +31 -0
- package/dist/shell-shard/modes/prefs.test.d.ts +1 -0
- package/dist/shell-shard/modes/prefs.test.js +46 -0
- package/dist/shell-shard/modes/registry.d.ts +7 -0
- package/dist/shell-shard/modes/registry.js +27 -0
- package/dist/shell-shard/modes/registry.test.d.ts +1 -0
- package/dist/shell-shard/modes/registry.test.js +35 -0
- package/dist/shell-shard/modes/types.d.ts +8 -0
- package/dist/shell-shard/modes/types.js +1 -0
- package/dist/shell-shard/protocol.d.ts +6 -0
- package/dist/shell-shard/shellShard.svelte.js +5 -1
- package/dist/shell-shard/tenant-fs-client.d.ts +24 -0
- package/dist/shell-shard/tenant-fs-client.js +44 -0
- package/dist/shell-shard/tenant-fs-client.test.d.ts +1 -0
- package/dist/shell-shard/tenant-fs-client.test.js +49 -0
- package/dist/shell-shard/terminal-dispatch.test.d.ts +1 -0
- package/dist/shell-shard/terminal-dispatch.test.js +53 -0
- package/dist/shell-shard/toolbar/Toolbar.svelte +62 -0
- package/dist/shell-shard/toolbar/Toolbar.svelte.d.ts +11 -0
- package/dist/shell-shard/toolbar/slots/FocusLockSlot.svelte +28 -0
- package/dist/shell-shard/toolbar/slots/FocusLockSlot.svelte.d.ts +7 -0
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte +102 -0
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte.d.ts +11 -0
- package/dist/shell-shard/toolbar/slots/TargetShardSlot.svelte +17 -0
- package/dist/shell-shard/toolbar/slots/TargetShardSlot.svelte.d.ts +6 -0
- package/dist/shell-shard/toolbar/slots.d.ts +17 -0
- package/dist/shell-shard/toolbar/slots.js +26 -0
- package/dist/shell-shard/toolbar/slots.test.d.ts +1 -0
- package/dist/shell-shard/toolbar/slots.test.js +28 -0
- package/dist/shell-shard/verbs/cat.d.ts +2 -0
- package/dist/shell-shard/verbs/cat.js +34 -0
- package/dist/shell-shard/verbs/cd.test.d.ts +1 -0
- package/dist/shell-shard/verbs/cd.test.js +56 -0
- package/dist/shell-shard/verbs/env.d.ts +2 -0
- package/dist/shell-shard/verbs/env.js +14 -0
- package/dist/shell-shard/verbs/index.js +6 -1
- package/dist/shell-shard/verbs/ls.d.ts +2 -0
- package/dist/shell-shard/verbs/ls.js +29 -0
- package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
- package/dist/shell-shard/verbs/ls.test.js +49 -0
- package/dist/shell-shard/verbs/session.d.ts +0 -1
- package/dist/shell-shard/verbs/session.js +58 -26
- package/dist/verbs/types.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -1
|
@@ -32,9 +32,59 @@
|
|
|
32
32
|
* edge-case escape hatch reserved by the design. Not wired yet —
|
|
33
33
|
* phase 6 has no view that needs it.
|
|
34
34
|
*/
|
|
35
|
-
import { getView } from '../shards/registry';
|
|
35
|
+
import { getView, __addViewRegistrationListener } from '../shards/registry';
|
|
36
36
|
const pool = new Map();
|
|
37
37
|
const pendingDestroy = new Set();
|
|
38
|
+
/**
|
|
39
|
+
* Called by the registry whenever a new ViewFactory is registered.
|
|
40
|
+
* Scans the pool for entries that have the matching viewId but were not
|
|
41
|
+
* mounted (because the factory wasn't available at acquire-time). Mounts
|
|
42
|
+
* them now.
|
|
43
|
+
*
|
|
44
|
+
* This handles the "late factory registration" case: a shard activates
|
|
45
|
+
* after the layout has already acquired a slot for its view. Rather than
|
|
46
|
+
* requiring the layout to re-acquire, the pool retries the mount here.
|
|
47
|
+
*/
|
|
48
|
+
function onViewRegistered(viewId, factory) {
|
|
49
|
+
for (const [slotId, entry] of pool.entries()) {
|
|
50
|
+
if (entry.viewId !== viewId || entry.handle !== undefined)
|
|
51
|
+
continue;
|
|
52
|
+
// Entry is in the pool, matches the view, and has no handle yet —
|
|
53
|
+
// the factory wasn't available when the microtask ran. Mount now.
|
|
54
|
+
const ctx = {
|
|
55
|
+
slotId,
|
|
56
|
+
viewId,
|
|
57
|
+
label: entry.label,
|
|
58
|
+
meta: entry.meta,
|
|
59
|
+
setDirty(dirty) {
|
|
60
|
+
dirtyState[slotId] = dirty;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
queueMicrotask(() => {
|
|
64
|
+
var _a, _b;
|
|
65
|
+
// Re-check: entry may have been released before this microtask fires.
|
|
66
|
+
if (!pool.has(slotId))
|
|
67
|
+
return;
|
|
68
|
+
if (entry.handle !== undefined)
|
|
69
|
+
return; // already mounted by a race
|
|
70
|
+
entry.handle = factory.mount(entry.host, ctx);
|
|
71
|
+
if (((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) || slotId.startsWith('float:')) {
|
|
72
|
+
closableState[slotId] = true;
|
|
73
|
+
}
|
|
74
|
+
if ((_b = entry.handle) === null || _b === void 0 ? void 0 : _b.onResize) {
|
|
75
|
+
const onResize = entry.handle.onResize.bind(entry.handle);
|
|
76
|
+
entry.resizeObserver = new ResizeObserver((entries) => {
|
|
77
|
+
for (const e of entries) {
|
|
78
|
+
const box = e.contentRect;
|
|
79
|
+
onResize(Math.round(box.width), Math.round(box.height));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
entry.resizeObserver.observe(entry.host);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
__addViewRegistrationListener(onViewRegistered);
|
|
38
88
|
/**
|
|
39
89
|
* Reactive dirty-state map. Keyed by slotId, values are $state so
|
|
40
90
|
* Svelte tracks reads in `isSlotDirty()` and re-renders the tab strip
|
|
@@ -75,7 +125,7 @@ const closableState = $state({});
|
|
|
75
125
|
* is destroyed before its deferred mount ever runs (e.g. rapid
|
|
76
126
|
* add-then-remove of a slot during a drag).
|
|
77
127
|
*/
|
|
78
|
-
function createHost(slotId, viewId, label) {
|
|
128
|
+
function createHost(slotId, viewId, label, meta) {
|
|
79
129
|
const host = document.createElement('div');
|
|
80
130
|
host.className = 'slot-host';
|
|
81
131
|
host.dataset.slotId = slotId;
|
|
@@ -93,6 +143,7 @@ function createHost(slotId, viewId, label) {
|
|
|
93
143
|
handle: undefined,
|
|
94
144
|
viewId,
|
|
95
145
|
label,
|
|
146
|
+
meta,
|
|
96
147
|
refcount: 0,
|
|
97
148
|
resizeObserver: undefined,
|
|
98
149
|
cancelPendingMount: () => {
|
|
@@ -108,6 +159,7 @@ function createHost(slotId, viewId, label) {
|
|
|
108
159
|
slotId,
|
|
109
160
|
viewId: viewId !== null && viewId !== void 0 ? viewId : '',
|
|
110
161
|
label,
|
|
162
|
+
meta,
|
|
111
163
|
setDirty(dirty) {
|
|
112
164
|
dirtyState[slotId] = dirty;
|
|
113
165
|
},
|
|
@@ -140,13 +192,13 @@ function createHost(slotId, viewId, label) {
|
|
|
140
192
|
* the pool does not know which wrapper owns the host at any given time,
|
|
141
193
|
* and that is intentional. The same host may be passed around.
|
|
142
194
|
*/
|
|
143
|
-
export function acquireSlotHost(slotId, viewId, label) {
|
|
195
|
+
export function acquireSlotHost(slotId, viewId, label, meta) {
|
|
144
196
|
// If the slot was about to be destroyed, cancel — this acquire is the
|
|
145
197
|
// "other half" of a re-parent (teardown was the previous container).
|
|
146
198
|
pendingDestroy.delete(slotId);
|
|
147
199
|
let entry = pool.get(slotId);
|
|
148
200
|
if (!entry) {
|
|
149
|
-
entry = createHost(slotId, viewId, label);
|
|
201
|
+
entry = createHost(slotId, viewId, label, meta);
|
|
150
202
|
pool.set(slotId, entry);
|
|
151
203
|
}
|
|
152
204
|
entry.refcount++;
|
|
@@ -162,8 +214,16 @@ export function releaseSlotHost(slotId) {
|
|
|
162
214
|
if (!entry)
|
|
163
215
|
return;
|
|
164
216
|
entry.refcount--;
|
|
165
|
-
if (entry.refcount > 0)
|
|
217
|
+
if (entry.refcount > 0) {
|
|
218
|
+
// Refcount is still > 0 (e.g. acquireAppSlotHolds holds a ref), but
|
|
219
|
+
// the renderer releasing this slot is done with it. Detach the host
|
|
220
|
+
// from its current DOM parent so it doesn't remain visible in the old
|
|
221
|
+
// SlotContainer. The pool entry (and view) stays alive for re-acquisition
|
|
222
|
+
// — for example, a preset switch back to this slot's preset will re-append
|
|
223
|
+
// the host to a new SlotContainer without destroying the view.
|
|
224
|
+
entry.host.remove();
|
|
166
225
|
return;
|
|
226
|
+
}
|
|
167
227
|
pendingDestroy.add(slotId);
|
|
168
228
|
queueMicrotask(() => {
|
|
169
229
|
var _a, _b;
|
|
@@ -200,6 +260,7 @@ export function resetSlotHostPool() {
|
|
|
200
260
|
delete dirtyState[key];
|
|
201
261
|
for (const key of Object.keys(closableState))
|
|
202
262
|
delete closableState[key];
|
|
263
|
+
handleOverrides.clear();
|
|
203
264
|
}
|
|
204
265
|
/**
|
|
205
266
|
* Read the current ViewHandle for a slot. Returns undefined if the slot
|
|
@@ -208,6 +269,9 @@ export function resetSlotHostPool() {
|
|
|
208
269
|
*/
|
|
209
270
|
export function getSlotHandle(slotId) {
|
|
210
271
|
var _a;
|
|
272
|
+
const override = handleOverrides.get(slotId);
|
|
273
|
+
if (override)
|
|
274
|
+
return override;
|
|
211
275
|
return (_a = pool.get(slotId)) === null || _a === void 0 ? void 0 : _a.handle;
|
|
212
276
|
}
|
|
213
277
|
/**
|
|
@@ -225,5 +289,59 @@ export function isSlotDirty(slotId) {
|
|
|
225
289
|
*/
|
|
226
290
|
export function isSlotClosable(slotId) {
|
|
227
291
|
var _a;
|
|
292
|
+
if (handleOverrides.has(slotId)) {
|
|
293
|
+
const h = handleOverrides.get(slotId);
|
|
294
|
+
return !!h.closable;
|
|
295
|
+
}
|
|
228
296
|
return (_a = closableState[slotId]) !== null && _a !== void 0 ? _a : false;
|
|
229
297
|
}
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Test-only handle override map — lets tests inject closable/canClose policy
|
|
300
|
+
// without needing a real ViewFactory. Not exported from src/index.ts.
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
/**
|
|
303
|
+
* Map of slotId → synthetic ViewHandle injected by tests. These take
|
|
304
|
+
* priority over the pool's real handles in `getSlotHandle` and
|
|
305
|
+
* `isSlotClosable`. Cleared by `resetSlotHostPool`.
|
|
306
|
+
*/
|
|
307
|
+
const handleOverrides = new Map();
|
|
308
|
+
/**
|
|
309
|
+
* Test-only: set the closable policy for a slot without going through a
|
|
310
|
+
* ViewFactory. Call BEFORE launchApp so the tab strip reads the right
|
|
311
|
+
* closable state when it renders.
|
|
312
|
+
*
|
|
313
|
+
* `true` — tab shows a close button and is removed on click.
|
|
314
|
+
* `false` — tab shows no close button (non-closable).
|
|
315
|
+
*/
|
|
316
|
+
export function setSlotClosableForTest(slotId, closable) {
|
|
317
|
+
const existing = handleOverrides.get(slotId);
|
|
318
|
+
if (existing) {
|
|
319
|
+
existing.closable = closable;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
handleOverrides.set(slotId, { unmount() { }, closable });
|
|
323
|
+
}
|
|
324
|
+
// Keep reactive closableState in sync so the tab strip re-renders.
|
|
325
|
+
if (closable) {
|
|
326
|
+
closableState[slotId] = true;
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
delete closableState[slotId];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Test-only: attach a canClose guard to a slot. The slot must have been
|
|
334
|
+
* marked closable via `setSlotClosableForTest` first (or the guard will
|
|
335
|
+
* be installed alongside a `true` closable flag).
|
|
336
|
+
*/
|
|
337
|
+
export function setSlotCanCloseForTest(slotId, canClose) {
|
|
338
|
+
const existing = handleOverrides.get(slotId);
|
|
339
|
+
if (existing) {
|
|
340
|
+
existing.closable = { canClose };
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
handleOverrides.set(slotId, { unmount() { }, closable: { canClose } });
|
|
344
|
+
}
|
|
345
|
+
// Guarded slots are still considered "closable" for tab strip rendering.
|
|
346
|
+
closableState[slotId] = true;
|
|
347
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { resetFramework } from '../__test__/reset';
|
|
4
|
+
import { acquireSlotHost, releaseSlotHost } from './slotHostPool.svelte';
|
|
5
|
+
import { registerView } from '../shards/registry';
|
|
6
|
+
import SlotContainer from './SlotContainer.svelte';
|
|
7
|
+
import { renderWithShell } from '../__test__/render';
|
|
8
|
+
import { switchToHome, switchToApp } from './store.svelte';
|
|
9
|
+
import { makeApp, makeAppManifest, makeSlotNode, makeTree } from '../__test__/fixtures';
|
|
10
|
+
import { registerApp } from '../apps/registry.svelte';
|
|
11
|
+
import { launchApp } from '../apps/lifecycle';
|
|
12
|
+
// ─── D.1 ─────────────────────────────────────────────────────────────────────
|
|
13
|
+
describe('slotHostPool — D.1 refcount destroys once', () => {
|
|
14
|
+
beforeEach(resetFramework);
|
|
15
|
+
it('runs the factory teardown exactly once after the final release, in a microtask', async () => {
|
|
16
|
+
const teardown = vi.fn();
|
|
17
|
+
// ViewFactory is an object: { mount(el, ctx): ViewHandle }
|
|
18
|
+
// ViewHandle has unmount(), NOT dispose()
|
|
19
|
+
registerView('v', {
|
|
20
|
+
mount: () => ({
|
|
21
|
+
unmount: teardown,
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
acquireSlotHost('s1', 'v', 's1');
|
|
25
|
+
acquireSlotHost('s1', 'v', 's1');
|
|
26
|
+
releaseSlotHost('s1');
|
|
27
|
+
await Promise.resolve();
|
|
28
|
+
expect(teardown).not.toHaveBeenCalled();
|
|
29
|
+
releaseSlotHost('s1');
|
|
30
|
+
await Promise.resolve();
|
|
31
|
+
expect(teardown).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
// ─── D.2 ─────────────────────────────────────────────────────────────────────
|
|
35
|
+
describe('slotHostPool — D.2 re-acquire cancels destroy', () => {
|
|
36
|
+
beforeEach(resetFramework);
|
|
37
|
+
it('does not destroy the host when released and re-acquired in the same microtask', async () => {
|
|
38
|
+
const teardown = vi.fn();
|
|
39
|
+
registerView('v', {
|
|
40
|
+
mount: () => ({
|
|
41
|
+
unmount: teardown,
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
acquireSlotHost('s2', 'v', 's2');
|
|
45
|
+
releaseSlotHost('s2');
|
|
46
|
+
acquireSlotHost('s2', 'v', 's2');
|
|
47
|
+
await Promise.resolve();
|
|
48
|
+
expect(teardown).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
// ─── D.3 ─────────────────────────────────────────────────────────────────────
|
|
52
|
+
describe('slotHostPool — D.3 late factory registration', () => {
|
|
53
|
+
beforeEach(resetFramework);
|
|
54
|
+
it('mounts the view when a factory registers after acquireSlotHost', async () => {
|
|
55
|
+
const mount = vi.fn((el) => {
|
|
56
|
+
const span = document.createElement('span');
|
|
57
|
+
span.dataset.mountedFor = 'late';
|
|
58
|
+
el.appendChild(span);
|
|
59
|
+
return {
|
|
60
|
+
unmount: () => span.remove(),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
acquireSlotHost('late-slot', 'late:view', 'late-slot');
|
|
64
|
+
await Promise.resolve();
|
|
65
|
+
registerView('late:view', { mount });
|
|
66
|
+
await Promise.resolve();
|
|
67
|
+
expect(mount).toHaveBeenCalledTimes(1);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
// ─── D.4 ─────────────────────────────────────────────────────────────────────
|
|
71
|
+
describe('slotHostPool — D.4 shared host across containers', () => {
|
|
72
|
+
beforeEach(resetFramework);
|
|
73
|
+
it('mounts the factory once across two SlotContainers for the same slotId', async () => {
|
|
74
|
+
const mount = vi.fn((_el) => ({ unmount: () => { } }));
|
|
75
|
+
registerView('shared:view', { mount });
|
|
76
|
+
// SlotNode for both containers — same slotId, same viewId
|
|
77
|
+
const node = { type: 'slot', slotId: 'shared', viewId: 'shared:view' };
|
|
78
|
+
renderWithShell(SlotContainer, { node, label: 'A' });
|
|
79
|
+
renderWithShell(SlotContainer, { node, label: 'B' });
|
|
80
|
+
await tick();
|
|
81
|
+
expect(mount).toHaveBeenCalledTimes(1);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
// ─── D.5 ─────────────────────────────────────────────────────────────────────
|
|
85
|
+
describe('slotHostPool — D.5 root swap preserves app slots', () => {
|
|
86
|
+
beforeEach(resetFramework);
|
|
87
|
+
it('does not destroy app-held pooled hosts when swapping app to home and back', async () => {
|
|
88
|
+
const teardown = vi.fn();
|
|
89
|
+
registerView('persist:view', { mount: () => ({ unmount: teardown }) });
|
|
90
|
+
registerApp(makeApp({
|
|
91
|
+
manifest: makeAppManifest({ id: 'd5' }),
|
|
92
|
+
initialLayout: [
|
|
93
|
+
{ name: 'default', tree: makeTree(makeSlotNode('d5-slot', 'persist:view')) },
|
|
94
|
+
],
|
|
95
|
+
}));
|
|
96
|
+
await launchApp('d5');
|
|
97
|
+
await Promise.resolve();
|
|
98
|
+
switchToHome();
|
|
99
|
+
await Promise.resolve();
|
|
100
|
+
switchToApp();
|
|
101
|
+
await Promise.resolve();
|
|
102
|
+
expect(teardown).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -7,6 +7,22 @@ import type { App } from '../apps/types';
|
|
|
7
7
|
* Does NOT switch the active root. Call switchToApp() separately.
|
|
8
8
|
*/
|
|
9
9
|
export declare function attachApp(app: App): void;
|
|
10
|
+
/**
|
|
11
|
+
* Second-phase attach: take refcount holds on every slot in the active
|
|
12
|
+
* preset's tree. Must run AFTER required shards have activated (and
|
|
13
|
+
* therefore registered their view factories), so the pool's microtask
|
|
14
|
+
* factory lookup sees them. See the refcount-hold discipline comment at
|
|
15
|
+
* the top of this module and the createHost microtask in slotHostPool.
|
|
16
|
+
*
|
|
17
|
+
* TODO(preset-switch leak): holds are taken for the INITIAL preset only
|
|
18
|
+
* and released only in detachApp. On presetManager.switch(), the old
|
|
19
|
+
* preset's slot hosts stay pool-resident with refcount >= 1 (view handle
|
|
20
|
+
* and ResizeObserver not torn down) for the lifetime of the app attach.
|
|
21
|
+
* Acceptable for small preset sets; revisit if presets become churnier.
|
|
22
|
+
* Proper fix: re-scope holds on preset switch, or make this first-frame
|
|
23
|
+
* only and let renderers own all refcounts.
|
|
24
|
+
*/
|
|
25
|
+
export declare function acquireAppSlotHolds(): void;
|
|
10
26
|
/**
|
|
11
27
|
* Detach the currently-attached app. Releases its refcount holds; the
|
|
12
28
|
* pool's microtask cleanup drops the pooled hosts if they also have no
|
|
@@ -41,3 +57,9 @@ export declare const layoutStore: {
|
|
|
41
57
|
readonly tree: LayoutTree;
|
|
42
58
|
readonly floats: FloatEntry[];
|
|
43
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* Test-only reset. Restores the layout store to its boot state: no app
|
|
62
|
+
* attached, active root = 'home'. Not exported from `src/index.ts` —
|
|
63
|
+
* tests import this submodule path directly.
|
|
64
|
+
*/
|
|
65
|
+
export declare function __resetLayoutStoreForTest(): void;
|
|
@@ -133,17 +133,40 @@ export function attachApp(app) {
|
|
|
133
133
|
workspace: defaultBlob,
|
|
134
134
|
});
|
|
135
135
|
const proxy = state.workspace;
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
136
|
+
// Create the entry with no slot holds yet; `acquireAppSlotHolds` does
|
|
137
|
+
// that as a second phase, after shards have had a chance to register
|
|
138
|
+
// their view factories. Binding the preset manager proxy happens here
|
|
139
|
+
// so shards can read/switch presets from their activate() hook.
|
|
140
|
+
appEntry = { appId: app.manifest.id, proxy, heldSlotIds: [] };
|
|
141
|
+
bindPresetBlob(proxy);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Second-phase attach: take refcount holds on every slot in the active
|
|
145
|
+
* preset's tree. Must run AFTER required shards have activated (and
|
|
146
|
+
* therefore registered their view factories), so the pool's microtask
|
|
147
|
+
* factory lookup sees them. See the refcount-hold discipline comment at
|
|
148
|
+
* the top of this module and the createHost microtask in slotHostPool.
|
|
149
|
+
*
|
|
150
|
+
* TODO(preset-switch leak): holds are taken for the INITIAL preset only
|
|
151
|
+
* and released only in detachApp. On presetManager.switch(), the old
|
|
152
|
+
* preset's slot hosts stay pool-resident with refcount >= 1 (view handle
|
|
153
|
+
* and ResizeObserver not torn down) for the lifetime of the app attach.
|
|
154
|
+
* Acceptable for small preset sets; revisit if presets become churnier.
|
|
155
|
+
* Proper fix: re-scope holds on preset switch, or make this first-frame
|
|
156
|
+
* only and let renderers own all refcounts.
|
|
157
|
+
*/
|
|
158
|
+
export function acquireAppSlotHolds() {
|
|
159
|
+
if (!appEntry) {
|
|
160
|
+
throw new Error('acquireAppSlotHolds: no app attached');
|
|
161
|
+
}
|
|
162
|
+
if (appEntry.heldSlotIds.length > 0)
|
|
163
|
+
return; // idempotent
|
|
164
|
+
const tree = currentTree(appEntry.proxy);
|
|
139
165
|
const refs = collectTreeSlotRefs(tree);
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
heldSlotIds.push(slotId);
|
|
166
|
+
for (const { slotId, viewId, label, meta } of refs) {
|
|
167
|
+
acquireSlotHost(slotId, viewId, label, meta);
|
|
168
|
+
appEntry.heldSlotIds.push(slotId);
|
|
144
169
|
}
|
|
145
|
-
appEntry = { appId: app.manifest.id, proxy, heldSlotIds };
|
|
146
|
-
bindPresetBlob(proxy);
|
|
147
170
|
}
|
|
148
171
|
/**
|
|
149
172
|
* Detach the currently-attached app. Releases its refcount holds; the
|
|
@@ -171,6 +194,33 @@ export function switchToApp() {
|
|
|
171
194
|
}
|
|
172
195
|
activeRoot = 'app';
|
|
173
196
|
}
|
|
197
|
+
// ---------- `layoutStore` back-compat shim -------------------------------
|
|
198
|
+
/**
|
|
199
|
+
* Reactive-derived active LayoutTree. Using `$derived.by` gives us a
|
|
200
|
+
* stable, memoized reactive node that downstream accessors
|
|
201
|
+
* (`layoutStore.root`, `.tree`, `.floats`) all subscribe to once —
|
|
202
|
+
* instead of re-running the proxy-read chain on every access.
|
|
203
|
+
*
|
|
204
|
+
* Reading `blob.activePreset` by name inside this derived is load-bearing:
|
|
205
|
+
* it registers the preset-name signal as a dependency, so
|
|
206
|
+
* `presetManager.switch()` invalidates the derived and triggers re-render.
|
|
207
|
+
* Do not collapse this into a plain function that only reads the preset
|
|
208
|
+
* object — the derived would still recompute, but consumers that cached
|
|
209
|
+
* the result via a different path could miss the invalidation.
|
|
210
|
+
*/
|
|
211
|
+
const activeTree = $derived.by(() => {
|
|
212
|
+
if (activeRoot === 'app' && appEntry) {
|
|
213
|
+
// Read activePreset by name explicitly so Svelte tracks this signal.
|
|
214
|
+
const blob = appEntry.proxy;
|
|
215
|
+
const presetName = blob.activePreset;
|
|
216
|
+
const preset = blob.presets[presetName];
|
|
217
|
+
if (!preset) {
|
|
218
|
+
throw new Error(`AppLayoutBlob active preset "${presetName}" not found in presets map`);
|
|
219
|
+
}
|
|
220
|
+
return preset.default;
|
|
221
|
+
}
|
|
222
|
+
return HOME_TREE;
|
|
223
|
+
});
|
|
174
224
|
/**
|
|
175
225
|
* The currently-rendered LayoutTree. LayoutRenderer reads this via
|
|
176
226
|
* layoutStore.tree. Home uses a framework constant; app reads the
|
|
@@ -178,9 +228,11 @@ export function switchToApp() {
|
|
|
178
228
|
* mutations from splitter/drag/ops reach the renderer unchanged).
|
|
179
229
|
*/
|
|
180
230
|
export function activeLayout() {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
231
|
+
// Delegates to the $derived.by so callers outside component contexts
|
|
232
|
+
// (e.g. inspection.ts, ops.ts) still read the correct tree. The $derived
|
|
233
|
+
// is recalculated whenever activeRoot, appEntry, or blob.activePreset
|
|
234
|
+
// changes, so these callers always see a fresh snapshot.
|
|
235
|
+
return activeTree;
|
|
184
236
|
}
|
|
185
237
|
export function getActiveRoot() {
|
|
186
238
|
return activeRoot;
|
|
@@ -189,7 +241,6 @@ export function getAttachedAppId() {
|
|
|
189
241
|
var _a;
|
|
190
242
|
return (_a = appEntry === null || appEntry === void 0 ? void 0 : appEntry.appId) !== null && _a !== void 0 ? _a : null;
|
|
191
243
|
}
|
|
192
|
-
// ---------- `layoutStore` back-compat shim -------------------------------
|
|
193
244
|
/**
|
|
194
245
|
* Preserved for callers that still read `layoutStore.root`. The `root`
|
|
195
246
|
* getter is an alias for `tree.docked` so existing callers still compile
|
|
@@ -204,12 +255,23 @@ export function getAttachedAppId() {
|
|
|
204
255
|
*/
|
|
205
256
|
export const layoutStore = {
|
|
206
257
|
get root() {
|
|
207
|
-
return
|
|
258
|
+
return activeTree.docked;
|
|
208
259
|
},
|
|
209
260
|
get tree() {
|
|
210
|
-
return
|
|
261
|
+
return activeTree;
|
|
211
262
|
},
|
|
212
263
|
get floats() {
|
|
213
|
-
return
|
|
264
|
+
return activeTree.floats;
|
|
214
265
|
},
|
|
215
266
|
};
|
|
267
|
+
/**
|
|
268
|
+
* Test-only reset. Restores the layout store to its boot state: no app
|
|
269
|
+
* attached, active root = 'home'. Not exported from `src/index.ts` —
|
|
270
|
+
* tests import this submodule path directly.
|
|
271
|
+
*/
|
|
272
|
+
export function __resetLayoutStoreForTest() {
|
|
273
|
+
appEntry = null;
|
|
274
|
+
activeRoot = 'home';
|
|
275
|
+
HOME_TREE.floats.length = 0;
|
|
276
|
+
HOME_TREE.docked = HOME_LAYOUT;
|
|
277
|
+
}
|
|
@@ -12,6 +12,7 @@ export declare function collectSlotRefs(tree: LayoutNode): {
|
|
|
12
12
|
slotId: string;
|
|
13
13
|
viewId: string | null;
|
|
14
14
|
label: string;
|
|
15
|
+
meta?: Record<string, unknown>;
|
|
15
16
|
}[];
|
|
16
17
|
/**
|
|
17
18
|
* Multi-root version of `collectSlotRefs`: walks the docked tree first
|
|
@@ -23,4 +24,5 @@ export declare function collectTreeSlotRefs(tree: LayoutTree): {
|
|
|
23
24
|
slotId: string;
|
|
24
25
|
viewId: string | null;
|
|
25
26
|
label: string;
|
|
27
|
+
meta?: Record<string, unknown>;
|
|
26
28
|
}[];
|
package/dist/layout/tree-walk.js
CHANGED
|
@@ -20,7 +20,7 @@ export function collectSlotRefs(tree) {
|
|
|
20
20
|
}
|
|
21
21
|
if (node.type === 'tabs') {
|
|
22
22
|
for (const t of node.tabs) {
|
|
23
|
-
out.push({ slotId: t.slotId, viewId: t.viewId, label: t.label });
|
|
23
|
+
out.push({ slotId: t.slotId, viewId: t.viewId, label: t.label, meta: t.meta });
|
|
24
24
|
}
|
|
25
25
|
return;
|
|
26
26
|
}
|
package/dist/layout/types.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export interface TabEntry {
|
|
|
38
38
|
label: string;
|
|
39
39
|
/** Optional icon hint (not yet rendered in phase 8). */
|
|
40
40
|
icon?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Caller-supplied instance data, threaded to `MountContext.meta`.
|
|
43
|
+
* Ephemeral — not serialized with the layout tree.
|
|
44
|
+
*/
|
|
45
|
+
meta?: Record<string, unknown>;
|
|
41
46
|
}
|
|
42
47
|
/**
|
|
43
48
|
* A layout node that groups one or more slots as tabs, showing one at a time.
|
package/dist/overlays/float.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface FloatOptions {
|
|
|
7
7
|
y: number;
|
|
8
8
|
};
|
|
9
9
|
size?: Size;
|
|
10
|
+
/** Instance data threaded to the view factory via `MountContext.meta`. */
|
|
11
|
+
meta?: Record<string, unknown>;
|
|
10
12
|
}
|
|
11
13
|
export interface FloatManager {
|
|
12
14
|
open(viewId: string, options?: FloatOptions): string;
|
package/dist/overlays/float.js
CHANGED
|
@@ -74,9 +74,12 @@ function openFloat(viewId, options = {}) {
|
|
|
74
74
|
// float body; the frame header still moves the float as a whole.
|
|
75
75
|
const slotId = mintFloatSlotId(viewId);
|
|
76
76
|
const label = (_a = options.title) !== null && _a !== void 0 ? _a : viewId;
|
|
77
|
+
const tab = { slotId, viewId, label };
|
|
78
|
+
if (options.meta)
|
|
79
|
+
tab.meta = options.meta;
|
|
77
80
|
const content = {
|
|
78
81
|
type: 'tabs',
|
|
79
|
-
tabs: [
|
|
82
|
+
tabs: [tab],
|
|
80
83
|
activeTab: 0,
|
|
81
84
|
};
|
|
82
85
|
const computedMin = computeMinSize(content);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { floatManager, __resetFloatManagerForTest } from './float';
|
|
2
|
+
import { floatManager, __resetFloatManagerForTest, bindFloatStore } from './float';
|
|
3
|
+
import { layoutStore } from '../layout/store.svelte';
|
|
3
4
|
describe('floatManager', () => {
|
|
4
5
|
beforeEach(() => {
|
|
5
6
|
__resetFloatManagerForTest();
|
|
@@ -34,4 +35,104 @@ describe('floatManager', () => {
|
|
|
34
35
|
expect(f.position).toEqual({ x: 100, y: 200 });
|
|
35
36
|
expect(f.size).toEqual({ w: 800, h: 500 });
|
|
36
37
|
});
|
|
38
|
+
it('open() threads meta into the content TabEntry', () => {
|
|
39
|
+
const meta = { viewConfigId: 'vc-42' };
|
|
40
|
+
const id = floatManager.open('test:view', { meta });
|
|
41
|
+
const f = floatManager.list().find((e) => e.id === id);
|
|
42
|
+
const tabs = f.content;
|
|
43
|
+
expect(tabs.type).toBe('tabs');
|
|
44
|
+
if (tabs.type === 'tabs') {
|
|
45
|
+
expect(tabs.tabs[0].meta).toEqual({ viewConfigId: 'vc-42' });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
it('open() without meta leaves TabEntry.meta undefined', () => {
|
|
49
|
+
const id = floatManager.open('test:view');
|
|
50
|
+
const f = floatManager.list().find((e) => e.id === id);
|
|
51
|
+
const tabs = f.content;
|
|
52
|
+
expect(tabs.type).toBe('tabs');
|
|
53
|
+
if (tabs.type === 'tabs') {
|
|
54
|
+
expect(tabs.tabs[0].meta).toBeUndefined();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// DOM tests — floatManager + FloatLayer.svelte in happy-dom
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
import { renderWithShell } from '../__test__/render';
|
|
62
|
+
import FloatLayer from './FloatLayer.svelte';
|
|
63
|
+
import { tick } from 'svelte';
|
|
64
|
+
import { resetFramework } from '../__test__/reset';
|
|
65
|
+
/**
|
|
66
|
+
* Wire the floatManager to the same FloatEntry[] that FloatLayer reads
|
|
67
|
+
* (layoutStore.floats → HOME_TREE.floats). Without this binding, the
|
|
68
|
+
* manager writes to its internal fallback array, which the component
|
|
69
|
+
* never observes.
|
|
70
|
+
*/
|
|
71
|
+
function bindManagerToStore() {
|
|
72
|
+
bindFloatStore(layoutStore.floats, () => ({ w: 1024, h: 768 }));
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// F.1 — open mounts a frame into the DOM; close removes it
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
describe('floats — F.1 open/close mounts DOM', () => {
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
resetFramework();
|
|
80
|
+
bindManagerToStore();
|
|
81
|
+
});
|
|
82
|
+
it('mounts a FloatFrame on open() and removes it on close()', async () => {
|
|
83
|
+
const { container } = renderWithShell(FloatLayer, {});
|
|
84
|
+
const id = floatManager.open('test:view', { title: 'Test Float' });
|
|
85
|
+
await tick();
|
|
86
|
+
// FloatFrame renders a div[role="dialog"] with the title as aria-label.
|
|
87
|
+
const frame = container.querySelector('[role="dialog"][aria-label="Test Float"]');
|
|
88
|
+
expect(frame).toBeTruthy();
|
|
89
|
+
floatManager.close(id);
|
|
90
|
+
await tick();
|
|
91
|
+
expect(container.querySelector('[role="dialog"][aria-label="Test Float"]')).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// F.2 — focus stack: last opened is top, previous is restored after close
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
describe('floats — F.2 focus stack', () => {
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
resetFramework();
|
|
100
|
+
bindManagerToStore();
|
|
101
|
+
});
|
|
102
|
+
it('raises the newer float above the prior and restores previous on close', async () => {
|
|
103
|
+
var _a, _b;
|
|
104
|
+
renderWithShell(FloatLayer, {});
|
|
105
|
+
const id1 = floatManager.open('test:view', { title: 'First' });
|
|
106
|
+
const id2 = floatManager.open('test:view', { title: 'Second' });
|
|
107
|
+
await tick();
|
|
108
|
+
// The most recently focused float sits at the end of list() (top z-order).
|
|
109
|
+
expect((_a = floatManager.list().at(-1)) === null || _a === void 0 ? void 0 : _a.id).toBe(id2);
|
|
110
|
+
floatManager.close(id2);
|
|
111
|
+
await tick();
|
|
112
|
+
// After closing the top, id1 becomes the sole entry (top of z-order).
|
|
113
|
+
expect((_b = floatManager.list().at(-1)) === null || _b === void 0 ? void 0 : _b.id).toBe(id1);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// F.3 — close button in FloatFrame calls floatManager.close()
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
describe('floats — F.3 close button removes float', () => {
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
resetFramework();
|
|
122
|
+
bindManagerToStore();
|
|
123
|
+
});
|
|
124
|
+
it('clicking the close button removes the float from list()', async () => {
|
|
125
|
+
const { container } = renderWithShell(FloatLayer, {});
|
|
126
|
+
const id = floatManager.open('test:view', { title: 'Closeable' });
|
|
127
|
+
await tick();
|
|
128
|
+
// FloatFrame renders a button[aria-label="Close float"] inside the frame.
|
|
129
|
+
const closeBtn = container.querySelector('[role="dialog"] button[aria-label="Close float"]');
|
|
130
|
+
expect(closeBtn).toBeTruthy();
|
|
131
|
+
closeBtn.click();
|
|
132
|
+
await tick();
|
|
133
|
+
// Float must be gone from the manager's list.
|
|
134
|
+
expect(floatManager.list().some((f) => f.id === id)).toBe(false);
|
|
135
|
+
// And its frame must be removed from the DOM.
|
|
136
|
+
expect(container.querySelector('[role="dialog"][aria-label="Closeable"]')).toBeNull();
|
|
137
|
+
});
|
|
37
138
|
});
|
|
@@ -221,10 +221,12 @@
|
|
|
221
221
|
class="splitter-handle"
|
|
222
222
|
class:dragging={drag?.handleIndex === i}
|
|
223
223
|
class:disabled={isCollapsed(i) || isCollapsed(i + 1)}
|
|
224
|
+
data-testid="splitter-handle-{i}"
|
|
224
225
|
onpointerdown={(e) => beginDrag(e, i)}
|
|
225
226
|
onpointermove={moveDrag}
|
|
226
227
|
onpointerup={endDrag}
|
|
227
228
|
onpointercancel={endDrag}
|
|
229
|
+
ondblclick={() => onCollapseToggle?.(i, !isCollapsed(i))}
|
|
228
230
|
role="separator"
|
|
229
231
|
aria-orientation={direction === 'horizontal' ? 'vertical' : 'horizontal'}
|
|
230
232
|
></div>
|