@jxsuite/studio 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (607) hide show
  1. package/README.md +1 -1
  2. package/data/elements-meta.json +5 -15
  3. package/data/html-meta.json +51 -6
  4. package/dist/chunks/{2020-n6tfwnda.js → 2020-g79n01v9.js} +14 -14
  5. package/dist/chunks/action-group-sp6tna8v.js +118 -0
  6. package/dist/chunks/adapter-nj7tnjfn.js +13 -0
  7. package/dist/chunks/cf-account-picker-fyf63pmy.js +495 -0
  8. package/dist/chunks/cf-settings-71bv5zn9.js +20 -0
  9. package/dist/chunks/color-area-v17df7b8.js +154 -0
  10. package/dist/chunks/color-field-anwybnkn.js +208 -0
  11. package/dist/chunks/color-slider-44ez96fc.js +79 -0
  12. package/dist/chunks/color-xgg3kfw7.js +35 -0
  13. package/dist/chunks/combobox-5e7jev23.js +236 -0
  14. package/dist/chunks/components-2w1f35qs.js +18 -0
  15. package/dist/chunks/content-source-3wqeas45.js +46 -0
  16. package/dist/chunks/convert-to-repeater-d0cgnb6d.js +639 -0
  17. package/dist/chunks/dialog-s890b918.js +85 -0
  18. package/dist/chunks/editor-3qhbg51x.js +35 -0
  19. package/dist/chunks/entry-commands-d6g50j7s.js +42 -0
  20. package/dist/chunks/extension-sections-fy7q4fe9.js +651 -0
  21. package/dist/chunks/file-ops-3btj6st0.js +47 -0
  22. package/dist/chunks/files-ef8fgs3w.js +72 -0
  23. package/dist/chunks/format-host-3c33nwr8.js +40 -0
  24. package/dist/chunks/fs-events-13tj6sex.js +18 -0
  25. package/dist/chunks/git-panel-4wp2rn9b.js +49 -0
  26. package/dist/chunks/icons-yy10wk9r.js +402 -0
  27. package/dist/chunks/iframe-entry-15g3n6xv.js +114 -0
  28. package/dist/chunks/iframe-entry-b27vq81n.js +203 -0
  29. package/dist/chunks/iframe-entry-nbyxee1y.js +154 -0
  30. package/dist/chunks/iframe-host-jgesd924.js +115 -0
  31. package/dist/chunks/{index-wcnt3r9g.js → index-mdmyx6s2.js} +11 -11
  32. package/dist/chunks/index-n4j7qxyr.js +109 -0
  33. package/dist/chunks/{javascript-q8kpmfah.js → javascript-xvrfrpkb.js} +8 -9
  34. package/dist/chunks/{jsonMode-dkr040ga.js → jsonMode-tyx2kj6p.js} +526 -527
  35. package/dist/chunks/jx-validate-j0wxk72w.js +15 -0
  36. package/dist/chunks/layers-1nq5evsh.js +29 -0
  37. package/dist/chunks/layers-panel-ar2gh90g.js +40 -0
  38. package/dist/chunks/listbox-6v2ac08j.js +44 -0
  39. package/dist/chunks/lit-html-m244w9nh.js +17 -0
  40. package/dist/chunks/media-picker-2qbetq73.js +33 -0
  41. package/dist/chunks/menu-q99sy9rf.js +285 -0
  42. package/dist/chunks/menu-rf7n72mf.js +14 -0
  43. package/dist/chunks/{monaco-binding-rj6xpx9y.js → monaco-binding-sw6en4dw.js} +12 -15
  44. package/dist/chunks/monaco-buffer-bhy3hb6m.js +24 -0
  45. package/dist/chunks/{monaco-setup-vx9bhp1d.js → monaco-setup-8r62fj6y.js} +27 -37
  46. package/dist/chunks/notify-628t0anh.js +21 -0
  47. package/dist/chunks/number-field-5cndyft8.js +72 -0
  48. package/dist/chunks/parse-0zx14y15.js +10 -0
  49. package/dist/chunks/platform-ts9cg1a4.js +15 -0
  50. package/dist/chunks/popover-47z3ytva.js +25 -0
  51. package/dist/chunks/publish-panel-98f7sr3g.js +27 -0
  52. package/dist/chunks/quick-search-srzz83k2.js +45 -0
  53. package/dist/chunks/references-n553f0e1.js +26 -0
  54. package/dist/chunks/right-panel-rrgx2dn3.js +45 -0
  55. package/dist/chunks/section-registry-5pq32t0q.js +25 -0
  56. package/dist/chunks/select-2r16ez41.js +49 -0
  57. package/dist/chunks/split-g7nm1rgb.js +191 -0
  58. package/dist/chunks/studio-0hqbpbd0.js +62 -0
  59. package/dist/chunks/studio-18avnhjp.js +2430 -0
  60. package/dist/chunks/{studio-kryvsw6j.js → studio-1k325pwf.js} +48 -48
  61. package/dist/chunks/{studio-msd847zn.js → studio-25hkdrkw.js} +357 -86
  62. package/dist/chunks/studio-26d7myd1.js +258 -0
  63. package/dist/chunks/{studio-4r1pv6zf.js → studio-38emantv.js} +22 -5
  64. package/dist/chunks/studio-3gxv60tn.js +422 -0
  65. package/dist/chunks/{studio-fbf92ckf.js → studio-3hxkd92t.js} +275 -163
  66. package/dist/chunks/studio-4m0haseg.js +185 -0
  67. package/dist/chunks/{studio-ynxwkrzd.js → studio-4wbb9ct0.js} +1953 -1785
  68. package/dist/chunks/{studio-1wagb550.js → studio-5523bpzt.js} +11 -11
  69. package/dist/chunks/{studio-x4qkdat5.js → studio-57mf66w2.js} +1749 -2481
  70. package/dist/chunks/studio-5jy81t3s.js +17463 -0
  71. package/dist/chunks/studio-5rt7sqa0.js +1168 -0
  72. package/dist/chunks/studio-5te5tqng.js +81 -0
  73. package/dist/chunks/{studio-p4ngc046.js → studio-6ewqv390.js} +9 -9
  74. package/dist/chunks/{studio-9ab8gqsm.js → studio-6nzaktvy.js} +7 -7
  75. package/dist/chunks/studio-6xa21ej9.js +28 -0
  76. package/dist/chunks/{studio-5ehhackm.js → studio-727xh0e3.js} +19 -19
  77. package/dist/chunks/{studio-ahn3pdgx.js → studio-79y5sz51.js} +52 -38
  78. package/dist/chunks/{studio-gcye7qka.js → studio-7e11jpa9.js} +21 -21
  79. package/dist/chunks/{studio-q7v10cg2.js → studio-7r42c6ca.js} +17 -7
  80. package/dist/chunks/studio-7xptrg0j.js +954 -0
  81. package/dist/chunks/{studio-cft9r4r7.js → studio-81390n6f.js} +22 -22
  82. package/dist/chunks/{studio-q43a5y9h.js → studio-8jn5yhyn.js} +16 -16
  83. package/dist/chunks/studio-8qtrpzr3.js +839 -0
  84. package/dist/chunks/{studio-dg7zyt5x.js → studio-98b3z07n.js} +1 -1
  85. package/dist/chunks/studio-9xxz1q69.js +1356 -0
  86. package/dist/chunks/studio-akbww0j5.js +13615 -0
  87. package/dist/chunks/{studio-x813zp48.js → studio-arfpe389.js} +30 -30
  88. package/dist/chunks/{studio-trxrabqf.js → studio-az3zd49t.js} +475 -283
  89. package/dist/chunks/studio-cpvb2ed1.js +1171 -0
  90. package/dist/chunks/{studio-f3wqjtk5.js → studio-d0300636.js} +5076 -18
  91. package/dist/chunks/{studio-bgxt93ga.js → studio-d055c37e.js} +13 -13
  92. package/dist/chunks/studio-d1ja2x9r.js +362 -0
  93. package/dist/chunks/{studio-b9b6t1dg.js → studio-e1f24eff.js} +5664 -5666
  94. package/dist/chunks/{studio-3bqpr0zh.js → studio-em9nw8eh.js} +299 -110
  95. package/dist/chunks/studio-erdatzvp.js +1190 -0
  96. package/dist/chunks/{studio-g1wdche0.js → studio-esrxgwh6.js} +80 -80
  97. package/dist/chunks/studio-fabxk6r9.js +2811 -0
  98. package/dist/chunks/{studio-3n4ndrzh.js → studio-h4nvnjvz.js} +303 -303
  99. package/dist/chunks/{studio-dm19q43r.js → studio-hhvs4sdg.js} +10 -10
  100. package/dist/chunks/{studio-q9jd1a60.js → studio-kcgff4r0.js} +15 -18
  101. package/dist/chunks/{studio-685s0ytq.js → studio-nqgawfks.js} +572 -572
  102. package/dist/chunks/{studio-xrapxwwc.js → studio-p1dw8wfw.js} +55 -29
  103. package/dist/chunks/studio-pm6hgy7r.js +4763 -0
  104. package/dist/chunks/studio-q0a3yk4n.js +33 -0
  105. package/dist/chunks/{studio-wcy8trfx.js → studio-q3x06vmj.js} +23 -22
  106. package/dist/chunks/studio-qahxvnan.js +111 -0
  107. package/dist/chunks/{studio-53n8bp04.js → studio-r6xmmtfh.js} +1 -1
  108. package/dist/chunks/{studio-2b946z26.js → studio-rkstz940.js} +3658 -6134
  109. package/dist/chunks/{studio-tvgyw9xw.js → studio-rnyf3f8e.js} +83 -250
  110. package/dist/chunks/studio-rpyah2ew.js +2805 -0
  111. package/dist/chunks/studio-rs76k67r.js +12 -0
  112. package/dist/chunks/{studio-jkftxyvz.js → studio-s6z853aq.js} +5858 -2152
  113. package/dist/chunks/{studio-6yw4y3my.js → studio-sgdc5pfc.js} +8 -8
  114. package/dist/chunks/studio-tb3ev01j.js +763 -0
  115. package/dist/chunks/{studio-t5wqwzdh.js → studio-tjjk7jvk.js} +1 -1
  116. package/dist/chunks/studio-vhv5tgxm.js +51 -0
  117. package/dist/chunks/{studio-qjy2z9bv.js → studio-vz68yn9e.js} +32 -137
  118. package/dist/chunks/studio-wqpngpsk.js +174 -0
  119. package/dist/chunks/{studio-b55y6ewx.js → studio-x3rty6tr.js} +58 -58
  120. package/dist/chunks/studio-xsykpvsh.js +8448 -0
  121. package/dist/chunks/{studio-92ab2jqe.js → studio-y9zedyjn.js} +14 -14
  122. package/dist/chunks/studio-yxa81f45.js +12 -0
  123. package/dist/chunks/swatch-group-t0n6h2rn.js +139 -0
  124. package/dist/chunks/tabs-wbp1cdws.js +156 -0
  125. package/dist/chunks/textfield-abk6qyee.js +73 -0
  126. package/dist/chunks/toast-780pvfbv.js +27 -0
  127. package/dist/chunks/toast-host-3sqv46sn.js +116 -0
  128. package/dist/chunks/toolbar-xbh5bhtr.js +194 -0
  129. package/dist/chunks/tooltip-eptb614w.js +369 -0
  130. package/dist/chunks/tree-a73p0pqm.js +367 -0
  131. package/dist/chunks/{tsMode-76kr53q0.js → tsMode-jy4m86rv.js} +83 -85
  132. package/dist/chunks/workspace-wrew8zk4.js +69 -0
  133. package/dist/chunks/{ws-client-6shc5rze.js → ws-client-ny4zc51r.js} +20 -22
  134. package/dist/iframe-entry.js +1780 -667
  135. package/dist/studio.js +52819 -36480
  136. package/dist/workers/editor.worker.js +1040 -1085
  137. package/dist/workers/json.worker.js +1946 -1991
  138. package/dist/workers/ts.worker.js +4960 -5005
  139. package/index.html +1 -0
  140. package/package.json +18 -52
  141. package/src/about/about-modal.ts +72 -123
  142. package/src/browse/library-commands.ts +20 -49
  143. package/src/browse/library-layouts.ts +87 -218
  144. package/src/browse/library-pane.ts +406 -438
  145. package/src/canvas/canvas-helpers.ts +0 -49
  146. package/src/canvas/canvas-live-render.ts +25 -2
  147. package/src/canvas/canvas-render.ts +666 -231
  148. package/src/canvas/canvas-surface.ts +0 -1
  149. package/src/canvas/canvas-utils.ts +310 -117
  150. package/src/canvas/dialog-path.ts +76 -0
  151. package/src/canvas/dialog-state.ts +67 -0
  152. package/src/canvas/diff-marks.ts +252 -0
  153. package/src/canvas/diff-toolbar.ts +349 -0
  154. package/src/canvas/diff-view.ts +123 -0
  155. package/src/canvas/edit-width-drag.ts +187 -0
  156. package/src/canvas/edit-width.ts +150 -0
  157. package/src/canvas/iframe-entry.ts +125 -8
  158. package/src/canvas/iframe-host.ts +271 -15
  159. package/src/canvas/iframe-interaction.ts +133 -13
  160. package/src/canvas/iframe-patch.ts +7 -1
  161. package/src/canvas/iframe-position.ts +5 -6
  162. package/src/canvas/iframe-protocol.ts +106 -5
  163. package/src/canvas/iframe-render.ts +373 -5
  164. package/src/canvas/iframe-slash.ts +2 -2
  165. package/src/canvas/iframe-subtree.ts +5 -8
  166. package/src/canvas/path-mapping.ts +25 -3
  167. package/src/canvas/popover-path.ts +106 -0
  168. package/src/canvas/popover-state.ts +155 -0
  169. package/src/canvas/surface-registry.ts +21 -10
  170. package/src/collab/presence-chips.ts +51 -73
  171. package/src/commands/app-commands.ts +10 -0
  172. package/src/commands/budget.ts +12 -0
  173. package/src/commands/command-args.ts +369 -6
  174. package/src/commands/defaults.ts +44 -14
  175. package/src/commands/live-context.ts +11 -5
  176. package/src/commands/reference.ts +12 -2
  177. package/src/commands/registry.ts +165 -8
  178. package/src/content/entry-commands.ts +16 -23
  179. package/src/content/entry-editor.ts +144 -101
  180. package/src/editor/canvas-slash-bridge.ts +2 -2
  181. package/src/editor/context-menu.ts +49 -200
  182. package/src/editor/convert-to-component.ts +42 -82
  183. package/src/editor/convert-to-repeater.ts +132 -173
  184. package/src/editor/inline-edit.ts +11 -0
  185. package/src/editor/shortcuts.ts +65 -26
  186. package/src/editor/slash-menu.ts +150 -157
  187. package/src/files/file-ops.ts +117 -11
  188. package/src/files/files.ts +943 -660
  189. package/src/files/json-layout.ts +474 -0
  190. package/src/files/serialize-document.ts +8 -1
  191. package/src/format/convert-file.ts +16 -10
  192. package/src/format/format-host.ts +50 -1
  193. package/src/github/github-auth.ts +71 -29
  194. package/src/github/github-publish.ts +57 -65
  195. package/src/grid/cell-editors.ts +46 -4
  196. package/src/grid/cell-popovers.ts +65 -62
  197. package/src/grid/grid-open.ts +86 -64
  198. package/src/grid/grid-panel.ts +390 -469
  199. package/src/grid/redirects-grid.ts +40 -44
  200. package/src/hosting/layout.ts +2 -0
  201. package/src/i18n/i18n-commands.ts +127 -62
  202. package/src/media/media-pane.ts +83 -133
  203. package/src/new-project/add-repo-modal.ts +125 -176
  204. package/src/new-project/import-tab.ts +181 -252
  205. package/src/new-project/location-fields.ts +93 -160
  206. package/src/new-project/new-project-modal.ts +376 -367
  207. package/src/packages/jxsuite-update.ts +76 -18
  208. package/src/panels/activity-panel.ts +137 -104
  209. package/src/panels/ai-chat/chat-markdown.ts +15 -13
  210. package/src/panels/ai-chat/chat-view.ts +307 -472
  211. package/src/panels/ai-chat/composer.ts +175 -163
  212. package/src/panels/ai-chat/sessions-view.ts +31 -62
  213. package/src/panels/ai-panel.ts +210 -185
  214. package/src/panels/block-action-bar.ts +437 -643
  215. package/src/panels/bottom-dock.ts +112 -47
  216. package/src/panels/chat-panel.ts +19 -14
  217. package/src/panels/data-explorer.ts +141 -102
  218. package/src/panels/data-grid.ts +183 -150
  219. package/src/panels/dnd.ts +75 -20
  220. package/src/panels/editors.ts +26 -19
  221. package/src/panels/elements-panel.ts +243 -186
  222. package/src/panels/empty-state.ts +26 -42
  223. package/src/panels/events-panel.ts +950 -729
  224. package/src/panels/formula-workspace.ts +434 -207
  225. package/src/panels/frontmatter-fields.ts +185 -153
  226. package/src/panels/frontmatter-panel.ts +411 -187
  227. package/src/panels/git-diff-open.ts +148 -0
  228. package/src/panels/git-panel.ts +679 -605
  229. package/src/panels/head-panel.ts +617 -435
  230. package/src/panels/i18n-panel.ts +153 -133
  231. package/src/panels/imports-panel.ts +388 -414
  232. package/src/panels/jump-bar.ts +174 -147
  233. package/src/panels/layers-panel.ts +785 -671
  234. package/src/panels/left-panel.ts +166 -94
  235. package/src/panels/navigator-panels.ts +1 -1
  236. package/src/panels/pane-context.ts +529 -677
  237. package/src/panels/pane-grid.ts +265 -241
  238. package/src/panels/panel-registry.ts +15 -4
  239. package/src/panels/problems-panel.ts +177 -99
  240. package/src/panels/properties-panel.ts +1525 -966
  241. package/src/panels/provenance.ts +31 -113
  242. package/src/panels/quick-search.ts +217 -172
  243. package/src/panels/right-panel.ts +135 -169
  244. package/src/panels/seo-modal.ts +346 -167
  245. package/src/panels/settings-menu.ts +109 -646
  246. package/src/panels/settings-pane.ts +102 -86
  247. package/src/panels/signals-panel.ts +1318 -1172
  248. package/src/panels/statement-editor.ts +556 -428
  249. package/src/panels/style-panel.ts +1367 -630
  250. package/src/panels/style-utils.ts +36 -32
  251. package/src/panels/stylebook-doc.ts +30 -73
  252. package/src/panels/stylebook-layers-panel.ts +202 -109
  253. package/src/panels/stylebook-panel.ts +124 -88
  254. package/src/panels/tab-strip.ts +385 -287
  255. package/src/platforms/cloud.ts +106 -16
  256. package/src/platforms/devserver.ts +13 -0
  257. package/src/publish/deploy-checklist.ts +124 -78
  258. package/src/publish/publish-commands.ts +4 -12
  259. package/src/publish/publish-panel.ts +134 -328
  260. package/src/services/a11y-report.ts +100 -43
  261. package/src/services/ai-command-tools.ts +468 -0
  262. package/src/services/ai-session-store.ts +6 -0
  263. package/src/services/ai-system-prompt.ts +110 -18
  264. package/src/services/ai-tools.ts +28 -143
  265. package/src/services/ai-write-report.ts +174 -0
  266. package/src/services/document-assistant.ts +50 -2
  267. package/src/services/idle.ts +16 -14
  268. package/src/services/live-surfaces.ts +237 -0
  269. package/src/services/model-uri.ts +75 -0
  270. package/src/services/notify.ts +63 -6
  271. package/src/services/popover-report.ts +325 -0
  272. package/src/services/surface-registry.ts +56 -0
  273. package/src/services/tool-executor.ts +6 -0
  274. package/src/settings/contexts-section.ts +301 -299
  275. package/src/settings/contributed-section.ts +361 -222
  276. package/src/settings/css-vars-editor.ts +320 -325
  277. package/src/settings/defs-editor.ts +572 -535
  278. package/src/settings/dependencies-editor.ts +79 -107
  279. package/src/settings/extension-commands.ts +379 -0
  280. package/src/settings/extension-rows.ts +113 -0
  281. package/src/settings/extensions-section.ts +287 -0
  282. package/src/settings/general-settings.ts +130 -181
  283. package/src/settings/head-editor.ts +10 -305
  284. package/src/settings/locales-section.ts +146 -196
  285. package/src/settings/preferences-accounts.ts +1 -1
  286. package/src/settings/preferences-dialog.ts +350 -330
  287. package/src/settings/project-sections.ts +108 -181
  288. package/src/settings/schema-field-ui.ts +11 -399
  289. package/src/settings/section-registry.ts +8 -1
  290. package/src/settings/settings-document.ts +31 -29
  291. package/src/shell/tree.ts +42 -87
  292. package/src/shell.ts +48 -36
  293. package/src/studio.ts +90 -67
  294. package/src/style/token-ref.ts +0 -39
  295. package/src/surfaces/about.json +263 -0
  296. package/src/surfaces/about.ts +163 -0
  297. package/src/surfaces/add-repo.json +486 -0
  298. package/src/surfaces/add-repo.ts +236 -0
  299. package/src/surfaces/ai-chat.json +2185 -0
  300. package/src/surfaces/ai-chat.ts +549 -0
  301. package/src/surfaces/ai-credentials-form.json +332 -0
  302. package/src/surfaces/ai-credentials-form.ts +151 -0
  303. package/src/surfaces/ai-managed-connect.json +108 -0
  304. package/src/surfaces/ai-managed-connect.ts +123 -0
  305. package/src/surfaces/ai-model-picker.json +114 -0
  306. package/src/surfaces/ai-model-picker.ts +139 -0
  307. package/src/surfaces/block-action-bar.json +640 -0
  308. package/src/surfaces/block-action-bar.ts +368 -0
  309. package/src/surfaces/bottom-dock.json +154 -0
  310. package/src/surfaces/bottom-dock.ts +125 -0
  311. package/src/surfaces/canvas-stage.json +444 -0
  312. package/src/surfaces/canvas-stage.ts +253 -0
  313. package/src/surfaces/cf-account-picker.json +300 -0
  314. package/src/surfaces/cf-account-picker.ts +214 -0
  315. package/src/surfaces/commandbar.json +984 -0
  316. package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +414 -389
  317. package/src/surfaces/convert-repeater.json +309 -0
  318. package/src/surfaces/convert-repeater.ts +276 -0
  319. package/src/surfaces/data-actions.json +157 -0
  320. package/src/surfaces/data-actions.ts +115 -0
  321. package/src/surfaces/dialog.json +289 -0
  322. package/src/surfaces/dialog.ts +343 -0
  323. package/src/surfaces/diff-toolbar.json +266 -0
  324. package/src/surfaces/diff-toolbar.ts +139 -0
  325. package/src/surfaces/doc-header.json +739 -0
  326. package/src/surfaces/doc-header.ts +223 -0
  327. package/src/surfaces/empty-state.json +152 -0
  328. package/src/surfaces/empty-state.ts +173 -0
  329. package/src/surfaces/entry-editor.json +281 -0
  330. package/src/surfaces/entry-editor.ts +191 -0
  331. package/src/surfaces/expression-editor.json +977 -0
  332. package/src/surfaces/expression-editor.ts +266 -0
  333. package/src/surfaces/files-panel.json +592 -0
  334. package/src/surfaces/files-panel.ts +301 -0
  335. package/src/surfaces/formula-palette.json +364 -0
  336. package/src/surfaces/formula-palette.ts +340 -0
  337. package/src/surfaces/git-panel.json +1822 -0
  338. package/src/surfaces/git-panel.ts +272 -0
  339. package/src/surfaces/github-auth.json +112 -0
  340. package/src/surfaces/github-auth.ts +139 -0
  341. package/src/surfaces/github-publish.json +206 -0
  342. package/src/surfaces/github-publish.ts +169 -0
  343. package/src/surfaces/grid-cell.json +218 -0
  344. package/src/surfaces/grid-cell.ts +120 -0
  345. package/src/surfaces/grid-open.json +147 -0
  346. package/src/surfaces/grid-open.ts +87 -0
  347. package/src/surfaces/grid-panel.json +706 -0
  348. package/src/surfaces/grid-panel.ts +206 -0
  349. package/src/surfaces/grid-replace.json +179 -0
  350. package/src/surfaces/grid-replace.ts +89 -0
  351. package/src/surfaces/grid-views.json +446 -0
  352. package/src/surfaces/grid-views.ts +172 -0
  353. package/src/surfaces/inspector-dock.json +187 -0
  354. package/src/surfaces/inspector-dock.ts +161 -0
  355. package/src/surfaces/jump-bar.json +243 -0
  356. package/src/surfaces/jump-bar.ts +129 -0
  357. package/src/surfaces/jxsuite-update.json +146 -0
  358. package/src/surfaces/jxsuite-update.ts +149 -0
  359. package/src/surfaces/library-pane.json +1509 -0
  360. package/src/surfaces/library-pane.ts +363 -0
  361. package/src/surfaces/logic-panel.json +1949 -0
  362. package/src/surfaces/logic-panel.ts +369 -0
  363. package/src/surfaces/logic-workspace.json +685 -0
  364. package/src/surfaces/logic-workspace.ts +238 -0
  365. package/src/surfaces/media-browser.json +303 -0
  366. package/src/surfaces/media-browser.ts +145 -0
  367. package/src/surfaces/media-field.json +163 -0
  368. package/src/surfaces/media-field.ts +116 -0
  369. package/src/surfaces/media-pane.json +513 -0
  370. package/src/surfaces/media-pane.ts +197 -0
  371. package/src/surfaces/menu.json +232 -0
  372. package/src/surfaces/menu.ts +375 -0
  373. package/src/surfaces/navigator-dock.json +113 -0
  374. package/src/surfaces/navigator-dock.ts +130 -0
  375. package/src/surfaces/new-project.json +1654 -0
  376. package/src/surfaces/new-project.ts +411 -0
  377. package/src/surfaces/palette.json +535 -0
  378. package/src/surfaces/pane-context.json +1497 -0
  379. package/src/surfaces/pane-context.ts +513 -0
  380. package/src/surfaces/pane-grid.json +188 -0
  381. package/src/surfaces/pane-grid.ts +250 -0
  382. package/src/surfaces/panel-activity.json +563 -0
  383. package/src/surfaces/panel-activity.ts +187 -0
  384. package/src/surfaces/panel-data.json +141 -0
  385. package/src/surfaces/panel-data.ts +156 -0
  386. package/src/surfaces/panel-deploy-checklist.json +283 -0
  387. package/src/surfaces/panel-deploy-checklist.ts +175 -0
  388. package/src/surfaces/panel-elements.json +367 -0
  389. package/src/surfaces/panel-elements.ts +175 -0
  390. package/src/surfaces/panel-i18n.json +569 -0
  391. package/src/surfaces/panel-i18n.ts +212 -0
  392. package/src/surfaces/panel-imports.json +780 -0
  393. package/src/surfaces/panel-imports.ts +209 -0
  394. package/src/surfaces/panel-outline.json +734 -0
  395. package/src/surfaces/panel-outline.ts +326 -0
  396. package/src/surfaces/panel-page.json +1017 -0
  397. package/src/surfaces/panel-page.ts +240 -0
  398. package/src/surfaces/panel-problems.json +497 -0
  399. package/src/surfaces/panel-problems.ts +157 -0
  400. package/src/surfaces/panel-signals.json +1973 -0
  401. package/src/surfaces/panel-signals.ts +436 -0
  402. package/src/surfaces/panel-stylebook-layers.json +317 -0
  403. package/src/surfaces/panel-stylebook-layers.ts +155 -0
  404. package/src/surfaces/preferences.json +850 -0
  405. package/src/surfaces/preferences.ts +332 -0
  406. package/src/surfaces/progress-modal.json +110 -0
  407. package/src/surfaces/progress-modal.ts +150 -0
  408. package/src/surfaces/properties-panel.json +1578 -0
  409. package/src/surfaces/properties-panel.ts +337 -0
  410. package/src/surfaces/publish.json +945 -0
  411. package/src/surfaces/publish.ts +392 -0
  412. package/src/surfaces/push-plan.json +110 -0
  413. package/src/surfaces/push-plan.ts +87 -0
  414. package/src/surfaces/rail.json +255 -0
  415. package/src/surfaces/rail.ts +206 -0
  416. package/src/surfaces/reference-field.json +191 -0
  417. package/src/surfaces/reference-field.ts +168 -0
  418. package/src/surfaces/schema-builder.json +1011 -0
  419. package/src/surfaces/schema-builder.ts +181 -0
  420. package/src/surfaces/schema-form.json +1013 -0
  421. package/src/surfaces/schema-form.ts +287 -0
  422. package/src/surfaces/secret-field.json +60 -0
  423. package/src/surfaces/secret-field.ts +146 -0
  424. package/src/surfaces/seo.json +932 -0
  425. package/src/surfaces/seo.ts +334 -0
  426. package/src/surfaces/settings-contexts.json +538 -0
  427. package/src/surfaces/settings-contexts.ts +196 -0
  428. package/src/surfaces/settings-contributed.json +489 -0
  429. package/src/surfaces/settings-contributed.ts +199 -0
  430. package/src/surfaces/settings-css-vars.json +820 -0
  431. package/src/surfaces/settings-css-vars.ts +199 -0
  432. package/src/surfaces/settings-defs.json +1363 -0
  433. package/src/surfaces/settings-defs.ts +232 -0
  434. package/src/surfaces/settings-deploy.json +108 -0
  435. package/src/surfaces/settings-deploy.ts +135 -0
  436. package/src/surfaces/settings-extensions.json +486 -0
  437. package/src/surfaces/settings-extensions.ts +201 -0
  438. package/src/surfaces/settings-head.json +779 -0
  439. package/src/surfaces/settings-head.ts +397 -0
  440. package/src/surfaces/settings-locales.json +420 -0
  441. package/src/surfaces/settings-locales.ts +137 -0
  442. package/src/surfaces/settings-overview.json +423 -0
  443. package/src/surfaces/settings-overview.ts +218 -0
  444. package/src/surfaces/settings-packages.json +549 -0
  445. package/src/surfaces/settings-packages.ts +158 -0
  446. package/src/surfaces/settings-pane.json +147 -0
  447. package/src/surfaces/settings-pane.ts +150 -0
  448. package/src/surfaces/settings-rawjson.json +104 -0
  449. package/src/surfaces/settings-rawjson.ts +107 -0
  450. package/src/surfaces/shell.json +99 -0
  451. package/src/surfaces/shell.ts +30 -0
  452. package/src/surfaces/slash-menu.json +180 -0
  453. package/src/surfaces/slash-menu.ts +272 -0
  454. package/src/surfaces/statements.json +607 -0
  455. package/src/surfaces/statements.ts +238 -0
  456. package/src/surfaces/statusbar.json +94 -0
  457. package/src/{panels → surfaces}/statusbar.ts +163 -90
  458. package/src/surfaces/style-panel.json +1643 -0
  459. package/src/surfaces/style-panel.ts +338 -0
  460. package/src/surfaces/stylebook-chrome.json +122 -0
  461. package/src/surfaces/stylebook-chrome.ts +122 -0
  462. package/src/surfaces/tab-strip.json +448 -0
  463. package/src/surfaces/tab-strip.ts +200 -0
  464. package/src/surfaces/target-line.json +419 -0
  465. package/src/surfaces/target-line.ts +349 -0
  466. package/src/surfaces/toasts.json +189 -0
  467. package/src/surfaces/welcome.json +571 -0
  468. package/src/surfaces/welcome.ts +338 -0
  469. package/src/tabs/project-config.ts +71 -15
  470. package/src/tabs/tab.ts +52 -1
  471. package/src/types.ts +22 -0
  472. package/src/ui/ai-credentials-form.ts +104 -100
  473. package/src/ui/ai-managed-connect.ts +54 -36
  474. package/src/ui/ai-model-picker.ts +75 -47
  475. package/src/ui/cf-account-picker.ts +76 -141
  476. package/src/ui/color-selector.ts +80 -301
  477. package/src/ui/dynamic-slot.ts +43 -283
  478. package/src/ui/expression-editor.ts +797 -576
  479. package/src/ui/field-input.ts +8 -181
  480. package/src/ui/form-controls.ts +838 -366
  481. package/src/ui/formula-chips.ts +80 -85
  482. package/src/ui/kit.ts +33 -0
  483. package/src/ui/layers.ts +293 -593
  484. package/src/ui/media-picker.ts +203 -284
  485. package/src/ui/panel-resize.ts +251 -64
  486. package/src/ui/popover-surface.ts +153 -0
  487. package/src/ui/progress-modal.ts +22 -60
  488. package/src/ui/regions.ts +13 -7
  489. package/src/ui/schema-form.ts +906 -440
  490. package/src/ui/surface.ts +122 -0
  491. package/src/ui/unit-selector.ts +15 -131
  492. package/src/ui/value-source.ts +14 -0
  493. package/src/ui/virtual-window.ts +88 -9
  494. package/src/utils/canvas-media.ts +72 -0
  495. package/src/utils/edit-display.ts +7 -2
  496. package/src/utils/element-selectors.ts +69 -0
  497. package/src/utils/link-target.ts +2 -2
  498. package/src/utils/studio-utils.ts +0 -10
  499. package/src/view.ts +15 -0
  500. package/src/workspace/pane-derive.ts +36 -14
  501. package/src/workspace/workspace.ts +15 -6
  502. package/styles/canvas.css +72 -467
  503. package/styles/forced-colors.css +48 -82
  504. package/styles/forced-colors.json +41 -0
  505. package/styles/inspector.css +2 -1301
  506. package/styles/overlays.css +96 -2823
  507. package/styles/panels.css +6 -1423
  508. package/styles/shell-frame.css +272 -0
  509. package/styles/shell-frame.json +227 -0
  510. package/styles/shell.css +0 -1568
  511. package/styles/tokens.css +65 -136
  512. package/styles/tokens.json +84 -0
  513. package/dist/chunks/Overlay.dev-etqjwyf9.js +0 -23
  514. package/dist/chunks/adapter-jkcyhgtr.js +0 -14
  515. package/dist/chunks/cf-account-picker-en86v7x7.js +0 -163
  516. package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
  517. package/dist/chunks/components-zmx10t62.js +0 -21
  518. package/dist/chunks/content-source-48xg86wj.js +0 -49
  519. package/dist/chunks/convert-to-repeater-43tfn7cs.js +0 -268
  520. package/dist/chunks/editor-cptq5jp1.js +0 -37
  521. package/dist/chunks/entry-commands-e63f1xc0.js +0 -46
  522. package/dist/chunks/extension-sections-txv08xxg.js +0 -311
  523. package/dist/chunks/file-ops-wbh4hhsx.js +0 -48
  524. package/dist/chunks/files-3kfkwhk4.js +0 -93
  525. package/dist/chunks/flow-qbfgrpeh.js +0 -634
  526. package/dist/chunks/focus-trap.esm-9t2drp0g.js +0 -1025
  527. package/dist/chunks/focus-visible-6eg70z18.js +0 -167
  528. package/dist/chunks/format-host-w16q81tf.js +0 -43
  529. package/dist/chunks/fs-events-hrrg328g.js +0 -21
  530. package/dist/chunks/git-panel-532whr1y.js +0 -59
  531. package/dist/chunks/index-zqxmqxmh.js +0 -109
  532. package/dist/chunks/jx-validate-8er6gbxy.js +0 -19
  533. package/dist/chunks/layers-panel-07hm7g5j.js +0 -67
  534. package/dist/chunks/layers-q75tj2mf.js +0 -45
  535. package/dist/chunks/lit-html-j055mkh8.js +0 -22
  536. package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
  537. package/dist/chunks/notify-yjm79n4s.js +0 -30
  538. package/dist/chunks/parse-24d0gay3.js +0 -19
  539. package/dist/chunks/platform-ba1j1gbh.js +0 -16
  540. package/dist/chunks/publish-panel-v9st96ja.js +0 -29
  541. package/dist/chunks/quick-search-zccjp6gv.js +0 -66
  542. package/dist/chunks/references-3qx8fkyp.js +0 -29
  543. package/dist/chunks/right-panel-vm3b6kwy.js +0 -46
  544. package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
  545. package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +0 -18
  546. package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
  547. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
  548. package/dist/chunks/sp-tooltip.dev-4sastdnw.js +0 -15
  549. package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
  550. package/dist/chunks/studio-0a5qrh01.js +0 -3
  551. package/dist/chunks/studio-0bq6eh7v.js +0 -12
  552. package/dist/chunks/studio-18j3bmcz.js +0 -14
  553. package/dist/chunks/studio-1ngbejft.js +0 -181
  554. package/dist/chunks/studio-4b5z1x43.js +0 -424
  555. package/dist/chunks/studio-4n2b5gzb.js +0 -357
  556. package/dist/chunks/studio-4rzyrtjz.js +0 -426
  557. package/dist/chunks/studio-5fx3g9jg.js +0 -4325
  558. package/dist/chunks/studio-5myknm0a.js +0 -1903
  559. package/dist/chunks/studio-7afd21nw.js +0 -67
  560. package/dist/chunks/studio-7nte2hj4.js +0 -613
  561. package/dist/chunks/studio-8jpzqk5s.js +0 -128
  562. package/dist/chunks/studio-8q517dfr.js +0 -24
  563. package/dist/chunks/studio-950k0dx6.js +0 -6939
  564. package/dist/chunks/studio-9n4mrmnb.js +0 -184
  565. package/dist/chunks/studio-a0tswy0w.js +0 -58
  566. package/dist/chunks/studio-ahnzd9rg.js +0 -23
  567. package/dist/chunks/studio-cmb03bg5.js +0 -13890
  568. package/dist/chunks/studio-d1ks410h.js +0 -175
  569. package/dist/chunks/studio-ef49pnzx.js +0 -387
  570. package/dist/chunks/studio-eqq1w638.js +0 -113
  571. package/dist/chunks/studio-ew0djmhg.js +0 -341
  572. package/dist/chunks/studio-gfgb8pe6.js +0 -1652
  573. package/dist/chunks/studio-h4rxscfc.js +0 -10
  574. package/dist/chunks/studio-jc994dnr.js +0 -1066
  575. package/dist/chunks/studio-kyh9avvv.js +0 -28
  576. package/dist/chunks/studio-m3hwn7y1.js +0 -120
  577. package/dist/chunks/studio-mtfnaqs9.js +0 -659
  578. package/dist/chunks/studio-n6j9t10q.js +0 -318
  579. package/dist/chunks/studio-n8b9wqwy.js +0 -291
  580. package/dist/chunks/studio-qcy05dp8.js +0 -1743
  581. package/dist/chunks/studio-qq6jym04.js +0 -53
  582. package/dist/chunks/studio-rdx8jcxy.js +0 -83
  583. package/dist/chunks/studio-rn9ta2mz.js +0 -111
  584. package/dist/chunks/studio-t7qb9db8.js +0 -665
  585. package/dist/chunks/studio-tmc5emrw.js +0 -8
  586. package/dist/chunks/studio-z6nw2ek2.js +0 -18
  587. package/dist/chunks/studio-zczps70q.js +0 -378
  588. package/dist/chunks/studio-zqazsct2.js +0 -1823
  589. package/dist/chunks/workspace-fjx52ryv.js +0 -89
  590. package/src/panels/activity-bar.ts +0 -251
  591. package/src/panels/panel-scheduler.ts +0 -219
  592. package/src/panels/style-inputs.ts +0 -237
  593. package/src/panels/target-line.ts +0 -274
  594. package/src/panels/welcome-screen.ts +0 -346
  595. package/src/ui/button-group.ts +0 -102
  596. package/src/ui/choice-field.ts +0 -72
  597. package/src/ui/field-row.ts +0 -249
  598. package/src/ui/formula-palette.ts +0 -213
  599. package/src/ui/icons.ts +0 -52
  600. package/src/ui/jx-theme.ts +0 -130
  601. package/src/ui/spectrum.ts +0 -340
  602. package/src/ui/value-selector.ts +0 -174
  603. package/src/ui/widgets.ts +0 -172
  604. /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
  605. /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
  606. /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
  607. /package/dist/chunks/{tsMode-ef187txx.css → tsMode-hnxxb73h.css} +0 -0
@@ -3,19 +3,26 @@
3
3
  * Layers panel — the Outline: the document tree, with collapse, selection, drag-and-drop
4
4
  * reordering, and per-row actions that are RENDERINGS of the command registry.
5
5
  *
6
- * Three things this file is deliberate about.
6
+ * **One panel, two bodies, and both of them are documents now.** The tree is
7
+ * `surfaces/panel-outline.json`; the Project Styles catalogue is
8
+ * `surfaces/panel-stylebook-layers.json` (`panels/stylebook-layers-panel.ts`). This module renders
9
+ * neither. It owns the record — only the record can know which body the pane is asking for — and
10
+ * everything that is a DECISION: which rows exist and in what order, what a row is called, what the
11
+ * window onto them is, which verbs the registry places on which row, and the whole of the keyboard
12
+ * model. The surface reads values. The two bodies are appended into the same `.panel-content`, so
13
+ * each mode takes the other's document down on the way in; that symmetry is the whole of the seam.
7
14
  *
8
- * **Rows are `registry.forPlacement("outline/row")`.** Every row used to carry five hand-built
9
- * action buttons, always visible, on every row — five custom elements with shadow roots per visible
10
- * row. They now collapse to the selected row plus the hovered one, which is Gutenberg's rule and
11
- * the one plan §3.2 ⑩ codifies: the floating bar owns selection-scoped verbs, the inspector owns
12
- * values. The verbs, their names, their chords and their disabled reasons all come from the records
13
- * in `block-action-bar.ts` — the surface renders, it does not decide.
15
+ * Four things this file is deliberate about.
14
16
  *
15
- * The hovered row's cluster is mounted imperatively ({@link mountHoverActions}) rather than by a
16
- * `display: none` CSS rule, because a hidden `sp-action-button` is still an upgraded custom element
17
- * with a shadow root: CSS reveal would have kept the whole cost the collapse exists to remove. At
18
- * most two clusters exist at any moment — the selected row's, and the one under the pointer.
17
+ * **Rows are `registry.forPlacement("outline/row")`.** Every row used to carry five hand-built
18
+ * action buttons, always visible, on every row. They collapse to the selected row plus the hovered
19
+ * one, which is Gutenberg's rule and the one plan §3.2 ⑩ codifies: the floating bar owns
20
+ * selection-scoped verbs, the inspector owns values. The verbs, their names, their chords and their
21
+ * disabled reasons all come from the records in `block-action-bar.ts` — the surface renders, it
22
+ * does not decide. A row that is neither selected nor hovered is projected with an EMPTY command
23
+ * list, so the cluster costs nothing rather than being hidden by a rule: a `display: none` kit
24
+ * element is still an upgraded custom element, which is the whole cost the collapse exists to
25
+ * remove.
19
26
  *
20
27
  * **A row says something.** On a real page the tree was a wall of rows all reading "div": only
21
28
  * text-bearing nodes got a preview and containers got the tag they already wear as a coloured
@@ -24,25 +31,23 @@
24
31
  *
25
32
  * **It is a tree, and it is reachable.** `role="tree"` / `role="treeitem"` with a roving tabindex
26
33
  * and the arrow-key model ARIA specifies: ↑↓ walk the visible rows, → expands then descends, ←
27
- * collapses then ascends, Enter/F2 renames.
34
+ * collapses then ascends, Enter/F2 renames. The keys are cases in the document (`$switch` on
35
+ * `event#/key`); what each one MEANS is {@link onOutlineKey}.
28
36
  *
29
37
  * **The rows are a MODEL, and the DOM holds a window onto it** ({@link OutlineRow}, `ui/
30
- * virtual-window.ts`). A 5 000-node page drew 5 000 rows, each carrying an `sp-icon` custom
31
- * element, on every repaint. It now draws the viewport plus three rows of overscan — and because
32
- * the DOM is no longer the whole list, nothing may ASK the DOM what the whole list is. Every
33
- * question about "which rows exist, and in what order" — the shift-range, the arrow walk, Home/End,
34
- * ←'s climb to the parent, the reveal that follows the selection — is answered from the array
35
- * {@link buildOutlineRows} produces. A shift-range read off the DOM would silently select the wrong
36
- * set the moment either end of it scrolled out of the window: a correctness bug wearing a
37
- * performance change's clothes.
38
+ * virtual-window.ts`). A 5 000-node page drew 5 000 rows on every repaint. It now draws the
39
+ * viewport plus three rows of overscan — and because the DOM is no longer the whole list, nothing
40
+ * may ASK the DOM what the whole list is. Every question about "which rows exist, and in what
41
+ * order" — the shift-range, the arrow walk, Home/End, ←'s climb to the parent, the reveal that
42
+ * follows the selection — is answered from the array {@link buildOutlineRows} produces. A
43
+ * shift-range read off the DOM would silently select the wrong set the moment either end of it
44
+ * scrolled out of the window: a correctness bug wearing a performance change's clothes.
45
+ *
46
+ * @docs studio/design/layers
38
47
  */
39
48
 
40
- import { html, render as litRender, nothing } from "lit-html";
49
+ import { nothing } from "lit-html";
41
50
  import { displayTagName, isTagExpression, tagNameCandidates } from "@jxsuite/schema/guards";
42
- import { classMap } from "lit-html/directives/class-map.js";
43
- import { ifDefined } from "lit-html/directives/if-defined.js";
44
- import { ref } from "lit-html/directives/ref.js";
45
- import { repeat } from "lit-html/directives/repeat.js";
46
51
  import {
47
52
  VOID_ELEMENTS,
48
53
  childIndex,
@@ -66,15 +71,13 @@ import type { JxMutableNode } from "@jxsuite/schema/types";
66
71
  import { mutateUpdateProperty, transactDoc } from "../tabs/transact";
67
72
  import { view } from "../view";
68
73
  import { setActivityTab } from "../shell";
69
- import { renderEmptyState } from "./empty-state";
70
74
  import { registerPanel } from "./panel-registry";
71
- import { renderStylebookLayersTemplate } from "./stylebook-layers-panel";
75
+ import { detachStylebookLayers, mountStylebookLayersPanel } from "./stylebook-layers-panel";
72
76
  import { selectStylebookTag, stylebookMeta } from "./stylebook-panel";
73
77
  import { isInlineElement } from "../editor/inline-edit";
74
78
  import { showContextMenu } from "../editor/context-menu";
75
- import { panToElement } from "../canvas/canvas-utils";
79
+ import { revealPathInCanvas } from "../canvas/popover-state";
76
80
  import {
77
- commandIcon,
78
81
  commandTooltip,
79
82
  runCommand,
80
83
  selectionCommandRegistry,
@@ -87,15 +90,25 @@ import {
87
90
  revealListRow,
88
91
  watchListWindow,
89
92
  } from "../ui/virtual-window";
90
- import type { CommandRegistry } from "../commands/registry";
93
+ import { mountOutlineSurface } from "../surfaces/panel-outline";
94
+ import type { AnyCommand, CommandRegistry } from "../commands/registry";
91
95
  import type { ListWindowWatch } from "../ui/virtual-window";
92
- import type { TemplateResult } from "lit-html";
96
+ import type { PanelBody } from "./panel-registry";
97
+ import type {
98
+ OutlineCommandView,
99
+ OutlineRowView,
100
+ OutlineSurfaceHandle,
101
+ OutlineValues,
102
+ } from "../surfaces/panel-outline";
93
103
 
94
104
  // ─── What a row says ─────────────────────────────────────────────────────────
95
105
 
96
106
  /** How much of a text preview a 240px column can carry before it is just noise. */
97
107
  const LABEL_MAX = 32;
98
108
 
109
+ /** How much of a text NODE's own content the row previews. */
110
+ const TEXT_PREVIEW_MAX = 40;
111
+
99
112
  /**
100
113
  * Tags whose human name is worth more than the tag itself.
101
114
  *
@@ -208,144 +221,47 @@ export function outlineLabel(node: JxMutableNode): string {
208
221
  export const OUTLINE_ROW_MAX_ITEMS = 4;
209
222
 
210
223
  /**
211
- * The row's action cluster.
224
+ * The row's action cluster, as values.
225
+ *
226
+ * The glyph is `command.icon`, verbatim — the same one line `block-action-bar.ts`'s own `toolOf`
227
+ * projection is, because the records carry kit names and "what glyph does this record draw" must
228
+ * have exactly one answer. A record with no icon projects "" and the button draws its title.
212
229
  *
213
230
  * Wrapped in {@link withCommandTarget} so every record is evaluated against THIS ROW'S node rather
214
231
  * than the selection — `PLACEMENT_MATRIX["outline/row"]` says row actions act on the row's node,
215
232
  * and the hovered row is not the selected one.
216
233
  */
217
- export function renderRowCommands(registry: CommandRegistry, path: JxPath) {
234
+ export function rowCommandViews(
235
+ registry: CommandRegistry,
236
+ path: JxPath,
237
+ key: string,
238
+ ): { commands: OutlineCommandView[]; overflow: AnyCommand[] } {
218
239
  return withCommandTarget(path, () => {
219
240
  const placed = registry.forPlacement("outline/row");
220
- const shown = placed.slice(0, OUTLINE_ROW_MAX_ITEMS);
221
- const overflow = placed.slice(OUTLINE_ROW_MAX_ITEMS);
222
- return html`${shown.map(
223
- (command) => html`<sp-action-button
224
- quiet
225
- size="xs"
226
- class=${command.destructive ? "layer-action layer-delete" : "layer-action"}
227
- data-command=${command.id}
228
- aria-label=${command.title}
229
- title=${commandTooltip(registry, command)}
230
- ?disabled=${registry.disabledReason(command.id) !== undefined}
231
- @click=${(e: MouseEvent) => {
232
- e.stopPropagation();
233
- (e.currentTarget as HTMLElement).blur();
234
- runCommand(registry, command.id, path);
235
- }}
236
- >${commandIcon(command)}</sp-action-button
237
- >`,
238
- )}
239
- ${
240
- overflow.length === 0
241
- ? nothing
242
- : html`<sp-action-button
243
- quiet
244
- size="xs"
245
- class="layer-action layer-overflow"
246
- aria-label="More actions"
247
- aria-haspopup="menu"
248
- title="More actions"
249
- @click=${(e: MouseEvent) => {
250
- e.stopPropagation();
251
- showCommandOverflow(e.currentTarget as HTMLElement, registry, overflow, path);
252
- }}
253
- >
254
- <sp-icon-more slot="icon"></sp-icon-more>
255
- </sp-action-button>`
256
- }`;
241
+ return {
242
+ commands: placed.slice(0, OUTLINE_ROW_MAX_ITEMS).map((command) => ({
243
+ destructive: command.destructive === true ? "true" : "false",
244
+ disabled: registry.disabledReason(command.id) !== undefined,
245
+ icon: command.icon ?? "",
246
+ id: command.id,
247
+ row: key,
248
+ title: command.title,
249
+ tooltip: commandTooltip(registry, command),
250
+ })),
251
+ overflow: placed.slice(OUTLINE_ROW_MAX_ITEMS),
252
+ };
257
253
  });
258
254
  }
259
255
 
260
- // ─── The hovered row's cluster ───────────────────────────────────────────────
261
-
262
- /**
263
- * The row whose cluster this module mounted on hover, so it can be taken down again.
264
- *
265
- * One at a time: the pointer is in one place. The SELECTED row's cluster is lit's (it is in the row
266
- * template), and is never touched from here.
267
- */
268
- let _hoverActionsRow: HTMLElement | null = null;
269
-
270
- /** The empty span a non-selected row keeps for {@link mountHoverActions} to render into. */
271
- function actionSlot(row: HTMLElement): HTMLElement | null {
272
- return row.classList.contains("selected")
273
- ? null
274
- : row.querySelector<HTMLElement>(".layer-actions");
275
- }
276
-
277
- /** Take down the hover cluster, if one is up. */
278
- export function clearHoverActions(): void {
279
- const slot = _hoverActionsRow ? actionSlot(_hoverActionsRow) : null;
280
- if (slot) {
281
- litRender(nothing, slot);
282
- }
283
- _hoverActionsRow = null;
284
- }
285
-
286
- /** Build `row`'s cluster into its slot, replacing whatever was there. */
287
- function mountHoverActions(row: HTMLElement): void {
288
- const slot = actionSlot(row);
289
- const key = row.dataset.path;
290
- if (!slot || key === undefined) {
291
- return;
292
- }
293
- _hoverActionsRow = row;
294
- litRender(renderRowCommands(selectionCommandRegistry(), pathFromKey(key)), slot);
295
- }
296
-
297
- /**
298
- * Delegated `mouseover` (which bubbles; `mouseenter` does not): reveal the cluster on the row under
299
- * the pointer, and only that row.
300
- */
301
- export function onTreeHover(e: Event): void {
302
- const row = (e.target as HTMLElement | null)?.closest<HTMLElement>('.layer-row[role="treeitem"]');
303
- if (!row || row === _hoverActionsRow) {
304
- return;
305
- }
306
- clearHoverActions();
307
- mountHoverActions(row);
308
- }
256
+ // ─── The tree, as a keyboard surface ─────────────────────────────────────────
309
257
 
310
258
  /**
311
- * The two things that can only be decided once the rows are in the document.
312
- *
313
- * Deferred by a microtask on purpose. A `ref` on the tree element commits BEFORE the child part
314
- * holding the rows, so on a first render the callback sees an empty tree; and the template this
315
- * module returns is rendered by its caller, so a microtask is the first tick at which the new DOM
316
- * exists either way.
317
- *
318
- * - The roving tabindex needs to know which rows survived their collapsed ancestors.
319
- * - The hover cluster needs re-mounting: rows are keyed by path, so a document edit can leave the
320
- * pointer on a DOM row that now stands for a different node, with different disabled reasons.
259
+ * Pixels of indent per level, the depth past which the column stops paying, and the row's own
260
+ * gutter.
321
261
  */
322
- export function afterTreeRender(tree: HTMLElement): void {
323
- const row = _hoverActionsRow;
324
- queueMicrotask(() => {
325
- adoptOutlineTree(tree);
326
- applyTreeRovingTabindex(tree);
327
- // A keyboard jump that had to scroll first left its target here, for the render it provoked.
328
- const wanted = _pendingFocusKey;
329
- if (wanted !== null) {
330
- _pendingFocusKey = null;
331
- focusRow(tree, rowElementFor(tree, wanted) ?? undefined);
332
- }
333
- if (!row || _hoverActionsRow !== row) {
334
- return;
335
- }
336
- if (row.isConnected && !row.classList.contains("selected")) {
337
- mountHoverActions(row);
338
- } else {
339
- _hoverActionsRow = null;
340
- }
341
- });
342
- }
343
-
344
- // ─── The tree, as a keyboard surface ─────────────────────────────────────────
345
-
346
- /** Pixels of indent per level, and the depth past which the column stops paying for more. */
347
262
  const INDENT_STEP = 16;
348
263
  const INDENT_MAX_DEPTH = 6;
264
+ const INDENT_BASE = 8;
349
265
 
350
266
  /**
351
267
  * The indent for `depth`, capped.
@@ -368,12 +284,12 @@ function pathFromKey(key: string): JxPath {
368
284
  * One row the Outline WOULD draw, whether or not it is currently in the window.
369
285
  *
370
286
  * Built in a first pass that decides visibility (collapsed ancestors, inline elements, the content
371
- * root) and costs nothing but the decision; the templates are built in a second pass, for the
372
- * window only. Splitting the two is the whole saving — the expensive half is the `sp-icon` and
373
- * `sp-action-button` custom elements a row template mounts, not the walk that finds it.
287
+ * root) and costs nothing but the decision; the projections are built in a second pass, for the
288
+ * window only. Splitting the two is the whole saving — the expensive half is the kit elements a
289
+ * row's cluster mounts, not the walk that finds it.
374
290
  */
375
291
  interface OutlineRow {
376
- /** `pathKey(path)` — the drag-and-drop and roving-tabindex key, and lit's `repeat` key. */
292
+ /** `pathKey(path)` — the drag-and-drop and roving-focus key, and the `$map`'s reconcile key. */
377
293
  key: string;
378
294
  path: JxPath;
379
295
  depth: number;
@@ -383,8 +299,8 @@ interface OutlineRow {
383
299
  * Whether this row is a `role="treeitem"`.
384
300
  *
385
301
  * Text-node rows are drawn but are not tree items, and were never part of the keyboard walk or a
386
- * shift-range: `treeRows()` selected on the role, so moving those questions to the model has to
387
- * carry the same distinction rather than quietly start selecting text nodes.
302
+ * shift-range — so moving those questions to the model has to carry the same distinction rather
303
+ * than quietly start selecting text nodes.
388
304
  */
389
305
  item: boolean;
390
306
  node: JxMutableNode | string | number | boolean;
@@ -396,26 +312,33 @@ interface OutlineRow {
396
312
  }
397
313
 
398
314
  /**
399
- * The declared height of one row — `styles/panels.css` `.layer-row { block-size: 24px }`.
315
+ * The declared height of one row — `surfaces/panel-outline.json`'s `[part="row"] { block-size }`.
400
316
  *
401
317
  * A window needs a row height BEFORE the first row exists, so this constant is what the first paint
402
318
  * windows by; {@link outlineRowHeight} measures a real row afterwards and believes the measurement.
403
- * The stylesheet declares the height explicitly so the two cannot drift apart in silence.
319
+ * The document declares the height explicitly so the two cannot drift apart in silence.
404
320
  */
405
321
  export const OUTLINE_ROW_HEIGHT = 24;
406
322
 
323
+ /** The selector the window, the reveal and the focus move address a drawn row by. */
324
+ const ROW_SELECTOR = '[part="row"]';
325
+
407
326
  /** The rows the Outline last built, in display order. */
408
327
  let _outlineRows: OutlineRow[] = [];
409
- /** The `.layers-tree` element, kept between renders so a window can be computed for the next one. */
328
+ /** The `[part="tree"]` element, kept between renders so a window can be computed for the next one. */
410
329
  let _outlineList: HTMLElement | null = null;
411
330
  /** The scroll watch that repaints the Outline as its scroller moves. */
412
331
  let _outlineWatch: ListWindowWatch | null = null;
413
- /** The Navigator repaint, captured per render so the scroll watch never holds a stale one. */
332
+ /** The Navigator repaint, captured per mount so the scroll watch never holds a stale one. */
414
333
  let _outlineRerender: (() => void) | null = null;
334
+ /** The row under the pointer, which is the second row that carries a cluster. */
335
+ let _hoveredKey: string | null = null;
336
+ /** The row being renamed, the text typed into it so far, and whose repaint to spend on commit. */
337
+ let _editing: { key: string; text: string; rerender: () => void } | null = null;
415
338
 
416
339
  /** The height one row actually has; the declared constant until a row has been laid out. */
417
340
  function outlineRowHeight(): number {
418
- return measuredRowHeight(_outlineList, ".layer-row", OUTLINE_ROW_HEIGHT);
341
+ return measuredRowHeight(_outlineList, ROW_SELECTOR, OUTLINE_ROW_HEIGHT);
419
342
  }
420
343
 
421
344
  /** The model index of the row keyed `key`, or -1. */
@@ -475,38 +398,45 @@ function revealOutlineRow(index: number): boolean {
475
398
  /**
476
399
  * Repaint the Outline because its window changed.
477
400
  *
478
- * Deferred to a microtask so a scroll that arrives while lit is committing cannot re-enter the
479
- * render that is producing the rows.
401
+ * Deferred to a microtask so a scroll that arrives while the runtime is committing cannot re-enter
402
+ * the render that is producing the rows.
480
403
  *
481
404
  * **Never during a drag.** `panels/dnd.ts` holds the drop targets it registered on the rendered
482
405
  * rows and shifts the rows either side of the pointer by a transform; re-rendering underneath it
483
406
  * would drop both. A wheel-scroll mid-drag therefore keeps the window the drag started with, and
484
- * the drop's own repaint restores it.
407
+ * the drop's own repaint restores it. The drag marks its row with `data-dragging`, which is an
408
+ * attribute rather than a class for the reason the document's style block gives.
485
409
  *
486
410
  * **Never for a tree that is gone.** The stylebook draws a different panel into the same dock,
487
411
  * whose scroller is the one this watch is still listening to.
488
412
  */
489
413
  function outlineWindowChanged(): void {
490
- if (_outlineList?.isConnected !== true || _outlineList.querySelector(".layer-row.dragging")) {
414
+ if (_outlineList?.isConnected !== true || _outlineList.querySelector("[data-dragging]")) {
491
415
  return;
492
416
  }
493
417
  queueMicrotask(() => _outlineRerender?.());
494
418
  }
495
419
 
496
420
  /**
497
- * Adopt the rendered tree: remember it, and keep it watching its scroller.
421
+ * Keep the tree watching whatever scrolls it.
498
422
  *
499
- * Called from {@link afterTreeRender}, one microtask after the rows are committed — the first moment
500
- * they exist AND the element can be resolved to whatever scrolls it (`#left-panel`, or the bottom
501
- * dock). The first paint of a session therefore draws every row, because nothing can be measured
502
- * before it; the watch's opening measurement is what asks for the second, windowed one.
423
+ * Split from {@link OutlineActions.treeReady} on purpose, and the split is the whole of why the
424
+ * window works. `onNodeCreated` hands over the element as it is BUILT — one tick before it is in
425
+ * the document — so `nearestScroller` walking up from it at that moment finds nothing, binds no
426
+ * watch, and the tree silently draws all five thousand rows for the rest of the session. The
427
+ * element is remembered there, because the next projection's `listWindow` needs it; the watch is
428
+ * bound HERE, from the post-mount tick, where the element is connected and the scroller resolves.
503
429
  *
504
430
  * Idempotent by construction: `watchListWindow` hands back the same watch for the same element and
505
- * scroller, so calling this after every render costs a comparison.
431
+ * scroller, so calling it after every draw costs a comparison. The first paint of a session draws
432
+ * every row, because nothing can be measured before them; the watch's opening measurement is what
433
+ * asks for the second, windowed one.
506
434
  */
507
- function adoptOutlineTree(tree: HTMLElement): void {
508
- _outlineList = tree;
509
- _outlineWatch = watchListWindow(_outlineWatch, tree, {
435
+ function watchOutlineTree(): void {
436
+ if (!_outlineList) {
437
+ return;
438
+ }
439
+ _outlineWatch = watchListWindow(_outlineWatch, _outlineList, {
510
440
  count: () => _outlineRows.length,
511
441
  onChange: outlineWindowChanged,
512
442
  rowHeight: outlineRowHeight,
@@ -517,13 +447,13 @@ function adoptOutlineTree(tree: HTMLElement): void {
517
447
  * The node an Outline row stands for, read back off the row.
518
448
  *
519
449
  * Rows carry their `JxPath` verbatim, as JSON, in `data-jx-path` — node IDENTITY in the DOM. That
520
- * is a different thing from the neighbouring `data-path`, which is `pathKey`'s lossy `join("/")`
521
- * string and exists as the drag-and-drop and roving-tabindex Map key; `["children", "0"]` and
522
- * `["children", 0]` share a key and are different nodes, and a segment containing a slash has no
523
- * key at all.
450
+ * is a different thing from the neighbouring `data-value`, which is `pathKey`'s lossy `join("/")`
451
+ * string and is the row's `value`: the drag-and-drop key, the caret key, and the detail of every
452
+ * event the row provokes. `["children", "0"]` and `["children", 0]` share a key and are different
453
+ * nodes, and a segment containing a slash has no key at all.
524
454
  *
525
- * Everything that has to point at a node from outside the render — shift-range multi-select,
526
- * drag-reorder, canvas to Outline sync, a collaborator's cursor, a jump from Problems — needs the
455
+ * Everything that has to point at a node from outside the projection — the context menu, drag
456
+ * reorder, canvas to Outline sync, a collaborator's cursor, a jump from Problems — needs the
527
457
  * unambiguous one.
528
458
  */
529
459
  export function outlineRowPath(el: Element | null): JxPath | null {
@@ -539,77 +469,29 @@ export function outlineRowPath(el: Element | null): JxPath | null {
539
469
  }
540
470
  }
541
471
 
542
- /** Every row currently on screen, in visual order. */
543
- function treeRows(tree: HTMLElement): HTMLElement[] {
544
- return [...tree.querySelectorAll<HTMLElement>('.layer-row[role="treeitem"]')];
545
- }
546
-
547
- /**
548
- * Roving tabindex over the rows: the selected row is the tab stop, else the first.
549
- *
550
- * Applied after every render ({@link afterTreeRender}) rather than baked into each row's template,
551
- * because "is the selected row actually on screen" is only known once the collapsed ancestors have
552
- * been skipped — a selection inside a collapsed branch would otherwise leave the tree with no tab
553
- * stop at all.
554
- */
555
- export function applyTreeRovingTabindex(tree: HTMLElement): void {
556
- const rows = treeRows(tree);
557
- if (rows.length === 0) {
558
- return;
559
- }
560
- // The PRIMARY row is the tab stop, not merely the first selected one: a multi-selection has one
561
- // Keyboard position, and it is the row the author last pointed at. `data-primary` is stamped by
562
- // The row template, so the two never have to re-derive the same answer.
563
- const primary = rows.find((row) => row.dataset.primary !== undefined);
564
- const selected = primary ?? rows.find((row) => row.getAttribute("aria-selected") === "true");
565
- const stop = selected ?? rows[0]!;
566
- for (const row of rows) {
567
- row.tabIndex = row === stop ? 0 : -1;
568
- }
569
- }
570
-
571
- /** Focus `row`, and make it the tree's single tab stop. */
572
- function focusRow(tree: HTMLElement, row: HTMLElement | undefined): void {
573
- if (!row) {
574
- return;
575
- }
576
- for (const other of treeRows(tree)) {
577
- other.tabIndex = other === row ? 0 : -1;
578
- }
579
- row.focus();
580
- }
581
-
582
472
  /** The rendered row for a model key, or null when the window does not currently hold it. */
583
- function rowElementFor(tree: HTMLElement, key: string): HTMLElement | null {
584
- return tree.querySelector<HTMLElement>(`.layer-row[data-path="${CSS.escape(key)}"]`);
473
+ function rowElementFor(key: string): HTMLElement | null {
474
+ return (
475
+ _outlineList?.querySelector<HTMLElement>(`${ROW_SELECTOR}[data-value="${CSS.escape(key)}"]`) ??
476
+ null
477
+ );
585
478
  }
586
479
 
587
480
  /**
588
- * The row a repaint should hand the keyboard to, once it exists.
589
- *
590
- * A keyboard jump can name a row the window does not hold — End on a 5 000-row page, or ← climbing
591
- * to a parent far above the viewport. Scrolling to it is immediate; DRAWING it is the Navigator
592
- * scheduler's business, and `ctx.rerender` is explicitly "not a synchronous re-render". So the
593
- * request outlives the keystroke by exactly one render: {@link afterTreeRender} spends it, and
594
- * spends it once — a focus request that survived its own repaint is stale, and stealing the
595
- * keyboard later is worse than not having moved it.
481
+ * Bring the model row at `index` into the window, when the window does not already hold it.
482
+ *
483
+ * **The keyboard is deliberately not touched here.** `jx-tree` keeps it across a handed-off caret
484
+ * move: it focuses the caret row once its own sync has written the roving `tabindex` onto the
485
+ * revealed element, which is the only moment focusing it does anything at all. What this used to do
486
+ * instead was queue a microtask and focus whatever answered to the key then — and the microtask was
487
+ * queued BEFORE {@link redrawOutline}, so it ran while the previous projection was still drawn and
488
+ * found either the row that used to stand at that key or, for a row the window did not hold,
489
+ * nothing. One shot, spent early, every time.
596
490
  */
597
- let _pendingFocusKey: string | null = null;
598
-
599
- /** Move the keyboard to the model row at `index`, bringing it into the window if it is outside one. */
600
- function focusModelRow(tree: HTMLElement, index: number): void {
491
+ function revealModelRow(index: number): void {
601
492
  const row = _outlineRows[index];
602
- if (!row) {
603
- return;
604
- }
605
- const el = rowElementFor(tree, row.key);
606
- if (el) {
607
- focusRow(tree, el);
608
- return;
609
- }
610
- if (revealOutlineRow(index)) {
611
- _pendingFocusKey = row.key;
612
- _outlineRerender?.();
493
+ if (row && !rowElementFor(row.key)) {
494
+ revealOutlineRow(index);
613
495
  }
614
496
  }
615
497
 
@@ -629,10 +511,6 @@ function selectModelRow(index: number, gesture: { range?: boolean } = {}): void
629
511
  * - Ctrl/Cmd: toggle this path in or out, leaving the rest alone.
630
512
  * - Shift: the contiguous run of VISIBLE rows from the anchor to here.
631
513
  *
632
- * It took the tree element as its first parameter while a range was resolved by reading the rows
633
- * out of the DOM. It no longer is ({@link visibleRowPaths}), and the parameter went with the read —
634
- * a signature that still asked for the tree would suggest the answer depends on what is painted.
635
- *
636
514
  * @param {JxPath} path
637
515
  * @param {{ additive?: boolean; range?: boolean }} gesture
638
516
  */
@@ -660,76 +538,170 @@ export function applyRowSelection(
660
538
  }
661
539
 
662
540
  /**
663
- * The ARIA tree keyboard model.
664
- *
665
- * ↑ / ↓ walk the visible rows and take the selection with them — in an outline, "focus follows
666
- * selection" is what an author means by pressing Down. → expands a collapsed row and otherwise
667
- * descends into it; ← collapses an expanded one and otherwise climbs to its parent. Enter and F2
668
- * rename. Delete is deliberately absent: it is `selection.delete`'s chord, and the registry owns
669
- * it.
670
- *
671
- * Every one of those moves is an index into {@link outlineRows}, not into the rendered rows. The DOM
672
- * answer and the model answer agreed exactly while the tree drew everything; now the DOM holds a
673
- * window, and asking it for "the row after this one" would stop the walk at the window's edge and
674
- * make ← climb to whichever ancestor happened to be painted.
541
+ * The reader meant that row, and `mode` says what by.
542
+ *
543
+ * The three modes are the ARIA tree's own — a plain move or click replaces, `Ctrl`/`Cmd` toggles,
544
+ * `Shift` extends — and each maps onto the gesture {@link applyRowSelection} already had. What
545
+ * `jx-tree` cannot do is RESOLVE the range: naming every row between two of them means naming rows
546
+ * the drawn slice does not have, so the element dispatches the intent and the range is resolved
547
+ * here against {@link visibleRowPaths}, which reads the MODEL.
675
548
  */
676
- export function onTreeKeydown(
677
- e: KeyboardEvent,
678
- collapsed: Set<string>,
679
- rerender: () => void,
680
- ): void {
681
- const tree = e.currentTarget as HTMLElement;
682
- const row = (e.target as HTMLElement).closest<HTMLElement>('.layer-row[role="treeitem"]');
549
+ function selectOutlineRow(key: string, mode: string): void {
550
+ const row = _outlineRows[outlineIndexOfKey(key)];
683
551
  if (!row) {
684
552
  return;
685
553
  }
686
- const key = row.dataset.path ?? "";
687
- const index = outlineIndexOfKey(row.dataset.path);
688
- const expanded = row.getAttribute("aria-expanded");
689
-
690
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
691
- e.preventDefault();
692
- const next = outlineStep(index, e.key === "ArrowDown" ? 1 : -1);
693
- if (next >= 0) {
694
- // Shift+Arrow extends the range, the same gesture shift-click makes and through the same
695
- // Function. Without Shift the walk replaces the selection, exactly as it always has.
696
- selectModelRow(next, { range: e.shiftKey });
697
- focusModelRow(tree, next);
554
+ applyRowSelection(row.path, { additive: mode === "toggle", range: mode === "range" });
555
+ redrawOutline();
556
+ }
557
+
558
+ /**
559
+ * A row should be opened or closed.
560
+ *
561
+ * The detail says the state the row should be PUT INTO, so there is nothing left to work out about
562
+ * which way a toggle was going. The guard is still load bearing, in BOTH directions: a leaf's
563
+ * twisty box is drawn empty and still answers a click, and the element reads a leaf's expansion as
564
+ * "open me" — so the dead 14px in front of a row with nothing under it is a click that must change
565
+ * nothing at all, rather than one that quietly reaches into the collapsed set on its way past. The
566
+ * row must also still be in the model: a key from a stale projection would go into that set with
567
+ * nothing left to take it out again.
568
+ */
569
+ function expandOutlineRow(key: string, expanded: boolean): void {
570
+ const row = _outlineRows[outlineIndexOfKey(key)];
571
+ if (!row || !isExpandable(row)) {
572
+ return;
573
+ }
574
+ const collapsed = outlineCollapsed();
575
+ if (expanded) {
576
+ collapsed.delete(key);
577
+ } else {
578
+ collapsed.add(key);
579
+ }
580
+ redrawOutline();
581
+ }
582
+
583
+ /**
584
+ * The caret has to reach a row the window did not draw.
585
+ *
586
+ * `jx-tree` walks the DRAWN rows, which is all it can see; when a pad says the model continues that
587
+ * way it dispatches `move` and performs nothing. Each key is answered here exactly as the element
588
+ * would have answered it over a slice that held everything — and ← is the one that could never have
589
+ * been the element's, because the model records each row's PARENT as it builds it and a scan for
590
+ * "the nearest row above at a shallower level" can only ever see painted rows.
591
+ *
592
+ * The selection comes with it, for two reasons that point the same way. In an outline "focus
593
+ * follows selection" is what an author means by pressing Down; and `jx-tree` raises `select` beside
594
+ * `change` on every arrow it can perform itself, so a step the window could not satisfy must not be
595
+ * the one step that silently does not — which is also what puts the tab stop on the revealed row,
596
+ * rather than leaving the reader FOCUSED on one row while Tab comes back to another.
597
+ *
598
+ * @param {string} from The row the caret is on
599
+ * @param {string} keyName The key the element could not perform
600
+ */
601
+ function moveOutlineCaret(from: string, keyName: string): void {
602
+ const index = outlineIndexOfKey(from);
603
+ let target: number;
604
+ switch (keyName) {
605
+ case "Home": {
606
+ target = outlineStep(-1, 1);
607
+ break;
698
608
  }
699
- } else if (e.key === "ArrowRight") {
700
- e.preventDefault();
701
- if (expanded === "false") {
702
- collapsed.delete(key);
703
- rerender();
704
- } else if (expanded === "true") {
705
- focusModelRow(tree, outlineStep(index, 1));
609
+ case "End": {
610
+ target = outlineStep(_outlineRows.length, -1);
611
+ break;
706
612
  }
707
- } else if (e.key === "ArrowLeft") {
708
- e.preventDefault();
709
- if (expanded === "true") {
710
- collapsed.add(key);
711
- rerender();
712
- } else {
713
- // The model records each row's parent as it builds them, so the climb is exact rather than a
714
- // Backwards scan for a smaller `aria-level` — which could only ever see painted rows.
715
- const parent = _outlineRows[index]?.parent ?? -1;
716
- if (parent >= 0) {
717
- selectModelRow(parent);
718
- focusModelRow(tree, parent);
719
- }
613
+ case "ArrowUp": {
614
+ target = outlineStep(index, -1);
615
+ break;
616
+ }
617
+ case "ArrowLeft": {
618
+ target = _outlineRows[index]?.parent ?? -1;
619
+ break;
720
620
  }
721
- } else if (e.key === "Home" || e.key === "End") {
722
- e.preventDefault();
723
- const target = e.key === "Home" ? outlineStep(-1, 1) : outlineStep(_outlineRows.length, -1);
724
- if (target >= 0) {
725
- selectModelRow(target);
726
- focusModelRow(tree, target);
621
+ // ↓ and → both ask for the next drawn row; over the model they are the same step.
622
+ case "ArrowDown":
623
+ case "ArrowRight": {
624
+ target = outlineStep(index, 1);
625
+ break;
727
626
  }
728
- } else if (e.key === "Enter" || e.key === "F2") {
729
- e.preventDefault();
730
- selectModelRow(index);
731
- if (row.dataset.path !== undefined) {
732
- startLayerTitleEdit(pathFromKey(row.dataset.path), rerender);
627
+ default: {
628
+ return;
629
+ }
630
+ }
631
+ landOutlineCaret(target);
632
+ }
633
+
634
+ /**
635
+ * Put the caret on the model row at `target`, or do nothing when there is no such row.
636
+ *
637
+ * Shared by the two events the element cannot answer for itself, because they end the same way and
638
+ * that is not a coincidence: both are a row named over the MODEL, and reaching it means selecting
639
+ * it, revealing it and repainting the window around it in that order. The selection is also what
640
+ * moves the CARET — the projection seeds `current` from the primary selection — and that write is
641
+ * the whole of this panel's side of the handoff: `jx-tree` takes the keyboard to the revealed row
642
+ * itself, once its own sync has made that row focusable.
643
+ *
644
+ * @param {number} target
645
+ */
646
+ function landOutlineCaret(target: number): void {
647
+ if (!_outlineRows[target]) {
648
+ return;
649
+ }
650
+ selectModelRow(target);
651
+ revealModelRow(target);
652
+ redrawOutline();
653
+ }
654
+
655
+ /**
656
+ * What a row is CALLED — the one string the reader sees, hears and types the first letter of.
657
+ *
658
+ * One function rather than the three expressions it replaced, because typeahead now matches over
659
+ * the MODEL and the drawn row's `label` is what it must agree with. `jx-tree` matches a drawn row
660
+ * on its `label` prop precisely because that is the row's accessible name; a model-side search that
661
+ * computed the name a second way would move the caret to a row the reader was never told about, and
662
+ * nothing on screen would explain it.
663
+ *
664
+ * It answers for a TEXT row too, and that is what makes the `item` filter in
665
+ * {@link typeaheadOutlineCaret} do real work rather than merely read well: a text row draws its own
666
+ * preview, so it has a name a letter could reach, and the only thing keeping the caret off it is
667
+ * that it is not a tree item — the same rule {@link outlineStep} applies to the arrows.
668
+ *
669
+ * @param {OutlineRow} row
670
+ */
671
+ function outlineRowLabel(row: OutlineRow): string {
672
+ if (!row.item) {
673
+ const text = String(row.node);
674
+ return text.length > TEXT_PREVIEW_MAX ? `${text.slice(0, TEXT_PREVIEW_MAX)}…` : text;
675
+ }
676
+ const node = row.node as JxMutableNode;
677
+ return row.nodeType === "case-ref" ? node.$ref || "external" : outlineLabel(node);
678
+ }
679
+
680
+ /**
681
+ * A printable character on a windowed tree: find the model row it names, and go there.
682
+ *
683
+ * `jx-tree` resolves typeahead itself only when the drawn rows ARE the model; past that it
684
+ * dispatches, because a search over a slice always answers and the answer is the wrong row. The
685
+ * search here is the element's own, moved onto the full list: forward from the caret, wrapping
686
+ * once, matching the row's name case-insensitively, and skipping the rows that are drawn but are
687
+ * not tree items — text nodes, which the arrows already step over.
688
+ *
689
+ * Every model row is named on each keystroke, and that is affordable where it would not be in a
690
+ * paint: this runs once per key rather than once per frame, and {@link outlineRowView} is still
691
+ * called for the window alone.
692
+ *
693
+ * @param {string} from The row the caret is on
694
+ * @param {string} char The character typed, lowercased
695
+ */
696
+ function typeaheadOutlineCaret(from: string, char: string): void {
697
+ const start = outlineIndexOfKey(from);
698
+ const total = _outlineRows.length;
699
+ for (let step = 1; step <= total; step++) {
700
+ const at = (start + step + total) % total;
701
+ const row = _outlineRows[at];
702
+ if (row?.item && outlineRowLabel(row).trim().toLowerCase().startsWith(char)) {
703
+ landOutlineCaret(at);
704
+ return;
733
705
  }
734
706
  }
735
707
  }
@@ -737,88 +709,86 @@ export function onTreeKeydown(
737
709
  /**
738
710
  * Start inline title editing on a layer row.
739
711
  *
712
+ * The input is a CASE of the row now, not a node this module creates and inserts: the document
713
+ * draws it when the projection says this row is the one being renamed, and takes it away again when
714
+ * the projection stops saying so. That is what retired the four imperative writes the predecessor
715
+ * made into a tree it did not own — and with them the stale `display: none` a keyed re-render could
716
+ * leave on the label of whichever node inherited the row.
717
+ *
718
+ * A row the window does not hold is scrolled to and left to the scroll watch, which is the same
719
+ * "the row exists on the next pass" the reveal has always relied on. Renaming through a command
720
+ * (⌘↵, the palette, the block bar) is the one gesture in the app where re-pressing it is obvious.
721
+ *
740
722
  * @param {JxPath} path
741
723
  * @param {() => void} rerender
742
724
  */
743
725
  export function startLayerTitleEdit(path: JxPath, rerender: () => void) {
744
- const key = pathKey(path);
745
- /* Scoped to THIS panel's tree, and escaped, through the same helper every other row lookup in
746
- this file uses. The predecessor was a bare `document.querySelector` with the key interpolated
747
- raw, which is wrong twice over: a second pane — or the stylebook, which draws a layer tree of
748
- its own — puts more than one `.layer-row[data-path=…]` in the document, and the first match
749
- wins rather than the right one; and an unescaped key containing a quote or a bracket is not a
750
- selector the parser accepts, so a legitimately-named node throws instead of renaming.
751
- `_outlineList` is null before the first render, which is the same "row the window does not
752
- hold" case the reveal-and-repaint path below already handles. */
753
- const row = _outlineList ? rowElementFor(_outlineList, key) : null;
754
- if (!row) {
755
- // Renaming through a command (⌘↵, the palette, the block bar) can name a row the window does
756
- // Not hold. Scroll to it and repaint; the row exists on the next pass, and the caller's own
757
- // Chord is the one gesture in the app where re-pressing it is obvious.
758
- const index = outlineIndexOfPath(path);
759
- if (index >= 0 && revealOutlineRow(index)) {
760
- rerender();
761
- }
726
+ const tab = activeTab.value;
727
+ if (!tab) {
762
728
  return;
763
729
  }
764
- const label = row.querySelector(".layer-label") as HTMLElement | null;
765
- if (!label) {
730
+ const index = outlineIndexOfPath(path);
731
+ const row = _outlineRows[index];
732
+ /* The row's own node, not a second `getNodeAtPath` walk: the model already holds it, and reading
733
+ it back off the model is what makes "a text line has no `$title` to write" a real branch rather
734
+ than a defensive null check nothing can reach. */
735
+ if (!row?.item || typeof row.node !== "object") {
766
736
  return;
767
737
  }
738
+ _editing = { key: row.key, rerender, text: row.node.$title || "" };
739
+ if (!rowElementFor(row.key)) {
740
+ revealOutlineRow(index);
741
+ }
742
+ redrawOutline();
743
+ }
744
+
745
+ /** Whether a rename is live on `key`. */
746
+ function isEditing(key: string): boolean {
747
+ return _editing?.key === key;
748
+ }
768
749
 
750
+ /** Record what the rename input holds, so a blur can commit it without reading the DOM back. */
751
+ function editInput(text: string): void {
752
+ if (_editing) {
753
+ _editing.text = text;
754
+ }
755
+ }
756
+
757
+ /** Write the typed title (or clear it) and end the rename. Idempotent — a blur follows an Enter. */
758
+ function editCommit(): void {
759
+ const session = _editing;
769
760
  const tab = activeTab.value;
770
- if (!tab) {
761
+ if (!session) {
771
762
  return;
772
763
  }
773
- const node = getNodeAtPath(tab.doc.document, path);
774
- if (!node) {
764
+ _editing = null;
765
+ const value = session.text.trim();
766
+ if (tab) {
767
+ transactDoc(tab, (t) =>
768
+ mutateUpdateProperty(t, pathFromKey(session.key), "$title", value || undefined),
769
+ );
770
+ }
771
+ session.rerender();
772
+ redrawOutline();
773
+ }
774
+
775
+ /** End the rename without writing anything. */
776
+ function editCancel(): void {
777
+ const session = _editing;
778
+ if (!session) {
775
779
  return;
776
780
  }
781
+ _editing = null;
782
+ session.rerender();
783
+ redrawOutline();
784
+ }
777
785
 
778
- label.style.display = "none";
779
- const input = document.createElement("input");
780
- input.className = "layer-title-input";
781
- input.value = node.$title || "";
782
- const { $title: _, ...nodeWithoutTitle } = node;
783
- input.placeholder = outlineLabel(nodeWithoutTitle) || displayTagName(node.tagName) || "div";
784
- label.after(input);
785
- input.focus();
786
- input.select();
787
-
788
- let committed = false;
789
- const cleanup = () => {
790
- input.remove();
791
- label.style.display = "";
792
- };
793
- const commit = () => {
794
- if (committed) {
795
- return;
796
- }
797
- committed = true;
798
- cleanup();
799
- const val = input.value.trim();
800
- transactDoc(tab, (t) => mutateUpdateProperty(t, path, "$title", val || undefined));
801
- rerender();
802
- };
803
- const cancel = () => {
804
- if (committed) {
805
- return;
806
- }
807
- committed = true;
808
- cleanup();
809
- rerender();
810
- };
811
- input.addEventListener("blur", commit);
812
- input.addEventListener("keydown", (e: KeyboardEvent) => {
813
- if (e.key === "Enter") {
814
- e.preventDefault();
815
- input.blur();
816
- }
817
- if (e.key === "Escape") {
818
- e.preventDefault();
819
- cancel();
820
- }
821
- });
786
+ // ─── The rows, and the window ────────────────────────────────────────────────
787
+
788
+ /** The collapsed set, which is module state on `view` and survives every repaint. */
789
+ function outlineCollapsed(): Set<string> {
790
+ view._layersCollapsed ||= new Set();
791
+ return view._layersCollapsed;
822
792
  }
823
793
 
824
794
  /**
@@ -826,9 +796,9 @@ export function startLayerTitleEdit(path: JxPath, rerender: () => void) {
826
796
  *
827
797
  * Every line here is a decision about VISIBILITY — a collapsed ancestor, an inline element the
828
798
  * canvas edits as text rather than as a block, the root the content mode does not own — and no line
829
- * here builds a template. That split is the whole saving: the walk is O(nodes) and costs a few
830
- * comparisons per node, while the half it defers mounts an `sp-icon` per row and an action cluster
831
- * on the selected one. Only the rows in the window ever pay it.
799
+ * here builds a projection. That split is the whole saving: the walk is O(nodes) and costs a few
800
+ * comparisons per node, while the half it defers mounts a kit element per verb on the rows that
801
+ * offer them. Only the rows in the window ever pay it.
832
802
  *
833
803
  * @param {JxMutableNode} doc @param {string} mode @param {Set<string>} collapsed
834
804
  */
@@ -917,326 +887,465 @@ function numberOutlineSets(rows: OutlineRow[]): OutlineRow[] {
917
887
  return rows;
918
888
  }
919
889
 
890
+ /** Whether the row has anything under it — children, a map template, or `$switch` cases. */
891
+ function isExpandable(row: OutlineRow): boolean {
892
+ if (!row.item || typeof row.node !== "object") {
893
+ return false;
894
+ }
895
+ const node = row.node as JxMutableNode;
896
+ const hasChildren = Array.isArray(node.children) && node.children.length > 0;
897
+ const hasMapChildren =
898
+ node.children !== undefined &&
899
+ typeof node.children === "object" &&
900
+ (node.children as unknown as Record<string, unknown>).$prototype === "Array";
901
+ const hasCases =
902
+ Boolean(node.$switch) &&
903
+ typeof node.cases === "object" &&
904
+ node.cases !== null &&
905
+ Object.keys(node.cases).length > 0;
906
+ return hasChildren || hasMapChildren || hasCases || (row.nodeType === "map" && Boolean(node.map));
907
+ }
908
+
909
+ /** The badge: what it says, which of the six drawings it gets, and what it says on hover. */
910
+ function rowBadge(row: OutlineRow): {
911
+ badge: string;
912
+ badgeKind: string;
913
+ badgeTitle: string | null;
914
+ } {
915
+ const node = row.node as JxMutableNode;
916
+ if (row.nodeType === "map") {
917
+ return { badge: "↻", badgeKind: "map", badgeTitle: "Repeating list — one copy per item" };
918
+ }
919
+ if (row.nodeType === "case" || row.nodeType === "case-ref") {
920
+ const name = String(row.path.at(-1) ?? "");
921
+ return { badge: name, badgeKind: "case", badgeTitle: `Condition case: ${name}` };
922
+ }
923
+ if (node.$switch) {
924
+ return { badge: "⇄", badgeKind: "switch", badgeTitle: "Condition" };
925
+ }
926
+ if (node.tagName === "slot") {
927
+ const slotName = node.attributes?.name;
928
+ return {
929
+ badge: "▣",
930
+ badgeKind: "slot",
931
+ badgeTitle:
932
+ typeof slotName === "string" && slotName.trim()
933
+ ? `Slot "${slotName.trim()}"`
934
+ : "Default slot",
935
+ };
936
+ }
937
+ /* The ROW BADGE, which is the one place the tag is shown as itself rather than folded into a
938
+ label — so it is the one that rendered `[object Object]` for a chosen tag after the other reads
939
+ were fixed. `outlineLabel` never returns a tag (it prefers a title, an id, text, a class, a
940
+ landmark), which is why fixing `nodeLabel` did not reach here. */
941
+ return {
942
+ badge: displayTagName(node.tagName) || "div",
943
+ badgeKind: "tag",
944
+ badgeTitle: isTagExpression(node.tagName)
945
+ ? `Tag chosen when the element is created: ${tagNameCandidates(node.tagName).join(" or ")}`
946
+ : null,
947
+ };
948
+ }
949
+
920
950
  /** A text node's row: drawn, and deliberately not a tree item — there is nothing to do to it. */
921
- function textRowTemplate(row: OutlineRow): TemplateResult {
922
- const text = String(row.node);
923
- const preview = text.length > 40 ? `${text.slice(0, 40)}…` : text;
924
- return html`
925
- <div
926
- class="layer-row"
927
- data-jx-path=${JSON.stringify(row.path)}
928
- style="padding-left:${indentWidth(row.depth) + 8}px; opacity: 0.6; font-style: italic;"
929
- >
930
- <span
931
- class="layer-tag"
932
- style="background: var(--spectrum-gray-500, #64748b); font-size: 0.65rem;"
933
- >text</span
934
- >
935
- <span class="layer-label">${preview}</span>
936
- </div>
937
- `;
951
+ function textRowView(row: OutlineRow): OutlineRowView {
952
+ return {
953
+ actions: "hidden",
954
+ badge: "text",
955
+ badgeKind: "text",
956
+ badgeTitle: null,
957
+ commands: [],
958
+ dndDepth: null,
959
+ dndExpanded: null,
960
+ dndRow: null,
961
+ dndVoid: null,
962
+ draggable: "false",
963
+ editValue: "",
964
+ editing: "false",
965
+ expanded: "",
966
+ indent: `${indentWidth(row.depth) + INDENT_BASE}px`,
967
+ jxPath: JSON.stringify(row.path),
968
+ key: row.key,
969
+ kind: "text",
970
+ label: outlineRowLabel(row),
971
+ labelItalic: "false",
972
+ level: "",
973
+ overflow: "false",
974
+ placeholder: "",
975
+ posInSet: "",
976
+ selected: "false",
977
+ setSize: "",
978
+ };
938
979
  }
939
980
 
940
981
  /**
941
- * Pass two: one row, drawn.
982
+ * Pass two: one row, projected.
942
983
  *
943
984
  * Called for the rows in the window and for no others, which is why it takes the document facts it
944
985
  * needs (`selection`, `mode`) as arguments instead of reading `activeTab` for each row.
945
986
  */
946
- function outlineRowTemplate(
987
+ function outlineRowView(
947
988
  row: OutlineRow,
948
989
  doc: { selection: JxPath[]; mode: string },
949
990
  collapsed: Set<string>,
950
991
  registry: CommandRegistry,
951
- ctx: { rerender: () => void },
952
- ): TemplateResult {
992
+ ): OutlineRowView {
953
993
  if (!row.item) {
954
- return textRowTemplate(row);
994
+ return textRowView(row);
955
995
  }
956
996
  const { depth, key, nodeType, path } = row;
957
- const jxNode = row.node as JxMutableNode;
997
+ const node = row.node as JxMutableNode;
958
998
 
959
- // Every member of the set draws selected; the PRIMARY additionally carries the roving tab stop,
960
- // So a batch has one keyboard position rather than six.
961
- const isSelected = isPathSelected(doc.selection, path);
962
- const isPrimary = pathsEqual(path, primarySelection(doc.selection));
963
- const hasChildren = Array.isArray(jxNode.children) && jxNode.children.length > 0;
964
- const hasMapChildren =
965
- jxNode.children &&
966
- typeof jxNode.children === "object" &&
967
- (jxNode.children as unknown as Record<string, unknown>).$prototype === "Array";
968
- const hasCases =
969
- jxNode.$switch &&
970
- jxNode.cases &&
971
- typeof jxNode.cases === "object" &&
972
- Object.keys(jxNode.cases).length > 0;
973
- const isExpandable =
974
- hasChildren || hasMapChildren || hasCases || (nodeType === "map" && jxNode.map);
999
+ const selected = isPathSelected(doc.selection, path);
1000
+ const expandable = isExpandable(row);
975
1001
  // Array nodes can't accept dropped children (their content is the single map template), so they
976
1002
  // Block the make-child drop instruction like void elements do.
977
1003
  const isVoidEl =
978
- VOID_ELEMENTS.has((displayTagName(jxNode.tagName) || "div").toLowerCase()) ||
979
- nodeType === "map";
980
-
981
- /** @type {string} */
982
- let badgeClass;
983
- /** @type {string | number} */
984
- let badgeText;
985
- /** @type {string | undefined} */
986
- let badgeTitle;
987
- if (nodeType === "map") {
988
- badgeClass = "layer-tag map-tag";
989
- badgeText = "↻";
990
- badgeTitle = "Repeating list — one copy per item";
991
- } else if (nodeType === "case" || nodeType === "case-ref") {
992
- badgeClass = "layer-tag case-tag";
993
- badgeText = path.at(-1);
994
- badgeTitle = `Condition case: ${path.at(-1)}`;
995
- } else if (jxNode.$switch) {
996
- badgeClass = "layer-tag switch-tag";
997
- badgeText = "⇄";
998
- badgeTitle = "Condition";
999
- } else if (jxNode.tagName === "slot") {
1000
- const slotName = jxNode.attributes?.name;
1001
- badgeClass = "layer-tag slot-tag";
1002
- badgeText = "▣";
1003
- badgeTitle =
1004
- typeof slotName === "string" && slotName.trim()
1005
- ? `Slot "${slotName.trim()}"`
1006
- : "Default slot";
1007
- } else {
1008
- badgeClass = "layer-tag";
1009
- /* The ROW BADGE, which is the one place the tag is shown as itself rather than folded into a
1010
- label — so it is the one that rendered `[object Object]` for a chosen tag after the other
1011
- reads were fixed. `outlineLabel` never returns a tag (it prefers a title, an id, text, a
1012
- class, a landmark), which is why fixing `nodeLabel` did not reach here. */
1013
- badgeText = displayTagName(jxNode.tagName) || "div";
1014
- badgeTitle = isTagExpression(jxNode.tagName)
1015
- ? `Tag chosen when the element is created: ${tagNameCandidates(jxNode.tagName).join(" or ")}`
1016
- : undefined;
1017
- }
1018
-
1019
- /** @type {string} */
1020
- let labelText;
1021
- /** @type {boolean} */
1022
- let labelItalic;
1023
- if (nodeType === "case-ref") {
1024
- labelText = jxNode.$ref || "external";
1025
- labelItalic = true;
1026
- } else {
1027
- labelText = outlineLabel(jxNode);
1028
- labelItalic = false;
1029
- }
1030
-
1004
+ VOID_ELEMENTS.has((displayTagName(node.tagName) || "div").toLowerCase()) || nodeType === "map";
1031
1005
  // Array (repeater) nodes are first-class structural nodes — movable/draggable/deletable like
1032
1006
  // Elements. Both sit at a numeric child index; templates (path tail "map") and case nodes do
1033
1007
  // Not, so they stay selectable/editable but not structurally manipulable.
1034
- const isStructural =
1008
+ const structural =
1035
1009
  (nodeType === "element" || nodeType === "map") && typeof childIndex(path) === "number";
1036
1010
  const isRoot = doc.mode === "content" ? path.length === 0 : path.length < 2;
1011
+ const grabbable = structural && !isRoot;
1012
+ const open = expandable && !collapsed.has(key);
1013
+ const editing = isEditing(key);
1014
+
1015
+ /* The cluster exists for the selected row and the hovered one, and for no other. While the row is
1016
+ being renamed it exists for neither: the input owns the row's whole width, and the verbs would
1017
+ sit on top of its right edge. */
1018
+ const showActions = grabbable && !editing && (selected || key === _hoveredKey);
1019
+ const { commands, overflow } = showActions
1020
+ ? rowCommandViews(registry, path, key)
1021
+ : { commands: [], overflow: [] };
1022
+
1023
+ const { badge, badgeKind, badgeTitle } = rowBadge(row);
1024
+ const { $title: _ignored, ...withoutTitle } = node;
1025
+ return {
1026
+ actions: showActions ? "shown" : "hidden",
1027
+ badge,
1028
+ badgeKind,
1029
+ badgeTitle,
1030
+ commands,
1031
+ dndDepth: structural ? String(depth) : null,
1032
+ dndExpanded: structural && open ? "" : null,
1033
+ dndRow: structural ? key : null,
1034
+ dndVoid: structural && isVoidEl ? "" : null,
1035
+ draggable: grabbable ? "true" : "false",
1036
+ editValue: editing ? (node.$title ?? "") : "",
1037
+ editing: editing ? "true" : "false",
1038
+ /* `""` is a LEAF, and it is not "no answer": it is what tells `jx-tree-item` to draw no chevron
1039
+ and write no `aria-expanded`, and what makes `ArrowRight` on a row with nothing under it do
1040
+ nothing instead of stepping onto the row below. The chevron used to be a second field saying
1041
+ the same three things. */
1042
+ expanded: expandable ? (open ? "true" : "false") : "",
1043
+ indent: `${indentWidth(depth) + INDENT_BASE}px`,
1044
+ jxPath: JSON.stringify(path),
1045
+ key,
1046
+ kind: "element",
1047
+ label: outlineRowLabel(row),
1048
+ labelItalic: nodeType === "case-ref" ? "true" : "false",
1049
+ level: String(depth + 1),
1050
+ overflow: overflow.length > 0 ? "true" : "false",
1051
+ placeholder: outlineLabel(withoutTitle) || displayTagName(node.tagName) || "div",
1052
+ posInSet: String(row.posInSet),
1053
+ selected: selected ? "true" : "false",
1054
+ setSize: String(row.setSize),
1055
+ };
1056
+ }
1037
1057
 
1038
- return html`
1039
- <div
1040
- class=${classMap({ "layer-row": true, selected: isSelected })}
1041
- role="treeitem"
1042
- aria-level=${depth + 1}
1043
- aria-posinset=${row.posInSet}
1044
- aria-setsize=${row.setSize}
1045
- aria-selected=${isSelected ? "true" : "false"}
1046
- aria-expanded=${isExpandable ? (collapsed.has(key) ? "false" : "true") : nothing}
1047
- tabindex=${isPrimary ? "0" : "-1"}
1048
- data-primary=${isPrimary ? "" : nothing}
1049
- data-jx-path=${JSON.stringify(path)}
1050
- data-path=${key}
1051
- data-dnd-row=${isStructural ? key : nothing}
1052
- data-dnd-depth=${isStructural ? depth : nothing}
1053
- data-dnd-void=${isStructural && isVoidEl ? "" : nothing}
1054
- data-dnd-expanded=${isStructural && isExpandable && !collapsed.has(key) ? "" : nothing}
1055
- @click=${(e: MouseEvent) => {
1056
- applyRowSelection(path, { additive: e.ctrlKey || e.metaKey, range: e.shiftKey });
1057
- panToElement(path);
1058
- }}
1059
- @dblclick=${
1060
- isStructural
1061
- ? (e: MouseEvent) => {
1062
- e.stopPropagation();
1063
- startLayerTitleEdit(path, ctx.rerender);
1064
- }
1065
- : nothing
1066
- }
1067
- @contextmenu=${
1068
- isStructural
1069
- ? (e: MouseEvent) =>
1070
- showContextMenu(e, path, {
1071
- rerender: ctx.rerender,
1072
- })
1073
- : nothing
1074
- }
1075
- >
1076
- <span class="layer-indent" style="width:${indentWidth(depth)}px"></span>
1077
- <span class="layer-toggle"
1078
- >${
1079
- isExpandable
1080
- ? html`
1081
- ${
1082
- collapsed.has(key)
1083
- ? html`<sp-icon-chevron-right></sp-icon-chevron-right>`
1084
- : html`<sp-icon-chevron-down></sp-icon-chevron-down>`
1085
- }
1086
- `
1087
- : nothing
1088
- }</span
1089
- >
1090
- <span class=${badgeClass} title=${ifDefined(badgeTitle ?? undefined)}>${badgeText}</span>
1091
- <span class="layer-label" style=${labelItalic ? "font-style:italic" : nothing}
1092
- >${labelText}</span
1093
- >
1094
- ${
1095
- isStructural && !isRoot
1096
- ? html`<span class="layer-drag-handle" title="Drag to reorder">⠿</span>`
1097
- : nothing
1098
- }
1099
- ${
1100
- // The selected row's cluster is declared here so it survives every re-render; every
1101
- // Other structural row keeps an EMPTY slot, which the pointer fills (mountHoverActions)
1102
- // And empties again. The two branches are different templates, so lit swaps the DOM
1103
- // Between them and a hover cluster can never outlive the row becoming selected.
1104
- isStructural && !isRoot
1105
- ? isSelected
1106
- ? html`<span class="layer-actions">${renderRowCommands(registry, path)}</span>`
1107
- : html`<span class="layer-actions"></span>`
1108
- : nothing
1109
- }
1110
- </div>
1111
- `;
1058
+ /**
1059
+ * The row the caret starts on, as the projection reports it.
1060
+ *
1061
+ * A SEED, not a clamp. The tab stop this replaced was decided over the WINDOW and re-decided on
1062
+ * every paint, so a wheel moved it; `jx-tree` owns the caret, holds it across every repaint that
1063
+ * does not change `current`, and clamps only the tab STOP into the drawn rows — so this module
1064
+ * keeps no caret of its own. What it owes the element is a row to start on and a row to come back
1065
+ * to when the one it was on has gone, and the primary selection is that row.
1066
+ *
1067
+ * It must NAME one, and the fallback is what guarantees it rather than tidiness: `move` reports the
1068
+ * caret's own row as `from`, and a tree nobody has touched yet — or one scrolled far enough that
1069
+ * the selected row is not drawn — would report `""` and send every ↓ off the bottom of the window
1070
+ * back to the top of the document.
1071
+ */
1072
+ function outlineCaretKey(window: OutlineRow[], selection: JxPath[]): string {
1073
+ const primary = primarySelection(selection);
1074
+ const held = primary === null ? -1 : outlineIndexOfPath(primary);
1075
+ const row = _outlineRows[held];
1076
+ return row?.item === true ? row.key : (window.find((line) => line.item)?.key ?? "");
1112
1077
  }
1113
1078
 
1114
1079
  /**
1115
- * @param {{ navigateToComponent: (path: string) => void; rerender: () => void }} ctx
1116
- * @returns {import("lit-html").TemplateResult}
1080
+ * What the surface should be showing right now.
1081
+ *
1082
+ * Exported because it is the half of this module that is worth testing on its own: given the open
1083
+ * document, the collapsed set and the window, it is the whole of what the Outline draws.
1084
+ *
1085
+ * @returns {OutlineValues}
1117
1086
  */
1118
- export function renderLayersTemplate(ctx: {
1119
- navigateToComponent: (path: string) => void;
1120
- rerender: () => void;
1121
- }) {
1087
+ export function outlineValues(): OutlineValues {
1122
1088
  const tab = activeTab.value;
1123
- // The scroll watch outlives this call and must never repaint through a closure from an earlier
1124
- // One — the Navigator's scheduler is the only thing that knows how to draw the Outline.
1125
- _outlineRerender = ctx.rerender;
1126
-
1127
- for (const fn of view.dndCleanups) {
1128
- fn();
1129
- }
1130
- view.dndCleanups = [];
1131
-
1132
- view._layersCollapsed ||= new Set();
1133
- const collapsed = view._layersCollapsed;
1089
+ const collapsed = outlineCollapsed();
1134
1090
  const registry = selectionCommandRegistry();
1091
+ const mode = tab?.doc.mode ?? "";
1135
1092
 
1136
- _outlineRows = buildOutlineRows(tab!.doc.document, tab?.doc.mode ?? "", collapsed);
1093
+ _outlineRows = tab ? buildOutlineRows(tab.doc.document, mode, collapsed) : [];
1137
1094
  // The window is computed from the PREVIOUS render's tree element, because that is the only one
1138
- // That exists while this template is being built. On the first paint of a session there is none
1095
+ // That exists while this projection is being built. On the first paint of a session there is none
1139
1096
  // And `listWindow` answers "all of them" — which is exactly what the Outline did before it
1140
- // Windowed, and what the panel's `afterRender` then measures in order to ask for a second pass.
1097
+ // Windowed, and what the tree's `onNodeCreated` then measures in order to ask for a second pass.
1141
1098
  const range = listWindow(_outlineList, {
1142
1099
  count: _outlineRows.length,
1143
1100
  rowHeight: outlineRowHeight(),
1144
1101
  });
1145
- const doc = { mode: tab?.doc.mode ?? "", selection: tab!.session.selection };
1146
- const layerRows = _outlineRows
1147
- .slice(range.start, range.end)
1148
- .map((row) => ({ key: row.key, tpl: outlineRowTemplate(row, doc, collapsed, registry, ctx) }));
1149
-
1150
- return html`
1151
- <div class="layers-container" style="position:relative">
1152
- <div
1153
- class="layers-tree"
1154
- role="tree"
1155
- aria-label="Document outline"
1156
- ${ref((el) => {
1157
- if (el) {
1158
- afterTreeRender(el as HTMLElement);
1159
- }
1160
- })}
1161
- @keydown=${(e: KeyboardEvent) => onTreeKeydown(e, collapsed, ctx.rerender)}
1162
- @mouseover=${onTreeHover}
1163
- @mouseleave=${clearHoverActions}
1164
- @click=${(e: MouseEvent) => {
1165
- const toggle = (e.target as HTMLElement).closest(".layer-toggle");
1166
- if (!toggle) {
1167
- return;
1168
- }
1169
- e.stopPropagation();
1170
- const row = toggle.closest(".layer-row");
1171
- if (!row) {
1172
- return;
1173
- }
1174
- const key = (row as HTMLElement).dataset.path;
1175
- if (!key) {
1176
- return;
1177
- }
1178
- if (collapsed.has(key)) {
1179
- collapsed.delete(key);
1180
- } else {
1181
- collapsed.add(key);
1182
- }
1183
- ctx.rerender();
1184
- }}
1185
- >
1186
- ${
1187
- _outlineRows.length === 0
1188
- ? renderEmptyState({
1189
- actions: [
1190
- {
1191
- label: "Add an element",
1192
- run: () => {
1193
- // `"insert"`, not `"blocks"`. The panel was renamed in P3.1 and this call
1194
- // Kept the old id for three phases, so the one action an empty page offers
1195
- // Landed the Navigator on "No Navigator panel is registered as blocks".
1196
- // `setActivityTab` takes a `NavigatorPanelId` now, so this cannot recur.
1197
- setActivityTab("insert");
1198
- },
1199
- },
1200
- ],
1201
- message: "This page is empty. Everything you add to it is listed here, in order.",
1202
- })
1203
- : html`
1204
- <!-- The rows the window left above and below, as the height they would have
1205
- occupied: the scrollbar stays the length of the whole document, and
1206
- aria-hidden keeps two empty spacers out of a tree that owns treeitems. -->
1207
- <div style="height:${range.padTop}px" aria-hidden="true"></div>
1208
- ${repeat(
1209
- layerRows,
1210
- (r) => r.key,
1211
- (r) => r.tpl,
1212
- )}
1213
- <div style="height:${range.padBottom}px" aria-hidden="true"></div>
1214
- `
1215
- }
1216
- </div>
1217
- </div>
1218
- `;
1102
+ const selection = tab?.session.selection ?? [];
1103
+ const drawn = _outlineRows.slice(range.start, range.end);
1104
+ return {
1105
+ current: outlineCaretKey(drawn, selection),
1106
+ emptyLabel: "Add an element",
1107
+ emptyMessage: "This page is empty. Everything you add to it is listed here, in order.",
1108
+ padBottom: range.padBottom,
1109
+ padTop: range.padTop,
1110
+ rows: drawn.map((row) => outlineRowView(row, { mode, selection }, collapsed, registry)),
1111
+ view: _outlineRows.length === 0 ? "empty" : "rows",
1112
+ };
1113
+ }
1114
+
1115
+ // ─── The standing surface ────────────────────────────────────────────────────
1116
+
1117
+ /**
1118
+ * The surface standing in the Navigator, and the node it was mounted into.
1119
+ *
1120
+ * One slot rather than a per-host map, for the reason `panels/stylebook-layers-panel.ts` gives:
1121
+ * there is one Navigator, so a mount into a DIFFERENT node is the old one being replaced, and
1122
+ * holding both would leave the first one's effects running against a scope nobody writes any more.
1123
+ */
1124
+ let standing: { host: HTMLElement; handle: OutlineSurfaceHandle } | null = null;
1125
+ /** Re-register drag-and-drop once the rows the projection asked for are actually in the DOM. */
1126
+ let _registerDnD: (() => void) | null = null;
1127
+
1128
+ /** Update the standing surface with a fresh projection. A no-op before the first mount. */
1129
+ function redrawOutline(): void {
1130
+ standing?.handle.update(outlineValues());
1219
1131
  }
1220
1132
 
1221
1133
  /**
1222
- * Keep the selected row on screen after a repaint.
1223
- *
1224
- * Two cases now, where there used to be one. If the row is drawn, it scrolls itself into view, as
1225
- * it always has. If it is NOT — the canvas selected a node three thousand rows down, a jump from
1226
- * Problems, a collaborator's edit — then the row the author is meant to see is precisely the one
1227
- * with no element to call `scrollIntoView` on, and the reveal has to be arithmetic: scroll to where
1228
- * the model says the row is, and let the scroll watch draw it. Silently doing nothing would be the
1229
- * windowing bug that looks like a selection bug.
1134
+ * The selection the last reveal followed, or null before there has been one.
1135
+ *
1136
+ * `<tab id>|<path>`, because a tab switch moves the selection as surely as a click does and two
1137
+ * documents can hold the same path. Reset by {@link detachOutline}, so coming back to the tree from
1138
+ * Project Styles shows the reader where they are rather than wherever the scroller was left.
1230
1139
  */
1231
- function revealSelectedRow(host: HTMLElement): void {
1232
- const drawn = host.querySelector(".layer-row.selected");
1140
+ let _revealedSelection: string | null = null;
1141
+
1142
+ /** What the Outline is being asked to keep on screen — the identity a reveal follows. */
1143
+ function selectionRevealKey(): string {
1144
+ const tab = activeTab.value;
1145
+ const path = primarySelection(tab?.session.selection);
1146
+ return `${tab?.id ?? ""}|${path === null ? "" : JSON.stringify(path)}`;
1147
+ }
1148
+
1149
+ /**
1150
+ * Reveal the selected row when the SELECTION is what moved, and never merely because a repaint did.
1151
+ *
1152
+ * The Outline was unbrowsable past its own selection, and the loop is short enough to state: a
1153
+ * scroll changes the window, the window asks for a repaint, the repaint re-runs the panel's
1154
+ * `afterRender`, and the mount ended by scrolling the selected row back into view. Measured on
1155
+ * `sites/jxsuite.com/pages/compare.json` with the selected row 840px down a 890px viewport: asked
1156
+ * 400 → got 400, asked 800 → got 800, asked 1500 → got 555, asked 2400 → got 555. Everything past
1157
+ * the selection was unreachable, which for a document outline is most of the document.
1158
+ *
1159
+ * So the reveal is bound to the thing it is FOR. The selection moving is the event — the canvas
1160
+ * selecting a node three thousand rows down, a jump from Problems, a keyboard walk, a collaborator
1161
+ * — and a repaint that leaves the selection where it was is not that event. The in-tree gestures
1162
+ * pass this gate as well as the external ones, which is right: they change the selection too, and
1163
+ * `scrollIntoView({ block: "nearest" })` on a row already in view does nothing.
1164
+ */
1165
+ function revealChangedSelection(): void {
1166
+ const key = selectionRevealKey();
1167
+ if (key === _revealedSelection) {
1168
+ return;
1169
+ }
1170
+ _revealedSelection = key;
1171
+ revealSelectedRow();
1172
+ }
1173
+
1174
+ /**
1175
+ * Keep the selected row on screen.
1176
+ *
1177
+ * Two cases, where there used to be one. If the row is drawn, it scrolls itself into view. If it is
1178
+ * NOT — the canvas selected a node three thousand rows down, a jump from Problems, a collaborator's
1179
+ * edit — then the row the author is meant to see is precisely the one with no element to call
1180
+ * `scrollIntoView` on, and the reveal has to be arithmetic: scroll to where the model says the row
1181
+ * is, and let the scroll watch draw it. Silently doing nothing would be the windowing bug that
1182
+ * looks like a selection bug.
1183
+ *
1184
+ * WHEN this runs is {@link revealChangedSelection}'s decision, not this function's.
1185
+ */
1186
+ function revealSelectedRow(): void {
1187
+ const drawn = _outlineList?.querySelector(`${ROW_SELECTOR}[aria-selected="true"]`);
1233
1188
  if (drawn) {
1234
- drawn.scrollIntoView({ behavior: "smooth", block: "nearest" });
1189
+ drawn.scrollIntoView?.({ behavior: "smooth", block: "nearest" });
1235
1190
  return;
1236
1191
  }
1237
1192
  revealOutlineRow(outlineIndexOfPath(primarySelection(activeTab.value?.session.selection)));
1238
1193
  }
1239
1194
 
1195
+ /** The pointer moved onto a row: it, and the selected row, are the two that carry a cluster. */
1196
+ function hoverRow(key: string): void {
1197
+ if (_hoveredKey === key) {
1198
+ return;
1199
+ }
1200
+ _hoveredKey = key;
1201
+ redrawOutline();
1202
+ }
1203
+
1204
+ /** The pointer left the tree, so no row is under it. Also the suites' reset. */
1205
+ export function clearOutlineHover(): void {
1206
+ if (_hoveredKey === null) {
1207
+ return;
1208
+ }
1209
+ _hoveredKey = null;
1210
+ redrawOutline();
1211
+ }
1212
+
1213
+ /**
1214
+ * Draw the Outline — mounting the document the first time, updating it every time after.
1215
+ *
1216
+ * `panels/left-panel.ts` hands a panel's `afterRender` the CONTENT box itself, which is the node
1217
+ * lit renders this panel's (empty) body into and the one whose comment markers must survive. The
1218
+ * `.panel-content` lookup below is what that call used to require and is kept for a caller handing
1219
+ * the body around it; either way the document must not be appended to the wrong one of the two, or
1220
+ * the tree is drawn under whatever the Navigator paints next.
1221
+ *
1222
+ * @param {{ rerender: () => void; registerDnD: () => void }} ctx
1223
+ * @param {HTMLElement} host - The panel's content box
1224
+ */
1225
+ export function mountOutlinePanel(
1226
+ ctx: { rerender: () => void; registerDnD: () => void },
1227
+ host: HTMLElement,
1228
+ ): void {
1229
+ const target = host.querySelector<HTMLElement>(".panel-content") ?? host;
1230
+ if (standing && (standing.host !== target || !standing.handle.connected())) {
1231
+ standing.handle.dispose();
1232
+ standing = null;
1233
+ }
1234
+ // The scroll watch outlives this call and must never repaint through a closure from an earlier
1235
+ // One — the Navigator's scheduler is the only thing that knows how to draw the Outline.
1236
+ _outlineRerender = ctx.rerender;
1237
+ _registerDnD = ctx.registerDnD;
1238
+
1239
+ const values = outlineValues();
1240
+ if (standing) {
1241
+ standing.handle.update(values);
1242
+ } else {
1243
+ standing = {
1244
+ handle: mountOutlineSurface(target, values, {
1245
+ contextMenu: (_scope, event) => {
1246
+ const path = outlineRowPath(event.target as Element | null);
1247
+ if (path) {
1248
+ showContextMenu(event as MouseEvent, path, { rerender: () => _outlineRerender?.() });
1249
+ redrawOutline();
1250
+ }
1251
+ },
1252
+ editCancel,
1253
+ editCommit,
1254
+ editInput,
1255
+ editReady: (element) => {
1256
+ /* On a microtask, because `onNodeCreated` fires as the node is BUILT and one tick before
1257
+ it is in the document — which is exactly why it is the seam (guidelines §9.4): it
1258
+ hands over the element earlier than awaiting the mount would, and the host decides
1259
+ when it is worth anything. An unconnected input cannot take focus. */
1260
+ queueMicrotask(() => {
1261
+ const input = element as HTMLInputElement;
1262
+ if (input.isConnected) {
1263
+ input.focus();
1264
+ input.select?.();
1265
+ }
1266
+ });
1267
+ },
1268
+ expand: expandOutlineRow,
1269
+ emptyAction: () => {
1270
+ /* `"insert"`, not `"blocks"`. The panel was renamed in P3.1 and this call kept the old id
1271
+ for three phases, so the one action an empty page offers landed the Navigator on "No
1272
+ Navigator panel is registered as blocks". `setActivityTab` takes a `NavigatorPanelId`
1273
+ now, so this cannot recur. */
1274
+ setActivityTab("insert");
1275
+ },
1276
+ hover: hoverRow,
1277
+ hoverOut: clearOutlineHover,
1278
+ move: moveOutlineCaret,
1279
+ overflowRow: (key, opener) => {
1280
+ const path = pathFromKey(key);
1281
+ const registry = selectionCommandRegistry();
1282
+ const { overflow } = rowCommandViews(registry, path, key);
1283
+ if (opener instanceof HTMLElement && overflow.length > 0) {
1284
+ showCommandOverflow(opener, registry, overflow, path);
1285
+ }
1286
+ },
1287
+ rename: (key) => {
1288
+ /* The selection follows the rename, which is what `Enter` always did: the row being
1289
+ renamed is the row the inspector and the canvas are about. */
1290
+ const index = outlineIndexOfKey(key);
1291
+ selectModelRow(index);
1292
+ startLayerTitleEdit(pathFromKey(key), () => _outlineRerender?.());
1293
+ },
1294
+ reveal: (key) => {
1295
+ revealPathInCanvas(pathFromKey(key));
1296
+ },
1297
+ runRow: (id, key, control) => {
1298
+ if (control instanceof HTMLElement) {
1299
+ control.blur();
1300
+ }
1301
+ runCommand(selectionCommandRegistry(), id, pathFromKey(key));
1302
+ redrawOutline();
1303
+ },
1304
+ select: selectOutlineRow,
1305
+ treeReady: (element) => {
1306
+ _outlineList = element;
1307
+ },
1308
+ typeahead: typeaheadOutlineCaret,
1309
+ }),
1310
+ host: target,
1311
+ };
1312
+ }
1313
+ /* A resolved promise's `.then` is a microtask, so this is one tick after the projection either
1314
+ way — but on the FIRST pass the mount is genuinely asynchronous (the kit has to be defined
1315
+ before a document can render), and a `requestAnimationFrame` inside `registerLayersDnD` would
1316
+ otherwise find an empty container and register nothing at all. */
1317
+ void standing.handle.ready.then(() => {
1318
+ watchOutlineTree();
1319
+ /* `panels/dnd.ts` releases what it previously took, so this is a re-registration and not a
1320
+ second one. Taking the list down HERE, before the call, is what put 369 duplicate-registration
1321
+ warnings in one session's console: the registration is deferred a frame, so a second repaint
1322
+ arriving inside that frame released an empty list and queued a second pass over the same rows. */
1323
+ _registerDnD?.();
1324
+ revealChangedSelection();
1325
+ });
1326
+ }
1327
+
1328
+ /**
1329
+ * Take the tree down.
1330
+ *
1331
+ * Called by the record when the pane starts showing Project Styles. Without it the document
1332
+ * survives its own irrelevance: both bodies are appended into the same `.panel-content`, so the one
1333
+ * nobody took down sits under the one that replaced it for the rest of the session.
1334
+ */
1335
+ export function detachOutline(): void {
1336
+ standing?.handle.dispose();
1337
+ standing = null;
1338
+ _outlineList = null;
1339
+ _outlineWatch?.window.destroy();
1340
+ _outlineWatch = null;
1341
+ _hoveredKey = null;
1342
+ _editing = null;
1343
+ /* The tree the next mount draws is a fresh scroller at the top, so whatever the last reveal
1344
+ followed says nothing about it: forgetting is what makes the first paint after a return show
1345
+ the reader where the selection is. */
1346
+ _revealedSelection = null;
1347
+ }
1348
+
1240
1349
  /**
1241
1350
  * Contribute the Outline panel.
1242
1351
  *
@@ -1250,23 +1359,28 @@ export function registerLayersPanel(): void {
1250
1359
  title: "Outline",
1251
1360
  level: "document",
1252
1361
  dock: "navigator",
1253
- icon: "sp-icon-layers",
1362
+ icon: "stack",
1254
1363
  requiresDocument: "Open a page to see the elements it is built from.",
1255
- render: (ctx) =>
1256
- ctx.deps.getCanvasMode() === "stylebook"
1257
- ? renderStylebookLayersTemplate({
1258
- selectStylebookTag,
1259
- stylebookMeta,
1260
- } as Parameters<typeof renderStylebookLayersTemplate>[0])
1261
- : renderLayersTemplate({
1262
- navigateToComponent: ctx.deps.navigateToComponent,
1263
- rerender: ctx.rerender,
1264
- }),
1364
+ /*
1365
+ * One panel, two bodies, and neither of them is lit's. The body is a document either way, so
1366
+ * there is nothing for lit to draw here — a document CLEARS nothing and is APPENDED, and lit's
1367
+ * own comment markers inside `.panel-content` are what it finds its (empty) content by.
1368
+ */
1369
+ render: (): PanelBody => nothing,
1370
+ /*
1371
+ * `afterRender` runs on every repaint, and both mounts are idempotent — the standing surface is
1372
+ * updated where it is still there and re-mounted only where something has taken it out. The
1373
+ * detach on each side is not symmetry for its own sake: the two documents are appended into the
1374
+ * same node, so a body nobody took down would sit under the one that replaced it.
1375
+ */
1265
1376
  afterRender: (ctx, host) => {
1266
- if (ctx.deps.getCanvasMode() !== "stylebook") {
1267
- ctx.deps.registerLayersDnD();
1377
+ if (ctx.deps.getCanvasMode() === "stylebook") {
1378
+ detachOutline();
1379
+ mountStylebookLayersPanel({ selectStylebookTag, stylebookMeta }, host);
1380
+ return;
1268
1381
  }
1269
- revealSelectedRow(host);
1382
+ detachStylebookLayers();
1383
+ mountOutlinePanel({ registerDnD: ctx.deps.registerLayersDnD, rerender: ctx.rerender }, host);
1270
1384
  },
1271
1385
  });
1272
1386
  }