@jxsuite/studio 4.1.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (612) 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-qwkm9zrf.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-vj72gqtq.js +20 -0
  15. package/dist/chunks/content-source-5mex4x1h.js +46 -0
  16. package/dist/chunks/convert-to-repeater-n64h8znc.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-234hjnqw.js +42 -0
  20. package/dist/chunks/extension-sections-3hpcambp.js +651 -0
  21. package/dist/chunks/file-ops-8yhd83xr.js +47 -0
  22. package/dist/chunks/files-hnxqxtsg.js +72 -0
  23. package/dist/chunks/format-host-779sfp2n.js +40 -0
  24. package/dist/chunks/fs-events-b88z7g4h.js +18 -0
  25. package/dist/chunks/git-panel-xwj8fpab.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-syr0f9sb.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-ff81wthn.js +30 -0
  37. package/dist/chunks/layers-panel-pnnghw12.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-cqhhxhhe.js +33 -0
  41. package/dist/chunks/menu-9p2w46gj.js +15 -0
  42. package/dist/chunks/menu-q99sy9rf.js +285 -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-5zt840sy.js +27 -0
  52. package/dist/chunks/quick-search-3748564r.js +45 -0
  53. package/dist/chunks/references-qvvxej7h.js +26 -0
  54. package/dist/chunks/right-panel-eccz92qt.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-kryvsw6j.js → studio-1k325pwf.js} +48 -48
  60. package/dist/chunks/{studio-b55y6ewx.js → studio-1knfqgy7.js} +58 -58
  61. package/dist/chunks/{studio-2b946z26.js → studio-1z1kcjv4.js} +3628 -6095
  62. package/dist/chunks/studio-3773086z.js +111 -0
  63. package/dist/chunks/{studio-4r1pv6zf.js → studio-38emantv.js} +22 -5
  64. package/dist/chunks/{studio-x813zp48.js → studio-3rz5md06.js} +30 -30
  65. package/dist/chunks/{studio-ynxwkrzd.js → studio-4wbb9ct0.js} +1953 -1785
  66. package/dist/chunks/studio-5te5tqng.js +81 -0
  67. package/dist/chunks/studio-5wt9nvhd.js +4763 -0
  68. package/dist/chunks/{studio-p4ngc046.js → studio-6ewqv390.js} +9 -9
  69. package/dist/chunks/{studio-9ab8gqsm.js → studio-6nzaktvy.js} +7 -7
  70. package/dist/chunks/studio-6xa21ej9.js +28 -0
  71. package/dist/chunks/{studio-5ehhackm.js → studio-727xh0e3.js} +19 -19
  72. package/dist/chunks/{studio-gcye7qka.js → studio-7e11jpa9.js} +21 -21
  73. package/dist/chunks/{studio-q7v10cg2.js → studio-7r42c6ca.js} +17 -7
  74. package/dist/chunks/studio-7xnmgv66.js +13615 -0
  75. package/dist/chunks/studio-8scdew0e.js +258 -0
  76. package/dist/chunks/{studio-fbf92ckf.js → studio-93zafjcm.js} +275 -163
  77. package/dist/chunks/{studio-dg7zyt5x.js → studio-98b3z07n.js} +1 -1
  78. package/dist/chunks/studio-a79k90n9.js +1162 -0
  79. package/dist/chunks/studio-aj37eer3.js +422 -0
  80. package/dist/chunks/studio-b0k5tqgp.js +2432 -0
  81. package/dist/chunks/studio-b4s9eykd.js +2811 -0
  82. package/dist/chunks/{studio-f3wqjtk5.js → studio-d0300636.js} +5076 -18
  83. package/dist/chunks/{studio-b9b6t1dg.js → studio-e1f24eff.js} +5664 -5666
  84. package/dist/chunks/studio-e1pgga3k.js +839 -0
  85. package/dist/chunks/{studio-g1wdche0.js → studio-esrxgwh6.js} +80 -80
  86. package/dist/chunks/{studio-q43a5y9h.js → studio-g3jevyjt.js} +16 -16
  87. package/dist/chunks/studio-gb13095a.js +2806 -0
  88. package/dist/chunks/{studio-3n4ndrzh.js → studio-h4nvnjvz.js} +303 -303
  89. package/dist/chunks/{studio-dm19q43r.js → studio-hhvs4sdg.js} +10 -10
  90. package/dist/chunks/{studio-x4qkdat5.js → studio-js8nv53n.js} +1749 -2481
  91. package/dist/chunks/{studio-qjy2z9bv.js → studio-jxp12rnj.js} +32 -137
  92. package/dist/chunks/{studio-trxrabqf.js → studio-k75n7vhp.js} +527 -283
  93. package/dist/chunks/{studio-q9jd1a60.js → studio-kcgff4r0.js} +15 -18
  94. package/dist/chunks/studio-kkqkcp3d.js +17668 -0
  95. package/dist/chunks/studio-mhhyamxf.js +763 -0
  96. package/dist/chunks/{studio-cft9r4r7.js → studio-nq5yf5mp.js} +22 -22
  97. package/dist/chunks/{studio-685s0ytq.js → studio-nqgawfks.js} +572 -572
  98. package/dist/chunks/studio-nzzpvzx9.js +185 -0
  99. package/dist/chunks/{studio-xrapxwwc.js → studio-p1dw8wfw.js} +55 -29
  100. package/dist/chunks/{studio-3bqpr0zh.js → studio-pxbgypmg.js} +299 -110
  101. package/dist/chunks/studio-q0a3yk4n.js +33 -0
  102. package/dist/chunks/{studio-wcy8trfx.js → studio-q3x06vmj.js} +23 -22
  103. package/dist/chunks/{studio-6yw4y3my.js → studio-qhhm6hbt.js} +8 -8
  104. package/dist/chunks/studio-qk4bfsq2.js +362 -0
  105. package/dist/chunks/{studio-msd847zn.js → studio-qk90fgqm.js} +361 -90
  106. package/dist/chunks/{studio-jkftxyvz.js → studio-r6nvacqs.js} +5861 -2154
  107. package/dist/chunks/{studio-53n8bp04.js → studio-r6xmmtfh.js} +1 -1
  108. package/dist/chunks/{studio-tvgyw9xw.js → studio-rnyf3f8e.js} +83 -250
  109. package/dist/chunks/studio-rs76k67r.js +12 -0
  110. package/dist/chunks/studio-sbjyc8bt.js +954 -0
  111. package/dist/chunks/{studio-1wagb550.js → studio-syg2tadj.js} +11 -11
  112. package/dist/chunks/{studio-t5wqwzdh.js → studio-tjjk7jvk.js} +1 -1
  113. package/dist/chunks/studio-tqgryxwg.js +1171 -0
  114. package/dist/chunks/studio-try13h8c.js +8448 -0
  115. package/dist/chunks/studio-vfq8qpjt.js +1219 -0
  116. package/dist/chunks/studio-vhv5tgxm.js +51 -0
  117. package/dist/chunks/{studio-ahn3pdgx.js → studio-vw7mzp6z.js} +52 -38
  118. package/dist/chunks/studio-wqpngpsk.js +174 -0
  119. package/dist/chunks/studio-wqz6dggm.js +1356 -0
  120. package/dist/chunks/{studio-92ab2jqe.js → studio-y9zedyjn.js} +14 -14
  121. package/dist/chunks/studio-yxa81f45.js +12 -0
  122. package/dist/chunks/studio-zdn2g6dt.js +148 -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-t55jdjbx.js +69 -0
  133. package/dist/chunks/{ws-client-6shc5rze.js → ws-client-ny4zc51r.js} +20 -22
  134. package/dist/iframe-entry.js +1873 -673
  135. package/dist/studio.js +52757 -36535
  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 +333 -113
  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 +404 -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 +177 -10
  178. package/src/commands/run-reported.ts +120 -0
  179. package/src/content/entry-commands.ts +16 -23
  180. package/src/content/entry-editor.ts +144 -101
  181. package/src/editor/canvas-slash-bridge.ts +2 -2
  182. package/src/editor/context-menu.ts +49 -200
  183. package/src/editor/convert-to-component.ts +42 -82
  184. package/src/editor/convert-to-repeater.ts +132 -173
  185. package/src/editor/inline-edit.ts +11 -0
  186. package/src/editor/shortcuts.ts +76 -49
  187. package/src/editor/slash-menu.ts +150 -157
  188. package/src/files/components.ts +39 -0
  189. package/src/files/file-ops.ts +117 -11
  190. package/src/files/files.ts +944 -660
  191. package/src/files/json-layout.ts +474 -0
  192. package/src/files/serialize-document.ts +8 -1
  193. package/src/format/convert-file.ts +16 -10
  194. package/src/format/format-host.ts +50 -1
  195. package/src/github/github-auth.ts +71 -29
  196. package/src/github/github-publish.ts +57 -65
  197. package/src/grid/cell-editors.ts +46 -4
  198. package/src/grid/cell-popovers.ts +65 -62
  199. package/src/grid/grid-open.ts +86 -64
  200. package/src/grid/grid-panel.ts +390 -469
  201. package/src/grid/redirects-grid.ts +40 -44
  202. package/src/hosting/layout.ts +2 -0
  203. package/src/i18n/i18n-commands.ts +137 -62
  204. package/src/media/media-pane.ts +83 -133
  205. package/src/new-project/add-repo-modal.ts +125 -176
  206. package/src/new-project/import-tab.ts +181 -252
  207. package/src/new-project/location-fields.ts +93 -160
  208. package/src/new-project/new-project-modal.ts +376 -367
  209. package/src/packages/jxsuite-update.ts +76 -18
  210. package/src/packages/pull-package-sync.ts +5 -1
  211. package/src/panels/activity-panel.ts +137 -104
  212. package/src/panels/ai-chat/chat-markdown.ts +15 -13
  213. package/src/panels/ai-chat/chat-view.ts +307 -472
  214. package/src/panels/ai-chat/composer.ts +175 -163
  215. package/src/panels/ai-chat/sessions-view.ts +31 -62
  216. package/src/panels/ai-panel.ts +213 -185
  217. package/src/panels/block-action-bar.ts +444 -645
  218. package/src/panels/bottom-dock.ts +112 -47
  219. package/src/panels/chat-panel.ts +19 -14
  220. package/src/panels/data-explorer.ts +141 -102
  221. package/src/panels/data-grid.ts +183 -150
  222. package/src/panels/dnd.ts +75 -20
  223. package/src/panels/editors.ts +26 -19
  224. package/src/panels/elements-panel.ts +243 -186
  225. package/src/panels/empty-state.ts +26 -42
  226. package/src/panels/events-panel.ts +950 -729
  227. package/src/panels/formula-workspace.ts +434 -207
  228. package/src/panels/frontmatter-fields.ts +185 -153
  229. package/src/panels/frontmatter-panel.ts +411 -187
  230. package/src/panels/git-diff-open.ts +148 -0
  231. package/src/panels/git-panel.ts +679 -605
  232. package/src/panels/head-panel.ts +617 -435
  233. package/src/panels/i18n-panel.ts +148 -135
  234. package/src/panels/imports-panel.ts +388 -414
  235. package/src/panels/jump-bar.ts +175 -147
  236. package/src/panels/layers-panel.ts +785 -671
  237. package/src/panels/left-panel.ts +166 -94
  238. package/src/panels/navigator-panels.ts +1 -1
  239. package/src/panels/pane-context.ts +529 -677
  240. package/src/panels/pane-grid.ts +265 -241
  241. package/src/panels/panel-registry.ts +15 -4
  242. package/src/panels/problems-panel.ts +181 -99
  243. package/src/panels/properties-panel.ts +1525 -966
  244. package/src/panels/provenance.ts +31 -113
  245. package/src/panels/quick-search.ts +223 -184
  246. package/src/panels/right-panel.ts +135 -169
  247. package/src/panels/seo-modal.ts +346 -167
  248. package/src/panels/settings-menu.ts +114 -661
  249. package/src/panels/settings-pane.ts +102 -86
  250. package/src/panels/signals-panel.ts +1318 -1172
  251. package/src/panels/statement-editor.ts +556 -428
  252. package/src/panels/style-panel.ts +1367 -630
  253. package/src/panels/style-utils.ts +36 -32
  254. package/src/panels/stylebook-doc.ts +30 -73
  255. package/src/panels/stylebook-layers-panel.ts +202 -109
  256. package/src/panels/stylebook-panel.ts +124 -88
  257. package/src/panels/tab-strip.ts +386 -288
  258. package/src/platforms/cloud.ts +106 -16
  259. package/src/platforms/devserver.ts +13 -0
  260. package/src/publish/deploy-checklist.ts +124 -78
  261. package/src/publish/publish-commands.ts +4 -12
  262. package/src/publish/publish-panel.ts +134 -328
  263. package/src/services/a11y-report.ts +100 -43
  264. package/src/services/ai-command-tools.ts +538 -0
  265. package/src/services/ai-project-tools.ts +8 -2
  266. package/src/services/ai-session-store.ts +6 -0
  267. package/src/services/ai-system-prompt.ts +112 -24
  268. package/src/services/ai-tools.ts +36 -212
  269. package/src/services/ai-write-report.ts +174 -0
  270. package/src/services/document-assistant.ts +59 -8
  271. package/src/services/idle.ts +16 -14
  272. package/src/services/live-surfaces.ts +237 -0
  273. package/src/services/model-uri.ts +75 -0
  274. package/src/services/notify.ts +63 -6
  275. package/src/services/popover-report.ts +325 -0
  276. package/src/services/surface-registry.ts +56 -0
  277. package/src/services/tool-executor.ts +6 -0
  278. package/src/settings/contexts-section.ts +301 -299
  279. package/src/settings/contributed-section.ts +361 -222
  280. package/src/settings/css-vars-editor.ts +320 -325
  281. package/src/settings/defs-editor.ts +572 -535
  282. package/src/settings/dependencies-editor.ts +79 -107
  283. package/src/settings/extension-commands.ts +379 -0
  284. package/src/settings/extension-rows.ts +113 -0
  285. package/src/settings/extensions-section.ts +287 -0
  286. package/src/settings/general-settings.ts +130 -181
  287. package/src/settings/head-editor.ts +10 -305
  288. package/src/settings/locales-section.ts +146 -196
  289. package/src/settings/preferences-accounts.ts +1 -1
  290. package/src/settings/preferences-dialog.ts +350 -330
  291. package/src/settings/project-sections.ts +108 -181
  292. package/src/settings/schema-field-ui.ts +11 -399
  293. package/src/settings/section-registry.ts +8 -1
  294. package/src/settings/settings-document.ts +31 -29
  295. package/src/shell/tree.ts +42 -87
  296. package/src/shell.ts +48 -36
  297. package/src/studio.ts +104 -69
  298. package/src/style/token-ref.ts +0 -39
  299. package/src/surfaces/about.json +263 -0
  300. package/src/surfaces/about.ts +163 -0
  301. package/src/surfaces/add-repo.json +486 -0
  302. package/src/surfaces/add-repo.ts +236 -0
  303. package/src/surfaces/ai-chat.json +2185 -0
  304. package/src/surfaces/ai-chat.ts +549 -0
  305. package/src/surfaces/ai-credentials-form.json +332 -0
  306. package/src/surfaces/ai-credentials-form.ts +151 -0
  307. package/src/surfaces/ai-managed-connect.json +108 -0
  308. package/src/surfaces/ai-managed-connect.ts +123 -0
  309. package/src/surfaces/ai-model-picker.json +114 -0
  310. package/src/surfaces/ai-model-picker.ts +139 -0
  311. package/src/surfaces/block-action-bar.json +640 -0
  312. package/src/surfaces/block-action-bar.ts +368 -0
  313. package/src/surfaces/bottom-dock.json +154 -0
  314. package/src/surfaces/bottom-dock.ts +125 -0
  315. package/src/surfaces/canvas-stage.json +444 -0
  316. package/src/surfaces/canvas-stage.ts +253 -0
  317. package/src/surfaces/cf-account-picker.json +300 -0
  318. package/src/surfaces/cf-account-picker.ts +214 -0
  319. package/src/surfaces/commandbar.json +985 -0
  320. package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +417 -391
  321. package/src/surfaces/convert-repeater.json +309 -0
  322. package/src/surfaces/convert-repeater.ts +276 -0
  323. package/src/surfaces/data-actions.json +157 -0
  324. package/src/surfaces/data-actions.ts +115 -0
  325. package/src/surfaces/dialog.json +289 -0
  326. package/src/surfaces/dialog.ts +343 -0
  327. package/src/surfaces/diff-toolbar.json +266 -0
  328. package/src/surfaces/diff-toolbar.ts +139 -0
  329. package/src/surfaces/doc-header.json +739 -0
  330. package/src/surfaces/doc-header.ts +223 -0
  331. package/src/surfaces/empty-state.json +152 -0
  332. package/src/surfaces/empty-state.ts +173 -0
  333. package/src/surfaces/entry-editor.json +281 -0
  334. package/src/surfaces/entry-editor.ts +191 -0
  335. package/src/surfaces/expression-editor.json +977 -0
  336. package/src/surfaces/expression-editor.ts +266 -0
  337. package/src/surfaces/files-panel.json +592 -0
  338. package/src/surfaces/files-panel.ts +301 -0
  339. package/src/surfaces/formula-palette.json +364 -0
  340. package/src/surfaces/formula-palette.ts +340 -0
  341. package/src/surfaces/git-panel.json +1822 -0
  342. package/src/surfaces/git-panel.ts +272 -0
  343. package/src/surfaces/github-auth.json +112 -0
  344. package/src/surfaces/github-auth.ts +139 -0
  345. package/src/surfaces/github-publish.json +206 -0
  346. package/src/surfaces/github-publish.ts +169 -0
  347. package/src/surfaces/grid-cell.json +218 -0
  348. package/src/surfaces/grid-cell.ts +120 -0
  349. package/src/surfaces/grid-open.json +147 -0
  350. package/src/surfaces/grid-open.ts +87 -0
  351. package/src/surfaces/grid-panel.json +706 -0
  352. package/src/surfaces/grid-panel.ts +206 -0
  353. package/src/surfaces/grid-replace.json +179 -0
  354. package/src/surfaces/grid-replace.ts +89 -0
  355. package/src/surfaces/grid-views.json +446 -0
  356. package/src/surfaces/grid-views.ts +172 -0
  357. package/src/surfaces/inspector-dock.json +187 -0
  358. package/src/surfaces/inspector-dock.ts +161 -0
  359. package/src/surfaces/jump-bar.json +243 -0
  360. package/src/surfaces/jump-bar.ts +129 -0
  361. package/src/surfaces/jxsuite-update.json +146 -0
  362. package/src/surfaces/jxsuite-update.ts +149 -0
  363. package/src/surfaces/library-pane.json +1509 -0
  364. package/src/surfaces/library-pane.ts +363 -0
  365. package/src/surfaces/logic-panel.json +1949 -0
  366. package/src/surfaces/logic-panel.ts +369 -0
  367. package/src/surfaces/logic-workspace.json +685 -0
  368. package/src/surfaces/logic-workspace.ts +238 -0
  369. package/src/surfaces/media-browser.json +303 -0
  370. package/src/surfaces/media-browser.ts +145 -0
  371. package/src/surfaces/media-field.json +163 -0
  372. package/src/surfaces/media-field.ts +116 -0
  373. package/src/surfaces/media-pane.json +513 -0
  374. package/src/surfaces/media-pane.ts +197 -0
  375. package/src/surfaces/menu.json +232 -0
  376. package/src/surfaces/menu.ts +375 -0
  377. package/src/surfaces/navigator-dock.json +113 -0
  378. package/src/surfaces/navigator-dock.ts +130 -0
  379. package/src/surfaces/new-project.json +1654 -0
  380. package/src/surfaces/new-project.ts +411 -0
  381. package/src/surfaces/palette.json +535 -0
  382. package/src/surfaces/pane-context.json +1497 -0
  383. package/src/surfaces/pane-context.ts +513 -0
  384. package/src/surfaces/pane-grid.json +188 -0
  385. package/src/surfaces/pane-grid.ts +250 -0
  386. package/src/surfaces/panel-activity.json +563 -0
  387. package/src/surfaces/panel-activity.ts +187 -0
  388. package/src/surfaces/panel-data.json +141 -0
  389. package/src/surfaces/panel-data.ts +156 -0
  390. package/src/surfaces/panel-deploy-checklist.json +283 -0
  391. package/src/surfaces/panel-deploy-checklist.ts +175 -0
  392. package/src/surfaces/panel-elements.json +367 -0
  393. package/src/surfaces/panel-elements.ts +175 -0
  394. package/src/surfaces/panel-i18n.json +569 -0
  395. package/src/surfaces/panel-i18n.ts +212 -0
  396. package/src/surfaces/panel-imports.json +780 -0
  397. package/src/surfaces/panel-imports.ts +209 -0
  398. package/src/surfaces/panel-outline.json +734 -0
  399. package/src/surfaces/panel-outline.ts +326 -0
  400. package/src/surfaces/panel-page.json +1017 -0
  401. package/src/surfaces/panel-page.ts +240 -0
  402. package/src/surfaces/panel-problems.json +497 -0
  403. package/src/surfaces/panel-problems.ts +157 -0
  404. package/src/surfaces/panel-signals.json +1973 -0
  405. package/src/surfaces/panel-signals.ts +436 -0
  406. package/src/surfaces/panel-stylebook-layers.json +317 -0
  407. package/src/surfaces/panel-stylebook-layers.ts +155 -0
  408. package/src/surfaces/preferences.json +850 -0
  409. package/src/surfaces/preferences.ts +332 -0
  410. package/src/surfaces/progress-modal.json +110 -0
  411. package/src/surfaces/progress-modal.ts +150 -0
  412. package/src/surfaces/properties-panel.json +1578 -0
  413. package/src/surfaces/properties-panel.ts +337 -0
  414. package/src/surfaces/publish.json +945 -0
  415. package/src/surfaces/publish.ts +392 -0
  416. package/src/surfaces/push-plan.json +110 -0
  417. package/src/surfaces/push-plan.ts +87 -0
  418. package/src/surfaces/rail.json +255 -0
  419. package/src/surfaces/rail.ts +206 -0
  420. package/src/surfaces/reference-field.json +191 -0
  421. package/src/surfaces/reference-field.ts +168 -0
  422. package/src/surfaces/schema-builder.json +1011 -0
  423. package/src/surfaces/schema-builder.ts +181 -0
  424. package/src/surfaces/schema-form.json +1013 -0
  425. package/src/surfaces/schema-form.ts +287 -0
  426. package/src/surfaces/secret-field.json +60 -0
  427. package/src/surfaces/secret-field.ts +146 -0
  428. package/src/surfaces/seo.json +932 -0
  429. package/src/surfaces/seo.ts +334 -0
  430. package/src/surfaces/settings-contexts.json +538 -0
  431. package/src/surfaces/settings-contexts.ts +196 -0
  432. package/src/surfaces/settings-contributed.json +489 -0
  433. package/src/surfaces/settings-contributed.ts +199 -0
  434. package/src/surfaces/settings-css-vars.json +820 -0
  435. package/src/surfaces/settings-css-vars.ts +199 -0
  436. package/src/surfaces/settings-defs.json +1363 -0
  437. package/src/surfaces/settings-defs.ts +232 -0
  438. package/src/surfaces/settings-deploy.json +108 -0
  439. package/src/surfaces/settings-deploy.ts +135 -0
  440. package/src/surfaces/settings-extensions.json +486 -0
  441. package/src/surfaces/settings-extensions.ts +201 -0
  442. package/src/surfaces/settings-head.json +779 -0
  443. package/src/surfaces/settings-head.ts +397 -0
  444. package/src/surfaces/settings-locales.json +420 -0
  445. package/src/surfaces/settings-locales.ts +137 -0
  446. package/src/surfaces/settings-overview.json +423 -0
  447. package/src/surfaces/settings-overview.ts +218 -0
  448. package/src/surfaces/settings-packages.json +549 -0
  449. package/src/surfaces/settings-packages.ts +158 -0
  450. package/src/surfaces/settings-pane.json +147 -0
  451. package/src/surfaces/settings-pane.ts +150 -0
  452. package/src/surfaces/settings-rawjson.json +104 -0
  453. package/src/surfaces/settings-rawjson.ts +107 -0
  454. package/src/surfaces/shell.json +99 -0
  455. package/src/surfaces/shell.ts +30 -0
  456. package/src/surfaces/slash-menu.json +180 -0
  457. package/src/surfaces/slash-menu.ts +272 -0
  458. package/src/surfaces/statements.json +607 -0
  459. package/src/surfaces/statements.ts +238 -0
  460. package/src/surfaces/statusbar.json +94 -0
  461. package/src/{panels → surfaces}/statusbar.ts +164 -90
  462. package/src/surfaces/style-panel.json +1643 -0
  463. package/src/surfaces/style-panel.ts +338 -0
  464. package/src/surfaces/stylebook-chrome.json +122 -0
  465. package/src/surfaces/stylebook-chrome.ts +122 -0
  466. package/src/surfaces/tab-strip.json +448 -0
  467. package/src/surfaces/tab-strip.ts +200 -0
  468. package/src/surfaces/target-line.json +419 -0
  469. package/src/surfaces/target-line.ts +349 -0
  470. package/src/surfaces/toasts.json +189 -0
  471. package/src/surfaces/welcome.json +571 -0
  472. package/src/surfaces/welcome.ts +338 -0
  473. package/src/tabs/project-config.ts +111 -21
  474. package/src/tabs/tab.ts +52 -1
  475. package/src/types.ts +22 -0
  476. package/src/ui/ai-credentials-form.ts +104 -100
  477. package/src/ui/ai-managed-connect.ts +54 -36
  478. package/src/ui/ai-model-picker.ts +75 -47
  479. package/src/ui/cf-account-picker.ts +76 -141
  480. package/src/ui/color-selector.ts +80 -301
  481. package/src/ui/dynamic-slot.ts +43 -283
  482. package/src/ui/expression-editor.ts +797 -576
  483. package/src/ui/field-input.ts +8 -181
  484. package/src/ui/form-controls.ts +838 -366
  485. package/src/ui/formula-chips.ts +80 -85
  486. package/src/ui/kit.ts +33 -0
  487. package/src/ui/layers.ts +296 -593
  488. package/src/ui/media-picker.ts +203 -284
  489. package/src/ui/panel-resize.ts +251 -64
  490. package/src/ui/popover-surface.ts +153 -0
  491. package/src/ui/progress-modal.ts +22 -60
  492. package/src/ui/regions.ts +13 -7
  493. package/src/ui/schema-form.ts +906 -440
  494. package/src/ui/surface.ts +122 -0
  495. package/src/ui/unit-selector.ts +15 -131
  496. package/src/ui/value-source.ts +14 -0
  497. package/src/ui/virtual-window.ts +88 -9
  498. package/src/utils/canvas-media.ts +72 -0
  499. package/src/utils/edit-display.ts +7 -2
  500. package/src/utils/element-selectors.ts +69 -0
  501. package/src/utils/link-target.ts +2 -2
  502. package/src/utils/studio-utils.ts +0 -10
  503. package/src/view.ts +15 -0
  504. package/src/workspace/pane-derive.ts +36 -14
  505. package/src/workspace/workspace.ts +75 -6
  506. package/styles/canvas.css +72 -467
  507. package/styles/forced-colors.css +48 -82
  508. package/styles/forced-colors.json +41 -0
  509. package/styles/inspector.css +2 -1301
  510. package/styles/overlays.css +96 -2823
  511. package/styles/panels.css +6 -1423
  512. package/styles/shell-frame.css +272 -0
  513. package/styles/shell-frame.json +227 -0
  514. package/styles/shell.css +0 -1568
  515. package/styles/tokens.css +65 -136
  516. package/styles/tokens.json +84 -0
  517. package/dist/chunks/Overlay.dev-etqjwyf9.js +0 -23
  518. package/dist/chunks/adapter-jkcyhgtr.js +0 -14
  519. package/dist/chunks/cf-account-picker-en86v7x7.js +0 -163
  520. package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
  521. package/dist/chunks/components-zmx10t62.js +0 -21
  522. package/dist/chunks/content-source-48xg86wj.js +0 -49
  523. package/dist/chunks/convert-to-repeater-43tfn7cs.js +0 -268
  524. package/dist/chunks/editor-cptq5jp1.js +0 -37
  525. package/dist/chunks/entry-commands-e63f1xc0.js +0 -46
  526. package/dist/chunks/extension-sections-txv08xxg.js +0 -311
  527. package/dist/chunks/file-ops-wbh4hhsx.js +0 -48
  528. package/dist/chunks/files-3kfkwhk4.js +0 -93
  529. package/dist/chunks/flow-qbfgrpeh.js +0 -634
  530. package/dist/chunks/focus-trap.esm-9t2drp0g.js +0 -1025
  531. package/dist/chunks/focus-visible-6eg70z18.js +0 -167
  532. package/dist/chunks/format-host-w16q81tf.js +0 -43
  533. package/dist/chunks/fs-events-hrrg328g.js +0 -21
  534. package/dist/chunks/git-panel-532whr1y.js +0 -59
  535. package/dist/chunks/index-zqxmqxmh.js +0 -109
  536. package/dist/chunks/jx-validate-8er6gbxy.js +0 -19
  537. package/dist/chunks/layers-panel-07hm7g5j.js +0 -67
  538. package/dist/chunks/layers-q75tj2mf.js +0 -45
  539. package/dist/chunks/lit-html-j055mkh8.js +0 -22
  540. package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
  541. package/dist/chunks/notify-yjm79n4s.js +0 -30
  542. package/dist/chunks/parse-24d0gay3.js +0 -19
  543. package/dist/chunks/platform-ba1j1gbh.js +0 -16
  544. package/dist/chunks/publish-panel-v9st96ja.js +0 -29
  545. package/dist/chunks/quick-search-zccjp6gv.js +0 -66
  546. package/dist/chunks/references-3qx8fkyp.js +0 -29
  547. package/dist/chunks/right-panel-vm3b6kwy.js +0 -46
  548. package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
  549. package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +0 -18
  550. package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
  551. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
  552. package/dist/chunks/sp-tooltip.dev-4sastdnw.js +0 -15
  553. package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
  554. package/dist/chunks/studio-0a5qrh01.js +0 -3
  555. package/dist/chunks/studio-0bq6eh7v.js +0 -12
  556. package/dist/chunks/studio-18j3bmcz.js +0 -14
  557. package/dist/chunks/studio-1ngbejft.js +0 -181
  558. package/dist/chunks/studio-4b5z1x43.js +0 -424
  559. package/dist/chunks/studio-4n2b5gzb.js +0 -357
  560. package/dist/chunks/studio-4rzyrtjz.js +0 -426
  561. package/dist/chunks/studio-5fx3g9jg.js +0 -4325
  562. package/dist/chunks/studio-5myknm0a.js +0 -1903
  563. package/dist/chunks/studio-7afd21nw.js +0 -67
  564. package/dist/chunks/studio-7nte2hj4.js +0 -613
  565. package/dist/chunks/studio-8jpzqk5s.js +0 -128
  566. package/dist/chunks/studio-8q517dfr.js +0 -24
  567. package/dist/chunks/studio-950k0dx6.js +0 -6939
  568. package/dist/chunks/studio-9n4mrmnb.js +0 -184
  569. package/dist/chunks/studio-a0tswy0w.js +0 -58
  570. package/dist/chunks/studio-ahnzd9rg.js +0 -23
  571. package/dist/chunks/studio-bgxt93ga.js +0 -62
  572. package/dist/chunks/studio-cmb03bg5.js +0 -13890
  573. package/dist/chunks/studio-d1ks410h.js +0 -175
  574. package/dist/chunks/studio-ef49pnzx.js +0 -387
  575. package/dist/chunks/studio-eqq1w638.js +0 -113
  576. package/dist/chunks/studio-ew0djmhg.js +0 -341
  577. package/dist/chunks/studio-gfgb8pe6.js +0 -1652
  578. package/dist/chunks/studio-h4rxscfc.js +0 -10
  579. package/dist/chunks/studio-jc994dnr.js +0 -1066
  580. package/dist/chunks/studio-kyh9avvv.js +0 -28
  581. package/dist/chunks/studio-m3hwn7y1.js +0 -120
  582. package/dist/chunks/studio-mtfnaqs9.js +0 -659
  583. package/dist/chunks/studio-n6j9t10q.js +0 -318
  584. package/dist/chunks/studio-n8b9wqwy.js +0 -291
  585. package/dist/chunks/studio-qcy05dp8.js +0 -1743
  586. package/dist/chunks/studio-qq6jym04.js +0 -53
  587. package/dist/chunks/studio-rdx8jcxy.js +0 -83
  588. package/dist/chunks/studio-rn9ta2mz.js +0 -111
  589. package/dist/chunks/studio-t7qb9db8.js +0 -665
  590. package/dist/chunks/studio-tmc5emrw.js +0 -8
  591. package/dist/chunks/studio-z6nw2ek2.js +0 -18
  592. package/dist/chunks/studio-zczps70q.js +0 -378
  593. package/dist/chunks/studio-zqazsct2.js +0 -1823
  594. package/dist/chunks/workspace-fjx52ryv.js +0 -89
  595. package/src/panels/activity-bar.ts +0 -251
  596. package/src/panels/panel-scheduler.ts +0 -219
  597. package/src/panels/style-inputs.ts +0 -237
  598. package/src/panels/target-line.ts +0 -274
  599. package/src/panels/welcome-screen.ts +0 -346
  600. package/src/ui/button-group.ts +0 -102
  601. package/src/ui/choice-field.ts +0 -72
  602. package/src/ui/field-row.ts +0 -249
  603. package/src/ui/formula-palette.ts +0 -213
  604. package/src/ui/icons.ts +0 -52
  605. package/src/ui/jx-theme.ts +0 -130
  606. package/src/ui/spectrum.ts +0 -340
  607. package/src/ui/value-selector.ts +0 -174
  608. package/src/ui/widgets.ts +0 -172
  609. /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
  610. /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
  611. /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
  612. /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,16 @@ 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";
73
+ import { runReported } from "../commands/run-reported";
60
74
  import { editorKindForMode, makeContext } from "../commands/context";
61
75
  import { defaultCommands, noopCommandDeps } from "../commands/defaults";
76
+ import { emptyBlockBarView, mountBlockActionBar } from "../surfaces/block-action-bar";
77
+ import { openMenu } from "../surfaces/menu";
62
78
 
63
79
  import type { AnyCommand, CommandRegistry } from "../commands/registry";
64
80
  import type { CommandContext } from "../commands/context";
@@ -66,8 +82,9 @@ import type { CommandDeps } from "../commands/defaults";
66
82
  import type { ApplyFormatIntent } from "../canvas/iframe-protocol";
67
83
  import type { JxPath } from "../state";
68
84
  import type { JxMutableNode } from "@jxsuite/schema/types";
69
- import type { TemplateResult } from "lit-html";
70
85
  import type { SlashCommand } from "../editor/convert-targets.js";
86
+ import type { BlockBarSurface, BlockBarTool, BlockBarView } from "../surfaces/block-action-bar";
87
+ import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
71
88
 
72
89
  /**
73
90
  * The plain format commands — everything an action button posts except link/insertData.
@@ -137,7 +154,7 @@ let _scrollRafPending = false;
137
154
  * @param {Event} e
138
155
  */
139
156
  export function onCanvasScroll(e: Event): void {
140
- if (!_ctx || _linkPopoverOpen) {
157
+ if (!_ctx || isLinkPopoverOpen()) {
141
158
  return;
142
159
  }
143
160
  if (activeTab.value?.session.selection.length === 0) {
@@ -162,27 +179,32 @@ export function onCanvasScroll(e: Event): void {
162
179
  }
163
180
 
164
181
  /**
165
- * Style-only fast path: move the existing bar to the anchor's current position (fresh
182
+ * Position-only fast path: move the standing bar to the anchor's current position (fresh
166
183
  * {@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.
184
+ * when the anchor left the canvas area so the mounted document — and with it the drag handle's dnd
185
+ * registration and whatever holds the caret — survives; a bar hidden by the `visible` switch is
186
+ * genuinely torn down, which is right for a dismissal and wrong for a scroll that will bring the
187
+ * anchor straight back.
169
188
  */
170
189
  function repositionBlockActionBar(): void {
171
- const bar = view.blockActionBarEl?.firstElementChild as HTMLElement | null;
172
- if (!bar) {
190
+ const surface = _surface;
191
+ if (!surface || !_view.visible) {
173
192
  renderBlockActionBar();
174
193
  return;
175
194
  }
176
195
  const anchor = getEditBarAnchorRect();
177
196
  const pos = anchor ? barPosition(anchor) : null;
178
197
  if (!pos) {
179
- bar.style.visibility = "hidden";
198
+ applyBarView({ ..._view, offscreen: true });
180
199
  return;
181
200
  }
182
- bar.style.visibility = "";
183
- bar.style.left = `${pos.left}px`;
184
- bar.style.top = `${pos.top}px`;
185
- clampBarToWindow(bar);
201
+ applyBarView({ ..._view, barX: pos.left, barY: pos.top, offscreen: false });
202
+ requestAnimationFrame(() => {
203
+ const bar = surface.bar();
204
+ if (bar) {
205
+ clampBarToWindow(bar);
206
+ }
207
+ });
186
208
  }
187
209
 
188
210
  /**
@@ -219,12 +241,25 @@ function barPosition(anchor: {
219
241
  /**
220
242
  * Pull the bar back inside the window's right edge.
221
243
  *
244
+ * A measurement and a write of the value it measured, which is why it stays imperative and stays
245
+ * here: the bar's own box is the input, and no projection can carry a number that depends on the
246
+ * layout the projection produced. It writes the same custom property the document's `left` reads.
247
+ *
248
+ * **It releases its own previous write before measuring, and that is load-bearing.** The projected
249
+ * position is set on the document's ROOT and reaches the bar by inheritance, while this override is
250
+ * set on the bar itself — and a value an element carries always beats one it inherits, at every
251
+ * specificity. So a clamp that was never released would pin the bar at the right edge for the rest
252
+ * of the session, and every later `barX` would be projected into a variable nothing reads. Removing
253
+ * it first also makes the measurement the right one: the box is read at the position the projection
254
+ * asked for, rather than at the position the last clamp left it in.
255
+ *
222
256
  * @param {HTMLElement} bar
223
257
  */
224
258
  function clampBarToWindow(bar: HTMLElement): void {
259
+ bar.style.removeProperty("--jx-bar-x");
225
260
  const barRect = rectOf(bar);
226
261
  if (barRect.right > window.innerWidth) {
227
- bar.style.left = `${Math.max(0, window.innerWidth - barRect.width)}px`;
262
+ bar.style.setProperty("--jx-bar-x", `${Math.max(0, window.innerWidth - barRect.width)}px`);
228
263
  }
229
264
  }
230
265
 
@@ -282,17 +317,17 @@ export function formatCommands(): AnyCommand[] {
282
317
  });
283
318
 
284
319
  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+`"),
320
+ record("bold", "Bold", "text-b", "mod+b"),
321
+ record("italic", "Italic", "text-italic", "mod+i"),
322
+ record("underline", "Underline", "text-underline", "mod+u"),
323
+ record("strikethrough", "Strikethrough", "text-strikethrough"),
324
+ record("superscript", "Superscript", "text-superscript"),
325
+ record("subscript", "Subscript", "text-subscript"),
326
+ record("code", "Code", "code", "mod+`"),
292
327
  {
293
328
  category: "Edit",
294
329
  group: "6_format",
295
- icon: "sp-icon-link",
330
+ icon: "link",
296
331
  id: "format.link",
297
332
  /* ⌘K, at `caret` scope, beside `palette.open`'s global ⌘K. Two scopes may hold one chord —
298
333
  that is what a shadowing ladder is — and the caret's wins only while a caret is in the
@@ -632,7 +667,7 @@ export function registerSelectionCommands(
632
667
  category: "Selection",
633
668
  enablement: canMoveUp,
634
669
  group: "1_move_1",
635
- icon: "sp-icon-arrow-up",
670
+ icon: "arrow-up",
636
671
  id: "selection.moveUp",
637
672
  keyScope: "canvas",
638
673
  level: "selection",
@@ -647,7 +682,7 @@ export function registerSelectionCommands(
647
682
  category: "Selection",
648
683
  enablement: canMoveDown,
649
684
  group: "1_move_2",
650
- icon: "sp-icon-arrow-down",
685
+ icon: "arrow-down",
651
686
  id: "selection.moveDown",
652
687
  keyScope: "canvas",
653
688
  level: "selection",
@@ -662,7 +697,7 @@ export function registerSelectionCommands(
662
697
  category: "Selection",
663
698
  enablement: canMoveIn,
664
699
  group: "1_move_3",
665
- icon: "sp-icon-arrow-right",
700
+ icon: "arrow-right",
666
701
  id: "selection.moveIn",
667
702
  keyScope: "canvas",
668
703
  level: "selection",
@@ -677,7 +712,7 @@ export function registerSelectionCommands(
677
712
  category: "Selection",
678
713
  enablement: canMoveOut,
679
714
  group: "1_move_4",
680
- icon: "sp-icon-arrow-left",
715
+ icon: "arrow-left",
681
716
  id: "selection.moveOut",
682
717
  keyScope: "canvas",
683
718
  level: "selection",
@@ -698,7 +733,7 @@ export function registerSelectionCommands(
698
733
  enablement: (ctx) =>
699
734
  Boolean(ctx.selection.kind) && !ctx.selection.isRoot && ctx.editor.kind === "canvas",
700
735
  group: "4_component",
701
- icon: "sp-icon-box",
736
+ icon: "cube",
702
737
  id: "selection.convertToComponent",
703
738
  level: "selection",
704
739
  // Three surfaces, ONE record. `editor/context-menu.ts` carried a second `convertToComponent`
@@ -718,7 +753,7 @@ export function registerSelectionCommands(
718
753
  {
719
754
  category: "Selection",
720
755
  group: "4_component",
721
- icon: "sp-icon-edit",
756
+ icon: "pencil-simple",
722
757
  id: "selection.editComponent",
723
758
  level: "selection",
724
759
  menus: ["blockbar", "context/element", "palette"],
@@ -796,43 +831,6 @@ export function selectionCommandRegistry(): CommandRegistry {
796
831
  /** How many verb buttons the bar renders before the rest fold into `⋮` (plan §3.2 ⑩). */
797
832
  export const BLOCKBAR_MAX_ITEMS = 5;
798
833
 
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
834
  /**
837
835
  * The tooltip: the chord when the control can act, the `requires` sentence when it cannot.
838
836
  *
@@ -848,11 +846,15 @@ export function commandTooltip(registry: CommandRegistry, command: AnyCommand):
848
846
  return chord ? `${command.title} (${chord})` : command.title;
849
847
  }
850
848
 
851
- /** Run a command, swallowing the refusal a disabled control should never have produced. */
849
+ /**
850
+ * Run a command, swallowing the refusal a disabled control should never have produced — and
851
+ * reporting, through `commands/run-reported.ts`, the one an enabled control can still meet: a `run`
852
+ * body that throws or rejects.
853
+ */
852
854
  export function runCommand(registry: CommandRegistry, id: string, target?: JxPath | null): void {
853
855
  const invoke = () => {
854
856
  if (registry.isEnabled(id)) {
855
- void registry.run(id);
857
+ void runReported(registry, id, undefined, "Editor");
856
858
  }
857
859
  };
858
860
  if (target) {
@@ -863,135 +865,141 @@ export function runCommand(registry: CommandRegistry, id: string, target?: JxPat
863
865
  }
864
866
 
865
867
  /**
866
- * Prevent the bar from stealing focus from contenteditable
868
+ * One button, projected from its record.
867
869
  *
868
- * @param {MouseEvent} e
870
+ * The surface is told what a button says, never asked to work it out: the glyph is the record's own
871
+ * kit name, the tooltip is {@link commandTooltip}, and `disabled` is the registry's refusal rather
872
+ * than a second opinion this module could hold about the same record.
869
873
  */
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();
874
+ function toolOf(
875
+ registry: CommandRegistry,
876
+ command: AnyCommand,
877
+ extra: { disabled?: boolean; selected?: boolean; value?: string } = {},
878
+ ): BlockBarTool {
879
+ return {
880
+ destructive: command.destructive === true,
881
+ disabled: extra.disabled ?? registry.disabledReason(command.id) !== undefined,
882
+ hasIcon: Boolean(command.icon),
883
+ icon: command.icon ?? "",
884
+ id: command.id,
885
+ selected: extra.selected ?? false,
886
+ title: command.title,
887
+ tooltip: commandTooltip(registry, command),
888
+ value: extra.value ?? "",
889
+ };
890
+ }
891
+
892
+ // ─── The mounted surface ─────────────────────────────────────────────────────
893
+
894
+ /** The bar's surface, mounted on the first render and kept for the life of the window. */
895
+ let _surface: BlockBarSurface | null = null;
896
+
897
+ /** The last projection handed to the surface, so a fast path can vary one field of it. */
898
+ let _view: BlockBarView = emptyBlockBarView();
899
+
900
+ /** Hand the surface a projection, remembering it. */
901
+ function applyBarView(next: BlockBarView): void {
902
+ _view = next;
903
+ _surface?.update(next);
881
904
  }
882
905
 
883
906
  /**
884
- * @param {MouseEvent} e
885
- * @param {import("../editor/convert-targets.js").SlashCommand[]} targets
886
- * @param {JxPath} selection
907
+ * The layer slot the bar draws into, mounting the document the first time it is asked for.
908
+ *
909
+ * One mount for the life of the window: a document reconciles by assignment, so hiding the bar is a
910
+ * `visible` switch rather than a teardown, and the caret, the drag registration and an open link
911
+ * panel all survive a repaint that would have re-rendered a template.
887
912
  */
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
- });
913
+ function ensureSurface(): BlockBarSurface {
914
+ if (!view.blockActionBarEl) {
915
+ view.blockActionBarEl = getLayerSlot("popover", "block-action-bar");
916
+ }
917
+ _surface ??= mountBlockActionBar(view.blockActionBarEl, {
918
+ applyLink: (href) => {
919
+ postApplyFormat({ command: "link", href: href || "" });
897
920
  },
898
- showFilter: targets.length > 6,
921
+ leaveBar: () => {
922
+ dismissBlockBarOverflow();
923
+ returnFocusToCanvas();
924
+ },
925
+ onDragHandle: attachDragHandle,
926
+ openMergeTags: openMergeTagMenu,
927
+ openOverflow: (anchor) => {
928
+ const registry = selectionCommandRegistry();
929
+ showCommandOverflow(
930
+ anchor,
931
+ registry,
932
+ registry.forPlacement("blockbar").slice(BLOCKBAR_MAX_ITEMS),
933
+ );
934
+ },
935
+ openTagMenu: openConvertMenu,
936
+ removeLink: () => {
937
+ postApplyFormat({ command: "link", href: null });
938
+ },
939
+ run: (id) => runCommand(selectionCommandRegistry(), id),
940
+ selectParent: () => runCommand(selectionCommandRegistry(), "selection.selectParent"),
899
941
  });
942
+ return _surface;
900
943
  }
901
944
 
902
945
  /**
903
- * The parent selector.
946
+ * Register the drag handle the document just drew.
904
947
  *
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.
948
+ * Released before installed, always. The handle's node is re-created every time the bar is hidden
949
+ * and shown again, and two live pragmatic-dnd registrations on one handle is a drag that fires
950
+ * twice — `view.selDragCleanup` is the single field that guarantees there is never more than one.
951
+ * `canDrag` is asked at the PRESS rather than here, so a selection moving to the document root
952
+ * greys the handle without any re-registration at all.
953
+ *
954
+ * @param {HTMLElement} handle
909
955
  */
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
- >`;
956
+ function attachDragHandle(handle: HTMLElement): void {
957
+ view.selDragCleanup?.();
958
+ view.selDragCleanup = draggable({
959
+ canDrag: () => structuralTarget(primarySelection(activeTab.value?.session.selection)) !== null,
960
+ element: handle,
961
+ getInitialData: () => ({
962
+ // Snapshot the selection: the live array is a Vue reactive proxy, which structured clone
963
+ // Rejects when the src crosses postMessage (DataCloneError killed the whole handle drag),
964
+ // And a live reference would also mutate the retained srcData if the selection changed
965
+ // Mid-drag.
966
+ path: [...(primarySelection(activeTab.value?.session.selection) ?? [])],
967
+ type: "tree-node",
968
+ }),
969
+ onGenerateDragPreview: ({
970
+ nativeSetDragImage,
971
+ }: {
972
+ nativeSetDragImage: ((image: Element, x: number, y: number) => void) | null;
973
+ }) => {
974
+ // Suppress the native drag image; the cross-frame ghost is the drag affordance.
975
+ disableNativeDragPreview({ nativeSetDragImage });
976
+ },
977
+ });
958
978
  }
959
979
 
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
- }
980
+ // ─── The `⋮` menu ────────────────────────────────────────────────────────────
980
981
 
981
982
  /** 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;
983
+ let _overflowHandle: MenuHandle | null = null;
983
984
 
984
985
  /** Close the block action bar's `⋮` menu if it is open. */
985
986
  export function dismissBlockBarOverflow(): void {
986
- _overflowHandle?.dismiss();
987
+ const handle = _overflowHandle;
987
988
  _overflowHandle = null;
989
+ handle?.close();
990
+ if (_view.overflowOpen) {
991
+ applyBarView({ ..._view, overflowOpen: false });
992
+ }
988
993
  }
989
994
 
990
995
  /**
991
996
  * Show the `⋮` menu under `anchor`. Rows carry the same names, chords and refusals as the buttons.
992
997
  *
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.
998
+ * Shared by the block action bar and the Outline rows, and it is the KIT menu
999
+ * (`surfaces/menu.json`, §12.5) rather than a list of this surface's own: one popover, one roving
1000
+ * caret, one typeahead, one Escape. `target` (the Outline row's node) is captured per row and
1001
+ * re-applied when a row is chosen, because a row action acts on the row's node and the hovered row
1002
+ * is not the selected one.
995
1003
  */
996
1004
  export function showCommandOverflow(
997
1005
  anchor: HTMLElement,
@@ -1000,95 +1008,46 @@ export function showCommandOverflow(
1000
1008
  target: JxPath | null = null,
1001
1009
  ): void {
1002
1010
  dismissBlockBarOverflow();
1003
- const rect = rectOf(anchor);
1004
1011
  // 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
- }));
1012
+ // Rows are handed over — the menu prints what it is given.
1013
+ const project = (): MenuRowProjection[] =>
1014
+ commands.map((command, index) => {
1015
+ const chord = registry.keymap.formatBinding(command.id);
1016
+ const reason = registry.disabledReason(command.id);
1017
+ return {
1018
+ destructive: command.destructive === true,
1019
+ disabled: reason !== undefined,
1020
+ dividerAbove: index > 0 && command.destructive === true,
1021
+ id: command.id,
1022
+ run: () => {
1023
+ runCommand(registry, command.id, target);
1024
+ },
1025
+ title: command.title,
1026
+ ...(chord ? { chord } : {}),
1027
+ ...(reason === undefined ? {} : { requires: reason }),
1028
+ };
1029
+ });
1015
1030
  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: () => {
1031
+ _overflowHandle = openMenu({
1032
+ label: "More block actions",
1033
+ onClosed: (closed) => {
1034
+ if (_overflowHandle === closed) {
1048
1035
  _overflowHandle = null;
1049
- },
1036
+ if (_view.overflowOpen) {
1037
+ applyBarView({ ..._view, overflowOpen: false });
1038
+ }
1039
+ }
1050
1040
  },
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;
1041
+ opener: anchor,
1042
+ region: "block-actions",
1043
+ rows,
1044
+ });
1045
+ if (!target) {
1046
+ applyBarView({ ..._view, overflowOpen: true });
1077
1047
  }
1078
1048
  }
1079
1049
 
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
- }
1050
+ // ─── Keyboard ────────────────────────────────────────────────────────────────
1092
1051
 
1093
1052
  /** Put the keyboard back where it came from — the canvas iframe, else its wrapper. */
1094
1053
  function returnFocusToCanvas(): void {
@@ -1099,32 +1058,6 @@ function returnFocusToCanvas(): void {
1099
1058
  (iframe ?? stage)?.focus();
1100
1059
  }
1101
1060
 
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
1061
  /**
1129
1062
  * ⌥↑ enters the bar from the canvas.
1130
1063
  *
@@ -1132,6 +1065,11 @@ export function onToolbarKeydown(e: KeyboardEvent): void {
1132
1065
  * inside the canvas iframe does not arrive: `canvas/iframe-keys.ts` forwards bare navigation keys
1133
1066
  * only when the target is not editable, and the canvas root is permanently `contenteditable`.
1134
1067
  * Exported so a test can dispatch it directly.
1068
+ *
1069
+ * The caret it places is the kit's: `jx-action-group` owns the roving tabindex and every arrow key
1070
+ * after this one, so entering the bar is the only part of the toolbar keyboard this module holds.
1071
+ *
1072
+ * @param {KeyboardEvent} e
1135
1073
  */
1136
1074
  export function handleBlockBarEntryKey(e: KeyboardEvent): void {
1137
1075
  if (e.key !== "ArrowUp" || !e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
@@ -1140,24 +1078,74 @@ export function handleBlockBarEntryKey(e: KeyboardEvent): void {
1140
1078
  if (isModalOpen()) {
1141
1079
  return;
1142
1080
  }
1143
- const bar = view.blockActionBarEl?.querySelector<HTMLElement>(".block-action-bar");
1144
- if (!bar) {
1081
+ const toolbar = _surface?.toolbar();
1082
+ if (!toolbar) {
1145
1083
  return;
1146
1084
  }
1147
1085
  e.preventDefault();
1148
- focusToolbarItem(bar, 0);
1086
+ focusItem(toolbar, 0);
1087
+ }
1088
+
1089
+ // ─── The badge's convert list and the merge-tag list ─────────────────────────
1090
+
1091
+ /**
1092
+ * The tags this node may be converted into — empty for a component instance and for a repeater,
1093
+ * whose content is a `map` template rather than a tag.
1094
+ *
1095
+ * @param {JxMutableNode | null} node
1096
+ */
1097
+ function convertTargetsFor(node: JxMutableNode | null): SlashCommand[] {
1098
+ if (!node) {
1099
+ return [];
1100
+ }
1101
+ const literal = displayTagName(node.tagName);
1102
+ const isComponent = literal.includes("-") && componentRegistry.some((c) => c.tagName === literal);
1103
+ if (isComponent || node.$prototype === "Array") {
1104
+ return [];
1105
+ }
1106
+ const children = childList(node);
1107
+ const isEmpty =
1108
+ !node.textContent &&
1109
+ (children.length === 0 ||
1110
+ (children.length === 1 && typeof children[0] === "object" && children[0]?.tagName === "br"));
1111
+ return getConvertTargets((literal || "div").toLowerCase(), isEmpty);
1112
+ }
1113
+
1114
+ /**
1115
+ * Open the badge's convert list under it, and retag the node on a choice.
1116
+ *
1117
+ * Resolved at the PRESS rather than captured at projection time: the badge is one node for the life
1118
+ * of the mount, so a closure taken when it was drawn would retag whatever was selected then.
1119
+ *
1120
+ * @param {HTMLElement} anchor
1121
+ */
1122
+ function openConvertMenu(anchor: HTMLElement): void {
1123
+ const tab = activeTab.value;
1124
+ const selection = primarySelection(tab?.session.selection);
1125
+ const node = tab && selection ? getNodeAtPath(tab.doc.document, selection) : null;
1126
+ const targets = convertTargetsFor(node);
1127
+ if (!selection || targets.length === 0) {
1128
+ return;
1129
+ }
1130
+ showSlashMenu(anchor, "", {
1131
+ commands: targets,
1132
+ onSelect: (cmd) => {
1133
+ transactDoc(activeTab.value, (t) => {
1134
+ mutateUpdateProperty(t, selection, "tagName", cmd.tag);
1135
+ });
1136
+ },
1137
+ showFilter: targets.length > 6,
1138
+ });
1149
1139
  }
1150
1140
 
1151
1141
  /**
1152
1142
  * 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).
1143
+ * the current state. Reuses the shared slash-menu surface (filter + keyboard nav + dismiss).
1154
1144
  * Selecting a token posts an `insertData` intent the iframe applies at its caret.
1155
1145
  *
1156
- * @param {MouseEvent} e
1146
+ * @param {HTMLElement} anchor
1157
1147
  */
1158
- function onMergeTagClick(e: MouseEvent) {
1159
- e.stopPropagation();
1160
- const anchorEl = e.currentTarget as HTMLElement;
1148
+ function openMergeTagMenu(anchor: HTMLElement): void {
1161
1149
  const tab = activeTab.value;
1162
1150
  const state = (tab?.doc.document.state ?? {}) as Record<string, unknown>;
1163
1151
  // The live resolved scope lives inside the iframe realm and is not threaded out, so the parent
@@ -1169,45 +1157,79 @@ function onMergeTagClick(e: MouseEvent) {
1169
1157
  // Parent-side from schema, keyed off the selected element's doc path (which carries a `map` segment).
1170
1158
  const selPath = getEditSnapshot().snapshot?.path ?? primarySelection(tab?.session.selection);
1171
1159
  const arrayNode = tab && selPath ? findEnclosingRepeater(tab.doc.document, selPath) : null;
1172
- if (arrayNode) {
1160
+ if (arrayNode && tab) {
1173
1161
  const tokens = resolveRepeaterItemFields(
1174
1162
  arrayNode,
1175
- tab!.doc.document.state as Record<string, unknown>,
1163
+ tab.doc.document.state as Record<string, unknown>,
1176
1164
  projectState?.projectConfig,
1177
1165
  );
1178
1166
  tags.push(...buildRepeaterTagsFromFields(tokens));
1179
1167
  }
1180
1168
 
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,
1169
+ showSlashMenu(anchor, "", {
1170
+ commands: tags.map((t) => ({ description: t.hint, label: t.label, tag: t.token })),
1189
1171
  onSelect: (cmd) => postApplyFormat({ command: "insertData", token: cmd.tag }),
1190
1172
  showFilter: true,
1191
1173
  });
1192
1174
  }
1193
1175
 
1176
+ // ─── The link panel ──────────────────────────────────────────────────────────
1177
+
1194
1178
  /** Dismiss the link popover if open. */
1195
- export function dismissLinkPopover() {
1196
- _linkPopoverOpen = false;
1197
- const host = getLayerSlot("popover", "link-popover");
1198
- litRender(nothing, host);
1179
+ export function dismissLinkPopover(): void {
1180
+ _surface?.closeLink();
1181
+ }
1182
+
1183
+ /** True while the link URL popover is open (so a scroll does not move the bar out from under it). */
1184
+ export function isLinkPopoverOpen(): boolean {
1185
+ return _surface?.linkOpen() === true;
1186
+ }
1187
+
1188
+ /**
1189
+ * Open the link popover, anchored to the toolbar's Link button if it is on screen, else the bar.
1190
+ *
1191
+ * The iframe owns the Selection, so the existing-link state comes from the latest selection
1192
+ * snapshot; Apply/Remove post `applyFormat` link intents the iframe applies.
1193
+ *
1194
+ * By RECORD ID, not by rendered text. This queried `sp-action-button[title^="Link"]`, which is the
1195
+ * addressing §13's first rule bans in the screenshot manifest — a match on a title the app DERIVES
1196
+ * (it now reads "Link… (⌘K)", and on Windows it reads something else again). The id is the input
1197
+ * the app accepts, and the button is one the surface ANNOUNCED as it drew it rather than one this
1198
+ * module re-finds.
1199
+ */
1200
+ export function openLinkPopoverFromShortcut(): void {
1201
+ const surface = _surface;
1202
+ const anchor = surface?.button("format.link") ?? surface?.bar();
1203
+ if (!surface || !anchor) {
1204
+ return;
1205
+ }
1206
+ const link = getEditSnapshot().snapshot?.link ?? { active: false, href: null };
1207
+ surface.openLink(anchor, link.href, link.active);
1199
1208
  }
1200
1209
 
1210
+ // ─── Dismissal and suppression ───────────────────────────────────────────────
1211
+
1201
1212
  /** Dismiss the block action bar. */
1202
- export function dismissBlockActionBar() {
1213
+ export function dismissBlockActionBar(): void {
1203
1214
  /* A dismiss is the bar's STRUCTURAL teardown — `canvas/canvas-render.ts` calls it when a stage is
1204
1215
  torn down and again on a mode transition — so it also drops any suppression. The bar it would
1205
1216
  otherwise be holding down does not exist any more; what comes back is a different one. */
1206
1217
  _suppressedFor = null;
1207
1218
  dismissBlockBarOverflow();
1208
- if (view.blockActionBarEl) {
1209
- litRender(nothing, view.blockActionBarEl);
1219
+ hideBar();
1220
+ }
1221
+
1222
+ /**
1223
+ * Take the bar off the screen: the `visible` switch, plus the drag registration that goes with the
1224
+ * node the switch is about to remove. The mount itself stays — it is the one thing a repaint must
1225
+ * never rebuild.
1226
+ */
1227
+ function hideBar(): void {
1228
+ if (_surface) {
1229
+ applyBarView({ ...emptyBlockBarView(), visible: false });
1210
1230
  }
1231
+ view.selDragCleanup?.();
1232
+ view.selDragCleanup = null;
1211
1233
  }
1212
1234
 
1213
1235
  /**
@@ -1258,9 +1280,6 @@ function suppressionKey(tab: ActiveTab | null, path: JxPath | null): string {
1258
1280
  export function suppressBlockActionBar(): void {
1259
1281
  // Clears any earlier key as its first act; this selection's replaces it below.
1260
1282
  dismissBlockActionBar();
1261
- if (_linkPopoverOpen) {
1262
- dismissLinkPopover();
1263
- }
1264
1283
  const tab = activeTab.value;
1265
1284
  _suppressedFor = { key: suppressionKey(tab, primarySelection(tab?.session.selection)) };
1266
1285
  }
@@ -1285,22 +1304,13 @@ export function releaseBlockActionBar(): void {
1285
1304
  }
1286
1305
 
1287
1306
  /**
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.
1307
+ * Whether `target` sits inside the edit-session chrome — the block action bar, its `⋮` menu, or the
1308
+ * slash menu. The parent's pointerdown commit-guard must NOT end the inline-edit session for clicks
1309
+ * on these (they operate ON the session); any other parent-chrome press commits it. This module
1310
+ * owns those popover roots, hence the helper lives here.
1311
+ *
1312
+ * The link panel is no longer named separately: it is a `jx-popover` inside the bar's own document,
1313
+ * so the bar's host contains it and one root answers for both.
1304
1314
  *
1305
1315
  * @param {EventTarget | null} target
1306
1316
  */
@@ -1311,369 +1321,158 @@ export function isEditChromeTarget(target: EventTarget | null): boolean {
1311
1321
  const roots = [
1312
1322
  view.blockActionBarEl,
1313
1323
  _overflowHandle?.host ?? null,
1314
- getLayerSlot("popover", "link-popover"),
1315
1324
  getLayerSlot("popover", "slash-menu"),
1316
1325
  ];
1317
1326
  return roots.some((root) => root != null && root.contains(target));
1318
1327
  }
1319
1328
 
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
- }
1329
+ // ─── The projection ──────────────────────────────────────────────────────────
1401
1330
 
1402
1331
  /**
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.
1332
+ * What the bar says right now, or `null` when it must not draw at all.
1333
+ *
1334
+ * Every branch that used to end in `litRender(nothing)` ends here in `null`, and the five reasons
1335
+ * are unchanged: no context, no tab, no selection, a canvas mode that is not design or edit, a
1336
+ * selection that resolves to no node, and an anchor the stage no longer shows.
1405
1337
  */
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
-
1338
+ function projectBar(): BlockBarView | null {
1446
1339
  const tab = activeTab.value;
1447
- const canvasMode = _ctx.getCanvasMode();
1448
-
1340
+ const canvasMode = _ctx?.getCanvasMode();
1449
1341
  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
1342
  if (!tab || !selection || (canvasMode !== "design" && canvasMode !== "edit")) {
1464
- litRender(nothing, view.blockActionBarEl);
1465
- return;
1343
+ return null;
1466
1344
  }
1467
-
1468
1345
  const node = getNodeAtPath(tab.doc.document, selection);
1469
1346
  if (!node) {
1470
- litRender(nothing, view.blockActionBarEl);
1471
- return;
1347
+ return null;
1472
1348
  }
1473
-
1474
1349
  // Position from the iframe-host's viewport-space anchor (the bar is position:fixed). The parent
1475
1350
  // Never reads the iframe DOM, so geometry crosses the bridge as the selection snapshot's rect.
1476
1351
  const anchor = getEditBarAnchorRect();
1477
1352
  const pos = anchor ? barPosition(anchor) : null;
1478
1353
  if (!pos) {
1479
- litRender(nothing, view.blockActionBarEl);
1480
- return;
1354
+ return null;
1481
1355
  }
1482
1356
 
1357
+ const registry = selectionCommandRegistry();
1483
1358
  const tag = (displayTagName(node.tagName) || "div").toLowerCase();
1484
-
1485
- // Inline format state, sourced from the iframe's selection snapshot.
1486
1359
  const { editingProp, snapshot } = getEditSnapshot();
1487
1360
  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).
1361
+ /* ONE bar, one shape. The format group used to appear only during an "inline edit session", so
1362
+ the toolbar rearranged itself under the author's cursor the moment they started typing. With a
1363
+ document-wide caret there is no session to be in or out of: the group is shown whenever the
1364
+ selected block can carry inline markup, and the buttons enable when there is a range to apply
1365
+ them to. A prop-bound block still suppresses it — it edits a single plain string (belt and
1366
+ braces: component tags have no $inlineActions, so `actions` is empty there anyway). */
1496
1367
  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
1368
  // Formatting applies to a RANGE: disabled for a collapsed caret, and for a block selected without
1502
1369
  // One at all (from the layers panel, or by a structural edit moving the selection).
1503
1370
  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
1371
  /* The SET is the schema's, the VERBS are the registry's (see {@link formatCommands}).
1525
1372
  `$inlineActions` says which of the eight this tag accepts and in what order — four on an `<h1>`,
1526
1373
  eight on a `<p>` — and each one's name, icon, chord and behaviour come off its record. An action
1527
1374
  the registry has no record for is dropped rather than drawn as a button that runs nothing, so
1528
1375
  adding a ninth verb to the data file without a record shows up as a missing button and as a red
1529
1376
  `tests/block-action-bar.test.ts`, not as a silent no-op. */
1530
- const formatButtons = actions.flatMap((action) => {
1377
+ const formats = actions.flatMap((action) => {
1531
1378
  const command = registry.get(`format.${action.command}`);
1532
- return command ? [{ action, command }] : [];
1379
+ return command
1380
+ ? [
1381
+ toolOf(registry, command, {
1382
+ disabled: formatDisabled && action.command !== "link",
1383
+ selected: snapshot?.activeTags.includes(action.tag) === true,
1384
+ value: action.tag,
1385
+ }),
1386
+ ]
1387
+ : [];
1533
1388
  });
1389
+
1390
+ // The verb cluster, sliced at the cap. `forPlacement` already dropped the records whose `when` is
1391
+ // False and sorted the rest by group; everything past the cap keeps its name and its chord in the
1392
+ // `⋮` menu rather than being silently unavailable.
1534
1393
  const placed = registry.forPlacement("blockbar");
1535
- const shown = placed.slice(0, BLOCKBAR_MAX_ITEMS);
1536
- const overflow = placed.slice(BLOCKBAR_MAX_ITEMS);
1394
+
1395
+ const parentPath = parentElementPath(selection);
1396
+ const parentNode = parentPath ? getNodeAtPath(tab.doc.document, parentPath) : null;
1397
+ const parentCommand = registry.get("selection.selectParent");
1398
+ /* The parent selector renders the `selection.selectParent` record — its name, its chord and its
1399
+ handler — but as fixed chrome rather than from `forPlacement("blockbar")`, because that record
1400
+ does not declare the placement. Its disabled state is "the selection has no parent", which the
1401
+ record's own `enablement` (a bare "there is a selection") cannot express yet. */
1402
+ const parentName = parentCommand?.title ?? "Select Parent";
1403
+ const parentLabel = parentNode ? `Select parent: ${nodeLabel(parentNode)}` : parentName;
1404
+ const parentChord = parentCommand ? registry.keymap.formatBinding(parentCommand.id) : undefined;
1405
+
1406
+ const convertTargets = convertTargetsFor(node);
1407
+ // Repeater ($prototype:"Array") pseudo-elements have no tagName — show the "Repeater → items"
1408
+ // Label (not a bare "div").
1409
+ const tagLabel =
1410
+ (node.$prototype === "Array"
1411
+ ? nodeLabel(node)
1412
+ : node.$id || displayTagName(node.tagName) || "div") +
1413
+ (editingProp ? ` · ${editingProp}` : "");
1537
1414
  // The handle is chrome, not a verb, so it renders on every selection; only a node that actually
1538
1415
  // Sits at a child index can be dragged, and at the root it is a disabled affordance rather than a
1539
1416
  // 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
- );
1417
+ const canDrag = structuralTarget(selection) !== null;
1418
+
1419
+ return {
1420
+ barX: pos.left,
1421
+ barY: pos.top,
1422
+ canDrag,
1423
+ dragHint: canDrag ? "Drag to reorder" : "Drag to reorder — the document root cannot move",
1424
+ formats,
1425
+ hasOverflow: placed.length > BLOCKBAR_MAX_ITEMS,
1426
+ offscreen: false,
1427
+ overflowOpen: _overflowHandle !== null,
1428
+ parentDisabled: !parentPath,
1429
+ parentHint: parentChord ? `${parentLabel} (${parentChord})` : parentLabel,
1430
+ parentLabel,
1431
+ showFormat,
1432
+ tagDisabled: convertTargets.length === 0,
1433
+ tagHint: convertTargets.length > 0 ? "Change element type" : tagLabel,
1434
+ tagLabel,
1435
+ tagPopup: convertTargets.length > 0 ? "menu" : "",
1436
+ verbs: placed.slice(0, BLOCKBAR_MAX_ITEMS).map((command) => toolOf(registry, command)),
1437
+ visible: true,
1438
+ };
1439
+ }
1665
1440
 
1666
- const rendered = view.blockActionBarEl.querySelector<HTMLElement>(".block-action-bar");
1667
- if (rendered) {
1668
- applyRovingTabindex(rendered);
1441
+ /** Render the unified block action bar above the selected element. */
1442
+ export function renderBlockActionBar(): void {
1443
+ if (!_ctx) {
1444
+ return;
1669
1445
  }
1446
+ ensureSurface();
1670
1447
 
1671
- // Post-render side effects
1672
- requestAnimationFrame(() => {
1673
- const bar = view.blockActionBarEl?.firstElementChild as HTMLElement | null;
1674
- if (!bar) {
1448
+ const tab = activeTab.value;
1449
+ 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
+ hideBar();
1675
1458
  return;
1676
1459
  }
1677
- clampBarToWindow(bar);
1460
+ _suppressedFor = null;
1461
+ }
1462
+
1463
+ const next = projectBar();
1464
+ if (!next) {
1465
+ hideBar();
1466
+ return;
1467
+ }
1468
+ applyBarView(next);
1469
+
1470
+ // Post-render side effect: the clamp measures a box that does not exist until the document has
1471
+ // Reconciled, so it waits a frame rather than reading a stale one.
1472
+ requestAnimationFrame(() => {
1473
+ const bar = _surface?.bar();
1474
+ if (bar) {
1475
+ clampBarToWindow(bar);
1476
+ }
1678
1477
  });
1679
1478
  }