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,100 @@
1
+ import type { LayoutNode, SplitNode, TabsNode, SlotNode, TabEntry } from './types';
2
+ /** A path down the tree: the list of child indices walked from the root. */
3
+ export type LayoutPath = number[];
4
+ /** A located tab: where it lives and which entry index inside its group. */
5
+ export interface LocatedTab {
6
+ tabsNode: TabsNode;
7
+ tabsPath: LayoutPath;
8
+ tabIndex: number;
9
+ entry: TabEntry;
10
+ }
11
+ /**
12
+ * Find a tab by its slotId anywhere in the tree. Returns null if no
13
+ * tab carries that slotId. Used by the drag engine to look up the
14
+ * source tab before any mutation.
15
+ */
16
+ export declare function findTabBySlotId(root: LayoutNode, slotId: string): LocatedTab | null;
17
+ /** Find a standalone slot leaf by its slotId (only leaves, not tab entries). */
18
+ export declare function findSlotBySlotId(root: LayoutNode, slotId: string): {
19
+ node: SlotNode;
20
+ parent: SplitNode | null;
21
+ index: number;
22
+ } | null;
23
+ /**
24
+ * Remove a tab from its current location, returning the removed entry
25
+ * (or null if not found). The tabs group it was in may become empty
26
+ * and is cleaned up by `cleanupTree` after the caller has finished its
27
+ * reinsertion. The two-phase shape (mutate → cleanup) means the caller
28
+ * can remove a tab and re-insert it into the SAME tabs group without
29
+ * the group being collapsed mid-move.
30
+ */
31
+ export declare function removeTabBySlotId(root: LayoutNode, slotId: string): TabEntry | null;
32
+ /**
33
+ * Insert a tab entry into an existing tabs group at a specific index.
34
+ * Clamps the index into range. The inserted tab becomes active — the
35
+ * user's drag landed there, so they want to see it.
36
+ */
37
+ export declare function insertTabIntoTabs(tabsNode: TabsNode, entry: TabEntry, index: number): void;
38
+ /**
39
+ * Atomic same-group tab move. Used by the drag engine when the drop
40
+ * target is the tab strip of the same group the source tab lives in.
41
+ *
42
+ * Why atomic: the naive two-step flow (`removeTabBySlotId` then
43
+ * `insertTabIntoTabs`) calls `Array.prototype.splice` twice against the
44
+ * same reactive `$state` array. Splice's internal `[[Delete]]`
45
+ * operations trip Svelte's proxy `deleteProperty` trap during the
46
+ * remove step, which can flush reactive consumers while `tabs.length`
47
+ * is transiently `N - 1`. A LayoutRenderer `$derived` evaluated in that
48
+ * window sees the shrunk array and the index-`N - 1` snippet body
49
+ * dereferences a now-`undefined` entry — crashing at `.slotId`. Going
50
+ * through a fresh non-reactive array and then reassigning `tabs` in
51
+ * one shot means the proxy only ever sees a single atomic transition
52
+ * from the old array to the new one.
53
+ *
54
+ * `postRemovalToIndex` is in post-removal coordinates, which matches
55
+ * the coordinate space drop targets use (see LayoutRenderer's
56
+ * `onStripHover`, which normalizes same-strip indices before setting
57
+ * the drop target).
58
+ */
59
+ export declare function moveTabWithinTabs(tabsNode: TabsNode, fromIndex: number, postRemovalToIndex: number): void;
60
+ /** Which side of a split the dropped tab should land on. */
61
+ export type SplitSide = 'left' | 'right' | 'top' | 'bottom';
62
+ /**
63
+ * Split a slot-leaf (or tabs-leaf) into a new SplitNode, placing a
64
+ * single-tab group containing `entry` on the requested side and the
65
+ * existing node on the other side. The existing node is preserved
66
+ * as-is (it keeps its slotId, viewId, etc. — critical for re-parenting
67
+ * of the untouched side).
68
+ *
69
+ * Sides map to split direction:
70
+ * left/right → horizontal split, new tab goes left or right
71
+ * top/bottom → vertical split, new tab goes top or bottom
72
+ *
73
+ * The target is identified by a LayoutPath from the root, because
74
+ * splitting requires rewriting the parent's child array and we need
75
+ * the parent reference. If the target IS the root, the root itself is
76
+ * replaced — callers that pass the root by reference need the wrapper
77
+ * form, so this function returns the new subtree and the caller
78
+ * reassigns parent.children[i] = returned value (or reassigns root).
79
+ */
80
+ export declare function makeSplitWithNewTab(existing: LayoutNode, entry: TabEntry, side: SplitSide): SplitNode;
81
+ /**
82
+ * Apply a slot-split as a tree mutation: find the target node at the
83
+ * given path and replace it with a new split. Handles the root case
84
+ * (path = []) by mutating the root's fields in place. The root object
85
+ * identity is preserved so Svelte's $state proxy keeps its reactivity.
86
+ */
87
+ export declare function splitNodeAtPath(root: LayoutNode, path: LayoutPath, entry: TabEntry, side: SplitSide): void;
88
+ /** Walk a LayoutPath and return the node at its tip, or null. */
89
+ export declare function nodeAtPath(root: LayoutNode, path: LayoutPath): LayoutNode | null;
90
+ /**
91
+ * Post-mutation cleanup pass. Removes empty tabs groups from their
92
+ * parents and collapses single-child splits. Runs until the tree
93
+ * stabilizes (a collapse can reveal another single-child split one
94
+ * level up).
95
+ *
96
+ * Called once at the end of every drag commit. Called separately from
97
+ * the mutation primitives so callers can do "remove then insert into
98
+ * the same group" without the group being collapsed between calls.
99
+ */
100
+ export declare function cleanupTree(root: LayoutNode): void;
@@ -0,0 +1,310 @@
1
+ /*
2
+ * Layout ops — pure(-ish) mutations on a LayoutNode tree.
3
+ *
4
+ * All drag-reorganize gestures commit through this module. The drag
5
+ * engine figures out *what* the user meant (which tab, which target,
6
+ * which drop zone); the ops figure out *how* to edit the tree so the
7
+ * result is well-formed.
8
+ *
9
+ * Mutation style:
10
+ * Trees are mutated in place. The root is a Svelte `$state` object
11
+ * owned by the module-level `layoutStore` (see layout/store.svelte.ts),
12
+ * so in-place edits are observed by the layout renderer automatically.
13
+ * Since phase 7, that store is the workspace state-zone proxy, so each
14
+ * mutation also triggers a debounced flush to localStorage — no action
15
+ * required from op callers. Returning a new
16
+ * tree each time would also work but would force callers to reassign
17
+ * the root — messier and no upside here.
18
+ *
19
+ * Invariants the ops preserve:
20
+ * 1. No empty TabsNode. A tab group that loses its last tab is
21
+ * removed from its parent.
22
+ * 2. No single-child split. A split whose children collapse to one
23
+ * is replaced by that child (direction and proportion of the
24
+ * parent reassert themselves naturally).
25
+ * 3. Sizes array length matches children length. removeAt / insertAt
26
+ * splice the size array alongside the children array.
27
+ * 4. activeTab stays in range after tab removal/insertion. When the
28
+ * active tab itself moves, activeTab follows it.
29
+ *
30
+ * Not enforced (intentionally):
31
+ * - Uniqueness of slotIds across the tree. The slot host pool keys
32
+ * by slotId; duplicates would collide. The ops assume upstream
33
+ * doesn't produce duplicates; the drag engine only moves existing
34
+ * tabs, so no new slotIds are introduced.
35
+ */
36
+ /**
37
+ * Find a tab by its slotId anywhere in the tree. Returns null if no
38
+ * tab carries that slotId. Used by the drag engine to look up the
39
+ * source tab before any mutation.
40
+ */
41
+ export function findTabBySlotId(root, slotId) {
42
+ const walk = (node, path) => {
43
+ if (node.type === 'tabs') {
44
+ const idx = node.tabs.findIndex((t) => t.slotId === slotId);
45
+ if (idx >= 0) {
46
+ return { tabsNode: node, tabsPath: path, tabIndex: idx, entry: node.tabs[idx] };
47
+ }
48
+ }
49
+ else if (node.type === 'split') {
50
+ for (let i = 0; i < node.children.length; i++) {
51
+ const hit = walk(node.children[i], [...path, i]);
52
+ if (hit)
53
+ return hit;
54
+ }
55
+ }
56
+ return null;
57
+ };
58
+ return walk(root, []);
59
+ }
60
+ /** Find a standalone slot leaf by its slotId (only leaves, not tab entries). */
61
+ export function findSlotBySlotId(root, slotId) {
62
+ const walk = (node, parent, index) => {
63
+ if (node.type === 'slot') {
64
+ if (node.slotId === slotId)
65
+ return { node, parent, index };
66
+ return null;
67
+ }
68
+ if (node.type === 'split') {
69
+ for (let i = 0; i < node.children.length; i++) {
70
+ const hit = walk(node.children[i], node, i);
71
+ if (hit)
72
+ return hit;
73
+ }
74
+ }
75
+ return null;
76
+ };
77
+ return walk(root, null, 0);
78
+ }
79
+ // ---------- Tab removal ----------------------------------------------------
80
+ /**
81
+ * Remove a tab from its current location, returning the removed entry
82
+ * (or null if not found). The tabs group it was in may become empty
83
+ * and is cleaned up by `cleanupTree` after the caller has finished its
84
+ * reinsertion. The two-phase shape (mutate → cleanup) means the caller
85
+ * can remove a tab and re-insert it into the SAME tabs group without
86
+ * the group being collapsed mid-move.
87
+ */
88
+ export function removeTabBySlotId(root, slotId) {
89
+ const located = findTabBySlotId(root, slotId);
90
+ if (!located)
91
+ return null;
92
+ const { tabsNode, tabIndex, entry } = located;
93
+ tabsNode.tabs.splice(tabIndex, 1);
94
+ // Keep activeTab sensible: clamp, and shift down if we removed an
95
+ // earlier tab than the active one.
96
+ if (tabsNode.tabs.length === 0) {
97
+ tabsNode.activeTab = 0;
98
+ }
99
+ else if (tabIndex < tabsNode.activeTab) {
100
+ tabsNode.activeTab -= 1;
101
+ }
102
+ else if (tabsNode.activeTab >= tabsNode.tabs.length) {
103
+ tabsNode.activeTab = tabsNode.tabs.length - 1;
104
+ }
105
+ return entry;
106
+ }
107
+ // ---------- Tab insertion --------------------------------------------------
108
+ /**
109
+ * Insert a tab entry into an existing tabs group at a specific index.
110
+ * Clamps the index into range. The inserted tab becomes active — the
111
+ * user's drag landed there, so they want to see it.
112
+ */
113
+ export function insertTabIntoTabs(tabsNode, entry, index) {
114
+ const clamped = Math.max(0, Math.min(index, tabsNode.tabs.length));
115
+ tabsNode.tabs.splice(clamped, 0, entry);
116
+ tabsNode.activeTab = clamped;
117
+ }
118
+ /**
119
+ * Atomic same-group tab move. Used by the drag engine when the drop
120
+ * target is the tab strip of the same group the source tab lives in.
121
+ *
122
+ * Why atomic: the naive two-step flow (`removeTabBySlotId` then
123
+ * `insertTabIntoTabs`) calls `Array.prototype.splice` twice against the
124
+ * same reactive `$state` array. Splice's internal `[[Delete]]`
125
+ * operations trip Svelte's proxy `deleteProperty` trap during the
126
+ * remove step, which can flush reactive consumers while `tabs.length`
127
+ * is transiently `N - 1`. A LayoutRenderer `$derived` evaluated in that
128
+ * window sees the shrunk array and the index-`N - 1` snippet body
129
+ * dereferences a now-`undefined` entry — crashing at `.slotId`. Going
130
+ * through a fresh non-reactive array and then reassigning `tabs` in
131
+ * one shot means the proxy only ever sees a single atomic transition
132
+ * from the old array to the new one.
133
+ *
134
+ * `postRemovalToIndex` is in post-removal coordinates, which matches
135
+ * the coordinate space drop targets use (see LayoutRenderer's
136
+ * `onStripHover`, which normalizes same-strip indices before setting
137
+ * the drop target).
138
+ */
139
+ export function moveTabWithinTabs(tabsNode, fromIndex, postRemovalToIndex) {
140
+ const next = tabsNode.tabs.slice();
141
+ const [entry] = next.splice(fromIndex, 1);
142
+ const clamped = Math.max(0, Math.min(postRemovalToIndex, next.length));
143
+ next.splice(clamped, 0, entry);
144
+ tabsNode.tabs = next;
145
+ tabsNode.activeTab = clamped;
146
+ }
147
+ /**
148
+ * Split a slot-leaf (or tabs-leaf) into a new SplitNode, placing a
149
+ * single-tab group containing `entry` on the requested side and the
150
+ * existing node on the other side. The existing node is preserved
151
+ * as-is (it keeps its slotId, viewId, etc. — critical for re-parenting
152
+ * of the untouched side).
153
+ *
154
+ * Sides map to split direction:
155
+ * left/right → horizontal split, new tab goes left or right
156
+ * top/bottom → vertical split, new tab goes top or bottom
157
+ *
158
+ * The target is identified by a LayoutPath from the root, because
159
+ * splitting requires rewriting the parent's child array and we need
160
+ * the parent reference. If the target IS the root, the root itself is
161
+ * replaced — callers that pass the root by reference need the wrapper
162
+ * form, so this function returns the new subtree and the caller
163
+ * reassigns parent.children[i] = returned value (or reassigns root).
164
+ */
165
+ export function makeSplitWithNewTab(existing, entry, side) {
166
+ const direction = side === 'left' || side === 'right' ? 'horizontal' : 'vertical';
167
+ const newGroup = {
168
+ type: 'tabs',
169
+ activeTab: 0,
170
+ tabs: [entry],
171
+ };
172
+ const newFirst = side === 'left' || side === 'top';
173
+ const children = newFirst ? [newGroup, existing] : [existing, newGroup];
174
+ return {
175
+ type: 'split',
176
+ direction,
177
+ sizes: [1, 1], // 50/50 by default; splitter drag adjusts from there
178
+ children,
179
+ };
180
+ }
181
+ /**
182
+ * Apply a slot-split as a tree mutation: find the target node at the
183
+ * given path and replace it with a new split. Handles the root case
184
+ * (path = []) by mutating the root's fields in place. The root object
185
+ * identity is preserved so Svelte's $state proxy keeps its reactivity.
186
+ */
187
+ export function splitNodeAtPath(root, path, entry, side) {
188
+ const target = nodeAtPath(root, path);
189
+ if (!target)
190
+ return;
191
+ const replacement = makeSplitWithNewTab(target, entry, side);
192
+ if (path.length === 0) {
193
+ // Replace root contents in place. The root is a discriminated
194
+ // union; to rewrite it we cast once through `unknown` and then to
195
+ // a loose record shape, overwrite fields, and clear stale keys
196
+ // from the previous node kind so the union stays well-formed.
197
+ const rootAsRecord = root;
198
+ // Clear stale keys first so Object.assign doesn't leave a hybrid.
199
+ delete rootAsRecord.tabs;
200
+ delete rootAsRecord.activeTab;
201
+ delete rootAsRecord.slotId;
202
+ delete rootAsRecord.viewId;
203
+ delete rootAsRecord.direction;
204
+ delete rootAsRecord.sizes;
205
+ delete rootAsRecord.pinned;
206
+ delete rootAsRecord.children;
207
+ Object.assign(rootAsRecord, replacement);
208
+ return;
209
+ }
210
+ const parentPath = path.slice(0, -1);
211
+ const indexInParent = path[path.length - 1];
212
+ const parent = nodeAtPath(root, parentPath);
213
+ if (!parent || parent.type !== 'split')
214
+ return;
215
+ parent.children[indexInParent] = replacement;
216
+ }
217
+ /** Walk a LayoutPath and return the node at its tip, or null. */
218
+ export function nodeAtPath(root, path) {
219
+ let cur = root;
220
+ for (const idx of path) {
221
+ if (cur.type !== 'split')
222
+ return null;
223
+ if (idx < 0 || idx >= cur.children.length)
224
+ return null;
225
+ cur = cur.children[idx];
226
+ }
227
+ return cur;
228
+ }
229
+ // ---------- Cleanup --------------------------------------------------------
230
+ /**
231
+ * Post-mutation cleanup pass. Removes empty tabs groups from their
232
+ * parents and collapses single-child splits. Runs until the tree
233
+ * stabilizes (a collapse can reveal another single-child split one
234
+ * level up).
235
+ *
236
+ * Called once at the end of every drag commit. Called separately from
237
+ * the mutation primitives so callers can do "remove then insert into
238
+ * the same group" without the group being collapsed between calls.
239
+ */
240
+ export function cleanupTree(root) {
241
+ let changed = true;
242
+ while (changed) {
243
+ changed = cleanupPass(root, null, 0);
244
+ }
245
+ }
246
+ function cleanupPass(node, parent, indexInParent) {
247
+ if (node.type === 'split') {
248
+ // Recurse first so we collapse bottom-up.
249
+ let recursed = false;
250
+ for (let i = 0; i < node.children.length; i++) {
251
+ if (cleanupPass(node.children[i], node, i))
252
+ recursed = true;
253
+ }
254
+ // Drop empty tabs children — unless the tabs node is persistent.
255
+ for (let i = node.children.length - 1; i >= 0; i--) {
256
+ const child = node.children[i];
257
+ if (child.type === 'tabs' && child.tabs.length === 0 && !child.persistent) {
258
+ node.children.splice(i, 1);
259
+ node.sizes.splice(i, 1);
260
+ if (node.pinned)
261
+ node.pinned.splice(i, 1);
262
+ if (node.collapsed)
263
+ node.collapsed.splice(i, 1);
264
+ recursed = true;
265
+ }
266
+ }
267
+ // Collapse a split that has one (or zero) children left. With zero
268
+ // children the split is meaningless; with one child, the split is
269
+ // redundant and the surviving child should take its place.
270
+ if (node.children.length <= 1 && parent) {
271
+ if (node.children.length === 1) {
272
+ parent.children[indexInParent] = node.children[0];
273
+ }
274
+ else {
275
+ // Zero children — shouldn't normally happen, but remove to keep
276
+ // the tree well-formed.
277
+ parent.children.splice(indexInParent, 1);
278
+ parent.sizes.splice(indexInParent, 1);
279
+ if (parent.pinned)
280
+ parent.pinned.splice(indexInParent, 1);
281
+ if (parent.collapsed)
282
+ parent.collapsed.splice(indexInParent, 1);
283
+ }
284
+ return true;
285
+ }
286
+ // Root-level split with one child: promote the surviving child to
287
+ // root by overwriting the root's fields in place (same technique
288
+ // as splitNodeAtPath's root case — preserves $state proxy identity).
289
+ if (node.children.length === 1 && !parent) {
290
+ const survivor = node.children[0];
291
+ const rootAsRecord = node;
292
+ // Clear all split-specific keys.
293
+ delete rootAsRecord.direction;
294
+ delete rootAsRecord.sizes;
295
+ delete rootAsRecord.pinned;
296
+ delete rootAsRecord.collapsed;
297
+ delete rootAsRecord.children;
298
+ Object.assign(rootAsRecord, survivor);
299
+ return true;
300
+ }
301
+ return recursed;
302
+ }
303
+ if (node.type === 'tabs') {
304
+ // Empty tabs at the root level has no parent to drop it from; the
305
+ // caller is expected to handle the "layout is empty" case
306
+ // elsewhere (phase 7). We do nothing here.
307
+ return false;
308
+ }
309
+ return false;
310
+ }
@@ -0,0 +1,36 @@
1
+ import type { ViewHandle } from '../shards/types';
2
+ /**
3
+ * Acquire (or create) the pooled host for a slot. The caller is
4
+ * expected to `appendChild` the returned host into its own wrapper —
5
+ * the pool does not know which wrapper owns the host at any given time,
6
+ * and that is intentional. The same host may be passed around.
7
+ */
8
+ export declare function acquireSlotHost(slotId: string, viewId: string | null, label: string): HTMLDivElement;
9
+ /**
10
+ * Release the pooled host. If this was the last reference, a
11
+ * destruction is queued to run in a microtask; a later acquire before
12
+ * that microtask cancels the destroy (the re-parent case).
13
+ */
14
+ export declare function releaseSlotHost(slotId: string): void;
15
+ /**
16
+ * Test / teardown helper — destroys every pooled host immediately. Used
17
+ * by HMR boundaries and tests; not part of normal runtime flow.
18
+ */
19
+ export declare function resetSlotHostPool(): void;
20
+ /**
21
+ * Read the current ViewHandle for a slot. Returns undefined if the slot
22
+ * is not in the pool or hasn't finished mounting yet. Used by the close
23
+ * protocol to check closable and call canClose().
24
+ */
25
+ export declare function getSlotHandle(slotId: string): ViewHandle | undefined;
26
+ /**
27
+ * Read the dirty state for a slot. Returns false if the slot is not in
28
+ * the pool. Used by the tab strip to render the dirty indicator.
29
+ */
30
+ export declare function isSlotDirty(slotId: string): boolean;
31
+ /**
32
+ * Read the closable state for a slot. Returns false if the slot is not
33
+ * in the pool or hasn't finished mounting yet. Reactive — Svelte will
34
+ * re-render when the deferred mount sets the flag.
35
+ */
36
+ export declare function isSlotClosable(slotId: string): boolean;
@@ -0,0 +1,229 @@
1
+ /*
2
+ * Slot host pool — the re-parenting contract, made operational.
3
+ *
4
+ * docs/design/layout.md:70 — "the layout engine moves the existing
5
+ * container element rather than destroying and recreating it". This
6
+ * module is where that happens. Views are mounted into detached
7
+ * `<div.slot-host>` elements owned by the pool, keyed by slotId, and
8
+ * survive across arbitrary Svelte remounts of their containing
9
+ * SlotContainer.
10
+ *
11
+ * Why a pool, not per-component state:
12
+ * When a user drags a tab from one TabbedPanel to another, the slot's
13
+ * SlotContainer leaves one Svelte subtree and reappears in another.
14
+ * Svelte will tear down the old component instance and mount a new one
15
+ * — even though the slot logically hasn't changed. If the view lives
16
+ * inside that component's own DOM, it dies with it. Pooling the host
17
+ * outside the Svelte tree and letting each SlotContainer instance
18
+ * merely *attach* the pooled host to its wrapper decouples the view's
19
+ * lifetime from the component's lifetime.
20
+ *
21
+ * Refcount + deferred destroy:
22
+ * A tab-move produces a teardown/mount pair in the same microtask.
23
+ * Naive lifecycle (destroy on refcount 0) would destroy the view
24
+ * between those two events. Instead, a 0-refcount schedules a
25
+ * destroy in a microtask; if someone re-acquires before the microtask
26
+ * runs, the destroy is cancelled. Result: in-tick moves survive,
27
+ * genuine removals (tab closed, layout discards the slotId) still
28
+ * tear the view down promptly.
29
+ *
30
+ * Opt-out:
31
+ * ViewHandle.remountOnMove (unused in phase 6) is the GL/Safari
32
+ * edge-case escape hatch reserved by the design. Not wired yet —
33
+ * phase 6 has no view that needs it.
34
+ */
35
+ import { getView } from '../shards/registry';
36
+ const pool = new Map();
37
+ const pendingDestroy = new Set();
38
+ /**
39
+ * Reactive dirty-state map. Keyed by slotId, values are $state so
40
+ * Svelte tracks reads in `isSlotDirty()` and re-renders the tab strip
41
+ * when a shard calls `setDirty()`. Separate from PooledHost because
42
+ * the pool's plain Map is not reactive.
43
+ */
44
+ const dirtyState = $state({});
45
+ /**
46
+ * Reactive closable-state map. Same pattern as dirtyState — the pool's
47
+ * plain Map is not reactive, so closable flags derived from ViewHandle
48
+ * would never trigger a re-render. This record is $state so Svelte
49
+ * tracks reads in `isSlotClosable()` and re-renders the tab strip once
50
+ * the deferred mount completes and sets the flag.
51
+ */
52
+ const closableState = $state({});
53
+ /*
54
+ * Detaching the view mount from the caller's effect scope.
55
+ *
56
+ * `acquireSlotHost` is called from inside SlotContainer's `$effect`.
57
+ * Svelte 5 attributes any reactive subscriptions created during that
58
+ * call to the currently-active effect — and `$effect.root` is NOT
59
+ * sufficient to escape this, because `active_effect` is still set
60
+ * when the root scope runs synchronously inline. When the caller's
61
+ * effect is later torn down (because SlotContainer remounts under a
62
+ * new branch of the layout tree), those subscriptions are severed.
63
+ * The pool keeps the view's DOM alive across the remount, but its
64
+ * `$derived`s and `$effect`s stop firing — the exact symptom we hit.
65
+ *
66
+ * Fix: return the host element synchronously, but defer the
67
+ * `factory.mount(host)` call to a `queueMicrotask`. Microtasks run
68
+ * with a clean execution stack, so `active_effect` is null when mount
69
+ * runs — the view's reactive graph is a true top-level root with no
70
+ * ancestor effect to be destroyed by. The one-microtask delay before
71
+ * the view appears is invisible to the user: microtasks run before
72
+ * the browser paints, so the initial frame still shows the view.
73
+ *
74
+ * A `cancelled` flag guards against the edge case where the entry
75
+ * is destroyed before its deferred mount ever runs (e.g. rapid
76
+ * add-then-remove of a slot during a drag).
77
+ */
78
+ function createHost(slotId, viewId, label) {
79
+ const host = document.createElement('div');
80
+ host.className = 'slot-host';
81
+ host.dataset.slotId = slotId;
82
+ // Position:absolute inset:0 so the host fills whichever wrapper it is
83
+ // attached to. The wrapper is what the layout engine sizes; the host
84
+ // just tracks it. Styles are set inline (not in a class) so consumers
85
+ // don't need to import a stylesheet to get correct layout.
86
+ host.style.position = 'absolute';
87
+ host.style.inset = '0';
88
+ host.style.minWidth = '0';
89
+ host.style.minHeight = '0';
90
+ let cancelled = false;
91
+ const entry = {
92
+ host,
93
+ handle: undefined,
94
+ viewId,
95
+ label,
96
+ refcount: 0,
97
+ resizeObserver: undefined,
98
+ cancelPendingMount: () => {
99
+ cancelled = true;
100
+ },
101
+ };
102
+ queueMicrotask(() => {
103
+ var _a, _b;
104
+ if (cancelled)
105
+ return;
106
+ const factory = viewId ? getView(viewId) : undefined;
107
+ const ctx = {
108
+ slotId,
109
+ viewId: viewId !== null && viewId !== void 0 ? viewId : '',
110
+ label,
111
+ setDirty(dirty) {
112
+ dirtyState[slotId] = dirty;
113
+ },
114
+ };
115
+ entry.handle = factory === null || factory === void 0 ? void 0 : factory.mount(host, ctx);
116
+ if ((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) {
117
+ closableState[slotId] = true;
118
+ }
119
+ // The pool owns the ResizeObserver so its lifetime matches the
120
+ // view handle's lifetime, not the containing SlotContainer's.
121
+ // Moving the host between wrappers (drag-reorganize) keeps the
122
+ // same observer, and the view keeps receiving size updates
123
+ // through the move.
124
+ if ((_b = entry.handle) === null || _b === void 0 ? void 0 : _b.onResize) {
125
+ const onResize = entry.handle.onResize.bind(entry.handle);
126
+ entry.resizeObserver = new ResizeObserver((entries) => {
127
+ for (const e of entries) {
128
+ const box = e.contentRect;
129
+ onResize(Math.round(box.width), Math.round(box.height));
130
+ }
131
+ });
132
+ entry.resizeObserver.observe(host);
133
+ }
134
+ });
135
+ return entry;
136
+ }
137
+ /**
138
+ * Acquire (or create) the pooled host for a slot. The caller is
139
+ * expected to `appendChild` the returned host into its own wrapper —
140
+ * the pool does not know which wrapper owns the host at any given time,
141
+ * and that is intentional. The same host may be passed around.
142
+ */
143
+ export function acquireSlotHost(slotId, viewId, label) {
144
+ // If the slot was about to be destroyed, cancel — this acquire is the
145
+ // "other half" of a re-parent (teardown was the previous container).
146
+ pendingDestroy.delete(slotId);
147
+ let entry = pool.get(slotId);
148
+ if (!entry) {
149
+ entry = createHost(slotId, viewId, label);
150
+ pool.set(slotId, entry);
151
+ }
152
+ entry.refcount++;
153
+ return entry.host;
154
+ }
155
+ /**
156
+ * Release the pooled host. If this was the last reference, a
157
+ * destruction is queued to run in a microtask; a later acquire before
158
+ * that microtask cancels the destroy (the re-parent case).
159
+ */
160
+ export function releaseSlotHost(slotId) {
161
+ const entry = pool.get(slotId);
162
+ if (!entry)
163
+ return;
164
+ entry.refcount--;
165
+ if (entry.refcount > 0)
166
+ return;
167
+ pendingDestroy.add(slotId);
168
+ queueMicrotask(() => {
169
+ var _a, _b;
170
+ if (!pendingDestroy.has(slotId))
171
+ return;
172
+ pendingDestroy.delete(slotId);
173
+ const current = pool.get(slotId);
174
+ if (!current || current.refcount > 0)
175
+ return; // re-acquired, keep
176
+ (_a = current.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
177
+ (_b = current.handle) === null || _b === void 0 ? void 0 : _b.unmount();
178
+ current.cancelPendingMount();
179
+ current.host.remove();
180
+ pool.delete(slotId);
181
+ delete dirtyState[slotId];
182
+ delete closableState[slotId];
183
+ });
184
+ }
185
+ /**
186
+ * Test / teardown helper — destroys every pooled host immediately. Used
187
+ * by HMR boundaries and tests; not part of normal runtime flow.
188
+ */
189
+ export function resetSlotHostPool() {
190
+ var _a, _b;
191
+ pendingDestroy.clear();
192
+ for (const entry of pool.values()) {
193
+ (_a = entry.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
194
+ (_b = entry.handle) === null || _b === void 0 ? void 0 : _b.unmount();
195
+ entry.cancelPendingMount();
196
+ entry.host.remove();
197
+ }
198
+ pool.clear();
199
+ for (const key of Object.keys(dirtyState))
200
+ delete dirtyState[key];
201
+ for (const key of Object.keys(closableState))
202
+ delete closableState[key];
203
+ }
204
+ /**
205
+ * Read the current ViewHandle for a slot. Returns undefined if the slot
206
+ * is not in the pool or hasn't finished mounting yet. Used by the close
207
+ * protocol to check closable and call canClose().
208
+ */
209
+ export function getSlotHandle(slotId) {
210
+ var _a;
211
+ return (_a = pool.get(slotId)) === null || _a === void 0 ? void 0 : _a.handle;
212
+ }
213
+ /**
214
+ * Read the dirty state for a slot. Returns false if the slot is not in
215
+ * the pool. Used by the tab strip to render the dirty indicator.
216
+ */
217
+ export function isSlotDirty(slotId) {
218
+ var _a;
219
+ return (_a = dirtyState[slotId]) !== null && _a !== void 0 ? _a : false;
220
+ }
221
+ /**
222
+ * Read the closable state for a slot. Returns false if the slot is not
223
+ * in the pool or hasn't finished mounting yet. Reactive — Svelte will
224
+ * re-render when the deferred mount sets the flag.
225
+ */
226
+ export function isSlotClosable(slotId) {
227
+ var _a;
228
+ return (_a = closableState[slotId]) !== null && _a !== void 0 ? _a : false;
229
+ }