@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
@@ -1,7 +1,12 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Style panel — CSS property editor under the **Target Line** (§6.1), with provenance-coded rows
4
- * (§6.2), a section accordion, shorthand expand/compress, and a property filter.
3
+ * Style panel — the FLOW behind the Inspector's Style tab (§6).
4
+ *
5
+ * The tab itself is a Jx document over the kit (`surfaces/style-panel.json`, mounted by
6
+ * `surfaces/style-panel.ts`), so this module draws no markup: it projects. What stays here is
7
+ * everything that is a DECISION — which compound coordinate the tab is editing, what css-meta says
8
+ * a property is, where a value came from, what a commit writes and into how many elements, and
9
+ * which of the four lists a control offers. The surface reads values.
5
10
  *
6
11
  * The panel resolves a compound coordinate before it can commit anything: `(selection, breakpoint,
7
12
  * scheme layer, nested selector)`. It has always computed that tuple — it is the per-field key and
@@ -10,27 +15,48 @@
10
15
  * gone: the breakpoint and scheme axes are selected on the pane context bar (region ⑦), and the
11
16
  * selector is the one axis this tab owns.
12
17
  *
13
- * Every row then says where its value came from. `provenance.ts` supplies the chip;
18
+ * Every row then says where its value came from. `provenance.ts` supplies the vocabulary;
14
19
  * `computeInheritedSources()` supplies the donor breakpoint the cascade walk always knew and this
15
20
  * panel used to throw away.
21
+ *
22
+ * Three consequences of the conversion are worth knowing before editing it.
23
+ *
24
+ * **The tab keeps ITSELF up to date, and must never go back through the scheduler.**
25
+ * `panels/panel-scheduler.ts`'s focus guard exists because a lit repaint of the whole dock could
26
+ * take the field a reader is typing into; {@link mountStyleTab} owns an `effect()` instead, so a
27
+ * commit, a breakpoint change and a settled usage query each reach the document as one write per
28
+ * bound property, and an equal write is skipped. `renderOnly("rightPanel")` is gone from this file
29
+ * with it.
30
+ *
31
+ * **Two controls are lists, and the list is the KIT MENU** (`surfaces/menu.ts`, §12.5). The unit
32
+ * picker and the keyword suggestions were each an `sp-overlay` + `sp-menu` of their own; the Value
33
+ * Source picker was a third. All three are `openMenu()` now, which is also how roving focus,
34
+ * typeahead and Escape arrive here without a line of keyboard code.
35
+ *
36
+ * **Nothing here is an island any more.** One control was: `specs/ui.md` §5.6 (Colour) was Pending,
37
+ * so a colour row drew an announced `[part="control-host"]` and a lit `paintColorControl` filled
38
+ * it, and that was the last Spectrum on this surface. §5.6 landed. The row is a `jx-color-field` in
39
+ * the document beside every other control, its palette is a `jx-swatch-group` slotted into the
40
+ * picker, and `ui/color-selector.ts` is what remains: which colours THIS PROJECT has named, which
41
+ * is a question no kit element can answer.
42
+ *
43
+ * @docs studio/design/style-inspector
16
44
  */
17
45
 
18
- import { html, nothing } from "lit-html";
19
46
  import { getNestedStyle } from "@jxsuite/schema/guards";
20
- import { live } from "lit-html/directives/live.js";
21
- import { ifDefined } from "lit-html/directives/if-defined.js";
47
+ import { effect, effectScope, reactive } from "../reactivity";
22
48
  import {
23
- COMMON_SELECTORS,
49
+ cancelStyleDebounce,
24
50
  debouncedStyleCommit,
25
51
  getNodeAtPath,
26
52
  isNestedSelector,
27
- renderOnly,
28
53
  updateUi,
29
54
  } from "../store";
30
55
  import { activeTab } from "../workspace/workspace";
31
56
  import { primarySelection, unifyValues } from "../tabs/selection";
32
57
  import { shell } from "../shell";
33
58
  import { activeRegistry } from "../commands/active-registry";
59
+ import { selectorsForNode } from "../utils/element-selectors";
34
60
  import { selectStylebookTag } from "./stylebook-panel";
35
61
  import {
36
62
  mutateUpdateMediaNestedStyle,
@@ -41,29 +67,35 @@ import {
41
67
  mutateUpdateStyle,
42
68
  transactDoc,
43
69
  } from "../tabs/transact";
44
- import { inferInputType, propLabel } from "../utils/studio-utils";
70
+ import {
71
+ abbreviateValue,
72
+ friendlyNameToVar,
73
+ inferInputType,
74
+ kebabToLabel,
75
+ propLabel,
76
+ varDisplayName,
77
+ } from "../utils/studio-utils";
45
78
  import { stringArg } from "../commands/command-args";
46
79
  import type { AnyCommand, CommandRegistry } from "../commands/registry";
47
- import { renderFieldRow } from "../ui/field-row";
48
80
  import { showPromptDialog } from "../ui/layers";
49
- import { renderDynamicSlot } from "../ui/dynamic-slot";
50
81
  import { parseMediaEntries, schemeOfQuery } from "../utils/canvas-media";
51
82
  import { getEffectiveMedia, getEffectiveStyle } from "../site-context";
52
83
  import { computeInheritedSources } from "../utils/inherited-style";
53
84
  import { loadUsages, peekUsages, usageFiles } from "../services/references";
54
85
  import { mediaDisplayName } from "./shared";
55
- import { countProvenance, renderProvenanceChip, renderProvenanceDots } from "./provenance";
56
86
  import {
57
- openSelectorMenu,
58
- renderTargetLine,
59
- resetSelectorTrigger as resetTargetSelector,
60
- } from "./target-line";
87
+ countProvenance,
88
+ provenanceSummaryText,
89
+ provenanceText,
90
+ provenanceTitle,
91
+ } from "./provenance";
61
92
  import {
62
- clickAnythingTo,
63
- openPageAction,
64
- renderEmptyState,
65
- staleSelectionMessage,
66
- } from "./empty-state";
93
+ attachTargetLine,
94
+ openSelectorMenu,
95
+ resetTargetLine as resetTargetSelector,
96
+ setTargetLine,
97
+ } from "../surfaces/target-line";
98
+ import { clickAnythingTo, openPageAction, staleSelectionMessage } from "./empty-state";
67
99
  import {
68
100
  allConditionsPass,
69
101
  autoOpenSections,
@@ -73,12 +105,34 @@ import {
73
105
  expandBorderSide,
74
106
  expandShorthand,
75
107
  getCssInitialMap,
108
+ getFontVars,
109
+ TYPO_PREVIEW_CHANNELS,
76
110
  getLonghands,
77
111
  } from "./style-utils";
78
- import { widgetForType } from "./style-inputs";
79
-
112
+ import { UNIT_RE } from "../ui/unit-selector";
113
+ import { colorTokens, resolvedColor } from "../ui/color-selector";
114
+ import { resolveTokenValue, toTokenRef, tokenRefName } from "../style/token-ref";
115
+ import { mountStylePanelSurface } from "../surfaces/style-panel";
116
+ import { openMenu } from "../surfaces/menu";
117
+ import type { MenuHandle } from "../surfaces/menu";
118
+ import { cloneValue } from "../tabs/doc-op-apply";
119
+ import { effectiveSlotMode, slotModeSeed, switchSlotMode } from "../ui/dynamic-slot";
120
+ import { slotCaps, VALUE_SOURCE_HINTS, VALUE_SOURCE_LABELS } from "../ui/value-source";
121
+ import { dialogPathFor } from "../canvas/dialog-path";
122
+
123
+ import type { SlotMode } from "../ui/value-source";
80
124
  import type { FieldProvenance, ProvenanceState } from "./provenance";
81
- import type { TargetScope, TargetSegment } from "./target-line";
125
+ import type { TargetScope, TargetSegment } from "../surfaces/target-line";
126
+ import type {
127
+ StyleButtonView,
128
+ StyleOptionView,
129
+ StylePanelActions,
130
+ StylePanelSurface,
131
+ StylePanelView,
132
+ StyleRowView,
133
+ StyleSectionView,
134
+ } from "../surfaces/style-panel";
135
+ import type { EffectScope } from "../reactivity";
82
136
  import type { Tab } from "../tabs/tab";
83
137
  import type { JxPath } from "../state";
84
138
  import type { JsonValue } from "../types";
@@ -256,217 +310,6 @@ function mixedStyleProps(
256
310
  return mixed;
257
311
  }
258
312
 
259
- // ─── Row renderers ──────────────────────────────────────────────────────────
260
-
261
- function renderStyleRow(
262
- entry: CssPropertyEntry,
263
- prop: string,
264
- value: string,
265
- onCommit: (v: string | undefined) => void,
266
- isWarning: boolean,
267
- gridMode: boolean,
268
- inheritedValue: string | undefined,
269
- templateSignals: string[] = [],
270
- fieldKey: string = prop,
271
- provenance?: FieldProvenance,
272
- ) {
273
- const chip: FieldProvenance = provenance ?? DEFAULT_PROVENANCE;
274
- const type = inferInputType(entry);
275
- const hasVal = value !== undefined && value !== "";
276
- const placeholder = !hasVal && inheritedValue ? String(inheritedValue) : "";
277
- const spanVal = gridMode && (entry as Record<string, unknown>).$span === 2 ? 2 : undefined;
278
- // The rungs come from `StyleObject.additionalProperties`, not from a list here (§6.6 rule 2).
279
- // That derives literal + ${} template: a CSS value carrying `${…}` IS a signal binding, which
280
- // `emitStyleString` compiles to a reactive declaration, and `$ref` is not admitted by JxStyle.
281
- // Naming the POSITION rather than the answer is the point — a schema that later admits another
282
- // Rung cannot then be contradicted by a hand-written array sitting here.
283
- const slot = renderDynamicSlot({
284
- caps: "styleProperty",
285
- fieldKey,
286
- onChange: (v?: JsonValue) => onCommit(v === undefined || v === "" ? undefined : String(v)),
287
- staticWidget: widgetForType(type, entry, prop, value, onCommit, { placeholder }),
288
- stateDefs: templateSignals,
289
- value,
290
- });
291
- // `hasValue` is false because the chip supersedes the row's derived set-dot: the dot could only
292
- // Say set-or-not, and three of the four states this panel can now answer with were previously
293
- // Indistinguishable from "unset".
294
- return renderFieldRow({
295
- prop,
296
- label: propLabel(entry, prop),
297
- hasValue: false,
298
- widget: slot.widget,
299
- provenance: chip,
300
- labelExtra: slot.modeButton,
301
- ...(spanVal != null && { span: spanVal }),
302
- warning: isWarning,
303
- });
304
- }
305
-
306
- /** What {@link renderShorthandRow} needs to draw one shorthand and its longhand children. */
307
- interface ShorthandRowOptions {
308
- prop: string;
309
- entry: CssPropertyEntry;
310
- /** The style block at the edited coordinate — the primary element's, as every row reads it. */
311
- style: Record<string, unknown>;
312
- /** The write, already fanned across the selection. Called INSIDE a transaction. */
313
- mutate: StyleMutateFn;
314
- inherited: Record<string, string | number>;
315
- ctx: ProvenanceCtx;
316
- }
317
-
318
- /**
319
- * A shorthand property row: the header field, plus one row per longhand when it is expanded.
320
- *
321
- * Two things were wrong with it, and they were the same thing. It took a single-target mutation
322
- * while every other row in the tab took the selection-wide one, so "set padding on six cards in one
323
- * decision" — the sentence §6.5 makes, with padding as its example — wrote to one card. And it drew
324
- * its own `.set-dot` rather than a provenance chip, so a shorthand `mixedStyleProps` had ALREADY
325
- * flagged as mixed offered a plain "Clear padding" affordance instead of saying so.
326
- *
327
- * Both are fixed by taking the same two things every other row takes: `mutate`, which fans across
328
- * the selection, and `ctx`, which answers where a value came from.
329
- */
330
- function renderShorthandRow({
331
- prop: shortProp,
332
- entry,
333
- style,
334
- mutate,
335
- inherited,
336
- ctx,
337
- }: ShorthandRowOptions) {
338
- const tab = activeTab.value!;
339
- const longhands = getLonghands(shortProp) as CssLonghand[];
340
- const shortVal = style[shortProp];
341
- const hasLonghands = longhands.some((l: CssLonghand) => style[l.name] !== undefined);
342
- const isExpanded = tab.session.ui.styleShorthands[shortProp] ?? hasLonghands;
343
- const hasAnyVal = shortVal !== undefined || hasLonghands;
344
-
345
- /**
346
- * Drop every longhand, then write the shorthand — the one write order this row has.
347
- *
348
- * The longhands go unconditionally rather than "the ones the panel can see set", because what the
349
- * panel can see is the PRIMARY element's style: a second selected element with a `paddingTop` of
350
- * its own would otherwise keep it and quietly out-rank the shorthand just written over it.
351
- * Deleting a property an element does not have is a no-op on that element.
352
- */
353
- const writeShorthand = (val?: string | undefined) =>
354
- transactDoc(activeTab.value, (t) => {
355
- for (const l of longhands) {
356
- mutate(t, l.name);
357
- }
358
- mutate(t, shortProp, val);
359
- });
360
- const clearAll = () =>
361
- transactDoc(activeTab.value, (t) => {
362
- mutate(t, shortProp);
363
- for (const l of longhands) {
364
- mutate(t, l.name);
365
- }
366
- });
367
-
368
- const headerWidget = html`
369
- <div class="style-shorthand-header">
370
- <sp-textfield
371
- size="s"
372
- .value=${live(shortVal || "")}
373
- placeholder=${
374
- !shortVal && hasLonghands
375
- ? longhands.map((l: CssLonghand) => style[l.name] || "0").join(" ")
376
- : !shortVal && inherited[shortProp]
377
- ? inherited[shortProp]
378
- : !shortVal && longhands.some((l: CssLonghand) => inherited[l.name])
379
- ? longhands.map((l: CssLonghand) => inherited[l.name] || "0").join(" ")
380
- : ""
381
- }
382
- @input=${debouncedStyleCommit(`short:${shortProp}`, 400, (e: Event) => {
383
- writeShorthand((e.target as HTMLInputElement).value || undefined);
384
- })}
385
- ></sp-textfield>
386
- <sp-action-button
387
- size="xs"
388
- quiet
389
- @click=${(e: Event) => {
390
- e.stopPropagation();
391
- activeTab.value!.session.ui.styleShorthands = {
392
- ...activeTab.value!.session.ui.styleShorthands,
393
- [shortProp]: !isExpanded,
394
- };
395
- }}
396
- >
397
- ${
398
- isExpanded
399
- ? html`<sp-icon-chevron-down slot="icon"></sp-icon-chevron-down>`
400
- : html`<sp-icon-chevron-right slot="icon"></sp-icon-chevron-right>`
401
- }
402
- </sp-action-button>
403
- </div>
404
- `;
405
-
406
- return html`
407
- ${renderFieldRow({
408
- prop: shortProp,
409
- label: propLabel(entry, shortProp),
410
- hasValue: hasAnyVal,
411
- widget: headerWidget,
412
- provenance: shorthandProvenance(shortProp, ctx, hasAnyVal ? clearAll : undefined),
413
- })}
414
- ${
415
- isExpanded
416
- ? (() => {
417
- const isBorderSide =
418
- (entry as Record<string, unknown>).$shorthandType === "border-side";
419
- const expanded = shortVal
420
- ? isBorderSide
421
- ? expandBorderSide(shortVal as string)
422
- : expandShorthand(shortVal as string, longhands.length)
423
- : null;
424
- const compress = isBorderSide ? compressBorderSide : compressShorthand;
425
- const emptyVal = isBorderSide ? "" : "0";
426
- /** This longhand becomes `val`; the others keep whatever they show. */
427
- const recompress = (idx: number, val: string) =>
428
- writeShorthand(
429
- compress(
430
- longhands.map((l: CssLonghand, i: number) =>
431
- i === idx ? val : (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
432
- ) as string[],
433
- ),
434
- );
435
- return longhands.map(({ name, entry: lEntry }: CssLonghand, idx: number) => {
436
- const lVal = style[name] ?? (expanded ? expanded[idx] : "");
437
- const hasLVal = lVal !== undefined && lVal !== "";
438
- return html`
439
- <div class="style-row style-row--child" data-prop=${name}>
440
- <div class="style-row-label">
441
- ${renderProvenanceChip(
442
- name,
443
- longhandProvenance(name, shortProp, hasLVal, ctx, () =>
444
- recompress(idx, emptyVal),
445
- ),
446
- )}
447
- <sp-field-label size="s" title=${name}
448
- >${propLabel(lEntry, name)}</sp-field-label
449
- >
450
- </div>
451
- ${widgetForType(
452
- inferInputType(lEntry),
453
- lEntry,
454
- name,
455
- lVal as string,
456
- (newVal: string) => recompress(idx, newVal || emptyVal),
457
- {
458
- placeholder: !lVal && inherited[name] ? String(inherited[name]) : "",
459
- },
460
- )}
461
- </div>
462
- `;
463
- });
464
- })()
465
- : nothing
466
- }
467
- `;
468
- }
469
-
470
313
  // ─── The edit target ─────────────────────────────────────────────────────────
471
314
 
472
315
  /** Forget the selector menu's element handle — the Inspector unmounted, or a test starts clean. */
@@ -478,13 +321,15 @@ export function resetSelectorMenu(): void {
478
321
  * Whether the "show affected" list under a project-wide warning band is open.
479
322
  *
480
323
  * Module-local rather than a `session.ui` field: it is a disclosure on a project-level warning, and
481
- * a per-document field would make the same warning open in one tab and closed in another.
324
+ * a per-document field would make the same warning open in one tab and closed in another. Reactive
325
+ * because the tab is now driven by an `effect()` — a plain field would move the disclosure and
326
+ * nothing would redraw.
482
327
  */
483
- let _showAffected = false;
328
+ const _disclosure = reactive({ showAffected: false, usageRevision: 0 });
484
329
 
485
330
  /** Fold the affected list away — the Inspector unmounted, or a test starts clean. */
486
331
  export function resetAffectedDisclosure(): void {
487
- _showAffected = false;
332
+ _disclosure.showAffected = false;
488
333
  }
489
334
 
490
335
  /**
@@ -580,21 +425,32 @@ function projectScope(label: string, tagName: string | null): TargetScope {
580
425
  return { affected: "how many pages that is, is unknown", kind: "project", label };
581
426
  }
582
427
  const query = { tagName };
428
+ // The cache behind `peekUsages` is a plain Map, so a settled sweep is invisible to the effect
429
+ // Driving this tab. The revision is what makes it visible, and reading it here is what enrols
430
+ // The projection in it.
431
+ void _disclosure.usageRevision;
583
432
  const state = peekUsages(query);
584
433
  if (state === null) {
585
- // First paint for this tag: ask once, and repaint when the answer lands. `loadUsages` joins an
586
- // In-flight request, so a panel repainting sixty times a second is still one sweep.
587
- void loadUsages(query).then(() => renderOnly("rightPanel"));
434
+ // First projection for this tag: ask once, and re-project when the answer lands. `loadUsages`
435
+ // Joins an in-flight request, so a tab re-projecting sixty times a second is still one sweep.
436
+ void loadUsages(query).then(() => {
437
+ /* The bump is conditional on the cache having actually SETTLED, and that condition is what
438
+ makes the loop terminate. Bumping unconditionally would re-run the effect, which would find
439
+ `peekUsages` still null, ask again and bump again — a spin, not a repaint. A host that
440
+ cannot answer this query at all is exactly the state that produces it. */
441
+ if (peekUsages(query) !== null) {
442
+ _disclosure.usageRevision += 1;
443
+ }
444
+ });
588
445
  }
589
446
  return {
590
447
  kind: "project",
591
448
  label,
592
449
  affected: affectedSentence(state),
593
450
  affectedFiles: state?.status === "ready" ? usageFiles(state.result) : [],
594
- showAffected: _showAffected,
451
+ showAffected: _disclosure.showAffected,
595
452
  onToggleAffected: () => {
596
- _showAffected = !_showAffected;
597
- renderOnly("rightPanel");
453
+ _disclosure.showAffected = !_disclosure.showAffected;
598
454
  },
599
455
  };
600
456
  }
@@ -841,25 +697,737 @@ function sectionProvenance(
841
697
  );
842
698
  }
843
699
 
844
- // ─── Main template ──────────────────────────────────────────────────────────
700
+ // ─── The row register ────────────────────────────────────────────────────────
701
+
702
+ /** One choice a row's list offers. `divider` draws a rule above the entry. */
703
+ interface StyleChoice {
704
+ value: string;
705
+ label: string;
706
+ divider?: boolean;
707
+ checked?: boolean;
708
+ }
709
+
710
+ /**
711
+ * What one projected row can DO, held beside the row the document draws.
712
+ *
713
+ * The document names a row by its `key` and nothing else, so this is where a key becomes a verb.
714
+ * Every handler is optional because rows differ in what they own: a longhand child has no Value
715
+ * Source ladder, a custom pair has no unit list, and the add-a-property field has no value to
716
+ * clear.
717
+ */
718
+ interface RowActions {
719
+ /** The reader is typing — debounced under {@link RowActions.debounceId}. */
720
+ edit?: (value: string) => void;
721
+ /** The reader left the field or pressed Enter. Cancels the pending debounce first. */
722
+ commit?: (value: string) => void;
723
+ debounceId?: string;
724
+ /** The provenance chip, a custom pair's remove and a nested rule's remove are one verb. */
725
+ chip?: () => void;
726
+ /** The list this row's chooser offers, and what taking one from it does. */
727
+ choices?: StyleChoice[];
728
+ choicesLabel?: string;
729
+ choose?: (value: string) => void;
730
+ /** The Value Source ladder for this position (§6.3). */
731
+ source?: {
732
+ mode: SlotMode;
733
+ caps: SlotMode[];
734
+ fieldKey: string;
735
+ value: unknown;
736
+ stateDefs: string[];
737
+ onChange: (value?: JsonValue) => void;
738
+ };
739
+ /** Expand or collapse a shorthand's longhands. */
740
+ toggle?: () => void;
741
+ /** Rename a custom property, keeping each element's own value. */
742
+ rename?: (name: string) => void;
743
+ /** Seed a new custom property. Returns whether anything was written. */
744
+ add?: (name: string) => boolean;
745
+ /** Open a nested rule as the active selector. */
746
+ open?: () => void;
747
+ }
748
+
749
+ /** Every row on screen, by key. Rebuilt whole on every projection; read by every action. */
750
+ let _rows = new Map<string, RowActions>();
751
+
752
+ /** What each section's clear-all dot does. Rebuilt with the sections it belongs to. */
753
+ let _sectionClears = new Map<string, () => void>();
754
+
755
+ /** What "+ Add" under Relative Styling does. Rebuilt with the section. */
756
+ let _nestedAdd: (() => void) | null = null;
757
+
758
+ /** The value each button-group row is holding — pressing the selected button again clears it. */
759
+ let _buttonValues = new Map<string, string>();
760
+
761
+ /** Look one row up. A key the projection no longer has is a stale click, and does nothing. */
762
+ function rowActions(key: string): RowActions | undefined {
763
+ return _rows.get(key);
764
+ }
765
+
766
+ // ─── Widgets ─────────────────────────────────────────────────────────────────
767
+
768
+ /** Whether a typed value is a bare number — the unit control's own test. */
769
+ function isNumericValue(value: string): boolean {
770
+ return /^-?\d*\.?\d*$/.test(value);
771
+ }
772
+
773
+ /**
774
+ * Kit glyph names for the values a button-group offers.
775
+ *
776
+ * Only names the kit ACTUALLY has (`@jxsuite/ui/icons`) are here, and every other value falls back
777
+ * to `abbreviateValue`, which is the same fallback the group has always had for a value with no
778
+ * icon. The alternative was to guess an approximation — `align-left` for `justify-content:
779
+ * flex-start`, say, which is a text-alignment glyph — and a glyph that means something else is
780
+ * worse than three letters that mean what they say. The seventeen names still missing are recorded
781
+ * against the kit rather than approximated here.
782
+ */
783
+ const BUTTON_GLYPHS: Readonly<Record<string, string>> = {
784
+ "arrow-down": "arrow-down",
785
+ "arrow-left": "arrow-left",
786
+ "arrow-right": "arrow-right",
787
+ "arrow-up": "arrow-up",
788
+ "text-align-center": "text-align-center",
789
+ "text-align-justify": "text-align-justify",
790
+ "text-align-left": "text-align-left",
791
+ "text-align-right": "text-align-right",
792
+ };
793
+
794
+ /** The buttons a button-group row draws, and the overflow list it hands to the kit menu. */
795
+ function buttonGroup(
796
+ entry: CssPropertyEntry,
797
+ rowKey: string,
798
+ value: string,
799
+ ): { buttons: StyleButtonView[]; extra: string[] } {
800
+ const values = (entry.$buttonValues || entry.enum || []) as string[];
801
+ const iconMap = (entry.$icons || {}) as Record<string, string>;
802
+ const buttonValues = entry.$buttonValues as string[] | undefined;
803
+ const enumValues = entry.enum as string[] | undefined;
804
+ const extra =
805
+ buttonValues && enumValues && enumValues.length > buttonValues.length
806
+ ? enumValues.filter((v) => !buttonValues.includes(v))
807
+ : [];
808
+ const buttons = values.map((v) => {
809
+ const icon = BUTTON_GLYPHS[iconMap[v] ?? ""] ?? "";
810
+ return {
811
+ icon,
812
+ row: rowKey,
813
+ selected: v === value,
814
+ text: icon === "" ? abbreviateValue(v) : "",
815
+ title: v,
816
+ value: v,
817
+ };
818
+ });
819
+ return { buttons, extra };
820
+ }
821
+
822
+ /** How a keyword reads in a list: `Inline block` for `inline-block`, `Flex` for `flex`. */
823
+ function keywordLabel(value: string): string {
824
+ return value.includes("-") ? kebabToLabel(value) : value.replace(/^./, (c) => c.toUpperCase());
825
+ }
826
+
827
+ /** The keyword list a button row's overflow menu offers, in the words the menu prints. */
828
+ function keywordChoices(options: string[], value: string): StyleChoice[] {
829
+ return options.map((v) => ({ checked: v === value, label: keywordLabel(v), value: v }));
830
+ }
831
+
832
+ /**
833
+ * The keyword list a `keywords` row suggests under its field.
834
+ *
835
+ * A typography row's options are their own preview: the axis the row moves is set on each option as
836
+ * the value it offers, through the `jx-option` channel `TYPO_PREVIEW_CHANNELS` names, and every
837
+ * such option is set in the element's own `face` — so a weight row shows `700` at 700 in the
838
+ * typeface the element actually uses, rather than in the panel's. Any other row's options are the
839
+ * words alone.
840
+ */
841
+ function keywordOptions(options: string[], prop: string, face: string): StyleOptionView[] {
842
+ const channel = TYPO_PREVIEW_CHANNELS[prop];
843
+ return options.map((v) => {
844
+ const option: StyleOptionView = { label: keywordLabel(v), value: v };
845
+ if (channel) {
846
+ option[channel] = v;
847
+ option.face = face;
848
+ }
849
+ return option;
850
+ });
851
+ }
852
+
853
+ /**
854
+ * The typeface the edited element is set in, for the typography rows to preview their values in:
855
+ * its own `fontFamily`, else the base context's when a breakpoint is being edited (which is what
856
+ * `inheritedStyle` holds — the cascade within the coordinate, not the ancestors'), followed through
857
+ * the effective style to the stack at the end of the chain. Empty when the element has no typeface
858
+ * of its own or the reference cannot be followed, in which case the rows are set in the panel's.
859
+ */
860
+ function previewFace(ctx: EditorCtx): string {
861
+ const raw = ctx.activeStyle.fontFamily ?? ctx.inheritedStyle.fontFamily;
862
+ if (typeof raw !== "string" && typeof raw !== "number") {
863
+ return "";
864
+ }
865
+ const resolved = resolveTokenValue(getEffectiveStyle(ctx.tab.doc.document?.style), raw);
866
+ return resolved === undefined ? "" : String(resolved);
867
+ }
868
+
869
+ /** The Modern Font Stacks a `fontFamily` entry offers, as css-meta.json states them. */
870
+ function fontPresets(entry: CssPropertyEntry): { title: string; value: string }[] {
871
+ return Array.isArray(entry.presets) ? (entry.presets as { title: string; value: string }[]) : [];
872
+ }
873
+
874
+ /**
875
+ * The font list: the project's `--font-*` tokens first — the site's and the document's together,
876
+ * which is where a project declares its fonts once — then the presets it has not minted yet. Every
877
+ * row is its own specimen through `face`, which is the reason this is a combobox over `jx-option`
878
+ * rows rather than the kit menu it used to be: a menu row is an action and cannot draw itself in a
879
+ * typeface (ui.md §5.1), and a font is the one thing a reader chooses by looking.
880
+ *
881
+ * A row's VALUE is a token's name, minted or not, because that is what the field holds after a pick
882
+ * and what the commit turns into `var(--name)`. A token row says its name beside its title, so what
883
+ * the pick will write is in the list; a preset row is bare, which is what tells the two groups
884
+ * apart in a list that draws no divider.
885
+ */
886
+ function fontOptions(entry: CssPropertyEntry): StyleOptionView[] {
887
+ const fontVars = getFontVars();
888
+ const style = getEffectiveStyle(activeTab.value?.doc.document?.style);
889
+ const options: StyleOptionView[] = fontVars.map((fv) => ({
890
+ description: fv.name,
891
+ face: String(resolveTokenValue(style, fv.value) ?? fv.value),
892
+ label: varDisplayName(fv.name, "--font-"),
893
+ value: fv.name,
894
+ }));
895
+ for (const preset of fontPresets(entry)) {
896
+ const name = friendlyNameToVar(preset.title, "--font-");
897
+ if (!fontVars.some((fv) => fv.name === name)) {
898
+ options.push({ face: preset.value, label: preset.title, value: name });
899
+ }
900
+ }
901
+ return options;
902
+ }
903
+
904
+ /**
905
+ * What the font field WRITES. A token name becomes the reference that points at it; free text —
906
+ * `Georgia, serif` — is the value itself; and the empty string clears. A name that is a preset's
907
+ * and no token yet is minted into the document's own style first, so the token exists before
908
+ * anything references it — but only on a COMMIT, never on the debounced edit that follows each
909
+ * keystroke: a reader halfway through typing `--font-slab-serif` has not asked for a token, and one
910
+ * minted early would be left behind when they finished typing something else.
911
+ */
912
+ function commitFont(
913
+ entry: CssPropertyEntry,
914
+ raw: string,
915
+ mint: boolean,
916
+ onCommit: (value: string) => void,
917
+ ): void {
918
+ const name = raw.trim();
919
+ if (!name.startsWith("--")) {
920
+ onCommit(raw);
921
+ return;
922
+ }
923
+ if (mint && getEffectiveStyle(activeTab.value?.doc.document?.style)[name] === undefined) {
924
+ const preset = fontPresets(entry).find((p) => friendlyNameToVar(p.title, "--font-") === name);
925
+ if (preset) {
926
+ transactDoc(activeTab.value, (t) => mutateUpdateStyle(t, [], name, preset.value));
927
+ }
928
+ }
929
+ onCommit(toTokenRef(name));
930
+ }
931
+
932
+ /** A blank row, so every `$switch` case reads a path that exists whatever the row draws. */
933
+ function blankRow(key: string, prop: string, kind: StyleRowView["kind"]): StyleRowView {
934
+ return {
935
+ buttons: [],
936
+ child: false,
937
+ chip: "none",
938
+ chipState: "default",
939
+ chipText: "",
940
+ chipTitle: "",
941
+ choicesHint: "",
942
+ choicesLabel: "",
943
+ expandIcon: "caret-right",
944
+ expandLabel: "",
945
+ expanded: false,
946
+ hasChoices: false,
947
+ hasLabel: false,
948
+ hasSource: false,
949
+ hasTokens: false,
950
+ key,
951
+ kind,
952
+ label: "",
953
+ max: "",
954
+ min: "",
955
+ mono: false,
956
+ name: "",
957
+ openTitle: "",
958
+ options: [],
959
+ overflowSelected: false,
960
+ placeholder: "",
961
+ prop,
962
+ removeLabel: "",
963
+ resolved: "",
964
+ sourceHint: "",
965
+ sourceLabel: "",
966
+ sourceState: "literal",
967
+ span: false,
968
+ step: "",
969
+ tokens: [],
970
+ value: "",
971
+ warning: false,
972
+ widget: "text",
973
+ };
974
+ }
975
+
976
+ /** Fill a row's chip fields from the provenance answer, and register its click. */
977
+ function applyChip(row: StyleRowView, actions: RowActions, prov: FieldProvenance): void {
978
+ if (prov.state === "default") {
979
+ row.chip = "none";
980
+ return;
981
+ }
982
+ row.chipState = prov.state;
983
+ row.chipText = provenanceText(prov);
984
+ row.chipTitle = provenanceTitle(row.prop, prov);
985
+ if (prov.onClick) {
986
+ row.chip = "button";
987
+ actions.chip = prov.onClick;
988
+ } else {
989
+ row.chip = "text";
990
+ }
991
+ }
992
+
993
+ // ─── The projection ──────────────────────────────────────────────────────────
994
+
995
+ /** What {@link buildEditor} is handed once, having resolved the coordinate. */
996
+ interface EditorCtx {
997
+ tab: Tab;
998
+ /** The block at the edited coordinate, as the rows display it. */
999
+ activeStyle: JxStyle;
1000
+ activeSelector: string | null;
1001
+ editMedia: string | null;
1002
+ inheritedStyle: Record<string, string | number>;
1003
+ provCtx: ProvenanceCtx;
1004
+ templateSignals: string[];
1005
+ /** One write, fanned across every selected element, inside the caller's transaction. */
1006
+ mutate: StyleMutateFn;
1007
+ /** The same write in a transaction of its own. */
1008
+ commit: (prop: string, value?: string | Record<string, unknown> | undefined) => void;
1009
+ /** The key prefix a row's identity is built from — the coordinate, spelled once. */
1010
+ coord: string;
1011
+ targets: readonly JxPath[];
1012
+ filter: string;
1013
+ }
1014
+
1015
+ /**
1016
+ * The rungs a CSS declaration admits.
1017
+ *
1018
+ * Derived from `StyleObject.additionalProperties` rather than listed (§6.6 rule 2), minus `ref` —
1019
+ * and that subtraction is the rule the panel already wrote down being enforced rather than a
1020
+ * narrowing of it. **`JxStyle` has no `$ref` branch at all**: a CSS value carrying `${…}` IS the
1021
+ * signal binding, which `emitStyleString` compiles to a reactive declaration, and a `{ $ref }` in a
1022
+ * declaration is a document the schema refuses. The generic ladder offered the rung anyway whenever
1023
+ * the document happened to declare a signal — the old panel's own test asserted "literal and
1024
+ * template only" and passed only because its fixture had none — so a document with one signal in it
1025
+ * put a control on every style row that writes an invalid style block.
1026
+ */
1027
+ function styleCaps(): SlotMode[] {
1028
+ return slotCaps("styleProperty").filter((mode) => mode !== "ref");
1029
+ }
1030
+
1031
+ /** One ordinary property row: the label, the chip, the ladder and whichever control it draws. */
1032
+ function fieldRow(
1033
+ ctx: EditorCtx,
1034
+ entry: CssPropertyEntry,
1035
+ prop: string,
1036
+ value: string,
1037
+ opts: {
1038
+ child?: boolean;
1039
+ warning?: boolean;
1040
+ span?: boolean;
1041
+ provenance: FieldProvenance;
1042
+ onCommit: (value: string | undefined) => void;
1043
+ placeholder?: string;
1044
+ },
1045
+ ): { row: StyleRowView; actions: RowActions } {
1046
+ const key = `${ctx.coord}|${prop}${opts.child ? "|child" : ""}`;
1047
+ const row = blankRow(key, prop, "field");
1048
+ const actions: RowActions = {};
1049
+ const type = inferInputType(entry);
1050
+ const cssInitial = getCssInitialMap().get(prop) ?? "";
1051
+ const placeholder = opts.placeholder || cssInitial;
1052
+ const commitLiteral = (v: string) => opts.onCommit(v === "" ? undefined : v);
1053
+
1054
+ row.label = propLabel(entry, prop);
1055
+ row.hasLabel = true;
1056
+ row.child = opts.child === true;
1057
+ row.warning = opts.warning === true;
1058
+ row.span = opts.span === true;
1059
+ row.value = value;
1060
+ row.placeholder = placeholder;
1061
+ applyChip(row, actions, opts.provenance);
1062
+
1063
+ // The Value Source ladder (§6.3), and only where the position has one to offer. A longhand child
1064
+ // Is not a document position of its own — its value is a slice of the shorthand above it — so it
1065
+ // Carries no chip, which is what it has always done.
1066
+ if (!opts.child) {
1067
+ const caps = styleCaps();
1068
+ const fieldKey = `${ctx.coord}|${prop}`;
1069
+ const mode = effectiveSlotMode(fieldKey, value === "" ? undefined : value);
1070
+ row.hasSource = true;
1071
+ row.sourceState = mode;
1072
+ row.sourceLabel = VALUE_SOURCE_LABELS[mode];
1073
+ row.sourceHint = `Value source: ${VALUE_SOURCE_LABELS[mode]} — click to change`;
1074
+ actions.source = {
1075
+ caps,
1076
+ fieldKey,
1077
+ mode,
1078
+ onChange: (v) => opts.onCommit(v === undefined || v === "" ? undefined : String(v)),
1079
+ stateDefs: ctx.templateSignals,
1080
+ value: value === "" ? undefined : value,
1081
+ };
1082
+ if (mode === "template") {
1083
+ // The `${…}` rung: a mixed-text value is committed when the field is LEFT, never while it is
1084
+ // Typed — a half-written `${state.m` is not a binding, and writing one would move the row's
1085
+ // Rung out from under the caret.
1086
+ row.widget = "text";
1087
+ row.mono = true;
1088
+ row.placeholder = "${state.…}";
1089
+ actions.commit = (v) => opts.onCommit(v === "" ? undefined : v);
1090
+ return { actions, row };
1091
+ }
1092
+ }
1093
+
1094
+ switch (type) {
1095
+ case "color": {
1096
+ row.widget = "color";
1097
+ row.tokens = colorTokens();
1098
+ row.hasTokens = row.tokens.length > 0;
1099
+ /* The literal behind a token, for the chip: `var(--color-accent)` resolves in the canvas and
1100
+ nowhere in this page, so without it a pick from the palette drew the no-colour chip. */
1101
+ row.resolved = resolvedColor(value);
1102
+ /* Both verbs, as a text row has: the field says `input` on every frame of a drag and
1103
+ `change` when the reader lets go, so the drag is debounced into one write per pause and
1104
+ the release lands immediately. */
1105
+ actions.edit = commitLiteral;
1106
+ actions.commit = commitLiteral;
1107
+ actions.debounceId = `color:${key}`;
1108
+ break;
1109
+ }
1110
+ case "button-group": {
1111
+ const { buttons, extra } = buttonGroup(entry, key, value);
1112
+ row.widget = "buttons";
1113
+ row.buttons = buttons;
1114
+ row.hasChoices = extra.length > 0;
1115
+ row.overflowSelected = extra.includes(value);
1116
+ row.choicesLabel = "";
1117
+ row.choicesHint = `More ${row.label.toLowerCase()} values`;
1118
+ actions.choices = keywordChoices(extra, value);
1119
+ actions.choose = commitLiteral;
1120
+ break;
1121
+ }
1122
+ case "number-unit": {
1123
+ const units = (entry.$units ?? []) as string[];
1124
+ const keywords = (entry.$keywords ?? []) as string[];
1125
+ const match = UNIT_RE.exec(value);
1126
+ const isKeyword = !match && value !== "" && keywords.includes(value);
1127
+ const currentUnit = isKeyword ? (units[0] ?? "") : (match?.[2] ?? units[0] ?? "");
1128
+ row.widget = "group";
1129
+ row.value = unitDisplayValue(value, match, isKeyword);
1130
+ row.placeholder = unitPlaceholder(placeholder);
1131
+ row.hasChoices = units.length > 0 || keywords.length > 0;
1132
+ row.choicesLabel = currentUnit || units[0] || "";
1133
+ row.choicesHint = `Unit for ${row.label.toLowerCase()}`;
1134
+ actions.choices = [
1135
+ ...units.map((u) => ({ checked: u === currentUnit, label: u, value: u })),
1136
+ ...keywords.map((kw, index) => ({
1137
+ checked: kw === value,
1138
+ label: kw,
1139
+ value: kw,
1140
+ ...(index === 0 && units.length > 0 ? { divider: true } : {}),
1141
+ })),
1142
+ ];
1143
+ actions.choose = (chosen) => {
1144
+ if (keywords.includes(chosen)) {
1145
+ opts.onCommit(chosen);
1146
+ return;
1147
+ }
1148
+ const numPart = UNIT_RE.exec(value)?.[1];
1149
+ if (numPart) {
1150
+ opts.onCommit(numPart + chosen);
1151
+ }
1152
+ };
1153
+ {
1154
+ const commitUnitValue = (raw: string) => {
1155
+ const v = raw.trim();
1156
+ if (v === "") {
1157
+ opts.onCommit("");
1158
+ return;
1159
+ }
1160
+ opts.onCommit(isNumericValue(v) && units.length > 0 ? v + currentUnit : v);
1161
+ };
1162
+ actions.edit = commitUnitValue;
1163
+ actions.commit = commitUnitValue;
1164
+ actions.debounceId = `nui:${key}`;
1165
+ }
1166
+ break;
1167
+ }
1168
+ case "number": {
1169
+ row.widget = "number";
1170
+ row.min = entry.minimum === undefined ? "" : String(entry.minimum);
1171
+ row.max = entry.maximum === undefined ? "" : String(entry.maximum);
1172
+ row.step = typeof entry.maximum === "number" && entry.maximum <= 1 ? "0.1" : "";
1173
+ actions.commit = commitLiteral;
1174
+ break;
1175
+ }
1176
+ case "select":
1177
+ case "combobox": {
1178
+ const options = Array.isArray(entry.enum)
1179
+ ? (entry.enum as string[])
1180
+ : Array.isArray(entry.examples)
1181
+ ? (entry.examples as string[])
1182
+ : [];
1183
+ // A keyword row is the combobox contract exactly: the field IS the value, and the rows
1184
+ // Under it are the values worth offering. `allows-custom-value` is what makes an enum a
1185
+ // List of suggestions rather than a whitelist — `full-width` is a text-transform whether or
1186
+ // Not the catalogue names it. The font row is the same element over a different list, with
1187
+ // A token showing as its own name rather than as the `var()` around it: the field edits
1188
+ // Which token this is, and `var(--font-body)` is punctuation the reader did not type. What
1189
+ // Differs is what a commit WRITES, and that is `commitFont`'s.
1190
+ row.widget = "keywords";
1191
+ if (prop === "fontFamily") {
1192
+ row.value = tokenRefName(value) ?? value;
1193
+ row.options = fontOptions(entry);
1194
+ actions.edit = (v) => commitFont(entry, v, false, commitLiteral);
1195
+ actions.commit = (v) => commitFont(entry, v, true, commitLiteral);
1196
+ } else {
1197
+ row.options = keywordOptions(options, prop, previewFace(ctx));
1198
+ actions.edit = commitLiteral;
1199
+ actions.commit = commitLiteral;
1200
+ }
1201
+ actions.debounceId = `kw:${key}`;
1202
+ break;
1203
+ }
1204
+ default: {
1205
+ row.widget = "text";
1206
+ actions.edit = commitLiteral;
1207
+ actions.commit = commitLiteral;
1208
+ actions.debounceId = `text:${key}`;
1209
+ break;
1210
+ }
1211
+ }
1212
+ return { actions, row };
1213
+ }
1214
+
1215
+ /** What a unit field SHOWS: the number alone, or the keyword, or whatever could not be parsed. */
1216
+ function unitDisplayValue(
1217
+ value: string,
1218
+ match: RegExpExecArray | null,
1219
+ isKeyword: boolean,
1220
+ ): string {
1221
+ if (isKeyword) {
1222
+ return value;
1223
+ }
1224
+ if (match) {
1225
+ return match[1] ?? "";
1226
+ }
1227
+ if (value === "") {
1228
+ return "";
1229
+ }
1230
+ // Intentional partial parse: a shorthand like "10px 20px" shows its leading number.
1231
+ // oxlint-disable-next-line unicorn/prefer-number-coercion
1232
+ const num = Number.parseFloat(value);
1233
+ return Number.isNaN(num) ? value : String(num);
1234
+ }
1235
+
1236
+ /** A unit field's placeholder is the inherited NUMBER — "500", not "500px". */
1237
+ function unitPlaceholder(placeholder: string): string {
1238
+ return UNIT_RE.exec(placeholder)?.[1] ?? placeholder ?? "0";
1239
+ }
1240
+
1241
+ /** A shorthand header row and, when it is expanded, the longhand children under it. */
1242
+ function shorthandRows(
1243
+ ctx: EditorCtx,
1244
+ entry: CssPropertyEntry,
1245
+ shortProp: string,
1246
+ ): { row: StyleRowView; actions: RowActions }[] {
1247
+ const longhands = getLonghands(shortProp) as CssLonghand[];
1248
+ const style = ctx.activeStyle as Record<string, unknown>;
1249
+ const shortVal = style[shortProp];
1250
+ const hasLonghands = longhands.some((l) => style[l.name] !== undefined);
1251
+ const isExpanded = ctx.tab.session.ui.styleShorthands[shortProp] ?? hasLonghands;
1252
+ const hasAnyVal = shortVal !== undefined || hasLonghands;
1253
+ const inherited = ctx.inheritedStyle;
1254
+
1255
+ /**
1256
+ * Drop every longhand, then write the shorthand — the one write order this row has.
1257
+ *
1258
+ * The longhands go unconditionally rather than "the ones the panel can see set", because what the
1259
+ * panel can see is the PRIMARY element's style: a second selected element with a `paddingTop` of
1260
+ * its own would otherwise keep it and quietly out-rank the shorthand just written over it.
1261
+ * Deleting a property an element does not have is a no-op on that element.
1262
+ */
1263
+ const writeShorthand = (val?: string | undefined) =>
1264
+ transactDoc(activeTab.value, (t) => {
1265
+ for (const l of longhands) {
1266
+ ctx.mutate(t, l.name);
1267
+ }
1268
+ ctx.mutate(t, shortProp, val);
1269
+ });
1270
+ const clearAll = () =>
1271
+ transactDoc(activeTab.value, (t) => {
1272
+ ctx.mutate(t, shortProp);
1273
+ for (const l of longhands) {
1274
+ ctx.mutate(t, l.name);
1275
+ }
1276
+ });
1277
+
1278
+ const key = `${ctx.coord}|${shortProp}`;
1279
+ const row = blankRow(key, shortProp, "shorthand");
1280
+ const actions: RowActions = {};
1281
+ row.widget = "shorthand";
1282
+ row.hasLabel = true;
1283
+ row.label = propLabel(entry, shortProp);
1284
+ row.value = typeof shortVal === "string" ? shortVal : "";
1285
+ row.placeholder = shorthandPlaceholder(shortVal, longhands, style, inherited, shortProp);
1286
+ row.expanded = isExpanded;
1287
+ row.expandIcon = isExpanded ? "caret-down" : "caret-right";
1288
+ row.expandLabel = `${isExpanded ? "Collapse" : "Expand"} ${row.label.toLowerCase()}`;
1289
+ applyChip(
1290
+ row,
1291
+ actions,
1292
+ shorthandProvenance(shortProp, ctx.provCtx, hasAnyVal ? clearAll : undefined),
1293
+ );
1294
+ actions.edit = (v) => writeShorthand(v || undefined);
1295
+ actions.commit = (v) => writeShorthand(v || undefined);
1296
+ actions.debounceId = `short:${key}`;
1297
+ actions.toggle = () => {
1298
+ const tab = activeTab.value;
1299
+ if (tab) {
1300
+ tab.session.ui.styleShorthands = {
1301
+ ...tab.session.ui.styleShorthands,
1302
+ [shortProp]: !isExpanded,
1303
+ };
1304
+ }
1305
+ };
1306
+
1307
+ const out = [{ actions, row }];
1308
+ if (!isExpanded) {
1309
+ return out;
1310
+ }
1311
+
1312
+ const isBorderSide = entry.$shorthandType === "border-side";
1313
+ const expanded =
1314
+ typeof shortVal === "string" && shortVal
1315
+ ? isBorderSide
1316
+ ? expandBorderSide(shortVal)
1317
+ : expandShorthand(shortVal, longhands.length)
1318
+ : null;
1319
+ const compress = isBorderSide ? compressBorderSide : compressShorthand;
1320
+ const emptyVal = isBorderSide ? "" : "0";
1321
+ /** This longhand becomes `val`; the others keep whatever they show. */
1322
+ const recompress = (idx: number, val: string) =>
1323
+ writeShorthand(
1324
+ compress(
1325
+ longhands.map((l, i) =>
1326
+ i === idx ? val : (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
1327
+ ) as string[],
1328
+ ),
1329
+ );
1330
+
1331
+ for (const [idx, { name, entry: lEntry }] of longhands.entries()) {
1332
+ const lVal = style[name] ?? (expanded ? expanded[idx] : "");
1333
+ const hasLVal = lVal !== undefined && lVal !== "";
1334
+ out.push(
1335
+ fieldRow(ctx, lEntry, name, String(lVal ?? ""), {
1336
+ child: true,
1337
+ onCommit: (newVal) => recompress(idx, newVal || emptyVal),
1338
+ placeholder: !lVal && inherited[name] !== undefined ? String(inherited[name]) : "",
1339
+ provenance: longhandProvenance(name, shortProp, hasLVal, ctx.provCtx, () =>
1340
+ recompress(idx, emptyVal),
1341
+ ),
1342
+ }),
1343
+ );
1344
+ }
1345
+ return out;
1346
+ }
1347
+
1348
+ /** The shorthand header's placeholder: the longhands it would compress, or what shows through. */
1349
+ function shorthandPlaceholder(
1350
+ shortVal: unknown,
1351
+ longhands: CssLonghand[],
1352
+ style: Record<string, unknown>,
1353
+ inherited: Record<string, string | number>,
1354
+ shortProp: string,
1355
+ ): string {
1356
+ if (shortVal) {
1357
+ return "";
1358
+ }
1359
+ if (longhands.some((l) => style[l.name] !== undefined)) {
1360
+ return longhands.map((l) => style[l.name] || "0").join(" ");
1361
+ }
1362
+ if (inherited[shortProp] !== undefined) {
1363
+ return String(inherited[shortProp]);
1364
+ }
1365
+ if (longhands.some((l) => inherited[l.name] !== undefined)) {
1366
+ return longhands.map((l) => inherited[l.name] || "0").join(" ");
1367
+ }
1368
+ return "";
1369
+ }
1370
+
1371
+ // ─── The whole view ──────────────────────────────────────────────────────────
1372
+
1373
+ /** The three teaching states, and the one that offers a button. */
1374
+ function emptyView(message: string, withOpen: boolean): StylePanelView {
1375
+ return {
1376
+ emptyActionLabel: withOpen ? openPageAction().label : "",
1377
+ emptyMessage: message,
1378
+ filter: "",
1379
+ hasEmptyAction: withOpen,
1380
+ sections: [],
1381
+ view: "empty",
1382
+ };
1383
+ }
1384
+
1385
+ /** What the tab is showing, from the state it can read right now. */
1386
+ function buildView(canvasMode: () => string): StylePanelView {
1387
+ _rows = new Map();
1388
+ const tab = activeTab.value;
1389
+ if (!tab) {
1390
+ return emptyView("Open a page to style what you click.", true);
1391
+ }
1392
+ if (canvasMode() === "stylebook" && shell.stylebook.selection) {
1393
+ const node = tab.doc.document;
1394
+ if (!node) {
1395
+ return emptyView("Open a page to style what you click.", true);
1396
+ }
1397
+ // No separate `Styling: <h1>` header. It said the same thing the Target Line's scope chip says,
1398
+ // Except that it said it as a caption, after the fact, and without the blast radius (§6.1).
1399
+ return buildEditor(tab, node, {
1400
+ effectiveStyle: getEffectiveStyle(node.style),
1401
+ stylebookSelector: shell.stylebook.selection,
1402
+ });
1403
+ }
1404
+ const selected = primarySelection(tab.session.selection);
1405
+ if (!selected) {
1406
+ return emptyView(clickAnythingTo("style it"), false);
1407
+ }
1408
+ const node = getNodeAtPath(tab.doc.document, selected) as JxMutableNode | undefined;
1409
+ if (!node) {
1410
+ return emptyView(staleSelectionMessage(), false);
1411
+ }
1412
+ return buildEditor(tab, node, {});
1413
+ }
845
1414
 
846
1415
  /**
1416
+ * Resolve the coordinate, then project every section under it.
1417
+ *
1418
+ * @param {Tab} tab
847
1419
  * @param {JxMutableNode} node
848
- * @param {string | null} activeMediaTab
849
- * @param {string | null} activeSelector
850
- * @param {{ effectiveStyle?: JxStyle; stylebookSelector?: string | null }} [opts] —
851
- * `effectiveStyle` is the site-merged style Stylebook edits against; `stylebookSelector` is the
852
- * tag catalogue entry being styled, which is what makes the edit tag-wide rather than
853
- * element-wide and therefore what the scope chip reports.
1420
+ * @param {{ effectiveStyle?: JxStyle; stylebookSelector?: string | null }} opts — `effectiveStyle`
1421
+ * is the site-merged style Stylebook edits against; `stylebookSelector` is the tag catalogue
1422
+ * entry being styled, which is what makes the edit tag-wide rather than element-wide and
1423
+ * therefore what the scope chip reports.
854
1424
  */
855
- function styleSidebarTemplate(
1425
+ function buildEditor(
1426
+ tab: Tab,
856
1427
  node: JxMutableNode,
857
- activeMediaTab: string | null,
858
- activeSelector: string | null,
859
- opts: { effectiveStyle?: JxStyle | undefined; stylebookSelector?: string | null } = {},
860
- ) {
1428
+ opts: { effectiveStyle?: JxStyle | undefined; stylebookSelector?: string | null },
1429
+ ): StylePanelView {
861
1430
  const { effectiveStyle, stylebookSelector = null } = opts;
862
- const tab = activeTab.value!;
863
1431
  const sel = primarySelection(tab.session.selection) as JxPath;
864
1432
  /** The document's OWN style — what a commit writes into. */
865
1433
  const ownRootStyle: JxStyle = node.style || {};
@@ -874,7 +1442,8 @@ function styleSidebarTemplate(
874
1442
  .map(([defName]) => defName);
875
1443
  const { sizeBreakpoints } = parseMediaEntries(getEffectiveMedia(tab.doc.document.$media));
876
1444
  const mediaNames = sizeBreakpoints.map((bp) => bp.name);
877
- const mediaTab = activeMediaTab || null;
1445
+ const mediaTab = tab.session.ui.activeMedia || null;
1446
+ const { activeSelector } = tab.session.ui;
878
1447
 
879
1448
  // ── Scheme-layer routing (spec §9.5) ────────────────────────────────────────
880
1449
  // With the tab-bar scheme control forcing a scheme that has a matching declared scheme query,
@@ -895,29 +1464,49 @@ function styleSidebarTemplate(
895
1464
  ? (style[`@${editMedia}`] as Record<string, unknown>) || {}
896
1465
  : style;
897
1466
  const declaredSelectors = new Set(Object.keys(contextStyle).filter((s) => isNestedSelector(s)));
1467
+ /* Element-aware, not the flat global list: `:popover-open` on a `<p>` is a rule that can never
1468
+ match, and a menu that offers unmatchable states is a menu people stop reading. What the
1469
+ element already DECLARES and the active selector are unioned in after, so nothing an author has
1470
+ written can drop out of it. */
898
1471
  const selectorOptions = [
899
1472
  ...new Set([
900
- ...COMMON_SELECTORS,
1473
+ ...selectorsForNode(node),
901
1474
  ...declaredSelectors,
902
1475
  ...(activeSelector ? [activeSelector] : []),
903
1476
  ]),
904
1477
  ];
905
1478
 
906
1479
  // ── The Target Line (§6.1) ─────────────────────────────────────────────────
907
- // The three widgets this replaces are gone: the breakpoint `<sp-tabs>` strip, the
908
- // `.selector-select` picker and the `.style-scheme-badge`. The first two named axes the pane
909
- // Context bar already selects; the third emitted a class with no CSS rule anywhere in the repo.
910
1480
  const stylebookTag = stylebookTagOf(stylebookSelector);
911
1481
  const elementLabel =
912
1482
  stylebookTag ?? (typeof node.tagName === "string" ? node.tagName : "element");
913
- const targetLineT = renderTargetLine({
1483
+ setTargetLine({
914
1484
  segments: targetSegments(elementLabel, mediaTab, mediaNames.length > 0, schemeLayer),
915
1485
  selector: {
916
1486
  value: activeSelector,
917
1487
  options: selectorOptions,
918
1488
  declared: declaredSelectors,
919
1489
  onSelect: (value) => {
920
- activeTab.value!.session.ui.activeSelector = value;
1490
+ const current = activeTab.value;
1491
+ if (!current) {
1492
+ return;
1493
+ }
1494
+ current.session.ui.activeSelector = value;
1495
+ /* §6.2's own rule: a control that selects a rendering context has to change the rendering,
1496
+ or it is a control over a label. `:hover` gets away with not doing this because you can
1497
+ hover the element; `:popover-open` cannot, because a closed popover is not on the screen
1498
+ to be put into that state by hand. This is the ONE state the canvas simulates — extending
1499
+ it to `:hover` and `:focus` is a larger decision and is deliberately not taken here. */
1500
+ if (value?.startsWith(":popover-open")) {
1501
+ runCommand("canvas.setPopoverOpen", { open: true });
1502
+ }
1503
+ // The dialog's open states, by the same rule, when the selection is in a dialog to open.
1504
+ if (
1505
+ (value?.startsWith("[open]") || value?.startsWith(":modal")) &&
1506
+ dialogPathFor(current) !== null
1507
+ ) {
1508
+ runCommand("canvas.setDialogOpen", { open: true });
1509
+ }
921
1510
  },
922
1511
  onAddCustom: () => {
923
1512
  // One flow, through `ui/layers.ts` — the imperative `<input>` this replaces was appended to
@@ -929,9 +1518,8 @@ function styleSidebarTemplate(
929
1518
  validate: (v) =>
930
1519
  isNestedSelector(v.trim()) ? "" : 'A selector must start with ":", ".", "&" or "[".',
931
1520
  }).then((value) => {
932
- if (value) {
933
- activeTab.value!.session.ui.activeSelector = value.trim();
934
- renderOnly("rightPanel");
1521
+ if (value && activeTab.value) {
1522
+ activeTab.value.session.ui.activeSelector = value.trim();
935
1523
  }
936
1524
  });
937
1525
  },
@@ -939,24 +1527,6 @@ function styleSidebarTemplate(
939
1527
  scope: resolveScope(tab, stylebookTag),
940
1528
  });
941
1529
 
942
- // ── Filter bar ─────────────────────────────────────────────────────────────
943
- // One control. The "Active" toggle is gone: it existed only because provenance was invisible
944
- // With a section closed, and a heading that says "3 set here · 2 inherited" answers the same
945
- // Question without hiding two thirds of the panel to do it (§6.2).
946
- const filterBarT = html`
947
- <div class="style-filter-bar">
948
- <sp-textfield
949
- size="s"
950
- class="style-filter-input"
951
- placeholder="Filter properties…"
952
- .value=${live(tab.session.ui.styleFilter || "")}
953
- @input=${(e: Event) => {
954
- activeTab.value!.session.ui.styleFilter = (e.target as HTMLInputElement).value;
955
- }}
956
- ></sp-textfield>
957
- </div>
958
- `;
959
-
960
1530
  // ── Determine the active style object ──────────────────────────────────────
961
1531
  const activeStyle = resolveContextStyle(style, activeSelector, editMedia);
962
1532
  // The SAME coordinate, resolved against the document's own style. In Stylebook `style` is the
@@ -987,11 +1557,6 @@ function styleSidebarTemplate(
987
1557
  };
988
1558
  const commitStyle = (prop: string, val?: string | Record<string, unknown> | undefined) =>
989
1559
  transactDoc(activeTab.value, (t) => mutateTargets(t, prop, val));
990
- /** One target's OWN block at this coordinate — what a write that preserves values reads first. */
991
- const targetStyleOf = (t: Tab, target: JxPath): JxStyle => {
992
- const targetNode = getNodeAtPath(t.doc.document, target) as JxMutableNode | undefined;
993
- return resolveContextStyle(targetNode?.style ?? {}, activeSelector, editMedia);
994
- };
995
1560
 
996
1561
  // ── Compute inherited style, and NAME THE DONOR ────────────────────────────
997
1562
  // Scheme layer: the base styles show through as inherited; breakpoint tabs inherit from lower
@@ -1034,390 +1599,562 @@ function styleSidebarTemplate(
1034
1599
  ...(mixedProps.size > 0 ? { mixed: { count: targets.length, props: mixedProps } } : {}),
1035
1600
  };
1036
1601
 
1037
- // Auto-open sections that have properties
1602
+ // Auto-open sections that have properties. Written back only when it actually moves, so the
1603
+ // Effect this runs inside settles after one extra pass rather than chasing its own write.
1038
1604
  const newSections = autoOpenSections({ style: activeStyle }, tab.session.ui.styleSections);
1039
1605
  if (JSON.stringify(newSections) !== JSON.stringify(tab.session.ui.styleSections)) {
1040
1606
  tab.session.ui.styleSections = newSections;
1041
1607
  }
1042
1608
 
1043
- // Partition properties into sections
1609
+ const filterText = (tab.session.ui.styleFilter || "").toLowerCase();
1610
+ const ctx: EditorCtx = {
1611
+ activeSelector,
1612
+ activeStyle,
1613
+ commit: commitStyle,
1614
+ coord: `style|${sel.join("/")}|${editMedia ?? ""}|${activeSelector ?? ""}`,
1615
+ editMedia,
1616
+ filter: filterText,
1617
+ inheritedStyle,
1618
+ mutate: mutateTargets,
1619
+ provCtx,
1620
+ tab,
1621
+ targets,
1622
+ templateSignals,
1623
+ };
1624
+
1625
+ const sections = [...propertySections(ctx), ...nestedSection(ctx), ...customSection(ctx)];
1626
+ return {
1627
+ emptyActionLabel: "",
1628
+ emptyMessage: "",
1629
+ filter: tab.session.ui.styleFilter || "",
1630
+ hasEmptyAction: false,
1631
+ sections,
1632
+ view: "editor",
1633
+ };
1634
+ }
1635
+
1636
+ /** Register a projected row's verbs, and hand back the row. */
1637
+ function register(entry: { row: StyleRowView; actions: RowActions }): StyleRowView {
1638
+ _rows.set(entry.row.key, entry.actions);
1639
+ return entry.row;
1640
+ }
1641
+
1642
+ /** The css-meta sections, in their declared order, minus the catch-all. */
1643
+ function propertySections(ctx: EditorCtx): StyleSectionView[] {
1644
+ const { activeStyle, tab } = ctx;
1645
+ const isFiltering = ctx.filter.length > 0;
1044
1646
  const sectionProps: Record<string, { prop: string; entry: CssPropertyEntry }[]> = {};
1045
1647
  for (const sec of cssMeta.$sections) {
1046
1648
  sectionProps[sec.key] = [];
1047
1649
  }
1048
-
1049
1650
  for (const [prop, entry] of Object.entries(cssMeta.$defs) as [string, CssPropertyEntry][]) {
1050
- if (typeof (entry as Record<string, unknown>).$shorthand === "string") {
1651
+ if (typeof entry.$shorthand === "string") {
1051
1652
  continue;
1052
1653
  }
1053
- const sec = ((entry as Record<string, unknown>).$section as string) || "other";
1054
- sectionProps[sec]!.push({ entry, prop });
1654
+ sectionProps[(entry.$section as string) || "other"]!.push({ entry, prop });
1055
1655
  }
1056
1656
  for (const sec of cssMeta.$sections) {
1057
- sectionProps[sec.key]!.sort(
1058
- (
1059
- a: { prop: string; entry: CssPropertyEntry },
1060
- b: { prop: string; entry: CssPropertyEntry },
1061
- ) =>
1062
- ((a.entry as Record<string, unknown>).$order as number) -
1063
- ((b.entry as Record<string, unknown>).$order as number),
1064
- );
1657
+ sectionProps[sec.key]!.sort((a, b) => (a.entry.$order as number) - (b.entry.$order as number));
1065
1658
  }
1066
1659
 
1067
- const otherProps = [];
1068
- for (const prop of Object.keys(activeStyle)) {
1069
- if (!(cssMeta.$defs as Record<string, unknown>)[prop]) {
1070
- const val = activeStyle[prop];
1071
- if (val !== null && typeof val === "object") {
1660
+ const out: StyleSectionView[] = [];
1661
+ for (const sec of cssMeta.$sections) {
1662
+ if (sec.key === "other") {
1663
+ continue;
1664
+ }
1665
+ const entries = sectionProps[sec.key]!;
1666
+ const sectionActiveProps = entries.filter(({ prop, entry }) => {
1667
+ if (activeStyle[prop] !== undefined) {
1668
+ return true;
1669
+ }
1670
+ return (
1671
+ inferInputType(entry) === "shorthand" &&
1672
+ (getLonghands(prop) as CssLonghand[]).some((l) => activeStyle[l.name] !== undefined)
1673
+ );
1674
+ });
1675
+ const clearSection = () => {
1676
+ transactDoc(activeTab.value, (t) => {
1677
+ for (const { prop, entry } of sectionActiveProps) {
1678
+ if (activeStyle[prop] !== undefined) {
1679
+ ctx.mutate(t, prop);
1680
+ }
1681
+ if (inferInputType(entry) === "shorthand") {
1682
+ for (const l of getLonghands(prop) as CssLonghand[]) {
1683
+ ctx.mutate(t, l.name);
1684
+ }
1685
+ }
1686
+ }
1687
+ });
1688
+ };
1689
+
1690
+ // The heading's own answer to "is anything in here set, inherited or bound" — the question the
1691
+ // Retired "Active" toggle answered by hiding everything that was not.
1692
+ const counts = countProvenance(sectionProvenance(entries, ctx.provCtx));
1693
+ const isOpen = isFiltering ? true : (tab.session.ui.styleSections[sec.key] ?? false);
1694
+ const section = sectionShell(sec.key, sec.label, isOpen, counts, {
1695
+ canClear: sectionActiveProps.length > 0,
1696
+ clearTitle: `Clear all ${sec.label.toLowerCase()} properties`,
1697
+ layout: sec.$layout === "grid" ? "grid" : "",
1698
+ onClear: clearSection,
1699
+ });
1700
+ if (isOpen) {
1701
+ section.rows = sectionRows(ctx, entries, sec.$layout === "grid");
1702
+ }
1703
+ if (isFiltering && section.rows.length === 0) {
1704
+ continue;
1705
+ }
1706
+ out.push(section);
1707
+ }
1708
+ return out;
1709
+ }
1710
+
1711
+ /** One section's rows: every property that is set, or whose `$show` condition passes. */
1712
+ function sectionRows(
1713
+ ctx: EditorCtx,
1714
+ entries: { prop: string; entry: CssPropertyEntry }[],
1715
+ grid: boolean,
1716
+ ): StyleRowView[] {
1717
+ const { activeStyle } = ctx;
1718
+ const rows: StyleRowView[] = [];
1719
+ for (const { prop, entry } of entries) {
1720
+ const val = activeStyle[prop];
1721
+ const hasVal = val !== undefined;
1722
+ const condMet = allConditionsPass(entry, activeStyle);
1723
+ const type = inferInputType(entry);
1724
+ if (!hasVal && !condMet) {
1725
+ continue;
1726
+ }
1727
+ if (ctx.filter) {
1728
+ const label = propLabel(entry, prop).toLowerCase();
1729
+ if (!prop.includes(ctx.filter) && !label.includes(ctx.filter)) {
1072
1730
  continue;
1073
1731
  }
1074
- otherProps.push(prop);
1075
1732
  }
1733
+ if (type === "shorthand") {
1734
+ const longhands = getLonghands(prop) as CssLonghand[];
1735
+ const hasAny = hasVal || longhands.some((l) => activeStyle[l.name] !== undefined);
1736
+ if (!hasAny && !condMet) {
1737
+ continue;
1738
+ }
1739
+ for (const built of shorthandRows(ctx, entry, prop)) {
1740
+ rows.push(register(built));
1741
+ }
1742
+ continue;
1743
+ }
1744
+ const inherited = ctx.inheritedStyle[prop];
1745
+ const built = fieldRow(ctx, entry, prop, (val as string) ?? "", {
1746
+ onCommit: (newVal) => ctx.commit(prop, newVal || undefined),
1747
+ placeholder: !hasVal && inherited !== undefined ? String(inherited) : "",
1748
+ provenance: provenanceOf(prop, ctx.provCtx),
1749
+ span: grid && entry.$span === 2,
1750
+ warning: hasVal && !condMet,
1751
+ });
1752
+ rows.push(register(built));
1753
+ }
1754
+ return rows;
1755
+ }
1756
+
1757
+ /** A section with its tally worked out and no rows yet. */
1758
+ function sectionShell(
1759
+ key: string,
1760
+ label: string,
1761
+ open: boolean,
1762
+ counts: ReturnType<typeof countProvenance>,
1763
+ opts: { canClear: boolean; clearTitle: string; layout: string; onClear: () => void },
1764
+ ): StyleSectionView {
1765
+ const marks: { state: string }[] = [];
1766
+ if (counts.inherited > 0) {
1767
+ marks.push({ state: "inherited" });
1076
1768
  }
1769
+ if (counts.bound > 0) {
1770
+ marks.push({ state: "bound" });
1771
+ }
1772
+ if (counts.mixed > 0) {
1773
+ marks.push({ state: "mixed" });
1774
+ }
1775
+ const canClear = counts.set > 0 && opts.canClear;
1776
+ if (canClear) {
1777
+ _sectionClears.set(key, opts.onClear);
1778
+ }
1779
+ return {
1780
+ canClear,
1781
+ clearTitle: opts.clearTitle,
1782
+ hasMarks: marks.length > 0,
1783
+ key,
1784
+ label,
1785
+ layout: opts.layout,
1786
+ marks,
1787
+ open,
1788
+ rows: [],
1789
+ tally: provenanceSummaryText(counts),
1790
+ };
1791
+ }
1077
1792
 
1793
+ /** Relative Styling — the nested rules declared at this coordinate, and the way to add one. */
1794
+ function nestedSection(ctx: EditorCtx): StyleSectionView[] {
1078
1795
  const nestedRules: string[] = [];
1079
- for (const [prop, val] of Object.entries(activeStyle)) {
1796
+ for (const [prop, val] of Object.entries(ctx.activeStyle)) {
1080
1797
  if (val !== null && typeof val === "object" && !Array.isArray(val) && !prop.startsWith("@")) {
1081
1798
  nestedRules.push(prop);
1082
1799
  }
1083
1800
  }
1801
+ if (nestedRules.length === 0) {
1802
+ return [];
1803
+ }
1804
+ const open = ctx.tab.session.ui.styleSections.nested ?? true;
1805
+ const section = sectionShell("nested", "Relative Styling", open, countProvenance([]), {
1806
+ canClear: false,
1807
+ clearTitle: "",
1808
+ layout: "",
1809
+ onClear: () => {},
1810
+ });
1811
+ if (!open) {
1812
+ return [section];
1813
+ }
1814
+ for (const rule of nestedRules) {
1815
+ const key = `${ctx.coord}|nested|${rule}`;
1816
+ const row = blankRow(key, rule, "nested");
1817
+ row.widget = "nested";
1818
+ row.name = rule;
1819
+ row.openTitle = `Edit ${rule} under this rule`;
1820
+ row.removeLabel = `Remove ${rule}`;
1821
+ _rows.set(key, {
1822
+ chip: () => ctx.commit(rule),
1823
+ open: () => {
1824
+ const compound = ctx.activeSelector ? `${ctx.activeSelector} ${rule}` : rule;
1825
+ selectStylebookTag(compound, undefined, { panCanvas: true });
1826
+ },
1827
+ });
1828
+ section.rows.push(row);
1829
+ }
1830
+ const addKey = `${ctx.coord}|nested|+add`;
1831
+ const addRow = blankRow(addKey, "+add", "nested-add");
1832
+ addRow.widget = "nestedadd";
1833
+ _rows.set(addKey, {});
1834
+ section.rows.push(addRow);
1835
+ _nestedAdd = () => {
1836
+ void showPromptDialog("Add Nested Selector", {
1837
+ confirmLabel: "Add",
1838
+ message: "Enter a selector to nest under the current rule.",
1839
+ placeholder: "th, :hover, .active",
1840
+ validate: (v) => (v.trim() ? "" : "Enter a selector."),
1841
+ }).then((name) => {
1842
+ if (name) {
1843
+ ctx.commit(name, {});
1844
+ }
1845
+ });
1846
+ };
1847
+ return [section];
1848
+ }
1084
1849
 
1085
- // ── Filter state ─────────────────────────────────────────────────────────
1086
- const filterText = (tab.session.ui.styleFilter || "").toLowerCase();
1087
- const isFiltering = filterText.length > 0;
1088
-
1089
- // ── Section templates ────────────────────────────────────────────────────
1090
- const sectionTemplates = cssMeta.$sections
1091
- .filter((sec) => sec.key !== "other")
1092
- .map((sec) => {
1093
- const entries = sectionProps[sec.key]!;
1094
-
1095
- const sectionActiveProps = entries.filter(
1096
- ({ prop, entry }: { prop: string; entry: CssPropertyEntry }) => {
1097
- if (activeStyle[prop] !== undefined) {
1098
- return true;
1099
- }
1100
- if (inferInputType(entry) === "shorthand") {
1101
- return (getLonghands(prop) as CssLonghand[]).some(
1102
- (l: CssLonghand) => activeStyle[l.name] !== undefined,
1103
- );
1104
- }
1105
- return false;
1106
- },
1107
- );
1108
-
1109
- const clearSection = () => {
1850
+ /** Custom — every declaration css-meta does not know about, plus the field that adds one. */
1851
+ function customSection(ctx: EditorCtx): StyleSectionView[] {
1852
+ const otherProps: string[] = [];
1853
+ for (const prop of Object.keys(ctx.activeStyle)) {
1854
+ if (!(cssMeta.$defs as Record<string, unknown>)[prop]) {
1855
+ const val = ctx.activeStyle[prop];
1856
+ if (val !== null && typeof val === "object") {
1857
+ continue;
1858
+ }
1859
+ otherProps.push(prop);
1860
+ }
1861
+ }
1862
+ const open = ctx.tab.session.ui.styleSections.other ?? otherProps.length > 0;
1863
+ const section = sectionShell("other", "Custom", open, countProvenance([]), {
1864
+ canClear: false,
1865
+ clearTitle: "",
1866
+ layout: "",
1867
+ onClear: () => {},
1868
+ });
1869
+ if (!open) {
1870
+ return [section];
1871
+ }
1872
+ const cssInitialMap = getCssInitialMap();
1873
+ /** One target's OWN block at this coordinate — what a write that preserves values reads first. */
1874
+ const targetStyleOf = (t: Tab, target: JxPath): JxStyle => {
1875
+ const targetNode = getNodeAtPath(t.doc.document, target) as JxMutableNode | undefined;
1876
+ return resolveContextStyle(targetNode?.style ?? {}, ctx.activeSelector, ctx.editMedia);
1877
+ };
1878
+ for (const prop of otherProps) {
1879
+ const key = `${ctx.coord}|custom|${prop}`;
1880
+ const row = blankRow(key, prop, "custom");
1881
+ row.widget = "kv";
1882
+ row.name = prop;
1883
+ row.value = String(ctx.activeStyle[prop]);
1884
+ row.placeholder = cssInitialMap.get(prop) ?? "";
1885
+ row.removeLabel = `Remove ${prop}`;
1886
+ _rows.set(key, {
1887
+ chip: () => ctx.commit(prop),
1888
+ debounceId: `custom:${key}`,
1889
+ edit: (value) => ctx.commit(prop, value),
1890
+ rename: (raw) => {
1891
+ const newProp = raw.trim();
1892
+ if (!newProp || newProp === prop) {
1893
+ return;
1894
+ }
1895
+ // A rename is the one write that cannot be fanned out with a single value: each element
1896
+ // Keeps ITS OWN value under the new key. The old code batched half of the rename (the
1897
+ // Delete) against the primary and the other half against the primary too, while the value
1898
+ // Cell beside it wrote to the whole selection — so renaming a property the selection shared
1899
+ // Moved it on one element and left it on the rest.
1110
1900
  transactDoc(activeTab.value, (t) => {
1111
- for (const { prop, entry } of sectionActiveProps) {
1112
- if (activeStyle[prop] !== undefined) {
1113
- mutateTargets(t, prop);
1114
- }
1115
- if (inferInputType(entry) === "shorthand") {
1116
- for (const l of getLonghands(prop) as CssLonghand[]) {
1117
- mutateTargets(t, l.name);
1118
- }
1901
+ for (const target of ctx.targets) {
1902
+ const current = targetStyleOf(t, target)[prop];
1903
+ if (current === undefined) {
1904
+ continue;
1119
1905
  }
1906
+ const write = contextMutate(target, ctx.activeSelector, ctx.editMedia);
1907
+ write(t, prop);
1908
+ write(t, newProp, String(current));
1120
1909
  }
1121
1910
  });
1122
- };
1123
-
1124
- // The heading's own answer to "is anything in here set, inherited or bound" — the question
1125
- // The retired "Active" toggle answered by hiding everything that was not.
1126
- const counts = countProvenance(sectionProvenance(entries, provCtx));
1127
- const headingT = html`
1128
- <span slot="heading" class="style-section-heading">
1129
- ${sec.label}
1130
- ${renderProvenanceDots(counts, {
1131
- onClearSet: sectionActiveProps.length > 0 ? clearSection : undefined,
1132
- clearTitle: `Clear all ${sec.label.toLowerCase()} properties`,
1133
- })}
1134
- </span>
1135
- `;
1136
-
1137
- const isOpen = isFiltering ? true : (tab.session.ui.styleSections[sec.key] ?? false);
1138
-
1139
- if (!isOpen) {
1140
- return html`
1141
- <sp-accordion-item
1142
- label=${sec.label}
1143
- .open=${false}
1144
- @sp-accordion-item-toggle=${(e: Event) => {
1145
- activeTab.value!.session.ui.styleSections = {
1146
- ...activeTab.value!.session.ui.styleSections,
1147
- [sec.key]: (e.target as HTMLElement & { open: boolean }).open,
1148
- };
1149
- }}
1150
- >
1151
- ${headingT}
1152
- </sp-accordion-item>
1153
- `;
1911
+ },
1912
+ });
1913
+ section.rows.push(row);
1914
+ }
1915
+ const addKey = `${ctx.coord}|custom|+add`;
1916
+ const addRow = blankRow(addKey, "+add", "custom-add");
1917
+ addRow.widget = "kvadd";
1918
+ _rows.set(addKey, {
1919
+ add: (raw) => {
1920
+ const prop = raw.trim();
1921
+ if (!prop) {
1922
+ return false;
1154
1923
  }
1924
+ ctx.commit(prop, cssInitialMap.get(prop) || "");
1925
+ return true;
1926
+ },
1927
+ });
1928
+ section.rows.push(addRow);
1929
+ return [section];
1930
+ }
1155
1931
 
1156
- const rows = [];
1157
- for (const { prop, entry } of entries) {
1158
- const val = activeStyle[prop];
1159
- const hasVal = val !== undefined;
1160
- const condMet = allConditionsPass(entry, activeStyle);
1161
- const type = inferInputType(entry);
1162
- if (!hasVal && !condMet) {
1163
- continue;
1164
- }
1932
+ // ─── The menus ───────────────────────────────────────────────────────────────
1165
1933
 
1166
- if (filterText) {
1167
- const label = propLabel(entry, prop).toLowerCase();
1168
- if (!prop.includes(filterText) && !label.includes(filterText)) {
1169
- continue;
1170
- }
1171
- }
1934
+ /**
1935
+ * The popover slot every list on this tab opens in; its region is `overlay.menu:style-value`.
1936
+ *
1937
+ * ONE slot, because only one of these can be open at a time — a unit list, a keyword list and a
1938
+ * Value Source picker are three uses of the same gesture, not three overlays — and a second menu
1939
+ * opening in the same slot would otherwise leave the first one's document mounted under it.
1940
+ */
1941
+ const MENU_REGION = "style-value";
1172
1942
 
1173
- if (type === "shorthand") {
1174
- const longhands = getLonghands(prop) as CssLonghand[];
1175
- const hasAny =
1176
- hasVal || longhands.some((l: CssLonghand) => activeStyle[l.name] !== undefined);
1177
- if (!hasAny && !condMet) {
1178
- continue;
1179
- }
1180
- rows.push(
1181
- renderShorthandRow({
1182
- prop,
1183
- entry,
1184
- style: activeStyle,
1185
- mutate: mutateTargets,
1186
- inherited: inheritedStyle,
1187
- ctx: provCtx,
1188
- }),
1189
- );
1190
- } else {
1191
- const isWarning = hasVal && !condMet;
1192
- if (hasVal || condMet) {
1193
- rows.push(
1194
- renderStyleRow(
1195
- entry,
1196
- prop,
1197
- (val as string) ?? "",
1198
- (newVal: string | undefined) => commitStyle(prop, newVal || undefined),
1199
- isWarning,
1200
- sec.$layout === "grid",
1201
- inheritedStyle[prop] as string | undefined,
1202
- templateSignals,
1203
- `style|${sel.join("/")}|${editMedia ?? ""}|${activeSelector ?? ""}|${prop}`,
1204
- provenanceOf(prop, provCtx),
1205
- ),
1206
- );
1207
- }
1208
- }
1209
- }
1943
+ /** The list that is open, so opening another closes it first. */
1944
+ let _menu: MenuHandle | null = null;
1945
+
1946
+ /** Close the row list — every choice does this, so the row is readable straight after. */
1947
+ function closeRowMenu(): void {
1948
+ const menu = _menu;
1949
+ _menu = null;
1950
+ menu?.close();
1951
+ }
1210
1952
 
1211
- if (isFiltering && rows.length === 0) {
1212
- return nothing;
1953
+ /** Open a list under the control that asked for it. One menu at a time, the kit's. */
1954
+ function openRowMenu(
1955
+ anchor: unknown,
1956
+ label: string,
1957
+ choices: StyleChoice[],
1958
+ run: (value: string) => void,
1959
+ ): void {
1960
+ const trigger = anchor instanceof HTMLElement ? anchor : null;
1961
+ closeRowMenu();
1962
+ if (!trigger || choices.length === 0) {
1963
+ return;
1964
+ }
1965
+ _menu = openMenu({
1966
+ label,
1967
+ onClosed: (closed) => {
1968
+ if (_menu === closed) {
1969
+ _menu = null;
1213
1970
  }
1971
+ },
1972
+ opener: trigger,
1973
+ region: MENU_REGION,
1974
+ rows: choices.map((choice) => ({
1975
+ ...(choice.checked === undefined
1976
+ ? {}
1977
+ : { checked: (choice.checked ? "true" : "false") as "true" | "false" }),
1978
+ destructive: false,
1979
+ disabled: false,
1980
+ dividerAbove: choice.divider === true,
1981
+ id: choice.value,
1982
+ title: choice.label,
1983
+ })),
1984
+ run: (value) => run(value),
1985
+ });
1986
+ }
1214
1987
 
1215
- return html`
1216
- <sp-accordion-item
1217
- label=${sec.label}
1218
- .open=${live(isOpen)}
1219
- @sp-accordion-item-toggle=${(e: Event) => {
1220
- activeTab.value!.session.ui.styleSections = {
1221
- ...activeTab.value!.session.ui.styleSections,
1222
- [sec.key]: (e.target as HTMLElement & { open: boolean }).open,
1223
- };
1224
- }}
1225
- >
1226
- ${headingT}
1227
- <div class=${sec.$layout === "grid" ? "style-section-body--grid" : ""}>${rows}</div>
1228
- </sp-accordion-item>
1229
- `;
1230
- });
1988
+ // ─── The mount ───────────────────────────────────────────────────────────────
1231
1989
 
1232
- // ── Custom section ─────────────────────────────────────────────────────────
1233
- const cssInitialMap = getCssInitialMap();
1234
- const customIsOpen = tab.session.ui.styleSections.other ?? otherProps.length > 0;
1235
- const customSectionT = html`
1236
- <sp-accordion-item
1237
- label="Custom"
1238
- .open=${live(customIsOpen)}
1239
- @sp-accordion-item-toggle=${(e: Event) => {
1240
- activeTab.value!.session.ui.styleSections = {
1241
- ...activeTab.value!.session.ui.styleSections,
1242
- other: (e.target as HTMLElement & { open: boolean }).open,
1243
- };
1244
- }}
1245
- >
1246
- <div>
1247
- ${otherProps.map(
1248
- (prop) => html`
1249
- <div class="kv-row">
1250
- <sp-textfield
1251
- size="s"
1252
- class="kv-key"
1253
- .value=${live(prop)}
1254
- @change=${(e: Event) => {
1255
- const newProp = (e.target as HTMLInputElement).value.trim();
1256
- if (!newProp || newProp === prop) {
1257
- return;
1258
- }
1259
- // A rename is the one write that cannot be fanned out with a single value: each
1260
- // Element keeps ITS OWN value under the new key. The old code batched half of the
1261
- // Rename (the delete) against the primary and the other half against the primary
1262
- // Too, while the value cell beside it wrote to the whole selection — so renaming
1263
- // A property the selection shared moved it on one element and left it on the
1264
- // Rest.
1265
- transactDoc(activeTab.value, (t) => {
1266
- for (const target of targets) {
1267
- const current = targetStyleOf(t, target)[prop];
1268
- if (current === undefined) {
1269
- continue;
1270
- }
1271
- const write = contextMutate(target, activeSelector, editMedia);
1272
- write(t, prop);
1273
- write(t, newProp, String(current));
1274
- }
1275
- });
1276
- }}
1277
- ></sp-textfield>
1278
- <sp-textfield
1279
- size="s"
1280
- class="kv-val"
1281
- .value=${live(String(activeStyle[prop]))}
1282
- placeholder=${ifDefined(cssInitialMap.get(prop))}
1283
- @input=${debouncedStyleCommit(`custom:${prop}`, 400, (e: Event) => {
1284
- commitStyle(prop, (e.target as HTMLInputElement).value);
1285
- })}
1286
- ></sp-textfield>
1287
- <sp-action-button size="xs" quiet @click=${() => commitStyle(prop)}>
1288
- <sp-icon-close slot="icon"></sp-icon-close>
1289
- </sp-action-button>
1290
- </div>
1291
- `,
1292
- )}
1293
- <div style="display:flex;gap:4px;padding-top:4px">
1294
- <sp-textfield
1295
- size="s"
1296
- placeholder="Property name…"
1297
- style="flex:1"
1298
- @keydown=${(e: KeyboardEvent) => {
1299
- if (e.key === "Enter") {
1300
- e.preventDefault();
1301
- const prop = (e.target as HTMLInputElement).value.trim();
1302
- if (prop) {
1303
- const initial = cssInitialMap.get(prop) || "";
1304
- commitStyle(prop, initial || "");
1305
- (e.target as HTMLInputElement).value = "";
1306
- }
1307
- }
1308
- }}
1309
- ></sp-textfield>
1310
- </div>
1311
- </div>
1312
- </sp-accordion-item>
1313
- `;
1314
-
1315
- // ── Relative Styling section (nested rules) ───────────────────────────────
1316
- const nestedIsOpen = tab.session.ui.styleSections.nested ?? nestedRules.length > 0;
1317
- const nestedSectionT =
1318
- nestedRules.length > 0
1319
- ? html`
1320
- <sp-accordion-item
1321
- label="Relative Styling"
1322
- .open=${live(nestedIsOpen)}
1323
- @sp-accordion-item-toggle=${(e: Event) => {
1324
- activeTab.value!.session.ui.styleSections = {
1325
- ...activeTab.value!.session.ui.styleSections,
1326
- nested: (e.target as HTMLElement & { open: boolean }).open,
1327
- };
1328
- }}
1329
- >
1330
- <div style="display:flex;flex-direction:column;gap:4px;padding:4px 0">
1331
- ${nestedRules.map(
1332
- (rule) => html`
1333
- <div style="display:flex;align-items:center;gap:4px">
1334
- <button
1335
- style="flex:1;text-align:left;padding:6px 10px;background:var(--spectrum-gray-200, #1a1a1a);border:none;border-radius:var(--radius);color:var(--spectrum-gray-900, #fafafa);font-size:var(--spectrum-font-size-75, 12px);cursor:pointer"
1336
- @click=${() => {
1337
- const newSelector = activeSelector ? `${activeSelector} ${rule}` : rule;
1338
- selectStylebookTag(newSelector, undefined, {
1339
- panCanvas: true,
1340
- });
1341
- }}
1342
- >
1343
- ${rule}
1344
- </button>
1345
- <sp-action-button size="xs" quiet @click=${() => commitStyle(rule)}>
1346
- <sp-icon-delete slot="icon"></sp-icon-delete>
1347
- </sp-action-button>
1348
- </div>
1349
- `,
1350
- )}
1351
- <button
1352
- style="padding:6px 10px;background:none;border:1px dashed var(--spectrum-gray-400, #333);border-radius:var(--radius);color:var(--spectrum-gray-700, #a1a1aa);font-size:var(--spectrum-font-size-75, 12px);cursor:pointer"
1353
- @click=${async () => {
1354
- const name = await showPromptDialog("Add Nested Selector", {
1355
- confirmLabel: "Add",
1356
- message: "Enter a selector to nest under the current rule.",
1357
- placeholder: "th, :hover, .active",
1358
- validate: (v) => (v.trim() ? "" : "Enter a selector."),
1359
- });
1360
- if (name) {
1361
- commitStyle(name, {});
1362
- }
1363
- }}
1364
- >
1365
- + Add
1366
- </button>
1367
- </div>
1368
- </sp-accordion-item>
1369
- `
1370
- : nothing;
1371
-
1372
- return html`
1373
- <div class="style-sidebar">
1374
- ${targetLineT} ${filterBarT}
1375
- <sp-accordion allow-multiple size="s">
1376
- ${sectionTemplates} ${nestedSectionT} ${customSectionT}
1377
- </sp-accordion>
1378
- </div>
1379
- `;
1990
+ /** Everything the document can ask for, resolved through {@link rowActions}. */
1991
+ const ACTIONS: StylePanelActions = {
1992
+ addCustom: (key, field) => {
1993
+ const add = rowActions(key)?.add;
1994
+ if (!add || !(field instanceof HTMLInputElement)) {
1995
+ return;
1996
+ }
1997
+ if (add(field.value)) {
1998
+ field.value = "";
1999
+ }
2000
+ },
2001
+ addNested: () => _nestedAdd?.(),
2002
+ chipClick: (key) => rowActions(key)?.chip?.(),
2003
+ clearSection: (key) => _sectionClears.get(key)?.(),
2004
+ commitText: (key, value) => {
2005
+ const actions = rowActions(key);
2006
+ if (!actions) {
2007
+ return;
2008
+ }
2009
+ if (actions.debounceId) {
2010
+ cancelStyleDebounce(actions.debounceId);
2011
+ }
2012
+ actions.commit?.(value);
2013
+ },
2014
+ editText: (key, value) => {
2015
+ const actions = rowActions(key);
2016
+ if (!actions?.edit) {
2017
+ return;
2018
+ }
2019
+ if (actions.debounceId) {
2020
+ debouncedStyleCommit(actions.debounceId, 400, actions.edit)(value);
2021
+ return;
2022
+ }
2023
+ actions.edit(value);
2024
+ },
2025
+ openNested: (key) => rowActions(key)?.open?.(),
2026
+ pickChoice: (key, anchor) => {
2027
+ const actions = rowActions(key);
2028
+ if (!actions?.choose || !actions.choices) {
2029
+ return;
2030
+ }
2031
+ const { choose } = actions;
2032
+ openRowMenu(anchor, "Values", actions.choices, (value) => choose(value));
2033
+ },
2034
+ pickSource: (key, anchor) => {
2035
+ const source = rowActions(key)?.source;
2036
+ if (!source) {
2037
+ return;
2038
+ }
2039
+ openRowMenu(
2040
+ anchor,
2041
+ "Value source",
2042
+ source.caps.map((mode) => ({
2043
+ checked: mode === source.mode,
2044
+ label: `${VALUE_SOURCE_LABELS[mode]} — ${VALUE_SOURCE_HINTS[mode]}`,
2045
+ value: mode,
2046
+ })),
2047
+ (chosen) => {
2048
+ const next = chosen as SlotMode;
2049
+ if (next === source.mode) {
2050
+ return;
2051
+ }
2052
+ source.onChange(
2053
+ switchSlotMode(
2054
+ source.fieldKey,
2055
+ source.mode,
2056
+ next,
2057
+ cloneValue(source.value as JsonValue | undefined),
2058
+ slotModeSeed(next, { stateDefs: source.stateDefs }),
2059
+ ),
2060
+ );
2061
+ },
2062
+ );
2063
+ },
2064
+ pressButton: (key, value) => {
2065
+ const actions = rowActions(key);
2066
+ actions?.choose?.(value === currentButtonValue(key) ? "" : value);
2067
+ },
2068
+ renameCustom: (key, name) => rowActions(key)?.rename?.(name),
2069
+ runEmptyAction: () => openPageAction().run(),
2070
+ setFilter: (value) => {
2071
+ const tab = activeTab.value;
2072
+ if (tab) {
2073
+ tab.session.ui.styleFilter = value;
2074
+ }
2075
+ },
2076
+ toggleSection: (key, open) => {
2077
+ const tab = activeTab.value;
2078
+ if (!tab) {
2079
+ return;
2080
+ }
2081
+ tab.session.ui.styleSections = { ...tab.session.ui.styleSections, [key]: open };
2082
+ },
2083
+ toggleShorthand: (key) => rowActions(key)?.toggle?.(),
2084
+ };
2085
+
2086
+ function currentButtonValue(key: string): string {
2087
+ return _buttonValues.get(key) ?? "";
2088
+ }
2089
+
2090
+ /** The standing mount: one per window, because the Inspector draws one Style tab. */
2091
+ let _standing: { host: HTMLElement; handle: StylePanelSurface; scope: EffectScope } | null = null;
2092
+
2093
+ /** What the Style tab cannot read for itself: which mode the canvas beside it is in. */
2094
+ export interface StyleHostDeps {
2095
+ /** `stylebook` edits a tag catalogue entry; anything else edits the element selection. */
2096
+ getCanvasMode: () => string;
1380
2097
  }
1381
2098
 
1382
- // ─── Entry point ────────────────────────────────────────────────────────────
2099
+ /** The default the tab falls back to when the dock names no mode. */
2100
+ const EDIT_MODE = () => "edit";
2101
+
2102
+ /** The canvas mode the dock last reported — Stylebook edits a tag, Edit edits a selection. */
2103
+ let _canvasMode: () => string = EDIT_MODE;
2104
+
2105
+ /** Remember the button values, so a second press on the selected one clears it. */
2106
+ function rememberButtons(view: StylePanelView): void {
2107
+ _buttonValues = new Map();
2108
+ for (const section of view.sections) {
2109
+ for (const row of section.rows) {
2110
+ if (row.widget === "buttons") {
2111
+ _buttonValues.set(row.key, row.buttons.find((b) => b.selected)?.value ?? "");
2112
+ }
2113
+ }
2114
+ }
2115
+ }
1383
2116
 
1384
2117
  /**
1385
- * Top-level Style panel — returns a lit-html template.
2118
+ * Give the Style tab somewhere to draw, or take it away.
2119
+ *
2120
+ * The document is mounted once and then only ASSIGNED to: its own `effect()` re-projects whenever
2121
+ * anything the projection read moves, and the surface writes one property per binding with an equal
2122
+ * write skipped. It must NOT be routed through `panels/panel-scheduler.ts` — that scheduler defers
2123
+ * a repaint while a field in the dock has focus, because a lit render of the whole panel replaces
2124
+ * the node the reader is typing into, and here the deferral would buy nothing and make the canvas
2125
+ * lag the keystroke.
1386
2126
  *
1387
- * @param {{ getCanvasMode: () => string }} ctx
1388
- * @returns {import("lit-html").TemplateResult}
2127
+ * `null` takes the tab down: the dock unmounted, or a test is starting clean.
2128
+ *
2129
+ * @param {HTMLElement | null} el The Inspector's Style tab body.
2130
+ * @param {StyleHostDeps} [ctx] What the tab cannot read for itself.
1389
2131
  */
1390
- export function renderStylePanelTemplate(ctx: { getCanvasMode: () => string }) {
1391
- const tab = activeTab.value;
1392
- const noDocument = () =>
1393
- renderEmptyState({
1394
- actions: [openPageAction()],
1395
- message: "Open a page to style what you click.",
1396
- });
1397
- if (!tab) {
1398
- return noDocument();
2132
+ export function bindStyleHost(el: HTMLElement | null, ctx?: StyleHostDeps): void {
2133
+ closeRowMenu();
2134
+ _standing?.scope.stop();
2135
+ _standing?.handle.dispose();
2136
+ _standing = null;
2137
+ _rows = new Map();
2138
+ _sectionClears = new Map();
2139
+ _nestedAdd = null;
2140
+ if (!el) {
2141
+ return;
1399
2142
  }
1400
- if (ctx.getCanvasMode() === "stylebook" && shell.stylebook.selection) {
1401
- const node = tab.doc.document;
1402
- if (!node) {
1403
- return noDocument();
1404
- }
1405
- // No separate `Styling: <h1>` header. It said the same thing the Target Line's scope chip says,
1406
- // Except that it said it as a caption, after the fact, and without the blast radius (§6.1).
1407
- return styleSidebarTemplate(node, tab.session.ui.activeMedia, tab.session.ui.activeSelector, {
1408
- effectiveStyle: getEffectiveStyle(node.style),
1409
- stylebookSelector: shell.stylebook.selection,
2143
+ _canvasMode = ctx?.getCanvasMode ?? EDIT_MODE;
2144
+ const first = buildView(_canvasMode);
2145
+ rememberButtons(first);
2146
+ const handle = mountStylePanelSurface(el, first, ACTIONS, {
2147
+ target: (element) => attachTargetLine(element ?? undefined),
2148
+ });
2149
+ const scope = effectScope();
2150
+ scope.run(() => {
2151
+ effect(() => {
2152
+ const view = buildView(_canvasMode);
2153
+ rememberButtons(view);
2154
+ handle.update(view);
1410
2155
  });
1411
- }
1412
- const selected = primarySelection(tab.session.selection);
1413
- if (!selected) {
1414
- return renderEmptyState({ message: clickAnythingTo("style it") });
1415
- }
1416
- const node = getNodeAtPath(tab.doc.document, selected);
1417
- if (!node) {
1418
- return renderEmptyState({ message: staleSelectionMessage() });
1419
- }
1420
- return styleSidebarTemplate(node, tab.session.ui.activeMedia, tab.session.ui.activeSelector);
2156
+ });
2157
+ _standing = { handle, host: el, scope };
1421
2158
  }
1422
2159
 
1423
2160
  // ─── Commands ─────────────────────────────────────────────────────────────────
@@ -1428,9 +2165,9 @@ export function renderStylePanelTemplate(ctx: { getCanvasMode: () => string }) {
1428
2165
  * `style.openSelectorMenu` is the one command here that addresses a CONTROL rather than a state,
1429
2166
  * and it earns that on purpose: the shot it serves photographs the open menu, so the menu is the
1430
2167
  * subject. What it does not do is hand a CSS selector back to the runner for a synthetic mouse
1431
- * press — the element comes from the Target Line's own `ref`, so a refactor of that markup moves
1432
- * the handle with it. The menu it opens is now a segment of the Target Line rather than a picker on
1433
- * a retired toolbar; the id, and therefore the shot, is unchanged.
2168
+ * press — the element comes from the Target Line's own handle, so a refactor of that markup moves
2169
+ * the handle with it. The menu it opens is a segment of the Target Line rather than a picker on a
2170
+ * retired toolbar; the id, and therefore the shot, is unchanged.
1434
2171
  *
1435
2172
  * `style.setSelector` is the state half of the same idea, and it is what a caller that wants a
1436
2173
  * RESULT rather than a picture should use: the menu exists to choose a selector, and choosing one