@jxsuite/studio 4.1.0 → 5.0.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 (607) hide show
  1. package/README.md +1 -1
  2. package/data/elements-meta.json +5 -15
  3. package/data/html-meta.json +51 -6
  4. package/dist/chunks/{2020-n6tfwnda.js → 2020-g79n01v9.js} +14 -14
  5. package/dist/chunks/action-group-sp6tna8v.js +118 -0
  6. package/dist/chunks/adapter-nj7tnjfn.js +13 -0
  7. package/dist/chunks/cf-account-picker-fyf63pmy.js +495 -0
  8. package/dist/chunks/cf-settings-71bv5zn9.js +20 -0
  9. package/dist/chunks/color-area-v17df7b8.js +154 -0
  10. package/dist/chunks/color-field-anwybnkn.js +208 -0
  11. package/dist/chunks/color-slider-44ez96fc.js +79 -0
  12. package/dist/chunks/color-xgg3kfw7.js +35 -0
  13. package/dist/chunks/combobox-5e7jev23.js +236 -0
  14. package/dist/chunks/components-2w1f35qs.js +18 -0
  15. package/dist/chunks/content-source-3wqeas45.js +46 -0
  16. package/dist/chunks/convert-to-repeater-d0cgnb6d.js +639 -0
  17. package/dist/chunks/dialog-s890b918.js +85 -0
  18. package/dist/chunks/editor-3qhbg51x.js +35 -0
  19. package/dist/chunks/entry-commands-d6g50j7s.js +42 -0
  20. package/dist/chunks/extension-sections-fy7q4fe9.js +651 -0
  21. package/dist/chunks/file-ops-3btj6st0.js +47 -0
  22. package/dist/chunks/files-ef8fgs3w.js +72 -0
  23. package/dist/chunks/format-host-3c33nwr8.js +40 -0
  24. package/dist/chunks/fs-events-13tj6sex.js +18 -0
  25. package/dist/chunks/git-panel-4wp2rn9b.js +49 -0
  26. package/dist/chunks/icons-yy10wk9r.js +402 -0
  27. package/dist/chunks/iframe-entry-15g3n6xv.js +114 -0
  28. package/dist/chunks/iframe-entry-b27vq81n.js +203 -0
  29. package/dist/chunks/iframe-entry-nbyxee1y.js +154 -0
  30. package/dist/chunks/iframe-host-jgesd924.js +115 -0
  31. package/dist/chunks/{index-wcnt3r9g.js → index-mdmyx6s2.js} +11 -11
  32. package/dist/chunks/index-n4j7qxyr.js +109 -0
  33. package/dist/chunks/{javascript-q8kpmfah.js → javascript-xvrfrpkb.js} +8 -9
  34. package/dist/chunks/{jsonMode-dkr040ga.js → jsonMode-tyx2kj6p.js} +526 -527
  35. package/dist/chunks/jx-validate-j0wxk72w.js +15 -0
  36. package/dist/chunks/layers-1nq5evsh.js +29 -0
  37. package/dist/chunks/layers-panel-ar2gh90g.js +40 -0
  38. package/dist/chunks/listbox-6v2ac08j.js +44 -0
  39. package/dist/chunks/lit-html-m244w9nh.js +17 -0
  40. package/dist/chunks/media-picker-2qbetq73.js +33 -0
  41. package/dist/chunks/menu-q99sy9rf.js +285 -0
  42. package/dist/chunks/menu-rf7n72mf.js +14 -0
  43. package/dist/chunks/{monaco-binding-rj6xpx9y.js → monaco-binding-sw6en4dw.js} +12 -15
  44. package/dist/chunks/monaco-buffer-bhy3hb6m.js +24 -0
  45. package/dist/chunks/{monaco-setup-vx9bhp1d.js → monaco-setup-8r62fj6y.js} +27 -37
  46. package/dist/chunks/notify-628t0anh.js +21 -0
  47. package/dist/chunks/number-field-5cndyft8.js +72 -0
  48. package/dist/chunks/parse-0zx14y15.js +10 -0
  49. package/dist/chunks/platform-ts9cg1a4.js +15 -0
  50. package/dist/chunks/popover-47z3ytva.js +25 -0
  51. package/dist/chunks/publish-panel-98f7sr3g.js +27 -0
  52. package/dist/chunks/quick-search-srzz83k2.js +45 -0
  53. package/dist/chunks/references-n553f0e1.js +26 -0
  54. package/dist/chunks/right-panel-rrgx2dn3.js +45 -0
  55. package/dist/chunks/section-registry-5pq32t0q.js +25 -0
  56. package/dist/chunks/select-2r16ez41.js +49 -0
  57. package/dist/chunks/split-g7nm1rgb.js +191 -0
  58. package/dist/chunks/studio-0hqbpbd0.js +62 -0
  59. package/dist/chunks/studio-18avnhjp.js +2430 -0
  60. package/dist/chunks/{studio-kryvsw6j.js → studio-1k325pwf.js} +48 -48
  61. package/dist/chunks/{studio-msd847zn.js → studio-25hkdrkw.js} +357 -86
  62. package/dist/chunks/studio-26d7myd1.js +258 -0
  63. package/dist/chunks/{studio-4r1pv6zf.js → studio-38emantv.js} +22 -5
  64. package/dist/chunks/studio-3gxv60tn.js +422 -0
  65. package/dist/chunks/{studio-fbf92ckf.js → studio-3hxkd92t.js} +275 -163
  66. package/dist/chunks/studio-4m0haseg.js +185 -0
  67. package/dist/chunks/{studio-ynxwkrzd.js → studio-4wbb9ct0.js} +1953 -1785
  68. package/dist/chunks/{studio-1wagb550.js → studio-5523bpzt.js} +11 -11
  69. package/dist/chunks/{studio-x4qkdat5.js → studio-57mf66w2.js} +1749 -2481
  70. package/dist/chunks/studio-5jy81t3s.js +17463 -0
  71. package/dist/chunks/studio-5rt7sqa0.js +1168 -0
  72. package/dist/chunks/studio-5te5tqng.js +81 -0
  73. package/dist/chunks/{studio-p4ngc046.js → studio-6ewqv390.js} +9 -9
  74. package/dist/chunks/{studio-9ab8gqsm.js → studio-6nzaktvy.js} +7 -7
  75. package/dist/chunks/studio-6xa21ej9.js +28 -0
  76. package/dist/chunks/{studio-5ehhackm.js → studio-727xh0e3.js} +19 -19
  77. package/dist/chunks/{studio-ahn3pdgx.js → studio-79y5sz51.js} +52 -38
  78. package/dist/chunks/{studio-gcye7qka.js → studio-7e11jpa9.js} +21 -21
  79. package/dist/chunks/{studio-q7v10cg2.js → studio-7r42c6ca.js} +17 -7
  80. package/dist/chunks/studio-7xptrg0j.js +954 -0
  81. package/dist/chunks/{studio-cft9r4r7.js → studio-81390n6f.js} +22 -22
  82. package/dist/chunks/{studio-q43a5y9h.js → studio-8jn5yhyn.js} +16 -16
  83. package/dist/chunks/studio-8qtrpzr3.js +839 -0
  84. package/dist/chunks/{studio-dg7zyt5x.js → studio-98b3z07n.js} +1 -1
  85. package/dist/chunks/studio-9xxz1q69.js +1356 -0
  86. package/dist/chunks/studio-akbww0j5.js +13615 -0
  87. package/dist/chunks/{studio-x813zp48.js → studio-arfpe389.js} +30 -30
  88. package/dist/chunks/{studio-trxrabqf.js → studio-az3zd49t.js} +475 -283
  89. package/dist/chunks/studio-cpvb2ed1.js +1171 -0
  90. package/dist/chunks/{studio-f3wqjtk5.js → studio-d0300636.js} +5076 -18
  91. package/dist/chunks/{studio-bgxt93ga.js → studio-d055c37e.js} +13 -13
  92. package/dist/chunks/studio-d1ja2x9r.js +362 -0
  93. package/dist/chunks/{studio-b9b6t1dg.js → studio-e1f24eff.js} +5664 -5666
  94. package/dist/chunks/{studio-3bqpr0zh.js → studio-em9nw8eh.js} +299 -110
  95. package/dist/chunks/studio-erdatzvp.js +1190 -0
  96. package/dist/chunks/{studio-g1wdche0.js → studio-esrxgwh6.js} +80 -80
  97. package/dist/chunks/studio-fabxk6r9.js +2811 -0
  98. package/dist/chunks/{studio-3n4ndrzh.js → studio-h4nvnjvz.js} +303 -303
  99. package/dist/chunks/{studio-dm19q43r.js → studio-hhvs4sdg.js} +10 -10
  100. package/dist/chunks/{studio-q9jd1a60.js → studio-kcgff4r0.js} +15 -18
  101. package/dist/chunks/{studio-685s0ytq.js → studio-nqgawfks.js} +572 -572
  102. package/dist/chunks/{studio-xrapxwwc.js → studio-p1dw8wfw.js} +55 -29
  103. package/dist/chunks/studio-pm6hgy7r.js +4763 -0
  104. package/dist/chunks/studio-q0a3yk4n.js +33 -0
  105. package/dist/chunks/{studio-wcy8trfx.js → studio-q3x06vmj.js} +23 -22
  106. package/dist/chunks/studio-qahxvnan.js +111 -0
  107. package/dist/chunks/{studio-53n8bp04.js → studio-r6xmmtfh.js} +1 -1
  108. package/dist/chunks/{studio-2b946z26.js → studio-rkstz940.js} +3658 -6134
  109. package/dist/chunks/{studio-tvgyw9xw.js → studio-rnyf3f8e.js} +83 -250
  110. package/dist/chunks/studio-rpyah2ew.js +2805 -0
  111. package/dist/chunks/studio-rs76k67r.js +12 -0
  112. package/dist/chunks/{studio-jkftxyvz.js → studio-s6z853aq.js} +5858 -2152
  113. package/dist/chunks/{studio-6yw4y3my.js → studio-sgdc5pfc.js} +8 -8
  114. package/dist/chunks/studio-tb3ev01j.js +763 -0
  115. package/dist/chunks/{studio-t5wqwzdh.js → studio-tjjk7jvk.js} +1 -1
  116. package/dist/chunks/studio-vhv5tgxm.js +51 -0
  117. package/dist/chunks/{studio-qjy2z9bv.js → studio-vz68yn9e.js} +32 -137
  118. package/dist/chunks/studio-wqpngpsk.js +174 -0
  119. package/dist/chunks/{studio-b55y6ewx.js → studio-x3rty6tr.js} +58 -58
  120. package/dist/chunks/studio-xsykpvsh.js +8448 -0
  121. package/dist/chunks/{studio-92ab2jqe.js → studio-y9zedyjn.js} +14 -14
  122. package/dist/chunks/studio-yxa81f45.js +12 -0
  123. package/dist/chunks/swatch-group-t0n6h2rn.js +139 -0
  124. package/dist/chunks/tabs-wbp1cdws.js +156 -0
  125. package/dist/chunks/textfield-abk6qyee.js +73 -0
  126. package/dist/chunks/toast-780pvfbv.js +27 -0
  127. package/dist/chunks/toast-host-3sqv46sn.js +116 -0
  128. package/dist/chunks/toolbar-xbh5bhtr.js +194 -0
  129. package/dist/chunks/tooltip-eptb614w.js +369 -0
  130. package/dist/chunks/tree-a73p0pqm.js +367 -0
  131. package/dist/chunks/{tsMode-76kr53q0.js → tsMode-jy4m86rv.js} +83 -85
  132. package/dist/chunks/workspace-wrew8zk4.js +69 -0
  133. package/dist/chunks/{ws-client-6shc5rze.js → ws-client-ny4zc51r.js} +20 -22
  134. package/dist/iframe-entry.js +1780 -667
  135. package/dist/studio.js +52819 -36480
  136. package/dist/workers/editor.worker.js +1040 -1085
  137. package/dist/workers/json.worker.js +1946 -1991
  138. package/dist/workers/ts.worker.js +4960 -5005
  139. package/index.html +1 -0
  140. package/package.json +18 -52
  141. package/src/about/about-modal.ts +72 -123
  142. package/src/browse/library-commands.ts +20 -49
  143. package/src/browse/library-layouts.ts +87 -218
  144. package/src/browse/library-pane.ts +406 -438
  145. package/src/canvas/canvas-helpers.ts +0 -49
  146. package/src/canvas/canvas-live-render.ts +25 -2
  147. package/src/canvas/canvas-render.ts +666 -231
  148. package/src/canvas/canvas-surface.ts +0 -1
  149. package/src/canvas/canvas-utils.ts +310 -117
  150. package/src/canvas/dialog-path.ts +76 -0
  151. package/src/canvas/dialog-state.ts +67 -0
  152. package/src/canvas/diff-marks.ts +252 -0
  153. package/src/canvas/diff-toolbar.ts +349 -0
  154. package/src/canvas/diff-view.ts +123 -0
  155. package/src/canvas/edit-width-drag.ts +187 -0
  156. package/src/canvas/edit-width.ts +150 -0
  157. package/src/canvas/iframe-entry.ts +125 -8
  158. package/src/canvas/iframe-host.ts +271 -15
  159. package/src/canvas/iframe-interaction.ts +133 -13
  160. package/src/canvas/iframe-patch.ts +7 -1
  161. package/src/canvas/iframe-position.ts +5 -6
  162. package/src/canvas/iframe-protocol.ts +106 -5
  163. package/src/canvas/iframe-render.ts +373 -5
  164. package/src/canvas/iframe-slash.ts +2 -2
  165. package/src/canvas/iframe-subtree.ts +5 -8
  166. package/src/canvas/path-mapping.ts +25 -3
  167. package/src/canvas/popover-path.ts +106 -0
  168. package/src/canvas/popover-state.ts +155 -0
  169. package/src/canvas/surface-registry.ts +21 -10
  170. package/src/collab/presence-chips.ts +51 -73
  171. package/src/commands/app-commands.ts +10 -0
  172. package/src/commands/budget.ts +12 -0
  173. package/src/commands/command-args.ts +369 -6
  174. package/src/commands/defaults.ts +44 -14
  175. package/src/commands/live-context.ts +11 -5
  176. package/src/commands/reference.ts +12 -2
  177. package/src/commands/registry.ts +165 -8
  178. package/src/content/entry-commands.ts +16 -23
  179. package/src/content/entry-editor.ts +144 -101
  180. package/src/editor/canvas-slash-bridge.ts +2 -2
  181. package/src/editor/context-menu.ts +49 -200
  182. package/src/editor/convert-to-component.ts +42 -82
  183. package/src/editor/convert-to-repeater.ts +132 -173
  184. package/src/editor/inline-edit.ts +11 -0
  185. package/src/editor/shortcuts.ts +65 -26
  186. package/src/editor/slash-menu.ts +150 -157
  187. package/src/files/file-ops.ts +117 -11
  188. package/src/files/files.ts +943 -660
  189. package/src/files/json-layout.ts +474 -0
  190. package/src/files/serialize-document.ts +8 -1
  191. package/src/format/convert-file.ts +16 -10
  192. package/src/format/format-host.ts +50 -1
  193. package/src/github/github-auth.ts +71 -29
  194. package/src/github/github-publish.ts +57 -65
  195. package/src/grid/cell-editors.ts +46 -4
  196. package/src/grid/cell-popovers.ts +65 -62
  197. package/src/grid/grid-open.ts +86 -64
  198. package/src/grid/grid-panel.ts +390 -469
  199. package/src/grid/redirects-grid.ts +40 -44
  200. package/src/hosting/layout.ts +2 -0
  201. package/src/i18n/i18n-commands.ts +127 -62
  202. package/src/media/media-pane.ts +83 -133
  203. package/src/new-project/add-repo-modal.ts +125 -176
  204. package/src/new-project/import-tab.ts +181 -252
  205. package/src/new-project/location-fields.ts +93 -160
  206. package/src/new-project/new-project-modal.ts +376 -367
  207. package/src/packages/jxsuite-update.ts +76 -18
  208. package/src/panels/activity-panel.ts +137 -104
  209. package/src/panels/ai-chat/chat-markdown.ts +15 -13
  210. package/src/panels/ai-chat/chat-view.ts +307 -472
  211. package/src/panels/ai-chat/composer.ts +175 -163
  212. package/src/panels/ai-chat/sessions-view.ts +31 -62
  213. package/src/panels/ai-panel.ts +210 -185
  214. package/src/panels/block-action-bar.ts +437 -643
  215. package/src/panels/bottom-dock.ts +112 -47
  216. package/src/panels/chat-panel.ts +19 -14
  217. package/src/panels/data-explorer.ts +141 -102
  218. package/src/panels/data-grid.ts +183 -150
  219. package/src/panels/dnd.ts +75 -20
  220. package/src/panels/editors.ts +26 -19
  221. package/src/panels/elements-panel.ts +243 -186
  222. package/src/panels/empty-state.ts +26 -42
  223. package/src/panels/events-panel.ts +950 -729
  224. package/src/panels/formula-workspace.ts +434 -207
  225. package/src/panels/frontmatter-fields.ts +185 -153
  226. package/src/panels/frontmatter-panel.ts +411 -187
  227. package/src/panels/git-diff-open.ts +148 -0
  228. package/src/panels/git-panel.ts +679 -605
  229. package/src/panels/head-panel.ts +617 -435
  230. package/src/panels/i18n-panel.ts +153 -133
  231. package/src/panels/imports-panel.ts +388 -414
  232. package/src/panels/jump-bar.ts +174 -147
  233. package/src/panels/layers-panel.ts +785 -671
  234. package/src/panels/left-panel.ts +166 -94
  235. package/src/panels/navigator-panels.ts +1 -1
  236. package/src/panels/pane-context.ts +529 -677
  237. package/src/panels/pane-grid.ts +265 -241
  238. package/src/panels/panel-registry.ts +15 -4
  239. package/src/panels/problems-panel.ts +177 -99
  240. package/src/panels/properties-panel.ts +1525 -966
  241. package/src/panels/provenance.ts +31 -113
  242. package/src/panels/quick-search.ts +217 -172
  243. package/src/panels/right-panel.ts +135 -169
  244. package/src/panels/seo-modal.ts +346 -167
  245. package/src/panels/settings-menu.ts +109 -646
  246. package/src/panels/settings-pane.ts +102 -86
  247. package/src/panels/signals-panel.ts +1318 -1172
  248. package/src/panels/statement-editor.ts +556 -428
  249. package/src/panels/style-panel.ts +1367 -630
  250. package/src/panels/style-utils.ts +36 -32
  251. package/src/panels/stylebook-doc.ts +30 -73
  252. package/src/panels/stylebook-layers-panel.ts +202 -109
  253. package/src/panels/stylebook-panel.ts +124 -88
  254. package/src/panels/tab-strip.ts +385 -287
  255. package/src/platforms/cloud.ts +106 -16
  256. package/src/platforms/devserver.ts +13 -0
  257. package/src/publish/deploy-checklist.ts +124 -78
  258. package/src/publish/publish-commands.ts +4 -12
  259. package/src/publish/publish-panel.ts +134 -328
  260. package/src/services/a11y-report.ts +100 -43
  261. package/src/services/ai-command-tools.ts +468 -0
  262. package/src/services/ai-session-store.ts +6 -0
  263. package/src/services/ai-system-prompt.ts +110 -18
  264. package/src/services/ai-tools.ts +28 -143
  265. package/src/services/ai-write-report.ts +174 -0
  266. package/src/services/document-assistant.ts +50 -2
  267. package/src/services/idle.ts +16 -14
  268. package/src/services/live-surfaces.ts +237 -0
  269. package/src/services/model-uri.ts +75 -0
  270. package/src/services/notify.ts +63 -6
  271. package/src/services/popover-report.ts +325 -0
  272. package/src/services/surface-registry.ts +56 -0
  273. package/src/services/tool-executor.ts +6 -0
  274. package/src/settings/contexts-section.ts +301 -299
  275. package/src/settings/contributed-section.ts +361 -222
  276. package/src/settings/css-vars-editor.ts +320 -325
  277. package/src/settings/defs-editor.ts +572 -535
  278. package/src/settings/dependencies-editor.ts +79 -107
  279. package/src/settings/extension-commands.ts +379 -0
  280. package/src/settings/extension-rows.ts +113 -0
  281. package/src/settings/extensions-section.ts +287 -0
  282. package/src/settings/general-settings.ts +130 -181
  283. package/src/settings/head-editor.ts +10 -305
  284. package/src/settings/locales-section.ts +146 -196
  285. package/src/settings/preferences-accounts.ts +1 -1
  286. package/src/settings/preferences-dialog.ts +350 -330
  287. package/src/settings/project-sections.ts +108 -181
  288. package/src/settings/schema-field-ui.ts +11 -399
  289. package/src/settings/section-registry.ts +8 -1
  290. package/src/settings/settings-document.ts +31 -29
  291. package/src/shell/tree.ts +42 -87
  292. package/src/shell.ts +48 -36
  293. package/src/studio.ts +90 -67
  294. package/src/style/token-ref.ts +0 -39
  295. package/src/surfaces/about.json +263 -0
  296. package/src/surfaces/about.ts +163 -0
  297. package/src/surfaces/add-repo.json +486 -0
  298. package/src/surfaces/add-repo.ts +236 -0
  299. package/src/surfaces/ai-chat.json +2185 -0
  300. package/src/surfaces/ai-chat.ts +549 -0
  301. package/src/surfaces/ai-credentials-form.json +332 -0
  302. package/src/surfaces/ai-credentials-form.ts +151 -0
  303. package/src/surfaces/ai-managed-connect.json +108 -0
  304. package/src/surfaces/ai-managed-connect.ts +123 -0
  305. package/src/surfaces/ai-model-picker.json +114 -0
  306. package/src/surfaces/ai-model-picker.ts +139 -0
  307. package/src/surfaces/block-action-bar.json +640 -0
  308. package/src/surfaces/block-action-bar.ts +368 -0
  309. package/src/surfaces/bottom-dock.json +154 -0
  310. package/src/surfaces/bottom-dock.ts +125 -0
  311. package/src/surfaces/canvas-stage.json +444 -0
  312. package/src/surfaces/canvas-stage.ts +253 -0
  313. package/src/surfaces/cf-account-picker.json +300 -0
  314. package/src/surfaces/cf-account-picker.ts +214 -0
  315. package/src/surfaces/commandbar.json +984 -0
  316. package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +414 -389
  317. package/src/surfaces/convert-repeater.json +309 -0
  318. package/src/surfaces/convert-repeater.ts +276 -0
  319. package/src/surfaces/data-actions.json +157 -0
  320. package/src/surfaces/data-actions.ts +115 -0
  321. package/src/surfaces/dialog.json +289 -0
  322. package/src/surfaces/dialog.ts +343 -0
  323. package/src/surfaces/diff-toolbar.json +266 -0
  324. package/src/surfaces/diff-toolbar.ts +139 -0
  325. package/src/surfaces/doc-header.json +739 -0
  326. package/src/surfaces/doc-header.ts +223 -0
  327. package/src/surfaces/empty-state.json +152 -0
  328. package/src/surfaces/empty-state.ts +173 -0
  329. package/src/surfaces/entry-editor.json +281 -0
  330. package/src/surfaces/entry-editor.ts +191 -0
  331. package/src/surfaces/expression-editor.json +977 -0
  332. package/src/surfaces/expression-editor.ts +266 -0
  333. package/src/surfaces/files-panel.json +592 -0
  334. package/src/surfaces/files-panel.ts +301 -0
  335. package/src/surfaces/formula-palette.json +364 -0
  336. package/src/surfaces/formula-palette.ts +340 -0
  337. package/src/surfaces/git-panel.json +1822 -0
  338. package/src/surfaces/git-panel.ts +272 -0
  339. package/src/surfaces/github-auth.json +112 -0
  340. package/src/surfaces/github-auth.ts +139 -0
  341. package/src/surfaces/github-publish.json +206 -0
  342. package/src/surfaces/github-publish.ts +169 -0
  343. package/src/surfaces/grid-cell.json +218 -0
  344. package/src/surfaces/grid-cell.ts +120 -0
  345. package/src/surfaces/grid-open.json +147 -0
  346. package/src/surfaces/grid-open.ts +87 -0
  347. package/src/surfaces/grid-panel.json +706 -0
  348. package/src/surfaces/grid-panel.ts +206 -0
  349. package/src/surfaces/grid-replace.json +179 -0
  350. package/src/surfaces/grid-replace.ts +89 -0
  351. package/src/surfaces/grid-views.json +446 -0
  352. package/src/surfaces/grid-views.ts +172 -0
  353. package/src/surfaces/inspector-dock.json +187 -0
  354. package/src/surfaces/inspector-dock.ts +161 -0
  355. package/src/surfaces/jump-bar.json +243 -0
  356. package/src/surfaces/jump-bar.ts +129 -0
  357. package/src/surfaces/jxsuite-update.json +146 -0
  358. package/src/surfaces/jxsuite-update.ts +149 -0
  359. package/src/surfaces/library-pane.json +1509 -0
  360. package/src/surfaces/library-pane.ts +363 -0
  361. package/src/surfaces/logic-panel.json +1949 -0
  362. package/src/surfaces/logic-panel.ts +369 -0
  363. package/src/surfaces/logic-workspace.json +685 -0
  364. package/src/surfaces/logic-workspace.ts +238 -0
  365. package/src/surfaces/media-browser.json +303 -0
  366. package/src/surfaces/media-browser.ts +145 -0
  367. package/src/surfaces/media-field.json +163 -0
  368. package/src/surfaces/media-field.ts +116 -0
  369. package/src/surfaces/media-pane.json +513 -0
  370. package/src/surfaces/media-pane.ts +197 -0
  371. package/src/surfaces/menu.json +232 -0
  372. package/src/surfaces/menu.ts +375 -0
  373. package/src/surfaces/navigator-dock.json +113 -0
  374. package/src/surfaces/navigator-dock.ts +130 -0
  375. package/src/surfaces/new-project.json +1654 -0
  376. package/src/surfaces/new-project.ts +411 -0
  377. package/src/surfaces/palette.json +535 -0
  378. package/src/surfaces/pane-context.json +1497 -0
  379. package/src/surfaces/pane-context.ts +513 -0
  380. package/src/surfaces/pane-grid.json +188 -0
  381. package/src/surfaces/pane-grid.ts +250 -0
  382. package/src/surfaces/panel-activity.json +563 -0
  383. package/src/surfaces/panel-activity.ts +187 -0
  384. package/src/surfaces/panel-data.json +141 -0
  385. package/src/surfaces/panel-data.ts +156 -0
  386. package/src/surfaces/panel-deploy-checklist.json +283 -0
  387. package/src/surfaces/panel-deploy-checklist.ts +175 -0
  388. package/src/surfaces/panel-elements.json +367 -0
  389. package/src/surfaces/panel-elements.ts +175 -0
  390. package/src/surfaces/panel-i18n.json +569 -0
  391. package/src/surfaces/panel-i18n.ts +212 -0
  392. package/src/surfaces/panel-imports.json +780 -0
  393. package/src/surfaces/panel-imports.ts +209 -0
  394. package/src/surfaces/panel-outline.json +734 -0
  395. package/src/surfaces/panel-outline.ts +326 -0
  396. package/src/surfaces/panel-page.json +1017 -0
  397. package/src/surfaces/panel-page.ts +240 -0
  398. package/src/surfaces/panel-problems.json +497 -0
  399. package/src/surfaces/panel-problems.ts +157 -0
  400. package/src/surfaces/panel-signals.json +1973 -0
  401. package/src/surfaces/panel-signals.ts +436 -0
  402. package/src/surfaces/panel-stylebook-layers.json +317 -0
  403. package/src/surfaces/panel-stylebook-layers.ts +155 -0
  404. package/src/surfaces/preferences.json +850 -0
  405. package/src/surfaces/preferences.ts +332 -0
  406. package/src/surfaces/progress-modal.json +110 -0
  407. package/src/surfaces/progress-modal.ts +150 -0
  408. package/src/surfaces/properties-panel.json +1578 -0
  409. package/src/surfaces/properties-panel.ts +337 -0
  410. package/src/surfaces/publish.json +945 -0
  411. package/src/surfaces/publish.ts +392 -0
  412. package/src/surfaces/push-plan.json +110 -0
  413. package/src/surfaces/push-plan.ts +87 -0
  414. package/src/surfaces/rail.json +255 -0
  415. package/src/surfaces/rail.ts +206 -0
  416. package/src/surfaces/reference-field.json +191 -0
  417. package/src/surfaces/reference-field.ts +168 -0
  418. package/src/surfaces/schema-builder.json +1011 -0
  419. package/src/surfaces/schema-builder.ts +181 -0
  420. package/src/surfaces/schema-form.json +1013 -0
  421. package/src/surfaces/schema-form.ts +287 -0
  422. package/src/surfaces/secret-field.json +60 -0
  423. package/src/surfaces/secret-field.ts +146 -0
  424. package/src/surfaces/seo.json +932 -0
  425. package/src/surfaces/seo.ts +334 -0
  426. package/src/surfaces/settings-contexts.json +538 -0
  427. package/src/surfaces/settings-contexts.ts +196 -0
  428. package/src/surfaces/settings-contributed.json +489 -0
  429. package/src/surfaces/settings-contributed.ts +199 -0
  430. package/src/surfaces/settings-css-vars.json +820 -0
  431. package/src/surfaces/settings-css-vars.ts +199 -0
  432. package/src/surfaces/settings-defs.json +1363 -0
  433. package/src/surfaces/settings-defs.ts +232 -0
  434. package/src/surfaces/settings-deploy.json +108 -0
  435. package/src/surfaces/settings-deploy.ts +135 -0
  436. package/src/surfaces/settings-extensions.json +486 -0
  437. package/src/surfaces/settings-extensions.ts +201 -0
  438. package/src/surfaces/settings-head.json +779 -0
  439. package/src/surfaces/settings-head.ts +397 -0
  440. package/src/surfaces/settings-locales.json +420 -0
  441. package/src/surfaces/settings-locales.ts +137 -0
  442. package/src/surfaces/settings-overview.json +423 -0
  443. package/src/surfaces/settings-overview.ts +218 -0
  444. package/src/surfaces/settings-packages.json +549 -0
  445. package/src/surfaces/settings-packages.ts +158 -0
  446. package/src/surfaces/settings-pane.json +147 -0
  447. package/src/surfaces/settings-pane.ts +150 -0
  448. package/src/surfaces/settings-rawjson.json +104 -0
  449. package/src/surfaces/settings-rawjson.ts +107 -0
  450. package/src/surfaces/shell.json +99 -0
  451. package/src/surfaces/shell.ts +30 -0
  452. package/src/surfaces/slash-menu.json +180 -0
  453. package/src/surfaces/slash-menu.ts +272 -0
  454. package/src/surfaces/statements.json +607 -0
  455. package/src/surfaces/statements.ts +238 -0
  456. package/src/surfaces/statusbar.json +94 -0
  457. package/src/{panels → surfaces}/statusbar.ts +163 -90
  458. package/src/surfaces/style-panel.json +1643 -0
  459. package/src/surfaces/style-panel.ts +338 -0
  460. package/src/surfaces/stylebook-chrome.json +122 -0
  461. package/src/surfaces/stylebook-chrome.ts +122 -0
  462. package/src/surfaces/tab-strip.json +448 -0
  463. package/src/surfaces/tab-strip.ts +200 -0
  464. package/src/surfaces/target-line.json +419 -0
  465. package/src/surfaces/target-line.ts +349 -0
  466. package/src/surfaces/toasts.json +189 -0
  467. package/src/surfaces/welcome.json +571 -0
  468. package/src/surfaces/welcome.ts +338 -0
  469. package/src/tabs/project-config.ts +71 -15
  470. package/src/tabs/tab.ts +52 -1
  471. package/src/types.ts +22 -0
  472. package/src/ui/ai-credentials-form.ts +104 -100
  473. package/src/ui/ai-managed-connect.ts +54 -36
  474. package/src/ui/ai-model-picker.ts +75 -47
  475. package/src/ui/cf-account-picker.ts +76 -141
  476. package/src/ui/color-selector.ts +80 -301
  477. package/src/ui/dynamic-slot.ts +43 -283
  478. package/src/ui/expression-editor.ts +797 -576
  479. package/src/ui/field-input.ts +8 -181
  480. package/src/ui/form-controls.ts +838 -366
  481. package/src/ui/formula-chips.ts +80 -85
  482. package/src/ui/kit.ts +33 -0
  483. package/src/ui/layers.ts +293 -593
  484. package/src/ui/media-picker.ts +203 -284
  485. package/src/ui/panel-resize.ts +251 -64
  486. package/src/ui/popover-surface.ts +153 -0
  487. package/src/ui/progress-modal.ts +22 -60
  488. package/src/ui/regions.ts +13 -7
  489. package/src/ui/schema-form.ts +906 -440
  490. package/src/ui/surface.ts +122 -0
  491. package/src/ui/unit-selector.ts +15 -131
  492. package/src/ui/value-source.ts +14 -0
  493. package/src/ui/virtual-window.ts +88 -9
  494. package/src/utils/canvas-media.ts +72 -0
  495. package/src/utils/edit-display.ts +7 -2
  496. package/src/utils/element-selectors.ts +69 -0
  497. package/src/utils/link-target.ts +2 -2
  498. package/src/utils/studio-utils.ts +0 -10
  499. package/src/view.ts +15 -0
  500. package/src/workspace/pane-derive.ts +36 -14
  501. package/src/workspace/workspace.ts +15 -6
  502. package/styles/canvas.css +72 -467
  503. package/styles/forced-colors.css +48 -82
  504. package/styles/forced-colors.json +41 -0
  505. package/styles/inspector.css +2 -1301
  506. package/styles/overlays.css +96 -2823
  507. package/styles/panels.css +6 -1423
  508. package/styles/shell-frame.css +272 -0
  509. package/styles/shell-frame.json +227 -0
  510. package/styles/shell.css +0 -1568
  511. package/styles/tokens.css +65 -136
  512. package/styles/tokens.json +84 -0
  513. package/dist/chunks/Overlay.dev-etqjwyf9.js +0 -23
  514. package/dist/chunks/adapter-jkcyhgtr.js +0 -14
  515. package/dist/chunks/cf-account-picker-en86v7x7.js +0 -163
  516. package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
  517. package/dist/chunks/components-zmx10t62.js +0 -21
  518. package/dist/chunks/content-source-48xg86wj.js +0 -49
  519. package/dist/chunks/convert-to-repeater-43tfn7cs.js +0 -268
  520. package/dist/chunks/editor-cptq5jp1.js +0 -37
  521. package/dist/chunks/entry-commands-e63f1xc0.js +0 -46
  522. package/dist/chunks/extension-sections-txv08xxg.js +0 -311
  523. package/dist/chunks/file-ops-wbh4hhsx.js +0 -48
  524. package/dist/chunks/files-3kfkwhk4.js +0 -93
  525. package/dist/chunks/flow-qbfgrpeh.js +0 -634
  526. package/dist/chunks/focus-trap.esm-9t2drp0g.js +0 -1025
  527. package/dist/chunks/focus-visible-6eg70z18.js +0 -167
  528. package/dist/chunks/format-host-w16q81tf.js +0 -43
  529. package/dist/chunks/fs-events-hrrg328g.js +0 -21
  530. package/dist/chunks/git-panel-532whr1y.js +0 -59
  531. package/dist/chunks/index-zqxmqxmh.js +0 -109
  532. package/dist/chunks/jx-validate-8er6gbxy.js +0 -19
  533. package/dist/chunks/layers-panel-07hm7g5j.js +0 -67
  534. package/dist/chunks/layers-q75tj2mf.js +0 -45
  535. package/dist/chunks/lit-html-j055mkh8.js +0 -22
  536. package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
  537. package/dist/chunks/notify-yjm79n4s.js +0 -30
  538. package/dist/chunks/parse-24d0gay3.js +0 -19
  539. package/dist/chunks/platform-ba1j1gbh.js +0 -16
  540. package/dist/chunks/publish-panel-v9st96ja.js +0 -29
  541. package/dist/chunks/quick-search-zccjp6gv.js +0 -66
  542. package/dist/chunks/references-3qx8fkyp.js +0 -29
  543. package/dist/chunks/right-panel-vm3b6kwy.js +0 -46
  544. package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
  545. package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +0 -18
  546. package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
  547. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
  548. package/dist/chunks/sp-tooltip.dev-4sastdnw.js +0 -15
  549. package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
  550. package/dist/chunks/studio-0a5qrh01.js +0 -3
  551. package/dist/chunks/studio-0bq6eh7v.js +0 -12
  552. package/dist/chunks/studio-18j3bmcz.js +0 -14
  553. package/dist/chunks/studio-1ngbejft.js +0 -181
  554. package/dist/chunks/studio-4b5z1x43.js +0 -424
  555. package/dist/chunks/studio-4n2b5gzb.js +0 -357
  556. package/dist/chunks/studio-4rzyrtjz.js +0 -426
  557. package/dist/chunks/studio-5fx3g9jg.js +0 -4325
  558. package/dist/chunks/studio-5myknm0a.js +0 -1903
  559. package/dist/chunks/studio-7afd21nw.js +0 -67
  560. package/dist/chunks/studio-7nte2hj4.js +0 -613
  561. package/dist/chunks/studio-8jpzqk5s.js +0 -128
  562. package/dist/chunks/studio-8q517dfr.js +0 -24
  563. package/dist/chunks/studio-950k0dx6.js +0 -6939
  564. package/dist/chunks/studio-9n4mrmnb.js +0 -184
  565. package/dist/chunks/studio-a0tswy0w.js +0 -58
  566. package/dist/chunks/studio-ahnzd9rg.js +0 -23
  567. package/dist/chunks/studio-cmb03bg5.js +0 -13890
  568. package/dist/chunks/studio-d1ks410h.js +0 -175
  569. package/dist/chunks/studio-ef49pnzx.js +0 -387
  570. package/dist/chunks/studio-eqq1w638.js +0 -113
  571. package/dist/chunks/studio-ew0djmhg.js +0 -341
  572. package/dist/chunks/studio-gfgb8pe6.js +0 -1652
  573. package/dist/chunks/studio-h4rxscfc.js +0 -10
  574. package/dist/chunks/studio-jc994dnr.js +0 -1066
  575. package/dist/chunks/studio-kyh9avvv.js +0 -28
  576. package/dist/chunks/studio-m3hwn7y1.js +0 -120
  577. package/dist/chunks/studio-mtfnaqs9.js +0 -659
  578. package/dist/chunks/studio-n6j9t10q.js +0 -318
  579. package/dist/chunks/studio-n8b9wqwy.js +0 -291
  580. package/dist/chunks/studio-qcy05dp8.js +0 -1743
  581. package/dist/chunks/studio-qq6jym04.js +0 -53
  582. package/dist/chunks/studio-rdx8jcxy.js +0 -83
  583. package/dist/chunks/studio-rn9ta2mz.js +0 -111
  584. package/dist/chunks/studio-t7qb9db8.js +0 -665
  585. package/dist/chunks/studio-tmc5emrw.js +0 -8
  586. package/dist/chunks/studio-z6nw2ek2.js +0 -18
  587. package/dist/chunks/studio-zczps70q.js +0 -378
  588. package/dist/chunks/studio-zqazsct2.js +0 -1823
  589. package/dist/chunks/workspace-fjx52ryv.js +0 -89
  590. package/src/panels/activity-bar.ts +0 -251
  591. package/src/panels/panel-scheduler.ts +0 -219
  592. package/src/panels/style-inputs.ts +0 -237
  593. package/src/panels/target-line.ts +0 -274
  594. package/src/panels/welcome-screen.ts +0 -346
  595. package/src/ui/button-group.ts +0 -102
  596. package/src/ui/choice-field.ts +0 -72
  597. package/src/ui/field-row.ts +0 -249
  598. package/src/ui/formula-palette.ts +0 -213
  599. package/src/ui/icons.ts +0 -52
  600. package/src/ui/jx-theme.ts +0 -130
  601. package/src/ui/spectrum.ts +0 -340
  602. package/src/ui/value-selector.ts +0 -174
  603. package/src/ui/widgets.ts +0 -172
  604. /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
  605. /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
  606. /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
  607. /package/dist/chunks/{tsMode-ef187txx.css → tsMode-hnxxb73h.css} +0 -0
@@ -8,10 +8,8 @@
8
8
  * colour-scheme switch and feature toggles (rendering state), and Export (a mode action). Plan §3.2
9
9
  * ⑦ replaces them with three axes that each say what they are:
10
10
  *
11
- * - **Editor kind** — `Canvas ⌄`, offering only the kinds this document declares AND its pane can
12
- * host ({@link hostableKindsOf}), so the control can never contain a permanently dead entry. A
13
- * document with one kind renders the name as text rather than as a dropdown that cannot go
14
- * anywhere.
11
+ * - **Editor kind** — `Canvas ⌄`, offering only the kinds this document declares. A document with one
12
+ * kind renders the name as text rather than as a dropdown that cannot go anywhere.
15
13
  * - **Canvas view** — `Edit │ Design` as a radio ({@link canvasBaseViewsFor}) with a **Preview**
16
14
  * toggle beside it ({@link previewStateOf}), and none at all in a pane that may not host the
17
15
  * Canvas. The two are drawn apart because they are two axes: preview is a flag over an edit or
@@ -27,23 +25,25 @@
27
25
  * because these are values you type and everything in the rendering-context popover is something
28
26
  * you pick — and because a row of text fields is what made the 28px band unreadable.
29
27
  *
28
+ * **This module is the FLOW; `surfaces/pane-context.json` is the markup.** Everything below decides
29
+ * — which kinds a document declares, what a lens may never write, which pane's stage a zoom verb
30
+ * lands on, what a route param's candidates are, what a typed test prop parses as — and hands one
31
+ * projection of words and keyed rows to `surfaces/pane-context.ts`, one mount per pane. Nothing
32
+ * here renders.
33
+ *
30
34
  * **The bar is not a grid row.** It renders inside the pane's own cell (`#pane-chrome`, stacked
31
35
  * over `#canvas-wrap`), because a per-pane surface cannot be a row of the application grid — the
32
36
  * second pane would have no way to have one. The stage is offset by {@link PANE_CONTEXT_VAR} rather
33
37
  * than by a track, and the zoom pod floats bottom-right over the canvas exactly as §3.2 ⑩ asks.
34
38
  *
35
- * **The read-only banner rides with the bar.** `collab/presence-chips.ts` wrote the sentence a
36
- * read-only guest needs before their first keystroke (§7.4); the surface that owes it a home is
37
- * this one, because it is the per-pane, per-document chrome that sits directly above the editing
38
- * surface. It is stacked under the bar inside `.pc-band` and the offset is MEASURED from that band
39
+ * **The read-only banner rides with the bar.** `collab/collab-state.ts` says whether a guest may
40
+ * write (§7.4); the surface that owes them the sentence is this one, because it is the per-pane,
41
+ * per-document chrome that sits directly above the editing surface. It is stacked under the bar
42
+ * inside the document's `[part="band"]` and the offset is MEASURED from that band
39
43
  * ({@link applyPaneContextOffset}), so a two-line banner pushes the stage down instead of covering
40
44
  * the document it is warning you about.
41
- *
42
- * Module shape follows `tab-strip.ts`: mount(host, ctx) → effectScope/effect → render().
43
45
  */
44
46
 
45
- import { html, render as litRender, nothing } from "lit-html";
46
- import { ref } from "lit-html/directives/ref.js";
47
47
  import { projectState, updateUi } from "../store";
48
48
  import { effect, effectScope } from "../reactivity";
49
49
  import { PRIMARY_PANE, focusPane, workspace } from "../workspace/workspace";
@@ -63,8 +63,9 @@ import {
63
63
  pinRefusal,
64
64
  presetRefusal,
65
65
  } from "../workspace/pane-derive";
66
- import { renderPopover } from "../ui/layers";
67
- import { rectOf } from "../utils/geometry";
66
+ import { openMenu } from "../surfaces/menu";
67
+ import { emptyPaneContextView, mountPaneContextSurface } from "../surfaces/pane-context";
68
+ import { PANE_SELECTOR } from "../surfaces/pane-grid";
68
69
  import { paneRegion } from "../ui/regions";
69
70
  import {
70
71
  fitToScreen,
@@ -81,7 +82,6 @@ import {
81
82
  } from "../canvas/canvas-utils";
82
83
  import { editorKindOf, editorKindsOf, modeForEditorKind } from "../tabs/tab";
83
84
  import { collabState } from "../collab/collab-state";
84
- import { readOnlyBannerTemplate } from "../collab/presence-chips";
85
85
  import { activeRegistry } from "../commands/active-registry";
86
86
  import { getEffectiveLayoutPath, getEffectiveLocales, getEffectiveMedia } from "../site-context";
87
87
  import { localeLabel, localeOfPath } from "@jxsuite/schema/locale";
@@ -97,7 +97,15 @@ import type { Tab } from "../tabs/tab";
97
97
  import type { ResolvedI18n } from "@jxsuite/schema/locale";
98
98
  import type { JsonValue } from "../types";
99
99
  import type { EffectScope } from "@vue/reactivity";
100
- import type { TemplateResult } from "lit-html";
100
+ import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
101
+ import { rectOf } from "../utils/geometry";
102
+ import type {
103
+ PaneContextActions,
104
+ PaneContextChoice,
105
+ PaneContextSurfaceHandle,
106
+ PaneContextToggle,
107
+ PaneContextView,
108
+ } from "../surfaces/pane-context";
101
109
 
102
110
  /**
103
111
  * The CSS variable the stage is offset by while a context bar is on screen.
@@ -117,10 +125,10 @@ export interface PaneContextCtx {
117
125
  * There is no `getCanvasMode` here, and there cannot be one.
118
126
  *
119
127
  * It answered for the FOCUSED pane — `studio.ts` composes it from `workspace.activePaneId` — and
120
- * this bar is drawn once per pane, from `tabOfPane(paneId)`. One reader was left: `exportTpl`,
121
- * which is why entering Code in EITHER pane put an Export button in BOTH bars. Every mode
122
- * question this module asks is now asked of the pane it is drawing: `canvasModeOfTab(tab)` for
123
- * the tab's own effective mode, `canvasModeOfPane(paneId)` for the stage's.
128
+ * this bar is drawn once per pane, from `tabOfPane(paneId)`. One reader was left: the Export
129
+ * control, which is why entering Code in EITHER pane put an Export button in BOTH bars. Every
130
+ * mode question this module asks is now asked of the pane it is drawing: `canvasModeOfTab(tab)`
131
+ * for the tab's own effective mode, `canvasModeOfPane(paneId)` for the stage's.
124
132
  */
125
133
  /**
126
134
  * Write the BASE mode of the tab it is GIVEN. The editor-kind dropdown and the view control both
@@ -150,6 +158,16 @@ export interface PaneContextCtx {
150
158
  */
151
159
  const _hosts = new Map<string, HTMLElement>();
152
160
 
161
+ /**
162
+ * Each pane's mounted document, keyed the same way and for the same reason.
163
+ *
164
+ * A surface handle is per-pane state as much as a host is: it holds that pane's scope, its two
165
+ * popovers and whether the "resolving with" one is showing. A module-level `let` here would give
166
+ * the second pane's `canvas.setResolvingOpen` the first pane's panel — the exact shape
167
+ * `scripts/check-pane-singletons.ts` exists to refuse.
168
+ */
169
+ const _surfaces = new Map<string, PaneContextSurfaceHandle>();
170
+
153
171
  let _ctx: PaneContextCtx | null = null;
154
172
 
155
173
  let _scope: EffectScope | null = null;
@@ -182,7 +200,7 @@ export function attachPaneChromeHost(paneId: string, host: HTMLElement | null):
182
200
  return;
183
201
  }
184
202
  if (previous) {
185
- litRender(nothing, previous);
203
+ disposePane(paneId);
186
204
  applyPaneContextOffset(0, previous);
187
205
  }
188
206
  if (host) {
@@ -193,6 +211,12 @@ export function attachPaneChromeHost(paneId: string, host: HTMLElement | null):
193
211
  }
194
212
  }
195
213
 
214
+ /** Take one pane's document down and forget it. Idempotent. */
215
+ function disposePane(paneId: string): void {
216
+ _surfaces.get(paneId)?.dispose();
217
+ _surfaces.delete(paneId);
218
+ }
219
+
196
220
  /**
197
221
  * Mount the pane chrome. Idempotent.
198
222
  *
@@ -214,17 +238,15 @@ export function mount(host: HTMLElement, ctx: PaneContextCtx) {
214
238
  `activeTab` alone left the side pane's bar frozen describing whatever it last drew. */
215
239
  for (const pane of workspace.panes) {
216
240
  /* NO DERIVATION READS HERE, and the reason is one rule rather than a tally. `render()`
217
- runs inside this effect and draws EVERY attached pane, so whatever a template here reads
218
- is ALREADY a dependency and restating it as a `void` line chooses nothing: `derived.kind`
219
- picks the bar's shape, `activeMediaOfPane` reads `kind`/`preset`/`media` for the Context
220
- axis, `zoomOf` reads `derived.zoom` for the pod, and {@link zoomPodTpl} asks
241
+ runs inside this effect and projects EVERY attached pane, so whatever the projection
242
+ reads is ALREADY a dependency and restating it as a `void` line chooses nothing:
243
+ `derived.kind` picks the bar's shape, `activeMediaOfPane` reads `kind`/`preset`/`media`
244
+ for the Context axis, `zoomOf` reads `derived.zoom` for the pod, and {@link podFor} asks
221
245
  `canvasModeOfPane`, which for a lens answers `derived.mode` and decides whether the pod
222
246
  is drawn at all — pinned by lens-chrome's "the pod is drawn from the LENS's mode".
223
- An earlier version of this comment listed `mode` among the fields no template reads,
224
- which was wrong about the fact and right about the deletion. `status` and `reason` really
225
- are unread here, so tracking them only repainted this bar for a change it does not draw.
226
- The stage is the surface that needs those two declared, because `renderCanvasImpl` runs
227
- in a rAF rather than in an effect — see `studio.ts`. */
247
+ `status` and `reason` really are unread here, so tracking them only repainted this bar
248
+ for a change it does not draw. The stage is the surface that needs those two declared,
249
+ because `renderCanvasImpl` runs in a rAF rather than in an effect — see `studio.ts`. */
228
250
  const tab = tabOfPane(pane.id);
229
251
  if (!tab) {
230
252
  continue;
@@ -260,7 +282,9 @@ export function mount(host: HTMLElement, ctx: PaneContextCtx) {
260
282
  export function unmount() {
261
283
  _scope?.stop();
262
284
  _scope = null;
263
- for (const host of _hosts.values()) {
285
+ dismissPresetMenu();
286
+ for (const [paneId, host] of _hosts) {
287
+ disposePane(paneId);
264
288
  applyPaneContextOffset(0, host);
265
289
  }
266
290
  _hosts.clear();
@@ -284,7 +308,7 @@ export function unmount() {
284
308
  * @param {HTMLElement | null} [host] The bar's host. Its cell takes the variable when it has one.
285
309
  */
286
310
  export function applyPaneContextOffset(height: number, host?: HTMLElement | null): void {
287
- const target = host?.closest<HTMLElement>(".pane") ?? document.documentElement;
311
+ const target = host?.closest<HTMLElement>(PANE_SELECTOR) ?? document.documentElement;
288
312
  target.style.setProperty(PANE_CONTEXT_VAR, `${height}px`);
289
313
  }
290
314
 
@@ -295,10 +319,13 @@ export function applyPaneContextOffset(height: number, host?: HTMLElement | null
295
319
  * knows how many lines that is. `offsetHeight` is 0 in a DOM with no layout engine (every unit
296
320
  * test), so the bar's declared height is the floor: the offset is then exactly what it was before
297
321
  * banners existed, which is the honest answer when nothing has been laid out.
322
+ *
323
+ * The band is the DOCUMENT's, handed back by the mount rather than found by selector — a surface
324
+ * that re-queries its own markup is holding a node that the next render may already have replaced
325
+ * (guidelines §9.4).
298
326
  */
299
- function topBandHeight(host: HTMLElement): number {
300
- const band = host.querySelector<HTMLElement>(".pc-band");
301
- return Math.max(band?.offsetHeight ?? 0, PANE_CONTEXT_HEIGHT);
327
+ function topBandHeight(surface: PaneContextSurfaceHandle): number {
328
+ return Math.max(surface.band()?.offsetHeight ?? 0, PANE_CONTEXT_HEIGHT);
302
329
  }
303
330
 
304
331
  /**
@@ -316,14 +343,14 @@ function wantsContextBar(tab: Tab): boolean {
316
343
  return tab.session.ui.canvasMode !== "settings";
317
344
  }
318
345
 
319
- /** Paint every attached pane's chrome. */
346
+ /** Project every attached pane's chrome. */
320
347
  export function render() {
321
348
  for (const [paneId, host] of _hosts) {
322
349
  renderPane(paneId, host);
323
350
  }
324
351
  }
325
352
 
326
- /** Paint one pane's chrome into its own host, from its own tab. */
353
+ /** Project one pane's chrome into its own mount, from its own tab. */
327
354
  function renderPane(paneId: string, host: HTMLElement) {
328
355
  if (!_ctx) {
329
356
  return;
@@ -331,8 +358,32 @@ function renderPane(paneId: string, host: HTMLElement) {
331
358
  try {
332
359
  const tab = tabOfPane(paneId);
333
360
  const show = Boolean(tab) && wantsContextBar(tab as Tab);
334
- litRender(show ? paneChromeTemplate(tab as Tab, paneId, _ctx) : nothing, host);
335
- applyPaneContextOffset(show ? topBandHeight(host) : 0, host);
361
+ if (!show) {
362
+ disposePane(paneId);
363
+ applyPaneContextOffset(0, host);
364
+ return;
365
+ }
366
+ const view = viewFor(tab as Tab, paneId, _ctx);
367
+ let surface = _surfaces.get(paneId);
368
+ if (!surface || !surface.connected()) {
369
+ disposePane(paneId);
370
+ surface = mountPaneContextSurface(paneId, host, view, actionsFor(paneId, _ctx));
371
+ _surfaces.set(paneId, surface);
372
+ /* The band cannot be measured until the document has drawn it, so the first offset lands
373
+ after the mount settles; every later projection writes it synchronously from the standing
374
+ band. The stage is offset by the declared height in between, which is what it was before
375
+ banners existed. */
376
+ applyPaneContextOffset(PANE_CONTEXT_HEIGHT, host);
377
+ void surface.ready.then(() => {
378
+ const live = _surfaces.get(paneId);
379
+ if (live) {
380
+ applyPaneContextOffset(topBandHeight(live), host);
381
+ }
382
+ });
383
+ return;
384
+ }
385
+ surface.update(view);
386
+ applyPaneContextOffset(topBandHeight(surface), host);
336
387
  } catch (error) {
337
388
  console.error("pane-context render error:", error);
338
389
  }
@@ -341,36 +392,20 @@ function renderPane(paneId: string, host: HTMLElement) {
341
392
  // ─── The preset menu · the first renderer of `context/pane` ──────────────────
342
393
 
343
394
  /** The open preset menu, if any. One at a time, like every other menu in this shell. */
344
- let _presetMenu: { dismiss: () => void } | null = null;
395
+ let _presetMenu: MenuHandle | null = null;
345
396
 
346
397
  /** Close the preset menu, if it is open. Idempotent. */
347
398
  export function dismissPresetMenu(): void {
348
- _presetMenu?.dismiss();
399
+ _presetMenu?.close();
349
400
  _presetMenu = null;
350
401
  }
351
402
 
352
403
  /**
353
- * The `⟲` trigger, in the context bar's leading slot — the one that was empty.
404
+ * Open the preset menu, from the ⟲ trigger in the bar's leading slot — the one that was empty.
354
405
  *
355
- * `.pc-spacer` existed to push the three axes right. It is where §18.4's preset menu goes because
356
- * the menu is about THIS PANE and the bar is the pane's own chrome; the alternative homes (the tab
357
- * strip, the jump bar) are about a document and an address respectively.
358
- */
359
- function presetTriggerTpl(paneId: string): TemplateResult {
360
- return html`<button
361
- class="pc-derive-trigger"
362
- aria-haspopup="menu"
363
- title="Show something beside this pane"
364
- @click=${(event: MouseEvent) => {
365
- openPresetMenu(event, paneId);
366
- }}
367
- >
368
- <span aria-hidden="true">⟲</span>
369
- </button>`;
370
- }
371
-
372
- /**
373
- * Open the preset menu.
406
+ * The leading slot existed to push the three axes right. It is where §18.4's preset menu goes
407
+ * because the menu is about THIS PANE and the bar is the pane's own chrome; the alternative homes
408
+ * (the tab strip, the jump bar) are about a document and an address respectively.
374
409
  *
375
410
  * **There is no `pane.showDerivePresets`.** §13.5, quoted verbatim in `canvas/canvas-render.ts`:
376
411
  * opening a menu to press an item names a CONTROL; the item is the command. Every row here runs
@@ -378,53 +413,81 @@ function presetTriggerTpl(paneId: string): TemplateResult {
378
413
  * addresses those ids directly rather than driving this widget. Its region is
379
414
  * `overlay.menu:derive-presets`, which `ui/layers.ts` derives from the slot key — no budget is
380
415
  * spent.
416
+ *
417
+ * **It is the kit menu, not a second one.** `surfaces/menu.ts` already owns roving focus,
418
+ * typeahead, light dismissal, the disabled row's `requires` sentence and the Escape that closes it
419
+ * (guidelines §12.5); this hands it rows.
381
420
  */
382
- function openPresetMenu(event: MouseEvent, paneId: string): void {
383
- event.preventDefault();
384
- event.stopPropagation();
421
+ function openPresetMenu(paneId: string, anchor: HTMLElement): void {
385
422
  dismissPresetMenu();
386
423
  const registry = activeRegistry();
387
424
  if (!registry) {
388
425
  return;
389
426
  }
390
- const anchor = rectOf(event.currentTarget as HTMLElement);
391
- const left = Math.round(Math.min(anchor.left, window.innerWidth - 4));
392
- const top = Math.round(anchor.bottom);
393
427
  const rows = presetRows(paneId);
394
- _presetMenu = renderPopover(
395
- html`<sp-popover open style="position:fixed;z-index:10000;left:${left}px;top:${top}px">
396
- <sp-menu role="menu" aria-label="Show beside this pane">
397
- ${rows.map(
398
- (row) => html`<sp-menu-item
399
- role="menuitem"
400
- ?disabled=${row.disabled !== null}
401
- title=${row.disabled ? `requires ${row.disabled}` : row.label}
402
- @click=${() => {
403
- dismissPresetMenu();
404
- if (row.disabled === null) {
405
- /* THE PANE THE MENU IS ABOUT, before the verb that resolves the focus runs.
406
- See {@link PresetRow.pane}: all three of these commands take the focused pane as
407
- their subject, and a pointer gesture on this pane has already focused it — but a
408
- keyboard activation has not, because `panels/pane-grid.ts` focuses on
409
- pointerdown. Without this line the secondary pane's menu derived from the
410
- primary and its Unsplit closed the wrong pane, by keyboard only. */
411
- focusPane(row.pane);
412
- void registry.run(row.command, row.args);
413
- }
414
- }}
415
- >${row.label}</sp-menu-item
416
- >`,
417
- )}
418
- </sp-menu>
419
- </sp-popover>`,
420
- {
421
- dismissOnOutsideClick: true,
422
- onDismiss: () => {
423
- _presetMenu = null;
424
- },
425
- region: "derive-presets",
428
+ _presetMenu = openMenu({
429
+ label: "Show beside this pane",
430
+ opener: anchor,
431
+ onClosed: () => {
432
+ _presetMenu = null;
426
433
  },
427
- );
434
+ place: (box) => {
435
+ const at = rectOf(anchor);
436
+ return {
437
+ x: Math.round(Math.min(at.left, window.innerWidth - box.width - 4)),
438
+ y: Math.round(at.bottom),
439
+ };
440
+ },
441
+ region: "derive-presets",
442
+ rows: rows.map((row) => menuRow(row)),
443
+ });
444
+ }
445
+
446
+ /**
447
+ * One preset row, as the kit menu reads it: an identity, a sentence, and what pressing it runs.
448
+ *
449
+ * **There is no second refusal inside `run`, and the lit version's is deliberately gone.** The
450
+ * handler used to re-ask `row.disabled !== null` before doing anything, which was a second answer
451
+ * to the question the line above it already settles: `jx-menu-item` writes `aria-disabled` from
452
+ * this flag and raises no `select` while it is true, so a refused row never reaches `run` at all.
453
+ * Keeping both left the projection's own flag with nothing able to tell right from wrong about it —
454
+ * `check-lens-mutants.ts` said so, by surviving.
455
+ */
456
+ function menuRow(row: PresetRow): MenuRowProjection {
457
+ return {
458
+ destructive: false,
459
+ disabled: row.disabled !== null,
460
+ dividerAbove: false,
461
+ id: rowId(row),
462
+ ...(row.disabled === null ? {} : { requires: row.disabled }),
463
+ run: () => {
464
+ /* THE PANE THE MENU IS ABOUT, before the verb that resolves the focus runs.
465
+ See {@link PresetRow.pane}: all three of these commands take the focused pane as their
466
+ subject, and a pointer gesture on this pane has already focused it — but a keyboard
467
+ activation has not, because `panels/pane-grid.ts` focuses on pointerdown. Without this line
468
+ the secondary pane's menu derived from the primary and its Unsplit closed the wrong pane,
469
+ by keyboard only. */
470
+ focusPane(row.pane);
471
+ void activeRegistry()?.run(row.command, row.args);
472
+ },
473
+ title: row.label,
474
+ };
475
+ }
476
+
477
+ /**
478
+ * A row's identity in the menu, which is not simply its command: `pane.derive` appears once per
479
+ * projection, once per declared breakpoint and once per declared locale, and a keyed `$map` cannot
480
+ * reconcile three rows that all call themselves `pane.derive`.
481
+ */
482
+ function rowId(row: PresetRow): string {
483
+ const parts = [row.command];
484
+ for (const key of ["preset", "media", "locale"] as const) {
485
+ const value = row.args[key];
486
+ if (typeof value === "string") {
487
+ parts.push(value);
488
+ }
489
+ }
490
+ return parts.join(":");
428
491
  }
429
492
 
430
493
  /** One row of the preset menu: a command, its arguments, and why it cannot run when it cannot. */
@@ -547,18 +610,10 @@ export function presetRows(paneId: string): PresetRow[] {
547
610
  return rows;
548
611
  }
549
612
 
550
- /** One labelled axis. The label is the point: five unlabelled controls is what this replaces. */
551
- function axisTpl(label: string, control: TemplateResult): TemplateResult {
552
- return html`
553
- <div class="pc-axis">
554
- <span class="pc-axis-label">${label}</span>
555
- ${control}
556
- </div>
557
- `;
558
- }
613
+ // ─── The projection ──────────────────────────────────────────────────────────
559
614
 
560
615
  /**
561
- * The bar, always the same three axes.
616
+ * Everything one pane's chrome draws, as words and keyed rows.
562
617
  *
563
618
  * There is no takeover branch. Opening a function body or a formula reveals the dock's Logic tab
564
619
  * (P8) and leaves the canvas standing underneath it, so the axes still describe the document on the
@@ -569,8 +624,7 @@ function axisTpl(label: string, control: TemplateResult): TemplateResult {
569
624
  * and the Logic tab's own header carries the Close. This bar drew a second Back and a second trail
570
625
  * beside both of them.
571
626
  */
572
- function paneChromeTemplate(tab: Tab, paneId: string, ctx: PaneContextCtx): TemplateResult {
573
- const kind = editorKindOf(tab);
627
+ function viewFor(tab: Tab, paneId: string, ctx: PaneContextCtx): PaneContextView {
574
628
  /* A LENS suppresses the two axes that WRITE. Editor kind and Canvas view both land in
575
629
  `ctx.setCanvasMode(tab, …)`, and that tab belongs to the pane beside this one — so a control
576
630
  drawn in the lens would flip the document the author is editing. The lens's own mode is the
@@ -578,34 +632,52 @@ function paneChromeTemplate(tab: Tab, paneId: string, ctx: PaneContextCtx): Temp
578
632
  a static summary for the same reason (it writes `updateUi(tab, …)`), and the zoom pod stays
579
633
  because zoom is the one view fact a lens genuinely owns. */
580
634
  const lens = derivationOfPane(paneId)?.kind === "lens";
635
+ const readOnly = collabState(tab).active && collabState(tab).readOnly;
636
+ const view: PaneContextView = {
637
+ ...baseView(paneId),
638
+ /* …and NO ⟲ trigger in a lens. Every projection row in the menu it opens is permanently
639
+ disabled from there (a derived pane cannot derive again) and the breakpoint rows are
640
+ suppressed outright, leaving one live row — Unsplit — which the derivation chip's ✕ in this
641
+ pane's own strip already runs. A control that can do nothing from where it is drawn is the
642
+ class this phase has deleted three times. A COMPANION keeps it: "Keep This Document" is live
643
+ there and it is genuinely about that pane. The leading slot stays either way: it is what
644
+ pushes the axes right, and dropping it would move the one control a lens does draw. */
645
+ presetState: lens ? "hidden" : "shown",
646
+ /* A lens draws no banner: the projection would be announcing a collaboration session it is not
647
+ in, twice on one screen, about a document it does not own. */
648
+ bannerState: !lens && readOnly ? "shown" : "hidden",
649
+ barMode: lens ? "lens" : "full",
650
+ ...podFor(tab, paneId),
651
+ };
652
+ if (lens) {
653
+ view.summary = lensSummary(paneId);
654
+ return view;
655
+ }
656
+ Object.assign(view, editorAxis(tab));
657
+ if (editorKindOf(tab) === "canvas") {
658
+ Object.assign(view, viewAxis(tab));
659
+ }
660
+ Object.assign(view, contextAxis(tab, paneId, ctx));
661
+ /* THIS tab's effective mode. `ctx.getCanvasMode()` answered for the focused pane, so a document
662
+ opened as Code in either pane put an Export button in the OTHER pane's bar as well — over a
663
+ document that is not the one the button exports. */
664
+ view.exportState = canvasModeOfTab(tab) === "source" ? "shown" : "hidden";
665
+ return view;
666
+ }
581
667
 
582
- return html`
583
- <div class="pc-band">
584
- <div class="pane-context" data-jx-region=${paneRegion(paneId, "context")}>
585
- <div class="pc-spacer">
586
- ${
587
- /* …and NO ⟲ trigger in a lens. Every projection row in the menu it opens is permanently
588
- disabled from there (a derived pane cannot derive again) and the breakpoint rows are
589
- suppressed outright, leaving one live row — Unsplit — which the derivation chip's ✕
590
- in this pane's own strip already runs. A control that can do nothing from where it is
591
- drawn is the class this phase has deleted three times. A COMPANION keeps it: "Keep
592
- This Document" is live there and it is genuinely about that pane. The SPACER stays
593
- either way: it is what pushes the axes right, and dropping it would move the one
594
- control a lens does draw. */
595
- lens ? nothing : presetTriggerTpl(paneId)
596
- }
597
- </div>
598
- ${
599
- lens
600
- ? renderingSummaryTpl(paneId)
601
- : html`${editorKindTpl(tab, ctx)} ${kind === "canvas" ? viewTpl(tab, ctx) : nothing}
602
- ${renderingContextTpl(tab, paneId, ctx)} ${exportTpl(tab, ctx)}`
603
- }
604
- </div>
605
- ${lens ? nothing : readOnlyBannerTemplate(tab)}
606
- </div>
607
- ${zoomPodTpl(tab, paneId)}
608
- `;
668
+ /**
669
+ * The projection before any axis has spoken: the two regions, and everything else absent.
670
+ *
671
+ * The empty shape comes from the SURFACE — one list of fields, beside the interface that declares
672
+ * them — because two hand-kept copies of a 36-field record is exactly how a new field ends up
673
+ * projected in one place and forgotten in the other.
674
+ */
675
+ function baseView(paneId: string): PaneContextView {
676
+ return {
677
+ ...emptyPaneContextView(),
678
+ contextRegion: paneRegion(paneId, "context"),
679
+ zoomRegion: paneRegion(paneId, "zoom"),
680
+ };
609
681
  }
610
682
 
611
683
  /**
@@ -616,7 +688,7 @@ function paneChromeTemplate(tab: Tab, paneId: string, ctx: PaneContextCtx): Temp
616
688
  * screen — but the popover behind it writes the source tab's `session.ui`, which is the one thing a
617
689
  * lens must never do.
618
690
  */
619
- function renderingSummaryTpl(paneId: string): TemplateResult {
691
+ function lensSummary(paneId: string): string {
620
692
  /* No `ctx.parseMediaEntries` here. It parsed the document's whole `$media` map, discarded the
621
693
  result through `void sizeBreakpoints` and printed the media NAME — a parse per lens-bar render
622
694
  for a value that was never read. `mediaDisplayName` is the only lookup this line needs. */
@@ -632,15 +704,14 @@ function renderingSummaryTpl(paneId: string): TemplateResult {
632
704
  document's own language, for the reason the trigger beside it is: a pane drawing the file it
633
705
  has open in the language that file is written in has nothing to report. */
634
706
  const locale = localeOf(tabOfPane(paneId));
635
- const stated = `${media ? mediaDisplayName(media) : "Base"}${
707
+ return `${media ? mediaDisplayName(media) : "Base"}${
636
708
  locale?.overridden ? ` · ${localeLabel(locale.effective)}` : ""
637
709
  }`;
638
- return axisTpl("Context", html`<span class="pc-static">${stated}</span>`);
639
710
  }
640
711
 
641
712
  // ─── Axis 1 · Editor kind ────────────────────────────────────────────────────
642
713
 
643
- function editorKindTpl(tab: Tab, ctx: PaneContextCtx): TemplateResult {
714
+ function editorAxis(tab: Tab): Partial<PaneContextView> {
644
715
  /* What the DOCUMENT declares, and nothing else. This read `hostableKindsOf` — the declared kinds
645
716
  narrowed by what the tab's pane was allowed to host — so a page in the side pane was offered
646
717
  Code and not Design. The pane cap is gone; a control that cannot go anywhere is still the
@@ -650,43 +721,23 @@ function editorKindTpl(tab: Tab, ctx: PaneContextCtx): TemplateResult {
650
721
  if (kinds.length < 2) {
651
722
  // One kind is not a choice. Rendering it as a dropdown would be a control that cannot move —
652
723
  // Which is the defect this axis exists to remove, in miniature.
653
- return axisTpl("Editor", html`<span class="pc-static">${EDITOR_KIND_LABELS[current]}</span>`);
724
+ return { editorLabel: EDITOR_KIND_LABELS[current], editorMode: "static" };
654
725
  }
655
- return axisTpl(
656
- "Editor",
657
- html`
658
- <sp-picker
659
- size="s"
660
- quiet
661
- class="pc-editor-kind"
662
- label="Editor"
663
- value=${current}
664
- @change=${(e: Event) => {
665
- const kind = (e.target as HTMLInputElement).value as EditorKind;
666
- const mode = modeForEditorKind(tab, kind);
667
- if (!mode) {
668
- return;
669
- }
670
- tab.session.ui.preview = false;
671
- ctx.setCanvasMode(tab, mode);
672
- }}
673
- >
674
- ${kinds.map(
675
- (kind) => html`<sp-menu-item value=${kind}>${EDITOR_KIND_LABELS[kind]}</sp-menu-item>`,
676
- )}
677
- </sp-picker>
678
- `,
679
- );
726
+ return {
727
+ editorMode: "picker",
728
+ editorOptions: kinds.map((kind) => ({ label: EDITOR_KIND_LABELS[kind], value: kind })),
729
+ editorValue: current,
730
+ };
680
731
  }
681
732
 
682
733
  // ─── Axis 2 · Canvas view ────────────────────────────────────────────────────
683
734
 
684
- function viewTpl(tab: Tab, ctx: PaneContextCtx): TemplateResult | typeof nothing {
735
+ function viewAxis(tab: Tab): Partial<PaneContextView> {
685
736
  // Every pane draws a live Canvas, so this is no longer narrowed by WHERE the tab is — only by
686
737
  // What the document declares. A document with no Canvas view still draws no view group.
687
738
  const views = canvasBaseViewsFor(tab);
688
739
  if (views.length === 0) {
689
- return nothing;
740
+ return {};
690
741
  }
691
742
  const current = canvasBaseViewOf(tab);
692
743
  const preview = previewStateOf(tab);
@@ -698,54 +749,21 @@ function viewTpl(tab: Tab, ctx: PaneContextCtx): TemplateResult | typeof nothing
698
749
  * say which mode you were previewing — or which one Escape would return you to. Now the radio
699
750
  * marks the base the whole time and the toggle sits beside it, pressed.
700
751
  */
701
- return axisTpl(
702
- "View",
703
- html`
704
- <sp-action-group compact size="s" class="pc-view" role="radiogroup" aria-label="Canvas view">
705
- ${views.map(
706
- (value) => html`
707
- <sp-action-button
708
- size="s"
709
- role="radio"
710
- aria-checked=${value === current ? "true" : "false"}
711
- ?selected=${value === current}
712
- title=${`Show this document in ${CANVAS_VIEW_LABELS[value]}`}
713
- @click=${() => setCanvasView(tab, value, ctx.setCanvasMode)}
714
- >
715
- ${CANVAS_VIEW_LABELS[value]}
716
- </sp-action-button>
717
- `,
718
- )}
719
- </sp-action-group>
720
- ${
721
- preview.available
722
- ? html`
723
- <sp-action-button
724
- size="s"
725
- class="pc-preview-toggle"
726
- toggles
727
- ?selected=${preview.on}
728
- aria-pressed=${preview.on ? "true" : "false"}
729
- title=${
730
- preview.on
731
- ? `Stop previewing — back to ${CANVAS_VIEW_LABELS[current ?? "edit"]}`
732
- : "Preview: the page as it ships, with editing off"
733
- }
734
- @click=${() => {
735
- setCanvasView(
736
- tab,
737
- preview.on ? (current ?? "edit") : "preview",
738
- ctx.setCanvasMode,
739
- );
740
- }}
741
- >
742
- ${CANVAS_VIEW_LABELS.preview}
743
- </sp-action-button>
744
- `
745
- : nothing
746
- }
747
- `,
748
- );
752
+ return {
753
+ previewHint: preview.on
754
+ ? `Stop previewing — back to ${CANVAS_VIEW_LABELS[current ?? "edit"]}`
755
+ : "Preview: the page as it ships, with editing off",
756
+ previewOn: preview.on,
757
+ previewState: preview.available ? "shown" : "hidden",
758
+ views: views.map((value) => ({
759
+ checked: value === current ? "true" : "false",
760
+ hint: `Show this document in ${CANVAS_VIEW_LABELS[value]}`,
761
+ key: value,
762
+ label: CANVAS_VIEW_LABELS[value],
763
+ value,
764
+ })),
765
+ viewState: "shown",
766
+ };
749
767
  }
750
768
 
751
769
  // ─── Axis 3 · Rendering context ──────────────────────────────────────────────
@@ -775,9 +793,9 @@ interface PaneLocale {
775
793
  /**
776
794
  * The language axis for a tab, or `null` when the project has no language question to ask.
777
795
  *
778
- * Called at RENDER time and never cached: `projectState` is replaced wholesale rather than mutated,
779
- * so a locale added in Settings reaches this bar on the next paint and a module-level copy would
780
- * still be describing the project that was open when this file loaded.
796
+ * Called at PROJECTION time and never cached: `projectState` is replaced wholesale rather than
797
+ * mutated, so a locale added in Settings reaches this bar on the next paint and a module-level copy
798
+ * would still be describing the project that was open when this file loaded.
781
799
  *
782
800
  * A single declared locale is the same as none for this control — there is nothing to switch TO,
783
801
  * and "groups a document declares nothing for are absent" is the rule the whole popover follows.
@@ -798,7 +816,7 @@ function localeOf(tab: Tab | null): PaneLocale | null {
798
816
  return { effective, i18n, overridden: effective !== own, pathLocale };
799
817
  }
800
818
 
801
- function renderingContextTpl(tab: Tab, paneId: string, ctx: PaneContextCtx): TemplateResult {
819
+ function contextAxis(tab: Tab, paneId: string, ctx: PaneContextCtx): Partial<PaneContextView> {
802
820
  const { ui } = tab.session;
803
821
  const { featureQueries, sizeBreakpoints } = ctx.parseMediaEntries(
804
822
  getEffectiveMedia(tab.doc.document?.$media as Record<string, string> | undefined),
@@ -813,7 +831,7 @@ function renderingContextTpl(tab: Tab, paneId: string, ctx: PaneContextCtx): Tem
813
831
  /* The language joins the trigger ONLY when the pane is not drawing the document's own — a French
814
832
  page open in a French pane is not a rendering context worth three more characters, and a bar
815
833
  that grew a third term in every multilingual project would stop reading as a state. */
816
- const summary = [
834
+ const contextSummary = [
817
835
  sizeLabel,
818
836
  schemeQueries.length > 0 ? schemeLabel : null,
819
837
  locale?.overridden ? localeLabel(locale.effective) : null,
@@ -821,285 +839,74 @@ function renderingContextTpl(tab: Tab, paneId: string, ctx: PaneContextCtx): Tem
821
839
  .filter((part) => part !== null)
822
840
  .join(" · ");
823
841
  const hasLayout = isPageDoc(tab) && Boolean(getEffectiveLayoutPath(tab.doc.document?.$layout));
824
- const resolving = isPageDoc(tab) ? paramPickersTpl(tab, paneId) : propFieldsTpl(tab, paneId);
842
+ const page = isPageDoc(tab);
843
+ const params = page ? paramRows(tab) : [];
844
+ const props = page ? [] : propRows(tab, paneId);
825
845
  /* How many of those fields carry a value. The trigger has to say something true at a glance, the
826
846
  way the Context trigger says `Base · Auto` — a chevron with no reading is a control you have to
827
847
  open in order to learn whether it was worth opening. */
828
- const resolvingSet = isPageDoc(tab)
848
+ const resolvingSet = page
829
849
  ? Object.values(ui.previewParams ?? {}).filter((v) => v !== "" && v !== undefined).length
830
850
  : Object.keys(ui.previewProps ?? {}).length;
831
-
832
- return axisTpl(
833
- "Context",
834
- html`
835
- ${resolvingTpl(paneId, resolving, resolvingSet)}
836
- <overlay-trigger placement="bottom-end" triggered-by="click">
837
- <sp-action-button
838
- slot="trigger"
839
- size="s"
840
- quiet
841
- class="pc-context-trigger"
842
- title="What this document is being rendered with"
843
- >
844
- ${summary} ⌄
845
- </sp-action-button>
846
- <sp-popover slot="click-content" tip class="pc-context-popover">
847
- <div class="pc-ctx">
848
- ${sizeGroupTpl(paneId, sizeBreakpoints, activeMedia)}
849
- ${
850
- schemeQueries.length > 0
851
- ? groupTpl(
852
- "Color scheme",
853
- html`
854
- <sp-action-group compact size="s">
855
- ${SCHEMES.map(
856
- ([value, label, title]) => html`
857
- <sp-action-button
858
- size="s"
859
- role="radio"
860
- aria-checked=${scheme === value ? "true" : "false"}
861
- title=${title}
862
- ?selected=${scheme === value}
863
- @click=${() =>
864
- runContextCommand(paneId, "canvas.setColorScheme", {
865
- scheme: value,
866
- })}
867
- >
868
- ${label}
869
- </sp-action-button>
870
- `,
871
- )}
872
- </sp-action-group>
873
- `,
874
- )
875
- : nothing
876
- }
877
- ${locale ? localeGroupTpl(paneId, locale) : nothing}
878
- ${
879
- plainQueries.length > 0
880
- ? groupTpl(
881
- "Features",
882
- html`
883
- <sp-action-group compact size="s">
884
- ${plainQueries.map(
885
- ({ name, query }) => html`
886
- <sp-action-button
887
- size="s"
888
- toggles
889
- title=${query}
890
- ?selected=${Boolean(ui.featureToggles[name])}
891
- @click=${() => {
892
- updateUi(tab, "featureToggles", {
893
- ...tab.session.ui.featureToggles,
894
- [name]: !tab.session.ui.featureToggles[name],
895
- });
896
- }}
897
- >
898
- ${mediaDisplayName(name)}
899
- </sp-action-button>
900
- `,
901
- )}
902
- </sp-action-group>
903
- `,
904
- )
905
- : nothing
906
- }
907
- ${
908
- hasLayout
909
- ? groupTpl(
910
- "Layout",
911
- html`
912
- <sp-switch
913
- size="s"
914
- class="pc-layout-switch"
915
- ?checked=${ui.showLayout !== false}
916
- @change=${() =>
917
- runContextCommand(paneId, "canvas.setLayoutVisible", {
918
- visible: tab.session.ui.showLayout === false,
919
- })}
920
- >
921
- Show layout elements
922
- </sp-switch>
923
- `,
924
- )
925
- : nothing
926
- }
927
- <button
928
- class="pc-ctx-manage"
929
- type="button"
930
- title="Breakpoints, schemes and feature queries are defined in Project Settings › Contexts"
931
- @click=${() => {
932
- void activeRegistry()?.run("settings.open", { section: "contexts" });
933
- }}
934
- >
935
- Manage contexts…
936
- </button>
937
- </div>
938
- </sp-popover>
939
- </overlay-trigger>
940
- `,
941
- );
942
- }
943
-
944
- /**
945
- * The document DATA a render resolves against — route params, component test props — in a popover.
946
- *
947
- * §4.2 folds these in behind the words "resolving with…", and this is where that lands: the phrase
948
- * is the popover's header and the fields are a vertical stack under it, the same shape as the
949
- * rendering-context popover beside it.
950
- *
951
- * They used to sit OPEN on the bar, and the argument for that was the screenshot contract (§13.1):
952
- * behind a click, the one first-hour flow the docs teach — typing a test prop on
953
- * `start/first-component` — costs a second gesture, and the manifest's input budget may only
954
- * ratchet down. The answer is not to keep n text fields on a 28px band that also carries the
955
- * editor, the view and the rendering context; it is that a transient surface opens by COMMAND (plan
956
- * §13.2), so the shot spends a `cmd` step rather than a selector and the input budget is untouched.
957
- * `canvas.setResolvingOpen` is that command, and it is the same door the pointer uses.
958
- *
959
- * @param {string} paneId
960
- * @param {TemplateResult | typeof nothing} body
961
- * @param {number} setCount — how many fields carry a value, for the trigger's summary
962
- */
963
- function resolvingTpl(
964
- paneId: string,
965
- body: TemplateResult | typeof nothing,
966
- setCount: number,
967
- ): TemplateResult | typeof nothing {
968
- if (body === nothing) {
969
- return nothing;
970
- }
971
- return html`
972
- <overlay-trigger
973
- placement="bottom-end"
974
- triggered-by="click"
975
- ${ref((el: Element | undefined) => {
976
- if (el) {
977
- _resolvingTrigger.set(paneId, el as HTMLElement & { open?: string | undefined });
978
- }
979
- })}
980
- >
981
- <sp-action-button
982
- slot="trigger"
983
- size="s"
984
- quiet
985
- class="pc-resolving-trigger"
986
- title="The values this document is being rendered with"
987
- >
988
- ${setCount > 0 ? `${setCount} set` : "Defaults"} ⌄
989
- </sp-action-button>
990
- <sp-popover slot="click-content" tip class="pc-context-popover">
991
- <div class="pc-ctx">${groupTpl("resolving with", body as TemplateResult)}</div>
992
- </sp-popover>
993
- </overlay-trigger>
994
- `;
995
- }
996
-
997
- /**
998
- * Each pane's "resolving with" `overlay-trigger`, which IS the open state.
999
- *
1000
- * No parallel `Set`. The overlay owns whether it is showing — it opens on a pointer click without
1001
- * telling anyone first — so a second record of the same fact could only ever be the stale one. The
1002
- * element's `open` property is what the pointer writes and what the command writes, and reading it
1003
- * back is how anything else asks.
1004
- */
1005
- const _resolvingTrigger = new Map<string, HTMLElement & { open?: string | undefined }>();
1006
-
1007
- /** Whether this pane's resolving popover is open. Exported for the tests. */
1008
- export function isResolvingOpen(paneId: string): boolean {
1009
- return _resolvingTrigger.get(paneId)?.open === "click";
1010
- }
1011
-
1012
- /**
1013
- * Open or close it.
1014
- *
1015
- * A named end state rather than a toggle, so `canvas.setResolvingOpen { open: false }` means the
1016
- * same thing twice and a screenshot can photograph it (§13's R1).
1017
- *
1018
- * It writes the element and does NOT re-render the bar. `overlay-trigger` moves the popover into an
1019
- * overlay portal while it is open and restores it on close; re-rendering this template in the
1020
- * middle of that leaves the moved copy painted over the canvas with no way to dismiss it. The
1021
- * overlay owns its own visibility — the only thing the bar redraws for is the trigger's summary,
1022
- * and that changes when a VALUE changes, which already renders.
1023
- */
1024
- export function setResolvingOpen(paneId: string, open: boolean): void {
1025
- const trigger = _resolvingTrigger.get(paneId);
1026
- if (trigger && isResolvingOpen(paneId) !== open) {
1027
- trigger.open = open ? "click" : undefined;
1028
- }
1029
- }
1030
-
1031
- /** Forget the held triggers — a fresh window, and the tests. */
1032
- export function resetResolvingOpen(): void {
1033
- _resolvingTrigger.clear();
1034
- }
1035
-
1036
- /** One labelled group inside the rendering-context popover. */
1037
- function groupTpl(label: string, body: TemplateResult): TemplateResult {
1038
- return html`
1039
- <div class="pc-ctx-group">
1040
- <span class="pc-ctx-label">${label}</span>
1041
- ${body}
1042
- </div>
1043
- `;
851
+ const hasResolving = page ? params.length > 0 : props.length > 0;
852
+
853
+ return {
854
+ contextSummary,
855
+ featureState: plainQueries.length > 0 ? "shown" : "hidden",
856
+ features: plainQueries.map(({ name, query }) => ({
857
+ hint: query,
858
+ key: name,
859
+ label: mediaDisplayName(name),
860
+ selected: Boolean(ui.featureToggles[name]),
861
+ value: name,
862
+ })) satisfies PaneContextToggle[],
863
+ layoutOn: ui.showLayout !== false,
864
+ layoutState: hasLayout ? "shown" : "hidden",
865
+ ...localeGroup(locale),
866
+ params,
867
+ props,
868
+ resolvingKind: page ? "params" : "props",
869
+ resolvingLabel: resolvingSet > 0 ? `${resolvingSet} set` : "Defaults",
870
+ resolvingState: hasResolving ? "shown" : "hidden",
871
+ schemes: SCHEMES.map(([value, label, hint]) => ({
872
+ checked: scheme === value ? "true" : "false",
873
+ hint,
874
+ key: value,
875
+ label,
876
+ value,
877
+ })) satisfies PaneContextChoice[],
878
+ schemeState: schemeQueries.length > 0 ? "shown" : "hidden",
879
+ sizes: sizeRows(sizeBreakpoints, activeMedia),
880
+ };
1044
881
  }
1045
882
 
1046
883
  /**
1047
884
  * The size segment: the base width plus every declared size breakpoint.
1048
885
  *
1049
886
  * It writes `ui.activeMedia`, the same field a canvas panel header click writes — one axis, one
1050
- * field, two ways in. `null` is the base, which is why the list is not simply the breakpoints.
1051
- */
1052
- /**
1053
- * Run a rendering-context verb through the registry.
1054
- *
1055
- * These four controls wrote `session.ui` directly through `updateUi`, which is why none of the
1056
- * three axes was a command: the popover WAS the capability, and the palette, the assistant and
1057
- * `__jxAutomation` had no name for it. Going through the registry makes the control and the verb
1058
- * one thing (§2, principle 1) and gets the breakpoint refusal for free.
887
+ * field, two ways in. The base is the empty string on the wire, because that is what "no breakpoint
888
+ * applied" is: `canvas.setBreakpoint` reads it back as `null`.
1059
889
  */
1060
- function runContextCommand(paneId: string, id: string, args: Record<string, unknown>): void {
1061
- // THIS pane, named. The bar is drawn once per pane and the side bar's controls write the side
1062
- // Pane's tab; a verb defaulting to the focused pane would have made the side bar edit the
1063
- // Foreground document the moment its control became a command.
1064
- void activeRegistry()?.run(id, { ...args, pane: paneId });
1065
- }
1066
-
1067
- function sizeGroupTpl(
1068
- paneId: string,
890
+ function sizeRows(
1069
891
  breakpoints: { name: string; width: number }[],
1070
892
  activeMedia: string | null,
1071
- ): TemplateResult {
1072
- return groupTpl(
1073
- "Size",
1074
- html`
1075
- <sp-action-group compact size="s" class="pc-sizes">
1076
- <sp-action-button
1077
- size="s"
1078
- role="radio"
1079
- aria-checked=${activeMedia === null ? "true" : "false"}
1080
- title="The base rendering, with no breakpoint applied"
1081
- ?selected=${activeMedia === null}
1082
- @click=${() => runContextCommand(paneId, "canvas.setBreakpoint", { media: null })}
1083
- >
1084
- Base
1085
- </sp-action-button>
1086
- ${breakpoints.map(
1087
- ({ name, width }) => html`
1088
- <sp-action-button
1089
- size="s"
1090
- role="radio"
1091
- aria-checked=${activeMedia === name ? "true" : "false"}
1092
- title=${`${mediaDisplayName(name)} — ${width}px`}
1093
- ?selected=${activeMedia === name}
1094
- @click=${() => runContextCommand(paneId, "canvas.setBreakpoint", { media: name })}
1095
- >
1096
- ${mediaDisplayName(name)}
1097
- </sp-action-button>
1098
- `,
1099
- )}
1100
- </sp-action-group>
1101
- `,
1102
- );
893
+ ): PaneContextChoice[] {
894
+ return [
895
+ {
896
+ checked: activeMedia === null ? "true" : "false",
897
+ hint: "The base rendering, with no breakpoint applied",
898
+ key: "--base",
899
+ label: "Base",
900
+ value: "",
901
+ },
902
+ ...breakpoints.map(({ name, width }) => ({
903
+ checked: activeMedia === name ? ("true" as const) : ("false" as const),
904
+ hint: `${mediaDisplayName(name)} — ${width}px`,
905
+ key: name,
906
+ label: mediaDisplayName(name),
907
+ value: name,
908
+ })),
909
+ ];
1103
910
  }
1104
911
 
1105
912
  /**
@@ -1120,57 +927,25 @@ function sizeGroupTpl(
1120
927
  * has no room for a second visual state, and the author needs to know which entry is "no override"
1121
928
  * before clicking, not after.
1122
929
  */
1123
- function localeGroupTpl(paneId: string, locale: PaneLocale): TemplateResult {
1124
- return groupTpl(
1125
- "Language",
1126
- html`
1127
- <sp-action-group
1128
- compact
1129
- size="s"
1130
- title="The language this pane renders as — its lang and direction only. The text is whatever file is open."
1131
- >
1132
- ${locale.i18n.locales.map(
1133
- (tag) => html`
1134
- <sp-action-button
1135
- size="s"
1136
- role="radio"
1137
- aria-checked=${locale.effective === tag ? "true" : "false"}
1138
- title=${
1139
- tag === locale.pathLocale
1140
- ? `${localeLabel(tag)} — the language of the file this pane has open`
1141
- : `Render as ${localeLabel(tag)} (${tag})`
1142
- }
1143
- ?selected=${locale.effective === tag}
1144
- @click=${() => runContextCommand(paneId, "i18n.switchLocale", { locale: tag })}
1145
- >
1146
- ${localeLabel(tag)}
1147
- </sp-action-button>
1148
- `,
1149
- )}
1150
- </sp-action-group>
1151
- `,
1152
- );
1153
- }
1154
-
1155
- // ─── The mode action ─────────────────────────────────────────────────────────
1156
- // Export is not an axis, and it is the one control in the old bar with nowhere to go yet: retiring
1157
- // It without a command name, a chord and a residue would be deletion, not consolidation (§2
1158
- // Principle 9). It stays in the Code view's trailing slot until the Code editor kind owns an action
1159
- // Strip of its own.
1160
-
1161
- function exportTpl(tab: Tab, ctx: PaneContextCtx): TemplateResult | typeof nothing {
1162
- /* THIS tab's effective mode. `ctx.getCanvasMode()` answered for the focused pane, so a document
1163
- opened as Code in either pane put an Export button in the OTHER pane's bar as well — over a
1164
- document that is not the one the button exports. */
1165
- if (canvasModeOfTab(tab) !== "source") {
1166
- return nothing;
930
+ function localeGroup(locale: PaneLocale | null): Partial<PaneContextView> {
931
+ if (!locale) {
932
+ return { localeState: "hidden" };
1167
933
  }
1168
- return html`
1169
- <sp-action-button size="s" class="pc-export" @click=${ctx.exportFile}>
1170
- <sp-icon-export slot="icon"></sp-icon-export>
1171
- Export
1172
- </sp-action-button>
1173
- `;
934
+ return {
935
+ localeHint:
936
+ "The language this pane renders as — its lang and direction only. The text is whatever file is open.",
937
+ locales: locale.i18n.locales.map((tag) => ({
938
+ checked: locale.effective === tag ? "true" : "false",
939
+ hint:
940
+ tag === locale.pathLocale
941
+ ? `${localeLabel(tag)} — the language of the file this pane has open`
942
+ : `Render as ${localeLabel(tag)} (${tag})`,
943
+ key: tag,
944
+ label: localeLabel(tag),
945
+ value: tag,
946
+ })),
947
+ localeState: "shown",
948
+ };
1174
949
  }
1175
950
 
1176
951
  // ─── ⑩ The floating zoom pod ─────────────────────────────────────────────────
@@ -1191,6 +966,9 @@ function fitChoiceValue(fit: FitMode): string {
1191
966
  return fit;
1192
967
  }
1193
968
 
969
+ /** The modes whose stage is the panzoom surface — the only ones with a fit to state. */
970
+ const STAGE_ZOOM_MODES = new Set(["design", "stylebook", "git-diff"]);
971
+
1194
972
  /**
1195
973
  * Zoom and fit, floating over the canvas bottom-right.
1196
974
  *
@@ -1199,7 +977,7 @@ function fitChoiceValue(fit: FitMode): string {
1199
977
  * fit, because a fit is a statement about an artboard. Preview is deliberately absent: its frame is
1200
978
  * a real viewport that scrolls its own document, so there is nothing to zoom.
1201
979
  */
1202
- function zoomPodTpl(tab: Tab, paneId: string): TemplateResult | typeof nothing {
980
+ function podFor(tab: Tab, paneId: string): Partial<PaneContextView> {
1203
981
  /* THIS pane's mode. `ctx.getCanvasMode()` answers for the focused pane, so the unfocused pod
1204
982
  offered an `editZoom` control over a stage drawing Design — and hid the fit that stage has. */
1205
983
  const mode = canvasModeOfPane(paneId);
@@ -1209,91 +987,205 @@ function zoomPodTpl(tab: Tab, paneId: string): TemplateResult | typeof nothing {
1209
987
  const surface = surfaceForPane(paneId);
1210
988
  if (mode === "edit") {
1211
989
  const editZoom = tab.session.ui.editZoom ?? 1;
1212
- return podTpl(
1213
- paneId,
1214
- html`
1215
- ${zoomButton("Zoom out (Ctrl+-)", "−", () =>
1216
- setEditZoom((tab.session.ui.editZoom ?? 1) / 1.2, surface),
1217
- )}
1218
- <sp-action-button
1219
- size="s"
1220
- class="pc-zoom-label"
1221
- title="Reset to 100% (Ctrl+0)"
1222
- @click=${() => setEditZoom(1, surface)}
1223
- >
1224
- ${Math.round(editZoom * 100)}%
1225
- </sp-action-button>
1226
- ${zoomButton("Zoom in (Ctrl+=)", "+", () =>
1227
- setEditZoom((tab.session.ui.editZoom ?? 1) * 1.2, surface),
1228
- )}
1229
- `,
1230
- );
990
+ return { podState: "shown", zoomLabel: `${Math.round(editZoom * 100)}%` };
1231
991
  }
1232
- if (mode !== "design" && mode !== "stylebook" && mode !== "git-diff") {
1233
- return nothing;
992
+ if (!STAGE_ZOOM_MODES.has(mode)) {
993
+ return { podState: "hidden" };
1234
994
  }
1235
995
  const zoom = stageZoom(surface);
1236
- const chosen = fitChoiceValue(getFit(surface));
1237
- return podTpl(
1238
- paneId,
1239
- html`
1240
- ${zoomButton("Zoom out (Ctrl+-)", "−", () => setUserZoom(stageZoom(surface) / 1.2, surface))}
1241
- <sp-action-button
1242
- size="s"
1243
- class="pc-zoom-label"
1244
- title="Reset to 100% (Ctrl+0)"
1245
- @click=${() => resetZoom(surface)}
1246
- >
1247
- ${Math.round(zoom * 100)}%
1248
- </sp-action-button>
1249
- ${zoomButton("Zoom in (Ctrl+=)", "+", () => setUserZoom(stageZoom(surface) * 1.2, surface))}
1250
- <sp-picker
1251
- size="s"
1252
- quiet
1253
- class="pc-fit"
1254
- label="Fit"
1255
- value=${chosen}
1256
- title="How this document is framed"
1257
- @change=${(e: Event) => {
1258
- const { value } = e.target as HTMLInputElement;
1259
- const choice = FIT_CHOICES.find((entry) => entry.value === value);
1260
- if (!choice) {
1261
- return;
1262
- }
1263
- // "Fit page" from the control may magnify a small artboard past life size; the fit APPLIED
1264
- // On arrival never does. Same declared state, two caps — see fitToScreen's maxZoom.
1265
- if (choice.fit === "page") {
1266
- fitToScreen({ surface });
1267
- return;
1268
- }
1269
- setFit(choice.fit, surface);
1270
- }}
1271
- >
1272
- ${FIT_CHOICES.map(
1273
- ({ value, label }) => html`<sp-menu-item value=${value}>${label}</sp-menu-item>`,
1274
- )}
1275
- </sp-picker>
1276
- `,
1277
- );
996
+ return {
997
+ fitOptions: FIT_CHOICES.map(({ label, value }) => ({ label, value })),
998
+ fitState: "shown",
999
+ fitValue: fitChoiceValue(getFit(surface)),
1000
+ podState: "shown",
1001
+ zoomLabel: `${Math.round(zoom * 100)}%`,
1002
+ };
1278
1003
  }
1279
1004
 
1280
- function podTpl(paneId: string, body: TemplateResult): TemplateResult {
1281
- return html`
1282
- <div class="pane-zoom" data-jx-region=${paneRegion(paneId, "zoom")}>
1283
- <sp-action-group compact size="s">${body}</sp-action-group>
1284
- </div>
1285
- `;
1005
+ // ─── What the controls do ────────────────────────────────────────────────────
1006
+
1007
+ /**
1008
+ * Run a rendering-context verb through the registry.
1009
+ *
1010
+ * These four controls wrote `session.ui` directly through `updateUi`, which is why none of the
1011
+ * three axes was a command: the popover WAS the capability, and the palette, the assistant and
1012
+ * `__jxAutomation` had no name for it. Going through the registry makes the control and the verb
1013
+ * one thing (§2, principle 1) and gets the breakpoint refusal for free.
1014
+ */
1015
+ function runContextCommand(paneId: string, id: string, args: Record<string, unknown>): void {
1016
+ // THIS pane, named. The bar is drawn once per pane and the side bar's controls write the side
1017
+ // Pane's tab; a verb defaulting to the focused pane would have made the side bar edit the
1018
+ // Foreground document the moment its control became a command.
1019
+ void activeRegistry()?.run(id, { ...args, pane: paneId });
1286
1020
  }
1287
1021
 
1288
- function zoomButton(title: string, glyph: string, onClick: () => void): TemplateResult {
1289
- return html`
1290
- <sp-action-button size="s" title=${title} @click=${onClick}>${glyph}</sp-action-button>
1291
- `;
1022
+ /**
1023
+ * Every gesture the pane's chrome offers, bound to the pane it is drawn for.
1024
+ *
1025
+ * Built once per mount rather than per projection: the document holds these by reference, and a new
1026
+ * closure on every paint would replace a listener the reader may be mid-press on.
1027
+ */
1028
+ function actionsFor(paneId: string, ctx: PaneContextCtx): PaneContextActions {
1029
+ /** The tab this pane is drawing, at the moment the gesture happens rather than when it was drawn. */
1030
+ const tabNow = (): Tab | null => tabOfPane(paneId);
1031
+ return {
1032
+ chooseEditor: (kind) => {
1033
+ const tab = tabNow();
1034
+ if (!tab) {
1035
+ return;
1036
+ }
1037
+ const mode = modeForEditorKind(tab, kind as EditorKind);
1038
+ if (!mode) {
1039
+ return;
1040
+ }
1041
+ tab.session.ui.preview = false;
1042
+ ctx.setCanvasMode(tab, mode);
1043
+ },
1044
+ chooseFit: (value) => {
1045
+ const choice = FIT_CHOICES.find((entry) => entry.value === value);
1046
+ if (!choice) {
1047
+ return;
1048
+ }
1049
+ const surface = surfaceForPane(paneId);
1050
+ // "Fit page" from the control may magnify a small artboard past life size; the fit APPLIED
1051
+ // On arrival never does. Same declared state, two caps — see fitToScreen's maxZoom.
1052
+ if (choice.fit === "page") {
1053
+ fitToScreen({ surface });
1054
+ return;
1055
+ }
1056
+ setFit(choice.fit, surface);
1057
+ },
1058
+ chooseView: (value) => {
1059
+ const tab = tabNow();
1060
+ if (tab) {
1061
+ setCanvasView(tab, value as CanvasView, ctx.setCanvasMode);
1062
+ }
1063
+ },
1064
+ exportFile: ctx.exportFile,
1065
+ manageContexts: () => {
1066
+ void activeRegistry()?.run("settings.open", { section: "contexts" });
1067
+ },
1068
+ openPresetMenu: (anchor) => {
1069
+ openPresetMenu(paneId, anchor);
1070
+ },
1071
+ setBreakpoint: (media) => {
1072
+ // The base row's value is the empty string, which the command reads back as "no breakpoint".
1073
+ runContextCommand(paneId, "canvas.setBreakpoint", { media: media === "" ? null : media });
1074
+ },
1075
+ setLocale: (locale) => {
1076
+ runContextCommand(paneId, "i18n.switchLocale", { locale });
1077
+ },
1078
+ setParam: (name, value) => {
1079
+ // Through the registry, like every control in the popover beside this one.
1080
+ runContextCommand(paneId, "canvas.setRouteParam", { name, value });
1081
+ },
1082
+ setProp: (name, raw) => {
1083
+ // The command owns the write; this control owns only the parse, because "what a typed string
1084
+ // Means" is a fact about a text field and not about the value.
1085
+ runContextCommand(paneId, "canvas.setTestProp", {
1086
+ name,
1087
+ value: raw === "" ? null : parsePropValue(raw),
1088
+ });
1089
+ },
1090
+ setScheme: (scheme) => {
1091
+ runContextCommand(paneId, "canvas.setColorScheme", { scheme });
1092
+ },
1093
+ toggleFeature: (name) => {
1094
+ const tab = tabNow();
1095
+ if (tab) {
1096
+ updateUi(tab, "featureToggles", {
1097
+ ...tab.session.ui.featureToggles,
1098
+ [name]: !tab.session.ui.featureToggles[name],
1099
+ });
1100
+ }
1101
+ },
1102
+ toggleLayout: () => {
1103
+ runContextCommand(paneId, "canvas.setLayoutVisible", {
1104
+ visible: tabNow()?.session.ui.showLayout === false,
1105
+ });
1106
+ },
1107
+ togglePreview: () => {
1108
+ const tab = tabNow();
1109
+ if (!tab) {
1110
+ return;
1111
+ }
1112
+ const preview = previewStateOf(tab);
1113
+ const current = canvasBaseViewOf(tab);
1114
+ setCanvasView(tab, preview.on ? (current ?? "edit") : "preview", ctx.setCanvasMode);
1115
+ },
1116
+ zoomIn: () => {
1117
+ const tab = tabNow();
1118
+ const surface = surfaceForPane(paneId);
1119
+ if (canvasModeOfPane(paneId) === "edit") {
1120
+ setEditZoom((tab?.session.ui.editZoom ?? 1) * 1.2, surface);
1121
+ return;
1122
+ }
1123
+ setUserZoom(stageZoom(surface) * 1.2, surface);
1124
+ },
1125
+ zoomOut: () => {
1126
+ const tab = tabNow();
1127
+ const surface = surfaceForPane(paneId);
1128
+ if (canvasModeOfPane(paneId) === "edit") {
1129
+ setEditZoom((tab?.session.ui.editZoom ?? 1) / 1.2, surface);
1130
+ return;
1131
+ }
1132
+ setUserZoom(stageZoom(surface) / 1.2, surface);
1133
+ },
1134
+ zoomReset: () => {
1135
+ const surface = surfaceForPane(paneId);
1136
+ if (canvasModeOfPane(paneId) === "edit") {
1137
+ setEditZoom(1, surface);
1138
+ return;
1139
+ }
1140
+ resetZoom(surface);
1141
+ },
1142
+ };
1143
+ }
1144
+
1145
+ // ─── The resolving-with popover's open state ─────────────────────────────────
1146
+
1147
+ /**
1148
+ * Whether this pane's resolving popover is open. Exported for the tests.
1149
+ *
1150
+ * The MOUNT owns the answer, not this module: the panel is a `jx-popover` in that pane's document
1151
+ * and the platform can close it without telling anyone (a light dismiss, an Escape, a second panel
1152
+ * taking the top layer). The handle mirrors the platform's own `toggle`, so a pane with no chrome —
1153
+ * no tab, or Project Settings — is closed by definition.
1154
+ */
1155
+ export function isResolvingOpen(paneId: string): boolean {
1156
+ return _surfaces.get(paneId)?.isResolvingOpen() ?? false;
1157
+ }
1158
+
1159
+ /**
1160
+ * Open or close it.
1161
+ *
1162
+ * A named end state rather than a toggle, so `canvas.setResolvingOpen { open: false }` means the
1163
+ * same thing twice and a screenshot can photograph it (§13's R1).
1164
+ *
1165
+ * It writes the panel and does NOT re-project the bar. A popover moves into the top layer while it
1166
+ * is open; nothing about the bar changes when it does, and the only thing the bar redraws for is
1167
+ * the trigger's summary, which changes when a VALUE changes and already projects.
1168
+ */
1169
+ export function setResolvingOpen(paneId: string, open: boolean): void {
1170
+ // Asked through {@link isResolvingOpen}, so "is it already what you are asking for" has one
1171
+ // Answer in this module rather than one here and another in the mount.
1172
+ if (isResolvingOpen(paneId) === open) {
1173
+ return;
1174
+ }
1175
+ _surfaces.get(paneId)?.setResolvingOpen(open);
1176
+ }
1177
+
1178
+ /** Forget the mounted chrome — a fresh window, and the tests. */
1179
+ export function resetResolvingOpen(): void {
1180
+ for (const surface of _surfaces.values()) {
1181
+ surface.dispose();
1182
+ }
1183
+ _surfaces.clear();
1292
1184
  }
1293
1185
 
1294
1186
  // ─── Dynamic route-param pickers ─────────────────────────────────────────────
1295
1187
  // Candidate values load asynchronously (ContentCollection resolution / data-file read); the module
1296
- // Caches the last result per (documentPath, $paths) and re-renders when it lands — the same lazy
1188
+ // Caches the last result per (documentPath, $paths) and re-projects when it lands — the same lazy
1297
1189
  // Fill pattern as head-panel's loadLayoutEntries. When values arrive, any param without a chosen
1298
1190
  // Value auto-selects the first candidate (matching the compiler, whose first expanded route is the
1299
1191
  // First path entry).
@@ -1304,14 +1196,14 @@ function zoomButton(title: string, glyph: string, onClick: () => void): Template
1304
1196
  * **A Map, not one slot, and that is a livelock fix rather than a cache-size preference.** It was
1305
1197
  * `_paramValues` + `_paramValuesKey`, holding exactly ONE result, while {@link render} loops every
1306
1198
  * attached pane. Two panes on documents with different keys evicted each other on every pass: pane
1307
- * A's render stored A's key and cleared the value, pane B's render in the same loop stored B's, and
1199
+ * A's projection stored A's key and cleared the value, pane B's in the same loop stored B's, and
1308
1200
  * whichever load landed found its key gone — or, once the "still shown somewhere" guard let it
1309
1201
  * through, stored its value and called `render()`, which re-issued both loads again. An unbounded
1310
1202
  * microtask chain: no rAF, no paint, no input. `⌘\` with two pages under dynamic routes was enough,
1311
1203
  * and the probe never returned.
1312
1204
  *
1313
- * `_paramLoading` is what stops a re-render re-issuing a load that has not landed yet; the value
1314
- * map is what stops one pane's answer erasing the other's.
1205
+ * `_paramLoading` is what stops a re-projection re-issuing a load that has not landed yet; the
1206
+ * value map is what stops one pane's answer erasing the other's.
1315
1207
  */
1316
1208
  const _paramValues = new Map<string, ParamValues>();
1317
1209
 
@@ -1377,47 +1269,29 @@ function autoSelectParams(tab: Tab, values: ParamValues) {
1377
1269
  }
1378
1270
 
1379
1271
  /**
1272
+ * One picker per route param the document declares, in the order the path names them.
1273
+ *
1380
1274
  * @param {Tab} tab
1381
- * @returns {TemplateResult | typeof nothing}
1382
1275
  */
1383
- function paramPickersTpl(tab: Tab, paneId: string): TemplateResult | typeof nothing {
1276
+ function paramRows(tab: Tab): PaneContextView["params"] {
1384
1277
  const values = paramValuesFor(tab);
1385
1278
  const names = new Set(dynamicRouteParams(tab.documentPath));
1386
1279
  for (const name of Object.keys(values ?? {})) {
1387
1280
  names.add(name);
1388
1281
  }
1389
- if (names.size === 0) {
1390
- return nothing;
1391
- }
1392
1282
  const { previewParams } = tab.session.ui;
1393
- return html`
1394
- ${[...names].map(
1395
- (name) => html`
1396
- <sp-picker
1397
- size="s"
1398
- quiet
1399
- class="pc-param"
1400
- label=${name}
1401
- title=${`Preview value for [${name}]`}
1402
- value=${previewParams?.[name] ?? ""}
1403
- @change=${(e: Event) => {
1404
- const { value } = e.target as HTMLInputElement;
1405
- // Through the registry, like every control in the popover beside this one.
1406
- runContextCommand(paneId, "canvas.setRouteParam", { name, value });
1407
- }}
1408
- >
1409
- ${(values?.[name] ?? []).map(
1410
- (v: string) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`,
1411
- )}
1412
- </sp-picker>
1413
- `,
1414
- )}
1415
- `;
1283
+ return [...names].map((name) => ({
1284
+ key: name,
1285
+ label: `Preview value for [${name}]`,
1286
+ name,
1287
+ options: (values?.[name] ?? []).map((value: string) => ({ label: value, value })),
1288
+ value: previewParams?.[name] ?? "",
1289
+ }));
1416
1290
  }
1417
1291
 
1418
1292
  // ─── Component test-prop fields ──────────────────────────────────────────────
1419
1293
  // The previewParams mirror for component docs: one small field per prop entry (the doc's
1420
- // Plain-data state entries), committed on change so typing never re-renders the bar mid-edit. A
1294
+ // Plain-data state entries), committed on change so typing never re-projects the bar mid-edit. A
1421
1295
  // Value parses as JSON when it can (numbers, booleans, arrays) and falls back to the raw string;
1422
1296
  // Clearing a field removes the override so the prop returns to its authored default.
1423
1297
 
@@ -1436,42 +1310,20 @@ function parsePropValue(raw: string): JsonValue {
1436
1310
  /**
1437
1311
  * @param {Tab} tab
1438
1312
  * @param {string} paneId
1439
- * @returns {TemplateResult | typeof nothing}
1440
1313
  */
1441
- function propFieldsTpl(tab: Tab, paneId: string): TemplateResult | typeof nothing {
1314
+ function propRows(tab: Tab, paneId: string): PaneContextView["props"] {
1442
1315
  const doc = tab.doc.document;
1443
1316
  if (!isComponentDoc(doc)) {
1444
- return nothing;
1445
- }
1446
- const entries = componentPropEntries(doc);
1447
- if (entries.length === 0) {
1448
- return nothing;
1317
+ return [];
1449
1318
  }
1450
1319
  const { previewProps } = tab.session.ui;
1451
1320
  const display = (v: JsonValue | undefined) =>
1452
1321
  v === undefined ? "" : typeof v === "string" ? v : JSON.stringify(v);
1453
- return html`
1454
- ${entries.map(
1455
- ({ name }) => html`
1456
- <sp-textfield
1457
- size="s"
1458
- quiet
1459
- class="pc-prop"
1460
- data-jx-region=${paneRegion(paneId, `prop:${name}`)}
1461
- placeholder=${name}
1462
- title=${`Test value for ${name}`}
1463
- .value=${display(previewProps?.[name])}
1464
- @change=${(e: Event) => {
1465
- const raw = (e.target as HTMLInputElement).value;
1466
- // The command owns the write; this control owns only the parse, because "what a typed
1467
- // String means" is a fact about a text field and not about the value.
1468
- runContextCommand(paneId, "canvas.setTestProp", {
1469
- name,
1470
- value: raw === "" ? null : parsePropValue(raw),
1471
- });
1472
- }}
1473
- ></sp-textfield>
1474
- `,
1475
- )}
1476
- `;
1322
+ return componentPropEntries(doc).map(({ name }) => ({
1323
+ key: name,
1324
+ label: `Test value for ${name}`,
1325
+ name,
1326
+ region: paneRegion(paneId, `prop:${name}`),
1327
+ value: display(previewProps?.[name]),
1328
+ }));
1477
1329
  }