@jxsuite/studio 2.0.1 → 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-799jpvhm.js → components-2bpqp4mq.js} +5 -7
  7. package/dist/chunks/{components-799jpvhm.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-8jxwts2p.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-gs6kqdt1.js → studio-adn0z1k3.js} +14 -8
  74. package/dist/chunks/{studio-gs6kqdt1.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-esrewxap.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-v7jkjqng.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-jm0z592w.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 +21080 -32585
  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 +7 -0
  214. package/src/new-project/location-fields.ts +9 -1
  215. package/src/new-project/new-project-modal.ts +221 -245
  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 -53
  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 +30 -1
  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 +7 -1
  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-qr3drkkw.js +0 -531
  363. package/dist/chunks/extension-sections-qr3drkkw.js.map +0 -12
  364. package/dist/chunks/files-j8a1t06k.js +0 -50
  365. package/dist/chunks/files-j8a1t06k.js.map +0 -9
  366. package/dist/chunks/git-panel-xtq5z81x.js +0 -28
  367. package/dist/chunks/git-panel-xtq5z81x.js.map +0 -9
  368. package/dist/chunks/layers-panel-ancn2wpq.js +0 -26
  369. package/dist/chunks/layers-panel-ancn2wpq.js.map +0 -9
  370. package/dist/chunks/settings-modal-17ttr96g.js +0 -29
  371. package/dist/chunks/settings-modal-17ttr96g.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-0xgs7xrs.js +0 -429
  381. package/dist/chunks/studio-0xgs7xrs.js.map +0 -13
  382. package/dist/chunks/studio-4d8hzcz5.js +0 -3427
  383. package/dist/chunks/studio-4d8hzcz5.js.map +0 -26
  384. package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
  385. package/dist/chunks/studio-9smfzqhc.js +0 -19
  386. package/dist/chunks/studio-9smfzqhc.js.map +0 -10
  387. package/dist/chunks/studio-jm0z592w.js.map +0 -115
  388. package/dist/chunks/studio-m2t4mdg5.js +0 -1854
  389. package/dist/chunks/studio-m2t4mdg5.js.map +0 -18
  390. package/dist/chunks/studio-n4p11dfy.js +0 -853
  391. package/dist/chunks/studio-n4p11dfy.js.map +0 -12
  392. package/dist/chunks/studio-qkvt37c0.js.map +0 -9
  393. package/dist/chunks/studio-sjh1wt1a.js +0 -3744
  394. package/dist/chunks/studio-sjh1wt1a.js.map +0 -22
  395. package/dist/chunks/studio-v7jkjqng.js.map +0 -46
  396. package/dist/chunks/studio-xq0t7n5t.js +0 -140
  397. package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
  398. package/dist/chunks/studio-xze4zrs1.js.map +0 -10
  399. package/dist/chunks/studio-yfcstdzy.js +0 -399
  400. package/dist/chunks/studio-yfcstdzy.js.map +0 -12
  401. package/dist/chunks/studio-zp7jncs8.js +0 -469
  402. package/dist/chunks/studio-zp7jncs8.js.map +0 -14
  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-8jxwts2p.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-esrewxap.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
@@ -1,33 +1,80 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Editor panels — extracted from studio.js (Phase 4g). Monaco-based function editor (JS mode) and
4
- * completion provider for state scope variables.
3
+ * The Logic dock's CODE surface — the Monaco function-body editor and its state-scope completions.
4
+ *
5
+ * It used to take over the canvas: `renderFunctionEditor` cleared `canvasWrap`, dropped every
6
+ * canvas panel and mounted Monaco over the stage, so the page whose handler you were writing was
7
+ * the one thing you could not see while writing it. It is a surface of the Bottom dock's **Logic**
8
+ * tab now (plan §12 P8.5); `panels/formula-workspace.ts` owns that tab's record and calls the three
9
+ * exports below — {@link functionEditorTemplate} for the container, {@link syncFunctionEditor} from
10
+ * the panel's `afterRender`, and {@link closeFunctionEditor} for its Close. The canvas keeps
11
+ * rendering the page underneath the dock; `canvas/canvas-render.ts` no longer knows this surface
12
+ * exists.
13
+ *
14
+ * **The mount is driven by the DOM, not by a render call.** A dock tab's body is re-rendered by lit
15
+ * whenever anything it reads changes, and lit will happily replace the container element out from
16
+ * under a live Monaco instance (switching to the formula surface and back does exactly that). So
17
+ * `syncFunctionEditor` asks the one question that matters — is the editor I hold still inside the
18
+ * container I was just handed? — and rebuilds when the answer is no. Re-checking the target string
19
+ * alone, which is all the takeover ever did, would have left a detached editor holding the user's
20
+ * unsaved body.
21
+ *
22
+ * **The move changed the REPAINT rate, not just the teardown rate, and the repaint is the one that
23
+ * runs with the user's hands on the keyboard.** The dock's `afterRender` effect tracks every badge
24
+ * in the strip — Problems' count, Activity's running list, Source Control's file count, which git
25
+ * polls every 30 seconds — so this module's sync runs on events that have nothing to do with the
26
+ * code surface. Every write below therefore asks `services/monaco-buffer.ts` whether the buffer has
27
+ * moved on first. Identity ("is this still the same editor?") is the OTHER question, still asked
28
+ * where it applies, and it is passed by a repaint into a live editor mid-word.
5
29
  */
6
30
 
7
31
  import type * as monaco from "monaco-editor";
8
- import { loadMonaco } from "../services/monaco-lazy";
32
+ import { loadMonaco, mountStillWanted } from "../services/monaco-lazy";
9
33
  import { isJsonObject } from "@jxsuite/schema/guards";
10
- import { html, render as litRender, nothing } from "lit-html";
11
- import { ref } from "lit-html/directives/ref.js";
34
+ import { html } from "lit-html";
12
35
 
13
- import { canvasPanels, canvasWrap, getNodeAtPath, renderOnly } from "../store";
14
- import { activeTab } from "../workspace/workspace";
36
+ import { getNodeAtPath, renderOnly, updateUi } from "../store";
37
+ import { activeTab, tabIsLive } from "../workspace/workspace";
15
38
  import { mutateUpdateDef, mutateUpdateProperty, transactDoc } from "../tabs/transact";
16
39
  import { view } from "../view";
17
40
  import { codeService, getFunctionArgs, setLintMarkers } from "../services/code-services";
41
+ import {
42
+ BUFFER_COMMIT,
43
+ bufferIsLive,
44
+ bufferMovedOn,
45
+ bufferWrites,
46
+ cancelBufferWrites,
47
+ commitBufferWrites,
48
+ } from "../services/monaco-buffer";
18
49
  import { globalEntries, namedFormulaEntries } from "../ui/formula-catalog";
50
+ import { notify } from "../services/notify";
19
51
 
52
+ import type { BufferWrites } from "../services/monaco-buffer";
20
53
  import type { OxLintDiagnostic } from "../services/code-services";
21
- import type { JxMutableNode, JxPrototypeDef } from "@jxsuite/schema/types";
54
+ import type { JxPrototypeDef } from "@jxsuite/schema/types";
22
55
  import type { JxPath } from "../state";
56
+ import type { Tab } from "../tabs/tab";
57
+ import type { TemplateResult } from "lit-html";
23
58
 
24
59
  type EditingTarget =
25
60
  | { type: "def"; defName: string }
26
61
  | { type: "event"; path: JxPath; eventKey: string };
27
62
 
28
- /** @param {EditingTarget | null | undefined} editing */
29
- function getFunctionBody(editing: EditingTarget | null | undefined) {
30
- const document = activeTab.value?.doc.document;
63
+ /** The Monaco instance plus the four fields this module hangs off it (declared in `../view`). */
64
+ type FunctionEditor = NonNullable<typeof view.functionEditor>;
65
+
66
+ /**
67
+ * The body `editing` names, read out of `tab`'s document.
68
+ *
69
+ * The tab is a PARAMETER, not `activeTab.value`. Every read and every write in this module is about
70
+ * one document — the one whose editor is mounted — and the focused tab is only incidentally that
71
+ * document.
72
+ *
73
+ * @param {Tab | null | undefined} tab
74
+ * @param {EditingTarget | null | undefined} editing
75
+ */
76
+ function getFunctionBody(tab: Tab | null | undefined, editing: EditingTarget | null | undefined) {
77
+ const document = tab?.doc.document;
31
78
  // Read body off any object-shaped def (covers legacy entries without $prototype).
32
79
  const bodyOf = (def: unknown) =>
33
80
  isJsonObject(def) && typeof def.body === "string" ? def.body : "";
@@ -40,87 +87,335 @@ function getFunctionBody(editing: EditingTarget | null | undefined) {
40
87
  return "";
41
88
  }
42
89
 
43
- export function renderFunctionEditor() {
44
- const editing = activeTab.value?.session.ui.editingFunction as EditingTarget | null | undefined;
90
+ /** Where Monaco goes inside the Logic tab. Empty on purpose — the editor fills it imperatively. */
91
+ export function functionEditorTemplate(): TemplateResult {
92
+ return html`<div class="fw-code"></div>`;
93
+ }
45
94
 
46
- // If editor already exists and matches current target, just sync value
47
- if (view.functionEditor && view.functionEditor._editingTarget === JSON.stringify(editing)) {
48
- const body = getFunctionBody(editing);
49
- const currentVal = view.functionEditor.getValue();
50
- if (currentVal !== body) {
51
- view.functionEditor._ignoreNextChange = true;
52
- view.functionEditor.setValue(body);
95
+ /**
96
+ * Create, re-target or tear down the Monaco instance for the Logic tab's current body.
97
+ *
98
+ * The panel's `afterRender` hook, so it runs against the DOM lit has just painted.
99
+ *
100
+ * @param {HTMLElement} host The panel body element the Logic tab was rendered into.
101
+ */
102
+ export function syncFunctionEditor(host: HTMLElement): void {
103
+ const tab = activeTab.value;
104
+ const editing = tab?.session.ui.editingFunction as EditingTarget | null | undefined;
105
+ const container = editing && tab ? host.querySelector<HTMLElement>(".fw-code") : null;
106
+ if (!container || !tab || !editing) {
107
+ // Either nothing is open in the code surface, or the formula surface is showing: whichever
108
+ // Monaco instance we hold is detached DOM, and holding it leaks a model plus its listeners.
109
+ disposeFunctionEditor();
110
+ return;
111
+ }
112
+
113
+ const current = view.functionEditor;
114
+ const attached = Boolean(current && container.contains(current.getDomNode() as Node | null));
115
+ /* THE TAB IS HALF OF THE IDENTITY, and the half that used to be missing.
116
+ `_editingTarget` is a JSON string built from the target alone, and the ordinary target
117
+ `{"eventKey":"onclick","path":["children",0],"type":"event"}` is the SAME string for the first
118
+ button on any two pages. Two tabs whose strings match would take this branch across a tab
119
+ switch, keeping an editor whose buffer holds the OTHER document's handler — and, once the
120
+ buffer is ahead, keeping it without even re-syncing. Comparing the tab by identity turns that
121
+ into the rebuild it always was. */
122
+ if (
123
+ current &&
124
+ attached &&
125
+ current._editingTab === tab &&
126
+ current._editingTarget === JSON.stringify(editing)
127
+ ) {
128
+ /* THE RE-SYNC, and the one thing it is not allowed to do.
129
+ This branch exists for the edit that arrives from somewhere else — an undo, a collaborator,
130
+ a rename that rewrote the body — and its job is to show it. The comparison it makes is
131
+ "buffer ≠ document", which is ALSO true of every keystroke between a keypress and the 500ms
132
+ commit; resolving that in the document's favour reverts what is being typed. And because the
133
+ dock repaints on Problems counts, Activity and a 30-second git poll, that window is hit by
134
+ events the author never connected to their editor: the word half-finished under the cursor
135
+ jumps back to what it was a moment ago, for no reason they can see.
136
+ `bufferMovedOn` is the difference between the two cases. An edit from elsewhere lands while
137
+ the buffer is settled; a keystroke does not. */
138
+ const body = getFunctionBody(tab, editing);
139
+ if (!bufferMovedOn(current) && current.getValue() !== body) {
140
+ current._ignoreNextChange = true;
141
+ current.setValue(body);
142
+ // The buffer just took the DOCUMENT's text, so the two agree again. Declared rather than
143
+ // Inferred: clause 3 is a fact the writer owns, and this write is the one that clears it.
144
+ current._writes?.markSettled();
53
145
  }
54
146
  return;
55
147
  }
56
148
 
57
- // Dispose previous editors
58
- if (view.functionEditor) {
59
- view.functionEditor.dispose();
149
+ disposeFunctionEditor();
150
+ container.textContent = "";
151
+ const body = getFunctionBody(tab, editing);
152
+ const args = getFunctionArgs(editing, tab.doc.document);
153
+ void mountFunctionEditor(container, tab, editing, body, args);
154
+ }
155
+
156
+ /**
157
+ * Drop the function editor, its model, and the debounced work armed over it. Safe to call when
158
+ * there is nothing to drop.
159
+ *
160
+ * **What happens to the armed commit is the whole correctness of the teardown.** Monaco's
161
+ * `dispose()` runs `_detachModel()`, and `CodeEditorWidget.getValue()` opens with `if
162
+ * (!this._modelData) return "";`. A disposed editor therefore answers the EMPTY STRING, not the
163
+ * buffer it was holding. The mount arms a 500ms timer that reads `getValue()` and writes it into
164
+ * the document, so every surviving timer was a scheduled deletion of the user's handler:
165
+ *
166
+ * - Type, then click Problems / the dock's × / ⌘\ to another pane — `syncFunctionEditor` disposes
167
+ * synchronously, and half a second later the body is `""` and the tab is dirty.
168
+ * - Worse, click **Close** — `closeFunctionEditor` minifies, writes the correct body, disposes, and
169
+ * the orphan timer overwrites that correct write with `""`. The action that exists to save the
170
+ * work was the one that destroyed it.
171
+ *
172
+ * The takeover never saw this because it was torn down by exactly one thing (closing it, which
173
+ * happened to be slower than the debounce). A dock tab is torn down by five, four of them new in
174
+ * P8. Handling it here rather than guarding inside each callback is deliberate: a guard on "is this
175
+ * still the current editor?" is passed by a timer that fires after a REMOUNT.
176
+ *
177
+ * **But cancelling alone is the same loss with the alarm switched off.** The armed commit IS the
178
+ * last half-second of typing; dropping it means the pre-typing body survives instead of an empty
179
+ * one, and `doc.dirty` stays `false`, so nothing anywhere says an edit went missing. Five ways out
180
+ * of the surface measured as silent loss — switching the dock tab, opening another target, opening
181
+ * a formula through `openLogicTarget`, collapsing the dock, and a same-target switch to another
182
+ * TAB. {@link commitBufferWrites} reads the buffer while it is still alive, writes it to the tab it
183
+ * was mounted for, and only then drops what is left. The order is the fix, not the call.
184
+ *
185
+ * **Seven, and the last two are not this function's.** ⌘W / the tab × and quitting destroy the TAB:
186
+ * `closeTab` deletes it from `workspace.tabs` before any repaint reaches this disposer, so the
187
+ * commit flushed here finds `tabIsLive(tab) === false` and correctly writes nothing. Those two are
188
+ * fixed at the gate that decides — `services/monaco-buffer.ts`'s `commitTabBuffers` runs the commit
189
+ * while the tab is still open, and `tabBufferUnsaved` answers for typing no commit could land.
190
+ *
191
+ * **AND THE COMMIT IS ALLOWED TO FAIL, at which point this is a deletion.** The collab freeze
192
+ * refuses the body write outright; a collaborator deleting the element the handler hangs off leaves
193
+ * `editing.path` resolving to nothing. Either way `commitBufferWrites` returns false, and the very
194
+ * next line detaches the model — so `buffersForTab` stops finding the buffer and `tabBufferUnsaved`
195
+ * goes back to answering "nothing to lose" about text that no longer exists anywhere.
196
+ *
197
+ * Refusing the teardown is not on offer here, which is why the answer is a message rather than a
198
+ * bail-out: every caller is a repaint or a mode transition that has ALREADY replaced the container
199
+ * (the dock's `afterRender`, `closeFunctionEditor`'s tail), so keeping the instance would keep a
200
+ * live Monaco bound to detached DOM — the leak `syncFunctionEditor` exists to close, and an editor
201
+ * the author cannot see or reach is not a rescued edit. `closeFunctionEditor` is where a refusal
202
+ * DOES keep the surface, because it is a gesture and the surface is still standing.
203
+ */
204
+ export function disposeFunctionEditor(): void {
205
+ const editor = view.functionEditor;
206
+ if (editor) {
207
+ commitBufferWrites(editor, "logic");
208
+ editor.dispose();
60
209
  view.functionEditor = null;
61
210
  }
62
- if (view.monacoEditor) {
63
- view.monacoEditor.dispose();
64
- view.monacoEditor = null;
65
- }
211
+ }
66
212
 
67
- // Clean up canvas DnD and event handlers
68
- for (const fn of view.canvasDndCleanups) {
69
- fn();
213
+ /**
214
+ * The one way a body is written into a document, bound to the tab and target it is for.
215
+ *
216
+ * Two callers used to carry their own copy of this def/event branch — the 500ms commit and the
217
+ * Close — and each resolved the tab it wrote to at CALL time, through `activeTab.value`. That is
218
+ * "whatever is focused now", which is the tab the buffer came from only when nothing happened in
219
+ * between. Bound here instead, at mount, so the two cannot answer differently and neither can
220
+ * answer with a document the buffer was never filled from.
221
+ *
222
+ * `tabIsLive` rather than "is it active": the write belongs to its tab whether or not that tab is
223
+ * the one on screen, and a tab that has been CLOSED is the one case where the write must not happen
224
+ * at all — nothing would ever read it, and `transactDoc` would still push history and dirty a
225
+ * document the user has already dismissed.
226
+ *
227
+ * **A WRITE IS ALLOWED TO BE REFUSED, and the buffer has to hear about it.** `transactDoc` consults
228
+ * the collab gate, which under the source-canonical freeze blocks every structural edit including
229
+ * the lock holder's own — so this returns having changed nothing. Settling the buffer anyway told
230
+ * `tabBufferUnsaved` (and therefore `shouldWarnOnClose` and `hasUnsavedTabs`) that the document had
231
+ * been given the author's text when it had not, and every subsequent close discarded it without a
232
+ * word. So the settle is conditional on the write LANDING, and the answer is handed back to the
233
+ * caller: the debounce has nothing useful to do with it (the gate raises its own keyed toast on
234
+ * every refusal), but the Close does — see {@link closeFunctionEditor} — and so does every
235
+ * disposer, through `commitBufferWrites`.
236
+ *
237
+ * **Three ways it does not land, not one.** The tab is gone; the collab gate refuses; or the node
238
+ * `editing.path` names has been deleted out from under the open editor, in which case there is no
239
+ * coordinate to write to at all.
240
+ *
241
+ * @param {Tab} tab The document this buffer was filled from.
242
+ * @param {EditingTarget} editing The def or event binding inside it.
243
+ * @param {BufferWrites} [writes] The buffer's debounce holder, when there is a buffer: a completed
244
+ * write is precisely what settles clause 3.
245
+ * @returns {(body: string) => boolean} True when the body reached the document.
246
+ */
247
+ function bodyWriter(tab: Tab, editing: EditingTarget, writes?: BufferWrites) {
248
+ return (newBody: string): boolean => {
249
+ if (!tabIsLive(tab)) {
250
+ return false;
251
+ }
252
+ let wrote = false;
253
+ if (editing.type === "def") {
254
+ wrote = transactDoc(tab, (t) => mutateUpdateDef(t, editing.defName, { body: newBody }));
255
+ } else if (editing.type === "event") {
256
+ const node = getNodeAtPath(tab.doc.document, editing.path);
257
+ /* THE ELEMENT THE HANDLER HANGS OFF CAN GO WHILE THE EDITOR IS OPEN — a collaborator's
258
+ delete arriving over the wire, or the author's own ⌘Z. `editing.path` then resolves to
259
+ nothing, and `mutateUpdateProperty` read `getNodeAtPath(...)[key]` straight through it:
260
+ `undefined is not an object`, thrown out of the 500ms commit and, through
261
+ `commitBufferWrites`, out of the dock panel's `afterRender`.
262
+ There is no coordinate left to write the body to, so the honest answer is that the write
263
+ did not happen. Reporting it as a success would settle the buffer and tell every close
264
+ gate the document had been given text that has nowhere to be. */
265
+ if (!node) {
266
+ return false;
267
+ }
268
+ const current = node[editing.eventKey] || {};
269
+ wrote = transactDoc(tab, (t) =>
270
+ mutateUpdateProperty(t, editing.path, editing.eventKey, {
271
+ ...(current as object),
272
+ $prototype: "Function",
273
+ body: newBody,
274
+ }),
275
+ );
276
+ }
277
+ // The document now holds what the buffer holds — clause 3 is cleared by the write that made it
278
+ // True, not by a timer expiring, and never by a write that was refused.
279
+ if (wrote) {
280
+ writes?.markSettled();
281
+ }
282
+ return wrote;
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Close the code surface: minify what is in the buffer, write it back, and clear the target.
288
+ *
289
+ * The write is the reason the Close is a real one rather than a tab switch. It lived in `studio.ts`
290
+ * beside the pane context bar's crumb, which was its only caller; the Logic tab's Close is the
291
+ * second, and a body-saving routine that two surfaces call belongs beside the editor whose buffer
292
+ * it reads.
293
+ *
294
+ * **The minify is an await, and a retarget fits inside it.** The buffer and the target are both
295
+ * read BEFORE it; the teardown after it acts on whatever is mounted then. Open A, click Close, and
296
+ * while `minify` is in flight click "Open in code editor" on B: the write still belongs to A and
297
+ * still lands on A, but the dispose-and-clear would have killed B's live editor and closed a
298
+ * document position the user had just asked for. So the write is unconditional and the TEARDOWN is
299
+ * conditional — they answer to different moments in time and only one of them is still A's.
300
+ *
301
+ * **But that is TWO obligations, and only one of them is the editor's.** Guarding the whole tail on
302
+ * `view.functionEditor === editor` conflated them, and the case it got wrong is the ordinary one:
303
+ * an editor torn down inside the minify and NOT retargeted — collapse the dock, select Problems, ⌘\
304
+ * to another pane — leaves `view.functionEditor` null, so the guard bailed and `editingFunction`
305
+ * was never cleared. Nothing else clears it, so the Logic tab stayed on the strip pointing at a
306
+ * body the user had just dismissed, and re-opening the dock restored the editor they closed.
307
+ * Disposing belongs to the instance; clearing the target belongs to the TARGET, and the only thing
308
+ * with a claim on the target is a retarget that already replaced it.
309
+ *
310
+ * **The write goes through the editor's own `_commitBody`, not through the focused tab.** That
311
+ * writer was bound at mount to the tab and target this buffer belongs to, so the Close and the
312
+ * debounce cannot land in different places. An editor handle this module did not mount carries no
313
+ * writer, and then the only meaning the gesture has left is its own tab and its own target — the
314
+ * same {@link bodyWriter}, bound to those.
315
+ *
316
+ * **AND THE WRITE CAN BE REFUSED, in which case there is nothing left to close over.** Under the
317
+ * source-canonical freeze the collab gate blocks the body commit, and this used to carry on:
318
+ * minify, refused write, `cancelBufferWrites` (the armed commit gone too), dispose, target cleared.
319
+ * The Close button — the one action whose entire purpose is to save the body — destroyed it, and
320
+ * the standing "frozen" chip in the presence strip does not say that a button just did nothing. A
321
+ * refusal therefore keeps the surface exactly as it is (the text stays on screen, the target stays
322
+ * open) and says why, once, through the same Problems/toast channel every other failed write uses.
323
+ * Nothing is re-armed: the buffer is still marked typed, so the close and quit gates already know
324
+ * the work exists.
325
+ */
326
+ export async function closeFunctionEditor(): Promise<void> {
327
+ const tab = activeTab.value;
328
+ const editing = tab?.session.ui.editingFunction as EditingTarget | null | undefined;
329
+ if (!tab || !editing) {
330
+ return;
70
331
  }
71
- view.canvasDndCleanups = [];
72
- for (const fn of view.canvasEventCleanups) {
73
- fn();
332
+ const editor = view.functionEditor;
333
+ if (editor) {
334
+ const currentCode = editor.getValue();
335
+ const minResult = await codeService("minify", { code: currentCode });
336
+ /* Cancelled AFTER the round trip, not before it. Either position stops the armed commit from
337
+ replaying raw text over the minified body, but cancelling first means a minify that REJECTS
338
+ takes the user's last keystrokes with it — there would be no armed commit left to write them.
339
+ Left armed, the worst case is that the timer fires mid-await and writes the same text to the
340
+ same target a moment before the minified version lands on top of it. */
341
+ cancelBufferWrites(editor);
342
+ const write = editor._commitBody ?? bodyWriter(tab, editing);
343
+ if (!write(minResult?.code ?? currentCode)) {
344
+ notify.warn(
345
+ "The handler could not be saved — source editing holds this document. Your code is still in the editor.",
346
+ {
347
+ key: "logic.close-refused",
348
+ source: "Logic",
349
+ ...(tab.documentPath ? { path: tab.documentPath } : {}),
350
+ },
351
+ );
352
+ return;
353
+ }
354
+ if (view.functionEditor !== null && view.functionEditor !== editor) {
355
+ return;
356
+ }
357
+ // A no-op when the teardown already happened, which is the whole point: the clear below still
358
+ // Runs, because a target with no editor is precisely the state nothing else resolves.
359
+ disposeFunctionEditor();
74
360
  }
75
- view.canvasEventCleanups = [];
76
- canvasPanels.length = 0;
77
-
78
- litRender(nothing, canvasWrap);
79
- canvasWrap.style.padding = "0";
80
- canvasWrap.style.flexDirection = "column";
81
- canvasWrap.style.alignItems = "stretch";
82
-
83
- // The tab bar renders the Back button + breadcrumb context for the function editor.
84
- let editorContainer: HTMLDivElement | null = null;
85
- litRender(
86
- html`<div class="source-wrap">
87
- <div
88
- class="source-editor"
89
- ${ref((el) => {
90
- if (el) {
91
- editorContainer = el as HTMLDivElement;
92
- }
93
- })}
94
- ></div>
95
- </div>`,
96
- canvasWrap,
97
- );
98
-
99
- const body = getFunctionBody(editing);
100
- const args = getFunctionArgs(
101
- /** @type {EditingTarget} */ editing as EditingTarget,
102
- activeTab.value?.doc.document,
103
- );
361
+ updateUi(activeTab.value, "editingFunction", null);
362
+ }
104
363
 
105
- void mountFunctionEditor(editorContainer as unknown as HTMLElement, body, args, editing);
364
+ /**
365
+ * Whether the mount started for `container` / `tab` / `editing` is still the one the app wants.
366
+ *
367
+ * The three clauses `services/monaco-lazy.ts` states, spelled for this surface: the container is
368
+ * still in the document, no other mount has already produced the editor, and the dock still wants
369
+ * exactly this tab and this target. Asked AFTER the Monaco load, because that load is a cold
370
+ * dynamic import of 12.6 MB and the user has hundreds of milliseconds to close the surface,
371
+ * retarget it or switch tabs inside it — each of which used to end with a live editor nobody could
372
+ * reach, assigned to `view.functionEditor` where the next sync would overwrite the handle rather
373
+ * than dispose it.
374
+ *
375
+ * `activeTab.value === tab` covers the tab having been closed as well as merely left: a closed tab
376
+ * is nobody's active tab, and the dock only ever shows the active one's target.
377
+ */
378
+ function functionMountWanted(container: HTMLElement, tab: Tab, editing: EditingTarget): boolean {
379
+ return mountStillWanted(container, view.functionEditor, () => {
380
+ const current = activeTab.value;
381
+ return (
382
+ current === tab &&
383
+ JSON.stringify(current.session.ui.editingFunction ?? null) === JSON.stringify(editing)
384
+ );
385
+ });
106
386
  }
107
387
 
108
388
  /**
109
- * Create the function-body editor once Monaco has loaded. The container is already in the DOM; a
110
- * teardown or mode switch during the load is caught by the `view.functionEditor` re-check in
111
- * `resetCanvasView`, which disposes whatever is current.
389
+ * Create the function-body editor once Monaco has loaded.
390
+ *
391
+ * The container is in the DOM when this is called and the target is current — but neither is still
392
+ * guaranteed on the far side of the `await`, which is why {@link functionMountWanted} is asked
393
+ * again there. Nothing else covers it: `syncFunctionEditor` runs from the dock's `afterRender`, so
394
+ * it cannot see a mount that has not produced an editor yet.
395
+ *
396
+ * **`tab` is captured here and never re-read.** Everything below — the commit, the format
397
+ * continuation, the lint continuations — names THIS tab, because a buffer belongs to the document
398
+ * it was filled from and to no other. Resolving through `activeTab.value` at callback time meant
399
+ * the 500ms commit addressed whatever tab was focused when the timer fired: open `a.json`, Edit
400
+ * Event Handler on `children[0].onclick`, open `b.json`, same handler, type one character in A and
401
+ * click B's tab inside the window, and B's handler was replaced by A's buffer while A never
402
+ * received the edit at all.
112
403
  */
113
404
  async function mountFunctionEditor(
114
405
  editorContainer: HTMLElement,
406
+ tab: Tab,
407
+ editing: EditingTarget,
115
408
  body: string,
116
409
  args: string[],
117
- editing: unknown,
118
410
  ): Promise<void> {
119
411
  const monacoNs = await loadMonaco();
412
+ if (!functionMountWanted(editorContainer, tab, editing)) {
413
+ return;
414
+ }
120
415
  // Completions used to register at studio startup, which would now await the lazy load and undo it.
121
416
  // The editor is the only surface they serve, and the registration is idempotent.
122
417
  void registerFunctionCompletions();
123
- view.functionEditor = monacoNs.editor.create(editorContainer, {
418
+ const editor: FunctionEditor = monacoNs.editor.create(editorContainer, {
124
419
  automaticLayout: true,
125
420
  fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace",
126
421
  fontSize: 12,
@@ -133,67 +428,99 @@ async function mountFunctionEditor(
133
428
  value: body,
134
429
  wordWrap: "on",
135
430
  });
136
- view.functionEditor._editingTarget = JSON.stringify(editing);
137
- const editor = view.functionEditor;
431
+ editor._editingTarget = JSON.stringify(editing);
432
+ editor._editingTab = tab;
433
+ const writes = bufferWrites(editor);
138
434
 
139
- // Format on open — show pretty-printed code, then run initial lint
435
+ // The writer this buffer will use for the rest of its life, bound to the tab and target it was
436
+ // Mounted for. The debounce below and `closeFunctionEditor` both go through it.
437
+ editor._commitBody = bodyWriter(tab, editing, writes);
438
+ view.functionEditor = editor;
439
+
440
+ let lintGen = 0;
441
+
442
+ // Format on open — show pretty-printed code, then run initial lint.
443
+ //
444
+ // Both continuations address `editor`, not `view.functionEditor`. That handle is a variable, and
445
+ // A code service round-trip is long enough to retarget twice: it names whatever is mounted NOW,
446
+ // So answering a stale request through it wrote one def's formatted body into another def's
447
+ // Editor. `editor` is the instance the request was made FOR, and the identity check is how a
448
+ // Request that outlived it says nothing at all.
449
+ //
450
+ // Identity is where that reasoning STOPPED, and the common case is the one it misses: no
451
+ // Retarget happened, so the guard passes — and a cold format round trip is long enough to type
452
+ // Into. What lands is the formatting of the body as it was BEFORE those keystrokes, on top of
453
+ // Them. `body` is what the request was computed from, so passing it as `expected` is the whole
454
+ // Difference between "still the same editor" and "still the same text".
140
455
  void codeService("format", { args, code: body }).then((result) => {
141
- if (result?.code != null && view.functionEditor) {
142
- view.functionEditor._ignoreNextChange = true;
143
- view.functionEditor.setValue(result.code);
456
+ if (
457
+ result?.code != null &&
458
+ view.functionEditor === editor &&
459
+ tabIsLive(tab) &&
460
+ !bufferMovedOn(editor, body)
461
+ ) {
462
+ editor._ignoreNextChange = true;
463
+ editor.setValue(result.code);
464
+ /* AND SAY SO, or the next repaint un-formats it in front of the author.
465
+ This text did not come from the document and is deliberately never committed to it, so the
466
+ buffer is now ahead. Inferred from the debounce it would read as "settled" — `setValue`
467
+ sets `_ignoreNextChange`, the change handler returns before it can arm anything — and the
468
+ re-sync branch would resolve the difference in the document's favour. With the dock
469
+ repainting on the Problems badge, on Activity and on a 30-second git poll, "Format on open"
470
+ visibly undid itself within seconds of every open. */
471
+ writes.markAhead();
144
472
  }
145
473
  });
146
474
  void codeService("lint", { args, code: body }).then((result) => {
147
- if (result?.diagnostics && view.functionEditor) {
148
- setLintMarkers(view.functionEditor, result.diagnostics as OxLintDiagnostic[]);
475
+ if (
476
+ result?.diagnostics &&
477
+ view.functionEditor === editor &&
478
+ tabIsLive(tab) &&
479
+ bufferIsLive(editor)
480
+ ) {
481
+ setLintMarkers(editor, result.diagnostics as OxLintDiagnostic[]);
149
482
  }
150
483
  });
151
484
 
152
- // Debounced sync back to state + lint on edit
153
- /** @type {ReturnType<typeof setTimeout> | undefined} */
154
- let syncDebounce: ReturnType<typeof setTimeout> | undefined;
155
- /** @type {ReturnType<typeof setTimeout> | undefined} */
156
- let lintDebounce: ReturnType<typeof setTimeout> | undefined;
157
- let lintGen = 0;
158
485
  editor.onDidChangeModelContent(() => {
159
486
  if (editor._ignoreNextChange) {
160
487
  editor._ignoreNextChange = false;
161
488
  return;
162
489
  }
163
490
 
164
- clearTimeout(syncDebounce);
165
- syncDebounce = setTimeout(() => {
166
- const newBody = editor.getValue();
167
- const target = editing as EditingTarget;
168
- if (target.type === "def") {
169
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, target.defName, { body: newBody }));
170
- } else if (target.type === "event") {
171
- const node = getNodeAtPath(activeTab.value?.doc.document as JxMutableNode, target.path);
172
- const current = node?.[target.eventKey] || {};
173
- transactDoc(activeTab.value, (t) =>
174
- mutateUpdateProperty(t, target.path, target.eventKey, {
175
- ...current,
176
- $prototype: "Function",
177
- body: newBody,
178
- }),
179
- );
180
- }
491
+ // A keystroke. The buffer is ahead of the document from this instant until the commit lands —
492
+ // Stated as a fact rather than left to be read off the timer, because the timer is only ONE of
493
+ // The ways a buffer gets ahead and the other one is invisible to it. `markTyped` rather than
494
+ // `markAhead`: this text is the AUTHOR'S, so it is also the text a close would destroy, and
495
+ // Format-on-open's (below) is not.
496
+ writes.markTyped();
497
+
498
+ writes.arm(BUFFER_COMMIT, 500, () => {
499
+ editor._commitBody?.(editor.getValue());
181
500
  renderOnly("leftPanel");
182
- }, 500);
501
+ });
183
502
 
184
- clearTimeout(lintDebounce);
185
- lintDebounce = setTimeout(() => {
186
- const gen = (lintGen += 1);
503
+ writes.arm("lint", 750, () => {
504
+ lintGen += 1;
505
+ const gen = lintGen;
187
506
  const currentCode = editor.getValue();
188
507
  void codeService("lint", { args, code: currentCode }).then((result) => {
189
- if (gen !== lintGen) {
508
+ // `lintGen` is per-MOUNT, so it can only order this editor's own requests; a remount
509
+ // Starts a fresh generation that a stale closure's `gen` still matches. The identity check
510
+ // Is the half that spans mounts, and the tab is the half that spans documents.
511
+ if (
512
+ gen !== lintGen ||
513
+ view.functionEditor !== editor ||
514
+ !tabIsLive(tab) ||
515
+ !bufferIsLive(editor)
516
+ ) {
190
517
  return;
191
518
  }
192
- if (result?.diagnostics && view.functionEditor) {
193
- setLintMarkers(view.functionEditor, result.diagnostics as OxLintDiagnostic[]);
519
+ if (result?.diagnostics) {
520
+ setLintMarkers(editor, result.diagnostics as OxLintDiagnostic[]);
194
521
  }
195
522
  });
196
- }, 750);
523
+ });
197
524
  });
198
525
  }
199
526