@jxsuite/studio 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/canvas.html +21 -0
  2. package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
  3. package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
  4. package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
  5. package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
  6. package/dist/chunks/{components-0hzentwq.js → components-2bpqp4mq.js} +5 -7
  7. package/dist/chunks/{components-0hzentwq.js.map → components-2bpqp4mq.js.map} +1 -1
  8. package/dist/chunks/content-source-34gnkbnn.js +38 -0
  9. package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
  10. package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
  11. package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
  12. package/dist/chunks/extension-sections-6mqydz35.js +301 -0
  13. package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
  14. package/dist/chunks/files-8kn321h1.js +70 -0
  15. package/dist/chunks/files-8kn321h1.js.map +9 -0
  16. package/dist/chunks/{format-host-2c188n41.js → format-host-mkwsmt8d.js} +3 -3
  17. package/dist/chunks/git-panel-tjbr191d.js +51 -0
  18. package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
  19. package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
  20. package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
  21. package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
  22. package/dist/chunks/layers-panel-87pb25km.js +58 -0
  23. package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
  24. package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
  25. package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
  26. package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
  27. package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
  28. package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
  29. package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
  30. package/dist/chunks/right-panel-zpdxd98t.js +37 -0
  31. package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
  32. package/dist/chunks/section-registry-zmckaxf4.js +30 -0
  33. package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
  34. package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
  35. package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
  36. package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
  37. package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
  38. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
  39. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
  40. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
  41. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
  42. package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
  43. package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
  44. package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
  45. package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
  46. package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
  47. package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
  48. package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
  49. package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
  50. package/dist/chunks/studio-41exgzhq.js +6690 -0
  51. package/dist/chunks/studio-41exgzhq.js.map +38 -0
  52. package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
  53. package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
  54. package/dist/chunks/studio-50k77cca.js +1787 -0
  55. package/dist/chunks/studio-50k77cca.js.map +18 -0
  56. package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
  57. package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
  58. package/dist/chunks/studio-6f0am8f2.js +53 -0
  59. package/dist/chunks/studio-6f0am8f2.js.map +10 -0
  60. package/dist/chunks/studio-6nmjc5qk.js +7002 -0
  61. package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
  62. package/dist/chunks/studio-7afd21nw.js +67 -0
  63. package/dist/chunks/studio-7afd21nw.js.map +10 -0
  64. package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
  65. package/dist/chunks/studio-7bmdvzne.js.map +10 -0
  66. package/dist/chunks/studio-8jpzqk5s.js +128 -0
  67. package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
  68. package/dist/chunks/studio-94m77tgk.js +1482 -0
  69. package/dist/chunks/studio-94m77tgk.js.map +13 -0
  70. package/dist/chunks/studio-9je6w5sq.js +1000 -0
  71. package/dist/chunks/studio-9je6w5sq.js.map +16 -0
  72. package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
  73. package/dist/chunks/{studio-97qcgvdn.js → studio-adn0z1k3.js} +14 -8
  74. package/dist/chunks/{studio-97qcgvdn.js.map → studio-adn0z1k3.js.map} +3 -3
  75. package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
  76. package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
  77. package/dist/chunks/studio-bw5dz6km.js +608 -0
  78. package/dist/chunks/studio-bw5dz6km.js.map +21 -0
  79. package/dist/chunks/studio-c6s5getc.js +4232 -0
  80. package/dist/chunks/studio-c6s5getc.js.map +39 -0
  81. package/dist/chunks/studio-cjm7fdjf.js +451 -0
  82. package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
  83. package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
  84. package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
  85. package/dist/chunks/studio-dg7zyt5x.js +28 -0
  86. package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
  87. package/dist/chunks/studio-dzcyvvvz.js +166 -0
  88. package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
  89. package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
  90. package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
  91. package/dist/chunks/studio-f12h6177.js +411 -0
  92. package/dist/chunks/studio-f12h6177.js.map +13 -0
  93. package/dist/chunks/studio-hbp1s8m9.js +1817 -0
  94. package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
  95. package/dist/chunks/studio-k2sxjph5.js +15295 -0
  96. package/dist/chunks/studio-k2sxjph5.js.map +235 -0
  97. package/dist/chunks/studio-myje51t3.js +385 -0
  98. package/dist/chunks/studio-myje51t3.js.map +12 -0
  99. package/dist/chunks/studio-na6rwzhw.js +353 -0
  100. package/dist/chunks/studio-na6rwzhw.js.map +11 -0
  101. package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
  102. package/dist/chunks/studio-p471gg5n.js.map +14 -0
  103. package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
  104. package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
  105. package/dist/chunks/{studio-hhj92gcr.js → studio-s519zc99.js} +3 -3
  106. package/dist/chunks/studio-sas7mt3k.js +75 -0
  107. package/dist/chunks/studio-sas7mt3k.js.map +10 -0
  108. package/dist/chunks/studio-t5wqwzdh.js +74 -0
  109. package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
  110. package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
  111. package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
  112. package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
  113. package/dist/chunks/studio-tr20m8g8.js.map +9 -0
  114. package/dist/chunks/{studio-ygzav7z5.js → studio-v96qfnww.js} +5514 -4291
  115. package/dist/chunks/studio-v96qfnww.js.map +54 -0
  116. package/dist/chunks/studio-vcetc91k.js +659 -0
  117. package/dist/chunks/studio-vcetc91k.js.map +10 -0
  118. package/dist/chunks/{studio-n9zmpxjg.js → studio-vmymjefp.js} +2972 -4937
  119. package/dist/chunks/studio-vmymjefp.js.map +98 -0
  120. package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
  121. package/dist/chunks/studio-x9hmy5kr.js +390 -0
  122. package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
  123. package/dist/chunks/studio-xa0ce0n7.js +335 -0
  124. package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
  125. package/dist/chunks/studio-xm3dbxvn.js +378 -0
  126. package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
  127. package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
  128. package/dist/chunks/studio-zv37yrfa.js +1054 -0
  129. package/dist/chunks/studio-zv37yrfa.js.map +13 -0
  130. package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
  131. package/dist/chunks/ws-client-654ts66j.js.map +11 -0
  132. package/dist/iframe-entry.js +668 -230
  133. package/dist/studio.js +23711 -35185
  134. package/index.html +35 -4685
  135. package/package.json +17 -15
  136. package/src/about/about-modal.ts +42 -11
  137. package/src/browse/library-commands.ts +240 -0
  138. package/src/browse/library-layouts.ts +369 -0
  139. package/src/browse/library-model.ts +396 -0
  140. package/src/browse/library-pane.ts +1049 -0
  141. package/src/browse/library-preview.ts +252 -0
  142. package/src/browse/library-source.ts +150 -0
  143. package/src/canvas/canvas-helpers.ts +33 -9
  144. package/src/canvas/canvas-live-render.ts +45 -29
  145. package/src/canvas/canvas-patcher.ts +151 -31
  146. package/src/canvas/canvas-perf.ts +46 -7
  147. package/src/canvas/canvas-render.ts +1092 -220
  148. package/src/canvas/canvas-surface.ts +240 -0
  149. package/src/canvas/canvas-utils.ts +1166 -124
  150. package/src/canvas/iframe-drop.ts +2 -1
  151. package/src/canvas/iframe-entry.ts +241 -4
  152. package/src/canvas/iframe-host.ts +1226 -143
  153. package/src/canvas/iframe-inline-edit.ts +31 -0
  154. package/src/canvas/iframe-interaction.ts +76 -16
  155. package/src/canvas/iframe-keys.ts +107 -45
  156. package/src/canvas/iframe-overlay.ts +38 -1
  157. package/src/canvas/iframe-protocol.ts +137 -2
  158. package/src/canvas/iframe-render.ts +70 -2
  159. package/src/canvas/iframe-slash.ts +6 -0
  160. package/src/canvas/path-mapping.ts +56 -6
  161. package/src/canvas/surface-registry.ts +292 -0
  162. package/src/collab/collab-commands.ts +244 -0
  163. package/src/collab/collab-session.ts +198 -32
  164. package/src/collab/collab-state.ts +28 -2
  165. package/src/collab/presence-chips.ts +100 -9
  166. package/src/commands/active-registry.ts +42 -0
  167. package/src/commands/app-commands.ts +176 -0
  168. package/src/commands/budget.ts +158 -0
  169. package/src/commands/command-args.ts +253 -0
  170. package/src/commands/context.ts +387 -0
  171. package/src/commands/defaults.ts +580 -0
  172. package/src/commands/keymap.ts +571 -0
  173. package/src/commands/levels.ts +345 -0
  174. package/src/commands/live-context.ts +193 -0
  175. package/src/commands/reference.ts +227 -0
  176. package/src/commands/registry.ts +293 -0
  177. package/src/component-props.ts +3 -2
  178. package/src/content/draft-state.ts +106 -0
  179. package/src/content/entry-commands.ts +282 -0
  180. package/src/content/entry-editor.ts +327 -0
  181. package/src/content/entry-fields.ts +116 -0
  182. package/src/content/entry-model.ts +175 -0
  183. package/src/editor/canvas-context-menu.ts +17 -11
  184. package/src/editor/canvas-slash-bridge.ts +11 -4
  185. package/src/editor/context-menu.ts +804 -204
  186. package/src/editor/convert-to-component.ts +31 -10
  187. package/src/editor/convert-to-repeater.ts +3 -2
  188. package/src/editor/inline-edit-apply.ts +5 -5
  189. package/src/editor/inline-edit.ts +86 -50
  190. package/src/editor/insert-zone-action.ts +1 -1
  191. package/src/editor/shortcuts.ts +1043 -329
  192. package/src/editor/slash-menu.ts +1 -0
  193. package/src/files/elements.ts +166 -0
  194. package/src/files/file-ops.ts +178 -31
  195. package/src/files/files.ts +898 -268
  196. package/src/files/fs-events.ts +21 -0
  197. package/src/files/media-meta.ts +203 -0
  198. package/src/files/media-paths.ts +141 -0
  199. package/src/files/media-upload.ts +7 -3
  200. package/src/files/media-usage.ts +189 -0
  201. package/src/github/github-auth.ts +235 -33
  202. package/src/github/github-publish.ts +80 -21
  203. package/src/grid/grid-controller.ts +152 -12
  204. package/src/grid/grid-layout.ts +225 -20
  205. package/src/grid/grid-open.ts +120 -0
  206. package/src/grid/grid-panel.ts +554 -39
  207. package/src/grid/grid-source.ts +27 -1
  208. package/src/grid/redirects-grid.ts +599 -0
  209. package/src/grid/redirects.ts +398 -0
  210. package/src/grid/sources/content-source.ts +108 -2
  211. package/src/i18n/i18n-commands.ts +322 -0
  212. package/src/new-project/add-repo-modal.ts +8 -10
  213. package/src/new-project/import-tab.ts +34 -9
  214. package/src/new-project/location-fields.ts +9 -1
  215. package/src/new-project/new-project-modal.ts +270 -262
  216. package/src/packages/jxsuite-update.ts +97 -48
  217. package/src/packages/pull-package-sync.ts +18 -9
  218. package/src/page-params.ts +2 -1
  219. package/src/panels/activity-bar.ts +152 -91
  220. package/src/panels/activity-panel.ts +481 -0
  221. package/src/panels/ai-chat/chat-markdown.ts +8 -3
  222. package/src/panels/ai-chat/chat-view.ts +245 -18
  223. package/src/panels/ai-chat/composer.ts +31 -5
  224. package/src/panels/ai-chat/sessions-view.ts +17 -10
  225. package/src/panels/ai-panel.ts +279 -120
  226. package/src/panels/block-action-bar.ts +1115 -224
  227. package/src/panels/bottom-dock.ts +337 -0
  228. package/src/panels/canvas-dnd-bridge.ts +37 -8
  229. package/src/panels/chat-panel.ts +29 -12
  230. package/src/panels/data-explorer.ts +254 -112
  231. package/src/panels/data-grid.ts +10 -4
  232. package/src/panels/dnd.ts +28 -51
  233. package/src/panels/editors.ts +434 -107
  234. package/src/panels/elements-panel.ts +66 -2
  235. package/src/panels/empty-state.ts +112 -0
  236. package/src/panels/events-panel.ts +596 -82
  237. package/src/panels/formula-workspace.ts +408 -218
  238. package/src/panels/frontmatter-fields.ts +51 -10
  239. package/src/panels/frontmatter-panel.ts +357 -79
  240. package/src/panels/git-panel.ts +375 -164
  241. package/src/panels/head-panel.ts +791 -85
  242. package/src/panels/i18n-panel.ts +592 -0
  243. package/src/panels/imports-panel.ts +95 -85
  244. package/src/panels/jump-bar.ts +584 -0
  245. package/src/panels/layers-panel.ts +1106 -328
  246. package/src/panels/left-panel.ts +109 -204
  247. package/src/panels/navigator-panels.ts +131 -0
  248. package/src/panels/overlays.ts +7 -2
  249. package/src/panels/pane-context.ts +1477 -0
  250. package/src/panels/pane-grid.ts +406 -0
  251. package/src/panels/panel-registry.ts +340 -0
  252. package/src/panels/panel-scheduler.ts +75 -0
  253. package/src/panels/problems-panel.ts +234 -0
  254. package/src/panels/properties-panel.ts +946 -1139
  255. package/src/panels/provenance.ts +258 -0
  256. package/src/panels/quick-search.ts +791 -122
  257. package/src/panels/right-panel.ts +228 -102
  258. package/src/panels/seo-modal.ts +395 -0
  259. package/src/panels/settings-pane.ts +203 -0
  260. package/src/panels/signals-panel.ts +354 -137
  261. package/src/panels/statement-editor.ts +41 -25
  262. package/src/panels/statusbar.ts +342 -106
  263. package/src/panels/style-panel.ts +974 -463
  264. package/src/panels/style-utils.ts +3 -3
  265. package/src/panels/stylebook-doc.ts +4 -2
  266. package/src/panels/stylebook-layers-panel.ts +3 -2
  267. package/src/panels/stylebook-panel.ts +51 -26
  268. package/src/panels/tab-strip.ts +1058 -78
  269. package/src/panels/target-line.ts +274 -0
  270. package/src/panels/toolbar.ts +631 -484
  271. package/src/panels/welcome-screen.ts +301 -198
  272. package/src/platform-errors.ts +26 -3
  273. package/src/platform.ts +96 -2
  274. package/src/platforms/cloud.ts +71 -8
  275. package/src/platforms/devserver.ts +92 -29
  276. package/src/publish/deploy-checklist.ts +383 -0
  277. package/src/publish/pages-service.ts +10 -1
  278. package/src/publish/publish-commands.ts +218 -0
  279. package/src/publish/publish-panel.ts +110 -16
  280. package/src/recent-projects.ts +3 -2
  281. package/src/services/a11y-report.ts +505 -0
  282. package/src/services/ai-models.ts +75 -2
  283. package/src/services/ai-project-tools.ts +29 -10
  284. package/src/services/ai-system-prompt.ts +53 -14
  285. package/src/services/ai-tools.ts +39 -3
  286. package/src/services/ai-writes.ts +154 -0
  287. package/src/services/announce.ts +97 -0
  288. package/src/services/automation.ts +424 -175
  289. package/src/services/cem-export.ts +9 -4
  290. package/src/services/clock.ts +61 -0
  291. package/src/services/document-assistant.ts +36 -8
  292. package/src/services/idle.ts +179 -0
  293. package/src/services/import-client.ts +26 -0
  294. package/src/services/monaco-buffer.ts +627 -0
  295. package/src/services/monaco-lazy.ts +36 -0
  296. package/src/services/notify.ts +271 -0
  297. package/src/services/profile.ts +155 -0
  298. package/src/services/references.ts +0 -0
  299. package/src/services/token-lint.ts +3 -2
  300. package/src/services/tool-executor.ts +53 -7
  301. package/src/services/trusted-types.ts +123 -0
  302. package/src/settings/contexts-section.ts +427 -0
  303. package/src/settings/contributed-section.ts +149 -11
  304. package/src/settings/css-vars-editor.ts +306 -349
  305. package/src/settings/defs-editor.ts +93 -34
  306. package/src/settings/dependencies-editor.ts +7 -7
  307. package/src/settings/extension-sections.ts +37 -5
  308. package/src/settings/general-settings.ts +204 -121
  309. package/src/settings/locales-section.ts +347 -0
  310. package/src/settings/preferences-accounts.ts +140 -0
  311. package/src/settings/preferences-dialog.ts +620 -0
  312. package/src/settings/preferences-keymap.ts +270 -0
  313. package/src/settings/project-sections.ts +260 -0
  314. package/src/settings/section-registry.ts +169 -0
  315. package/src/settings/settings-document.ts +385 -0
  316. package/src/shell.ts +1549 -0
  317. package/src/site-context.ts +45 -20
  318. package/src/state.ts +14 -275
  319. package/src/store.ts +88 -43
  320. package/src/studio.ts +790 -346
  321. package/src/style/live-preview.ts +36 -0
  322. package/src/style/project-styles.ts +349 -0
  323. package/src/style/token-ref.ts +123 -0
  324. package/src/tabs/patch-ops.ts +16 -4
  325. package/src/tabs/project-config.ts +416 -0
  326. package/src/tabs/selection.ts +323 -0
  327. package/src/tabs/tab.ts +182 -47
  328. package/src/tabs/transact.ts +209 -28
  329. package/src/types.ts +68 -13
  330. package/src/ui/ai-credentials-form.ts +39 -35
  331. package/src/ui/ai-managed-connect.ts +113 -0
  332. package/src/ui/dynamic-slot.ts +224 -81
  333. package/src/ui/expression-editor.ts +71 -63
  334. package/src/ui/field-input.ts +4 -6
  335. package/src/ui/field-row.ts +215 -38
  336. package/src/ui/form-controls.ts +194 -7
  337. package/src/ui/layers.ts +492 -69
  338. package/src/ui/media-picker.ts +115 -25
  339. package/src/ui/panel-resize.ts +109 -91
  340. package/src/ui/progress-modal.ts +131 -50
  341. package/src/ui/regions.ts +462 -0
  342. package/src/ui/schema-form.ts +229 -45
  343. package/src/ui/spectrum.ts +24 -8
  344. package/src/ui/timing.ts +49 -0
  345. package/src/ui/value-selector.ts +20 -0
  346. package/src/ui/value-source.ts +348 -0
  347. package/src/ui/virtual-window.ts +400 -0
  348. package/src/utils/edit-display.ts +3 -2
  349. package/src/utils/inherited-style.ts +67 -37
  350. package/src/view.ts +64 -78
  351. package/src/workspace/pane-derive.ts +1258 -0
  352. package/src/workspace/session.ts +317 -0
  353. package/src/workspace/workspace.ts +1345 -23
  354. package/styles/canvas.css +653 -0
  355. package/styles/forced-colors.css +106 -0
  356. package/styles/inspector.css +1454 -0
  357. package/styles/overlays.css +2704 -0
  358. package/styles/panels.css +1701 -0
  359. package/styles/shell.css +1423 -0
  360. package/styles/tokens.css +151 -0
  361. package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
  362. package/dist/chunks/extension-sections-qct0gt8f.js +0 -531
  363. package/dist/chunks/extension-sections-qct0gt8f.js.map +0 -12
  364. package/dist/chunks/files-02zn5y24.js +0 -50
  365. package/dist/chunks/files-02zn5y24.js.map +0 -9
  366. package/dist/chunks/git-panel-mbb0y0hz.js +0 -28
  367. package/dist/chunks/git-panel-mbb0y0hz.js.map +0 -9
  368. package/dist/chunks/layers-panel-9qekmqk1.js +0 -26
  369. package/dist/chunks/layers-panel-9qekmqk1.js.map +0 -9
  370. package/dist/chunks/settings-modal-gj0hrah4.js +0 -29
  371. package/dist/chunks/settings-modal-gj0hrah4.js.map +0 -9
  372. package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
  373. package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
  374. package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
  375. package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
  376. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
  377. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
  378. package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
  379. package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
  380. package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
  381. package/dist/chunks/studio-568nx4t5.js +0 -469
  382. package/dist/chunks/studio-568nx4t5.js.map +0 -14
  383. package/dist/chunks/studio-9smfzqhc.js +0 -19
  384. package/dist/chunks/studio-9smfzqhc.js.map +0 -10
  385. package/dist/chunks/studio-asvf0j9e.js +0 -429
  386. package/dist/chunks/studio-asvf0j9e.js.map +0 -13
  387. package/dist/chunks/studio-d77qm3pk.js +0 -3427
  388. package/dist/chunks/studio-d77qm3pk.js.map +0 -26
  389. package/dist/chunks/studio-j6gqyjf4.js +0 -1854
  390. package/dist/chunks/studio-j6gqyjf4.js.map +0 -18
  391. package/dist/chunks/studio-n9zmpxjg.js.map +0 -115
  392. package/dist/chunks/studio-qkvt37c0.js.map +0 -9
  393. package/dist/chunks/studio-rq0vxb19.js +0 -3744
  394. package/dist/chunks/studio-rq0vxb19.js.map +0 -22
  395. package/dist/chunks/studio-wd6ewdvd.js +0 -853
  396. package/dist/chunks/studio-wd6ewdvd.js.map +0 -12
  397. package/dist/chunks/studio-xq0t7n5t.js +0 -140
  398. package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
  399. package/dist/chunks/studio-xze4zrs1.js.map +0 -10
  400. package/dist/chunks/studio-ygzav7z5.js.map +0 -46
  401. package/dist/chunks/studio-zmy7241e.js +0 -399
  402. package/dist/chunks/studio-zmy7241e.js.map +0 -12
  403. package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
  404. package/src/browse/browse-modal.ts +0 -69
  405. package/src/browse/browse.ts +0 -904
  406. package/src/canvas/canvas-diff.ts +0 -216
  407. package/src/canvas/nested-site-style.ts +0 -58
  408. package/src/new-project/design-fields.ts +0 -264
  409. package/src/new-project/templates.ts +0 -61
  410. package/src/panels/tab-bar.ts +0 -548
  411. package/src/settings/settings-modal.ts +0 -203
  412. /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
  413. /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
  414. /package/dist/chunks/{format-host-2c188n41.js.map → format-host-mkwsmt8d.js.map} +0 -0
  415. /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
  416. /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
  417. /package/dist/chunks/{studio-hhj92gcr.js.map → studio-s519zc99.js.map} +0 -0
  418. /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
  419. /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
@@ -1,15 +1,36 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Style panel — CSS property editor with media breakpoint tabs, selector dropdown, section
4
- * accordion, shorthand expand/compress, and filter.
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.
5
+ *
6
+ * The panel resolves a compound coordinate before it can commit anything: `(selection, breakpoint,
7
+ * scheme layer, nested selector)`. It has always computed that tuple — it is the per-field key and
8
+ * the five-branch if/else below — and it used to hide it behind three disconnected widgets on two
9
+ * different bars. `target-line.ts` renders it as one sentence instead, and the three widgets are
10
+ * gone: the breakpoint and scheme axes are selected on the pane context bar (region ⑦), and the
11
+ * selector is the one axis this tab owns.
12
+ *
13
+ * Every row then says where its value came from. `provenance.ts` supplies the chip;
14
+ * `computeInheritedSources()` supplies the donor breakpoint the cascade walk always knew and this
15
+ * panel used to throw away.
5
16
  */
6
17
 
7
18
  import { html, nothing } from "lit-html";
8
19
  import { getNestedStyle } from "@jxsuite/schema/guards";
9
20
  import { live } from "lit-html/directives/live.js";
10
21
  import { ifDefined } from "lit-html/directives/if-defined.js";
11
- import { COMMON_SELECTORS, debouncedStyleCommit, getNodeAtPath, isNestedSelector } from "../store";
22
+ import {
23
+ COMMON_SELECTORS,
24
+ debouncedStyleCommit,
25
+ getNodeAtPath,
26
+ isNestedSelector,
27
+ renderOnly,
28
+ updateUi,
29
+ } from "../store";
12
30
  import { activeTab } from "../workspace/workspace";
31
+ import { primarySelection, unifyValues } from "../tabs/selection";
32
+ import { shell } from "../shell";
33
+ import { activeRegistry } from "../commands/active-registry";
13
34
  import { selectStylebookTag } from "./stylebook-panel";
14
35
  import {
15
36
  mutateUpdateMediaNestedStyle,
@@ -21,13 +42,28 @@ import {
21
42
  transactDoc,
22
43
  } from "../tabs/transact";
23
44
  import { inferInputType, propLabel } from "../utils/studio-utils";
45
+ import { stringArg } from "../commands/command-args";
46
+ import type { AnyCommand, CommandRegistry } from "../commands/registry";
24
47
  import { renderFieldRow } from "../ui/field-row";
25
48
  import { showPromptDialog } from "../ui/layers";
26
49
  import { renderDynamicSlot } from "../ui/dynamic-slot";
27
50
  import { parseMediaEntries, schemeOfQuery } from "../utils/canvas-media";
28
51
  import { getEffectiveMedia, getEffectiveStyle } from "../site-context";
29
- import { computeInheritedStyle } from "../utils/inherited-style";
52
+ import { computeInheritedSources } from "../utils/inherited-style";
53
+ import { loadUsages, peekUsages, usageFiles } from "../services/references";
30
54
  import { mediaDisplayName } from "./shared";
55
+ import { countProvenance, renderProvenanceChip, renderProvenanceDots } from "./provenance";
56
+ import {
57
+ openSelectorMenu,
58
+ renderTargetLine,
59
+ resetSelectorTrigger as resetTargetSelector,
60
+ } from "./target-line";
61
+ import {
62
+ clickAnythingTo,
63
+ openPageAction,
64
+ renderEmptyState,
65
+ staleSelectionMessage,
66
+ } from "./empty-state";
31
67
  import {
32
68
  allConditionsPass,
33
69
  autoOpenSections,
@@ -41,6 +77,8 @@ import {
41
77
  } from "./style-utils";
42
78
  import { widgetForType } from "./style-inputs";
43
79
 
80
+ import type { FieldProvenance, ProvenanceState } from "./provenance";
81
+ import type { TargetScope, TargetSegment } from "./target-line";
44
82
  import type { Tab } from "../tabs/tab";
45
83
  import type { JxPath } from "../state";
46
84
  import type { JsonValue } from "../types";
@@ -53,6 +91,9 @@ interface CssLonghand {
53
91
 
54
92
  type CssPropertyEntry = Record<string, unknown>;
55
93
 
94
+ /** The chip a row wears when its caller has nothing more specific to say. */
95
+ const DEFAULT_PROVENANCE: FieldProvenance = { state: "default" };
96
+
56
97
  type StyleMutateFn = (
57
98
  t: Tab,
58
99
  prop: string,
@@ -88,6 +129,133 @@ function resolveNestedTagStyle(style: JxStyle, tagPath: string): JxStyle {
88
129
  return obj;
89
130
  }
90
131
 
132
+ /**
133
+ * The style object one compound coordinate addresses.
134
+ *
135
+ * Split out of the panel body because the coordinate has to be resolved TWICE: once against the
136
+ * effective (site-merged) style, which is what the rows display, and once against the document's
137
+ * own, which is what a commit writes. The difference between the two is the "from site tokens"
138
+ * donor — invisible for as long as there was only one resolution.
139
+ *
140
+ * @param {JxStyle} style
141
+ * @param {string | null} activeSelector
142
+ * @param {string | null} editMedia
143
+ * @returns {JxStyle}
144
+ */
145
+ function resolveContextStyle(
146
+ style: JxStyle,
147
+ activeSelector: string | null,
148
+ editMedia: string | null,
149
+ ): JxStyle {
150
+ const base = editMedia ? (getNestedStyle(style, `@${editMedia}`) ?? {}) : style;
151
+ if (activeSelector) {
152
+ return isTagPath(activeSelector)
153
+ ? resolveNestedTagStyle(base, activeSelector)
154
+ : (getNestedStyle(base, activeSelector) ?? {});
155
+ }
156
+ const flat: JxStyle = {};
157
+ for (const [p, v] of Object.entries(base)) {
158
+ if (typeof v !== "object") {
159
+ flat[p] = v;
160
+ }
161
+ }
162
+ return flat;
163
+ }
164
+
165
+ /**
166
+ * The mutation that writes to one compound coordinate — the other half of
167
+ * {@link resolveContextStyle}, and the reason the two now sit side by side: they are one decision
168
+ * expressed twice, and they used to be interleaved in a five-branch `if/else` where a new branch
169
+ * could easily read one coordinate and write another.
170
+ *
171
+ * @param {JxPath} sel
172
+ * @param {string | null} activeSelector
173
+ * @param {string | null} editMedia
174
+ * @returns {StyleMutateFn}
175
+ */
176
+ function contextMutate(
177
+ sel: JxPath,
178
+ activeSelector: string | null,
179
+ editMedia: string | null,
180
+ ): StyleMutateFn {
181
+ if (activeSelector && isTagPath(activeSelector)) {
182
+ const stylePath = activeSelector.split(" ");
183
+ return editMedia
184
+ ? (t, prop, val) =>
185
+ mutateUpdateMediaNestedStylePath(
186
+ t,
187
+ sel,
188
+ editMedia,
189
+ stylePath,
190
+ prop,
191
+ val as string | undefined,
192
+ )
193
+ : (t, prop, val) =>
194
+ mutateUpdateNestedStylePath(t, sel, stylePath, prop, val as string | undefined);
195
+ }
196
+ if (activeSelector) {
197
+ return editMedia
198
+ ? (t, prop, val) =>
199
+ mutateUpdateMediaNestedStyle(
200
+ t,
201
+ sel,
202
+ editMedia,
203
+ activeSelector,
204
+ prop,
205
+ val as string | undefined,
206
+ )
207
+ : (t, prop, val) =>
208
+ mutateUpdateNestedStyle(t, sel, activeSelector, prop, val as string | undefined);
209
+ }
210
+ return editMedia
211
+ ? (t, prop, val) => mutateUpdateMediaStyle(t, sel, editMedia, prop, val as string | undefined)
212
+ : (t, prop, val) => mutateUpdateStyle(t, sel, prop, val as string | undefined);
213
+ }
214
+
215
+ /**
216
+ * Which properties the selected elements disagree about, at the coordinate the tab is editing.
217
+ *
218
+ * Resolved through the SAME `resolveContextStyle` the panel reads its own values with, so "mixed"
219
+ * is decided at exactly the breakpoint, scheme layer and selector the Target Line names — two cards
220
+ * that differ only inside `@md` are not mixed while you are editing Base.
221
+ *
222
+ * Returns an empty set for a selection of one, which is the whole reason a single selection renders
223
+ * no Mixed state anywhere: the loop never finds a second value to disagree with.
224
+ *
225
+ * @param {Tab} tab
226
+ * @param {readonly JxPath[]} targets
227
+ * @param {string | null} activeSelector
228
+ * @param {string | null} editMedia
229
+ * @returns {Set<string>}
230
+ */
231
+ function mixedStyleProps(
232
+ tab: Tab,
233
+ targets: readonly JxPath[],
234
+ activeSelector: string | null,
235
+ editMedia: string | null,
236
+ ): Set<string> {
237
+ const mixed = new Set<string>();
238
+ if (targets.length < 2) {
239
+ return mixed;
240
+ }
241
+ const blocks = targets.map((path) => {
242
+ const node = getNodeAtPath(tab.doc.document, path) as JxMutableNode | undefined;
243
+ return resolveContextStyle(node?.style ?? {}, activeSelector, editMedia);
244
+ });
245
+ const props = new Set<string>();
246
+ for (const block of blocks) {
247
+ for (const prop of Object.keys(block)) {
248
+ props.add(prop);
249
+ }
250
+ }
251
+ for (const prop of props) {
252
+ if (unifyValues(blocks.map((block) => block[prop] ?? null)).mixed) {
253
+ mixed.add(prop);
254
+ }
255
+ }
256
+ return mixed;
257
+ }
258
+
91
259
  // ─── Row renderers ──────────────────────────────────────────────────────────
92
260
 
93
261
  function renderStyleRow(
@@ -95,131 +263,154 @@ function renderStyleRow(
95
263
  prop: string,
96
264
  value: string,
97
265
  onCommit: (v: string | undefined) => void,
98
- onDelete: () => void,
99
266
  isWarning: boolean,
100
267
  gridMode: boolean,
101
268
  inheritedValue: string | undefined,
102
269
  templateSignals: string[] = [],
103
270
  fieldKey: string = prop,
271
+ provenance?: FieldProvenance,
104
272
  ) {
273
+ const chip: FieldProvenance = provenance ?? DEFAULT_PROVENANCE;
105
274
  const type = inferInputType(entry);
106
275
  const hasVal = value !== undefined && value !== "";
107
276
  const placeholder = !hasVal && inheritedValue ? String(inheritedValue) : "";
108
277
  const spanVal = gridMode && (entry as Record<string, unknown>).$span === 2 ? 2 : undefined;
109
- // Style values are schema-legal at two rungs: literal and ${} template (no $ref in JxStyle).
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.
110
283
  const slot = renderDynamicSlot({
111
- caps: ["literal", "template"],
284
+ caps: "styleProperty",
112
285
  fieldKey,
113
286
  onChange: (v?: JsonValue) => onCommit(v === undefined || v === "" ? undefined : String(v)),
114
287
  staticWidget: widgetForType(type, entry, prop, value, onCommit, { placeholder }),
115
288
  stateDefs: templateSignals,
116
289
  value,
117
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".
118
294
  return renderFieldRow({
119
295
  prop,
120
296
  label: propLabel(entry, prop),
121
- hasValue: hasVal,
122
- onClear: onDelete,
297
+ hasValue: false,
123
298
  widget: slot.widget,
299
+ provenance: chip,
124
300
  labelExtra: slot.modeButton,
125
301
  ...(spanVal != null && { span: spanVal }),
126
302
  warning: isWarning,
127
303
  });
128
304
  }
129
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
+
130
318
  /**
131
- * @param {string} shortProp
132
- * @param {CssPropertyEntry} entry
133
- * @param {Record<string, unknown>} style
134
- * @param {(t: Tab, prop: string, val: string | Record<string, unknown> | undefined) => void} mutateFn
135
- * @param {() => void} _deleteFn
136
- * @param {Record<string, string | number>} inherited
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.
137
329
  */
138
- function renderShorthandRow(
139
- shortProp: string,
140
- entry: CssPropertyEntry,
141
- style: Record<string, unknown>,
142
- mutateFn: StyleMutateFn,
143
- _deleteFn: () => void,
144
- inherited: Record<string, string | number> = {},
145
- ) {
330
+ function renderShorthandRow({
331
+ prop: shortProp,
332
+ entry,
333
+ style,
334
+ mutate,
335
+ inherited,
336
+ ctx,
337
+ }: ShorthandRowOptions) {
146
338
  const tab = activeTab.value!;
147
339
  const longhands = getLonghands(shortProp) as CssLonghand[];
148
340
  const shortVal = style[shortProp];
149
341
  const hasLonghands = longhands.some((l: CssLonghand) => style[l.name] !== undefined);
150
342
  const isExpanded = tab.session.ui.styleShorthands[shortProp] ?? hasLonghands;
151
- const hasAnyVal =
152
- shortVal !== undefined || longhands.some((l: CssLonghand) => style[l.name] !== undefined);
343
+ const hasAnyVal = shortVal !== undefined || hasLonghands;
153
344
 
154
- return html`
155
- <div class="style-row" data-prop=${shortProp}>
156
- <div class="style-row-label">
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
+ >
157
397
  ${
158
- hasAnyVal
159
- ? html`<span
160
- class="set-dot"
161
- title="Clear ${shortProp}"
162
- @click=${(e: Event) => {
163
- e.stopPropagation();
164
- transactDoc(activeTab.value, (t) => {
165
- if (shortVal !== undefined) {
166
- mutateFn(t, shortProp);
167
- }
168
- for (const l of longhands) {
169
- if (style[l.name] !== undefined) {
170
- mutateFn(t, l.name);
171
- }
172
- }
173
- });
174
- }}
175
- ></span>`
176
- : nothing
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>`
177
401
  }
178
- <sp-field-label size="s" title=${shortProp}>${propLabel(entry, shortProp)}</sp-field-label>
179
- </div>
180
- <div class="style-shorthand-header">
181
- <sp-textfield
182
- size="s"
183
- .value=${live(shortVal || "")}
184
- placeholder=${
185
- !shortVal && hasLonghands
186
- ? longhands.map((l: CssLonghand) => style[l.name] || "0").join(" ")
187
- : !shortVal && inherited[shortProp]
188
- ? inherited[shortProp]
189
- : !shortVal && longhands.some((l: CssLonghand) => inherited[l.name])
190
- ? longhands.map((l: CssLonghand) => inherited[l.name] || "0").join(" ")
191
- : ""
192
- }
193
- @input=${debouncedStyleCommit(`short:${shortProp}`, 400, (e: Event) => {
194
- transactDoc(activeTab.value, (t) => {
195
- for (const l of longhands) {
196
- if (style[l.name] !== undefined) {
197
- mutateFn(t, l.name);
198
- }
199
- }
200
- mutateFn(t, shortProp, (e.target as HTMLInputElement).value || undefined);
201
- });
202
- })}
203
- ></sp-textfield>
204
- <sp-action-button
205
- size="xs"
206
- quiet
207
- @click=${(e: Event) => {
208
- e.stopPropagation();
209
- activeTab.value!.session.ui.styleShorthands = {
210
- ...activeTab.value!.session.ui.styleShorthands,
211
- [shortProp]: !isExpanded,
212
- };
213
- }}
214
- >
215
- ${
216
- isExpanded
217
- ? html`<sp-icon-chevron-down slot="icon"></sp-icon-chevron-down>`
218
- : html`<sp-icon-chevron-right slot="icon"></sp-icon-chevron-right>`
219
- }
220
- </sp-action-button>
221
- </div>
402
+ </sp-action-button>
222
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
+ })}
223
414
  ${
224
415
  isExpanded
225
416
  ? (() => {
@@ -232,35 +423,27 @@ function renderShorthandRow(
232
423
  : null;
233
424
  const compress = isBorderSide ? compressBorderSide : compressShorthand;
234
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
+ );
235
435
  return longhands.map(({ name, entry: lEntry }: CssLonghand, idx: number) => {
236
436
  const lVal = style[name] ?? (expanded ? expanded[idx] : "");
437
+ const hasLVal = lVal !== undefined && lVal !== "";
237
438
  return html`
238
439
  <div class="style-row style-row--child" data-prop=${name}>
239
440
  <div class="style-row-label">
240
- ${
241
- lVal !== undefined && lVal !== ""
242
- ? html`<span
243
- class="set-dot"
244
- title="Clear ${name}"
245
- @click=${(e: Event) => {
246
- e.stopPropagation();
247
- const vals = longhands.map((l: CssLonghand, i: number) =>
248
- i === idx
249
- ? emptyVal
250
- : (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
251
- );
252
- transactDoc(activeTab.value, (t) => {
253
- for (const l of longhands) {
254
- if (style[l.name] !== undefined) {
255
- mutateFn(t, l.name);
256
- }
257
- }
258
- mutateFn(t, shortProp, compress(vals as string[]));
259
- });
260
- }}
261
- ></span>`
262
- : nothing
263
- }
441
+ ${renderProvenanceChip(
442
+ name,
443
+ longhandProvenance(name, shortProp, hasLVal, ctx, () =>
444
+ recompress(idx, emptyVal),
445
+ ),
446
+ )}
264
447
  <sp-field-label size="s" title=${name}
265
448
  >${propLabel(lEntry, name)}</sp-field-label
266
449
  >
@@ -270,21 +453,7 @@ function renderShorthandRow(
270
453
  lEntry,
271
454
  name,
272
455
  lVal as string,
273
- (newVal: string) => {
274
- const vals = longhands.map((l: CssLonghand, i: number) =>
275
- i === idx
276
- ? newVal || emptyVal
277
- : (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
278
- );
279
- transactDoc(activeTab.value, (t) => {
280
- for (const l of longhands) {
281
- if (style[l.name] !== undefined) {
282
- mutateFn(t, l.name);
283
- }
284
- }
285
- mutateFn(t, shortProp, compress(vals as string[]));
286
- });
287
- },
456
+ (newVal: string) => recompress(idx, newVal || emptyVal),
288
457
  {
289
458
  placeholder: !lVal && inherited[name] ? String(inherited[name]) : "",
290
459
  },
@@ -298,23 +467,403 @@ function renderShorthandRow(
298
467
  `;
299
468
  }
300
469
 
470
+ // ─── The edit target ─────────────────────────────────────────────────────────
471
+
472
+ /** Forget the selector menu's element handle — the Inspector unmounted, or a test starts clean. */
473
+ export function resetSelectorMenu(): void {
474
+ resetTargetSelector();
475
+ }
476
+
477
+ /**
478
+ * Whether the "show affected" list under a project-wide warning band is open.
479
+ *
480
+ * 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.
482
+ */
483
+ let _showAffected = false;
484
+
485
+ /** Fold the affected list away — the Inspector unmounted, or a test starts clean. */
486
+ export function resetAffectedDisclosure(): void {
487
+ _showAffected = false;
488
+ }
489
+
490
+ /**
491
+ * Ask a registered command to do something this panel does not own.
492
+ *
493
+ * The breakpoint and the colour scheme are DEFINED in Project Settings › Contexts and SELECTED on
494
+ * the pane context bar. A Target Line segment that opened its own list would be the third selector
495
+ * §6.4 forbids, so it routes to the definition site through the same command the context bar's
496
+ * "Manage contexts…" footer runs.
497
+ */
498
+ function runCommand(id: string, args?: Record<string, unknown>): void {
499
+ void activeRegistry()?.run(id, args);
500
+ }
501
+
502
+ /**
503
+ * The tag a stylebook rule restyles, when one is being edited.
504
+ *
505
+ * In Stylebook the "selector" is a bare tag path (`h1`, `table th`) written into the DOCUMENT's
506
+ * root style, so the first segment of the path is the tag whose blast radius the scope chip
507
+ * states.
508
+ */
509
+ function stylebookTagOf(selector: string | null): string | null {
510
+ return selector && isTagPath(selector) ? (selector.split(" ")[0] ?? null) : null;
511
+ }
512
+
513
+ /**
514
+ * Whether this document IS the project's stylesheet.
515
+ *
516
+ * `project.json`'s `style` is not a document's style. The compiler hands it to EVERY route's
517
+ * compile as `projectStyle` (`site/site-build.ts`), and `compileStyles` emits its custom properties
518
+ * as `:root`, its flat properties as `body`, and every bare tag key as a global rule — so `{ "h1":
519
+ * { "color": "red" } }` in project.json becomes `h1 { color: red }` in the stylesheet of every
520
+ * page, reaching inside every component instance too (nothing in the compiler or the runtime
521
+ * attaches a shadow root). Studio shows the same cascade: `getEffectiveStyle` merges the object
522
+ * into every open document.
523
+ *
524
+ * That makes the document Stylebook opens by default the widest blast radius in the app — and it is
525
+ * the one `resolveScope` used to label "in this **document**", the narrowest.
526
+ *
527
+ * Matched by suffix rather than by equality, the way `studio.ts` and `imports-panel.ts` already
528
+ * address this file: a config reached from a sub-directory is still a config, and it still has no
529
+ * elements of its own for "in this document" to be about.
530
+ */
531
+ function isProjectStylesheet(path: string): boolean {
532
+ return /(?:^|\/)project\.json$/.test(path);
533
+ }
534
+
535
+ /**
536
+ * How wide an edit here reaches, and — for the project case — how wide, in numbers.
537
+ *
538
+ * Three kinds, in the order a user meets them:
539
+ *
540
+ * - **element** — an element is selected; the rule is scoped to it and to nothing else.
541
+ * - **document** — Stylebook on an ordinary document: every `<tag>` on this page.
542
+ * - **project** — the same keystroke restyles files that are not open. Two documents reach this far:
543
+ * a LAYOUT, whose root style is inherited by every page declaring it, and
544
+ * {@link isProjectStylesheet}, which is inherited by all of them.
545
+ *
546
+ * The project stylesheet reaches it with NO tag as well, which no other document does: deselecting
547
+ * in Stylebook leaves the tab editing project.json's own root style — `:root` and `body` on every
548
+ * route — and there is no element anywhere for "this element" to have meant.
549
+ */
550
+ function resolveScope(tab: Tab, stylebookTag: string | null): TargetScope {
551
+ const path = tab.documentPath ?? "";
552
+ const isProjectStyle = isProjectStylesheet(path);
553
+ if (!stylebookTag) {
554
+ return isProjectStyle
555
+ ? projectScope("every page in this project", null)
556
+ : { kind: "element", label: "this element" };
557
+ }
558
+ const shared = isProjectStyle || /^\.?\/?layouts\//.test(path);
559
+ const label = `all <${stylebookTag}> in this ${shared ? "project" : "document"}`;
560
+ return shared ? projectScope(label, stylebookTag) : { kind: "document", label };
561
+ }
562
+
563
+ /**
564
+ * The project-wide warning band: its label, its count, and its disclosure.
565
+ *
566
+ * The count is `findReferences`' tag walk, which counts `tagName` matches across the project. Where
567
+ * the host cannot answer — no `findReferences` route, a failed sweep, or no tag to ask about — the
568
+ * band says **unknown** rather than a confident zero, exactly as `services/references.ts` requires
569
+ * of every rendering.
570
+ *
571
+ * @param {string} label
572
+ * @param {string | null} tagName — null for a root-style edit, which has no tag to count.
573
+ * @returns {TargetScope}
574
+ */
575
+ function projectScope(label: string, tagName: string | null): TargetScope {
576
+ if (tagName === null) {
577
+ // No tag, so no query: `:root`/`body` on every route is not a `tagName` sweep, and how many
578
+ // Routes that is, is a question no surface here can answer. A zero would be the same lie the
579
+ // Band exists to stop, in a smaller font — and with no file list, there is nothing to disclose.
580
+ return { affected: "how many pages that is, is unknown", kind: "project", label };
581
+ }
582
+ const query = { tagName };
583
+ const state = peekUsages(query);
584
+ 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"));
588
+ }
589
+ return {
590
+ kind: "project",
591
+ label,
592
+ affected: affectedSentence(state),
593
+ affectedFiles: state?.status === "ready" ? usageFiles(state.result) : [],
594
+ showAffected: _showAffected,
595
+ onToggleAffected: () => {
596
+ _showAffected = !_showAffected;
597
+ renderOnly("rightPanel");
598
+ },
599
+ };
600
+ }
601
+
602
+ /** The affected count, in words — and "unknown" wherever the query cannot answer. */
603
+ function affectedSentence(state: ReturnType<typeof peekUsages>): string {
604
+ if (state === null || state.status === "pending") {
605
+ return "counting the elements it affects…";
606
+ }
607
+ if (state.status !== "ready") {
608
+ return "how many elements that is, is unknown";
609
+ }
610
+ const { files, refsTotal } = state.result;
611
+ if (refsTotal === 0) {
612
+ return "no element in the project uses it yet";
613
+ }
614
+ return `${refsTotal} element${refsTotal === 1 ? "" : "s"} in ${files.length} file${
615
+ files.length === 1 ? "" : "s"
616
+ }`;
617
+ }
618
+
619
+ /** The two axes the Style tab states but does not own, plus the element it is pointed at. */
620
+ function targetSegments(
621
+ elementLabel: string,
622
+ mediaTab: string | null,
623
+ hasBreakpoints: boolean,
624
+ schemeLayer: string | null,
625
+ ): TargetSegment[] {
626
+ const segments: TargetSegment[] = [
627
+ {
628
+ key: "element",
629
+ label: elementLabel,
630
+ title: "What these edits are pointed at — click to find it in the Outline",
631
+ onActivate: () => runCommand("view.setActivity", { tab: "layers" }),
632
+ },
633
+ {
634
+ key: "media",
635
+ label: hasBreakpoints && mediaTab ? `@${mediaDisplayName(mediaTab)}` : "Base",
636
+ title:
637
+ "The breakpoint these edits land in. Choose it on the pane's Context control; click to " +
638
+ "manage breakpoints.",
639
+ onActivate: () => runCommand("settings.open", { section: "contexts" }),
640
+ },
641
+ ];
642
+ if (schemeLayer) {
643
+ segments.push({
644
+ key: "scheme",
645
+ label: `${mediaDisplayName(schemeLayer)} variant`,
646
+ title:
647
+ "Edits land in this colour-scheme variant. Set the pane's Context control back to Auto " +
648
+ "to edit base styles; click to manage schemes.",
649
+ onActivate: () => runCommand("settings.open", { section: "contexts" }),
650
+ });
651
+ }
652
+ return segments;
653
+ }
654
+
655
+ // ─── Provenance ──────────────────────────────────────────────────────────────
656
+
657
+ /** An inherited value, its donor's name, and how to go there. */
658
+ interface InheritedInfo {
659
+ value: string | number;
660
+ donor: string;
661
+ jump?: (() => void) | undefined;
662
+ }
663
+
664
+ /**
665
+ * The signal a `${…}` style value reads, when it reads one.
666
+ *
667
+ * `state.` is optional because both spellings occur in documents and both mean the same entry — the
668
+ * chip names the signal, not the accessor path it was reached by.
669
+ */
670
+ function templateSignalOf(value: unknown): string | null {
671
+ if (typeof value !== "string") {
672
+ return null;
673
+ }
674
+ return /\$\{\s*(?:state\.)?([A-Za-z_$][\w$]*)/.exec(value)?.[1] ?? null;
675
+ }
676
+
677
+ /** Whether a value is a `${}` template rather than a literal — the `bound` rung for a CSS value. */
678
+ function isTemplateValue(value: unknown): boolean {
679
+ return typeof value === "string" && value.includes("${");
680
+ }
681
+
682
+ /** What the Style tab can say about one property's origin. */
683
+ interface ProvenanceCtx {
684
+ own: JxStyle;
685
+ active: JxStyle;
686
+ inherited: Record<string, InheritedInfo>;
687
+ signals: string[];
688
+ clear: (prop: string) => void;
689
+ /**
690
+ * Properties the selected elements DISAGREE about, and how many elements are selected (§6.5).
691
+ *
692
+ * Empty for a selection of one — which is what keeps every other branch of `provenanceOf`
693
+ * reachable exactly as often as it was before the selection became a set.
694
+ */
695
+ mixed?: { props: Set<string>; count: number } | undefined;
696
+ }
697
+
698
+ /**
699
+ * Where this property's value came from.
700
+ *
701
+ * Four answers, and three of them were previously indistinguishable from each other: an inherited
702
+ * value arrived as a placeholder identical to the CSS initial value, a site token arrived looking
703
+ * like a local override, and a `${}` binding arrived looking like a literal with braces in it.
704
+ */
705
+ function provenanceOf(prop: string, ctx: ProvenanceCtx): FieldProvenance {
706
+ // Mixed is decided FIRST, because it is a fact about the selection rather than about the value:
707
+ // The primary's own value may be perfectly ordinary while five other elements disagree with it,
708
+ // And drawing that as `set` is exactly the lie this state exists to stop.
709
+ if (ctx.mixed?.props.has(prop)) {
710
+ return {
711
+ state: "mixed",
712
+ donor: String(ctx.mixed.count),
713
+ onClick: () => ctx.clear(prop),
714
+ };
715
+ }
716
+ const value = ctx.active[prop];
717
+ if (value !== undefined && ctx.own[prop] === undefined) {
718
+ // Present in the effective style but not in the document's own: it came from the site style.
719
+ // No jump: the project style document is not something this tab can open.
720
+ return {
721
+ state: "inherited",
722
+ donor: "site tokens",
723
+ title: `Inherited from the project's site tokens — ${prop} is not set on this document`,
724
+ };
725
+ }
726
+ if (value !== undefined) {
727
+ if (isTemplateValue(value)) {
728
+ const signal = templateSignalOf(value);
729
+ return {
730
+ state: "bound",
731
+ ...(signal ? { donor: signal } : {}),
732
+ ...(signal && ctx.signals.includes(signal)
733
+ ? {
734
+ onClick: () => {
735
+ runCommand("view.setActivity", { tab: "data" });
736
+ runCommand("data.expandRow", { name: signal });
737
+ },
738
+ }
739
+ : {}),
740
+ };
741
+ }
742
+ return { state: "set", onClick: () => ctx.clear(prop) };
743
+ }
744
+ const inherited = ctx.inherited[prop];
745
+ if (inherited) {
746
+ return {
747
+ state: "inherited",
748
+ donor: inherited.donor,
749
+ ...(inherited.jump ? { onClick: inherited.jump } : {}),
750
+ };
751
+ }
752
+ return { state: "default" };
753
+ }
754
+
755
+ /**
756
+ * Where a SHORTHAND row's value came from.
757
+ *
758
+ * A shorthand is less a value than a name for four of them, so every answer it can give is about
759
+ * five properties at once: `padding` is set when `padding` or any of `paddingTop…Left` is, mixed
760
+ * when the selection disagrees about any of the five, and inherited when the cascade supplies one
761
+ * and this document supplies none. Asking only about the shorthand key is what let the row draw a
762
+ * plain "Clear padding" dot over a property the panel had already computed as mixed.
763
+ *
764
+ * @param {string} shortProp
765
+ * @param {ProvenanceCtx} ctx
766
+ * @param {(() => void) | undefined} [clearAll] — clears the shorthand AND its longhands, which is
767
+ * the only clear this row has ever meant; omitted where there is nothing to clear.
768
+ * @returns {FieldProvenance}
769
+ */
770
+ function shorthandProvenance(
771
+ shortProp: string,
772
+ ctx: ProvenanceCtx,
773
+ clearAll?: (() => void) | undefined,
774
+ ): FieldProvenance {
775
+ const props = [shortProp, ...(getLonghands(shortProp) as CssLonghand[]).map((l) => l.name)];
776
+ const { mixed } = ctx;
777
+ if (mixed && props.some((p) => mixed.props.has(p))) {
778
+ return {
779
+ state: "mixed",
780
+ donor: String(mixed.count),
781
+ ...(clearAll ? { onClick: clearAll } : {}),
782
+ };
783
+ }
784
+ for (const p of props) {
785
+ const prov = provenanceOf(p, ctx);
786
+ if (prov.state === "default") {
787
+ continue;
788
+ }
789
+ return prov.state === "set" && clearAll ? { ...prov, onClick: clearAll } : prov;
790
+ }
791
+ return DEFAULT_PROVENANCE;
792
+ }
793
+
794
+ /**
795
+ * Where a longhand CHILD row's value came from.
796
+ *
797
+ * A longhand can hold a value it was never given: `padding: 1px 2px` fills four child rows, and
798
+ * clearing one of them recompresses the shorthand rather than deleting a key. So a value derived
799
+ * from the shorthand counts as `set` here, and a selection that disagrees about the shorthand
800
+ * disagrees about every child the shorthand fills.
801
+ *
802
+ * @param {string} name
803
+ * @param {string} shortProp
804
+ * @param {boolean} hasValue — set here, or filled in by the shorthand above.
805
+ * @param {ProvenanceCtx} ctx
806
+ * @param {() => void} clearChild
807
+ * @returns {FieldProvenance}
808
+ */
809
+ function longhandProvenance(
810
+ name: string,
811
+ shortProp: string,
812
+ hasValue: boolean,
813
+ ctx: ProvenanceCtx,
814
+ clearChild: () => void,
815
+ ): FieldProvenance {
816
+ const { mixed } = ctx;
817
+ if (mixed && (mixed.props.has(name) || (hasValue && mixed.props.has(shortProp)))) {
818
+ return { state: "mixed", donor: String(mixed.count), onClick: clearChild };
819
+ }
820
+ const prov = provenanceOf(name, ctx);
821
+ if (prov.state === "set" || (prov.state === "default" && hasValue)) {
822
+ return { state: "set", onClick: clearChild };
823
+ }
824
+ return prov;
825
+ }
826
+
827
+ /**
828
+ * A section's tally, counting a shorthand as set when any of its longhands is.
829
+ *
830
+ * @param {{ prop: string; entry: CssPropertyEntry }[]} entries
831
+ * @param {ProvenanceCtx} ctx
832
+ */
833
+ function sectionProvenance(
834
+ entries: { prop: string; entry: CssPropertyEntry }[],
835
+ ctx: ProvenanceCtx,
836
+ ): ProvenanceState[] {
837
+ return entries.map(({ prop, entry }) =>
838
+ inferInputType(entry) === "shorthand"
839
+ ? shorthandProvenance(prop, ctx).state
840
+ : provenanceOf(prop, ctx).state,
841
+ );
842
+ }
843
+
301
844
  // ─── Main template ──────────────────────────────────────────────────────────
302
845
 
303
846
  /**
304
847
  * @param {JxMutableNode} node
305
848
  * @param {string | null} activeMediaTab
306
849
  * @param {string | null} activeSelector
307
- * @param {JxStyle} [effectiveStyle]
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.
308
854
  */
309
855
  function styleSidebarTemplate(
310
856
  node: JxMutableNode,
311
857
  activeMediaTab: string | null,
312
858
  activeSelector: string | null,
313
- effectiveStyle?: JxStyle,
859
+ opts: { effectiveStyle?: JxStyle | undefined; stylebookSelector?: string | null } = {},
314
860
  ) {
861
+ const { effectiveStyle, stylebookSelector = null } = opts;
315
862
  const tab = activeTab.value!;
316
- const sel = tab.session.selection as JxPath;
317
- const style = effectiveStyle || node.style || {};
863
+ const sel = primarySelection(tab.session.selection) as JxPath;
864
+ /** The document's OWN style — what a commit writes into. */
865
+ const ownRootStyle: JxStyle = node.style || {};
866
+ const style = effectiveStyle ?? ownRootStyle;
318
867
  // Signals seeding the ${} template default when a style value escalates (no $ref in JxStyle).
319
868
  const templateSignals = Object.entries(tab.doc.document.state || {})
320
869
  .filter(
@@ -341,130 +890,59 @@ function styleSidebarTemplate(
341
890
  // The media context edits actually target: a valid breakpoint tab, else the scheme layer.
342
891
  const editMedia = mediaTab && mediaNames.length > 0 ? mediaTab : schemeLayer;
343
892
 
344
- // ── Media tabs template ──────────────────────────────────────────────────
345
- const mediaTabsT =
346
- mediaNames.length > 0
347
- ? html`
348
- <sp-tabs
349
- size="s"
350
- selected=${mediaTab || "base"}
351
- @change=${(e: Event) => {
352
- const val = (e.target as HTMLElement & { selected: string }).selected;
353
- const newMedia = val === "base" ? null : val;
354
- if (newMedia !== tab.session.ui.activeMedia) {
355
- tab.session.ui.activeMedia = newMedia;
356
- }
357
- }}
358
- >
359
- <sp-tab label="Base" value="base"></sp-tab>
360
- ${mediaNames.map(
361
- (name) => html` <sp-tab label=${mediaDisplayName(name)} value=${name}></sp-tab> `,
362
- )}
363
- </sp-tabs>
364
- `
365
- : nothing;
366
-
367
- // ── Selector dropdown ──────────────────────────────────────────────────────
893
+ // ── The selector axis — the one this tab owns ──────────────────────────────
368
894
  const contextStyle = editMedia
369
895
  ? (style[`@${editMedia}`] as Record<string, unknown>) || {}
370
896
  : style;
371
- const existingSelectors = Object.keys(contextStyle).filter((s) => isNestedSelector(s));
372
- const existingSet = new Set(existingSelectors);
373
- const commonSet = new Set(COMMON_SELECTORS);
374
- const extraSelectors = existingSelectors.filter((s) => !commonSet.has(s));
375
- if (activeSelector && !commonSet.has(activeSelector) && !existingSet.has(activeSelector)) {
376
- extraSelectors.unshift(activeSelector);
377
- }
378
-
379
- const _selectorVal = activeSelector || "__base__";
380
- const selectorT = html`
381
- <sp-picker
382
- size="s"
383
- class="selector-select"
384
- quiet
385
- .value=${live(_selectorVal)}
386
- @change=${(e: Event) => {
387
- const val = (e.target as HTMLElement & { value: string }).value;
388
- if (val === "__add_custom__") {
389
- requestAnimationFrame(() => {
390
- (e.target as HTMLElement & { value: string }).value = activeSelector || "__base__";
391
- });
392
- const picker = e.target as HTMLElement;
393
- const bar = picker.closest(".style-toolbar") as HTMLElement;
394
- picker.style.display = "none";
395
- const inp = document.createElement("input");
396
- inp.type = "text";
397
- inp.className = "selector-custom-input";
398
- inp.placeholder = ":hover, .child, &.active, [attr]";
399
- bar.append(inp);
400
- inp.focus();
401
- let done = false;
402
- const finish = (accept: boolean) => {
403
- if (done) {
404
- return;
405
- }
406
- done = true;
407
- const v = inp.value.trim();
408
- inp.remove();
409
- picker.style.display = "";
410
- if (accept && v && isNestedSelector(v)) {
411
- activeTab.value!.session.ui.activeSelector = v;
412
- }
413
- };
414
- inp.addEventListener("keydown", (ev) => {
415
- if (ev.key === "Enter") {
416
- finish(true);
417
- } else if (ev.key === "Escape") {
418
- finish(false);
419
- }
420
- });
421
- inp.addEventListener("blur", () => finish(inp.value.trim().length > 0));
422
- return;
423
- }
424
- const newSelector = val === "__base__" ? null : val;
425
- activeTab.value!.session.ui.activeSelector = newSelector;
426
- }}
427
- >
428
- <sp-menu-item value="__base__">(base)</sp-menu-item>
429
- <sp-menu-divider></sp-menu-divider>
430
- ${COMMON_SELECTORS.map(
431
- (s) => html`
432
- <sp-menu-item value=${s}>${existingSet.has(s) ? `${s} \u25CF` : s}</sp-menu-item>
433
- `,
434
- )}
435
- ${
436
- extraSelectors.length > 0
437
- ? html`
438
- <sp-menu-divider></sp-menu-divider>
439
- ${extraSelectors.map((s) => html` <sp-menu-item value=${s}>${s} ●</sp-menu-item> `)}
440
- `
441
- : nothing
442
- }
443
- <sp-menu-divider></sp-menu-divider>
444
- <sp-menu-item value="__add_custom__">+ Add custom…</sp-menu-item>
445
- </sp-picker>
446
- `;
897
+ const declaredSelectors = new Set(Object.keys(contextStyle).filter((s) => isNestedSelector(s)));
898
+ const selectorOptions = [
899
+ ...new Set([
900
+ ...COMMON_SELECTORS,
901
+ ...declaredSelectors,
902
+ ...(activeSelector ? [activeSelector] : []),
903
+ ]),
904
+ ];
447
905
 
448
- // ── Combined toolbar (media tabs + scheme badge + selector) ────────────────
449
- // No scheme tabs — the tab-bar control is the one switch; the badge shows where edits land.
450
- const schemeBadgeT = schemeLayer
451
- ? html`
452
- <span
453
- class="style-scheme-badge"
454
- title="Edits target the @${schemeLayer} variant — set the tab-bar scheme control to Auto to edit base styles"
455
- >
456
- ${mediaDisplayName(schemeLayer)} variant
457
- </span>
458
- `
459
- : nothing;
460
- const toolbarT = html`
461
- <div class="style-toolbar">
462
- <div class="style-toolbar-tabs">${mediaTabsT} ${schemeBadgeT}</div>
463
- ${selectorT}
464
- </div>
465
- `;
906
+ // ── 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
+ const stylebookTag = stylebookTagOf(stylebookSelector);
911
+ const elementLabel =
912
+ stylebookTag ?? (typeof node.tagName === "string" ? node.tagName : "element");
913
+ const targetLineT = renderTargetLine({
914
+ segments: targetSegments(elementLabel, mediaTab, mediaNames.length > 0, schemeLayer),
915
+ selector: {
916
+ value: activeSelector,
917
+ options: selectorOptions,
918
+ declared: declaredSelectors,
919
+ onSelect: (value) => {
920
+ activeTab.value!.session.ui.activeSelector = value;
921
+ },
922
+ onAddCustom: () => {
923
+ // One flow, through `ui/layers.ts` — the imperative `<input>` this replaces was appended to
924
+ // A bar lit-html owns, bypassing both the renderer and the overlay stack (§11.4).
925
+ void showPromptDialog("Add Selector", {
926
+ confirmLabel: "Use",
927
+ message: "A state or nested rule to edit under this element.",
928
+ placeholder: ":hover, .child, &.active, [attr]",
929
+ validate: (v) =>
930
+ isNestedSelector(v.trim()) ? "" : 'A selector must start with ":", ".", "&" or "[".',
931
+ }).then((value) => {
932
+ if (value) {
933
+ activeTab.value!.session.ui.activeSelector = value.trim();
934
+ renderOnly("rightPanel");
935
+ }
936
+ });
937
+ },
938
+ },
939
+ scope: resolveScope(tab, stylebookTag),
940
+ });
466
941
 
467
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).
468
946
  const filterBarT = html`
469
947
  <div class="style-filter-bar">
470
948
  <sp-textfield
@@ -476,85 +954,85 @@ function styleSidebarTemplate(
476
954
  activeTab.value!.session.ui.styleFilter = (e.target as HTMLInputElement).value;
477
955
  }}
478
956
  ></sp-textfield>
479
- <sp-action-button
480
- size="xs"
481
- class="style-filter-toggle"
482
- ?selected=${tab.session.ui.styleFilterActive}
483
- @click=${() => {
484
- activeTab.value!.session.ui.styleFilterActive =
485
- !activeTab.value!.session.ui.styleFilterActive;
486
- }}
487
- >
488
- Active
489
- </sp-action-button>
490
957
  </div>
491
958
  `;
492
959
 
493
960
  // ── Determine the active style object ──────────────────────────────────────
494
- let activeStyle: JxStyle;
495
- let commitMutate: StyleMutateFn;
496
- if (activeSelector && isTagPath(activeSelector) && editMedia) {
497
- const mediaObj = getNestedStyle(style, `@${editMedia}`) ?? {};
498
- activeStyle = resolveNestedTagStyle(mediaObj, activeSelector);
499
- const stylePath = activeSelector.split(" ");
500
- commitMutate = (t: Tab, prop: string, val: string | Record<string, unknown> | undefined) =>
501
- mutateUpdateMediaNestedStylePath(
502
- t,
503
- sel,
504
- editMedia,
505
- stylePath,
506
- prop,
507
- val as string | undefined,
508
- );
509
- } else if (activeSelector && isTagPath(activeSelector)) {
510
- activeStyle = resolveNestedTagStyle(style, activeSelector);
511
- const stylePath = activeSelector.split(" ");
512
- commitMutate = (t: Tab, prop: string, val: string | Record<string, unknown> | undefined) =>
513
- mutateUpdateNestedStylePath(t, sel, stylePath, prop, val as string | undefined);
514
- } else if (activeSelector && editMedia) {
515
- const mediaObj = getNestedStyle(style, `@${editMedia}`) ?? {};
516
- activeStyle = getNestedStyle(mediaObj, activeSelector) ?? {};
517
- commitMutate = (t: Tab, prop: string, val: string | Record<string, unknown> | undefined) =>
518
- mutateUpdateMediaNestedStyle(
519
- t,
520
- sel,
521
- editMedia,
522
- activeSelector,
523
- prop,
524
- val as string | undefined,
525
- );
526
- } else if (activeSelector) {
527
- activeStyle = getNestedStyle(style, activeSelector) ?? {};
528
- commitMutate = (t: Tab, prop: string, val: string | Record<string, unknown> | undefined) =>
529
- mutateUpdateNestedStyle(t, sel, activeSelector, prop, val as string | undefined);
530
- } else {
531
- activeStyle = {};
532
- const inMediaCtx = editMedia !== null;
533
- const flatSource = inMediaCtx ? (getNestedStyle(style, `@${editMedia}`) ?? {}) : style;
534
- for (const [p, v] of Object.entries(flatSource)) {
961
+ const activeStyle = resolveContextStyle(style, activeSelector, editMedia);
962
+ // The SAME coordinate, resolved against the document's own style. In Stylebook `style` is the
963
+ // Site-merged effective style, so a property present here and absent there arrived from the
964
+ // Project's tokens — a donor the panel can now name instead of drawing it as a local override
965
+ // Whose "clear" would silently do nothing.
966
+ const ownStyle =
967
+ style === ownRootStyle
968
+ ? activeStyle
969
+ : resolveContextStyle(ownRootStyle, activeSelector, editMedia);
970
+ // ── The selection, and what it means to commit to it (§6.5) ────────────────
971
+ // Every selected element gets the same write, inside ONE `transactDoc` — so setting padding on
972
+ // Six cards is one decision and one undo step. With one element selected this loop runs once
973
+ // Against `sel` and is indistinguishable from the single-target commit it replaces.
974
+ const targets = tab.session.selection.length > 0 ? tab.session.selection : [sel];
975
+ /**
976
+ * ONE write, applied to every selected element — the only write function this tab has.
977
+ *
978
+ * It takes the caller's transaction rather than opening its own, because a row that writes twice
979
+ * (a shorthand and the longhands it supersedes; a renamed custom property and its old key) must
980
+ * still be one undo step. There is no single-target counterpart: every row that had one wrote to
981
+ * the primary element and silently dropped the rest of the selection on the floor.
982
+ */
983
+ const mutateTargets: StyleMutateFn = (t, prop, val) => {
984
+ for (const target of targets) {
985
+ contextMutate(target, activeSelector, editMedia)(t, prop, val);
986
+ }
987
+ };
988
+ const commitStyle = (prop: string, val?: string | Record<string, unknown> | undefined) =>
989
+ 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
+
996
+ // ── Compute inherited style, and NAME THE DONOR ────────────────────────────
997
+ // Scheme layer: the base styles show through as inherited; breakpoint tabs inherit from lower
998
+ // Breakpoints as before. Either way the donor is known at this point and is now kept: it is the
999
+ // Difference between "some value is showing through" and "Base sets this to 16px, go and look".
1000
+ const inheritedInfo: Record<string, InheritedInfo> = {};
1001
+ if (schemeLayer) {
1002
+ const baseBlock = activeSelector ? (getNestedStyle(style, activeSelector) ?? {}) : style;
1003
+ for (const [p, v] of Object.entries(baseBlock)) {
535
1004
  if (typeof v !== "object") {
536
- activeStyle[p] = v;
1005
+ inheritedInfo[p] = {
1006
+ value: (v as string | number) ?? "",
1007
+ donor: "Base",
1008
+ jump: () => updateUi(activeTab.value, "previewColorScheme", "auto"),
1009
+ };
537
1010
  }
538
1011
  }
539
- commitMutate = inMediaCtx
540
- ? (t: Tab, prop: string, val: string | Record<string, unknown> | undefined) =>
541
- mutateUpdateMediaStyle(t, sel, editMedia, prop, val as string | undefined)
542
- : (t: Tab, prop: string, val: string | Record<string, unknown> | undefined) =>
543
- mutateUpdateStyle(t, sel, prop, val as string | undefined);
1012
+ } else {
1013
+ for (const [p, source] of Object.entries(
1014
+ computeInheritedSources(style, mediaNames, mediaTab, activeSelector),
1015
+ )) {
1016
+ inheritedInfo[p] = {
1017
+ value: source.value,
1018
+ donor: source.donor === null ? "Base" : mediaDisplayName(source.donor),
1019
+ jump: () => updateUi(activeTab.value, "activeMedia", source.donor),
1020
+ };
1021
+ }
544
1022
  }
545
- const commitStyle = (prop: string, val?: string | Record<string, unknown> | undefined) =>
546
- transactDoc(activeTab.value, (t) => commitMutate(t, prop, val));
547
-
548
- // ── Compute inherited style ────────────────────────────────────────────────
549
- // Scheme layer: the base styles show through as inherited (placeholders); breakpoint tabs
550
- // Inherit from higher breakpoints as before.
551
- const inheritedStyle: Record<string, string | number> = schemeLayer
552
- ? (Object.fromEntries(
553
- Object.entries(
554
- activeSelector ? (getNestedStyle(style, activeSelector) ?? {}) : style,
555
- ).filter(([, v]) => typeof v !== "object"),
556
- ) as Record<string, string | number>)
557
- : computeInheritedStyle(style, mediaNames, mediaTab, activeSelector);
1023
+ const inheritedStyle: Record<string, string | number> = {};
1024
+ for (const [p, info] of Object.entries(inheritedInfo)) {
1025
+ inheritedStyle[p] = info.value;
1026
+ }
1027
+ const mixedProps = mixedStyleProps(tab, targets, activeSelector, editMedia);
1028
+ const provCtx: ProvenanceCtx = {
1029
+ own: ownStyle,
1030
+ active: activeStyle,
1031
+ inherited: inheritedInfo,
1032
+ signals: templateSignals,
1033
+ clear: (prop: string) => commitStyle(prop),
1034
+ ...(mixedProps.size > 0 ? { mixed: { count: targets.length, props: mixedProps } } : {}),
1035
+ };
558
1036
 
559
1037
  // Auto-open sections that have properties
560
1038
  const newSections = autoOpenSections({ style: activeStyle }, tab.session.ui.styleSections);
@@ -606,8 +1084,7 @@ function styleSidebarTemplate(
606
1084
 
607
1085
  // ── Filter state ─────────────────────────────────────────────────────────
608
1086
  const filterText = (tab.session.ui.styleFilter || "").toLowerCase();
609
- const filterActive = tab.session.ui.styleFilterActive;
610
- const isFiltering = filterText.length > 0 || filterActive;
1087
+ const isFiltering = filterText.length > 0;
611
1088
 
612
1089
  // ── Section templates ────────────────────────────────────────────────────
613
1090
  const sectionTemplates = cssMeta.$sections
@@ -629,6 +1106,34 @@ function styleSidebarTemplate(
629
1106
  },
630
1107
  );
631
1108
 
1109
+ const clearSection = () => {
1110
+ 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
+ }
1119
+ }
1120
+ }
1121
+ });
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
+
632
1137
  const isOpen = isFiltering ? true : (tab.session.ui.styleSections[sec.key] ?? false);
633
1138
 
634
1139
  if (!isOpen) {
@@ -643,37 +1148,7 @@ function styleSidebarTemplate(
643
1148
  };
644
1149
  }}
645
1150
  >
646
- ${
647
- sectionActiveProps.length > 0
648
- ? html`
649
- <span slot="heading" style="display:flex;align-items:center;gap:6px">
650
- ${sec.label}
651
- <span
652
- class="set-dot set-dot--section"
653
- title="Clear all ${sec.label.toLowerCase()} properties"
654
- @click=${(e: Event) => {
655
- e.stopPropagation();
656
- e.preventDefault();
657
- transactDoc(activeTab.value, (t) => {
658
- for (const { prop, entry } of sectionActiveProps) {
659
- if (activeStyle[prop] !== undefined) {
660
- commitMutate(t, prop);
661
- }
662
- if (inferInputType(entry) === "shorthand") {
663
- for (const l of getLonghands(prop) as CssLonghand[]) {
664
- if (activeStyle[l.name] !== undefined) {
665
- commitMutate(t, l.name);
666
- }
667
- }
668
- }
669
- }
670
- });
671
- }}
672
- ></span>
673
- </span>
674
- `
675
- : nothing
676
- }
1151
+ ${headingT}
677
1152
  </sp-accordion-item>
678
1153
  `;
679
1154
  }
@@ -694,18 +1169,6 @@ function styleSidebarTemplate(
694
1169
  continue;
695
1170
  }
696
1171
  }
697
- if (filterActive) {
698
- if (type === "shorthand") {
699
- const longhands = getLonghands(prop) as CssLonghand[];
700
- const hasAnySet =
701
- hasVal || longhands.some((l: CssLonghand) => activeStyle[l.name] !== undefined);
702
- if (!hasAnySet) {
703
- continue;
704
- }
705
- } else if (!hasVal) {
706
- continue;
707
- }
708
- }
709
1172
 
710
1173
  if (type === "shorthand") {
711
1174
  const longhands = getLonghands(prop) as CssLonghand[];
@@ -715,7 +1178,14 @@ function styleSidebarTemplate(
715
1178
  continue;
716
1179
  }
717
1180
  rows.push(
718
- renderShorthandRow(prop, entry, activeStyle, commitMutate, () => {}, inheritedStyle),
1181
+ renderShorthandRow({
1182
+ prop,
1183
+ entry,
1184
+ style: activeStyle,
1185
+ mutate: mutateTargets,
1186
+ inherited: inheritedStyle,
1187
+ ctx: provCtx,
1188
+ }),
719
1189
  );
720
1190
  } else {
721
1191
  const isWarning = hasVal && !condMet;
@@ -726,12 +1196,12 @@ function styleSidebarTemplate(
726
1196
  prop,
727
1197
  (val as string) ?? "",
728
1198
  (newVal: string | undefined) => commitStyle(prop, newVal || undefined),
729
- () => commitStyle(prop),
730
1199
  isWarning,
731
1200
  sec.$layout === "grid",
732
1201
  inheritedStyle[prop] as string | undefined,
733
1202
  templateSignals,
734
1203
  `style|${sel.join("/")}|${editMedia ?? ""}|${activeSelector ?? ""}|${prop}`,
1204
+ provenanceOf(prop, provCtx),
735
1205
  ),
736
1206
  );
737
1207
  }
@@ -753,37 +1223,7 @@ function styleSidebarTemplate(
753
1223
  };
754
1224
  }}
755
1225
  >
756
- ${
757
- sectionActiveProps.length > 0
758
- ? html`
759
- <span slot="heading" style="display:flex;align-items:center;gap:6px">
760
- ${sec.label}
761
- <span
762
- class="set-dot set-dot--section"
763
- title="Clear all ${sec.label.toLowerCase()} properties"
764
- @click=${(e: Event) => {
765
- e.stopPropagation();
766
- e.preventDefault();
767
- transactDoc(activeTab.value, (t) => {
768
- for (const { prop, entry } of sectionActiveProps) {
769
- if (activeStyle[prop] !== undefined) {
770
- commitMutate(t, prop);
771
- }
772
- if (inferInputType(entry) === "shorthand") {
773
- for (const l of getLonghands(prop) as CssLonghand[]) {
774
- if (activeStyle[l.name] !== undefined) {
775
- commitMutate(t, l.name);
776
- }
777
- }
778
- }
779
- }
780
- });
781
- }}
782
- ></span>
783
- </span>
784
- `
785
- : nothing
786
- }
1226
+ ${headingT}
787
1227
  <div class=${sec.$layout === "grid" ? "style-section-body--grid" : ""}>${rows}</div>
788
1228
  </sp-accordion-item>
789
1229
  `;
@@ -813,12 +1253,26 @@ function styleSidebarTemplate(
813
1253
  .value=${live(prop)}
814
1254
  @change=${(e: Event) => {
815
1255
  const newProp = (e.target as HTMLInputElement).value.trim();
816
- if (newProp && newProp !== prop) {
817
- transactDoc(activeTab.value, (t) => {
818
- commitMutate(t, prop);
819
- commitMutate(t, newProp, String(activeStyle[prop]));
820
- });
1256
+ if (!newProp || newProp === prop) {
1257
+ return;
821
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
+ });
822
1276
  }}
823
1277
  ></sp-textfield>
824
1278
  <sp-textfield
@@ -917,7 +1371,7 @@ function styleSidebarTemplate(
917
1371
 
918
1372
  return html`
919
1373
  <div class="style-sidebar">
920
- ${toolbarT} ${filterBarT}
1374
+ ${targetLineT} ${filterBarT}
921
1375
  <sp-accordion allow-multiple size="s">
922
1376
  ${sectionTemplates} ${nestedSectionT} ${customSectionT}
923
1377
  </sp-accordion>
@@ -935,71 +1389,128 @@ function styleSidebarTemplate(
935
1389
  */
936
1390
  export function renderStylePanelTemplate(ctx: { getCanvasMode: () => string }) {
937
1391
  const tab = activeTab.value;
1392
+ const noDocument = () =>
1393
+ renderEmptyState({
1394
+ actions: [openPageAction()],
1395
+ message: "Open a page to style what you click.",
1396
+ });
938
1397
  if (!tab) {
939
- return html`<div class="empty-state">No document loaded</div>`;
1398
+ return noDocument();
940
1399
  }
941
- if (ctx.getCanvasMode() === "stylebook" && tab.session.ui.stylebookSelection) {
1400
+ if (ctx.getCanvasMode() === "stylebook" && shell.stylebook.selection) {
942
1401
  const node = tab.doc.document;
943
1402
  if (!node) {
944
- return html`<div class="empty-state">No document loaded</div>`;
1403
+ return noDocument();
945
1404
  }
946
- return html`
947
- <div class="stylebook-style-header">
948
- Styling: &lt;${tab.session.ui.stylebookSelection}&gt;
949
- </div>
950
- ${styleSidebarTemplate(
951
- node,
952
- tab.session.ui.activeMedia,
953
- tab.session.ui.activeSelector,
954
- getEffectiveStyle(node.style),
955
- )}
956
- `;
957
- }
958
- if (!tab.session.selection) {
959
- return html`<div class="empty-state">Select an element to style</div>`;
960
- }
961
- const node = getNodeAtPath(tab.doc.document, tab.session.selection);
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,
1410
+ });
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);
962
1417
  if (!node) {
963
- return html`<div class="empty-state">Select an element to style</div>`;
1418
+ return renderEmptyState({ message: staleSelectionMessage() });
964
1419
  }
965
1420
  return styleSidebarTemplate(node, tab.session.ui.activeMedia, tab.session.ui.activeSelector);
966
1421
  }
967
1422
 
968
- /** Single property input row (generic field row helper) */
969
- export function _fieldRow(
970
- label: string,
971
- type: string,
972
- value: string,
973
- onChange: (v: string | boolean) => void,
974
- _datalistId: string | undefined,
975
- ) {
976
- let debounceTimer: ReturnType<typeof setTimeout> | undefined;
977
- const onInput = (e: Event) => {
978
- clearTimeout(debounceTimer);
979
- debounceTimer = setTimeout(() => onChange((e.target as HTMLInputElement).value), 400);
1423
+ // ─── Commands ─────────────────────────────────────────────────────────────────
1424
+
1425
+ /**
1426
+ * The Style tab's selector verbs.
1427
+ *
1428
+ * `style.openSelectorMenu` is the one command here that addresses a CONTROL rather than a state,
1429
+ * and it earns that on purpose: the shot it serves photographs the open menu, so the menu is the
1430
+ * 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.
1434
+ *
1435
+ * `style.setSelector` is the state half of the same idea, and it is what a caller that wants a
1436
+ * RESULT rather than a picture should use: the menu exists to choose a selector, and choosing one
1437
+ * is expressible.
1438
+ *
1439
+ * @returns {AnyCommand[]}
1440
+ */
1441
+ export function styleCommands(): AnyCommand[] {
1442
+ const styleTabOpen = (ctx: { document: { open: boolean }; selection: { count: number } }) =>
1443
+ ctx.document.open && ctx.selection.count > 0;
1444
+
1445
+ return [
1446
+ {
1447
+ category: "View",
1448
+ id: "style.openSelectorMenu",
1449
+ level: "selection",
1450
+ menus: ["palette"],
1451
+ group: "8_style",
1452
+ requires: "the Style tab showing a selected element",
1453
+ when: styleTabOpen,
1454
+ run: () => openSelectorMenu(),
1455
+ title: "Open Selector Menu",
1456
+ },
1457
+ {
1458
+ args: {
1459
+ additionalProperties: false,
1460
+ properties: {
1461
+ selector: pathlessSelectorProperty(),
1462
+ },
1463
+ required: ["selector"],
1464
+ type: "object",
1465
+ },
1466
+ category: "View",
1467
+ id: "style.setSelector",
1468
+ level: "selection",
1469
+ menus: ["palette"],
1470
+ group: "8_style",
1471
+ requires: "an element selection",
1472
+ when: styleTabOpen,
1473
+ run: (_commandCtx, args) => {
1474
+ const { selector } = args as { selector?: unknown };
1475
+ if (selector === null) {
1476
+ const tab = activeTab.value;
1477
+ if (tab) {
1478
+ tab.session.ui.activeSelector = null;
1479
+ }
1480
+ return;
1481
+ }
1482
+ const value = stringArg("style.setSelector", args, "selector");
1483
+ if (!isNestedSelector(value)) {
1484
+ throw new RangeError(
1485
+ `command "style.setSelector" argument "selector": "${value}" is not a nested ` +
1486
+ `selector — it must start with ":", ".", "&" or "["`,
1487
+ );
1488
+ }
1489
+ const tab = activeTab.value;
1490
+ if (!tab) {
1491
+ throw new RangeError(`command "style.setSelector" needs an open document`);
1492
+ }
1493
+ tab.session.ui.activeSelector = value;
1494
+ },
1495
+ title: "Set Style Selector",
1496
+ },
1497
+ ];
1498
+ }
1499
+
1500
+ /** The `selector` property's schema: a nested selector, or `null` for the base context. */
1501
+ function pathlessSelectorProperty(): object {
1502
+ return {
1503
+ description:
1504
+ 'A nested selector such as ":hover", ".child" or "[open]". null edits the base context.',
1505
+ oneOf: [{ type: "string" }, { type: "null" }],
980
1506
  };
981
- const inputTpl =
982
- type === "textarea"
983
- ? html`<sp-textfield
984
- multiline
985
- size="s"
986
- .value=${live(value ?? "")}
987
- @input=${onInput}
988
- ></sp-textfield>`
989
- : type === "checkbox"
990
- ? html`<sp-checkbox
991
- ?checked=${Boolean(value)}
992
- @change=${(e: Event) => onChange((e.target as HTMLInputElement).checked)}
993
- ></sp-checkbox>`
994
- : html`<sp-textfield
995
- size="s"
996
- .value=${live(value ?? "")}
997
- @input=${onInput}
998
- ></sp-textfield>`;
999
- return html`
1000
- <div class="field-row">
1001
- <sp-field-label size="s">${label}</sp-field-label>
1002
- ${inputTpl}
1003
- </div>
1004
- `;
1507
+ }
1508
+
1509
+ /**
1510
+ * Register the Style tab's selector verbs.
1511
+ *
1512
+ * @param {CommandRegistry} registry
1513
+ */
1514
+ export function registerStyleCommands(registry: CommandRegistry): void {
1515
+ registry.registerAll(styleCommands());
1005
1516
  }