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.
Files changed (61) hide show
  1. package/dist/Shell.svelte +3 -2
  2. package/dist/__test__/fixtures.d.ts +12 -0
  3. package/dist/__test__/fixtures.js +62 -0
  4. package/dist/__test__/render.d.ts +3 -0
  5. package/dist/__test__/render.js +11 -0
  6. package/dist/__test__/reset.d.ts +14 -0
  7. package/dist/__test__/reset.js +34 -0
  8. package/dist/__test__/setup-dom.d.ts +1 -0
  9. package/dist/__test__/setup-dom.js +26 -0
  10. package/dist/__test__/smoke.test.d.ts +1 -0
  11. package/dist/__test__/smoke.test.js +28 -0
  12. package/dist/api.d.ts +4 -0
  13. package/dist/apps/lifecycle.js +27 -10
  14. package/dist/apps/lifecycle.test.d.ts +1 -0
  15. package/dist/apps/lifecycle.test.js +260 -0
  16. package/dist/apps/registry.svelte.d.ts +2 -0
  17. package/dist/apps/registry.svelte.js +5 -0
  18. package/dist/apps/types.d.ts +17 -0
  19. package/dist/contract.d.ts +10 -0
  20. package/dist/contract.js +10 -0
  21. package/dist/layout/LayoutRenderer.browser.test.d.ts +1 -0
  22. package/dist/layout/LayoutRenderer.browser.test.js +274 -0
  23. package/dist/layout/LayoutRenderer.svelte +2 -1
  24. package/dist/layout/LayoutRenderer.test.d.ts +1 -0
  25. package/dist/layout/LayoutRenderer.test.js +143 -0
  26. package/dist/layout/SlotContainer.svelte +8 -2
  27. package/dist/layout/SlotDropZone.svelte +19 -0
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. package/dist/layout/drag.svelte.d.ts +5 -0
  34. package/dist/layout/drag.svelte.js +15 -0
  35. package/dist/layout/inspection.js +58 -7
  36. package/dist/layout/ops.js +25 -6
  37. package/dist/layout/ops.test.js +51 -1
  38. package/dist/layout/slotHostPool.svelte.d.ts +16 -1
  39. package/dist/layout/slotHostPool.svelte.js +124 -6
  40. package/dist/layout/slotHostPool.test.d.ts +1 -0
  41. package/dist/layout/slotHostPool.test.js +104 -0
  42. package/dist/layout/store.svelte.d.ts +22 -0
  43. package/dist/layout/store.svelte.js +80 -18
  44. package/dist/layout/tree-walk.d.ts +2 -0
  45. package/dist/layout/tree-walk.js +1 -1
  46. package/dist/layout/types.d.ts +5 -0
  47. package/dist/overlays/FloatFrame.svelte +1 -0
  48. package/dist/overlays/float.d.ts +2 -0
  49. package/dist/overlays/float.js +4 -1
  50. package/dist/overlays/float.test.js +102 -1
  51. package/dist/primitives/ResizableSplitter.svelte +2 -0
  52. package/dist/primitives/TabbedPanel.svelte +4 -0
  53. package/dist/primitives/TabbedPanel.svelte.d.ts +2 -0
  54. package/dist/shards/activate.svelte.d.ts +6 -0
  55. package/dist/shards/activate.svelte.js +10 -0
  56. package/dist/shards/registry.d.ts +4 -0
  57. package/dist/shards/registry.js +18 -0
  58. package/dist/shards/types.d.ts +6 -0
  59. package/dist/version.d.ts +1 -1
  60. package/dist/version.js +1 -1
  61. 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;
@@ -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;
@@ -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: [{ slotId, viewId, label }],
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
  >&#x2715;</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;
@@ -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
+ }
@@ -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.1";
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.1';
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.1",
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",