@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
@@ -0,0 +1,1258 @@
1
+ /**
2
+ * Derived panes (§18.4) — the pane beside this one, showing a projection of it.
3
+ *
4
+ * **Two families, one field.** `Pane.derived` carries either a LENS or a COMPANION, and the
5
+ * distinction is not cosmetic — it is what keeps §14.1 intact:
6
+ *
7
+ * - A **lens** (`code`, `diff`, `breakpoint`) draws the SAME document the source pane owns, in a
8
+ * mode, at a breakpoint or against a diff of its own. It owns no tab at all, so no id maps to two
9
+ * documents and no document takes two ids. `canvas-surface.ts`'s `tabOfPane` takes one hop
10
+ * through `sourcePaneId`, and every pane-scoped renderer in the package starts working on it for
11
+ * free — which is also why the follow costs nothing: switching tabs in the source pane repaints
12
+ * the lens because it is the same reactive read.
13
+ * - A **companion** (`layout`, `component`) shows a DIFFERENT document chosen by a rule from the
14
+ * source. That is an ordinary path-keyed tab this pane owns; the derivation only decides which of
15
+ * its tabs is on screen, which is what activation has always done.
16
+ *
17
+ * §14.1 is therefore SHARPENED rather than amended: **ownership is one-to-one, display is
18
+ * many-to-one.** A tab id appears in at most one pane's `tabOrder`, and every verb that closes,
19
+ * disposes, reorders, pins or MRU-tracks a tab resolves through `paneOfTab` — so it addresses the
20
+ * owner and only the owner. `closeTab`, `detachTab`, `setTabPinned`, `moveTab`, `renameTab`,
21
+ * `rememberClosedTab`, `ensureCollab`, `promoteMru` and `openFileInTab`'s dedupe need no change at
22
+ * all: none of them can see a lens pane, whose `tabOrder` is empty.
23
+ *
24
+ * **What is NOT here, and why.** No derived Tab of any kind — no `derived:code:<path>` id sharing a
25
+ * document, no off-map `Map<paneId, Tab>`. Both mint a second tab LIFETIME: the first costs four
26
+ * exclusion predicates (MRU, reopen-closed, collab keying, close-all counting) that must each be
27
+ * written and tested rather than assumed; the second costs a second disposal path, where one missed
28
+ * `disposeTab` is an `effectScope` reacting to a document forever. Neither is needed once ownership
29
+ * and display are separated.
30
+ */
31
+
32
+ import { effect } from "../reactivity";
33
+ import { componentRegistry } from "../files/components";
34
+ import { getNodeAtPath, parentElementPath } from "../store";
35
+
36
+ import { canvasPerf } from "../canvas/canvas-perf";
37
+ import { canvasModeOfTab, tabOfPane } from "../canvas/canvas-surface";
38
+ import { parseMediaEntries } from "../utils/canvas-media";
39
+ import { getEffectiveLayoutPath, getEffectiveLocales, getEffectiveMedia } from "../site-context";
40
+ import { localeLabel, translationPathFor } from "@jxsuite/schema/locale";
41
+ import { shell } from "../shell";
42
+ import {
43
+ activePane,
44
+ detachTab,
45
+ insertIntoPane,
46
+ paneById,
47
+ promoteTab,
48
+ sidePane,
49
+ workspace,
50
+ } from "./workspace";
51
+ import {
52
+ argsSchema,
53
+ enumArg,
54
+ enumProperty,
55
+ optionalStringArg,
56
+ stringProperty,
57
+ } from "../commands/command-args";
58
+
59
+ import type { DerivationStatus, Pane, PaneDerivation } from "./workspace";
60
+ import type { AnyCommand } from "../commands/registry";
61
+ import type { Tab } from "../tabs/tab";
62
+ import type { GitDiffState, GitFileStatus } from "../types";
63
+ import type { JxPath } from "../state";
64
+
65
+ /** Every projection `pane.derive` offers, in the order the preset menu lists them. */
66
+ export const DERIVE_PRESETS = [
67
+ "code",
68
+ "layout",
69
+ "component",
70
+ "diff",
71
+ "breakpoint",
72
+ "locale",
73
+ ] as const;
74
+
75
+ export type DerivePreset = (typeof DERIVE_PRESETS)[number];
76
+
77
+ /**
78
+ * The canvas mode a preset needs the SOURCE document to DECLARE, for the presets that need one.
79
+ *
80
+ * `tab.capabilities.modes` is the format's own answer to "what can this document be shown as", and
81
+ * the preset menu consulted it nowhere: over a document that declares `["settings", "stylebook",
82
+ * "source"]` it still offered "Same page at Base" — a lens whose stage can only draw an empty
83
+ * artboard — and "Component definition", whose rule can never resolve because nothing in a settings
84
+ * form is a component instance. Both produced a pane the author then had to close.
85
+ *
86
+ * Two presets are absent, for two different reasons. `diff` needs no mode: a comparison is two
87
+ * texts and every document has those, and it is already gated on the file having changes. `layout`
88
+ * is gated on the layout RESOLVING, which {@link presetRefusal} checks directly.
89
+ *
90
+ * **That second gate is NOT a mode check in disguise, and a comment here said it was** — "a
91
+ * document with no design board has no layout either". It is false: `getEffectiveLayoutPath` falls
92
+ * back to `projectConfig.defaults.layout`, which every starter sets, so a document declaring
93
+ * `["stylebook", "source"]` resolves the project's layout and IS offered the row. That is a
94
+ * deliberate answer rather than an oversight — the companion opens a real layout document the
95
+ * author can edit, unlike the empty artboard `breakpoint` would draw or the rule `component` could
96
+ * never resolve — but it is a fact about the project's default, not about this document, so it is
97
+ * pinned by a test (`per-preset refusals` · "Layout's gate is the layout RESOLVING") instead of
98
+ * being asserted here in prose.
99
+ */
100
+ const MODE_FOR_PRESET: Readonly<Partial<Record<DerivePreset, string>>> = {
101
+ breakpoint: "design",
102
+ code: "source",
103
+ component: "design",
104
+ };
105
+
106
+ /** How a mode is spelled in a refusal — the words on the Editor axis, not the internal ids. */
107
+ const MODE_WORDS: Readonly<Record<string, string>> = { design: "Design", source: "Code" };
108
+
109
+ /** Said by a diff lens whose comparison came back empty. One sentence, one home. */
110
+ const DIFF_UNREADABLE = "Could not read this file's comparison against HEAD.";
111
+
112
+ /** What the derivation commands and the follow need from the rest of Studio. */
113
+ export interface DerivationDeps {
114
+ /**
115
+ * `files/files.ts`'s `openFileInPane` — read a document from disk and show it in a named pane,
116
+ * browsing rather than committing and leaving the keyboard where it is.
117
+ *
118
+ * Injected rather than imported so this module owns no I/O and no test has to stand up a platform
119
+ * to prove the follow memoises.
120
+ */
121
+ openFileInPane: (paneId: string, path: string) => void | Promise<void>;
122
+ /**
123
+ * Read one file's comparison against HEAD — `gitShow(HEAD)` plus the working copy.
124
+ *
125
+ * **The diff lens has to load its own, and that is the whole of finding 4.** It used to snapshot
126
+ * `shell.git.diffState`: an app-level slot written only when the author clicks a file IN THE GIT
127
+ * PANEL, carrying THAT file's path and never re-read. So the preset was refused for a page that
128
+ * genuinely has changes, and when it was offered it could render a comparison of an unrelated
129
+ * file under a label promising this pane's document. A lens draws the source pane's document or
130
+ * it is not a lens.
131
+ *
132
+ * Resolves to `null` when the comparison cannot be produced; the pane says so rather than drawing
133
+ * somebody else's file.
134
+ */
135
+ loadDiff: (path: string, fileStatus: string) => Promise<GitDiffState | null>;
136
+ /**
137
+ * Is there a file at this project-relative path? The locale companion's one question.
138
+ *
139
+ * Injected for the reason {@link openFileInPane} is, and asked at all for a sharper one:
140
+ * {@link companionTarget} is pure and synchronous, so it cannot stat anything — and a companion
141
+ * that answers with the path a translation WOULD have hands `openFileInPane` a file that is not
142
+ * there, which fails into a blank pane under a chip naming a language. The pane has to be able to
143
+ * say "there is no French copy of this yet", and only a read of the disk can tell it that.
144
+ */
145
+ fileExists: (path: string) => Promise<boolean>;
146
+ }
147
+
148
+ /**
149
+ * A dependency set that does nothing — for callers that read DECLARATIONS rather than run verbs.
150
+ *
151
+ * The sibling of `commands/defaults.ts`'s `noopCommandDeps()`, and here for the same reason: the
152
+ * command-level check, the chrome budget and the generated keyboard sheet all build the whole
153
+ * command set to inspect its records, and every one of them would otherwise hand-write a stub for a
154
+ * verb it will never call. One stub, named, beside the interface it satisfies.
155
+ *
156
+ * @returns {DerivationDeps}
157
+ */
158
+ export function noopDerivationDeps(): DerivationDeps {
159
+ return {
160
+ fileExists: () => Promise.resolve(false),
161
+ loadDiff: () => Promise.resolve(null),
162
+ openFileInPane: () => {},
163
+ };
164
+ }
165
+
166
+ /** The label each preset takes in the menu and in the derived pane's own strip chip. */
167
+ export const PRESET_LABELS: Readonly<Record<DerivePreset, string>> = {
168
+ breakpoint: "Same page at",
169
+ code: "Code",
170
+ component: "Component definition",
171
+ diff: "Diff vs HEAD",
172
+ layout: "Layout",
173
+ /* An UNFINISHED sentence, like `breakpoint`'s, because the row and the chip both finish it with
174
+ the locale's own autonym — "Same page in français". Jx has no message catalogue (§13.3): a
175
+ translation is a different file in a different directory, so this preset opens that file
176
+ rather than re-rendering this one, and the label says "the same page" about the document
177
+ rather than about the text on screen. */
178
+ locale: "Same page in",
179
+ };
180
+
181
+ // ─── Reading ──────────────────────────────────────────────────────────────────
182
+
183
+ /**
184
+ * The derivation a pane is drawing under, or null.
185
+ *
186
+ * Re-exported from `canvas/canvas-surface.ts`, which is where the pane-scoped RENDERERS already
187
+ * resolve everything else and which this module imports from. One name, one answer.
188
+ */
189
+ export { derivationOfPane } from "../canvas/canvas-surface";
190
+
191
+ /** What a derived pane SHOULD be showing right now. The pure half of the follow. */
192
+ export interface DerivedTarget {
193
+ kind: "lens" | "companion";
194
+ /** Lens: the canvas mode this pane draws in. Null for a companion. */
195
+ mode: string | null;
196
+ /** Lens (`breakpoint`): the media name, null for base. */
197
+ media: string | null;
198
+ /** Companion: the path this pane should show, or null to HOLD the one it has. */
199
+ path: string | null;
200
+ /**
201
+ * Lens (`diff`): the document whose comparison this pane must still LOAD, or null when it has the
202
+ * right one. The impure half acts on it; resolving it stays a read.
203
+ */
204
+ diffPath: string | null;
205
+ /**
206
+ * Companion (`locale`): the translation whose EXISTENCE this pane must still establish, or null
207
+ * when the answer is already in hand. {@link diffPath}'s companion twin, and for the same reason —
208
+ * the resolver decides WHAT to ask, {@link probeTranslationFor} asks it, and neither answer
209
+ * carries a `probePath`, so the follow cannot re-enter.
210
+ */
211
+ probePath: string | null;
212
+ /**
213
+ * Companion (`layout`): the node to select once the document is on screen, or null.
214
+ *
215
+ * The deleted `openLayoutAtNode` existed precisely because "Open Layout →" dropped the author
216
+ * into a layout file with nothing selected and left them to find the header again by eye. The
217
+ * derivation carries it now — which is what `panels/properties-panel.ts`'s ledger entry claimed
218
+ * for a release while nothing read `layoutPath` at all.
219
+ */
220
+ select: JxPath | null;
221
+ status: DerivationStatus;
222
+ /** Printed by the pane when `status` is `unavailable`. Empty otherwise. */
223
+ reason: string;
224
+ }
225
+
226
+ /**
227
+ * Resolve what `paneId` should be showing, from its derivation and its source pane.
228
+ *
229
+ * **Pure**: it reads state and returns an answer, writes nothing, and schedules nothing. That is
230
+ * what makes the follow testable with no rAF, no platform and no canvas — the whole of the
231
+ * derivation's decision-making is one function returning a value.
232
+ *
233
+ * "State" includes {@link _diffLoads}, which is module-scoped rather than on the derivation — a
234
+ * read, and still no write. A comparison that came back empty has to be REMEMBERED somewhere the
235
+ * resolver can see, because a terminal answer written straight onto `derived.status` is overwritten
236
+ * by the next frame's resolve, and the pane then says "Loading…" forever while asking for nothing.
237
+ *
238
+ * @param {string} paneId
239
+ * @returns {DerivedTarget | null} Null when this pane is not derived.
240
+ */
241
+ export function derivedTarget(paneId: string): DerivedTarget | null {
242
+ const derived = paneById(paneId)?.derived;
243
+ if (!derived) {
244
+ return null;
245
+ }
246
+ const source = tabOfPane(derived.sourcePaneId);
247
+ if (!source) {
248
+ return {
249
+ diffPath: null,
250
+ kind: derived.kind,
251
+ media: null,
252
+ mode: null,
253
+ path: null,
254
+ probePath: null,
255
+ reason: "The pane this one follows has no document open.",
256
+ select: null,
257
+ status: "unavailable",
258
+ };
259
+ }
260
+ if (derived.kind === "lens") {
261
+ return lensTarget(paneId, derived, source);
262
+ }
263
+ return companionTarget(paneId, derived, source);
264
+ }
265
+
266
+ /** A lens's target: a mode, and for `breakpoint` a media the source document actually declares. */
267
+ function lensTarget(
268
+ paneId: string,
269
+ derived: Extract<PaneDerivation, { kind: "lens" }>,
270
+ source: Tab,
271
+ ): DerivedTarget {
272
+ const base = {
273
+ diffPath: null,
274
+ kind: "lens" as const,
275
+ media: derived.media,
276
+ mode: derived.mode,
277
+ path: null,
278
+ probePath: null,
279
+ select: null,
280
+ };
281
+ if (derived.preset === "diff") {
282
+ /* THE SOURCE PANE'S DOCUMENT, resolved every time rather than snapshotted once. This branch
283
+ used to read `derived.diff === null`, a copy of `shell.git.diffState` taken when the lens
284
+ was made — so the lens went on rendering whatever file the Git panel had last opened, and
285
+ switching the source tab changed nothing at all. */
286
+ const path = source.documentPath;
287
+ if (!gitChangeFor(path)) {
288
+ return {
289
+ ...base,
290
+ reason: "Nothing to compare — this file matches HEAD.",
291
+ status: "unavailable",
292
+ };
293
+ }
294
+ if (derived.diff?.filePath === path) {
295
+ return { ...base, reason: "", status: "ready" };
296
+ }
297
+ /* A read that came back EMPTY is an answer, and it has to survive the next frame. The failure
298
+ used to be written straight onto `derived.status`, where the follow's very next resolve
299
+ overwrote it with `loading` — so a lens whose comparison could not be read said "Loading
300
+ this file's changes…" for the rest of the session while asking for nothing. `_diffLoads`
301
+ remembers per pane which path was asked and how it went, and the answer is composed here
302
+ with every other one. */
303
+ if (_diffLoads.get(paneId)?.failed && _diffLoads.get(paneId)?.path === path) {
304
+ return { ...base, reason: DIFF_UNREADABLE, status: "unavailable" };
305
+ }
306
+ return { ...base, diffPath: path, reason: "", status: "loading" };
307
+ }
308
+ if (derived.preset === "code" && canvasModeOfTab(source) === "source") {
309
+ /* TWO MONACO MODELS ON ONE URI, refused for as long as the state lasts rather than once at
310
+ creation. `presetRefusal` decides whether a Code lens can be MADE; nothing decided whether
311
+ it could still be drawn, and one click on the source pane's Editor axis put both panes in
312
+ Code over the same file. Real Monaco throws `Cannot create model because a model with the
313
+ same URI already exists` inside the floating `void mountSourceEditor(…)` in
314
+ `canvas/canvas-render.ts` — an unhandled rejection and a blank Code pane. Unreachable before
315
+ derived panes, because one pane could show one document once. */
316
+ return {
317
+ ...base,
318
+ reason: "The pane this one follows is showing Code — one document has one editor.",
319
+ status: "unavailable",
320
+ };
321
+ }
322
+ if (derived.preset === "breakpoint" && !declaredMedia(source).includes(derived.media)) {
323
+ return {
324
+ ...base,
325
+ reason: `This document does not declare a "${derived.media ?? "base"}" breakpoint.`,
326
+ status: "unavailable",
327
+ };
328
+ }
329
+ return { ...base, reason: "", status: "ready" };
330
+ }
331
+
332
+ /**
333
+ * The source-control entry for `path`, when it has changes a comparison can be built from.
334
+ *
335
+ * `M` and `A` and nothing else — the same two `panels/git-panel.ts` opens a diff for, because an
336
+ * untracked or deleted file has no pair of texts to put side by side. `shell.git.status` is the
337
+ * project's whole answer to "what has changed", so a diff lens can be offered for a page the author
338
+ * has never clicked in the Git panel, which is the common case and was the refused one.
339
+ *
340
+ * A `null` path needs no guard of its own: no entry in `files` has a null path, so the search
341
+ * simply finds nothing, and a guard whose true branch returns what the false branch already returns
342
+ * is a branch no test can distinguish.
343
+ *
344
+ * @param {string | null} path
345
+ * @returns {GitFileStatus | null}
346
+ */
347
+ function gitChangeFor(path: string | null): GitFileStatus | null {
348
+ return (
349
+ shell.git.status?.files.find(
350
+ (file) => file.path === path && (file.status === "M" || file.status === "A"),
351
+ ) ?? null
352
+ );
353
+ }
354
+
355
+ /**
356
+ * A companion's target: the path the rule resolves to, or `null` to HOLD.
357
+ *
358
+ * **`null` is a hold, not a change**, and that is the difference between a usable following pane
359
+ * and one nobody leaves open. Clicking a paragraph outside any component resolves to no definition;
360
+ * blanking the pane on that click makes it flicker between a document and an empty state as the
361
+ * author works, and this is the preset somebody leaves open all day.
362
+ *
363
+ * **A hold is only honest where the next gesture could answer differently.** `component` holds
364
+ * because the author's next click may land inside one. `locale` does not: a document with no copy
365
+ * in that language has no copy in it until somebody writes one, and a pane silently holding the
366
+ * previous document under a chip saying "Same page in français" is a lie the author cannot see
367
+ * through. That answer is `unavailable` with the sentence, which is what makes creating the
368
+ * translation the obvious next move rather than a guess.
369
+ */
370
+ function companionTarget(
371
+ paneId: string,
372
+ derived: Extract<PaneDerivation, { kind: "companion" }>,
373
+ source: Tab,
374
+ ): DerivedTarget {
375
+ const base = { diffPath: null, kind: "companion" as const, media: null, mode: null };
376
+ if (derived.preset === "layout") {
377
+ /* THE FILE THE CLICKED CHROME CAME FROM, when there is one.
378
+ `shell.layoutSelection` is a `LayoutHit`: the layout document the canvas hit test resolved
379
+ and the node's path INSIDE it. Resolving the page's own `$layout` instead is a different
380
+ answer for a nested chain — click a header contributed by `layouts/marketing.json`, which
381
+ `layouts/base.json` wraps, and the page's `$layout` names the wrong one of the two. It is
382
+ also the only thing that carries the SELECTION, which is what "Open Layout →" is for. The
383
+ hit is transient — `iframe-host.ts` clears it the moment the author clicks page content —
384
+ so the page's own layout remains the answer for a derivation created from the preset menu. */
385
+ const hit = shell.layoutSelection;
386
+ const path =
387
+ hit?.layoutFile ??
388
+ getEffectiveLayoutPath(source.doc.document.$layout as string | false | undefined);
389
+ return path === null
390
+ ? {
391
+ ...base,
392
+ path: null,
393
+ probePath: null,
394
+ reason: "This page has no layout.",
395
+ select: null,
396
+ status: "unavailable",
397
+ }
398
+ : {
399
+ ...base,
400
+ path,
401
+ probePath: null,
402
+ reason: "",
403
+ /* A SELECTION only when a click produced one. `path` IS `hit.layoutFile` whenever there
404
+ is a hit — the line above says so — so a second `hit.layoutFile === path` guard here
405
+ was a condition with no false branch, and an unreachable branch is a claim nobody can
406
+ check. What stops a node path from layout A landing inside layout B is
407
+ {@link selectInPane}, which compares against the document actually on screen. */
408
+ select: hit ? (hit.layoutPath as JxPath) : null,
409
+ status: "ready",
410
+ };
411
+ }
412
+ if (derived.preset === "locale") {
413
+ /* WHERE THE TRANSLATION WOULD LIVE, which is a different question from whether it is there.
414
+ `translationPathFor` is the same string math the build runs, so the path this resolves to is
415
+ the path the site would serve; it answers null for a document no locale can address at all —
416
+ a layout, a component, a file at the project root — and for a tag the project stopped
417
+ declaring, which is the case {@link presetRefusal} cannot catch because it runs once. */
418
+ const i18n = getEffectiveLocales();
419
+ const wanted =
420
+ source.documentPath === null || derived.locale === null
421
+ ? null
422
+ : translationPathFor(source.documentPath, derived.locale, i18n);
423
+ if (wanted === null) {
424
+ return {
425
+ ...base,
426
+ path: null,
427
+ probePath: null,
428
+ reason: "This document has no path in that locale.",
429
+ select: null,
430
+ status: "unavailable",
431
+ };
432
+ }
433
+ /* AND WHETHER IT IS THERE, which this function cannot ask: it is pure and synchronous, and
434
+ handing back a path for a file that does not exist makes `openFileInPane` fail into a blank
435
+ pane — §18.4's last paragraph refuses exactly that. {@link _localeProbes} is the same shape
436
+ {@link _diffLoads} is, for the same reason: a terminal answer written onto `derived.status`
437
+ is overwritten by the next frame's resolve, so the pane would say "Loading…" forever about a
438
+ file nobody is reading. */
439
+ const probe = _localeProbes.get(paneId);
440
+ if (probe?.path !== wanted || probe.exists === null) {
441
+ return {
442
+ ...base,
443
+ path: null,
444
+ probePath: wanted,
445
+ reason: "",
446
+ select: null,
447
+ status: "loading",
448
+ };
449
+ }
450
+ if (!probe.exists) {
451
+ return {
452
+ ...base,
453
+ path: null,
454
+ probePath: null,
455
+ reason:
456
+ `No ${localeLabel(derived.locale)} copy of this document yet — ` +
457
+ "the Languages panel can create one.",
458
+ select: null,
459
+ status: "unavailable",
460
+ };
461
+ }
462
+ return { ...base, path: wanted, probePath: null, reason: "", select: null, status: "ready" };
463
+ }
464
+ const path = componentPathUnderSelection(source);
465
+ if (path === null) {
466
+ return derived.resolved === null
467
+ ? {
468
+ ...base,
469
+ path: null,
470
+ probePath: null,
471
+ reason: "Select an element inside a component to see its definition.",
472
+ select: null,
473
+ status: "unavailable",
474
+ }
475
+ : { ...base, path: null, probePath: null, reason: "", select: null, status: "ready" };
476
+ }
477
+ return { ...base, path, probePath: null, reason: "", select: null, status: "ready" };
478
+ }
479
+
480
+ /** Every size breakpoint a document declares, plus `null` for base. */
481
+ export function declaredMedia(tab: Tab): (string | null)[] {
482
+ const { sizeBreakpoints } = parseMediaEntries(getEffectiveMedia(tab.doc.document.$media));
483
+ return [null, ...sizeBreakpoints.map((bp) => bp.name)];
484
+ }
485
+
486
+ /* There is no `rawDocOf`. It read the document through `toRaw` under a docstring explaining that
487
+ traversing the reactive proxy would make every node the walk touches a dependency — true of a
488
+ read INSIDE an effect, and none of these are. **Tracking is a property of where a read happens,
489
+ not of which object it happens on**: {@link derivedTarget} and {@link presetRefusal} are reached
490
+ from `queueRetarget`'s rAF, from `loadDiffFor`'s `.then`, from a command's `run` and from
491
+ `panels/pane-context.ts`'s click handler, and there is no active effect at any of those. So the
492
+ `toRaw` bought nothing that being outside an effect had not already bought, and no state the app
493
+ can reach could tell it from its absence. What the resolvers DO read is declared by
494
+ {@link installDerivationEffects}, which is the only place a read of theirs is ever observed. */
495
+
496
+ /**
497
+ * The component definition the selection sits inside, or null.
498
+ *
499
+ * Walks UP from the selected node, because the author clicks the paragraph and means the card. That
500
+ * is also what makes the memo work: twenty clicks inside one `<my-card>` all resolve to the same
501
+ * definition, so `applyDerivation` does nothing at all — no render, no host post, no file read.
502
+ *
503
+ * @param {Tab | null} tab
504
+ * @returns {string | null}
505
+ */
506
+ export function componentPathUnderSelection(tab: Tab | null): string | null {
507
+ const [selected] = tab?.session.selection ?? [];
508
+ if (!tab || !selected) {
509
+ return null;
510
+ }
511
+ const doc = tab.doc.document;
512
+ let path: JxPath | null = [...selected];
513
+ while (path) {
514
+ const node = getNodeAtPath(doc, path);
515
+ const entry = node
516
+ ? componentRegistry.find((candidate) => candidate.tagName === node.tagName)
517
+ : null;
518
+ if (entry?.path) {
519
+ return entry.path;
520
+ }
521
+ path = parentElementPath(path) as JxPath | null;
522
+ }
523
+ return null;
524
+ }
525
+
526
+ // ─── Writing ─────────────────────────────────────────────────────────────────
527
+
528
+ /**
529
+ * Make `paneId` a derived pane. The one writer of `Pane.derived`, with {@link clearPaneDerivation}.
530
+ *
531
+ * Refuses a derivation naming a pane that is not in the grid, or naming this pane — invariant D1,
532
+ * asserted and never repaired: a self-derivation is an infinite `tabOfPane` hop, and a dangling
533
+ * `sourcePaneId` is a stage drawing a document that has no pane.
534
+ *
535
+ * @param {string} paneId
536
+ * @param {PaneDerivation} derivation
537
+ */
538
+ export function setPaneDerivation(paneId: string, derivation: PaneDerivation): void {
539
+ if (derivation.sourcePaneId === paneId || !paneById(derivation.sourcePaneId)) {
540
+ throw new RangeError(
541
+ `pane "${paneId}" cannot derive from "${derivation.sourcePaneId}" — a derivation names ` +
542
+ `another pane that is in the grid`,
543
+ );
544
+ }
545
+ writeDerivation(paneId, derivation);
546
+ }
547
+
548
+ /**
549
+ * Write `pane.derived` and forget the answers the previous one had in hand — its comparison and its
550
+ * translation probe alike. **The only writer, both ways.**
551
+ *
552
+ * A new derivation is a new question, so the previous one's answer is not an answer to it — and a
553
+ * pane that has stopped deriving has no question at all. That was two `_diffLoads.delete` calls
554
+ * under a comment reading "both writers forget it; nothing else may", which is an invariant stated
555
+ * twice and enforced in neither place: only one of the two could be reached by a test, so inverting
556
+ * the other was a mutation the whole suite could not see. One line, one call site each.
557
+ *
558
+ * `workspace.ts` writes `pane.derived = null` twice more — {@link receivingPane} and `closePane`'s
559
+ * D4 — and deliberately does not route through here: those are grid facts, this module imports from
560
+ * that one, and a pane can only derive AGAIN through {@link setPaneDerivation}, which forgets.
561
+ */
562
+ function writeDerivation(paneId: string, derivation: PaneDerivation | null): void {
563
+ const pane = paneById(paneId);
564
+ if (pane) {
565
+ _diffLoads.delete(paneId);
566
+ _localeProbes.delete(paneId);
567
+ pane.derived = derivation;
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Stop deriving. The pane keeps whatever tabs it owns — a companion's document stays open, which is
573
+ * exactly what Pin means — and a lens, owning none, becomes an empty pane its next collapse
574
+ * removes.
575
+ *
576
+ * @param {string} paneId
577
+ */
578
+ export function clearPaneDerivation(paneId: string): void {
579
+ writeDerivation(paneId, null);
580
+ }
581
+
582
+ /**
583
+ * Bring `paneId` into line with {@link derivedTarget}. Idempotent, and safe to run every frame.
584
+ *
585
+ * The only impure half of the follow, and it does exactly two things: it records the status the
586
+ * pane draws, and — for a companion whose answer has CHANGED — it re-points the pane at the new
587
+ * document. The memo is on `resolved`, the answer, not on the selection that produced it.
588
+ *
589
+ * @param {string} paneId
590
+ * @param {DerivationDeps} deps
591
+ */
592
+ export function applyDerivation(paneId: string, deps: DerivationDeps): void {
593
+ const pane = paneById(paneId);
594
+ const derived = pane?.derived;
595
+ const target = derivedTarget(paneId);
596
+ if (!pane || !derived || !target) {
597
+ return;
598
+ }
599
+ canvasPerf.derivedResolves += 1;
600
+ derived.status = target.status;
601
+ derived.reason = target.reason;
602
+ if (derived.kind === "lens") {
603
+ /* Invariant D2, enforced rather than assumed: a lens owns NO tab. Anything sitting in its
604
+ strip when the lens was created has already been handed back to the source pane by
605
+ `pane.derive`; this is the belt to those braces, and it is what keeps `paneOfTab` — which
606
+ every destructive tab verb resolves through — unable to name a lens pane at all.
607
+ The tabs MOVE rather than being dropped: a detach with no insert would leave a live tab in
608
+ `workspace.tabs` that no pane holds, which is a document nobody can reach and nobody can
609
+ close. Nothing a derivation does may lose a document. */
610
+ const source = paneById(derived.sourcePaneId);
611
+ if (pane.tabOrder.length > 0 || pane.activeTabId !== null) {
612
+ const owned = [...pane.tabOrder];
613
+ for (const tabId of owned) {
614
+ detachTab(tabId);
615
+ if (source) {
616
+ insertIntoPane(source, tabId);
617
+ }
618
+ }
619
+ pane.activeTabId = null;
620
+ }
621
+ /* A DIFF LENS HOLDS A COMPARISON ONLY WHILE IT IS THE SOURCE DOCUMENT'S, and nothing enforced
622
+ that. `derivedTarget` says `ready` for exactly the case where the one in hand is the right
623
+ one; every other answer — the wrong file in hand after a tab switch, a source document with
624
+ no changes, no source document at all — means the pane is holding somebody else's, and
625
+ `canvas/canvas-render.ts` draws whatever is in this field rather than the notice beside it.
626
+ Clearing it is what makes that file's `if (!gitDiffState)` fire. The previous release
627
+ shipped a comment claiming this was fixed and no line that did it. */
628
+ if (derived.preset === "diff" && target.status !== "ready") {
629
+ derived.diff = null;
630
+ }
631
+ loadDiffFor(paneId, target, deps);
632
+ return;
633
+ }
634
+ /* FIRST, unlike {@link loadDiffFor}, which is the last thing the lens half does. A probe that has
635
+ not answered yet resolves to `path: null` — the hold — and the hold returns two statements
636
+ below, so a call at the end of this function would never be reached by the one target that
637
+ needs it. */
638
+ probeTranslationFor(paneId, target, deps);
639
+ /* THE MEMO IS ABOUT WHAT IS ON SCREEN, so it cannot outlive the document being on screen.
640
+ A resolved companion owns a real tab, so `panels/tab-strip.ts` draws a real chip with a real
641
+ ✕ — and closing it left the pane holding a derivation whose `resolved` still named the file
642
+ that had just gone. `paneIsEmpty` counts the derivation as a subject so the pane stayed; the
643
+ strip fell back to the derivation chip; and the stage said "Looking for something to show
644
+ here…" for the rest of the session, because every following frame found `target.path ===
645
+ derived.resolved` and did nothing. Forgetting the answer when the pane is showing nothing is
646
+ what lets the next frame ask the question again. */
647
+ if (derived.resolved !== null && tabOfPane(paneId) === null) {
648
+ derived.resolved = null;
649
+ }
650
+ if (target.path === null || target.path === derived.resolved) {
651
+ // Same document, possibly a different node inside it — clicking a second piece of layout chrome
652
+ // Moves the selection without re-reading the file.
653
+ selectInPane(paneId, target);
654
+ return;
655
+ }
656
+ derived.resolved = target.path;
657
+ canvasPerf.derivedRetargets += 1;
658
+ const opening = deps.openFileInPane(paneId, target.path);
659
+ if (target.select === null) {
660
+ return;
661
+ }
662
+ /* The selection lands AFTER the open, because there is no tab to select in until then. Awaiting
663
+ rather than firing both is what makes "Open Layout →" arrive at the node the author clicked
664
+ instead of at the top of a file they now have to search. */
665
+ void Promise.resolve(opening).then(() => {
666
+ selectInPane(paneId, target);
667
+ });
668
+ }
669
+
670
+ /**
671
+ * Select `target.select` in whatever `paneId` is showing, when that is the document it is about.
672
+ *
673
+ * Idempotent, and it has to be: {@link applyDerivation} runs every frame the follow fires, and a
674
+ * `session.selection` write is a reactive write that repaints the Inspector, the overlays and the
675
+ * block bar. Comparing first is what keeps a following pane free.
676
+ */
677
+ function selectInPane(paneId: string, target: DerivedTarget): void {
678
+ const wanted = target.select;
679
+ const tab = tabOfPane(paneId);
680
+ if (!wanted || !tab || (target.path !== null && tab.documentPath !== target.path)) {
681
+ return;
682
+ }
683
+ const [current] = tab.session.selection;
684
+ if (current?.length === wanted.length && current.every((seg, i) => seg === wanted[i])) {
685
+ return;
686
+ }
687
+ tab.session.selection = [[...wanted]];
688
+ }
689
+
690
+ /**
691
+ * The comparison each diff lens has ASKED for, and how that went.
692
+ *
693
+ * Never cleared on COMPLETION, only when the wanted path changes, so the follow's next frame cannot
694
+ * re-issue a failing request forever. `failed` is the half that was missing: the status
695
+ * `loadDiffFor` wrote when the read came back empty was overwritten by the very next resolve, and
696
+ * the pane went back to saying "Loading this file's changes…" permanently. A terminal answer has to
697
+ * live where {@link lensTarget} — which composes every other answer — can see it.
698
+ */
699
+ const _diffLoads = new Map<string, { path: string; failed: boolean }>();
700
+
701
+ /**
702
+ * Fetch the comparison a diff lens is missing, once.
703
+ *
704
+ * @param {string} paneId
705
+ * @param {DerivedTarget} target
706
+ * @param {DerivationDeps} deps
707
+ */
708
+ function loadDiffFor(paneId: string, target: DerivedTarget, deps: DerivationDeps): void {
709
+ const path = target.diffPath;
710
+ const change = gitChangeFor(path);
711
+ if (path === null || !change || _diffLoads.get(paneId)?.path === path) {
712
+ return;
713
+ }
714
+ _diffLoads.set(paneId, { failed: false, path });
715
+ deps
716
+ .loadDiff(path, change.status)
717
+ .then((state) => {
718
+ const live = paneById(paneId)?.derived;
719
+ if (
720
+ live?.kind !== "lens" ||
721
+ live.preset !== "diff" ||
722
+ _diffLoads.get(paneId)?.path !== path
723
+ ) {
724
+ return;
725
+ }
726
+ if (state) {
727
+ live.diff = state;
728
+ } else {
729
+ _diffLoads.set(paneId, { failed: true, path });
730
+ }
731
+ /* Re-resolve rather than writing a status here: `ready` with the comparison in hand,
732
+ `unavailable` with {@link DIFF_UNREADABLE} when the read came back empty — composed in the
733
+ one function that composes every other answer, so the next frame agrees with this one. It
734
+ cannot re-enter, because neither of those answers carries a `diffPath`. */
735
+ applyDerivation(paneId, deps);
736
+ })
737
+ .catch((error: unknown) => {
738
+ console.error("loadDiff error:", error);
739
+ });
740
+ }
741
+
742
+ /**
743
+ * Which translation each locale companion has ASKED about, and whether it is there.
744
+ *
745
+ * {@link _diffLoads}'s twin, and cleared by the same writer for the same reason. `exists` is
746
+ * THREE-valued on purpose: `null` is "asked, no answer yet", which is the state the pane draws
747
+ * "Loading…" for, and it cannot be folded into `false` — `false` is a terminal answer with a
748
+ * sentence naming the language and offering to create the file, and showing that sentence for the
749
+ * frame between the question and the answer would tell the author their translation is missing
750
+ * every time they open the pane.
751
+ */
752
+ const _localeProbes = new Map<string, { path: string; exists: boolean | null }>();
753
+
754
+ /**
755
+ * Ask whether the translation a locale companion wants is on disk, once per wanted path.
756
+ *
757
+ * @param {string} paneId
758
+ * @param {DerivedTarget} target
759
+ * @param {DerivationDeps} deps
760
+ */
761
+ function probeTranslationFor(paneId: string, target: DerivedTarget, deps: DerivationDeps): void {
762
+ const path = target.probePath;
763
+ if (path === null || _localeProbes.get(paneId)?.path === path) {
764
+ return;
765
+ }
766
+ _localeProbes.set(paneId, { exists: null, path });
767
+ deps
768
+ .fileExists(path)
769
+ .then((exists) => {
770
+ const live = paneById(paneId)?.derived;
771
+ /* THE ANSWER TO THE QUESTION THAT IS STILL BEING ASKED. A read issued for `fr` that lands
772
+ after the author retargeted the pane at `de` would otherwise write `de`'s memo with `fr`'s
773
+ answer — the same late-landing hazard {@link loadDiffFor} carries, and the same guard. */
774
+ if (
775
+ live?.kind !== "companion" ||
776
+ live.preset !== "locale" ||
777
+ _localeProbes.get(paneId)?.path !== path
778
+ ) {
779
+ return;
780
+ }
781
+ _localeProbes.set(paneId, { exists, path });
782
+ /* Re-resolve rather than writing a status here, for {@link loadDiffFor}'s reason: both
783
+ answers are composed by {@link companionTarget} beside every other one, and neither of
784
+ them carries a `probePath`, so this cannot re-enter. */
785
+ applyDerivation(paneId, deps);
786
+ })
787
+ .catch((error: unknown) => {
788
+ console.error("fileExists error:", error);
789
+ });
790
+ }
791
+
792
+ // ─── The follow ───────────────────────────────────────────────────────────────
793
+
794
+ /** One pending retarget per pane. A shared id would let one pane swallow the other's frame. */
795
+ const _retargetRafIds = new Map<string, number>();
796
+
797
+ /**
798
+ * Run `applyDerivation` on the next frame, once per pane however many times it is asked.
799
+ *
800
+ * **Scheduled, never synchronous.** The effect below reads `workspace.panes`, and a retarget writes
801
+ * `pane.tabOrder` through `openFileInPane` — an effect that triggers itself, which is the hazard
802
+ * `panels/pane-grid.ts` names at its own reconciler. The rAF hop is the same shape
803
+ * `scheduleCanvasRender` uses, for the same reason.
804
+ */
805
+ function queueRetarget(paneId: string, deps: DerivationDeps): void {
806
+ if (_retargetRafIds.has(paneId)) {
807
+ return;
808
+ }
809
+ _retargetRafIds.set(
810
+ paneId,
811
+ requestAnimationFrame(() => {
812
+ _retargetRafIds.delete(paneId);
813
+ try {
814
+ applyDerivation(paneId, deps);
815
+ } catch (error) {
816
+ console.error("applyDerivation error:", error);
817
+ }
818
+ }),
819
+ );
820
+ }
821
+
822
+ /**
823
+ * Subscribe ONE pane's follow. Called from `studio.ts`'s per-pane effect scope, so it is stopped
824
+ * when the pane leaves the grid.
825
+ *
826
+ * **Most of the follow is STRUCTURAL, and that is still the design.** `tabOfPane(paneId)` reads the
827
+ * source pane's `activeTabId`, so the render effects already installed for this pane track it and
828
+ * repaint when the source pane switches document.
829
+ *
830
+ * **Every derivation subscribes, and this effect is where.** No preset "follows structurally and
831
+ * declares nothing": the body below reads THIS pane's `activeTabId` and `tabOfPane(sourcePaneId)`
832
+ * unconditionally, whatever the preset — `diff` and `breakpoint` are the two that add nothing on
833
+ * top. An input a resolver consults and the effect does not observe is an answer nobody asks for:
834
+ * this docstring claimed the `$layout` subscription for a release while the effect did not have it,
835
+ * the Code lens's refusal against a source pane showing Code arrived the same way, and so did
836
+ * {@link applyDerivation}'s memo-forget, whose comment described closing a companion's document
837
+ * while nothing re-ran the follow when it closed.
838
+ *
839
+ * The list is BY INPUT rather than by preset, because `layout` declares two of them. Every input is
840
+ * a HUMAN GESTURE rather than a keystroke — closing a tab in this pane, switching the source pane's
841
+ * document, a layout click, a selection, a click on the Editor axis — except the page's own
842
+ * `$layout`, whose per-transaction cost is stated at the read itself. `layout` follows the layout
843
+ * HIT and `$layout`; `component` is the only preset that observes the SELECTION, and it memoises on
844
+ * its answer; `code` observes the source pane's canvas mode, because the refusal that keeps two
845
+ * Monaco models off one URI is a standing answer about a value one click changes.
846
+ *
847
+ * Each of those claims is a test rather than a sentence — see `pane-derive.test.ts`'s "the follow
848
+ * NOTICES the close", "a companion re-resolves when the SOURCE pane switches document", "the layout
849
+ * follow OBSERVES the hit", "changing the page's layout MOVES the companion", "clicking into a
850
+ * component RE-POINTS the pane" and "a Code lens notices the source pane switching to Code".
851
+ *
852
+ * @param {string} paneId
853
+ * @param {DerivationDeps} deps
854
+ */
855
+ export function installDerivationEffects(paneId: string, deps: DerivationDeps): void {
856
+ effect(() => {
857
+ const pane = paneById(paneId);
858
+ const derived = pane?.derived;
859
+ if (!derived) {
860
+ return;
861
+ }
862
+ /* THIS PANE'S OWN TAB, for both kinds, and its absence is the interesting value.
863
+ `applyDerivation` forgets a companion's memo when the pane is showing nothing, so the next
864
+ frame asks the question again — and there was no next frame. The tracked inputs were the
865
+ SOURCE pane's tab and the selection, neither of which a `closeTab` in the following pane
866
+ touches, so closing the document a layout companion had opened left the pane stranded behind
867
+ a memo naming a file that was gone: the exact failure that comment describes, with the fix
868
+ one call away from ever running. A lens reads it too, for one line's worth of the same
869
+ reason: invariant D2 says a lens owns no tab, `applyDerivation` hands one back if it finds
870
+ one, and an insertion nothing observes is an insertion nothing repairs.
871
+ It costs one resolve per open or close in this pane — a gesture, not a keystroke — and no
872
+ retarget, because the answer the memo compares against has not changed. */
873
+ void pane.activeTabId;
874
+ /* The rest of the tracked inputs, and only these: which tab the source pane is showing, and —
875
+ for the presets that observe them — that tab's selection, editor kind and `$layout`. What
876
+ the RESOLVERS read is not among them, because they run in `queueRetarget`'s rAF where no
877
+ effect is active: an untracked read is untracked because of WHERE it happens. So typing,
878
+ which replaces `tab.doc.document`, re-runs this effect only for the one preset that reads
879
+ that field HERE. */
880
+ const source = tabOfPane(derived.sourcePaneId);
881
+ if (derived.kind === "lens" && derived.preset === "code") {
882
+ /* The SOURCE pane's editor kind, and only this preset reads it. {@link lensTarget} refuses a
883
+ Code lens for as long as the pane it follows is ALSO showing Code — two Monaco models on
884
+ one URI — and that is a standing answer about a value one click on the Editor axis
885
+ changes. Untracked, the refusal would be computed only on some later frame that
886
+ re-resolved for another reason, and until then the stage mounts the second model, which is
887
+ the throw the refusal exists to prevent. One human gesture per change, like the layout
888
+ hit; nothing per keystroke. */
889
+ void source?.session.ui.canvasMode;
890
+ }
891
+ if (derived.kind === "companion" && derived.preset === "component") {
892
+ void source?.session.selection;
893
+ }
894
+ /* `locale` ADDS NOTHING, and the omission is the honest answer rather than a gap. The two
895
+ inputs above — this pane's own tab and the source pane's — are exactly what it reads: the
896
+ locale is fixed on the derivation, and the file it wants is a function of the source
897
+ document's path. Its third input, the project's declared locales, CANNOT be tracked:
898
+ `projectState` is a plain `let` (`store/state.ts`) that `setProjectState` replaces whole, so
899
+ no effect anywhere observes it. Adding a locale in Project Settings therefore does not
900
+ re-run this follow; the next retarget — any tab switch in either pane — picks it up, and
901
+ {@link companionTarget} re-reads the list every time it resolves, so the pane cannot go on
902
+ showing a locale the project has stopped declaring. A `void` read of a non-reactive value
903
+ would look like a subscription and be none, which is the shape the `layout` preset shipped
904
+ a release of. */
905
+ if (derived.kind === "companion" && derived.preset === "layout") {
906
+ /* The LAYOUT hit, which is what "Open Layout →" is a follow of. It changes only when the
907
+ author clicks layout chrome (`iframe-host.ts` writes it) or clicks page content (which
908
+ clears it) — a human gesture, not a keystroke — so it costs nothing to observe and it is
909
+ the difference between a following layout pane and one frozen on the first header. */
910
+ void shell.layoutSelection;
911
+ /* AND the page's own `$layout`, which is the answer whenever there is no hit. The docstring
912
+ above claimed this preset followed it while the effect did not read it at all: the
913
+ resolver's read happens in the rAF, where nothing is tracking, so changing a page's layout
914
+ in the Inspector left the companion on the old file with the pure half answering correctly
915
+ and nothing calling it. An input a resolver consults and the effect does not observe is an
916
+ answer nobody asks for.
917
+ The cost is stated rather than hidden. `tab.doc.document` is replaced by every
918
+ transaction, so this re-runs on a keystroke — but `queueRetarget` collapses a frame's
919
+ worth into one rAF and `applyDerivation` memoises on the resolved PATH, so the expensive
920
+ half (the file read) still happens exactly once per real change of layout. `component`
921
+ does NOT read it, and its memo test is what holds that: it types twenty characters and
922
+ asserts the resolve count does not move, which a per-transaction read here would break. */
923
+ void source?.doc.document?.$layout;
924
+ }
925
+ queueRetarget(paneId, deps);
926
+ });
927
+ }
928
+
929
+ // ─── Commands ─────────────────────────────────────────────────────────────────
930
+
931
+ /**
932
+ * The refusal for a preset that cannot be built right now, or null when it can.
933
+ *
934
+ * **Run-time, not `enablement`** — the same shape `canvas.setMode` uses, and for the same reason:
935
+ * `enablement` is ONE sentence for the whole command and five presets have five reasons. The menu
936
+ * renders each row disabled with the sentence this returns, and `run` throws a `RangeError`
937
+ * carrying it, so the tooltip and the refusal are the same words.
938
+ *
939
+ * `locale` takes a fourth argument, DEFAULTED rather than added to every call: the two callers that
940
+ * ask about a breakpoint have nothing to say about a language, and a required parameter would have
941
+ * made them pass a `null` that means nothing at those sites.
942
+ *
943
+ * @param {DerivePreset} preset
944
+ * @param {string} sourcePaneId
945
+ * @param {string | null} media
946
+ * @param {string | null} locale
947
+ * @returns {string | null}
948
+ */
949
+ export function presetRefusal(
950
+ preset: DerivePreset,
951
+ sourcePaneId: string,
952
+ media: string | null,
953
+ locale: string | null = null,
954
+ ): string | null {
955
+ const source = tabOfPane(sourcePaneId);
956
+ if (!source) {
957
+ return "an open document";
958
+ }
959
+ /* WHAT THE DOCUMENT CAN BE SHOWN AS, asked before anything else. See {@link MODE_FOR_PRESET}:
960
+ three of the five presets put the pane into a named canvas mode, and the menu offered all five
961
+ over a document whose format declares neither. */
962
+ const needed = MODE_FOR_PRESET[preset];
963
+ if (needed && !source.capabilities.modes.includes(needed)) {
964
+ return `a document with a ${MODE_WORDS[needed]} view — this one declares none`;
965
+ }
966
+ if (preset === "code") {
967
+ /* Refused at CREATION, and refused again for as long as it lasts. Two Code views of one file
968
+ is a second Monaco model on one URI, which real Monaco throws on; this predicate stops the
969
+ lens being MADE that way, and {@link lensTarget} stops it being DRAWN that way when the
970
+ source pane switches to Code afterwards. A comment here once claimed one predicate bought
971
+ the whole of that safety, and one click on the Editor axis was enough to disprove it. */
972
+ return canvasModeOfTab(source) === "source"
973
+ ? "a source pane that is not already showing Code"
974
+ : null;
975
+ }
976
+ if (preset === "diff") {
977
+ /* Asked of THIS DOCUMENT's source-control status, not of an app-level slot. It read
978
+ `shell.git.diffState === null`, which is written only when the author clicks a file in the
979
+ Git panel — so the row was refused for a page that genuinely has changes, and offered for a
980
+ page that does not the moment some other file's diff was on screen. */
981
+ return gitChangeFor(source.documentPath) === null ? "a file with changes against HEAD" : null;
982
+ }
983
+ if (preset === "breakpoint") {
984
+ return declaredMedia(source).includes(media)
985
+ ? null
986
+ : "a breakpoint this document declares — see Project Settings › Contexts";
987
+ }
988
+ if (preset === "layout") {
989
+ /* REFUSED UP FRONT, because the answer is a stable fact about the document rather than a
990
+ standing rule waiting on the author's next click. Offered anyway, choosing it published a
991
+ derivation whose companion never opened anything: a pane holding a derivation and no tabs,
992
+ which `paneIsEmpty` will not collapse, whose strip drew nothing and whose stage drew nothing.
993
+ `component` is deliberately NOT refused the same way — "select an element inside a component"
994
+ is a rule that resolves on the next click, which is the whole point of leaving it open. */
995
+ return getEffectiveLayoutPath(source.doc.document.$layout as string | false | undefined) ===
996
+ null
997
+ ? "a page with a layout — this one declares none"
998
+ : null;
999
+ }
1000
+ if (preset === "locale") {
1001
+ /* THE PROJECT'S OWN LIST, read at the moment the menu is drawn. `projectState` is a plain
1002
+ module-level `let` replaced wholesale by `setProjectState`, so a locale added in Project
1003
+ Settings is visible to the next call and to nothing that cached one. */
1004
+ const i18n = getEffectiveLocales();
1005
+ if (i18n === null || i18n.locales.length < 2) {
1006
+ return "a project that declares more than one locale — see Project Settings › Locales";
1007
+ }
1008
+ /* A PATH, because a translation is a FILE (§13.3) rather than a rendering: an unsaved document
1009
+ has nowhere for its sibling to be, and `translationPathFor` would answer null for it one
1010
+ frame later with a sentence about locales instead of about saving. */
1011
+ if (source.documentPath === null) {
1012
+ return "a document that has been saved";
1013
+ }
1014
+ return locale !== null && i18n.locales.includes(locale)
1015
+ ? null
1016
+ : "a locale this project declares";
1017
+ }
1018
+ return null;
1019
+ }
1020
+
1021
+ /**
1022
+ * Why `paneId` cannot derive, or null when it can. **A pure function OF THE PANE.**
1023
+ *
1024
+ * `pane.derive`'s `enablement` opened with `activePane()` and the menu asked the registry
1025
+ * `disabledReason("pane.derive")` for a pane it had been handed by name — so the same pane got two
1026
+ * answers depending on where the keyboard was, and every row in the SECONDARY pane's menu was
1027
+ * permanently disabled the moment the author clicked into the lens beside it.
1028
+ *
1029
+ * **The structural point, stated once here.** A command's `enablement(ctx)` receives no pane, and
1030
+ * it cannot: a command's subject IS the focused pane, which is what makes `⌘\` mean "split the
1031
+ * thing I am looking at". A per-pane MENU is a different question with a different subject, so it
1032
+ * must not be routed through a predicate that resolves one. The answer therefore lives in a pure
1033
+ * predicate the menu calls with its own pane, and the command record reuses it for the focused one
1034
+ * — one rule, two subjects, no second definition site.
1035
+ *
1036
+ * `scripts/check-pane-singletons.ts` rule 4 charges a pane-scoped function for reading the focus
1037
+ * one hop in, and it could not see this one: the hop was a METHOD CALL on a registry value,
1038
+ * dispatched by a string id to a closure built in another module. Following THAT needs a type
1039
+ * checker. What the rule can decide is that the question was asked, so `disabledReason` and
1040
+ * `isEnabled` are focus reads now — see that file's {@link FOCUS_NAMES}. The closure is still
1041
+ * invisible and is not pretended otherwise.
1042
+ *
1043
+ * @param {string} paneId
1044
+ * @returns {string | null}
1045
+ */
1046
+ export function deriveRefusal(paneId: string): string | null {
1047
+ const pane = paneById(paneId);
1048
+ return pane && pane.activeTabId !== null && pane.derived === null ? null : DERIVE_REQUIRES;
1049
+ }
1050
+
1051
+ /** The one sentence `pane.derive` refuses with — its `requires`, and the menu's disabled reason. */
1052
+ const DERIVE_REQUIRES = "an open document in a pane that is not itself derived";
1053
+
1054
+ /** The derivation record a preset builds, given the pane it derives from. */
1055
+ function derivationFor(
1056
+ preset: DerivePreset,
1057
+ sourcePaneId: string,
1058
+ media: string | null,
1059
+ locale: string | null = null,
1060
+ ): PaneDerivation {
1061
+ /* A COMPANION, because the French copy of this page is a different FILE — §13.3 has no message
1062
+ catalogue, so there is no rendering of this document that is the French one. A lens here would
1063
+ be a second copy of the same page under a chip naming another language, which is the defect
1064
+ this module's own header warns about: a projection that changes only the chip. */
1065
+ if (preset === "locale") {
1066
+ return {
1067
+ kind: "companion",
1068
+ locale,
1069
+ preset,
1070
+ reason: "",
1071
+ resolved: null,
1072
+ sourcePaneId,
1073
+ status: "loading",
1074
+ };
1075
+ }
1076
+ if (preset === "layout" || preset === "component") {
1077
+ return {
1078
+ kind: "companion",
1079
+ preset,
1080
+ reason: "",
1081
+ resolved: null,
1082
+ sourcePaneId,
1083
+ status: "loading",
1084
+ };
1085
+ }
1086
+ const mode = preset === "code" ? "source" : preset === "diff" ? "git-diff" : "design";
1087
+ return {
1088
+ /* NO COMPARISON, whatever the Git panel happens to be holding. This seeded itself from
1089
+ `shell.git.diffState` — an app-level slot carrying the file the author last clicked in the
1090
+ Git panel — so a brand-new Diff lens's FIRST PAINT was somebody else's document, under a
1091
+ chip naming this one, until the follow's frame corrected it. The lens loads its own. */
1092
+ diff: null,
1093
+ kind: "lens",
1094
+ media: preset === "breakpoint" ? media : null,
1095
+ mode,
1096
+ preset,
1097
+ reason: "",
1098
+ sourcePaneId,
1099
+ status: "loading",
1100
+ zoom: 1,
1101
+ };
1102
+ }
1103
+
1104
+ /**
1105
+ * Whether `pane` can be pinned — a companion can, a lens cannot.
1106
+ *
1107
+ * A companion names a different FILE, so promoting its preview tab and dropping the derivation
1108
+ * leaves an ordinary tab, exactly as §18.4 says. A lens has no document of its own: pinning it
1109
+ * would have to mint a second tab for a path that already has one, which is the §14.1 violation the
1110
+ * whole design exists to avoid. Its exit is `pane.unsplit`.
1111
+ */
1112
+ function pinnablePane(): Pane | null {
1113
+ return workspace.panes.find((pane) => pinRefusal(pane.id) === null) ?? null;
1114
+ }
1115
+
1116
+ /**
1117
+ * Why `paneId` cannot be pinned, or null when it can. Pure, like {@link deriveRefusal}.
1118
+ *
1119
+ * **Unlike `pane.derive`, the COMMAND's subject is the grid rather than the focused pane**, and
1120
+ * that is deliberate: the author reaches "Keep This Document" from the palette while the keyboard
1121
+ * is in the page they are editing, and a grid holds at most one derived pane — `pane.derive` puts
1122
+ * the projection in `sidePane()`, and `workspace.ts`'s `MAX_PANES` is 2. {@link pinnablePane} finds
1123
+ * it by scanning, which reads no focus and is therefore the same answer from anywhere — so the
1124
+ * preset menu can ask it about its own pane and get an answer about its own pane.
1125
+ *
1126
+ * @param {string} paneId
1127
+ * @returns {string | null}
1128
+ */
1129
+ export function pinRefusal(paneId: string): string | null {
1130
+ const pane = paneById(paneId);
1131
+ return pane?.derived?.kind === "companion" && pane.activeTabId ? null : PIN_REQUIRES;
1132
+ }
1133
+
1134
+ /** `pane.pin`'s `requires`, and the menu's disabled reason. One sentence, one home. */
1135
+ const PIN_REQUIRES =
1136
+ "a derived pane showing a document of its own — Code, Diff and breakpoint views project " +
1137
+ "the document already open beside them";
1138
+
1139
+ /**
1140
+ * The derivation commands: `pane.derive` and `pane.pin`.
1141
+ *
1142
+ * They live here rather than in `paneCommands()` because a capability's record belongs beside its
1143
+ * implementation — the same rule the tab and pane commands follow. `pane.compareWith` deliberately
1144
+ * stays in `workspace.ts`: it is a transport verb over the pane model and needs none of this.
1145
+ *
1146
+ * @param {DerivationDeps} deps
1147
+ * @returns {AnyCommand[]}
1148
+ */
1149
+ export function derivationCommands(deps: DerivationDeps): AnyCommand[] {
1150
+ return [
1151
+ {
1152
+ args: argsSchema(
1153
+ {
1154
+ locale: stringProperty('BCP 47 tag for preset "locale". Ignored by every other preset.'),
1155
+ media: stringProperty(
1156
+ 'Breakpoint name for preset "breakpoint" — omit or leave empty for the base size. ' +
1157
+ "Ignored by every other preset.",
1158
+ ),
1159
+ preset: enumProperty(
1160
+ DERIVE_PRESETS,
1161
+ "Which projection of this pane's document to show beside it.",
1162
+ ),
1163
+ },
1164
+ ["preset"],
1165
+ ),
1166
+ id: "pane.derive",
1167
+ title: "Show Beside This…",
1168
+ category: "View",
1169
+ level: "document",
1170
+ /* NOT in the palette, and that is a fact about the palette rather than a judgement about
1171
+ this verb: `panels/quick-search.ts`'s `paletteArgs` can prompt for exactly ONE closed value
1172
+ space, so a command with two properties is dropped from command mode entirely. Declaring
1173
+ `"palette"` here would declare a surface that never renders the row — the same
1174
+ "declared and never drawn" defect `context/pane` itself spent two phases being. The preset
1175
+ menu on the pane's context bar is this command's surface, and every row there IS the
1176
+ command with its arguments (§13.5), which is also how the screenshot manifest addresses
1177
+ it. */
1178
+ menus: ["context/pane"],
1179
+ group: "5_pane",
1180
+ when: (ctx) => ctx.document.open,
1181
+ /* One sentence, because `enablement` only has one. What each preset needs is decided at run
1182
+ time by {@link presetRefusal}, which the menu also reads to disable a row with its own
1183
+ reason.
1184
+ The FOCUS is read HERE and only here — a command's subject is the pane the keyboard is in,
1185
+ and that read belongs at the record where a reviewer can see it. {@link deriveRefusal} is
1186
+ the pane-scoped predicate underneath, which is what the per-pane menu calls instead. */
1187
+ enablement: () => deriveRefusal(activePane().id) === null,
1188
+ requires: DERIVE_REQUIRES,
1189
+ undo: "none",
1190
+ aiTool: {
1191
+ description: "Show a projection of the current document in the pane beside it.",
1192
+ name: "show_beside",
1193
+ },
1194
+ run: (_ctx, args) => {
1195
+ const preset = enumArg("pane.derive", args, "preset", DERIVE_PRESETS);
1196
+ /* `??`, not `||`. `optionalStringArg` answers `undefined` or a non-empty string — it is
1197
+ `stringArg` underneath, which REFUSES `""` — so the two operators cannot disagree here,
1198
+ and the `||` implied a blank the caller can never deliver. */
1199
+ const media = optionalStringArg("pane.derive", args, "media") ?? null;
1200
+ const locale = optionalStringArg("pane.derive", args, "locale") ?? null;
1201
+ const source = activePane();
1202
+ const refusal = presetRefusal(preset, source.id, media, locale);
1203
+ if (refusal) {
1204
+ throw new RangeError(
1205
+ `command "pane.derive" argument "preset": "${preset}" requires ${refusal}`,
1206
+ );
1207
+ }
1208
+ const target = sidePane();
1209
+ /* The DERIVATION IS PUBLISHED FIRST, and the order is not cosmetic.
1210
+ §18.1 rule 3 removes a pane with no subject, and `detachTab` applies it as each tab
1211
+ leaves — so handing the side pane's tabs back before it had a derivation collapsed the
1212
+ very pane about to become the lens, and `setPaneDerivation` then wrote onto a pane that
1213
+ was no longer in the grid. `paneIsEmpty` counts a derivation as a subject; giving the
1214
+ pane one first is what makes the hand-back safe. Nothing renders between these two
1215
+ statements — `run` is synchronous to the end — so no frame observes a lens with tabs. */
1216
+ setPaneDerivation(target.id, derivationFor(preset, source.id, media, locale));
1217
+ /* NOTHING IS CLOSED. Whatever the side pane was holding goes back to the pane the author
1218
+ is in — the same promise `closePane` makes — so a derivation is a layout action rather
1219
+ than a destructive one. */
1220
+ /* `detachTab` reassigns `activeTabId` to `tabOrder.at(-1) ?? null` as each tab leaves, so
1221
+ the pane's own field is already `null` when the last one has gone: a trailing
1222
+ `target.activeTabId = null` here wrote the value it already held, and inverting it
1223
+ changed nothing any state could reach. `applyDerivation` still owns the D2 repair for a
1224
+ tab that arrives afterwards. */
1225
+ const displaced = [...target.tabOrder];
1226
+ for (const tabId of displaced) {
1227
+ detachTab(tabId);
1228
+ insertIntoPane(source, tabId);
1229
+ }
1230
+ applyDerivation(target.id, deps);
1231
+ // The focus does NOT move. An assistant pane does not take the keyboard: the author asked
1232
+ // To SEE something beside what they are doing, not to go and work on it.
1233
+ },
1234
+ },
1235
+ {
1236
+ id: "pane.pin",
1237
+ title: "Keep This Document",
1238
+ category: "View",
1239
+ level: "document",
1240
+ menus: ["context/pane", "palette"],
1241
+ group: "5_pane",
1242
+ /* Enabled for a COMPANION only, and the sentence says which — §18.4's "until it is pinned, at
1243
+ which point it becomes an ordinary tab" is true for `layout` and `component` and impossible
1244
+ for `code`, `diff` and `breakpoint`. */
1245
+ enablement: () => pinnablePane() !== null,
1246
+ requires: PIN_REQUIRES,
1247
+ undo: "none",
1248
+ run: () => {
1249
+ const pane = pinnablePane();
1250
+ if (!pane?.activeTabId) {
1251
+ return;
1252
+ }
1253
+ promoteTab(pane.activeTabId);
1254
+ clearPaneDerivation(pane.id);
1255
+ },
1256
+ },
1257
+ ];
1258
+ }