@jxsuite/studio 2.0.0 → 2.1.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 (419) hide show
  1. package/canvas.html +21 -0
  2. package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
  3. package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
  4. package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
  5. package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
  6. package/dist/chunks/{components-0hzentwq.js → components-2bpqp4mq.js} +5 -7
  7. package/dist/chunks/{components-0hzentwq.js.map → components-2bpqp4mq.js.map} +1 -1
  8. package/dist/chunks/content-source-34gnkbnn.js +38 -0
  9. package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
  10. package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
  11. package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
  12. package/dist/chunks/extension-sections-6mqydz35.js +301 -0
  13. package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
  14. package/dist/chunks/files-8kn321h1.js +70 -0
  15. package/dist/chunks/files-8kn321h1.js.map +9 -0
  16. package/dist/chunks/{format-host-2c188n41.js → format-host-mkwsmt8d.js} +3 -3
  17. package/dist/chunks/git-panel-tjbr191d.js +51 -0
  18. package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
  19. package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
  20. package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
  21. package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
  22. package/dist/chunks/layers-panel-87pb25km.js +58 -0
  23. package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
  24. package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
  25. package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
  26. package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
  27. package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
  28. package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
  29. package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
  30. package/dist/chunks/right-panel-zpdxd98t.js +37 -0
  31. package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
  32. package/dist/chunks/section-registry-zmckaxf4.js +30 -0
  33. package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
  34. package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
  35. package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
  36. package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
  37. package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
  38. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
  39. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
  40. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
  41. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
  42. package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
  43. package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
  44. package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
  45. package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
  46. package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
  47. package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
  48. package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
  49. package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
  50. package/dist/chunks/studio-41exgzhq.js +6690 -0
  51. package/dist/chunks/studio-41exgzhq.js.map +38 -0
  52. package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
  53. package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
  54. package/dist/chunks/studio-50k77cca.js +1787 -0
  55. package/dist/chunks/studio-50k77cca.js.map +18 -0
  56. package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
  57. package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
  58. package/dist/chunks/studio-6f0am8f2.js +53 -0
  59. package/dist/chunks/studio-6f0am8f2.js.map +10 -0
  60. package/dist/chunks/studio-6nmjc5qk.js +7002 -0
  61. package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
  62. package/dist/chunks/studio-7afd21nw.js +67 -0
  63. package/dist/chunks/studio-7afd21nw.js.map +10 -0
  64. package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
  65. package/dist/chunks/studio-7bmdvzne.js.map +10 -0
  66. package/dist/chunks/studio-8jpzqk5s.js +128 -0
  67. package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
  68. package/dist/chunks/studio-94m77tgk.js +1482 -0
  69. package/dist/chunks/studio-94m77tgk.js.map +13 -0
  70. package/dist/chunks/studio-9je6w5sq.js +1000 -0
  71. package/dist/chunks/studio-9je6w5sq.js.map +16 -0
  72. package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
  73. package/dist/chunks/{studio-97qcgvdn.js → studio-adn0z1k3.js} +14 -8
  74. package/dist/chunks/{studio-97qcgvdn.js.map → studio-adn0z1k3.js.map} +3 -3
  75. package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
  76. package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
  77. package/dist/chunks/studio-bw5dz6km.js +608 -0
  78. package/dist/chunks/studio-bw5dz6km.js.map +21 -0
  79. package/dist/chunks/studio-c6s5getc.js +4232 -0
  80. package/dist/chunks/studio-c6s5getc.js.map +39 -0
  81. package/dist/chunks/studio-cjm7fdjf.js +451 -0
  82. package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
  83. package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
  84. package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
  85. package/dist/chunks/studio-dg7zyt5x.js +28 -0
  86. package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
  87. package/dist/chunks/studio-dzcyvvvz.js +166 -0
  88. package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
  89. package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
  90. package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
  91. package/dist/chunks/studio-f12h6177.js +411 -0
  92. package/dist/chunks/studio-f12h6177.js.map +13 -0
  93. package/dist/chunks/studio-hbp1s8m9.js +1817 -0
  94. package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
  95. package/dist/chunks/studio-k2sxjph5.js +15295 -0
  96. package/dist/chunks/studio-k2sxjph5.js.map +235 -0
  97. package/dist/chunks/studio-myje51t3.js +385 -0
  98. package/dist/chunks/studio-myje51t3.js.map +12 -0
  99. package/dist/chunks/studio-na6rwzhw.js +353 -0
  100. package/dist/chunks/studio-na6rwzhw.js.map +11 -0
  101. package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
  102. package/dist/chunks/studio-p471gg5n.js.map +14 -0
  103. package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
  104. package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
  105. package/dist/chunks/{studio-hhj92gcr.js → studio-s519zc99.js} +3 -3
  106. package/dist/chunks/studio-sas7mt3k.js +75 -0
  107. package/dist/chunks/studio-sas7mt3k.js.map +10 -0
  108. package/dist/chunks/studio-t5wqwzdh.js +74 -0
  109. package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
  110. package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
  111. package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
  112. package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
  113. package/dist/chunks/studio-tr20m8g8.js.map +9 -0
  114. package/dist/chunks/{studio-ygzav7z5.js → studio-v96qfnww.js} +5514 -4291
  115. package/dist/chunks/studio-v96qfnww.js.map +54 -0
  116. package/dist/chunks/studio-vcetc91k.js +659 -0
  117. package/dist/chunks/studio-vcetc91k.js.map +10 -0
  118. package/dist/chunks/{studio-n9zmpxjg.js → studio-vmymjefp.js} +2972 -4937
  119. package/dist/chunks/studio-vmymjefp.js.map +98 -0
  120. package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
  121. package/dist/chunks/studio-x9hmy5kr.js +390 -0
  122. package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
  123. package/dist/chunks/studio-xa0ce0n7.js +335 -0
  124. package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
  125. package/dist/chunks/studio-xm3dbxvn.js +378 -0
  126. package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
  127. package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
  128. package/dist/chunks/studio-zv37yrfa.js +1054 -0
  129. package/dist/chunks/studio-zv37yrfa.js.map +13 -0
  130. package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
  131. package/dist/chunks/ws-client-654ts66j.js.map +11 -0
  132. package/dist/iframe-entry.js +668 -230
  133. package/dist/studio.js +23711 -35185
  134. package/index.html +35 -4685
  135. package/package.json +17 -15
  136. package/src/about/about-modal.ts +42 -11
  137. package/src/browse/library-commands.ts +240 -0
  138. package/src/browse/library-layouts.ts +369 -0
  139. package/src/browse/library-model.ts +396 -0
  140. package/src/browse/library-pane.ts +1049 -0
  141. package/src/browse/library-preview.ts +252 -0
  142. package/src/browse/library-source.ts +150 -0
  143. package/src/canvas/canvas-helpers.ts +33 -9
  144. package/src/canvas/canvas-live-render.ts +45 -29
  145. package/src/canvas/canvas-patcher.ts +151 -31
  146. package/src/canvas/canvas-perf.ts +46 -7
  147. package/src/canvas/canvas-render.ts +1092 -220
  148. package/src/canvas/canvas-surface.ts +240 -0
  149. package/src/canvas/canvas-utils.ts +1166 -124
  150. package/src/canvas/iframe-drop.ts +2 -1
  151. package/src/canvas/iframe-entry.ts +241 -4
  152. package/src/canvas/iframe-host.ts +1226 -143
  153. package/src/canvas/iframe-inline-edit.ts +31 -0
  154. package/src/canvas/iframe-interaction.ts +76 -16
  155. package/src/canvas/iframe-keys.ts +107 -45
  156. package/src/canvas/iframe-overlay.ts +38 -1
  157. package/src/canvas/iframe-protocol.ts +137 -2
  158. package/src/canvas/iframe-render.ts +70 -2
  159. package/src/canvas/iframe-slash.ts +6 -0
  160. package/src/canvas/path-mapping.ts +56 -6
  161. package/src/canvas/surface-registry.ts +292 -0
  162. package/src/collab/collab-commands.ts +244 -0
  163. package/src/collab/collab-session.ts +198 -32
  164. package/src/collab/collab-state.ts +28 -2
  165. package/src/collab/presence-chips.ts +100 -9
  166. package/src/commands/active-registry.ts +42 -0
  167. package/src/commands/app-commands.ts +176 -0
  168. package/src/commands/budget.ts +158 -0
  169. package/src/commands/command-args.ts +253 -0
  170. package/src/commands/context.ts +387 -0
  171. package/src/commands/defaults.ts +580 -0
  172. package/src/commands/keymap.ts +571 -0
  173. package/src/commands/levels.ts +345 -0
  174. package/src/commands/live-context.ts +193 -0
  175. package/src/commands/reference.ts +227 -0
  176. package/src/commands/registry.ts +293 -0
  177. package/src/component-props.ts +3 -2
  178. package/src/content/draft-state.ts +106 -0
  179. package/src/content/entry-commands.ts +282 -0
  180. package/src/content/entry-editor.ts +327 -0
  181. package/src/content/entry-fields.ts +116 -0
  182. package/src/content/entry-model.ts +175 -0
  183. package/src/editor/canvas-context-menu.ts +17 -11
  184. package/src/editor/canvas-slash-bridge.ts +11 -4
  185. package/src/editor/context-menu.ts +804 -204
  186. package/src/editor/convert-to-component.ts +31 -10
  187. package/src/editor/convert-to-repeater.ts +3 -2
  188. package/src/editor/inline-edit-apply.ts +5 -5
  189. package/src/editor/inline-edit.ts +86 -50
  190. package/src/editor/insert-zone-action.ts +1 -1
  191. package/src/editor/shortcuts.ts +1043 -329
  192. package/src/editor/slash-menu.ts +1 -0
  193. package/src/files/elements.ts +166 -0
  194. package/src/files/file-ops.ts +178 -31
  195. package/src/files/files.ts +898 -268
  196. package/src/files/fs-events.ts +21 -0
  197. package/src/files/media-meta.ts +203 -0
  198. package/src/files/media-paths.ts +141 -0
  199. package/src/files/media-upload.ts +7 -3
  200. package/src/files/media-usage.ts +189 -0
  201. package/src/github/github-auth.ts +235 -33
  202. package/src/github/github-publish.ts +80 -21
  203. package/src/grid/grid-controller.ts +152 -12
  204. package/src/grid/grid-layout.ts +225 -20
  205. package/src/grid/grid-open.ts +120 -0
  206. package/src/grid/grid-panel.ts +554 -39
  207. package/src/grid/grid-source.ts +27 -1
  208. package/src/grid/redirects-grid.ts +599 -0
  209. package/src/grid/redirects.ts +398 -0
  210. package/src/grid/sources/content-source.ts +108 -2
  211. package/src/i18n/i18n-commands.ts +322 -0
  212. package/src/new-project/add-repo-modal.ts +8 -10
  213. package/src/new-project/import-tab.ts +34 -9
  214. package/src/new-project/location-fields.ts +9 -1
  215. package/src/new-project/new-project-modal.ts +270 -262
  216. package/src/packages/jxsuite-update.ts +97 -48
  217. package/src/packages/pull-package-sync.ts +18 -9
  218. package/src/page-params.ts +2 -1
  219. package/src/panels/activity-bar.ts +152 -91
  220. package/src/panels/activity-panel.ts +481 -0
  221. package/src/panels/ai-chat/chat-markdown.ts +8 -3
  222. package/src/panels/ai-chat/chat-view.ts +245 -18
  223. package/src/panels/ai-chat/composer.ts +31 -5
  224. package/src/panels/ai-chat/sessions-view.ts +17 -10
  225. package/src/panels/ai-panel.ts +279 -120
  226. package/src/panels/block-action-bar.ts +1115 -224
  227. package/src/panels/bottom-dock.ts +337 -0
  228. package/src/panels/canvas-dnd-bridge.ts +37 -8
  229. package/src/panels/chat-panel.ts +29 -12
  230. package/src/panels/data-explorer.ts +254 -112
  231. package/src/panels/data-grid.ts +10 -4
  232. package/src/panels/dnd.ts +28 -51
  233. package/src/panels/editors.ts +434 -107
  234. package/src/panels/elements-panel.ts +66 -2
  235. package/src/panels/empty-state.ts +112 -0
  236. package/src/panels/events-panel.ts +596 -82
  237. package/src/panels/formula-workspace.ts +408 -218
  238. package/src/panels/frontmatter-fields.ts +51 -10
  239. package/src/panels/frontmatter-panel.ts +357 -79
  240. package/src/panels/git-panel.ts +375 -164
  241. package/src/panels/head-panel.ts +791 -85
  242. package/src/panels/i18n-panel.ts +592 -0
  243. package/src/panels/imports-panel.ts +95 -85
  244. package/src/panels/jump-bar.ts +584 -0
  245. package/src/panels/layers-panel.ts +1106 -328
  246. package/src/panels/left-panel.ts +109 -204
  247. package/src/panels/navigator-panels.ts +131 -0
  248. package/src/panels/overlays.ts +7 -2
  249. package/src/panels/pane-context.ts +1477 -0
  250. package/src/panels/pane-grid.ts +406 -0
  251. package/src/panels/panel-registry.ts +340 -0
  252. package/src/panels/panel-scheduler.ts +75 -0
  253. package/src/panels/problems-panel.ts +234 -0
  254. package/src/panels/properties-panel.ts +946 -1139
  255. package/src/panels/provenance.ts +258 -0
  256. package/src/panels/quick-search.ts +791 -122
  257. package/src/panels/right-panel.ts +228 -102
  258. package/src/panels/seo-modal.ts +395 -0
  259. package/src/panels/settings-pane.ts +203 -0
  260. package/src/panels/signals-panel.ts +354 -137
  261. package/src/panels/statement-editor.ts +41 -25
  262. package/src/panels/statusbar.ts +342 -106
  263. package/src/panels/style-panel.ts +974 -463
  264. package/src/panels/style-utils.ts +3 -3
  265. package/src/panels/stylebook-doc.ts +4 -2
  266. package/src/panels/stylebook-layers-panel.ts +3 -2
  267. package/src/panels/stylebook-panel.ts +51 -26
  268. package/src/panels/tab-strip.ts +1058 -78
  269. package/src/panels/target-line.ts +274 -0
  270. package/src/panels/toolbar.ts +631 -484
  271. package/src/panels/welcome-screen.ts +301 -198
  272. package/src/platform-errors.ts +26 -3
  273. package/src/platform.ts +96 -2
  274. package/src/platforms/cloud.ts +71 -8
  275. package/src/platforms/devserver.ts +92 -29
  276. package/src/publish/deploy-checklist.ts +383 -0
  277. package/src/publish/pages-service.ts +10 -1
  278. package/src/publish/publish-commands.ts +218 -0
  279. package/src/publish/publish-panel.ts +110 -16
  280. package/src/recent-projects.ts +3 -2
  281. package/src/services/a11y-report.ts +505 -0
  282. package/src/services/ai-models.ts +75 -2
  283. package/src/services/ai-project-tools.ts +29 -10
  284. package/src/services/ai-system-prompt.ts +53 -14
  285. package/src/services/ai-tools.ts +39 -3
  286. package/src/services/ai-writes.ts +154 -0
  287. package/src/services/announce.ts +97 -0
  288. package/src/services/automation.ts +424 -175
  289. package/src/services/cem-export.ts +9 -4
  290. package/src/services/clock.ts +61 -0
  291. package/src/services/document-assistant.ts +36 -8
  292. package/src/services/idle.ts +179 -0
  293. package/src/services/import-client.ts +26 -0
  294. package/src/services/monaco-buffer.ts +627 -0
  295. package/src/services/monaco-lazy.ts +36 -0
  296. package/src/services/notify.ts +271 -0
  297. package/src/services/profile.ts +155 -0
  298. package/src/services/references.ts +0 -0
  299. package/src/services/token-lint.ts +3 -2
  300. package/src/services/tool-executor.ts +53 -7
  301. package/src/services/trusted-types.ts +123 -0
  302. package/src/settings/contexts-section.ts +427 -0
  303. package/src/settings/contributed-section.ts +149 -11
  304. package/src/settings/css-vars-editor.ts +306 -349
  305. package/src/settings/defs-editor.ts +93 -34
  306. package/src/settings/dependencies-editor.ts +7 -7
  307. package/src/settings/extension-sections.ts +37 -5
  308. package/src/settings/general-settings.ts +204 -121
  309. package/src/settings/locales-section.ts +347 -0
  310. package/src/settings/preferences-accounts.ts +140 -0
  311. package/src/settings/preferences-dialog.ts +620 -0
  312. package/src/settings/preferences-keymap.ts +270 -0
  313. package/src/settings/project-sections.ts +260 -0
  314. package/src/settings/section-registry.ts +169 -0
  315. package/src/settings/settings-document.ts +385 -0
  316. package/src/shell.ts +1549 -0
  317. package/src/site-context.ts +45 -20
  318. package/src/state.ts +14 -275
  319. package/src/store.ts +88 -43
  320. package/src/studio.ts +790 -346
  321. package/src/style/live-preview.ts +36 -0
  322. package/src/style/project-styles.ts +349 -0
  323. package/src/style/token-ref.ts +123 -0
  324. package/src/tabs/patch-ops.ts +16 -4
  325. package/src/tabs/project-config.ts +416 -0
  326. package/src/tabs/selection.ts +323 -0
  327. package/src/tabs/tab.ts +182 -47
  328. package/src/tabs/transact.ts +209 -28
  329. package/src/types.ts +68 -13
  330. package/src/ui/ai-credentials-form.ts +39 -35
  331. package/src/ui/ai-managed-connect.ts +113 -0
  332. package/src/ui/dynamic-slot.ts +224 -81
  333. package/src/ui/expression-editor.ts +71 -63
  334. package/src/ui/field-input.ts +4 -6
  335. package/src/ui/field-row.ts +215 -38
  336. package/src/ui/form-controls.ts +194 -7
  337. package/src/ui/layers.ts +492 -69
  338. package/src/ui/media-picker.ts +115 -25
  339. package/src/ui/panel-resize.ts +109 -91
  340. package/src/ui/progress-modal.ts +131 -50
  341. package/src/ui/regions.ts +462 -0
  342. package/src/ui/schema-form.ts +229 -45
  343. package/src/ui/spectrum.ts +24 -8
  344. package/src/ui/timing.ts +49 -0
  345. package/src/ui/value-selector.ts +20 -0
  346. package/src/ui/value-source.ts +348 -0
  347. package/src/ui/virtual-window.ts +400 -0
  348. package/src/utils/edit-display.ts +3 -2
  349. package/src/utils/inherited-style.ts +67 -37
  350. package/src/view.ts +64 -78
  351. package/src/workspace/pane-derive.ts +1258 -0
  352. package/src/workspace/session.ts +317 -0
  353. package/src/workspace/workspace.ts +1345 -23
  354. package/styles/canvas.css +653 -0
  355. package/styles/forced-colors.css +106 -0
  356. package/styles/inspector.css +1454 -0
  357. package/styles/overlays.css +2704 -0
  358. package/styles/panels.css +1701 -0
  359. package/styles/shell.css +1423 -0
  360. package/styles/tokens.css +151 -0
  361. package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
  362. package/dist/chunks/extension-sections-qct0gt8f.js +0 -531
  363. package/dist/chunks/extension-sections-qct0gt8f.js.map +0 -12
  364. package/dist/chunks/files-02zn5y24.js +0 -50
  365. package/dist/chunks/files-02zn5y24.js.map +0 -9
  366. package/dist/chunks/git-panel-mbb0y0hz.js +0 -28
  367. package/dist/chunks/git-panel-mbb0y0hz.js.map +0 -9
  368. package/dist/chunks/layers-panel-9qekmqk1.js +0 -26
  369. package/dist/chunks/layers-panel-9qekmqk1.js.map +0 -9
  370. package/dist/chunks/settings-modal-gj0hrah4.js +0 -29
  371. package/dist/chunks/settings-modal-gj0hrah4.js.map +0 -9
  372. package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
  373. package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
  374. package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
  375. package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
  376. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
  377. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
  378. package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
  379. package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
  380. package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
  381. package/dist/chunks/studio-568nx4t5.js +0 -469
  382. package/dist/chunks/studio-568nx4t5.js.map +0 -14
  383. package/dist/chunks/studio-9smfzqhc.js +0 -19
  384. package/dist/chunks/studio-9smfzqhc.js.map +0 -10
  385. package/dist/chunks/studio-asvf0j9e.js +0 -429
  386. package/dist/chunks/studio-asvf0j9e.js.map +0 -13
  387. package/dist/chunks/studio-d77qm3pk.js +0 -3427
  388. package/dist/chunks/studio-d77qm3pk.js.map +0 -26
  389. package/dist/chunks/studio-j6gqyjf4.js +0 -1854
  390. package/dist/chunks/studio-j6gqyjf4.js.map +0 -18
  391. package/dist/chunks/studio-n9zmpxjg.js.map +0 -115
  392. package/dist/chunks/studio-qkvt37c0.js.map +0 -9
  393. package/dist/chunks/studio-rq0vxb19.js +0 -3744
  394. package/dist/chunks/studio-rq0vxb19.js.map +0 -22
  395. package/dist/chunks/studio-wd6ewdvd.js +0 -853
  396. package/dist/chunks/studio-wd6ewdvd.js.map +0 -12
  397. package/dist/chunks/studio-xq0t7n5t.js +0 -140
  398. package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
  399. package/dist/chunks/studio-xze4zrs1.js.map +0 -10
  400. package/dist/chunks/studio-ygzav7z5.js.map +0 -46
  401. package/dist/chunks/studio-zmy7241e.js +0 -399
  402. package/dist/chunks/studio-zmy7241e.js.map +0 -12
  403. package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
  404. package/src/browse/browse-modal.ts +0 -69
  405. package/src/browse/browse.ts +0 -904
  406. package/src/canvas/canvas-diff.ts +0 -216
  407. package/src/canvas/nested-site-style.ts +0 -58
  408. package/src/new-project/design-fields.ts +0 -264
  409. package/src/new-project/templates.ts +0 -61
  410. package/src/panels/tab-bar.ts +0 -548
  411. package/src/settings/settings-modal.ts +0 -203
  412. /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
  413. /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
  414. /package/dist/chunks/{format-host-2c188n41.js.map → format-host-mkwsmt8d.js.map} +0 -0
  415. /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
  416. /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
  417. /package/dist/chunks/{studio-hhj92gcr.js.map → studio-s519zc99.js.map} +0 -0
  418. /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
  419. /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
@@ -7,34 +7,81 @@
7
7
  import { html, render as litRender, nothing } from "lit-html";
8
8
  import { ref } from "lit-html/directives/ref.js";
9
9
  import type * as monaco from "monaco-editor";
10
- import { loadedMonaco, loadMonaco } from "../services/monaco-lazy";
10
+ import { loadedMonaco, loadMonaco, mountStillWanted } from "../services/monaco-lazy";
11
11
 
12
- import { canvasPanels, canvasWrap, updateCanvas } from "../store";
13
- import { activeTab } from "../workspace/workspace";
12
+ import { getNodeAtPath, updateCanvas } from "../store";
13
+ import { activeTab, tabIsLive, workspace } from "../workspace/workspace";
14
+ import {
15
+ activeMediaOfPane,
16
+ canvasModeOfPane,
17
+ derivationOfPane,
18
+ nextRenderGeneration,
19
+ setSurfaceTeardown,
20
+ surfaceForPane,
21
+ tabOfPane,
22
+ } from "./canvas-surface";
23
+ import type { CanvasSurface } from "./canvas-surface";
24
+ import { primarySelection, uniquePaths } from "../tabs/selection";
25
+ import {
26
+ argsSchema,
27
+ nullablePathArg,
28
+ pathListArg,
29
+ pathListProperty,
30
+ pathProperty,
31
+ stringArg,
32
+ stringProperty,
33
+ } from "../commands/command-args";
14
34
  import { collabSourceContext } from "../collab/collab-session";
15
35
  import { attachCursorStyles } from "../collab/monaco-cursors";
16
36
  import type { AwarenessLike } from "../collab/monaco-cursors";
17
- import { view } from "../view";
37
+ import { shell } from "../shell";
18
38
  import { parseSourceForPath, serializeDocument } from "../files/file-ops";
19
39
  import { detachGridPanel, gridPanelMounted, renderGridMode } from "../grid/grid-panel";
40
+ import { detachLibraryPane, libraryPaneMounted, renderLibraryMode } from "../browse/library-pane";
41
+ import { detachEntryPane, entryPaneMounted, renderEntryMode } from "../content/entry-editor";
42
+ import {
43
+ detachSettingsPane,
44
+ renderSettingsPane,
45
+ settingsPaneMounted,
46
+ } from "../panels/settings-pane";
20
47
  import { formatByName, formatForPath } from "../format/format-host";
48
+ import {
49
+ BUFFER_COMMIT,
50
+ bufferIsLive,
51
+ bufferMovedOn,
52
+ bufferWrites,
53
+ commitBufferWrites,
54
+ } from "../services/monaco-buffer";
21
55
  import { modelUriFor } from "../services/model-uri";
22
56
  import { renderWelcome } from "../panels/welcome-screen";
57
+ import { renderEmptyState } from "../panels/empty-state";
58
+ import {
59
+ attachDocumentHeaderHost,
60
+ documentHeaderHost,
61
+ hasDocumentHeader,
62
+ } from "../panels/frontmatter-panel";
23
63
  import { projectState } from "../state";
24
64
  import {
25
65
  applyEditZoom,
26
66
  applyTransform,
27
67
  canvasPanelTemplate,
68
+ fitOnCanvasEntry,
28
69
  observeCenterUntilStable,
29
70
  updateActivePanelHeaders,
30
71
  } from "./canvas-utils";
31
72
  import { parseMediaEntries } from "../utils/canvas-media";
32
73
  import { getEffectiveMedia, getEffectiveStyle } from "../site-context";
33
74
  import {
75
+ adoptCanvasPreviewMode,
34
76
  commitActiveEditSession,
77
+ getEditSnapshot,
35
78
  mountIframeCanvas,
79
+ postApplyFormat,
80
+ postOpenSlash,
36
81
  postStyleUpdateToStylebookHosts,
82
+ releaseCanvasHosts,
37
83
  } from "./iframe-host";
84
+ import { findEnclosingRepeater } from "../editor/repeater-scope";
38
85
  import {
39
86
  canvasPerf,
40
87
  SPAN_FULL_RENDER,
@@ -47,19 +94,21 @@ import { transposeStylebookStyle } from "../panels/stylebook-doc";
47
94
  import { dismissBlockActionBar, dismissLinkPopover } from "../panels/block-action-bar";
48
95
  import { dismissContextMenu } from "../editor/context-menu";
49
96
  import { dismissSlashMenu } from "../editor/slash-menu";
50
- import { renderFunctionEditor } from "../panels/editors";
51
- import { renderFormulaWorkspace } from "../panels/formula-workspace";
52
97
  import { mediaDisplayName } from "../panels/shared";
53
- import { statusMessage } from "../panels/statusbar";
98
+ import { tabLabel } from "../panels/tab-strip";
99
+ import { activeRegistry } from "../commands/active-registry";
100
+ import { notify } from "../services/notify";
54
101
  import * as overlaysPanel from "../panels/overlays";
55
102
 
103
+ import type { TemplateResult } from "lit-html";
56
104
  import type { CanvasPanel, GitDiffState } from "../types";
105
+ import type { AnyCommand, CommandRegistry } from "../commands/registry";
57
106
  import type { JxMutableNode } from "@jxsuite/schema/types";
58
107
  import type { Tab } from "../tabs/tab.js";
108
+ import { mediaTypeEssence } from "@jxsuite/schema/media-type";
59
109
 
60
110
  interface CanvasRenderCtx {
61
- getCanvasMode: () => string;
62
- setCanvasMode: (mode: string) => void;
111
+ setCanvasMode: (tab: Tab | null, mode: string) => void;
63
112
  openFileFromTree: (path: string) => void;
64
113
  gitDiffState: GitDiffState | null;
65
114
  setGitDiffState: (state: GitDiffState | null) => void;
@@ -67,9 +116,6 @@ interface CanvasRenderCtx {
67
116
 
68
117
  let _ctx: CanvasRenderCtx | null = null;
69
118
 
70
- let _prevStylebookFilter = "";
71
- let _prevStylebookCustomizedOnly = false;
72
-
73
119
  /**
74
120
  * Initialize the canvas render module.
75
121
  *
@@ -79,26 +125,29 @@ export function initCanvasRender(ctx: CanvasRenderCtx) {
79
125
  _ctx = ctx;
80
126
  }
81
127
 
82
- /** Monaco language for the source view of a tab's document. */
128
+ /**
129
+ * Monaco language for the source view of a tab's document.
130
+ *
131
+ * A document tab is a format tab or it is JSON — every opener (`files.ts`, `file-ops.ts`, the
132
+ * project-URL path in `studio.ts`) refuses an extension no format class claims, with
133
+ * `noFormatError`. This used to answer `"javascript"` for a `.js` path, which no tab can have, and
134
+ * the language flowed on into a source commit that marked such a tab dirty for text the document
135
+ * cannot hold.
136
+ */
83
137
  function sourceLang(tab: Tab) {
84
138
  const format = formatByName(tab.doc.sourceFormat);
85
- if (format) {
86
- return format.mediaType?.split("/").pop() ?? "plaintext";
87
- }
88
- return (tab.documentPath || "").endsWith(".js") ? "javascript" : "json";
139
+ // The essence: splitting the declared type would yield `markdown; variant=GFM` as a language id.
140
+ return format ? (mediaTypeEssence(format.mediaType)?.split("/").pop() ?? "plaintext") : "json";
89
141
  }
90
142
 
91
143
  /**
92
144
  * The full source text for the source view. Format-class files serialize to their on-disk form
93
145
  * (e.g. frontmatter YAML plus the body for markdown), not just the JSON of the body tree.
94
146
  */
95
- async function sourceContent(tab: Tab, lang: string) {
147
+ async function sourceContent(tab: Tab) {
96
148
  if (formatByName(tab.doc.sourceFormat)) {
97
149
  return serializeDocument(tab);
98
150
  }
99
- if (lang === "javascript") {
100
- return tab.doc.document?.toString?.() || "";
101
- }
102
151
  return JSON.stringify(tab.doc.document, null, 2);
103
152
  }
104
153
 
@@ -108,34 +157,101 @@ async function sourceContent(tab: Tab, lang: string) {
108
157
  Panels have since grown their own rAF scheduler (see panel-scheduler.ts), so the second frame
109
158
  bought nothing and put a hard ~32 ms floor under every escalated edit — canvas-patcher's
110
159
  escalateToFullRender routes through here. */
111
- let _canvasRafId = 0;
112
- export function scheduleCanvasRender() {
113
- if (_canvasRafId) {
160
+ /*
161
+ * One pending frame PER PANE. A shared id would have let the pane that scheduled first swallow the
162
+ * other pane's render for that frame — the dedupe has to be scoped to the surface it dedupes.
163
+ */
164
+ const _canvasRafIds = new Map<string, number>();
165
+
166
+ /**
167
+ * Schedule a full render of one pane's stage.
168
+ *
169
+ * @param {string} [paneId] — defaults to the focused pane
170
+ */
171
+ export function scheduleCanvasRender(paneId: string = workspace.activePaneId) {
172
+ if (_canvasRafIds.has(paneId)) {
114
173
  return;
115
174
  }
116
- _canvasRafId = requestAnimationFrame(() => {
117
- _canvasRafId = 0;
118
- try {
119
- renderCanvas();
120
- } catch (error) {
121
- console.error("renderCanvas error:", error);
122
- }
123
- });
175
+ _canvasRafIds.set(
176
+ paneId,
177
+ requestAnimationFrame(() => {
178
+ _canvasRafIds.delete(paneId);
179
+ try {
180
+ renderCanvas(paneId);
181
+ } catch (error) {
182
+ console.error("renderCanvas error:", error);
183
+ }
184
+ }),
185
+ );
124
186
  }
125
187
 
188
+ /* `handOverCanvasStage` is gone with §18.3's single stage.
189
+ It existed because both render effects key on the active TAB while a handover changes the PANE,
190
+ so nothing else would have repainted the stage that had just changed hands. With a cell per pane
191
+ nothing changes hands: `panels/pane-grid.ts` registers a surface when it builds a cell and
192
+ disposes it when it removes one, and each pane's render is scheduled by its own arrival. */
193
+
126
194
  /**
127
- * Eject canvasWrap's DOM and Lit render part together. Setting textContent/innerHTML removes the
195
+ * Eject a stage's DOM and Lit render part together. Setting textContent/innerHTML removes the
128
196
  * comment nodes Lit uses as a ChildPart's markers; if the private `_$litPart$` reference is left
129
197
  * behind, the next litRender() reuses a part whose markers are detached from the DOM and throws
130
198
  * "This `ChildPart` has no `parentNode`…". Always eject the markers through this helper so the two
131
199
  * operations can never drift apart.
132
200
  */
133
- function hardClearCanvasWrap() {
201
+ function hardClearCanvasWrap(canvasWrap: HTMLElement) {
202
+ /* The frames go FIRST, while they are still reachable from the stage.
203
+ `textContent = ""` detaches every iframe in one statement, and a detached host is only ever
204
+ noticed lazily — by whichever `liveHosts` walk next happens to run — so its channel's `window`
205
+ "message" listener, its overlay and its pending reqIds outlive the DOM until then. Releasing
206
+ here makes the teardown synchronous with the clear that causes it. */
207
+ releaseCanvasHosts(canvasWrap);
134
208
  canvasWrap.textContent = "";
135
209
  // @ts-expect-error -- _$litPart$ is Lit's private render-part marker, not in the DOM types
136
210
  delete canvasWrap["_$litPart$"];
137
211
  }
138
212
 
213
+ /**
214
+ * Hand the Document Header card the node the stage just made for it.
215
+ *
216
+ * One stable callback, so Lit invokes it once per host element rather than on every render. The
217
+ * `undefined` branch is Lit reporting a removal WITHOUT saying which node — and the two placements
218
+ * share this callback, so an Edit→Design swap can report the outgoing host after the incoming one
219
+ * has already been bound. Connectivity is the fact that settles it: only a host that really left
220
+ * the document releases the card.
221
+ */
222
+ const _docHeaderRefs = new Map<string, (el: Element | undefined) => void>();
223
+
224
+ function docHeaderRef(paneId: string): (el: Element | undefined) => void {
225
+ let bind = _docHeaderRefs.get(paneId);
226
+ if (!bind) {
227
+ bind = (el: Element | undefined) => {
228
+ if (el) {
229
+ attachDocumentHeaderHost(paneId, el as HTMLElement);
230
+ return;
231
+ }
232
+ if (!documentHeaderHost(paneId)?.isConnected) {
233
+ attachDocumentHeaderHost(paneId, null);
234
+ }
235
+ };
236
+ _docHeaderRefs.set(paneId, bind);
237
+ }
238
+ return bind;
239
+ }
240
+
241
+ /**
242
+ * The stage's slot for the Document Header card.
243
+ *
244
+ * `in-column` is Edit: the card is a block of the artefact's own column and scrolls with it.
245
+ * `pinned` is Design: the artboards are drawn under a pan/zoom transform, so the card sits above
246
+ * the surface at 1:1 and keeps its own scroll.
247
+ *
248
+ * @param {"in-column" | "pinned"} placement
249
+ * @returns {TemplateResult}
250
+ */
251
+ function docHeaderSlot(placement: "in-column" | "pinned", paneId: string): TemplateResult {
252
+ return html`<div class="doc-header-host ${placement}" ${ref(docHeaderRef(paneId))}></div>`;
253
+ }
254
+
139
255
  /**
140
256
  * Tear the canvas all the way back down to a pristine state. Invoked whenever there is no active
141
257
  * tab (e.g. every tab was closed) so the canvas can never get wedged in a half-initialized state:
@@ -144,12 +260,14 @@ function hardClearCanvasWrap() {
144
260
  * the very next render is treated as a fresh mode transition and rebuilds the surface from
145
261
  * scratch.
146
262
  */
147
- /** Live source-mode collab binding teardown (unbind + release the canonical lock). */
148
- let sourceCollabCleanup: (() => void) | null = null;
149
-
150
- function disposeSourceCollab(): void {
151
- sourceCollabCleanup?.();
152
- sourceCollabCleanup = null;
263
+ /* The source-mode collab teardown is a field of the STAGE, not of the module.
264
+ `disposeSourceCollab()` released whichever binding was last created, so with two Code panes one
265
+ pane leaving source mode dropped the OTHER pane's y-monaco binding and handed back a canonical
266
+ lock it did not hold. */
267
+
268
+ function disposeSourceCollab(surface: CanvasSurface): void {
269
+ surface.sourceCollabCleanup?.();
270
+ surface.sourceCollabCleanup = null;
153
271
  }
154
272
 
155
273
  /**
@@ -180,39 +298,92 @@ async function createSourceCollabBinding(
180
298
  };
181
299
  }
182
300
 
183
- function resetCanvasView() {
184
- if (view.functionEditor) {
185
- view.functionEditor.dispose();
186
- view.functionEditor = null;
187
- }
188
- detachGridPanel();
189
- disposeSourceCollab();
190
- if (view.monacoEditor) {
191
- view.monacoEditor.getModel()?.dispose();
192
- view.monacoEditor.dispose();
193
- view.monacoEditor = null;
194
- }
195
- if (view.centerObserver) {
196
- view.centerObserver.disconnect();
197
- view.centerObserver = null;
198
- }
199
- for (const fn of view.canvasDndCleanups) {
200
- fn();
301
+ /*
302
+ * There is no `view.functionEditor` teardown in here, and that is deliberate.
303
+ *
304
+ * Every other disposal below is a surface this module CREATED — the grid panel, the source-mode
305
+ * Monaco and its collab binding, the centering observer. The function editor is not: the Bottom
306
+ * dock's Logic tab creates it and `panels/editors.ts`'s `syncFunctionEditor` disposes it, from an
307
+ * `afterRender` that `panels/bottom-dock.ts` runs for every registered tab whether or not it is
308
+ * showing — precisely so a surface that outlives its own markup can release it. Losing the last tab
309
+ * is one of the state changes that hook already covers.
310
+ *
311
+ * Reaching in from here was also wrong per PANE. `resetCanvasView` runs for a pane with no tab
312
+ * while `view.functionEditor` is app-wide, so a stage-holding pane going empty disposed an editor
313
+ * the dock was still showing for another pane's tab — and, having cleared the handle, left nothing
314
+ * to rebuild it until the next dock repaint.
315
+ */
316
+ /**
317
+ * Drop the source-mode Monaco, its model, and the debounced work armed over it.
318
+ *
319
+ * The three sites below used to inline the same four lines, and the four lines were incomplete in
320
+ * the same way at all three: they never cancelled the 600ms sync timer. That timer reads
321
+ * `getValue()` off the editor it closes over, a disposed Monaco answers the empty string, and for a
322
+ * format-backed document the callback then runs `parseSourceForPath(path, "")` and assigns the
323
+ * result — **the page's body replaced with an empty parse, 600ms after the user left Code view,
324
+ * with the tab marked dirty so the next ⌘S writes it to disk.** For a `.json` tab the same path
325
+ * threw inside `JSON.parse("")` and was swallowed by the catch, which is luck rather than design
326
+ * and is why only one of the two shapes was ever reported.
327
+ *
328
+ * One function, so there is one place for the teardown to be and no fourth site can omit it.
329
+ *
330
+ * **And it commits before it cancels.** Cancelling alone traded a dead buffer's `""` for the user's
331
+ * last 600ms of typing, which is still lost work and is now silent: `doc.dirty` stays `false`, so
332
+ * nothing says the edit went missing. {@link commitBufferWrites} runs the armed parse WHILE the
333
+ * model is still attached — `getValue()` is read before its first `await` — and only then drops
334
+ * what is left, so leaving Code view saves the edit instead of choosing which way to discard it.
335
+ *
336
+ * **And the parse is allowed not to land, which is the third way out.** Unparseable source
337
+ * deliberately keeps the buffer rather than resyncing over a half-typed heading — the right call
338
+ * while the surface is standing, and a deletion the moment it is not, because the next two lines
339
+ * detach the model and the text existed nowhere else. `commitBufferWrites` says so; it cannot say
340
+ * it synchronously, because the parse runs through the format host, so the answer arrives with the
341
+ * promise. Refusing the teardown instead is not available to any of the three callers: they are a
342
+ * pane reset, a mode transition and a model-URI swap, each of which has already taken the container
343
+ * or the URI this editor needs, and a Monaco kept alive over either is unreachable rather than
344
+ * rescued.
345
+ */
346
+ /*
347
+ * **And it is the STAGE's editor, which is the data-loss half.**
348
+ *
349
+ * `view.monacoEditor` was app-wide, so this disposed whichever Code buffer was open — not this
350
+ * pane's. With two Code panes, leaving source mode in one committed and destroyed the other's
351
+ * model, and `commitBufferWrites` parses `getValue()` against the tab THAT editor was mounted for,
352
+ * so the surviving pane's document took a write built from a buffer belonging to a different file.
353
+ */
354
+ function disposeSourceEditor(surface: CanvasSurface): void {
355
+ const editor = surface.monacoEditor;
356
+ if (!editor) {
357
+ return;
201
358
  }
202
- view.canvasDndCleanups = [];
203
- for (const fn of view.canvasEventCleanups) {
204
- fn();
359
+ commitBufferWrites(editor, "source");
360
+ editor.getModel()?.dispose();
361
+ editor.dispose();
362
+ surface.monacoEditor = null;
363
+ }
364
+
365
+ function resetCanvasView(surface: CanvasSurface) {
366
+ const canvasWrap = surface.wrap;
367
+ const { paneId } = surface;
368
+ detachGridPanel(paneId);
369
+ detachLibraryPane(paneId);
370
+ detachEntryPane(paneId);
371
+ detachSettingsPane(paneId);
372
+ disposeSourceCollab(surface);
373
+ disposeSourceEditor(surface);
374
+ if (surface.centerObserver) {
375
+ surface.centerObserver.disconnect();
376
+ surface.centerObserver = null;
205
377
  }
206
- view.canvasEventCleanups = [];
207
- for (const p of canvasPanels) {
378
+ for (const p of surface.panels) {
208
379
  p.renderScope?.stop();
209
380
  p.renderScope = null;
210
381
  }
211
- canvasPanels.length = 0;
382
+ surface.panels.length = 0;
212
383
 
213
- hardClearCanvasWrap();
384
+ hardClearCanvasWrap(canvasWrap);
214
385
 
215
- view.panzoomWrap = null;
386
+ surface.panzoomWrap = null;
216
387
  canvasWrap.style.padding = "";
217
388
  canvasWrap.style.alignItems = "";
218
389
  canvasWrap.style.flexDirection = "";
@@ -222,7 +393,7 @@ function resetCanvasView() {
222
393
  dismissLinkPopover();
223
394
  dismissContextMenu();
224
395
  dismissSlashMenu();
225
- view.prevCanvasMode = null;
396
+ surface.prevCanvasMode = null;
226
397
  }
227
398
 
228
399
  /**
@@ -232,14 +403,34 @@ function resetCanvasView() {
232
403
  * before this runs, and every continuation below re-checks `view.monacoEditor`, so a teardown or a
233
404
  * mode switch while the module loads is handled the same way a teardown mid-y-monaco-load already
234
405
  * was.
406
+ *
407
+ * **The mount ITSELF owed the same re-check and did not make it.** This was the fourth Monaco mount
408
+ * path and the only one with no post-await guard: `renderCanvasImpl` sets `surface.prevCanvasMode`
409
+ * BEFORE it reaches here, so a second synchronous `renderCanvas()` inside the cold load sees
410
+ * `modeChanged === false` and a still-null `view.monacoEditor`, skips the source fast path and
411
+ * falls through to mount again. `store.ts`'s `render()`/`renderOnly()` coalesce nothing, so two
412
+ * mounts is a plain sequence of two renders. {@link mountStillWanted} is asked before `createModel`
413
+ * rather than after, because the duplicate is not just a wasted editor — the second `createModel`
414
+ * claims a URI the first already registered, which real Monaco throws on.
235
415
  */
236
416
  async function mountSourceEditor(
237
417
  tab: Tab,
418
+ surface: CanvasSurface,
238
419
  editorContainer: Element,
239
420
  filePath: string,
240
421
  lang: string,
241
422
  ): Promise<void> {
423
+ const { paneId } = surface;
242
424
  const monaco = await loadMonaco();
425
+ if (
426
+ !mountStillWanted(
427
+ editorContainer,
428
+ surface.monacoEditor,
429
+ () => tabOfPane(paneId) === tab && canvasModeOfPane(paneId) === "source",
430
+ )
431
+ ) {
432
+ return;
433
+ }
243
434
  const modelUri = monaco.Uri.parse(modelUriFor(filePath));
244
435
  const model = monaco.editor.createModel("", lang, modelUri);
245
436
  // Co-edited tabs bind the buffer to the shared Y.Text instead of a local serialization: the
@@ -250,95 +441,172 @@ async function mountSourceEditor(
250
441
  void collabCtx
251
442
  .enter()
252
443
  .then(async () => {
253
- const editor = view.monacoEditor;
444
+ const editor = surface.monacoEditor;
254
445
  if (!editor || editor.getModel() !== model) {
446
+ // The lock was taken FOR a surface that is no longer here, and `leave()` reaches this
447
+ // Mount from exactly one place: the cleanup below, which this path never gets. See the
448
+ // Catch for what a lock nobody releases now costs.
449
+ collabCtx.leave();
255
450
  return;
256
451
  }
257
452
  const cleanup = await createSourceCollabBinding(model, editor, collabCtx);
258
453
  // The editor may have been torn down while y-monaco loaded — unbind immediately.
259
- if (view.monacoEditor !== editor || editor.getModel() !== model) {
454
+ if (surface.monacoEditor !== editor || editor.getModel() !== model) {
260
455
  cleanup();
261
456
  return;
262
457
  }
263
- sourceCollabCleanup = cleanup;
458
+ surface.sourceCollabCleanup = cleanup;
459
+ /* AND THE BUFFER SAYS SO, because from this instant it is not ours.
460
+ The binding is two-way: peers' keystrokes arrive in the model with nothing declared, and
461
+ anything WE write into the model is published to all of them. The repaint's fast path
462
+ below would happily serialize the structure tree over it — and the structure tree is what
463
+ the source reconciler PARSED OUT of this same shared text, so every round trip that is
464
+ not byte-stable was a whole-document replace pushed to every peer, on every repaint, with
465
+ `hasTextFocus()` protecting only the person currently typing. Clause 5 refuses it. */
466
+ editor._writes?.markShared();
264
467
  if (collabCtx.readOnly) {
265
468
  editor.updateOptions({ readOnly: true });
266
469
  }
267
470
  })
268
471
  .catch(() => {
269
- // Binding failures degrade to a read-only-ish local buffer; the session stays live.
472
+ /* A BINDING FAILURE HOLDS A LOCK AND CARRIES NOTHING, and both halves have to be said.
473
+ `enter()` flips the room's canonical lock to "source" before y-monaco is even imported,
474
+ and `ctx.leave()` lives in exactly one place — the cleanup `createSourceCollabBinding`
475
+ returns. So a failure past that point left the lock held by a client with no binding, and
476
+ the freeze is now honest enough to include that client too (`collab-session.ts`'s transact
477
+ gate no longer exempts the lock holder): nobody in the room could edit the structure, and
478
+ nobody could edit the source either, until someone reloaded. Hand it back.
479
+ And say what the buffer is, because it is not an editing surface. The mount returns before
480
+ installing its change handler on a co-edited tab, so on this path a keystroke arms no
481
+ commit, marks nothing, leaves `tabBufferUnsaved` false — and the next repaint replaces it,
482
+ with clause 5 unable to help because `markShared` is what never ran. `readOnly` is what
483
+ "degrades to a read-only-ish local buffer" was always trying to say. */
484
+ collabCtx.leave();
485
+ const editor = surface.monacoEditor;
486
+ if (editor && editor.getModel() === model) {
487
+ editor.updateOptions({ readOnly: true });
488
+ }
270
489
  });
271
490
  } else {
272
- sourceContent(tab, lang)
491
+ sourceContent(tab)
273
492
  .then((content) => {
274
- const editor = view.monacoEditor;
275
- if (editor && editor.getModel() === model) {
493
+ const editor = surface.monacoEditor;
494
+ // The model identity says "this load is still for this buffer"; `bufferMovedOn` says "and
495
+ // The buffer is still the empty one it was created as". Serializing a document is a round
496
+ // Trip through the format host, and a user who starts typing into the empty box before it
497
+ // Returns had their first words replaced by the file they were already looking at.
498
+ if (editor && editor.getModel() === model && !bufferMovedOn(editor, "")) {
276
499
  editor._ignoreNextChange = true;
277
500
  model.setValue(content);
501
+ // The buffer took the DOCUMENT's own text, so the two agree — stated, because clause 3 is
502
+ // A fact each programmatic write owns rather than something a timer can be read for.
503
+ editor._writes?.markSettled();
278
504
  }
279
505
  })
280
506
  .catch(() => {
281
507
  // Serialization unavailable — leave the buffer empty rather than crash the render
282
508
  });
283
509
  }
284
- view.monacoEditor = monaco.editor.create(editorContainer as unknown as HTMLElement, {
285
- automaticLayout: true,
286
- fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace",
287
- fontSize: 12,
288
- lineNumbers: "on",
289
- minimap: { enabled: false },
290
- model,
291
- scrollBeyondLastLine: false,
292
- tabSize: 2,
293
- theme: "vs-dark",
294
- wordWrap: "on",
295
- });
510
+ // Typed as the surface `view` declares (the editor plus `_ignoreNextChange` / `_writes`), because
511
+ // The instance is HELD here now rather than re-read off `view.monacoEditor` in every callback.
512
+ const sourceEditor: NonNullable<typeof surface.monacoEditor> = monaco.editor.create(
513
+ editorContainer as unknown as HTMLElement,
514
+ {
515
+ automaticLayout: true,
516
+ fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace",
517
+ fontSize: 12,
518
+ lineNumbers: "on",
519
+ minimap: { enabled: false },
520
+ model,
521
+ scrollBeyondLastLine: false,
522
+ tabSize: 2,
523
+ theme: "vs-dark",
524
+ wordWrap: "on",
525
+ },
526
+ );
527
+ surface.monacoEditor = sourceEditor;
528
+ /* WHOSE BUFFER THIS IS, in the one spelling the dock's editor already used.
529
+ `tab` was named only by the closures below, so a caller holding a `Tab` — the ⌘W gate, the quit
530
+ gate — could ask the function editor whether the buffer was its tab's and had no way to ask
531
+ this one at all. Half an answer is what let a source tab close over unprompted typing. */
532
+ sourceEditor._editingTab = tab;
533
+
534
+ // The debounce holder goes on the editor, not in this closure. A closure variable is reachable
535
+ // Only from inside `mountSourceEditor`, which is why `disposeSourceEditor` had nothing to cancel
536
+ // And a timer outlived every teardown; hanging it off the instance gives it the instance's
537
+ // Lifetime. Installed before the collab early-return so the canceller exists either way.
538
+ const writes = bufferWrites(sourceEditor);
296
539
 
297
540
  // Debounced sync back to state (solo tabs only: co-edited buffers flow through the shared
298
541
  // Y.Text and the source reconciler's parse mirror instead of a whole-doc replace).
299
542
  if (collabCtx) {
300
543
  return;
301
544
  }
302
- let debounce: ReturnType<typeof setTimeout> | undefined;
303
- view.monacoEditor.onDidChangeModelContent(() => {
304
- const editor = view.monacoEditor;
305
- if (!editor) {
545
+ sourceEditor.onDidChangeModelContent(() => {
546
+ // Identity, not existence: a change fired after this instance stopped being the mounted one has
547
+ // Nothing to say, and asking `view.monacoEditor` for the editor instead of holding it is how
548
+ // The callback below came to address a tab it was never mounted for.
549
+ if (surface.monacoEditor !== sourceEditor) {
306
550
  return;
307
551
  }
308
- if (editor._ignoreNextChange) {
309
- editor._ignoreNextChange = false;
552
+ if (sourceEditor._ignoreNextChange) {
553
+ sourceEditor._ignoreNextChange = false;
310
554
  return;
311
555
  }
312
- clearTimeout(debounce);
313
- debounce = setTimeout(async () => {
314
- const tabNow = activeTab.value;
315
- if (!tabNow) {
556
+ // A keystroke: the buffer is ahead of the document until the commit lands, and the text is the
557
+ // User's own — which is the narrower fact the close and quit gates read.
558
+ writes.markTyped();
559
+ writes.arm(BUFFER_COMMIT, 600, async () => {
560
+ /* `tab` is the tab this editor was MOUNTED for, captured above and never re-read. It used to
561
+ be `activeTab.value` — whatever was focused when the timer fired — which is the same
562
+ cross-document write the dock's commit had, surviving here only because the model-URI swap
563
+ happens to dispose this editor on a source→source tab change. A coincidence in one caller
564
+ is not a rule, and the tab still has to be OPEN: a closed tab is not in `workspace.tabs`,
565
+ so a parse written into it is a parse nothing will ever read. */
566
+ if (!tabIsLive(tab) || !bufferIsLive(sourceEditor)) {
316
567
  return;
317
568
  }
318
- if (formatByName(tabNow.doc.sourceFormat) && tabNow.documentPath) {
569
+ if (formatByName(tab.doc.sourceFormat) && tab.documentPath) {
319
570
  try {
320
571
  // Parse the full source back into body + frontmatter (title, $head, etc.).
321
572
  const { document, frontmatter } = await parseSourceForPath(
322
- tabNow.documentPath,
323
- editor.getValue(),
573
+ tab.documentPath,
574
+ sourceEditor.getValue(),
324
575
  );
325
- tabNow.doc.document = document as JxMutableNode;
326
- tabNow.doc.content.frontmatter = frontmatter;
327
- tabNow.doc.dirty = true;
576
+ /* AND THE TAB IS ASKED AGAIN, because the parse is an await and the check above it is
577
+ not. The whole reason `tabIsLive` is here is that a parse written into a closed tab is
578
+ a parse nothing will ever read — and the window that closes the tab is precisely this
579
+ one: `commitTabBuffers` is called by two destroyers (⌘W / the × via `requestClose`,
580
+ and the preview slot's replacement in `openTab`), and the second cannot wait for a
581
+ promise. Checked before the await as well as after, because a flush that starts on a
582
+ dead tab should not pay for a format round trip to find out. */
583
+ if (!tabIsLive(tab)) {
584
+ return;
585
+ }
586
+ tab.doc.document = document as JxMutableNode;
587
+ tab.doc.content.frontmatter = frontmatter;
588
+ tab.doc.dirty = true;
589
+ writes.markSettled();
328
590
  } catch {
329
- // Unparseable source — don't update state
591
+ // Unparseable source — don't update state. And do NOT settle: the buffer really is ahead
592
+ // Of the document, so a repaint must go on refusing to overwrite the half-typed heading.
330
593
  }
331
594
  } else if (lang === "json") {
332
595
  try {
333
- tabNow.doc.document = JSON.parse(editor.getValue()) as JxMutableNode;
334
- tabNow.doc.dirty = true;
596
+ tab.doc.document = JSON.parse(sourceEditor.getValue()) as JxMutableNode;
597
+ tab.doc.dirty = true;
598
+ writes.markSettled();
335
599
  } catch {
336
- // Invalid JSON — don't update state
600
+ // Invalid JSON — same: unparseable is a reason to keep the buffer, not to resync over it.
337
601
  }
338
- } else {
339
- tabNow.doc.dirty = true;
340
602
  }
341
- }, 600);
603
+ /* There is no third branch, and the one that used to be here could not have worked.
604
+ It marked the tab DIRTY for text the document has no representation of — `sourceContent`
605
+ answered `doc.document?.toString?.()`, i.e. `"[object Object]"` — so ⌘S would have written
606
+ that string to disk. It only ever ran for a `.js` document tab, and nothing opens one:
607
+ `files.ts`, `file-ops.ts` and the project-URL open all end at `noFormatError` for an
608
+ extension no format class claims, so a document tab is a format tab or it is JSON. */
609
+ });
342
610
  });
343
611
  }
344
612
 
@@ -347,17 +615,97 @@ async function mountSourceEditor(
347
615
  * several early returns, so the span is closed by {@link timeSpan}'s `finally` rather than by
348
616
  * hand.
349
617
  */
350
- export function renderCanvas() {
351
- timeSpan(SPAN_FULL_RENDER, renderCanvasImpl);
618
+ export function renderCanvas(paneId: string = workspace.activePaneId) {
619
+ const surface = surfaceForPane(paneId);
620
+ timeSpan(SPAN_FULL_RENDER, () => {
621
+ renderCanvasImpl(surface);
622
+ });
352
623
  }
353
624
 
354
- function renderCanvasImpl() {
355
- const tab = activeTab.value;
625
+ /**
626
+ * Say, on this stage, why a derived pane has nothing to draw.
627
+ *
628
+ * **`derived.reason` had a writer, a render-input tracker and no reader at all.** It is written by
629
+ * `applyDerivation`, tracked by `panels/pane-context.ts`, and `grep -rn "\.reason" src/` found
630
+ * nothing that printed it — so choosing Layout on a page with no layout gave a pane with a
631
+ * derivation, no tabs, no strip chip, no context bar and a blank stage that `paneIsEmpty` would not
632
+ * collapse. The sentence written for the honest empty state was dead code.
633
+ *
634
+ * Through {@link renderEmptyState} rather than a block of its own: the copy rules are inherited,
635
+ * and a derived pane with nothing to show is the same kind of region as any other.
636
+ *
637
+ * **`held` is the document this pane still OWNS, and it is the difference between a notice and a
638
+ * riddle.** A companion that resolved once has a real tab, so `panels/tab-strip.ts` draws a real
639
+ * chip with a real ✕ — and when the rule then dies, the strip says `base.json` while the stage says
640
+ * "This page has no layout." The sentence is true and it is about something else; nothing on screen
641
+ * says why the file named in the strip is not drawn, or how to get it back. Naming it, and putting
642
+ * the one verb that ends the follow beside it, is what makes the two halves agree. A LENS never
643
+ * passes one: it owns no document, `tabOfPane` would hand back the SOURCE pane's, and `pane.pin` is
644
+ * refused for a projection (§14.1).
645
+ *
646
+ * @param {CanvasSurface} surface
647
+ * @param {string} message
648
+ * @param {Tab | null} [held]
649
+ */
650
+ function renderDerivationNotice(
651
+ surface: CanvasSurface,
652
+ message: string,
653
+ held: Tab | null = null,
654
+ ): void {
655
+ surface.wrap.style.display = "block";
656
+ const registry = activeRegistry();
657
+ const pin = registry?.get("pane.pin");
658
+ litRender(
659
+ renderEmptyState({
660
+ message,
661
+ ...(held && { detail: `${tabLabel(held)} is still open here.` }),
662
+ ...(held &&
663
+ pin && {
664
+ actions: [
665
+ {
666
+ label: pin.title,
667
+ run: () => {
668
+ void registry?.run(pin.id);
669
+ },
670
+ },
671
+ ],
672
+ }),
673
+ }),
674
+ surface.wrap,
675
+ );
676
+ }
677
+
678
+ function renderCanvasImpl(surface: CanvasSurface) {
679
+ /* Every read below is scoped to ONE pane. The stage, the panels it mounted and the mode it draws
680
+ are the surface's; the document is whatever tab that pane is showing, which is `activeTab` only
681
+ when the pane is the focused one. */
682
+ const canvasWrap = surface.wrap;
683
+ /* A pane with no stage renders nothing — it is not an error and it is not a fallback to some
684
+ other pane's stage. Two schedulers can name a stage-less pane: a frame queued for a pane that
685
+ lost the shell's single stage between the schedule and the rAF, and `escalateToFullRender` on
686
+ a tab whose pane is not the one on screen. Both mean the same thing — there is no surface to
687
+ repaint — and rendering "the canvas" instead is how one pane's escalation used to redraw the
688
+ other. Left unguarded this threw on Lit's render part, which is what `⌘\` did. */
689
+ if (!canvasWrap) {
690
+ return;
691
+ }
692
+ const canvasPanels = surface.panels;
693
+ const tab = tabOfPane(surface.paneId);
356
694
  if (!tab) {
357
695
  // No active tab — reset every piece of canvas view state so reopening a file can never inherit
358
696
  // A stale Lit part, a dead Monaco editor, or a mismatched prevCanvasMode (the toxic states that
359
697
  // Previously left the canvas unrenderable until a full reload).
360
- resetCanvasView();
698
+ attachDocumentHeaderHost(surface.paneId, null);
699
+ resetCanvasView(surface);
700
+ /* A DERIVED pane with nothing to draw explains itself. It is not the welcome screen and it is
701
+ not a blank stage: it is a standing rule that has not resolved — "Select an element inside a
702
+ component to see its definition" — and until this branch existed that sentence was written,
703
+ tracked as a render input and never printed anywhere. */
704
+ const derived = derivationOfPane(surface.paneId);
705
+ if (derived) {
706
+ renderDerivationNotice(surface, derived.reason || "Looking for something to show here…");
707
+ return;
708
+ }
361
709
  // Nothing is open. A dev-server root that is only a workspace/monorepo (no project.json) still
362
710
  // Populates projectState, but it is not an open project — the file tree prompts to open one and
363
711
  // The canvas must keep showing the welcome screen rather than going blank.
@@ -366,24 +714,75 @@ function renderCanvasImpl() {
366
714
  }
367
715
  return;
368
716
  }
717
+ /* A derivation that HAS a document and has gone stale says so too.
718
+ The notice above lives under `if (!tab)`, and a pane that already opened something keeps its
719
+ tab — so a layout companion whose page dropped its `$layout`, and a Code lens whose source
720
+ pane switched to Code beside it, both had `status: "unavailable"` and a sentence written by
721
+ `applyDerivation`, tracked as a render input by `panels/pane-context.ts`, and drawn nowhere.
722
+ The companion went on showing a file it was no longer about; the lens mounted a SECOND Monaco
723
+ model on the source pane's URI, which real Monaco throws on.
724
+ `reason` and not just `status`, because a derivation is `unavailable` for one frame before the
725
+ follow has run and a stage that blanked on that would flicker on every retarget. */
726
+ const staleDerivation = derivationOfPane(surface.paneId);
727
+ if (staleDerivation?.status === "unavailable" && staleDerivation.reason) {
728
+ attachDocumentHeaderHost(surface.paneId, null);
729
+ resetCanvasView(surface);
730
+ /* …and for a COMPANION, what it is still holding. `tab` here is the pane's OWN document for a
731
+ companion and the SOURCE pane's for a lens — the hop `tabOfPane` takes — so the kind decides
732
+ whether there is anything to name. See {@link renderDerivationNotice}. */
733
+ renderDerivationNotice(
734
+ surface,
735
+ staleDerivation.reason,
736
+ staleDerivation.kind === "companion" ? tab : null,
737
+ );
738
+ return;
739
+ }
369
740
  const ctx = _ctx as CanvasRenderCtx;
370
741
  const S = {
371
742
  document: tab.doc.document,
372
743
  mode: tab.doc.mode,
373
744
  ui: tab.session.ui,
374
745
  };
375
- // Base mode drives the host surface (panel structure, panzoom vs centered column). The preview
376
- // Toggle changes only what the iframe renders — resolveCanvasDocument reads the effective mode
377
- // Via its own getCanvasMode. Flipping the toggle therefore re-renders panels in place (no
378
- // ModeChanged teardown, no pan reset).
379
- const { canvasMode } = S.ui;
380
-
381
- // Advance render generation so stale async renders from the previous cycle bail out
382
- view.renderGeneration += 1;
746
+ // The EFFECTIVE mode drives the host surface (panel structure, panzoom vs centered column vs the
747
+ // Preview stage). Preview is its own surface — a real, viewport-sized iframe that scrolls its own
748
+ // Document — so flipping the toggle IS a mode transition and rebuilds the surface, rather than
749
+ // Re-rendering the previous mode's panels in place.
750
+ const canvasMode = canvasModeOfPane(surface.paneId);
751
+
752
+ /* The Document Header card (§3.2 ⑧) is drawn by the STAGE, not by a band above it. Two surfaces
753
+ draw a page you can author: the centered Edit column, where the card goes INSIDE the document
754
+ column and scrolls with the artefact, and the Design artboards, where it is pinned above the
755
+ panzoom surface because a form drawn at the artboard's scale is a picture of a form, not a
756
+ control. `hasDocumentHeader` is the only remaining predicate and it is a fact about the
757
+ DOCUMENT — the one condition left, because the mode is the only other thing that decides
758
+ whether a page is being authored. It used to carry two more clauses, suppressing the card while
759
+ a function body or a formula was open on the grounds that those sub-editors took the whole
760
+ stage. They open in the dock's Logic tab now (P8) and the page is still on screen behind it, so
761
+ those clauses only DETACHED the visible card: it stopped re-rendering and quietly showed
762
+ frontmatter from before the edit. */
763
+ /* …and NOT in a lens. The Document Header is an editing surface — Title, Route, SEO, the raw
764
+ frontmatter — over a document this pane does not own, and two cards editing one document's
765
+ frontmatter side by side is two writers for one field. A lens is a view; the card belongs to
766
+ the pane that owns the tab. */
767
+ const wantsDocHeader =
768
+ (canvasMode === "edit" || canvasMode === "design") &&
769
+ derivationOfPane(surface.paneId)?.kind !== "lens" &&
770
+ hasDocumentHeader(tab);
771
+ if (!wantsDocHeader) {
772
+ attachDocumentHeaderHost(surface.paneId, null);
773
+ }
774
+
775
+ /* Advance THIS stage's render generation so stale async renders from its previous pass bail out.
776
+ Per-surface, because the comparison at the end of `renderCanvasIntoPanel` asks "is my pass
777
+ still the current one" — of this pane. App-wide, pane A opening a pass invalidated pane B's
778
+ in-flight one, so B's deferred artboards never reached `ready` and `classifyOps`' every-panel
779
+ gate escalated every subsequent edit in B to a full render, for good. The VALUE is still
780
+ globally unique, because the iframe host caches a pass's resolved document by it. */
781
+ surface.renderGeneration = nextRenderGeneration();
383
782
  canvasPerf.fullRenders += 1;
384
783
 
385
784
  // Detect whether this is a mode transition or a content-only re-render
386
- const modeChanged = canvasMode !== view.prevCanvasMode;
785
+ const modeChanged = canvasMode !== surface.prevCanvasMode;
387
786
 
388
787
  // Only clear Lit's internal state on mode transitions (structural panel changes).
389
788
  // For content re-renders in the same mode, Lit's template diffing preserves
@@ -391,27 +790,16 @@ function renderCanvasImpl() {
391
790
  // RenderCanvasLive uses atomic clear (innerHTML = "" right before appendChild).
392
791
  // @ts-expect-error -- _$litPart$ is Lit's private render-part marker, not in the DOM types
393
792
  if (modeChanged && canvasWrap["_$litPart$"]) {
394
- hardClearCanvasWrap();
395
- }
396
-
397
- // Function editor mode: editing a function body in Monaco (JS)
398
- if (S.ui.editingFunction) {
399
- renderFunctionEditor();
400
- return;
401
- }
402
-
403
- // Dispose function editor if switching away
404
- if (view.functionEditor) {
405
- view.functionEditor.dispose();
406
- view.functionEditor = null;
793
+ hardClearCanvasWrap(canvasWrap);
407
794
  }
408
795
 
409
- // Formula workspace mode: full-screen structured editing of an $expression (the function editor
410
- // Takes precedence when both targets are set).
411
- if (S.ui.editingFormula) {
412
- renderFormulaWorkspace();
413
- return;
414
- }
796
+ /* There are no `editingFunction` / `editingFormula` branches here, and that is the point.
797
+ Both used to RETURN from this function, which is what made the logic editors a takeover: the
798
+ stage kept whatever DOM it was last painted with and stopped tracking the document. P8 moved
799
+ both into the dock's Logic tab, `panels/bottom-dock.ts` reveals it from its own effect, and
800
+ `panels/editors.ts`'s `syncFunctionEditor` owns the Monaco instance from the panel's
801
+ `afterRender` — including disposing it. Nothing about the canvas render depends on a logic
802
+ target any more, so the page underneath the dock keeps rendering while you edit a formula. */
415
803
 
416
804
  /* Source mode across a TAB switch: the buffer swap below reuses the existing model, and a model
417
805
  carries the file identity Monaco validates against — its URI is what the JSON language service
@@ -420,32 +808,53 @@ function renderCanvasImpl() {
420
808
  folder (`./project.schema.json` → `file:///pages/project.schema.json`, unregistered, back to
421
809
  "No schema request service available"). Tear down instead and let the creation path below build
422
810
  a model with the right URI. */
423
- const loadedForSourceSwap = canvasMode === "source" && view.monacoEditor ? loadedMonaco() : null;
424
- if (loadedForSourceSwap && view.monacoEditor) {
811
+ const loadedForSourceSwap =
812
+ canvasMode === "source" && surface.monacoEditor ? loadedMonaco() : null;
813
+ if (loadedForSourceSwap && surface.monacoEditor) {
425
814
  const expectedUri = loadedForSourceSwap.Uri.parse(
426
815
  modelUriFor(tab.documentPath || "document.json"),
427
816
  );
428
- if (view.monacoEditor.getModel()?.uri.toString() !== expectedUri.toString()) {
429
- disposeSourceCollab();
430
- view.monacoEditor.getModel()?.dispose();
431
- view.monacoEditor.dispose();
432
- view.monacoEditor = null;
817
+ if (surface.monacoEditor.getModel()?.uri.toString() !== expectedUri.toString()) {
818
+ disposeSourceCollab(surface);
819
+ disposeSourceEditor(surface);
433
820
  }
434
821
  }
435
822
 
436
- // Source mode: update existing Monaco editor without recreating. Don't replace the buffer while
437
- // The user is actively typing in it — that would reformat under the cursor (the source view is
438
- // The editing surface here, mirroring the panel draft-state behaviour).
439
- if (canvasMode === "source" && view.monacoEditor) {
440
- const editor = view.monacoEditor;
441
- sourceContent(tab, sourceLang(tab))
823
+ /* THE FAST PATHS, and the one condition every one of them owes.
824
+ Each says "the structure this mode needs is already standing on the stage, so there is nothing
825
+ to build". None of them can see the stage: they ask a MODULE whether it mounted (`view
826
+ .monacoEditor`, `gridPanelMounted`, …), and that answer outlives the DOM. `modeChanged` is
827
+ exactly "the structure on this stage is not mine" — a real mode transition, or a surface whose
828
+ `prevCanvasMode` was released because the stage changed hands — and the mode-transition clear
829
+ ABOVE has already emptied the wrap by the time any of them is reached.
830
+ Unguarded, this is what survived the Unsplit fix: the stage came back to the primary pane,
831
+ `prevCanvasMode` was null, the wrap was cleared, and the source fast path then returned on the
832
+ strength of a Monaco editor whose container had just been thrown away. An empty stage, a live
833
+ editor nobody could see, and only a reload to get it back. */
834
+
835
+ /* Source mode: update the existing Monaco editor without recreating it. Don't replace the buffer
836
+ while it has moved on — that would reformat under the cursor (the source view is the editing
837
+ surface here, mirroring the panel draft-state behaviour). This branch is where the rule was
838
+ first written, as a bare `!editor.hasTextFocus()`; `services/monaco-buffer.ts` is where it
839
+ lives now, and it gained the clauses this spelling was missing — a buffer holding text the
840
+ document has not been given (the user typed, then clicked away), an editor disposed inside the
841
+ round trip, and clause 5: a CO-EDITED buffer, which this write does not merely overwrite but
842
+ PUBLISHES over, to every peer in the room. The text below is `serializeDocument` of a structure
843
+ tree the source reconciler parsed out of that same shared text, so on a co-edited tab this was
844
+ the round trip asserting itself over its own input on every repaint. */
845
+ if (canvasMode === "source" && surface.monacoEditor && !modeChanged) {
846
+ const editor = surface.monacoEditor;
847
+ sourceContent(tab)
442
848
  .then((newVal) => {
443
- if (view.monacoEditor !== editor) {
849
+ if (surface.monacoEditor !== editor) {
444
850
  return;
445
851
  }
446
- if (!editor.hasTextFocus() && editor.getValue() !== newVal) {
852
+ if (!bufferMovedOn(editor) && editor.getValue() !== newVal) {
447
853
  editor._ignoreNextChange = true;
448
854
  editor.setValue(newVal);
855
+ // Buffer and document agree again, by the document's text winning. Same declaration the
856
+ // Dock's re-sync makes, for the same reason: clause 3 belongs to whoever writes.
857
+ editor._writes?.markSettled();
449
858
  }
450
859
  })
451
860
  .catch(() => {
@@ -456,7 +865,25 @@ function renderCanvasImpl() {
456
865
 
457
866
  // Grid fast-path: the grid panel runs its own effect scope (toolbar + engine stay live), so a
458
867
  // Same-tab re-render while the panel is mounted needs nothing from the canvas pipeline.
459
- if (canvasMode === "grid" && gridPanelMounted(tab)) {
868
+ if (canvasMode === "grid" && gridPanelMounted(surface.paneId, tab) && !modeChanged) {
869
+ return;
870
+ }
871
+
872
+ // Library fast-path, same shape: the Library pane owns its own reactivity (view state, scan,
873
+ // Window), so a same-tab re-render while it is mounted needs nothing from the canvas pipeline.
874
+ if (canvasMode === "manage" && libraryPaneMounted(surface.paneId, tab) && !modeChanged) {
875
+ return;
876
+ }
877
+
878
+ // Entry fast-path, same shape: the entry form owns its own effect over the tab's frontmatter, so
879
+ // A field commit repaints the form and nothing reaches the canvas pipeline.
880
+ if (canvasMode === "entry" && entryPaneMounted(surface.paneId, tab) && !modeChanged) {
881
+ return;
882
+ }
883
+
884
+ // Settings fast-path, same shape: the Project Settings editor subscribes to the section registry
885
+ // And to its own chosen section, so a mounted editor needs nothing from the canvas pipeline.
886
+ if (canvasMode === "settings" && settingsPaneMounted(surface) && !modeChanged) {
460
887
  return;
461
888
  }
462
889
 
@@ -465,10 +892,15 @@ function renderCanvasImpl() {
465
892
  // Filter/Customized changes fall through to the full rebuild (they change which specimens exist),
466
893
  // As does a zero-host post (no stylebook iframe live yet).
467
894
  if (canvasMode === "stylebook" && !modeChanged) {
468
- const curFilter = tab.session.ui.stylebookFilter || "";
469
- const curCustomized = Boolean(tab.session.ui.stylebookCustomizedOnly);
895
+ /* THIS stage's last catalogue, not the module's. Both panes render in the same frame when
896
+ `shell.stylebook.filter` changes, so a single slot was advanced by whichever stage went
897
+ first and the second one then saw `filterChanged === false` — returning through the cheap
898
+ style-post path with a catalogue built for the PREVIOUS filter. */
899
+ const curFilter = shell.stylebook.filter;
900
+ const curCustomized = shell.stylebook.customizedOnly;
470
901
  const filterChanged =
471
- curFilter !== _prevStylebookFilter || curCustomized !== _prevStylebookCustomizedOnly;
902
+ curFilter !== surface.prevStylebookFilter ||
903
+ curCustomized !== surface.prevStylebookCustomizedOnly;
472
904
  if (!filterChanged) {
473
905
  const style = transposeStylebookStyle(getEffectiveStyle(tab.doc.document?.style));
474
906
  if (postStyleUpdateToStylebookHosts(style as Record<string, unknown>) > 0) {
@@ -478,7 +910,7 @@ function renderCanvasImpl() {
478
910
  }
479
911
 
480
912
  // Detect whether this is a mode transition or a content-only re-render
481
- view.prevCanvasMode = canvasMode;
913
+ surface.prevCanvasMode = canvasMode;
482
914
 
483
915
  // Best-effort commit of a live inline-edit session BEFORE lit rebuilds the panel DOM. Covers
484
916
  // Keyboard-driven tab switches / mode changes where no parent pointerdown preceded the render —
@@ -486,19 +918,11 @@ function renderCanvasImpl() {
486
918
  // Still routes to the tab the session belonged to (the host's tabId flips only on renderComplete).
487
919
  commitActiveEditSession();
488
920
 
489
- // DnD handlers are registered on inner canvas elements that get replaced on every
490
- // Content render, so always clean them up.
491
- for (const fn of view.canvasDndCleanups) {
492
- fn();
493
- }
494
- view.canvasDndCleanups = [];
495
-
496
- // Panel event handlers (click, dblclick, etc.) capture closures over panel references.
497
- // Always re-register to keep closures fresh across document switches.
498
- for (const fn of view.canvasEventCleanups) {
499
- fn();
500
- }
501
- view.canvasEventCleanups = [];
921
+ /* There is no `canvasDndCleanups` / `canvasEventCleanups` sweep here any more, and there never
922
+ needed to be one. Both arrays were written by nothing in `src/` — the DnD and panel handlers
923
+ they were built for moved inside the canvas iframe, whose overlay owns their lifetime — so the
924
+ two loops ran over an empty array on every render and the two fields existed only to be
925
+ cleared. Six test assertions were their only producers. */
502
926
 
503
927
  // Panel JS objects are cheap — always clear and repopulate from templates.
504
928
  // The actual DOM elements are preserved by Lit's diffing on content-only re-renders.
@@ -512,24 +936,33 @@ function renderCanvasImpl() {
512
936
 
513
937
  if (modeChanged) {
514
938
  // Full teardown on mode transitions — new panel structure needed
515
- if (view.centerObserver) {
516
- view.centerObserver.disconnect();
517
- view.centerObserver = null;
939
+ if (surface.centerObserver) {
940
+ surface.centerObserver.disconnect();
941
+ surface.centerObserver = null;
518
942
  }
519
943
 
520
944
  // Destroy the grid panel if switching away from grid mode
521
- detachGridPanel();
945
+ detachGridPanel(surface.paneId);
946
+
947
+ // The Library holds an IntersectionObserver and an LRU of live runtime subtrees; leaving the
948
+ // Mode without releasing them is exactly the unbounded retention P7.1 exists to remove.
949
+ detachLibraryPane(surface.paneId);
950
+
951
+ // Same for the entry form, whose effect scope subscribes to the document's frontmatter
952
+ detachEntryPane(surface.paneId);
953
+
954
+ // Same for the Project Settings editor, which holds a registry subscription
955
+ detachSettingsPane(surface.paneId);
522
956
 
523
957
  // Dispose Monaco editor if switching away from source mode
524
- disposeSourceCollab();
525
- if (view.monacoEditor) {
526
- view.monacoEditor.getModel()?.dispose();
527
- view.monacoEditor.dispose();
528
- view.monacoEditor = null;
529
- }
958
+ disposeSourceCollab(surface);
959
+ disposeSourceEditor(surface);
530
960
 
961
+ // Same reason as `hardClearCanvasWrap`: lit is about to detach every artboard's iframe, and a
962
+ // Detached host is otherwise only noticed by whichever lazy `liveHosts` walk runs next.
963
+ releaseCanvasHosts(canvasWrap);
531
964
  litRender(nothing, canvasWrap);
532
- view.panzoomWrap = null;
965
+ surface.panzoomWrap = null;
533
966
  // Reset inline style overrides from other modes
534
967
  canvasWrap.style.padding = "";
535
968
  canvasWrap.style.alignItems = "";
@@ -547,14 +980,89 @@ function renderCanvasImpl() {
547
980
 
548
981
  // Stylebook mode: render element catalog with panzoom surface
549
982
  if (canvasMode === "stylebook") {
550
- _prevStylebookFilter = tab.session.ui.stylebookFilter || "";
551
- _prevStylebookCustomizedOnly = Boolean(tab.session.ui.stylebookCustomizedOnly);
552
- renderStylebookMode({
983
+ surface.prevStylebookFilter = shell.stylebook.filter;
984
+ surface.prevStylebookCustomizedOnly = shell.stylebook.customizedOnly;
985
+ renderStylebookMode(surface, {
553
986
  applyTransform,
554
987
  canvasPanelTemplate,
555
988
  observeCenterUntilStable,
556
989
  updateActivePanelHeaders,
557
990
  });
991
+ if (modeChanged) {
992
+ fitOnCanvasEntry(surface);
993
+ }
994
+ return;
995
+ }
996
+
997
+ /* Preview — the fidelity surface. One stage, one iframe, sized to the pane and scrolling its own
998
+ document, with no panzoom-wrap: the pan transform is precisely what stopped `position:sticky`,
999
+ scroll-driven animation and IntersectionObserver reveals from ever firing in the one mode whose
1000
+ job is to show the page as it ships. The host keeps the iframe at its CSS height for the same
1001
+ reason (see the `contentHeight` case in iframe-host.ts), and gates every editing affordance —
1002
+ hits, hover, the insertion "+", the Jx context menu and drops — off this render. */
1003
+ if (canvasMode === "preview") {
1004
+ if (modeChanged) {
1005
+ canvasWrap.style.padding = "0";
1006
+ canvasWrap.style.display = "block";
1007
+ canvasWrap.style.overflow = "hidden";
1008
+ }
1009
+ /*
1010
+ * PREVIEW IS AS WIDE AS THE CHOSEN BREAKPOINT, for the reason Edit is (see that branch).
1011
+ *
1012
+ * It filled the pane at every size: `--sm` selected, 640px in Edit, 924px here. The size
1013
+ * switcher is one per-pane setting and preview is the mode whose entire job is showing the page
1014
+ * as it will be seen — a control that changes the rendering context has to change the
1015
+ * rendering. Same expression as Edit's column, so the same page at `md` is the same page
1016
+ * whichever of the two the toggle is over.
1017
+ *
1018
+ * The HEIGHT is untouched: the frame stays the pane's height and the document scrolls itself,
1019
+ * which is what makes sticky, scroll-driven animation and IntersectionObserver fire here.
1020
+ */
1021
+ const { baseWidth: previewBase, sizeBreakpoints: previewBreakpoints } = parseMediaEntries(
1022
+ getEffectiveMedia(S.document.$media),
1023
+ );
1024
+ const previewMedia = activeMediaOfPane(surface.paneId);
1025
+ const previewWidth =
1026
+ previewBreakpoints.find((bp) => bp.name === previewMedia)?.width ?? previewBase;
1027
+ const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
1028
+ litRender(
1029
+ html`<div class="preview-stage" style="max-width:${previewWidth}px">${panelTpl}</div>`,
1030
+ canvasWrap,
1031
+ );
1032
+ canvasPanels.push(panel as unknown as CanvasPanel);
1033
+ renderCanvasIntoPanel(surface, panel as unknown as CanvasPanel, S.ui.featureToggles);
1034
+ return;
1035
+ }
1036
+
1037
+ /* Settings mode: the Project Settings document (plan §9.3). Same non-iframe-editor pattern as
1038
+ grid and stylebook — the panel owns its own reactivity from here — and the reason the seven
1039
+ settings screenshots now crop `pane.primary` instead of `overlay.dialog:settings` without one
1040
+ manifest step changing. */
1041
+ if (canvasMode === "settings") {
1042
+ canvasWrap.style.padding = "0";
1043
+ canvasWrap.style.display = "block";
1044
+ renderSettingsPane(surface);
1045
+ return;
1046
+ }
1047
+
1048
+ /* Library mode: the project's own contents, over a GridSource, in one of five layouts
1049
+ (`browse/library-pane.ts`). `commands/context.ts` already mapped `manage` to the `library`
1050
+ editor kind — this is the surface that map was pointing at. */
1051
+ if (canvasMode === "manage") {
1052
+ canvasWrap.style.padding = "0";
1053
+ canvasWrap.style.display = "block";
1054
+ renderLibraryMode(surface, tab);
1055
+ return;
1056
+ }
1057
+
1058
+ /* Entry mode: one content entry's fields, typed by its collection's schema
1059
+ (`content/entry-editor.ts`). Same non-iframe-editor pattern as the Library and Settings — and
1060
+ the reason `entry` is in `commands/context.ts`'s mode map, without which a form over a
1061
+ frontmatter record would have resolved into the CANVAS key scope. */
1062
+ if (canvasMode === "entry") {
1063
+ canvasWrap.style.padding = "0";
1064
+ canvasWrap.style.display = "block";
1065
+ renderEntryMode(surface, tab);
558
1066
  return;
559
1067
  }
560
1068
 
@@ -563,7 +1071,7 @@ function renderCanvasImpl() {
563
1071
  if (canvasMode === "grid") {
564
1072
  canvasWrap.style.padding = "0";
565
1073
  canvasWrap.style.display = "block";
566
- renderGridMode(canvasWrap, tab);
1074
+ renderGridMode(surface, tab);
567
1075
  return;
568
1076
  }
569
1077
 
@@ -588,15 +1096,30 @@ function renderCanvasImpl() {
588
1096
 
589
1097
  const filePath = tab.documentPath || "document.json";
590
1098
  const lang = sourceLang(tab);
591
- void mountSourceEditor(tab, editorContainer as unknown as Element, filePath, lang);
1099
+ void mountSourceEditor(tab, surface, editorContainer as unknown as Element, filePath, lang);
592
1100
  return;
593
1101
  }
594
1102
 
595
1103
  // Git diff mode — render original (left) and current (right) side-by-side on panzoom surface
596
1104
  if (canvasMode === "git-diff") {
597
- if (!ctx.gitDiffState) {
598
- ctx.setCanvasMode("design");
599
- renderCanvas();
1105
+ /* THIS PANE's diff. `shell.git.diffState` is one app-level slot, so with a diff lens open
1106
+ beside a page the two stages would draw the same comparison — and changing the primary's
1107
+ mode would clear the diff the side pane is still showing. A diff lens carries its own. */
1108
+ const paneDiff = derivationOfPane(surface.paneId);
1109
+ const lensDiff = paneDiff?.kind === "lens" && paneDiff.preset === "diff";
1110
+ const gitDiffState = lensDiff ? paneDiff.diff : ctx.gitDiffState;
1111
+ if (!gitDiffState) {
1112
+ /* A LENS NEVER TAKES THE FALLBACK. `setCanvasMode(tab, "design")` writes the SOURCE pane's
1113
+ tab — the one thing a lens must not do — so a diff lens whose comparison had not landed
1114
+ yet flipped the document the author was editing out of whatever mode they had it in. Its
1115
+ own comparison is still loading (or could not be read): say so on this stage and leave the
1116
+ tab alone. */
1117
+ if (lensDiff) {
1118
+ renderDerivationNotice(surface, paneDiff.reason || "Loading this file's changes…");
1119
+ return;
1120
+ }
1121
+ ctx.setCanvasMode(tab, "design");
1122
+ renderCanvas(surface.paneId);
600
1123
  return;
601
1124
  }
602
1125
 
@@ -605,7 +1128,6 @@ function renderCanvasImpl() {
605
1128
  canvasWrap.style.overflow = "hidden";
606
1129
  }
607
1130
 
608
- const { gitDiffState } = ctx;
609
1131
  const panelWidth = 800;
610
1132
 
611
1133
  const { tpl: origTpl, panel: origPanel } = canvasPanelTemplate(
@@ -628,7 +1150,7 @@ function renderCanvasImpl() {
628
1150
  style="transform-origin:0 0"
629
1151
  ${ref((el) => {
630
1152
  if (el) {
631
- view.panzoomWrap = el as HTMLDivElement;
1153
+ surface.panzoomWrap = el as HTMLDivElement;
632
1154
  }
633
1155
  })}
634
1156
  >
@@ -659,27 +1181,34 @@ function renderCanvasImpl() {
659
1181
  };
660
1182
 
661
1183
  const { featureToggles } = S.ui;
1184
+ // Both sides mount after an await, so the pass's generation is captured now — see the note on
1185
+ // `passGen` at the design-mode artboard loop.
1186
+ const diffGen = surface.renderGeneration;
662
1187
  void Promise.all([
663
1188
  parseContent(gitDiffState.originalContent || ""),
664
1189
  parseContent(gitDiffState.currentContent || ""),
665
1190
  ]).then(([originalDoc, currentDoc]) => {
666
1191
  renderCanvasIntoPanel(
1192
+ surface,
667
1193
  origPanel as unknown as CanvasPanel,
668
1194
  featureToggles,
669
1195
  originalDoc,
670
1196
  gitDiffState,
1197
+ diffGen,
671
1198
  );
672
1199
  renderCanvasIntoPanel(
1200
+ surface,
673
1201
  currPanel as unknown as CanvasPanel,
674
1202
  featureToggles,
675
1203
  currentDoc,
676
1204
  gitDiffState,
1205
+ diffGen,
677
1206
  );
678
1207
  });
679
1208
 
680
- applyTransform();
1209
+ applyTransform(surface);
681
1210
  if (modeChanged) {
682
- observeCenterUntilStable();
1211
+ observeCenterUntilStable(surface);
683
1212
  }
684
1213
  return;
685
1214
  }
@@ -692,7 +1221,23 @@ function renderCanvasImpl() {
692
1221
  canvasWrap.style.overflow = "hidden";
693
1222
  }
694
1223
 
695
- const { baseWidth } = parseMediaEntries(getEffectiveMedia(S.document.$media));
1224
+ /*
1225
+ * THE COLUMN IS AS WIDE AS THE CHOSEN BREAKPOINT.
1226
+ *
1227
+ * It was always `baseWidth`, so the Context bar's size switcher did nothing here: it wrote
1228
+ * `session.ui.activeMedia`, Design used that to highlight one of the artboards it already draws
1229
+ * side by side, and Edit — which draws ONE column — ignored it. A control that changes the
1230
+ * rendering context has to change the rendering, or it is a control over a label.
1231
+ *
1232
+ * The width is the artboard width Design would give that breakpoint, so the same page at `md`
1233
+ * is the same page in both modes; the iframe is that wide, so the document's own media queries
1234
+ * evaluate against it and the content reflows for real rather than being scaled.
1235
+ */
1236
+ const { baseWidth, sizeBreakpoints: editBreakpoints } = parseMediaEntries(
1237
+ getEffectiveMedia(S.document.$media),
1238
+ );
1239
+ const editMedia = activeMediaOfPane(surface.paneId);
1240
+ const columnWidth = editBreakpoints.find((bp) => bp.name === editMedia)?.width ?? baseWidth;
696
1241
  const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
697
1242
  // A component-definition doc (root tag is a custom element) is a fragment, not a page: it should
698
1243
  // Hug its content rather than have the column fill+stretch to the viewport (dead scroll space).
@@ -706,16 +1251,18 @@ function renderCanvasImpl() {
706
1251
  panel.scrollContainer = (el as HTMLElement) || null;
707
1252
  })}
708
1253
  >
709
- <div class=${columnClass} style="max-width:${baseWidth}px">${panelTpl}</div>
1254
+ <div class=${columnClass} style="max-width:${columnWidth}px">
1255
+ ${wantsDocHeader ? docHeaderSlot("in-column", surface.paneId) : nothing}${panelTpl}
1256
+ </div>
710
1257
  </div>
711
1258
  `;
712
1259
  litRender(editTpl, canvasWrap);
713
1260
  canvasPanels.push(panel as unknown as CanvasPanel);
714
- renderCanvasIntoPanel(panel as unknown as CanvasPanel, S.ui.featureToggles);
1261
+ renderCanvasIntoPanel(surface, panel as unknown as CanvasPanel, S.ui.featureToggles);
715
1262
  // The column must exist in the DOM before the zoom's live width measurement — so the zoom is
716
1263
  // Applied after the render rather than baked into the template (the panel mounts fluid and is
717
1264
  // Immediately re-fitted; the iframe hasn't painted yet, so nothing visibly jumps).
718
- applyEditZoom();
1265
+ applyEditZoom(surface);
719
1266
  return;
720
1267
  }
721
1268
 
@@ -724,6 +1271,13 @@ function renderCanvasImpl() {
724
1271
  canvasWrap.style.padding = "0";
725
1272
  canvasWrap.style.overflow = "hidden";
726
1273
  }
1274
+ /* The stage stacks when it carries the card: header first, artboards below. Set on every render
1275
+ rather than on the transition, because whether a document HAS a header changes with the tab and
1276
+ not with the mode. `editors.ts` and `formula-workspace.ts` already claim the column this way —
1277
+ `#canvas-wrap` is a row by default and each surface states its own axis. */
1278
+ canvasWrap.style.flexDirection = wantsDocHeader ? "column" : "";
1279
+ canvasWrap.style.alignItems = wantsDocHeader ? "stretch" : "";
1280
+ const designHeaderTpl = wantsDocHeader ? docHeaderSlot("pinned", surface.paneId) : nothing;
727
1281
 
728
1282
  const {
729
1283
  sizeBreakpoints,
@@ -747,12 +1301,13 @@ function renderCanvasImpl() {
747
1301
  );
748
1302
  litRender(
749
1303
  html`
1304
+ ${designHeaderTpl}
750
1305
  <div
751
1306
  class="panzoom-wrap"
752
1307
  style="transform-origin:0 0"
753
1308
  ${ref((el) => {
754
1309
  if (el) {
755
- view.panzoomWrap = el as HTMLDivElement;
1310
+ surface.panzoomWrap = el as HTMLDivElement;
756
1311
  }
757
1312
  })}
758
1313
  >
@@ -762,10 +1317,14 @@ function renderCanvasImpl() {
762
1317
  canvasWrap,
763
1318
  );
764
1319
  canvasPanels.push(panel as unknown as CanvasPanel);
765
- renderCanvasIntoPanel(panel as unknown as CanvasPanel, featureToggles);
766
- applyTransform();
1320
+ renderCanvasIntoPanel(surface, panel as unknown as CanvasPanel, featureToggles);
1321
+ applyTransform(surface);
767
1322
  if (modeChanged) {
768
- observeCenterUntilStable();
1323
+ // Fit BEFORE centering: the fit picks the zoom, centerCanvas then places the artboard at that
1324
+ // Zoom (and top-aligns it, which beats fit's vertical centering for a page taller than the
1325
+ // Viewport).
1326
+ fitOnCanvasEntry(surface);
1327
+ observeCenterUntilStable(surface);
769
1328
  }
770
1329
  return;
771
1330
  }
@@ -787,7 +1346,20 @@ function renderCanvasImpl() {
787
1346
  });
788
1347
  }
789
1348
 
790
- const panelEntries = allPanelDefs.map((def) => {
1349
+ /* A BREAKPOINT LENS is one artboard, not a second copy of the design board.
1350
+ Design mode already draws every declared breakpoint side by side on one panzoom surface, so
1351
+ "the same page at two breakpoints" was never a rendering problem — it was that a tab lives in
1352
+ one pane. The lens draws exactly the one artboard it names, filling its own pane at its own
1353
+ zoom; the fallback to the whole set is for a media the document has since stopped declaring,
1354
+ which `derivedTarget` also reports as `unavailable`. */
1355
+ const lens = derivationOfPane(surface.paneId);
1356
+ const lensMedia =
1357
+ lens?.kind === "lens" && lens.preset === "breakpoint" ? (lens.media ?? "base") : null;
1358
+ const chosen =
1359
+ lensMedia === null ? allPanelDefs : allPanelDefs.filter((d) => d.name === lensMedia);
1360
+ const panelDefs = chosen.length > 0 ? chosen : allPanelDefs;
1361
+
1362
+ const panelEntries = panelDefs.map((def) => {
791
1363
  const label = `${def.displayName} (${def.width}px)`;
792
1364
  const { tpl, panel } = canvasPanelTemplate(def.name, label, false, def.width);
793
1365
  return { panel, tpl };
@@ -795,12 +1367,13 @@ function renderCanvasImpl() {
795
1367
 
796
1368
  litRender(
797
1369
  html`
1370
+ ${designHeaderTpl}
798
1371
  <div
799
1372
  class="panzoom-wrap"
800
1373
  style="transform-origin:0 0"
801
1374
  ${ref((el) => {
802
1375
  if (el) {
803
- view.panzoomWrap = el as HTMLDivElement;
1376
+ surface.panzoomWrap = el as HTMLDivElement;
804
1377
  }
805
1378
  })}
806
1379
  >
@@ -810,24 +1383,35 @@ function renderCanvasImpl() {
810
1383
  canvasWrap,
811
1384
  );
812
1385
 
1386
+ /* Every artboard of this pass mounts under the generation the pass opened with, captured HERE
1387
+ rather than read inside each deferred callback. Two things depend on it. The iframe drops a
1388
+ render whose gen is older than the one it last accepted, so a pass that has been superseded
1389
+ while its artboards were still queued is discarded by the frame instead of posting a duplicate
1390
+ render under the NEW pass's number. And the host resolves the document once per generation
1391
+ (`preparePassRender`), which only fans out if the pass has one. */
1392
+ const passGen = surface.renderGeneration;
813
1393
  for (let i = 0; i < panelEntries.length; i++) {
814
1394
  const { panel } = panelEntries[i]!;
815
1395
  const p = panel as CanvasPanel;
816
1396
  canvasPanels.push(p);
817
1397
  if (i === 0) {
818
- renderCanvasIntoPanel(p, featureToggles);
1398
+ renderCanvasIntoPanel(surface, p, featureToggles);
819
1399
  } else {
820
- setTimeout(() => renderCanvasIntoPanel(p, featureToggles), 0);
1400
+ // Yield between artboards so the first one paints before the rest mount. They no longer pay
1401
+ // For the document — the pass already resolved it — but each is still a live iframe render.
1402
+ setTimeout(() => renderCanvasIntoPanel(surface, p, featureToggles, null, null, passGen), 0);
821
1403
  }
822
1404
  }
823
1405
 
824
- // Highlight active panel header
825
- updateActivePanelHeaders();
1406
+ // Highlight active panel header — this pass's stage, not the focused pane's.
1407
+ updateActivePanelHeaders(surface);
826
1408
 
827
1409
  // Apply current zoom + pan transform
828
- applyTransform();
1410
+ applyTransform(surface);
829
1411
  if (modeChanged) {
830
- observeCenterUntilStable();
1412
+ // See the single-panel path: fit picks the zoom, centerCanvas then places the artboards.
1413
+ fitOnCanvasEntry(surface);
1414
+ observeCenterUntilStable(surface);
831
1415
  }
832
1416
  }
833
1417
 
@@ -837,6 +1421,8 @@ function renderCanvasImpl() {
837
1421
  * `data-jx-path`/`data-jx-layout`, and draws its own overlays). Git-diff/preview overrides render
838
1422
  * but stay un-patchable (`ready` only flips for the real tab document).
839
1423
  *
1424
+ * @param {CanvasSurface} surface - The pane's stage this panel belongs to. The document rendered
1425
+ * and the preview flag both come from THAT pane, never from "the active tab".
840
1426
  * @param {CanvasPanel} panel
841
1427
  * @param {Record<string, boolean>} _featureToggles - Accepted for call-site symmetry (the iframe
842
1428
  * render needs no structural-preview fallback); unused.
@@ -844,21 +1430,33 @@ function renderCanvasImpl() {
844
1430
  * active tab doc if not provided.
845
1431
  * @param {GitDiffState | null} [_gitDiffState] - Accepted for call-site symmetry; the iframe path
846
1432
  * does not apply parent-side diff highlighting.
1433
+ * @param {number | null} [passGen] - The generation of the render pass this panel belongs to.
1434
+ * Passed explicitly by a DEFERRED mount, whose callback would otherwise read whatever generation
1435
+ * is current when the timer fires — a later pass's number, stamped on an earlier pass's
1436
+ * artboard.
847
1437
  */
848
1438
  function renderCanvasIntoPanel(
1439
+ surface: CanvasSurface,
849
1440
  panel: CanvasPanel,
850
1441
  _featureToggles: Record<string, boolean>,
851
1442
  docOverride: JxMutableNode | null = null,
852
1443
  _gitDiffState: GitDiffState | null = null,
1444
+ passGen: number | null = null,
853
1445
  ) {
854
- const gen = view.renderGeneration;
855
- const tab = activeTab.value;
1446
+ const gen = passGen ?? surface.renderGeneration;
1447
+ const tab = tabOfPane(surface.paneId);
856
1448
  const docToRender = docOverride || (tab?.doc.document as JxMutableNode);
857
1449
  const canvas = panel.canvas as HTMLElement;
858
1450
 
859
1451
  canvasPerf.panelRenders += 1;
860
1452
  panel.ready = false;
861
1453
 
1454
+ // The host's frame box depends on whether this is a preview render, and mountIframeCanvas only
1455
+ // Learns that after it has awaited the resolved document — one await during which the iframe can
1456
+ // Post a contentHeight that the OUTGOING mode would answer. Declare it here, before the await,
1457
+ // Where it is already known: this is the mode whose surface was just built.
1458
+ adoptCanvasPreviewMode(canvas, canvasModeOfPane(surface.paneId) === "preview");
1459
+
862
1460
  // Overrides (git-diff docs) mount with a null tab identity: their iframes must never route doc
863
1461
  // Mutations anywhere. The real doc carries its tab id so edit/drop messages route to THAT tab.
864
1462
  void timeSpanAsync(SPAN_MOUNT_CANVAS, () =>
@@ -868,23 +1466,297 @@ function renderCanvasIntoPanel(
868
1466
  canvas,
869
1467
  panel._width,
870
1468
  docOverride ? null : (tab?.id ?? null),
1469
+ // The VIEW tab, which an override does not null out: a git-diff frame routes no mutations
1470
+ // But is still a view of this pane's document, and its docBase, layout toggle and mode are
1471
+ // That tab's. Explicit rather than left to the default, because this line already knows.
1472
+ tab,
871
1473
  ),
872
1474
  )
873
1475
  .then(() => {
874
- if (gen === view.renderGeneration) {
1476
+ if (gen === surface.renderGeneration) {
875
1477
  // Mark the panel patchable once the real document is mounted (not a diff/preview override)
876
1478
  // So classifyOps admits surgical patches; the iframe holds the render context, so this
877
1479
  // Panel needs no parent-side render scope.
878
1480
  panel.ready = !docOverride;
879
- updateCanvas({ error: null, scope: null, status: "ready" });
880
- statusMessage("Iframe render OK", 1500);
1481
+ updateCanvas(tab, { error: null, scope: null, status: "ready" });
1482
+ // A successful render used to announce itself as "Iframe render OK" — a debug string
1483
+ // Shipped to end users and legible in a published docs screenshot. A render that worked is
1484
+ // The canvas you are looking at; it says so itself.
881
1485
  }
882
1486
  })
883
1487
  .catch((error: unknown) => {
884
- console.warn("mountIframeCanvas failed:", error instanceof Error ? error.message : error);
1488
+ const detail = error instanceof Error ? error.message : String(error);
1489
+ notify.error("The canvas could not be mounted.", {
1490
+ detail,
1491
+ key: "canvas.mount",
1492
+ source: "Canvas",
1493
+ });
885
1494
  });
886
1495
  }
887
1496
 
1497
+ /*
1498
+ * How a pane's surface releases what THIS module created.
1499
+ *
1500
+ * Registered by injection rather than imported, because `canvas-surface.ts` sits below this file in
1501
+ * the graph and must stay there: it is imported by `store.ts`, the patcher and the iframe host.
1502
+ * Same idiom as `setIframePatchEscalation`. Without it a pane removed from the grid would drop its
1503
+ * record while its Monaco, its collab lock and its centering observer stayed alive.
1504
+ */
1505
+ setSurfaceTeardown((surface) => {
1506
+ detachGridPanel(surface.paneId);
1507
+ detachLibraryPane(surface.paneId);
1508
+ detachEntryPane(surface.paneId);
1509
+ detachSettingsPane(surface.paneId);
1510
+ attachDocumentHeaderHost(surface.paneId, null);
1511
+ disposeSourceCollab(surface);
1512
+ disposeSourceEditor(surface);
1513
+ surface.centerObserver?.disconnect();
1514
+ surface.centerObserver = null;
1515
+ surface.panzoomWrap = null;
1516
+ _docHeaderRefs.delete(surface.paneId);
1517
+ });
1518
+
888
1519
  export function renderOverlays() {
889
1520
  overlaysPanel.render();
890
1521
  }
1522
+
1523
+ // ─── Commands ─────────────────────────────────────────────────────────────────
1524
+
1525
+ /** The root identifier a merge-tag token binds to — `state`, `item` or `index`, or `""`. */
1526
+ function tokenRoot(token: string): string {
1527
+ return /^[A-Za-z_$][\w$]*/.exec(token)?.[0] ?? "";
1528
+ }
1529
+
1530
+ /**
1531
+ * Refuse a `${…}` token the open document cannot resolve, naming what it could have been.
1532
+ *
1533
+ * The same rule `data.expandRow` applies to a row name, for the same reason: an unresolvable token
1534
+ * renders as EMPTY TEXT, so inserting one silently produces a document that looks bound and is not
1535
+ * — and a capture of it documents a binding that never existed.
1536
+ */
1537
+ function refuseUnresolvableToken(token: string): void {
1538
+ const tab = activeTab.value;
1539
+ if (!tab) {
1540
+ throw new RangeError(`command "insert.data" needs an open document; no tab is active`);
1541
+ }
1542
+ const root = tokenRoot(token);
1543
+ const state = (tab.doc.document.state ?? {}) as Record<string, unknown>;
1544
+ if (root === "state") {
1545
+ const name = token.split(".")[1] ?? "";
1546
+ if (!(name in state)) {
1547
+ const defined = Object.keys(state);
1548
+ throw new RangeError(
1549
+ `command "insert.data" argument "token": "${token}" names no state entry — this ` +
1550
+ `document defines: ${defined.length > 0 ? defined.join(", ") : "nothing"}`,
1551
+ );
1552
+ }
1553
+ return;
1554
+ }
1555
+ if (root === "item" || root === "index") {
1556
+ // The repeater scope exists only INSIDE a `map` template, and the caret's own doc path is what
1557
+ // Says whether it does — the same question `panels/block-action-bar.ts` asks before it offers
1558
+ // These tokens at all.
1559
+ const path = getEditSnapshot().snapshot?.path ?? primarySelection(tab.session.selection);
1560
+ if (!findEnclosingRepeater(tab.doc.document, path)) {
1561
+ throw new RangeError(
1562
+ `command "insert.data" argument "token": "${token}" is a repeater-scope token, and the ` +
1563
+ `caret is not inside a repeater template`,
1564
+ );
1565
+ }
1566
+ return;
1567
+ }
1568
+ throw new RangeError(
1569
+ `command "insert.data" argument "token": "${token}" is not an insertable token — a token ` +
1570
+ `reads "state.<name>", "item", "item.<field>" or "index"`,
1571
+ );
1572
+ }
1573
+
1574
+ /**
1575
+ * The two verbs that address what the canvas pane is pointing AT — the element, and the caret.
1576
+ *
1577
+ * Both are defined beside the surface that DRAWS them rather than beside the fields that store
1578
+ * them: every consumer of `session.selection` — the overlay boxes, the Outline's selected row, the
1579
+ * Inspector, the block action bar — is downstream of this module's render, and both verbs must
1580
+ * guarantee that what they accept is something those surfaces can actually draw.
1581
+ *
1582
+ * `selection.set`: `path: []` is the document ROOT and is a legal selection (`selector-menu-shot`
1583
+ * uses it). `null` clears the selection. Anything else is resolved against the open document and
1584
+ * REFUSED when it addresses nothing — a stale path used to select a hole, and every panel
1585
+ * downstream then rendered its empty state while the shot recorded that as the truth.
1586
+ *
1587
+ * `insert.data`: the capability has shipped since the merge-tag menu landed and has been reachable
1588
+ * from that menu ALONE — `panels/block-action-bar.ts` posts an `insertData` intent straight to the
1589
+ * canvas bridge, so the palette, the keyboard, the AI and `__jxAutomation` could not do the one
1590
+ * thing the whole Insert-data affordance exists for. The screenshot manifest reached it by clicking
1591
+ * the toolbar button and then a row of the menu it opens, which is the shape §13.5 refuses on
1592
+ * principle: opening a menu to press an item names a CONTROL; the item is the command.
1593
+ */
1594
+ export function selectionCommands(): AnyCommand[] {
1595
+ return [
1596
+ {
1597
+ args: argsSchema({
1598
+ path: pathProperty(
1599
+ "The document path to select — an array of keys and indexes. [] is the document " +
1600
+ "root; null clears the selection.",
1601
+ true,
1602
+ ),
1603
+ }),
1604
+ category: "Selection",
1605
+ id: "selection.set",
1606
+ level: "document",
1607
+ menus: ["palette"],
1608
+ group: "2_navigate",
1609
+ requires: "an open document",
1610
+ when: (ctx) => ctx.document.open,
1611
+ aiTool: {
1612
+ description:
1613
+ "Select the element at a document path so the Inspector, the Outline and the canvas " +
1614
+ "overlay all address it. Pass null to clear the selection.",
1615
+ name: "select_node",
1616
+ },
1617
+ run: (_commandCtx, args) => {
1618
+ const path = nullablePathArg("selection.set", args, "path");
1619
+ const tab = activeTab.value;
1620
+ if (!tab) {
1621
+ throw new RangeError(`command "selection.set" needs an open document; no tab is active`);
1622
+ }
1623
+ if (path !== null && path.length > 0 && !getNodeAtPath(tab.doc.document, path)) {
1624
+ throw new RangeError(
1625
+ `command "selection.set" argument "path": [${path.join(", ")}] addresses no node in ` +
1626
+ `${tab.documentPath ?? "the open document"}`,
1627
+ );
1628
+ }
1629
+ tab.session.selection = path === null ? [] : [path];
1630
+ },
1631
+ title: "Select Element",
1632
+ },
1633
+ {
1634
+ args: argsSchema({
1635
+ paths: pathListProperty(
1636
+ "The document paths to select, in order — an array of paths. The LAST one becomes the " +
1637
+ "primary (what the Inspector edits); the first is the anchor a shift-range extends " +
1638
+ "from. [] selects nothing.",
1639
+ ),
1640
+ }),
1641
+ category: "Selection",
1642
+ id: "selection.setPaths",
1643
+ level: "document",
1644
+ menus: ["palette"],
1645
+ group: "2_navigate",
1646
+ requires: "an open document",
1647
+ when: (ctx) => ctx.document.open,
1648
+ aiTool: {
1649
+ description:
1650
+ "Select several elements at once so one decision — a style paste, a delete, a duplicate " +
1651
+ "— applies to all of them as a single undoable step. Pass [] to select nothing.",
1652
+ name: "select_nodes",
1653
+ },
1654
+ /**
1655
+ * The idempotent SET for the whole selection, beside `selection.set`'s single path.
1656
+ *
1657
+ * There is deliberately no `selection.add` or `selection.toggle`: an accumulate verb names a
1658
+ * DELTA against state the caller cannot observe, which is the same objection that bans a
1659
+ * `toggle*` id without a `set*` beside it (§13). Ctrl-clicking twice is a gesture; the
1660
+ * command is always "the selection is now exactly these". `selection.set { path }` survives
1661
+ * unchanged because selecting one node is what almost every caller — every screenshot step,
1662
+ * and the assistant — actually does.
1663
+ */
1664
+ run: (_commandCtx, args) => {
1665
+ const paths = pathListArg("selection.setPaths", args, "paths");
1666
+ const tab = activeTab.value;
1667
+ if (!tab) {
1668
+ throw new RangeError(
1669
+ `command "selection.setPaths" needs an open document; no tab is active`,
1670
+ );
1671
+ }
1672
+ for (const path of paths) {
1673
+ if (path.length > 0 && !getNodeAtPath(tab.doc.document, path)) {
1674
+ throw new RangeError(
1675
+ `command "selection.setPaths" argument "paths": [${path.join(", ")}] addresses no ` +
1676
+ `node in ${tab.documentPath ?? "the open document"}`,
1677
+ );
1678
+ }
1679
+ }
1680
+ // Deduplicated, so running the same list twice lands on the same anchor and the same
1681
+ // Primary — the property that makes it photographable.
1682
+ tab.session.selection = uniquePaths(paths);
1683
+ },
1684
+ title: "Select Elements",
1685
+ },
1686
+ {
1687
+ args: argsSchema({
1688
+ token: stringProperty(
1689
+ 'The token to insert, without the ${…} wrapper — "state.<name>", "item", ' +
1690
+ '"item.<field>" or "index".',
1691
+ ),
1692
+ }),
1693
+ category: "Insert",
1694
+ id: "insert.data",
1695
+ level: "selection",
1696
+ keyScope: "caret",
1697
+ // NOT `blockbar`: the bar's Insert-data control opens a PICKER, and a picker cannot be the
1698
+ // Rendering of a record that requires a token. The record is what the picker's rows run.
1699
+ menus: ["palette"],
1700
+ group: "5_data",
1701
+ undo: "document",
1702
+ requires: "a live text caret in the canvas",
1703
+ /* `inCanvas`, not `active`. `caret.active` is also true while focus is in a parent-realm text
1704
+ field (`commands/live-context.ts` folds both, because the SCOPE STACK wants them folded),
1705
+ and inserting a data token into the Inspector's href field is not a thing this verb can do
1706
+ — `postApplyFormat` would no-op against a frame with no session and the palette row would
1707
+ read as available. The `format.*` family is gated the same way, for the same reason. */
1708
+ when: (ctx) => ctx.caret.inCanvas,
1709
+ aiTool: {
1710
+ description:
1711
+ "Insert a live data placeholder at the text caret, binding that run of text to a state " +
1712
+ "entry or, inside a repeater template, to the current item.",
1713
+ name: "insert_data_token",
1714
+ },
1715
+ run: (_commandCtx, args) => {
1716
+ const token = stringArg("insert.data", args, "token");
1717
+ refuseUnresolvableToken(token);
1718
+ postApplyFormat({ command: "insertData", token });
1719
+ },
1720
+ title: "Insert Data",
1721
+ },
1722
+ {
1723
+ category: "Insert",
1724
+ /**
1725
+ * The slash menu's door, and for two phases its ONLY working one.
1726
+ *
1727
+ * "/" at the caret is the gesture the docs describe and §10 documents, and it was recognised
1728
+ * in a `keydown` listener bound to the BLOCK — while the editing host is the canvas
1729
+ * container, so the listener never fired and typing "/" simply inserted a slash. The gesture
1730
+ * is fixed where it belongs (`canvas/iframe-inline-edit.ts` listens at the host), and this
1731
+ * record is the half that gesture never had: a name, so the palette, a rebound chord, the
1732
+ * automation runner and the screenshot manifest can all reach the same menu.
1733
+ *
1734
+ * Opened this way the menu has no "/" in the document to filter against, so the frame gives
1735
+ * it its own filter field and selecting a block deletes nothing — see `openSlashMenu`.
1736
+ */
1737
+ id: "insert.openSlashMenu",
1738
+ level: "selection",
1739
+ keyScope: "caret",
1740
+ // No `blockbar`: the bar already carries the tag badge, which opens the same list for a
1741
+ // CONVERSION. A second control for "insert" beside it would be two buttons over one menu.
1742
+ menus: ["palette"],
1743
+ group: "5_data",
1744
+ undo: "document",
1745
+ requires: "a live text caret in the canvas",
1746
+ when: (ctx) => ctx.caret.inCanvas,
1747
+ run: () => {
1748
+ postOpenSlash();
1749
+ },
1750
+ title: "Insert Block…",
1751
+ },
1752
+ ];
1753
+ }
1754
+
1755
+ /**
1756
+ * Register the canvas pane's addressing verbs. Called from the bootstrap.
1757
+ *
1758
+ * @param {CommandRegistry} registry
1759
+ */
1760
+ export function registerSelectionSetCommand(registry: CommandRegistry): void {
1761
+ registry.registerAll(selectionCommands());
1762
+ }