sh3-core 0.7.1 → 0.7.5
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/Shell.svelte +3 -2
- 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 +4 -0
- package/dist/apps/lifecycle.js +27 -10
- package/dist/apps/lifecycle.test.d.ts +1 -0
- package/dist/apps/lifecycle.test.js +260 -0
- package/dist/apps/registry.svelte.d.ts +2 -0
- package/dist/apps/registry.svelte.js +5 -0
- package/dist/apps/types.d.ts +17 -0
- package/dist/contract.d.ts +10 -0
- package/dist/contract.js +10 -0
- 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/inspection.js +58 -7
- package/dist/layout/ops.js +25 -6
- package/dist/layout/ops.test.js +51 -1
- package/dist/layout/slotHostPool.svelte.d.ts +16 -1
- package/dist/layout/slotHostPool.svelte.js +124 -6
- 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 +80 -18
- 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/FloatFrame.svelte +1 -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/shards/activate.svelte.d.ts +6 -0
- package/dist/shards/activate.svelte.js +10 -0
- package/dist/shards/registry.d.ts +4 -0
- package/dist/shards/registry.js +18 -0
- package/dist/shards/types.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -1
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
function onHeaderPointerDown(e: PointerEvent): void {
|
|
32
32
|
if (e.button !== 0) return;
|
|
33
|
+
if ((e.target as HTMLElement).closest('.sh3-float-close')) return;
|
|
33
34
|
const target = e.currentTarget as HTMLElement;
|
|
34
35
|
target.setPointerCapture(e.pointerId);
|
|
35
36
|
dragging = true;
|
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>
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
closable,
|
|
77
77
|
dirty,
|
|
78
78
|
onClose,
|
|
79
|
+
tabIds,
|
|
79
80
|
}: {
|
|
80
81
|
labels: string[];
|
|
81
82
|
icons?: (string | undefined)[];
|
|
@@ -100,6 +101,8 @@
|
|
|
100
101
|
dirty?: (boolean | undefined)[];
|
|
101
102
|
/** Called when the user clicks a tab's close button. */
|
|
102
103
|
onClose?: (index: number) => void;
|
|
104
|
+
/** Optional stable ids for each tab (e.g. slotId). Used as data-testid suffixes on close buttons. */
|
|
105
|
+
tabIds?: (string | undefined)[];
|
|
103
106
|
} = $props();
|
|
104
107
|
|
|
105
108
|
function select(i: number) {
|
|
@@ -172,6 +175,7 @@
|
|
|
172
175
|
role="button"
|
|
173
176
|
tabindex="-1"
|
|
174
177
|
title="Close"
|
|
178
|
+
data-testid={tabIds?.[i] ? `tab-close-${tabIds[i]}` : undefined}
|
|
175
179
|
onclick={(e) => handleClose(i, e)}
|
|
176
180
|
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleClose(i, e); }}
|
|
177
181
|
>✕</span>
|
|
@@ -44,6 +44,8 @@ type $$ComponentProps = {
|
|
|
44
44
|
dirty?: (boolean | undefined)[];
|
|
45
45
|
/** Called when the user clicks a tab's close button. */
|
|
46
46
|
onClose?: (index: number) => void;
|
|
47
|
+
/** Optional stable ids for each tab (e.g. slotId). Used as data-testid suffixes on close buttons. */
|
|
48
|
+
tabIds?: (string | undefined)[];
|
|
47
49
|
};
|
|
48
50
|
declare const TabbedPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
49
51
|
type TabbedPanel = ReturnType<typeof TabbedPanel>;
|
|
@@ -50,3 +50,9 @@ export declare function isActive(id: string): boolean;
|
|
|
50
50
|
* Used by lifecycle.ts to pass context to `shard.resume()`.
|
|
51
51
|
*/
|
|
52
52
|
export declare function getShardContext(id: string): ShardContext | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Test-only reset. Tears down any active shard entries (without running
|
|
55
|
+
* deactivate hooks — tests should run deactivate explicitly if they care)
|
|
56
|
+
* and clears both registered and active maps.
|
|
57
|
+
*/
|
|
58
|
+
export declare function __resetShardRegistryForTest(): void;
|
|
@@ -191,3 +191,13 @@ export function getShardContext(id) {
|
|
|
191
191
|
var _a;
|
|
192
192
|
return (_a = active.get(id)) === null || _a === void 0 ? void 0 : _a.ctx;
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Test-only reset. Tears down any active shard entries (without running
|
|
196
|
+
* deactivate hooks — tests should run deactivate explicitly if they care)
|
|
197
|
+
* and clears both registered and active maps.
|
|
198
|
+
*/
|
|
199
|
+
export function __resetShardRegistryForTest() {
|
|
200
|
+
active.clear();
|
|
201
|
+
activeShards.clear();
|
|
202
|
+
registeredShards.clear();
|
|
203
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ViewFactory } from './types';
|
|
2
|
+
export declare function __addViewRegistrationListener(fn: (viewId: string, factory: ViewFactory) => void): void;
|
|
3
|
+
export declare function __removeViewRegistrationListener(fn: (viewId: string, factory: ViewFactory) => void): void;
|
|
2
4
|
export declare function registerView(viewId: string, factory: ViewFactory): void;
|
|
3
5
|
export declare function getView(viewId: string): ViewFactory | undefined;
|
|
4
6
|
export declare function unregisterView(viewId: string): void;
|
|
@@ -7,3 +9,5 @@ export declare function registerVerb(name: string, verb: Verb): void;
|
|
|
7
9
|
export declare function getVerb(name: string): Verb | undefined;
|
|
8
10
|
export declare function unregisterVerb(name: string): void;
|
|
9
11
|
export declare function listVerbs(): Verb[];
|
|
12
|
+
/** Test-only reset: clear the view and verb registries. */
|
|
13
|
+
export declare function __resetViewRegistryForTest(): void;
|
package/dist/shards/registry.js
CHANGED
|
@@ -14,11 +14,22 @@
|
|
|
14
14
|
* hotkeys get their own sibling maps when those kinds land.
|
|
15
15
|
*/
|
|
16
16
|
const views = new Map();
|
|
17
|
+
/** Listeners called after a new view factory is registered. */
|
|
18
|
+
const viewRegistrationListeners = new Set();
|
|
19
|
+
export function __addViewRegistrationListener(fn) {
|
|
20
|
+
viewRegistrationListeners.add(fn);
|
|
21
|
+
}
|
|
22
|
+
export function __removeViewRegistrationListener(fn) {
|
|
23
|
+
viewRegistrationListeners.delete(fn);
|
|
24
|
+
}
|
|
17
25
|
export function registerView(viewId, factory) {
|
|
18
26
|
if (views.has(viewId)) {
|
|
19
27
|
throw new Error(`View "${viewId}" is already registered`);
|
|
20
28
|
}
|
|
21
29
|
views.set(viewId, factory);
|
|
30
|
+
for (const listener of viewRegistrationListeners) {
|
|
31
|
+
listener(viewId, factory);
|
|
32
|
+
}
|
|
22
33
|
}
|
|
23
34
|
export function getView(viewId) {
|
|
24
35
|
return views.get(viewId);
|
|
@@ -42,3 +53,10 @@ export function unregisterVerb(name) {
|
|
|
42
53
|
export function listVerbs() {
|
|
43
54
|
return Array.from(verbs.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
44
55
|
}
|
|
56
|
+
/** Test-only reset: clear the view and verb registries. */
|
|
57
|
+
export function __resetViewRegistryForTest() {
|
|
58
|
+
views.clear();
|
|
59
|
+
verbs.clear();
|
|
60
|
+
// Do NOT clear viewRegistrationListeners — they are module-level subscriptions
|
|
61
|
+
// (e.g. slotHostPool's late-factory listener) that must survive registry resets.
|
|
62
|
+
}
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ export interface MountContext {
|
|
|
43
43
|
viewId: string;
|
|
44
44
|
/** Initial label for the tab; may be updated by the view via future API. */
|
|
45
45
|
label: string;
|
|
46
|
+
/**
|
|
47
|
+
* Caller-supplied instance data. Present when the mount was triggered with
|
|
48
|
+
* metadata (e.g. `shell.float.open(viewId, { meta: { ... } })`).
|
|
49
|
+
* Not persisted with the layout — ephemeral per mount.
|
|
50
|
+
*/
|
|
51
|
+
meta?: Record<string, unknown>;
|
|
46
52
|
/**
|
|
47
53
|
* Push dirty-state to the tab strip. The framework renders a dirty
|
|
48
54
|
* indicator (filled dot) on the tab when true, clears it when false.
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export declare const VERSION = "0.7.
|
|
2
|
+
export declare const VERSION = "0.7.5";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export const VERSION = '0.7.
|
|
2
|
+
export const VERSION = '0.7.5';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sh3-core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
34
34
|
"pack": "npm run build && npm pack",
|
|
35
35
|
"test": "vitest run --passWithNoTests",
|
|
36
|
+
"test:node": "vitest run --project node --passWithNoTests",
|
|
37
|
+
"test:dom": "vitest run --project dom --passWithNoTests",
|
|
38
|
+
"test:browser": "vitest run --project browser --passWithNoTests",
|
|
36
39
|
"test:watch": "vitest"
|
|
37
40
|
},
|
|
38
41
|
"dependencies": {
|
|
@@ -50,7 +53,12 @@
|
|
|
50
53
|
"devDependencies": {
|
|
51
54
|
"@sveltejs/package": "^2.3.0",
|
|
52
55
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
56
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
57
|
+
"@testing-library/svelte": "^5.3.1",
|
|
53
58
|
"@tsconfig/svelte": "^5.0.4",
|
|
59
|
+
"@vitest/browser": "^2.1.9",
|
|
60
|
+
"happy-dom": "^15.11.7",
|
|
61
|
+
"playwright": "^1.59.1",
|
|
54
62
|
"svelte": "^5.0.0",
|
|
55
63
|
"svelte-check": "^4.0.0",
|
|
56
64
|
"tsx": "^4.21.0",
|