sh3-core 0.6.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 (242) hide show
  1. package/README.md +9 -0
  2. package/dist/Shell.svelte +283 -0
  3. package/dist/Shell.svelte.d.ts +5 -0
  4. package/dist/api.d.ts +28 -0
  5. package/dist/api.js +50 -0
  6. package/dist/app/admin/ApiKeysView.svelte +169 -0
  7. package/dist/app/admin/ApiKeysView.svelte.d.ts +3 -0
  8. package/dist/app/admin/AuthSettingsView.svelte +105 -0
  9. package/dist/app/admin/AuthSettingsView.svelte.d.ts +3 -0
  10. package/dist/app/admin/SystemView.svelte +73 -0
  11. package/dist/app/admin/SystemView.svelte.d.ts +3 -0
  12. package/dist/app/admin/UsersView.svelte +188 -0
  13. package/dist/app/admin/UsersView.svelte.d.ts +3 -0
  14. package/dist/app/admin/adminApp.d.ts +7 -0
  15. package/dist/app/admin/adminApp.js +25 -0
  16. package/dist/app/admin/adminShard.svelte.d.ts +4 -0
  17. package/dist/app/admin/adminShard.svelte.js +62 -0
  18. package/dist/app/store/InstalledView.svelte +246 -0
  19. package/dist/app/store/InstalledView.svelte.d.ts +3 -0
  20. package/dist/app/store/StoreView.svelte +522 -0
  21. package/dist/app/store/StoreView.svelte.d.ts +3 -0
  22. package/dist/app/store/storeApp.d.ts +10 -0
  23. package/dist/app/store/storeApp.js +26 -0
  24. package/dist/app/store/storeShard.svelte.d.ts +38 -0
  25. package/dist/app/store/storeShard.svelte.js +218 -0
  26. package/dist/apps/lifecycle.d.ts +42 -0
  27. package/dist/apps/lifecycle.js +184 -0
  28. package/dist/apps/registry.svelte.d.ts +40 -0
  29. package/dist/apps/registry.svelte.js +59 -0
  30. package/dist/apps/terminal/manifest.d.ts +8 -0
  31. package/dist/apps/terminal/manifest.js +13 -0
  32. package/dist/apps/terminal/terminal-app.d.ts +7 -0
  33. package/dist/apps/terminal/terminal-app.js +14 -0
  34. package/dist/apps/types.d.ts +93 -0
  35. package/dist/apps/types.js +10 -0
  36. package/dist/artifact.d.ts +32 -0
  37. package/dist/artifact.js +1 -0
  38. package/dist/assets/SH3.png +0 -0
  39. package/dist/assets/icons.svg +1126 -0
  40. package/dist/assets.d.ts +13 -0
  41. package/dist/auth/GuestBanner.svelte +134 -0
  42. package/dist/auth/GuestBanner.svelte.d.ts +3 -0
  43. package/dist/auth/SignInWall.svelte +203 -0
  44. package/dist/auth/SignInWall.svelte.d.ts +7 -0
  45. package/dist/auth/auth.svelte.d.ts +69 -0
  46. package/dist/auth/auth.svelte.js +165 -0
  47. package/dist/auth/index.d.ts +2 -0
  48. package/dist/auth/index.js +1 -0
  49. package/dist/auth/types.d.ts +41 -0
  50. package/dist/auth/types.js +6 -0
  51. package/dist/build.d.ts +49 -0
  52. package/dist/build.js +236 -0
  53. package/dist/contract.d.ts +20 -0
  54. package/dist/contract.js +28 -0
  55. package/dist/createShell.d.ts +24 -0
  56. package/dist/createShell.js +131 -0
  57. package/dist/documents/backends.d.ts +17 -0
  58. package/dist/documents/backends.js +156 -0
  59. package/dist/documents/config.d.ts +7 -0
  60. package/dist/documents/config.js +27 -0
  61. package/dist/documents/handle.d.ts +6 -0
  62. package/dist/documents/handle.js +154 -0
  63. package/dist/documents/http-backend.d.ts +22 -0
  64. package/dist/documents/http-backend.js +78 -0
  65. package/dist/documents/index.d.ts +6 -0
  66. package/dist/documents/index.js +8 -0
  67. package/dist/documents/notifications.d.ts +9 -0
  68. package/dist/documents/notifications.js +39 -0
  69. package/dist/documents/types.d.ts +97 -0
  70. package/dist/documents/types.js +12 -0
  71. package/dist/env/client.d.ts +44 -0
  72. package/dist/env/client.js +106 -0
  73. package/dist/env/index.d.ts +2 -0
  74. package/dist/env/index.js +1 -0
  75. package/dist/env/types.d.ts +12 -0
  76. package/dist/env/types.js +8 -0
  77. package/dist/host-entry.d.ts +13 -0
  78. package/dist/host-entry.js +17 -0
  79. package/dist/host.d.ts +15 -0
  80. package/dist/host.js +86 -0
  81. package/dist/index.d.ts +4 -0
  82. package/dist/index.js +14 -0
  83. package/dist/layout/DragPreview.svelte +63 -0
  84. package/dist/layout/DragPreview.svelte.d.ts +3 -0
  85. package/dist/layout/LayoutRenderer.svelte +262 -0
  86. package/dist/layout/LayoutRenderer.svelte.d.ts +6 -0
  87. package/dist/layout/SlotContainer.svelte +140 -0
  88. package/dist/layout/SlotContainer.svelte.d.ts +8 -0
  89. package/dist/layout/SlotDropZone.svelte +122 -0
  90. package/dist/layout/SlotDropZone.svelte.d.ts +8 -0
  91. package/dist/layout/drag.svelte.d.ts +45 -0
  92. package/dist/layout/drag.svelte.js +200 -0
  93. package/dist/layout/inspection.d.ts +72 -0
  94. package/dist/layout/inspection.js +209 -0
  95. package/dist/layout/ops.d.ts +100 -0
  96. package/dist/layout/ops.js +310 -0
  97. package/dist/layout/slotHostPool.svelte.d.ts +36 -0
  98. package/dist/layout/slotHostPool.svelte.js +229 -0
  99. package/dist/layout/store.svelte.d.ts +39 -0
  100. package/dist/layout/store.svelte.js +153 -0
  101. package/dist/layout/tree-walk.d.ts +15 -0
  102. package/dist/layout/tree-walk.js +33 -0
  103. package/dist/layout/types.d.ts +108 -0
  104. package/dist/layout/types.js +25 -0
  105. package/dist/migrations/shell-rename.d.ts +16 -0
  106. package/dist/migrations/shell-rename.js +48 -0
  107. package/dist/overlays/ModalFrame.svelte +87 -0
  108. package/dist/overlays/ModalFrame.svelte.d.ts +10 -0
  109. package/dist/overlays/PopupFrame.svelte +85 -0
  110. package/dist/overlays/PopupFrame.svelte.d.ts +10 -0
  111. package/dist/overlays/ToastItem.svelte +77 -0
  112. package/dist/overlays/ToastItem.svelte.d.ts +9 -0
  113. package/dist/overlays/focusTrap.d.ts +1 -0
  114. package/dist/overlays/focusTrap.js +64 -0
  115. package/dist/overlays/modal.d.ts +9 -0
  116. package/dist/overlays/modal.js +141 -0
  117. package/dist/overlays/popup.d.ts +9 -0
  118. package/dist/overlays/popup.js +108 -0
  119. package/dist/overlays/roots.d.ts +4 -0
  120. package/dist/overlays/roots.js +31 -0
  121. package/dist/overlays/toast.d.ts +6 -0
  122. package/dist/overlays/toast.js +93 -0
  123. package/dist/overlays/types.d.ts +31 -0
  124. package/dist/overlays/types.js +15 -0
  125. package/dist/platform/index.d.ts +10 -0
  126. package/dist/platform/index.js +33 -0
  127. package/dist/platform/tauri-backend.d.ts +15 -0
  128. package/dist/platform/tauri-backend.js +58 -0
  129. package/dist/primitives/.gitkeep +0 -0
  130. package/dist/primitives/ResizableSplitter.svelte +333 -0
  131. package/dist/primitives/ResizableSplitter.svelte.d.ts +35 -0
  132. package/dist/primitives/TabbedPanel.svelte +305 -0
  133. package/dist/primitives/TabbedPanel.svelte.d.ts +50 -0
  134. package/dist/primitives/base.css +42 -0
  135. package/dist/registry/client.d.ts +74 -0
  136. package/dist/registry/client.js +117 -0
  137. package/dist/registry/index.d.ts +13 -0
  138. package/dist/registry/index.js +14 -0
  139. package/dist/registry/installer.d.ts +53 -0
  140. package/dist/registry/installer.js +168 -0
  141. package/dist/registry/integrity.d.ts +32 -0
  142. package/dist/registry/integrity.js +92 -0
  143. package/dist/registry/loader.d.ts +50 -0
  144. package/dist/registry/loader.js +145 -0
  145. package/dist/registry/schema.d.ts +47 -0
  146. package/dist/registry/schema.js +185 -0
  147. package/dist/registry/storage.d.ts +37 -0
  148. package/dist/registry/storage.js +101 -0
  149. package/dist/registry/types.d.ts +262 -0
  150. package/dist/registry/types.js +14 -0
  151. package/dist/server-shard/types.d.ts +67 -0
  152. package/dist/server-shard/types.js +13 -0
  153. package/dist/sh3core-shard/ShellHome.svelte +192 -0
  154. package/dist/sh3core-shard/ShellHome.svelte.d.ts +3 -0
  155. package/dist/sh3core-shard/ShellTitle.svelte +171 -0
  156. package/dist/sh3core-shard/ShellTitle.svelte.d.ts +3 -0
  157. package/dist/sh3core-shard/sh3coreShard.svelte.d.ts +2 -0
  158. package/dist/sh3core-shard/sh3coreShard.svelte.js +53 -0
  159. package/dist/shards/activate.svelte.d.ts +52 -0
  160. package/dist/shards/activate.svelte.js +186 -0
  161. package/dist/shards/registry.d.ts +4 -0
  162. package/dist/shards/registry.js +28 -0
  163. package/dist/shards/types.d.ts +207 -0
  164. package/dist/shards/types.js +20 -0
  165. package/dist/shell-shard/InputLine.svelte +133 -0
  166. package/dist/shell-shard/InputLine.svelte.d.ts +11 -0
  167. package/dist/shell-shard/ScrollbackView.svelte +47 -0
  168. package/dist/shell-shard/ScrollbackView.svelte.d.ts +7 -0
  169. package/dist/shell-shard/Terminal.svelte +122 -0
  170. package/dist/shell-shard/Terminal.svelte.d.ts +8 -0
  171. package/dist/shell-shard/entries/PromptEntry.svelte +25 -0
  172. package/dist/shell-shard/entries/PromptEntry.svelte.d.ts +7 -0
  173. package/dist/shell-shard/entries/RichEntry.svelte +19 -0
  174. package/dist/shell-shard/entries/RichEntry.svelte.d.ts +8 -0
  175. package/dist/shell-shard/entries/StatusEntry.svelte +22 -0
  176. package/dist/shell-shard/entries/StatusEntry.svelte.d.ts +7 -0
  177. package/dist/shell-shard/entries/TextEntry.svelte +25 -0
  178. package/dist/shell-shard/entries/TextEntry.svelte.d.ts +7 -0
  179. package/dist/shell-shard/manifest.d.ts +2 -0
  180. package/dist/shell-shard/manifest.js +11 -0
  181. package/dist/shell-shard/protocol.d.ts +90 -0
  182. package/dist/shell-shard/protocol.js +11 -0
  183. package/dist/shell-shard/registry.d.ts +69 -0
  184. package/dist/shell-shard/registry.js +47 -0
  185. package/dist/shell-shard/rich/AppCard.svelte +25 -0
  186. package/dist/shell-shard/rich/AppCard.svelte.d.ts +10 -0
  187. package/dist/shell-shard/rich/AppsTable.svelte +29 -0
  188. package/dist/shell-shard/rich/AppsTable.svelte.d.ts +12 -0
  189. package/dist/shell-shard/rich/EnvTable.svelte +27 -0
  190. package/dist/shell-shard/rich/EnvTable.svelte.d.ts +8 -0
  191. package/dist/shell-shard/rich/HelpTable.svelte +29 -0
  192. package/dist/shell-shard/rich/HelpTable.svelte.d.ts +12 -0
  193. package/dist/shell-shard/rich/HistoryList.svelte +37 -0
  194. package/dist/shell-shard/rich/HistoryList.svelte.d.ts +9 -0
  195. package/dist/shell-shard/rich/ShardsTable.svelte +28 -0
  196. package/dist/shell-shard/rich/ShardsTable.svelte.d.ts +12 -0
  197. package/dist/shell-shard/rich/ViewsTable.svelte +31 -0
  198. package/dist/shell-shard/rich/ViewsTable.svelte.d.ts +13 -0
  199. package/dist/shell-shard/rich/ZoneTree.svelte +19 -0
  200. package/dist/shell-shard/rich/ZoneTree.svelte.d.ts +8 -0
  201. package/dist/shell-shard/rich/ZonesTable.svelte +27 -0
  202. package/dist/shell-shard/rich/ZonesTable.svelte.d.ts +11 -0
  203. package/dist/shell-shard/scrollback.svelte.d.ts +36 -0
  204. package/dist/shell-shard/scrollback.svelte.js +43 -0
  205. package/dist/shell-shard/session-client.svelte.d.ts +23 -0
  206. package/dist/shell-shard/session-client.svelte.js +120 -0
  207. package/dist/shell-shard/shellShard.svelte.d.ts +2 -0
  208. package/dist/shell-shard/shellShard.svelte.js +139 -0
  209. package/dist/shell-shard/verbs/apps.d.ts +3 -0
  210. package/dist/shell-shard/verbs/apps.js +50 -0
  211. package/dist/shell-shard/verbs/clear.d.ts +2 -0
  212. package/dist/shell-shard/verbs/clear.js +7 -0
  213. package/dist/shell-shard/verbs/help.d.ts +2 -0
  214. package/dist/shell-shard/verbs/help.js +21 -0
  215. package/dist/shell-shard/verbs/history.d.ts +2 -0
  216. package/dist/shell-shard/verbs/history.js +20 -0
  217. package/dist/shell-shard/verbs/index.d.ts +2 -0
  218. package/dist/shell-shard/verbs/index.js +29 -0
  219. package/dist/shell-shard/verbs/session.d.ts +5 -0
  220. package/dist/shell-shard/verbs/session.js +65 -0
  221. package/dist/shell-shard/verbs/shards.d.ts +2 -0
  222. package/dist/shell-shard/verbs/shards.js +14 -0
  223. package/dist/shell-shard/verbs/views.d.ts +4 -0
  224. package/dist/shell-shard/verbs/views.js +90 -0
  225. package/dist/shell-shard/verbs/zones.d.ts +3 -0
  226. package/dist/shell-shard/verbs/zones.js +38 -0
  227. package/dist/shellRuntime.svelte.d.ts +27 -0
  228. package/dist/shellRuntime.svelte.js +27 -0
  229. package/dist/state/backends.d.ts +26 -0
  230. package/dist/state/backends.js +99 -0
  231. package/dist/state/manage.d.ts +14 -0
  232. package/dist/state/manage.js +40 -0
  233. package/dist/state/types.d.ts +55 -0
  234. package/dist/state/types.js +17 -0
  235. package/dist/state/zones.svelte.d.ts +53 -0
  236. package/dist/state/zones.svelte.js +141 -0
  237. package/dist/theme.d.ts +28 -0
  238. package/dist/theme.js +92 -0
  239. package/dist/tokens.css +102 -0
  240. package/dist/version.d.ts +2 -0
  241. package/dist/version.js +2 -0
  242. package/package.json +60 -0
@@ -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 {};
@@ -0,0 +1,200 @@
1
+ /*
2
+ * Tab drag engine — state machine + global listeners + commit.
3
+ *
4
+ * Lifecycle:
5
+ * 1. TabbedPanel fires `beginTabDrag(slotId, entry, pointerEvent)`
6
+ * from pointerdown on a tab. The engine records the starting
7
+ * pointer position and the source slotId but does NOT enter the
8
+ * "dragging" phase yet.
9
+ * 2. A global pointermove listener watches for movement exceeding
10
+ * DRAG_THRESHOLD_PX. Clicks that never cross the threshold are
11
+ * not drags — they fall through to the tab's own click handler.
12
+ * 3. On threshold cross, the engine transitions to `dragging` and
13
+ * starts tracking the pointer; the status (visible ghost, drop
14
+ * indicators) is driven by the reactive state.
15
+ * 4. pointerup commits a drop target (if one is currently hovered)
16
+ * and tears down. No commit if the user released over nothing.
17
+ *
18
+ * State shape:
19
+ * The engine exposes a single `dragState` $state object. Components
20
+ * subscribe by reading its fields in a $derived or $effect. The
21
+ * fields:
22
+ * - phase: 'idle' | 'pending' | 'dragging'
23
+ * - source: { slotId, entry } | null
24
+ * - pointer: { x, y } current viewport coords while dragging
25
+ * - target: DropTarget | null — where the commit would land now
26
+ *
27
+ * Drop targets:
28
+ * Two kinds:
29
+ * { kind: 'strip', tabsNode, insertIndex } — between tabs in a strip
30
+ * { kind: 'split', path, side } — quadrant split of a node at path
31
+ * The owning components compute these from their bounding rects and
32
+ * call `setDropTarget` / `clearDropTarget` when the pointer enters /
33
+ * leaves. Multiple overlapping targets: the one most recently set
34
+ * wins (innermost component takes priority because it fires last).
35
+ *
36
+ * Commit:
37
+ * `commit()` is called on pointerup. It looks at dragState.target
38
+ * and runs the appropriate ops mutation against the shared root.
39
+ * The root is handed to the engine on init (via `initDragEngine`)
40
+ * because the engine is a module-level singleton and the root comes
41
+ * from the composition layer.
42
+ */
43
+ import { cleanupTree, insertTabIntoTabs, moveTabWithinTabs, removeTabBySlotId, splitNodeAtPath, } from './ops';
44
+ import { layoutStore } from './store.svelte';
45
+ export const dragState = $state({
46
+ phase: 'idle',
47
+ source: null,
48
+ pointerX: 0,
49
+ pointerY: 0,
50
+ target: null,
51
+ });
52
+ /**
53
+ * True for exactly one macrotask after a drag ends, so the synthetic
54
+ * `click` the browser fires on the original tab (right after
55
+ * pointerup) can be ignored. Consumers check `suppressNextClick()` at
56
+ * the top of their click handler. Without this, the stale click would
57
+ * call the tab's `select(i)` on an index that no longer maps to the
58
+ * same tab after the layout mutation.
59
+ */
60
+ let clickSuppressedUntil = 0;
61
+ export function suppressNextClick() {
62
+ return performance.now() < clickSuppressedUntil;
63
+ }
64
+ const DRAG_THRESHOLD_PX = 4;
65
+ let pendingStartX = 0;
66
+ let pendingStartY = 0;
67
+ /**
68
+ * Begin a potential tab drag. Call from pointerdown on a tab element.
69
+ * This does not yet enter the dragging phase — movement past the
70
+ * threshold is required.
71
+ */
72
+ export function beginTabDrag(slotId, entry, event, tabElement) {
73
+ if (dragState.phase !== 'idle')
74
+ return;
75
+ const rect = tabElement.getBoundingClientRect();
76
+ dragState.phase = 'pending';
77
+ dragState.source = {
78
+ slotId,
79
+ entry,
80
+ startRect: rect,
81
+ offsetX: event.clientX - rect.left,
82
+ offsetY: event.clientY - rect.top,
83
+ };
84
+ dragState.pointerX = event.clientX;
85
+ dragState.pointerY = event.clientY;
86
+ dragState.target = null;
87
+ pendingStartX = event.clientX;
88
+ pendingStartY = event.clientY;
89
+ installGlobalListeners();
90
+ }
91
+ function installGlobalListeners() {
92
+ window.addEventListener('pointermove', onPointerMove, true);
93
+ window.addEventListener('pointerup', onPointerUp, true);
94
+ window.addEventListener('pointercancel', onPointerCancel, true);
95
+ // Lock selection / text caret during drag. Restored in teardown.
96
+ document.body.style.userSelect = 'none';
97
+ // Signal to non-drag-aware components that a drag is in progress,
98
+ // so they can suppress misleading hover states via CSS.
99
+ document.body.dataset.dragging = '';
100
+ }
101
+ function removeGlobalListeners() {
102
+ window.removeEventListener('pointermove', onPointerMove, true);
103
+ window.removeEventListener('pointerup', onPointerUp, true);
104
+ window.removeEventListener('pointercancel', onPointerCancel, true);
105
+ document.body.style.userSelect = '';
106
+ delete document.body.dataset.dragging;
107
+ }
108
+ function onPointerMove(e) {
109
+ dragState.pointerX = e.clientX;
110
+ dragState.pointerY = e.clientY;
111
+ if (dragState.phase === 'pending') {
112
+ const dx = e.clientX - pendingStartX;
113
+ const dy = e.clientY - pendingStartY;
114
+ if (dx * dx + dy * dy >= DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX) {
115
+ dragState.phase = 'dragging';
116
+ }
117
+ }
118
+ }
119
+ function onPointerUp(_e) {
120
+ const wasDragging = dragState.phase === 'dragging';
121
+ if (wasDragging) {
122
+ commit();
123
+ // Swallow the synthetic click that fires on the source tab
124
+ // immediately after pointerup. A few ms is plenty — the click
125
+ // is dispatched synchronously or on the next microtask.
126
+ clickSuppressedUntil = performance.now() + 50;
127
+ }
128
+ teardown();
129
+ }
130
+ function onPointerCancel(_e) {
131
+ teardown();
132
+ }
133
+ function commit() {
134
+ const { source, target } = dragState;
135
+ if (!source || !target)
136
+ return;
137
+ const root = layoutStore.root;
138
+ // Same-group strip drop: atomic move. The two-step remove/insert
139
+ // flow splices the reactive tabs array twice; splice's internal
140
+ // `[[Delete]]` trips Svelte's proxy deleteProperty trap, which can
141
+ // flush reactive consumers while the array is transiently length
142
+ // `N - 1`. A LayoutRenderer `$derived` evaluated in that window
143
+ // sees the shrunk array and the index-`N - 1` snippet body reads an
144
+ // `undefined` entry — which crashes at `.slotId`. `moveTabWithinTabs`
145
+ // avoids the intermediate state by building the next array off-proxy
146
+ // and reassigning `tabs` in one shot. See the helper's doc comment.
147
+ //
148
+ // LayoutRenderer's `onStripHover` normalizes the insert index for
149
+ // same-strip drags into post-removal coordinates, so we can pass
150
+ // `target.insertIndex` straight through.
151
+ if (target.kind === 'strip') {
152
+ const srcIdx = target.tabsNode.tabs.findIndex((t) => t.slotId === source.slotId);
153
+ if (srcIdx >= 0) {
154
+ moveTabWithinTabs(target.tabsNode, srcIdx, target.insertIndex);
155
+ cleanupTree(root);
156
+ return;
157
+ }
158
+ }
159
+ // Cross-group strip drop or split drop: remove from source, then
160
+ // insert into destination (or split target into a new group). The
161
+ // destination's tabs array only grows in the strip case, so no
162
+ // observer sees a shrunk intermediate — the splice-based flow is
163
+ // fine here.
164
+ const removed = removeTabBySlotId(root, source.slotId);
165
+ if (!removed)
166
+ return;
167
+ if (target.kind === 'strip') {
168
+ insertTabIntoTabs(target.tabsNode, removed, target.insertIndex);
169
+ }
170
+ else {
171
+ splitNodeAtPath(root, target.path, removed, target.side);
172
+ }
173
+ cleanupTree(root);
174
+ }
175
+ function teardown() {
176
+ dragState.phase = 'idle';
177
+ dragState.source = null;
178
+ dragState.target = null;
179
+ removeGlobalListeners();
180
+ }
181
+ /**
182
+ * Called by drop zone components when the pointer is over them. The
183
+ * last call wins, so innermost / most-specific zones should call this
184
+ * on pointermove over their geometry. `clearDropTarget` is called when
185
+ * the pointer leaves.
186
+ */
187
+ export function setDropTarget(target) {
188
+ if (dragState.phase !== 'dragging')
189
+ return;
190
+ dragState.target = target;
191
+ }
192
+ export function clearDropTarget(match) {
193
+ if (dragState.phase !== 'dragging')
194
+ return;
195
+ if (!dragState.target)
196
+ return;
197
+ if (match && !match(dragState.target))
198
+ return;
199
+ dragState.target = null;
200
+ }
@@ -0,0 +1,72 @@
1
+ import type { LayoutNode, TabEntry } from './types';
2
+ /**
3
+ * Read-only snapshot of the currently-rendered layout tree. The return
4
+ * value is the live object — callers MUST NOT mutate it directly;
5
+ * mutations go through `spliceIntoActiveLayout`. Returning the live
6
+ * object (not a deep clone) means reactive consumers can re-read on
7
+ * updates without manual subscription wiring.
8
+ */
9
+ export declare function inspectActiveLayout(): {
10
+ root: LayoutNode;
11
+ source: 'home' | 'app';
12
+ };
13
+ /**
14
+ * Add a new tab at the end of the first tabs group found in the
15
+ * currently-rendered layout. If no tabs group exists (e.g. the active
16
+ * root is the single-slot home layout), throws. Phase 8 scope — richer
17
+ * variants arrive later.
18
+ */
19
+ export declare function spliceIntoActiveLayout(entry: TabEntry): void;
20
+ /**
21
+ * Activate the tab whose slot matches `slotId` in the currently-rendered
22
+ * layout. Returns `true` if a matching tab was found and activated.
23
+ */
24
+ export declare function focusTab(slotId: string): boolean;
25
+ /**
26
+ * Activate the first tab whose `viewId` matches in the currently-rendered
27
+ * layout. Returns `true` if a matching tab was found and activated.
28
+ */
29
+ export declare function focusView(viewId: string): boolean;
30
+ /**
31
+ * Collapse a child of a split node at the given path. Returns true if
32
+ * the split was found and the child was collapsed.
33
+ */
34
+ export declare function collapseChild(splitPath: number[], childIndex: number): boolean;
35
+ /**
36
+ * Expand a previously collapsed child of a split node. Returns true if
37
+ * the split was found and the child was expanded.
38
+ */
39
+ export declare function expandChild(splitPath: number[], childIndex: number): boolean;
40
+ /**
41
+ * Request to close a tab by its slot ID. Respects the view's closable
42
+ * policy:
43
+ * - Non-closable (closable undefined/false): returns false immediately.
44
+ * - Pure (closable true): removes the tab, returns true.
45
+ * - Guarded (closable { canClose }): awaits canClose(); removes if
46
+ * resolved true, returns false if resolved false.
47
+ *
48
+ * This is the single entry point for closing tabs — the tab strip's X
49
+ * button and programmatic callers both use it. The layout engine remains
50
+ * the sole authority on tree mutations.
51
+ */
52
+ export declare function closeTab(slotId: string): Promise<boolean>;
53
+ /**
54
+ * Dock a view into the currently-rendered layout without caring which
55
+ * root it is. Used by the Ctrl+` shell hotkey and other "just put it
56
+ * somewhere sensible" callers. Policy:
57
+ *
58
+ * 1. If a tab with the same `viewId` already exists, focus it and
59
+ * return. Callers don't want a second instance of a singleton view
60
+ * every time they hit the shortcut.
61
+ * 2. Otherwise, append the entry to the first tabs group found
62
+ * (`spliceIntoActiveLayout` semantics).
63
+ * 3. If there's no tabs group (e.g. the home root is a single slot),
64
+ * split the first slot leaf horizontally and put the entry on the
65
+ * right. This is the "floating window" fallback described in
66
+ * roadmap SH9 / DF3 — when floating panels land, callers should
67
+ * prefer that path for ephemeral docks.
68
+ *
69
+ * Returns true if the view was focused or inserted, false if the layout
70
+ * was empty or otherwise un-dockable.
71
+ */
72
+ export declare function dockIntoActiveLayout(entry: TabEntry): boolean;
@@ -0,0 +1,209 @@
1
+ /*
2
+ * Layout inspection / mutation — public API for advanced shards.
3
+ *
4
+ * These helpers are how shards like `diagnostic` inject themselves into
5
+ * the currently-rendered layout. The mutation primitives delegate to
6
+ * ops.ts for the actual tree work; inspection reads through the layout
7
+ * manager so diagnostic content stays live as the user rearranges things.
8
+ *
9
+ * Scope note: `spliceIntoActiveLayout` targets the CURRENTLY-RENDERED
10
+ * root only. Mutating a held-but-not-active app tree (e.g. while the
11
+ * user is on home) is not supported in phase 8. If an advanced shard
12
+ * wants to reach the held tree, it has to do so while the app is
13
+ * rendered.
14
+ */
15
+ import { activeLayout, getActiveRoot } from './store.svelte';
16
+ import { nodeAtPath, findTabBySlotId, removeTabBySlotId, cleanupTree, splitNodeAtPath, } from './ops';
17
+ import { getSlotHandle } from './slotHostPool.svelte';
18
+ /**
19
+ * Read-only snapshot of the currently-rendered layout tree. The return
20
+ * value is the live object — callers MUST NOT mutate it directly;
21
+ * mutations go through `spliceIntoActiveLayout`. Returning the live
22
+ * object (not a deep clone) means reactive consumers can re-read on
23
+ * updates without manual subscription wiring.
24
+ */
25
+ export function inspectActiveLayout() {
26
+ return { root: activeLayout(), source: getActiveRoot() };
27
+ }
28
+ /**
29
+ * Add a new tab at the end of the first tabs group found in the
30
+ * currently-rendered layout. If no tabs group exists (e.g. the active
31
+ * root is the single-slot home layout), throws. Phase 8 scope — richer
32
+ * variants arrive later.
33
+ */
34
+ export function spliceIntoActiveLayout(entry) {
35
+ const root = activeLayout();
36
+ const target = findFirstTabsNode(root);
37
+ if (!target) {
38
+ throw new Error('spliceIntoActiveLayout: no tabs group found in the active layout; ' +
39
+ 'phase 8 only supports splicing into an existing tab group.');
40
+ }
41
+ target.tabs.push(entry);
42
+ target.activeTab = target.tabs.length - 1;
43
+ }
44
+ /**
45
+ * Activate the tab whose slot matches `slotId` in the currently-rendered
46
+ * layout. Returns `true` if a matching tab was found and activated.
47
+ */
48
+ export function focusTab(slotId) {
49
+ const root = activeLayout();
50
+ return focusTabWhere(root, (entry) => entry.slotId === slotId);
51
+ }
52
+ /**
53
+ * Activate the first tab whose `viewId` matches in the currently-rendered
54
+ * layout. Returns `true` if a matching tab was found and activated.
55
+ */
56
+ export function focusView(viewId) {
57
+ const root = activeLayout();
58
+ return focusTabWhere(root, (entry) => entry.viewId === viewId);
59
+ }
60
+ /** Walk the tree looking for a tab entry that satisfies `pred`, activate it. */
61
+ function focusTabWhere(node, pred) {
62
+ if (node.type === 'tabs') {
63
+ const idx = node.tabs.findIndex(pred);
64
+ if (idx >= 0) {
65
+ node.activeTab = idx;
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ if (node.type === 'split') {
71
+ for (const child of node.children) {
72
+ if (focusTabWhere(child, pred))
73
+ return true;
74
+ }
75
+ }
76
+ return false;
77
+ }
78
+ /**
79
+ * Collapse a child of a split node at the given path. Returns true if
80
+ * the split was found and the child was collapsed.
81
+ */
82
+ export function collapseChild(splitPath, childIndex) {
83
+ return setCollapsed(splitPath, childIndex, true);
84
+ }
85
+ /**
86
+ * Expand a previously collapsed child of a split node. Returns true if
87
+ * the split was found and the child was expanded.
88
+ */
89
+ export function expandChild(splitPath, childIndex) {
90
+ return setCollapsed(splitPath, childIndex, false);
91
+ }
92
+ function setCollapsed(splitPath, childIndex, value) {
93
+ const root = activeLayout();
94
+ const node = nodeAtPath(root, splitPath);
95
+ if (!node || node.type !== 'split')
96
+ return false;
97
+ const split = node;
98
+ if (childIndex < 0 || childIndex >= split.children.length)
99
+ return false;
100
+ if (!split.collapsed)
101
+ split.collapsed = split.children.map(() => false);
102
+ split.collapsed[childIndex] = value;
103
+ return true;
104
+ }
105
+ /**
106
+ * Request to close a tab by its slot ID. Respects the view's closable
107
+ * policy:
108
+ * - Non-closable (closable undefined/false): returns false immediately.
109
+ * - Pure (closable true): removes the tab, returns true.
110
+ * - Guarded (closable { canClose }): awaits canClose(); removes if
111
+ * resolved true, returns false if resolved false.
112
+ *
113
+ * This is the single entry point for closing tabs — the tab strip's X
114
+ * button and programmatic callers both use it. The layout engine remains
115
+ * the sole authority on tree mutations.
116
+ */
117
+ export async function closeTab(slotId) {
118
+ const root = activeLayout();
119
+ const located = findTabBySlotId(root, slotId);
120
+ if (!located)
121
+ return false;
122
+ const handle = getSlotHandle(slotId);
123
+ const closable = handle === null || handle === void 0 ? void 0 : handle.closable;
124
+ // Non-closable: no action.
125
+ if (!closable)
126
+ return false;
127
+ // Guarded: ask the shard.
128
+ if (typeof closable === 'object') {
129
+ const allowed = await closable.canClose();
130
+ if (!allowed)
131
+ return false;
132
+ // Re-verify the tab still exists after the async gap — another close
133
+ // request or a layout mutation may have removed it while we awaited.
134
+ if (!findTabBySlotId(root, slotId))
135
+ return false;
136
+ }
137
+ // Remove the tab from the tree. This causes Svelte to tear down the
138
+ // SlotContainer, which calls releaseSlotHost() — the pool's deferred
139
+ // destroy microtask then fires handle.unmount(). We do NOT call
140
+ // releaseSlotHost here; the existing component lifecycle handles it.
141
+ removeTabBySlotId(root, slotId);
142
+ // Cleanup: prune empty (non-persistent) tab groups, collapse single-child splits.
143
+ cleanupTree(root);
144
+ return true;
145
+ }
146
+ function findFirstTabsNode(node) {
147
+ if (node.type === 'tabs')
148
+ return node;
149
+ if (node.type === 'split') {
150
+ for (const c of node.children) {
151
+ const hit = findFirstTabsNode(c);
152
+ if (hit)
153
+ return hit;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ /** Find the path to the first slot leaf with a bound viewId. */
159
+ function findFirstSlotPath(node, path = []) {
160
+ if (node.type === 'slot')
161
+ return node.viewId !== null ? path : null;
162
+ if (node.type === 'split') {
163
+ for (let i = 0; i < node.children.length; i++) {
164
+ const hit = findFirstSlotPath(node.children[i], [...path, i]);
165
+ if (hit)
166
+ return hit;
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+ /**
172
+ * Dock a view into the currently-rendered layout without caring which
173
+ * root it is. Used by the Ctrl+` shell hotkey and other "just put it
174
+ * somewhere sensible" callers. Policy:
175
+ *
176
+ * 1. If a tab with the same `viewId` already exists, focus it and
177
+ * return. Callers don't want a second instance of a singleton view
178
+ * every time they hit the shortcut.
179
+ * 2. Otherwise, append the entry to the first tabs group found
180
+ * (`spliceIntoActiveLayout` semantics).
181
+ * 3. If there's no tabs group (e.g. the home root is a single slot),
182
+ * split the first slot leaf horizontally and put the entry on the
183
+ * right. This is the "floating window" fallback described in
184
+ * roadmap SH9 / DF3 — when floating panels land, callers should
185
+ * prefer that path for ephemeral docks.
186
+ *
187
+ * Returns true if the view was focused or inserted, false if the layout
188
+ * was empty or otherwise un-dockable.
189
+ */
190
+ export function dockIntoActiveLayout(entry) {
191
+ var _a;
192
+ const root = activeLayout();
193
+ // 1. Already present? Focus it.
194
+ if (focusView((_a = entry.viewId) !== null && _a !== void 0 ? _a : ''))
195
+ return true;
196
+ // 2. Existing tabs group wins.
197
+ const tabs = findFirstTabsNode(root);
198
+ if (tabs) {
199
+ tabs.tabs.push(entry);
200
+ tabs.activeTab = tabs.tabs.length - 1;
201
+ return true;
202
+ }
203
+ // 3. Fallback: split the first valid slot leaf.
204
+ const slotPath = findFirstSlotPath(root);
205
+ if (!slotPath)
206
+ return false;
207
+ splitNodeAtPath(root, slotPath, entry, 'right');
208
+ return true;
209
+ }