@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
@@ -43,9 +43,9 @@ import {
43
43
  activePane,
44
44
  detachTab,
45
45
  insertIntoPane,
46
+ paneBeside,
46
47
  paneById,
47
48
  promoteTab,
48
- sidePane,
49
49
  workspace,
50
50
  } from "./workspace";
51
51
  import {
@@ -1143,9 +1143,11 @@ function pinnablePane(): Pane | null {
1143
1143
  * **Unlike `pane.derive`, the COMMAND's subject is the grid rather than the focused pane**, and
1144
1144
  * that is deliberate: the author reaches "Keep This Document" from the palette while the keyboard
1145
1145
  * is in the page they are editing, and a grid holds at most one derived pane — `pane.derive` puts
1146
- * the projection in `sidePane()`, and `workspace.ts`'s `MAX_PANES` is 2. {@link pinnablePane} finds
1147
- * it by scanning, which reads no focus and is therefore the same answer from anywhere — so the
1148
- * preset menu can ask it about its own pane and get an answer about its own pane.
1146
+ * the projection in `paneBeside(source.id)`, and `workspace.ts`'s `MAX_PANES` is 2.
1147
+ * {@link
1148
+ * pinnablePane} finds it by scanning, which reads no focus and is therefore the same answer
1149
+ * from anywhere — so the preset menu can ask it about its own pane and get an answer about its own
1150
+ * pane.
1149
1151
  *
1150
1152
  * @param {string} paneId
1151
1153
  * @returns {string | null}
@@ -1227,7 +1229,7 @@ export function derivationCommands(deps: DerivationDeps): AnyCommand[] {
1227
1229
  `command "pane.derive" argument "preset": "${preset}" requires ${refusal}`,
1228
1230
  );
1229
1231
  }
1230
- const target = sidePane();
1232
+ const target = paneBeside(source.id);
1231
1233
  /* The DERIVATION IS PUBLISHED FIRST, and the order is not cosmetic.
1232
1234
  §18.1 rule 3 removes a pane with no subject, and `detachTab` applies it as each tab
1233
1235
  leaves — so handing the side pane's tabs back before it had a derivation collapsed the
@@ -217,9 +217,9 @@ export interface RestoreDeps {
217
217
  /**
218
218
  * Make sure the grid HAS a second pane, without moving anything into it.
219
219
  *
220
- * `receivingPane()`, not `splitRight()`: a split moves the focused pane's active tab, and at this
221
- * point nothing is open — so `splitRight()` returns `null`, the pane is never created, and the
222
- * second pane's documents all land in the first.
220
+ * `receivingPane(PRIMARY_PANE)`, not `splitRight()`: a split moves the focused pane's active tab,
221
+ * and at this point nothing is open — so `splitRight()` returns `null`, the pane is never
222
+ * created, and the second pane's documents all land in the first.
223
223
  */
224
224
  ensureSecondPane: () => void;
225
225
  }
@@ -10,6 +10,7 @@ import {
10
10
  stringArg,
11
11
  stringProperty,
12
12
  } from "../commands/command-args";
13
+ import { EDITOR_KIND_LABELS } from "../commands/context";
13
14
  import type { Tab, TabOrigin } from "../tabs/tab";
14
15
  import type { JsonLayout } from "../files/json-layout";
15
16
 
@@ -367,68 +368,82 @@ export function focusOtherPane() {
367
368
  */
368
369
 
369
370
  /**
370
- * Split the grid and move the focused tab into the new pane, which becomes focused.
371
+ * Split the grid and move a tab into the new pane, which becomes focused.
371
372
  *
372
373
  * Moves rather than duplicates: one tab is one document with one undo history and one collab
373
374
  * session, and two strips claiming the same id is the duplicate-`repeat`-key bug §4.3 describes.
374
375
  *
376
+ * The subject is `tabId` when given, else the focused pane's active tab. The drag resolver names
377
+ * the tab it is carrying — the gesture points at a chip, not at whatever the keyboard is in — and
378
+ * `⌘\` and `pane.splitRight` call it bare, which is the focused tab as it has always been.
379
+ *
380
+ * @param {string} [tabId] The tab to move; defaults to the focused pane's active tab.
375
381
  * @returns {Pane | null} The pane the tab landed in, or null when the split was refused
376
382
  */
377
- export function splitRight(): Pane | null {
378
- const source = activePane();
379
- const tabId = source.activeTabId;
380
- const tab = tabId ? workspace.tabs.get(tabId) : null;
381
- if (!tabId || !tab) {
383
+ export function splitRight(tabId?: string): Pane | null {
384
+ const id = tabId ?? activePane().activeTabId;
385
+ const tab = id ? workspace.tabs.get(id) : null;
386
+ if (!id || !tab) {
387
+ return null;
388
+ }
389
+ const source = paneOfTab(id);
390
+ if (!source) {
382
391
  return null;
383
392
  }
384
393
  /* The tab moves AS IT IS. `capToPaneKind` used to run here and rewrite `session.ui.canvasMode`
385
394
  when the target pane could not host the tab's kind — which, for a Design tab, meant `⌘\`
386
395
  silently reopened your page as Code in the pane you had just made. Both panes host every kind
387
396
  now, so a split is a move and nothing else. */
388
- const target = receivingPane();
389
- detachTab(tabId);
390
- insertIntoPane(target, tabId);
397
+ const target = receivingPane(source.id);
398
+ detachTab(id);
399
+ insertIntoPane(target, id);
391
400
  /* Focus moves LAST, and every write above went through {@link addPane}'s reactive record.
392
401
  Publishing the focus first made the pane observable while it still showed nothing: the
393
402
  `activeTab` computed re-ran, cached null, and the jump bar, the Inspector and the toolbar all
394
403
  printed "no document" over a stage that was drawing one. The write that should have corrected
395
404
  them notified nobody, because it went through the raw literal this used to push. */
396
- target.activeTabId = tabId;
405
+ target.activeTabId = id;
397
406
  workspace.activePaneId = target.id;
398
407
  resetTabCycle();
399
- promoteMru(tabId);
408
+ promoteMru(id);
400
409
  return target;
401
410
  }
402
411
 
403
412
  /**
404
- * The pane BESIDE the focused one, created if the grid has only one.
413
+ * The pane beside `paneId`, created if the grid has only one.
405
414
  *
406
415
  * The `existing ?? addPane(SECONDARY_PANE)` shape {@link splitRight} has always used, named because
407
- * three callers now want it: the split, `pane.compareWith` and `pane.derive`. Unlike `splitRight`
408
- * it moves NOTHING — "give me the other pane" and "send my document to it" are two requests, and
409
- * only the split makes both.
416
+ * four callers now want it: the split, `pane.compareWith`, `pane.derive` and the drag resolver.
417
+ * Unlike `splitRight` it moves NOTHING — "give me the other pane" and "send my document to it" are
418
+ * two requests, and only the split makes both.
419
+ *
420
+ * Takes the pane it is asked about rather than reading the focus, because a drag names the pane its
421
+ * chip came from and `pane.derive`'s preset menu asks about its own pane — and
422
+ * `scripts/check-pane-singletons.ts` rule 4 refuses a function handed a `paneId` that reads the
423
+ * focus. The one caller that means "beside ME" passes `activePane().id`.
410
424
  *
411
425
  * `SECONDARY_PANE` unconditionally, and it is free unconditionally: {@link closePane} refuses to
412
426
  * remove the primary, so "there is no other pane" can only mean the grid is exactly `[primary]` and
413
- * the focused pane is that primary. This used to be `existing?.id ?? SECONDARY_PANE` handed
427
+ * the asked-about pane is that primary. This used to be `existing?.id ?? SECONDARY_PANE` handed
414
428
  * straight to `addPane` with no check that the id was free — which, on a grid of `["secondary"]`,
415
429
  * pushed a SECOND record under that id and gave lit's keyed `repeat` a duplicate key. Both ends are
416
430
  * closed: the state is unreachable, and {@link addPane} would not mint the duplicate even if it
417
431
  * were. {@link MAX_PANES} is therefore satisfied by construction — there are two ids in play.
418
432
  *
433
+ * @param {string} paneId The pane to answer "beside" for.
419
434
  * @returns {Pane}
435
+ * @docs studio/interface/tabs
420
436
  */
421
- export function sidePane(): Pane {
422
- const source = activePane();
423
- return workspace.panes.find((pane) => pane.id !== source.id) ?? addPane(SECONDARY_PANE);
437
+ export function paneBeside(paneId: string): Pane {
438
+ return workspace.panes.find((pane) => pane.id !== paneId) ?? addPane(SECONDARY_PANE);
424
439
  }
425
440
 
426
441
  /**
427
- * The pane beside the focused one, ready to OWN a tab.
442
+ * The pane beside `paneId`, ready to OWN a tab.
428
443
  *
429
- * {@link sidePane} answers "which pane is beside me". This answers "which pane may I put a document
430
- * IN", and they differ by exactly one case — a LENS, whose invariant D2 is that it owns no tab at
431
- * all. Three callers mean the second question and all three were asking the first:
444
+ * {@link paneBeside} answers "which pane is beside that one". This answers "which pane may I put a
445
+ * document IN", and they differ by exactly one case — a LENS, whose invariant D2 is that it owns no
446
+ * tab at all. Four callers mean the second question and all four were asking the first:
432
447
  *
433
448
  * - `splitRight` moved the focused tab into a lens. Between that write and the follow's next frame
434
449
  * the pane held both a derivation and a tab — D2 violated and observable — and `applyDerivation`
@@ -440,6 +455,8 @@ export function sidePane(): Pane {
440
455
  * touches — never ran to repair it.
441
456
  * - `navigateToComponent` read `tabOfPane(target.id)` back and got the SOURCE tab, so the §14.2
442
457
  * `openedFrom` relationship the function exists to record was silently skipped.
458
+ * - The drag resolver dropped a tab or a file onto a lens's strip, where the same three failures
459
+ * apply with a gesture instead of a command.
443
460
  *
444
461
  * **The derivation is released rather than the request refused.** "Put this document over there" is
445
462
  * an explicit instruction about the pane a projection is currently borrowing, and a projection is a
@@ -455,10 +472,11 @@ export function sidePane(): Pane {
455
472
  * `activeTabId` — promotes whichever document is on top, which after `⌘\` is the page, not the
456
473
  * layout the author was keeping. A gesture that empties a derivation of its meaning ends it.
457
474
  *
475
+ * @param {string} paneId The pane the receiving pane sits beside.
458
476
  * @returns {Pane}
459
477
  */
460
- export function receivingPane(): Pane {
461
- const target = sidePane();
478
+ export function receivingPane(paneId: string): Pane {
479
+ const target = paneBeside(paneId);
462
480
  if (target.derived) {
463
481
  /* The same write {@link closePane} makes for D4, for the same reason and in the same module:
464
482
  clearing a derivation is a fact about the PANE GRID. `pane-derive.ts`'s
@@ -581,8 +599,11 @@ export function closePane(paneId: string) {
581
599
  * same question while every pane's subject was its tabs. A lens pane's subject is its derivation —
582
600
  * its `tabOrder` is empty BY DESIGN — so the literal test would collapse it the instant it was
583
601
  * created.
602
+ *
603
+ * Exported for `panels/tab-drop.ts`, whose edge-drop resolution refuses to leave a pane it minted
604
+ * standing empty over a refused open — the same rule `document.openToSide` applies in-module.
584
605
  */
585
- function paneIsEmpty(pane: Pane): boolean {
606
+ export function paneIsEmpty(pane: Pane): boolean {
586
607
  return pane.tabOrder.length === 0 && pane.derived === null;
587
608
  }
588
609
 
@@ -758,12 +779,12 @@ function syncTreeSelection(tab: Tab) {
758
779
  /**
759
780
  * Make `tabId` the active tab OF ITS OWN PANE. The one place activation happens.
760
781
  *
761
- * `focus: false` is "put it on screen there, and leave the keyboard where it is" — the shape a
762
- * side-open needs. It writes `pane.activeTabId` and stops: no `activePaneId` write, no
763
- * `resetTabCycle`, no `promoteMru`, no `syncTreeSelection`, because every one of those describes
764
- * where the AUTHOR is and the author has not moved. Drilling into a component with the focus
765
- * following it is the failure it exists to prevent: the pane the author is typing in changes under
766
- * them, and their next keystroke edits the definition instead of the page.
782
+ * `focus: false` is "put it on screen there, and leave the keyboard where it is" — the shape a READ
783
+ * needs: the derivation follow, `pane.compareWith`, session restore. It writes `pane.activeTabId`
784
+ * and stops: no `activePaneId` write, no `resetTabCycle`, no `promoteMru`, no `syncTreeSelection`,
785
+ * because every one of those describes where the AUTHOR is and the author has not moved. The
786
+ * gestures that OPEN something — drilling into a component, Open to the Side — follow instead: the
787
+ * pane they open into takes the keyboard, because the author asked to go there.
767
788
  *
768
789
  * Legal here and nowhere else — this module is `check-pane-singletons.ts`'s `FOCUS_OWNER`, and the
769
790
  * default is still to move the focus, so no existing caller changes behaviour.
@@ -808,10 +829,12 @@ function setActiveTab(tabId: string, opts: { cycling?: boolean; focus?: boolean
808
829
  *
809
830
  * `paneId` says WHERE, and `focus` says whether the keyboard goes with it. Both default to today's
810
831
  * answer — the focused pane, and yes — because "open this document" has always meant "in front of
811
- * me". They exist because drilling into a component, comparing two documents and following a
812
- * selection all mean the opposite: put it in the pane beside this one and leave me where I am. A
813
- * `paneId` no pane carries falls back to the focused pane rather than dropping the open, which is
814
- * the same answer a stale persisted layout gets everywhere else in this module.
832
+ * me". They exist because the gestures that mean the opposite are the READS: comparing two
833
+ * documents and following a derivation put it in the pane beside this one and leave the author
834
+ * where they are. The gestures that OPEN something — drill-in, Open to the Side — pass neither: the
835
+ * pane they open into takes the keyboard, because the author asked to go there. A `paneId` no pane
836
+ * carries falls back to the focused pane rather than dropping the open, which is the same answer a
837
+ * stale persisted layout gets everywhere else in this module.
815
838
  *
816
839
  * @param {{
817
840
  * id: string;
@@ -959,12 +982,33 @@ export function moveTab(tabId: string, toIndex: number) {
959
982
  if (!tab || !pane) {
960
983
  return;
961
984
  }
985
+ pane.tabOrder = reorderWithinPane(pane, tabId, toIndex);
986
+ }
987
+
988
+ /**
989
+ * The order `pane`'s strip takes when `tabId` moves to `toIndex`, clamped into the region the tab's
990
+ * pinned state allows — the pinned prefix and the unpinned tail never interleave.
991
+ *
992
+ * Extracted from `moveTab` because the cross-pane move needs the SAME clamp: a tab dragged into
993
+ * another pane's strip at index 0 must land after that pane's pinned prefix, not inside it, and
994
+ * duplicating the boundary arithmetic is how the two moves drift apart.
995
+ *
996
+ * @param {Pane} pane
997
+ * @param {string} tabId
998
+ * @param {number} toIndex
999
+ * @returns {string[]} The pane's new `tabOrder`.
1000
+ */
1001
+ function reorderWithinPane(pane: Pane, tabId: string, toIndex: number): string[] {
1002
+ const tab = workspace.tabs.get(tabId);
1003
+ if (!tab) {
1004
+ return pane.tabOrder;
1005
+ }
962
1006
  const without = pane.tabOrder.filter((id) => id !== tabId);
963
1007
  const boundary = without.filter((id) => workspace.tabs.get(id)?.pinned === true).length;
964
1008
  const lower = tab.pinned ? 0 : boundary;
965
1009
  const upper = tab.pinned ? boundary : without.length;
966
1010
  const at = Math.min(Math.max(toIndex, lower), upper);
967
- pane.tabOrder = [...without.slice(0, at), tabId, ...without.slice(at)];
1011
+ return [...without.slice(0, at), tabId, ...without.slice(at)];
968
1012
  }
969
1013
 
970
1014
  /**
@@ -1176,20 +1220,30 @@ export function activateTab(tabId: string, opts: { focus?: boolean } = {}) {
1176
1220
  *
1177
1221
  * Idempotent for a tab already in `paneId`, which is what makes it safe on the follow path.
1178
1222
  *
1223
+ * `index` is the slot in `paneId`'s order the tab takes, clamped into the region the tab's pinned
1224
+ * state allows — the same clamp `moveTab` applies, so a cross-pane move can never interleave a
1225
+ * pinned tab with an unpinned one. Omitted, the tab takes the default slot: inside the pinned
1226
+ * prefix if pinned, else at the end. A same-pane move with an index delegates to `moveTab`, so the
1227
+ * drag-reorder and the cross-pane move share one clamp rather than two.
1228
+ *
1179
1229
  * @param {string} tabId
1180
1230
  * @param {string} paneId
1231
+ * @param {number} [index]
1181
1232
  * @returns {Pane | null} The pane it landed in, or null when either id names nothing.
1182
1233
  */
1183
- export function moveTabToPane(tabId: string, paneId: string): Pane | null {
1234
+ export function moveTabToPane(tabId: string, paneId: string, index?: number): Pane | null {
1184
1235
  const pane = paneById(paneId);
1185
1236
  if (!pane || !workspace.tabs.has(tabId)) {
1186
1237
  return null;
1187
1238
  }
1188
1239
  if (pane.tabOrder.includes(tabId)) {
1240
+ if (index !== undefined) {
1241
+ pane.tabOrder = reorderWithinPane(pane, tabId, index);
1242
+ }
1189
1243
  return pane;
1190
1244
  }
1191
1245
  detachTab(tabId);
1192
- insertIntoPane(pane, tabId);
1246
+ insertIntoPane(pane, tabId, index);
1193
1247
  return pane;
1194
1248
  }
1195
1249
 
@@ -1225,8 +1279,16 @@ export function renameTab(oldId: string, newId: string, newDocumentPath: string)
1225
1279
 
1226
1280
  /** What the tab commands need from the rest of Studio. */
1227
1281
  export interface TabCommandDeps {
1228
- /** Read a file from disk and show it — `files/files.ts`'s `openFileInTab`. */
1229
- openFile: (path: string) => void | Promise<void>;
1282
+ /**
1283
+ * Read a file from disk and show it — `files/files.ts`'s `openFileInTab`. The options are the
1284
+ * opener's own: `paneId` names the pane the tab lands in (defaulting to the focused one), and
1285
+ * `focus: false` browses without moving the keyboard — the shape session restore and the
1286
+ * derivation follow need.
1287
+ */
1288
+ openFile: (
1289
+ path: string,
1290
+ opts?: { paneId?: string; focus?: boolean; preview?: boolean },
1291
+ ) => void | Promise<void>;
1230
1292
  /**
1231
1293
  * Read a file from disk and show it in a NAMED pane, browsing rather than committing and leaving
1232
1294
  * the keyboard where it is — `files/files.ts`'s `openFileInPane`.
@@ -1297,6 +1359,117 @@ export function tabCommands(deps: TabCommandDeps): AnyCommand[] {
1297
1359
  cycleTab(-1);
1298
1360
  },
1299
1361
  },
1362
+ {
1363
+ args: argsSchema({
1364
+ path: stringProperty(
1365
+ 'Project-relative path of the file to open, e.g. "pages/about.json" or ' +
1366
+ '"components/nav-bar.json". It must exist.',
1367
+ ),
1368
+ }),
1369
+ id: "document.open",
1370
+ title: "Open Document",
1371
+ category: "Document",
1372
+ /* PROJECT level, though the namespace says document: the level is what a record acts on, and
1373
+ this one acts on the workspace — it adds a tab — from a state where no document is open
1374
+ at all. A document-level record would be gated on the one thing it exists to produce. */
1375
+ level: "project",
1376
+ menus: ["palette"],
1377
+ group: "1_file",
1378
+ when: (ctx) => ctx.project.open,
1379
+ requires: "an open project",
1380
+ aiTool: {
1381
+ description:
1382
+ "Open a project file as the active document; the document tools then operate on it. " +
1383
+ "Use it when the user should SEE the page, or for iterative visual refinement after " +
1384
+ "creating a page or component.",
1385
+ name: "open_document",
1386
+ /* The person's own read: `activeTab` is what the tab strip highlights, and `editor.kind`
1387
+ is the fact the document-tree tier is gated on. Both are named so the model learns in one
1388
+ round whether the tree tools reach the file it opened — a `.csv` lands in the Grid, a
1389
+ `.png` in the Media viewer, and neither is an element tree. The `.value` read is module
1390
+ state, which a projected `report` may close over (§12.4); `after` alone cannot say WHICH
1391
+ document is active. */
1392
+ report: ({ after, args }) => {
1393
+ const path = stringArg("document.open", args, "path");
1394
+ const active = activeTab.value?.documentPath ?? null;
1395
+ if (active !== path) {
1396
+ return `Opened "${path}", but the active document is ${active === null ? "none" : `"${active}"`}.`;
1397
+ }
1398
+ const where =
1399
+ after.editor.kind === "canvas"
1400
+ ? "on the canvas; the document tools now operate on it"
1401
+ : `in the ${EDITOR_KIND_LABELS[after.editor.kind]} editor, which is not an element tree the document tools can edit`;
1402
+ return `"${path}" is the active document, ${where}.`;
1403
+ },
1404
+ },
1405
+ /* Refuses by THROWING (§12.4's first review rule). `openFileInTab` reports a file it cannot
1406
+ open as a Problem and returns normally — the right answer for a click in the Files tree,
1407
+ where the toast is in front of the person — so the record has to look for the tab itself.
1408
+ The hand tool this replaced read `getTab()` after the open and called whatever it found a
1409
+ success, so a missing file left the PREVIOUS document active and reported "Switched to". */
1410
+ run: async (_ctx, args) => {
1411
+ const path = stringArg("document.open", args, "path");
1412
+ await deps.openFile(path);
1413
+ if (![...workspace.tabs.values()].some((tab) => tab.documentPath === path)) {
1414
+ throw new RangeError(
1415
+ `command "document.open" argument "path": "${path}" could not be opened — it does ` +
1416
+ `not exist, or no editor claims its format. Problems has the reason.`,
1417
+ );
1418
+ }
1419
+ },
1420
+ },
1421
+ {
1422
+ args: argsSchema({
1423
+ file: stringProperty(
1424
+ 'Project-relative path of the file to open in the other pane, e.g. "pages/about.json". ' +
1425
+ "It must exist.",
1426
+ ),
1427
+ }),
1428
+ id: "document.openToSide",
1429
+ title: "Open to the Side",
1430
+ category: "Document",
1431
+ /* PROJECT level for the same reason `document.open` is: it adds a tab to the workspace, and
1432
+ a second pane may not exist yet — the record mints one. */
1433
+ level: "project",
1434
+ menus: ["context/file", "palette"],
1435
+ group: "1_file",
1436
+ when: (ctx) => ctx.project.open,
1437
+ requires: "an open project",
1438
+ /* No `aiTool`, by §12.4's first deletion rule: chrome — it arranges what the person is
1439
+ looking at. No `undo`: opening a tab is not a state the undo stack owns. */
1440
+ undo: "none",
1441
+ /* The non-drag equivalent of dragging a file row onto the other pane's strip (SC 2.5.7): the
1442
+ same `receivingPane` the drag resolver uses, so a file tree row and this command can never
1443
+ disagree about where "the side" is. The open is a FOLLOW — the pane it lands in takes the
1444
+ keyboard, which is what "open to the side" means when the side pane did not exist a moment
1445
+ ago. */
1446
+ run: async (_ctx, args) => {
1447
+ const file = stringArg("document.openToSide", args, "file");
1448
+ const target = receivingPane(activePane().id);
1449
+ await deps.openFile(file, { paneId: target.id });
1450
+ if (![...workspace.tabs.values()].some((tab) => tab.documentPath === file)) {
1451
+ /* The open failed (a missing file, or no editor claims the format) and `openFileInTab`
1452
+ reported it as a Problem rather than throwing. If the target pane was minted for this
1453
+ open and holds nothing, close it — a command that fails must not leave a new empty
1454
+ pane behind.
1455
+
1456
+ `target.id !== PRIMARY_PANE` GUARDS it, and the guard is load-bearing rather than
1457
+ defensive: `receivingPane` answers PRIMARY when the author is focused on the secondary,
1458
+ and an empty primary is a legitimate state on its own (`detachTab`'s "welcome screen
1459
+ beside the document" exemption) — not a hole this command minted. `closePane`'s own
1460
+ contract reads `PRIMARY_PANE` as "collapse the OTHER pane", so calling it unguarded here
1461
+ would close the secondary the author was just looking at, over a refusal about a
1462
+ completely different pane. */
1463
+ if (paneIsEmpty(target) && target.id !== PRIMARY_PANE) {
1464
+ closePane(target.id);
1465
+ }
1466
+ throw new RangeError(
1467
+ `command "document.openToSide" argument "file": "${file}" could not be opened — it ` +
1468
+ `does not exist, or no editor claims its format. Problems has the reason.`,
1469
+ );
1470
+ }
1471
+ },
1472
+ },
1300
1473
  {
1301
1474
  id: "document.reopenClosed",
1302
1475
  title: "Reopen Closed Document",
@@ -1410,6 +1583,10 @@ export function paneCommands(deps: TabCommandDeps): AnyCommand[] {
1410
1583
  requires: "an open document",
1411
1584
  undo: "none",
1412
1585
  run: () => {
1586
+ /* The non-drag equivalent of every drag move (SC 2.5.7): `splitRight` moves the active tab
1587
+ to a new pane at the right of the grid, which is what dragging a tab onto the grid's
1588
+ right edge does, and `document.openToSide` is its file-tree counterpart. A keyboard-only
1589
+ author can reach every arrangement the drag offers. */
1413
1590
  splitRight();
1414
1591
  },
1415
1592
  },
@@ -1448,11 +1625,11 @@ export function paneCommands(deps: TabCommandDeps): AnyCommand[] {
1448
1625
  );
1449
1626
  }
1450
1627
  /* Nothing closes, nothing unsplits, and the FOCUS DOES NOT MOVE: comparing is a read.
1451
- {@link receivingPane} rather than `sidePane`, because the pane beside this one may be a
1628
+ {@link receivingPane} rather than `paneBeside`, because the pane beside this one may be a
1452
1629
  LENS — and a lens owns no tab, so the document landed in a `tabOrder` that `tabOfPane`
1453
1630
  hops straight past. Nothing was on screen, nothing was in a strip, and
1454
1631
  `workspace.activeTabId` went on reporting the source. */
1455
- await deps.openFileInPane(receivingPane().id, path);
1632
+ await deps.openFileInPane(receivingPane(here.id).id, path);
1456
1633
  },
1457
1634
  },
1458
1635
  {
@@ -54,6 +54,14 @@
54
54
  #toolbar.electrobun-webkit-app-region-drag {
55
55
  -webkit-app-region: drag;
56
56
  }
57
+ /* The mirror of #toolbar's own drag class above: every interactive control inside the Command Bar
58
+ carries this (commandbar.json) so Electrobun's ancestry walk finds a working no-drag signal on
59
+ the button itself instead of falling through to the toolbar's drag region. Unscoped, like
60
+ .resize-edge below, because the class name is self-describing and used nowhere else. A no-op
61
+ outside Electrobun. */
62
+ .electrobun-webkit-app-region-no-drag {
63
+ -webkit-app-region: no-drag;
64
+ }
57
65
  /* Resize edge zones */
58
66
  .resize-edge {
59
67
  position: fixed;
@@ -35,6 +35,10 @@
35
35
  "#toolbar.electrobun-webkit-app-region-drag": {
36
36
  "-webkit-app-region": "drag"
37
37
  },
38
+ ".electrobun-webkit-app-region-no-drag": {
39
+ "$description": "The mirror of #toolbar's own drag class above: every interactive control inside the Command Bar carries this (commandbar.json) so Electrobun's ancestry walk finds a working no-drag signal on the button itself instead of falling through to the toolbar's drag region. Unscoped, like .resize-edge below, because the class name is self-describing and used nowhere else. A no-op outside Electrobun.",
40
+ "-webkit-app-region": "no-drag"
41
+ },
38
42
  ".resize-edge": {
39
43
  "$description": "Resize edge zones",
40
44
  "position": "fixed",
@@ -1,18 +0,0 @@
1
- import {
2
- componentRegistry2,
3
- loadComponentRegistry2,
4
- buildComponentInstance2,
5
- computeRelativePath2
6
- } from "./studio-d055c37e.js";
7
- import"./studio-6ewqv390.js";
8
- import"./studio-6nzaktvy.js";
9
- import"./studio-7r42c6ca.js";
10
- export {
11
- buildComponentInstance2 as buildComponentInstance,
12
- componentRegistry2 as componentRegistry,
13
- computeRelativePath2 as computeRelativePath,
14
- loadComponentRegistry2 as loadComponentRegistry
15
- };
16
-
17
- //# debugId=B5E086B31CB59BC964756E2164756E21
18
- //# sourceMappingURL=components-2w1f35qs.js.map
@@ -1,46 +0,0 @@
1
- import {
2
- collectionInfo2,
3
- collectionDirs2,
4
- listCollectionEntryIds2,
5
- reloadDraftAwareGrids2,
6
- createCollectionSource2,
7
- createPagesSource2
8
- } from "./studio-x3rty6tr.js";
9
- import"./studio-rpyah2ew.js";
10
- import"./studio-8jn5yhyn.js";
11
- import"./studio-q0a3yk4n.js";
12
- import"./studio-qahxvnan.js";
13
- import"./studio-cpvb2ed1.js";
14
- import"./studio-tb3ev01j.js";
15
- import"./studio-26d7myd1.js";
16
- import"./studio-81390n6f.js";
17
- import"./studio-rkstz940.js";
18
- import"./studio-25hkdrkw.js";
19
- import"./studio-d055c37e.js";
20
- import"./studio-y9zedyjn.js";
21
- import"./studio-vz68yn9e.js";
22
- import"./studio-4m0haseg.js";
23
- import"./studio-az3zd49t.js";
24
- import"./studio-q3x06vmj.js";
25
- import"./studio-6ewqv390.js";
26
- import"./studio-yxa81f45.js";
27
- import"./studio-esrxgwh6.js";
28
- import"./studio-79y5sz51.js";
29
- import"./studio-6nzaktvy.js";
30
- import"./studio-3gxv60tn.js";
31
- import"./studio-erdatzvp.js";
32
- import"./studio-1k325pwf.js";
33
- import"./studio-5jy81t3s.js";
34
- import"./studio-p1dw8wfw.js";
35
- import"./studio-7r42c6ca.js";
36
- export {
37
- collectionDirs2 as collectionDirs,
38
- collectionInfo2 as collectionInfo,
39
- createCollectionSource2 as createCollectionSource,
40
- createPagesSource2 as createPagesSource,
41
- listCollectionEntryIds2 as listCollectionEntryIds,
42
- reloadDraftAwareGrids2 as reloadDraftAwareGrids
43
- };
44
-
45
- //# debugId=597BB9A23802D85864756E2164756E21
46
- //# sourceMappingURL=content-source-3wqeas45.js.map
@@ -1,42 +0,0 @@
1
- import {
2
- createEntry2,
3
- registerContentCommands2
4
- } from "./studio-fabxk6r9.js";
5
- import"./studio-sgdc5pfc.js";
6
- import"./studio-57mf66w2.js";
7
- import"./studio-x3rty6tr.js";
8
- import"./studio-rpyah2ew.js";
9
- import"./studio-8jn5yhyn.js";
10
- import"./studio-q0a3yk4n.js";
11
- import"./studio-qahxvnan.js";
12
- import"./studio-cpvb2ed1.js";
13
- import"./studio-tb3ev01j.js";
14
- import"./studio-26d7myd1.js";
15
- import"./studio-81390n6f.js";
16
- import"./studio-rkstz940.js";
17
- import"./studio-25hkdrkw.js";
18
- import"./studio-d055c37e.js";
19
- import"./studio-y9zedyjn.js";
20
- import"./studio-vz68yn9e.js";
21
- import"./studio-4m0haseg.js";
22
- import"./studio-az3zd49t.js";
23
- import"./studio-q3x06vmj.js";
24
- import"./studio-6ewqv390.js";
25
- import"./studio-yxa81f45.js";
26
- import"./studio-esrxgwh6.js";
27
- import"./studio-79y5sz51.js";
28
- import"./studio-6nzaktvy.js";
29
- import"./studio-3gxv60tn.js";
30
- import"./studio-erdatzvp.js";
31
- import"./studio-1k325pwf.js";
32
- import"./studio-5jy81t3s.js";
33
- import"./studio-p1dw8wfw.js";
34
- import"./studio-7r42c6ca.js";
35
- import"./studio-tjjk7jvk.js";
36
- export {
37
- createEntry2 as createEntry,
38
- registerContentCommands2 as registerContentCommands
39
- };
40
-
41
- //# debugId=474288D19E3E71A764756E2164756E21
42
- //# sourceMappingURL=entry-commands-d6g50j7s.js.map
@@ -1,47 +0,0 @@
1
- import {
2
- parseSourceForPath2,
3
- parseCollabSource2,
4
- setDocumentSavedListener2,
5
- saveFile2,
6
- exportFile2,
7
- confirmFileDelete2,
8
- renamePromptMessage2
9
- } from "./studio-rpyah2ew.js";
10
- import"./studio-8jn5yhyn.js";
11
- import"./studio-q0a3yk4n.js";
12
- import"./studio-qahxvnan.js";
13
- import"./studio-cpvb2ed1.js";
14
- import"./studio-tb3ev01j.js";
15
- import"./studio-26d7myd1.js";
16
- import"./studio-81390n6f.js";
17
- import"./studio-rkstz940.js";
18
- import"./studio-25hkdrkw.js";
19
- import"./studio-d055c37e.js";
20
- import"./studio-y9zedyjn.js";
21
- import"./studio-vz68yn9e.js";
22
- import"./studio-4m0haseg.js";
23
- import"./studio-az3zd49t.js";
24
- import"./studio-q3x06vmj.js";
25
- import"./studio-6ewqv390.js";
26
- import"./studio-yxa81f45.js";
27
- import"./studio-esrxgwh6.js";
28
- import"./studio-79y5sz51.js";
29
- import"./studio-6nzaktvy.js";
30
- import"./studio-3gxv60tn.js";
31
- import"./studio-erdatzvp.js";
32
- import"./studio-1k325pwf.js";
33
- import"./studio-5jy81t3s.js";
34
- import"./studio-p1dw8wfw.js";
35
- import"./studio-7r42c6ca.js";
36
- export {
37
- confirmFileDelete2 as confirmFileDelete,
38
- exportFile2 as exportFile,
39
- parseCollabSource2 as parseCollabSource,
40
- parseSourceForPath2 as parseSourceForPath,
41
- renamePromptMessage2 as renamePromptMessage,
42
- saveFile2 as saveFile,
43
- setDocumentSavedListener2 as setDocumentSavedListener
44
- };
45
-
46
- //# debugId=C4DCE4C4E32E01A564756E2164756E21
47
- //# sourceMappingURL=file-ops-3btj6st0.js.map