@jxsuite/studio 6.0.0 → 6.1.1

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 (208) hide show
  1. package/dist/chunks/2020-va2wwra4.js +4 -0
  2. package/dist/chunks/adapter-f93pafd7.js +4 -0
  3. package/dist/chunks/{cf-account-picker-bb2yq946.js → cf-account-picker-af1h381v.js} +3 -3
  4. package/dist/chunks/cf-settings-125tdm0w.js +4 -0
  5. package/dist/chunks/components-ax2f1fnn.js +4 -0
  6. package/dist/chunks/content-source-108aqtk1.js +4 -0
  7. package/dist/chunks/{convert-to-repeater-vqkt7s3s.js → convert-to-repeater-ry5rmppr.js} +3 -3
  8. package/dist/chunks/editor-pfgdmkb0.js +4 -0
  9. package/dist/chunks/entry-commands-t0k1q0kx.js +4 -0
  10. package/dist/chunks/{extension-sections-8beq9ac3.js → extension-sections-6mwh2hf8.js} +3 -3
  11. package/dist/chunks/file-ops-meej5mqf.js +4 -0
  12. package/dist/chunks/files-44vp8a2s.js +4 -0
  13. package/dist/chunks/format-host-7q1petb9.js +4 -0
  14. package/dist/chunks/fs-events-h2mwnxaz.js +4 -0
  15. package/dist/chunks/git-panel-zngbymvd.js +4 -0
  16. package/dist/chunks/{grid-view-ca10def6.js → grid-view-sx9vynsp.js} +15 -15
  17. package/dist/chunks/icons-p44xhjf0.js +4 -0
  18. package/dist/chunks/iframe-host-qk3ycq87.js +4 -0
  19. package/dist/chunks/{index-9ydfe04y.js → index-2veqmnhr.js} +3 -3
  20. package/dist/chunks/index-wkyfjeqz.js +4 -0
  21. package/dist/chunks/{javascript-d9pqgc2p.js → javascript-8nbvs12s.js} +3 -3
  22. package/dist/chunks/jsonMode-s8kt9dsj.js +13 -0
  23. package/dist/chunks/jx-validate-psgd7vsx.js +4 -0
  24. package/dist/chunks/layers-panel-1kq2jyfk.js +4 -0
  25. package/dist/chunks/layers-xd92fg8d.js +4 -0
  26. package/dist/chunks/lit-html-ac4cwwr4.js +4 -0
  27. package/dist/chunks/media-picker-0svzkdyb.js +4 -0
  28. package/dist/chunks/menu-5c55ea3d.js +4 -0
  29. package/dist/chunks/{monaco-binding-62dtmgbf.js → monaco-binding-z6r70pdt.js} +3 -3
  30. package/dist/chunks/monaco-buffer-fz6gq5y7.js +4 -0
  31. package/dist/chunks/monaco-setup-q4axkgj7.js +4 -0
  32. package/dist/chunks/notify-0xgtn8q1.js +4 -0
  33. package/dist/chunks/platform-v1qctbe6.js +4 -0
  34. package/dist/chunks/publish-panel-ym1fmgq5.js +4 -0
  35. package/dist/chunks/quick-search-a54e6t5n.js +4 -0
  36. package/dist/chunks/references-gmqp82fs.js +4 -0
  37. package/dist/chunks/right-panel-v7t9awbk.js +4 -0
  38. package/dist/chunks/section-registry-vhyf3zsr.js +4 -0
  39. package/dist/chunks/studio-0bhx5hz6.js +5 -0
  40. package/dist/chunks/studio-0k1ddnyy.js +6 -0
  41. package/dist/chunks/studio-1b04pd0j.js +64 -0
  42. package/dist/chunks/{studio-sxc0zvpt.js → studio-1q1sh2ax.js} +3 -3
  43. package/dist/chunks/studio-4dwbj6gb.js +5 -0
  44. package/dist/chunks/studio-4tanh828.js +5 -0
  45. package/dist/chunks/{studio-fqq890ap.js → studio-55h4mstj.js} +3 -3
  46. package/dist/chunks/studio-5tnvjkz4.js +5 -0
  47. package/dist/chunks/studio-60dqgb0c.js +5 -0
  48. package/dist/chunks/studio-61dp09qg.js +9 -0
  49. package/dist/chunks/studio-63epcrj2.js +5 -0
  50. package/dist/chunks/studio-74k29dy9.js +5 -0
  51. package/dist/chunks/{studio-qa73881a.js → studio-7vg6jcd3.js} +4 -4
  52. package/dist/chunks/studio-7zx31knw.js +9 -0
  53. package/dist/chunks/studio-8bxk97km.js +5 -0
  54. package/dist/chunks/{studio-edbtvcd1.js → studio-8grwxtqk.js} +4 -4
  55. package/dist/chunks/studio-933e30n4.js +5 -0
  56. package/dist/chunks/{studio-rr4xkv81.js → studio-98wnwhbg.js} +4 -4
  57. package/dist/chunks/studio-9mccjwjh.js +14 -0
  58. package/dist/chunks/studio-a2ke1e4t.js +5 -0
  59. package/dist/chunks/studio-e3a2a6v3.js +4 -0
  60. package/dist/chunks/{studio-pwqfmt4p.js → studio-e81q1vb9.js} +4 -4
  61. package/dist/chunks/{studio-7pgy3xaf.js → studio-epgrmqqm.js} +4 -4
  62. package/dist/chunks/studio-g1shz58g.js +5 -0
  63. package/dist/chunks/studio-gf0fz03c.js +5 -0
  64. package/dist/chunks/studio-gjtpe62q.js +6 -0
  65. package/dist/chunks/{studio-tzegmy4w.js → studio-gkskhg7m.js} +4 -4
  66. package/dist/chunks/studio-gx4dnjzd.js +5 -0
  67. package/dist/chunks/{studio-8xcwewjq.js → studio-h46vtzk8.js} +4 -4
  68. package/dist/chunks/studio-j03zga3y.js +9 -0
  69. package/dist/chunks/{studio-4r0sjtx8.js → studio-jhpxeckz.js} +4 -4
  70. package/dist/chunks/{studio-zd4kayn2.js → studio-k01r7ykp.js} +4 -4
  71. package/dist/chunks/{studio-03wwfvyt.js → studio-kggey1jt.js} +4 -4
  72. package/dist/chunks/{studio-hnvnfbdv.js → studio-kk7h30ne.js} +6 -6
  73. package/dist/chunks/{studio-01qza1n3.js → studio-kt1zqney.js} +4 -4
  74. package/dist/chunks/{studio-r3kaka4d.js → studio-m3cwhvye.js} +4 -4
  75. package/dist/chunks/{studio-evwgp1ym.js → studio-mcayt6hz.js} +4 -4
  76. package/dist/chunks/{studio-5vy2wfm5.js → studio-myjcghv3.js} +4 -4
  77. package/dist/chunks/studio-n8bxwc3y.js +4 -0
  78. package/dist/chunks/{studio-ztvcxm3w.js → studio-ngm8gvpb.js} +4 -4
  79. package/dist/chunks/{studio-01kbbqg3.js → studio-nj5a01jy.js} +196 -196
  80. package/dist/chunks/{studio-xkfs0dp2.js → studio-pcgz8heb.js} +4 -4
  81. package/dist/chunks/{studio-0bbxhhce.js → studio-pre0skq6.js} +4 -4
  82. package/dist/chunks/{studio-n78gjfy8.js → studio-qcsq0t15.js} +4 -4
  83. package/dist/chunks/studio-qkstvve9.js +5 -0
  84. package/dist/chunks/studio-qv1pwd3c.js +5 -0
  85. package/dist/chunks/{studio-rmn2dz59.js → studio-rc6dvxjb.js} +5 -5
  86. package/dist/chunks/{studio-st75kn99.js → studio-rv11kfwa.js} +7 -7
  87. package/dist/chunks/studio-s8dmmz66.js +5 -0
  88. package/dist/chunks/studio-sp5xzbj4.js +6 -0
  89. package/dist/chunks/{studio-tmeq6mjb.js → studio-szsjbj0g.js} +4 -4
  90. package/dist/chunks/studio-t1xzawhr.js +16 -0
  91. package/dist/chunks/{studio-mk00zd5d.js → studio-t6b6qe31.js} +4 -4
  92. package/dist/chunks/{studio-bftp4n4b.js → studio-tdcprbqb.js} +4 -4
  93. package/dist/chunks/{studio-ge5fqjth.js → studio-vrmcjf9g.js} +5 -5
  94. package/dist/chunks/studio-vyc0b4he.js +10 -0
  95. package/dist/chunks/{studio-semd1s8s.js → studio-w0cgf6jg.js} +4 -4
  96. package/dist/chunks/{studio-gp02nqnw.js → studio-w3hskzqk.js} +4 -4
  97. package/dist/chunks/{studio-1wvfxkyy.js → studio-wszjyh0j.js} +4 -4
  98. package/dist/chunks/studio-xgrgcdyh.js +9 -0
  99. package/dist/chunks/{studio-y7jehjrp.js → studio-xz2yxfcb.js} +4 -4
  100. package/dist/chunks/{studio-aje966s9.js → studio-y6n42tgs.js} +4 -4
  101. package/dist/chunks/{studio-yr04mv2j.js → studio-yb560vxr.js} +4 -4
  102. package/dist/chunks/{studio-tp642n5g.js → studio-yn9f8vtx.js} +4 -4
  103. package/dist/chunks/studio-yw05w8f0.js +7 -0
  104. package/dist/chunks/{studio-cbr9czkn.js → studio-z16sgavc.js} +4 -4
  105. package/dist/chunks/studio-zey7zsdw.js +12 -0
  106. package/dist/chunks/{studio-1q6amtph.js → studio-zyqn40w4.js} +4 -4
  107. package/dist/chunks/tsMode-2frah6h1.js +14 -0
  108. package/dist/chunks/workspace-zt81g1ms.js +4 -0
  109. package/dist/chunks/ws-client-n2bh4hkf.js +4 -0
  110. package/dist/iframe-entry.js +13 -13
  111. package/dist/manifest.json +62 -62
  112. package/dist/studio.js +87 -87
  113. package/dist/workers/ts.worker.js +3 -3
  114. package/package.json +10 -10
  115. package/src/canvas/canvas-render.ts +4 -1
  116. package/src/canvas/canvas-utils.ts +73 -15
  117. package/src/canvas/iframe-host.ts +67 -21
  118. package/src/canvas/iframe-inline-edit.ts +22 -4
  119. package/src/canvas/iframe-interaction.ts +7 -1
  120. package/src/canvas/iframe-protocol.ts +3 -0
  121. package/src/canvas/iframe-render.ts +5 -2
  122. package/src/commands/defaults.ts +4 -1
  123. package/src/commands/live-context.ts +3 -1
  124. package/src/editor/shortcuts.ts +55 -27
  125. package/src/files/media-paths.ts +3 -1
  126. package/src/new-project/add-repo-modal.ts +459 -59
  127. package/src/panels/editors.ts +3 -1
  128. package/src/panels/style-panel.ts +36 -11
  129. package/src/platforms/cloud.ts +95 -18
  130. package/src/platforms/devserver.ts +47 -12
  131. package/src/publish/pages-service.ts +7 -0
  132. package/src/publish/publish-panel.ts +16 -2
  133. package/src/studio.ts +14 -2
  134. package/src/surfaces/add-repo.json +721 -224
  135. package/src/surfaces/add-repo.ts +196 -31
  136. package/src/surfaces/library-pane.ts +6 -0
  137. package/src/surfaces/rail.json +126 -73
  138. package/src/surfaces/rail.ts +31 -5
  139. package/src/tabs/typing.ts +31 -0
  140. package/src/types.ts +42 -3
  141. package/src/utils/edit-display.ts +30 -1
  142. package/src/utils/project-dir.ts +24 -0
  143. package/src/workspace/workspace.ts +7 -0
  144. package/dist/chunks/2020-cs2sb9t1.js +0 -4
  145. package/dist/chunks/adapter-y5gdq19c.js +0 -4
  146. package/dist/chunks/cf-settings-20dznc3j.js +0 -4
  147. package/dist/chunks/components-q037dya7.js +0 -4
  148. package/dist/chunks/content-source-t0q184js.js +0 -4
  149. package/dist/chunks/editor-j5sq149q.js +0 -4
  150. package/dist/chunks/entry-commands-fxzgcc4k.js +0 -4
  151. package/dist/chunks/file-ops-2n77759h.js +0 -4
  152. package/dist/chunks/files-fjy7zpxt.js +0 -4
  153. package/dist/chunks/format-host-wj3mf3n8.js +0 -4
  154. package/dist/chunks/fs-events-7kcgn4e9.js +0 -4
  155. package/dist/chunks/git-panel-7z2mpt2d.js +0 -4
  156. package/dist/chunks/icons-qbh8akb1.js +0 -4
  157. package/dist/chunks/iframe-host-a0zjd6xp.js +0 -4
  158. package/dist/chunks/index-m0x5ztax.js +0 -4
  159. package/dist/chunks/jsonMode-800yvbem.js +0 -13
  160. package/dist/chunks/jx-validate-zc9zp77s.js +0 -4
  161. package/dist/chunks/layers-p677nrh5.js +0 -4
  162. package/dist/chunks/layers-panel-y4w1dq90.js +0 -4
  163. package/dist/chunks/lit-html-cfn61dn7.js +0 -4
  164. package/dist/chunks/media-picker-xg7yjpz9.js +0 -4
  165. package/dist/chunks/menu-fpekzj6w.js +0 -4
  166. package/dist/chunks/monaco-buffer-nn4dtx27.js +0 -4
  167. package/dist/chunks/monaco-setup-tj13ndtm.js +0 -4
  168. package/dist/chunks/notify-ahs57a2z.js +0 -4
  169. package/dist/chunks/platform-v9xyfkbx.js +0 -4
  170. package/dist/chunks/publish-panel-xtbfgg2r.js +0 -4
  171. package/dist/chunks/quick-search-d3a6m7j3.js +0 -4
  172. package/dist/chunks/references-99ws0b0m.js +0 -4
  173. package/dist/chunks/right-panel-31vbk5xp.js +0 -4
  174. package/dist/chunks/section-registry-b2yzp9sw.js +0 -4
  175. package/dist/chunks/studio-1a99w39s.js +0 -5
  176. package/dist/chunks/studio-3ck2a5c4.js +0 -10
  177. package/dist/chunks/studio-4fde4a0x.js +0 -5
  178. package/dist/chunks/studio-4jj9gme2.js +0 -6
  179. package/dist/chunks/studio-4m50vh74.js +0 -16
  180. package/dist/chunks/studio-4xppwncr.js +0 -4
  181. package/dist/chunks/studio-5d7wfmnp.js +0 -5
  182. package/dist/chunks/studio-5e9zsamh.js +0 -9
  183. package/dist/chunks/studio-5gs9th4b.js +0 -5
  184. package/dist/chunks/studio-7qvs7v0m.js +0 -5
  185. package/dist/chunks/studio-84rtc120.js +0 -5
  186. package/dist/chunks/studio-aekhcdzw.js +0 -7
  187. package/dist/chunks/studio-aqmph6k4.js +0 -5
  188. package/dist/chunks/studio-c6zdm25z.js +0 -9
  189. package/dist/chunks/studio-capk0583.js +0 -9
  190. package/dist/chunks/studio-chdmwwzw.js +0 -5
  191. package/dist/chunks/studio-cp1gbff9.js +0 -14
  192. package/dist/chunks/studio-cqmtbg2p.js +0 -5
  193. package/dist/chunks/studio-e47215hh.js +0 -5
  194. package/dist/chunks/studio-fbx6twps.js +0 -6
  195. package/dist/chunks/studio-fthwn4kf.js +0 -5
  196. package/dist/chunks/studio-g8dsj3d2.js +0 -6
  197. package/dist/chunks/studio-gjknvzam.js +0 -9
  198. package/dist/chunks/studio-hjbg8d5y.js +0 -12
  199. package/dist/chunks/studio-nbxqcgvf.js +0 -5
  200. package/dist/chunks/studio-t1eedsyx.js +0 -4
  201. package/dist/chunks/studio-vavmwshr.js +0 -5
  202. package/dist/chunks/studio-x5g1xk77.js +0 -5
  203. package/dist/chunks/studio-x92tgnee.js +0 -64
  204. package/dist/chunks/studio-xg1942as.js +0 -5
  205. package/dist/chunks/studio-zev8ssp1.js +0 -5
  206. package/dist/chunks/tsMode-k9s9fcag.js +0 -14
  207. package/dist/chunks/workspace-5vmgsagj.js +0 -4
  208. package/dist/chunks/ws-client-0ja08haa.js +0 -4
@@ -563,6 +563,9 @@ export type IframeToParent =
563
563
  // Inline editing started in the iframe (the parent shows the format toolbar from here). `prop`
564
564
  // Names the component-instance prop when the session edits prop-bound text (plain sessions).
565
565
  | { kind: "editStart"; path: (string | number)[]; prop?: string }
566
+ // The first keystroke of a typing burst, ahead of the idle tick's commit: the parent marks the tab
567
+ // Dirty now, so Save is enabled the instant there is text a save would keep.
568
+ | { kind: "editInput" }
566
569
  // Committed inline-edit content (rich `children` else `textContent`) for the parent to persist.
567
570
  | {
568
571
  kind: "editCommit";
@@ -1018,8 +1018,11 @@ export async function renderResolvedDocument(opts: {
1018
1018
  // Sections feeding back into an ever-growing height. Set every render (the iframe always wants it).
1019
1019
  setCanvasViewportTranspose(true);
1020
1020
  // De-link `<a href>` in design/edit so clicks select the anchor instead of navigating the iframe;
1021
- // Preview keeps real links live (mirrors the server-function gate above).
1022
- setCanvasDelinkAnchors(opts.mode !== "preview");
1021
+ // Preview keeps real links live (mirrors the server-function gate above). So does Project Styles:
1022
+ // An `<a>` without `href` is not a link, so `:link`, `:any-link` and the UA's link colour could
1023
+ // Never match its specimen, and a project could not see the link styles it was setting there.
1024
+ // Its clicks are refused in the frame instead (`iframe-interaction.ts`).
1025
+ setCanvasDelinkAnchors(opts.mode !== "preview" && opts.mode !== "stylebook");
1023
1026
  /* De-popover in the same modes and for the same reason: an OPEN popover is in the top layer,
1024
1027
  whose containing block is the viewport — a fiction here, since the frame is sized to its own
1025
1028
  content — and which contributes to no ancestor's scrollable overflow, so the artboard could
@@ -236,7 +236,10 @@ export function defaultCommands(deps: CommandDeps): AnyCommand[] {
236
236
  group: "1_file",
237
237
  undo: "none",
238
238
  when: documentOpen,
239
- requires: "an open document",
239
+ // Nothing to save is a disabled button, not a no-op one. A disabled chord is still claimed,
240
+ // So ⌘S on a clean document never falls through to the browser's own Save Page.
241
+ enablement: (ctx) => ctx.document.dirty,
242
+ requires: "unsaved changes",
240
243
  run: () => deps.saveDocument(),
241
244
  },
242
245
  {
@@ -32,6 +32,7 @@ import { shell } from "../shell";
32
32
  import { activeTab, workspace } from "../workspace/workspace";
33
33
  import { primarySelection } from "../tabs/selection";
34
34
  import { canRedo, canUndo } from "../tabs/transact";
35
+ import { hasTyping } from "../tabs/typing";
35
36
  import { collabState } from "../collab/collab-state";
36
37
 
37
38
  import { canvasViewForMode, editorKindForMode, emptyContext } from "./context";
@@ -133,7 +134,8 @@ export function createLiveContext(sources: LiveContextSources): () => CommandCon
133
134
  ctx.git.dirtyCount = shell.git.status?.files.length ?? 0;
134
135
 
135
136
  ctx.document.open = tab !== null;
136
- ctx.document.dirty = tab?.doc.dirty === true;
137
+ // Typing still in a source buffer is unsaved work too, though the document has not seen it.
138
+ ctx.document.dirty = tab ? tab.doc.dirty === true || hasTyping(tab.id) : false;
137
139
  ctx.document.mode = tab?.doc.mode ?? "";
138
140
  ctx.document.canUndo = tab ? canUndo(tab) : false;
139
141
  ctx.document.canRedo = tab ? canRedo(tab) : false;
@@ -51,16 +51,16 @@ import {
51
51
  } from "../tabs/transact";
52
52
  import {
53
53
  applyEditZoom,
54
- markExplicitZoom,
55
54
  requestEditZoom,
56
55
  setEditZoom,
56
+ setUserZoom,
57
+ stageZoom,
57
58
  } from "../canvas/canvas-utils";
58
59
  import { openQuickSearch } from "../panels/quick-search";
59
60
  import { inspectorTab } from "../panels/right-panel";
60
61
  import { requestClose } from "../panels/tab-strip";
61
62
  import { layerHost } from "../ui/layers";
62
63
  import { openDialogSurface } from "../surfaces/dialog";
63
- import { rectOf } from "../utils/geometry";
64
64
  import {
65
65
  DOCK_IDS,
66
66
  requireNavigatorPanelId,
@@ -78,6 +78,8 @@ import { hasElementSelection, hasSelection, inCanvas, keyScopeStack } from "../c
78
78
  import { defaultCommands } from "../commands/defaults";
79
79
  import { setActiveRegistry } from "../commands/active-registry";
80
80
  import { runReported } from "../commands/run-reported";
81
+ import { pointerAnchor } from "../canvas/iframe-host";
82
+ import type { PointerAnchor } from "../canvas/iframe-host";
81
83
  import type { CommandContext } from "../commands/context";
82
84
  import type { DockId as CommandDockId } from "../commands/defaults";
83
85
  import type { AnyCommand, CommandRegistry } from "../commands/registry";
@@ -314,11 +316,15 @@ function redoDocument(): void {
314
316
 
315
317
  // ─── Canvas zoom ──────────────────────────────────────────────────────────────
316
318
 
317
- /** Design-mode zoom bounds. Edit mode clamps inside `canvas-utils`. */
318
- const ZOOM_MIN = 0.05;
319
- const ZOOM_MAX = 5;
319
+ /** One chord step. Both zooms clamp inside `canvas-utils`. */
320
320
  const ZOOM_STEP = 1.2;
321
321
 
322
+ /** How long after an Edit zoom step its anchor is measured again: the zoom's frame, then a reflow. */
323
+ const EDIT_ZOOM_SETTLE_MS = 34;
324
+
325
+ /** How long an Edit zoom burst must be idle before its anchor is let go. */
326
+ const EDIT_ZOOM_RELEASE_MS = 300;
327
+
322
328
  /** Whether the zoom verbs address the reflowing content zoom rather than the artboard transform. */
323
329
  function isContentZoom(ctx: CommandContext): boolean {
324
330
  return ctx.canvas.view === "edit";
@@ -346,8 +352,8 @@ function zoomBy(ctx: CommandContext, factor: number, redraw: () => void): void {
346
352
  setEditZoom((tab.session.ui.editZoom ?? 1) * factor);
347
353
  return;
348
354
  }
349
- const next = (tab.session.ui.zoom ?? 1) * factor;
350
- tab.session.ui.zoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, next));
355
+ // Around the stage's middle, like the zoom pod's buttons.
356
+ setUserZoom(stageZoom() * factor);
351
357
  redraw();
352
358
  }
353
359
 
@@ -1074,6 +1080,26 @@ let _stageContext: StageContext | null = null;
1074
1080
  */
1075
1081
  export function installStageGestures(surface: CanvasSurface): () => void {
1076
1082
  const canvasWrap = surface.wrap;
1083
+ /* An Edit zoom burst's anchor: the element under the cursor when the burst began. The frame
1084
+ reflows at every step, so after each one the element is measured again and the scroller takes
1085
+ back whatever it moved; the anchor is let go once the burst has been idle a while. */
1086
+ let zoomAnchor: PointerAnchor | null = null;
1087
+ let zoomRelease: ReturnType<typeof setTimeout> | undefined;
1088
+ let zoomCorrecting = false;
1089
+ const correctEditZoom = () => {
1090
+ const anchor = zoomAnchor;
1091
+ if (!anchor || zoomCorrecting) {
1092
+ return;
1093
+ }
1094
+ zoomCorrecting = true;
1095
+ void anchor.settle().then((top) => {
1096
+ zoomCorrecting = false;
1097
+ const sc = canvasWrap.querySelector<HTMLElement>('[part="edit-canvas"]');
1098
+ if (top !== null && sc) {
1099
+ sc.scrollTop += top - anchor.top;
1100
+ }
1101
+ });
1102
+ };
1077
1103
  const getContext = () => _stageContext?.(surface) ?? stageless();
1078
1104
  const controller = new AbortController();
1079
1105
  const { signal } = controller;
@@ -1093,7 +1119,16 @@ export function installStageGestures(surface: CanvasSurface): () => void {
1093
1119
  if (e.ctrlKey || e.metaKey) {
1094
1120
  e.preventDefault();
1095
1121
  const editZoom = paneTab?.session.ui.editZoom ?? 1;
1096
- requestEditZoom(editZoom * (1 + -e.deltaY * 0.005), surface);
1122
+ const canvasEl = surface.panels[0]?.canvas;
1123
+ zoomAnchor ??= canvasEl ? pointerAnchor(canvasEl) : null;
1124
+ requestEditZoom(editZoom * (1 + -e.deltaY * 0.005), surface, e.clientY);
1125
+ // Measured after the zoom's own frame has applied, and the frame has had one to reflow.
1126
+ setTimeout(correctEditZoom, EDIT_ZOOM_SETTLE_MS);
1127
+ clearTimeout(zoomRelease);
1128
+ zoomRelease = setTimeout(() => {
1129
+ correctEditZoom();
1130
+ zoomAnchor = null;
1131
+ }, EDIT_ZOOM_RELEASE_MS);
1097
1132
  return;
1098
1133
  }
1099
1134
  /* The stage's own scroller, by the `part` the stage document draws it with — the same
@@ -1125,24 +1160,14 @@ export function installStageGestures(surface: CanvasSurface): () => void {
1125
1160
  }
1126
1161
  e.preventDefault();
1127
1162
  if (e.ctrlKey || e.metaKey) {
1128
- // Zoom towards cursor
1129
- const rect = rectOf(canvasWrap);
1130
- const cursorX = e.clientX - rect.left;
1131
- const cursorY = e.clientY - rect.top;
1132
- /* THIS pane's tab, not the focused pane's. Both reads were `activeTab`, so a wheel over
1133
- the unfocused stage magnified a document on the other side of the splitter. */
1134
- const oldZoom = paneTab?.session.ui.zoom ?? 1;
1135
- const delta = -e.deltaY * 0.005;
1136
- const newZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, oldZoom * (1 + delta)));
1137
- const ratio = newZoom / oldZoom;
1138
- // Adjust pan so the point under cursor stays stationary
1139
- setPan(cursorX - (cursorX - panX) * ratio, cursorY - (cursorY - panY) * ratio);
1140
- if (paneTab) {
1141
- paneTab.session.ui.zoom = newZoom;
1142
- }
1143
- // The author chose this zoom, so re-entering Design keeps it instead of auto-fitting.
1144
- // On THIS stage: the declared fit is a fact about the document under the cursor.
1145
- markExplicitZoom(surface);
1163
+ /* Zoom toward the cursor, on THIS stage (a lens pane carries its own scale). The author
1164
+ chose this zoom, so it is declared as the document's fit and re-entering Design keeps
1165
+ it instead of auto-fitting. */
1166
+ setUserZoom(stageZoom(surface) * (1 + -e.deltaY * 0.005), surface, {
1167
+ clientX: e.clientX,
1168
+ clientY: e.clientY,
1169
+ });
1170
+ return;
1146
1171
  } else if (e.shiftKey) {
1147
1172
  // Shift+scroll = horizontal pan
1148
1173
  setPan(panX - e.deltaY, panY);
@@ -1206,7 +1231,10 @@ export function installStageGestures(surface: CanvasSurface): () => void {
1206
1231
  { signal },
1207
1232
  );
1208
1233
 
1209
- return () => controller.abort();
1234
+ return () => {
1235
+ clearTimeout(zoomRelease);
1236
+ controller.abort();
1237
+ };
1210
1238
  }
1211
1239
 
1212
1240
  /** The context a stage answers with before the bootstrap has published a reader. */
@@ -134,5 +134,7 @@ export function previewFileSrc(path: string): string {
134
134
  if (space === "repo") {
135
135
  return withVersion(`${fileBaseUrl}${encodeProjectPath(normalized)}`, versionOf(normalized));
136
136
  }
137
- return loopbackAssetSrc(mediaSiteUrl(normalized));
137
+ /* Encoded per segment for the `src` only. `mediaSiteUrl` is also what an author's document is
138
+ given, and stays as typed; a `#` or `?` in a filename would cut this URL short. */
139
+ return loopbackAssetSrc(`/${encodeProjectPath(mediaSiteUrl(normalized).slice(1))}`);
138
140
  }