@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
@@ -5,40 +5,111 @@
5
5
  */
6
6
 
7
7
  import { html, nothing } from "lit-html";
8
- import type { CanvasPanel } from "../types";
8
+ import type { CanvasPanel, JsonValue } from "../types";
9
9
  import { ref } from "lit-html/directives/ref.js";
10
10
  import { classMap } from "lit-html/directives/class-map.js";
11
11
  import { styleMap } from "lit-html/directives/style-map.js";
12
12
  import { ifDefined } from "lit-html/directives/if-defined.js";
13
13
 
14
- import { canvasPanels, canvasWrap, renderOnly, updateUi } from "../store";
15
- import { activeTab } from "../workspace/workspace";
16
- import { view } from "../view";
17
- import { findCanvasElement, getActivePanel, panelMediaToActiveMedia } from "./canvas-helpers";
14
+ import { renderOnly } from "../store";
15
+ import {
16
+ activeCanvasSurface,
17
+ activeMediaOfPane,
18
+ canvasModeOfPane,
19
+ derivationOfPane,
20
+ tabOfMountedPanel,
21
+ tabOfPane,
22
+ } from "./canvas-surface";
23
+ import type { CanvasSurface } from "./canvas-surface";
24
+ import { activeTab, workspace } from "../workspace/workspace";
25
+ import {
26
+ findCanvasElement,
27
+ getActivePanel,
28
+ panelMediaToActiveMedia,
29
+ panelOfSurface,
30
+ } from "./canvas-helpers";
18
31
  import { rectOf } from "../utils/geometry";
32
+ import { getEffectiveLocales, getEffectiveMedia } from "../site-context";
33
+ import { dynamicRouteParams } from "../page-params";
34
+ import {
35
+ argsSchema,
36
+ booleanArg,
37
+ booleanProperty,
38
+ boundedNumberArg,
39
+ enumArg,
40
+ enumProperty,
41
+ numberProperty,
42
+ stringArg,
43
+ stringProperty,
44
+ } from "../commands/command-args";
19
45
  import type { TemplateResult } from "lit-html";
46
+ import type { AnyCommand, CommandRegistry } from "../commands/registry";
47
+ import type { CommandArgValues } from "../commands/command-args";
48
+ import type { Tab } from "../tabs/tab";
20
49
 
21
- let _ctx: {
22
- getCanvasMode: () => string;
23
- getZoom: () => number;
24
- setZoomDirect: (zoom: number) => void;
25
- };
50
+ /*
51
+ * There is no `initCanvasUtils`, and no `_ctx`.
52
+ *
53
+ * It injected three functions — `getCanvasMode`, `getZoom`, `setZoomDirect` — and all three were
54
+ * spelled `activeTab.value`, which is the FOCUSED pane's tab. Every geometry function here already
55
+ * takes a `CanvasSurface`, so the pan/zoom offsets and the pan-zoom wrap moved onto it while the
56
+ * SCALE stayed behind the injection, and the two disagreed in four visible ways:
57
+ *
58
+ * 1 · the unfocused pane drew at the focused tab's scale — `scale(1)` for a document at 2×;
59
+ * 2 · the side pane's `+` zoomed the PRIMARY's document, by a factor computed from the side's
60
+ * own zoom (`b.zoom * 1.2` written to `a`);
61
+ * 3 · `getFit()` keyed on `activeTab`, so the unfocused pod reported the focused pane's fit;
62
+ * 4 · the side pane entering Design ran `fitOnCanvasEntry` → `setZoomDirect(0.163)` and the
63
+ * PRIMARY snapped to 16%, because the other pane had fitted itself.
64
+ *
65
+ * `scripts/check-pane-singletons.ts` could not see any of it: it bans `view.<field>` by name and
66
+ * the zoom was never on `view`. The rule it enforces is now the general one — per-stage state is
67
+ * reached through a surface — and this module reads `activeTab` in exactly one place, the command
68
+ * verbs, where "the active pane" is the whole meaning of the request.
69
+ */
70
+
71
+ /** The tab a stage is showing. The one route from a surface to the state its geometry needs. */
72
+ function tabOfSurface(surface: CanvasSurface): Tab | null {
73
+ return tabOfPane(surface.paneId);
74
+ }
26
75
 
27
76
  /**
28
- * Initialize the canvas utils module.
77
+ * A stage's own pan-zoom scale. 1 when its pane shows nothing.
29
78
  *
30
- * @param {{
31
- * getCanvasMode: () => string;
32
- * getZoom: () => number;
33
- * setZoomDirect: (zoom: number) => void;
34
- * }} ctx
79
+ * **`session.ui.zoom` is per-TAB, and a LENS shares its tab with the pane beside it** — so under a
80
+ * lens the two stages would zoom together, which is exactly what "the same page at another
81
+ * breakpoint" must not do. The derivation carries the lens's own scale.
35
82
  */
36
- export function initCanvasUtils(ctx: {
37
- getCanvasMode: () => string;
38
- getZoom: () => number;
39
- setZoomDirect: (zoom: number) => void;
40
- }) {
41
- _ctx = ctx;
83
+ function zoomOf(surface: CanvasSurface): number {
84
+ const derived = derivationOfPane(surface.paneId);
85
+ if (derived?.kind === "lens") {
86
+ return derived.zoom;
87
+ }
88
+ return tabOfSurface(surface)?.session.ui.zoom ?? 1;
89
+ }
90
+
91
+ /**
92
+ * A stage's own pan-zoom scale, for the CONTROL that reports it.
93
+ *
94
+ * The zoom pod printed `tab.session.ui.zoom`, which is the source pane's number under a lens — so a
95
+ * breakpoint lens zoomed to 40% reported whatever the desktop pane was at, and its `-` divided the
96
+ * wrong figure. Every zoom verb already takes a surface; the readout has to as well.
97
+ */
98
+ export function stageZoom(surface: CanvasSurface = activeCanvasSurface()): number {
99
+ return zoomOf(surface);
100
+ }
101
+
102
+ /** Write a stage's own pan-zoom scale. A no-op when its pane shows nothing. See {@link zoomOf}. */
103
+ function setZoomOf(surface: CanvasSurface, zoom: number): void {
104
+ const derived = derivationOfPane(surface.paneId);
105
+ if (derived?.kind === "lens") {
106
+ derived.zoom = zoom;
107
+ return;
108
+ }
109
+ const tab = tabOfSurface(surface);
110
+ if (tab) {
111
+ tab.session.ui.zoom = zoom;
112
+ }
42
113
  }
43
114
 
44
115
  /**
@@ -83,7 +154,15 @@ export function canvasPanelTemplate(
83
154
  <div
84
155
  class="canvas-panel-header"
85
156
  @click=${() => {
86
- updateUi("activeMedia", panelMediaToActiveMedia(mediaName));
157
+ // The breakpoint belongs to the tab of the pane that MOUNTED this artboard — the
158
+ // Parent-side twin of the iframe's `hit` message, resolved the same way.
159
+ // `updateUi` writes to `activeTab`, which is the FOCUSED pane's tab, so clicking
160
+ // A header in an unfocused pane set another document's breakpoint and the Style
161
+ // Panel then edited a compound block the person never opened.
162
+ const tab = tabOfMountedPanel(panel);
163
+ if (tab) {
164
+ tab.session.ui.activeMedia = panelMediaToActiveMedia(mediaName);
165
+ }
87
166
  }}
88
167
  >
89
168
  ${label}
@@ -115,54 +194,64 @@ export function canvasPanelTemplate(
115
194
  return { panel, tpl };
116
195
  }
117
196
 
118
- /** Center canvas horizontally in viewport, top-aligned vertically. */
119
- export function centerCanvas() {
120
- if (!view.panzoomWrap) {
197
+ /*
198
+ * Every geometry function below takes its surface EXPLICITLY, and none of them defaults to
199
+ * `activeCanvasSurface()`.
200
+ *
201
+ * A default is how one pane's fit lands on the other's transform: the fit that runs on a mode
202
+ * transition, the observer that re-centres until content settles and the wheel that pans are all
203
+ * scheduled from a render, and a render belongs to the pane it was scheduled for — which is the
204
+ * focused pane only by coincidence. Every call site already holds a surface or a tab.
205
+ */
206
+
207
+ /** Center a stage's content horizontally in its viewport, top-aligned vertically. */
208
+ export function centerCanvas(surface: CanvasSurface) {
209
+ if (!surface.panzoomWrap) {
121
210
  return;
122
211
  }
123
- const wrapWidth = canvasWrap.clientWidth;
124
- const contentWidth = view.panzoomWrap.scrollWidth;
125
- const zoom = _ctx.getZoom();
212
+ const wrapWidth = surface.wrap.clientWidth;
213
+ const contentWidth = surface.panzoomWrap.scrollWidth;
214
+ const zoom = zoomOf(surface);
126
215
  const scaledWidth = contentWidth * zoom;
127
- view.panX = Math.max(16, (wrapWidth - scaledWidth) / 2);
128
- view.panY = 0;
216
+ surface.panX = Math.max(16, (wrapWidth - scaledWidth) / 2);
217
+ surface.panY = 0;
129
218
  }
130
219
 
131
220
  /**
132
221
  * Attach a ResizeObserver to view.panzoomWrap that re-centers until the user pans. Handles async
133
222
  * content (runtime rendering, data fetching) that changes layout after initial paint.
134
223
  */
135
- export function observeCenterUntilStable() {
136
- if (view.centerObserver) {
137
- view.centerObserver.disconnect();
138
- view.centerObserver = null;
224
+ export function observeCenterUntilStable(surface: CanvasSurface) {
225
+ if (surface.centerObserver) {
226
+ surface.centerObserver.disconnect();
227
+ surface.centerObserver = null;
139
228
  }
140
- if (!view.panzoomWrap) {
229
+ if (!surface.panzoomWrap) {
141
230
  return;
142
231
  }
143
- view.needsCenter = true;
144
- view.centerObserver = new ResizeObserver(() => {
145
- if (!view.needsCenter) {
146
- view.centerObserver?.disconnect();
147
- view.centerObserver = null;
232
+ surface.needsCenter = true;
233
+ surface.centerObserver = new ResizeObserver(() => {
234
+ if (!surface.needsCenter) {
235
+ surface.centerObserver?.disconnect();
236
+ surface.centerObserver = null;
148
237
  return;
149
238
  }
150
- centerCanvas();
151
- applyTransform();
239
+ centerCanvas(surface);
240
+ applyTransform(surface);
152
241
  });
153
- view.centerObserver.observe(view.panzoomWrap);
154
- centerCanvas();
242
+ surface.centerObserver.observe(surface.panzoomWrap);
243
+ centerCanvas(surface);
155
244
  }
156
245
 
157
246
  /** Apply the current zoom + pan transform to the panzoom wrapper. */
158
- export function applyTransform() {
159
- if (!view.panzoomWrap) {
247
+ export function applyTransform(surface: CanvasSurface) {
248
+ if (!surface.panzoomWrap) {
160
249
  return;
161
250
  }
162
- const zoom = _ctx.getZoom();
163
- view.panzoomWrap.style.transform = `translate(${view.panX}px, ${view.panY}px) scale(${zoom})`;
251
+ const zoom = zoomOf(surface);
252
+ surface.panzoomWrap.style.transform = `translate(${surface.panX}px, ${surface.panY}px) scale(${zoom})`;
164
253
  // Overlays live INSIDE the scaled panzoom-wrap (iframe hosts draw there), so no per-mode redraw
165
- // Is needed here — the flush only re-anchors the fixed block-action-bar. The tab-bar zoom widget
254
+ // Is needed here — the flush only re-anchors the fixed block-action-bar. The floating zoom pod
166
255
  // Tracks `ui.zoom` reactively, so no explicit indicator refresh is needed either.
167
256
  renderOnly("overlays");
168
257
  }
@@ -200,17 +289,19 @@ export function clampEditZoom(zoom: number): number {
200
289
  * bare style writes only; the iframe's own ResizeObserver re-posts `contentHeight` after the
201
290
  * reflow, which also finalizes the viewport height.
202
291
  */
203
- export function applyEditZoom() {
204
- if (_ctx.getCanvasMode() !== "edit") {
292
+ export function applyEditZoom(surface: CanvasSurface = activeCanvasSurface()) {
293
+ // THIS pane's effective mode. It was the focused pane's, so a side pane in Edit was refused its
294
+ // Own content zoom whenever the primary happened to be in Design.
295
+ if (canvasModeOfPane(surface.paneId) !== "edit") {
205
296
  return;
206
297
  }
207
- const [panel] = canvasPanels;
298
+ const [panel] = surface.panels;
208
299
  if (!panel?.canvas || !panel.viewport) {
209
300
  return;
210
301
  }
211
302
  const canvasEl = panel.canvas;
212
303
  const iframe = canvasEl.querySelector("iframe");
213
- const editZoom = activeTab.value?.session.ui.editZoom ?? 1;
304
+ const editZoom = tabOfSurface(surface)?.session.ui.editZoom ?? 1;
214
305
  if (editZoom === 1) {
215
306
  // Exactly today's fluid behavior — no inline width, no transform, auto viewport height.
216
307
  panel._width = null;
@@ -222,7 +313,7 @@ export function applyEditZoom() {
222
313
  }
223
314
  panel.viewport.style.height = "";
224
315
  } else {
225
- const column = canvasWrap.querySelector<HTMLElement>(".content-edit-column");
316
+ const column = surface.wrap.querySelector<HTMLElement>(".content-edit-column");
226
317
  if (!column) {
227
318
  return;
228
319
  }
@@ -250,126 +341,404 @@ export function applyEditZoom() {
250
341
  renderOnly("overlays");
251
342
  }
252
343
 
253
- /** Set the active tab's edit zoom (clamped) and apply it synchronously. */
254
- export function setEditZoom(zoom: number) {
255
- const tab = activeTab.value;
344
+ /**
345
+ * Set a stage's edit zoom (clamped) and apply it synchronously.
346
+ *
347
+ * The SURFACE decides whose zoom this is, not the focus. Both this and {@link requestEditZoom}
348
+ * opened `activeTab.value`, so the side pane's `+` and the wheel over the side pane's stage — which
349
+ * had already resolved the right surface and the right tab — wrote the primary's `editZoom`.
350
+ */
351
+ export function setEditZoom(zoom: number, surface: CanvasSurface = activeCanvasSurface()) {
352
+ const tab = tabOfSurface(surface);
256
353
  if (!tab) {
257
354
  return;
258
355
  }
259
356
  tab.session.ui.editZoom = clampEditZoom(zoom);
260
- applyEditZoom();
357
+ applyEditZoom(surface);
261
358
  }
262
359
 
263
- let _editZoomRaf = 0;
360
+ /**
361
+ * One pending edit-zoom frame PER STAGE.
362
+ *
363
+ * A single `let _editZoomRaf` was a shared slot: a wheel burst over one pane suppressed the other
364
+ * pane's coalesced reflow entirely, and the frame that did run applied whichever surface won the
365
+ * race. Keyed by pane id, for the reason `canvas-render.ts` keys its own render frames that way.
366
+ */
367
+ const _editZoomRafs = new Map<string, number>();
264
368
 
265
369
  /**
266
- * Wheel-rate edit-zoom setter: the reactive `editZoom` write lands immediately (the tab-bar label
267
- * tracks it), but the DOM work — an iframe width resize, i.e. a real reflow — is coalesced to one
268
- * `applyEditZoom()` per animation frame so a fast ctrl+scroll burst doesn't thrash layout.
370
+ * Wheel-rate edit-zoom setter: the reactive `editZoom` write lands immediately (the zoom pod's
371
+ * label tracks it), but the DOM work — an iframe width resize, i.e. a real reflow — is coalesced to
372
+ * one `applyEditZoom()` per animation frame so a fast ctrl+scroll burst doesn't thrash layout.
269
373
  */
270
- export function requestEditZoom(zoom: number) {
271
- const tab = activeTab.value;
374
+ export function requestEditZoom(zoom: number, surface: CanvasSurface = activeCanvasSurface()) {
375
+ const tab = tabOfSurface(surface);
272
376
  if (!tab) {
273
377
  return;
274
378
  }
275
379
  tab.session.ui.editZoom = clampEditZoom(zoom);
276
- if (_editZoomRaf) {
380
+ if (_editZoomRafs.has(surface.paneId)) {
277
381
  return;
278
382
  }
279
- _editZoomRaf = requestAnimationFrame(() => {
280
- _editZoomRaf = 0;
281
- applyEditZoom();
282
- });
383
+ _editZoomRafs.set(
384
+ surface.paneId,
385
+ requestAnimationFrame(() => {
386
+ _editZoomRafs.delete(surface.paneId);
387
+ applyEditZoom(surface);
388
+ }),
389
+ );
283
390
  }
284
391
 
285
- /** Calculate zoom + pan to fit all panels within the viewport. */
286
- export function fitToScreen() {
287
- if (!view.panzoomWrap) {
392
+ /**
393
+ * Compute and apply a geometric fit. Pure arithmetic — it declares nothing.
394
+ *
395
+ * `axis` is the fit's own meaning: `"page"` fits both axes (the whole artboard, in view), `"width"`
396
+ * fits the horizontal axis and lets the page run off the bottom, which is what you want on a long
397
+ * document you are about to scroll. `maxZoom` caps the result.
398
+ */
399
+ function applyGeometricFit(surface: CanvasSurface, axis: "width" | "page", maxZoom: number): void {
400
+ if (!surface.panzoomWrap) {
288
401
  return;
289
402
  }
290
- const wrapWidth = canvasWrap.clientWidth;
291
- const wrapHeight = canvasWrap.clientHeight;
403
+ const wrapWidth = surface.wrap.clientWidth;
404
+ const wrapHeight = surface.wrap.clientHeight;
292
405
  const gap = 24;
293
406
  const padding = 32;
294
407
  let totalPanelWidth = 0;
295
- for (const p of canvasPanels) {
408
+ const { panels } = surface;
409
+ for (const p of panels) {
296
410
  totalPanelWidth += p._width || 800;
297
411
  }
298
- totalPanelWidth += gap * Math.max(0, canvasPanels.length - 1) + padding;
412
+ totalPanelWidth += gap * Math.max(0, panels.length - 1) + padding;
299
413
 
300
- const zoom = _ctx.getZoom();
301
- const wrapRect = rectOf(view.panzoomWrap);
414
+ const zoom = zoomOf(surface);
415
+ const wrapRect = rectOf(surface.panzoomWrap);
302
416
  const unscaledHeight = wrapRect.height / zoom;
303
417
  const maxPanelHeight = unscaledHeight + padding;
304
418
 
305
419
  const fitZoomW = wrapWidth / totalPanelWidth;
306
420
  const fitZoomH = wrapHeight / maxPanelHeight;
307
- const fitZoom = Math.min(5, Math.max(0.05, Math.min(fitZoomW, fitZoomH)));
421
+ const wanted = axis === "width" ? fitZoomW : Math.min(fitZoomW, fitZoomH);
422
+ const fitZoom = Math.min(maxZoom, Math.max(PAN_ZOOM_MIN, wanted));
308
423
 
309
- _ctx.setZoomDirect(fitZoom);
424
+ setZoomOf(surface, fitZoom);
310
425
 
311
426
  const scaledWidth = totalPanelWidth * fitZoom;
312
427
  const scaledHeight = maxPanelHeight * fitZoom;
313
- view.panX = Math.max(0, (wrapWidth - scaledWidth) / 2);
314
- view.panY = Math.max(0, (wrapHeight - scaledHeight) / 2);
315
- applyTransform();
428
+ surface.panX = Math.max(0, (wrapWidth - scaledWidth) / 2);
429
+ surface.panY = Math.max(0, (wrapHeight - scaledHeight) / 2);
430
+ applyTransform(surface);
431
+ }
432
+
433
+ /**
434
+ * Fit all panels within the viewport, and declare that as the document's fit.
435
+ *
436
+ * `maxZoom` caps the result. The Fit control passes the full {@link PAN_ZOOM_MAX} (asking to fit is
437
+ * asking to magnify a small artboard too); the automatic fit on entering a panzoom mode passes 1,
438
+ * because arriving at a document should never blow it up past life size.
439
+ */
440
+ export function fitToScreen({
441
+ maxZoom = PAN_ZOOM_MAX,
442
+ surface = activeCanvasSurface(),
443
+ }: { maxZoom?: number; surface?: CanvasSurface } = {}) {
444
+ // Declared before the guard: "frame the whole page" is a preference the author expressed, and it
445
+ // Stays true whether or not there is a laid-out artboard to apply it to this instant.
446
+ declareFit("page", surface);
447
+ applyGeometricFit(surface, "page", maxZoom);
448
+ }
449
+
450
+ // ─── Pan-zoom (design / stylebook / git-diff artboards) ──────────────────────
451
+
452
+ export const PAN_ZOOM_MIN = 0.05;
453
+
454
+ export const PAN_ZOOM_MAX = 5;
455
+
456
+ /** Clamp a pan-zoom value to the supported range. */
457
+ export function clampPanZoom(zoom: number): number {
458
+ return Math.min(PAN_ZOOM_MAX, Math.max(PAN_ZOOM_MIN, zoom));
316
459
  }
317
460
 
318
- /** Reset zoom to 100% and re-center horizontally. */
319
- export function resetZoom() {
320
- if (!view.panzoomWrap) {
461
+ /**
462
+ * How a document is framed when you arrive at it — **declared state**, per tab + document.
463
+ *
464
+ * - `"page"` — the whole artboard in view (the default: arriving at a document should show it).
465
+ * - `"width"` — fit the horizontal axis; the page runs off the bottom, which is what a long document
466
+ * you are about to scroll actually wants.
467
+ * - `"none"` — frame nothing; whatever the zoom is, is the zoom.
468
+ * - A number — that exact pan-zoom.
469
+ *
470
+ * This replaces an IMPLICIT fit-on-entry plus a `Set` of "documents the author has zoomed by hand",
471
+ * which had three problems. It could not be read, so the zoom control could not show it. It could
472
+ * not be written, so a preference users expect to persist silently reset. And because entering a
473
+ * mode applied a transform nobody had asked for, anything measuring the canvas from outside had to
474
+ * guess whether a fit had happened — which is precisely how a screen coordinate ends up wrong.
475
+ *
476
+ * "The author chose 84%" is not a special case here: it is the fit `0.84`.
477
+ */
478
+ export type FitMode = "width" | "page" | "none" | number;
479
+
480
+ /**
481
+ * The fit a document gets when it has not declared one.
482
+ *
483
+ * `"page"` because a 1280px artboard used to land clipped mid-word in a ~700px viewport, and
484
+ * arriving at a document should never require a zoom-out before you can read it.
485
+ */
486
+ export const DEFAULT_FIT: FitMode = "page";
487
+
488
+ /** The three named fits, as the `canvas.setFit` schema and its coercion both read them. */
489
+ export const FIT_WORDS: readonly string[] = ["width", "page", "none"];
490
+
491
+ /**
492
+ * Coerce a `canvas.setFit` argument, refusing anything that is neither a named fit nor a scale.
493
+ *
494
+ * Written here rather than in `commands/command-args.ts` because the value space is this module's:
495
+ * {@link FitMode} is declared three lines up, and a second copy of "which words are fits" in the
496
+ * shared helpers is exactly the drift the registry projection exists to prevent.
497
+ */
498
+ function fitArg(commandId: string, args: Record<string, unknown>, key: string): FitMode {
499
+ const value = args[key];
500
+ if (typeof value === "string" && FIT_WORDS.includes(value)) {
501
+ return value as FitMode;
502
+ }
503
+ if (typeof value === "number" && value >= PAN_ZOOM_MIN && value <= PAN_ZOOM_MAX) {
504
+ return value;
505
+ }
506
+ throw new RangeError(
507
+ `command "${commandId}" argument "${key}": expected ${FIT_WORDS.map((w) => `"${w}"`).join(" | ")} ` +
508
+ `or a number in [${PAN_ZOOM_MIN}, ${PAN_ZOOM_MAX}], got ${JSON.stringify(value)}`,
509
+ );
510
+ }
511
+
512
+ /** Declared fits, keyed per tab + document. */
513
+ const _fits = new Map<string, FitMode>();
514
+
515
+ /**
516
+ * The registry key for the document a STAGE is showing (null when its pane shows none).
517
+ *
518
+ * It read `activeTab`, which is why the unfocused pane's zoom pod reported the focused pane's fit —
519
+ * and why "Fit page" chosen in one pane changed the label in the other.
520
+ */
521
+ function fitKey(surface: CanvasSurface): string | null {
522
+ const tab = tabOfSurface(surface);
523
+ if (!tab) {
524
+ return null;
525
+ }
526
+ /* A LENS shares the tab with the pane it derives from, so the tab alone stops being a key: the
527
+ mobile lens and the desktop pane would declare ONE fit between them and re-frame each other on
528
+ every mode transition. Same reason `zoomOf` reads the derivation. */
529
+ const lens = derivationOfPane(surface.paneId)?.kind === "lens" ? `::${surface.paneId}` : "";
530
+ return `${tab.id}::${tab.documentPath ?? ""}${lens}`;
531
+ }
532
+
533
+ /** Write a stage's document's fit without applying it — the internal half of {@link setFit}. */
534
+ function declareFit(fit: FitMode, surface: CanvasSurface): void {
535
+ const key = fitKey(surface);
536
+ if (key) {
537
+ _fits.set(key, fit);
538
+ }
539
+ }
540
+
541
+ /** A stage's declared fit, or {@link DEFAULT_FIT}. Readable by that pane's zoom control. */
542
+ export function getFit(surface: CanvasSurface = activeCanvasSurface()): FitMode {
543
+ const key = fitKey(surface);
544
+ return (key === null ? undefined : _fits.get(key)) ?? DEFAULT_FIT;
545
+ }
546
+
547
+ /** Whether a stage's document has declared a fit of its own. */
548
+ export function hasDeclaredFit(surface: CanvasSurface = activeCanvasSurface()): boolean {
549
+ const key = fitKey(surface);
550
+ return key !== null && _fits.has(key);
551
+ }
552
+
553
+ /** Drop every declared fit — a fresh session, and the tests. */
554
+ export function resetFits(): void {
555
+ _fits.clear();
556
+ }
557
+
558
+ /** Honour a fit right now. Defaults to the STAGE's own declared one. */
559
+ export function applyFit(fit?: FitMode, surface: CanvasSurface = activeCanvasSurface()): void {
560
+ /* Resolved in the body rather than as a parameter default: a default may only read parameters
561
+ declared BEFORE it, and the fit belongs to the surface that comes after. Reading `getFit()`
562
+ with no surface is exactly what made the default answer for the FOCUSED pane. */
563
+ const chosen = fit ?? getFit(surface);
564
+ if (chosen === "none") {
565
+ applyTransform(surface);
566
+ return;
567
+ }
568
+ if (typeof chosen === "number") {
569
+ setZoomOf(surface, clampPanZoom(chosen));
570
+ applyTransform(surface);
321
571
  return;
322
572
  }
323
- _ctx.setZoomDirect(1);
324
- centerCanvas();
325
- applyTransform();
573
+ // Capped at life size: arriving at a document should never blow it up past 100%.
574
+ applyGeometricFit(surface, chosen, 1);
575
+ }
576
+
577
+ /** Declare the active document's fit AND honour it. The one writer every control routes through. */
578
+ export function setFit(fit: FitMode, surface: CanvasSurface = activeCanvasSurface()): void {
579
+ declareFit(fit, surface);
580
+ applyFit(fit, surface);
326
581
  }
327
582
 
328
583
  /**
329
- * Smoothly pan/scroll the canvas vertically to center the given DOM element.
584
+ * Record the active document's CURRENT pan-zoom as its declared fit.
330
585
  *
331
- * @param {HTMLElement} el
332
- * @param {{ scrollContainer?: HTMLElement | null }} [panel]
333
- */
334
- function _panToEl(el: HTMLElement, panel?: { scrollContainer?: HTMLElement | null }) {
335
- const wrapRect = rectOf(canvasWrap);
336
- const elRect = rectOf(el);
337
- const elCenterY = elRect.top + elRect.height / 2 - wrapRect.top;
338
- const vpCenterY = wrapRect.height / 2;
339
- const offsetY = vpCenterY - elCenterY;
340
-
341
- if (panel?.scrollContainer) {
342
- panel.scrollContainer.scrollTo({
343
- behavior: "smooth",
344
- top: panel.scrollContainer.scrollTop - offsetY,
345
- });
346
- } else {
347
- animatePanBy(offsetY);
586
+ * For the gesture paths that move the zoom themselves and then have to say so — ctrl+scroll writes
587
+ * `ui.zoom` directly, pinch will too. An author-chosen zoom is a numeric fit, so re-entering the
588
+ * mode restores it instead of re-framing over the top of it.
589
+ */
590
+ export function markExplicitZoom(surface: CanvasSurface = activeCanvasSurface()): void {
591
+ declareFit(clampPanZoom(zoomOf(surface)), surface);
592
+ }
593
+
594
+ /**
595
+ * Set the active tab's pan-zoom on the author's behalf: clamped, declared as this document's fit,
596
+ * and applied. Every author-facing pan-zoom control routes through here.
597
+ */
598
+ export function setUserZoom(zoom: number, surface: CanvasSurface = activeCanvasSurface()): void {
599
+ const tab = tabOfSurface(surface);
600
+ if (!tab) {
601
+ return;
348
602
  }
603
+ // The fit is declared BEFORE the reactive write, not after: `ui.zoom` is tracked, effects run
604
+ // Synchronously on assignment, and the zoom pod reads `getFit()` while it renders — so declaring
605
+ // Second means the control repaints one interaction behind the state it is reporting.
606
+ const next = clampPanZoom(zoom);
607
+ declareFit(next, surface);
608
+ setZoomOf(surface, next);
609
+ applyTransform(surface);
349
610
  }
350
611
 
351
612
  /**
352
- * Pan the panzoom canvas vertically so a PARENT-VIEWPORT rect is centered — for callers whose
353
- * target lives inside an iframe (no parent DOM element to measure; the host converts the measured
354
- * iframe rect and passes it here).
613
+ * Honour the declared fit when a pane enters a panzoom mode (Design, Stylebook).
614
+ *
615
+ * Called on the mode transition only. At that point the panels exist with their declared widths but
616
+ * the iframes have not painted, which is why the geometry is driven by the panel widths rather than
617
+ * by anything measured from content.
355
618
  */
356
- export function panToParentRect(rect: { top: number; height: number }) {
357
- const wrapRect = rectOf(canvasWrap);
619
+ export function fitOnCanvasEntry(surface: CanvasSurface): void {
620
+ /* An unmeasurable viewport would fit to the 5% floor, which is worse than the clipping this
621
+ replaces. Leave the zoom alone.
622
+ This guard is load-bearing rather than defensive now: a zoomed pane grid hides the other cell
623
+ with `display:none`, so its stage really does measure zero — and unzooming re-fits for free
624
+ through {@link observeCenterUntilStable}'s observer, which fires on the restored layout. */
625
+ if (surface.wrap.clientWidth <= 0) {
626
+ applyTransform(surface);
627
+ return;
628
+ }
629
+ applyFit(getFit(surface), surface);
630
+ }
631
+
632
+ /** Reset zoom to 100% and re-center horizontally, declaring 100% as this document's fit. */
633
+ export function resetZoom(surface: CanvasSurface = activeCanvasSurface()) {
634
+ declareFit(1, surface);
635
+ if (!surface.panzoomWrap) {
636
+ return;
637
+ }
638
+ setZoomOf(surface, 1);
639
+ centerCanvas(surface);
640
+ applyTransform(surface);
641
+ }
642
+
643
+ /**
644
+ * How far, in parent-viewport px, the pane must move down-screen to centre `rect`.
645
+ *
646
+ * Both inputs are `getBoundingClientRect` space, which is also the space the canvas host answers in
647
+ * (`iframe-host.ts`'s `pointForRect` composes the frame offset and the empirical scale before it
648
+ * hands a rect back), so one arithmetic serves a rect measured from a parent DOM element and a rect
649
+ * measured inside a canvas iframe alike.
650
+ */
651
+ function centeringOffset(surface: CanvasSurface, rect: { top: number; height: number }): number {
652
+ const wrapRect = rectOf(surface.wrap);
358
653
  const elCenterY = rect.top + rect.height / 2 - wrapRect.top;
359
- animatePanBy(wrapRect.height / 2 - elCenterY);
654
+ return wrapRect.height / 2 - elCenterY;
655
+ }
656
+
657
+ /**
658
+ * The element a reveal actually MOVES, or `null` when the panzoom transform is the only mover.
659
+ *
660
+ * This is the whole Edit-vs-Design distinction, in one place. Design, Stylebook and git-diff render
661
+ * a `.panzoom-wrap` and are moved by writing `view.panY`; **Edit renders no panzoom wrap at all** —
662
+ * it is an ordinary scrolling column (`.content-edit-canvas`, the edit branch of `renderCanvas`),
663
+ * and the only thing that brings a node below the fold into view there is its scroll container.
664
+ *
665
+ * Reading it off the stage's own active panel is what keeps the two callers from disagreeing. It is
666
+ * the discriminator `_panToEl` has always used, and `renderCanvas` re-establishes it on every
667
+ * render: a surface's `panels` is emptied and its panzoom wrap nulled before any branch runs, so
668
+ * the panel that answers here is this mode's, never the last mode's.
669
+ *
670
+ * **It took its surface as a parameter only after the rule grew a second hop.** It was zero-arity
671
+ * and called `getActivePanel()`, which is the FOCUSED pane's — so `revealBy(surface, …)` was handed
672
+ * one stage and scrolled another's. Benign in the tree, because both callers happened to pass the
673
+ * focused surface, and invisible to the one-hop rule for exactly the wrong reason: a forwarder
674
+ * whose whole body is `getActivePanel()?.scrollContainer` launders a focus read behind a name.
675
+ *
676
+ * @param {CanvasSurface} surface
677
+ * @returns {HTMLElement | null}
678
+ */
679
+ export function revealScroller(surface: CanvasSurface): HTMLElement | null {
680
+ return panelOfSurface(surface)?.scrollContainer ?? null;
681
+ }
682
+
683
+ /**
684
+ * Move the pane by `offsetY`, on whichever surface this mode actually moves.
685
+ *
686
+ * `smooth` is the caller's intent, not a property of the surface: the Outline's jump-to-node is a
687
+ * gesture an author watches, so it eases. The panzoom branch always eases — its 250ms tween is the
688
+ * one {@link import("./iframe-host").revealCanvasPath} waits out frame by frame — but a scroll has
689
+ * no such settle protocol, so an un-eased caller gets a scroll that is finished on return.
690
+ */
691
+ function revealBy(surface: CanvasSurface, offsetY: number, smooth: boolean): void {
692
+ const scroller = revealScroller(surface);
693
+ if (!scroller) {
694
+ animatePanBy(surface, offsetY);
695
+ return;
696
+ }
697
+ const top = scroller.scrollTop - offsetY;
698
+ if (smooth) {
699
+ scroller.scrollTo({ behavior: "smooth", top });
700
+ return;
701
+ }
702
+ // No clamp: the browser clamps `scrollTop` into `[0, scrollHeight - clientHeight]` itself, and
703
+ // Clamping here would only make a unit test agree with arithmetic the DOM never performs.
704
+ scroller.scrollTop = top;
705
+ }
706
+
707
+ /**
708
+ * Smoothly pan/scroll the canvas vertically to center the given DOM element.
709
+ *
710
+ * @param {HTMLElement} el
711
+ */
712
+ function _panToEl(surface: CanvasSurface, el: HTMLElement) {
713
+ revealBy(surface, centeringOffset(surface, rectOf(el)), true);
714
+ }
715
+
716
+ /**
717
+ * Centre a PARENT-VIEWPORT rect in the pane — for callers whose target lives inside an iframe (no
718
+ * parent DOM element to measure; the host converts the measured iframe rect and passes it here).
719
+ *
720
+ * Was `animatePanBy` unconditionally, which made it a NO-OP on the Edit surface: `view.panY` only
721
+ * reaches the screen through a `.panzoom-wrap` transform, and Edit has none — so `revealCanvasPath`
722
+ * reported a node's unchanged off-screen point and the click that followed selected nothing.
723
+ */
724
+ export function panToParentRect(
725
+ rect: { top: number; height: number },
726
+ surface: CanvasSurface = activeCanvasSurface(),
727
+ ) {
728
+ revealBy(surface, centeringOffset(surface, rect), false);
360
729
  }
361
730
 
362
- /** Animate `view.panY` by `offsetY` with the shared 250ms ease-out. */
363
- function animatePanBy(offsetY: number) {
364
- const startY = view.panY;
731
+ /** Animate a stage's `panY` by `offsetY` with the shared 250ms ease-out. */
732
+ function animatePanBy(surface: CanvasSurface, offsetY: number) {
733
+ const startY = surface.panY;
365
734
  const targetY = startY + offsetY;
366
735
  const start = performance.now();
367
736
  const duration = 250;
368
737
  const step = (now: number) => {
369
738
  const t = Math.min((now - start) / duration, 1);
370
739
  const ease = t * (2 - t);
371
- view.panY = startY + (targetY - startY) * ease;
372
- applyTransform();
740
+ surface.panY = startY + (targetY - startY) * ease;
741
+ applyTransform(surface);
373
742
  if (t < 1) {
374
743
  requestAnimationFrame(step);
375
744
  }
@@ -383,6 +752,7 @@ function animatePanBy(offsetY: number) {
383
752
  * @param {(string | number)[]} path
384
753
  */
385
754
  export function panToElement(path: (string | number)[]) {
755
+ const surface = activeCanvasSurface();
386
756
  const panel = getActivePanel();
387
757
  if (!panel?.canvas) {
388
758
  return;
@@ -391,13 +761,685 @@ export function panToElement(path: (string | number)[]) {
391
761
  if (!el) {
392
762
  return;
393
763
  }
394
- _panToEl(el, panel);
764
+ _panToEl(surface, el);
395
765
  }
396
766
 
397
- /** Toggle "active" class on canvas panel headers based on activeMedia. */
398
- export function updateActivePanelHeaders() {
399
- const activeMedia = activeTab.value?.session.ui.activeMedia ?? null;
400
- for (const p of canvasPanels) {
767
+ // ─── Commands ─────────────────────────────────────────────────────────────────
768
+
769
+ /**
770
+ * Every canvas mode a pane can render, in the order the mode switcher lists them.
771
+ *
772
+ * `"preview"` is in the set even though it is not a base mode: `getCanvasMode()` composes the
773
+ * per-tab `ui.preview` flag with an `edit`/`design` base and presents "preview" to every downstream
774
+ * gate, and `tab.capabilities.modes` lists it exactly the same way. A caller naming a mode is
775
+ * naming the mode it will observe, so the enum is the observable set, not the storage set.
776
+ */
777
+ export const CANVAS_MODES = [
778
+ "edit",
779
+ "design",
780
+ "preview",
781
+ "source",
782
+ "stylebook",
783
+ "grid",
784
+ "git-diff",
785
+ ] as const;
786
+
787
+ export type CanvasMode = (typeof CANVAS_MODES)[number];
788
+
789
+ /** The base modes the preview flag composes with — the two the Canvas view axis shows it beside. */
790
+ const PREVIEWABLE_BASE_MODES = new Set(["edit", "design"]);
791
+
792
+ // ─── §4.2 axis 2 — the Canvas view ───────────────────────────────────────────
793
+ // `Edit │ Design` is a radio and `Preview` is a TOGGLE beside it, because that is what the storage
794
+ // Has always been: `preview` is not a base mode, it is a flag that composes with one.
795
+ //
796
+ // This was argued the other way, and the argument is worth keeping because it is half right. One
797
+ // Three-value control, it said, because a switcher showing "Design" selected while the pane was
798
+ // Previewing reports a state the app is not in. But the app IS in Design — with preview composed
799
+ // Over it — and a three-value radio could only ever show one of the two facts. It picked the
800
+ // Composed one and hid the base, so while previewing nothing said what you were previewing or what
801
+ // Leaving would return you to. A pressed toggle beside a marked radio shows both, which is the
802
+ // State, and neither half is silent.
803
+
804
+ /**
805
+ * Every value of the Canvas view axis. `preview` composes with the other two rather than replacing
806
+ * them.
807
+ */
808
+ export const CANVAS_VIEWS = ["edit", "design", "preview"] as const;
809
+
810
+ export type CanvasView = (typeof CANVAS_VIEWS)[number];
811
+
812
+ /** A tab's view state, as the axis reads it — its base mode and whether preview composes over it. */
813
+ interface ViewableTab {
814
+ capabilities: { modes: string[] };
815
+ session: { ui: { canvasMode: string; preview?: boolean } };
816
+ }
817
+
818
+ /*
819
+ * `canvasViewOf` lived here: it collapsed the base and the preview flag into ONE effective view, so
820
+ * a three-value radio could mark exactly one of Edit/Design/Preview. Splitting that radio into a
821
+ * two-value base plus a preview toggle took its last caller — the two halves are read separately
822
+ * now, by `canvasBaseViewOf` and `previewStateOf` below. `canvasModeOfPane` still composes the two
823
+ * for the RENDERER, which does want one answer.
824
+ */
825
+
826
+ /**
827
+ * The views this document supports, in axis order — the control's entries.
828
+ *
829
+ * Filtered by `capabilities.modes` for the same reason the editor-kind dropdown is (§4.2): a
830
+ * permanently dead segment is a question the app refuses to answer.
831
+ */
832
+ export function canvasViewsFor(tab: ViewableTab): CanvasView[] {
833
+ return CANVAS_VIEWS.filter((value) => tab.capabilities.modes.includes(value));
834
+ }
835
+
836
+ /**
837
+ * The BASE views a document supports — the radio's entries. Preview is not among them.
838
+ *
839
+ * Preview was the third value of a three-way radio, which made it read as a mode you leave Design
840
+ * to enter: while previewing, the control could not tell you which mode you would come back to. It
841
+ * has always been stored as a per-tab flag over an edit/design base, so the radio was describing
842
+ * the storage inaccurately. Split so the two axes are drawn as what they are.
843
+ */
844
+ export function canvasBaseViewsFor(tab: ViewableTab): CanvasView[] {
845
+ return canvasViewsFor(tab).filter((value) => value !== "preview");
846
+ }
847
+
848
+ /** The base view the tab returns to — what the radio marks while preview is on. */
849
+ export function canvasBaseViewOf(tab: ViewableTab): CanvasView | null {
850
+ const base = tab.session.ui.canvasMode;
851
+ return PREVIEWABLE_BASE_MODES.has(base) ? (base as CanvasView) : null;
852
+ }
853
+
854
+ /**
855
+ * Whether this document can be previewed at all, and whether it is being.
856
+ *
857
+ * `available` is a fact about the DOCUMENT (its declared modes) and about the base it is currently
858
+ * in — `source` composes with no preview, so the toggle is not offered there.
859
+ */
860
+ export function previewStateOf(tab: ViewableTab): { available: boolean; on: boolean } {
861
+ return {
862
+ available:
863
+ tab.capabilities.modes.includes("preview") &&
864
+ PREVIEWABLE_BASE_MODES.has(tab.session.ui.canvasMode),
865
+ on: tab.session.ui.preview === true,
866
+ };
867
+ }
868
+
869
+ /**
870
+ * Move a pane onto one of the three views. The one writer the segmented control routes through.
871
+ *
872
+ * Idempotent in both directions, which is what `canvas.togglePreview` could not be: arriving at
873
+ * Edit or Design clears the preview flag, so "Design" means Design from any starting state.
874
+ *
875
+ * @param tab The pane's tab.
876
+ * @param value The view to land on.
877
+ * @param setCanvasMode Writes the BASE mode of the tab it is GIVEN — `studio.ts`'s own, injected
878
+ * because this module does not own the canvas render loop. It took only a mode, which is how the
879
+ * side pane's `Edit │ Design │ Preview` control cleared its own tab's preview flag and then moved
880
+ * the FOCUSED pane's tab to `design`.
881
+ */
882
+ export function setCanvasView<T extends ViewableTab>(
883
+ tab: T,
884
+ value: CanvasView,
885
+ setCanvasMode: (tab: T, mode: string) => void,
886
+ ): void {
887
+ if (value === "preview") {
888
+ if (PREVIEWABLE_BASE_MODES.has(tab.session.ui.canvasMode)) {
889
+ tab.session.ui.preview = true;
890
+ }
891
+ return;
892
+ }
893
+ tab.session.ui.preview = false;
894
+ if (tab.session.ui.canvasMode !== value) {
895
+ setCanvasMode(tab, value);
896
+ }
897
+ }
898
+
899
+ /** What the canvas view verbs need that this module does not own. */
900
+ export interface CanvasCommandDeps {
901
+ /** The effective mode, `ui.preview` already composed in — `studio.ts`'s `getCanvasMode`. */
902
+ getCanvasMode: () => string;
903
+ /** Write the BASE mode (`ui.canvasMode`) of the tab it is given — `studio.ts`'s `setCanvasMode`. */
904
+ setCanvasMode: (tab: Tab | null, mode: string) => void;
905
+ /** Open or close a pane's resolving-with popover — `pane-context.ts`'s `setResolvingOpen`. */
906
+ setResolvingOpen: (paneId: string, open: boolean) => void;
907
+ /**
908
+ * Repaint ONE pane's stage — `studio.ts`'s `renderCanvas`, taking the pane.
909
+ *
910
+ * Injected rather than imported: `canvas-render.ts` imports this module, so reaching back for
911
+ * `renderCanvas` would close a cycle. It takes the pane id because the rendering-context verbs
912
+ * do: `renderOnly("canvas")` resolves the FOCUSED pane, so the side bar's size switcher wrote the
913
+ * side pane's tab and repainted the primary, leaving the stage it changed showing the old width.
914
+ */
915
+ renderPane: (paneId: string) => void;
916
+ }
917
+
918
+ /** A document is open in a pane — every verb here writes that pane's own view state. */
919
+ /* The pan-zoom surface, as one set rather than three spellings of it.
920
+ `canvas.setZoom` and `canvas.setFit` used to gate on `document.open` alone while their peers
921
+ `canvas.zoomIn`/`zoomOut`/`zoomReset` gate on `document.open && editor.kind === "canvas"` — so
922
+ driving Studio with a Library pane focused, `setZoom` and `setFit` both SUCCEEDED and wrote a
923
+ zoom the surface they name was not showing, in the same state where the three verbs that mean
924
+ the same thing refused with "requires an open document". `pane-context.ts`'s zoom pod already
925
+ draws itself for exactly these three modes; this is that rule, written once. */
926
+ const PANZOOM_MODES = new Set(["design", "stylebook", "git-diff"]);
927
+
928
+ const documentOpen = (ctx: { document: { open: boolean } }) => ctx.document.open;
929
+
930
+ /** The three values `session.ui.previewColorScheme` may hold (spec §9.5). */
931
+ const COLOR_SCHEMES = ["auto", "light", "dark"] as const;
932
+
933
+ /**
934
+ * The canvas view verbs — `setMode`, `setZoom`, `setEditZoom`.
935
+ *
936
+ * **These are the setters that retire `canvas.togglePreview`** (plan §13.3 clause 3). A toggle
937
+ * cannot say which state it ends in, so six screenshots taken through it photographed whichever way
938
+ * the default happened to point that week; `canvas.setMode { mode: "preview" }` means the same
939
+ * thing twice in a row and from any starting state.
940
+ *
941
+ * Three refusals, each a wrong picture that used to be accepted silently:
942
+ *
943
+ * - A mode the open document cannot render (`tab.capabilities.modes`) — a `.csv` has no `design`.
944
+ * - `"preview"` over a base mode it does not compose with — `ui.preview` is ignored in `source`, so
945
+ * setting it there would report a state the app is not in.
946
+ * - A zoom outside the supported range, which the interactive controls clamp and a caller may not.
947
+ *
948
+ * @param {CanvasCommandDeps} deps
949
+ * @returns {AnyCommand[]}
950
+ */
951
+ export function canvasViewCommands(deps: CanvasCommandDeps): AnyCommand[] {
952
+ /** The open tab, or a refusal naming why there is nothing to act on. */
953
+ function requireTab(commandId: string) {
954
+ const tab = activeTab.value;
955
+ if (!tab) {
956
+ throw new RangeError(`command "${commandId}" needs an open document; no tab is active`);
957
+ }
958
+ return tab;
959
+ }
960
+
961
+ /**
962
+ * The tab a rendering-context verb addresses: the named pane's, or the focused one's.
963
+ *
964
+ * The bar is drawn once PER PANE (§18), and the side pane's controls have always written the side
965
+ * pane's tab — so a verb that could only reach the focused one would be a narrower capability
966
+ * than the control it replaced, which is the opposite of the point.
967
+ */
968
+ function contextTab(commandId: string, args: CommandArgValues) {
969
+ const { pane } = args as { pane?: unknown };
970
+ if (pane === undefined) {
971
+ return requireTab(commandId);
972
+ }
973
+ const paneId = stringArg(commandId, args, "pane");
974
+ const tab = tabOfPane(paneId);
975
+ if (!tab) {
976
+ throw new RangeError(
977
+ `command "${commandId}" argument "pane": "${paneId}" has no open document`,
978
+ );
979
+ }
980
+ return tab;
981
+ }
982
+
983
+ /** The optional pane selector every rendering-context verb accepts. */
984
+ const paneArg = {
985
+ pane: stringProperty("Which pane to render this way. Defaults to the focused one."),
986
+ };
987
+
988
+ /**
989
+ * Repaint the pane a rendering-context verb just wrote — not the focused one.
990
+ *
991
+ * `renderOnly("canvas")` calls `renderCanvas()` with no pane, which resolves the FOCUSED one.
992
+ * These verbs take a `pane` precisely because the side bar addresses the side pane, so the two
993
+ * resolutions disagree exactly when it matters.
994
+ */
995
+ function repaint(args: CommandArgValues): void {
996
+ const { pane } = args as { pane?: unknown };
997
+ deps.renderPane(typeof pane === "string" ? pane : workspace.activePaneId);
998
+ }
999
+
1000
+ return [
1001
+ {
1002
+ args: argsSchema({
1003
+ mode: enumProperty(CANVAS_MODES, "The canvas mode to switch the active pane to."),
1004
+ }),
1005
+ category: "View",
1006
+ id: "canvas.setMode",
1007
+ level: "document",
1008
+ menus: ["palette"],
1009
+ group: "3_canvas",
1010
+ requires: "an open document",
1011
+ when: documentOpen,
1012
+ aiTool: {
1013
+ description:
1014
+ "Switch the active pane's canvas to a mode: edit, design, preview, source, stylebook, " +
1015
+ "grid or git-diff. Fails when the open document does not support that mode.",
1016
+ name: "set_canvas_mode",
1017
+ },
1018
+ run: (_commandCtx, args) => {
1019
+ const mode = enumArg("canvas.setMode", args, "mode", CANVAS_MODES);
1020
+ const tab = requireTab("canvas.setMode");
1021
+ const { modes } = tab.capabilities;
1022
+ if (!modes.includes(mode)) {
1023
+ throw new RangeError(
1024
+ `command "canvas.setMode" argument "mode": "${mode}" is not a mode this document ` +
1025
+ `supports — it declares: ${modes.join(", ")}`,
1026
+ );
1027
+ }
1028
+ /* There is no second refusal here. The pane used to be asked too — the side pane hosted
1029
+ Code, Diff, Config, Entry, Grid and Library only, so a Canvas mode was refused by
1030
+ NAME with a sentence telling you to unsplit first. Both panes draw a live Canvas now
1031
+ (`panels/pane-grid.ts`), so the document's own declared modes are the whole answer. */
1032
+ if (mode === "preview") {
1033
+ const base = tab.session.ui.canvasMode;
1034
+ if (!PREVIEWABLE_BASE_MODES.has(base)) {
1035
+ throw new RangeError(
1036
+ `command "canvas.setMode" argument "mode": "preview" composes with the edit and ` +
1037
+ `design base modes; this pane is in "${base}"`,
1038
+ );
1039
+ }
1040
+ tab.session.ui.preview = true;
1041
+ return;
1042
+ }
1043
+ // Idempotent in both directions: leaving preview is part of arriving anywhere else, so a
1044
+ // Shot that says "design" gets design whether or not the previous step turned preview on.
1045
+ tab.session.ui.preview = false;
1046
+ // The tab `requireTab()` already resolved, not "whatever is focused when the write lands".
1047
+ deps.setCanvasMode(tab, mode);
1048
+ },
1049
+ title: "Set Canvas Mode",
1050
+ },
1051
+ {
1052
+ args: argsSchema({
1053
+ zoom: numberProperty("Pan-zoom scale, 1 = 100%.", {
1054
+ maximum: PAN_ZOOM_MAX,
1055
+ minimum: PAN_ZOOM_MIN,
1056
+ }),
1057
+ }),
1058
+ category: "View",
1059
+ id: "canvas.setZoom",
1060
+ level: "document",
1061
+ menus: ["palette"],
1062
+ group: "3_canvas",
1063
+ requires: "a document on the pan-zoom surface",
1064
+ when: documentOpen,
1065
+ enablement: () => PANZOOM_MODES.has(deps.getCanvasMode()),
1066
+ run: (_commandCtx, args) => {
1067
+ requireTab("canvas.setZoom");
1068
+ setUserZoom(boundedNumberArg("canvas.setZoom", args, "zoom", PAN_ZOOM_MIN, PAN_ZOOM_MAX));
1069
+ },
1070
+ title: "Set Zoom",
1071
+ },
1072
+ {
1073
+ args: argsSchema({
1074
+ fit: {
1075
+ description:
1076
+ 'How the document is framed: "width" | "page" | "none", or a numeric pan-zoom scale.',
1077
+ oneOf: [
1078
+ { enum: [...FIT_WORDS], type: "string" },
1079
+ { maximum: PAN_ZOOM_MAX, minimum: PAN_ZOOM_MIN, type: "number" },
1080
+ ],
1081
+ },
1082
+ }),
1083
+ category: "View",
1084
+ id: "canvas.setFit",
1085
+ level: "document",
1086
+ menus: ["palette"],
1087
+ group: "3_canvas",
1088
+ // The sentence has to match the gate, or the refusal teaches the wrong thing.
1089
+ requires: "a document on the pan-zoom surface",
1090
+ when: documentOpen,
1091
+ enablement: () => PANZOOM_MODES.has(deps.getCanvasMode()),
1092
+ run: (_commandCtx, args) => {
1093
+ requireTab("canvas.setFit");
1094
+ setFit(fitArg("canvas.setFit", args, "fit"));
1095
+ },
1096
+ title: "Set Fit",
1097
+ },
1098
+ {
1099
+ args: argsSchema({
1100
+ zoom: numberProperty("Edit-mode content zoom, 1 = 100%.", {
1101
+ maximum: EDIT_ZOOM_MAX,
1102
+ minimum: EDIT_ZOOM_MIN,
1103
+ }),
1104
+ }),
1105
+ category: "View",
1106
+ id: "canvas.setEditZoom",
1107
+ level: "document",
1108
+ menus: ["palette"],
1109
+ group: "3_canvas",
1110
+ requires: "a document in edit mode",
1111
+ when: documentOpen,
1112
+ enablement: () => deps.getCanvasMode() === "edit",
1113
+ run: (_commandCtx, args) => {
1114
+ requireTab("canvas.setEditZoom");
1115
+ setEditZoom(
1116
+ boundedNumberArg("canvas.setEditZoom", args, "zoom", EDIT_ZOOM_MIN, EDIT_ZOOM_MAX),
1117
+ );
1118
+ },
1119
+ title: "Set Edit Zoom",
1120
+ },
1121
+ /*
1122
+ * ── The rendering context (§4.2's control ③) ──────────────────────────────────────────────
1123
+ *
1124
+ * Three setters for the three axes the Context popover offers. The popover wrote
1125
+ * `session.ui.activeMedia` / `previewColorScheme` / `showLayout` through `updateUi` directly, so
1126
+ * none of them was a command: not in the palette, not scriptable, not addressable by the
1127
+ * assistant, and not bindable — in a design whose first principle is that a capability exists
1128
+ * as a `Command` record and every surface projects it.
1129
+ *
1130
+ * SETTERS, not cycles. §5.3's keymap declares `⌘⌥↑`/`⌘⌥↓` and `⌘⌥⇧S` to cycle the size and
1131
+ * scheme axes, and a chord carries no argument, so those chords need `next`/`prev` records of
1132
+ * their own — a delta each, which is what §13's R1 forbids a screenshot from naming. Naming the
1133
+ * state you end in works from every surface; the chords are a separate decision and are not
1134
+ * made here.
1135
+ */
1136
+ {
1137
+ args: {
1138
+ additionalProperties: false,
1139
+ properties: {
1140
+ ...paneArg,
1141
+ media: {
1142
+ description:
1143
+ "A breakpoint key the document renders under, or null for the base rendering.",
1144
+ type: ["string", "null"],
1145
+ },
1146
+ },
1147
+ required: ["media"],
1148
+ type: "object",
1149
+ },
1150
+ category: "View",
1151
+ id: "canvas.setBreakpoint",
1152
+ level: "document",
1153
+ menus: ["palette"],
1154
+ group: "3_canvas",
1155
+ requires: "an open document",
1156
+ when: documentOpen,
1157
+ run: (_commandCtx, args) => {
1158
+ const tab = contextTab("canvas.setBreakpoint", args);
1159
+ const { media } = args as { media?: unknown };
1160
+ if (media !== null && typeof media !== "string") {
1161
+ throw new RangeError(
1162
+ `command "canvas.setBreakpoint" argument "media": expected a breakpoint key or null`,
1163
+ );
1164
+ }
1165
+ /* REFUSES a key the document cannot render under, rather than rendering at a breakpoint
1166
+ that does not exist — the rule `data.expandRow` applies to a state entry's name.
1167
+ `getEffectiveMedia` and not `document.$media`: the popover offers the SITE's breakpoints
1168
+ too, and a command that refused what the control offers would be worse than no command. */
1169
+ const declared = Object.keys(
1170
+ getEffectiveMedia(tab.doc.document?.$media as Record<string, string> | undefined),
1171
+ );
1172
+ if (media !== null && !declared.includes(media)) {
1173
+ throw new RangeError(
1174
+ `command "canvas.setBreakpoint" argument "media": "${media}" is not a breakpoint this ` +
1175
+ `document defines — it defines: ${declared.length > 0 ? declared.join(", ") : "nothing"}`,
1176
+ );
1177
+ }
1178
+ tab.session.ui.activeMedia = media;
1179
+ repaint(args);
1180
+ },
1181
+ title: "Set Breakpoint",
1182
+ },
1183
+ {
1184
+ args: argsSchema({
1185
+ ...paneArg,
1186
+ scheme: enumProperty(COLOR_SCHEMES, "Which color scheme the canvas renders in."),
1187
+ }),
1188
+ category: "View",
1189
+ id: "canvas.setColorScheme",
1190
+ level: "document",
1191
+ menus: ["palette"],
1192
+ group: "3_canvas",
1193
+ requires: "an open document",
1194
+ when: documentOpen,
1195
+ run: (_commandCtx, args) => {
1196
+ const tab = contextTab("canvas.setColorScheme", args);
1197
+ tab.session.ui.previewColorScheme = enumArg(
1198
+ "canvas.setColorScheme",
1199
+ args,
1200
+ "scheme",
1201
+ COLOR_SCHEMES,
1202
+ ) as "auto" | "dark" | "light";
1203
+ repaint(args);
1204
+ },
1205
+ title: "Set Color Scheme",
1206
+ },
1207
+ /*
1208
+ * The language a pane RENDERS AS — and only that.
1209
+ *
1210
+ * Jx has no message catalogue: a translation is a different file in a different directory
1211
+ * (site-architecture §13.3), so "show this page in French" is a navigation and the locale
1212
+ * preset owns it. What is left over is genuinely a rendering context, and it is the half of
1213
+ * §13.4 the canvas can show today: the artboard root's `lang` and `dir`. That is why this is a
1214
+ * verb and not a label — an RTL locale mirrors the layout on screen. (§13.4's other half,
1215
+ * `$page.locale` in the rendered state, is injected by the compiler and not by the canvas's
1216
+ * `substitutePreviewParams`; previewing it is a separate change to the render path.)
1217
+ *
1218
+ * It lives in this file rather than in `i18n/i18n-commands.ts` because `contextTab`, `paneArg`
1219
+ * and `repaint` are local closures here, and every rendering-context verb needs all three: the
1220
+ * bar is drawn per pane, so a verb that could only reach the focused one would repaint the
1221
+ * wrong stage. `insert.data` sits outside its namespace's module for the same reason.
1222
+ */
1223
+ {
1224
+ args: argsSchema(
1225
+ {
1226
+ ...paneArg,
1227
+ locale: stringProperty("BCP 47 tag this pane renders as."),
1228
+ },
1229
+ ["locale"],
1230
+ ),
1231
+ category: "View",
1232
+ id: "i18n.switchLocale",
1233
+ level: "document",
1234
+ menus: ["palette"],
1235
+ group: "3_canvas",
1236
+ requires: "an open document in a project that declares more than one locale",
1237
+ when: (ctx) => ctx.document.open && ctx.project.isMultilingual,
1238
+ run: (_commandCtx, args) => {
1239
+ const tab = contextTab("i18n.switchLocale", args);
1240
+ const locale = stringArg("i18n.switchLocale", args, "locale");
1241
+ /* Read at RUN time. `projectState` is replaced wholesale rather than mutated, so a value
1242
+ cached at module scope answers for whichever project was open when this file loaded. */
1243
+ const i18n = getEffectiveLocales();
1244
+ /* REFUSES a tag the project does not declare, rather than clamping to the default — the
1245
+ rule `canvas.setBreakpoint` applies to a breakpoint key. A locale is a URL prefix and an
1246
+ `hreflang` at the same time, so rendering "as" a language the site does not have would
1247
+ preview a page that cannot exist. */
1248
+ if (i18n === null || !i18n.locales.includes(locale)) {
1249
+ throw new RangeError(
1250
+ `command "i18n.switchLocale" argument "locale": "${locale}" is not a locale this ` +
1251
+ `project declares — it declares: ${i18n === null ? "nothing" : i18n.locales.join(", ")}`,
1252
+ );
1253
+ }
1254
+ tab.session.ui.previewLocale = locale;
1255
+ repaint(args);
1256
+ },
1257
+ title: "Set Rendering Language",
1258
+ },
1259
+ {
1260
+ args: argsSchema({
1261
+ ...paneArg,
1262
+ visible: booleanProperty("True to draw the page's layout elements, false to hide them."),
1263
+ }),
1264
+ category: "View",
1265
+ id: "canvas.setLayoutVisible",
1266
+ level: "document",
1267
+ menus: ["palette"],
1268
+ group: "3_canvas",
1269
+ requires: "an open document",
1270
+ when: documentOpen,
1271
+ run: (_commandCtx, args) => {
1272
+ const tab = contextTab("canvas.setLayoutVisible", args);
1273
+ tab.session.ui.showLayout = booleanArg("canvas.setLayoutVisible", args, "visible");
1274
+ repaint(args);
1275
+ },
1276
+ title: "Show Layout Elements",
1277
+ },
1278
+ {
1279
+ args: {
1280
+ additionalProperties: false,
1281
+ properties: {
1282
+ ...paneArg,
1283
+ open: {
1284
+ default: true,
1285
+ description: "True to open the resolving-with popover, false to close it.",
1286
+ type: "boolean",
1287
+ },
1288
+ },
1289
+ required: [],
1290
+ type: "object",
1291
+ },
1292
+ category: "View",
1293
+ id: "canvas.setResolvingOpen",
1294
+ level: "document",
1295
+ menus: ["palette"],
1296
+ group: "3_canvas",
1297
+ requires: "an open document",
1298
+ when: documentOpen,
1299
+ /*
1300
+ * The route params and component test props live in a popover now, and a transient surface
1301
+ * opens by COMMAND rather than by clicking (plan §13.2) — otherwise the one shot that types a
1302
+ * test value would need a CSS selector to reach it, which §13's contract forbids outright.
1303
+ * The pointer and the camera use the same door.
1304
+ */
1305
+ run: (_commandCtx, args) => {
1306
+ // Resolve the tab first, so a pane with no document refuses before anything opens.
1307
+ contextTab("canvas.setResolvingOpen", args);
1308
+ const { open, pane } = args as { open?: unknown; pane?: unknown };
1309
+ // `open` defaults to true and is never COERCED: `{ open: "no" }` would otherwise read as a
1310
+ // Close, the class of silent wrong answer this setter family exists to stop.
1311
+ deps.setResolvingOpen(
1312
+ typeof pane === "string" ? pane : workspace.activePaneId,
1313
+ open === undefined || booleanArg("canvas.setResolvingOpen", args, "open"),
1314
+ );
1315
+ },
1316
+ title: "Show Resolving Values",
1317
+ },
1318
+ /*
1319
+ * ── The values a render resolves WITH ─────────────────────────────────────────────────────
1320
+ *
1321
+ * The two controls in that popover wrote `session.ui` through `updateUi` directly, while every
1322
+ * control in the rendering-context popover beside them runs a command — and `runContextCommand`
1323
+ * says why in as many words: "the popover WAS the capability, and the palette, the assistant and
1324
+ * `__jxAutomation` had no name for it." Moving these behind a click without naming them would
1325
+ * have reintroduced that, one layer deeper: a value you can now only reach by opening a popover
1326
+ * and typing.
1327
+ *
1328
+ * The screenshot budget agrees. `counter-test-prop` typed into `pane.primary/prop:count`; with
1329
+ * a verb it spends a `cmd` instead, so `inputSteps` falls 14 → 13 and the region leaves the
1330
+ * manifest, dropping `nonDerivedRegions` 11 → 10. Both budgets may only ratchet down, and this
1331
+ * ratchets both.
1332
+ */
1333
+ {
1334
+ args: {
1335
+ additionalProperties: false,
1336
+ properties: {
1337
+ ...paneArg,
1338
+ name: stringProperty("The prop's name, as the component's state declares it."),
1339
+ value: {
1340
+ description:
1341
+ "The test value — any JSON. Null clears it and the prop returns to its default.",
1342
+ type: ["string", "number", "boolean", "array", "object", "null"],
1343
+ },
1344
+ },
1345
+ required: ["name"],
1346
+ type: "object",
1347
+ },
1348
+ category: "View",
1349
+ id: "canvas.setTestProp",
1350
+ level: "document",
1351
+ menus: ["palette"],
1352
+ group: "3_canvas",
1353
+ requires: "an open component document",
1354
+ when: documentOpen,
1355
+ run: (_commandCtx, args) => {
1356
+ const tab = contextTab("canvas.setTestProp", args);
1357
+ const name = stringArg("canvas.setTestProp", args, "name");
1358
+ // REFUSES a prop the component does not declare, rather than seeding a value nothing reads
1359
+ // — the rule `data.expandRow` and `canvas.setBreakpoint` both apply to their own names.
1360
+ const declared = Object.keys(
1361
+ (tab.doc.document?.state as Record<string, unknown> | undefined) ?? {},
1362
+ );
1363
+ if (!declared.includes(name)) {
1364
+ throw new RangeError(
1365
+ `command "canvas.setTestProp" argument "name": "${name}" is not a prop this component ` +
1366
+ `declares — it declares: ${declared.length > 0 ? declared.join(", ") : "nothing"}`,
1367
+ );
1368
+ }
1369
+ const { value } = args as { value?: unknown };
1370
+ const next = { ...tab.session.ui.previewProps };
1371
+ if (value === null || value === undefined) {
1372
+ delete next[name];
1373
+ } else {
1374
+ next[name] = value as JsonValue;
1375
+ }
1376
+ tab.session.ui.previewProps = Object.keys(next).length > 0 ? next : null;
1377
+ repaint(args);
1378
+ },
1379
+ title: "Set Test Value",
1380
+ },
1381
+ {
1382
+ args: {
1383
+ additionalProperties: false,
1384
+ properties: {
1385
+ ...paneArg,
1386
+ name: stringProperty("The route parameter's name, without brackets."),
1387
+ value: stringProperty("The value to preview the route with. Empty clears it."),
1388
+ },
1389
+ required: ["name", "value"],
1390
+ type: "object",
1391
+ },
1392
+ category: "View",
1393
+ id: "canvas.setRouteParam",
1394
+ level: "document",
1395
+ menus: ["palette"],
1396
+ group: "3_canvas",
1397
+ requires: "an open page with a dynamic route",
1398
+ when: documentOpen,
1399
+ run: (_commandCtx, args) => {
1400
+ const tab = contextTab("canvas.setRouteParam", args);
1401
+ const name = stringArg("canvas.setRouteParam", args, "name");
1402
+ const declared = dynamicRouteParams(tab.documentPath);
1403
+ if (!declared.includes(name)) {
1404
+ throw new RangeError(
1405
+ `command "canvas.setRouteParam" argument "name": "${name}" is not a parameter of this ` +
1406
+ `route — it has: ${declared.length > 0 ? declared.join(", ") : "none"}`,
1407
+ );
1408
+ }
1409
+ tab.session.ui.previewParams = {
1410
+ ...tab.session.ui.previewParams,
1411
+ [name]: stringArg("canvas.setRouteParam", args, "value"),
1412
+ };
1413
+ repaint(args);
1414
+ },
1415
+ title: "Set Route Parameter",
1416
+ },
1417
+ ];
1418
+ }
1419
+
1420
+ /**
1421
+ * Register the canvas view verbs. Called from the bootstrap, beside the transforms they drive.
1422
+ *
1423
+ * @param {CommandRegistry} registry
1424
+ * @param {CanvasCommandDeps} deps
1425
+ */
1426
+ export function registerCanvasViewCommands(
1427
+ registry: CommandRegistry,
1428
+ deps: CanvasCommandDeps,
1429
+ ): void {
1430
+ registry.registerAll(canvasViewCommands(deps));
1431
+ }
1432
+
1433
+ /**
1434
+ * Toggle the "active" class on a stage's artboard headers, from that stage's own `activeMedia`.
1435
+ *
1436
+ * Surface-taking for the same reason everything else here is: the breakpoint a header reports is a
1437
+ * fact about the document that stage is drawing, and reading `activeTab` drew the focused pane's
1438
+ * breakpoint onto the other pane's artboards.
1439
+ */
1440
+ export function updateActivePanelHeaders(surface: CanvasSurface = activeCanvasSurface()) {
1441
+ const activeMedia = activeMediaOfPane(surface.paneId);
1442
+ for (const p of surface.panels) {
401
1443
  const header = p.element?.querySelector(".canvas-panel-header");
402
1444
  if (header) {
403
1445
  const isActive =