sh3-core 0.1.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.
Files changed (134) hide show
  1. package/dist/Shell.svelte +185 -0
  2. package/dist/Shell.svelte.d.ts +4 -0
  3. package/dist/api.d.ts +22 -0
  4. package/dist/api.js +45 -0
  5. package/dist/apps/lifecycle.d.ts +37 -0
  6. package/dist/apps/lifecycle.js +153 -0
  7. package/dist/apps/registry.svelte.d.ts +37 -0
  8. package/dist/apps/registry.svelte.js +60 -0
  9. package/dist/apps/types.d.ts +61 -0
  10. package/dist/apps/types.js +10 -0
  11. package/dist/assets/icons.svg +1119 -0
  12. package/dist/auth/auth.svelte.d.ts +44 -0
  13. package/dist/auth/auth.svelte.js +119 -0
  14. package/dist/auth/index.d.ts +1 -0
  15. package/dist/auth/index.js +1 -0
  16. package/dist/build.d.ts +29 -0
  17. package/dist/build.js +85 -0
  18. package/dist/contract.d.ts +20 -0
  19. package/dist/contract.js +28 -0
  20. package/dist/documents/backends.d.ts +17 -0
  21. package/dist/documents/backends.js +156 -0
  22. package/dist/documents/config.d.ts +7 -0
  23. package/dist/documents/config.js +27 -0
  24. package/dist/documents/handle.d.ts +6 -0
  25. package/dist/documents/handle.js +154 -0
  26. package/dist/documents/http-backend.d.ts +22 -0
  27. package/dist/documents/http-backend.js +78 -0
  28. package/dist/documents/index.d.ts +6 -0
  29. package/dist/documents/index.js +8 -0
  30. package/dist/documents/notifications.d.ts +9 -0
  31. package/dist/documents/notifications.js +39 -0
  32. package/dist/documents/types.d.ts +97 -0
  33. package/dist/documents/types.js +12 -0
  34. package/dist/host-entry.d.ts +9 -0
  35. package/dist/host-entry.js +15 -0
  36. package/dist/host.d.ts +13 -0
  37. package/dist/host.js +73 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +13 -0
  40. package/dist/layout/DragPreview.svelte +63 -0
  41. package/dist/layout/DragPreview.svelte.d.ts +3 -0
  42. package/dist/layout/LayoutRenderer.svelte +260 -0
  43. package/dist/layout/LayoutRenderer.svelte.d.ts +6 -0
  44. package/dist/layout/SlotContainer.svelte +140 -0
  45. package/dist/layout/SlotContainer.svelte.d.ts +8 -0
  46. package/dist/layout/SlotDropZone.svelte +122 -0
  47. package/dist/layout/SlotDropZone.svelte.d.ts +8 -0
  48. package/dist/layout/drag.svelte.d.ts +45 -0
  49. package/dist/layout/drag.svelte.js +191 -0
  50. package/dist/layout/inspection.d.ts +52 -0
  51. package/dist/layout/inspection.js +157 -0
  52. package/dist/layout/ops.d.ts +78 -0
  53. package/dist/layout/ops.js +281 -0
  54. package/dist/layout/slotHostPool.svelte.d.ts +36 -0
  55. package/dist/layout/slotHostPool.svelte.js +229 -0
  56. package/dist/layout/store.svelte.d.ts +39 -0
  57. package/dist/layout/store.svelte.js +150 -0
  58. package/dist/layout/tree-walk.d.ts +15 -0
  59. package/dist/layout/tree-walk.js +33 -0
  60. package/dist/layout/types.d.ts +108 -0
  61. package/dist/layout/types.js +25 -0
  62. package/dist/overlays/ModalFrame.svelte +87 -0
  63. package/dist/overlays/ModalFrame.svelte.d.ts +10 -0
  64. package/dist/overlays/PopupFrame.svelte +85 -0
  65. package/dist/overlays/PopupFrame.svelte.d.ts +10 -0
  66. package/dist/overlays/ToastItem.svelte +77 -0
  67. package/dist/overlays/ToastItem.svelte.d.ts +9 -0
  68. package/dist/overlays/focusTrap.d.ts +1 -0
  69. package/dist/overlays/focusTrap.js +64 -0
  70. package/dist/overlays/modal.d.ts +9 -0
  71. package/dist/overlays/modal.js +141 -0
  72. package/dist/overlays/popup.d.ts +9 -0
  73. package/dist/overlays/popup.js +108 -0
  74. package/dist/overlays/roots.d.ts +4 -0
  75. package/dist/overlays/roots.js +31 -0
  76. package/dist/overlays/toast.d.ts +6 -0
  77. package/dist/overlays/toast.js +93 -0
  78. package/dist/overlays/types.d.ts +31 -0
  79. package/dist/overlays/types.js +15 -0
  80. package/dist/primitives/.gitkeep +0 -0
  81. package/dist/primitives/ResizableSplitter.svelte +333 -0
  82. package/dist/primitives/ResizableSplitter.svelte.d.ts +35 -0
  83. package/dist/primitives/TabbedPanel.svelte +305 -0
  84. package/dist/primitives/TabbedPanel.svelte.d.ts +50 -0
  85. package/dist/registry/client.d.ts +74 -0
  86. package/dist/registry/client.js +118 -0
  87. package/dist/registry/index.d.ts +13 -0
  88. package/dist/registry/index.js +14 -0
  89. package/dist/registry/installer.d.ts +53 -0
  90. package/dist/registry/installer.js +170 -0
  91. package/dist/registry/integrity.d.ts +32 -0
  92. package/dist/registry/integrity.js +92 -0
  93. package/dist/registry/loader.d.ts +50 -0
  94. package/dist/registry/loader.js +145 -0
  95. package/dist/registry/schema.d.ts +47 -0
  96. package/dist/registry/schema.js +180 -0
  97. package/dist/registry/storage.d.ts +37 -0
  98. package/dist/registry/storage.js +101 -0
  99. package/dist/registry/types.d.ts +245 -0
  100. package/dist/registry/types.js +14 -0
  101. package/dist/registry-shard/RegistryView.svelte +561 -0
  102. package/dist/registry-shard/RegistryView.svelte.d.ts +3 -0
  103. package/dist/registry-shard/registryApp.d.ts +10 -0
  104. package/dist/registry-shard/registryApp.js +24 -0
  105. package/dist/registry-shard/registryShard.svelte.d.ts +45 -0
  106. package/dist/registry-shard/registryShard.svelte.js +125 -0
  107. package/dist/shards/activate.svelte.d.ts +45 -0
  108. package/dist/shards/activate.svelte.js +124 -0
  109. package/dist/shards/registry.d.ts +4 -0
  110. package/dist/shards/registry.js +28 -0
  111. package/dist/shards/types.d.ts +155 -0
  112. package/dist/shards/types.js +20 -0
  113. package/dist/shell-shard/ShellHome.svelte +285 -0
  114. package/dist/shell-shard/ShellHome.svelte.d.ts +3 -0
  115. package/dist/shell-shard/shellShard.svelte.d.ts +2 -0
  116. package/dist/shell-shard/shellShard.svelte.js +47 -0
  117. package/dist/shellRuntime.svelte.d.ts +27 -0
  118. package/dist/shellRuntime.svelte.js +27 -0
  119. package/dist/state/backends.d.ts +26 -0
  120. package/dist/state/backends.js +99 -0
  121. package/dist/state/types.d.ts +38 -0
  122. package/dist/state/types.js +15 -0
  123. package/dist/state/zones.svelte.d.ts +52 -0
  124. package/dist/state/zones.svelte.js +141 -0
  125. package/dist/store/InstalledView.svelte +201 -0
  126. package/dist/store/InstalledView.svelte.d.ts +3 -0
  127. package/dist/store/StoreView.svelte +470 -0
  128. package/dist/store/StoreView.svelte.d.ts +3 -0
  129. package/dist/store/storeApp.d.ts +11 -0
  130. package/dist/store/storeApp.js +26 -0
  131. package/dist/store/storeShard.svelte.d.ts +29 -0
  132. package/dist/store/storeShard.svelte.js +99 -0
  133. package/dist/tokens.css +79 -0
  134. package/package.json +50 -0
@@ -0,0 +1,260 @@
1
+ <script lang="ts">
2
+ /*
3
+ * LayoutRenderer — recursive walker for a LayoutNode tree.
4
+ *
5
+ * Dispatches on the node kind:
6
+ * split → <ResizableSplitter> with one recursive <Self> per pane
7
+ * tabs → <TabbedPanel> with one <SlotContainer> per tab, a
8
+ * SlotDropZone overlay, and a drag controller wired to
9
+ * the drag engine
10
+ * slot → <SlotContainer> wrapped in a SlotDropZone
11
+ *
12
+ * Props: only `path` — a list of child indices from the root. The
13
+ * node itself is resolved as a $derived from `layoutStore.root` by
14
+ * walking the path. This is how we sidestep Svelte 5's
15
+ * `ownership_invalid_mutation` warning for a recursive component:
16
+ * - Passing `node` as a prop would make every mutation (e.g.
17
+ * `node.activeTab = i`) a write to a child-received prop, which
18
+ * the ownership tracker flags regardless of whether the
19
+ * underlying $state lives in a component or in a module.
20
+ * - Passing only `path` and deriving `node` locally means
21
+ * mutations go through a $derived of module state — no prop is
22
+ * involved, no ownership warning fires.
23
+ *
24
+ * `path` is a plain array, not reactive state, so there is no
25
+ * ownership concern on it. Each recursive call builds a new child
26
+ * path via `[...path, i]`.
27
+ */
28
+
29
+ import type { TabsNode, LayoutNode } from './types';
30
+ import ResizableSplitter from '../primitives/ResizableSplitter.svelte';
31
+ import TabbedPanel, { type TabDragController } from '../primitives/TabbedPanel.svelte';
32
+ import SlotContainer from './SlotContainer.svelte';
33
+ import SlotDropZone from './SlotDropZone.svelte';
34
+ import Self from './LayoutRenderer.svelte';
35
+ import { layoutStore } from './store.svelte';
36
+ import { nodeAtPath } from './ops';
37
+ import { isSlotClosable, isSlotDirty } from './slotHostPool.svelte';
38
+ import { closeTab } from './inspection';
39
+ import {
40
+ dragState,
41
+ beginTabDrag,
42
+ setDropTarget,
43
+ clearDropTarget,
44
+ suppressNextClick,
45
+ } from './drag.svelte';
46
+
47
+ let { path = [] }: { path?: number[] } = $props();
48
+
49
+ /**
50
+ * Resolve the current node by walking `layoutStore.root` along the
51
+ * path. $derived tracks the reads so Svelte re-runs this when the
52
+ * layout mutates. If the path becomes invalid mid-mutation (a
53
+ * cleanup pass can collapse nodes out from under a recursive
54
+ * renderer), we render null.
55
+ */
56
+ const node = $derived(nodeAtPath(layoutStore.root, path));
57
+
58
+ /**
59
+ * Build a TabDragController bound to the current tabs node.
60
+ * Rebuilt whenever `node` changes identity (mutation can replace
61
+ * the node at this path with a new one during cleanup).
62
+ */
63
+ function makeController(tabsNode: TabsNode): TabDragController {
64
+ return {
65
+ get isDragging() {
66
+ return dragState.phase === 'dragging';
67
+ },
68
+ onPointerDown(index, event, element) {
69
+ const entry = tabsNode.tabs[index];
70
+ if (!entry) return;
71
+ beginTabDrag(entry.slotId, entry, event, element);
72
+ },
73
+ onStripHover(stripRect, pointerX, pointerY, tabRects) {
74
+ if (pointerY < stripRect.top || pointerY > stripRect.bottom) {
75
+ clearDropTarget((t) => t.kind === 'strip' && t.tabsNode === tabsNode);
76
+ return null;
77
+ }
78
+ let insertIndex = tabRects.length;
79
+ for (let i = 0; i < tabRects.length; i++) {
80
+ const r = tabRects[i];
81
+ const mid = r.left + r.width / 2;
82
+ if (pointerX < mid) {
83
+ insertIndex = i;
84
+ break;
85
+ }
86
+ }
87
+ // Normalize for same-strip reorder so the engine's commit
88
+ // doesn't double-count the removal shift: if the source tab
89
+ // lives in this strip at index < insertIndex, subtract one.
90
+ const source = dragState.source;
91
+ if (source) {
92
+ const srcIdx = tabsNode.tabs.findIndex((t) => t.slotId === source.slotId);
93
+ if (srcIdx >= 0 && srcIdx < insertIndex) insertIndex -= 1;
94
+ }
95
+ setDropTarget({ kind: 'strip', tabsNode, insertIndex });
96
+ return insertIndex;
97
+ },
98
+ onStripLeave() {
99
+ clearDropTarget((t) => t.kind === 'strip' && t.tabsNode === tabsNode);
100
+ },
101
+ };
102
+ }
103
+
104
+ // Narrowing helpers — Svelte templates can't narrow a $derived
105
+ // across block boundaries, so we re-cast inside each branch below.
106
+ // These getters are just there to make the template readable.
107
+ function asSplit(n: LayoutNode) {
108
+ return n.type === 'split' ? n : null;
109
+ }
110
+ function asTabs(n: LayoutNode) {
111
+ return n.type === 'tabs' ? n : null;
112
+ }
113
+ function asSlot(n: LayoutNode) {
114
+ return n.type === 'slot' ? n : null;
115
+ }
116
+
117
+ /** Build per-tab closable flags from reactive pool state. */
118
+ function tabClosable(tabs: import('./types').TabEntry[]): (boolean | undefined)[] {
119
+ return tabs.map((t) => isSlotClosable(t.slotId) || undefined);
120
+ }
121
+
122
+ /** Build per-tab dirty flags for TabbedPanel from live pool state. */
123
+ function tabDirty(tabs: import('./types').TabEntry[]): (boolean | undefined)[] {
124
+ return tabs.map((t) => isSlotDirty(t.slotId) || undefined);
125
+ }
126
+
127
+ /** Handle close button click from TabbedPanel. */
128
+ function handleTabClose(tabs: import('./types').TabEntry[], index: number) {
129
+ const entry = tabs[index];
130
+ if (entry) closeTab(entry.slotId);
131
+ }
132
+
133
+ /** Svelte action: mount a custom empty renderer into the element. */
134
+ function mountEmptyRenderer(node: HTMLElement, renderer: (el: HTMLElement) => void) {
135
+ renderer(node);
136
+ }
137
+
138
+ /**
139
+ * Drop handler for empty persistent tab groups. The whole area acts as
140
+ * a single "insert as first tab" target — no quadrant splits.
141
+ */
142
+ function onEmptyTabsDrop(_e: PointerEvent, tabsNode: import('./types').TabsNode) {
143
+ if (dragState.phase !== 'dragging') return;
144
+ setDropTarget({ kind: 'strip', tabsNode, insertIndex: 0 });
145
+ }
146
+
147
+ function onEmptyTabsLeave() {
148
+ clearDropTarget((t) => t.kind === 'strip' && t.insertIndex === 0);
149
+ }
150
+ </script>
151
+
152
+ {#if node}
153
+ {#if node.type === 'split'}
154
+ {@const split = asSplit(node)!}
155
+ <ResizableSplitter
156
+ direction={split.direction}
157
+ sizes={split.sizes}
158
+ pinned={split.pinned}
159
+ collapsed={split.collapsed}
160
+ count={split.children.length}
161
+ pane={splitPane}
162
+ onResize={(i, v) => (split.sizes[i] = v)}
163
+ onCollapseToggle={(i, v) => {
164
+ if (!split.collapsed) split.collapsed = split.children.map(() => false);
165
+ split.collapsed[i] = v;
166
+ }}
167
+ />
168
+ {#snippet splitPane(i: number)}
169
+ <Self path={[...path, i]} />
170
+ {/snippet}
171
+ {:else if node.type === 'tabs'}
172
+ {@const tabs = asTabs(node)}
173
+ {#if tabs && tabs.tabs.length > 0}
174
+ {@const controller = makeController(tabs)}
175
+ <TabbedPanel
176
+ labels={tabs.tabs.map((t) => t.label)}
177
+ icons={tabs.tabs.map((t) => t.icon)}
178
+ activeTab={tabs.activeTab}
179
+ onActiveChange={(i) => (tabs.activeTab = i)}
180
+ body={tabBody}
181
+ dragController={controller}
182
+ clickGuard={suppressNextClick}
183
+ closable={tabClosable(tabs.tabs)}
184
+ dirty={tabDirty(tabs.tabs)}
185
+ onClose={(i) => handleTabClose(tabs.tabs, i)}
186
+ />
187
+ {#snippet tabBody(i: number)}
188
+ {@const entry = tabs.tabs[i]}
189
+ <div class="tab-slot-wrapper">
190
+ <SlotContainer node={{ type: 'slot', slotId: entry.slotId, viewId: entry.viewId }} label={entry.label} />
191
+ <SlotDropZone path={path} />
192
+ </div>
193
+ {/snippet}
194
+ {:else if tabs?.persistent}
195
+ <div class="empty-tabs-placeholder">
196
+ {#if tabs.emptyRenderer}
197
+ <div class="empty-tabs-custom" use:mountEmptyRenderer={tabs.emptyRenderer}></div>
198
+ {:else}
199
+ <div class="empty-tabs-default">
200
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
201
+ <div
202
+ class="empty-tabs-drop"
203
+ onpointermove={(e) => onEmptyTabsDrop(e, tabs)}
204
+ onpointerleave={onEmptyTabsLeave}
205
+ ></div>
206
+ </div>
207
+ {/if}
208
+ </div>
209
+ {/if}
210
+ {:else}
211
+ {@const slot = asSlot(node)!}
212
+ <div class="leaf-slot-wrapper">
213
+ <SlotContainer node={slot} />
214
+ <SlotDropZone path={path} />
215
+ </div>
216
+ {/if}
217
+ {/if}
218
+
219
+ <style>
220
+ .tab-slot-wrapper,
221
+ .leaf-slot-wrapper {
222
+ position: absolute;
223
+ inset: 0;
224
+ min-width: 0;
225
+ min-height: 0;
226
+ }
227
+ .empty-tabs-placeholder {
228
+ width: 100%;
229
+ height: 100%;
230
+ min-width: 0;
231
+ min-height: 0;
232
+ position: relative;
233
+ }
234
+ .empty-tabs-default {
235
+ position: absolute;
236
+ inset: 0;
237
+ display: flex;
238
+ flex-direction: column;
239
+ align-items: center;
240
+ justify-content: center;
241
+ color: var(--shell-fg-muted);
242
+ font-size: 12px;
243
+ background:
244
+ repeating-linear-gradient(
245
+ 45deg,
246
+ var(--shell-bg) 0 10px,
247
+ var(--shell-bg-elevated) 10px 20px
248
+ );
249
+ border: 1px dashed var(--shell-border-strong);
250
+ }
251
+ .empty-tabs-custom {
252
+ position: absolute;
253
+ inset: 0;
254
+ }
255
+ .empty-tabs-drop {
256
+ position: absolute;
257
+ inset: 0;
258
+ pointer-events: auto;
259
+ }
260
+ </style>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ path?: number[];
3
+ };
4
+ declare const LayoutRenderer: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type LayoutRenderer = ReturnType<typeof LayoutRenderer>;
6
+ export default LayoutRenderer;
@@ -0,0 +1,140 @@
1
+ <script lang="ts">
2
+ /*
3
+ * SlotContainer — the leaf of the layout tree and the hand-off point
4
+ * between the framework and shard-contributed views.
5
+ *
6
+ * Phase 6 change: the mounted view no longer lives inside this
7
+ * component's own DOM. Instead, the slot host is owned by the
8
+ * `slotHostPool` module and SlotContainer merely attaches (and later
9
+ * releases) the pooled host to its own wrapper. This is what makes
10
+ * drag-to-reorganize survive: when a tab moves, the old SlotContainer
11
+ * tears down and a new one mounts, but the pooled host (and the view
12
+ * mounted into it) is re-parented to the new wrapper without being
13
+ * destroyed. See slotHostPool.ts for the refcount / deferred-destroy
14
+ * details.
15
+ *
16
+ * Responsibilities:
17
+ * 1. Acquire the pooled host for `node.slotId` on mount and append
18
+ * it to the wrapper.
19
+ * 2. Release the pooled host on unmount. The pool decides whether
20
+ * that's a genuine destroy or the first half of a re-parent.
21
+ * 3. If no factory is registered for the viewId (empty slot or the
22
+ * shard providing it hasn't activated yet), render a placeholder
23
+ * in the wrapper alongside the empty host. A local ResizeObserver
24
+ * feeds the placeholder's dimensions readout.
25
+ *
26
+ * The view's own onResize delivery is NOT SlotContainer's job — the
27
+ * pool owns a ResizeObserver on each host that outlives this component
28
+ * across re-parents. See slotHostPool.ts.
29
+ *
30
+ * Note on the placeholder: the pool creates a host even when there is
31
+ * no factory, so the placeholder is layered *on top* of the empty
32
+ * host. That keeps the acquire/release path uniform — phase 7's
33
+ * "factory registered after layout render" case will just replace the
34
+ * host's contents without rewriting SlotContainer.
35
+ */
36
+
37
+ import type { SlotNode } from './types';
38
+ import { getView } from '../shards/registry';
39
+ import { acquireSlotHost, releaseSlotHost } from './slotHostPool.svelte';
40
+
41
+ let { node, label = '' }: { node: SlotNode; label?: string } = $props();
42
+
43
+ let wrapper: HTMLDivElement | undefined = $state();
44
+ let width = $state(0);
45
+ let height = $state(0);
46
+ // Whether a factory is registered — drives the placeholder. The pool
47
+ // owns the actual mount call; we mirror the registry lookup here just
48
+ // to decide whether to show the "no factory" hint.
49
+ const hasFactory = $derived(node.viewId ? !!getView(node.viewId) : false);
50
+
51
+ $effect(() => {
52
+ if (!wrapper) return;
53
+
54
+ const host = acquireSlotHost(node.slotId, node.viewId, label || node.viewId || node.slotId);
55
+ wrapper.appendChild(host);
56
+
57
+ // Local observer exists only to drive the placeholder's dims text;
58
+ // the view's own onResize is delivered by the pool.
59
+ const ro = new ResizeObserver((entries) => {
60
+ for (const entry of entries) {
61
+ const box = entry.contentRect;
62
+ width = Math.round(box.width);
63
+ height = Math.round(box.height);
64
+ }
65
+ });
66
+ ro.observe(wrapper);
67
+
68
+ return () => {
69
+ ro.disconnect();
70
+ releaseSlotHost(node.slotId);
71
+ };
72
+ });
73
+ </script>
74
+
75
+ <div
76
+ class="slot"
77
+ data-slot-id={node.slotId}
78
+ data-view-id={node.viewId ?? ''}
79
+ bind:this={wrapper}
80
+ >
81
+ {#if !hasFactory}
82
+ <div class="slot-placeholder">
83
+ <div class="slot-id">{node.slotId}</div>
84
+ <div class="slot-meta">
85
+ {#if node.viewId}
86
+ no factory for <code>{node.viewId}</code>
87
+ {:else}
88
+ <em>empty slot</em>
89
+ {/if}
90
+ </div>
91
+ <div class="slot-dims">{width} × {height}</div>
92
+ </div>
93
+ {/if}
94
+ </div>
95
+
96
+ <style>
97
+ .slot {
98
+ position: relative;
99
+ width: 100%;
100
+ height: 100%;
101
+ min-width: 0;
102
+ min-height: 0;
103
+ overflow: hidden;
104
+ }
105
+ .slot-placeholder {
106
+ position: absolute;
107
+ inset: 0;
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ justify-content: center;
112
+ gap: var(--shell-pad-sm);
113
+ color: var(--shell-fg-muted);
114
+ font-size: 12px;
115
+ text-align: center;
116
+ padding: var(--shell-pad-md);
117
+ background:
118
+ repeating-linear-gradient(
119
+ 45deg,
120
+ var(--shell-bg) 0 10px,
121
+ var(--shell-bg-elevated) 10px 20px
122
+ );
123
+ border: 1px dashed var(--shell-border-strong);
124
+ pointer-events: none;
125
+ }
126
+ .slot-id {
127
+ color: var(--shell-fg);
128
+ font-family: var(--shell-font-mono);
129
+ font-size: 13px;
130
+ }
131
+ .slot-meta code {
132
+ font-family: var(--shell-font-mono);
133
+ color: var(--shell-accent);
134
+ }
135
+ .slot-dims {
136
+ font-family: var(--shell-font-mono);
137
+ color: var(--shell-fg-subtle);
138
+ font-size: 11px;
139
+ }
140
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { SlotNode } from './types';
2
+ type $$ComponentProps = {
3
+ node: SlotNode;
4
+ label?: string;
5
+ };
6
+ declare const SlotContainer: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type SlotContainer = ReturnType<typeof SlotContainer>;
8
+ export default SlotContainer;
@@ -0,0 +1,122 @@
1
+ <script lang="ts">
2
+ /*
3
+ * SlotDropZone — an overlay covering a slot's body that, during a
4
+ * drag, reports 4-quadrant split-drop targets to the drag engine
5
+ * and draws a colored quadrant highlight matching the hovered side.
6
+ *
7
+ * Sits in the tab-body pane (for tab leaves) or directly over a
8
+ * standalone slot container. Does not intercept pointer events when
9
+ * no drag is active; during a drag, it captures pointermove to
10
+ * compute the hovered quadrant.
11
+ *
12
+ * Quadrant math:
13
+ * The body is divided into 4 triangles meeting at the center, so
14
+ * each triangle maps the nearest edge to a split side. Top → top
15
+ * split (vertical, new tab above). Same for bottom / left / right.
16
+ * The inner "center" region is deliberately absent in phase 6 —
17
+ * we don't support "merge into same tabs group" via body drop,
18
+ * only via strip drop. This keeps the UX unambiguous: body = split,
19
+ * strip = merge.
20
+ */
21
+
22
+ import { dragState, setDropTarget, clearDropTarget, type DropTarget } from './drag.svelte';
23
+ import type { LayoutPath, SplitSide } from './ops';
24
+
25
+ let {
26
+ path,
27
+ }: {
28
+ /** Path of the node this zone covers, used when reporting the drop. */
29
+ path: LayoutPath;
30
+ } = $props();
31
+
32
+ let zoneEl: HTMLDivElement | undefined = $state();
33
+ let hoveredSide: SplitSide | null = $state(null);
34
+
35
+ // Don't capture pointer events unless a drag is in progress — otherwise
36
+ // the zone would shadow the slot's own interactions.
37
+ const active = $derived(dragState.phase === 'dragging');
38
+
39
+ function quadrantFor(x: number, y: number, rect: DOMRect): SplitSide {
40
+ const cx = rect.left + rect.width / 2;
41
+ const cy = rect.top + rect.height / 2;
42
+ const dx = x - cx;
43
+ const dy = y - cy;
44
+ // Triangles: compare which signed half-plane the point falls in,
45
+ // determined by the cell aspect ratio so the diagonals meet at the
46
+ // center regardless of shape.
47
+ const nx = dx / rect.width;
48
+ const ny = dy / rect.height;
49
+ if (Math.abs(nx) > Math.abs(ny)) {
50
+ return nx < 0 ? 'left' : 'right';
51
+ }
52
+ return ny < 0 ? 'top' : 'bottom';
53
+ }
54
+
55
+ function onMove(e: PointerEvent) {
56
+ if (!zoneEl) return;
57
+ const rect = zoneEl.getBoundingClientRect();
58
+ // If pointer is outside the zone (pointercapture from elsewhere),
59
+ // clear.
60
+ if (
61
+ e.clientX < rect.left ||
62
+ e.clientX > rect.right ||
63
+ e.clientY < rect.top ||
64
+ e.clientY > rect.bottom
65
+ ) {
66
+ if (hoveredSide !== null) {
67
+ hoveredSide = null;
68
+ clearDropTarget((t) => t.kind === 'split' && t.path.join('/') === path.join('/'));
69
+ }
70
+ return;
71
+ }
72
+ const side = quadrantFor(e.clientX, e.clientY, rect);
73
+ if (side !== hoveredSide) {
74
+ hoveredSide = side;
75
+ const target: DropTarget = { kind: 'split', path: [...path], side };
76
+ setDropTarget(target);
77
+ }
78
+ }
79
+
80
+ function onLeave() {
81
+ if (hoveredSide !== null) {
82
+ hoveredSide = null;
83
+ clearDropTarget((t) => t.kind === 'split' && t.path.join('/') === path.join('/'));
84
+ }
85
+ }
86
+ </script>
87
+
88
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
89
+ <div
90
+ class="slot-drop-zone"
91
+ class:active
92
+ bind:this={zoneEl}
93
+ onpointermove={onMove}
94
+ onpointerleave={onLeave}
95
+ >
96
+ {#if hoveredSide}
97
+ <div class="quad-highlight quad-{hoveredSide}"></div>
98
+ {/if}
99
+ </div>
100
+
101
+ <style>
102
+ .slot-drop-zone {
103
+ position: absolute;
104
+ inset: 0;
105
+ pointer-events: none;
106
+ }
107
+ .slot-drop-zone.active {
108
+ pointer-events: auto;
109
+ }
110
+ .quad-highlight {
111
+ position: absolute;
112
+ background: var(--shell-accent);
113
+ opacity: 0.18;
114
+ border: 1px dashed var(--shell-accent);
115
+ pointer-events: none;
116
+ transition: inset 80ms ease;
117
+ }
118
+ .quad-highlight.quad-left { top: 0; bottom: 0; left: 0; right: 50%; }
119
+ .quad-highlight.quad-right { top: 0; bottom: 0; left: 50%; right: 0; }
120
+ .quad-highlight.quad-top { left: 0; right: 0; top: 0; bottom: 50%; }
121
+ .quad-highlight.quad-bottom { left: 0; right: 0; top: 50%; bottom: 0; }
122
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { LayoutPath } from './ops';
2
+ type $$ComponentProps = {
3
+ /** Path of the node this zone covers, used when reporting the drop. */
4
+ path: LayoutPath;
5
+ };
6
+ declare const SlotDropZone: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type SlotDropZone = ReturnType<typeof SlotDropZone>;
8
+ export default SlotDropZone;
@@ -0,0 +1,45 @@
1
+ import type { TabEntry, TabsNode } from './types';
2
+ import { type LayoutPath, type SplitSide } from './ops';
3
+ export type DropTarget = {
4
+ kind: 'strip';
5
+ tabsNode: TabsNode;
6
+ /** Insertion index within tabsNode.tabs (0..length). */
7
+ insertIndex: number;
8
+ } | {
9
+ kind: 'split';
10
+ path: LayoutPath;
11
+ side: SplitSide;
12
+ };
13
+ interface DragSource {
14
+ slotId: string;
15
+ entry: TabEntry;
16
+ /** The tab's viewport rect at drag start — used to offset the ghost. */
17
+ startRect: DOMRect;
18
+ /** Pointer offset inside the tab at drag start. */
19
+ offsetX: number;
20
+ offsetY: number;
21
+ }
22
+ interface DragState {
23
+ phase: 'idle' | 'pending' | 'dragging';
24
+ source: DragSource | null;
25
+ pointerX: number;
26
+ pointerY: number;
27
+ target: DropTarget | null;
28
+ }
29
+ export declare const dragState: DragState;
30
+ export declare function suppressNextClick(): boolean;
31
+ /**
32
+ * Begin a potential tab drag. Call from pointerdown on a tab element.
33
+ * This does not yet enter the dragging phase — movement past the
34
+ * threshold is required.
35
+ */
36
+ export declare function beginTabDrag(slotId: string, entry: TabEntry, event: PointerEvent, tabElement: HTMLElement): void;
37
+ /**
38
+ * Called by drop zone components when the pointer is over them. The
39
+ * last call wins, so innermost / most-specific zones should call this
40
+ * on pointermove over their geometry. `clearDropTarget` is called when
41
+ * the pointer leaves.
42
+ */
43
+ export declare function setDropTarget(target: DropTarget): void;
44
+ export declare function clearDropTarget(match?: (target: DropTarget) => boolean): void;
45
+ export {};