@jxsuite/studio 4.0.0 → 5.0.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 (613) hide show
  1. package/README.md +1 -1
  2. package/data/elements-meta.json +5 -15
  3. package/data/html-meta.json +51 -6
  4. package/dist/chunks/{2020-xp8vnwma.js → 2020-g79n01v9.js} +14 -14
  5. package/dist/chunks/action-group-sp6tna8v.js +118 -0
  6. package/dist/chunks/adapter-nj7tnjfn.js +13 -0
  7. package/dist/chunks/cf-account-picker-fyf63pmy.js +495 -0
  8. package/dist/chunks/cf-settings-71bv5zn9.js +20 -0
  9. package/dist/chunks/color-area-v17df7b8.js +154 -0
  10. package/dist/chunks/color-field-anwybnkn.js +208 -0
  11. package/dist/chunks/color-slider-44ez96fc.js +79 -0
  12. package/dist/chunks/color-xgg3kfw7.js +35 -0
  13. package/dist/chunks/combobox-5e7jev23.js +236 -0
  14. package/dist/chunks/components-2w1f35qs.js +18 -0
  15. package/dist/chunks/content-source-3wqeas45.js +46 -0
  16. package/dist/chunks/convert-to-repeater-d0cgnb6d.js +639 -0
  17. package/dist/chunks/dialog-s890b918.js +85 -0
  18. package/dist/chunks/editor-3qhbg51x.js +35 -0
  19. package/dist/chunks/entry-commands-d6g50j7s.js +42 -0
  20. package/dist/chunks/extension-sections-fy7q4fe9.js +651 -0
  21. package/dist/chunks/file-ops-3btj6st0.js +47 -0
  22. package/dist/chunks/files-ef8fgs3w.js +72 -0
  23. package/dist/chunks/format-host-3c33nwr8.js +40 -0
  24. package/dist/chunks/fs-events-13tj6sex.js +18 -0
  25. package/dist/chunks/git-panel-4wp2rn9b.js +49 -0
  26. package/dist/chunks/icons-yy10wk9r.js +402 -0
  27. package/dist/chunks/iframe-entry-15g3n6xv.js +114 -0
  28. package/dist/chunks/iframe-entry-b27vq81n.js +203 -0
  29. package/dist/chunks/iframe-entry-nbyxee1y.js +154 -0
  30. package/dist/chunks/iframe-host-jgesd924.js +115 -0
  31. package/dist/chunks/{index-nj44jpzj.js → index-mdmyx6s2.js} +11 -11
  32. package/dist/chunks/index-n4j7qxyr.js +109 -0
  33. package/dist/chunks/{javascript-q8kpmfah.js → javascript-xvrfrpkb.js} +8 -9
  34. package/dist/chunks/{jsonMode-dkr040ga.js → jsonMode-tyx2kj6p.js} +526 -527
  35. package/dist/chunks/jx-validate-j0wxk72w.js +15 -0
  36. package/dist/chunks/layers-1nq5evsh.js +29 -0
  37. package/dist/chunks/layers-panel-ar2gh90g.js +40 -0
  38. package/dist/chunks/listbox-6v2ac08j.js +44 -0
  39. package/dist/chunks/lit-html-m244w9nh.js +17 -0
  40. package/dist/chunks/media-picker-2qbetq73.js +33 -0
  41. package/dist/chunks/menu-q99sy9rf.js +285 -0
  42. package/dist/chunks/menu-rf7n72mf.js +14 -0
  43. package/dist/chunks/{monaco-binding-rj6xpx9y.js → monaco-binding-sw6en4dw.js} +12 -15
  44. package/dist/chunks/monaco-buffer-bhy3hb6m.js +24 -0
  45. package/dist/chunks/{monaco-setup-rws7k4br.js → monaco-setup-8r62fj6y.js} +27 -37
  46. package/dist/chunks/notify-628t0anh.js +21 -0
  47. package/dist/chunks/number-field-5cndyft8.js +72 -0
  48. package/dist/chunks/parse-0zx14y15.js +10 -0
  49. package/dist/chunks/platform-ts9cg1a4.js +15 -0
  50. package/dist/chunks/popover-47z3ytva.js +25 -0
  51. package/dist/chunks/publish-panel-98f7sr3g.js +27 -0
  52. package/dist/chunks/quick-search-srzz83k2.js +45 -0
  53. package/dist/chunks/references-n553f0e1.js +26 -0
  54. package/dist/chunks/right-panel-rrgx2dn3.js +45 -0
  55. package/dist/chunks/section-registry-5pq32t0q.js +25 -0
  56. package/dist/chunks/select-2r16ez41.js +49 -0
  57. package/dist/chunks/split-g7nm1rgb.js +191 -0
  58. package/dist/chunks/studio-0hqbpbd0.js +62 -0
  59. package/dist/chunks/studio-18avnhjp.js +2430 -0
  60. package/dist/chunks/{studio-kryvsw6j.js → studio-1k325pwf.js} +48 -48
  61. package/dist/chunks/{studio-hbtp0eh1.js → studio-25hkdrkw.js} +356 -85
  62. package/dist/chunks/studio-26d7myd1.js +258 -0
  63. package/dist/chunks/{studio-p8j86wag.js → studio-38emantv.js} +22 -5
  64. package/dist/chunks/studio-3gxv60tn.js +422 -0
  65. package/dist/chunks/{studio-8swrjbny.js → studio-3hxkd92t.js} +275 -163
  66. package/dist/chunks/studio-4m0haseg.js +185 -0
  67. package/dist/chunks/{studio-ynxwkrzd.js → studio-4wbb9ct0.js} +1953 -1785
  68. package/dist/chunks/{studio-gv8vvshc.js → studio-5523bpzt.js} +11 -11
  69. package/dist/chunks/{studio-5q69gfag.js → studio-57mf66w2.js} +1753 -2484
  70. package/dist/chunks/studio-5jy81t3s.js +17463 -0
  71. package/dist/chunks/studio-5rt7sqa0.js +1168 -0
  72. package/dist/chunks/studio-5te5tqng.js +81 -0
  73. package/dist/chunks/{studio-p4ngc046.js → studio-6ewqv390.js} +9 -9
  74. package/dist/chunks/{studio-9ab8gqsm.js → studio-6nzaktvy.js} +7 -7
  75. package/dist/chunks/studio-6xa21ej9.js +28 -0
  76. package/dist/chunks/{studio-5ehhackm.js → studio-727xh0e3.js} +19 -19
  77. package/dist/chunks/{studio-eenfkt4e.js → studio-79y5sz51.js} +52 -38
  78. package/dist/chunks/{studio-4q0wznzf.js → studio-7e11jpa9.js} +461 -145
  79. package/dist/chunks/{studio-q7v10cg2.js → studio-7r42c6ca.js} +17 -7
  80. package/dist/chunks/studio-7xptrg0j.js +954 -0
  81. package/dist/chunks/{studio-cft9r4r7.js → studio-81390n6f.js} +22 -22
  82. package/dist/chunks/{studio-q43a5y9h.js → studio-8jn5yhyn.js} +16 -16
  83. package/dist/chunks/studio-8qtrpzr3.js +839 -0
  84. package/dist/chunks/{studio-dg7zyt5x.js → studio-98b3z07n.js} +1 -1
  85. package/dist/chunks/studio-9xxz1q69.js +1356 -0
  86. package/dist/chunks/studio-akbww0j5.js +13615 -0
  87. package/dist/chunks/studio-arfpe389.js +195 -0
  88. package/dist/chunks/{studio-2zkz8evn.js → studio-az3zd49t.js} +473 -281
  89. package/dist/chunks/studio-cpvb2ed1.js +1171 -0
  90. package/dist/chunks/{studio-9r6j5ev1.js → studio-d0300636.js} +5078 -19
  91. package/dist/chunks/{studio-bgxt93ga.js → studio-d055c37e.js} +13 -13
  92. package/dist/chunks/studio-d1ja2x9r.js +362 -0
  93. package/dist/chunks/{studio-b9b6t1dg.js → studio-e1f24eff.js} +5664 -5666
  94. package/dist/chunks/{studio-4pqqxbrt.js → studio-em9nw8eh.js} +301 -112
  95. package/dist/chunks/studio-erdatzvp.js +1190 -0
  96. package/dist/chunks/{studio-g1wdche0.js → studio-esrxgwh6.js} +80 -80
  97. package/dist/chunks/studio-fabxk6r9.js +2811 -0
  98. package/dist/chunks/{studio-3n4ndrzh.js → studio-h4nvnjvz.js} +303 -303
  99. package/dist/chunks/studio-hhvs4sdg.js +195 -0
  100. package/dist/chunks/{studio-68n67094.js → studio-kcgff4r0.js} +15 -18
  101. package/dist/chunks/{studio-685s0ytq.js → studio-nqgawfks.js} +572 -572
  102. package/dist/chunks/{studio-xrapxwwc.js → studio-p1dw8wfw.js} +55 -29
  103. package/dist/chunks/studio-pm6hgy7r.js +4763 -0
  104. package/dist/chunks/studio-q0a3yk4n.js +33 -0
  105. package/dist/chunks/{studio-wcy8trfx.js → studio-q3x06vmj.js} +23 -22
  106. package/dist/chunks/studio-qahxvnan.js +111 -0
  107. package/dist/chunks/{studio-53n8bp04.js → studio-r6xmmtfh.js} +1 -1
  108. package/dist/chunks/{studio-kjjsjak4.js → studio-rkstz940.js} +3661 -6136
  109. package/dist/chunks/{studio-tvgyw9xw.js → studio-rnyf3f8e.js} +83 -250
  110. package/dist/chunks/studio-rpyah2ew.js +2805 -0
  111. package/dist/chunks/studio-rs76k67r.js +12 -0
  112. package/dist/chunks/{studio-yxrkyv8a.js → studio-s6z853aq.js} +12108 -11766
  113. package/dist/chunks/{studio-sf7zjgv1.js → studio-sgdc5pfc.js} +8 -8
  114. package/dist/chunks/studio-tb3ev01j.js +763 -0
  115. package/dist/chunks/{studio-t5wqwzdh.js → studio-tjjk7jvk.js} +1 -1
  116. package/dist/chunks/studio-vhv5tgxm.js +51 -0
  117. package/dist/chunks/{studio-htsf0w6m.js → studio-vz68yn9e.js} +31 -154
  118. package/dist/chunks/studio-wqpngpsk.js +174 -0
  119. package/dist/chunks/{studio-eesa1myv.js → studio-x3rty6tr.js} +58 -58
  120. package/dist/chunks/studio-xsykpvsh.js +8448 -0
  121. package/dist/chunks/{studio-92ab2jqe.js → studio-y9zedyjn.js} +14 -14
  122. package/dist/chunks/studio-yxa81f45.js +12 -0
  123. package/dist/chunks/swatch-group-t0n6h2rn.js +139 -0
  124. package/dist/chunks/tabs-wbp1cdws.js +156 -0
  125. package/dist/chunks/textfield-abk6qyee.js +73 -0
  126. package/dist/chunks/toast-780pvfbv.js +27 -0
  127. package/dist/chunks/toast-host-3sqv46sn.js +116 -0
  128. package/dist/chunks/toolbar-xbh5bhtr.js +194 -0
  129. package/dist/chunks/tooltip-eptb614w.js +369 -0
  130. package/dist/chunks/tree-a73p0pqm.js +367 -0
  131. package/dist/chunks/{tsMode-76kr53q0.js → tsMode-jy4m86rv.js} +83 -85
  132. package/dist/chunks/workspace-wrew8zk4.js +69 -0
  133. package/dist/chunks/{ws-client-6shc5rze.js → ws-client-ny4zc51r.js} +20 -22
  134. package/dist/iframe-entry.js +1812 -698
  135. package/dist/manifest.json +7 -0
  136. package/dist/studio.js +52900 -36573
  137. package/dist/workers/editor.worker.js +1040 -1085
  138. package/dist/workers/json.worker.js +1946 -1991
  139. package/dist/workers/ts.worker.js +4960 -5005
  140. package/favicon.ico +0 -0
  141. package/index.html +2 -0
  142. package/package.json +22 -55
  143. package/src/about/about-modal.ts +73 -123
  144. package/src/browse/library-commands.ts +20 -49
  145. package/src/browse/library-layouts.ts +87 -218
  146. package/src/browse/library-pane.ts +406 -438
  147. package/src/canvas/canvas-helpers.ts +0 -49
  148. package/src/canvas/canvas-live-render.ts +25 -2
  149. package/src/canvas/canvas-render.ts +666 -231
  150. package/src/canvas/canvas-surface.ts +0 -1
  151. package/src/canvas/canvas-utils.ts +310 -117
  152. package/src/canvas/dialog-path.ts +76 -0
  153. package/src/canvas/dialog-state.ts +67 -0
  154. package/src/canvas/diff-marks.ts +252 -0
  155. package/src/canvas/diff-toolbar.ts +349 -0
  156. package/src/canvas/diff-view.ts +123 -0
  157. package/src/canvas/edit-width-drag.ts +187 -0
  158. package/src/canvas/edit-width.ts +150 -0
  159. package/src/canvas/iframe-entry.ts +125 -8
  160. package/src/canvas/iframe-host.ts +271 -15
  161. package/src/canvas/iframe-interaction.ts +133 -13
  162. package/src/canvas/iframe-patch.ts +7 -1
  163. package/src/canvas/iframe-position.ts +5 -6
  164. package/src/canvas/iframe-protocol.ts +106 -5
  165. package/src/canvas/iframe-render.ts +373 -5
  166. package/src/canvas/iframe-slash.ts +2 -2
  167. package/src/canvas/iframe-subtree.ts +5 -8
  168. package/src/canvas/path-mapping.ts +25 -3
  169. package/src/canvas/popover-path.ts +106 -0
  170. package/src/canvas/popover-state.ts +155 -0
  171. package/src/canvas/surface-registry.ts +21 -10
  172. package/src/collab/presence-chips.ts +51 -73
  173. package/src/commands/app-commands.ts +10 -0
  174. package/src/commands/budget.ts +12 -0
  175. package/src/commands/command-args.ts +369 -6
  176. package/src/commands/defaults.ts +44 -14
  177. package/src/commands/live-context.ts +11 -5
  178. package/src/commands/reference.ts +12 -2
  179. package/src/commands/registry.ts +165 -8
  180. package/src/content/entry-commands.ts +16 -23
  181. package/src/content/entry-editor.ts +144 -101
  182. package/src/editor/canvas-slash-bridge.ts +2 -2
  183. package/src/editor/context-menu.ts +49 -200
  184. package/src/editor/convert-to-component.ts +42 -82
  185. package/src/editor/convert-to-repeater.ts +132 -173
  186. package/src/editor/inline-edit.ts +11 -0
  187. package/src/editor/shortcuts.ts +65 -26
  188. package/src/editor/slash-menu.ts +150 -157
  189. package/src/files/file-ops.ts +117 -11
  190. package/src/files/files.ts +949 -664
  191. package/src/files/json-layout.ts +474 -0
  192. package/src/files/media-paths.ts +13 -42
  193. package/src/files/media-usage.ts +41 -131
  194. package/src/files/serialize-document.ts +8 -1
  195. package/src/format/convert-file.ts +16 -10
  196. package/src/format/format-host.ts +50 -1
  197. package/src/github/github-auth.ts +71 -29
  198. package/src/github/github-publish.ts +57 -65
  199. package/src/grid/cell-editors.ts +46 -4
  200. package/src/grid/cell-popovers.ts +65 -62
  201. package/src/grid/grid-open.ts +86 -64
  202. package/src/grid/grid-panel.ts +390 -469
  203. package/src/grid/redirects-grid.ts +40 -44
  204. package/src/hosting/document.ts +2 -0
  205. package/src/hosting/layout.ts +10 -0
  206. package/src/i18n/i18n-commands.ts +127 -62
  207. package/src/media/media-pane.ts +83 -133
  208. package/src/new-project/add-repo-modal.ts +125 -176
  209. package/src/new-project/import-tab.ts +201 -250
  210. package/src/new-project/location-fields.ts +101 -165
  211. package/src/new-project/new-project-modal.ts +376 -367
  212. package/src/packages/jxsuite-update.ts +76 -18
  213. package/src/panels/activity-panel.ts +137 -104
  214. package/src/panels/ai-chat/chat-markdown.ts +15 -13
  215. package/src/panels/ai-chat/chat-view.ts +307 -472
  216. package/src/panels/ai-chat/composer.ts +175 -153
  217. package/src/panels/ai-chat/sessions-view.ts +31 -62
  218. package/src/panels/ai-panel.ts +210 -185
  219. package/src/panels/block-action-bar.ts +437 -643
  220. package/src/panels/bottom-dock.ts +112 -47
  221. package/src/panels/chat-panel.ts +19 -14
  222. package/src/panels/data-explorer.ts +141 -102
  223. package/src/panels/data-grid.ts +183 -150
  224. package/src/panels/dnd.ts +75 -20
  225. package/src/panels/editors.ts +26 -19
  226. package/src/panels/elements-panel.ts +243 -186
  227. package/src/panels/empty-state.ts +26 -42
  228. package/src/panels/events-panel.ts +950 -729
  229. package/src/panels/formula-workspace.ts +434 -207
  230. package/src/panels/frontmatter-fields.ts +185 -153
  231. package/src/panels/frontmatter-panel.ts +411 -187
  232. package/src/panels/git-diff-open.ts +148 -0
  233. package/src/panels/git-panel.ts +679 -605
  234. package/src/panels/head-panel.ts +617 -435
  235. package/src/panels/i18n-panel.ts +153 -133
  236. package/src/panels/imports-panel.ts +388 -414
  237. package/src/panels/jump-bar.ts +174 -147
  238. package/src/panels/layers-panel.ts +785 -671
  239. package/src/panels/left-panel.ts +166 -94
  240. package/src/panels/navigator-panels.ts +1 -1
  241. package/src/panels/pane-context.ts +529 -677
  242. package/src/panels/pane-grid.ts +265 -241
  243. package/src/panels/panel-registry.ts +15 -4
  244. package/src/panels/problems-panel.ts +177 -99
  245. package/src/panels/properties-panel.ts +1525 -966
  246. package/src/panels/provenance.ts +31 -113
  247. package/src/panels/quick-search.ts +217 -172
  248. package/src/panels/right-panel.ts +135 -169
  249. package/src/panels/seo-modal.ts +346 -167
  250. package/src/panels/settings-menu.ts +109 -646
  251. package/src/panels/settings-pane.ts +102 -86
  252. package/src/panels/signals-panel.ts +1318 -1172
  253. package/src/panels/statement-editor.ts +556 -428
  254. package/src/panels/style-panel.ts +1367 -630
  255. package/src/panels/style-utils.ts +36 -32
  256. package/src/panels/stylebook-doc.ts +30 -73
  257. package/src/panels/stylebook-layers-panel.ts +202 -109
  258. package/src/panels/stylebook-panel.ts +124 -88
  259. package/src/panels/tab-strip.ts +385 -287
  260. package/src/platforms/cloud.ts +347 -86
  261. package/src/platforms/devserver.ts +13 -0
  262. package/src/publish/deploy-checklist.ts +124 -78
  263. package/src/publish/publish-commands.ts +4 -12
  264. package/src/publish/publish-panel.ts +165 -299
  265. package/src/services/a11y-report.ts +100 -43
  266. package/src/services/ai-command-tools.ts +468 -0
  267. package/src/services/ai-import-tools.ts +50 -12
  268. package/src/services/ai-models.ts +108 -1
  269. package/src/services/ai-session-store.ts +6 -0
  270. package/src/services/ai-system-prompt.ts +110 -18
  271. package/src/services/ai-tools.ts +28 -143
  272. package/src/services/ai-write-report.ts +174 -0
  273. package/src/services/context-manager.ts +12 -1
  274. package/src/services/document-assistant.ts +50 -2
  275. package/src/services/idle.ts +16 -14
  276. package/src/services/live-surfaces.ts +237 -0
  277. package/src/services/model-uri.ts +75 -0
  278. package/src/services/notify.ts +63 -6
  279. package/src/services/popover-report.ts +325 -0
  280. package/src/services/surface-registry.ts +56 -0
  281. package/src/services/tool-executor.ts +17 -0
  282. package/src/settings/contexts-section.ts +301 -299
  283. package/src/settings/contributed-section.ts +361 -222
  284. package/src/settings/css-vars-editor.ts +320 -325
  285. package/src/settings/defs-editor.ts +572 -535
  286. package/src/settings/dependencies-editor.ts +79 -107
  287. package/src/settings/extension-commands.ts +379 -0
  288. package/src/settings/extension-rows.ts +113 -0
  289. package/src/settings/extensions-section.ts +287 -0
  290. package/src/settings/general-settings.ts +130 -181
  291. package/src/settings/head-editor.ts +10 -305
  292. package/src/settings/locales-section.ts +146 -196
  293. package/src/settings/preferences-accounts.ts +193 -12
  294. package/src/settings/preferences-dialog.ts +384 -307
  295. package/src/settings/project-sections.ts +108 -181
  296. package/src/settings/schema-field-ui.ts +11 -399
  297. package/src/settings/section-registry.ts +8 -1
  298. package/src/settings/settings-document.ts +31 -29
  299. package/src/shell/tree.ts +42 -87
  300. package/src/shell.ts +48 -36
  301. package/src/studio.ts +90 -67
  302. package/src/style/token-ref.ts +0 -39
  303. package/src/surfaces/about.json +263 -0
  304. package/src/surfaces/about.ts +163 -0
  305. package/src/surfaces/add-repo.json +486 -0
  306. package/src/surfaces/add-repo.ts +236 -0
  307. package/src/surfaces/ai-chat.json +2185 -0
  308. package/src/surfaces/ai-chat.ts +549 -0
  309. package/src/surfaces/ai-credentials-form.json +332 -0
  310. package/src/surfaces/ai-credentials-form.ts +151 -0
  311. package/src/surfaces/ai-managed-connect.json +108 -0
  312. package/src/surfaces/ai-managed-connect.ts +123 -0
  313. package/src/surfaces/ai-model-picker.json +114 -0
  314. package/src/surfaces/ai-model-picker.ts +139 -0
  315. package/src/surfaces/block-action-bar.json +640 -0
  316. package/src/surfaces/block-action-bar.ts +368 -0
  317. package/src/surfaces/bottom-dock.json +154 -0
  318. package/src/surfaces/bottom-dock.ts +125 -0
  319. package/src/surfaces/canvas-stage.json +444 -0
  320. package/src/surfaces/canvas-stage.ts +253 -0
  321. package/src/surfaces/cf-account-picker.json +300 -0
  322. package/src/surfaces/cf-account-picker.ts +214 -0
  323. package/src/surfaces/commandbar.json +984 -0
  324. package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +414 -389
  325. package/src/surfaces/convert-repeater.json +309 -0
  326. package/src/surfaces/convert-repeater.ts +276 -0
  327. package/src/surfaces/data-actions.json +157 -0
  328. package/src/surfaces/data-actions.ts +115 -0
  329. package/src/surfaces/dialog.json +289 -0
  330. package/src/surfaces/dialog.ts +343 -0
  331. package/src/surfaces/diff-toolbar.json +266 -0
  332. package/src/surfaces/diff-toolbar.ts +139 -0
  333. package/src/surfaces/doc-header.json +739 -0
  334. package/src/surfaces/doc-header.ts +223 -0
  335. package/src/surfaces/empty-state.json +152 -0
  336. package/src/surfaces/empty-state.ts +173 -0
  337. package/src/surfaces/entry-editor.json +281 -0
  338. package/src/surfaces/entry-editor.ts +191 -0
  339. package/src/surfaces/expression-editor.json +977 -0
  340. package/src/surfaces/expression-editor.ts +266 -0
  341. package/src/surfaces/files-panel.json +592 -0
  342. package/src/surfaces/files-panel.ts +301 -0
  343. package/src/surfaces/formula-palette.json +364 -0
  344. package/src/surfaces/formula-palette.ts +340 -0
  345. package/src/surfaces/git-panel.json +1822 -0
  346. package/src/surfaces/git-panel.ts +272 -0
  347. package/src/surfaces/github-auth.json +112 -0
  348. package/src/surfaces/github-auth.ts +139 -0
  349. package/src/surfaces/github-publish.json +206 -0
  350. package/src/surfaces/github-publish.ts +169 -0
  351. package/src/surfaces/grid-cell.json +218 -0
  352. package/src/surfaces/grid-cell.ts +120 -0
  353. package/src/surfaces/grid-open.json +147 -0
  354. package/src/surfaces/grid-open.ts +87 -0
  355. package/src/surfaces/grid-panel.json +706 -0
  356. package/src/surfaces/grid-panel.ts +206 -0
  357. package/src/surfaces/grid-replace.json +179 -0
  358. package/src/surfaces/grid-replace.ts +89 -0
  359. package/src/surfaces/grid-views.json +446 -0
  360. package/src/surfaces/grid-views.ts +172 -0
  361. package/src/surfaces/inspector-dock.json +187 -0
  362. package/src/surfaces/inspector-dock.ts +161 -0
  363. package/src/surfaces/jump-bar.json +243 -0
  364. package/src/surfaces/jump-bar.ts +129 -0
  365. package/src/surfaces/jxsuite-update.json +146 -0
  366. package/src/surfaces/jxsuite-update.ts +149 -0
  367. package/src/surfaces/library-pane.json +1509 -0
  368. package/src/surfaces/library-pane.ts +363 -0
  369. package/src/surfaces/logic-panel.json +1949 -0
  370. package/src/surfaces/logic-panel.ts +369 -0
  371. package/src/surfaces/logic-workspace.json +685 -0
  372. package/src/surfaces/logic-workspace.ts +238 -0
  373. package/src/surfaces/media-browser.json +303 -0
  374. package/src/surfaces/media-browser.ts +145 -0
  375. package/src/surfaces/media-field.json +163 -0
  376. package/src/surfaces/media-field.ts +116 -0
  377. package/src/surfaces/media-pane.json +513 -0
  378. package/src/surfaces/media-pane.ts +197 -0
  379. package/src/surfaces/menu.json +232 -0
  380. package/src/surfaces/menu.ts +375 -0
  381. package/src/surfaces/navigator-dock.json +113 -0
  382. package/src/surfaces/navigator-dock.ts +130 -0
  383. package/src/surfaces/new-project.json +1654 -0
  384. package/src/surfaces/new-project.ts +411 -0
  385. package/src/surfaces/palette.json +535 -0
  386. package/src/surfaces/pane-context.json +1497 -0
  387. package/src/surfaces/pane-context.ts +513 -0
  388. package/src/surfaces/pane-grid.json +188 -0
  389. package/src/surfaces/pane-grid.ts +250 -0
  390. package/src/surfaces/panel-activity.json +563 -0
  391. package/src/surfaces/panel-activity.ts +187 -0
  392. package/src/surfaces/panel-data.json +141 -0
  393. package/src/surfaces/panel-data.ts +156 -0
  394. package/src/surfaces/panel-deploy-checklist.json +283 -0
  395. package/src/surfaces/panel-deploy-checklist.ts +175 -0
  396. package/src/surfaces/panel-elements.json +367 -0
  397. package/src/surfaces/panel-elements.ts +175 -0
  398. package/src/surfaces/panel-i18n.json +569 -0
  399. package/src/surfaces/panel-i18n.ts +212 -0
  400. package/src/surfaces/panel-imports.json +780 -0
  401. package/src/surfaces/panel-imports.ts +209 -0
  402. package/src/surfaces/panel-outline.json +734 -0
  403. package/src/surfaces/panel-outline.ts +326 -0
  404. package/src/surfaces/panel-page.json +1017 -0
  405. package/src/surfaces/panel-page.ts +240 -0
  406. package/src/surfaces/panel-problems.json +497 -0
  407. package/src/surfaces/panel-problems.ts +157 -0
  408. package/src/surfaces/panel-signals.json +1973 -0
  409. package/src/surfaces/panel-signals.ts +436 -0
  410. package/src/surfaces/panel-stylebook-layers.json +317 -0
  411. package/src/surfaces/panel-stylebook-layers.ts +155 -0
  412. package/src/surfaces/preferences.json +850 -0
  413. package/src/surfaces/preferences.ts +332 -0
  414. package/src/surfaces/progress-modal.json +110 -0
  415. package/src/surfaces/progress-modal.ts +150 -0
  416. package/src/surfaces/properties-panel.json +1578 -0
  417. package/src/surfaces/properties-panel.ts +337 -0
  418. package/src/surfaces/publish.json +945 -0
  419. package/src/surfaces/publish.ts +392 -0
  420. package/src/surfaces/push-plan.json +110 -0
  421. package/src/surfaces/push-plan.ts +87 -0
  422. package/src/surfaces/rail.json +255 -0
  423. package/src/surfaces/rail.ts +206 -0
  424. package/src/surfaces/reference-field.json +191 -0
  425. package/src/surfaces/reference-field.ts +168 -0
  426. package/src/surfaces/schema-builder.json +1011 -0
  427. package/src/surfaces/schema-builder.ts +181 -0
  428. package/src/surfaces/schema-form.json +1013 -0
  429. package/src/surfaces/schema-form.ts +287 -0
  430. package/src/surfaces/secret-field.json +60 -0
  431. package/src/surfaces/secret-field.ts +146 -0
  432. package/src/surfaces/seo.json +932 -0
  433. package/src/surfaces/seo.ts +334 -0
  434. package/src/surfaces/settings-contexts.json +538 -0
  435. package/src/surfaces/settings-contexts.ts +196 -0
  436. package/src/surfaces/settings-contributed.json +489 -0
  437. package/src/surfaces/settings-contributed.ts +199 -0
  438. package/src/surfaces/settings-css-vars.json +820 -0
  439. package/src/surfaces/settings-css-vars.ts +199 -0
  440. package/src/surfaces/settings-defs.json +1363 -0
  441. package/src/surfaces/settings-defs.ts +232 -0
  442. package/src/surfaces/settings-deploy.json +108 -0
  443. package/src/surfaces/settings-deploy.ts +135 -0
  444. package/src/surfaces/settings-extensions.json +486 -0
  445. package/src/surfaces/settings-extensions.ts +201 -0
  446. package/src/surfaces/settings-head.json +779 -0
  447. package/src/surfaces/settings-head.ts +397 -0
  448. package/src/surfaces/settings-locales.json +420 -0
  449. package/src/surfaces/settings-locales.ts +137 -0
  450. package/src/surfaces/settings-overview.json +423 -0
  451. package/src/surfaces/settings-overview.ts +218 -0
  452. package/src/surfaces/settings-packages.json +549 -0
  453. package/src/surfaces/settings-packages.ts +158 -0
  454. package/src/surfaces/settings-pane.json +147 -0
  455. package/src/surfaces/settings-pane.ts +150 -0
  456. package/src/surfaces/settings-rawjson.json +104 -0
  457. package/src/surfaces/settings-rawjson.ts +107 -0
  458. package/src/surfaces/shell.json +99 -0
  459. package/src/surfaces/shell.ts +30 -0
  460. package/src/surfaces/slash-menu.json +180 -0
  461. package/src/surfaces/slash-menu.ts +272 -0
  462. package/src/surfaces/statements.json +607 -0
  463. package/src/surfaces/statements.ts +238 -0
  464. package/src/surfaces/statusbar.json +94 -0
  465. package/src/{panels → surfaces}/statusbar.ts +163 -90
  466. package/src/surfaces/style-panel.json +1643 -0
  467. package/src/surfaces/style-panel.ts +338 -0
  468. package/src/surfaces/stylebook-chrome.json +122 -0
  469. package/src/surfaces/stylebook-chrome.ts +122 -0
  470. package/src/surfaces/tab-strip.json +448 -0
  471. package/src/surfaces/tab-strip.ts +200 -0
  472. package/src/surfaces/target-line.json +419 -0
  473. package/src/surfaces/target-line.ts +349 -0
  474. package/src/surfaces/toasts.json +189 -0
  475. package/src/surfaces/welcome.json +571 -0
  476. package/src/surfaces/welcome.ts +338 -0
  477. package/src/tabs/project-config.ts +71 -15
  478. package/src/tabs/tab.ts +52 -1
  479. package/src/types.ts +49 -3
  480. package/src/ui/ai-credentials-form.ts +104 -100
  481. package/src/ui/ai-managed-connect.ts +116 -49
  482. package/src/ui/ai-model-picker.ts +102 -47
  483. package/src/ui/cf-account-picker.ts +135 -0
  484. package/src/ui/color-selector.ts +80 -301
  485. package/src/ui/dynamic-slot.ts +43 -283
  486. package/src/ui/expression-editor.ts +797 -576
  487. package/src/ui/field-input.ts +8 -181
  488. package/src/ui/form-controls.ts +838 -366
  489. package/src/ui/formula-chips.ts +80 -85
  490. package/src/ui/kit.ts +33 -0
  491. package/src/ui/layers.ts +293 -593
  492. package/src/ui/media-picker.ts +203 -284
  493. package/src/ui/panel-resize.ts +251 -64
  494. package/src/ui/popover-surface.ts +153 -0
  495. package/src/ui/progress-modal.ts +22 -60
  496. package/src/ui/regions.ts +13 -7
  497. package/src/ui/schema-form.ts +906 -440
  498. package/src/ui/surface.ts +122 -0
  499. package/src/ui/unit-selector.ts +15 -131
  500. package/src/ui/value-source.ts +14 -0
  501. package/src/ui/virtual-window.ts +88 -9
  502. package/src/utils/canvas-media.ts +72 -0
  503. package/src/utils/edit-display.ts +7 -2
  504. package/src/utils/element-selectors.ts +69 -0
  505. package/src/utils/link-target.ts +2 -2
  506. package/src/utils/studio-utils.ts +0 -10
  507. package/src/view.ts +15 -0
  508. package/src/workspace/pane-derive.ts +36 -14
  509. package/src/workspace/workspace.ts +15 -6
  510. package/styles/canvas.css +72 -467
  511. package/styles/forced-colors.css +48 -82
  512. package/styles/forced-colors.json +41 -0
  513. package/styles/inspector.css +2 -1301
  514. package/styles/overlays.css +96 -2823
  515. package/styles/panels.css +6 -1423
  516. package/styles/shell-frame.css +272 -0
  517. package/styles/shell-frame.json +227 -0
  518. package/styles/shell.css +0 -1532
  519. package/styles/tokens.css +65 -136
  520. package/styles/tokens.json +84 -0
  521. package/dist/chunks/Overlay.dev-723bv0df.js +0 -23
  522. package/dist/chunks/adapter-jkcyhgtr.js +0 -14
  523. package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
  524. package/dist/chunks/components-zmx10t62.js +0 -21
  525. package/dist/chunks/content-source-t88vb2dr.js +0 -49
  526. package/dist/chunks/convert-to-repeater-dhw1nrz9.js +0 -268
  527. package/dist/chunks/editor-cptq5jp1.js +0 -37
  528. package/dist/chunks/entry-commands-6z6bhxg0.js +0 -46
  529. package/dist/chunks/extension-sections-3ydgjbdy.js +0 -311
  530. package/dist/chunks/file-ops-rskmhxyq.js +0 -48
  531. package/dist/chunks/files-0cratsr0.js +0 -92
  532. package/dist/chunks/flow-qbfgrpeh.js +0 -634
  533. package/dist/chunks/focus-trap.esm-ggawf1hy.js +0 -1024
  534. package/dist/chunks/focus-visible-6eg70z18.js +0 -167
  535. package/dist/chunks/format-host-tgcmfs4p.js +0 -43
  536. package/dist/chunks/fs-events-hrrg328g.js +0 -21
  537. package/dist/chunks/git-panel-0yywyk76.js +0 -58
  538. package/dist/chunks/index-zqxmqxmh.js +0 -109
  539. package/dist/chunks/jx-validate-8q2g05x1.js +0 -19
  540. package/dist/chunks/layers-panel-z3tqe5cd.js +0 -67
  541. package/dist/chunks/layers-q75tj2mf.js +0 -45
  542. package/dist/chunks/lit-html-j055mkh8.js +0 -22
  543. package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
  544. package/dist/chunks/notify-yjm79n4s.js +0 -30
  545. package/dist/chunks/parse-24d0gay3.js +0 -19
  546. package/dist/chunks/platform-ba1j1gbh.js +0 -16
  547. package/dist/chunks/publish-panel-54jvhskq.js +0 -28
  548. package/dist/chunks/quick-search-cgfmjwq5.js +0 -65
  549. package/dist/chunks/references-3qx8fkyp.js +0 -29
  550. package/dist/chunks/right-panel-cqp8bc4v.js +0 -46
  551. package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
  552. package/dist/chunks/sp-overlay.dev-yztmtrs1.js +0 -18
  553. package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
  554. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
  555. package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +0 -15
  556. package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
  557. package/dist/chunks/studio-0a5qrh01.js +0 -3
  558. package/dist/chunks/studio-18j3bmcz.js +0 -14
  559. package/dist/chunks/studio-1hbqnt19.js +0 -1823
  560. package/dist/chunks/studio-4rzyrtjz.js +0 -426
  561. package/dist/chunks/studio-5rywrkxz.js +0 -1903
  562. package/dist/chunks/studio-7afd21nw.js +0 -67
  563. package/dist/chunks/studio-7nte2hj4.js +0 -613
  564. package/dist/chunks/studio-8jpzqk5s.js +0 -128
  565. package/dist/chunks/studio-8q517dfr.js +0 -24
  566. package/dist/chunks/studio-9c4k4vhv.js +0 -357
  567. package/dist/chunks/studio-9n4mrmnb.js +0 -184
  568. package/dist/chunks/studio-a0tswy0w.js +0 -58
  569. package/dist/chunks/studio-ahjfdy6e.js +0 -387
  570. package/dist/chunks/studio-ahnzd9rg.js +0 -23
  571. package/dist/chunks/studio-bw5dz6km.js +0 -608
  572. package/dist/chunks/studio-cb12yz9w.js +0 -15055
  573. package/dist/chunks/studio-d1ks410h.js +0 -175
  574. package/dist/chunks/studio-eqq1w638.js +0 -113
  575. package/dist/chunks/studio-ex8zyp98.js +0 -1813
  576. package/dist/chunks/studio-h4rxscfc.js +0 -10
  577. package/dist/chunks/studio-kyh9avvv.js +0 -28
  578. package/dist/chunks/studio-kyk2v92a.js +0 -659
  579. package/dist/chunks/studio-m3hwn7y1.js +0 -120
  580. package/dist/chunks/studio-mmv72a27.js +0 -4344
  581. package/dist/chunks/studio-n8b9wqwy.js +0 -291
  582. package/dist/chunks/studio-pdfjntbk.js +0 -6939
  583. package/dist/chunks/studio-qq6jym04.js +0 -53
  584. package/dist/chunks/studio-r1r7afxx.js +0 -1066
  585. package/dist/chunks/studio-rdx8jcxy.js +0 -83
  586. package/dist/chunks/studio-rn9ta2mz.js +0 -111
  587. package/dist/chunks/studio-t7qb9db8.js +0 -665
  588. package/dist/chunks/studio-tmc5emrw.js +0 -8
  589. package/dist/chunks/studio-txwkm6p9.js +0 -12
  590. package/dist/chunks/studio-v38hmvx1.js +0 -390
  591. package/dist/chunks/studio-vadzgkrz.js +0 -318
  592. package/dist/chunks/studio-wx5609xd.js +0 -341
  593. package/dist/chunks/studio-z6nw2ek2.js +0 -18
  594. package/dist/chunks/studio-zczps70q.js +0 -378
  595. package/dist/chunks/workspace-xpres40v.js +0 -89
  596. package/src/panels/activity-bar.ts +0 -251
  597. package/src/panels/panel-scheduler.ts +0 -219
  598. package/src/panels/style-inputs.ts +0 -237
  599. package/src/panels/target-line.ts +0 -274
  600. package/src/panels/welcome-screen.ts +0 -346
  601. package/src/ui/button-group.ts +0 -102
  602. package/src/ui/choice-field.ts +0 -72
  603. package/src/ui/field-row.ts +0 -249
  604. package/src/ui/formula-palette.ts +0 -213
  605. package/src/ui/icons.ts +0 -52
  606. package/src/ui/jx-theme.ts +0 -130
  607. package/src/ui/spectrum.ts +0 -340
  608. package/src/ui/value-selector.ts +0 -174
  609. package/src/ui/widgets.ts +0 -172
  610. /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
  611. /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
  612. /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
  613. /package/dist/chunks/{tsMode-ef187txx.css → tsMode-hnxxb73h.css} +0 -0
@@ -4,9 +4,9 @@
4
4
  * dispatches to manage/settings/source/edit/design/preview rendering paths.
5
5
  */
6
6
 
7
- import { html, render as litRender, nothing } from "lit-html";
8
- import { ref } from "lit-html/directives/ref.js";
7
+ import { render as litRender, nothing } from "lit-html";
9
8
  import type * as monaco from "monaco-editor";
9
+ import type { WireDiffMarks } from "./iframe-protocol";
10
10
  import type * as Y from "yjs";
11
11
  import { loadedMonaco, loadMonaco, mountStillWanted } from "../services/monaco-lazy";
12
12
 
@@ -33,12 +33,18 @@ import {
33
33
  stringProperty,
34
34
  } from "../commands/command-args";
35
35
  import { collabSourceContext } from "../collab/collab-session";
36
+ import { buildChangeMap } from "./diff-marks";
37
+ import type { ChangeMap, ChangeMark } from "./diff-marks";
38
+ import { clearDiffView, diffViewOf, setDiffChangeMap } from "./diff-view";
39
+ import { canRenderComparison } from "../panels/git-diff-open";
40
+ import { renderDiffToolbar, setDiffRepaint, setDiffToolbarHost } from "./diff-toolbar";
36
41
  import { attachCursorStyles } from "../collab/monaco-cursors";
37
42
  import type { AwarenessLike } from "../collab/monaco-cursors";
38
43
  import type { BindingAwareness } from "../collab/monaco-binding";
39
44
  import { monacoTheme, shell } from "../shell";
40
45
  import { parseSourceForPath } from "../files/file-ops";
41
46
  import { serializeDocument } from "../files/serialize-document";
47
+ import { parseJsonDocument, serializeJson } from "../files/json-layout";
42
48
  import { detachGridPanel, gridPanelMounted, renderGridMode } from "../grid/grid-panel";
43
49
  import { detachLibraryPane, libraryPaneMounted, renderLibraryMode } from "../browse/library-pane";
44
50
  import { detachEntryPane, entryPaneMounted, renderEntryMode } from "../content/entry-editor";
@@ -56,24 +62,31 @@ import {
56
62
  bufferWrites,
57
63
  commitBufferWrites,
58
64
  } from "../services/monaco-buffer";
59
- import { modelUriFor } from "../services/model-uri";
60
- import { renderWelcome } from "../panels/welcome-screen";
61
- import { renderEmptyState } from "../panels/empty-state";
62
- import {
63
- attachDocumentHeaderHost,
64
- documentHeaderHost,
65
- hasDocumentHeader,
66
- } from "../panels/frontmatter-panel";
65
+ import { diffModelUrisFor, modelUriFor, monacoLangForPath } from "../services/model-uri";
66
+ import { renderWelcome } from "../surfaces/welcome";
67
+ import { emptyState } from "../surfaces/empty-state";
68
+ import { attachDocumentHeaderHost, hasDocumentHeader } from "../panels/frontmatter-panel";
69
+ import { mountCanvasStage } from "../surfaces/canvas-stage";
70
+ import type {
71
+ CanvasStageActions,
72
+ CanvasStageHandle,
73
+ CanvasStageView,
74
+ } from "../surfaces/canvas-stage";
75
+ import type { CanvasPanelEntry } from "./canvas-utils";
67
76
  import { projectState } from "../state";
68
77
  import {
78
+ activateCanvasPanel,
69
79
  applyEditZoom,
70
80
  applyTransform,
71
- canvasPanelTemplate,
81
+ bindCanvasPanels,
82
+ canvasPanelEntry,
72
83
  fitOnCanvasEntry,
73
84
  observeCenterUntilStable,
74
85
  updateActivePanelHeaders,
75
86
  } from "./canvas-utils";
76
87
  import { parseMediaEntries } from "../utils/canvas-media";
88
+ import { clearEditWidth, resolveEditColumnWidth } from "./edit-width";
89
+ import { mountEditWidthHandle } from "./edit-width-drag";
77
90
  import { getEffectiveMedia, getEffectiveStyle } from "../site-context";
78
91
  import {
79
92
  adoptCanvasPreviewMode,
@@ -82,6 +95,7 @@ import {
82
95
  mountIframeCanvas,
83
96
  postApplyFormat,
84
97
  postOpenSlash,
98
+ postRedefineElementToLiveHosts,
85
99
  postStyleUpdateToStylebookHosts,
86
100
  releaseCanvasHosts,
87
101
  } from "./iframe-host";
@@ -104,7 +118,6 @@ import { activeRegistry } from "../commands/active-registry";
104
118
  import { notify } from "../services/notify";
105
119
  import * as overlaysPanel from "../panels/overlays";
106
120
 
107
- import type { TemplateResult } from "lit-html";
108
121
  import type { CanvasPanel, GitDiffState } from "../types";
109
122
  import type { AnyCommand, CommandRegistry } from "../commands/registry";
110
123
  import type { JxMutableNode } from "@jxsuite/schema/types";
@@ -152,14 +165,16 @@ async function sourceContent(tab: Tab) {
152
165
  if (formatByName(tab.doc.sourceFormat)) {
153
166
  return serializeDocument(tab);
154
167
  }
155
- return JSON.stringify(tab.doc.document, null, 2);
168
+ // The file's own layout (`files/json-layout.ts`), so the buffer shows the bytes a save writes and
169
+ // A commit from it reads the same layout back — what `serializeDocument`'s JSON branch does.
170
+ return serializeJson(tab.doc.document, tab.doc.layout ?? null);
156
171
  }
157
172
 
158
173
  // Single-RAF scheduling; concurrent schedule requests within the same frame are deduped.
159
174
  //
160
175
  /* Two nested rAFs used to make the canvas render "yield to higher-priority panel paints first".
161
- Panels have since grown their own rAF scheduler (see panel-scheduler.ts), so the second frame
162
- bought nothing and put a hard ~32 ms floor under every escalated edit — canvas-patcher's
176
+ Panels have since stopped repainting their own chrome at all (the docks are Jx documents), so
177
+ the second frame bought nothing and put a hard ~32 ms floor under every escalated edit — canvas-patcher's
163
178
  escalateToFullRender routes through here. */
164
179
  /*
165
180
  * One pending frame PER PANE. A shared id would have let the pane that scheduled first swallow the
@@ -215,45 +230,153 @@ function hardClearCanvasWrap(canvasWrap: HTMLElement) {
215
230
  }
216
231
 
217
232
  /**
218
- * Hand the Document Header card the node the stage just made for it.
233
+ * Where each stage's non-artboard hosts landed, keyed by part (and by the static mark that tells
234
+ * two of one part apart: a handle's side, a card's placement).
219
235
  *
220
- * One stable callback, so Lit invokes it once per host element rather than on every render. The
221
- * `undefined` branch is Lit reporting a removal WITHOUT saying which node — and the two placements
222
- * share this callback, so an Edit→Design swap can report the outgoing host after the incoming one
223
- * has already been bound. Connectivity is the fact that settles it: only a host that really left
224
- * the document releases the card.
236
+ * Per STAGE rather than per module, for the reason every other field on `CanvasSurface` is: two
237
+ * panes each have a Monaco host and a card slot, and one slot would hand the second pane's stage
238
+ * the first pane's editor.
225
239
  */
226
- const _docHeaderRefs = new Map<string, (el: Element | undefined) => void>();
227
-
228
- function docHeaderRef(paneId: string): (el: Element | undefined) => void {
229
- let bind = _docHeaderRefs.get(paneId);
230
- if (!bind) {
231
- bind = (el: Element | undefined) => {
232
- if (el) {
233
- attachDocumentHeaderHost(paneId, el as HTMLElement);
234
- return;
235
- }
236
- if (!documentHeaderHost(paneId)?.isConnected) {
237
- attachDocumentHeaderHost(paneId, null);
238
- }
239
- };
240
- _docHeaderRefs.set(paneId, bind);
240
+ const _stageHosts = new WeakMap<CanvasSurface, Map<string, HTMLElement>>();
241
+
242
+ /** The artboards the last pass described, so a header's click can resolve one. */
243
+ const _stageEntries = new WeakMap<CanvasSurface, CanvasPanelEntry[]>();
244
+
245
+ /**
246
+ * One stable action record per stage.
247
+ *
248
+ * Stable because the document reads it ONCE, when it mounts, and the mount outlives every repaint:
249
+ * a record rebuilt per pass would leave the header clicking through to the first pass's artboards
250
+ * for the life of the stage. Both members close over the surface and read the pass's state through
251
+ * the two maps above, which is what makes one record correct forever.
252
+ */
253
+ const _stageActions = new WeakMap<CanvasSurface, CanvasStageActions>();
254
+
255
+ /** This stage's host map, created on first use. */
256
+ function stageHosts(surface: CanvasSurface): Map<string, HTMLElement> {
257
+ let hosts = _stageHosts.get(surface);
258
+ if (!hosts) {
259
+ hosts = new Map();
260
+ _stageHosts.set(surface, hosts);
241
261
  }
242
- return bind;
262
+ return hosts;
263
+ }
264
+
265
+ /** The node this stage drew for a part, or null when this frame draws none. */
266
+ function stageHost(surface: CanvasSurface, part: string): HTMLElement | null {
267
+ return _stageHosts.get(surface)?.get(part) ?? null;
268
+ }
269
+
270
+ /** This stage's action record — see {@link _stageActions}. */
271
+ function stageActions(surface: CanvasSurface): CanvasStageActions {
272
+ let actions = _stageActions.get(surface);
273
+ if (actions) {
274
+ return actions;
275
+ }
276
+ actions = {
277
+ host: (part, element, detail) => {
278
+ stageHosts(surface).set(detail ? `${part}:${detail}` : part, element);
279
+ if (part === "panzoom") {
280
+ surface.panzoomWrap = element;
281
+ } else if (part === "doc-header") {
282
+ attachDocumentHeaderHost(surface.paneId, element);
283
+ } else if (part === "diff-toolbar") {
284
+ setDiffToolbarHost(surface.paneId, element);
285
+ }
286
+ },
287
+ pickPanel: (key) => activateCanvasPanel(_stageEntries.get(surface) ?? [], key),
288
+ };
289
+ _stageActions.set(surface, actions);
290
+ return actions;
291
+ }
292
+
293
+ /**
294
+ * Draw this pane's stage, and settle once its artboards are in the page.
295
+ *
296
+ * **The mount is kept, and the view is written into it.** Every artboard owns a live iframe holding
297
+ * a rendered document, so keeping the stage across a content-only repaint is the difference between
298
+ * a repaint and a reload — the same guarantee lit's positional diffing gave, from a keyed array
299
+ * that follows a breakpoint's NAME rather than its index. {@link detachCanvasStage} is what ends a
300
+ * mount, and it runs on a real mode transition, which is exactly when the structure on the stage
301
+ * stops being the stage's.
302
+ *
303
+ * **Nothing may read an artboard's DOM on the next line.** A mapped array reconciles on a microtask
304
+ * (`surfaces/canvas-stage.ts` says why), so the records are filled here, behind the promise, and
305
+ * every caller's geometry hangs off it.
306
+ *
307
+ * @param {CanvasSurface} surface
308
+ * @param {CanvasStageView} view
309
+ * @param {CanvasPanelEntry[]} entries - The artboards `view.panels` describes, in the same order
310
+ * @returns {Promise<void>}
311
+ */
312
+ function drawStage(
313
+ surface: CanvasSurface,
314
+ view: CanvasStageView,
315
+ entries: CanvasPanelEntry[],
316
+ ): Promise<CanvasStageHandle | null> {
317
+ _stageEntries.set(surface, entries);
318
+ /* There is no card-host release here, and there must not be one: `renderCanvasImpl` already
319
+ releases it for EVERY mode that draws no header, one branch above this one. A second answer to
320
+ the same question is how the two drift — and this one would answer only for the frames that
321
+ reach a stage, leaving grid, library, entry, media and settings to the first. */
322
+ const standing = surface.stage;
323
+ const stage = standing ?? mountCanvasStage(surface.wrap, view, stageActions(surface));
324
+ surface.stage = stage;
325
+ return (standing ? stage.update(view) : stage.ready).then(() => {
326
+ /* NULL means "this stage is gone", and it is the ONLY staleness this seam answers. A second
327
+ pass in the same mode shares the handle and passes here, which is deliberate: its artboards
328
+ still mount, under their own (now stale) generation, and the FRAME drops them — the contract
329
+ `passGen` exists for. What must never run is work against a stage a mode transition has
330
+ already disposed, whose hosts are released and whose records would bind to nothing. */
331
+ if (surface.stage !== stage) {
332
+ return null;
333
+ }
334
+ bindCanvasPanels(entries, stage);
335
+ return stage;
336
+ });
337
+ }
338
+
339
+ /**
340
+ * The two frames that hold no artboards: one Monaco filling the stage, with or without the Compare
341
+ * bar over it. Written once because the difference between them is a lead and a part.
342
+ *
343
+ * @param {"source" | "code"} frame
344
+ * @returns {CanvasStageView}
345
+ */
346
+ function editorStageView(frame: "source" | "code"): CanvasStageView {
347
+ return {
348
+ columnHeader: "hidden",
349
+ frame,
350
+ framePart: "panzoom",
351
+ frameVars: "",
352
+ handles: "hidden",
353
+ hug: false,
354
+ innerPart: "boards",
355
+ lead: frame === "code" ? "toolbar" : "none",
356
+ panels: [],
357
+ };
243
358
  }
244
359
 
245
360
  /**
246
- * The stage's slot for the Document Header card.
361
+ * Release this pane's stage document.
247
362
  *
248
- * `in-column` is Edit: the card is a block of the artefact's own column and scrolls with it.
249
- * `pinned` is Design: the artboards are drawn under a pan/zoom transform, so the card sits above
250
- * the surface at 1:1 and keeps its own scroll.
363
+ * Called on a real mode transition and on a full reset — the two moments the structure standing on
364
+ * the stage stops being the stage's. The two hosts that are handed to another module go WITH it: a
365
+ * card slot or a Compare bar pointing at a detached node is a surface that draws into nothing and
366
+ * never says so.
251
367
  *
252
- * @param {"in-column" | "pinned"} placement
253
- * @returns {TemplateResult}
368
+ * @param {CanvasSurface} surface
254
369
  */
255
- function docHeaderSlot(placement: "in-column" | "pinned", paneId: string): TemplateResult {
256
- return html`<div class="doc-header-host ${placement}" ${ref(docHeaderRef(paneId))}></div>`;
370
+ function detachCanvasStage(surface: CanvasSurface): void {
371
+ if (!surface.stage) {
372
+ return;
373
+ }
374
+ surface.stage.dispose();
375
+ surface.stage = null;
376
+ _stageHosts.delete(surface);
377
+ _stageEntries.delete(surface);
378
+ attachDocumentHeaderHost(surface.paneId, null);
379
+ setDiffToolbarHost(surface.paneId, null);
257
380
  }
258
381
 
259
382
  /**
@@ -366,6 +489,131 @@ function disposeSourceEditor(surface: CanvasSurface): void {
366
489
  surface.monacoEditor = null;
367
490
  }
368
491
 
492
+ /**
493
+ * Tear down a comparison's Code view.
494
+ *
495
+ * **Both models, explicitly.** Monaco never disposes models a caller created, and a leaked model
496
+ * keeps its URI claimed — which is the same throw one mount later, on a stage that looks empty
497
+ * rather than broken.
498
+ *
499
+ * **No `commitBufferWrites`.** `disposeSourceEditor` calls it because a source buffer can hold
500
+ * unsaved text; a comparison holds two read-only texts, one of which is a git object with no place
501
+ * on disk to be written back to. There is nothing to flush, which is the same reason this editor is
502
+ * deliberately absent from `buffersForTab`.
503
+ */
504
+ function disposeDiffEditor(surface: CanvasSurface): void {
505
+ const editor = surface.monacoDiffEditor;
506
+ if (!editor) {
507
+ return;
508
+ }
509
+ const model = editor.getModel();
510
+ editor.setModel(null);
511
+ model?.original.dispose();
512
+ model?.modified.dispose();
513
+ editor.dispose();
514
+ surface.monacoDiffEditor = null;
515
+ }
516
+
517
+ /**
518
+ * Mount the comparison's Monaco diff editor: HEAD on the left, the working copy on the right.
519
+ *
520
+ * This is {@link mountSourceEditor} minus everything about writing — no change handler, no
521
+ * debounce, no collab binding. **The collab lock in particular is never entered**: `enter()` flips
522
+ * the room's canonical lock to "source" and freezes structural editing for every peer, and doing
523
+ * that to show somebody a read-only comparison would freeze a live co-editing session on a gesture
524
+ * nobody made.
525
+ *
526
+ * The post-await guard is the one `mountSourceEditor`'s comment explains at length, and it is not
527
+ * belt-and-braces here either: `renderCanvasImpl` writes `surface.prevCanvasMode` BEFORE this runs,
528
+ * so a second synchronous render inside the 12.6 MB cold load sees `modeChanged === false` and a
529
+ * still-null slot, falls through, and mounts again — and the second `createModel` claims a URI the
530
+ * first registered, which real Monaco throws on. Asked before `createModel`, for that reason.
531
+ *
532
+ * A RETARGET is the second, independent race, and `mountStillWanted` cannot catch it: it answers
533
+ * false when the slot is already filled, so a comparison switching to a different file would keep
534
+ * the old editor and its claimed URIs. That one is disposed synchronously, before the await.
535
+ */
536
+ /**
537
+ * Whether this pane draws the comparison as TEXT.
538
+ *
539
+ * One definition, because two callers must never disagree about it: the render branch chooses which
540
+ * half to build, and the mount's post-await guard re-asks whether that half is still wanted. They
541
+ * were two spellings — the branch said "not renderable OR the author chose Code", the guard said
542
+ * only "the author chose Code" — so for a file with no visual half the branch built the Code
543
+ * container and the guard then refused to mount into it. An empty stage, no error, and a toolbar
544
+ * correctly describing an editor that was never there.
545
+ */
546
+ /**
547
+ * One artboard's marks, with a modification given the face that side wears.
548
+ *
549
+ * {@link ChangeMap} names a modification once, semantically; the two artboards draw it as the before
550
+ * and the after. The split happens here because this is the only place that knows which document
551
+ * each mark set is about to be painted on.
552
+ */
553
+ function sideMarks(
554
+ marks: readonly ChangeMark[] | undefined,
555
+ modified: "modified-before" | "modified-after",
556
+ ): WireDiffMarks | null {
557
+ return (
558
+ marks?.map((mark) => ({
559
+ kind: mark.kind === "modified" ? modified : mark.kind,
560
+ path: mark.path,
561
+ })) ?? null
562
+ );
563
+ }
564
+
565
+ function diffShowsCode(paneId: string, filePath: string): boolean {
566
+ return !canRenderComparison(filePath) || diffViewOf(paneId) === "code";
567
+ }
568
+
569
+ async function mountDiffEditor(
570
+ surface: CanvasSurface,
571
+ container: Element,
572
+ state: GitDiffState,
573
+ ): Promise<void> {
574
+ const { paneId } = surface;
575
+ const key = state.filePath;
576
+ if (surface.monacoDiffEditor && surface.monacoDiffEditor._diffKey !== key) {
577
+ disposeDiffEditor(surface);
578
+ }
579
+ const monaco = await loadMonaco();
580
+ if (
581
+ !mountStillWanted(
582
+ container,
583
+ surface.monacoDiffEditor,
584
+ () => canvasModeOfPane(paneId) === "git-diff" && diffShowsCode(paneId, key),
585
+ )
586
+ ) {
587
+ return;
588
+ }
589
+ const uris = diffModelUrisFor(paneId, key || "document.json");
590
+ const lang = monacoLangForPath(key || "document.json");
591
+ const original = monaco.editor.createModel(
592
+ state.originalContent || "",
593
+ lang,
594
+ monaco.Uri.parse(uris.head),
595
+ );
596
+ const modified = monaco.editor.createModel(
597
+ state.currentContent || "",
598
+ lang,
599
+ monaco.Uri.parse(uris.work),
600
+ );
601
+ const editor = monaco.editor.createDiffEditor(container as HTMLElement, {
602
+ automaticLayout: true,
603
+ fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace",
604
+ fontSize: 12,
605
+ minimap: { enabled: false },
606
+ originalEditable: false,
607
+ readOnly: true,
608
+ renderSideBySide: true,
609
+ scrollBeyondLastLine: false,
610
+ theme: monacoTheme(),
611
+ }) as NonNullable<CanvasSurface["monacoDiffEditor"]>;
612
+ editor.setModel({ modified, original });
613
+ editor._diffKey = key;
614
+ surface.monacoDiffEditor = editor;
615
+ }
616
+
369
617
  function resetCanvasView(surface: CanvasSurface) {
370
618
  const canvasWrap = surface.wrap;
371
619
  const { paneId } = surface;
@@ -376,6 +624,7 @@ function resetCanvasView(surface: CanvasSurface) {
376
624
  detachSettingsPane(paneId);
377
625
  disposeSourceCollab(surface);
378
626
  disposeSourceEditor(surface);
627
+ disposeDiffEditor(surface);
379
628
  if (surface.centerObserver) {
380
629
  surface.centerObserver.disconnect();
381
630
  surface.centerObserver = null;
@@ -598,7 +847,11 @@ async function mountSourceEditor(
598
847
  }
599
848
  } else if (lang === "json") {
600
849
  try {
601
- tab.doc.document = JSON.parse(sourceEditor.getValue()) as JxMutableNode;
850
+ // The buffer's text is the author's own layout now — the objects they opened up or
851
+ // Closed, the blank lines they left — so it is what the next save preserves.
852
+ const parsed = parseJsonDocument(sourceEditor.getValue());
853
+ tab.doc.document = parsed.document as JxMutableNode;
854
+ tab.doc.layout = parsed.layout;
602
855
  tab.doc.dirty = true;
603
856
  writes.markSettled();
604
857
  } catch {
@@ -627,6 +880,25 @@ export function renderCanvas(paneId: string = workspace.activePaneId) {
627
880
  });
628
881
  }
629
882
 
883
+ /**
884
+ * A kit component was saved: redefine it in every live frame, then render every pane, because a
885
+ * frame's instances keep the definition they rendered until something renders them again
886
+ * (embedding.md §7). The render is what makes the redefinition visible; the message alone would
887
+ * leave every open canvas drawing the old element beside a shell drawing the new one.
888
+ *
889
+ * @param {JxMutableNode} doc The component document, as saved.
890
+ * @param {string} base The URL the frame resolves the document's own references against — the
891
+ * file's URL under the project, so `$elements` siblings resolve to the project's copies.
892
+ * @returns {number} How many frames were told.
893
+ */
894
+ export function redefineElementOnCanvases(doc: JxMutableNode, base: string): number {
895
+ const posted = postRedefineElementToLiveHosts(doc, base);
896
+ for (const pane of workspace.panes) {
897
+ renderCanvas(pane.id);
898
+ }
899
+ return posted;
900
+ }
901
+
630
902
  /**
631
903
  * Say, on this stage, why a derived pane has nothing to draw.
632
904
  *
@@ -636,8 +908,11 @@ export function renderCanvas(paneId: string = workspace.activePaneId) {
636
908
  * derivation, no tabs, no strip chip, no context bar and a blank stage that `paneIsEmpty` would not
637
909
  * collapse. The sentence written for the honest empty state was dead code.
638
910
  *
639
- * Through {@link renderEmptyState} rather than a block of its own: the copy rules are inherited,
640
- * and a derived pane with nothing to show is the same kind of region as any other.
911
+ * Through {@link emptyState} rather than a block of its own: the copy rules are inherited, and a
912
+ * derived pane with nothing to show is the same kind of region as any other. The stage stays a lit
913
+ * render root and the notice is a NODE it interpolates — `surfaces/empty-state.ts` owns a `display:
914
+ * contents` host, mounts the document into it once, and assigns after that, so the stage flipping
915
+ * between an artboard and this neither rebuilds the document nor strands it.
641
916
  *
642
917
  * **`held` is the document this pane still OWNS, and it is the difference between a notice and a
643
918
  * riddle.** A companion that resolved once has a real tab, so `panels/tab-strip.ts` draws a real
@@ -661,7 +936,7 @@ function renderDerivationNotice(
661
936
  const registry = activeRegistry();
662
937
  const pin = registry?.get("pane.pin");
663
938
  litRender(
664
- renderEmptyState({
939
+ emptyState(surface.wrap, {
665
940
  message,
666
941
  ...(held && { detail: `${tabLabel(held)} is still open here.` }),
667
942
  ...(held &&
@@ -798,6 +1073,15 @@ function renderCanvasImpl(surface: CanvasSurface) {
798
1073
  hardClearCanvasWrap(canvasWrap);
799
1074
  }
800
1075
 
1076
+ /* LEAVING THE COMPARISON ENTIRELY, which is the one exit the git-diff branch cannot cover.
1077
+ The Visual/Code switch inside that branch disposes this itself, and so does a pane teardown —
1078
+ but a mode change OUT of git-diff reaches neither, and `hardClearCanvasWrap` above only detaches
1079
+ the DOM. A diff editor left alive holds two models whose URIs stay claimed, so reopening the
1080
+ comparison later finds a live editor for a stale pair of texts. */
1081
+ if (modeChanged && canvasMode !== "git-diff") {
1082
+ disposeDiffEditor(surface);
1083
+ }
1084
+
801
1085
  /* There are no `editingFunction` / `editingFormula` branches here, and that is the point.
802
1086
  Both used to RETURN from this function, which is what made the logic editors a takeover: the
803
1087
  stage kept whatever DOM it was last painted with and stopped tracking the document. P8 moved
@@ -972,9 +1256,12 @@ function renderCanvasImpl(surface: CanvasSurface) {
972
1256
  disposeSourceCollab(surface);
973
1257
  disposeSourceEditor(surface);
974
1258
 
975
- // Same reason as `hardClearCanvasWrap`: lit is about to detach every artboard's iframe, and a
976
- // Detached host is otherwise only noticed by whichever lazy `liveHosts` walk runs next.
1259
+ /* Same reason as `hardClearCanvasWrap`: the stage is about to detach every artboard's iframe,
1260
+ and a detached host is otherwise only noticed by whichever lazy `liveHosts` walk runs next.
1261
+ Both writers are ended here — the stage document, whose structure belongs to the mode that is
1262
+ leaving, and lit's part, which the empty-state and welcome paths own. */
977
1263
  releaseCanvasHosts(canvasWrap);
1264
+ detachCanvasStage(surface);
978
1265
  litRender(nothing, canvasWrap);
979
1266
  surface.panzoomWrap = null;
980
1267
  // Reset inline style overrides from other modes
@@ -996,15 +1283,19 @@ function renderCanvasImpl(surface: CanvasSurface) {
996
1283
  if (canvasMode === "stylebook") {
997
1284
  surface.prevStylebookFilter = shell.stylebook.filter;
998
1285
  surface.prevStylebookCustomizedOnly = shell.stylebook.customizedOnly;
999
- renderStylebookMode(surface, {
1286
+ void renderStylebookMode(surface, {
1000
1287
  applyTransform,
1001
- canvasPanelTemplate,
1288
+ canvasPanelEntry,
1289
+ drawStage,
1002
1290
  observeCenterUntilStable,
1291
+ stageHost,
1003
1292
  updateActivePanelHeaders,
1293
+ }).then(() => {
1294
+ // After the catalogue's artboards are in the page: the fit measures them.
1295
+ if (modeChanged) {
1296
+ fitOnCanvasEntry(surface);
1297
+ }
1004
1298
  });
1005
- if (modeChanged) {
1006
- fitOnCanvasEntry(surface);
1007
- }
1008
1299
  return;
1009
1300
  }
1010
1301
 
@@ -1029,6 +1320,13 @@ function renderCanvasImpl(surface: CanvasSurface) {
1029
1320
  * rendering. Same expression as Edit's column, so the same page at `md` is the same page
1030
1321
  * whichever of the two the toggle is over.
1031
1322
  *
1323
+ * That equality now holds at the BREAKPOINT, not at the pixel, and deliberately. Edit's column
1324
+ * can be dragged to any width between two declared ones (`edit-width.ts`); Preview does not read
1325
+ * that override, because Preview is the fidelity view and a fidelity view of "somewhere between
1326
+ * md and lg" is a width no visitor will ever have. Toggling Preview is a mode change, which is
1327
+ * also what discards the dragged width — so the two surfaces agree again the moment either one
1328
+ * is entered, rather than disagreeing silently.
1329
+ *
1032
1330
  * The HEIGHT is untouched: the frame stays the pane's height and the document scrolls itself,
1033
1331
  * which is what makes sticky, scroll-driven animation and IntersectionObserver fire here.
1034
1332
  */
@@ -1038,13 +1336,29 @@ function renderCanvasImpl(surface: CanvasSurface) {
1038
1336
  const previewMedia = activeMediaOfPane(surface.paneId);
1039
1337
  const previewWidth =
1040
1338
  previewBreakpoints.find((bp) => bp.name === previewMedia)?.width ?? previewBase;
1041
- const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
1042
- litRender(
1043
- html`<div class="preview-stage" style="max-width:${previewWidth}px">${panelTpl}</div>`,
1044
- canvasWrap,
1045
- );
1046
- canvasPanels.push(panel as unknown as CanvasPanel);
1047
- renderCanvasIntoPanel(surface, panel as unknown as CanvasPanel, S.ui.featureToggles);
1339
+ const entry = canvasPanelEntry(null, null, true);
1340
+ canvasPanels.push(entry.panel);
1341
+ const { featureToggles: previewToggles } = S.ui;
1342
+ void drawStage(
1343
+ surface,
1344
+ {
1345
+ columnHeader: "hidden",
1346
+ frame: "boards",
1347
+ framePart: "preview-stage",
1348
+ frameVars: `--preview-w:${previewWidth}px`,
1349
+ handles: "hidden",
1350
+ hug: false,
1351
+ innerPart: "boards",
1352
+ lead: "none",
1353
+ panels: [entry.item],
1354
+ },
1355
+ [entry],
1356
+ ).then((stage) => {
1357
+ if (!stage) {
1358
+ return;
1359
+ }
1360
+ renderCanvasIntoPanel(surface, entry.panel, previewToggles);
1361
+ });
1048
1362
  return;
1049
1363
  }
1050
1364
 
@@ -1102,24 +1416,17 @@ function renderCanvasImpl(surface: CanvasSurface) {
1102
1416
  if (canvasMode === "source") {
1103
1417
  canvasWrap.style.padding = "0";
1104
1418
  canvasWrap.style.display = "block";
1105
- let editorContainer: HTMLDivElement | null = null;
1106
- litRender(
1107
- html`<div class="source-wrap">
1108
- <div
1109
- class="source-editor"
1110
- ${ref((el) => {
1111
- if (el) {
1112
- editorContainer = el as HTMLDivElement;
1113
- }
1114
- })}
1115
- ></div>
1116
- </div>`,
1117
- canvasWrap,
1118
- );
1119
-
1120
1419
  const filePath = tab.documentPath || "document.json";
1121
1420
  const lang = sourceLang(tab);
1122
- void mountSourceEditor(tab, surface, editorContainer as unknown as Element, filePath, lang);
1421
+ void drawStage(surface, editorStageView("source"), []).then((stage) => {
1422
+ if (!stage) {
1423
+ return;
1424
+ }
1425
+ const editorContainer = stageHost(surface, "source-editor");
1426
+ if (editorContainer) {
1427
+ void mountSourceEditor(tab, surface, editorContainer, filePath, lang);
1428
+ }
1429
+ });
1123
1430
  return;
1124
1431
  }
1125
1432
 
@@ -1151,39 +1458,66 @@ function renderCanvasImpl(surface: CanvasSurface) {
1151
1458
  canvasWrap.style.overflow = "hidden";
1152
1459
  }
1153
1460
 
1461
+ /* THE CODE HALF, and it is the whole branch rather than an extra artboard.
1462
+ A comparison read as text has no artboards, no pan/zoom and no marks: Monaco computes its own
1463
+ line diff from the two texts `readGitDiff` already holds, and draws its own red and green.
1464
+ This is the ONLY view for a file the canvas cannot render — a .ts, a .css, a .gitignore — and
1465
+ an alternate one for a document it can. */
1466
+ /* A file the canvas cannot DRAW has only the one half, and the renderer is where that is
1467
+ decided rather than the opener: a comparison reaches this branch from three doors (the panel,
1468
+ a Diff lens, the Editor axis) and only this one sees every arrival. Forcing it here also
1469
+ leaves `diffView` alone, so a document opened after a `.css` still comes up Visual. */
1470
+ const renderable = canRenderComparison(gitDiffState.filePath ?? "");
1471
+ if (diffShowsCode(surface.paneId, gitDiffState.filePath ?? "")) {
1472
+ /* A null map is how the toolbar learns there is no visual half to offer: it draws Code as a
1473
+ static label rather than as half of a choice, and reports the line marks instead of a node
1474
+ count. Cleared rather than left, so a pane that compared a document and then a stylesheet
1475
+ does not keep the document's count over the stylesheet's text. */
1476
+ if (!renderable) {
1477
+ setDiffChangeMap(surface.paneId, null);
1478
+ }
1479
+ disposeSourceEditor(surface);
1480
+ void drawStage(surface, editorStageView("code"), []).then((stage) => {
1481
+ if (!stage) {
1482
+ return;
1483
+ }
1484
+ // After the stage, because the bar's host is one of the nodes it draws.
1485
+ renderDiffToolbar(surface.paneId);
1486
+ const editorEl = stageHost(surface, "diff-code-editor");
1487
+ if (editorEl) {
1488
+ void mountDiffEditor(surface, editorEl, gitDiffState);
1489
+ }
1490
+ });
1491
+ return;
1492
+ }
1493
+ disposeDiffEditor(surface);
1494
+
1154
1495
  const panelWidth = 800;
1155
1496
 
1156
- const { tpl: origTpl, panel: origPanel } = canvasPanelTemplate(
1157
- "git-diff-original",
1158
- "Original",
1159
- false,
1160
- panelWidth,
1161
- );
1162
- const { tpl: currTpl, panel: currPanel } = canvasPanelTemplate(
1163
- "git-diff-current",
1164
- "Current",
1165
- false,
1166
- panelWidth,
1167
- );
1497
+ const origEntry = canvasPanelEntry("git-diff-original", "Original", false, panelWidth);
1498
+ const currEntry = canvasPanelEntry("git-diff-current", "Current", false, panelWidth);
1168
1499
 
1169
- litRender(
1170
- html`
1171
- <div
1172
- class="panzoom-wrap"
1173
- style="transform-origin:0 0"
1174
- ${ref((el) => {
1175
- if (el) {
1176
- surface.panzoomWrap = el as HTMLDivElement;
1177
- }
1178
- })}
1179
- >
1180
- ${origTpl} ${currTpl}
1181
- </div>
1182
- `,
1183
- canvasWrap,
1500
+ /* The Compare bar leads the frame: it is absolutely positioned OVER the stage and a SIBLING of
1501
+ the pan/zoom surface, never a child. A child would be panned and scaled with the artboards,
1502
+ and a flow sibling would move the origin the pan transform and the centering observer both
1503
+ compute against. */
1504
+ const diffStage = drawStage(
1505
+ surface,
1506
+ {
1507
+ columnHeader: "hidden",
1508
+ frame: "boards",
1509
+ framePart: "panzoom",
1510
+ frameVars: "",
1511
+ handles: "hidden",
1512
+ hug: false,
1513
+ innerPart: "boards",
1514
+ lead: "toolbar",
1515
+ panels: [origEntry.item, currEntry.item],
1516
+ },
1517
+ [origEntry, currEntry],
1184
1518
  );
1185
1519
 
1186
- canvasPanels.push(origPanel as unknown as CanvasPanel, currPanel as unknown as CanvasPanel);
1520
+ canvasPanels.push(origEntry.panel, currEntry.panel);
1187
1521
 
1188
1522
  /** @param {string} content */
1189
1523
  const parseContent = (content: string): Promise<JxMutableNode> => {
@@ -1208,31 +1542,56 @@ function renderCanvasImpl(surface: CanvasSurface) {
1208
1542
  // `passGen` at the design-mode artboard loop.
1209
1543
  const diffGen = surface.renderGeneration;
1210
1544
  void Promise.all([
1545
+ diffStage,
1211
1546
  parseContent(gitDiffState.originalContent || ""),
1212
1547
  parseContent(gitDiffState.currentContent || ""),
1213
- ]).then(([originalDoc, currentDoc]) => {
1548
+ ]).then(([stage, originalDoc, currentDoc]) => {
1549
+ /* THE COMPARISON, computed once for both artboards and split by side.
1550
+ Structural rather than textual: the artboards render documents, so "what changed" has to be
1551
+ answered in document paths that `data-jx-path` can resolve. A failure here must not cost
1552
+ the author the comparison itself — an unparseable side already renders as a "Failed to
1553
+ parse" stub, and a stub aligns to zero changes rather than throwing — so the marks degrade
1554
+ to none and the two artboards draw exactly as they did before any of this existed. */
1555
+ let changeMap: ChangeMap | null = null;
1556
+ try {
1557
+ changeMap = buildChangeMap(originalDoc, currentDoc);
1558
+ } catch (error) {
1559
+ // Highlighting is an affordance over a comparison that is still on screen and still
1560
+ // Readable, so this degrades rather than surfacing: no toast, no empty stage.
1561
+ console.warn("buildChangeMap:", error);
1562
+ }
1563
+ if (!stage) {
1564
+ return;
1565
+ }
1566
+ setDiffChangeMap(surface.paneId, changeMap);
1567
+ renderDiffToolbar(surface.paneId);
1214
1568
  renderCanvasIntoPanel(
1215
1569
  surface,
1216
- origPanel as unknown as CanvasPanel,
1570
+ origEntry.panel,
1217
1571
  featureToggles,
1218
1572
  originalDoc,
1219
- gitDiffState,
1573
+ sideMarks(changeMap?.original, "modified-before"),
1220
1574
  diffGen,
1221
1575
  );
1222
1576
  renderCanvasIntoPanel(
1223
1577
  surface,
1224
- currPanel as unknown as CanvasPanel,
1578
+ currEntry.panel,
1225
1579
  featureToggles,
1226
1580
  currentDoc,
1227
- gitDiffState,
1581
+ sideMarks(changeMap?.current, "modified-after"),
1228
1582
  diffGen,
1229
1583
  );
1230
1584
  });
1231
1585
 
1232
- applyTransform(surface);
1233
- if (modeChanged) {
1234
- observeCenterUntilStable(surface);
1235
- }
1586
+ void diffStage.then((stage) => {
1587
+ if (!stage) {
1588
+ return;
1589
+ }
1590
+ applyTransform(surface);
1591
+ if (modeChanged) {
1592
+ observeCenterUntilStable(surface);
1593
+ }
1594
+ });
1236
1595
  return;
1237
1596
  }
1238
1597
 
@@ -1242,10 +1601,14 @@ function renderCanvasImpl(surface: CanvasSurface) {
1242
1601
  if (modeChanged) {
1243
1602
  canvasWrap.style.padding = "0";
1244
1603
  canvasWrap.style.overflow = "hidden";
1604
+ /* A dragged width is an INSPECTION, and it dies with the mode: entering Edit always starts at
1605
+ the breakpoint the switcher names. What survives is the breakpoint the drag landed on,
1606
+ which `activeMedia` already persists — see `canvas/edit-width.ts`. */
1607
+ clearEditWidth(surface.paneId);
1245
1608
  }
1246
1609
 
1247
1610
  /*
1248
- * THE COLUMN IS AS WIDE AS THE CHOSEN BREAKPOINT.
1611
+ * THE COLUMN IS AS WIDE AS THE CHOSEN BREAKPOINT — OR AS WIDE AS IT HAS BEEN DRAGGED.
1249
1612
  *
1250
1613
  * It was always `baseWidth`, so the Context bar's size switcher did nothing here: it wrote
1251
1614
  * `session.ui.activeMedia`, Design used that to highlight one of the artboards it already draws
@@ -1255,37 +1618,76 @@ function renderCanvasImpl(surface: CanvasSurface) {
1255
1618
  * The width is the artboard width Design would give that breakpoint, so the same page at `md`
1256
1619
  * is the same page in both modes; the iframe is that wide, so the document's own media queries
1257
1620
  * evaluate against it and the content reflows for real rather than being scaled.
1621
+ *
1622
+ * The switcher's answer is now the FALLBACK rather than the whole story: a handle on either
1623
+ * side of the column drags it to any width in between, and the breakpoint follows the width
1624
+ * (`edit-width.ts`). With nothing dragged this is byte-for-byte the expression it replaced.
1258
1625
  */
1259
- const { baseWidth, sizeBreakpoints: editBreakpoints } = parseMediaEntries(
1260
- getEffectiveMedia(S.document.$media),
1261
- );
1262
- const editMedia = activeMediaOfPane(surface.paneId);
1263
- const columnWidth = editBreakpoints.find((bp) => bp.name === editMedia)?.width ?? baseWidth;
1264
- const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
1626
+ const columnWidth = resolveEditColumnWidth(surface, tab);
1627
+ /*
1628
+ * THE TEMPLATE MUST NOT BIND THIS WIDTH, and that is not a style preference.
1629
+ *
1630
+ * The drag writes `column.style.maxWidth` imperatively, once per pointermove, because a render
1631
+ * per move would rebuild the iframe. lit dirty-checks its own committed value, so a later pass
1632
+ * that computes the SAME width it last committed skips the write — and the imperative value
1633
+ * stays. That is not hypothetical: drag a page down to 330px, open a different document in the
1634
+ * pane, and the new one is 330px wide too, because both passes computed the base width and lit
1635
+ * wrote neither. One writer, applied after the render, is the fix (§9.4 of the UI guidelines);
1636
+ * `applyEditZoom` below already has exactly this shape for exactly this reason.
1637
+ */
1638
+ const entry = canvasPanelEntry(null, null, true);
1265
1639
  // A component-definition doc (root tag is a custom element) is a fragment, not a page: it should
1266
1640
  // Hug its content rather than have the column fill+stretch to the viewport (dead scroll space).
1267
1641
  const rootTag = (S.document as { tagName?: unknown }).tagName;
1268
1642
  const isComponentDoc = typeof rootTag === "string" && rootTag.includes("-");
1269
- const columnClass = isComponentDoc ? "content-edit-column is-component" : "content-edit-column";
1270
- const editTpl = html`
1271
- <div
1272
- class="content-edit-canvas"
1273
- ${ref((el: Element | undefined) => {
1274
- panel.scrollContainer = (el as HTMLElement) || null;
1275
- })}
1276
- >
1277
- <div class=${columnClass} style="max-width:${columnWidth}px">
1278
- ${wantsDocHeader ? docHeaderSlot("in-column", surface.paneId) : nothing}${panelTpl}
1279
- </div>
1280
- </div>
1281
- `;
1282
- litRender(editTpl, canvasWrap);
1283
- canvasPanels.push(panel as unknown as CanvasPanel);
1284
- renderCanvasIntoPanel(surface, panel as unknown as CanvasPanel, S.ui.featureToggles);
1285
- // The column must exist in the DOM before the zoom's live width measurement — so the zoom is
1286
- // Applied after the render rather than baked into the template (the panel mounts fluid and is
1287
- // Immediately re-fitted; the iframe hasn't painted yet, so nothing visibly jumps).
1288
- applyEditZoom(surface);
1643
+ const { featureToggles: editToggles } = S.ui;
1644
+ void drawStage(
1645
+ surface,
1646
+ {
1647
+ /* The card is the column's FIRST child by contract (`tests/canvas-render.test.ts`, "Edit
1648
+ puts it INSIDE the document column"). The two handles are not in the column at all: they
1649
+ are `jx-split`s standing beside it in the canvas's row, because the canvas is the track
1650
+ a splitter has to measure and the column is the thing being resized. */
1651
+ columnHeader: wantsDocHeader ? "shown" : "hidden",
1652
+ frame: "boards",
1653
+ framePart: "edit-canvas",
1654
+ frameVars: "",
1655
+ handles: "shown",
1656
+ hug: isComponentDoc,
1657
+ innerPart: "edit-column",
1658
+ lead: "none",
1659
+ panels: [entry.item],
1660
+ },
1661
+ [entry],
1662
+ ).then((stage) => {
1663
+ if (!stage) {
1664
+ return;
1665
+ }
1666
+ const editColumn = stageHost(surface, "edit-column");
1667
+ entry.panel.scrollContainer = stageHost(surface, "edit-canvas") as HTMLElement;
1668
+ // The one writer of the column's width — see the note above.
1669
+ if (editColumn) {
1670
+ editColumn.style.maxWidth = `${Math.round(columnWidth)}px`;
1671
+ mountEditWidthHandle(
1672
+ surface,
1673
+ stageHost(surface, "edit-handle:start") ?? undefined,
1674
+ editColumn,
1675
+ -1,
1676
+ );
1677
+ mountEditWidthHandle(
1678
+ surface,
1679
+ stageHost(surface, "edit-handle:end") ?? undefined,
1680
+ editColumn,
1681
+ 1,
1682
+ );
1683
+ }
1684
+ renderCanvasIntoPanel(surface, entry.panel, editToggles);
1685
+ // The column must exist in the DOM before the zoom's live width measurement — so the zoom is
1686
+ // Applied after the render rather than baked into the stage (the panel mounts fluid and is
1687
+ // Immediately re-fitted; the iframe hasn't painted yet, so nothing visibly jumps).
1688
+ applyEditZoom(surface);
1689
+ });
1690
+ canvasPanels.push(entry.panel);
1289
1691
  return;
1290
1692
  }
1291
1693
 
@@ -1300,7 +1702,7 @@ function renderCanvasImpl(surface: CanvasSurface) {
1300
1702
  `#canvas-wrap` is a row by default and each surface states its own axis. */
1301
1703
  canvasWrap.style.flexDirection = wantsDocHeader ? "column" : "";
1302
1704
  canvasWrap.style.alignItems = wantsDocHeader ? "stretch" : "";
1303
- const designHeaderTpl = wantsDocHeader ? docHeaderSlot("pinned", surface.paneId) : nothing;
1705
+ const designLead = wantsDocHeader ? "header" : "none";
1304
1706
 
1305
1707
  const {
1306
1708
  sizeBreakpoints,
@@ -1316,39 +1718,41 @@ function renderCanvasImpl(surface: CanvasSurface) {
1316
1718
  const effectiveMedia = getEffectiveMedia(S.document.$media);
1317
1719
  const hasBaseWidth = effectiveMedia && effectiveMedia["--"];
1318
1720
  const label = hasBaseWidth ? `${mediaDisplayName("--")} (${baseWidth}px)` : null;
1319
- const { tpl: panelTpl, panel } = canvasPanelTemplate(
1721
+ const entry = canvasPanelEntry(
1320
1722
  hasBaseWidth ? "base" : null,
1321
1723
  label,
1322
1724
  !hasBaseWidth,
1323
1725
  hasBaseWidth ? baseWidth : undefined,
1324
1726
  );
1325
- litRender(
1326
- html`
1327
- ${designHeaderTpl}
1328
- <div
1329
- class="panzoom-wrap"
1330
- style="transform-origin:0 0"
1331
- ${ref((el) => {
1332
- if (el) {
1333
- surface.panzoomWrap = el as HTMLDivElement;
1334
- }
1335
- })}
1336
- >
1337
- ${panelTpl}
1338
- </div>
1339
- `,
1340
- canvasWrap,
1341
- );
1342
- canvasPanels.push(panel as unknown as CanvasPanel);
1343
- renderCanvasIntoPanel(surface, panel as unknown as CanvasPanel, featureToggles);
1344
- applyTransform(surface);
1345
- if (modeChanged) {
1346
- // Fit BEFORE centering: the fit picks the zoom, centerCanvas then places the artboard at that
1347
- // Zoom (and top-aligns it, which beats fit's vertical centering for a page taller than the
1348
- // Viewport).
1349
- fitOnCanvasEntry(surface);
1350
- observeCenterUntilStable(surface);
1351
- }
1727
+ void drawStage(
1728
+ surface,
1729
+ {
1730
+ columnHeader: "hidden",
1731
+ frame: "boards",
1732
+ framePart: "panzoom",
1733
+ frameVars: "",
1734
+ handles: "hidden",
1735
+ hug: false,
1736
+ innerPart: "boards",
1737
+ lead: designLead,
1738
+ panels: [entry.item],
1739
+ },
1740
+ [entry],
1741
+ ).then((stage) => {
1742
+ if (!stage) {
1743
+ return;
1744
+ }
1745
+ renderCanvasIntoPanel(surface, entry.panel, featureToggles);
1746
+ applyTransform(surface);
1747
+ if (modeChanged) {
1748
+ // Fit BEFORE centering: the fit picks the zoom, centerCanvas then places the artboard at
1749
+ // That zoom (and top-aligns it, which beats fit's vertical centering for a page taller
1750
+ // Than the viewport).
1751
+ fitOnCanvasEntry(surface);
1752
+ observeCenterUntilStable(surface);
1753
+ }
1754
+ });
1755
+ canvasPanels.push(entry.panel);
1352
1756
  return;
1353
1757
  }
1354
1758
 
@@ -1382,28 +1786,24 @@ function renderCanvasImpl(surface: CanvasSurface) {
1382
1786
  lensMedia === null ? allPanelDefs : allPanelDefs.filter((d) => d.name === lensMedia);
1383
1787
  const panelDefs = chosen.length > 0 ? chosen : allPanelDefs;
1384
1788
 
1385
- const panelEntries = panelDefs.map((def) => {
1386
- const label = `${def.displayName} (${def.width}px)`;
1387
- const { tpl, panel } = canvasPanelTemplate(def.name, label, false, def.width);
1388
- return { panel, tpl };
1389
- });
1789
+ const panelEntries = panelDefs.map((def) =>
1790
+ canvasPanelEntry(def.name, `${def.displayName} (${def.width}px)`, false, def.width),
1791
+ );
1390
1792
 
1391
- litRender(
1392
- html`
1393
- ${designHeaderTpl}
1394
- <div
1395
- class="panzoom-wrap"
1396
- style="transform-origin:0 0"
1397
- ${ref((el) => {
1398
- if (el) {
1399
- surface.panzoomWrap = el as HTMLDivElement;
1400
- }
1401
- })}
1402
- >
1403
- ${panelEntries.map((e) => e.tpl)}
1404
- </div>
1405
- `,
1406
- canvasWrap,
1793
+ const stageDrawn = drawStage(
1794
+ surface,
1795
+ {
1796
+ columnHeader: "hidden",
1797
+ frame: "boards",
1798
+ framePart: "panzoom",
1799
+ frameVars: "",
1800
+ handles: "hidden",
1801
+ hug: false,
1802
+ innerPart: "boards",
1803
+ lead: designLead,
1804
+ panels: panelEntries.map((e) => e.item),
1805
+ },
1806
+ panelEntries,
1407
1807
  );
1408
1808
 
1409
1809
  /* Every artboard of this pass mounts under the generation the pass opened with, captured HERE
@@ -1413,29 +1813,44 @@ function renderCanvasImpl(surface: CanvasSurface) {
1413
1813
  render under the NEW pass's number. And the host resolves the document once per generation
1414
1814
  (`preparePassRender`), which only fans out if the pass has one. */
1415
1815
  const passGen = surface.renderGeneration;
1416
- for (let i = 0; i < panelEntries.length; i++) {
1417
- const { panel } = panelEntries[i]!;
1418
- const p = panel as CanvasPanel;
1419
- canvasPanels.push(p);
1420
- if (i === 0) {
1421
- renderCanvasIntoPanel(surface, p, featureToggles);
1422
- } else {
1423
- // Yield between artboards so the first one paints before the rest mount. They no longer pay
1424
- // For the document — the pass already resolved it — but each is still a live iframe render.
1425
- setTimeout(() => renderCanvasIntoPanel(surface, p, featureToggles, null, null, passGen), 0);
1426
- }
1816
+ for (const { panel } of panelEntries) {
1817
+ canvasPanels.push(panel);
1427
1818
  }
1819
+ void stageDrawn.then((stage) => {
1820
+ if (!stage) {
1821
+ return;
1822
+ }
1823
+ for (let i = 0; i < panelEntries.length; i++) {
1824
+ const { panel } = panelEntries[i]!;
1825
+ if (i === 0) {
1826
+ /* `passGen` EXPLICITLY, including for the first board. It used to be left to the default —
1827
+ "whatever generation the surface is on" — which was the pass's own only because this
1828
+ board mounted synchronously inside the pass. It does not any more: the stage settles a
1829
+ microtask later, by which time a second pass may already have opened, and reading the
1830
+ live number there would stamp this pass's artboard with the NEXT pass's — a duplicate
1831
+ render the frame cannot recognise as superseded. */
1832
+ renderCanvasIntoPanel(surface, panel, featureToggles, null, null, passGen);
1833
+ } else {
1834
+ // Yield between artboards so the first one paints before the rest mount. They no longer pay
1835
+ // For the document — the pass already resolved it — but each is still a live iframe render.
1836
+ setTimeout(
1837
+ () => renderCanvasIntoPanel(surface, panel, featureToggles, null, null, passGen),
1838
+ 0,
1839
+ );
1840
+ }
1841
+ }
1428
1842
 
1429
- // Highlight active panel header — this pass's stage, not the focused pane's.
1430
- updateActivePanelHeaders(surface);
1843
+ // Highlight active panel header — this pass's stage, not the focused pane's.
1844
+ updateActivePanelHeaders(surface);
1431
1845
 
1432
- // Apply current zoom + pan transform
1433
- applyTransform(surface);
1434
- if (modeChanged) {
1435
- // See the single-panel path: fit picks the zoom, centerCanvas then places the artboards.
1436
- fitOnCanvasEntry(surface);
1437
- observeCenterUntilStable(surface);
1438
- }
1846
+ // Apply current zoom + pan transform
1847
+ applyTransform(surface);
1848
+ if (modeChanged) {
1849
+ // See the single-panel path: fit picks the zoom, centerCanvas then places the artboards.
1850
+ fitOnCanvasEntry(surface);
1851
+ observeCenterUntilStable(surface);
1852
+ }
1853
+ });
1439
1854
  }
1440
1855
 
1441
1856
  /**
@@ -1451,8 +1866,10 @@ function renderCanvasImpl(surface: CanvasSurface) {
1451
1866
  * render needs no structural-preview fallback); unused.
1452
1867
  * @param {JxMutableNode | null} [docOverride] - Optional document to render (for diff mode). Uses
1453
1868
  * active tab doc if not provided.
1454
- * @param {GitDiffState | null} [_gitDiffState] - Accepted for call-site symmetry; the iframe path
1455
- * does not apply parent-side diff highlighting.
1869
+ * @param {WireDiffMarks | null} [diffMarks] - This artboard's change marks, in ITS OWN document's
1870
+ * coordinates. The slot used to hold a `GitDiffState` "accepted for call-site symmetry" and
1871
+ * unread, which is what "the iframe path does not apply diff highlighting" meant; it does now,
1872
+ * and the marks are per-SIDE, so the whole comparison was never the right thing to pass.
1456
1873
  * @param {number | null} [passGen] - The generation of the render pass this panel belongs to.
1457
1874
  * Passed explicitly by a DEFERRED mount, whose callback would otherwise read whatever generation
1458
1875
  * is current when the timer fires — a later pass's number, stamped on an earlier pass's
@@ -1463,7 +1880,7 @@ function renderCanvasIntoPanel(
1463
1880
  panel: CanvasPanel,
1464
1881
  _featureToggles: Record<string, boolean>,
1465
1882
  docOverride: JxMutableNode | null = null,
1466
- _gitDiffState: GitDiffState | null = null,
1883
+ diffMarks: WireDiffMarks | null = null,
1467
1884
  passGen: number | null = null,
1468
1885
  ) {
1469
1886
  const gen = passGen ?? surface.renderGeneration;
@@ -1490,9 +1907,17 @@ function renderCanvasIntoPanel(
1490
1907
  panel._width,
1491
1908
  docOverride ? null : (tab?.id ?? null),
1492
1909
  // The VIEW tab, which an override does not null out: a git-diff frame routes no mutations
1493
- // But is still a view of this pane's document, and its docBase, layout toggle and mode are
1494
- // That tab's. Explicit rather than left to the default, because this line already knows.
1910
+ // But is still a view of this pane's document, and its docBase and layout toggle are that
1911
+ // Tab's. Explicit rather than left to the default, because this line already knows.
1495
1912
  tab,
1913
+ /* …but NOT its mode, which is the one thing the view tab cannot answer for a lens. A lens
1914
+ draws the source pane's document in a mode of its own that is never written onto the tab,
1915
+ so resolving from `tab` gave a Diff lens's artboards the SOURCE pane's mode — an editable
1916
+ `design` frame for a comparison, with repeater paths collapsed on one entry point to the
1917
+ diff and not the other. `canvasModeOfPane` degrades to exactly the old answer for every
1918
+ pane that is not a lens, so this is unconditional rather than a git-diff special case. */
1919
+ canvasModeOfPane(surface.paneId),
1920
+ diffMarks,
1496
1921
  ),
1497
1922
  )
1498
1923
  .then(() => {
@@ -1525,7 +1950,14 @@ function renderCanvasIntoPanel(
1525
1950
  * Same idiom as `setIframePatchEscalation`. Without it a pane removed from the grid would drop its
1526
1951
  * record while its Monaco, its collab lock and its centering observer stayed alive.
1527
1952
  */
1953
+ /* The diff toolbar's Visual/Code switch rebuilds the stage, and it cannot import this module to
1954
+ do it — this one imports IT. Same injection as the teardown below. */
1955
+ setDiffRepaint(renderCanvas);
1956
+
1528
1957
  setSurfaceTeardown((surface) => {
1958
+ disposeDiffEditor(surface);
1959
+ clearDiffView(surface.paneId);
1960
+ setDiffToolbarHost(surface.paneId, null);
1529
1961
  detachGridPanel(surface.paneId);
1530
1962
  detachLibraryPane(surface.paneId);
1531
1963
  detachEntryPane(surface.paneId);
@@ -1537,7 +1969,7 @@ setSurfaceTeardown((surface) => {
1537
1969
  surface.centerObserver?.disconnect();
1538
1970
  surface.centerObserver = null;
1539
1971
  surface.panzoomWrap = null;
1540
- _docHeaderRefs.delete(surface.paneId);
1972
+ detachCanvasStage(surface);
1541
1973
  });
1542
1974
 
1543
1975
  export function renderOverlays() {
@@ -1637,6 +2069,13 @@ export function selectionCommands(): AnyCommand[] {
1637
2069
  "Select the element at a document path so the Inspector, the Outline and the canvas " +
1638
2070
  "overlay all address it. Pass null to clear the selection.",
1639
2071
  name: "select_node",
2072
+ /* The one pointing verb: how the assistant shows the person which element it means. The
2073
+ report reads the context's own `selection.kind` — the tag the status bar prints — so the
2074
+ model learns what it pointed at without a second read. */
2075
+ report: ({ after }) =>
2076
+ after.selection.count === 0
2077
+ ? "Cleared the selection."
2078
+ : `Selected ${after.selection.kind || "the element"} at [${(after.selection.paths.at(-1) ?? []).join(", ")}]; the Inspector and the Outline now address it.`,
1640
2079
  },
1641
2080
  run: (_commandCtx, args) => {
1642
2081
  const path = nullablePathArg("selection.set", args, "path");
@@ -1669,12 +2108,12 @@ export function selectionCommands(): AnyCommand[] {
1669
2108
  group: "2_navigate",
1670
2109
  requires: "an open document",
1671
2110
  when: (ctx) => ctx.document.open,
1672
- aiTool: {
1673
- description:
1674
- "Select several elements at once so one decision — a style paste, a delete, a duplicate " +
1675
- "— applies to all of them as a single undoable step. Pass [] to select nothing.",
1676
- name: "select_nodes",
1677
- },
2111
+ /*
2112
+ * No `aiTool`, by §12.4's fourth deletion rule (redundant with a tool the model has): the
2113
+ * assistant's bridge runs THIS record by id as the selector step of every selection-level
2114
+ * tool — `delete_node(paths)` is `selection.setPaths` then `selection.delete` — so a second
2115
+ * selection tool beside `select_node` would be prompt cost with no second job.
2116
+ */
1678
2117
  /**
1679
2118
  * The idempotent SET for the whole selection, beside `selection.set`'s single path.
1680
2119
  *
@@ -1730,12 +2169,8 @@ export function selectionCommands(): AnyCommand[] {
1730
2169
  — `postApplyFormat` would no-op against a frame with no session and the palette row would
1731
2170
  read as available. The `format.*` family is gated the same way, for the same reason. */
1732
2171
  when: (ctx) => ctx.caret.inCanvas,
1733
- aiTool: {
1734
- description:
1735
- "Insert a live data placeholder at the text caret, binding that run of text to a state " +
1736
- "entry or, inside a repeater template, to the current item.",
1737
- name: "insert_data_token",
1738
- },
2172
+ /* No `aiTool`: `when` is the CARET's, and the caret is the person's — never true for the
2173
+ agent, so a projection would be a tool advertised in no state the model can reach. */
1739
2174
  run: (_commandCtx, args) => {
1740
2175
  const token = stringArg("insert.data", args, "token");
1741
2176
  refuseUnresolvableToken(token);