@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
@@ -0,0 +1,1477 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * The pane's own chrome — region ⑦ (context bar) and region ⑩ (the floating zoom pod).
4
+ *
5
+ * This replaces `#tab-bar`, which was a 28px band styled identically to the tab strip above it,
6
+ * holding **five unrelated axes** with no labels on any of them: a document-stack breadcrumb
7
+ * (navigation), a zoom widget (viewport), route params and component test props (document data), a
8
+ * colour-scheme switch and feature toggles (rendering state), and Export (a mode action). Plan §3.2
9
+ * ⑦ replaces them with three axes that each say what they are:
10
+ *
11
+ * - **Editor kind** — `Canvas ⌄`, offering only the kinds this document declares AND its pane can
12
+ * host ({@link hostableKindsOf}), so the control can never contain a permanently dead entry. A
13
+ * document with one kind renders the name as text rather than as a dropdown that cannot go
14
+ * anywhere.
15
+ * - **Canvas view** — `Edit │ Design` as a radio ({@link canvasBaseViewsFor}) with a **Preview**
16
+ * toggle beside it ({@link previewStateOf}), and none at all in a pane that may not host the
17
+ * Canvas. The two are drawn apart because they are two axes: preview is a flag over an edit or
18
+ * design base, so the radio marks the base throughout and the toggle says whether it is on.
19
+ * - **Rendering context** — `md ⌄ Light ⌄`, folding the size breakpoint, the colour scheme, the
20
+ * feature queries and the layout show/hide switch into one popover. Per §2 principle 5 this
21
+ * control **only selects**; its footer is "Manage contexts…", which routes to the definition site
22
+ * — Project Settings › Contexts (`settings/contexts-section.ts`), the one place a breakpoint, a
23
+ * colour scheme or a feature query is defined.
24
+ * - **Resolving with** — the document DATA a render resolves against: a page's route params, a
25
+ * component's test props. Its own popover beside the context one, headed "resolving with", with
26
+ * the fields in a vertical stack. A SECOND popover rather than a fourth group in the first,
27
+ * because these are values you type and everything in the rendering-context popover is something
28
+ * you pick — and because a row of text fields is what made the 28px band unreadable.
29
+ *
30
+ * **The bar is not a grid row.** It renders inside the pane's own cell (`#pane-chrome`, stacked
31
+ * over `#canvas-wrap`), because a per-pane surface cannot be a row of the application grid — the
32
+ * second pane would have no way to have one. The stage is offset by {@link PANE_CONTEXT_VAR} rather
33
+ * than by a track, and the zoom pod floats bottom-right over the canvas exactly as §3.2 ⑩ asks.
34
+ *
35
+ * **The read-only banner rides with the bar.** `collab/presence-chips.ts` wrote the sentence a
36
+ * read-only guest needs before their first keystroke (§7.4); the surface that owes it a home is
37
+ * this one, because it is the per-pane, per-document chrome that sits directly above the editing
38
+ * surface. It is stacked under the bar inside `.pc-band` and the offset is MEASURED from that band
39
+ * ({@link applyPaneContextOffset}), so a two-line banner pushes the stage down instead of covering
40
+ * the document it is warning you about.
41
+ *
42
+ * Module shape follows `tab-strip.ts`: mount(host, ctx) → effectScope/effect → render().
43
+ */
44
+
45
+ import { html, render as litRender, nothing } from "lit-html";
46
+ import { ref } from "lit-html/directives/ref.js";
47
+ import { projectState, updateUi } from "../store";
48
+ import { effect, effectScope } from "../reactivity";
49
+ import { PRIMARY_PANE, focusPane, workspace } from "../workspace/workspace";
50
+ import {
51
+ activeMediaOfPane,
52
+ canvasModeOfPane,
53
+ canvasModeOfTab,
54
+ derivationOfPane,
55
+ surfaceForPane,
56
+ tabOfPane,
57
+ } from "../canvas/canvas-surface";
58
+ import {
59
+ DERIVE_PRESETS,
60
+ PRESET_LABELS,
61
+ declaredMedia,
62
+ deriveRefusal,
63
+ pinRefusal,
64
+ presetRefusal,
65
+ } from "../workspace/pane-derive";
66
+ import { renderPopover } from "../ui/layers";
67
+ import { rectOf } from "../utils/geometry";
68
+ import { paneRegion } from "../ui/regions";
69
+ import {
70
+ fitToScreen,
71
+ getFit,
72
+ resetZoom,
73
+ canvasBaseViewOf,
74
+ canvasBaseViewsFor,
75
+ previewStateOf,
76
+ setCanvasView,
77
+ setEditZoom,
78
+ setFit,
79
+ setUserZoom,
80
+ stageZoom,
81
+ } from "../canvas/canvas-utils";
82
+ import { editorKindOf, editorKindsOf, modeForEditorKind } from "../tabs/tab";
83
+ import { collabState } from "../collab/collab-state";
84
+ import { readOnlyBannerTemplate } from "../collab/presence-chips";
85
+ import { activeRegistry } from "../commands/active-registry";
86
+ import { getEffectiveLayoutPath, getEffectiveLocales, getEffectiveMedia } from "../site-context";
87
+ import { localeLabel, localeOfPath } from "@jxsuite/schema/locale";
88
+ import { isSchemeQuery } from "../utils/canvas-media";
89
+ import { dynamicRouteParams, loadParamValues, pagePathsDef } from "../page-params";
90
+ import { componentPropEntries, isComponentDoc } from "../component-props";
91
+ import { mediaDisplayName } from "./shared";
92
+ import type { CanvasView, FitMode } from "../canvas/canvas-utils";
93
+ import { EDITOR_KIND_LABELS } from "../commands/context";
94
+ import type { EditorKind } from "../commands/context";
95
+ import type { ParamValues } from "../page-params";
96
+ import type { Tab } from "../tabs/tab";
97
+ import type { ResolvedI18n } from "@jxsuite/schema/locale";
98
+ import type { JsonValue } from "../types";
99
+ import type { EffectScope } from "@vue/reactivity";
100
+ import type { TemplateResult } from "lit-html";
101
+
102
+ /**
103
+ * The CSS variable the stage is offset by while a context bar is on screen.
104
+ *
105
+ * The bar overlays the pane cell, so something has to keep the canvas out from under it. A variable
106
+ * written by one projection is the same shape `applyDockLayout()` uses for the dock widths — and it
107
+ * means "no tab open" costs the welcome screen no dead band.
108
+ */
109
+ export const PANE_CONTEXT_VAR = "--pane-context-h";
110
+
111
+ /** The bar's height. Declared here because the offset projection and the stylesheet must agree. */
112
+ const PANE_CONTEXT_HEIGHT = 28;
113
+
114
+ export interface PaneContextCtx {
115
+ exportFile: () => void;
116
+ /*
117
+ * There is no `getCanvasMode` here, and there cannot be one.
118
+ *
119
+ * It answered for the FOCUSED pane — `studio.ts` composes it from `workspace.activePaneId` — and
120
+ * this bar is drawn once per pane, from `tabOfPane(paneId)`. One reader was left: `exportTpl`,
121
+ * which is why entering Code in EITHER pane put an Export button in BOTH bars. Every mode
122
+ * question this module asks is now asked of the pane it is drawing: `canvasModeOfTab(tab)` for
123
+ * the tab's own effective mode, `canvasModeOfPane(paneId)` for the stage's.
124
+ */
125
+ /**
126
+ * Write the BASE mode of the tab it is GIVEN. The editor-kind dropdown and the view control both
127
+ * land here, and both are drawn per pane — so the tab is a parameter. It used to be `(mode:
128
+ * string) => void`, resolving `activeTab.value` inside `studio.ts`, which made the side pane's
129
+ * Editor picker a control over the primary's document.
130
+ */
131
+ setCanvasMode: (tab: Tab, mode: string) => void;
132
+ parseMediaEntries: (media: Record<string, string> | null | undefined) => {
133
+ sizeBreakpoints: {
134
+ name: string;
135
+ query: string;
136
+ width: number;
137
+ type: string;
138
+ }[];
139
+ featureQueries: { name: string; query: string }[];
140
+ baseWidth: number;
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Where each pane's chrome renders. One entry per drawn cell.
146
+ *
147
+ * A Map rather than a `let _host`, for the reason the whole grid exists: this bar states one pane's
148
+ * editor kind, canvas view, rendering context and zoom, and two panes have two of each.
149
+ * `panels/pane-grid.ts` attaches a cell's `.pane-chrome` as the cell is built.
150
+ */
151
+ const _hosts = new Map<string, HTMLElement>();
152
+
153
+ let _ctx: PaneContextCtx | null = null;
154
+
155
+ let _scope: EffectScope | null = null;
156
+
157
+ /** Human names for the three Canvas views. */
158
+ const CANVAS_VIEW_LABELS: Readonly<Record<CanvasView, string>> = {
159
+ design: "Design",
160
+ edit: "Edit",
161
+ preview: "Preview",
162
+ };
163
+
164
+ /** The colour-scheme segment's values, labels and tooltips. */
165
+ const SCHEMES = [
166
+ ["auto", "Auto", "Follow the OS color scheme"],
167
+ ["light", "Light", "Force the light scheme"],
168
+ ["dark", "Dark", "Force the dark scheme"],
169
+ ] as const;
170
+
171
+ /**
172
+ * Give a pane's chrome somewhere to paint, or take it away.
173
+ *
174
+ * Called by `panels/pane-grid.ts` as a cell is built and as it is disposed.
175
+ *
176
+ * @param {string} paneId
177
+ * @param {HTMLElement | null} host
178
+ */
179
+ export function attachPaneChromeHost(paneId: string, host: HTMLElement | null): void {
180
+ const previous = _hosts.get(paneId);
181
+ if (previous === host) {
182
+ return;
183
+ }
184
+ if (previous) {
185
+ litRender(nothing, previous);
186
+ applyPaneContextOffset(0, previous);
187
+ }
188
+ if (host) {
189
+ _hosts.set(paneId, host);
190
+ renderPane(paneId, host);
191
+ } else {
192
+ _hosts.delete(paneId);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Mount the pane chrome. Idempotent.
198
+ *
199
+ * `host` is the PRIMARY pane's, the same bargain `panels/tab-strip.ts`'s `mount` makes: the
200
+ * bootstrap holds the primary's cell, and every other pane's host arrives from the grid through
201
+ * {@link attachPaneChromeHost}.
202
+ *
203
+ * @param {HTMLElement} host
204
+ * @param {PaneContextCtx} ctx
205
+ */
206
+ export function mount(host: HTMLElement, ctx: PaneContextCtx) {
207
+ _ctx = ctx;
208
+ attachPaneChromeHost(PRIMARY_PANE, host);
209
+ _scope = effectScope();
210
+ _scope.run(() => {
211
+ effect(() => {
212
+ /* EVERY pane's tab. The bar states its own pane's editor kind, canvas view, rendering
213
+ context and zoom, so the unfocused one has to repaint when ITS document moves — tracking
214
+ `activeTab` alone left the side pane's bar frozen describing whatever it last drew. */
215
+ for (const pane of workspace.panes) {
216
+ /* NO DERIVATION READS HERE, and the reason is one rule rather than a tally. `render()`
217
+ runs inside this effect and draws EVERY attached pane, so whatever a template here reads
218
+ is ALREADY a dependency and restating it as a `void` line chooses nothing: `derived.kind`
219
+ picks the bar's shape, `activeMediaOfPane` reads `kind`/`preset`/`media` for the Context
220
+ axis, `zoomOf` reads `derived.zoom` for the pod, and {@link zoomPodTpl} asks
221
+ `canvasModeOfPane`, which for a lens answers `derived.mode` and decides whether the pod
222
+ is drawn at all — pinned by lens-chrome's "the pod is drawn from the LENS's mode".
223
+ An earlier version of this comment listed `mode` among the fields no template reads,
224
+ which was wrong about the fact and right about the deletion. `status` and `reason` really
225
+ are unread here, so tracking them only repainted this bar for a change it does not draw.
226
+ The stage is the surface that needs those two declared, because `renderCanvasImpl` runs
227
+ in a rAF rather than in an effect — see `studio.ts`. */
228
+ const tab = tabOfPane(pane.id);
229
+ if (!tab) {
230
+ continue;
231
+ }
232
+ // Read reactive properties to establish tracking — mirrors the toolbar's subset.
233
+ void tab.doc.document;
234
+ void tab.doc.document?.$layout;
235
+ void tab.doc.mode;
236
+ void tab.documentPath;
237
+ void tab.capabilities.modes;
238
+ void tab.session.ui.activeMedia;
239
+ void tab.session.ui.canvasMode;
240
+ void tab.session.ui.editZoom;
241
+ void tab.session.ui.featureToggles;
242
+ void tab.session.ui.preview;
243
+ void tab.session.ui.previewColorScheme;
244
+ void tab.session.ui.previewLocale;
245
+ void tab.session.ui.previewParams;
246
+ void tab.session.ui.previewProps;
247
+ void tab.session.ui.showLayout;
248
+ void tab.session.ui.zoom;
249
+ // The read-only banner (§7.4) is part of this chrome, so its two facts are tracked here
250
+ // Too: a peer downgrading you mid-session must make the sentence appear, not wait for the
251
+ // Next zoom.
252
+ void collabState(tab).active;
253
+ void collabState(tab).readOnly;
254
+ }
255
+ render();
256
+ });
257
+ });
258
+ }
259
+
260
+ export function unmount() {
261
+ _scope?.stop();
262
+ _scope = null;
263
+ for (const host of _hosts.values()) {
264
+ applyPaneContextOffset(0, host);
265
+ }
266
+ _hosts.clear();
267
+ _ctx = null;
268
+ applyPaneContextOffset(0);
269
+ }
270
+
271
+ /**
272
+ * Keep the stage clear of the pane's top band.
273
+ *
274
+ * One projection, one variable — the canvas is offset only by what is actually rendered, so the
275
+ * welcome screen (no tab, no bar) does not open under a 28px gap nothing explains, and a read-only
276
+ * banner does not sit on top of the document it is warning you about.
277
+ *
278
+ * **On the PANE, not on `:root`**, for the reason `panels/jump-bar.ts` gives at the same seam: two
279
+ * cells have two bands of different heights, and a single document-level number offsets both stages
280
+ * by whichever pane painted last. A host outside a cell writes the root, which is what it meant
281
+ * when the shell had one bar.
282
+ *
283
+ * @param {number} height Band height in px. `0` when the pane has no chrome.
284
+ * @param {HTMLElement | null} [host] The bar's host. Its cell takes the variable when it has one.
285
+ */
286
+ export function applyPaneContextOffset(height: number, host?: HTMLElement | null): void {
287
+ const target = host?.closest<HTMLElement>(".pane") ?? document.documentElement;
288
+ target.style.setProperty(PANE_CONTEXT_VAR, `${height}px`);
289
+ }
290
+
291
+ /**
292
+ * How tall the top band came out — the bar plus whatever banners rode with it.
293
+ *
294
+ * MEASURED rather than summed, because the banner's height is its wrapped text and only layout
295
+ * knows how many lines that is. `offsetHeight` is 0 in a DOM with no layout engine (every unit
296
+ * test), so the bar's declared height is the floor: the offset is then exactly what it was before
297
+ * banners existed, which is the honest answer when nothing has been laid out.
298
+ */
299
+ function topBandHeight(host: HTMLElement): number {
300
+ const band = host.querySelector<HTMLElement>(".pc-band");
301
+ return Math.max(band?.offsetHeight ?? 0, PANE_CONTEXT_HEIGHT);
302
+ }
303
+
304
+ /**
305
+ * Whether this tab's editor has any of the bar's three axes to offer.
306
+ *
307
+ * Project Settings has none: it declares one editor kind, no canvas view, and no rendering context
308
+ * — and the one control the bar WOULD contribute, "Manage contexts…", routes to a section of the
309
+ * very document on screen. A bar of three inert controls above the definition site they point at is
310
+ * chrome that says nothing, so the stage takes the whole pane.
311
+ *
312
+ * @param {Tab} tab
313
+ * @returns {boolean}
314
+ */
315
+ function wantsContextBar(tab: Tab): boolean {
316
+ return tab.session.ui.canvasMode !== "settings";
317
+ }
318
+
319
+ /** Paint every attached pane's chrome. */
320
+ export function render() {
321
+ for (const [paneId, host] of _hosts) {
322
+ renderPane(paneId, host);
323
+ }
324
+ }
325
+
326
+ /** Paint one pane's chrome into its own host, from its own tab. */
327
+ function renderPane(paneId: string, host: HTMLElement) {
328
+ if (!_ctx) {
329
+ return;
330
+ }
331
+ try {
332
+ const tab = tabOfPane(paneId);
333
+ const show = Boolean(tab) && wantsContextBar(tab as Tab);
334
+ litRender(show ? paneChromeTemplate(tab as Tab, paneId, _ctx) : nothing, host);
335
+ applyPaneContextOffset(show ? topBandHeight(host) : 0, host);
336
+ } catch (error) {
337
+ console.error("pane-context render error:", error);
338
+ }
339
+ }
340
+
341
+ // ─── The preset menu · the first renderer of `context/pane` ──────────────────
342
+
343
+ /** The open preset menu, if any. One at a time, like every other menu in this shell. */
344
+ let _presetMenu: { dismiss: () => void } | null = null;
345
+
346
+ /** Close the preset menu, if it is open. Idempotent. */
347
+ export function dismissPresetMenu(): void {
348
+ _presetMenu?.dismiss();
349
+ _presetMenu = null;
350
+ }
351
+
352
+ /**
353
+ * The `⟲` trigger, in the context bar's leading slot — the one that was empty.
354
+ *
355
+ * `.pc-spacer` existed to push the three axes right. It is where §18.4's preset menu goes because
356
+ * the menu is about THIS PANE and the bar is the pane's own chrome; the alternative homes (the tab
357
+ * strip, the jump bar) are about a document and an address respectively.
358
+ */
359
+ function presetTriggerTpl(paneId: string): TemplateResult {
360
+ return html`<button
361
+ class="pc-derive-trigger"
362
+ aria-haspopup="menu"
363
+ title="Show something beside this pane"
364
+ @click=${(event: MouseEvent) => {
365
+ openPresetMenu(event, paneId);
366
+ }}
367
+ >
368
+ <span aria-hidden="true">⟲</span>
369
+ </button>`;
370
+ }
371
+
372
+ /**
373
+ * Open the preset menu.
374
+ *
375
+ * **There is no `pane.showDerivePresets`.** §13.5, quoted verbatim in `canvas/canvas-render.ts`:
376
+ * opening a menu to press an item names a CONTROL; the item is the command. Every row here runs
377
+ * `pane.derive`, `pane.pin` or `pane.unsplit` with its own arguments, and the screenshot manifest
378
+ * addresses those ids directly rather than driving this widget. Its region is
379
+ * `overlay.menu:derive-presets`, which `ui/layers.ts` derives from the slot key — no budget is
380
+ * spent.
381
+ */
382
+ function openPresetMenu(event: MouseEvent, paneId: string): void {
383
+ event.preventDefault();
384
+ event.stopPropagation();
385
+ dismissPresetMenu();
386
+ const registry = activeRegistry();
387
+ if (!registry) {
388
+ return;
389
+ }
390
+ const anchor = rectOf(event.currentTarget as HTMLElement);
391
+ const left = Math.round(Math.min(anchor.left, window.innerWidth - 4));
392
+ const top = Math.round(anchor.bottom);
393
+ const rows = presetRows(paneId);
394
+ _presetMenu = renderPopover(
395
+ html`<sp-popover open style="position:fixed;z-index:10000;left:${left}px;top:${top}px">
396
+ <sp-menu role="menu" aria-label="Show beside this pane">
397
+ ${rows.map(
398
+ (row) => html`<sp-menu-item
399
+ role="menuitem"
400
+ ?disabled=${row.disabled !== null}
401
+ title=${row.disabled ? `requires ${row.disabled}` : row.label}
402
+ @click=${() => {
403
+ dismissPresetMenu();
404
+ if (row.disabled === null) {
405
+ /* THE PANE THE MENU IS ABOUT, before the verb that resolves the focus runs.
406
+ See {@link PresetRow.pane}: all three of these commands take the focused pane as
407
+ their subject, and a pointer gesture on this pane has already focused it — but a
408
+ keyboard activation has not, because `panels/pane-grid.ts` focuses on
409
+ pointerdown. Without this line the secondary pane's menu derived from the
410
+ primary and its Unsplit closed the wrong pane, by keyboard only. */
411
+ focusPane(row.pane);
412
+ void registry.run(row.command, row.args);
413
+ }
414
+ }}
415
+ >${row.label}</sp-menu-item
416
+ >`,
417
+ )}
418
+ </sp-menu>
419
+ </sp-popover>`,
420
+ {
421
+ dismissOnOutsideClick: true,
422
+ onDismiss: () => {
423
+ _presetMenu = null;
424
+ },
425
+ region: "derive-presets",
426
+ },
427
+ );
428
+ }
429
+
430
+ /** One row of the preset menu: a command, its arguments, and why it cannot run when it cannot. */
431
+ export interface PresetRow {
432
+ label: string;
433
+ command: string;
434
+ args: Record<string, unknown>;
435
+ /**
436
+ * The pane this row is ABOUT — the one whose ⟲ opened the menu.
437
+ *
438
+ * The previous round made the row's ANSWER a pure function of the pane and left its ACTION
439
+ * resolving the focus: every one of these three commands takes the focused pane as its subject
440
+ * (`pane.derive`'s `activePane()`/`sidePane()`, `pane.unsplit`'s `workspace.activePaneId`), so a
441
+ * row read off the secondary pane's menu derived from the primary. Reachable by keyboard and only
442
+ * by keyboard, which is why it survived a browser pass: `panels/pane-grid.ts` focuses a pane on
443
+ * POINTERDOWN, and a keyboard activation of a menu item fires `click` alone.
444
+ *
445
+ * Carried on the row rather than passed as a command argument. A pane id in `args` would make
446
+ * "which pane" a public input of `pane.derive` — a third property on a record the palette already
447
+ * cannot prompt for, and a value the AI tool would have to invent — to say something the focus
448
+ * already says. The menu moves the focus to the pane it is a menu of, which is what a pointer
449
+ * gesture on that pane does anyway, and all three verbs then agree with the row.
450
+ */
451
+ pane: string;
452
+ /** The `requires` sentence, or null when the row can run. */
453
+ disabled: string | null;
454
+ }
455
+
456
+ /**
457
+ * The rows, built from the registry and from {@link presetRefusal} — one per projection, one per
458
+ * declared breakpoint, then the two exits.
459
+ *
460
+ * Exported because it is the whole content of the menu and it is a pure function of the pane: the
461
+ * widget is untestable in a DOM with no layout, and this is not.
462
+ *
463
+ * @param {string} paneId
464
+ * @returns {PresetRow[]}
465
+ */
466
+ export function presetRows(paneId: string): PresetRow[] {
467
+ const registry = activeRegistry();
468
+ const tab = tabOfPane(paneId);
469
+ const derived = derivationOfPane(paneId);
470
+ /* {@link deriveRefusal}, not `registry.disabledReason("pane.derive")`.
471
+ The registry resolves its context from the FOCUS, so a menu built for a pane it was handed by
472
+ name got the answer for whichever pane the keyboard happened to be in: the same pane's rows
473
+ read "enabled" or "an open document in a pane that is not itself derived" depending on where
474
+ the author had last clicked. `scripts/check-pane-singletons.ts` rule 4 could not see it — the
475
+ focus read was a method call on a registry value, dispatched by a string id into a closure in
476
+ another module. It counts `disabledReason` and `isEnabled` themselves as focus reads now,
477
+ which names the shape without pretending to follow the dispatch. */
478
+ const deriveReason = deriveRefusal(paneId);
479
+ const rows: PresetRow[] = [];
480
+ for (const preset of DERIVE_PRESETS) {
481
+ if (preset === "breakpoint" || preset === "locale") {
482
+ continue;
483
+ }
484
+ rows.push({
485
+ args: { preset },
486
+ command: "pane.derive",
487
+ disabled: deriveReason ?? presetRefusal(preset, paneId, null),
488
+ label: PRESET_LABELS[preset],
489
+ pane: paneId,
490
+ });
491
+ }
492
+ // "Same page at ⟨breakpoint⟩" — one row per breakpoint the document declares, because the item IS
493
+ // The command and a submenu would be a second control between the author and it.
494
+ for (const media of tab && !derived ? declaredMedia(tab) : []) {
495
+ rows.push({
496
+ /* BASE omits `media` rather than passing `""`. `optionalStringArg` refuses an empty string
497
+ by design — "present but blank" is the shape that hides a missing value — so the base row
498
+ has to say nothing rather than say nothing loudly. Caught by opening the menu in a real
499
+ browser: every row ran, and the base one threw `expected a non-empty string, got ""`. */
500
+ args: media === null ? { preset: "breakpoint" } : { media, preset: "breakpoint" },
501
+ command: "pane.derive",
502
+ disabled: deriveReason ?? presetRefusal("breakpoint", paneId, media),
503
+ label: `${PRESET_LABELS.breakpoint} ${media ? mediaDisplayName(media) : "Base"}`,
504
+ pane: paneId,
505
+ });
506
+ }
507
+ /* "Same page in ⟨language⟩" — one row per locale the PROJECT declares, in the same shape as the
508
+ breakpoint rows above and with one difference: there is no omit-the-argument row. A breakpoint
509
+ has a base size that is spelled by saying nothing; every locale is a real tag, including the
510
+ default one, whose file `translationPathFor` puts at the unprefixed path under
511
+ `prefix-except-default`. The label is the locale's own AUTONYM — "français", not "French" —
512
+ because that is what the person looking for their language scans a list for.
513
+ A project that declares no `i18n` block at all contributes no rows: `getEffectiveLocales()`
514
+ answers null, and the menu is the same menu it has always been. One that declares exactly one
515
+ locale gets its one row carrying {@link presetRefusal}'s sentence, which names Project
516
+ Settings › Locales — the author asked for languages, so the answer is where to add the second
517
+ one rather than silence. */
518
+ for (const locale of tab && !derived ? (getEffectiveLocales()?.locales ?? []) : []) {
519
+ rows.push({
520
+ args: { locale, preset: "locale" },
521
+ command: "pane.derive",
522
+ disabled: deriveReason ?? presetRefusal("locale", paneId, null, locale),
523
+ label: `${PRESET_LABELS.locale} ${localeLabel(locale)}`,
524
+ pane: paneId,
525
+ });
526
+ }
527
+ /* The two exits, and the reason only ONE of them takes a pane.
528
+ `pane.pin`'s subject is the grid's derived pane — the author reaches it from the palette while
529
+ the keyboard is in the page they are editing — and {@link pinRefusal} answers about a named
530
+ pane without reading the focus, so the menu can ask it about its own. `pane.unsplit`'s
531
+ enablement is `workspace.panes.length > 1`, a fact about the grid with no pane in it at all;
532
+ its `requires` is data on the record and the boolean is the same from anywhere. Its RUN is a
533
+ focus read, like `pane.derive`'s — which is what {@link PresetRow.pane} is for. */
534
+ const exits: [string, string | null][] = [
535
+ ["pane.pin", pinRefusal(paneId)],
536
+ [
537
+ "pane.unsplit",
538
+ workspace.panes.length > 1 ? null : (registry?.get("pane.unsplit")?.requires ?? null),
539
+ ],
540
+ ];
541
+ for (const [id, disabled] of exits) {
542
+ const command = registry?.get(id);
543
+ if (command) {
544
+ rows.push({ args: {}, command: id, disabled, label: command.title, pane: paneId });
545
+ }
546
+ }
547
+ return rows;
548
+ }
549
+
550
+ /** One labelled axis. The label is the point: five unlabelled controls is what this replaces. */
551
+ function axisTpl(label: string, control: TemplateResult): TemplateResult {
552
+ return html`
553
+ <div class="pc-axis">
554
+ <span class="pc-axis-label">${label}</span>
555
+ ${control}
556
+ </div>
557
+ `;
558
+ }
559
+
560
+ /**
561
+ * The bar, always the same three axes.
562
+ *
563
+ * There is no takeover branch. Opening a function body or a formula reveals the dock's Logic tab
564
+ * (P8) and leaves the canvas standing underneath it, so the axes still describe the document on the
565
+ * stage and the zoom pod still has something to zoom. Suppressing them while the dock was open
566
+ * removed the controls for the very document the reader could still see.
567
+ *
568
+ * **There is no breadcrumb either.** The address is ⑥'s job — `panels/jump-bar.ts`, one row above —
569
+ * and the Logic tab's own header carries the Close. This bar drew a second Back and a second trail
570
+ * beside both of them.
571
+ */
572
+ function paneChromeTemplate(tab: Tab, paneId: string, ctx: PaneContextCtx): TemplateResult {
573
+ const kind = editorKindOf(tab);
574
+ /* A LENS suppresses the two axes that WRITE. Editor kind and Canvas view both land in
575
+ `ctx.setCanvasMode(tab, …)`, and that tab belongs to the pane beside this one — so a control
576
+ drawn in the lens would flip the document the author is editing. The lens's own mode is the
577
+ derivation, chosen when it was created and changed by re-deriving. The Context axis stays as
578
+ a static summary for the same reason (it writes `updateUi(tab, …)`), and the zoom pod stays
579
+ because zoom is the one view fact a lens genuinely owns. */
580
+ const lens = derivationOfPane(paneId)?.kind === "lens";
581
+
582
+ return html`
583
+ <div class="pc-band">
584
+ <div class="pane-context" data-jx-region=${paneRegion(paneId, "context")}>
585
+ <div class="pc-spacer">
586
+ ${
587
+ /* …and NO ⟲ trigger in a lens. Every projection row in the menu it opens is permanently
588
+ disabled from there (a derived pane cannot derive again) and the breakpoint rows are
589
+ suppressed outright, leaving one live row — Unsplit — which the derivation chip's ✕
590
+ in this pane's own strip already runs. A control that can do nothing from where it is
591
+ drawn is the class this phase has deleted three times. A COMPANION keeps it: "Keep
592
+ This Document" is live there and it is genuinely about that pane. The SPACER stays
593
+ either way: it is what pushes the axes right, and dropping it would move the one
594
+ control a lens does draw. */
595
+ lens ? nothing : presetTriggerTpl(paneId)
596
+ }
597
+ </div>
598
+ ${
599
+ lens
600
+ ? renderingSummaryTpl(paneId)
601
+ : html`${editorKindTpl(tab, ctx)} ${kind === "canvas" ? viewTpl(tab, ctx) : nothing}
602
+ ${renderingContextTpl(tab, paneId, ctx)} ${exportTpl(tab, ctx)}`
603
+ }
604
+ </div>
605
+ ${lens ? nothing : readOnlyBannerTemplate(tab)}
606
+ </div>
607
+ ${zoomPodTpl(tab, paneId)}
608
+ `;
609
+ }
610
+
611
+ /**
612
+ * A lens's rendering context, stated rather than offered.
613
+ *
614
+ * Open question 1, answered "keep it, read-only": a lens drawn under different preview params than
615
+ * the pane it is a lens OF would be lying about what it is a lens of, so the summary has to be on
616
+ * screen — but the popover behind it writes the source tab's `session.ui`, which is the one thing a
617
+ * lens must never do.
618
+ */
619
+ function renderingSummaryTpl(paneId: string): TemplateResult {
620
+ /* No `ctx.parseMediaEntries` here. It parsed the document's whole `$media` map, discarded the
621
+ result through `void sizeBreakpoints` and printed the media NAME — a parse per lens-bar render
622
+ for a value that was never read. `mediaDisplayName` is the only lookup this line needs. */
623
+ /* {@link activeMediaOfPane}, and it exists for exactly this. `session.ui.activeMedia` is per-TAB
624
+ and a lens shares its tab with the pane beside it, so this axis printed the breakpoint the
625
+ SOURCE pane was on: the stage drew the Tablet artboard and the line under it said "Base". The
626
+ docstring above says a lens drawn under different params "would be lying about what it is a
627
+ lens of" — and it lied about the one axis the preset is named after. */
628
+ const media = activeMediaOfPane(paneId);
629
+ /* A lens shares its TAB, so it renders under the tab's preview locale whether it asked to or not
630
+ — and a lens whose stage is mirrored while its own line says nothing is the same lie about the
631
+ rendering context this summary exists to prevent. Stated only when it differs from the
632
+ document's own language, for the reason the trigger beside it is: a pane drawing the file it
633
+ has open in the language that file is written in has nothing to report. */
634
+ const locale = localeOf(tabOfPane(paneId));
635
+ const stated = `${media ? mediaDisplayName(media) : "Base"}${
636
+ locale?.overridden ? ` · ${localeLabel(locale.effective)}` : ""
637
+ }`;
638
+ return axisTpl("Context", html`<span class="pc-static">${stated}</span>`);
639
+ }
640
+
641
+ // ─── Axis 1 · Editor kind ────────────────────────────────────────────────────
642
+
643
+ function editorKindTpl(tab: Tab, ctx: PaneContextCtx): TemplateResult {
644
+ /* What the DOCUMENT declares, and nothing else. This read `hostableKindsOf` — the declared kinds
645
+ narrowed by what the tab's pane was allowed to host — so a page in the side pane was offered
646
+ Code and not Design. The pane cap is gone; a control that cannot go anywhere is still the
647
+ defect this axis exists to remove, and one declared kind still renders as text. */
648
+ const kinds = editorKindsOf(tab);
649
+ const current = editorKindOf(tab);
650
+ if (kinds.length < 2) {
651
+ // One kind is not a choice. Rendering it as a dropdown would be a control that cannot move —
652
+ // Which is the defect this axis exists to remove, in miniature.
653
+ return axisTpl("Editor", html`<span class="pc-static">${EDITOR_KIND_LABELS[current]}</span>`);
654
+ }
655
+ return axisTpl(
656
+ "Editor",
657
+ html`
658
+ <sp-picker
659
+ size="s"
660
+ quiet
661
+ class="pc-editor-kind"
662
+ label="Editor"
663
+ value=${current}
664
+ @change=${(e: Event) => {
665
+ const kind = (e.target as HTMLInputElement).value as EditorKind;
666
+ const mode = modeForEditorKind(tab, kind);
667
+ if (!mode) {
668
+ return;
669
+ }
670
+ tab.session.ui.preview = false;
671
+ ctx.setCanvasMode(tab, mode);
672
+ }}
673
+ >
674
+ ${kinds.map(
675
+ (kind) => html`<sp-menu-item value=${kind}>${EDITOR_KIND_LABELS[kind]}</sp-menu-item>`,
676
+ )}
677
+ </sp-picker>
678
+ `,
679
+ );
680
+ }
681
+
682
+ // ─── Axis 2 · Canvas view ────────────────────────────────────────────────────
683
+
684
+ function viewTpl(tab: Tab, ctx: PaneContextCtx): TemplateResult | typeof nothing {
685
+ // Every pane draws a live Canvas, so this is no longer narrowed by WHERE the tab is — only by
686
+ // What the document declares. A document with no Canvas view still draws no view group.
687
+ const views = canvasBaseViewsFor(tab);
688
+ if (views.length === 0) {
689
+ return nothing;
690
+ }
691
+ const current = canvasBaseViewOf(tab);
692
+ const preview = previewStateOf(tab);
693
+ /*
694
+ * TWO CONTROLS, because they are two axes.
695
+ *
696
+ * `Edit │ Design │ Preview` was one three-way radio, and it lied about the state it was showing:
697
+ * preview is stored as a flag OVER an edit/design base, so while it was on the radio could not
698
+ * say which mode you were previewing — or which one Escape would return you to. Now the radio
699
+ * marks the base the whole time and the toggle sits beside it, pressed.
700
+ */
701
+ return axisTpl(
702
+ "View",
703
+ html`
704
+ <sp-action-group compact size="s" class="pc-view" role="radiogroup" aria-label="Canvas view">
705
+ ${views.map(
706
+ (value) => html`
707
+ <sp-action-button
708
+ size="s"
709
+ role="radio"
710
+ aria-checked=${value === current ? "true" : "false"}
711
+ ?selected=${value === current}
712
+ title=${`Show this document in ${CANVAS_VIEW_LABELS[value]}`}
713
+ @click=${() => setCanvasView(tab, value, ctx.setCanvasMode)}
714
+ >
715
+ ${CANVAS_VIEW_LABELS[value]}
716
+ </sp-action-button>
717
+ `,
718
+ )}
719
+ </sp-action-group>
720
+ ${
721
+ preview.available
722
+ ? html`
723
+ <sp-action-button
724
+ size="s"
725
+ class="pc-preview-toggle"
726
+ toggles
727
+ ?selected=${preview.on}
728
+ aria-pressed=${preview.on ? "true" : "false"}
729
+ title=${
730
+ preview.on
731
+ ? `Stop previewing — back to ${CANVAS_VIEW_LABELS[current ?? "edit"]}`
732
+ : "Preview: the page as it ships, with editing off"
733
+ }
734
+ @click=${() => {
735
+ setCanvasView(
736
+ tab,
737
+ preview.on ? (current ?? "edit") : "preview",
738
+ ctx.setCanvasMode,
739
+ );
740
+ }}
741
+ >
742
+ ${CANVAS_VIEW_LABELS.preview}
743
+ </sp-action-button>
744
+ `
745
+ : nothing
746
+ }
747
+ `,
748
+ );
749
+ }
750
+
751
+ // ─── Axis 3 · Rendering context ──────────────────────────────────────────────
752
+
753
+ /** Whether the open document is a page of a site project — pages get route params, others props. */
754
+ function isPageDoc(tab: Tab): boolean {
755
+ const path = tab.documentPath;
756
+ return Boolean(
757
+ path &&
758
+ projectState?.isSiteProject &&
759
+ (path.startsWith("pages/") || path.startsWith("./pages/")),
760
+ );
761
+ }
762
+
763
+ /** What a pane is rendering AS, once — the trigger, the popover and the lens summary all ask this. */
764
+ interface PaneLocale {
765
+ /** The project's declared locales, default first. */
766
+ i18n: ResolvedI18n;
767
+ /** The locale of the FILE this pane has open, when its directory names one — `pages/fr/…`. */
768
+ pathLocale: string | null;
769
+ /** The tag the artboard is drawn under. */
770
+ effective: string;
771
+ /** Whether that is not the document's own language, i.e. whether the bar has something to say. */
772
+ overridden: boolean;
773
+ }
774
+
775
+ /**
776
+ * The language axis for a tab, or `null` when the project has no language question to ask.
777
+ *
778
+ * Called at RENDER time and never cached: `projectState` is replaced wholesale rather than mutated,
779
+ * so a locale added in Settings reaches this bar on the next paint and a module-level copy would
780
+ * still be describing the project that was open when this file loaded.
781
+ *
782
+ * A single declared locale is the same as none for this control — there is nothing to switch TO,
783
+ * and "groups a document declares nothing for are absent" is the rule the whole popover follows.
784
+ *
785
+ * The fallback chain is what makes the control honest about a file it has not been told about:
786
+ * `previewLocale` is the author's explicit choice, the path is the file's own language (§13.3 puts
787
+ * a translation in its own directory), and the default locale is what an unprefixed page renders
788
+ * as.
789
+ */
790
+ function localeOf(tab: Tab | null): PaneLocale | null {
791
+ const i18n = getEffectiveLocales();
792
+ if (!tab || i18n === null || i18n.locales.length < 2) {
793
+ return null;
794
+ }
795
+ const pathLocale = localeOfPath(tab.documentPath ?? "", i18n);
796
+ const own = pathLocale ?? i18n.defaultLocale;
797
+ const effective = tab.session.ui.previewLocale ?? own;
798
+ return { effective, i18n, overridden: effective !== own, pathLocale };
799
+ }
800
+
801
+ function renderingContextTpl(tab: Tab, paneId: string, ctx: PaneContextCtx): TemplateResult {
802
+ const { ui } = tab.session;
803
+ const { featureQueries, sizeBreakpoints } = ctx.parseMediaEntries(
804
+ getEffectiveMedia(tab.doc.document?.$media as Record<string, string> | undefined),
805
+ );
806
+ const schemeQueries = featureQueries.filter(({ query }) => isSchemeQuery(query));
807
+ const plainQueries = featureQueries.filter(({ query }) => !isSchemeQuery(query));
808
+ const scheme = (ui.previewColorScheme ?? "auto") as string;
809
+ const activeMedia = (ui.activeMedia ?? null) as string | null;
810
+ const sizeLabel = activeMedia ? mediaDisplayName(activeMedia) : "Base";
811
+ const schemeLabel = SCHEMES.find(([value]) => value === scheme)?.[1] ?? "Auto";
812
+ const locale = localeOf(tab);
813
+ /* The language joins the trigger ONLY when the pane is not drawing the document's own — a French
814
+ page open in a French pane is not a rendering context worth three more characters, and a bar
815
+ that grew a third term in every multilingual project would stop reading as a state. */
816
+ const summary = [
817
+ sizeLabel,
818
+ schemeQueries.length > 0 ? schemeLabel : null,
819
+ locale?.overridden ? localeLabel(locale.effective) : null,
820
+ ]
821
+ .filter((part) => part !== null)
822
+ .join(" · ");
823
+ const hasLayout = isPageDoc(tab) && Boolean(getEffectiveLayoutPath(tab.doc.document?.$layout));
824
+ const resolving = isPageDoc(tab) ? paramPickersTpl(tab, paneId) : propFieldsTpl(tab, paneId);
825
+ /* How many of those fields carry a value. The trigger has to say something true at a glance, the
826
+ way the Context trigger says `Base · Auto` — a chevron with no reading is a control you have to
827
+ open in order to learn whether it was worth opening. */
828
+ const resolvingSet = isPageDoc(tab)
829
+ ? Object.values(ui.previewParams ?? {}).filter((v) => v !== "" && v !== undefined).length
830
+ : Object.keys(ui.previewProps ?? {}).length;
831
+
832
+ return axisTpl(
833
+ "Context",
834
+ html`
835
+ ${resolvingTpl(paneId, resolving, resolvingSet)}
836
+ <overlay-trigger placement="bottom-end" triggered-by="click">
837
+ <sp-action-button
838
+ slot="trigger"
839
+ size="s"
840
+ quiet
841
+ class="pc-context-trigger"
842
+ title="What this document is being rendered with"
843
+ >
844
+ ${summary} ⌄
845
+ </sp-action-button>
846
+ <sp-popover slot="click-content" tip class="pc-context-popover">
847
+ <div class="pc-ctx">
848
+ ${sizeGroupTpl(paneId, sizeBreakpoints, activeMedia)}
849
+ ${
850
+ schemeQueries.length > 0
851
+ ? groupTpl(
852
+ "Color scheme",
853
+ html`
854
+ <sp-action-group compact size="s">
855
+ ${SCHEMES.map(
856
+ ([value, label, title]) => html`
857
+ <sp-action-button
858
+ size="s"
859
+ role="radio"
860
+ aria-checked=${scheme === value ? "true" : "false"}
861
+ title=${title}
862
+ ?selected=${scheme === value}
863
+ @click=${() =>
864
+ runContextCommand(paneId, "canvas.setColorScheme", {
865
+ scheme: value,
866
+ })}
867
+ >
868
+ ${label}
869
+ </sp-action-button>
870
+ `,
871
+ )}
872
+ </sp-action-group>
873
+ `,
874
+ )
875
+ : nothing
876
+ }
877
+ ${locale ? localeGroupTpl(paneId, locale) : nothing}
878
+ ${
879
+ plainQueries.length > 0
880
+ ? groupTpl(
881
+ "Features",
882
+ html`
883
+ <sp-action-group compact size="s">
884
+ ${plainQueries.map(
885
+ ({ name, query }) => html`
886
+ <sp-action-button
887
+ size="s"
888
+ toggles
889
+ title=${query}
890
+ ?selected=${Boolean(ui.featureToggles[name])}
891
+ @click=${() => {
892
+ updateUi(tab, "featureToggles", {
893
+ ...tab.session.ui.featureToggles,
894
+ [name]: !tab.session.ui.featureToggles[name],
895
+ });
896
+ }}
897
+ >
898
+ ${mediaDisplayName(name)}
899
+ </sp-action-button>
900
+ `,
901
+ )}
902
+ </sp-action-group>
903
+ `,
904
+ )
905
+ : nothing
906
+ }
907
+ ${
908
+ hasLayout
909
+ ? groupTpl(
910
+ "Layout",
911
+ html`
912
+ <sp-switch
913
+ size="s"
914
+ class="pc-layout-switch"
915
+ ?checked=${ui.showLayout !== false}
916
+ @change=${() =>
917
+ runContextCommand(paneId, "canvas.setLayoutVisible", {
918
+ visible: tab.session.ui.showLayout === false,
919
+ })}
920
+ >
921
+ Show layout elements
922
+ </sp-switch>
923
+ `,
924
+ )
925
+ : nothing
926
+ }
927
+ <button
928
+ class="pc-ctx-manage"
929
+ type="button"
930
+ title="Breakpoints, schemes and feature queries are defined in Project Settings › Contexts"
931
+ @click=${() => {
932
+ void activeRegistry()?.run("settings.open", { section: "contexts" });
933
+ }}
934
+ >
935
+ Manage contexts…
936
+ </button>
937
+ </div>
938
+ </sp-popover>
939
+ </overlay-trigger>
940
+ `,
941
+ );
942
+ }
943
+
944
+ /**
945
+ * The document DATA a render resolves against — route params, component test props — in a popover.
946
+ *
947
+ * §4.2 folds these in behind the words "resolving with…", and this is where that lands: the phrase
948
+ * is the popover's header and the fields are a vertical stack under it, the same shape as the
949
+ * rendering-context popover beside it.
950
+ *
951
+ * They used to sit OPEN on the bar, and the argument for that was the screenshot contract (§13.1):
952
+ * behind a click, the one first-hour flow the docs teach — typing a test prop on
953
+ * `start/first-component` — costs a second gesture, and the manifest's input budget may only
954
+ * ratchet down. The answer is not to keep n text fields on a 28px band that also carries the
955
+ * editor, the view and the rendering context; it is that a transient surface opens by COMMAND (plan
956
+ * §13.2), so the shot spends a `cmd` step rather than a selector and the input budget is untouched.
957
+ * `canvas.setResolvingOpen` is that command, and it is the same door the pointer uses.
958
+ *
959
+ * @param {string} paneId
960
+ * @param {TemplateResult | typeof nothing} body
961
+ * @param {number} setCount — how many fields carry a value, for the trigger's summary
962
+ */
963
+ function resolvingTpl(
964
+ paneId: string,
965
+ body: TemplateResult | typeof nothing,
966
+ setCount: number,
967
+ ): TemplateResult | typeof nothing {
968
+ if (body === nothing) {
969
+ return nothing;
970
+ }
971
+ return html`
972
+ <overlay-trigger
973
+ placement="bottom-end"
974
+ triggered-by="click"
975
+ ${ref((el: Element | undefined) => {
976
+ if (el) {
977
+ _resolvingTrigger.set(paneId, el as HTMLElement & { open?: string | undefined });
978
+ }
979
+ })}
980
+ >
981
+ <sp-action-button
982
+ slot="trigger"
983
+ size="s"
984
+ quiet
985
+ class="pc-resolving-trigger"
986
+ title="The values this document is being rendered with"
987
+ >
988
+ ${setCount > 0 ? `${setCount} set` : "Defaults"} ⌄
989
+ </sp-action-button>
990
+ <sp-popover slot="click-content" tip class="pc-context-popover">
991
+ <div class="pc-ctx">${groupTpl("resolving with", body as TemplateResult)}</div>
992
+ </sp-popover>
993
+ </overlay-trigger>
994
+ `;
995
+ }
996
+
997
+ /**
998
+ * Each pane's "resolving with" `overlay-trigger`, which IS the open state.
999
+ *
1000
+ * No parallel `Set`. The overlay owns whether it is showing — it opens on a pointer click without
1001
+ * telling anyone first — so a second record of the same fact could only ever be the stale one. The
1002
+ * element's `open` property is what the pointer writes and what the command writes, and reading it
1003
+ * back is how anything else asks.
1004
+ */
1005
+ const _resolvingTrigger = new Map<string, HTMLElement & { open?: string | undefined }>();
1006
+
1007
+ /** Whether this pane's resolving popover is open. Exported for the tests. */
1008
+ export function isResolvingOpen(paneId: string): boolean {
1009
+ return _resolvingTrigger.get(paneId)?.open === "click";
1010
+ }
1011
+
1012
+ /**
1013
+ * Open or close it.
1014
+ *
1015
+ * A named end state rather than a toggle, so `canvas.setResolvingOpen { open: false }` means the
1016
+ * same thing twice and a screenshot can photograph it (§13's R1).
1017
+ *
1018
+ * It writes the element and does NOT re-render the bar. `overlay-trigger` moves the popover into an
1019
+ * overlay portal while it is open and restores it on close; re-rendering this template in the
1020
+ * middle of that leaves the moved copy painted over the canvas with no way to dismiss it. The
1021
+ * overlay owns its own visibility — the only thing the bar redraws for is the trigger's summary,
1022
+ * and that changes when a VALUE changes, which already renders.
1023
+ */
1024
+ export function setResolvingOpen(paneId: string, open: boolean): void {
1025
+ const trigger = _resolvingTrigger.get(paneId);
1026
+ if (trigger && isResolvingOpen(paneId) !== open) {
1027
+ trigger.open = open ? "click" : undefined;
1028
+ }
1029
+ }
1030
+
1031
+ /** Forget the held triggers — a fresh window, and the tests. */
1032
+ export function resetResolvingOpen(): void {
1033
+ _resolvingTrigger.clear();
1034
+ }
1035
+
1036
+ /** One labelled group inside the rendering-context popover. */
1037
+ function groupTpl(label: string, body: TemplateResult): TemplateResult {
1038
+ return html`
1039
+ <div class="pc-ctx-group">
1040
+ <span class="pc-ctx-label">${label}</span>
1041
+ ${body}
1042
+ </div>
1043
+ `;
1044
+ }
1045
+
1046
+ /**
1047
+ * The size segment: the base width plus every declared size breakpoint.
1048
+ *
1049
+ * It writes `ui.activeMedia`, the same field a canvas panel header click writes — one axis, one
1050
+ * field, two ways in. `null` is the base, which is why the list is not simply the breakpoints.
1051
+ */
1052
+ /**
1053
+ * Run a rendering-context verb through the registry.
1054
+ *
1055
+ * These four controls wrote `session.ui` directly through `updateUi`, which is why none of the
1056
+ * three axes was a command: the popover WAS the capability, and the palette, the assistant and
1057
+ * `__jxAutomation` had no name for it. Going through the registry makes the control and the verb
1058
+ * one thing (§2, principle 1) and gets the breakpoint refusal for free.
1059
+ */
1060
+ function runContextCommand(paneId: string, id: string, args: Record<string, unknown>): void {
1061
+ // THIS pane, named. The bar is drawn once per pane and the side bar's controls write the side
1062
+ // Pane's tab; a verb defaulting to the focused pane would have made the side bar edit the
1063
+ // Foreground document the moment its control became a command.
1064
+ void activeRegistry()?.run(id, { ...args, pane: paneId });
1065
+ }
1066
+
1067
+ function sizeGroupTpl(
1068
+ paneId: string,
1069
+ breakpoints: { name: string; width: number }[],
1070
+ activeMedia: string | null,
1071
+ ): TemplateResult {
1072
+ return groupTpl(
1073
+ "Size",
1074
+ html`
1075
+ <sp-action-group compact size="s" class="pc-sizes">
1076
+ <sp-action-button
1077
+ size="s"
1078
+ role="radio"
1079
+ aria-checked=${activeMedia === null ? "true" : "false"}
1080
+ title="The base rendering, with no breakpoint applied"
1081
+ ?selected=${activeMedia === null}
1082
+ @click=${() => runContextCommand(paneId, "canvas.setBreakpoint", { media: null })}
1083
+ >
1084
+ Base
1085
+ </sp-action-button>
1086
+ ${breakpoints.map(
1087
+ ({ name, width }) => html`
1088
+ <sp-action-button
1089
+ size="s"
1090
+ role="radio"
1091
+ aria-checked=${activeMedia === name ? "true" : "false"}
1092
+ title=${`${mediaDisplayName(name)} — ${width}px`}
1093
+ ?selected=${activeMedia === name}
1094
+ @click=${() => runContextCommand(paneId, "canvas.setBreakpoint", { media: name })}
1095
+ >
1096
+ ${mediaDisplayName(name)}
1097
+ </sp-action-button>
1098
+ `,
1099
+ )}
1100
+ </sp-action-group>
1101
+ `,
1102
+ );
1103
+ }
1104
+
1105
+ /**
1106
+ * The language segment: which of the project's locales this pane renders AS.
1107
+ *
1108
+ * It is one radio group and one sentence, and the sentence is the load-bearing half. Jx has no
1109
+ * message catalogue — a translation is a different file in a different directory — so this control
1110
+ * cannot and must not claim to translate the page. What it does change is real: the artboard's
1111
+ * `lang` and its `dir`, and an RTL locale mirrors the layout on the stage. A control that let an
1112
+ * author believe it had translated anything would be worse than no control — which is also why the
1113
+ * sentence stops there: the build's `$page.locale` is not injected into the canvas render today, so
1114
+ * a tooltip promising it would be describing the site rather than the pane.
1115
+ *
1116
+ * Labelled with each locale's AUTONYM, for the reason `localeLabel` gives: a reader looking for
1117
+ * their own language scans for their own word for it.
1118
+ *
1119
+ * The document's own language is named in its tooltip rather than styled apart — a per-pane axis
1120
+ * has no room for a second visual state, and the author needs to know which entry is "no override"
1121
+ * before clicking, not after.
1122
+ */
1123
+ function localeGroupTpl(paneId: string, locale: PaneLocale): TemplateResult {
1124
+ return groupTpl(
1125
+ "Language",
1126
+ html`
1127
+ <sp-action-group
1128
+ compact
1129
+ size="s"
1130
+ title="The language this pane renders as — its lang and direction only. The text is whatever file is open."
1131
+ >
1132
+ ${locale.i18n.locales.map(
1133
+ (tag) => html`
1134
+ <sp-action-button
1135
+ size="s"
1136
+ role="radio"
1137
+ aria-checked=${locale.effective === tag ? "true" : "false"}
1138
+ title=${
1139
+ tag === locale.pathLocale
1140
+ ? `${localeLabel(tag)} — the language of the file this pane has open`
1141
+ : `Render as ${localeLabel(tag)} (${tag})`
1142
+ }
1143
+ ?selected=${locale.effective === tag}
1144
+ @click=${() => runContextCommand(paneId, "i18n.switchLocale", { locale: tag })}
1145
+ >
1146
+ ${localeLabel(tag)}
1147
+ </sp-action-button>
1148
+ `,
1149
+ )}
1150
+ </sp-action-group>
1151
+ `,
1152
+ );
1153
+ }
1154
+
1155
+ // ─── The mode action ─────────────────────────────────────────────────────────
1156
+ // Export is not an axis, and it is the one control in the old bar with nowhere to go yet: retiring
1157
+ // It without a command name, a chord and a residue would be deletion, not consolidation (§2
1158
+ // Principle 9). It stays in the Code view's trailing slot until the Code editor kind owns an action
1159
+ // Strip of its own.
1160
+
1161
+ function exportTpl(tab: Tab, ctx: PaneContextCtx): TemplateResult | typeof nothing {
1162
+ /* THIS tab's effective mode. `ctx.getCanvasMode()` answered for the focused pane, so a document
1163
+ opened as Code in either pane put an Export button in the OTHER pane's bar as well — over a
1164
+ document that is not the one the button exports. */
1165
+ if (canvasModeOfTab(tab) !== "source") {
1166
+ return nothing;
1167
+ }
1168
+ return html`
1169
+ <sp-action-button size="s" class="pc-export" @click=${ctx.exportFile}>
1170
+ <sp-icon-export slot="icon"></sp-icon-export>
1171
+ Export
1172
+ </sp-action-button>
1173
+ `;
1174
+ }
1175
+
1176
+ // ─── ⑩ The floating zoom pod ─────────────────────────────────────────────────
1177
+
1178
+ /** The fit entries the pod offers, in menu order. `1` is "actual size", a numeric fit. */
1179
+ const FIT_CHOICES: readonly { value: string; fit: FitMode; label: string }[] = [
1180
+ { fit: "page", label: "Fit page", value: "page" },
1181
+ { fit: "width", label: "Fit width", value: "width" },
1182
+ { fit: 1, label: "Actual size", value: "actual" },
1183
+ { fit: "none", label: "No fit", value: "none" },
1184
+ ];
1185
+
1186
+ /** Which entry the pod shows as chosen — a numeric fit reads as "actual size" only at 1. */
1187
+ function fitChoiceValue(fit: FitMode): string {
1188
+ if (typeof fit === "number") {
1189
+ return fit === 1 ? "actual" : "";
1190
+ }
1191
+ return fit;
1192
+ }
1193
+
1194
+ /**
1195
+ * Zoom and fit, floating over the canvas bottom-right.
1196
+ *
1197
+ * Two surfaces, one control: `edit` drives the content-reflow `editZoom`, while design / Stylebook
1198
+ * / git-diff render on the panzoom surface and drive `ui.zoom` — and only the panzoom surface has a
1199
+ * fit, because a fit is a statement about an artboard. Preview is deliberately absent: its frame is
1200
+ * a real viewport that scrolls its own document, so there is nothing to zoom.
1201
+ */
1202
+ function zoomPodTpl(tab: Tab, paneId: string): TemplateResult | typeof nothing {
1203
+ /* THIS pane's mode. `ctx.getCanvasMode()` answers for the focused pane, so the unfocused pod
1204
+ offered an `editZoom` control over a stage drawing Design — and hid the fit that stage has. */
1205
+ const mode = canvasModeOfPane(paneId);
1206
+ /* And THIS pane's stage. Every zoom verb below takes a surface: without one they all defaulted to
1207
+ `activeCanvasSurface()`, so the side pane's `+` magnified the primary's document by a factor
1208
+ computed from the side pane's own zoom, and its "100%" button reset the primary. */
1209
+ const surface = surfaceForPane(paneId);
1210
+ if (mode === "edit") {
1211
+ const editZoom = tab.session.ui.editZoom ?? 1;
1212
+ return podTpl(
1213
+ paneId,
1214
+ html`
1215
+ ${zoomButton("Zoom out (Ctrl+-)", "−", () =>
1216
+ setEditZoom((tab.session.ui.editZoom ?? 1) / 1.2, surface),
1217
+ )}
1218
+ <sp-action-button
1219
+ size="s"
1220
+ class="pc-zoom-label"
1221
+ title="Reset to 100% (Ctrl+0)"
1222
+ @click=${() => setEditZoom(1, surface)}
1223
+ >
1224
+ ${Math.round(editZoom * 100)}%
1225
+ </sp-action-button>
1226
+ ${zoomButton("Zoom in (Ctrl+=)", "+", () =>
1227
+ setEditZoom((tab.session.ui.editZoom ?? 1) * 1.2, surface),
1228
+ )}
1229
+ `,
1230
+ );
1231
+ }
1232
+ if (mode !== "design" && mode !== "stylebook" && mode !== "git-diff") {
1233
+ return nothing;
1234
+ }
1235
+ const zoom = stageZoom(surface);
1236
+ const chosen = fitChoiceValue(getFit(surface));
1237
+ return podTpl(
1238
+ paneId,
1239
+ html`
1240
+ ${zoomButton("Zoom out (Ctrl+-)", "−", () => setUserZoom(stageZoom(surface) / 1.2, surface))}
1241
+ <sp-action-button
1242
+ size="s"
1243
+ class="pc-zoom-label"
1244
+ title="Reset to 100% (Ctrl+0)"
1245
+ @click=${() => resetZoom(surface)}
1246
+ >
1247
+ ${Math.round(zoom * 100)}%
1248
+ </sp-action-button>
1249
+ ${zoomButton("Zoom in (Ctrl+=)", "+", () => setUserZoom(stageZoom(surface) * 1.2, surface))}
1250
+ <sp-picker
1251
+ size="s"
1252
+ quiet
1253
+ class="pc-fit"
1254
+ label="Fit"
1255
+ value=${chosen}
1256
+ title="How this document is framed"
1257
+ @change=${(e: Event) => {
1258
+ const { value } = e.target as HTMLInputElement;
1259
+ const choice = FIT_CHOICES.find((entry) => entry.value === value);
1260
+ if (!choice) {
1261
+ return;
1262
+ }
1263
+ // "Fit page" from the control may magnify a small artboard past life size; the fit APPLIED
1264
+ // On arrival never does. Same declared state, two caps — see fitToScreen's maxZoom.
1265
+ if (choice.fit === "page") {
1266
+ fitToScreen({ surface });
1267
+ return;
1268
+ }
1269
+ setFit(choice.fit, surface);
1270
+ }}
1271
+ >
1272
+ ${FIT_CHOICES.map(
1273
+ ({ value, label }) => html`<sp-menu-item value=${value}>${label}</sp-menu-item>`,
1274
+ )}
1275
+ </sp-picker>
1276
+ `,
1277
+ );
1278
+ }
1279
+
1280
+ function podTpl(paneId: string, body: TemplateResult): TemplateResult {
1281
+ return html`
1282
+ <div class="pane-zoom" data-jx-region=${paneRegion(paneId, "zoom")}>
1283
+ <sp-action-group compact size="s">${body}</sp-action-group>
1284
+ </div>
1285
+ `;
1286
+ }
1287
+
1288
+ function zoomButton(title: string, glyph: string, onClick: () => void): TemplateResult {
1289
+ return html`
1290
+ <sp-action-button size="s" title=${title} @click=${onClick}>${glyph}</sp-action-button>
1291
+ `;
1292
+ }
1293
+
1294
+ // ─── Dynamic route-param pickers ─────────────────────────────────────────────
1295
+ // Candidate values load asynchronously (ContentCollection resolution / data-file read); the module
1296
+ // Caches the last result per (documentPath, $paths) and re-renders when it lands — the same lazy
1297
+ // Fill pattern as head-panel's loadLayoutEntries. When values arrive, any param without a chosen
1298
+ // Value auto-selects the first candidate (matching the compiler, whose first expanded route is the
1299
+ // First path entry).
1300
+
1301
+ /**
1302
+ * Candidate values per `(documentPath, $paths)` key, and the keys whose load is in flight.
1303
+ *
1304
+ * **A Map, not one slot, and that is a livelock fix rather than a cache-size preference.** It was
1305
+ * `_paramValues` + `_paramValuesKey`, holding exactly ONE result, while {@link render} loops every
1306
+ * attached pane. Two panes on documents with different keys evicted each other on every pass: pane
1307
+ * A's render stored A's key and cleared the value, pane B's render in the same loop stored B's, and
1308
+ * whichever load landed found its key gone — or, once the "still shown somewhere" guard let it
1309
+ * through, stored its value and called `render()`, which re-issued both loads again. An unbounded
1310
+ * microtask chain: no rAF, no paint, no input. `⌘\` with two pages under dynamic routes was enough,
1311
+ * and the probe never returned.
1312
+ *
1313
+ * `_paramLoading` is what stops a re-render re-issuing a load that has not landed yet; the value
1314
+ * map is what stops one pane's answer erasing the other's.
1315
+ */
1316
+ const _paramValues = new Map<string, ParamValues>();
1317
+
1318
+ const _paramLoading = new Set<string>();
1319
+
1320
+ /** Drop every cached candidate list — a project switch, and the tests. */
1321
+ export function resetParamValues(): void {
1322
+ _paramValues.clear();
1323
+ _paramLoading.clear();
1324
+ }
1325
+
1326
+ /**
1327
+ * @param {Tab} tab
1328
+ * @returns {ParamValues | null} — null while loading (or when the doc declares no params)
1329
+ */
1330
+ function paramValuesFor(tab: Tab): ParamValues | null {
1331
+ const pathsDef = pagePathsDef({
1332
+ document: tab.doc.document,
1333
+ frontmatter: tab.doc.content.frontmatter,
1334
+ });
1335
+ if (!pathsDef && dynamicRouteParams(tab.documentPath).length === 0) {
1336
+ return null;
1337
+ }
1338
+ const key = `${tab.documentPath ?? ""}::${JSON.stringify(pathsDef)}`;
1339
+ const cached = _paramValues.get(key);
1340
+ if (cached) {
1341
+ return cached;
1342
+ }
1343
+ if (_paramLoading.has(key)) {
1344
+ return null;
1345
+ }
1346
+ _paramLoading.add(key);
1347
+ void loadParamValues(tab.documentPath, pathsDef).then((values) => {
1348
+ _paramLoading.delete(key);
1349
+ /* Still SHOWN somewhere, rather than still focused. The candidate values fill a picker in one
1350
+ pane's bar; a load that landed while the keyboard was in the other pane used to be discarded,
1351
+ leaving the picker permanently empty for whoever was not looking at it. */
1352
+ if (!workspace.panes.some((pane) => pane.activeTabId === tab.id)) {
1353
+ return;
1354
+ }
1355
+ _paramValues.set(key, values);
1356
+ autoSelectParams(tab, values);
1357
+ render();
1358
+ });
1359
+ return null;
1360
+ }
1361
+
1362
+ /**
1363
+ * @param {Tab} tab
1364
+ * @param {ParamValues} values
1365
+ */
1366
+ function autoSelectParams(tab: Tab, values: ParamValues) {
1367
+ const current = tab.session.ui.previewParams ?? {};
1368
+ const additions: Record<string, string> = {};
1369
+ for (const [name, list] of Object.entries(values)) {
1370
+ if (!current[name] && list.length > 0) {
1371
+ additions[name] = list[0]!;
1372
+ }
1373
+ }
1374
+ if (Object.keys(additions).length > 0) {
1375
+ updateUi(tab, "previewParams", { ...current, ...additions });
1376
+ }
1377
+ }
1378
+
1379
+ /**
1380
+ * @param {Tab} tab
1381
+ * @returns {TemplateResult | typeof nothing}
1382
+ */
1383
+ function paramPickersTpl(tab: Tab, paneId: string): TemplateResult | typeof nothing {
1384
+ const values = paramValuesFor(tab);
1385
+ const names = new Set(dynamicRouteParams(tab.documentPath));
1386
+ for (const name of Object.keys(values ?? {})) {
1387
+ names.add(name);
1388
+ }
1389
+ if (names.size === 0) {
1390
+ return nothing;
1391
+ }
1392
+ const { previewParams } = tab.session.ui;
1393
+ return html`
1394
+ ${[...names].map(
1395
+ (name) => html`
1396
+ <sp-picker
1397
+ size="s"
1398
+ quiet
1399
+ class="pc-param"
1400
+ label=${name}
1401
+ title=${`Preview value for [${name}]`}
1402
+ value=${previewParams?.[name] ?? ""}
1403
+ @change=${(e: Event) => {
1404
+ const { value } = e.target as HTMLInputElement;
1405
+ // Through the registry, like every control in the popover beside this one.
1406
+ runContextCommand(paneId, "canvas.setRouteParam", { name, value });
1407
+ }}
1408
+ >
1409
+ ${(values?.[name] ?? []).map(
1410
+ (v: string) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`,
1411
+ )}
1412
+ </sp-picker>
1413
+ `,
1414
+ )}
1415
+ `;
1416
+ }
1417
+
1418
+ // ─── Component test-prop fields ──────────────────────────────────────────────
1419
+ // The previewParams mirror for component docs: one small field per prop entry (the doc's
1420
+ // Plain-data state entries), committed on change so typing never re-renders the bar mid-edit. A
1421
+ // Value parses as JSON when it can (numbers, booleans, arrays) and falls back to the raw string;
1422
+ // Clearing a field removes the override so the prop returns to its authored default.
1423
+
1424
+ /**
1425
+ * @param {string} raw
1426
+ * @returns {JsonValue}
1427
+ */
1428
+ function parsePropValue(raw: string): JsonValue {
1429
+ try {
1430
+ return JSON.parse(raw) as JsonValue;
1431
+ } catch {
1432
+ return raw;
1433
+ }
1434
+ }
1435
+
1436
+ /**
1437
+ * @param {Tab} tab
1438
+ * @param {string} paneId
1439
+ * @returns {TemplateResult | typeof nothing}
1440
+ */
1441
+ function propFieldsTpl(tab: Tab, paneId: string): TemplateResult | typeof nothing {
1442
+ const doc = tab.doc.document;
1443
+ if (!isComponentDoc(doc)) {
1444
+ return nothing;
1445
+ }
1446
+ const entries = componentPropEntries(doc);
1447
+ if (entries.length === 0) {
1448
+ return nothing;
1449
+ }
1450
+ const { previewProps } = tab.session.ui;
1451
+ const display = (v: JsonValue | undefined) =>
1452
+ v === undefined ? "" : typeof v === "string" ? v : JSON.stringify(v);
1453
+ return html`
1454
+ ${entries.map(
1455
+ ({ name }) => html`
1456
+ <sp-textfield
1457
+ size="s"
1458
+ quiet
1459
+ class="pc-prop"
1460
+ data-jx-region=${paneRegion(paneId, `prop:${name}`)}
1461
+ placeholder=${name}
1462
+ title=${`Test value for ${name}`}
1463
+ .value=${display(previewProps?.[name])}
1464
+ @change=${(e: Event) => {
1465
+ const raw = (e.target as HTMLInputElement).value;
1466
+ // The command owns the write; this control owns only the parse, because "what a typed
1467
+ // String means" is a fact about a text field and not about the value.
1468
+ runContextCommand(paneId, "canvas.setTestProp", {
1469
+ name,
1470
+ value: raw === "" ? null : parsePropValue(raw),
1471
+ });
1472
+ }}
1473
+ ></sp-textfield>
1474
+ `,
1475
+ )}
1476
+ `;
1477
+ }