@jxsuite/studio 5.0.0 → 5.2.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 (132) hide show
  1. package/dist/chunks/{cf-account-picker-fyf63pmy.js → cf-account-picker-h4dab9bq.js} +10 -10
  2. package/dist/chunks/{cf-settings-71bv5zn9.js → cf-settings-adg74qsk.js} +4 -4
  3. package/dist/chunks/components-5y78e05d.js +20 -0
  4. package/dist/chunks/content-source-sgg1sv6n.js +46 -0
  5. package/dist/chunks/{convert-to-repeater-d0cgnb6d.js → convert-to-repeater-mf1cxgfn.js} +25 -25
  6. package/dist/chunks/entry-commands-w9qsxa6q.js +42 -0
  7. package/dist/chunks/{extension-sections-fy7q4fe9.js → extension-sections-c6zwzbre.js} +27 -27
  8. package/dist/chunks/file-ops-vrpq1jjk.js +47 -0
  9. package/dist/chunks/{files-ef8fgs3w.js → files-msaf95y6.js} +33 -32
  10. package/dist/chunks/{format-host-3c33nwr8.js → format-host-f64fnesp.js} +2 -2
  11. package/dist/chunks/{fs-events-13tj6sex.js → fs-events-3y6y0nrm.js} +6 -6
  12. package/dist/chunks/git-panel-bv5e22j0.js +50 -0
  13. package/dist/chunks/grid-view-1p6hsyjx.css +8406 -0
  14. package/dist/chunks/grid-view-paj67nr5.js +15322 -0
  15. package/dist/chunks/{iframe-host-jgesd924.js → iframe-host-dh5np8vk.js} +14 -14
  16. package/dist/chunks/{layers-1nq5evsh.js → layers-1vcfdbet.js} +7 -6
  17. package/dist/chunks/layers-panel-fksbdg4q.js +40 -0
  18. package/dist/chunks/{media-picker-2qbetq73.js → media-picker-p49nhqhs.js} +14 -14
  19. package/dist/chunks/menu-qht48vnc.js +15 -0
  20. package/dist/chunks/{monaco-buffer-bhy3hb6m.js → monaco-buffer-pykwbm92.js} +3 -3
  21. package/dist/chunks/{notify-628t0anh.js → notify-42gk8ha1.js} +2 -2
  22. package/dist/chunks/{parse-0zx14y15.js → parse-reg9eevt.js} +2 -2
  23. package/dist/chunks/publish-panel-vz23wwmg.js +27 -0
  24. package/dist/chunks/quick-search-e9rp74m8.js +46 -0
  25. package/dist/chunks/{references-n553f0e1.js → references-h69xp9zz.js} +4 -4
  26. package/dist/chunks/right-panel-j97t2yvy.js +45 -0
  27. package/dist/chunks/{studio-18avnhjp.js → studio-01jeck2y.js} +36 -30
  28. package/dist/chunks/{studio-9xxz1q69.js → studio-14d9tq4d.js} +9 -9
  29. package/dist/chunks/{studio-xsykpvsh.js → studio-15f3kxqe.js} +19 -19
  30. package/dist/chunks/studio-1tbjfqv4.js +146 -0
  31. package/dist/chunks/{studio-79y5sz51.js → studio-28vs5k2y.js} +2 -2
  32. package/dist/chunks/{studio-3hxkd92t.js → studio-2mdbbc76.js} +10 -10
  33. package/dist/chunks/{studio-7r42c6ca.js → studio-3ntvq6cg.js} +5 -5
  34. package/dist/chunks/{studio-8jn5yhyn.js → studio-4c041r8y.js} +3 -3
  35. package/dist/chunks/{studio-qahxvnan.js → studio-4r14yzyy.js} +4 -4
  36. package/dist/chunks/{studio-arfpe389.js → studio-4xmjcrg3.js} +6 -6
  37. package/dist/chunks/{studio-5523bpzt.js → studio-5nz00bhh.js} +3 -3
  38. package/dist/chunks/{studio-x3rty6tr.js → studio-5t7ena5z.js} +8 -8
  39. package/dist/chunks/{studio-5rt7sqa0.js → studio-66arbpc2.js} +13 -19
  40. package/dist/chunks/{studio-fabxk6r9.js → studio-68fydsrj.js} +15 -15
  41. package/dist/chunks/{studio-tb3ev01j.js → studio-9j6sfeba.js} +6 -6
  42. package/dist/chunks/{studio-akbww0j5.js → studio-a1pswps3.js} +38 -38
  43. package/dist/chunks/{studio-6xa21ej9.js → studio-c04nzbby.js} +2 -2
  44. package/dist/chunks/{studio-4m0haseg.js → studio-c07s5ckd.js} +4 -4
  45. package/dist/chunks/{studio-57mf66w2.js → studio-ctm7bhx5.js} +14 -14
  46. package/dist/chunks/{studio-p1dw8wfw.js → studio-d8z5bajd.js} +14 -10
  47. package/dist/chunks/{studio-vz68yn9e.js → studio-en82c639.js} +6 -6
  48. package/dist/chunks/{studio-rkstz940.js → studio-etvx9s2g.js} +21 -12
  49. package/dist/chunks/{studio-az3zd49t.js → studio-ez11dmdh.js} +127 -37
  50. package/dist/chunks/{studio-sgdc5pfc.js → studio-hdc2jt3z.js} +4 -4
  51. package/dist/chunks/{studio-s6z853aq.js → studio-hhskq2mr.js} +24 -23
  52. package/dist/chunks/{studio-6ewqv390.js → studio-m1p9b6c3.js} +5 -5
  53. package/dist/chunks/{studio-25hkdrkw.js → studio-mpzwze9x.js} +12 -12
  54. package/dist/chunks/{studio-8qtrpzr3.js → studio-ncn9e189.js} +7 -7
  55. package/dist/chunks/{studio-81390n6f.js → studio-nq5yf5mp.js} +1 -1
  56. package/dist/chunks/{studio-5jy81t3s.js → studio-p6pmwkvh.js} +229 -24
  57. package/dist/chunks/{studio-3gxv60tn.js → studio-qa7fzxan.js} +4 -4
  58. package/dist/chunks/{studio-7xptrg0j.js → studio-sar8g54p.js} +8 -8
  59. package/dist/chunks/studio-t171hxhe.js +23 -0
  60. package/dist/chunks/{studio-em9nw8eh.js → studio-t3adrr56.js} +20 -17
  61. package/dist/chunks/{studio-hhvs4sdg.js → studio-vn5hske5.js} +10 -4
  62. package/dist/chunks/{studio-pm6hgy7r.js → studio-vt0tj32j.js} +29 -42
  63. package/dist/chunks/{studio-d1ja2x9r.js → studio-wjpgxywa.js} +4 -4
  64. package/dist/chunks/{studio-q3x06vmj.js → studio-wnsyffg1.js} +2 -2
  65. package/dist/chunks/{studio-rpyah2ew.js → studio-x10fp4tp.js} +64 -24
  66. package/dist/chunks/{studio-26d7myd1.js → studio-x72j58vw.js} +4 -4
  67. package/dist/chunks/{studio-cpvb2ed1.js → studio-y4f9fdge.js} +8 -8
  68. package/dist/chunks/{studio-erdatzvp.js → studio-ympe0n7a.js} +35 -6
  69. package/dist/chunks/{studio-y9zedyjn.js → studio-z2smx5ce.js} +2 -2
  70. package/dist/chunks/studio-zjjde71a.js +148 -0
  71. package/dist/chunks/{workspace-wrew8zk4.js → workspace-bnw54cha.js} +18 -10
  72. package/dist/iframe-entry.js +102 -11
  73. package/dist/studio.css +1 -2
  74. package/dist/studio.js +1113 -16062
  75. package/package.json +11 -11
  76. package/src/canvas/canvas-utils.ts +29 -2
  77. package/src/commands/command-args.ts +37 -2
  78. package/src/commands/registry.ts +18 -8
  79. package/src/commands/run-reported.ts +120 -0
  80. package/src/editor/shortcuts.ts +11 -23
  81. package/src/files/components.ts +39 -0
  82. package/src/files/files.ts +41 -17
  83. package/src/grid/grid-lazy.ts +59 -0
  84. package/src/grid/grid-open.ts +20 -3
  85. package/src/grid/grid-panel.ts +43 -9
  86. package/src/i18n/i18n-commands.ts +10 -0
  87. package/src/media/media-open.ts +14 -2
  88. package/src/packages/pull-package-sync.ts +5 -1
  89. package/src/panels/ai-panel.ts +5 -2
  90. package/src/panels/block-action-bar.ts +7 -2
  91. package/src/panels/i18n-panel.ts +8 -15
  92. package/src/panels/jump-bar.ts +3 -2
  93. package/src/panels/pane-context.ts +5 -4
  94. package/src/panels/pane-grid.ts +100 -10
  95. package/src/panels/problems-panel.ts +6 -2
  96. package/src/panels/quick-search.ts +6 -12
  97. package/src/panels/settings-menu.ts +5 -15
  98. package/src/panels/tab-drop.ts +220 -0
  99. package/src/panels/tab-strip.ts +238 -33
  100. package/src/services/ai-command-tools.ts +78 -8
  101. package/src/services/ai-models.ts +13 -0
  102. package/src/services/ai-project-tools.ts +8 -2
  103. package/src/services/ai-system-prompt.ts +4 -8
  104. package/src/services/ai-tools.ts +8 -69
  105. package/src/services/document-assistant.ts +9 -6
  106. package/src/studio.ts +24 -8
  107. package/src/surfaces/commandbar.json +35 -24
  108. package/src/surfaces/commandbar.ts +6 -5
  109. package/src/surfaces/pane-grid.json +35 -3
  110. package/src/surfaces/pane-grid.ts +26 -4
  111. package/src/surfaces/statusbar.ts +2 -1
  112. package/src/surfaces/tab-strip.json +16 -39
  113. package/src/surfaces/tab-strip.ts +76 -12
  114. package/src/tabs/project-config.ts +45 -11
  115. package/src/ui/ai-credentials-form.ts +12 -1
  116. package/src/ui/layers.ts +4 -1
  117. package/src/workspace/pane-derive.ts +7 -5
  118. package/src/workspace/session.ts +3 -3
  119. package/src/workspace/workspace.ts +220 -43
  120. package/styles/shell-frame.css +8 -0
  121. package/styles/shell-frame.json +4 -0
  122. package/dist/chunks/components-2w1f35qs.js +0 -18
  123. package/dist/chunks/content-source-3wqeas45.js +0 -46
  124. package/dist/chunks/entry-commands-d6g50j7s.js +0 -42
  125. package/dist/chunks/file-ops-3btj6st0.js +0 -47
  126. package/dist/chunks/git-panel-4wp2rn9b.js +0 -49
  127. package/dist/chunks/layers-panel-ar2gh90g.js +0 -40
  128. package/dist/chunks/menu-rf7n72mf.js +0 -14
  129. package/dist/chunks/publish-panel-98f7sr3g.js +0 -27
  130. package/dist/chunks/quick-search-srzz83k2.js +0 -45
  131. package/dist/chunks/right-panel-rrgx2dn3.js +0 -45
  132. package/dist/chunks/studio-d055c37e.js +0 -62
@@ -36,10 +36,9 @@
36
36
  * @docs studio/projects/settings
37
37
  */
38
38
 
39
- import { errorMessage } from "@jxsuite/schema/parse";
40
39
  import { activeRegistry } from "../commands/active-registry";
40
+ import { runReported } from "../commands/run-reported";
41
41
  import { LEVELS } from "../commands/levels";
42
- import { notify } from "../services/notify";
43
42
  import { onSettingsDocumentChanged, sortedSettingsSections } from "../settings/section-registry";
44
43
  import { PREFERENCES_SECTIONS } from "../settings/preferences-sections";
45
44
  import { openMenu } from "../surfaces/menu";
@@ -92,25 +91,16 @@ function menuSectionsFor(id: string): readonly MenuSection[] {
92
91
  * Run a row's command, surfacing a refusal instead of stranding it.
93
92
  *
94
93
  * `settings.open`'s `run` is async and throws a `RangeError` AFTER awaiting the contributed-section
95
- * sync, and `registry.run` does not catch. `editor/context-menu.ts`'s bare `void result` is safe
96
- * only because every element verb is synchronous; here an unknown section would reject into
97
- * nothing.
94
+ * sync, and `registry.run` does not catch; a gate that turned false between the render and the
95
+ * click throws BEFORE `run`, synchronously. `commands/run-reported.ts` catches both and files the
96
+ * sentence in Problems, under this menu's name.
98
97
  */
99
98
  function runRow(id: string, args?: Record<string, unknown>): void {
100
99
  const registry = activeRegistry();
101
100
  if (!registry) {
102
101
  return;
103
102
  }
104
- let result: unknown;
105
- try {
106
- result = registry.run(id, args as never);
107
- } catch (error) {
108
- notify.error(errorMessage(error), { source: id });
109
- return;
110
- }
111
- void Promise.resolve(result).catch((error: unknown) => {
112
- notify.error(errorMessage(error), { source: id });
113
- });
103
+ void runReported(registry, id, args, "Settings");
114
104
  }
115
105
 
116
106
  // ─── Rows ─────────────────────────────────────────────────────────────────────
@@ -0,0 +1,220 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Resolve a completed pragmatic drag over a tab strip or a pane's right edge into a model write.
4
+ *
5
+ * `panels/tab-strip.ts` registers every chip as a source and every strip and chip as a target,
6
+ * `panels/pane-grid.ts` registers the right-edge zone, and both hand their monitor's `onDrop` here
7
+ * with nothing decided yet — the DATA a target's `getData` produced, and nothing else. This module
8
+ * is the one place that DATA becomes a write, which is what keeps the two registration sites (a
9
+ * strip, a grid cell) from having to agree twice about what a drop means.
10
+ *
11
+ * No import of `tab-strip.ts` or `pane-grid.ts`: this module reaches the model through
12
+ * `workspace/workspace.ts` and `workspace/pane-derive.ts`, and the one opener through
13
+ * `files/files.ts` — the same three a keyboard command would use, because a drag and `⌘\` / "Open
14
+ * to the Side" are the same act by a different gesture (studio-ui-guidelines.md §8.2).
15
+ *
16
+ * @docs studio/interface/tabs
17
+ */
18
+
19
+ import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
20
+ import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index";
21
+ import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
22
+ import {
23
+ activateTab,
24
+ closePane,
25
+ moveTab,
26
+ moveTabToPane,
27
+ paneById,
28
+ paneIsEmpty,
29
+ promoteTab,
30
+ receivingPane,
31
+ splitRight,
32
+ workspace,
33
+ } from "../workspace/workspace";
34
+ import { clearPaneDerivation } from "../workspace/pane-derive";
35
+ import { openFileInTab } from "../files/files";
36
+
37
+ /** A tab chip dragged off its own strip. */
38
+ export interface TabDragData extends Record<string, unknown> {
39
+ type: "tab";
40
+ tabId: string;
41
+ /**
42
+ * The pane the chip is being dragged FROM, read at drag start — a host can change which pane it
43
+ * draws, so this is never captured earlier than the gesture itself.
44
+ */
45
+ paneId?: string;
46
+ }
47
+
48
+ /** A file row dragged off the Files tree — `files.ts`'s own source shape, read structurally. */
49
+ export interface FileDragData extends Record<string, unknown> {
50
+ type: "file-tree";
51
+ entryType: string;
52
+ path: string;
53
+ }
54
+
55
+ export type TabDropSourceData = TabDragData | FileDragData;
56
+
57
+ /** A slot in a strip — a chip's edge, or the strip's own empty tail when `tabId` is absent. */
58
+ export interface TabSlotTarget extends Record<string, unknown> {
59
+ type: "tab-slot";
60
+ paneId: string;
61
+ tabId?: string;
62
+ /** The target's own slot in `paneId`'s order at `getData` time — the tail's length, otherwise. */
63
+ index: number;
64
+ }
65
+
66
+ export interface TabSlotRefusedTarget extends Record<string, unknown> {
67
+ type: "tab-slot-refused";
68
+ }
69
+
70
+ /** The right band of the last pane's stage, armed only while the grid is under {@link MAX_PANES}. */
71
+ export interface PaneEdgeTarget extends Record<string, unknown> {
72
+ type: "pane-edge";
73
+ paneId: string;
74
+ }
75
+
76
+ export interface PaneEdgeRefusedTarget extends Record<string, unknown> {
77
+ type: "pane-edge-refused";
78
+ }
79
+
80
+ export type TabDropTarget =
81
+ | TabSlotTarget
82
+ | TabSlotRefusedTarget
83
+ | PaneEdgeTarget
84
+ | PaneEdgeRefusedTarget;
85
+
86
+ /**
87
+ * Whether `data` — a drag source's own data, exactly as `getInitialData` produced it — is a source
88
+ * this module knows how to land. A directory row is refused HERE, in the data, rather than by
89
+ * omitting a `dropTargetForElements` registration: pragmatic-dnd bubbles a refusal to the next
90
+ * target OUT, and the next target out of a tab strip is the pane cell, which is not a target at all
91
+ * — so an unhandled refusal would not merely do nothing, it would do nothing silently in a way
92
+ * indistinguishable from a bug. Every caller answers `canDrop` with this, so a directory row (or
93
+ * anything else this module has no branch for) is refused at the SOURCE and never reaches a target
94
+ * closure at all.
95
+ */
96
+ export function isTabDropSource(data: Record<string, unknown>): data is TabDropSourceData {
97
+ return data.type === "tab" || (data.type === "file-tree" && data.entryType === "file");
98
+ }
99
+
100
+ /**
101
+ * Land a tab at a specific slot of `paneId`, releasing any derivation the pane is borrowing FIRST —
102
+ * the same order `receivingPane` uses, and for the same reason: inserting into a `tabOrder` while
103
+ * `pane.derived` is still set is the one-frame D2 violation its own docstring describes, observable
104
+ * to anything that reads the pane between the two writes.
105
+ */
106
+ function placeTab(tabId: string, paneId: string, index: number): void {
107
+ const pane = paneById(paneId);
108
+ if (pane?.derived) {
109
+ clearPaneDerivation(paneId);
110
+ }
111
+ const landed = moveTabToPane(tabId, paneId, index);
112
+ if (!landed) {
113
+ return;
114
+ }
115
+ // A drag is a commitment, exactly as a double-click promoting a preview tab is one.
116
+ promoteTab(tabId);
117
+ activateTab(tabId);
118
+ }
119
+
120
+ /** {@link placeTab}'s file half: land the file's tab at `index`, opening it first if it is not. */
121
+ async function placeFile(path: string, paneId: string, index: number): Promise<void> {
122
+ const existing = [...workspace.tabs.values()].find((tab) => tab.documentPath === path);
123
+ if (existing) {
124
+ placeTab(existing.id, paneId, index);
125
+ return;
126
+ }
127
+ await openFileInTab(path, { focus: true, paneId });
128
+ // Idempotent by construction: `openFileInTab` may already have landed the tab at its default
129
+ // Slot, and this re-finds it by path and moves it to the slot the drop actually asked for.
130
+ const opened = [...workspace.tabs.values()].find((tab) => tab.documentPath === path);
131
+ if (opened) {
132
+ placeTab(opened.id, paneId, index);
133
+ }
134
+ }
135
+
136
+ /** The landing index a chip-edge target names, or the tail target's own index when there is none. */
137
+ function edgeIndex(target: TabSlotTarget, edge: Edge | null): number {
138
+ if (target.tabId === undefined) {
139
+ return target.index;
140
+ }
141
+ return edge === "left" ? target.index : target.index + 1;
142
+ }
143
+
144
+ async function resolveTabSlotDrop(source: TabDropSourceData, target: TabSlotTarget): Promise<void> {
145
+ const edge = extractClosestEdge(target as unknown as Record<string | symbol, unknown>);
146
+ if (source.type === "tab") {
147
+ if (source.paneId === target.paneId) {
148
+ const pane = paneById(target.paneId);
149
+ if (!pane) {
150
+ return;
151
+ }
152
+ const startIndex = pane.tabOrder.indexOf(source.tabId);
153
+ if (startIndex === -1) {
154
+ return;
155
+ }
156
+ const to =
157
+ target.tabId === undefined
158
+ ? pane.tabOrder.length - 1
159
+ : getReorderDestinationIndex({
160
+ axis: "horizontal",
161
+ closestEdgeOfTarget: edge,
162
+ indexOfTarget: target.index,
163
+ startIndex,
164
+ });
165
+ if (to !== startIndex) {
166
+ moveTab(source.tabId, to);
167
+ }
168
+ return;
169
+ }
170
+ placeTab(source.tabId, target.paneId, edgeIndex(target, edge));
171
+ return;
172
+ }
173
+ await placeFile(source.path, target.paneId, edgeIndex(target, edge));
174
+ }
175
+
176
+ async function resolvePaneEdgeDrop(
177
+ source: TabDropSourceData,
178
+ target: PaneEdgeTarget,
179
+ ): Promise<void> {
180
+ if (source.type === "tab") {
181
+ // Ignores `target.paneId`: the split moves the tab beside ITS OWN pane, which is the edge zone's
182
+ // Pane by construction — the zone is only armed below `MAX_PANES`, so there is exactly one pane
183
+ // For a dragged tab to have come from.
184
+ const landed = splitRight(source.tabId);
185
+ if (landed) {
186
+ promoteTab(source.tabId);
187
+ }
188
+ return;
189
+ }
190
+ const pane = receivingPane(target.paneId);
191
+ await openFileInTab(source.path, { paneId: pane.id });
192
+ const landed = [...workspace.tabs.values()].some((tab) => tab.documentPath === source.path);
193
+ if (!landed && paneIsEmpty(pane)) {
194
+ // A refusal (a missing file, no editor for the format) must not leave a pane this drop minted
195
+ // Standing empty — the same rule `document.openToSide` applies to its own refusal.
196
+ closePane(pane.id);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Turn a completed drag into a write. `target` is the drop's own INNERMOST target data, or
202
+ * `undefined` for a drag cancelled off every target — both are legal and both mean nothing landed.
203
+ */
204
+ export async function resolveTabDrop(
205
+ source: { data: Record<string, unknown> },
206
+ target: { data: Record<string, unknown> } | undefined,
207
+ ): Promise<void> {
208
+ if (!target || !isTabDropSource(source.data)) {
209
+ return;
210
+ }
211
+ const sourceData = source.data;
212
+ if (target.data.type === "tab-slot") {
213
+ await resolveTabSlotDrop(sourceData, target.data as TabSlotTarget);
214
+ return;
215
+ }
216
+ if (target.data.type === "pane-edge") {
217
+ await resolvePaneEdgeDrop(sourceData, target.data as PaneEdgeTarget);
218
+ }
219
+ // `tab-slot-refused` / `pane-edge-refused` / anything else: nothing.
220
+ }
@@ -48,7 +48,6 @@ import {
48
48
  closeTab,
49
49
  focusPane,
50
50
  isPaneFocused,
51
- moveTab,
52
51
  promoteDirtyPreviewTabs,
53
52
  promoteTab,
54
53
  setTabPinned,
@@ -60,6 +59,7 @@ import { DRAFT_FIELD, isDraftEntry } from "../content/draft-state";
60
59
  import { entryFields } from "../content/entry-fields";
61
60
  import { collectionOfPath } from "../content/entry-model";
62
61
  import { activeRegistry } from "../commands/active-registry";
62
+ import { runActiveReported, runReported } from "../commands/run-reported";
63
63
  import { PRESET_LABELS } from "../workspace/pane-derive";
64
64
  import { localeLabel } from "@jxsuite/schema/locale";
65
65
  import { tabOfPane } from "../canvas/canvas-surface";
@@ -79,9 +79,17 @@ import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
79
79
  import { saveFile } from "../files/file-ops";
80
80
  import { collabReadOnly } from "../collab/collab-session";
81
81
  import { collabState } from "../collab/collab-state";
82
- import { rectOf } from "../utils/geometry";
82
+ import { elementAtPoint, rectOf } from "../utils/geometry";
83
83
  import { resolveRegion } from "../ui/regions";
84
84
  import { commitTabBuffers, tabBufferUnsaved } from "../services/monaco-buffer";
85
+ import {
86
+ draggable,
87
+ dropTargetForElements,
88
+ monitorForElements,
89
+ } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
90
+ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
91
+ import { attachClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
92
+ import { isTabDropSource, resolveTabDrop } from "./tab-drop";
85
93
  import type { EffectScope } from "@vue/reactivity";
86
94
 
87
95
  /**
@@ -118,8 +126,24 @@ const _overflowing = new Map<string, boolean>();
118
126
 
119
127
  let _overflowHandle: MenuHandle | null = null;
120
128
 
121
- /** The tab id currently being dragged, or null. */
122
- let _dragging: string | null = null;
129
+ /** What is being dragged right now, or null between drags — a tab off some strip, or a file row. */
130
+ let _drag: { kind: "tab"; tabId: string } | { kind: "file"; path: string } | null = null;
131
+
132
+ /**
133
+ * Where the pointer is hovering, in the vocabulary a strip draws: which pane, which chip slot (`-1`
134
+ * for the tail), and whether that slot IS the tail. Null off every target this module answers for —
135
+ * a `pane-edge` hover included, which is `panels/pane-grid.ts`'s own indicator to draw.
136
+ */
137
+ let _over: { paneId: string; index: number; tail: boolean } | null = null;
138
+
139
+ /** Every chip's pragmatic registration, per host, per tab id — released before a re-registration. */
140
+ const _chipDnd = new Map<HTMLElement, Map<string, () => void>>();
141
+
142
+ /** Each host's OWN registration — the strip's empty-tail target. */
143
+ const _stripDnd = new Map<HTMLElement, () => void>();
144
+
145
+ /** The one drag monitor, armed in {@link mount} and released in {@link unmount}. */
146
+ let _dropMonitor: (() => void) | null = null;
123
147
 
124
148
  /** Region id of a pane's tab strip. `ui/regions.ts` owns the grammar; this is one call site. */
125
149
  export function paneStripRegion(paneId: string): string {
@@ -152,6 +176,51 @@ function hostFor(pane: Pane): HTMLElement | null {
152
176
  export function mount(host: HTMLElement) {
153
177
  unmount();
154
178
  _primaryHost = host;
179
+ _dropMonitor = monitorForElements({
180
+ onDragStart({ source }) {
181
+ if (!isTabDropSource(source.data)) {
182
+ return;
183
+ }
184
+ _drag =
185
+ source.data.type === "tab"
186
+ ? { kind: "tab", tabId: source.data.tabId as string }
187
+ : { kind: "file", path: source.data.path as string };
188
+ // The tail target does not exist until a pane with no tabs is asked to grow one (§7).
189
+ render();
190
+ // `render()` alone does not write `dragKey` — only `syncDrag()` does — so without this the
191
+ // Source chip's `data-dragging` mark would not appear until the pointer's first move.
192
+ syncDrag();
193
+ },
194
+ onDrag({ location }) {
195
+ if (!_drag) {
196
+ return;
197
+ }
198
+ const [target] = location.current.dropTargets;
199
+ const data = target?.data;
200
+ _over =
201
+ data?.type === "tab-slot"
202
+ ? {
203
+ index: data.tabId === undefined ? -1 : (data.index as number),
204
+ paneId: data.paneId as string,
205
+ tail: data.tabId === undefined,
206
+ }
207
+ : null;
208
+ syncDrag();
209
+ },
210
+ onDrop({ location, source }) {
211
+ if (!isTabDropSource(source.data)) {
212
+ return;
213
+ }
214
+ const [target] = location.current.dropTargets;
215
+ _drag = null;
216
+ _over = null;
217
+ syncDrag();
218
+ render();
219
+ // A cancelled drag (dropped off every target) arrives with `dropTargets: []`; the resolver
220
+ // Reads `target === undefined` as "nothing landed", the same answer a refused target gives.
221
+ void resolveTabDrop(source, target);
222
+ },
223
+ });
155
224
  _scope = effectScope();
156
225
  _scope.run(() => {
157
226
  effect(() => {
@@ -194,18 +263,37 @@ export function unmount() {
194
263
  dismissTabContextMenu();
195
264
  _scope?.stop();
196
265
  _scope = null;
266
+ _dropMonitor?.();
267
+ _dropMonitor = null;
268
+ for (const host of _strips.keys()) {
269
+ releaseHostDnD(host);
270
+ }
197
271
  for (const strip of _strips.values()) {
198
272
  strip.dispose();
199
273
  }
200
274
  _strips.clear();
201
275
  _drawing.clear();
202
276
  _primaryHost = null;
203
- _dragging = null;
277
+ _drag = null;
278
+ _over = null;
204
279
  _lastActive.clear();
205
280
  _overflowing.clear();
206
281
  _hosts.clear();
207
282
  }
208
283
 
284
+ /** Take back every pragmatic registration a host holds — its chips and its own tail target. */
285
+ function releaseHostDnD(host: HTMLElement): void {
286
+ const chips = _chipDnd.get(host);
287
+ if (chips) {
288
+ for (const release of chips.values()) {
289
+ release();
290
+ }
291
+ _chipDnd.delete(host);
292
+ }
293
+ _stripDnd.get(host)?.();
294
+ _stripDnd.delete(host);
295
+ }
296
+
209
297
  function render() {
210
298
  /* One host draws ONE pane. While the shell has a single strip, both panes resolve to the same
211
299
  div — and the one whose tabs are on screen is the focused one, so it wins the tie. Without
@@ -236,6 +324,7 @@ function render() {
236
324
  _lastActive.delete(paneId);
237
325
  _overflowing.delete(paneId);
238
326
  if (!claims.has(host)) {
327
+ releaseHostDnD(host);
239
328
  _strips.get(host)?.dispose();
240
329
  _strips.delete(host);
241
330
  _drawing.delete(host);
@@ -260,6 +349,7 @@ function paint(pane: Pane, host: HTMLElement): void {
260
349
  } else {
261
350
  _strips.set(host, mountTabStripSurface(host, values, actionsFor(host)));
262
351
  }
352
+ reconcileStripDnD(host, values);
263
353
 
264
354
  /* The strip is HELD — `surfaces/tab-strip.ts` takes it as the element is created — so this is an
265
355
  imperative USE of a node this module owns rather than a re-find of one it renders
@@ -305,6 +395,9 @@ function actionsFor(host: HTMLElement): TabStripActions {
305
395
  activate: (id) => {
306
396
  activateTab(id);
307
397
  },
398
+ chipHost: (element, id) => {
399
+ adoptChip(host, element, id);
400
+ },
308
401
  closeTab: (id) => {
309
402
  void requestClose(id);
310
403
  },
@@ -314,11 +407,6 @@ function actionsFor(host: HTMLElement): TabStripActions {
314
407
  openTabContextMenu(tab, x, y);
315
408
  }
316
409
  },
317
- dragEnd: onDragEnd,
318
- dragStart: onDragStart,
319
- drop: (index, transfer) => {
320
- onDrop(host, index, transfer);
321
- },
322
410
  focusPane: () => {
323
411
  focusPane(drawnPane(host)?.id ?? PRIMARY_PANE);
324
412
  },
@@ -328,6 +416,9 @@ function actionsFor(host: HTMLElement): TabStripActions {
328
416
  promote: (id) => {
329
417
  promoteTab(id);
330
418
  },
419
+ stripHost: (element) => {
420
+ adoptStrip(host, element);
421
+ },
331
422
  togglePin: (id) => {
332
423
  const tab = workspace.tabs.get(id);
333
424
  if (tab) {
@@ -438,6 +529,26 @@ function project(pane: Pane): TabStripValues {
438
529
  return derivationValues(pane, derived);
439
530
  }
440
531
  if (pane.tabOrder.length === 0) {
532
+ /* An empty, non-derived pane WHILE A DRAG IS LIVE draws a `tabs` row with no chips instead of
533
+ going blank: `blankValues` hides the row outright, and a hidden row has no `jx-tabs` element
534
+ for `adoptStrip` to make a target of — so a tab dragged toward the empty primary beside a
535
+ derived secondary (the state `detachTab` allows) had nowhere to land. `_drag` gates it rather
536
+ than `pane.tabOrder.length === 0` alone, so the pane stays hidden the rest of the time. */
537
+ if (_drag) {
538
+ return {
539
+ active: "",
540
+ chipTitle: "",
541
+ focused: isPaneFocused(pane.id),
542
+ mode: "tabs",
543
+ preset: "",
544
+ stripLabel: stripLabel(pane),
545
+ subject: "",
546
+ tabs: [],
547
+ trailing: false,
548
+ trailingGlyph: "⌄",
549
+ trailingTitle: "Show hidden tabs",
550
+ };
551
+ }
441
552
  _lastActive.set(pane.id, null);
442
553
  _overflowing.set(pane.id, false);
443
554
  return blankValues(pane);
@@ -491,7 +602,6 @@ function chipView(
491
602
  {
492
603
  dirty: tab.doc.dirty,
493
604
  draft: pill?.draft ?? false,
494
- dragging: _dragging === id,
495
605
  index,
496
606
  key: id,
497
607
  label: labels.get(id) ?? "Untitled",
@@ -510,35 +620,131 @@ function chipView(
510
620
  ];
511
621
  }
512
622
 
513
- // ─── Drag reorder ─────────────────────────────────────────────────────────────
623
+ // ─── Drag and drop ────────────────────────────────────────────────────────────
514
624
 
515
- function onDragStart(id: string, transfer: DataTransfer | null) {
516
- _dragging = id;
517
- transfer?.setData("text/plain", id);
518
- if (transfer) {
519
- transfer.effectAllowed = "move";
625
+ /**
626
+ * Make one chip a pragmatic drag source and drop target. Called from `chipHost`, as the chip is
627
+ * CREATED (`onNodeCreated` fires once per chip; a repaint that keeps a chip alive fires it never
628
+ * again, and one that replaces it — a pane handover, §18.3 — fires it fresh).
629
+ *
630
+ * Released first, in case a chip element is ever handed a second registration for the same id: the
631
+ * SOURCE of truth for "is this id registered on this host" is the map, not whether pragmatic still
632
+ * remembers the old element, and releasing unconditionally is cheaper than asking.
633
+ */
634
+ function adoptChip(host: HTMLElement, element: HTMLElement, id: string): void {
635
+ let chips = _chipDnd.get(host);
636
+ if (!chips) {
637
+ chips = new Map();
638
+ _chipDnd.set(host, chips);
520
639
  }
521
- // The ghost is chrome state, not model state, so no effect fires for it.
522
- render();
640
+ chips.get(id)?.();
641
+
642
+ const cleanup = combine(
643
+ draggable({
644
+ element,
645
+ // A drag starting on the pin toggle or the kit's own close button is a click on THAT control,
646
+ // Not a request to move the chip — the same guard `panels/dnd.ts`'s Outline rows use.
647
+ canDrag({ input }) {
648
+ const target = elementAtPoint(input.clientX, input.clientY);
649
+ return !(target instanceof Element && target.closest('[part="pin"], [part="close"]'));
650
+ },
651
+ getInitialData() {
652
+ // Read at DRAG TIME, not captured at registration: the pane a host draws can change under
653
+ // An already-mounted chip (a handover, §18.3), and `drawnPane` always answers about now.
654
+ return { paneId: drawnPane(host)?.id, tabId: id, type: "tab" };
655
+ },
656
+ }),
657
+ dropTargetForElements({
658
+ canDrop({ source }) {
659
+ return isTabDropSource(source.data);
660
+ },
661
+ element,
662
+ getData({ element: el, input }) {
663
+ const pane = drawnPane(host);
664
+ if (!pane) {
665
+ return { type: "tab-slot-refused" };
666
+ }
667
+ // Computed HERE, not at chip creation: the target's own slot moves every time a tab is
668
+ // Reordered, and `resolveTabDrop` reads this fresh on every hover and on the drop itself.
669
+ return attachClosestEdge(
670
+ { index: pane.tabOrder.indexOf(id), paneId: pane.id, tabId: id, type: "tab-slot" },
671
+ { allowedEdges: ["left", "right"], element: el, input },
672
+ );
673
+ },
674
+ }),
675
+ );
676
+ chips.set(id, cleanup);
523
677
  }
524
678
 
525
- function onDragEnd() {
526
- _dragging = null;
527
- render();
679
+ /**
680
+ * Make a strip's OWN element a drop target — the empty tail after the last chip, and, for an empty
681
+ * pane growing one during a drag (§7), the whole of it. Innermost wins, so a chip under the pointer
682
+ * answers first; this only ever resolves a drop that landed on the strip itself.
683
+ */
684
+ function adoptStrip(host: HTMLElement, element: HTMLElement): void {
685
+ _stripDnd.get(host)?.();
686
+ const cleanup = dropTargetForElements({
687
+ canDrop({ source }) {
688
+ return isTabDropSource(source.data);
689
+ },
690
+ element,
691
+ getData() {
692
+ const pane = drawnPane(host);
693
+ return pane
694
+ ? { index: pane.tabOrder.length, paneId: pane.id, type: "tab-slot" }
695
+ : { type: "tab-slot-refused" };
696
+ },
697
+ });
698
+ _stripDnd.set(host, cleanup);
528
699
  }
529
700
 
530
701
  /**
531
- * Drop onto the chip at `index`. The model clamps the destination into the region the dragged tab's
532
- * pinned state allows, so this only has to say where the pointer was.
702
+ * Release a host's chip registrations for ids that no longer appear — a tab closed, or a pane
703
+ * handover replaced every chip with another pane's — and release the whole host when it is no
704
+ * longer drawing tabs at all. Run at the tail of every {@link paint}, which is what makes a handover
705
+ * self-correcting: the SAME call that draws the new pane's chips (through `onNodeCreated`) is the
706
+ * call that notices the old pane's ids are no longer live.
533
707
  */
534
- function onDrop(host: HTMLElement, index: number, transfer: DataTransfer | null) {
535
- const pane = drawnPane(host);
536
- const id = _dragging ?? transfer?.getData("text/plain") ?? null;
537
- _dragging = null;
538
- if (!pane || !id || !pane.tabOrder.includes(id)) {
708
+ function reconcileStripDnD(host: HTMLElement, values: TabStripValues): void {
709
+ if (values.mode !== "tabs") {
710
+ releaseHostDnD(host);
711
+ return;
712
+ }
713
+ const live = new Set(values.tabs.map((chip) => chip.key));
714
+ const chips = _chipDnd.get(host);
715
+ if (!chips) {
539
716
  return;
540
717
  }
541
- moveTab(id, index);
718
+ for (const [id, release] of chips) {
719
+ if (!live.has(id)) {
720
+ release();
721
+ chips.delete(id);
722
+ }
723
+ }
724
+ }
725
+
726
+ /**
727
+ * Write the drag indicators for every standing strip, from {@link _over} and {@link _drag}.
728
+ *
729
+ * `dragKey` is broadcast to every host unconditionally — a tab id only matches a chip on the ONE
730
+ * host that draws its pane, so the comparison in `tab-strip.json` is what scopes it, not this
731
+ * function. `dropIndex` / `dropTail` ARE scoped here, to the host currently drawing `_over.paneId`,
732
+ * because an index is meaningless on a strip it was not measured against.
733
+ */
734
+ function syncDrag(): void {
735
+ const dragKey = _drag?.kind === "tab" ? _drag.tabId : "";
736
+ for (const [host, drawing] of _drawing) {
737
+ const surface = _strips.get(host);
738
+ if (!surface) {
739
+ continue;
740
+ }
741
+ const hovering = _over !== null && _over.paneId === drawing.paneId;
742
+ surface.drag({
743
+ dragKey,
744
+ dropIndex: hovering && _over ? _over.index : -1,
745
+ dropTail: hovering && _over ? _over.tail : false,
746
+ });
747
+ }
542
748
  }
543
749
 
544
750
  /**
@@ -575,8 +781,7 @@ function openTrailing(host: HTMLElement, anchor: HTMLElement | null) {
575
781
  return;
576
782
  }
577
783
  if (_drawing.get(host)?.mode === "derivation") {
578
- const registry = activeRegistry();
579
- void registry?.run("pane.unsplit");
784
+ void runActiveReported("pane.unsplit", undefined, "Tabs");
580
785
  return;
581
786
  }
582
787
  openOverflowMenu(pane, tabLabels(pane), anchor);
@@ -807,7 +1012,7 @@ function placedTabItems(tab: Tab): MenuRowProjection[] {
807
1012
  ? {
808
1013
  disabled: false,
809
1014
  run: () => {
810
- void registry.run(command.id, args);
1015
+ void runReported(registry, command.id, args, "Tabs");
811
1016
  },
812
1017
  }
813
1018
  : { disabled: true, requires: reason }),