@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
@@ -0,0 +1,244 @@
1
+ /**
2
+ * The `Collaborate:` command family — the entry point and the exit co-editing has never had (§7.4).
3
+ *
4
+ * Collaboration in Studio has been a thing that HAPPENS to a document: a session attaches because
5
+ * the platform said the file was shared, presence chips appear in the toolbar, and there is no verb
6
+ * anywhere in the app for starting it, inviting anybody, following someone, or stopping. The
7
+ * palette had no `Collaborate:` rows at all, and `CATEGORIES` has carried the name since P2 with
8
+ * nothing filed under it.
9
+ *
10
+ * Five records, each registered here — in the module that owns the machinery, per P2's rule — and
11
+ * composed by `commands/app-commands.ts`. Every one is idempotent or refuses with a `requires`
12
+ * sentence, and none of them is a `toggle*` without a `set*` beside it: `collab.setEnabled` is the
13
+ * setter, `collab.stop` is the same setter's `false` and exists because "stop sharing" is what a
14
+ * person actually looks for in a palette.
15
+ */
16
+
17
+ import { collabState } from "./collab-state";
18
+ import { isCollabEnabled, setCollabEnabled } from "./collab-session";
19
+ import { notify } from "../services/notify";
20
+ import { activeTab } from "../workspace/workspace";
21
+ import { booleanArg, booleanProperty, stringProperty } from "../commands/command-args";
22
+
23
+ import type { AnyCommand, CommandRegistry } from "../commands/registry";
24
+ import type { Tab } from "../tabs/tab";
25
+
26
+ /** The tab a Collaborate verb acts on. */
27
+ function tab(): Tab | null {
28
+ return activeTab.value;
29
+ }
30
+
31
+ /** True while the active tab has a live session — what Stop and Follow require. */
32
+ function sessionLive(): boolean {
33
+ const active = tab();
34
+ return Boolean(active && collabState(active).active);
35
+ }
36
+
37
+ /**
38
+ * A shareable reference to the current session.
39
+ *
40
+ * The document path IS the session key — `platform.collab(path)` is how every attach resolves — so
41
+ * the link is derived, not minted. That is the whole reason this command can exist without a server
42
+ * round trip, and the reason it cannot promise the recipient has access: it names a room, it does
43
+ * not grant one.
44
+ *
45
+ * @param {Tab} active
46
+ * @returns {string}
47
+ */
48
+ export function sessionLink(active: Tab): string {
49
+ const path = active.documentPath ?? "";
50
+ return `jx://collab/${path.replace(/^\.?\//, "")}`;
51
+ }
52
+
53
+ /**
54
+ * The Collaborate verbs.
55
+ *
56
+ * @returns {AnyCommand[]}
57
+ */
58
+ export function collabCommands(): AnyCommand[] {
59
+ return [
60
+ {
61
+ id: "collab.setEnabled",
62
+ title: "Collaborate: Share this document",
63
+ category: "Collaborate",
64
+ level: "document",
65
+ menus: ["palette"],
66
+ group: "1_session",
67
+ requires: "an open document",
68
+ when: () => Boolean(tab()?.documentPath),
69
+ args: {
70
+ additionalProperties: false,
71
+ properties: {
72
+ enabled: booleanProperty("True to attach a live session, false to leave it."),
73
+ },
74
+ required: ["enabled"],
75
+ type: "object",
76
+ },
77
+ run: (_ctx, args) => {
78
+ const enabled = booleanArg("collab.setEnabled", args, "enabled");
79
+ const active = tab();
80
+ if (!active) {
81
+ return;
82
+ }
83
+ if (isCollabEnabled(active) === enabled) {
84
+ return;
85
+ }
86
+ setCollabEnabled(active, enabled);
87
+ notify.info(
88
+ enabled
89
+ ? "Connecting this document to its collaboration session…"
90
+ : "Left the collaboration session for this document.",
91
+ { source: "Collaboration" },
92
+ );
93
+ },
94
+ },
95
+ {
96
+ id: "collab.stop",
97
+ title: "Collaborate: Stop sharing",
98
+ category: "Collaborate",
99
+ level: "document",
100
+ menus: ["palette"],
101
+ group: "1_session",
102
+ requires: "a live collaboration session",
103
+ when: () => Boolean(tab()?.documentPath),
104
+ enablement: sessionLive,
105
+ run: () => {
106
+ /* The idempotent setter, said the way a person looks for it in a palette. `setEnabled
107
+ false` and this are one behaviour with two names, which is what stops "Stop" from being
108
+ a toggle with no setter beside it. */
109
+ const active = tab();
110
+ if (!active || !isCollabEnabled(active)) {
111
+ return;
112
+ }
113
+ setCollabEnabled(active, false);
114
+ notify.info("Left the collaboration session for this document.", {
115
+ source: "Collaboration",
116
+ });
117
+ },
118
+ },
119
+ {
120
+ id: "collab.copyLink",
121
+ title: "Collaborate: Copy session link",
122
+ category: "Collaborate",
123
+ level: "document",
124
+ menus: ["palette"],
125
+ group: "2_invite",
126
+ requires: "a live collaboration session",
127
+ when: () => Boolean(tab()?.documentPath),
128
+ enablement: sessionLive,
129
+ run: async () => {
130
+ const active = tab();
131
+ if (!active) {
132
+ return;
133
+ }
134
+ const link = sessionLink(active);
135
+ try {
136
+ await navigator.clipboard.writeText(link);
137
+ } catch (error) {
138
+ notify.error(
139
+ `Could not copy the session link — ${error instanceof Error ? error.message : String(error)}`,
140
+ { detail: link, source: "Collaboration" },
141
+ );
142
+ return;
143
+ }
144
+ notify.success("Session link copied. It names the room; it does not grant access to it.", {
145
+ source: "Collaboration",
146
+ });
147
+ },
148
+ },
149
+ {
150
+ id: "collab.follow",
151
+ title: "Collaborate: Follow a collaborator",
152
+ category: "Collaborate",
153
+ level: "document",
154
+ menus: ["palette"],
155
+ group: "3_presence",
156
+ requires: "another person in this session",
157
+ when: () => Boolean(tab()?.documentPath),
158
+ enablement: () => {
159
+ const active = tab();
160
+ return Boolean(active && collabState(active).peers.length > 0);
161
+ },
162
+ args: {
163
+ additionalProperties: false,
164
+ properties: {
165
+ login: stringProperty("The collaborator's login, as shown on their presence chip."),
166
+ },
167
+ required: [],
168
+ type: "object",
169
+ },
170
+ run: (_ctx, args) => {
171
+ const active = tab();
172
+ if (!active) {
173
+ return;
174
+ }
175
+ const login = (args as { login?: string } | undefined)?.login;
176
+ const { peers } = collabState(active);
177
+ const peer = login ? peers.find((p) => p.state.user.login === login) : peers[0];
178
+ if (!peer) {
179
+ notify.warn(
180
+ login ? `${login} is not in this session.` : "Nobody else is in this session.",
181
+ { source: "Collaboration" },
182
+ );
183
+ return;
184
+ }
185
+ const where = peer.state.focusedPath;
186
+ if (!where) {
187
+ notify.info(`${peer.state.user.login} is not in a document right now.`, {
188
+ source: "Collaboration",
189
+ });
190
+ return;
191
+ }
192
+ notify.info(`${peer.state.user.login} is in ${where}.`, {
193
+ action: "file.open",
194
+ actionArgs: { path: where },
195
+ source: "Collaboration",
196
+ });
197
+ },
198
+ },
199
+ {
200
+ id: "collab.showStatus",
201
+ title: "Collaborate: What is happening in this document?",
202
+ category: "Collaborate",
203
+ level: "document",
204
+ menus: ["palette"],
205
+ group: "3_presence",
206
+ /* THE DOCUMENT PATH, like its four peers. `sessionLink` above says outright that the path IS
207
+ the session key, so a session cannot exist for a tab that has none — and this asked only
208
+ for a tab, so on an untitled document the palette showed four Collaborate rows missing and
209
+ this one present, reporting on a session that cannot be started. */
210
+ requires: "an open document",
211
+ when: () => Boolean(tab()?.documentPath),
212
+ run: () => {
213
+ const active = tab();
214
+ if (!active) {
215
+ return;
216
+ }
217
+ const state = collabState(active);
218
+ /* This is the honesty command: every one of these states used to be either invisible or
219
+ spelled "detached", and the freeze in particular looked exactly like a bug (§7.4). */
220
+ const lines = [
221
+ `Status: ${state.status}${state.attachError ? ` — ${state.attachError}` : ""}`,
222
+ `People here: ${state.peers.length === 0 ? "just you" : state.peers.map((p) => p.state.user.login).join(", ")}`,
223
+ state.readOnly
224
+ ? "You have read access: your edits stay on this machine."
225
+ : "You can publish edits to this session.",
226
+ state.sourceCanonical
227
+ ? "Someone holds the code view, so structural edits are paused. This is not an error."
228
+ : "Structural editing is available.",
229
+ "Undo only takes back your own edits, never a collaborator's.",
230
+ ];
231
+ notify.info(lines[0]!, { detail: lines.join("\n"), source: "Collaboration" });
232
+ },
233
+ },
234
+ ];
235
+ }
236
+
237
+ /**
238
+ * Register the Collaborate family.
239
+ *
240
+ * @param {CommandRegistry} registry
241
+ */
242
+ export function registerCollabCommands(registry: CommandRegistry): void {
243
+ registry.registerAll(collabCommands());
244
+ }
@@ -20,9 +20,10 @@
20
20
  * evaluation defers until a collab session actually attaches.
21
21
  */
22
22
 
23
- import { effect, onScopeDispose, toRaw } from "../reactivity";
23
+ import { effect, onScopeDispose, reactive, toRaw } from "../reactivity";
24
24
  import { getPlatform } from "../platform";
25
25
  import { jsonClone } from "../utils/studio-utils";
26
+ import { cloneSelection } from "../tabs/selection";
26
27
  import {
27
28
  applyExternalDocOps,
28
29
  isBatching,
@@ -32,6 +33,7 @@ import {
32
33
  setTransactObserver,
33
34
  transactDoc,
34
35
  } from "../tabs/transact";
36
+ import { PROJECT_CONFIG_PATH } from "../tabs/tab";
35
37
  import type { TransactOrigin } from "../tabs/transact";
36
38
  import type { TransactionRecord } from "../tabs/patch-ops";
37
39
  import type { Tab } from "../tabs/tab";
@@ -40,6 +42,7 @@ import type { CollabHandle } from "@jxsuite/collab/provider";
40
42
  import type { JxMutableNode } from "@jxsuite/schema/types";
41
43
  import type * as CollabNS from "@jxsuite/collab";
42
44
  import { collabState, registerCollabPath, unregisterCollabPath } from "./collab-state";
45
+ import { notify } from "../services/notify";
43
46
 
44
47
  type CollabModule = typeof CollabNS;
45
48
 
@@ -116,13 +119,26 @@ interface ActiveSession {
116
119
  applyingRemoteFrontmatter: boolean;
117
120
  synced: boolean;
118
121
  canWrite: boolean;
119
- /** True while THIS client is in the code view (its structural freeze exempts itself). */
120
- inSourceMode: boolean;
121
122
  mirrorTimer: ReturnType<typeof setTimeout> | null;
122
123
  /** When the shared source text was last brought up to date; bounds the debounce's max lag. */
123
124
  lastMirrorAt?: number;
124
125
  /** Debounce for the source reconciler's Y.Text → structure parse mirror. */
125
126
  sourceParseTimer: ReturnType<typeof setTimeout> | null;
127
+ /**
128
+ * Which source surface currently speaks for this client's canonical lock.
129
+ *
130
+ * The lock is a room-level fact with no owner, and `leave()` reaches it from exactly one place —
131
+ * the cleanup {@link canvas/canvas-render.ts}'s `createSourceCollabBinding` returns. Toggling Code
132
+ * view off and on inside one round trip runs two mounts that overlap: the second `enter()` has
133
+ * already re-flipped the lock by the time the first mount's cleanup fires, and that cleanup
134
+ * released a lock the live mount was holding — leaving a co-edited buffer bound to a Y.Text that
135
+ * the room no longer treats as canonical, with the structure mirror free to serialize over it.
136
+ *
137
+ * A token, minted synchronously by each `enter()`, is the identity the release checks. Call
138
+ * order, not resolution order: `enter()` awaits a serialization, so the last mount to be ASKED
139
+ * must win regardless of which round trip returns first.
140
+ */
141
+ sourceLockOwner: object | null;
126
142
  disposers: (() => void)[];
127
143
  }
128
144
 
@@ -132,6 +148,14 @@ interface TabRuntime {
132
148
  generation: number;
133
149
  session: ActiveSession | null;
134
150
  attaching: Promise<void> | null;
151
+ /**
152
+ * The user asked to leave this document's session.
153
+ *
154
+ * Reactive, because the watcher effect that owns attach/detach has to re-run when it changes —
155
+ * which is also why leaving is a STATE rather than a teardown verb: the effect stays the single
156
+ * owner of the session lifecycle, and `Collaborate: Stop sharing` sets a flag it reads.
157
+ */
158
+ optedOut: { value: boolean };
135
159
  }
136
160
 
137
161
  const runtimes = new WeakMap<Tab, TabRuntime>();
@@ -149,7 +173,13 @@ function runtimeFor(tab: Tab): TabRuntime {
149
173
  const key = rawTab(tab);
150
174
  let runtime = runtimes.get(key);
151
175
  if (!runtime) {
152
- runtime = { attaching: null, generation: 0, session: null, watcherInstalled: false };
176
+ runtime = {
177
+ attaching: null,
178
+ generation: 0,
179
+ optedOut: reactive({ value: false }),
180
+ session: null,
181
+ watcherInstalled: false,
182
+ };
153
183
  runtimes.set(key, runtime);
154
184
  }
155
185
  return runtime;
@@ -166,11 +196,25 @@ function installGlobalHooks(): void {
166
196
  hooksInstalled = true;
167
197
  setTransactObserver(onTransact);
168
198
  setBatchEndNotifier(onBatchEnd);
169
- // Soft-freeze structural editing while a peer holds source-canonical (remote origins pass —
170
- // They ARE the reconciler's mirror of the frozen representation).
199
+ /* Soft-freeze structural editing while ANYONE holds source-canonical (remote origins pass — they
200
+ ARE the reconciler's mirror of the frozen representation).
201
+ **Including the client holding the lock, which used to be exempt** (`&& !session.inSourceMode`).
202
+ The exemption assumed a carrier that does not exist: a structural edit made by the lock holder
203
+ never reaches the shared `Y.Text`, because `scheduleMirror` returns early while canonical is
204
+ `"source"`, and nothing notices, because the source observer fires on TEXT changes only. So the
205
+ tree and the text diverged, held apart until `leave()` — which runs `sourceParseNow` before
206
+ releasing the lock, parses the UNCHANGED text back into a tree, and reverts the author's edit
207
+ under `MIRROR_ORIGIN`. It then arrives at every peer through `applyExternalDocOps` (origin
208
+ `"remote"`, which passes this gate by design), so the layer they deleted in the Outline
209
+ reappeared seconds later with no explanation — and they were the one client this toast was
210
+ never shown to.
211
+ Mirroring the other way while source is canonical is not the alternative: it is exactly the
212
+ round trip `services/monaco-buffer.ts`'s clause 5 exists to refuse. Clause 5's own reasoning
213
+ settles this one — if the CRDT owns that text and the tree is DERIVED from it, then nobody may
214
+ edit the tree directly, and "nobody" includes whoever is holding the pen. */
171
215
  setTransactGate((tab) => {
172
216
  const session = runtimeOf(tab)?.session;
173
- if (session?.synced && collabState(tab).sourceCanonical && !session.inSourceMode) {
217
+ if (session?.synced && collabState(tab).sourceCanonical) {
174
218
  _notify("Source editing in progress — structural edits are paused");
175
219
  return "source-canonical";
176
220
  }
@@ -424,10 +468,20 @@ export function collabSourceContext(tab: Tab): {
424
468
  return null;
425
469
  }
426
470
  const { collab, handle } = session;
471
+ /* THIS CONTEXT'S CLAIM ON THE LOCK, and the reason a release needs one.
472
+ One context object is handed to one mount, and `leave()` is called by that mount's cleanup —
473
+ but the lock it releases is the SESSION's, shared by every mount this client makes. Toggling
474
+ Code view off and on inside one round trip overlaps two of them, and the first one's cleanup
475
+ handed back a lock the second one had just taken. The token is minted synchronously at
476
+ `enter()` so the ordering is the order the surfaces were asked for, not the order two
477
+ serializations happened to resolve in. */
478
+ let claim: object | null = null;
479
+ const holdsClaim = () => claim !== null && session.sourceLockOwner === claim;
427
480
  return {
428
481
  awareness: handle.awareness,
429
482
  enter: async () => {
430
- session.inSourceMode = true;
483
+ claim = {};
484
+ session.sourceLockOwner = claim;
431
485
  const local = handle.awareness.getLocalState();
432
486
  if (local) {
433
487
  handle.awareness.setLocalState({ ...local, mode: "source" });
@@ -443,6 +497,11 @@ export function collabSourceContext(tab: Tab): {
443
497
  serialized = null;
444
498
  }
445
499
  }
500
+ // Asked again across the await: a later mount owns the surface now, and seeding the shared
501
+ // Text from THIS mount's serialization would push a stale document at the room.
502
+ if (!holdsClaim()) {
503
+ return;
504
+ }
446
505
  collab.acquireSourceCanonical(
447
506
  handle.doc,
448
507
  serialized ?? collab.sourceText(handle.doc).toString(),
@@ -450,19 +509,44 @@ export function collabSourceContext(tab: Tab): {
450
509
  );
451
510
  },
452
511
  leave: () => {
453
- session.inSourceMode = false;
512
+ // Not this mount's lock to hand back, and not its awareness state to reset either — the live
513
+ // Mount is in source mode and says so.
514
+ if (!holdsClaim()) {
515
+ claim = null;
516
+ return;
517
+ }
518
+ claim = null;
519
+ session.sourceLockOwner = null;
454
520
  const local = handle.awareness.getLocalState();
455
521
  if (local) {
456
522
  handle.awareness.setLocalState({ ...local, mode: "structure" });
457
523
  }
524
+ /* A client with no write access never reached `acquireSourceCanonical` above, so it holds
525
+ nothing to hand back — and `releaseSourceCanonical` is itself a write to shared state,
526
+ which the relay drops from a read-only socket anyway. The guard is right; it was never the
527
+ asymmetry. See below for the one that was. */
458
528
  if (!session.canWrite) {
459
529
  return;
460
530
  }
461
- const others = collab.otherSourceEditors(
462
- handle.awareness,
463
- session.path,
464
- handle.awareness.clientID,
465
- );
531
+ /* AND A READ-ONLY VIEWER MUST NOT COUNT AS SOMEONE STILL HOLDING THE LOCK.
532
+ `otherSourceEditors` answers "who else has Code view open", per awareness, which is a
533
+ different question from the one asked here: who else could be the reason not to release.
534
+ A read-only guest publishes `mode: "source"` when they open Code view and then returns at
535
+ the guard above — they can neither acquire the lock nor hand it back. Counted, they made
536
+ the LAST WRITE-CAPABLE editor's departure release nothing, and their own departure release
537
+ nothing: `meta.canonical` stayed `"source"` for the whole room, forever, and with the
538
+ lock-holder exemption gone from the transact gate that is every client frozen out of every
539
+ structural edit, with only the keyed "structural edits are paused" toast to explain it.
540
+ The same `canWrite !== false` filter the reconciler election above uses, for the same
541
+ reason: a client that cannot write cannot be the one holding a write. */
542
+ const others = collab
543
+ .otherSourceEditors(handle.awareness, session.path, handle.awareness.clientID)
544
+ .filter((clientId) => {
545
+ const peer = handle.awareness.getStates().get(clientId) as
546
+ | { canWrite?: boolean }
547
+ | undefined;
548
+ return peer?.canWrite !== false;
549
+ });
466
550
  if (others.length === 0) {
467
551
  // Freshen the structure mirror once more, then hand the lock back.
468
552
  void sourceParseNow(session).then(() => {
@@ -476,22 +560,43 @@ export function collabSourceContext(tab: Tab): {
476
560
  };
477
561
  }
478
562
 
563
+ /**
564
+ * Whether this client is in `tab`'s session **without write access**.
565
+ *
566
+ * The single spelling of "this tab cannot be saved by me". A read-only client edits its local
567
+ * reactive document freely — nothing blocks structural editing, and `transactDoc` still marks the
568
+ * tab dirty — but `onTransact` gates BOTH `publishRecord` and `scheduleMirror` behind `canWrite`,
569
+ * so those edits exist in this browser and nowhere else: not in the Y-doc, not on the relay, not on
570
+ * disk. Every question that turns on "is this work recoverable from somewhere?" has to ask this
571
+ * one, which is why it is exported rather than re-derived per caller.
572
+ */
573
+ export function collabReadOnly(tab: Tab): boolean {
574
+ const state = collabState(tab);
575
+ return state.active && state.readOnly;
576
+ }
577
+
479
578
  /**
480
579
  * Cmd+S for a collab tab: refresh the source mirror and ask the provider to persist now. Returns
481
- * false when the tab has no active session (caller saves through the file path as usual).
580
+ * false when the tab has no active session (caller saves through the file path as usual), and false
581
+ * when this client cannot write to the session it does have.
582
+ *
583
+ * **The read-only `false` is the load-bearing one.** This used to skip `mirrorNow` for a read-only
584
+ * client and then flush and return `true` anyway — flushing a Y-doc that had received none of the
585
+ * edits, and reporting the result as a save. `saveFile` stamped "Saved just now" on it and the tab
586
+ * strip's Save button closed the tab on top of work that was still only in the browser. Answering
587
+ * `false` is the truth; `saveFile` is where the read-only tab is refused outright, so this never
588
+ * becomes a licence to write the file behind the room's back.
482
589
  */
483
590
  export async function collabSave(tab: Tab): Promise<boolean> {
484
591
  const session = runtimeOf(tab)?.session;
485
- if (!session?.synced) {
592
+ if (!session?.synced || !session.canWrite) {
486
593
  return false;
487
594
  }
488
595
  if (session.mirrorTimer) {
489
596
  clearTimeout(session.mirrorTimer);
490
597
  session.mirrorTimer = null;
491
598
  }
492
- if (session.canWrite) {
493
- await mirrorNow(session);
494
- }
599
+ await mirrorNow(session);
495
600
  await session.handle.flush();
496
601
  return true;
497
602
  }
@@ -513,12 +618,26 @@ const liveTabs = new Set<Tab>();
513
618
 
514
619
  /**
515
620
  * Idempotently wire collaboration for a tab. Installs a per-tab watcher that attaches a session for
516
- * the tab's file while it is at the top of its document stack, detaches while drilled into a
517
- * component, and tears everything down when the tab's scope is disposed.
621
+ * the tab's file unless the author has opted out, and tears everything down when the tab's scope is
622
+ * disposed.
623
+ *
624
+ * The watcher used to carry a second condition — detach while the tab is "drilled into" a
625
+ * sub-document — guarding a `session.documentStack` that nothing could ever push onto. It never
626
+ * fired, and a tab holds one document now: drilling in opens a tab of its own, which gets its own
627
+ * watcher for its own file.
628
+ *
629
+ * **`project.json` is out of replication.** It is the one document whose edits arrive from surfaces
630
+ * that are not the canvas — every settings form, the imports panel, the deploy flow — and whose
631
+ * value the studio ITSELF reads to configure formats, extensions, schemas and the style cascade. A
632
+ * shared Y.Doc over it would mean a peer's extension list reconfiguring your editor mid-keystroke,
633
+ * and the source-canonical freeze would pause configuration edits that have nothing to do with
634
+ * text. Returning here is the whole gate: no session is attached, so `setHistoryDelegate` is never
635
+ * called for this tab and its history stays the local op log `tabs/project-config.ts` transacts
636
+ * on.
518
637
  */
519
638
  export function ensureCollab(tab: Tab): void {
520
639
  const platform = maybePlatform();
521
- if (!platform?.collab || !tab.documentPath) {
640
+ if (!platform?.collab || !tab.documentPath || tab.documentPath === PROJECT_CONFIG_PATH) {
522
641
  return;
523
642
  }
524
643
  const runtime = runtimeFor(tab);
@@ -530,8 +649,7 @@ export function ensureCollab(tab: Tab): void {
530
649
  liveTabs.add(tab);
531
650
  tab.scope.run(() => {
532
651
  effect(() => {
533
- const drilled = (tab.session.documentStack?.length ?? 0) > 0;
534
- if (drilled) {
652
+ if (runtime.optedOut.value) {
535
653
  detachSession(tab);
536
654
  } else {
537
655
  void attachSession(tab);
@@ -544,6 +662,33 @@ export function ensureCollab(tab: Tab): void {
544
662
  });
545
663
  }
546
664
 
665
+ /**
666
+ * Join or leave this document's collaboration session — the idempotent setter behind `Collaborate:
667
+ * Share this document` and `Collaborate: Stop sharing` (§7.4).
668
+ *
669
+ * Leaving sets a flag the watcher effect reads rather than calling `detachSession` directly, so the
670
+ * effect remains the one owner of the session lifecycle and re-joining is the same call with the
671
+ * other value. Calling it twice with the same value does nothing, which is the property the
672
+ * `app-commands` pairing test checks for.
673
+ *
674
+ * @param {Tab} tab @param {boolean} enabled
675
+ */
676
+ export function setCollabEnabled(tab: Tab, enabled: boolean): void {
677
+ const runtime = runtimeFor(tab);
678
+ if (runtime.optedOut.value === !enabled) {
679
+ return;
680
+ }
681
+ runtime.optedOut.value = !enabled;
682
+ if (!runtime.watcherInstalled && enabled) {
683
+ ensureCollab(tab);
684
+ }
685
+ }
686
+
687
+ /** True while the user has not opted this tab out of collaboration. */
688
+ export function isCollabEnabled(tab: Tab): boolean {
689
+ return runtimeOf(tab)?.optedOut.value !== true;
690
+ }
691
+
547
692
  /** Re-key after a file rename: tear down and re-attach against the new path. */
548
693
  export function rekeyCollab(tab: Tab): void {
549
694
  const runtime = runtimeOf(tab);
@@ -563,15 +708,20 @@ async function attachSession(tab: Tab): Promise<void> {
563
708
  const { documentPath: path } = tab;
564
709
  const platform = maybePlatform();
565
710
  if (!path || !platform?.collab) {
711
+ /* Not a failure and not a session: this build/platform has no collaboration to offer. Saying
712
+ so is the difference between "nobody else is here" and "something broke" (§7.4). */
713
+ collabState(tab).status = "unavailable";
566
714
  return;
567
715
  }
568
716
  const { generation } = runtime;
569
717
  const state = collabState(tab);
570
718
  state.status = "connecting";
719
+ state.attachError = "";
571
720
  const attempt = (async () => {
572
721
  try {
573
722
  const [collab, handle] = await Promise.all([loadCollab(), platform.collab!(path)]);
574
723
  if (!handle) {
724
+ // The platform offers collaboration; this document is simply not shared. Solo, not broken.
575
725
  state.status = "detached";
576
726
  return;
577
727
  }
@@ -598,10 +748,20 @@ async function attachSession(tab: Tab): Promise<void> {
598
748
  state.active = true;
599
749
  state.status = "synced";
600
750
  state.readOnly = !runtime.session.canWrite;
751
+ state.attachError = "";
601
752
  registerCollabPath(path);
602
- } catch {
603
- state.status = "detached";
753
+ } catch (error) {
754
+ /* An attach that threw is a FAILURE, and it used to be reported as "detached" — the same
755
+ value a solo document carries. A relay that is down, a token that expired and a document
756
+ nobody shared were one indistinguishable state (§7.4). */
757
+ state.status = "failed";
604
758
  state.active = false;
759
+ state.attachError = error instanceof Error ? error.message : String(error);
760
+ notify.warn(`Live collaboration is not available for "${path}" — ${state.attachError}`, {
761
+ key: `collab:${path}`,
762
+ path,
763
+ source: "Collaboration",
764
+ });
605
765
  } finally {
606
766
  runtime.attaching = null;
607
767
  }
@@ -621,6 +781,7 @@ function detachSession(tab: Tab): void {
621
781
  const state = collabState(tab);
622
782
  state.active = false;
623
783
  state.status = "detached";
784
+ state.attachError = "";
624
785
  state.peers = [];
625
786
  if (!session) {
626
787
  return;
@@ -644,7 +805,7 @@ function detachSession(tab: Tab): void {
644
805
  tab.history.snapshots = [
645
806
  {
646
807
  document: jsonClone(toRaw(tab.doc.document)) as Record<string, unknown>,
647
- selection: tab.session.selection ? [...tab.session.selection] : null,
808
+ selection: cloneSelection(tab.session.selection),
648
809
  },
649
810
  ];
650
811
  tab.history.index = 0;
@@ -667,10 +828,10 @@ function createSession(
667
828
  collab,
668
829
  disposers: [],
669
830
  handle,
670
- inSourceMode: false,
671
831
  lastSeenRef: toRaw(tab.doc.document) as object,
672
832
  mirrorTimer: null,
673
833
  path,
834
+ sourceLockOwner: null,
674
835
  sourceParseTimer: null,
675
836
  synced: false,
676
837
  tab,
@@ -742,11 +903,11 @@ function createSession(
742
903
  );
743
904
  session.undoManager = undoManager;
744
905
  const onStackAdd = ({ stackItem }: { stackItem: { meta: Map<unknown, unknown> } }) => {
745
- stackItem.meta.set("selection", tab.session.selection ? [...tab.session.selection] : null);
906
+ stackItem.meta.set("selection", cloneSelection(tab.session.selection));
746
907
  };
747
908
  const onStackPop = ({ stackItem }: { stackItem: { meta: Map<unknown, unknown> } }) => {
748
- const selection = stackItem.meta.get("selection") as (string | number)[] | null | undefined;
749
- tab.session.selection = selection ? [...selection] : null;
909
+ const selection = stackItem.meta.get("selection") as (string | number)[][] | undefined;
910
+ tab.session.selection = cloneSelection(selection ?? []);
750
911
  };
751
912
  undoManager.on("stack-item-added", onStackAdd);
752
913
  undoManager.on("stack-item-popped", onStackPop);
@@ -882,7 +1043,12 @@ function createSession(
882
1043
  // FocusedPath, so per-doc boxes come free from the one project-level awareness state). The
883
1044
  // Plain `selection` field is y-monaco's (in-buffer text cursors) — never write it here.
884
1045
  effect(() => {
885
- const structuralSelection = tab.session.selection ? [...tab.session.selection] : null;
1046
+ // The whole selection SET crosses awareness, so a peer's canvas draws every node the author
1047
+ // Has selected. A selection of one publishes a one-entry list, which is the same box.
1048
+ // Empty stays `null` rather than `[]`: "this peer is not pointing at anything" is one fact
1049
+ // With one wire value, and every consumer already tests it by presence.
1050
+ const structuralSelection =
1051
+ tab.session.selection.length > 0 ? cloneSelection(tab.session.selection) : null;
886
1052
  const local = handle.awareness.getLocalState();
887
1053
  if (local) {
888
1054
  handle.awareness.setLocalState({ ...local, structuralSelection });