@jxsuite/studio 5.1.0 → 5.3.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 (117) hide show
  1. package/dist/chunks/{cf-account-picker-qwkm9zrf.js → cf-account-picker-nwnwdm6p.js} +10 -10
  2. package/dist/chunks/{cf-settings-71bv5zn9.js → cf-settings-adg74qsk.js} +4 -4
  3. package/dist/chunks/{components-vj72gqtq.js → components-5y78e05d.js} +4 -4
  4. package/dist/chunks/content-source-4rzvmp0m.js +46 -0
  5. package/dist/chunks/{convert-to-repeater-n64h8znc.js → convert-to-repeater-jxsv62v7.js} +22 -22
  6. package/dist/chunks/entry-commands-1wnsqn5d.js +42 -0
  7. package/dist/chunks/{extension-sections-3hpcambp.js → extension-sections-2mrptyek.js} +27 -27
  8. package/dist/chunks/{file-ops-8yhd83xr.js → file-ops-knfqshrm.js} +22 -22
  9. package/dist/chunks/{files-hnxqxtsg.js → files-y6fr4he9.js} +32 -30
  10. package/dist/chunks/{format-host-779sfp2n.js → format-host-1k4qrcqd.js} +2 -2
  11. package/dist/chunks/{fs-events-b88z7g4h.js → fs-events-3y6y0nrm.js} +4 -4
  12. package/dist/chunks/{git-panel-xwj8fpab.js → git-panel-2rkrsjva.js} +23 -22
  13. package/dist/chunks/grid-view-1p6hsyjx.css +8406 -0
  14. package/dist/chunks/grid-view-a7b87n2c.js +15322 -0
  15. package/dist/chunks/{iframe-host-syr0f9sb.js → iframe-host-scpw01h8.js} +13 -13
  16. package/dist/chunks/{jx-validate-j0wxk72w.js → jx-validate-epp2z3s3.js} +2 -2
  17. package/dist/chunks/{layers-ff81wthn.js → layers-jnvdbqva.js} +5 -5
  18. package/dist/chunks/layers-panel-73gfkcga.js +41 -0
  19. package/dist/chunks/{listbox-6v2ac08j.js → listbox-b7jxnhq1.js} +19 -3
  20. package/dist/chunks/{media-picker-cqhhxhhe.js → media-picker-f66nav16.js} +13 -13
  21. package/dist/chunks/menu-1dfhry4s.js +15 -0
  22. package/dist/chunks/{monaco-buffer-bhy3hb6m.js → monaco-buffer-pykwbm92.js} +3 -3
  23. package/dist/chunks/{monaco-setup-8r62fj6y.js → monaco-setup-km6abc6q.js} +2 -2
  24. package/dist/chunks/{notify-628t0anh.js → notify-42gk8ha1.js} +2 -2
  25. package/dist/chunks/{parse-0zx14y15.js → parse-reg9eevt.js} +2 -2
  26. package/dist/chunks/publish-panel-z1zd2da1.js +27 -0
  27. package/dist/chunks/quick-search-zdv7h8ht.js +47 -0
  28. package/dist/chunks/{references-qvvxej7h.js → references-h69xp9zz.js} +2 -2
  29. package/dist/chunks/right-panel-889vbnm5.js +45 -0
  30. package/dist/chunks/{split-g7nm1rgb.js → split-4cz4b6ef.js} +14 -3
  31. package/dist/chunks/{studio-vfq8qpjt.js → studio-0j7r7s9g.js} +3 -3
  32. package/dist/chunks/{studio-b4s9eykd.js → studio-151w0xnh.js} +15 -15
  33. package/dist/chunks/studio-1tbjfqv4.js +146 -0
  34. package/dist/chunks/{studio-8scdew0e.js → studio-27q131j3.js} +4 -4
  35. package/dist/chunks/{studio-7r42c6ca.js → studio-3ntvq6cg.js} +5 -5
  36. package/dist/chunks/{studio-g3jevyjt.js → studio-4c041r8y.js} +2 -2
  37. package/dist/chunks/{studio-js8nv53n.js → studio-4f3psrk1.js} +14 -14
  38. package/dist/chunks/{studio-pxbgypmg.js → studio-50wfwy44.js} +17 -14
  39. package/dist/chunks/{studio-vw7mzp6z.js → studio-6gryj0sg.js} +5 -5
  40. package/dist/chunks/{studio-e1pgga3k.js → studio-6kvgrkrp.js} +7 -7
  41. package/dist/chunks/{studio-wqz6dggm.js → studio-7144epdr.js} +9 -9
  42. package/dist/chunks/{studio-b0k5tqgp.js → studio-7rrb1esn.js} +32 -28
  43. package/dist/chunks/{studio-38emantv.js → studio-a5h5j33a.js} +19 -5
  44. package/dist/chunks/{studio-3773086z.js → studio-a65w20ex.js} +4 -4
  45. package/dist/chunks/{studio-nzzpvzx9.js → studio-ab87phj1.js} +4 -4
  46. package/dist/chunks/{studio-r6nvacqs.js → studio-b66rsh78.js} +20 -20
  47. package/dist/chunks/{studio-jxp12rnj.js → studio-b7xacrwe.js} +6 -6
  48. package/dist/chunks/{studio-6xa21ej9.js → studio-c04nzbby.js} +2 -2
  49. package/dist/chunks/{studio-try13h8c.js → studio-c0qgc3tn.js} +18 -18
  50. package/dist/chunks/{studio-tqgryxwg.js → studio-cgprcdxn.js} +7 -7
  51. package/dist/chunks/{studio-kkqkcp3d.js → studio-d1n2bykn.js} +40 -13
  52. package/dist/chunks/{studio-p1dw8wfw.js → studio-d8z5bajd.js} +14 -10
  53. package/dist/chunks/{studio-3rz5md06.js → studio-dd1qqkry.js} +6 -6
  54. package/dist/chunks/{studio-1z1kcjv4.js → studio-drg8qj9k.js} +11 -11
  55. package/dist/chunks/{studio-kcgff4r0.js → studio-fgmfksz8.js} +2 -2
  56. package/dist/chunks/{studio-qk90fgqm.js → studio-fzjx96t2.js} +7 -7
  57. package/dist/chunks/{studio-a79k90n9.js → studio-g33v4fe9.js} +17 -16
  58. package/dist/chunks/{studio-k75n7vhp.js → studio-gk2twyh4.js} +74 -36
  59. package/dist/chunks/{studio-syg2tadj.js → studio-gtfq1dkz.js} +3 -3
  60. package/dist/chunks/{studio-1knfqgy7.js → studio-j0w60gft.js} +8 -8
  61. package/dist/chunks/{studio-aj37eer3.js → studio-jt27ta6x.js} +4 -4
  62. package/dist/chunks/{studio-gb13095a.js → studio-ke09tn9k.js} +59 -20
  63. package/dist/chunks/{studio-d0300636.js → studio-ks2zzth2.js} +17 -2
  64. package/dist/chunks/{studio-6ewqv390.js → studio-m1p9b6c3.js} +5 -5
  65. package/dist/chunks/{studio-qk4bfsq2.js → studio-msc1ee1m.js} +4 -4
  66. package/dist/chunks/{studio-93zafjcm.js → studio-nfx354t1.js} +10 -10
  67. package/dist/chunks/{studio-mhhyamxf.js → studio-nsgw7dbk.js} +6 -6
  68. package/dist/chunks/{studio-qhhm6hbt.js → studio-rtskkm43.js} +4 -4
  69. package/dist/chunks/studio-t171hxhe.js +23 -0
  70. package/dist/chunks/{studio-hhvs4sdg.js → studio-vn5hske5.js} +10 -4
  71. package/dist/chunks/{studio-q3x06vmj.js → studio-wnsyffg1.js} +2 -2
  72. package/dist/chunks/{studio-5wt9nvhd.js → studio-xajk70w0.js} +29 -42
  73. package/dist/chunks/{studio-7xnmgv66.js → studio-ye9drbk8.js} +45 -44
  74. package/dist/chunks/{studio-y9zedyjn.js → studio-z2smx5ce.js} +2 -2
  75. package/dist/chunks/{studio-sbjyc8bt.js → studio-ze46gzjh.js} +9 -9
  76. package/dist/chunks/{studio-zdn2g6dt.js → studio-zjjde71a.js} +2 -2
  77. package/dist/chunks/{workspace-t55jdjbx.js → workspace-np10mayv.js} +18 -10
  78. package/dist/iframe-entry.js +9 -5
  79. package/dist/studio.css +1 -2
  80. package/dist/studio.js +910 -15705
  81. package/package.json +10 -10
  82. package/src/files/files.ts +38 -15
  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/media/media-open.ts +14 -2
  87. package/src/panels/chat-panel.ts +40 -5
  88. package/src/panels/pane-context.ts +5 -4
  89. package/src/panels/pane-grid.ts +100 -10
  90. package/src/panels/tab-drop.ts +220 -0
  91. package/src/panels/tab-strip.ts +235 -30
  92. package/src/services/ai-models.ts +13 -0
  93. package/src/services/module-preloads.ts +67 -0
  94. package/src/studio.ts +16 -6
  95. package/src/surfaces/commandbar.json +26 -16
  96. package/src/surfaces/formula-palette.json +8 -7
  97. package/src/surfaces/formula-palette.ts +10 -6
  98. package/src/surfaces/palette.json +8 -7
  99. package/src/surfaces/pane-grid.json +35 -3
  100. package/src/surfaces/pane-grid.ts +26 -4
  101. package/src/surfaces/tab-strip.json +16 -39
  102. package/src/surfaces/tab-strip.ts +76 -12
  103. package/src/ui/ai-credentials-form.ts +12 -1
  104. package/src/ui/panel-resize.ts +3 -23
  105. package/src/ui/virtual-window.ts +57 -4
  106. package/src/workspace/pane-derive.ts +7 -5
  107. package/src/workspace/session.ts +3 -3
  108. package/src/workspace/workspace.ts +160 -43
  109. package/styles/shell-frame.css +8 -0
  110. package/styles/shell-frame.json +4 -0
  111. package/dist/chunks/content-source-5mex4x1h.js +0 -46
  112. package/dist/chunks/entry-commands-234hjnqw.js +0 -42
  113. package/dist/chunks/layers-panel-pnnghw12.js +0 -40
  114. package/dist/chunks/menu-9p2w46gj.js +0 -15
  115. package/dist/chunks/publish-panel-5zt840sy.js +0 -27
  116. package/dist/chunks/quick-search-3748564r.js +0 -45
  117. package/dist/chunks/right-panel-eccz92qt.js +0 -45
@@ -21,9 +21,6 @@
21
21
  "flex": "1",
22
22
  "flexShrink": "999"
23
23
  },
24
- "& jx-button, & jx-action-button, & button": {
25
- "WebkitAppRegion": "no-drag"
26
- },
27
24
  "& [part=\"csd-lead\"], & [part=\"csd-trail\"], & [part=\"menu-slot\"], & [part=\"layouts-slot\"], & [part=\"presence-slot\"]": {
28
25
  "display": "contents"
29
26
  },
@@ -264,7 +261,8 @@
264
261
  "size": "sm"
265
262
  },
266
263
  "attributes": {
267
- "data-control": "close"
264
+ "data-control": "close",
265
+ "class": "electrobun-webkit-app-region-no-drag"
268
266
  },
269
267
  "onclick": {
270
268
  "$prototype": "Function",
@@ -288,7 +286,8 @@
288
286
  "size": "sm"
289
287
  },
290
288
  "attributes": {
291
- "data-control": "minimize"
289
+ "data-control": "minimize",
290
+ "class": "electrobun-webkit-app-region-no-drag"
292
291
  },
293
292
  "onclick": {
294
293
  "$prototype": "Function",
@@ -312,7 +311,8 @@
312
311
  "size": "sm"
313
312
  },
314
313
  "attributes": {
315
- "data-control": "maximize"
314
+ "data-control": "maximize",
315
+ "class": "electrobun-webkit-app-region-no-drag"
316
316
  },
317
317
  "onclick": {
318
318
  "$prototype": "Function",
@@ -354,7 +354,8 @@
354
354
  }
355
355
  },
356
356
  "attributes": {
357
- "data-menu": "studio"
357
+ "data-menu": "studio",
358
+ "class": "electrobun-webkit-app-region-no-drag"
358
359
  },
359
360
  "onclick": {
360
361
  "$prototype": "Function",
@@ -438,7 +439,8 @@
438
439
  "aria-selected": "${$map.item.active ? 'true' : 'false'}",
439
440
  "data-layout": "${$map.item.id}",
440
441
  "data-active": "${$map.item.active ? '' : null}",
441
- "title": "${$map.item.title}"
442
+ "title": "${$map.item.title}",
443
+ "class": "electrobun-webkit-app-region-no-drag"
442
444
  },
443
445
  "textContent": "${$map.item.name}",
444
446
  "onclick": {
@@ -484,7 +486,8 @@
484
486
  "size": "sm"
485
487
  },
486
488
  "attributes": {
487
- "part": "layout-add"
489
+ "part": "layout-add",
490
+ "class": "electrobun-webkit-app-region-no-drag"
488
491
  },
489
492
  "onclick": {
490
493
  "$prototype": "Function",
@@ -514,7 +517,8 @@
514
517
  "attributes": {
515
518
  "part": "center",
516
519
  "role": "group",
517
- "aria-label": "Command Center"
520
+ "aria-label": "Command Center",
521
+ "class": "electrobun-webkit-app-region-no-drag"
518
522
  },
519
523
  "onclick": {
520
524
  "$prototype": "Function",
@@ -581,7 +585,8 @@
581
585
  "part": "segment",
582
586
  "type": "button",
583
587
  "data-segment": "${$map.item.key}",
584
- "title": "${$map.item.title}"
588
+ "title": "${$map.item.title}",
589
+ "class": "electrobun-webkit-app-region-no-drag"
585
590
  },
586
591
  "textContent": "${$map.item.label}",
587
592
  "onclick": {
@@ -666,7 +671,8 @@
666
671
  "quiet": true
667
672
  },
668
673
  "attributes": {
669
- "data-command-id": "${$map.item.id}"
674
+ "data-command-id": "${$map.item.id}",
675
+ "class": "electrobun-webkit-app-region-no-drag"
670
676
  },
671
677
  "onclick": {
672
678
  "$prototype": "Function",
@@ -865,7 +871,8 @@
865
871
  "size": "sm"
866
872
  },
867
873
  "attributes": {
868
- "data-command-id": "${$map.item.id}"
874
+ "data-command-id": "${$map.item.id}",
875
+ "class": "electrobun-webkit-app-region-no-drag"
869
876
  },
870
877
  "onchange": {
871
878
  "$prototype": "Function",
@@ -914,7 +921,8 @@
914
921
  "size": "sm"
915
922
  },
916
923
  "attributes": {
917
- "data-control": "minimize"
924
+ "data-control": "minimize",
925
+ "class": "electrobun-webkit-app-region-no-drag"
918
926
  },
919
927
  "onclick": {
920
928
  "$prototype": "Function",
@@ -938,7 +946,8 @@
938
946
  "size": "sm"
939
947
  },
940
948
  "attributes": {
941
- "data-control": "maximize"
949
+ "data-control": "maximize",
950
+ "class": "electrobun-webkit-app-region-no-drag"
942
951
  },
943
952
  "onclick": {
944
953
  "$prototype": "Function",
@@ -962,7 +971,8 @@
962
971
  "size": "sm"
963
972
  },
964
973
  "attributes": {
965
- "data-control": "close"
974
+ "data-control": "close",
975
+ "class": "electrobun-webkit-app-region-no-drag"
966
976
  },
967
977
  "onclick": {
968
978
  "$prototype": "Function",
@@ -20,6 +20,9 @@
20
20
  "paddingTop": "60px",
21
21
  "background": "rgba(0, 0, 0, 0.4)"
22
22
  },
23
+ "& [part=\"overlay\"][hidden]": {
24
+ "display": "none"
25
+ },
23
26
  "& [part=\"panel\"]": {
24
27
  "width": "560px",
25
28
  "maxWidth": "calc(100vw - 32px)",
@@ -97,14 +100,12 @@
97
100
  "color": "inherit"
98
101
  }
99
102
  },
100
- "$switch": {
101
- "$ref": "#/state/open"
102
- },
103
- "cases": {
104
- "true": {
103
+ "children": [
104
+ {
105
105
  "tagName": "div",
106
106
  "attributes": {
107
- "part": "overlay"
107
+ "part": "overlay",
108
+ "hidden": "${state.open ? null : ''}"
108
109
  },
109
110
  "onclick": {
110
111
  "$prototype": "Function",
@@ -360,5 +361,5 @@
360
361
  }
361
362
  ]
362
363
  }
363
- }
364
+ ]
364
365
  }
@@ -11,9 +11,12 @@
11
11
  *
12
12
  * **The mount is standing.** One slot in the popover layer, one document in it, and `open` is a
13
13
  * field of the scope rather than a render call — so opening the palette twice reuses the same
14
- * element tree and the `$switch` on `open` is what puts the overlay on screen and takes it away.
15
- * `getLayerSlot` hands back the same slot every time until something clears it, which is the one
16
- * case that re-mounts: a slot the layer no longer holds is not one this module may render into.
14
+ * element tree: the overlay is always PRESENT and a `hidden` attribute bound to `open` is what puts
15
+ * it on screen and takes it away, rather than a `$switch` that tore the whole subtree down and
16
+ * rebuilt it (and its `jx-listbox`) on every open — the per-open document-wide style recalculation
17
+ * that cost traced (`scripts/perf/REPORT.md`). `getLayerSlot` hands back the same slot every time
18
+ * until something clears it, which is the one case that re-mounts: a slot the layer no longer holds
19
+ * is not one this module may render into.
17
20
  *
18
21
  * **The rows and the highlight are separate fields**, exactly as `surfaces/palette.json`'s are —
19
22
  * and the highlight is now ONE field rather than a per-row comparison. A key moves `activeId`, the
@@ -285,9 +288,10 @@ function project(): void {
285
288
  /**
286
289
  * Focus the field once the document has drawn it.
287
290
  *
288
- * The mount settling means the DOCUMENT rendered; the `$switch` on `open` reconciles one microtask
289
- * later, so the field exists a turn after that — which is why this waits for the mount and then for
290
- * a turn, rather than reaching for the element straight away.
291
+ * The field is standing now, same as the overlay, but it stays behind `hidden` until the reactive
292
+ * binding on `open` reconciles — one microtask after the mount settles — so a focus reaching for it
293
+ * straight away would land on an element the reader cannot yet see. This waits for the mount and
294
+ * then for a turn, rather than reaching for the element immediately.
291
295
  */
292
296
  function focusInput(): void {
293
297
  const pending = _mount;
@@ -17,6 +17,9 @@
17
17
  "paddingTop": "60px",
18
18
  "background": "rgba(0, 0, 0, 0.4)"
19
19
  },
20
+ "& [part=\"overlay\"][hidden]": {
21
+ "display": "none"
22
+ },
20
23
  "& [part=\"panel\"]": {
21
24
  "width": "560px",
22
25
  "maxWidth": "calc(100vw - 32px)",
@@ -133,14 +136,12 @@
133
136
  "color": "var(--jx-fg-dim)"
134
137
  }
135
138
  },
136
- "$switch": {
137
- "$ref": "#/state/open"
138
- },
139
- "cases": {
140
- "true": {
139
+ "children": [
140
+ {
141
141
  "tagName": "div",
142
142
  "attributes": {
143
- "part": "overlay"
143
+ "part": "overlay",
144
+ "hidden": "${state.open ? null : ''}"
144
145
  },
145
146
  "onclick": {
146
147
  "$prototype": "Function",
@@ -531,5 +532,5 @@
531
532
  }
532
533
  ]
533
534
  }
534
- }
535
+ ]
535
536
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://jxsuite.com/schema/v1",
3
3
  "$id": "StudioPaneGrid",
4
- "$description": "The pane grid (studio.md §18.1): one cell per pane, with a splitter before every cell but the first. Every box here is EMPTY by design — the four inside a cell are hosts other modules fill (`panels/tab-strip.ts` the strip, `panels/jump-bar.ts` the jump bar, `panels/pane-context.ts` the chrome layer, `canvas/canvas-render.ts` the stage), and the stage is an ISLAND in the §9.4 sense: the canvas host mounts a cross-origin `<iframe>` into it, so nothing this document draws may ever go inside it. What the document owns is the STRUCTURE and the STYLE — the cell's own two-row grid, the three row-2 children that share its lower cell, and the splitter — which is `styles/shell.css`'s `.pane*` block and `styles/canvas.css`'s `.pane-stage` rule, carried here and keyed on `part`. Two things are load-bearing and easy to undo. (1) **A row that moves reloads an iframe**, so the keyed `$map` is not an optimisation: `workspace.panes[0]` is always the primary and `MAX_PANES` is 2, the key is the pane id, and the runtime's forward-cursor pass therefore never moves a kept row — gaining or losing the side pane leaves the primary's node, and the frame inside it, untouched. (2) **The splitter must sit BETWEEN two cells and inside neither**, so each row is a `display: contents` wrapper holding its own leading splitter and its cell: the wrapper generates no box, so the splitter and the two cells are the grid items of `#pane-grid` in DOM order, and a pointer landing on the splitter is in no cell at all. Every selector below is a `>` chain from the root, because a pane's stage hosts other surface documents and a bare `[part=\"row\"]` descendant rule would reach into them.",
4
+ "$description": "The pane grid (studio.md §18.1): one cell per pane, with a splitter before every cell but the first. Every box here is EMPTY by design — the FIVE inside a cell are hosts other modules fill (`panels/tab-strip.ts` the strip, `panels/jump-bar.ts` the jump bar, `panels/pane-context.ts` the chrome layer, `canvas/canvas-render.ts` the stage, `panels/pane-grid.ts` itself the right-edge drop zone), and the stage is an ISLAND in the §9.4 sense: the canvas host mounts a cross-origin `<iframe>` into it, so nothing this document draws may ever go inside it. What the document owns is the STRUCTURE and the STYLE — the cell's own two-row grid, the three row-2 children that share its lower cell, and the splitter — which is `styles/shell.css`'s `.pane*` block and `styles/canvas.css`'s `.pane-stage` rule, carried here and keyed on `part`. Two things are load-bearing and easy to undo. (1) **A row that moves reloads an iframe**, so the keyed `$map` is not an optimisation: `workspace.panes[0]` is always the primary and `MAX_PANES` is 2, the key is the pane id, and the runtime's forward-cursor pass therefore never moves a kept row — gaining or losing the side pane leaves the primary's node, and the frame inside it, untouched. (2) **The splitter must sit BETWEEN two cells and inside neither**, so each row is a `display: contents` wrapper holding its own leading splitter and its cell: the wrapper generates no box, so the splitter and the two cells are the grid items of `#pane-grid` in DOM order, and a pointer landing on the splitter is in no cell at all. Every selector below is a `>` chain from the root, because a pane's stage hosts other surface documents and a bare `[part=\"row\"]` descendant rule would reach into them.",
5
5
  "tagName": "div",
6
6
  "attributes": {
7
7
  "part": "grid",
@@ -27,7 +27,7 @@
27
27
  "zIndex": "10"
28
28
  },
29
29
  "& > [part=\"row\"] > [part=\"pane\"]": {
30
- "$description": "ONE PANE, and one COLUMN, declared. Three children name `grid-row: 2` in order to share the lower cell, and auto-placement answers that by inventing an implicit column each: the jump bar, the chrome layer and the stage lined up side by side, the stage taking a third of the pane and pinning the document to the right edge. `grid-template-columns` is what makes \"share the cell\" mean it, and every row-2 child restates `grid-column: 1` so a future fourth one cannot re-open the same hole. No unit test can see this — happy-dom does no layout, so every rect is zero — which is why the screenshot lane is what caught it.",
30
+ "$description": "ONE PANE, and one COLUMN, declared. Four children name `grid-row: 2` in order to share the lower cell, and auto-placement answers that by inventing an implicit column each: the jump bar, the chrome layer, the stage and the drop zone lined up side by side, the stage taking a third of the pane and pinning the document to the right edge. `grid-template-columns` is what makes \"share the cell\" mean it, and every row-2 child restates `grid-column: 1` so a future fifth one cannot re-open the same hole. No unit test can see this — happy-dom does no layout, so every rect is zero — which is why the screenshot lane is what caught it.",
31
31
  "display": "grid",
32
32
  "gridTemplateColumns": "minmax(0, 1fr)",
33
33
  "gridTemplateRows": "auto minmax(0, 1fr)",
@@ -82,11 +82,33 @@
82
82
  "display": "flex",
83
83
  "gap": "var(--jx-space-6)",
84
84
  "alignItems": "flex-start"
85
+ },
86
+ "& > [part=\"row\"] > [part=\"pane\"] > [part=\"drop-zone\"]": {
87
+ "$description": "The right-edge target that creates a second pane (§18.1). Sits ABOVE the stage (`z-index: 4` against the stage's own stacking, jump's 3, chrome's 2) so a drag reaches it before it reaches the cross-origin iframe underneath, which would otherwise swallow every drag event with no affordance to say so. `pointer-events: none` when unarmed — a mouse doing anything other than dragging a tab or a file must pass straight through to the stage — and `auto` only while `data-armed` is set, which `panels/pane-grid.ts`'s own arming monitor writes for the DURATION of a drag it recognises, never for a click.",
88
+ "gridRow": "2",
89
+ "gridColumn": "1",
90
+ "position": "relative",
91
+ "zIndex": "4",
92
+ "minWidth": "0",
93
+ "pointerEvents": "none"
94
+ },
95
+ "& > [part=\"row\"] > [part=\"pane\"] > [part=\"drop-zone\"][data-armed]": {
96
+ "pointerEvents": "auto"
97
+ },
98
+ "& > [part=\"row\"] > [part=\"pane\"] > [part=\"drop-zone\"][data-over]::after": {
99
+ "$description": "The right BAND, not the whole cell: the zone only ever ACCEPTS a drop in that band (`panels/pane-grid.ts`'s `edgeData`), so lighting the whole cell would promise a drop this element refuses everywhere else in it.",
100
+ "content": "\"\"",
101
+ "position": "absolute",
102
+ "insetBlock": "0",
103
+ "insetInlineEnd": "0",
104
+ "insetInlineStart": "50%",
105
+ "background": "color-mix(in srgb, var(--accent) 14%, transparent)",
106
+ "borderInlineStart": "2px solid var(--accent)"
85
107
  }
86
108
  },
87
109
  "children": [
88
110
  {
89
- "$description": "Keyed on the PANE ID, and that is the whole of §18.1 rule 1's structural half: the key is what makes the runtime MOVE a kept row rather than rebuild it, and re-parenting is not a move for an `<iframe>` — it reloads, dropping its `iframe-channel` connection, its shadow document and every `ready` panel with it. A row renders complete (its cell, its four hosts and its splitter) inside the row's own effect scope before it is inserted, so there is no frame in which a cell exists with no stage inside it.",
111
+ "$description": "Keyed on the PANE ID, and that is the whole of §18.1 rule 1's structural half: the key is what makes the runtime MOVE a kept row rather than rebuild it, and re-parenting is not a move for an `<iframe>` — it reloads, dropping its `iframe-channel` connection, its shadow document and every `ready` panel with it. A row renders complete (its cell, its five hosts and its splitter) inside the row's own effect scope before it is inserted, so there is no frame in which a cell exists with no stage inside it.",
90
112
  "$prototype": "Array",
91
113
  "items": { "$ref": "#/state/rows" },
92
114
  "key": { "$ref": "$map/item/id" },
@@ -178,6 +200,16 @@
178
200
  "part": "pane-stage",
179
201
  "data-jx-region": "${$map.item.region}"
180
202
  }
203
+ },
204
+ {
205
+ "$description": "The right-edge drop target (§18.1). No `data-jx-region`: `pane.<id>` uniqueness names the stage, and a second element answering to it would be a silently widened crop the moment a shot cropped this pane.",
206
+ "tagName": "div",
207
+ "attributes": {
208
+ "part": "drop-zone",
209
+ "role": "none",
210
+ "data-armed": "${state.armed ? '' : null}",
211
+ "data-over": "${state.edgePane === $map.item.id ? '' : null}"
212
+ }
181
213
  }
182
214
  ]
183
215
  }
@@ -63,8 +63,8 @@ export const PANE_SELECTOR = `[part="${PANE_PART}"]`;
63
63
  */
64
64
  export const STAGE_SELECTOR = `[part="${STAGE_PART}"]`;
65
65
 
66
- /** The five boxes a cell is made of, in the order the document draws them. */
67
- export type PaneCellPart = "pane" | "strip" | "jump" | "chrome" | "stage";
66
+ /** The six boxes a cell is made of, in the order the document draws them. */
67
+ export type PaneCellPart = "pane" | "strip" | "jump" | "chrome" | "stage" | "dropZone";
68
68
 
69
69
  const CELL_PARTS: ReadonlyMap<string, PaneCellPart> = new Map([
70
70
  ["pane", "pane"],
@@ -72,6 +72,7 @@ const CELL_PARTS: ReadonlyMap<string, PaneCellPart> = new Map([
72
72
  ["jump", "jump"],
73
73
  ["chrome", "chrome"],
74
74
  ["pane-stage", "stage"],
75
+ ["drop-zone", "dropZone"],
75
76
  ]);
76
77
 
77
78
  /**
@@ -134,6 +135,14 @@ export interface PaneGridActions {
134
135
  settle: () => void;
135
136
  }
136
137
 
138
+ /** Whether the right-edge zones are armed, and which pane's is under the pointer. */
139
+ export interface PaneGridDragState {
140
+ /** Whether ANY tab-drop-eligible drag is live — the zones' shared `pointer-events` gate. */
141
+ armed: boolean;
142
+ /** The pane whose zone is under the pointer, or `""` for none. */
143
+ edgePane: string;
144
+ }
145
+
137
146
  export interface PaneGridSurface {
138
147
  /** Resolves once the first projection is in the document. */
139
148
  ready: Promise<void>;
@@ -146,12 +155,18 @@ export interface PaneGridSurface {
146
155
  * gains or loses a pane, which is the only moment anything else could have moved it.
147
156
  */
148
157
  update: (rows: PaneGridRow[], split: number) => void;
158
+ /**
159
+ * Write the right-edge indicators, independently of {@link update} — the same reason
160
+ * `TabStripSurface.drag` is separate from its own `update`: a re-project the drag itself did not
161
+ * cause must not clear an indicator the arming monitor just lit.
162
+ */
163
+ drag: (state: PaneGridDragState) => void;
149
164
  /** Take the document down and give `#pane-grid` back empty. Idempotent. */
150
165
  dispose: () => void;
151
166
  }
152
167
 
153
168
  /** The scope `pane-grid.json` reads. */
154
- interface PaneGridScope extends Record<string, unknown> {
169
+ interface PaneGridScope extends Record<string, unknown>, PaneGridDragState {
155
170
  rows: PaneGridRow[];
156
171
  /** `jx-split`'s `value`. */
157
172
  paneSplit: number;
@@ -202,6 +217,8 @@ export function mountPaneGridSurface(
202
217
  ): PaneGridSurface {
203
218
  host.replaceChildren();
204
219
  const scope = reactive<PaneGridScope>({
220
+ armed: false,
221
+ edgePane: "",
205
222
  move: actions.move,
206
223
  paneSplit: split.value,
207
224
  rows,
@@ -217,7 +234,7 @@ export function mountPaneGridSurface(
217
234
  const ready = mountSurface("pane-grid", scope, host, {
218
235
  /* No `instanceof` narrowing and no "is there a pane" guard, and both absences are structural.
219
236
  `pane-grid.json` draws no text and no `textContent`, so every node the runtime reports here
220
- is an element; and the five cell parts exist only inside the repeater's rows, so a `$map`
237
+ is an element; and the six cell parts exist only inside the repeater's rows, so a `$map`
221
238
  scope is always there to name the pane. A guard on either would be a branch the document
222
239
  cannot reach — which is a line no test could ever cover, which is a claim nothing checks. */
223
240
  onNodeCreated: (element, _path, def, state) => {
@@ -241,6 +258,11 @@ export function mountPaneGridSurface(
241
258
  mounted = null;
242
259
  host.replaceChildren();
243
260
  },
261
+ drag(state) {
262
+ // Independent of `update`'s field list — see {@link PaneGridSurface.drag}.
263
+ scope.armed = state.armed;
264
+ scope.edgePane = state.edgePane;
265
+ },
244
266
  ready,
245
267
  update(next, value) {
246
268
  scope.rows = next;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://jxsuite.com/schema/v1",
3
3
  "$id": "StudioTabStrip",
4
- "$description": "One pane's tab strip, as a Jx document over the kit: a real `tablist` of `jx-tab`s, or — for a pane that owns no document — the derivation chip naming what it is a projection of, and in both cases one trailing button. `panels/tab-strip.ts` keeps every decision: which pane a host is drawing, what each tab is called (the shortest unique suffix, and a page's ROUTE), what its tooltip says, whether closing it needs a prompt, what the overflow chevron lists and what the context menu offers. This document owns the markup, the ARIA, the keyboard and the style, and it holds THREE things the old chip drew by hand and the kit now draws: the tab's own selected state (`aria-selected`, not an `.active` class), its close button, and its dirty dot. The three that are Studio's alone go in `jx-tab`'s slots — the drill-in marker in `icon`, the draft pill in `status`, the pin toggle in `actions` (ui.md §5.4) — which is the gap that kept this strip out of the tab pattern until now.",
4
+ "$description": "One pane's tab strip, as a Jx document over the kit: a real `tablist` of `jx-tab`s, or — for a pane that owns no document — the derivation chip naming what it is a projection of, and in both cases one trailing button. `panels/tab-strip.ts` keeps every decision: which pane a host is drawing, what each tab is called (the shortest unique suffix, and a page's ROUTE), what its tooltip says, whether closing it needs a prompt, what the overflow chevron lists and what the context menu offers. This document owns the markup, the ARIA, the keyboard and the style, and it holds THREE things the old chip drew by hand and the kit now draws: the tab's own selected state (`aria-selected`, not an `.active` class), its close button, and its dirty dot. The three that are Studio's alone go in `jx-tab`'s slots — the drill-in marker in `icon`, the draft pill in `status`, the pin toggle in `actions` (ui.md §5.4) — which is the gap that kept this strip out of the tab pattern until now. **Drag is `@atlaskit/pragmatic-drag-and-drop`, not native HTML5 DnD**: a chip carries no `draggable` attribute and no `ondrag*` handler, because `panels/tab-strip.ts` registers every chip and every strip as a pragmatic source/target through `onNodeCreated` and resolves a completed drag in `panels/tab-drop.ts`. What is left here is three SCOPE comparisons, exactly as `files-panel.json` marks a row under drag: `data-dragging` on the chip being carried, `data-drop` on the chip a drop would land beside, `data-drop-tail` on the strip when the drop is its empty tail.",
5
5
  "tagName": "div",
6
6
  "attributes": {
7
7
  "part": "strip-row",
@@ -44,6 +44,13 @@
44
44
  "display": "none"
45
45
  }
46
46
  },
47
+ "& [part=\"tabs\"][data-drop-tail]::after": {
48
+ "$description": "The strip's own tail target: a drop onto the empty space after the last chip, or onto a pane whose strip carries no chips at all — an empty pane made a target during a drag by `panels/tab-strip.ts`'s `project`. A fixed-width bar rather than a background tint, so it reads the same as a chip's own `data-drop` edge instead of a second visual language for the same gesture.",
49
+ "content": "\"\"",
50
+ "flex": "0 0 2px",
51
+ "alignSelf": "stretch",
52
+ "background": "var(--accent)"
53
+ },
47
54
  "& [part=\"tab\"]": {
48
55
  "$description": "`.tab-strip-tab`. The kit's tab caps itself at 220px and reserves a control's height; a chrome row is tighter than that and its labels are the shortest unique suffix, so both are re-declared. `flex-shrink: 0` is what makes the strip scroll rather than squeeze fifteen documents into one width.",
49
56
  "gap": "var(--jx-space-1)",
@@ -64,6 +71,10 @@
64
71
  },
65
72
  "&[data-dragging]": {
66
73
  "opacity": "0.4"
74
+ },
75
+ "&[data-drop]": {
76
+ "$description": "The chip a drop would land BESIDE. An inset shadow rather than a border, so the indicator draws no layout shift mid-drag — the same reason `files-panel.json`'s row indicators are backgrounds rather than borders.",
77
+ "boxShadow": "inset 2px 0 0 var(--accent)"
67
78
  }
68
79
  },
69
80
  "& [part=\"tab\"] [part=\"label\"]": {
@@ -206,7 +217,8 @@
206
217
  "attributes": {
207
218
  "part": "tabs",
208
219
  "label": "${state.stripLabel}",
209
- "selected": "${state.active}"
220
+ "selected": "${state.active}",
221
+ "data-drop-tail": "${state.dropTail ? '' : null}"
210
222
  },
211
223
  "onchange": {
212
224
  "$prototype": "Function",
@@ -245,11 +257,11 @@
245
257
  "label": "${$map.item.label}",
246
258
  "title": "${$map.item.title}",
247
259
  "closable": "",
248
- "draggable": "true",
249
260
  "dirty": "${$map.item.dirty ? '' : null}",
250
261
  "data-pinned": "${$map.item.pinned ? '' : null}",
251
262
  "data-preview": "${$map.item.preview ? '' : null}",
252
- "data-dragging": "${$map.item.dragging ? '' : null}"
263
+ "data-dragging": "${state.dragKey === $map.item.key ? '' : null}",
264
+ "data-drop": "${state.dropIndex === $map.item.index ? '' : null}"
253
265
  },
254
266
  "ondblclick": {
255
267
  "$prototype": "Function",
@@ -297,41 +309,6 @@
297
309
  }
298
310
  ]
299
311
  },
300
- "ondragstart": {
301
- "$prototype": "Function",
302
- "body": [
303
- {
304
- "operator": "call",
305
- "target": { "$ref": "#/state/dragStart" },
306
- "value": [{ "$ref": "$map/item/key" }, { "$ref": "event#/dataTransfer" }]
307
- }
308
- ]
309
- },
310
- "ondragend": {
311
- "$prototype": "Function",
312
- "body": [
313
- {
314
- "operator": "call",
315
- "target": { "$ref": "#/state/dragEnd" },
316
- "value": []
317
- }
318
- ]
319
- },
320
- "ondragover": {
321
- "$prototype": "Function",
322
- "body": [{ "preventDefault": true }]
323
- },
324
- "ondrop": {
325
- "$prototype": "Function",
326
- "body": [
327
- { "preventDefault": true },
328
- {
329
- "operator": "call",
330
- "target": { "$ref": "#/state/drop" },
331
- "value": [{ "$ref": "$map/item/index" }, { "$ref": "event#/dataTransfer" }]
332
- }
333
- ]
334
- },
335
312
  "children": [
336
313
  {
337
314
  "$description": "The drill-in marker: this document was opened FROM another one. `jx-tab`'s icon slot draws it before the label, which is where it has always been.",
@@ -31,7 +31,8 @@
31
31
  import { reactive } from "../reactivity";
32
32
  import { mountSurface, registerSurface } from "../ui/surface";
33
33
  import tabStripDoc from "./tab-strip.json";
34
- import type { JxDocument } from "@jxsuite/schema/types";
34
+ import type { JxDocument, JxElement } from "@jxsuite/schema/types";
35
+ import type { JxScope } from "@jxsuite/runtime/types";
35
36
  import type { SurfaceHandle } from "../ui/surface";
36
37
 
37
38
  registerSurface("tab-strip", tabStripDoc as unknown as JxDocument);
@@ -50,7 +51,6 @@ export interface TabChipView extends Record<string, unknown> {
50
51
  dirty: boolean;
51
52
  pinned: boolean;
52
53
  preview: boolean;
53
- dragging: boolean;
54
54
  /** Whether to draw the drill-in marker. */
55
55
  origin: boolean;
56
56
  /** "Pin" or "Unpin" — the pin button's whole accessible name. */
@@ -106,18 +106,39 @@ export interface TabStripActions {
106
106
  context: (id: string, x: number, y: number) => void;
107
107
  /** The chevron, or a derived pane's ✕. The anchor is the button that was pressed. */
108
108
  openTrailing: (anchor: HTMLElement | null) => void;
109
- /** A drag left this chip. The `DataTransfer` is the platform's and may be absent. */
110
- dragStart: (id: string, transfer: DataTransfer | null) => void;
111
- dragEnd: () => void;
112
- /** A drop landed on the chip at `index`. */
113
- drop: (index: number, transfer: DataTransfer | null) => void;
114
109
  /** A wheel over the strip. A host function in handler position, so it takes the event. */
115
110
  wheel: (scope: unknown, event: WheelEvent) => void;
111
+ /** A chip element exists. `panels/tab-strip.ts` makes it a pragmatic drag source and drop target. */
112
+ chipHost: (element: HTMLElement, id: string) => void;
113
+ /**
114
+ * The `jx-tabs` element exists. `panels/tab-strip.ts` makes it a drop target of its own — the
115
+ * strip's empty tail, and the whole of an empty pane's strip while a drag is live.
116
+ */
117
+ stripHost: (element: HTMLElement) => void;
118
+ }
119
+
120
+ /**
121
+ * What one strip shows about the drag in progress — written independently of {@link TabStripValues}
122
+ * so the indicators survive an ordinary re-project mid-drag.
123
+ */
124
+ export interface TabStripDragState {
125
+ /** The tab id being carried, or `""` when nothing from this strip is. */
126
+ dragKey: string;
127
+ /** The chip a drop would land beside, by slot index, or `-1` for none. */
128
+ dropIndex: number;
129
+ /** Whether a drop would land in the strip's empty tail. */
130
+ dropTail: boolean;
116
131
  }
117
132
 
118
133
  export interface TabStripSurface {
119
134
  /** Bring the standing document up to date. An assignment; the mount is never rebuilt. */
120
135
  update: (values: TabStripValues) => void;
136
+ /**
137
+ * Write the drag indicators, independently of {@link update}. The one writer of `dragKey`,
138
+ * `dropIndex` and `dropTail` — `update` never touches them, which is what keeps an indicator lit
139
+ * across a repaint the drag itself did not cause.
140
+ */
141
+ drag: (state: TabStripDragState) => void;
121
142
  /** The `jx-tabs` element, once it exists — the one thing a measurement needs. */
122
143
  strip: () => HTMLElement | null;
123
144
  /** Take the document down and give the host back empty. Idempotent. */
@@ -125,7 +146,21 @@ export interface TabStripSurface {
125
146
  }
126
147
 
127
148
  /** The scope `tab-strip.json` reads. */
128
- interface TabStripScope extends Record<string, unknown>, TabStripValues, TabStripActions {}
149
+ interface TabStripScope
150
+ extends Record<string, unknown>, TabStripValues, TabStripActions, TabStripDragState {}
151
+
152
+ /** The `part` a node's definition carries, or `""` for a text node or an unmarked element. */
153
+ function partOf(def: JxElement | string): string {
154
+ const part = typeof def === "string" ? undefined : def.attributes?.["part"];
155
+ return typeof part === "string" ? part : "";
156
+ }
157
+
158
+ /** The tab id a mapped `jx-tab` node was rendered for, read from the row scope, or `""`. */
159
+ function mappedChipKey(state: JxScope | undefined): string {
160
+ const map = state?.["$map"] as { item?: { key?: unknown } } | undefined;
161
+ const key = map?.item?.key;
162
+ return typeof key === "string" ? key : "";
163
+ }
129
164
 
130
165
  /**
131
166
  * Mount one strip into `host`.
@@ -143,17 +178,38 @@ export function mountTabStripSurface(
143
178
  values: TabStripValues,
144
179
  actions: TabStripActions,
145
180
  ): TabStripSurface {
146
- const scope = reactive<TabStripScope>({ ...values, ...actions }) as TabStripScope;
181
+ const scope = reactive<TabStripScope>({
182
+ ...values,
183
+ ...actions,
184
+ dragKey: "",
185
+ dropIndex: -1,
186
+ dropTail: false,
187
+ }) as TabStripScope;
147
188
 
148
189
  host.replaceChildren();
149
190
  let mounted: SurfaceHandle | null = null;
150
191
  let disposed = false;
151
192
  let strip: HTMLElement | null = null;
152
193
  void mountSurface("tab-strip", scope, host, {
153
- onNodeCreated: (element, _path, def) => {
154
- const part = typeof def === "string" ? undefined : def.attributes?.["part"];
155
- if (part === "tabs" && element instanceof HTMLElement) {
194
+ onNodeCreated: (element, _path, def, state) => {
195
+ /* `disposed` first, for the reason `files-panel.ts`'s `mountFilesPanelSurface` guards it: the
196
+ mount is asynchronous, and a strip taken down inside the same turn — the pane whose host
197
+ this is has gone away — still walks this callback for every node its document creates.
198
+ Adopting one then would register a drag source nothing is left to release. */
199
+ if (disposed || !(element instanceof HTMLElement)) {
200
+ return;
201
+ }
202
+ const part = partOf(def);
203
+ if (part === "tabs") {
156
204
  strip = element;
205
+ actions.stripHost(element);
206
+ return;
207
+ }
208
+ if (part === "tab") {
209
+ const id = mappedChipKey(state);
210
+ if (id) {
211
+ actions.chipHost(element, id);
212
+ }
157
213
  }
158
214
  },
159
215
  }).then((surface) => {
@@ -172,6 +228,14 @@ export function mountTabStripSurface(
172
228
  strip = null;
173
229
  host.replaceChildren();
174
230
  },
231
+ drag(state) {
232
+ /* Independent of {@link update}'s field list, which is the whole point: these three are
233
+ written by the drag monitor on every pointer move, and `update`'s ordinary re-project must
234
+ not clear them out from under a drag it did not cause. */
235
+ scope.dragKey = state.dragKey;
236
+ scope.dropIndex = state.dropIndex;
237
+ scope.dropTail = state.dropTail;
238
+ },
175
239
  strip: () => strip,
176
240
  update(next) {
177
241
  /* Field by field, so an unchanged one is inert. A whole-object assignment would re-run every