@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
package/src/studio.ts CHANGED
@@ -12,11 +12,9 @@
12
12
  * registration) on first use by source mode, the function editor, or the formula workspace.
13
13
  */
14
14
  import { errorMessage } from "@jxsuite/schema/parse";
15
+ import { getPanel } from "./panels/panel-registry";
15
16
 
16
17
  import {
17
- canvasWrap,
18
- chatPanelEl,
19
- getNodeAtPath,
20
18
  initShellRefs,
21
19
  projectState,
22
20
  registerRenderer,
@@ -25,36 +23,62 @@ import {
25
23
  setProjectState,
26
24
  toolbarEl,
27
25
  updateSession,
28
- updateUi,
29
26
  } from "./store";
30
27
 
31
28
  import {
29
+ PRIMARY_PANE,
32
30
  activeTab,
33
31
  closeAllTabs,
34
32
  openTab,
33
+ paneById,
34
+ receivingPane,
35
+ registerTabCommands,
35
36
  setWorkspaceProject,
36
37
  workspace,
37
38
  } from "./workspace/workspace";
38
- import { mutateUpdateDef, mutateUpdateProperty, transactDoc } from "./tabs/transact";
39
- import { effect } from "./reactivity";
39
+ import { derivationCommands, installDerivationEffects } from "./workspace/pane-derive";
40
+ import type { DerivationDeps } from "./workspace/pane-derive";
41
+ import { effect, effectScope } from "./reactivity";
42
+ import type { EffectScope } from "@vue/reactivity";
40
43
 
41
- import { view } from "./view";
44
+ import {
45
+ flushSession,
46
+ markSessionRestored,
47
+ mountShell,
48
+ registerShellViewCommands,
49
+ resetProjectShell,
50
+ setActivityTab,
51
+ shell,
52
+ } from "./shell";
42
53
 
43
54
  import { isEditing } from "./editor/inline-edit";
44
- import { applyTransform, initCanvasUtils } from "./canvas/canvas-utils";
55
+ import { applyTransform, registerCanvasViewCommands } from "./canvas/canvas-utils";
56
+ import type { CanvasSurface } from "./canvas/canvas-surface";
45
57
  import {
46
58
  initCanvasRender,
59
+ registerSelectionSetCommand,
47
60
  renderCanvas,
48
61
  renderOverlays,
49
62
  scheduleCanvasRender,
50
63
  } from "./canvas/canvas-render";
64
+ import {
65
+ canvasModeOfPane,
66
+ canvasModeOfTab,
67
+ stageContaining,
68
+ surfaceForPane,
69
+ tabOfPane,
70
+ } from "./canvas/canvas-surface";
51
71
  import { consumePatchedDocument, initCanvasPatcher } from "./canvas/canvas-patcher";
52
72
  import {
73
+ setKeymapSource,
53
74
  commitActiveEditSession,
54
75
  allowAutoRequestsOnNextRender,
55
76
  getEditSnapshot,
77
+ isCaretActive,
56
78
  postColorSchemeToLiveHosts,
79
+ postLocaleToLiveHosts,
57
80
  setCanvasContextMenuHandler,
81
+ setCanvasPointerDownHandler,
58
82
  setCanvasSlashHandler,
59
83
  setIframePatchEscalation,
60
84
  setFileDropHandler,
@@ -65,13 +89,11 @@ import {
65
89
  import { runInsertZoneAction } from "./editor/insert-zone-action";
66
90
  import { canvasSlashHandler } from "./editor/canvas-slash-bridge";
67
91
  import { makeCanvasContextMenuHandler } from "./editor/canvas-context-menu";
68
- import { initCanvasLiveRender } from "./canvas/canvas-live-render";
69
- import {
70
- mountStatusbar,
71
- renderStatusbar,
72
- setStatusbarRenderer,
73
- statusMessage,
74
- } from "./panels/statusbar";
92
+ import { mountStatusbar, renderStatusbar } from "./panels/statusbar";
93
+ import { mountJumpBar } from "./panels/jump-bar";
94
+ import { cellForPane } from "./panels/pane-grid";
95
+ import { notify } from "./services/notify";
96
+ import { beginActivity } from "./panels/activity-panel";
75
97
  import { exportFile, parseSourceForPath, saveFile, serializeDocument } from "./files/file-ops";
76
98
  import {
77
99
  formatForPath,
@@ -85,37 +107,41 @@ import {
85
107
  renderFilesTemplate as _renderFilesTemplate,
86
108
  findHomePage,
87
109
  loadDirectory,
110
+ openFileInPane,
88
111
  openFileInTab,
89
- openHomePage,
112
+ openLastSessionOrHome,
90
113
  registerFileTreeDnD,
91
114
  reloadCleanTab,
92
115
  setupTreeKeyboard,
93
116
  } from "./files/files";
94
117
  import { startFsSync } from "./files/fs-events";
118
+ import { invalidateParamValues } from "./page-params";
95
119
  import {
96
120
  configureCollabNotifier,
97
121
  configureCollabParser,
98
122
  configureCollabSerializer,
99
123
  } from "./collab/collab-session";
100
124
  import { renderImportsTemplate } from "./panels/imports-panel";
101
- import { renderHeadTemplate } from "./panels/head-panel";
125
+ import { invalidateLayoutPickerCache, renderHeadTemplate } from "./panels/head-panel";
102
126
  import { exportCemManifest as _exportCemManifest } from "./services/cem-export";
103
127
  import { installAutomationHook } from "./services/automation";
104
- import { openBrowseModal } from "./browse/browse-modal";
105
- import { invalidateBrowseCache } from "./browse/browse";
128
+ import { invalidateLibrary } from "./browse/library-pane";
106
129
  import { invalidateMediaCache } from "./ui/media-picker";
107
130
  import { setMediaChangedHandler } from "./files/media-upload";
108
131
  import { applyFileDrop } from "./editor/file-drop-action";
109
- import { seedAssistantMessages } from "./panels/ai-panel";
132
+ import { assistantCommands, isAssistantStreaming, seedAssistantMessages } from "./panels/ai-panel";
110
133
  import { seedPublishConnected } from "./publish/publish-panel";
111
- import { openConnectorGrid } from "./grid/grid-open";
112
134
 
113
135
  import { getPlatform, hasPlatform, registerPlatform } from "./platform";
114
136
  import { parseMediaEntries } from "./utils/canvas-media";
115
137
  import { resolveDefaultPlatform } from "./platforms/default-platform";
116
138
  import { mountResizeEdges } from "./resize-edges";
117
- import { codeService } from "./services/code-services";
118
- import { defBadgeLabel, defCategory, renderSignalsTemplate } from "./panels/signals-panel";
139
+ import {
140
+ defBadgeLabel,
141
+ defCategory,
142
+ registerSignalsCommands,
143
+ renderSignalsTemplate,
144
+ } from "./panels/signals-panel";
119
145
  import { loadComponentRegistry } from "./files/components";
120
146
  import { ensureDependenciesInstalled } from "./packages/ensure-deps";
121
147
  import { maybePromptJxsuiteUpdate } from "./packages/jxsuite-update";
@@ -124,8 +150,14 @@ import { autoSyncProjectOnOpen } from "./packages/pull-package-sync";
124
150
  import { html, render as litRender } from "lit-html";
125
151
 
126
152
  import webdata from "../data/webdata.json";
127
- import { renderDataExplorerTemplate } from "./panels/data-explorer";
128
- import { cloneRepository, renderGitPanel } from "./panels/git-panel";
153
+ import { registerDataExplorerCommands } from "./panels/data-explorer";
154
+ import {
155
+ cleanupGitPanel,
156
+ cloneRepository,
157
+ loadDiffForLens,
158
+ registerSourceControlCommands,
159
+ renderGitPanel,
160
+ } from "./panels/git-panel";
129
161
 
130
162
  // ─── Spectrum Web Components ──────────────────────────────────────────────────
131
163
  // Explicit class imports + registration — bare side-effect imports are tree-shaken
@@ -134,31 +166,42 @@ import { components as _swc } from "./ui/spectrum";
134
166
  import "./ui/panel-resize.js";
135
167
  // Built-in schema-form controls (schema-builder, secret) register on import
136
168
  import "./ui/form-controls.js";
137
- import { initLayers, showSaveDiscardDialog } from "./ui/layers";
138
- import { initShortcuts } from "./editor/shortcuts";
139
- import { renderActivityBar, mount as mountActivityBar } from "./panels/activity-bar";
169
+ import { initLayers, isModalOpen } from "./ui/layers";
170
+ import { initShortcuts, registerStudioCommands } from "./editor/shortcuts";
171
+ import type { ProjectOpenOutcome, ProjectOpenTarget } from "./editor/shortcuts";
172
+ import { createCommandRegistry } from "./commands/registry";
173
+ import { chordsInScopes } from "./commands/keymap";
174
+ import { FRAME_KEY_SCOPES } from "./canvas/iframe-keys";
175
+ import { createLiveContext } from "./commands/live-context";
176
+ import { hasAiCredentials } from "./services/ai-models";
177
+ import { mount as mountActivityBar } from "./panels/activity-bar";
140
178
  import * as toolbarPanel from "./panels/toolbar";
141
179
  import * as overlaysPanel from "./panels/overlays";
142
180
  import * as frontmatterPanelMod from "./panels/frontmatter-panel";
143
181
  import * as rightPanelMod from "./panels/right-panel";
144
182
  import * as chatPanelMod from "./panels/chat-panel";
145
183
  import { setProjectAdopter } from "./services/project-adoption";
184
+ import { tabBufferUnsaved } from "./services/monaco-buffer";
146
185
  import * as leftPanelMod from "./panels/left-panel";
147
186
  import * as tabStrip from "./panels/tab-strip";
148
- import * as tabBar from "./panels/tab-bar";
187
+ import * as paneContext from "./panels/pane-context";
149
188
  import { selectStylebookTag } from "./panels/stylebook-panel";
150
189
  import { registerLayersDnD, registerComponentsDnD, registerElementsDnD } from "./panels/dnd";
151
190
  import { registerCanvasDndBridge } from "./panels/canvas-dnd-bridge";
152
191
  import { defaultDef } from "./panels/shared";
153
- import { closeFormulaWorkspace } from "./panels/formula-workspace";
192
+ import { registerFormulaEditorCommands } from "./panels/formula-workspace";
193
+ import { closeFunctionEditor } from "./panels/editors";
154
194
  import {
195
+ formatCommands,
155
196
  initBlockActionBar,
156
197
  isEditChromeTarget,
198
+ registerSelectionCommands,
199
+ releaseBlockActionBar,
157
200
  renderBlockActionBar,
201
+ suppressBlockActionBar,
158
202
  } from "./panels/block-action-bar";
159
203
  import { initCssData } from "./panels/style-utils";
160
- import { initQuickSearch, openQuickSearch } from "./panels/quick-search";
161
- import { openSettingsModal } from "./settings/settings-modal";
204
+ import { initQuickSearch } from "./panels/quick-search";
162
205
  import { hydrateAccountStatus } from "./account-status";
163
206
  import { hydrateProjectList } from "./project-list";
164
207
  import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
@@ -169,199 +212,166 @@ import {
169
212
  openProjectPickerModal,
170
213
  platformUsesRepoPicker,
171
214
  } from "./new-project/add-repo-modal";
172
- import { openNewProjectModal } from "./new-project/new-project-modal";
173
- import type { DocumentStackEntry, GitDiffState } from "./types";
215
+ import { openNewProjectModal, registerNewProjectCommands } from "./new-project/new-project-modal";
216
+ import { invalidatePageRouteCache, registerInspectorCommands } from "./panels/properties-panel";
217
+ import { liveElementCommands, setContextMenuNavigate } from "./editor/context-menu";
218
+ import { registerSeoCommands, renderSeoModal } from "./panels/seo-modal";
219
+ import { registerA11yCommands } from "./services/a11y-report";
220
+ import { registerStyleCommands } from "./panels/style-panel";
221
+ import { registerGridCommands } from "./grid/grid-open";
222
+ import { registerSettingsCommands } from "./settings/settings-document";
223
+ import { registerPreferencesCommands } from "./settings/preferences-dialog";
224
+ import { registerAboutCommands } from "./about/about-modal";
225
+ import { registerCollabCommands } from "./collab/collab-commands";
226
+ import { registerLibraryCommands } from "./browse/library-commands";
227
+ import { registerPublishCommands } from "./publish/publish-commands";
228
+ import { registerGridViewCommands } from "./grid/grid-panel";
229
+ import { registerRedirectsCommands } from "./grid/redirects-grid";
230
+ import { registerContentCommands } from "./content/entry-commands";
231
+ import { registerI18nCommands } from "./i18n/i18n-commands";
232
+ import { convertToComponent } from "./editor/convert-to-component";
233
+ import type { GitDiffState } from "./types";
174
234
  import type { Tab } from "./tabs/tab";
175
- import type { JxPath } from "./state";
176
235
  import type { JxMutableNode, ProjectConfig } from "@jxsuite/schema/types";
177
236
 
178
237
  void _swc;
179
238
 
239
+ /**
240
+ * What the derivation's commands and follows need from the rest of Studio: an opener, and a reader
241
+ * for the one preset whose subject is not a document at all.
242
+ *
243
+ * `loadDiff` is here rather than in `workspace/pane-derive.ts` because that module owns no I/O —
244
+ * which is what lets its whole decision-making half be tested with no platform. It is the same pair
245
+ * of reads `panels/git-panel.ts` makes for a row click; an added file has no `HEAD` copy, so its
246
+ * "original" is the empty string rather than a `gitShow` that would throw.
247
+ */
248
+ const derivationDeps: DerivationDeps = {
249
+ /*
250
+ * `fileExists` answers the locale companion's one question: the resolver computes WHERE this
251
+ * document's copy in another language would live, and only the disk can say whether anybody has
252
+ * written it. A read that throws is the platform's own answer for "no such file" — every backend
253
+ * rejects rather than returning empty — so the catch is the negative case, not an error being
254
+ * swallowed. Memoised per wanted path by `probeTranslationFor`, so it costs one read per locale a
255
+ * pane is actually pointed at.
256
+ */
257
+ fileExists: async (path: string) => {
258
+ try {
259
+ await getPlatform().readFile(path);
260
+ return true;
261
+ } catch {
262
+ return false;
263
+ }
264
+ },
265
+ loadDiff: loadDiffForLens,
266
+ openFileInPane,
267
+ };
268
+
180
269
  // ─── Globals ──────────────────────────────────────────────────────────────────
181
270
  // These mutable variables are local to studio.js for now. As sections are extracted
182
271
  // Into their own modules, they will migrate to ctx in store.js.
183
272
 
184
- // Effective canvas mode: the per-tab preview toggle composes with an edit/design base mode and
185
- // Presents as "preview" to every downstream gate (doc resolution, iframe flags, interaction
186
- // Surfaces). Consumers needing the base mode (toolbar switcher selection, canvas host layout)
187
- // Read tab.session.ui.canvasMode directly.
273
+ // The FOCUSED pane's effective canvas mode. The composition itself (the per-tab preview toggle over
274
+ // An edit/design base) lives in `canvas/canvas-surface.ts`, beside the panes, because the answer is
275
+ // A property of a pane's tab and every other pane has its own — this is just the focused one, which
276
+ // Is what a panel drawn once for the whole shell means by "the canvas mode".
277
+ //
278
+ // It is a READ, and it is injected only into surfaces the shell draws once: the toolbar, the
279
+ // Inspector, the block bar, the overlays, the live-render context and the canvas view commands.
280
+ // `panels/pane-context.ts` used to take it too and is the reason this comment exists — a bar drawn
281
+ // Once per pane asking "what mode is the canvas in" got the answer for a pane it was not drawing,
282
+ // So the Export button appeared in BOTH bars the moment either pane entered Code. A pane-scoped
283
+ // Caller asks `canvasModeOfPane(paneId)` or `canvasModeOfTab(tab)`, and
284
+ // `scripts/check-pane-singletons.ts`'s fourth rule is what says so mechanically.
188
285
  function getCanvasMode() {
189
- const ui = activeTab.value?.session.ui;
190
- const base = ui?.canvasMode ?? "design";
191
- return ui?.preview && (base === "edit" || base === "design") ? "preview" : base;
286
+ return canvasModeOfPane(workspace.activePaneId);
192
287
  }
193
288
 
194
- /** @param {string} mode */
195
- function setCanvasMode(mode: string) {
196
- if (getCanvasMode() === "git-diff" && mode !== "git-diff") {
197
- gitDiffState = null;
289
+ /**
290
+ * Write a tab's BASE canvas mode. **The only writer, and it cannot find a tab on its own.**
291
+ *
292
+ * It used to open with `const tab = activeTab.value` and every injected `setCanvasMode` was `(mode:
293
+ * string) => void`, which is how the pane context bar's Editor picker — drawn per pane, from
294
+ * `tabOfPane(paneId)` — moved the FOCUSED pane's tab into Code: click "Code" in the side bar and
295
+ * the primary became a Code editor while the side pane went on drawing Design. There is no
296
+ * zero-argument variant anywhere in the graph now; a caller drawn once for the shell passes
297
+ * `activeTab.value` where a reviewer can see it, and a caller drawn for a pane passes that pane's
298
+ * tab.
299
+ *
300
+ * No pane check. It used to refuse a mode the tab's pane could not host, because the side pane was
301
+ * capped to the cheap editor kinds and a cap enforced only at the split is one a context-bar click
302
+ * walks straight back out of. Both panes host every kind, so the only thing that can refuse a mode
303
+ * is the document not declaring it — which `canvas.setMode` checks by name.
304
+ *
305
+ * @param {Tab | null} tab — the tab to move. `null` is a no-op.
306
+ * @param {string} mode
307
+ */
308
+ function setCanvasMode(tab: Tab | null, mode: string) {
309
+ if (!tab) {
310
+ return;
198
311
  }
199
- const tab = activeTab.value;
200
- if (tab) {
201
- tab.session.ui.canvasMode = mode;
312
+ /* THIS tab's mode, not the focused pane's. `getCanvasMode()` here meant that leaving git-diff in
313
+ the side pane kept `shell.git.diffState` alive whenever the primary was in some other mode —
314
+ and, worse, that changing the PRIMARY's mode cleared the diff the side pane was still showing. */
315
+ if (canvasModeOfTab(tab) === "git-diff" && mode !== "git-diff") {
316
+ shell.git.diffState = null;
202
317
  }
318
+ tab.session.ui.canvasMode = mode;
203
319
  }
204
320
 
205
- let gitDiffState: GitDiffState | null = null;
206
-
207
321
  // ─── Component registry ───────────────────────────────────────────────────────
208
322
 
209
- /** @param {string} componentPath */
210
- async function navigateToComponent(componentPath: string) {
211
- try {
212
- const platform = getPlatform();
213
- const content = await platform.readFile(componentPath);
214
- if (!content) {
215
- return;
216
- }
217
- const parsed = JSON.parse(content) as JxMutableNode;
218
- const tab = activeTab.value;
219
- if (!tab) {
220
- return;
221
- }
222
-
223
- // Push current state onto the document stack
224
- const frame = {
225
- dirty: tab.doc.dirty,
226
- document: tab.doc.document,
227
- documentPath: tab.documentPath,
228
- mode: tab.doc.mode,
229
- selection: tab.session.selection,
230
- sourceFormat: tab.doc.sourceFormat,
231
- };
232
- if (!tab.session.documentStack) {
233
- tab.session.documentStack = [];
234
- }
235
- tab.session.documentStack.push(frame);
236
-
237
- // Load the component
238
- tab.doc.document = parsed;
239
- tab.doc.dirty = false;
240
- tab.doc.mode = null as unknown as string;
241
- tab.doc.sourceFormat = null;
242
- tab.documentPath = componentPath;
243
- tab.session.selection = null;
244
- view.leftTab = "layers";
245
- tab.session.ui.activeMedia = null;
246
- tab.session.ui.activeSelector = null;
247
-
248
- render();
249
- statusMessage(`Editing component: ${parsed.tagName || componentPath}`);
250
- } catch (error) {
251
- const err = error as Error;
252
- statusMessage(`Error: ${err.message}`);
253
- }
254
- }
255
-
256
323
  /**
257
- * Leaving a drilled-in component discards its edits (the pop restores the parent). Because saving
258
- * is explicit, prompt when the child is dirty: Save writes it, Discard drops it, Cancel stays.
259
- * Returns false to abort navigation.
324
+ * Drill into a component (or a layout) — as a REAL TAB, not a document swapped in under the current
325
+ * tab's id, and BESIDE the page rather than over it.
326
+ *
327
+ * The old implementation rewrote `tab.documentPath` and left `tab.id` alone, and everything
328
+ * downstream believed the id: `openFileInTab`'s dedupe stopped matching, so re-opening the page
329
+ * from the tree called `openTab` with an id that was already in the map — overwriting the entry
330
+ * without disposing the old tab's effect scope and pushing a SECOND copy of the id into `tabOrder`,
331
+ * which is a duplicate lit `repeat` key and a lost document stack.
332
+ *
333
+ * What the drill-in keeps is the RELATIONSHIP: the new tab records the document it was opened from
334
+ * and the strip prints it. The parent stays open, right where it was.
260
335
  *
261
- * @param {Tab} tab
336
+ * Four deliberate properties, three of them new and each with a failure behind it:
337
+ *
338
+ * 1. **To the side.** §8.2 has promised this since P3 and it never shipped — the chain ran
339
+ * `openFileInTab` → `openTab` → `activePane()`, so "open the layout that wraps this page" opened
340
+ * it ON TOP of the page it was teaching about.
341
+ * 2. **Focus stays in the page.** An assistant pane that takes the keyboard means the author's next
342
+ * keystroke edits the definition instead of the document they are looking at — and a following
343
+ * pane would immediately have nothing to follow.
344
+ * 3. **A PREVIEW tab**, because drilling in is browsing: the second drill-in takes the same slot
345
+ * instead of littering the side strip, and an edit promotes it (`promoteDirtyPreviewTabs`).
346
+ * 4. **An ordinary tab, not a derivation.** "Edit definition" is a commitment to edit one component; a
347
+ * following pane would yank to a different one on the author's next canvas click. The following
348
+ * form is `pane.derive { preset: "component" }`, opted into by name.
349
+ *
350
+ * `openedFrom` is unchanged — §14.2's relationship, which nothing pops and nothing restores from.
351
+ *
352
+ * @param {string} componentPath
262
353
  */
263
- async function confirmLeaveDirtyChild(tab: Tab): Promise<boolean> {
264
- if (!tab.doc.dirty || !tab.documentPath) {
265
- return true;
266
- }
267
- const name = tab.documentPath.split("/").pop() || "component";
268
- const choice = await showSaveDiscardDialog("Unsaved Changes", `"${name}" has unsaved changes.`);
269
- if (choice === "cancel") {
270
- return false;
271
- }
272
- if (choice === "save") {
273
- try {
274
- await getPlatform().writeFile(tab.documentPath, await serializeDocument(tab));
275
- } catch (error) {
276
- statusMessage(`Save error: ${(error as Error).message}`);
277
- return false;
278
- }
279
- }
280
- // "discard": leave without writing — the child's edits are dropped with the popped frame.
281
- return true;
282
- }
283
-
284
- async function navigateBack() {
285
- const tab = activeTab.value;
286
- if (!tab?.session.documentStack || tab.session.documentStack.length === 0) {
287
- return;
288
- }
289
- if (!(await confirmLeaveDirtyChild(tab))) {
290
- return;
291
- }
292
-
293
- // Pop the stack
294
- const frame = tab.session.documentStack.pop() as Record<string, unknown> | undefined;
295
- if (!frame) {
296
- return;
297
- }
298
- tab.doc.document = frame.document as JxMutableNode;
299
- tab.doc.dirty = frame.dirty as boolean;
300
- tab.doc.mode = frame.mode as string;
301
- tab.doc.sourceFormat = frame.sourceFormat as string | null;
302
- tab.documentPath = frame.documentPath as string | null;
303
- tab.session.selection = frame.selection as JxPath | null;
304
- view.leftTab = "layers";
305
-
306
- render();
307
- statusMessage("Returned to parent document");
308
- }
309
-
310
- /** @param {number} targetIndex */
311
- async function navigateToLevel(targetIndex: number) {
312
- const tab = activeTab.value;
313
- const stack = tab?.session.documentStack;
314
- if (!stack || targetIndex < 0 || targetIndex >= stack.length) {
315
- return;
316
- }
317
- if (!(await confirmLeaveDirtyChild(tab))) {
318
- return;
354
+ async function navigateToComponent(componentPath: string) {
355
+ const from = activeTab.value;
356
+ /* {@link receivingPane}, not `sidePane`: the pane beside this one may be a LENS, which owns no
357
+ tab. The open then landed in a `tabOrder` `tabOfPane` hops straight past, so the read below got
358
+ the SOURCE tab back, `opened.documentPath !== componentPath`, and the one relationship this
359
+ function exists to record (§14.2) was skipped without a sound. */
360
+ const target = receivingPane();
361
+ const alreadyOpen = [...workspace.tabs.values()].some((t) => t.documentPath === componentPath);
362
+ await openFileInTab(componentPath, { focus: false, paneId: target.id, preview: true });
363
+ const opened = tabOfPane(target.id);
364
+ if (!alreadyOpen && from && opened && opened.documentPath === componentPath) {
365
+ opened.session.openedFrom = { documentPath: from.documentPath, tabId: from.id };
319
366
  }
320
-
321
- const frame = stack[targetIndex] as DocumentStackEntry;
322
- tab.session.documentStack = stack.slice(0, targetIndex);
323
- tab.doc.document = frame.document as JxMutableNode;
324
- tab.doc.dirty = frame.dirty as boolean;
325
- tab.doc.mode = frame.mode as string;
326
- tab.doc.sourceFormat = frame.sourceFormat as string | null;
327
- tab.documentPath = frame.documentPath as string | null;
328
- tab.session.selection = frame.selection as JxPath | null;
329
- view.leftTab = "layers";
330
-
331
- render();
332
- statusMessage("Returned to parent document");
367
+ setActivityTab("layers");
333
368
  }
334
369
 
335
- async function closeFunctionEditor() {
336
- const tab = activeTab.value;
337
- const editing =
338
- /** @type {{ type: string; defName?: string; path?: JxPath; eventKey?: string } | null} */ tab
339
- ?.session.ui.editingFunction;
340
- if (!editing || !tab) {
341
- return;
342
- }
343
- if (view.functionEditor) {
344
- const currentCode = view.functionEditor.getValue();
345
- const minResult = await codeService("minify", { code: currentCode });
346
- const bodyToStore = minResult?.code ?? currentCode;
347
- if (editing.type === "def") {
348
- transactDoc(tab, (t) => mutateUpdateDef(t, editing.defName as string, { body: bodyToStore }));
349
- } else if (editing.type === "event") {
350
- const node = getNodeAtPath(tab.doc.document, editing.path as JxPath);
351
- const current = node?.[editing.eventKey as string] || {};
352
- transactDoc(tab, (t) =>
353
- mutateUpdateProperty(t, editing.path as JxPath, editing.eventKey as string, {
354
- .../** @type {object} */ current,
355
- $prototype: "Function",
356
- body: bodyToStore,
357
- }),
358
- );
359
- }
360
- view.functionEditor.dispose();
361
- view.functionEditor = null;
362
- }
363
- updateUi("editingFunction", null);
364
- }
370
+ // There is no `navigateBack` / `navigateToLevel` here any more, and no `confirmLeaveDirtyChild`
371
+ // Beneath them. All three served the sub-document stack, which nothing could push onto — see
372
+ // `tabs/tab.ts`. The dirty prompt they shared was about a POPPED frame discarding a child's edits;
373
+ // A drilled-in component is a real tab now, and `panels/tab-strip.ts` owns the prompt for closing
374
+ // One. That prompt is two-way where this was three-way — see `showSaveDiscardDialog`'s ledger entry.
365
375
 
366
376
  // ─── Webdata: datalists for autocomplete ──────────────────────────────────────
367
377
 
@@ -405,28 +415,13 @@ if (!hasPlatform()) {
405
415
  registerPlatform(resolveDefaultPlatform());
406
416
  }
407
417
 
408
- // Screenshot/automation runners (scripts/screenshots/) await window.__jxAutomation right after
409
- // Navigation, so the gated hook must install before the async deep-link project load below.
410
- installAutomationHook({
411
- getCanvasMode,
412
- openBrowseModal,
413
- openConnectorGrid,
414
- openNewProjectModal,
415
- openQuickSearchPalette: openQuickSearch,
416
- openSettingsModal,
417
- render,
418
- renderActivityBar,
419
- seedAssistantMessages,
420
- seedPublishConnected,
421
- setCanvasMode,
422
- statusMessage,
423
- });
424
-
425
418
  mountResizeEdges();
426
419
 
427
420
  // ─── Render loop ──────────────────────────────────────────────────────────────
428
421
 
429
422
  initShellRefs();
423
+ // One effect projects the dock record onto the shell grid — collapse classes and column widths.
424
+ mountShell();
430
425
 
431
426
  // Mount extracted panel modules
432
427
  toolbarPanel.mount(toolbarEl, {
@@ -443,20 +438,22 @@ toolbarPanel.mount(toolbarEl, {
443
438
  initLayers();
444
439
  initQuickSearch({ openRecentProject: (root: string) => openRecentProject(root) });
445
440
 
446
- tabStrip.mount(document.querySelector("#tab-strip") as HTMLElement);
441
+ /* The pane's four surfaces come from its CELL, not from `document.querySelector`.
442
+ `panels/pane-grid.ts` mounted through `mountShell()` above, so the primary's cell exists by now;
443
+ each of these three modules still holds one host, which is exactly right while the grid draws one
444
+ cell and is what `mountForPane` replaces when it draws two. */
445
+ const primaryCell = cellForPane(PRIMARY_PANE);
447
446
 
448
- tabBar.mount(document.querySelector("#tab-bar") as HTMLElement, {
449
- closeFormulaWorkspace: () => closeFormulaWorkspace(),
450
- closeFunctionEditor: () => closeFunctionEditor(),
447
+ tabStrip.mount(primaryCell?.strip ?? document.createElement("div"));
448
+
449
+ paneContext.mount(primaryCell?.chrome ?? document.createElement("div"), {
451
450
  exportFile,
452
- getCanvasMode,
453
- navigateBack: () => navigateBack(),
454
- navigateToLevel: (i: number) => navigateToLevel(i),
451
+ // No `getCanvasMode`: the bar is drawn once per pane and asks its own pane. See `PaneContextCtx`.
455
452
  parseMediaEntries,
453
+ setCanvasMode,
456
454
  });
457
455
 
458
456
  overlaysPanel.mount({
459
- getCanvasMode,
460
457
  isEditing,
461
458
  renderBlockActionBar,
462
459
  });
@@ -467,6 +464,10 @@ initBlockActionBar({
467
464
  });
468
465
  // The iframe's re-emitted selection snapshot drives the parent format toolbar refresh (4b-2).
469
466
  setToolbarRefresh(renderBlockActionBar);
467
+ // The other half of that seam: a pointerdown inside a canvas frame ends the bar's suppression (the
468
+ // Canvas is in play again). It is the only signal that can, because clicking the SAME
469
+ // Already-selected element changes nothing the render path can compare.
470
+ setCanvasPointerDownHandler(releaseBlockActionBar);
470
471
  // The cross-origin insertion "+" click runs the parent-realm slash-menu → mutateInsertNode flow.
471
472
  setInsertZoneClickHandler(runInsertZoneAction);
472
473
  // Files dragged from the OS onto a canvas: upload, then replace an image's source or insert.
@@ -475,33 +476,56 @@ setFileDropHandler(applyFileDrop);
475
476
  // Files afterwards. Injected here because all three modules import from media-upload.
476
477
  setMediaChangedHandler(async (dir) => {
477
478
  invalidateMediaCache();
478
- invalidateBrowseCache();
479
+ invalidateLibrary();
479
480
  await loadDirectory(dir);
480
481
  renderLeftPanel();
481
482
  });
482
483
  // The in-iframe "/" trigger drives the parent-realm Spectrum slash menu across the bridge.
483
484
  setCanvasSlashHandler(canvasSlashHandler);
484
485
  // Canvas right-clicks show the parent-realm Jx element context menu across the bridge.
485
- setCanvasContextMenuHandler(makeCanvasContextMenuHandler({ navigateToComponent }));
486
+ setCanvasContextMenuHandler(makeCanvasContextMenuHandler());
486
487
  // Stylebook hits decode to a TAG in the host and route here (null = clicked chrome/empty space).
487
488
  setStylebookHitHandler((tag, media) => {
488
489
  if (tag) {
489
490
  selectStylebookTag(tag, media);
490
491
  } else {
491
- updateSession({ ui: { activeSelector: null, stylebookSelection: null } });
492
+ shell.stylebook.selection = null;
493
+ updateSession(activeTab.value, { ui: { activeSelector: null } });
492
494
  }
493
495
  });
494
- // Commit-on-parent-click: a pointerdown in PARENT chrome outside the edit-session chrome (format
495
- // Toolbar / link popover / slash menu) ends the live inline-edit session — the iframe can't observe
496
- // Parent-realm pointer events (layers panel, tab strip, right panel…). Pointerdowns over the canvas
497
- // Land inside the cross-origin iframe and never reach this listener, so it can't double-fire with
498
- // The iframe's own click-away commit.
496
+ /* Two duties, one listener, because both answer the same question: intent has left the canvas.
497
+ Pointerdowns over the canvas land inside the cross-origin iframe and never reach this listener,
498
+ so a hit here IS parent chrome — which is why the listener can be this blunt and why it cannot
499
+ double-fire with the iframe's own click-away commit.
500
+
501
+ 1. Commit-on-parent-click: it ends the live inline-edit session, which the iframe cannot observe
502
+ itself (layers panel, tab strip, right panel…).
503
+ 2. The block action bar goes with it. The bar is `position: fixed` and clamped into the window,
504
+ so it can sit over the Document Header card, the pane context bar and the docks; an author
505
+ working in the Inspector's Logic tab reported it "blocking a part of the interface that the
506
+ user needs to utilize". It is SUPPRESSED, not dismissed — a plain dismiss flashes back on the
507
+ next snapshot or overlay repaint — and it comes back the moment the canvas takes a pointer or
508
+ the selection moves. Nothing here touches the selection: the Inspector edits the selected
509
+ node, so a click into it must keep the thing it is about to edit.
510
+
511
+ Both spare the edit-session chrome (`isEditChromeTarget`: the bar, its `⋮` menu, the link
512
+ popover, the slash menu) — those surfaces act ON the session and the bar rather than away from
513
+ them. Only the suppression spares the canvas STAGE, whose margins, artboard headers and insertion
514
+ "+" are parent-realm elements around the frame: they are the canvas, not somewhere else in the
515
+ shell, and hiding the bar to pan or to nudge the zoom would be the app losing the author's place
516
+ for them. */
499
517
  document.addEventListener(
500
518
  "pointerdown",
501
519
  (e) => {
502
- if (getEditSnapshot().editing && !isEditChromeTarget(e.target)) {
520
+ if (isEditChromeTarget(e.target)) {
521
+ return;
522
+ }
523
+ if (getEditSnapshot().editing) {
503
524
  commitActiveEditSession();
504
525
  }
526
+ if (!(e.target instanceof Node) || stageContaining(e.target) === null) {
527
+ suppressBlockActionBar();
528
+ }
505
529
  },
506
530
  true,
507
531
  );
@@ -509,9 +533,19 @@ document.addEventListener(
509
533
  // Unsaved-changes guard: saving is explicit (no idle autosave), so warn before the window unloads
510
534
  // While any open tab has unsaved edits. For collab tabs, `dirty` reflects the room-level unsaved
511
535
  // State; closing loses in-memory edits that were never flushed to disk.
536
+ /*
537
+ * `dirty` alone could not see a Monaco buffer, and quitting is the one exit no disposer follows.
538
+ *
539
+ * A keystroke in the source view or the dock's Logic tab reaches the document on a 600ms / 500ms
540
+ * debounce, and nothing marks the tab dirty in the meantime — so typing the last character of a
541
+ * handler and pressing ⌘Q left with no prompt at all. There is no flush to make here: `beforeunload`
542
+ * cannot await, and the source view's commit parses through the format host before it assigns, so
543
+ * the answer would arrive after the window is gone. The buffer is asked directly instead —
544
+ * `tabBufferUnsaved` is the author's own typing that the document has not been given.
545
+ */
512
546
  export function hasUnsavedTabs(): boolean {
513
547
  for (const tab of workspace.tabs.values()) {
514
- if (tab.doc.dirty) {
548
+ if (tab.doc.dirty || tabBufferUnsaved(tab)) {
515
549
  return true;
516
550
  }
517
551
  }
@@ -519,6 +553,9 @@ export function hasUnsavedTabs(): boolean {
519
553
  }
520
554
 
521
555
  window.addEventListener("beforeunload", (e: BeforeUnloadEvent) => {
556
+ // The session, so a relaunch reopens what was open (§4.4). Before the prompt, not after: the
557
+ // Author may cancel the close, and the record is the same either way.
558
+ flushSession();
522
559
  if (hasUnsavedTabs()) {
523
560
  e.preventDefault();
524
561
  // Legacy browsers require a truthy returnValue to trigger the native confirm prompt.
@@ -526,20 +563,22 @@ window.addEventListener("beforeunload", (e: BeforeUnloadEvent) => {
526
563
  }
527
564
  });
528
565
 
529
- initCanvasUtils({
530
- getCanvasMode,
531
- getZoom: () => activeTab.value?.session.ui.zoom ?? 1,
532
- setZoomDirect: (zoom) => {
533
- if (activeTab.value) {
534
- activeTab.value.session.ui.zoom = zoom;
535
- }
536
- },
537
- });
538
- initCanvasLiveRender({
539
- getCanvasMode,
540
- });
566
+ /* No `initCanvasUtils` any more, and the three functions it injected are why.
567
+ `getZoom` and `setZoomDirect` were `activeTab.value?.session.ui.zoom` — the FOCUSED pane's tab —
568
+ while every geometry function in `canvas/canvas-utils.ts` already took an explicit
569
+ `CanvasSurface`. So the pan and the wrap were per-stage and the SCALE was not: the unfocused pane
570
+ drew at the focused tab's scale, the side pane's `+` zoomed the primary's document, and the side
571
+ pane entering Design snapped the primary to whatever it had fitted itself to. `getCanvasMode`
572
+ was the same shape one layer down. The module reaches all three through the surface it is given
573
+ (`tabOfPane(surface.paneId)`), so there is nothing left to inject.
574
+
575
+ No `initCanvasLiveRender` either, and it went for the same reason one layer further down again.
576
+ `getCanvasMode` was the ONLY thing that context carried, and `resolveCanvasDocument` used it
577
+ while ALSO reading `activeTab.value` for the document path, the layout toggle and the preview
578
+ params — so the pane the render had already been resolved for was discarded and the focused
579
+ pane's answers substituted for all six. It takes the tab now, and the injection point had
580
+ nothing else in it. */
541
581
  initCanvasPatcher({
542
- getCanvasMode,
543
582
  renderOverlays,
544
583
  scheduleCanvasRender,
545
584
  });
@@ -548,14 +587,13 @@ setIframePatchEscalation(scheduleCanvasRender);
548
587
  // One global coordinator monitor drives cross-frame palette→canvas drops (Phase 4c).
549
588
  registerCanvasDndBridge();
550
589
  initCanvasRender({
551
- getCanvasMode,
552
590
  get gitDiffState() {
553
- return gitDiffState;
591
+ return shell.git.diffState;
554
592
  },
555
593
  openFileFromTree,
556
594
  setCanvasMode,
557
595
  setGitDiffState: (state: GitDiffState | null) => {
558
- gitDiffState = state;
596
+ shell.git.diffState = state;
559
597
  },
560
598
  });
561
599
 
@@ -581,59 +619,157 @@ initWelcome({
581
619
  openRecentProject: (root: string) => openRecentProject(root),
582
620
  });
583
621
 
584
- // Effect-driven canvas rendering, split into two triggers so document changes can be
585
- // Distinguished from mode/UI changes:
586
- // - doc-effect: tracks only the document root reference. Document mutations that were
587
- // Consumed surgically by the canvas patcher skip the full render here.
588
- // - ui-effect: tracks canvas mode and UI flags; always schedules a full render.
589
- // Scheduling is deduped inside scheduleCanvasRender (double-RAF).
622
+ /* There is no stage-handover effect here any more.
623
+ It existed because the shell had one `#canvas-wrap` and a pane is the unit of render, so "which
624
+ pane owns the stage" had to be re-answered every time focus moved. `panels/pane-grid.ts` builds a
625
+ cell per pane and registers its stage with it; nothing moves, and nothing has to be repainted
626
+ because it changed hands. */
627
+
628
+ /**
629
+ * One pane's render subscriptions.
630
+ *
631
+ * Effect-driven canvas rendering, split into three triggers so document changes can be
632
+ * distinguished from mode/UI changes:
633
+ *
634
+ * - Doc-effect: tracks only the document root reference. Document mutations that were consumed
635
+ * surgically by the canvas patcher skip the full render here.
636
+ * - Ui-effect: tracks canvas mode and UI flags; always schedules a full render.
637
+ * - The colour-scheme post, which is a document-level attribute flip inside the iframe and
638
+ * deliberately never part of the ui-effect: flipping the scheme must not re-render.
639
+ *
640
+ * Scheduling is deduped inside `scheduleCanvasRender` (double-RAF).
641
+ *
642
+ * **Keyed on the PANE, not on `activeTab`, and this is what makes `⌘\` and Unsplit work at all.**
643
+ * Both effects used to read the focused pane's tab and schedule "the" canvas. A split moves a tab
644
+ * between panes without changing which tab is active, so neither effect re-ran: the pane the tab
645
+ * LEFT went on displaying it, and Unsplit — where the survivor's `activeTabId` changes but
646
+ * `activeTab` does not — left the primary showing document A while the strip, the jump bar and the
647
+ * Inspector all said B. `tabOfPane` tracks the pane's own `activeTabId`, so the pane that changed
648
+ * is the pane that repaints.
649
+ *
650
+ * @param {string} paneId
651
+ */
652
+ function installPaneRenderEffects(paneId: string): void {
653
+ /* The FOLLOW, in this pane's own scope so it stops when the pane leaves the grid.
654
+ No preset subscribes nothing: `installDerivationEffects` declares this pane's own
655
+ `activeTabId` and `tabOfPane(sourcePaneId)` for EVERY derivation, and `diff` and `breakpoint`
656
+ are the two that add nothing beyond that — a lens follows STRUCTURALLY, through `tabOfPane`'s
657
+ hop, which the three effects below already read. A comment here claimed the opposite of
658
+ `pane-derive.ts`'s own docstring for a release; the inputs are listed once, at the effect that
659
+ declares them, and this line points at it rather than restating it. */
660
+ installDerivationEffects(paneId, derivationDeps);
661
+ effect(() => {
662
+ const tab = tabOfPane(paneId);
663
+ if (tab) {
664
+ const doc = tab.doc.document;
665
+ if (doc && consumePatchedDocument(doc, paneId)) {
666
+ return;
667
+ }
668
+ }
669
+ scheduleCanvasRender(paneId);
670
+ });
671
+ effect(() => {
672
+ const tab = tabOfPane(paneId);
673
+ /* A derived pane's own view axes. A preset change and a breakpoint change are render inputs
674
+ that live on the DERIVATION rather than on the tab, so without these a lens would keep
675
+ drawing whatever it was created with. Accepted cost, stated: the shared `ui.canvasMode` read
676
+ below means a mode flip in the source pane also re-runs the lens's ui-effect — one extra
677
+ full render per human mode flip, which is not a per-keystroke path. */
678
+ const derived = paneById(paneId)?.derived;
679
+ void derived?.status;
680
+ /* …and the SENTENCE, because the stage draws it. `canvas/canvas-render.ts` prints
681
+ `derived.reason` for an unavailable derivation, and `status` alone does not move when the
682
+ reason does: a diff lens that could not read its comparison, whose file the author then
683
+ saves back to HEAD, stays `unavailable` while the sentence becomes "Nothing to compare".
684
+ Untracked, the stage would keep the old sentence until something else repainted it. */
685
+ void derived?.reason;
686
+ if (derived?.kind === "lens") {
687
+ void derived.mode;
688
+ void derived.media;
689
+ }
690
+ if (tab) {
691
+ void tab.doc.mode;
692
+ void tab.session.ui.canvasMode;
693
+ void tab.session.ui.editingFormula;
694
+ void tab.session.ui.editingFunction;
695
+ void tab.session.ui.featureToggles;
696
+ void tab.session.ui.preview;
697
+ void tab.session.ui.previewParams;
698
+ void tab.session.ui.previewProps;
699
+ void tab.session.ui.showLayout;
700
+ }
701
+ // Project-level render inputs: the stylebook catalogue's filters and the settings section are
702
+ // Shell state, so a change repaints the canvas with or without a document focused.
703
+ void shell.settingsTab;
704
+ void shell.stylebook.tab;
705
+ void shell.stylebook.filter;
706
+ void shell.stylebook.customizedOnly;
707
+ scheduleCanvasRender(paneId);
708
+ });
709
+ effect(() => {
710
+ const scheme = tabOfPane(paneId)?.session.ui.previewColorScheme;
711
+ // Scoped to THIS pane's stage: the scheme is a per-tab choice, and an unscoped post flipped
712
+ // Both documents from one pane's control.
713
+ postColorSchemeToLiveHosts(
714
+ scheme === "light" || scheme === "dark" ? scheme : null,
715
+ surfaceForPane(paneId).wrap,
716
+ );
717
+ });
718
+ effect(() => {
719
+ /*
720
+ * The other half of `i18n.switchLocale`. Without this the control moves a chip and the artboard
721
+ * goes on drawing the same document left-to-right, which makes the verb a label rather than a
722
+ * rendering context. Scoped to THIS pane's stage for the reason the scheme effect above gives.
723
+ */
724
+ const locale = tabOfPane(paneId)?.session.ui.previewLocale ?? null;
725
+ postLocaleToLiveHosts(locale, surfaceForPane(paneId).wrap);
726
+ });
727
+ }
728
+
729
+ /**
730
+ * Subscribe every drawn pane, and unsubscribe one that leaves the grid.
731
+ *
732
+ * A scope per pane rather than one loop over `workspace.panes`, because the distinction the two
733
+ * effects above draw only survives if each pane is tracked separately: an effect that read both
734
+ * panes' documents would re-render BOTH whenever either moved, and `consumePatchedDocument` would
735
+ * skip both whenever one was patched.
736
+ */
737
+ const _paneRenderScopes = new Map<string, EffectScope>();
738
+
590
739
  effect(() => {
591
- const tab = activeTab.value;
592
- if (tab) {
593
- const doc = tab.doc.document;
594
- if (doc && consumePatchedDocument(doc)) {
595
- return;
740
+ const wanted = new Set(workspace.panes.map((pane) => pane.id));
741
+ for (const [paneId, scope] of _paneRenderScopes) {
742
+ if (!wanted.has(paneId)) {
743
+ scope.stop();
744
+ _paneRenderScopes.delete(paneId);
596
745
  }
597
746
  }
598
- scheduleCanvasRender();
599
- });
600
- effect(() => {
601
- const tab = activeTab.value;
602
- if (tab) {
603
- void tab.doc.mode;
604
- void tab.session.ui.canvasMode;
605
- void tab.session.ui.editingFormula;
606
- void tab.session.ui.editingFunction;
607
- void tab.session.ui.featureToggles;
608
- void tab.session.ui.preview;
609
- void tab.session.ui.previewParams;
610
- void tab.session.ui.previewProps;
611
- void tab.session.ui.settingsTab;
612
- void tab.session.ui.showLayout;
613
- void tab.session.ui.stylebookTab;
614
- void tab.session.ui.stylebookFilter;
615
- void tab.session.ui.stylebookCustomizedOnly;
747
+ for (const paneId of wanted) {
748
+ if (_paneRenderScopes.has(paneId)) {
749
+ continue;
750
+ }
751
+ const scope = effectScope();
752
+ _paneRenderScopes.set(paneId, scope);
753
+ scope.run(() => {
754
+ installPaneRenderEffects(paneId);
755
+ });
616
756
  }
617
- scheduleCanvasRender();
618
- });
619
- // Color-scheme preview is a document-level attribute flip inside the iframe — deliberately its
620
- // Own effect, never part of the ui-effect above: flipping the scheme must not re-render.
621
- effect(() => {
622
- const s = activeTab.value?.session.ui.previewColorScheme;
623
- postColorSchemeToLiveHosts(s === "light" || s === "dark" ? s : null);
624
757
  });
625
758
 
626
759
  rightPanelMod.mount({
627
760
  getCanvasMode,
761
+ // The Assistant tab's body is built by the Inspector and handed to the module that owns the
762
+ // Chat. Injected rather than imported so the dependency runs one way: chat-panel.ts calls back
763
+ // Into right-panel.ts to select its own tab, and this is what keeps that from being a cycle.
764
+ mountAssistant: (host) => chatPanelMod.mount(host),
628
765
  navigateToComponent,
629
766
  renderCanvas: () => renderCanvas(),
630
767
  });
631
768
 
632
- // Above-canvas frontmatter Properties panel (content-collection docs, edit mode).
633
- frontmatterPanelMod.mount({ getCanvasMode });
769
+ // The Document Header card — every document with frontmatter or `$head`. It has no host of its own:
770
+ // The stage hands one over (`canvas-render.ts`), and this only starts the reactive subscription.
771
+ frontmatterPanelMod.mount();
634
772
 
635
- // The persistent AI chat sidebar — mounts once, available with or without a project/document.
636
- chatPanelMod.mount(chatPanelEl);
637
773
  // The assistant's create_project tool adopts freshly scaffolded projects through the same
638
774
  // Flow as the recent-projects list.
639
775
  setProjectAdopter(openRecentProject);
@@ -654,10 +790,22 @@ leftPanelMod.mount({
654
790
  // Fetch. Edit/design suppress them (a full render re-resolves every entry, so authoring would
655
791
  // Refetch constantly); re-firing them on demand is what that button is for.
656
792
  refreshData: () => {
657
- allowAutoRequestsOnNextRender();
658
- renderCanvas();
793
+ // ONE read of the focus, shared by the arm and the render. The Data panel is a Navigator
794
+ // Surface, so its subject genuinely is the focused pane — but `allowAutoRequestsOnNextRender()`
795
+ // And `renderCanvas()` each used to resolve that for themselves, and two resolutions of the
796
+ // Same fact are two facts: arming a pane and then rendering a different one is a Refresh that
797
+ // Refreshes nothing.
798
+ const paneId = workspace.activePaneId;
799
+ allowAutoRequestsOnNextRender(paneId);
800
+ // Say so BEFORE the render, and let the iframe's `dataScope` reply be what stops saying it.
801
+ // The button used to fire and repaint 200ms later on a timer, which reported "done" over the
802
+ // Old values for anything slower than that — a Refresh that visibly did nothing.
803
+ const tab = activeTab.value;
804
+ if (tab) {
805
+ tab.session.canvas.refreshing = true;
806
+ }
807
+ renderCanvas(paneId);
659
808
  },
660
- renderDataExplorerTemplate,
661
809
  renderFilesTemplate,
662
810
  renderGitPanel,
663
811
  renderHeadTemplate,
@@ -665,7 +813,7 @@ leftPanelMod.mount({
665
813
  renderSignalsTemplate,
666
814
  setCanvasMode,
667
815
  setGitDiffState: (state: GitDiffState | null) => {
668
- gitDiffState = state;
816
+ shell.git.diffState = state;
669
817
  },
670
818
  setupTreeKeyboard,
671
819
  webdata,
@@ -677,22 +825,21 @@ registerRenderer("leftPanel", () => leftPanelMod.render());
677
825
  registerRenderer("canvas", () => renderCanvas());
678
826
  registerRenderer("rightPanel", () => rightPanelMod.render());
679
827
  registerRenderer("frontmatterPanel", () => frontmatterPanelMod.render());
828
+ registerRenderer("seoModal", renderSeoModal);
680
829
  registerRenderer("chatPanel", () => chatPanelMod.render());
681
830
  registerRenderer("overlays", () => overlaysPanel.render());
682
- setStatusbarRenderer(() => renderStatusbar());
831
+ renderStatusbar();
683
832
  mountStatusbar();
833
+ // ⑥ The jump bar, in the pane's own grid cell above the context bar. It renders the whole address
834
+ // — project › file › node › node — and it is the only breadcrumb in the shell: the pane context
835
+ // Bar drew a second one, and it named a sub-document stack nothing could push onto.
836
+ mountJumpBar(primaryCell?.jump ?? document.createElement("div"));
684
837
  mountActivityBar();
685
838
 
686
- // Clicking on the canvas-wrap background (outside any canvas panel) deselects the current element
687
- canvasWrap.addEventListener("click", (e: MouseEvent) => {
688
- if (e.target !== canvasWrap && e.target !== view.panzoomWrap) {
689
- return;
690
- }
691
- if (!activeTab.value?.session.selection) {
692
- return;
693
- }
694
- activeTab.value.session.selection = null;
695
- });
839
+ /* The background-click deselect moved into `editor/shortcuts.ts`'s `installStageGestures`, beside
840
+ the wheel and the middle-drag, because it is a STAGE gesture: it compared against the app's one
841
+ `#canvas-wrap` and one `view.panzoomWrap` and cleared `activeTab`'s selection, all three of which
842
+ name the focused pane rather than the pane that was clicked. */
696
843
 
697
844
  function safeRenderRightPanel() {
698
845
  rightPanelMod.render();
@@ -712,13 +859,33 @@ configureCollabParser(async (tab, text) => {
712
859
  }
713
860
  return { document: JSON.parse(text) as JxMutableNode };
714
861
  });
715
- configureCollabNotifier(statusMessage);
862
+ // The source-canonical freeze is a STATE the author is being held in, not an error: a toast that
863
+ // Says so, keyed so a run of freezes is one message rather than a stack of identical ones.
864
+ configureCollabNotifier((message) => {
865
+ notify.warn(message, { key: "collab.freeze", source: "Collaboration" });
866
+ });
716
867
 
717
868
  let fsUnsub: (() => void) | null = null;
718
869
  /** (Re)subscribe the sidebar to backend filesystem events for the active project. */
719
870
  function ensureFsSync() {
720
871
  fsUnsub?.();
721
- fsUnsub = startFsSync({ onContentChange: reloadCleanTab, renderLeftPanel });
872
+ fsUnsub = startFsSync({
873
+ /* The four caches keyed on the project's file listing. Each one is derived — the pages tree
874
+ behind the Link-target picker, the layouts listing plus the effective layout's `$head`, the
875
+ `$paths` value enumerations, and the context bar's own memo of the enumerations it has
876
+ already asked for — so a file appearing or disappearing is the only event that can make any
877
+ of them wrong, and it is the same event for all four. The bar's memo is separate from
878
+ `page-params`' because it is keyed per DOCUMENT rather than globally; leaving it behind kept
879
+ a stale candidate list in the picker after the collection changed. */
880
+ invalidateDerivedCaches: () => {
881
+ invalidatePageRouteCache();
882
+ invalidateLayoutPickerCache();
883
+ invalidateParamValues();
884
+ paneContext.resetParamValues();
885
+ },
886
+ onContentChange: reloadCleanTab,
887
+ renderLeftPanel,
888
+ });
722
889
  }
723
890
 
724
891
  const _urlParams = new URLSearchParams(location.search);
@@ -751,13 +918,19 @@ if (_projectParam) {
751
918
  _projectParam.startsWith("~") ||
752
919
  /^[A-Za-z]:[/\\]/.test(_projectParam);
753
920
  if (!isAbsPath) {
754
- statusMessage(`Error: ?project= requires an absolute path (got "${_projectParam}")`);
921
+ notify.error(`?project= requires an absolute path (got "${_projectParam}").`, {
922
+ key: "startup.projectParam",
923
+ source: "Startup",
924
+ });
755
925
  render();
756
926
  } else {
757
927
  render();
758
928
  const platform = getPlatform();
759
929
  // oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: project probing must not block the initial render
760
930
  void (async () => {
931
+ // Hoisted out of the try so the failure can NAME what it failed to open. The catch used to
932
+ // Report a bare `Error: <message>` because the only identifying fact was still block-scoped.
933
+ let fileRelPath = _urlParams.get("file") || _projectParam;
761
934
  try {
762
935
  const siteCtx = platform.resolveSiteContext
763
936
  ? await platform.resolveSiteContext(_projectParam)
@@ -817,9 +990,39 @@ if (_projectParam) {
817
990
  void maybePromptJxsuiteUpdate(siteCtx.sitePath);
818
991
  }
819
992
 
993
+ /*
994
+ * THE SESSION — unless the URL named a document.
995
+ *
996
+ * A named document is an instruction and wins, whether it came from `?file=` or from a
997
+ * `?project=` that pointed INTO the project. A bare `?project=<dir>` is "open this
998
+ * project", and what that means is the documents it was last left with (§4.4) — the
999
+ * `project.json → home page` redirect below is the answer this replaces.
1000
+ *
1001
+ * This branch opens inline and never went near `openHomePage`, so it was the one door of
1002
+ * four the session work would have missed, and it is the door a browser reload comes
1003
+ * through.
1004
+ *
1005
+ * It returns when something LANDED — a restored session, or the home page
1006
+ * `openLastSessionOrHome` falls back to. A project with neither (no session, no
1007
+ * `pages/index.*`) falls through to the inline open below, which is what opens
1008
+ * `project.json` itself for a project that has nothing else to show.
1009
+ */
1010
+ const named =
1011
+ _urlParams.get("file") ||
1012
+ (siteCtx.fileRelPath && !siteCtx.fileRelPath.endsWith("project.json")
1013
+ ? siteCtx.fileRelPath
1014
+ : null);
1015
+ if (!named) {
1016
+ await openLastSessionOrHome();
1017
+ if (activeTab.value) {
1018
+ render();
1019
+ renderLeftPanel();
1020
+ return;
1021
+ }
1022
+ }
1023
+
820
1024
  // Read and open the file
821
- const _fileParam = _urlParams.get("file");
822
- let fileRelPath = _fileParam || siteCtx.fileRelPath || _projectParam;
1025
+ fileRelPath = named ?? siteCtx.fileRelPath ?? _projectParam;
823
1026
 
824
1027
  // When opening project.json, default to home page instead (listing-based, no 404 probes).
825
1028
  if (fileRelPath === "project.json" || fileRelPath.endsWith("/project.json")) {
@@ -828,7 +1031,9 @@ if (_projectParam) {
828
1031
 
829
1032
  const content = await platform.readFile(fileRelPath);
830
1033
  if (content) {
831
- let frontmatter, parsedDoc, parsedMode;
1034
+ let frontmatter;
1035
+ let parsedDoc;
1036
+ let parsedMode;
832
1037
  await loadFormats();
833
1038
  const fileFormat = formatForPath(fileRelPath);
834
1039
  if (fileFormat || !fileRelPath.endsWith(".json")) {
@@ -859,10 +1064,19 @@ if (_projectParam) {
859
1064
  }
860
1065
 
861
1066
  render();
862
- statusMessage(`Opened ${fileRelPath}`);
1067
+ // Opening a file is stated by the tab strip and the status bar's DOCUMENT field.
863
1068
  }
1069
+ /* This window may now write its session (§4.4). The `?file=` branch above opens inline and
1070
+ never reaches `openLastSessionOrHome`, which is the other place that says this — so
1071
+ without it a window opened at a named file would restore other windows' sessions and
1072
+ never record its own. */
1073
+ markSessionRestored(workspace.projectRoot);
864
1074
  } catch (error) {
865
- statusMessage(`Error: ${errorMessage(error)}`);
1075
+ notify.error(`Could not open ${fileRelPath || "the project"}.`, {
1076
+ detail: errorMessage(error),
1077
+ path: fileRelPath,
1078
+ source: "Open File",
1079
+ });
866
1080
  }
867
1081
  })();
868
1082
  }
@@ -911,28 +1125,73 @@ function renderLeftPanel() {
911
1125
  function loadProject() {
912
1126
  return _loadProject();
913
1127
  }
914
- async function openProject() {
1128
+ /**
1129
+ * Open a project, in the window the user asked for, and report what actually happened.
1130
+ *
1131
+ * THE TARGET IS HONOURED HERE OR NOWHERE. `openProjectFlow` asks the question and this is the
1132
+ * bootstrap's side of that contract — which it did not hold up: the hook was wired as `openProject:
1133
+ * () => openProject()` against a function that took no target, so "New Window" was collected,
1134
+ * dropped on the floor, and fell through to the line below that re-roots THIS window. The dialog
1135
+ * worked, the answer was discarded, and picking a project in the file browser replaced the project
1136
+ * the user had just said to keep.
1137
+ */
1138
+ async function openProject(target: ProjectOpenTarget = "thisWindow"): Promise<ProjectOpenOutcome> {
915
1139
  // Repo-list platforms (cloud) pick from the user's writable repositories instead of a
916
1140
  // Backend dialog; the choice opens through the same path as a recent project.
917
1141
  if (platformUsesRepoPicker()) {
918
1142
  const picked = await openProjectPickerModal();
919
- if (picked) {
920
- // The catalogue may have gained an entry; refresh it before navigating into the project.
921
- void hydrateProjectList().then(() => {
922
- render();
923
- });
924
- void openRecentProject(picked.root);
1143
+ if (!picked) {
1144
+ return "cancelled";
925
1145
  }
926
- return;
1146
+ // The catalogue may have gained an entry; refresh it before navigating into the project.
1147
+ void hydrateProjectList().then(() => {
1148
+ render();
1149
+ });
1150
+ void openRecentProject(picked.root);
1151
+ return "opened";
927
1152
  }
928
- const result = await _openProject({
929
- renderActivityBar: () => renderActivityBar(),
930
- renderLeftPanel,
931
- });
1153
+ const platform = getPlatform();
1154
+ // ELSEWHERE, and nothing here closes: no `confirmCloseAll`, no `replaceAllTabs`, no re-rooting.
1155
+ // `pickProject` exists precisely so this branch can ask which project without `openProject()`'s
1156
+ // Side effect of binding the asking window to the answer. The window that opens loads it, adds
1157
+ // Its own recent-projects entry, and this one carries on untouched.
1158
+ if (target === "newWindow" && platform.pickProject && platform.openProjectInNewWindow) {
1159
+ const picked = await platform.pickProject();
1160
+ if (!picked) {
1161
+ return "cancelled";
1162
+ }
1163
+ const { focused } = await platform.openProjectInNewWindow(picked.root);
1164
+ return focused ? "focused" : "newWindow";
1165
+ }
1166
+ // The SECOND destroyer, and it was ungated for as long as the first one was. `openRecentProject`
1167
+ // Below asks `confirmCloseAll` and then calls `closeAllTabs`; this branch reaches
1168
+ // `files.ts`'s `replaceAllTabs`, which throws the same documents away by a different name — so an
1169
+ // Enumeration of "who calls closeAllTabs" reported the matrix complete while ⌘O, the toolbar
1170
+ // Button and the welcome screen all discarded unsaved work in silence. `platformUsesRepoPicker()`
1171
+ // Is true only for the cloud platform, so desktop and browser both arrived here.
1172
+ if (!(await tabStrip.confirmCloseAll("Opening another project"))) {
1173
+ return "cancelled";
1174
+ }
1175
+ const opened = await _openProject({ renderLeftPanel });
932
1176
  ensureFsSync();
933
- return result;
1177
+ return opened ? "opened" : "cancelled";
934
1178
  }
935
1179
  async function openRecentProject(root: string) {
1180
+ // One entry for the whole sequence, not three surfaces for its three phases. Opening a project
1181
+ // Used to chain a blocking spinner (dependencies), a transient status line (git sync) and a
1182
+ // Confirm-plus-spinner (@jxsuite update), none of them cancellable and none of them surviving
1183
+ // The frame they were drawn in — so an open that took forty seconds was indistinguishable from
1184
+ // One that had silently failed. Spec studio.md §16.4.
1185
+ const activity = beginActivity({
1186
+ title: `Opening ${root.split("/").pop() || root}`,
1187
+ source: "Open Project",
1188
+ steps: [
1189
+ "Sync with the remote",
1190
+ "Install dependencies",
1191
+ "Read the project",
1192
+ "Open the home page",
1193
+ ],
1194
+ });
936
1195
  try {
937
1196
  const platform = getPlatform();
938
1197
 
@@ -940,6 +1199,24 @@ async function openRecentProject(root: string) {
940
1199
  // Window (focusing an existing window if it's already open) rather than replacing this project.
941
1200
  if (projectState && platform.openProjectInNewWindow) {
942
1201
  await platform.openProjectInNewWindow(root);
1202
+ activity.done("Opened in another window");
1203
+ return;
1204
+ }
1205
+
1206
+ /* THE LAST UNGUARDED DESTROYER, and it is the biggest one.
1207
+ Past this point the window replaces its project, and `closeAllTabs()` below disposes every
1208
+ open document — dirty or not, with no prompt anywhere on the path. ⌘W, the tab ×, quitting
1209
+ and the preview slot's replacement each acquired a gate; this one, which throws away the
1210
+ whole workspace at once, never had one.
1211
+ Asked HERE rather than lower down because everything below it is one-way: `setWindowProject`
1212
+ binds this window's backend to the new root, `platform.projectRoot` moves the base every
1213
+ relative path resolves against, and `resetProjectShell` drops the surfaces that describe the
1214
+ project being left. A prompt after any of those can be answered "keep editing" and leave the
1215
+ app pointing at a project it is not showing. The one cost of asking first is the
1216
+ already-open-elsewhere case just below, where the switch turns out to be a window focus and
1217
+ nothing is closed: a redundant question, and never a lost document. */
1218
+ if (!(await tabStrip.confirmCloseAll("Opening another project"))) {
1219
+ activity.done("Kept the current project");
943
1220
  return;
944
1221
  }
945
1222
 
@@ -948,11 +1225,17 @@ async function openRecentProject(root: string) {
948
1225
  if (platform.setWindowProject) {
949
1226
  const res = await platform.setWindowProject(root);
950
1227
  if (res.deduped) {
1228
+ activity.done("Already open in another window");
951
1229
  return;
952
1230
  }
953
1231
  }
954
1232
 
955
1233
  platform.projectRoot = root;
1234
+ // Source control, the stylebook selection and the settings tab describe the project being
1235
+ // Left behind; carrying them over showed the previous repository's branch and file count
1236
+ // Under the new project's name. The poll timer is re-armed by the panel's next render.
1237
+ cleanupGitPanel();
1238
+ resetProjectShell();
956
1239
  // The format registry is cached per project — the previous root's registry (often empty on a
957
1240
  // Fresh desktop launch) must not answer for this project, or non-JSON documents fail with
958
1241
  // "No format class imported" until a reload. Mirrors openProject in files.ts.
@@ -977,8 +1260,11 @@ async function openRecentProject(root: string) {
977
1260
  });
978
1261
  setWorkspaceProject(root, config);
979
1262
 
1263
+ activity.step("Sync with the remote");
980
1264
  await autoSyncProjectOnOpen();
1265
+ activity.step("Install dependencies");
981
1266
  await ensureDependenciesInstalled();
1267
+ activity.step("Read the project");
982
1268
  await loadDirectory(".");
983
1269
  await loadComponentRegistry();
984
1270
 
@@ -1000,13 +1286,14 @@ async function openRecentProject(root: string) {
1000
1286
  }
1001
1287
 
1002
1288
  addRecentProject(requireProjectState().name, root);
1003
- view.leftTab = "files";
1004
- renderActivityBar();
1289
+ setActivityTab("files");
1005
1290
  renderLeftPanel();
1006
- statusMessage(`Opened project: ${requireProjectState().name}`);
1291
+ // The project's name is now permanent state in the status bar's PROJECT field.
1007
1292
 
1008
- await openHomePage();
1293
+ activity.step("Open the home page");
1294
+ await openLastSessionOrHome();
1009
1295
  ensureFsSync();
1296
+ activity.done(`Opened ${requireProjectState().name}`);
1010
1297
  void maybePromptJxsuiteUpdate(root);
1011
1298
  } catch (error) {
1012
1299
  // The project likely moved or was deleted — drop the stale entry so it stops cluttering the
@@ -1014,7 +1301,10 @@ async function openRecentProject(root: string) {
1014
1301
  removeRecentProject(root);
1015
1302
  toolbarPanel.render();
1016
1303
  render();
1017
- statusMessage(`Error: ${errorMessage(error)}`);
1304
+ // `fail` raises the Problem, so this path does NOT also notify: an operation with an Activity
1305
+ // Entry reports once (`studio-ui-guidelines.md` §13.3 rule 3).
1306
+ activity.log(errorMessage(error));
1307
+ activity.fail(`Could not open the project at ${root}.`, { path: root });
1018
1308
  }
1019
1309
  }
1020
1310
  function renderFilesTemplate() {
@@ -1028,17 +1318,171 @@ function openFileFromTree(path: string) {
1028
1318
  return openFileInTab(path);
1029
1319
  }
1030
1320
 
1031
- // ─── Keyboard shortcuts ───────────────────────────────────────────────────────
1032
- initShortcuts(() => ({
1033
- applyTransform,
1034
- canvasMode: getCanvasMode(),
1035
- openProject,
1036
- panX: view.panX,
1037
- panY: view.panY,
1038
- saveFile,
1039
- setPan: (x, y) => {
1040
- view.panX = x;
1041
- view.panY = y;
1042
- view.needsCenter = false;
1321
+ // ─── Commands and the keyboard ────────────────────────────────────────────────
1322
+
1323
+ /**
1324
+ * Pointer/pan state for ONE pane's stage, read fresh on every gesture.
1325
+ *
1326
+ * Takes the surface rather than resolving `view.panX` / `getCanvasMode()`, both of which answered
1327
+ * for the focused pane: a wheel over the side pane wrote the primary's pan offsets and asked the
1328
+ * primary's mode whether panning was even allowed.
1329
+ */
1330
+ const stageContext = (surface: CanvasSurface) => ({
1331
+ applyTransform: () => applyTransform(surface),
1332
+ canvasMode: canvasModeOfPane(surface.paneId),
1333
+ panX: surface.panX,
1334
+ panY: surface.panY,
1335
+ setPan: (x: number, y: number) => {
1336
+ surface.panX = x;
1337
+ surface.panY = y;
1338
+ surface.needsCenter = false;
1339
+ },
1340
+ });
1341
+
1342
+ /**
1343
+ * The registry the keyboard dispatches through.
1344
+ *
1345
+ * Built here, in the bootstrap, because `createCommandRegistry` deliberately has no module-level
1346
+ * singleton: the context it closes over is this window's, and a second window gets its own.
1347
+ */
1348
+ const commandRegistry = createCommandRegistry({
1349
+ getContext: createLiveContext({
1350
+ aiConfigured: hasAiCredentials,
1351
+ // The probe `live-context.ts` declared optional and nobody ever passed, so `ctx.ai.streaming`
1352
+ // Read false forever. `assistant.stop` is gated on it.
1353
+ aiStreaming: isAssistantStreaming,
1354
+ canvasMode: getCanvasMode,
1355
+ isCaretActive,
1356
+ isModalOpen,
1357
+ platform: () => (hasPlatform() ? getPlatform() : null),
1358
+ }),
1359
+ });
1360
+
1361
+ registerStudioCommands(
1362
+ commandRegistry,
1363
+ {
1364
+ // The toolbar owns this, and the difference matters on the desktop: its `openUrlExternally`
1365
+ // Hands the URL to the launcher's preview-navigate handler — the OS browser — where a bare
1366
+ // `window.open` would open a webview with no address bar. The browser build falls back to a
1367
+ // New tab either way.
1368
+ openInBrowser: () => toolbarPanel.runOpenInBrowser(),
1369
+ openProject,
1370
+ // Wrapped rather than passed by reference: `saveFile` takes an optional tab and reports
1371
+ // Whether the bytes landed, and a hook that neither supplies one nor reads the answer must not
1372
+ // Silently forward its own first argument as the tab to save.
1373
+ saveDocument: async () => {
1374
+ await saveFile();
1375
+ },
1376
+ },
1377
+ stageContext,
1378
+ );
1379
+
1380
+ // Tab navigation (⌃Tab MRU cycling, ⌘⇧T reopen-closed) is defined beside the tab model it drives.
1381
+ registerTabCommands(commandRegistry, { openFile: openFileInTab, openFileInPane });
1382
+
1383
+ /* The derivation's own two verbs (§18.4), beside the `PaneDerivation` they read and write rather
1384
+ than in `paneCommands()`. `tests/app-commands-composition.test.ts` is the guard that this line
1385
+ and `appCommandSet()`'s entry stay in step. */
1386
+ commandRegistry.registerAll(derivationCommands(derivationDeps));
1387
+
1388
+ /*
1389
+ * The rest of the app's contribution points, each defined beside the state it writes.
1390
+ *
1391
+ * This block is the bootstrap's whole share of plan §13's registry work: every record below lives
1392
+ * in the module that implements it, and this is the ONE place that composes them into the registry
1393
+ * `__jxAutomation.run` projects. Nothing here decides what a command is called, when it is
1394
+ * available or what it does — that would be the second definition site the design exists to
1395
+ * prevent (plan §2, principle 1).
1396
+ */
1397
+ registerShellViewCommands(commandRegistry, {
1398
+ // The registry's own answer, so a gated-off panel cannot be persisted as a showing tab.
1399
+ panelAvailable: (id) => {
1400
+ const panel = getPanel(id);
1401
+ return panel ? (panel.when?.(commandRegistry.context()) ?? true) : true;
1043
1402
  },
1044
- }));
1403
+ inspectorTab: () => rightPanelMod.inspectorTab(),
1404
+ setInspectorTab: (tab) => rightPanelMod.setInspectorTab(tab),
1405
+ });
1406
+ registerCanvasViewCommands(commandRegistry, {
1407
+ getCanvasMode,
1408
+ renderPane: renderCanvas,
1409
+ setCanvasMode,
1410
+ setResolvingOpen: paneContext.setResolvingOpen,
1411
+ });
1412
+ registerSelectionSetCommand(commandRegistry);
1413
+ registerInspectorCommands(commandRegistry);
1414
+ /* Search appearance, behind one record with two buttons: the Document Header card's and the Page
1415
+ panel's. A surface that IS the capability is one the palette cannot reach. */
1416
+ registerSeoCommands(commandRegistry);
1417
+ registerA11yCommands(commandRegistry);
1418
+ /* The element menu's eight verbs, in the APP registry rather than only in the popover's own. They
1419
+ have always declared `menus: ["context/element", "palette"]`; the palette has never listed one,
1420
+ because the only registry holding them was the one `editor/context-menu.ts` builds for itself.
1421
+ Their target falls back to the selection when no menu is open (`commandTarget`), which is what
1422
+ makes "Paste Style" a keyboard-reachable verb rather than a right-click-only one. */
1423
+ commandRegistry.registerAll(liveElementCommands());
1424
+ /* …and the navigation seam for the popover's FALLBACK registry, which it uses only in the window
1425
+ before this line runs (`contextMenuRegistry()` returns the app's registry once one exists). A
1426
+ fallback holding a no-op where navigation belongs is a registry that lies. */
1427
+ setContextMenuNavigate(navigateToComponent);
1428
+ registerDataExplorerCommands(commandRegistry, { renderLeftPanel });
1429
+ registerSignalsCommands(commandRegistry);
1430
+ registerFormulaEditorCommands(commandRegistry);
1431
+ registerGridCommands(commandRegistry);
1432
+ registerSettingsCommands(commandRegistry);
1433
+ registerPreferencesCommands(commandRegistry);
1434
+ registerLibraryCommands(commandRegistry);
1435
+ registerContentCommands(commandRegistry);
1436
+ /* The four translation verbs: open a sibling translation, create the missing one, show the
1437
+ Languages panel, declare a language. `i18n.switchLocale` is deliberately not among them — it sets
1438
+ a rendering context, so it lives with the other axis-3 verbs in `canvasViewCommands`. */
1439
+ registerI18nCommands(commandRegistry);
1440
+ registerNewProjectCommands(commandRegistry);
1441
+ registerStyleCommands(commandRegistry);
1442
+ registerSourceControlCommands(commandRegistry);
1443
+ registerPublishCommands(commandRegistry);
1444
+ registerGridViewCommands(commandRegistry);
1445
+ registerRedirectsCommands(commandRegistry);
1446
+ registerAboutCommands(commandRegistry);
1447
+ registerCollabCommands(commandRegistry);
1448
+ /* The `Assistant:` family (§11.1) — Focus Composer, New Chat, Chat History, Attach Selection, Retry
1449
+ and Stop. Every one existed as a button in the chat view and as nothing else, so the category held
1450
+ zero records and none of them was in the palette, bindable, or reachable by name. The chat header
1451
+ and the error row render these ids through the registry now, which is what makes this line the
1452
+ definition site rather than a second copy. */
1453
+ commandRegistry.registerAll(assistantCommands());
1454
+ /*
1455
+ * The structural selection verbs — Move Up/Down/In/Out, Convert to Component, Edit Component.
1456
+ *
1457
+ * They were already defined in `panels/block-action-bar.ts` beside the mutations they perform, but
1458
+ * only ever registered into that panel's OWN registry, so the palette, the keyboard and
1459
+ * `__jxAutomation` could not see them. `commandTargetPath()` falls back to the current selection
1460
+ * when no menu is open, which is precisely the app-wide meaning of these verbs.
1461
+ */
1462
+ registerSelectionCommands(commandRegistry, { convertToComponent, navigateToComponent });
1463
+ /* The format family — Bold, Italic, Code, Link and the four with no chord.
1464
+ Same story as the structural verbs above, one wave later: they were the block action bar's own,
1465
+ as a hand-written keydown switch rather than records, and the switch returned early whenever
1466
+ focus was inside the canvas iframe — which is the only place a canvas caret can be. */
1467
+ commandRegistry.registerAll(formatCommands());
1468
+
1469
+ initShortcuts(commandRegistry, stageContext);
1470
+
1471
+ /* Hand the canvas frames the chord table.
1472
+ The iframe cannot see the registry, so it used to answer "does the parent want this keystroke?"
1473
+ from three hand-written lists that disagreed with the registry in both directions — ⌘A forwarded
1474
+ and prevented with nothing to run it, ⌘B withheld for an engine that never handled it. It
1475
+ resolves against this table instead, and `publishKeymap()` reposts it whenever a rebinding lands,
1476
+ which is what makes Preferences › Keyboard reach inside the page. */
1477
+ setKeymapSource(
1478
+ () => ({
1479
+ chords: chordsInScopes(commandRegistry.keymap, FRAME_KEY_SCOPES),
1480
+ mac: commandRegistry.keymap.mac,
1481
+ }),
1482
+ (listener) => commandRegistry.keymap.onChange(listener),
1483
+ );
1484
+
1485
+ // The gated scripting surface (`?automation=1` only) is a PROJECTION of the registry above, so it
1486
+ // Installs after it — still inside this module's synchronous body, which is what the screenshot
1487
+ // Runner's `waitForFunction(() => window.__jxAutomation)` and every deferred project load depend on.
1488
+ installAutomationHook({ registry: commandRegistry, seedAssistantMessages, seedPublishConnected });