@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
@@ -10,19 +10,32 @@
10
10
  * `studio-ui-guidelines.md` §8.6 is normative about the shape: **ONE shape.** The bar does not
11
11
  * rearrange itself when the author starts typing, and a control that cannot act is DISABLED, not
12
12
  * removed — a toolbar whose buttons move under the cursor is worse than one with a greyed button.
13
- * That is why the parent selector, the drag handle and every verb render unconditionally and take
14
- * their disabled state (and its one-sentence reason) from the record's own `enablement`.
13
+ * That is why the parent selector, the badge, the drag handle and every verb render unconditionally
14
+ * and take their disabled state (and its one-sentence reason) from the record's own `enablement`.
15
+ *
16
+ * **The markup left this file.** The bar is `surfaces/block-action-bar.json`, a Jx document over
17
+ * the UI kit (studio-ui-guidelines.md §1, §6, §9.3), mounted once by
18
+ * `surfaces/block-action-bar.ts`; what stays here is every DECISION — which records are placed,
19
+ * whether each can act, where the bar belongs against the selection rect, when it steps aside, and
20
+ * what a press does. The toolbar role, the roving caret and the ←/→/Home/End contract are
21
+ * `jx-action-group`'s, so the hand-written `[data-toolbar-item]` ring is gone with the template
22
+ * that needed it; the `⋮` menu is the kit menu every other Studio surface opens (§12.5), so there
23
+ * is no second list of actions.
15
24
  *
16
25
  * This module is also the single definition site for the structural selection verbs — move
17
26
  * up/down/in/out and the component pair — because their implementations live here.
18
27
  * {@link registerSelectionCommands} is what a host bootstrap calls to put them in the app-wide
19
28
  * registry; until one exists, {@link selectionCommandRegistry} builds the surface's own.
29
+ *
30
+ * A record's `icon` is a name in the UI KIT's manifest, so `commandIconMap` — the lit-side Spectrum
31
+ * alias every button used to be drawn through — is gone with the template that needed it.
32
+ * `jx-action-button` takes the name; nothing looks a glyph up any more.
33
+ *
34
+ * @docs studio/interface/canvas
20
35
  */
21
36
 
22
- import { html, render as litRender, nothing } from "lit-html";
23
37
  import { displayTagName } from "@jxsuite/schema/guards";
24
- import { styleMap } from "lit-html/directives/style-map.js";
25
- import { ref } from "lit-html/directives/ref.js";
38
+ import { focusItem } from "@jxsuite/ui/behaviors/action-group";
26
39
  import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
27
40
  import { disableNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview";
28
41
 
@@ -52,13 +65,15 @@ import { projectState } from "../state";
52
65
  import { componentRegistry } from "../files/components";
53
66
  import { convertToComponent } from "../editor/convert-to-component";
54
67
  import { getEditBarAnchorRect, getEditSnapshot, postApplyFormat } from "../canvas/iframe-host";
55
- import { getLayerSlot, isModalOpen, renderPopover } from "../ui/layers";
68
+ import { getLayerSlot, isModalOpen } from "../ui/layers";
56
69
  import { showSlashMenu } from "../editor/slash-menu";
57
70
  import { getConvertTargets } from "../editor/convert-targets";
58
71
  import { rectOf } from "../utils/geometry";
59
72
  import { createCommandRegistry } from "../commands/registry";
60
73
  import { editorKindForMode, makeContext } from "../commands/context";
61
74
  import { defaultCommands, noopCommandDeps } from "../commands/defaults";
75
+ import { emptyBlockBarView, mountBlockActionBar } from "../surfaces/block-action-bar";
76
+ import { openMenu } from "../surfaces/menu";
62
77
 
63
78
  import type { AnyCommand, CommandRegistry } from "../commands/registry";
64
79
  import type { CommandContext } from "../commands/context";
@@ -66,8 +81,9 @@ import type { CommandDeps } from "../commands/defaults";
66
81
  import type { ApplyFormatIntent } from "../canvas/iframe-protocol";
67
82
  import type { JxPath } from "../state";
68
83
  import type { JxMutableNode } from "@jxsuite/schema/types";
69
- import type { TemplateResult } from "lit-html";
70
84
  import type { SlashCommand } from "../editor/convert-targets.js";
85
+ import type { BlockBarSurface, BlockBarTool, BlockBarView } from "../surfaces/block-action-bar";
86
+ import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
71
87
 
72
88
  /**
73
89
  * The plain format commands — everything an action button posts except link/insertData.
@@ -137,7 +153,7 @@ let _scrollRafPending = false;
137
153
  * @param {Event} e
138
154
  */
139
155
  export function onCanvasScroll(e: Event): void {
140
- if (!_ctx || _linkPopoverOpen) {
156
+ if (!_ctx || isLinkPopoverOpen()) {
141
157
  return;
142
158
  }
143
159
  if (activeTab.value?.session.selection.length === 0) {
@@ -162,27 +178,32 @@ export function onCanvasScroll(e: Event): void {
162
178
  }
163
179
 
164
180
  /**
165
- * Style-only fast path: move the existing bar to the anchor's current position (fresh
181
+ * Position-only fast path: move the standing bar to the anchor's current position (fresh
166
182
  * {@link getEditBarAnchorRect} — the iframe's GBCR moves with the scroll). Hides via `visibility`
167
- * when the anchor left the canvas area so the lit tree (and the drag handle's dnd registration)
168
- * survives; a bar hidden by the full render path (`nothing`) is resurrected with a full render.
183
+ * when the anchor left the canvas area so the mounted document — and with it the drag handle's dnd
184
+ * registration and whatever holds the caret — survives; a bar hidden by the `visible` switch is
185
+ * genuinely torn down, which is right for a dismissal and wrong for a scroll that will bring the
186
+ * anchor straight back.
169
187
  */
170
188
  function repositionBlockActionBar(): void {
171
- const bar = view.blockActionBarEl?.firstElementChild as HTMLElement | null;
172
- if (!bar) {
189
+ const surface = _surface;
190
+ if (!surface || !_view.visible) {
173
191
  renderBlockActionBar();
174
192
  return;
175
193
  }
176
194
  const anchor = getEditBarAnchorRect();
177
195
  const pos = anchor ? barPosition(anchor) : null;
178
196
  if (!pos) {
179
- bar.style.visibility = "hidden";
197
+ applyBarView({ ..._view, offscreen: true });
180
198
  return;
181
199
  }
182
- bar.style.visibility = "";
183
- bar.style.left = `${pos.left}px`;
184
- bar.style.top = `${pos.top}px`;
185
- clampBarToWindow(bar);
200
+ applyBarView({ ..._view, barX: pos.left, barY: pos.top, offscreen: false });
201
+ requestAnimationFrame(() => {
202
+ const bar = surface.bar();
203
+ if (bar) {
204
+ clampBarToWindow(bar);
205
+ }
206
+ });
186
207
  }
187
208
 
188
209
  /**
@@ -219,12 +240,25 @@ function barPosition(anchor: {
219
240
  /**
220
241
  * Pull the bar back inside the window's right edge.
221
242
  *
243
+ * A measurement and a write of the value it measured, which is why it stays imperative and stays
244
+ * here: the bar's own box is the input, and no projection can carry a number that depends on the
245
+ * layout the projection produced. It writes the same custom property the document's `left` reads.
246
+ *
247
+ * **It releases its own previous write before measuring, and that is load-bearing.** The projected
248
+ * position is set on the document's ROOT and reaches the bar by inheritance, while this override is
249
+ * set on the bar itself — and a value an element carries always beats one it inherits, at every
250
+ * specificity. So a clamp that was never released would pin the bar at the right edge for the rest
251
+ * of the session, and every later `barX` would be projected into a variable nothing reads. Removing
252
+ * it first also makes the measurement the right one: the box is read at the position the projection
253
+ * asked for, rather than at the position the last clamp left it in.
254
+ *
222
255
  * @param {HTMLElement} bar
223
256
  */
224
257
  function clampBarToWindow(bar: HTMLElement): void {
258
+ bar.style.removeProperty("--jx-bar-x");
225
259
  const barRect = rectOf(bar);
226
260
  if (barRect.right > window.innerWidth) {
227
- bar.style.left = `${Math.max(0, window.innerWidth - barRect.width)}px`;
261
+ bar.style.setProperty("--jx-bar-x", `${Math.max(0, window.innerWidth - barRect.width)}px`);
228
262
  }
229
263
  }
230
264
 
@@ -282,17 +316,17 @@ export function formatCommands(): AnyCommand[] {
282
316
  });
283
317
 
284
318
  return [
285
- record("bold", "Bold", "sp-icon-text-bold", "mod+b"),
286
- record("italic", "Italic", "sp-icon-text-italic", "mod+i"),
287
- record("underline", "Underline", "sp-icon-text-underline", "mod+u"),
288
- record("strikethrough", "Strikethrough", "sp-icon-text-strikethrough"),
289
- record("superscript", "Superscript", "sp-icon-text-superscript"),
290
- record("subscript", "Subscript", "sp-icon-text-subscript"),
291
- record("code", "Code", "sp-icon-code", "mod+`"),
319
+ record("bold", "Bold", "text-b", "mod+b"),
320
+ record("italic", "Italic", "text-italic", "mod+i"),
321
+ record("underline", "Underline", "text-underline", "mod+u"),
322
+ record("strikethrough", "Strikethrough", "text-strikethrough"),
323
+ record("superscript", "Superscript", "text-superscript"),
324
+ record("subscript", "Subscript", "text-subscript"),
325
+ record("code", "Code", "code", "mod+`"),
292
326
  {
293
327
  category: "Edit",
294
328
  group: "6_format",
295
- icon: "sp-icon-link",
329
+ icon: "link",
296
330
  id: "format.link",
297
331
  /* ⌘K, at `caret` scope, beside `palette.open`'s global ⌘K. Two scopes may hold one chord —
298
332
  that is what a shadowing ladder is — and the caret's wins only while a caret is in the
@@ -632,7 +666,7 @@ export function registerSelectionCommands(
632
666
  category: "Selection",
633
667
  enablement: canMoveUp,
634
668
  group: "1_move_1",
635
- icon: "sp-icon-arrow-up",
669
+ icon: "arrow-up",
636
670
  id: "selection.moveUp",
637
671
  keyScope: "canvas",
638
672
  level: "selection",
@@ -647,7 +681,7 @@ export function registerSelectionCommands(
647
681
  category: "Selection",
648
682
  enablement: canMoveDown,
649
683
  group: "1_move_2",
650
- icon: "sp-icon-arrow-down",
684
+ icon: "arrow-down",
651
685
  id: "selection.moveDown",
652
686
  keyScope: "canvas",
653
687
  level: "selection",
@@ -662,7 +696,7 @@ export function registerSelectionCommands(
662
696
  category: "Selection",
663
697
  enablement: canMoveIn,
664
698
  group: "1_move_3",
665
- icon: "sp-icon-arrow-right",
699
+ icon: "arrow-right",
666
700
  id: "selection.moveIn",
667
701
  keyScope: "canvas",
668
702
  level: "selection",
@@ -677,7 +711,7 @@ export function registerSelectionCommands(
677
711
  category: "Selection",
678
712
  enablement: canMoveOut,
679
713
  group: "1_move_4",
680
- icon: "sp-icon-arrow-left",
714
+ icon: "arrow-left",
681
715
  id: "selection.moveOut",
682
716
  keyScope: "canvas",
683
717
  level: "selection",
@@ -698,7 +732,7 @@ export function registerSelectionCommands(
698
732
  enablement: (ctx) =>
699
733
  Boolean(ctx.selection.kind) && !ctx.selection.isRoot && ctx.editor.kind === "canvas",
700
734
  group: "4_component",
701
- icon: "sp-icon-box",
735
+ icon: "cube",
702
736
  id: "selection.convertToComponent",
703
737
  level: "selection",
704
738
  // Three surfaces, ONE record. `editor/context-menu.ts` carried a second `convertToComponent`
@@ -718,7 +752,7 @@ export function registerSelectionCommands(
718
752
  {
719
753
  category: "Selection",
720
754
  group: "4_component",
721
- icon: "sp-icon-edit",
755
+ icon: "pencil-simple",
722
756
  id: "selection.editComponent",
723
757
  level: "selection",
724
758
  menus: ["blockbar", "context/element", "palette"],
@@ -796,43 +830,6 @@ export function selectionCommandRegistry(): CommandRegistry {
796
830
  /** How many verb buttons the bar renders before the rest fold into `⋮` (plan §3.2 ⑩). */
797
831
  export const BLOCKBAR_MAX_ITEMS = 5;
798
832
 
799
- /**
800
- * Icons for command records, keyed by the record's own `icon`.
801
- *
802
- * A record with no icon renders its title as text rather than an unlabelled blank: the surface is
803
- * allowed to choose how a name is drawn, never what it is.
804
- */
805
- const commandIconMap: Record<string, TemplateResult> = {
806
- "sp-icon-arrow-down": html`<sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>`,
807
- "sp-icon-arrow-left": html`<sp-icon-arrow-left slot="icon"></sp-icon-arrow-left>`,
808
- "sp-icon-arrow-right": html`<sp-icon-arrow-right slot="icon"></sp-icon-arrow-right>`,
809
- "sp-icon-arrow-up": html`<sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>`,
810
- "sp-icon-box": html`<sp-icon-box slot="icon" size="xs"></sp-icon-box>`,
811
- "sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
812
- "sp-icon-delete": html`<sp-icon-delete slot="icon"></sp-icon-delete>`,
813
- "sp-icon-duplicate": html`<sp-icon-duplicate slot="icon"></sp-icon-duplicate>`,
814
- "sp-icon-edit": html`<sp-icon-edit slot="icon" size="xs"></sp-icon-edit>`,
815
- "sp-icon-link": html`<sp-icon-link slot="icon"></sp-icon-link>`,
816
- "sp-icon-text-bold": html`<sp-icon-text-bold slot="icon"></sp-icon-text-bold>`,
817
- "sp-icon-text-italic": html`<sp-icon-text-italic slot="icon"></sp-icon-text-italic>`,
818
- "sp-icon-text-strikethrough": html`<sp-icon-text-strikethrough
819
- slot="icon"
820
- ></sp-icon-text-strikethrough>`,
821
- "sp-icon-text-subscript": html`<sp-icon-text-subscript slot="icon"></sp-icon-text-subscript>`,
822
- "sp-icon-text-superscript": html`<sp-icon-text-superscript
823
- slot="icon"
824
- ></sp-icon-text-superscript>`,
825
- "sp-icon-text-underline": html`<sp-icon-text-underline slot="icon"></sp-icon-text-underline>`,
826
- };
827
-
828
- /** The record's icon, or its title drawn as a compact label. */
829
- export function commandIcon(command: AnyCommand): TemplateResult {
830
- return (
831
- (command.icon ? commandIconMap[command.icon] : undefined) ??
832
- html`<span class="cmd-label">${command.title}</span>`
833
- );
834
- }
835
-
836
833
  /**
837
834
  * The tooltip: the chord when the control can act, the `requires` sentence when it cannot.
838
835
  *
@@ -863,135 +860,141 @@ export function runCommand(registry: CommandRegistry, id: string, target?: JxPat
863
860
  }
864
861
 
865
862
  /**
866
- * Prevent the bar from stealing focus from contenteditable
863
+ * One button, projected from its record.
867
864
  *
868
- * @param {MouseEvent} e
865
+ * The surface is told what a button says, never asked to work it out: the glyph is the record's own
866
+ * kit name, the tooltip is {@link commandTooltip}, and `disabled` is the registry's refusal rather
867
+ * than a second opinion this module could hold about the same record.
869
868
  */
870
- function onBarMousedown(e: MouseEvent) {
871
- if ((e.target as HTMLElement).closest("sp-textfield")) {
872
- return;
873
- }
874
- if ((e.target as HTMLElement).closest(".bar-drag-handle")) {
875
- return;
876
- }
877
- if ((e.target as HTMLElement).closest(".bar-tag--interactive")) {
878
- return;
879
- }
880
- e.preventDefault();
869
+ function toolOf(
870
+ registry: CommandRegistry,
871
+ command: AnyCommand,
872
+ extra: { disabled?: boolean; selected?: boolean; value?: string } = {},
873
+ ): BlockBarTool {
874
+ return {
875
+ destructive: command.destructive === true,
876
+ disabled: extra.disabled ?? registry.disabledReason(command.id) !== undefined,
877
+ hasIcon: Boolean(command.icon),
878
+ icon: command.icon ?? "",
879
+ id: command.id,
880
+ selected: extra.selected ?? false,
881
+ title: command.title,
882
+ tooltip: commandTooltip(registry, command),
883
+ value: extra.value ?? "",
884
+ };
885
+ }
886
+
887
+ // ─── The mounted surface ─────────────────────────────────────────────────────
888
+
889
+ /** The bar's surface, mounted on the first render and kept for the life of the window. */
890
+ let _surface: BlockBarSurface | null = null;
891
+
892
+ /** The last projection handed to the surface, so a fast path can vary one field of it. */
893
+ let _view: BlockBarView = emptyBlockBarView();
894
+
895
+ /** Hand the surface a projection, remembering it. */
896
+ function applyBarView(next: BlockBarView): void {
897
+ _view = next;
898
+ _surface?.update(next);
881
899
  }
882
900
 
883
901
  /**
884
- * @param {MouseEvent} e
885
- * @param {import("../editor/convert-targets.js").SlashCommand[]} targets
886
- * @param {JxPath} selection
902
+ * The layer slot the bar draws into, mounting the document the first time it is asked for.
903
+ *
904
+ * One mount for the life of the window: a document reconciles by assignment, so hiding the bar is a
905
+ * `visible` switch rather than a teardown, and the caret, the drag registration and an open link
906
+ * panel all survive a repaint that would have re-rendered a template.
887
907
  */
888
- function onTagBadgeClick(e: MouseEvent, targets: SlashCommand[], selection: JxPath) {
889
- e.stopPropagation();
890
- const anchorEl = e.currentTarget as HTMLElement;
891
- showSlashMenu(anchorEl, "", {
892
- commands: targets,
893
- onSelect: (cmd) => {
894
- transactDoc(activeTab.value, (t) => {
895
- mutateUpdateProperty(t, selection, "tagName", cmd.tag);
896
- });
908
+ function ensureSurface(): BlockBarSurface {
909
+ if (!view.blockActionBarEl) {
910
+ view.blockActionBarEl = getLayerSlot("popover", "block-action-bar");
911
+ }
912
+ _surface ??= mountBlockActionBar(view.blockActionBarEl, {
913
+ applyLink: (href) => {
914
+ postApplyFormat({ command: "link", href: href || "" });
897
915
  },
898
- showFilter: targets.length > 6,
916
+ leaveBar: () => {
917
+ dismissBlockBarOverflow();
918
+ returnFocusToCanvas();
919
+ },
920
+ onDragHandle: attachDragHandle,
921
+ openMergeTags: openMergeTagMenu,
922
+ openOverflow: (anchor) => {
923
+ const registry = selectionCommandRegistry();
924
+ showCommandOverflow(
925
+ anchor,
926
+ registry,
927
+ registry.forPlacement("blockbar").slice(BLOCKBAR_MAX_ITEMS),
928
+ );
929
+ },
930
+ openTagMenu: openConvertMenu,
931
+ removeLink: () => {
932
+ postApplyFormat({ command: "link", href: null });
933
+ },
934
+ run: (id) => runCommand(selectionCommandRegistry(), id),
935
+ selectParent: () => runCommand(selectionCommandRegistry(), "selection.selectParent"),
899
936
  });
937
+ return _surface;
900
938
  }
901
939
 
902
940
  /**
903
- * The parent selector.
941
+ * Register the drag handle the document just drew.
904
942
  *
905
- * It renders the `selection.selectParent` record — its name, its chord and its handler — but as
906
- * fixed chrome rather than from `forPlacement("blockbar")`, because that record does not declare
907
- * the placement. Its disabled state is "the selection has no parent", which the record's own
908
- * `enablement` (a bare "there is a selection") cannot express yet.
943
+ * Released before installed, always. The handle's node is re-created every time the bar is hidden
944
+ * and shown again, and two live pragmatic-dnd registrations on one handle is a drag that fires
945
+ * twice — `view.selDragCleanup` is the single field that guarantees there is never more than one.
946
+ * `canDrag` is asked at the PRESS rather than here, so a selection moving to the document root
947
+ * greys the handle without any re-registration at all.
948
+ *
949
+ * @param {HTMLElement} handle
909
950
  */
910
- function renderParentButton(registry: CommandRegistry) {
911
- const tab = activeTab.value;
912
- const selection = primarySelection(tab?.session.selection);
913
- const parentPath = selection ? parentElementPath(selection) : null;
914
- const parentNode = tab && parentPath ? getNodeAtPath(tab.doc.document, parentPath) : null;
915
- const command = registry.get("selection.selectParent");
916
- const name = command?.title ?? "Select Parent";
917
- const chord = command ? registry.keymap.formatBinding(command.id) : undefined;
918
- const label = parentNode ? `Select parent: ${nodeLabel(parentNode)}` : name;
919
- return html`
920
- <sp-action-button
921
- size="xs"
922
- quiet
923
- data-toolbar-item
924
- tabindex="-1"
925
- aria-label=${label}
926
- title=${chord ? `${label} (${chord})` : label}
927
- ?disabled=${!parentPath}
928
- @click=${(e: MouseEvent) => {
929
- e.stopPropagation();
930
- runCommand(registry, "selection.selectParent");
931
- }}
932
- >
933
- <sp-icon-back slot="icon"></sp-icon-back>
934
- </sp-action-button>
935
- `;
936
- }
937
-
938
- /** One verb, rendered from its record. */
939
- function renderCommandButton(registry: CommandRegistry, command: AnyCommand) {
940
- const disabled = registry.disabledReason(command.id) !== undefined;
941
- return html`<sp-action-button
942
- size="xs"
943
- quiet
944
- class=${command.destructive ? "bar-cmd bar-cmd--danger" : "bar-cmd"}
945
- data-toolbar-item
946
- data-command=${command.id}
947
- tabindex="-1"
948
- aria-label=${command.title}
949
- title=${commandTooltip(registry, command)}
950
- ?disabled=${disabled}
951
- @mousedown=${(e: MouseEvent) => e.preventDefault()}
952
- @click=${(e: MouseEvent) => {
953
- e.stopPropagation();
954
- runCommand(registry, command.id);
955
- }}
956
- >${commandIcon(command)}</sp-action-button
957
- >`;
951
+ function attachDragHandle(handle: HTMLElement): void {
952
+ view.selDragCleanup?.();
953
+ view.selDragCleanup = draggable({
954
+ canDrag: () => structuralTarget(primarySelection(activeTab.value?.session.selection)) !== null,
955
+ element: handle,
956
+ getInitialData: () => ({
957
+ // Snapshot the selection: the live array is a Vue reactive proxy, which structured clone
958
+ // Rejects when the src crosses postMessage (DataCloneError killed the whole handle drag),
959
+ // And a live reference would also mutate the retained srcData if the selection changed
960
+ // Mid-drag.
961
+ path: [...(primarySelection(activeTab.value?.session.selection) ?? [])],
962
+ type: "tree-node",
963
+ }),
964
+ onGenerateDragPreview: ({
965
+ nativeSetDragImage,
966
+ }: {
967
+ nativeSetDragImage: ((image: Element, x: number, y: number) => void) | null;
968
+ }) => {
969
+ // Suppress the native drag image; the cross-frame ghost is the drag affordance.
970
+ disableNativeDragPreview({ nativeSetDragImage });
971
+ },
972
+ });
958
973
  }
959
974
 
960
- /** The `⋮` button and the menu of everything past {@link BLOCKBAR_MAX_ITEMS}. */
961
- function renderOverflowButton(registry: CommandRegistry, commands: readonly AnyCommand[]) {
962
- return html`<sp-action-button
963
- size="xs"
964
- quiet
965
- class="bar-overflow"
966
- data-toolbar-item
967
- tabindex="-1"
968
- aria-label="More block actions"
969
- aria-haspopup="menu"
970
- title="More block actions"
971
- @mousedown=${(e: MouseEvent) => e.preventDefault()}
972
- @click=${(e: MouseEvent) => {
973
- e.stopPropagation();
974
- showCommandOverflow(e.currentTarget as HTMLElement, registry, commands);
975
- }}
976
- >
977
- <sp-icon-more slot="icon"></sp-icon-more>
978
- </sp-action-button>`;
979
- }
975
+ // ─── The `⋮` menu ────────────────────────────────────────────────────────────
980
976
 
981
977
  /** The open overflow menu, so a second press (or a re-render) closes rather than stacks it. */
982
- let _overflowHandle: { dismiss: () => void; host: HTMLElement } | null = null;
978
+ let _overflowHandle: MenuHandle | null = null;
983
979
 
984
980
  /** Close the block action bar's `⋮` menu if it is open. */
985
981
  export function dismissBlockBarOverflow(): void {
986
- _overflowHandle?.dismiss();
982
+ const handle = _overflowHandle;
987
983
  _overflowHandle = null;
984
+ handle?.close();
985
+ if (_view.overflowOpen) {
986
+ applyBarView({ ..._view, overflowOpen: false });
987
+ }
988
988
  }
989
989
 
990
990
  /**
991
991
  * Show the `⋮` menu under `anchor`. Rows carry the same names, chords and refusals as the buttons.
992
992
  *
993
- * Shared by the block action bar and the Outline rows — one menu is open at a time, so `target`
994
- * (the Outline row's node) is captured per row and re-applied when a row is chosen.
993
+ * Shared by the block action bar and the Outline rows, and it is the KIT menu
994
+ * (`surfaces/menu.json`, §12.5) rather than a list of this surface's own: one popover, one roving
995
+ * caret, one typeahead, one Escape. `target` (the Outline row's node) is captured per row and
996
+ * re-applied when a row is chosen, because a row action acts on the row's node and the hovered row
997
+ * is not the selected one.
995
998
  */
996
999
  export function showCommandOverflow(
997
1000
  anchor: HTMLElement,
@@ -1000,95 +1003,46 @@ export function showCommandOverflow(
1000
1003
  target: JxPath | null = null,
1001
1004
  ): void {
1002
1005
  dismissBlockBarOverflow();
1003
- const rect = rectOf(anchor);
1004
1006
  // Resolve every row's name / chord / refusal against the TARGET, not the selection, before the
1005
- // Template is built — lit evaluates these eagerly, so the wrap has to cover the whole projection.
1006
- const project = () =>
1007
- commands.map((command) => ({
1008
- chord: registry.keymap.formatBinding(command.id),
1009
- destructive: command.destructive === true,
1010
- disabled: registry.disabledReason(command.id) !== undefined,
1011
- id: command.id,
1012
- title: command.title,
1013
- tooltip: commandTooltip(registry, command),
1014
- }));
1007
+ // Rows are handed over — the menu prints what it is given.
1008
+ const project = (): MenuRowProjection[] =>
1009
+ commands.map((command, index) => {
1010
+ const chord = registry.keymap.formatBinding(command.id);
1011
+ const reason = registry.disabledReason(command.id);
1012
+ return {
1013
+ destructive: command.destructive === true,
1014
+ disabled: reason !== undefined,
1015
+ dividerAbove: index > 0 && command.destructive === true,
1016
+ id: command.id,
1017
+ run: () => {
1018
+ runCommand(registry, command.id, target);
1019
+ },
1020
+ title: command.title,
1021
+ ...(chord ? { chord } : {}),
1022
+ ...(reason === undefined ? {} : { requires: reason }),
1023
+ };
1024
+ });
1015
1025
  const rows = target ? withCommandTarget(target, project) : project();
1016
- _overflowHandle = renderPopover(
1017
- html`<sp-popover
1018
- open
1019
- class="bar-overflow-menu"
1020
- style=${styleMap({
1021
- left: `${rect.left}px`,
1022
- position: "fixed",
1023
- top: `${rect.bottom + 4}px`,
1024
- zIndex: "101",
1025
- })}
1026
- >
1027
- <sp-menu>
1028
- ${rows.map(
1029
- (row) => html`<sp-menu-item
1030
- ?disabled=${row.disabled}
1031
- data-command=${row.id}
1032
- title=${row.tooltip}
1033
- style=${row.destructive ? "color: var(--danger)" : nothing}
1034
- @click=${() => {
1035
- dismissBlockBarOverflow();
1036
- runCommand(registry, row.id, target);
1037
- }}
1038
- >${row.title}${
1039
- row.chord ? html`<kbd slot="value" class="cmd-chord">${row.chord}</kbd>` : nothing
1040
- }</sp-menu-item
1041
- >`,
1042
- )}
1043
- </sp-menu>
1044
- </sp-popover>`,
1045
- {
1046
- dismissOnOutsideClick: true,
1047
- onDismiss: () => {
1026
+ _overflowHandle = openMenu({
1027
+ label: "More block actions",
1028
+ onClosed: (closed) => {
1029
+ if (_overflowHandle === closed) {
1048
1030
  _overflowHandle = null;
1049
- },
1031
+ if (_view.overflowOpen) {
1032
+ applyBarView({ ..._view, overflowOpen: false });
1033
+ }
1034
+ }
1050
1035
  },
1051
- );
1052
- }
1053
-
1054
- // ─── Toolbar keyboard model ──────────────────────────────────────────────────
1055
-
1056
- /** The bar's focusable items, in DOM order, skipping the ones that cannot act. */
1057
- function toolbarItems(bar: HTMLElement): HTMLElement[] {
1058
- return [...bar.querySelectorAll<HTMLElement>("[data-toolbar-item]")].filter(
1059
- (el) => !el.hasAttribute("disabled") && el.getAttribute("aria-disabled") !== "true",
1060
- );
1061
- }
1062
-
1063
- /**
1064
- * Apply the roving tabindex: exactly one item is in the tab order at a time.
1065
- *
1066
- * Called after every render, so the survivor is whichever item still holds focus and otherwise the
1067
- * first — a toolbar that resets to its first control on every keystroke is not navigable.
1068
- */
1069
- export function applyRovingTabindex(bar: HTMLElement): void {
1070
- const items = toolbarItems(bar);
1071
- const focused = items.findIndex(
1072
- (el) => el === document.activeElement || el.contains(document.activeElement),
1073
- );
1074
- const active = focused === -1 ? 0 : focused;
1075
- for (const [index, el] of items.entries()) {
1076
- el.tabIndex = index === active ? 0 : -1;
1036
+ opener: anchor,
1037
+ region: "block-actions",
1038
+ rows,
1039
+ });
1040
+ if (!target) {
1041
+ applyBarView({ ..._view, overflowOpen: true });
1077
1042
  }
1078
1043
  }
1079
1044
 
1080
- /** Move focus to item `index` (wrapping), and make it the one in the tab order. */
1081
- function focusToolbarItem(bar: HTMLElement, index: number): void {
1082
- const items = toolbarItems(bar);
1083
- if (items.length === 0) {
1084
- return;
1085
- }
1086
- const wrapped = ((index % items.length) + items.length) % items.length;
1087
- for (const [i, el] of items.entries()) {
1088
- el.tabIndex = i === wrapped ? 0 : -1;
1089
- }
1090
- items[wrapped]!.focus();
1091
- }
1045
+ // ─── Keyboard ────────────────────────────────────────────────────────────────
1092
1046
 
1093
1047
  /** Put the keyboard back where it came from — the canvas iframe, else its wrapper. */
1094
1048
  function returnFocusToCanvas(): void {
@@ -1099,32 +1053,6 @@ function returnFocusToCanvas(): void {
1099
1053
  (iframe ?? stage)?.focus();
1100
1054
  }
1101
1055
 
1102
- /** `role="toolbar"` navigation: ←/→ between items, Home/End to the ends, Esc back to the canvas. */
1103
- export function onToolbarKeydown(e: KeyboardEvent): void {
1104
- const bar = e.currentTarget as HTMLElement;
1105
- const items = toolbarItems(bar);
1106
- const current = items.findIndex(
1107
- (el) => el === document.activeElement || el.contains(document.activeElement),
1108
- );
1109
- if (e.key === "ArrowRight") {
1110
- e.preventDefault();
1111
- focusToolbarItem(bar, current + 1);
1112
- } else if (e.key === "ArrowLeft") {
1113
- e.preventDefault();
1114
- focusToolbarItem(bar, current - 1);
1115
- } else if (e.key === "Home") {
1116
- e.preventDefault();
1117
- focusToolbarItem(bar, 0);
1118
- } else if (e.key === "End") {
1119
- e.preventDefault();
1120
- focusToolbarItem(bar, items.length - 1);
1121
- } else if (e.key === "Escape") {
1122
- e.preventDefault();
1123
- dismissBlockBarOverflow();
1124
- returnFocusToCanvas();
1125
- }
1126
- }
1127
-
1128
1056
  /**
1129
1057
  * ⌥↑ enters the bar from the canvas.
1130
1058
  *
@@ -1132,6 +1060,11 @@ export function onToolbarKeydown(e: KeyboardEvent): void {
1132
1060
  * inside the canvas iframe does not arrive: `canvas/iframe-keys.ts` forwards bare navigation keys
1133
1061
  * only when the target is not editable, and the canvas root is permanently `contenteditable`.
1134
1062
  * Exported so a test can dispatch it directly.
1063
+ *
1064
+ * The caret it places is the kit's: `jx-action-group` owns the roving tabindex and every arrow key
1065
+ * after this one, so entering the bar is the only part of the toolbar keyboard this module holds.
1066
+ *
1067
+ * @param {KeyboardEvent} e
1135
1068
  */
1136
1069
  export function handleBlockBarEntryKey(e: KeyboardEvent): void {
1137
1070
  if (e.key !== "ArrowUp" || !e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
@@ -1140,24 +1073,74 @@ export function handleBlockBarEntryKey(e: KeyboardEvent): void {
1140
1073
  if (isModalOpen()) {
1141
1074
  return;
1142
1075
  }
1143
- const bar = view.blockActionBarEl?.querySelector<HTMLElement>(".block-action-bar");
1144
- if (!bar) {
1076
+ const toolbar = _surface?.toolbar();
1077
+ if (!toolbar) {
1145
1078
  return;
1146
1079
  }
1147
1080
  e.preventDefault();
1148
- focusToolbarItem(bar, 0);
1081
+ focusItem(toolbar, 0);
1082
+ }
1083
+
1084
+ // ─── The badge's convert list and the merge-tag list ─────────────────────────
1085
+
1086
+ /**
1087
+ * The tags this node may be converted into — empty for a component instance and for a repeater,
1088
+ * whose content is a `map` template rather than a tag.
1089
+ *
1090
+ * @param {JxMutableNode | null} node
1091
+ */
1092
+ function convertTargetsFor(node: JxMutableNode | null): SlashCommand[] {
1093
+ if (!node) {
1094
+ return [];
1095
+ }
1096
+ const literal = displayTagName(node.tagName);
1097
+ const isComponent = literal.includes("-") && componentRegistry.some((c) => c.tagName === literal);
1098
+ if (isComponent || node.$prototype === "Array") {
1099
+ return [];
1100
+ }
1101
+ const children = childList(node);
1102
+ const isEmpty =
1103
+ !node.textContent &&
1104
+ (children.length === 0 ||
1105
+ (children.length === 1 && typeof children[0] === "object" && children[0]?.tagName === "br"));
1106
+ return getConvertTargets((literal || "div").toLowerCase(), isEmpty);
1107
+ }
1108
+
1109
+ /**
1110
+ * Open the badge's convert list under it, and retag the node on a choice.
1111
+ *
1112
+ * Resolved at the PRESS rather than captured at projection time: the badge is one node for the life
1113
+ * of the mount, so a closure taken when it was drawn would retag whatever was selected then.
1114
+ *
1115
+ * @param {HTMLElement} anchor
1116
+ */
1117
+ function openConvertMenu(anchor: HTMLElement): void {
1118
+ const tab = activeTab.value;
1119
+ const selection = primarySelection(tab?.session.selection);
1120
+ const node = tab && selection ? getNodeAtPath(tab.doc.document, selection) : null;
1121
+ const targets = convertTargetsFor(node);
1122
+ if (!selection || targets.length === 0) {
1123
+ return;
1124
+ }
1125
+ showSlashMenu(anchor, "", {
1126
+ commands: targets,
1127
+ onSelect: (cmd) => {
1128
+ transactDoc(activeTab.value, (t) => {
1129
+ mutateUpdateProperty(t, selection, "tagName", cmd.tag);
1130
+ });
1131
+ },
1132
+ showFilter: targets.length > 6,
1133
+ });
1149
1134
  }
1150
1135
 
1151
1136
  /**
1152
1137
  * Open the merge-tag menu — a searchable list of `${…}` template tokens for the data available in
1153
- * the current state. Reuses the shared slash-menu popover (filter + keyboard nav + dismiss).
1138
+ * the current state. Reuses the shared slash-menu surface (filter + keyboard nav + dismiss).
1154
1139
  * Selecting a token posts an `insertData` intent the iframe applies at its caret.
1155
1140
  *
1156
- * @param {MouseEvent} e
1141
+ * @param {HTMLElement} anchor
1157
1142
  */
1158
- function onMergeTagClick(e: MouseEvent) {
1159
- e.stopPropagation();
1160
- const anchorEl = e.currentTarget as HTMLElement;
1143
+ function openMergeTagMenu(anchor: HTMLElement): void {
1161
1144
  const tab = activeTab.value;
1162
1145
  const state = (tab?.doc.document.state ?? {}) as Record<string, unknown>;
1163
1146
  // The live resolved scope lives inside the iframe realm and is not threaded out, so the parent
@@ -1169,45 +1152,79 @@ function onMergeTagClick(e: MouseEvent) {
1169
1152
  // Parent-side from schema, keyed off the selected element's doc path (which carries a `map` segment).
1170
1153
  const selPath = getEditSnapshot().snapshot?.path ?? primarySelection(tab?.session.selection);
1171
1154
  const arrayNode = tab && selPath ? findEnclosingRepeater(tab.doc.document, selPath) : null;
1172
- if (arrayNode) {
1155
+ if (arrayNode && tab) {
1173
1156
  const tokens = resolveRepeaterItemFields(
1174
1157
  arrayNode,
1175
- tab!.doc.document.state as Record<string, unknown>,
1158
+ tab.doc.document.state as Record<string, unknown>,
1176
1159
  projectState?.projectConfig,
1177
1160
  );
1178
1161
  tags.push(...buildRepeaterTagsFromFields(tokens));
1179
1162
  }
1180
1163
 
1181
- const commands = tags.map((t) => ({
1182
- description: t.hint,
1183
- label: t.label,
1184
- tag: t.token,
1185
- }));
1186
-
1187
- showSlashMenu(anchorEl, "", {
1188
- commands,
1164
+ showSlashMenu(anchor, "", {
1165
+ commands: tags.map((t) => ({ description: t.hint, label: t.label, tag: t.token })),
1189
1166
  onSelect: (cmd) => postApplyFormat({ command: "insertData", token: cmd.tag }),
1190
1167
  showFilter: true,
1191
1168
  });
1192
1169
  }
1193
1170
 
1171
+ // ─── The link panel ──────────────────────────────────────────────────────────
1172
+
1194
1173
  /** Dismiss the link popover if open. */
1195
- export function dismissLinkPopover() {
1196
- _linkPopoverOpen = false;
1197
- const host = getLayerSlot("popover", "link-popover");
1198
- litRender(nothing, host);
1174
+ export function dismissLinkPopover(): void {
1175
+ _surface?.closeLink();
1176
+ }
1177
+
1178
+ /** True while the link URL popover is open (so a scroll does not move the bar out from under it). */
1179
+ export function isLinkPopoverOpen(): boolean {
1180
+ return _surface?.linkOpen() === true;
1181
+ }
1182
+
1183
+ /**
1184
+ * Open the link popover, anchored to the toolbar's Link button if it is on screen, else the bar.
1185
+ *
1186
+ * The iframe owns the Selection, so the existing-link state comes from the latest selection
1187
+ * snapshot; Apply/Remove post `applyFormat` link intents the iframe applies.
1188
+ *
1189
+ * By RECORD ID, not by rendered text. This queried `sp-action-button[title^="Link"]`, which is the
1190
+ * addressing §13's first rule bans in the screenshot manifest — a match on a title the app DERIVES
1191
+ * (it now reads "Link… (⌘K)", and on Windows it reads something else again). The id is the input
1192
+ * the app accepts, and the button is one the surface ANNOUNCED as it drew it rather than one this
1193
+ * module re-finds.
1194
+ */
1195
+ export function openLinkPopoverFromShortcut(): void {
1196
+ const surface = _surface;
1197
+ const anchor = surface?.button("format.link") ?? surface?.bar();
1198
+ if (!surface || !anchor) {
1199
+ return;
1200
+ }
1201
+ const link = getEditSnapshot().snapshot?.link ?? { active: false, href: null };
1202
+ surface.openLink(anchor, link.href, link.active);
1199
1203
  }
1200
1204
 
1205
+ // ─── Dismissal and suppression ───────────────────────────────────────────────
1206
+
1201
1207
  /** Dismiss the block action bar. */
1202
- export function dismissBlockActionBar() {
1208
+ export function dismissBlockActionBar(): void {
1203
1209
  /* A dismiss is the bar's STRUCTURAL teardown — `canvas/canvas-render.ts` calls it when a stage is
1204
1210
  torn down and again on a mode transition — so it also drops any suppression. The bar it would
1205
1211
  otherwise be holding down does not exist any more; what comes back is a different one. */
1206
1212
  _suppressedFor = null;
1207
1213
  dismissBlockBarOverflow();
1208
- if (view.blockActionBarEl) {
1209
- litRender(nothing, view.blockActionBarEl);
1214
+ hideBar();
1215
+ }
1216
+
1217
+ /**
1218
+ * Take the bar off the screen: the `visible` switch, plus the drag registration that goes with the
1219
+ * node the switch is about to remove. The mount itself stays — it is the one thing a repaint must
1220
+ * never rebuild.
1221
+ */
1222
+ function hideBar(): void {
1223
+ if (_surface) {
1224
+ applyBarView({ ...emptyBlockBarView(), visible: false });
1210
1225
  }
1226
+ view.selDragCleanup?.();
1227
+ view.selDragCleanup = null;
1211
1228
  }
1212
1229
 
1213
1230
  /**
@@ -1258,9 +1275,6 @@ function suppressionKey(tab: ActiveTab | null, path: JxPath | null): string {
1258
1275
  export function suppressBlockActionBar(): void {
1259
1276
  // Clears any earlier key as its first act; this selection's replaces it below.
1260
1277
  dismissBlockActionBar();
1261
- if (_linkPopoverOpen) {
1262
- dismissLinkPopover();
1263
- }
1264
1278
  const tab = activeTab.value;
1265
1279
  _suppressedFor = { key: suppressionKey(tab, primarySelection(tab?.session.selection)) };
1266
1280
  }
@@ -1285,22 +1299,13 @@ export function releaseBlockActionBar(): void {
1285
1299
  }
1286
1300
 
1287
1301
  /**
1288
- * Whether the link popover is open. A snapshot-driven {@link renderBlockActionBar} must NOT
1289
- * re-render (and so re-mount) the open popover — typing the URL would re-create the field and lose
1290
- * focus/caret. Guarded around the toolbar re-render.
1291
- */
1292
- let _linkPopoverOpen = false;
1293
-
1294
- /** True while the link URL popover is open (so the toolbar refresh skips a disruptive re-render). */
1295
- export function isLinkPopoverOpen(): boolean {
1296
- return _linkPopoverOpen;
1297
- }
1298
-
1299
- /**
1300
- * Whether `target` sits inside the edit-session chrome — the block action bar, its link popover, or
1301
- * the slash menu. The parent's pointerdown commit-guard must NOT end the inline-edit session for
1302
- * clicks on these (they operate ON the session); any other parent-chrome press commits it. This
1303
- * module owns those popover roots, hence the helper lives here.
1302
+ * Whether `target` sits inside the edit-session chrome — the block action bar, its `⋮` menu, or the
1303
+ * slash menu. The parent's pointerdown commit-guard must NOT end the inline-edit session for clicks
1304
+ * on these (they operate ON the session); any other parent-chrome press commits it. This module
1305
+ * owns those popover roots, hence the helper lives here.
1306
+ *
1307
+ * The link panel is no longer named separately: it is a `jx-popover` inside the bar's own document,
1308
+ * so the bar's host contains it and one root answers for both.
1304
1309
  *
1305
1310
  * @param {EventTarget | null} target
1306
1311
  */
@@ -1311,369 +1316,158 @@ export function isEditChromeTarget(target: EventTarget | null): boolean {
1311
1316
  const roots = [
1312
1317
  view.blockActionBarEl,
1313
1318
  _overflowHandle?.host ?? null,
1314
- getLayerSlot("popover", "link-popover"),
1315
1319
  getLayerSlot("popover", "slash-menu"),
1316
1320
  ];
1317
1321
  return roots.some((root) => root != null && root.contains(target));
1318
1322
  }
1319
1323
 
1320
- /**
1321
- * Show the link URL popover. The iframe owns the Selection, so the existing-link state comes from
1322
- * the latest selection snapshot; Apply/Remove post `applyFormat` link intents the iframe applies.
1323
- *
1324
- * @param {HTMLElement} anchorBtn
1325
- */
1326
- function showLinkPopover(anchorBtn: HTMLElement) {
1327
- const host = getLayerSlot("popover", "link-popover");
1328
- litRender(nothing, host);
1329
-
1330
- const link = getEditSnapshot().snapshot?.link ?? { active: false, href: null };
1331
- const existing = link.active;
1332
-
1333
- const rect = rectOf(anchorBtn);
1334
-
1335
- let _linkField: HTMLInputElement | null = null;
1336
-
1337
- const close = () => {
1338
- _linkPopoverOpen = false;
1339
- litRender(nothing, host);
1340
- };
1341
-
1342
- const onApply = () => {
1343
- const url = _linkField?.value || "";
1344
- // Apply then let the popover close itself (do not steal focus back into the iframe here).
1345
- postApplyFormat({ command: "link", href: url || "" });
1346
- close();
1347
- };
1348
-
1349
- const onRemove = () => {
1350
- postApplyFormat({ command: "link", href: null });
1351
- close();
1352
- };
1353
-
1354
- const onKeydown = (e: KeyboardEvent) => {
1355
- if (e.key === "Enter") {
1356
- onApply();
1357
- } else if (e.key === "Escape") {
1358
- close();
1359
- }
1360
- };
1361
-
1362
- _linkPopoverOpen = true;
1363
- litRender(
1364
- html`
1365
- <sp-popover
1366
- class="link-popover"
1367
- open
1368
- style=${styleMap({
1369
- left: `${rect.left}px`,
1370
- position: "fixed",
1371
- top: `${rect.bottom + 4}px`,
1372
- zIndex: "30",
1373
- })}
1374
- >
1375
- <sp-textfield
1376
- placeholder="https://..."
1377
- size="s"
1378
- style="width:200px"
1379
- value=${link.href || ""}
1380
- @keydown=${onKeydown}
1381
- ${ref((el) => {
1382
- _linkField = (el as HTMLInputElement | null) || null;
1383
- if (el) {
1384
- requestAnimationFrame(() => (el as HTMLElement).focus());
1385
- }
1386
- })}
1387
- ></sp-textfield>
1388
- <sp-action-button size="xs" @click=${onApply}>
1389
- ${existing ? "Update" : "Apply"}
1390
- </sp-action-button>
1391
- ${
1392
- existing
1393
- ? html` <sp-action-button size="xs" @click=${onRemove}>Remove</sp-action-button> `
1394
- : nothing
1395
- }
1396
- </sp-popover>
1397
- `,
1398
- host,
1399
- );
1400
- }
1324
+ // ─── The projection ──────────────────────────────────────────────────────────
1401
1325
 
1402
1326
  /**
1403
- * Open the link popover from the Ctrl/Cmd+K shortcut (anchored to the toolbar's Link button if it
1404
- * is on screen, else the bar itself). Used by the parent-focus format-shortcut handler.
1327
+ * What the bar says right now, or `null` when it must not draw at all.
1328
+ *
1329
+ * Every branch that used to end in `litRender(nothing)` ends here in `null`, and the five reasons
1330
+ * are unchanged: no context, no tab, no selection, a canvas mode that is not design or edit, a
1331
+ * selection that resolves to no node, and an anchor the stage no longer shows.
1405
1332
  */
1406
- export function openLinkPopoverFromShortcut(): void {
1407
- const bar = view.blockActionBarEl?.querySelector(".block-action-bar") as HTMLElement | null;
1408
- /* By RECORD ID, not by rendered text. This queried `sp-action-button[title^="Link"]`, which is
1409
- the addressing §13's first rule bans in the screenshot manifest — a match on a title the app
1410
- DERIVES (it now reads "Link… (⌘K)", and on Windows it reads something else again). The id is
1411
- the input the app accepts. */
1412
- const linkBtn =
1413
- (bar?.querySelector('[data-command-id="format.link"]') as HTMLElement | null) ?? bar;
1414
- if (linkBtn) {
1415
- showLinkPopover(linkBtn);
1416
- }
1417
- }
1418
-
1419
- /** Render the unified block action bar above the selected element. */
1420
- export function renderBlockActionBar() {
1421
- if (!_ctx) {
1422
- return;
1423
- }
1424
- if (!view.blockActionBarEl) {
1425
- view.blockActionBarEl = getLayerSlot("popover", "block-action-bar");
1426
- }
1427
-
1428
- /* A snapshot-driven refresh must not re-mount an open link popover — it would re-create the URL
1429
- field and lose the caret — so this pass is skipped entirely.
1430
-
1431
- It sits AHEAD of the drag release below, because it is the one early return that LEAVES THE BAR
1432
- UP. Releasing and then returning without re-rendering left the ⠿ handle on screen and inert, and
1433
- nothing re-installs it: `dismissLinkPopover` does not re-render the bar. Any repaint reaches
1434
- here — `applyTransform` → `renderOnly("overlays")` → this — so a pan, a zoom or a pane resize
1435
- with the Link popover open silently killed dragging until the next selection change. Every
1436
- other early return draws `nothing`, so releasing before them is right. */
1437
- if (_linkPopoverOpen) {
1438
- return;
1439
- }
1440
-
1441
- if (view.selDragCleanup) {
1442
- view.selDragCleanup();
1443
- view.selDragCleanup = null;
1444
- }
1445
-
1333
+ function projectBar(): BlockBarView | null {
1446
1334
  const tab = activeTab.value;
1447
- const canvasMode = _ctx.getCanvasMode();
1448
-
1335
+ const canvasMode = _ctx?.getCanvasMode();
1449
1336
  const selection = primarySelection(tab?.session.selection);
1450
-
1451
- /* Suppressed by a chrome pointerdown, and this pass is the first of the two doors out of it (see
1452
- {@link _suppressedFor}). A different selection means the click that hid the bar also chose a new
1453
- target, so the suppression is over; the same selection keeps it, which is what stops the next
1454
- snapshot- or overlay-driven repaint from flashing the bar back under the author's cursor. */
1455
- if (_suppressedFor) {
1456
- if (_suppressedFor.key === suppressionKey(tab, selection)) {
1457
- litRender(nothing, view.blockActionBarEl);
1458
- return;
1459
- }
1460
- _suppressedFor = null;
1461
- }
1462
-
1463
1337
  if (!tab || !selection || (canvasMode !== "design" && canvasMode !== "edit")) {
1464
- litRender(nothing, view.blockActionBarEl);
1465
- return;
1338
+ return null;
1466
1339
  }
1467
-
1468
1340
  const node = getNodeAtPath(tab.doc.document, selection);
1469
1341
  if (!node) {
1470
- litRender(nothing, view.blockActionBarEl);
1471
- return;
1342
+ return null;
1472
1343
  }
1473
-
1474
1344
  // Position from the iframe-host's viewport-space anchor (the bar is position:fixed). The parent
1475
1345
  // Never reads the iframe DOM, so geometry crosses the bridge as the selection snapshot's rect.
1476
1346
  const anchor = getEditBarAnchorRect();
1477
1347
  const pos = anchor ? barPosition(anchor) : null;
1478
1348
  if (!pos) {
1479
- litRender(nothing, view.blockActionBarEl);
1480
- return;
1349
+ return null;
1481
1350
  }
1482
1351
 
1352
+ const registry = selectionCommandRegistry();
1483
1353
  const tag = (displayTagName(node.tagName) || "div").toLowerCase();
1484
-
1485
- // Inline format state, sourced from the iframe's selection snapshot.
1486
1354
  const { editingProp, snapshot } = getEditSnapshot();
1487
1355
  const actions = getInlineActions(tag) || [];
1488
- // ONE bar, one shape. The format group used to appear only during an "inline edit session", so
1489
- // The toolbar rearranged itself under the author's cursor the moment they started typing. With a
1490
- // Document-wide caret there is no session to be in or out of: the group is shown whenever the
1491
- // Selected block can carry inline markup, and the buttons enable when there is a range to apply
1492
- // Them to.
1493
- //
1494
- // A prop-bound block still suppresses it — it edits a single plain string (belt and braces:
1495
- // Component tags have no $inlineActions, so `actions` is empty there anyway).
1356
+ /* ONE bar, one shape. The format group used to appear only during an "inline edit session", so
1357
+ the toolbar rearranged itself under the author's cursor the moment they started typing. With a
1358
+ document-wide caret there is no session to be in or out of: the group is shown whenever the
1359
+ selected block can carry inline markup, and the buttons enable when there is a range to apply
1360
+ them to. A prop-bound block still suppresses it — it edits a single plain string (belt and
1361
+ braces: component tags have no $inlineActions, so `actions` is empty there anyway). */
1496
1362
  const showFormat = !editingProp && actions.length > 0;
1497
- const activeValues =
1498
- showFormat && snapshot
1499
- ? actions.filter((a) => snapshot.activeTags.includes(a.tag)).map((a) => a.tag)
1500
- : [];
1501
1363
  // Formatting applies to a RANGE: disabled for a collapsed caret, and for a block selected without
1502
1364
  // One at all (from the layers panel, or by a structural edit moving the selection).
1503
1365
  const formatDisabled = snapshot?.collapsed ?? true;
1504
-
1505
- // Conversion targets for badge click
1506
- const isComponent =
1507
- displayTagName(node.tagName).includes("-") &&
1508
- componentRegistry.some((/** @type {{ tagName: string }} */ c) => c.tagName === node.tagName);
1509
- const children = childList(node);
1510
- const isEmpty =
1511
- !node.textContent &&
1512
- (children.length === 0 ||
1513
- (children.length === 1 && typeof children[0] === "object" && children[0]?.tagName === "br"));
1514
- // Repeater ($prototype:"Array") pseudo-elements have no tagName — show the "Repeater → items" label
1515
- // (not a bare "div") and don't offer tag-conversion targets, which are meaningless for a repeater.
1516
- const isRepeater = node.$prototype === "Array";
1517
- const convertTargets = !isComponent && !isRepeater ? getConvertTargets(tag, isEmpty) : [];
1518
- const badgeInteractive = convertTargets.length > 0;
1519
-
1520
- // The verb cluster, sliced at the cap. `forPlacement` already dropped the records whose `when` is
1521
- // False and sorted the rest by group; everything past the cap keeps its name and its chord in the
1522
- // `⋮` menu rather than being silently unavailable.
1523
- const registry = selectionCommandRegistry();
1524
1366
  /* The SET is the schema's, the VERBS are the registry's (see {@link formatCommands}).
1525
1367
  `$inlineActions` says which of the eight this tag accepts and in what order — four on an `<h1>`,
1526
1368
  eight on a `<p>` — and each one's name, icon, chord and behaviour come off its record. An action
1527
1369
  the registry has no record for is dropped rather than drawn as a button that runs nothing, so
1528
1370
  adding a ninth verb to the data file without a record shows up as a missing button and as a red
1529
1371
  `tests/block-action-bar.test.ts`, not as a silent no-op. */
1530
- const formatButtons = actions.flatMap((action) => {
1372
+ const formats = actions.flatMap((action) => {
1531
1373
  const command = registry.get(`format.${action.command}`);
1532
- return command ? [{ action, command }] : [];
1374
+ return command
1375
+ ? [
1376
+ toolOf(registry, command, {
1377
+ disabled: formatDisabled && action.command !== "link",
1378
+ selected: snapshot?.activeTags.includes(action.tag) === true,
1379
+ value: action.tag,
1380
+ }),
1381
+ ]
1382
+ : [];
1533
1383
  });
1384
+
1385
+ // The verb cluster, sliced at the cap. `forPlacement` already dropped the records whose `when` is
1386
+ // False and sorted the rest by group; everything past the cap keeps its name and its chord in the
1387
+ // `⋮` menu rather than being silently unavailable.
1534
1388
  const placed = registry.forPlacement("blockbar");
1535
- const shown = placed.slice(0, BLOCKBAR_MAX_ITEMS);
1536
- const overflow = placed.slice(BLOCKBAR_MAX_ITEMS);
1389
+
1390
+ const parentPath = parentElementPath(selection);
1391
+ const parentNode = parentPath ? getNodeAtPath(tab.doc.document, parentPath) : null;
1392
+ const parentCommand = registry.get("selection.selectParent");
1393
+ /* The parent selector renders the `selection.selectParent` record — its name, its chord and its
1394
+ handler — but as fixed chrome rather than from `forPlacement("blockbar")`, because that record
1395
+ does not declare the placement. Its disabled state is "the selection has no parent", which the
1396
+ record's own `enablement` (a bare "there is a selection") cannot express yet. */
1397
+ const parentName = parentCommand?.title ?? "Select Parent";
1398
+ const parentLabel = parentNode ? `Select parent: ${nodeLabel(parentNode)}` : parentName;
1399
+ const parentChord = parentCommand ? registry.keymap.formatBinding(parentCommand.id) : undefined;
1400
+
1401
+ const convertTargets = convertTargetsFor(node);
1402
+ // Repeater ($prototype:"Array") pseudo-elements have no tagName — show the "Repeater → items"
1403
+ // Label (not a bare "div").
1404
+ const tagLabel =
1405
+ (node.$prototype === "Array"
1406
+ ? nodeLabel(node)
1407
+ : node.$id || displayTagName(node.tagName) || "div") +
1408
+ (editingProp ? ` · ${editingProp}` : "");
1537
1409
  // The handle is chrome, not a verb, so it renders on every selection; only a node that actually
1538
1410
  // Sits at a child index can be dragged, and at the root it is a disabled affordance rather than a
1539
1411
  // Missing one (§8.6: ONE shape).
1540
- const canDragSelection = structuralTarget(selection) !== null;
1541
-
1542
- litRender(
1543
- html`
1544
- <div
1545
- class="block-action-bar"
1546
- data-jx-region="overlay.menu:block-action-bar"
1547
- role="toolbar"
1548
- aria-label="Block actions"
1549
- aria-orientation="horizontal"
1550
- style=${styleMap({ left: `${pos.left}px`, top: `${pos.top}px` })}
1551
- @mousedown=${onBarMousedown}
1552
- @keydown=${onToolbarKeydown}
1553
- >
1554
- ${renderParentButton(registry)}
1555
-
1556
- <span
1557
- class="bar-tag${badgeInteractive ? " bar-tag--interactive" : ""}"
1558
- role=${badgeInteractive ? "button" : nothing}
1559
- data-toolbar-item=${badgeInteractive ? "" : nothing}
1560
- tabindex=${badgeInteractive ? "-1" : nothing}
1561
- title=${badgeInteractive ? "Change element type" : nothing}
1562
- @click=${
1563
- badgeInteractive
1564
- ? (e: MouseEvent) => onTagBadgeClick(e, convertTargets, selection)
1565
- : nothing
1566
- }
1567
- >${isRepeater ? nodeLabel(node) : node.$id || displayTagName(node.tagName) || "div"}${
1568
- editingProp ? ` · ${editingProp}` : ""
1569
- }</span
1570
- >
1571
-
1572
- <span
1573
- class="bar-drag-handle${canDragSelection ? "" : " bar-drag-handle--disabled"}"
1574
- title=${canDragSelection ? "Drag to reorder" : "Drag to reorder — the document root cannot move"}
1575
- aria-disabled=${canDragSelection ? nothing : "true"}
1576
- data-toolbar-item
1577
- tabindex="-1"
1578
- ${ref((handleEl) => {
1579
- if (!handleEl || !canDragSelection) {
1580
- return;
1581
- }
1582
- /* No release here: {@link renderBlockActionBar} already released at the top of this
1583
- pass, and that one — not this — is the load-bearing copy, because it also runs on
1584
- the passes that return before `litRender` and never reach this ref at all. A second
1585
- release here could only ever see null, which is why it was dead. */
1586
- view.selDragCleanup = draggable({
1587
- element: handleEl as HTMLElement,
1588
- getInitialData: () => ({
1589
- // Snapshot the selection: the live array is a Vue reactive proxy, which
1590
- // Structured clone rejects when the src crosses postMessage (DataCloneError
1591
- // Killed the whole handle drag), and a live reference would also mutate the
1592
- // Retained srcData if the selection changed mid-drag.
1593
- path: [...(primarySelection(activeTab.value?.session.selection) ?? [])],
1594
- type: "tree-node",
1595
- }),
1596
- onGenerateDragPreview: ({
1597
- nativeSetDragImage,
1598
- }: {
1599
- nativeSetDragImage: ((image: Element, x: number, y: number) => void) | null;
1600
- }) => {
1601
- // Suppress the native drag image; the cross-frame ghost is the drag affordance.
1602
- disableNativeDragPreview({ nativeSetDragImage });
1603
- },
1604
- });
1605
- })}
1606
- >⠿</span
1607
- >
1608
-
1609
- <sp-divider size="s" vertical></sp-divider>
1610
- ${shown.map((command) => renderCommandButton(registry, command))}
1611
- ${overflow.length > 0 ? renderOverflowButton(registry, overflow) : nothing}
1612
- ${
1613
- showFormat
1614
- ? html`
1615
- <sp-divider size="s" vertical></sp-divider>
1616
- <sp-action-group
1617
- size="xs"
1618
- compact
1619
- emphasized
1620
- selects="multiple"
1621
- selected=${activeValues.length > 0 ? JSON.stringify(activeValues) : nothing}
1622
- >
1623
- ${formatButtons.map(
1624
- ({ action, command }) => html`
1625
- <sp-action-button
1626
- size="xs"
1627
- value=${action.tag}
1628
- data-command-id=${command.id}
1629
- data-toolbar-item
1630
- tabindex="-1"
1631
- aria-label=${command.title}
1632
- title=${commandTooltip(registry, command)}
1633
- ?disabled=${formatDisabled && action.command !== "link"}
1634
- @mousedown=${(e: MouseEvent) => e.preventDefault()}
1635
- @click=${(e: MouseEvent) => {
1636
- e.stopPropagation();
1637
- runCommand(registry, command.id);
1638
- }}
1639
- >
1640
- ${commandIcon(command)}
1641
- </sp-action-button>
1642
- `,
1643
- )}
1644
- </sp-action-group>
1645
- <sp-action-button
1646
- size="xs"
1647
- quiet
1648
- data-toolbar-item
1649
- data-jx-region="overlay.menu:block-action-bar/insertData"
1650
- tabindex="-1"
1651
- aria-label="Insert data"
1652
- title="Insert data"
1653
- @mousedown=${(e: MouseEvent) => e.preventDefault()}
1654
- @click=${onMergeTagClick}
1655
- >
1656
- <sp-icon-data slot="icon"></sp-icon-data>
1657
- </sp-action-button>
1658
- `
1659
- : nothing
1660
- }
1661
- </div>
1662
- `,
1663
- view.blockActionBarEl,
1664
- );
1412
+ const canDrag = structuralTarget(selection) !== null;
1665
1413
 
1666
- const rendered = view.blockActionBarEl.querySelector<HTMLElement>(".block-action-bar");
1667
- if (rendered) {
1668
- applyRovingTabindex(rendered);
1414
+ return {
1415
+ barX: pos.left,
1416
+ barY: pos.top,
1417
+ canDrag,
1418
+ dragHint: canDrag ? "Drag to reorder" : "Drag to reorder — the document root cannot move",
1419
+ formats,
1420
+ hasOverflow: placed.length > BLOCKBAR_MAX_ITEMS,
1421
+ offscreen: false,
1422
+ overflowOpen: _overflowHandle !== null,
1423
+ parentDisabled: !parentPath,
1424
+ parentHint: parentChord ? `${parentLabel} (${parentChord})` : parentLabel,
1425
+ parentLabel,
1426
+ showFormat,
1427
+ tagDisabled: convertTargets.length === 0,
1428
+ tagHint: convertTargets.length > 0 ? "Change element type" : tagLabel,
1429
+ tagLabel,
1430
+ tagPopup: convertTargets.length > 0 ? "menu" : "",
1431
+ verbs: placed.slice(0, BLOCKBAR_MAX_ITEMS).map((command) => toolOf(registry, command)),
1432
+ visible: true,
1433
+ };
1434
+ }
1435
+
1436
+ /** Render the unified block action bar above the selected element. */
1437
+ export function renderBlockActionBar(): void {
1438
+ if (!_ctx) {
1439
+ return;
1669
1440
  }
1441
+ ensureSurface();
1670
1442
 
1671
- // Post-render side effects
1672
- requestAnimationFrame(() => {
1673
- const bar = view.blockActionBarEl?.firstElementChild as HTMLElement | null;
1674
- if (!bar) {
1443
+ const tab = activeTab.value;
1444
+ const selection = primarySelection(tab?.session.selection);
1445
+
1446
+ /* Suppressed by a chrome pointerdown, and this pass is the first of the two doors out of it (see
1447
+ {@link _suppressedFor}). A different selection means the click that hid the bar also chose a new
1448
+ target, so the suppression is over; the same selection keeps it, which is what stops the next
1449
+ snapshot- or overlay-driven repaint from flashing the bar back under the author's cursor. */
1450
+ if (_suppressedFor) {
1451
+ if (_suppressedFor.key === suppressionKey(tab, selection)) {
1452
+ hideBar();
1675
1453
  return;
1676
1454
  }
1677
- clampBarToWindow(bar);
1455
+ _suppressedFor = null;
1456
+ }
1457
+
1458
+ const next = projectBar();
1459
+ if (!next) {
1460
+ hideBar();
1461
+ return;
1462
+ }
1463
+ applyBarView(next);
1464
+
1465
+ // Post-render side effect: the clamp measures a box that does not exist until the document has
1466
+ // Reconciled, so it waits a frame rather than reading a stale one.
1467
+ requestAnimationFrame(() => {
1468
+ const bar = _surface?.bar();
1469
+ if (bar) {
1470
+ clampBarToWindow(bar);
1471
+ }
1678
1472
  });
1679
1473
  }