@jxsuite/studio 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (607) hide show
  1. package/README.md +1 -1
  2. package/data/elements-meta.json +5 -15
  3. package/data/html-meta.json +51 -6
  4. package/dist/chunks/{2020-n6tfwnda.js → 2020-g79n01v9.js} +14 -14
  5. package/dist/chunks/action-group-sp6tna8v.js +118 -0
  6. package/dist/chunks/adapter-nj7tnjfn.js +13 -0
  7. package/dist/chunks/cf-account-picker-fyf63pmy.js +495 -0
  8. package/dist/chunks/cf-settings-71bv5zn9.js +20 -0
  9. package/dist/chunks/color-area-v17df7b8.js +154 -0
  10. package/dist/chunks/color-field-anwybnkn.js +208 -0
  11. package/dist/chunks/color-slider-44ez96fc.js +79 -0
  12. package/dist/chunks/color-xgg3kfw7.js +35 -0
  13. package/dist/chunks/combobox-5e7jev23.js +236 -0
  14. package/dist/chunks/components-2w1f35qs.js +18 -0
  15. package/dist/chunks/content-source-3wqeas45.js +46 -0
  16. package/dist/chunks/convert-to-repeater-d0cgnb6d.js +639 -0
  17. package/dist/chunks/dialog-s890b918.js +85 -0
  18. package/dist/chunks/editor-3qhbg51x.js +35 -0
  19. package/dist/chunks/entry-commands-d6g50j7s.js +42 -0
  20. package/dist/chunks/extension-sections-fy7q4fe9.js +651 -0
  21. package/dist/chunks/file-ops-3btj6st0.js +47 -0
  22. package/dist/chunks/files-ef8fgs3w.js +72 -0
  23. package/dist/chunks/format-host-3c33nwr8.js +40 -0
  24. package/dist/chunks/fs-events-13tj6sex.js +18 -0
  25. package/dist/chunks/git-panel-4wp2rn9b.js +49 -0
  26. package/dist/chunks/icons-yy10wk9r.js +402 -0
  27. package/dist/chunks/iframe-entry-15g3n6xv.js +114 -0
  28. package/dist/chunks/iframe-entry-b27vq81n.js +203 -0
  29. package/dist/chunks/iframe-entry-nbyxee1y.js +154 -0
  30. package/dist/chunks/iframe-host-jgesd924.js +115 -0
  31. package/dist/chunks/{index-wcnt3r9g.js → index-mdmyx6s2.js} +11 -11
  32. package/dist/chunks/index-n4j7qxyr.js +109 -0
  33. package/dist/chunks/{javascript-q8kpmfah.js → javascript-xvrfrpkb.js} +8 -9
  34. package/dist/chunks/{jsonMode-dkr040ga.js → jsonMode-tyx2kj6p.js} +526 -527
  35. package/dist/chunks/jx-validate-j0wxk72w.js +15 -0
  36. package/dist/chunks/layers-1nq5evsh.js +29 -0
  37. package/dist/chunks/layers-panel-ar2gh90g.js +40 -0
  38. package/dist/chunks/listbox-6v2ac08j.js +44 -0
  39. package/dist/chunks/lit-html-m244w9nh.js +17 -0
  40. package/dist/chunks/media-picker-2qbetq73.js +33 -0
  41. package/dist/chunks/menu-q99sy9rf.js +285 -0
  42. package/dist/chunks/menu-rf7n72mf.js +14 -0
  43. package/dist/chunks/{monaco-binding-rj6xpx9y.js → monaco-binding-sw6en4dw.js} +12 -15
  44. package/dist/chunks/monaco-buffer-bhy3hb6m.js +24 -0
  45. package/dist/chunks/{monaco-setup-vx9bhp1d.js → monaco-setup-8r62fj6y.js} +27 -37
  46. package/dist/chunks/notify-628t0anh.js +21 -0
  47. package/dist/chunks/number-field-5cndyft8.js +72 -0
  48. package/dist/chunks/parse-0zx14y15.js +10 -0
  49. package/dist/chunks/platform-ts9cg1a4.js +15 -0
  50. package/dist/chunks/popover-47z3ytva.js +25 -0
  51. package/dist/chunks/publish-panel-98f7sr3g.js +27 -0
  52. package/dist/chunks/quick-search-srzz83k2.js +45 -0
  53. package/dist/chunks/references-n553f0e1.js +26 -0
  54. package/dist/chunks/right-panel-rrgx2dn3.js +45 -0
  55. package/dist/chunks/section-registry-5pq32t0q.js +25 -0
  56. package/dist/chunks/select-2r16ez41.js +49 -0
  57. package/dist/chunks/split-g7nm1rgb.js +191 -0
  58. package/dist/chunks/studio-0hqbpbd0.js +62 -0
  59. package/dist/chunks/studio-18avnhjp.js +2430 -0
  60. package/dist/chunks/{studio-kryvsw6j.js → studio-1k325pwf.js} +48 -48
  61. package/dist/chunks/{studio-msd847zn.js → studio-25hkdrkw.js} +357 -86
  62. package/dist/chunks/studio-26d7myd1.js +258 -0
  63. package/dist/chunks/{studio-4r1pv6zf.js → studio-38emantv.js} +22 -5
  64. package/dist/chunks/studio-3gxv60tn.js +422 -0
  65. package/dist/chunks/{studio-fbf92ckf.js → studio-3hxkd92t.js} +275 -163
  66. package/dist/chunks/studio-4m0haseg.js +185 -0
  67. package/dist/chunks/{studio-ynxwkrzd.js → studio-4wbb9ct0.js} +1953 -1785
  68. package/dist/chunks/{studio-1wagb550.js → studio-5523bpzt.js} +11 -11
  69. package/dist/chunks/{studio-x4qkdat5.js → studio-57mf66w2.js} +1749 -2481
  70. package/dist/chunks/studio-5jy81t3s.js +17463 -0
  71. package/dist/chunks/studio-5rt7sqa0.js +1168 -0
  72. package/dist/chunks/studio-5te5tqng.js +81 -0
  73. package/dist/chunks/{studio-p4ngc046.js → studio-6ewqv390.js} +9 -9
  74. package/dist/chunks/{studio-9ab8gqsm.js → studio-6nzaktvy.js} +7 -7
  75. package/dist/chunks/studio-6xa21ej9.js +28 -0
  76. package/dist/chunks/{studio-5ehhackm.js → studio-727xh0e3.js} +19 -19
  77. package/dist/chunks/{studio-ahn3pdgx.js → studio-79y5sz51.js} +52 -38
  78. package/dist/chunks/{studio-gcye7qka.js → studio-7e11jpa9.js} +21 -21
  79. package/dist/chunks/{studio-q7v10cg2.js → studio-7r42c6ca.js} +17 -7
  80. package/dist/chunks/studio-7xptrg0j.js +954 -0
  81. package/dist/chunks/{studio-cft9r4r7.js → studio-81390n6f.js} +22 -22
  82. package/dist/chunks/{studio-q43a5y9h.js → studio-8jn5yhyn.js} +16 -16
  83. package/dist/chunks/studio-8qtrpzr3.js +839 -0
  84. package/dist/chunks/{studio-dg7zyt5x.js → studio-98b3z07n.js} +1 -1
  85. package/dist/chunks/studio-9xxz1q69.js +1356 -0
  86. package/dist/chunks/studio-akbww0j5.js +13615 -0
  87. package/dist/chunks/{studio-x813zp48.js → studio-arfpe389.js} +30 -30
  88. package/dist/chunks/{studio-trxrabqf.js → studio-az3zd49t.js} +475 -283
  89. package/dist/chunks/studio-cpvb2ed1.js +1171 -0
  90. package/dist/chunks/{studio-f3wqjtk5.js → studio-d0300636.js} +5076 -18
  91. package/dist/chunks/{studio-bgxt93ga.js → studio-d055c37e.js} +13 -13
  92. package/dist/chunks/studio-d1ja2x9r.js +362 -0
  93. package/dist/chunks/{studio-b9b6t1dg.js → studio-e1f24eff.js} +5664 -5666
  94. package/dist/chunks/{studio-3bqpr0zh.js → studio-em9nw8eh.js} +299 -110
  95. package/dist/chunks/studio-erdatzvp.js +1190 -0
  96. package/dist/chunks/{studio-g1wdche0.js → studio-esrxgwh6.js} +80 -80
  97. package/dist/chunks/studio-fabxk6r9.js +2811 -0
  98. package/dist/chunks/{studio-3n4ndrzh.js → studio-h4nvnjvz.js} +303 -303
  99. package/dist/chunks/{studio-dm19q43r.js → studio-hhvs4sdg.js} +10 -10
  100. package/dist/chunks/{studio-q9jd1a60.js → studio-kcgff4r0.js} +15 -18
  101. package/dist/chunks/{studio-685s0ytq.js → studio-nqgawfks.js} +572 -572
  102. package/dist/chunks/{studio-xrapxwwc.js → studio-p1dw8wfw.js} +55 -29
  103. package/dist/chunks/studio-pm6hgy7r.js +4763 -0
  104. package/dist/chunks/studio-q0a3yk4n.js +33 -0
  105. package/dist/chunks/{studio-wcy8trfx.js → studio-q3x06vmj.js} +23 -22
  106. package/dist/chunks/studio-qahxvnan.js +111 -0
  107. package/dist/chunks/{studio-53n8bp04.js → studio-r6xmmtfh.js} +1 -1
  108. package/dist/chunks/{studio-2b946z26.js → studio-rkstz940.js} +3658 -6134
  109. package/dist/chunks/{studio-tvgyw9xw.js → studio-rnyf3f8e.js} +83 -250
  110. package/dist/chunks/studio-rpyah2ew.js +2805 -0
  111. package/dist/chunks/studio-rs76k67r.js +12 -0
  112. package/dist/chunks/{studio-jkftxyvz.js → studio-s6z853aq.js} +5858 -2152
  113. package/dist/chunks/{studio-6yw4y3my.js → studio-sgdc5pfc.js} +8 -8
  114. package/dist/chunks/studio-tb3ev01j.js +763 -0
  115. package/dist/chunks/{studio-t5wqwzdh.js → studio-tjjk7jvk.js} +1 -1
  116. package/dist/chunks/studio-vhv5tgxm.js +51 -0
  117. package/dist/chunks/{studio-qjy2z9bv.js → studio-vz68yn9e.js} +32 -137
  118. package/dist/chunks/studio-wqpngpsk.js +174 -0
  119. package/dist/chunks/{studio-b55y6ewx.js → studio-x3rty6tr.js} +58 -58
  120. package/dist/chunks/studio-xsykpvsh.js +8448 -0
  121. package/dist/chunks/{studio-92ab2jqe.js → studio-y9zedyjn.js} +14 -14
  122. package/dist/chunks/studio-yxa81f45.js +12 -0
  123. package/dist/chunks/swatch-group-t0n6h2rn.js +139 -0
  124. package/dist/chunks/tabs-wbp1cdws.js +156 -0
  125. package/dist/chunks/textfield-abk6qyee.js +73 -0
  126. package/dist/chunks/toast-780pvfbv.js +27 -0
  127. package/dist/chunks/toast-host-3sqv46sn.js +116 -0
  128. package/dist/chunks/toolbar-xbh5bhtr.js +194 -0
  129. package/dist/chunks/tooltip-eptb614w.js +369 -0
  130. package/dist/chunks/tree-a73p0pqm.js +367 -0
  131. package/dist/chunks/{tsMode-76kr53q0.js → tsMode-jy4m86rv.js} +83 -85
  132. package/dist/chunks/workspace-wrew8zk4.js +69 -0
  133. package/dist/chunks/{ws-client-6shc5rze.js → ws-client-ny4zc51r.js} +20 -22
  134. package/dist/iframe-entry.js +1780 -667
  135. package/dist/studio.js +52819 -36480
  136. package/dist/workers/editor.worker.js +1040 -1085
  137. package/dist/workers/json.worker.js +1946 -1991
  138. package/dist/workers/ts.worker.js +4960 -5005
  139. package/index.html +1 -0
  140. package/package.json +18 -52
  141. package/src/about/about-modal.ts +72 -123
  142. package/src/browse/library-commands.ts +20 -49
  143. package/src/browse/library-layouts.ts +87 -218
  144. package/src/browse/library-pane.ts +406 -438
  145. package/src/canvas/canvas-helpers.ts +0 -49
  146. package/src/canvas/canvas-live-render.ts +25 -2
  147. package/src/canvas/canvas-render.ts +666 -231
  148. package/src/canvas/canvas-surface.ts +0 -1
  149. package/src/canvas/canvas-utils.ts +310 -117
  150. package/src/canvas/dialog-path.ts +76 -0
  151. package/src/canvas/dialog-state.ts +67 -0
  152. package/src/canvas/diff-marks.ts +252 -0
  153. package/src/canvas/diff-toolbar.ts +349 -0
  154. package/src/canvas/diff-view.ts +123 -0
  155. package/src/canvas/edit-width-drag.ts +187 -0
  156. package/src/canvas/edit-width.ts +150 -0
  157. package/src/canvas/iframe-entry.ts +125 -8
  158. package/src/canvas/iframe-host.ts +271 -15
  159. package/src/canvas/iframe-interaction.ts +133 -13
  160. package/src/canvas/iframe-patch.ts +7 -1
  161. package/src/canvas/iframe-position.ts +5 -6
  162. package/src/canvas/iframe-protocol.ts +106 -5
  163. package/src/canvas/iframe-render.ts +373 -5
  164. package/src/canvas/iframe-slash.ts +2 -2
  165. package/src/canvas/iframe-subtree.ts +5 -8
  166. package/src/canvas/path-mapping.ts +25 -3
  167. package/src/canvas/popover-path.ts +106 -0
  168. package/src/canvas/popover-state.ts +155 -0
  169. package/src/canvas/surface-registry.ts +21 -10
  170. package/src/collab/presence-chips.ts +51 -73
  171. package/src/commands/app-commands.ts +10 -0
  172. package/src/commands/budget.ts +12 -0
  173. package/src/commands/command-args.ts +369 -6
  174. package/src/commands/defaults.ts +44 -14
  175. package/src/commands/live-context.ts +11 -5
  176. package/src/commands/reference.ts +12 -2
  177. package/src/commands/registry.ts +165 -8
  178. package/src/content/entry-commands.ts +16 -23
  179. package/src/content/entry-editor.ts +144 -101
  180. package/src/editor/canvas-slash-bridge.ts +2 -2
  181. package/src/editor/context-menu.ts +49 -200
  182. package/src/editor/convert-to-component.ts +42 -82
  183. package/src/editor/convert-to-repeater.ts +132 -173
  184. package/src/editor/inline-edit.ts +11 -0
  185. package/src/editor/shortcuts.ts +65 -26
  186. package/src/editor/slash-menu.ts +150 -157
  187. package/src/files/file-ops.ts +117 -11
  188. package/src/files/files.ts +943 -660
  189. package/src/files/json-layout.ts +474 -0
  190. package/src/files/serialize-document.ts +8 -1
  191. package/src/format/convert-file.ts +16 -10
  192. package/src/format/format-host.ts +50 -1
  193. package/src/github/github-auth.ts +71 -29
  194. package/src/github/github-publish.ts +57 -65
  195. package/src/grid/cell-editors.ts +46 -4
  196. package/src/grid/cell-popovers.ts +65 -62
  197. package/src/grid/grid-open.ts +86 -64
  198. package/src/grid/grid-panel.ts +390 -469
  199. package/src/grid/redirects-grid.ts +40 -44
  200. package/src/hosting/layout.ts +2 -0
  201. package/src/i18n/i18n-commands.ts +127 -62
  202. package/src/media/media-pane.ts +83 -133
  203. package/src/new-project/add-repo-modal.ts +125 -176
  204. package/src/new-project/import-tab.ts +181 -252
  205. package/src/new-project/location-fields.ts +93 -160
  206. package/src/new-project/new-project-modal.ts +376 -367
  207. package/src/packages/jxsuite-update.ts +76 -18
  208. package/src/panels/activity-panel.ts +137 -104
  209. package/src/panels/ai-chat/chat-markdown.ts +15 -13
  210. package/src/panels/ai-chat/chat-view.ts +307 -472
  211. package/src/panels/ai-chat/composer.ts +175 -163
  212. package/src/panels/ai-chat/sessions-view.ts +31 -62
  213. package/src/panels/ai-panel.ts +210 -185
  214. package/src/panels/block-action-bar.ts +437 -643
  215. package/src/panels/bottom-dock.ts +112 -47
  216. package/src/panels/chat-panel.ts +19 -14
  217. package/src/panels/data-explorer.ts +141 -102
  218. package/src/panels/data-grid.ts +183 -150
  219. package/src/panels/dnd.ts +75 -20
  220. package/src/panels/editors.ts +26 -19
  221. package/src/panels/elements-panel.ts +243 -186
  222. package/src/panels/empty-state.ts +26 -42
  223. package/src/panels/events-panel.ts +950 -729
  224. package/src/panels/formula-workspace.ts +434 -207
  225. package/src/panels/frontmatter-fields.ts +185 -153
  226. package/src/panels/frontmatter-panel.ts +411 -187
  227. package/src/panels/git-diff-open.ts +148 -0
  228. package/src/panels/git-panel.ts +679 -605
  229. package/src/panels/head-panel.ts +617 -435
  230. package/src/panels/i18n-panel.ts +153 -133
  231. package/src/panels/imports-panel.ts +388 -414
  232. package/src/panels/jump-bar.ts +174 -147
  233. package/src/panels/layers-panel.ts +785 -671
  234. package/src/panels/left-panel.ts +166 -94
  235. package/src/panels/navigator-panels.ts +1 -1
  236. package/src/panels/pane-context.ts +529 -677
  237. package/src/panels/pane-grid.ts +265 -241
  238. package/src/panels/panel-registry.ts +15 -4
  239. package/src/panels/problems-panel.ts +177 -99
  240. package/src/panels/properties-panel.ts +1525 -966
  241. package/src/panels/provenance.ts +31 -113
  242. package/src/panels/quick-search.ts +217 -172
  243. package/src/panels/right-panel.ts +135 -169
  244. package/src/panels/seo-modal.ts +346 -167
  245. package/src/panels/settings-menu.ts +109 -646
  246. package/src/panels/settings-pane.ts +102 -86
  247. package/src/panels/signals-panel.ts +1318 -1172
  248. package/src/panels/statement-editor.ts +556 -428
  249. package/src/panels/style-panel.ts +1367 -630
  250. package/src/panels/style-utils.ts +36 -32
  251. package/src/panels/stylebook-doc.ts +30 -73
  252. package/src/panels/stylebook-layers-panel.ts +202 -109
  253. package/src/panels/stylebook-panel.ts +124 -88
  254. package/src/panels/tab-strip.ts +385 -287
  255. package/src/platforms/cloud.ts +106 -16
  256. package/src/platforms/devserver.ts +13 -0
  257. package/src/publish/deploy-checklist.ts +124 -78
  258. package/src/publish/publish-commands.ts +4 -12
  259. package/src/publish/publish-panel.ts +134 -328
  260. package/src/services/a11y-report.ts +100 -43
  261. package/src/services/ai-command-tools.ts +468 -0
  262. package/src/services/ai-session-store.ts +6 -0
  263. package/src/services/ai-system-prompt.ts +110 -18
  264. package/src/services/ai-tools.ts +28 -143
  265. package/src/services/ai-write-report.ts +174 -0
  266. package/src/services/document-assistant.ts +50 -2
  267. package/src/services/idle.ts +16 -14
  268. package/src/services/live-surfaces.ts +237 -0
  269. package/src/services/model-uri.ts +75 -0
  270. package/src/services/notify.ts +63 -6
  271. package/src/services/popover-report.ts +325 -0
  272. package/src/services/surface-registry.ts +56 -0
  273. package/src/services/tool-executor.ts +6 -0
  274. package/src/settings/contexts-section.ts +301 -299
  275. package/src/settings/contributed-section.ts +361 -222
  276. package/src/settings/css-vars-editor.ts +320 -325
  277. package/src/settings/defs-editor.ts +572 -535
  278. package/src/settings/dependencies-editor.ts +79 -107
  279. package/src/settings/extension-commands.ts +379 -0
  280. package/src/settings/extension-rows.ts +113 -0
  281. package/src/settings/extensions-section.ts +287 -0
  282. package/src/settings/general-settings.ts +130 -181
  283. package/src/settings/head-editor.ts +10 -305
  284. package/src/settings/locales-section.ts +146 -196
  285. package/src/settings/preferences-accounts.ts +1 -1
  286. package/src/settings/preferences-dialog.ts +350 -330
  287. package/src/settings/project-sections.ts +108 -181
  288. package/src/settings/schema-field-ui.ts +11 -399
  289. package/src/settings/section-registry.ts +8 -1
  290. package/src/settings/settings-document.ts +31 -29
  291. package/src/shell/tree.ts +42 -87
  292. package/src/shell.ts +48 -36
  293. package/src/studio.ts +90 -67
  294. package/src/style/token-ref.ts +0 -39
  295. package/src/surfaces/about.json +263 -0
  296. package/src/surfaces/about.ts +163 -0
  297. package/src/surfaces/add-repo.json +486 -0
  298. package/src/surfaces/add-repo.ts +236 -0
  299. package/src/surfaces/ai-chat.json +2185 -0
  300. package/src/surfaces/ai-chat.ts +549 -0
  301. package/src/surfaces/ai-credentials-form.json +332 -0
  302. package/src/surfaces/ai-credentials-form.ts +151 -0
  303. package/src/surfaces/ai-managed-connect.json +108 -0
  304. package/src/surfaces/ai-managed-connect.ts +123 -0
  305. package/src/surfaces/ai-model-picker.json +114 -0
  306. package/src/surfaces/ai-model-picker.ts +139 -0
  307. package/src/surfaces/block-action-bar.json +640 -0
  308. package/src/surfaces/block-action-bar.ts +368 -0
  309. package/src/surfaces/bottom-dock.json +154 -0
  310. package/src/surfaces/bottom-dock.ts +125 -0
  311. package/src/surfaces/canvas-stage.json +444 -0
  312. package/src/surfaces/canvas-stage.ts +253 -0
  313. package/src/surfaces/cf-account-picker.json +300 -0
  314. package/src/surfaces/cf-account-picker.ts +214 -0
  315. package/src/surfaces/commandbar.json +984 -0
  316. package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +414 -389
  317. package/src/surfaces/convert-repeater.json +309 -0
  318. package/src/surfaces/convert-repeater.ts +276 -0
  319. package/src/surfaces/data-actions.json +157 -0
  320. package/src/surfaces/data-actions.ts +115 -0
  321. package/src/surfaces/dialog.json +289 -0
  322. package/src/surfaces/dialog.ts +343 -0
  323. package/src/surfaces/diff-toolbar.json +266 -0
  324. package/src/surfaces/diff-toolbar.ts +139 -0
  325. package/src/surfaces/doc-header.json +739 -0
  326. package/src/surfaces/doc-header.ts +223 -0
  327. package/src/surfaces/empty-state.json +152 -0
  328. package/src/surfaces/empty-state.ts +173 -0
  329. package/src/surfaces/entry-editor.json +281 -0
  330. package/src/surfaces/entry-editor.ts +191 -0
  331. package/src/surfaces/expression-editor.json +977 -0
  332. package/src/surfaces/expression-editor.ts +266 -0
  333. package/src/surfaces/files-panel.json +592 -0
  334. package/src/surfaces/files-panel.ts +301 -0
  335. package/src/surfaces/formula-palette.json +364 -0
  336. package/src/surfaces/formula-palette.ts +340 -0
  337. package/src/surfaces/git-panel.json +1822 -0
  338. package/src/surfaces/git-panel.ts +272 -0
  339. package/src/surfaces/github-auth.json +112 -0
  340. package/src/surfaces/github-auth.ts +139 -0
  341. package/src/surfaces/github-publish.json +206 -0
  342. package/src/surfaces/github-publish.ts +169 -0
  343. package/src/surfaces/grid-cell.json +218 -0
  344. package/src/surfaces/grid-cell.ts +120 -0
  345. package/src/surfaces/grid-open.json +147 -0
  346. package/src/surfaces/grid-open.ts +87 -0
  347. package/src/surfaces/grid-panel.json +706 -0
  348. package/src/surfaces/grid-panel.ts +206 -0
  349. package/src/surfaces/grid-replace.json +179 -0
  350. package/src/surfaces/grid-replace.ts +89 -0
  351. package/src/surfaces/grid-views.json +446 -0
  352. package/src/surfaces/grid-views.ts +172 -0
  353. package/src/surfaces/inspector-dock.json +187 -0
  354. package/src/surfaces/inspector-dock.ts +161 -0
  355. package/src/surfaces/jump-bar.json +243 -0
  356. package/src/surfaces/jump-bar.ts +129 -0
  357. package/src/surfaces/jxsuite-update.json +146 -0
  358. package/src/surfaces/jxsuite-update.ts +149 -0
  359. package/src/surfaces/library-pane.json +1509 -0
  360. package/src/surfaces/library-pane.ts +363 -0
  361. package/src/surfaces/logic-panel.json +1949 -0
  362. package/src/surfaces/logic-panel.ts +369 -0
  363. package/src/surfaces/logic-workspace.json +685 -0
  364. package/src/surfaces/logic-workspace.ts +238 -0
  365. package/src/surfaces/media-browser.json +303 -0
  366. package/src/surfaces/media-browser.ts +145 -0
  367. package/src/surfaces/media-field.json +163 -0
  368. package/src/surfaces/media-field.ts +116 -0
  369. package/src/surfaces/media-pane.json +513 -0
  370. package/src/surfaces/media-pane.ts +197 -0
  371. package/src/surfaces/menu.json +232 -0
  372. package/src/surfaces/menu.ts +375 -0
  373. package/src/surfaces/navigator-dock.json +113 -0
  374. package/src/surfaces/navigator-dock.ts +130 -0
  375. package/src/surfaces/new-project.json +1654 -0
  376. package/src/surfaces/new-project.ts +411 -0
  377. package/src/surfaces/palette.json +535 -0
  378. package/src/surfaces/pane-context.json +1497 -0
  379. package/src/surfaces/pane-context.ts +513 -0
  380. package/src/surfaces/pane-grid.json +188 -0
  381. package/src/surfaces/pane-grid.ts +250 -0
  382. package/src/surfaces/panel-activity.json +563 -0
  383. package/src/surfaces/panel-activity.ts +187 -0
  384. package/src/surfaces/panel-data.json +141 -0
  385. package/src/surfaces/panel-data.ts +156 -0
  386. package/src/surfaces/panel-deploy-checklist.json +283 -0
  387. package/src/surfaces/panel-deploy-checklist.ts +175 -0
  388. package/src/surfaces/panel-elements.json +367 -0
  389. package/src/surfaces/panel-elements.ts +175 -0
  390. package/src/surfaces/panel-i18n.json +569 -0
  391. package/src/surfaces/panel-i18n.ts +212 -0
  392. package/src/surfaces/panel-imports.json +780 -0
  393. package/src/surfaces/panel-imports.ts +209 -0
  394. package/src/surfaces/panel-outline.json +734 -0
  395. package/src/surfaces/panel-outline.ts +326 -0
  396. package/src/surfaces/panel-page.json +1017 -0
  397. package/src/surfaces/panel-page.ts +240 -0
  398. package/src/surfaces/panel-problems.json +497 -0
  399. package/src/surfaces/panel-problems.ts +157 -0
  400. package/src/surfaces/panel-signals.json +1973 -0
  401. package/src/surfaces/panel-signals.ts +436 -0
  402. package/src/surfaces/panel-stylebook-layers.json +317 -0
  403. package/src/surfaces/panel-stylebook-layers.ts +155 -0
  404. package/src/surfaces/preferences.json +850 -0
  405. package/src/surfaces/preferences.ts +332 -0
  406. package/src/surfaces/progress-modal.json +110 -0
  407. package/src/surfaces/progress-modal.ts +150 -0
  408. package/src/surfaces/properties-panel.json +1578 -0
  409. package/src/surfaces/properties-panel.ts +337 -0
  410. package/src/surfaces/publish.json +945 -0
  411. package/src/surfaces/publish.ts +392 -0
  412. package/src/surfaces/push-plan.json +110 -0
  413. package/src/surfaces/push-plan.ts +87 -0
  414. package/src/surfaces/rail.json +255 -0
  415. package/src/surfaces/rail.ts +206 -0
  416. package/src/surfaces/reference-field.json +191 -0
  417. package/src/surfaces/reference-field.ts +168 -0
  418. package/src/surfaces/schema-builder.json +1011 -0
  419. package/src/surfaces/schema-builder.ts +181 -0
  420. package/src/surfaces/schema-form.json +1013 -0
  421. package/src/surfaces/schema-form.ts +287 -0
  422. package/src/surfaces/secret-field.json +60 -0
  423. package/src/surfaces/secret-field.ts +146 -0
  424. package/src/surfaces/seo.json +932 -0
  425. package/src/surfaces/seo.ts +334 -0
  426. package/src/surfaces/settings-contexts.json +538 -0
  427. package/src/surfaces/settings-contexts.ts +196 -0
  428. package/src/surfaces/settings-contributed.json +489 -0
  429. package/src/surfaces/settings-contributed.ts +199 -0
  430. package/src/surfaces/settings-css-vars.json +820 -0
  431. package/src/surfaces/settings-css-vars.ts +199 -0
  432. package/src/surfaces/settings-defs.json +1363 -0
  433. package/src/surfaces/settings-defs.ts +232 -0
  434. package/src/surfaces/settings-deploy.json +108 -0
  435. package/src/surfaces/settings-deploy.ts +135 -0
  436. package/src/surfaces/settings-extensions.json +486 -0
  437. package/src/surfaces/settings-extensions.ts +201 -0
  438. package/src/surfaces/settings-head.json +779 -0
  439. package/src/surfaces/settings-head.ts +397 -0
  440. package/src/surfaces/settings-locales.json +420 -0
  441. package/src/surfaces/settings-locales.ts +137 -0
  442. package/src/surfaces/settings-overview.json +423 -0
  443. package/src/surfaces/settings-overview.ts +218 -0
  444. package/src/surfaces/settings-packages.json +549 -0
  445. package/src/surfaces/settings-packages.ts +158 -0
  446. package/src/surfaces/settings-pane.json +147 -0
  447. package/src/surfaces/settings-pane.ts +150 -0
  448. package/src/surfaces/settings-rawjson.json +104 -0
  449. package/src/surfaces/settings-rawjson.ts +107 -0
  450. package/src/surfaces/shell.json +99 -0
  451. package/src/surfaces/shell.ts +30 -0
  452. package/src/surfaces/slash-menu.json +180 -0
  453. package/src/surfaces/slash-menu.ts +272 -0
  454. package/src/surfaces/statements.json +607 -0
  455. package/src/surfaces/statements.ts +238 -0
  456. package/src/surfaces/statusbar.json +94 -0
  457. package/src/{panels → surfaces}/statusbar.ts +163 -90
  458. package/src/surfaces/style-panel.json +1643 -0
  459. package/src/surfaces/style-panel.ts +338 -0
  460. package/src/surfaces/stylebook-chrome.json +122 -0
  461. package/src/surfaces/stylebook-chrome.ts +122 -0
  462. package/src/surfaces/tab-strip.json +448 -0
  463. package/src/surfaces/tab-strip.ts +200 -0
  464. package/src/surfaces/target-line.json +419 -0
  465. package/src/surfaces/target-line.ts +349 -0
  466. package/src/surfaces/toasts.json +189 -0
  467. package/src/surfaces/welcome.json +571 -0
  468. package/src/surfaces/welcome.ts +338 -0
  469. package/src/tabs/project-config.ts +71 -15
  470. package/src/tabs/tab.ts +52 -1
  471. package/src/types.ts +22 -0
  472. package/src/ui/ai-credentials-form.ts +104 -100
  473. package/src/ui/ai-managed-connect.ts +54 -36
  474. package/src/ui/ai-model-picker.ts +75 -47
  475. package/src/ui/cf-account-picker.ts +76 -141
  476. package/src/ui/color-selector.ts +80 -301
  477. package/src/ui/dynamic-slot.ts +43 -283
  478. package/src/ui/expression-editor.ts +797 -576
  479. package/src/ui/field-input.ts +8 -181
  480. package/src/ui/form-controls.ts +838 -366
  481. package/src/ui/formula-chips.ts +80 -85
  482. package/src/ui/kit.ts +33 -0
  483. package/src/ui/layers.ts +293 -593
  484. package/src/ui/media-picker.ts +203 -284
  485. package/src/ui/panel-resize.ts +251 -64
  486. package/src/ui/popover-surface.ts +153 -0
  487. package/src/ui/progress-modal.ts +22 -60
  488. package/src/ui/regions.ts +13 -7
  489. package/src/ui/schema-form.ts +906 -440
  490. package/src/ui/surface.ts +122 -0
  491. package/src/ui/unit-selector.ts +15 -131
  492. package/src/ui/value-source.ts +14 -0
  493. package/src/ui/virtual-window.ts +88 -9
  494. package/src/utils/canvas-media.ts +72 -0
  495. package/src/utils/edit-display.ts +7 -2
  496. package/src/utils/element-selectors.ts +69 -0
  497. package/src/utils/link-target.ts +2 -2
  498. package/src/utils/studio-utils.ts +0 -10
  499. package/src/view.ts +15 -0
  500. package/src/workspace/pane-derive.ts +36 -14
  501. package/src/workspace/workspace.ts +15 -6
  502. package/styles/canvas.css +72 -467
  503. package/styles/forced-colors.css +48 -82
  504. package/styles/forced-colors.json +41 -0
  505. package/styles/inspector.css +2 -1301
  506. package/styles/overlays.css +96 -2823
  507. package/styles/panels.css +6 -1423
  508. package/styles/shell-frame.css +272 -0
  509. package/styles/shell-frame.json +227 -0
  510. package/styles/shell.css +0 -1568
  511. package/styles/tokens.css +65 -136
  512. package/styles/tokens.json +84 -0
  513. package/dist/chunks/Overlay.dev-etqjwyf9.js +0 -23
  514. package/dist/chunks/adapter-jkcyhgtr.js +0 -14
  515. package/dist/chunks/cf-account-picker-en86v7x7.js +0 -163
  516. package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
  517. package/dist/chunks/components-zmx10t62.js +0 -21
  518. package/dist/chunks/content-source-48xg86wj.js +0 -49
  519. package/dist/chunks/convert-to-repeater-43tfn7cs.js +0 -268
  520. package/dist/chunks/editor-cptq5jp1.js +0 -37
  521. package/dist/chunks/entry-commands-e63f1xc0.js +0 -46
  522. package/dist/chunks/extension-sections-txv08xxg.js +0 -311
  523. package/dist/chunks/file-ops-wbh4hhsx.js +0 -48
  524. package/dist/chunks/files-3kfkwhk4.js +0 -93
  525. package/dist/chunks/flow-qbfgrpeh.js +0 -634
  526. package/dist/chunks/focus-trap.esm-9t2drp0g.js +0 -1025
  527. package/dist/chunks/focus-visible-6eg70z18.js +0 -167
  528. package/dist/chunks/format-host-w16q81tf.js +0 -43
  529. package/dist/chunks/fs-events-hrrg328g.js +0 -21
  530. package/dist/chunks/git-panel-532whr1y.js +0 -59
  531. package/dist/chunks/index-zqxmqxmh.js +0 -109
  532. package/dist/chunks/jx-validate-8er6gbxy.js +0 -19
  533. package/dist/chunks/layers-panel-07hm7g5j.js +0 -67
  534. package/dist/chunks/layers-q75tj2mf.js +0 -45
  535. package/dist/chunks/lit-html-j055mkh8.js +0 -22
  536. package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
  537. package/dist/chunks/notify-yjm79n4s.js +0 -30
  538. package/dist/chunks/parse-24d0gay3.js +0 -19
  539. package/dist/chunks/platform-ba1j1gbh.js +0 -16
  540. package/dist/chunks/publish-panel-v9st96ja.js +0 -29
  541. package/dist/chunks/quick-search-zccjp6gv.js +0 -66
  542. package/dist/chunks/references-3qx8fkyp.js +0 -29
  543. package/dist/chunks/right-panel-vm3b6kwy.js +0 -46
  544. package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
  545. package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +0 -18
  546. package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
  547. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
  548. package/dist/chunks/sp-tooltip.dev-4sastdnw.js +0 -15
  549. package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
  550. package/dist/chunks/studio-0a5qrh01.js +0 -3
  551. package/dist/chunks/studio-0bq6eh7v.js +0 -12
  552. package/dist/chunks/studio-18j3bmcz.js +0 -14
  553. package/dist/chunks/studio-1ngbejft.js +0 -181
  554. package/dist/chunks/studio-4b5z1x43.js +0 -424
  555. package/dist/chunks/studio-4n2b5gzb.js +0 -357
  556. package/dist/chunks/studio-4rzyrtjz.js +0 -426
  557. package/dist/chunks/studio-5fx3g9jg.js +0 -4325
  558. package/dist/chunks/studio-5myknm0a.js +0 -1903
  559. package/dist/chunks/studio-7afd21nw.js +0 -67
  560. package/dist/chunks/studio-7nte2hj4.js +0 -613
  561. package/dist/chunks/studio-8jpzqk5s.js +0 -128
  562. package/dist/chunks/studio-8q517dfr.js +0 -24
  563. package/dist/chunks/studio-950k0dx6.js +0 -6939
  564. package/dist/chunks/studio-9n4mrmnb.js +0 -184
  565. package/dist/chunks/studio-a0tswy0w.js +0 -58
  566. package/dist/chunks/studio-ahnzd9rg.js +0 -23
  567. package/dist/chunks/studio-cmb03bg5.js +0 -13890
  568. package/dist/chunks/studio-d1ks410h.js +0 -175
  569. package/dist/chunks/studio-ef49pnzx.js +0 -387
  570. package/dist/chunks/studio-eqq1w638.js +0 -113
  571. package/dist/chunks/studio-ew0djmhg.js +0 -341
  572. package/dist/chunks/studio-gfgb8pe6.js +0 -1652
  573. package/dist/chunks/studio-h4rxscfc.js +0 -10
  574. package/dist/chunks/studio-jc994dnr.js +0 -1066
  575. package/dist/chunks/studio-kyh9avvv.js +0 -28
  576. package/dist/chunks/studio-m3hwn7y1.js +0 -120
  577. package/dist/chunks/studio-mtfnaqs9.js +0 -659
  578. package/dist/chunks/studio-n6j9t10q.js +0 -318
  579. package/dist/chunks/studio-n8b9wqwy.js +0 -291
  580. package/dist/chunks/studio-qcy05dp8.js +0 -1743
  581. package/dist/chunks/studio-qq6jym04.js +0 -53
  582. package/dist/chunks/studio-rdx8jcxy.js +0 -83
  583. package/dist/chunks/studio-rn9ta2mz.js +0 -111
  584. package/dist/chunks/studio-t7qb9db8.js +0 -665
  585. package/dist/chunks/studio-tmc5emrw.js +0 -8
  586. package/dist/chunks/studio-z6nw2ek2.js +0 -18
  587. package/dist/chunks/studio-zczps70q.js +0 -378
  588. package/dist/chunks/studio-zqazsct2.js +0 -1823
  589. package/dist/chunks/workspace-fjx52ryv.js +0 -89
  590. package/src/panels/activity-bar.ts +0 -251
  591. package/src/panels/panel-scheduler.ts +0 -219
  592. package/src/panels/style-inputs.ts +0 -237
  593. package/src/panels/target-line.ts +0 -274
  594. package/src/panels/welcome-screen.ts +0 -346
  595. package/src/ui/button-group.ts +0 -102
  596. package/src/ui/choice-field.ts +0 -72
  597. package/src/ui/field-row.ts +0 -249
  598. package/src/ui/formula-palette.ts +0 -213
  599. package/src/ui/icons.ts +0 -52
  600. package/src/ui/jx-theme.ts +0 -130
  601. package/src/ui/spectrum.ts +0 -340
  602. package/src/ui/value-selector.ts +0 -174
  603. package/src/ui/widgets.ts +0 -172
  604. /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
  605. /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
  606. /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
  607. /package/dist/chunks/{tsMode-ef187txx.css → tsMode-hnxxb73h.css} +0 -0
@@ -1,13 +1,27 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Signals panel — signal/def helpers, signals template, CEM editors, plugin schema forms.
3
+ * The Data panel's flow — what a state entry IS, and every transaction that edits one.
4
4
  *
5
- * Extracted from studio.js to reduce file size.
5
+ * The markup is `surfaces/panel-signals.json`, mounted by `surfaces/panel-signals.ts`. What is left
6
+ * here is the half a document cannot hold: which category an entry falls in, what its badge and its
7
+ * one-line summary say, WHICH CONTROL each field of a `$prototype` gets, what a commit is worth,
8
+ * what a rename collides with, and the four foreign surfaces that arrive as islands.
9
+ *
10
+ * **The field list is a projection.** A `Request` entry and an `IndexedDB` entry differ only in the
11
+ * rows {@link signalsView} hands over, so a new prototype is a case here and no markup at all —
12
+ * which is why `renderDataSourceFields`, `renderFunctionFields`, `renderParameterEditorTemplate`
13
+ * and `renderEmitsEditorTemplate` are gone rather than moved: each was a template per shape, and
14
+ * there is one shape now.
15
+ *
16
+ * **A control hands back names, and this module looks up what they mean.** Every field is
17
+ * registered as a {@link FieldPlan} under its entry and key while the view is built, so
18
+ * `commitField("$items", "url", "/api")` finds the writer that knows `url` is a `Request`'s URL.
19
+ * Nothing crosses the seam but strings.
20
+ *
21
+ * @docs studio/logic/data
6
22
  */
7
23
 
8
- import { html, nothing } from "lit-html";
9
24
  import { displayTagName } from "@jxsuite/schema/guards";
10
- import { classMap } from "lit-html/directives/class-map.js";
11
25
  import { dynamicRouteParams } from "../page-params";
12
26
  import { projectState } from "../state";
13
27
  import type { JsonValue } from "../types";
@@ -19,31 +33,26 @@ import {
19
33
  mutateUpdateDef,
20
34
  transactDoc,
21
35
  } from "../tabs/transact";
22
- import { renderFieldRow } from "../ui/field-row";
23
- import { rawTextArea, spTextField } from "../ui/field-input";
24
- import {
25
- expressionHint,
26
- isActionExpression,
27
- renderExpressionEditor,
28
- } from "../ui/expression-editor";
29
- import { renderEmptyState } from "./empty-state";
36
+ import { expressionHint, isActionExpression, mountExpressionEditor } from "../ui/expression-editor";
30
37
  import {
31
38
  dataTypeLabel,
32
39
  expandedDataRows,
33
40
  isDataRowExpanded,
34
- renderDataTreeTemplate,
41
+ paintDataTree,
35
42
  setDataRowExpanded,
36
43
  unwrapSignal,
37
44
  } from "./data-explorer";
45
+ import { disposeDetachedDataTrees } from "../surfaces/panel-data";
38
46
  import { openLogicTarget } from "./formula-workspace";
39
47
  import { bindableSignalNames } from "./properties-panel";
40
- import { renderStatementEditor } from "./statement-editor";
48
+ import { mountStatementEditor } from "./statement-editor";
41
49
  import { NAVIGATOR_STATEMENTS_REGION } from "../ui/regions";
42
50
  import { livePreviewExpression } from "../services/live-preview";
43
- import { renderMediaPicker } from "../ui/media-picker";
51
+ import { mountMediaPicker } from "../ui/media-picker";
44
52
  import { renderOnly } from "../store";
45
- import { renderForm } from "../ui/schema-form";
53
+ import { mountSchemaForm } from "../ui/schema-form";
46
54
  import { resolveContextPointer } from "../services/context-resolver";
55
+ import { mountSignalsSurface } from "../surfaces/panel-signals";
47
56
  import type { JsonSchema } from "../ui/schema-form";
48
57
  import type { TabUi } from "../tabs/tab";
49
58
  import type {
@@ -56,9 +65,21 @@ import type {
56
65
  import { fetchPluginSchema, pluginSchemaCache } from "../services/code-services";
57
66
  import { getExtensions, loadExtensions } from "../format/format-host";
58
67
  import { optionalStringArg, stringProperty } from "../commands/command-args";
59
- import type { TemplateResult } from "lit-html";
60
68
  import type { AnyCommand, CommandRegistry } from "../commands/registry";
61
69
  import { isMediaFormat } from "../utils/studio-utils";
70
+ import type {
71
+ SignalCategoryView,
72
+ SignalCellRowView,
73
+ SignalCellView,
74
+ SignalChipView,
75
+ SignalFieldKind,
76
+ SignalFieldView,
77
+ SignalOptionGroup,
78
+ SignalRowView,
79
+ SignalsActions,
80
+ SignalsSurfaceHandle,
81
+ SignalsView,
82
+ } from "../surfaces/panel-signals";
62
83
 
63
84
  interface SignalsPanelState {
64
85
  document: JxMutableNode;
@@ -71,13 +92,12 @@ interface SignalsPanelState {
71
92
  }
72
93
 
73
94
  /**
74
- * What the State panel's template needs from its host: a repaint, and nothing else.
95
+ * What the Data panel's surface needs from its host: a repaint, and the one verb a repaint is not.
75
96
  *
76
97
  * It carried `renderCanvas` and `updateSession` too, and both are gone for the same reason. The
77
- * canvas hook lost its last caller when the takeovers went and was left threaded from
78
- * `registerStatePanel`; the session writer lost its two when the Logic buttons started going
79
- * through `openLogicTarget`, which addresses the focused tab itself. A ctx field with no reader is
80
- * an invitation to write the wrong thing through it.
98
+ * canvas hook lost its last caller when the takeovers went; the session writer lost its two when
99
+ * the Logic buttons started going through `openLogicTarget`, which addresses the focused tab
100
+ * itself. A ctx field with no reader is an invitation to write the wrong thing through it.
81
101
  */
82
102
  interface SignalsPanelCtx {
83
103
  renderLeftPanel: () => void;
@@ -133,7 +153,7 @@ export interface SignalDef {
133
153
  let renameError: { name: string; message: string } | null = null;
134
154
 
135
155
  /** Track which functions have the advanced param editor open. */
136
- const advancedParamOpen = new Set();
156
+ const advancedParamOpen = new Set<string>();
137
157
 
138
158
  /** Default templates for creating new signal definitions. */
139
159
  const DEF_TEMPLATES = {
@@ -169,6 +189,9 @@ const STUDIO_RESERVED_KEYS = new Set([
169
189
  "emits",
170
190
  ]);
171
191
 
192
+ /** How long a typed value waits before it is written, where typing commits at all. */
193
+ const DEBOUNCE_MS = 500;
194
+
172
195
  // ─── Signals / defs helpers ──────────────────────────────────────────────────
173
196
 
174
197
  /**
@@ -196,7 +219,7 @@ export function extensionStateClasses(): {
196
219
 
197
220
  /**
198
221
  * View a state entry through the panel's flattened editing lens. Naked primitive and array entries
199
- * surface as an empty view — the renderers guard every field access.
222
+ * surface as an empty view — the builders guard every field access.
200
223
  *
201
224
  * @param {import("@jxsuite/schema/types").JxStateDefinition} def
202
225
  * @returns {SignalDef}
@@ -383,11 +406,11 @@ export function resolveDefaultForCanvas(
383
406
  }
384
407
  // Computed → expression indicator
385
408
  if (def.$compute) {
386
- return `\u0192(${defName})`;
409
+ return `ƒ(${defName})`;
387
410
  }
388
411
  // Request → URL hint
389
412
  if (def.$prototype === "Request") {
390
- return `\u27F3 ${def.url || "fetch"}`;
413
+ return `⟳ ${def.url || "fetch"}`;
391
414
  }
392
415
  // Storage → use default or key
393
416
  if (def.$prototype === "LocalStorage" || def.$prototype === "SessionStorage") {
@@ -405,40 +428,6 @@ export function resolveDefaultForCanvas(
405
428
  return `{${defName}}`;
406
429
  }
407
430
 
408
- // ─── Simple field row ────────────────────────────────────────────────────────
409
-
410
- /**
411
- * Simple field row for signal editors — vertical stacked layout.
412
- *
413
- * `error` paints the row invalid and prints the message in a `role="alert"` line, which is how a
414
- * refused rename says so instead of silently keeping the old name.
415
- */
416
- export function signalFieldRow(
417
- label: string,
418
- value: string,
419
- onChange: (value: string) => void,
420
- error?: string | undefined,
421
- ) {
422
- return renderFieldRow({
423
- prop: label,
424
- label,
425
- hasValue: false,
426
- ...(error === undefined ? {} : { error }),
427
- // CommitMode "blur": signal fields (rename, src, etc.) commit on blur/Enter only — a debounced
428
- // Mid-typing commit would, e.g., rename the signal on every keystroke pause.
429
- widget: spTextField(
430
- `sig:${label}`,
431
- value,
432
- (v: string) => {
433
- if (v !== value) {
434
- onChange(v);
435
- }
436
- },
437
- { commitMode: "blur" },
438
- ),
439
- });
440
- }
441
-
442
431
  /** Normalize a parameter entry to a CEM object. */
443
432
  export function normParam(p: string | CemParameter): CemParameter {
444
433
  return typeof p === "string" ? { name: p } : p;
@@ -455,1092 +444,908 @@ function cemTypeText(type: JsonValue | undefined): string {
455
444
  return "";
456
445
  }
457
446
 
458
- // ─── Left panel: Signals ─────────────────────────────────────────────────────
459
-
460
- /**
461
- * Add a def from one of the built-in templates under a free name, and expand it for editing. Shared
462
- * by the "+ Add…" picker and the panel's empty state, so both create the same thing.
463
- */
464
- function addTemplateDef(type: string, S: SignalsPanelState, ctx: SignalsPanelCtx) {
465
- const template = DEF_TEMPLATES[type];
466
- if (!template) {
467
- return;
447
+ /** A value as a field shows it: JSON for a structure, the string for anything else. */
448
+ function asText(value: unknown, indent = false): string {
449
+ if (value === undefined || value === null) {
450
+ return "";
468
451
  }
469
- const nameBase = type === "function" ? "newFunction" : "$newSignal";
470
- let n = nameBase;
471
- let i = 1;
472
- while (S.document.state && S.document.state[n]) {
473
- n = nameBase + i;
474
- i += 1;
452
+ if (typeof value === "object") {
453
+ return indent ? JSON.stringify(value, null, 2) : JSON.stringify(value);
475
454
  }
476
- transactDoc(activeTab.value, (t) =>
477
- mutateAddDef(t, n, structuredClone(template) as Record<string, JsonValue>),
478
- );
479
- setDataRowExpanded(n, true);
480
- ctx.renderLeftPanel();
455
+ return String(value);
481
456
  }
482
457
 
458
+ // ─── The field plans ─────────────────────────────────────────────────────────
459
+
483
460
  /**
484
- * @param {SignalsPanelState} S
485
- * @param {SignalsPanelCtx} ctx
486
- */
487
- /**
488
- * ONE summary slot per row, and the resolved value wins it as soon as there is one.
489
- *
490
- * The row wants to say four things about an entry — its category, its name, how it is defined and
491
- * what it became — and a 240px Navigator fits three. Eliding both summaries to make room produced
492
- * "recentproje… C… Array(3)": two truncated descriptions and a truncated identity. So the
493
- * definition hint holds the slot until the canvas resolves a value and then steps aside for it, and
494
- * the full definition is one click down in fields — which is what the hint was abbreviating.
461
+ * What one field of one entry can be asked to do.
495
462
  *
496
- * The switch is whether the canvas has reported a scope AT ALL, not whether this entry appears in
497
- * it. An entry the canvas ran and did not produce is "pending", which is a fact about the value; a
498
- * panel opened before the canvas has rendered knows nothing about any of them, and guessing
499
- * "pending" for the whole list there would be a fact about the panel dressed up as one about data.
500
- *
501
- * ENTRIES THAT CANNOT HOLD A VALUE never get the column. A function and an assignment expression
502
- * are things the page DOES, not things it knows, and they are absent from the resolved scope for
503
- * exactly that reason — so the value column called all eight of a component's `setFilter` handlers
504
- * "pending", which reads as "still loading" for something that will never load.
463
+ * The document hands back the entry's name and this field's key and nothing else, so this is where
464
+ * `url` becomes "a `Request`'s URL". Rebuilt on every projection, which is what keeps a writer
465
+ * closed over the def that is actually on screen.
505
466
  */
506
- function summary(name: string, def: SignalDef, live: unknown, resolved: boolean) {
507
- const holdsNoValue =
508
- defCategory(def) === "function" ||
509
- (def.$expression != null && isActionExpression(def.$expression));
510
- if (!resolved || holdsNoValue) {
511
- const hint = defHint(name, def);
512
- return html`<span class="signal-hint" title=${hint}>${hint}</span>`;
513
- }
514
- return html`<span
515
- class=${classMap({ "data-pending": unwrapSignal(live) === null, "data-type": true })}
516
- title="What this resolved to on the canvas"
517
- >${dataTypeLabel(live)}</span
518
- >`;
467
+ interface FieldPlan {
468
+ /** What the projection put in the control — a commit equal to it is nothing happening. */
469
+ value: string;
470
+ /** What a KEYSTROKE is worth here: nothing (commit on leaving), a debounce, or a write. */
471
+ live: "none" | "debounce" | "commit";
472
+ write?: (value: string) => void;
473
+ check?: (checked: boolean) => void;
474
+ press?: (action: string) => void;
475
+ cell?: (row: string, cell: string, value: string) => void;
476
+ cellCheck?: (row: string, cell: string, checked: boolean) => void;
477
+ dropRow?: (row: string) => void;
478
+ addRow?: () => void;
479
+ addChip?: (value: string) => void;
480
+ dropChip?: (chip: string) => void;
519
481
  }
520
482
 
521
- export function renderSignalsTemplate(S: SignalsPanelState, ctx: SignalsPanelCtx) {
522
- const defs = S.document.state || {};
523
- const entries = Object.entries(defs);
524
- // What the canvas actually resolved these to. `S.canvas` is the session's canvas record, so this
525
- // Costs a property read — the panel already had the scope in hand and rendered it in a second
526
- // Panel anyway.
527
- const liveScope = (S.canvas?.scope ?? null) as Record<string, unknown> | null;
528
- const scope = liveScope ?? {};
529
- // A Refresh is out and the canvas has not answered. Read off the tab, so it survives the repaints
530
- // Between the press and the `dataScope` that ends it.
531
- const refreshing = S.canvas?.refreshing === true;
483
+ /** Every field of every entry the last projection drew, by `<entry>` and `<field key>`. */
484
+ const plans = new Map<string, FieldPlan>();
532
485
 
533
- // Warm the extensions payload so manifest state classes appear in the add picker (the panel
534
- // Re-renders constantly; loadExtensions memoizes, so this is a one-time fetch per project).
535
- void loadExtensions();
486
+ /** Every island the last projection drew, by `<entry>/<slot>`. See {@link paintIsland}. */
487
+ const islands = new Map<string, (host: HTMLElement) => void>();
536
488
 
537
- // Group by category
538
- const groups = {
539
- computed: [],
540
- data: [],
541
- expression: [],
542
- function: [],
543
- state: [],
544
- } as Record<string, [string, SignalDef][]>;
545
- for (const [name, def] of entries) {
546
- groups[defCategory(def)]!.push([name, asSignalDef(def)]);
547
- }
548
-
549
- const categories = [
550
- { items: groups.state!, key: "state", label: "State" },
551
- { items: groups.computed!, key: "computed", label: "Computed" },
552
- { items: groups.data!, key: "data", label: "Data" },
553
- { items: groups.expression!, key: "expression", label: "Expressions" },
554
- { items: groups.function!, key: "function", label: "Functions" },
555
- ];
489
+ /** Debounced writes in flight, by the same key as {@link plans}. */
490
+ const timers = new Map<string, ReturnType<typeof setTimeout>>();
556
491
 
557
- S._collapsedSignalCats ||= new Set();
558
- const collapsedCats = S._collapsedSignalCats;
559
-
560
- const catTemplates = categories
561
- .filter((c) => c.items.length > 0)
562
- .map(
563
- ({ key, label, items }) => html`
564
- <sp-accordion-item
565
- label="${label} (${items.length})"
566
- ?open=${!collapsedCats.has(key)}
567
- @sp-accordion-item-toggle=${() => {
568
- if (collapsedCats.has(key)) {
569
- collapsedCats.delete(key);
570
- } else {
571
- collapsedCats.add(key);
572
- }
573
- ctx.renderLeftPanel();
574
- }}
575
- >
576
- ${items.map(([name, def]) => {
577
- const isExpanded: boolean = isDataRowExpanded(name);
578
- const live = scope[name];
579
- return html`
580
- <div
581
- class=${classMap({ expanded: isExpanded, "signal-row": true })}
582
- @click=${() => {
583
- setDataRowExpanded(name, !isExpanded);
584
- ctx.renderLeftPanel();
585
- }}
586
- >
587
- <span class="signal-badge ${defCategory(def)}">${defBadgeLabel(def)}</span>
588
- <span class="signal-name">${name}</span>
589
- ${summary(name, def, live, liveScope !== null)}
590
- <sp-action-button
591
- quiet
592
- size="xs"
593
- class="signal-del"
594
- @click=${(e: Event) => {
595
- e.stopPropagation();
596
- transactDoc(activeTab.value, (t) => mutateRemoveDef(t, name));
597
- }}
598
- >
599
- <sp-icon-delete slot="icon"></sp-icon-delete>
600
- </sp-action-button>
601
- </div>
602
- ${
603
- isExpanded
604
- ? html`<div class="signal-editor">
605
- ${renderSignalEditorTemplate(S, name, def, ctx)}
606
- <div class="signal-live">
607
- <span class="signal-live-label">Resolved to</span>
608
- <div class="data-tree">
609
- ${renderDataTreeTemplate(unwrapSignal(live), 0, 5, name)}
610
- </div>
611
- </div>
612
- </div>`
613
- : nothing
614
- }
615
- `;
616
- })}
617
- </sp-accordion-item>
618
- `,
619
- );
492
+ /** Plugin schemas asked for and not yet answered, by cache key. See {@link externalFields}. */
493
+ const schemaRequests = new Set<string>();
620
494
 
621
- return html`
622
- <div class=${classMap({ "is-refreshing": refreshing, "signals-panel": true })}>
623
- ${
624
- ctx.refreshData && entries.length > 0
625
- ? html`<div class="data-explorer-toolbar">
626
- <sp-action-button
627
- quiet
628
- size="s"
629
- class="data-refresh-btn"
630
- ?disabled=${refreshing}
631
- @click=${() => {
632
- ctx.refreshData?.();
633
- ctx.renderLeftPanel();
634
- }}
635
- >
636
- ${
637
- refreshing
638
- ? html`<sp-progress-circle
639
- slot="icon"
640
- indeterminate
641
- size="s"
642
- label="Refreshing"
643
- ></sp-progress-circle>`
644
- : html`<sp-icon-refresh slot="icon"></sp-icon-refresh>`
645
- }
646
- ${refreshing ? "Refreshing…" : "Refresh"}
647
- </sp-action-button>
648
- </div>`
649
- : nothing
650
- }
651
- <sp-accordion allow-multiple size="s"> ${catTemplates} </sp-accordion>
652
- ${
653
- entries.length === 0
654
- ? renderEmptyState({
655
- actions: [{ label: "Add a value", run: () => addTemplateDef("state", S, ctx) }],
656
- message:
657
- "Data lives here — values this page can read, compute or fetch, " +
658
- "ready to bind to any element.",
659
- })
660
- : nothing
661
- }
662
- <div class="signals-add">
663
- <sp-picker
664
- size="s"
665
- label="+ Add…"
666
- placeholder="+ Add…"
667
- @change=${(e: Event) => {
668
- const type = (e.target as HTMLInputElement).value;
669
- if (!type) {
670
- return;
671
- }
495
+ /** The panel state and host hooks the last render was given — what an action acts against. */
496
+ let panelState: SignalsPanelState | null = null;
497
+ let panelCtx: SignalsPanelCtx | null = null;
672
498
 
673
- // Extension-manifest state classes ("ext:Session"): no $src needed — the registry
674
- // Resolves them; the descriptor's stateDefaults seed the def (e.g. timing "client").
675
- if (type.startsWith("ext:")) {
676
- const protoName = type.slice(4);
677
- const cls = extensionStateClasses().find((c) => c.name === protoName);
678
- let n = `$${protoName.charAt(0).toLowerCase()}${protoName.slice(1)}`;
679
- let i = 1;
680
- const base = n;
681
- while (S.document.state && S.document.state[n]) {
682
- n = base + i;
683
- i += 1;
684
- }
685
- transactDoc(activeTab.value, (t) =>
686
- mutateAddDef(t, n, {
687
- $prototype: protoName,
688
- ...cls?.stateDefaults,
689
- } as Record<string, JsonValue>),
690
- );
691
- setDataRowExpanded(n, true);
692
- ctx.renderLeftPanel();
693
- return;
694
- }
499
+ /** One field's identity, as both maps key it. A state entry's name cannot contain a newline. */
500
+ function planKey(signal: string, fieldKey: string): string {
501
+ return `${signal}\n${fieldKey}`;
502
+ }
695
503
 
696
- // Handle import-based prototypes (e.g., "import:ContentCollection")
697
- if (type.startsWith("import:")) {
698
- const protoName = type.slice(7);
699
- const src = projectState?.projectConfig?.imports?.[protoName];
700
- let n = `$${protoName.charAt(0).toLowerCase()}${protoName.slice(1)}`;
701
- let i = 1;
702
- const base = n;
703
- while (S.document.state && S.document.state[n]) {
704
- n = base + i;
705
- i += 1;
706
- }
707
- transactDoc(activeTab.value, (t) =>
708
- mutateAddDef(
709
- t,
710
- n,
711
- /** @type {Record<string, JsonValue>} */ {
712
- $prototype: protoName,
713
- },
714
- ),
715
- );
716
- setDataRowExpanded(n, true);
717
- if (src) {
718
- void fetchPluginSchema(
719
- { $prototype: protoName, $src: src },
720
- {
721
- ...(S.documentPath != null && {
722
- documentPath: S.documentPath,
723
- }),
724
- },
725
- ).then(() => ctx.renderLeftPanel());
726
- } else {
727
- ctx.renderLeftPanel();
728
- }
729
- return;
730
- }
504
+ /** Register what a field's controls do, and return the field the document draws. */
505
+ function field(
506
+ spec: Partial<SignalFieldView> & { key: string; kind: SignalFieldKind; signal: string },
507
+ plan?: FieldPlan,
508
+ ): SignalFieldView {
509
+ const view: SignalFieldView = {
510
+ addLabel: "",
511
+ buttons: [],
512
+ cellRows: [],
513
+ checked: false,
514
+ chips: [],
515
+ error: "",
516
+ footer: "",
517
+ hasError: false,
518
+ hasFooter: false,
519
+ label: "",
520
+ mono: false,
521
+ options: [],
522
+ placeholder: "",
523
+ prop: spec.prop ?? spec.label ?? spec.key,
524
+ rows: "",
525
+ segments: [],
526
+ slot: "",
527
+ span: false,
528
+ value: "",
529
+ ...spec,
530
+ };
531
+ plans.set(planKey(spec.signal, spec.key), {
532
+ ...(plan ?? { live: "none", value: "" }),
533
+ value: view.value,
534
+ });
535
+ return view;
536
+ }
731
537
 
732
- addTemplateDef(type, S, ctx);
733
- }}
734
- >
735
- <sp-menu-item value="state">Value</sp-menu-item>
736
- <sp-menu-item value="computed">Computed</sp-menu-item>
737
- <sp-menu-divider></sp-menu-divider>
738
- <sp-menu-item value="request">Fetch from a URL</sp-menu-item>
739
- <sp-menu-item value="localStorage">LocalStorage</sp-menu-item>
740
- <sp-menu-item value="sessionStorage">SessionStorage</sp-menu-item>
741
- <sp-menu-item value="indexedDB">IndexedDB</sp-menu-item>
742
- <sp-menu-item value="cookie">Cookie</sp-menu-item>
743
- <sp-menu-item value="set">Set</sp-menu-item>
744
- <sp-menu-item value="map">Map</sp-menu-item>
745
- <sp-menu-item value="formData">FormData</sp-menu-item>
746
- <sp-menu-item value="external">From a module…</sp-menu-item>
747
- ${
748
- projectState?.projectConfig?.imports
749
- ? html`<sp-menu-divider></sp-menu-divider>${Object.keys(
750
- projectState.projectConfig.imports,
751
- ).map(
752
- (k: string) => html`<sp-menu-item value="import:${k}">${k}</sp-menu-item>`,
753
- )}`
754
- : nothing
755
- }
756
- ${
757
- extensionStateClasses().length > 0
758
- ? html`<sp-menu-divider></sp-menu-divider>${extensionStateClasses().map(
759
- (cls) => html`<sp-menu-item value="ext:${cls.name}">${cls.name}</sp-menu-item>`,
760
- )}`
761
- : nothing
762
- }
763
- <sp-menu-divider></sp-menu-divider>
764
- <sp-menu-item value="expression">Expression</sp-menu-item>
765
- <sp-menu-item value="function">Function</sp-menu-item>
766
- </sp-picker>
767
- </div>
768
- </div>
769
- `;
538
+ /** A text row committed when the reader leaves it — the shape every `signalFieldRow` had. */
539
+ function textField(
540
+ signal: string,
541
+ key: string,
542
+ label: string,
543
+ value: string,
544
+ write: (value: string) => void,
545
+ extra: Partial<SignalFieldView> = {},
546
+ ): SignalFieldView {
547
+ return field(
548
+ { key, kind: "text", label, signal, value, ...extra },
549
+ { live: "none", value, write },
550
+ );
770
551
  }
771
552
 
772
- /** Render inline editor fields for a specific signal/def type. */
773
- function renderSignalEditorTemplate(
774
- S: SignalsPanelState,
775
- name: string,
776
- defArg: SignalDef,
777
- ctx: SignalsPanelCtx,
778
- ) {
779
- const def = typeof defArg === "object" && defArg !== null ? defArg : { default: defArg };
780
- const cat = defCategory(def);
781
-
782
- // Helper for picker rows
783
- const pickerRow = (
784
- label: string,
785
- options: string[],
786
- currentVal: string,
787
- onChange: (value: string) => void,
788
- ) =>
789
- renderFieldRow({
790
- hasValue: false,
553
+ /** A closed set of values, committed on the pick. */
554
+ function selectField(
555
+ signal: string,
556
+ key: string,
557
+ label: string,
558
+ values: string[],
559
+ value: string,
560
+ write: (value: string) => void,
561
+ ): SignalFieldView {
562
+ return field(
563
+ {
564
+ key,
565
+ kind: "select",
791
566
  label,
792
- prop: label,
793
- widget: html`
794
- <sp-picker
795
- size="s"
796
- value=${currentVal}
797
- @change=${(e: Event) => onChange((e.target as HTMLInputElement).value)}
798
- >
799
- ${options.map((opt: string) => html`<sp-menu-item value=${opt}>${opt}</sp-menu-item>`)}
800
- </sp-picker>
801
- `,
802
- });
567
+ options: values.map((v) => ({ label: v, value: v })),
568
+ signal,
569
+ value,
570
+ },
571
+ { live: "none", value, write },
572
+ );
573
+ }
803
574
 
804
- // Helper for textarea rows — uses the shared draft layer (commits on blur/Enter and a 500ms
805
- // Debounce) so a panel re-render mid-edit can't truncate the in-progress text.
806
- const textareaRow = (
807
- label: string,
808
- value: string,
809
- onChange: (value: string) => void,
810
- opts: { minHeight?: string; mono?: boolean } = {},
811
- ) =>
812
- renderFieldRow({
813
- hasValue: false,
814
- label,
815
- prop: label,
816
- widget: rawTextArea(`sig:${label}`, value, onChange, {
817
- debounceMs: 500,
818
- ...(opts.minHeight != null && { minHeight: opts.minHeight }),
819
- ...(opts.mono != null && { mono: opts.mono }),
820
- }),
821
- });
575
+ // ─── The editor, field by field ──────────────────────────────────────────────
822
576
 
823
- /*
824
- * Name field (common to all).
825
- *
826
- * Every refusal SAYS SO. A collision silently kept the old name while the field showed the new
827
- * one, so the panel and the document disagreed and only the canvas could tell you which had won —
828
- * and an empty name did the same. The expansion follows the rename so the editor you are typing
829
- * in is still the one on screen afterwards.
830
- */
831
- const nameField = signalFieldRow(
832
- "Name",
833
- name,
834
- (v: string) => {
835
- const next = v.trim();
836
- if (next === name) {
837
- renameError = null;
838
- return;
839
- }
840
- if (!next) {
841
- renameError = { message: "A name is required.", name };
842
- } else if (S.document.state?.[next]) {
843
- renameError = { message: `"${next}" is already defined by this document.`, name };
844
- } else {
845
- renameError = null;
846
- // The expansion follows the rename, so the editor you are typing in is still the one on
847
- // Screen when the row list repaints under the new name.
848
- setDataRowExpanded(name, false);
849
- setDataRowExpanded(next, true);
850
- transactDoc(activeTab.value, (t) => mutateRenameDef(t, name, next));
851
- }
852
- ctx.renderLeftPanel();
577
+ /** The rename field, which every category starts with. */
578
+ function nameField(S: SignalsPanelState, name: string): SignalFieldView {
579
+ const message = renameError?.name === name ? renameError.message : "";
580
+ return field(
581
+ {
582
+ error: message,
583
+ hasError: message !== "",
584
+ key: "name",
585
+ kind: "text",
586
+ label: "Name",
587
+ prop: "Name",
588
+ signal: name,
589
+ value: name,
590
+ },
591
+ {
592
+ live: "none",
593
+ value: name,
594
+ /*
595
+ * Every refusal SAYS SO. A collision silently kept the old name while the field showed the
596
+ * new one, so the panel and the document disagreed and only the canvas could tell you which
597
+ * had won — and an empty name did the same. The expansion follows the rename so the editor
598
+ * you are typing in is still the one on screen afterwards.
599
+ */
600
+ write: (v: string) => {
601
+ const next = v.trim();
602
+ if (next === name) {
603
+ renameError = null;
604
+ return;
605
+ }
606
+ if (!next) {
607
+ renameError = { message: "A name is required.", name };
608
+ } else if (S.document.state?.[next]) {
609
+ renameError = { message: `"${next}" is already defined by this document.`, name };
610
+ } else {
611
+ renameError = null;
612
+ setDataRowExpanded(name, false);
613
+ setDataRowExpanded(next, true);
614
+ transactDoc(activeTab.value, (t) => mutateRenameDef(t, name, next));
615
+ }
616
+ repaint();
617
+ },
853
618
  },
854
- renameError?.name === name ? renameError.message : undefined,
855
619
  );
620
+ }
856
621
 
857
- let fields: TemplateResult | typeof nothing = nothing;
622
+ /** A plain value entry: its type, its default, and the CEM facts a custom element adds. */
623
+ function stateFields(S: SignalsPanelState, name: string, def: SignalDef): SignalFieldView[] {
624
+ const patch = (value: Record<string, JsonValue | undefined>): void => {
625
+ transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, value));
626
+ };
627
+ const defaultVal = asText(def.default);
628
+ const out: SignalFieldView[] = [
629
+ selectField(
630
+ name,
631
+ "type",
632
+ "Type",
633
+ ["string", "integer", "number", "boolean", "array", "object"],
634
+ def.type || "string",
635
+ (v) => patch({ type: v }),
636
+ ),
637
+ ];
638
+ if (def.type === "string" || !def.type) {
639
+ out.push(
640
+ selectField(name, "format", "Format", ["", "image", "date", "color"], def.format || "", (v) =>
641
+ patch({ format: v || undefined }),
642
+ ),
643
+ );
644
+ }
645
+ if (isMediaFormat(def.format)) {
646
+ out.push(
647
+ field({
648
+ key: "default",
649
+ kind: "slot-field",
650
+ label: "Default",
651
+ prop: "Default",
652
+ signal: name,
653
+ slot: "media",
654
+ }),
655
+ );
656
+ islands.set(`${name}/media`, (host) => {
657
+ mountMediaPicker(host, "default", defaultVal, (v: string) =>
658
+ patch({ default: v || undefined }),
659
+ );
660
+ });
661
+ } else {
662
+ out.push(
663
+ textField(name, "default", "Default", defaultVal, (v: string) => {
664
+ let parsed: unknown = v;
665
+ if (def.type === "integer") {
666
+ parsed = Math.trunc(Number(v)) || 0;
667
+ } else if (def.type === "number") {
668
+ parsed = Number(v) || 0;
669
+ } else if (def.type === "boolean") {
670
+ parsed = v === "true";
671
+ } else if (def.type === "array" || def.type === "object") {
672
+ try {
673
+ parsed = JSON.parse(v);
674
+ } catch {
675
+ parsed = v;
676
+ }
677
+ }
678
+ patch({ default: parsed as JsonValue });
679
+ }),
680
+ );
681
+ }
682
+ out.push(
683
+ textField(name, "description", "Description", def.description || "", (v) =>
684
+ patch({ description: v || undefined }),
685
+ ),
686
+ );
687
+ if (isCustomElementDoc(S)) {
688
+ out.push(
689
+ textField(name, "attribute", "Attribute", def.attribute || "", (v) =>
690
+ patch({ attribute: v || undefined }),
691
+ ),
692
+ field(
693
+ {
694
+ checked: Boolean(def.reflects),
695
+ key: "reflects",
696
+ kind: "checkbox",
697
+ label: "Reflects",
698
+ prop: "reflects",
699
+ signal: name,
700
+ },
701
+ {
702
+ check: (checked) => patch({ reflects: checked || undefined }),
703
+ live: "none",
704
+ value: "",
705
+ },
706
+ ),
707
+ textField(
708
+ name,
709
+ "deprecated",
710
+ "Deprecated",
711
+ typeof def.deprecated === "string" ? def.deprecated : "",
712
+ (v) => patch({ deprecated: v || undefined }),
713
+ ),
714
+ );
715
+ }
716
+ return out;
717
+ }
858
718
 
859
- if (cat === "state") {
860
- const defaultVal =
861
- def.default !== undefined && def.default !== null
862
- ? typeof def.default === "object"
863
- ? JSON.stringify(def.default)
864
- : String(def.default)
865
- : "";
866
-
867
- const cemFields = isCustomElementDoc(S)
868
- ? html`
869
- ${signalFieldRow("Attribute", def.attribute || "", (v: string) =>
870
- transactDoc(activeTab.value, (t) =>
871
- mutateUpdateDef(t, name, { attribute: v || undefined }),
872
- ),
873
- )}
874
- ${renderFieldRow({
875
- hasValue: false,
876
- label: "Reflects",
877
- prop: "reflects",
878
- widget: html`
879
- <sp-checkbox
880
- class="field-check"
881
- ?checked=${Boolean(def.reflects)}
882
- @change=${(e: Event) =>
883
- transactDoc(activeTab.value, (t) =>
884
- mutateUpdateDef(t, name, {
885
- reflects: (e.target as HTMLInputElement).checked || undefined,
886
- }),
887
- )}
888
- ></sp-checkbox>
889
- `,
890
- })}
891
- ${signalFieldRow(
892
- "Deprecated",
893
- typeof def.deprecated === "string" ? def.deprecated : "",
894
- (v: string) =>
895
- transactDoc(activeTab.value, (t) =>
896
- mutateUpdateDef(t, name, { deprecated: v || undefined }),
897
- ),
898
- )}
899
- `
900
- : nothing;
901
-
902
- fields = html`
903
- ${pickerRow(
904
- "Type",
905
- ["string", "integer", "number", "boolean", "array", "object"],
906
- def.type || "string",
907
- (v: string) => transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { type: v })),
908
- )}
909
- ${
910
- def.type === "string" || !def.type
911
- ? pickerRow("Format", ["", "image", "date", "color"], def.format || "", (v: string) =>
912
- transactDoc(activeTab.value, (t) =>
913
- mutateUpdateDef(t, name, { format: v || undefined }),
914
- ),
915
- )
916
- : nothing
917
- }
918
- ${
919
- isMediaFormat(def.format)
920
- ? renderFieldRow({
921
- hasValue: false,
922
- label: "Default",
923
- prop: "Default",
924
- widget: renderMediaPicker("default", defaultVal, (v: string) => {
925
- transactDoc(activeTab.value, (t) =>
926
- mutateUpdateDef(t, name, { default: v || undefined }),
927
- );
928
- }),
929
- })
930
- : signalFieldRow("Default", defaultVal, (v: string) => {
931
- let parsed: unknown = v;
932
- if (def.type === "integer") {
933
- parsed = Math.trunc(Number(v)) || 0;
934
- } else if (def.type === "number") {
935
- parsed = Number(v) || 0;
936
- } else if (def.type === "boolean") {
937
- parsed = v === "true";
938
- } else if (def.type === "array" || def.type === "object") {
939
- try {
940
- parsed = JSON.parse(v);
941
- } catch {
942
- parsed = v;
943
- }
944
- }
945
- transactDoc(activeTab.value, (t) =>
946
- mutateUpdateDef(t, name, { default: parsed as JsonValue }),
947
- );
948
- })
949
- }
950
- ${signalFieldRow("Description", def.description || "", (v: string) =>
951
- transactDoc(activeTab.value, (t) =>
952
- mutateUpdateDef(t, name, { description: v || undefined }),
953
- ),
954
- )}
955
- ${cemFields}
956
- `;
957
- } else if (cat === "computed") {
958
- let debounce: ReturnType<typeof setTimeout> | undefined;
959
- fields = html`
960
- ${renderFieldRow({
961
- hasValue: false,
719
+ /** A derived value: the expression, and the dependencies it was read out of. */
720
+ function computedFields(name: string, def: SignalDef): SignalFieldView[] {
721
+ const expression = def.$compute || "";
722
+ const out: SignalFieldView[] = [
723
+ field(
724
+ {
725
+ key: "expression",
726
+ kind: "multiline",
962
727
  label: "Expression",
963
728
  prop: "expression",
964
- widget: html`
965
- <textarea
966
- class="field-input"
967
- style="min-height:40px"
968
- .value=${def.$compute || ""}
969
- @input=${(e: Event) => {
970
- clearTimeout(debounce);
971
- debounce = setTimeout(() => {
972
- const expr = (e.target as HTMLInputElement).value;
973
- const depMatches = expr.match(/\$[a-zA-Z_]\w*/g) || [];
974
- const deps = [...new Set(depMatches)].map((d) => `#/state/${d}`);
975
- transactDoc(activeTab.value, (t) =>
976
- mutateUpdateDef(t, name, { $compute: expr, $deps: deps }),
977
- );
978
- }, 500);
979
- }}
980
- ></textarea>
981
- `,
982
- })}
983
- ${
984
- def.$deps && def.$deps.length > 0
985
- ? renderFieldRow({
986
- hasValue: false,
987
- label: "Dependencies",
988
- prop: "dependencies",
989
- widget: html`
990
- <span class="signal-hint" style="flex:1;max-width:none"
991
- >${def.$deps.map((d: string) => d.replace("#/state/", "")).join(", ")}</span
992
- >
993
- `,
994
- })
995
- : nothing
996
- }
997
- `;
998
- } else if (cat === "data") {
999
- fields = renderDataSourceFields(S, name, def, textareaRow, pickerRow, ctx);
1000
- } else if (cat === "function") {
1001
- fields = renderFunctionFields(S, name, def, textareaRow, ctx);
1002
- } else if (cat === "expression") {
1003
- const exprNode = def.$expression || { operator: "=", target: null };
1004
- fields = html`
1005
- <div style="display:flex;align-items:center;gap:4px">
1006
- <span class="field-label" style="flex:1">Expression</span>
1007
- <sp-action-button
1008
- size="xs"
1009
- quiet
1010
- title="Open in formula workspace"
1011
- @click=${() => {
1012
- // One call, because a click is BOTH events: it names the target and it asks for the
1013
- // Surface. Setting the field alone leaned on the dock's reveal effect, which fires at
1014
- // Most once per target — so clicking this again after closing the dock was a dead
1015
- // Click. It also left `editingFunction` set, and that one wins the tie.
1016
- openLogicTarget({ editing: { defName: name, type: "def" }, surface: "formula" });
1017
- }}
1018
- >
1019
- <sp-icon-align-bottom slot="icon"></sp-icon-align-bottom>
1020
- </sp-action-button>
1021
- </div>
1022
- ${renderExpressionEditor(
1023
- exprNode,
1024
- (newNode: unknown) =>
729
+ rows: "2",
730
+ signal: name,
731
+ value: expression,
732
+ },
733
+ {
734
+ live: "debounce",
735
+ value: expression,
736
+ write: (expr: string) => {
737
+ const depMatches = expr.match(/\$[a-zA-Z_]\w*/g) || [];
738
+ const deps = [...new Set(depMatches)].map((d) => `#/state/${d}`);
1025
739
  transactDoc(activeTab.value, (t) =>
1026
- // Expression editors emit JSON expression nodes.
1027
- mutateUpdateDef(t, name, { $expression: newNode as JsonValue }),
1028
- ),
1029
- {
1030
- allowEventRef: false,
1031
- // Live-context evaluation in the canvas iframe, snapshot fallback (M6). The signals
1032
- // Panel lives in the left panel — re-render it when a fresh live result lands.
1033
- preview: livePreviewExpression(activeTab.value, `def:${name}`, exprNode, null, () =>
1034
- renderOnly("leftPanel"),
1035
- ),
1036
- onInsertDef: (defName, vendored) =>
1037
- transactDoc(activeTab.value, (t) =>
1038
- mutateAddDef(t, defName, vendored as Record<string, JsonValue>),
1039
- ),
1040
- stateDefs: Object.keys(S.document.state || {}),
1041
- stateEntries: S.document.state || {},
740
+ mutateUpdateDef(t, name, { $compute: expr, $deps: deps }),
741
+ );
1042
742
  },
1043
- )}
1044
- `;
743
+ },
744
+ ),
745
+ ];
746
+ if (def.$deps && def.$deps.length > 0) {
747
+ out.push(
748
+ field({
749
+ key: "dependencies",
750
+ kind: "note",
751
+ label: "Dependencies",
752
+ prop: "dependencies",
753
+ signal: name,
754
+ value: def.$deps.map((d: string) => d.replace("#/state/", "")).join(", "),
755
+ }),
756
+ );
1045
757
  }
1046
-
1047
- return html`${nameField}${fields}`;
758
+ return out;
1048
759
  }
1049
760
 
1050
- /** Data source fields for signal editor */
1051
- function renderDataSourceFields(
1052
- S: SignalsPanelState,
1053
- name: string,
1054
- def: SignalDef,
1055
- textareaRow: (
1056
- label: string,
1057
- value: string,
1058
- onChange: (value: string) => void,
1059
- opts?: { minHeight?: string; mono?: boolean },
1060
- ) => TemplateResult,
1061
- pickerRow: (
1062
- label: string,
1063
- options: string[],
1064
- currentVal: string,
1065
- onChange: (value: string) => void,
1066
- ) => TemplateResult,
1067
- ctx: SignalsPanelCtx,
1068
- ) {
761
+ /** A fetched, stored or constructed value — one case per `$prototype`. */
762
+ function dataFields(S: SignalsPanelState, name: string, def: SignalDef): SignalFieldView[] {
763
+ const patch = (value: Record<string, JsonValue | undefined>): void => {
764
+ transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, value));
765
+ };
1069
766
  const proto = def.$prototype;
1070
767
 
1071
768
  if (proto === "Request") {
1072
- return html`
1073
- ${signalFieldRow("URL", def.url || "", (v: string) =>
1074
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { url: v })),
1075
- )}
1076
- ${pickerRow(
769
+ return [
770
+ textField(name, "url", "URL", def.url || "", (v) => patch({ url: v })),
771
+ selectField(
772
+ name,
773
+ "method",
1077
774
  "Method",
1078
775
  ["GET", "POST", "PUT", "DELETE", "PATCH"],
1079
776
  def.method || "GET",
1080
- (v: string) => transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { method: v })),
1081
- )}
1082
- ${pickerRow("Timing", ["client", "server"], def.timing || "client", (v: string) =>
1083
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { timing: v })),
1084
- )}
1085
- `;
777
+ (v) => patch({ method: v }),
778
+ ),
779
+ selectField(name, "timing", "Timing", ["client", "server"], def.timing || "client", (v) =>
780
+ patch({ timing: v }),
781
+ ),
782
+ ];
1086
783
  }
1087
784
  if (proto === "LocalStorage" || proto === "SessionStorage") {
1088
- const defaultStr =
1089
- def.default !== undefined && def.default !== null
1090
- ? typeof def.default === "object"
1091
- ? JSON.stringify(def.default, null, 2)
1092
- : String(def.default)
1093
- : "";
1094
- return html`
1095
- ${signalFieldRow("Key", def.key || "", (v: string) =>
1096
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { key: v })),
1097
- )}
1098
- ${textareaRow("Default", defaultStr, (v: string) => {
1099
- try {
1100
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { default: JSON.parse(v) }));
1101
- } catch {
1102
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { default: v }));
1103
- }
1104
- })}
1105
- `;
785
+ const stored = asText(def.default, true);
786
+ return [
787
+ textField(name, "key", "Key", def.key || "", (v) => patch({ key: v })),
788
+ field(
789
+ {
790
+ key: "default",
791
+ kind: "multiline",
792
+ label: "Default",
793
+ prop: "Default",
794
+ rows: "3",
795
+ signal: name,
796
+ value: stored,
797
+ },
798
+ {
799
+ live: "debounce",
800
+ value: stored,
801
+ write: (v: string) => {
802
+ try {
803
+ // `JSON.parse` answers `any`; the value written is a document default, which is a
804
+ // `JsonValue` by construction — naming it keeps the assignment checked.
805
+ patch({ default: JSON.parse(v) as JsonValue });
806
+ } catch {
807
+ patch({ default: v });
808
+ }
809
+ },
810
+ },
811
+ ),
812
+ ];
1106
813
  }
1107
814
  if (proto === "IndexedDB") {
1108
- return html`
1109
- ${signalFieldRow("Database", def.database || "", (v: string) =>
1110
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { database: v })),
1111
- )}
1112
- ${signalFieldRow("Store", def.store || "", (v: string) =>
1113
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { store: v })),
1114
- )}
1115
- ${signalFieldRow("Version", String(def.version || 1), (v: string) =>
1116
- transactDoc(activeTab.value, (t) =>
1117
- mutateUpdateDef(t, name, { version: Math.trunc(Number(v)) || 1 }),
1118
- ),
1119
- )}
1120
- `;
815
+ return [
816
+ textField(name, "database", "Database", def.database || "", (v) => patch({ database: v })),
817
+ textField(name, "store", "Store", def.store || "", (v) => patch({ store: v })),
818
+ textField(name, "version", "Version", String(def.version || 1), (v) =>
819
+ patch({ version: Math.trunc(Number(v)) || 1 }),
820
+ ),
821
+ ];
1121
822
  }
1122
823
  if (proto === "Cookie") {
1123
- return html`
1124
- ${signalFieldRow("Cookie", def.name || "", (v: string) =>
1125
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { name: v })),
1126
- )}
1127
- ${signalFieldRow("Default", String(def.default || ""), (v: string) =>
1128
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { default: v })),
1129
- )}
1130
- `;
824
+ return [
825
+ textField(name, "cookie", "Cookie", def.name || "", (v) => patch({ name: v })),
826
+ textField(name, "default", "Default", String(def.default || ""), (v) =>
827
+ patch({ default: v }),
828
+ ),
829
+ ];
1131
830
  }
1132
831
  if (proto === "Set" || proto === "Map" || proto === "FormData") {
1133
- const fieldName = proto === "FormData" ? "fields" : "default";
1134
- const fieldLabel = proto === "FormData" ? "Fields" : "Default";
1135
- const defaultStr =
832
+ const isForm = proto === "FormData";
833
+ const key = isForm ? "fields" : "default";
834
+ const label = isForm ? "Fields" : "Default";
835
+ const text =
1136
836
  def.default !== undefined && def.default !== null
1137
837
  ? JSON.stringify(def.default, null, 2)
1138
- : proto === "FormData"
838
+ : isForm
1139
839
  ? JSON.stringify(def.fields || {}, null, 2)
1140
840
  : "";
1141
- return textareaRow(fieldLabel, defaultStr, (v: string) => {
1142
- try {
1143
- transactDoc(activeTab.value, (t) =>
1144
- mutateUpdateDef(t, name, { [fieldName]: JSON.parse(v) as unknown }),
1145
- );
1146
- } catch {}
1147
- });
841
+ return [
842
+ field(
843
+ { key, kind: "multiline", label, prop: label, rows: "3", signal: name, value: text },
844
+ {
845
+ live: "debounce",
846
+ value: text,
847
+ write: (v: string) => {
848
+ try {
849
+ patch({ [key]: JSON.parse(v) as JsonValue });
850
+ } catch {}
851
+ },
852
+ },
853
+ ),
854
+ ];
1148
855
  }
1149
- // Schema-driven fallback
1150
- return renderExternalPrototypeEditorTemplate(S, name, def, ctx);
856
+ return externalFields(S, name, def);
1151
857
  }
1152
858
 
1153
- /** Function fields for signal editor */
1154
- function renderFunctionFields(
1155
- S: SignalsPanelState,
1156
- name: string,
1157
- def: SignalDef,
1158
- _textareaRow: (
1159
- label: string,
1160
- value: string,
1161
- onChange: (value: string) => void,
1162
- opts?: { minHeight?: string; mono?: boolean },
1163
- ) => TemplateResult,
1164
- ctx: SignalsPanelCtx,
1165
- ) {
1166
- const descriptionField = signalFieldRow("Description", def.description || "", (v: string) =>
1167
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { description: v || undefined })),
1168
- );
859
+ /**
860
+ * An external `$prototype` — its source, and the config form its own schema describes.
861
+ *
862
+ * The schema arrives asynchronously and is cached, so the field list says "Loading schema…" once
863
+ * and the fetch's resolution is what repaints the panel with the form in it.
864
+ */
865
+ function externalFields(S: SignalsPanelState, name: string, def: SignalDef): SignalFieldView[] {
866
+ const patch = (value: Record<string, JsonValue | undefined>): void => {
867
+ transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, value));
868
+ };
869
+ const importedPath = def.$prototype
870
+ ? projectState?.projectConfig?.imports?.[def.$prototype]
871
+ : null;
872
+ const out: SignalFieldView[] = [];
1169
873
 
1170
- // Structured bodies (spec §20) are a body MODE of the function category, not a new entity:
1171
- // "Statements" renders the statement-card editor over `body: JxStatement[]`; "Code" is the
1172
- // Textarea/Monaco path over `body: string`. Switching modes replaces the body with the other
1173
- // Representation's empty seed — an explicit mode change, nothing is converted.
1174
- const bodyIsStatements = Array.isArray(def.body);
1175
- const bodyModeToggle = html`
1176
- <sp-action-group size="s" compact class="body-mode-toggle">
1177
- <sp-action-button
1178
- size="s"
1179
- class="body-mode-statements"
1180
- ?selected=${bodyIsStatements}
1181
- @click=${() => {
1182
- if (!bodyIsStatements) {
1183
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { body: [] }));
1184
- ctx.renderLeftPanel();
1185
- }
1186
- }}
1187
- >
1188
- Statements
1189
- </sp-action-button>
1190
- <sp-action-button
1191
- size="s"
1192
- class="body-mode-code"
1193
- ?selected=${!bodyIsStatements}
1194
- @click=${() => {
1195
- if (bodyIsStatements) {
1196
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { body: "" }));
1197
- ctx.renderLeftPanel();
1198
- }
1199
- }}
1200
- >
1201
- Code
1202
- </sp-action-button>
1203
- </sp-action-group>
1204
- `;
1205
-
1206
- const bodyField = def.$src
1207
- ? html`
1208
- ${signalFieldRow("Source", def.$src || "", (v: string) =>
1209
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { $src: v || undefined })),
1210
- )}
1211
- ${signalFieldRow("Export", def.$export || "", (v: string) =>
1212
- transactDoc(activeTab.value, (t) =>
1213
- mutateUpdateDef(t, name, { $export: v || undefined }),
1214
- ),
1215
- )}
1216
- `
1217
- : html`
1218
- <div style="display:flex;align-items:center;gap:4px">
1219
- <span class="field-label" style="flex:1">Body</span>
1220
- ${bodyModeToggle}
1221
- ${
1222
- bodyIsStatements
1223
- ? nothing
1224
- : html`
1225
- <sp-action-button
1226
- size="xs"
1227
- quiet
1228
- title="Open in code editor"
1229
- @click=${() => {
1230
- // Target AND reveal, in one call. See the formula button above.
1231
- openLogicTarget({
1232
- editing: { defName: name, type: "def" },
1233
- surface: "function",
1234
- });
1235
- }}
1236
- >
1237
- <sp-icon-code slot="icon"></sp-icon-code>
1238
- </sp-action-button>
1239
- `
874
+ if (importedPath) {
875
+ out.push(field({ key: "prototype", kind: "hint", signal: name, value: def.$prototype || "" }));
876
+ } else {
877
+ out.push(
878
+ textField(name, "src", "Source", def.$src || "", (v) => {
879
+ patch({ $src: v || undefined });
880
+ pluginSchemaCache.delete(`${v}::${def.$prototype}`);
881
+ }),
882
+ textField(name, "kind", "Kind", def.$prototype || "", (v) => {
883
+ patch({ $prototype: v || undefined });
884
+ pluginSchemaCache.delete(`${def.$src}::${v}`);
885
+ }),
886
+ );
887
+ }
888
+ if (def.$export) {
889
+ out.push(
890
+ textField(name, "export", "Export", def.$export || "", (v) =>
891
+ patch({ $export: v || undefined }),
892
+ ),
893
+ );
894
+ }
895
+
896
+ const resolvedSrc = def.$src || importedPath;
897
+ if (!resolvedSrc || !def.$prototype) {
898
+ return out;
899
+ }
900
+ const cacheKey = `${resolvedSrc}::${def.$prototype}`;
901
+ if (!pluginSchemaCache.has(cacheKey)) {
902
+ out.push(
903
+ field({ key: "schema-loading", kind: "hint", signal: name, value: "Loading schema…" }),
904
+ );
905
+ /* ONE request per schema, not one per repaint. `fetchPluginSchema` caches what it got but has
906
+ nothing in flight to answer with, so two projections between the ask and the reply sent the
907
+ same request twice — and the Navigator repaints on every document change. */
908
+ if (!schemaRequests.has(cacheKey)) {
909
+ schemaRequests.add(cacheKey);
910
+ void fetchPluginSchema(def, {
911
+ ...(S.documentPath != null && { documentPath: S.documentPath }),
912
+ })
913
+ .then((schema) => {
914
+ if (schema) {
915
+ repaint();
1240
916
  }
1241
- </div>
1242
- ${
1243
- bodyIsStatements
1244
- ? renderStatementEditor(
1245
- def.body as JxStatement[],
1246
- (next) => {
1247
- transactDoc(activeTab.value, (t) =>
1248
- mutateUpdateDef(t, name, { body: next as unknown as JsonValue }),
1249
- );
1250
- ctx.renderLeftPanel();
1251
- },
1252
- {
1253
- allowEventRef: true,
1254
- emits: def.emits ?? [],
1255
- // This editor is in the Navigator's State panel; the Events tab's is not.
1256
- region: NAVIGATOR_STATEMENTS_REGION,
1257
- stateDefs: Object.keys(S.document.state || {}),
1258
- stateEntries: S.document.state || {},
1259
- },
1260
- )
1261
- : html`
1262
- <textarea
1263
- class="field-input"
1264
- style="min-height:60px;font-family:var(--font-mono);font-size:var(--spectrum-font-size-50, 11px)"
1265
- .value=${typeof def.body === "string" ? def.body : ""}
1266
- @input=${(e: Event) => {
1267
- const v = (e.target as HTMLInputElement).value;
1268
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { body: v }));
1269
- }}
1270
- ></textarea>
1271
- `
1272
- }
1273
- `;
917
+ })
918
+ .finally(() => schemaRequests.delete(cacheKey));
919
+ }
920
+ return out;
921
+ }
922
+ const schema = pluginSchemaCache.get(cacheKey);
923
+ if (!schema) {
924
+ return out;
925
+ }
926
+ if (schema.description) {
927
+ out.push(field({ key: "schema-note", kind: "hint", signal: name, value: schema.description }));
928
+ }
929
+ out.push(field({ key: "schema", kind: "slot", signal: name, slot: "schema" }));
930
+ islands.set(`${name}/schema`, (host) => {
931
+ placeSchemaForm(host, schema as JsonSchema, def, name, S);
932
+ });
933
+ return out;
934
+ }
935
+
936
+ /** Write one of the three CEM text columns, dropping the key when the value is emptied. */
937
+ function writeCemField<T extends CemParameter | CemEvent>(
938
+ entry: T,
939
+ column: string,
940
+ value: string,
941
+ ): T {
942
+ if (column === "name") {
943
+ return { ...entry, name: value };
944
+ }
945
+ if (column === "type") {
946
+ const { type: _type, ...rest } = entry;
947
+ return (value ? { ...rest, type: { text: value } } : rest) as T;
948
+ }
949
+ const { description: _description, ...rest } = entry;
950
+ return (value ? { ...rest, description: value } : rest) as T;
951
+ }
1274
952
 
1275
- return html`
1276
- ${descriptionField} ${renderParameterEditorTemplate(S, name, def, ctx)}
1277
- ${isCustomElementDoc(S) ? renderEmitsEditorTemplate(S, name, def) : nothing} ${bodyField}
1278
- `;
953
+ /** One text column of a CEM table. */
954
+ function cemCell(
955
+ key: string,
956
+ placeholder: string,
957
+ value: string,
958
+ grow: string,
959
+ ): Omit<SignalCellView, "field" | "row" | "signal"> {
960
+ return { checked: false, grow, key, kind: "text", label: placeholder, placeholder, value };
1279
961
  }
1280
962
 
1281
- // ─── CEM Editors ─────────────────────────────────────────────────────────────
963
+ /** One row of a CEM table, with the identity every cell in it has to carry. */
964
+ function cemRow(
965
+ signal: string,
966
+ fieldKey: string,
967
+ key: string,
968
+ label: string,
969
+ cells: Omit<SignalCellView, "field" | "row" | "signal">[],
970
+ ): SignalCellRowView {
971
+ return {
972
+ cells: cells.map((cell) => ({ ...cell, field: fieldKey, row: key, signal })),
973
+ field: fieldKey,
974
+ key,
975
+ removeLabel: `Remove ${label}`,
976
+ signal,
977
+ };
978
+ }
1282
979
 
1283
- /** Render CEM parameter editor with basic/advanced toggle. */
1284
- function renderParameterEditorTemplate(
1285
- _S: SignalsPanelState,
1286
- name: string,
1287
- def: SignalDef,
1288
- ctx: SignalsPanelCtx,
1289
- ) {
980
+ /** The CEM parameter editor, in whichever of its two views is open. */
981
+ function parameterField(name: string, def: SignalDef): SignalFieldView {
1290
982
  const params = (def.parameters || []).map((p) => normParam(p));
1291
- const isAdvanced = advancedParamOpen.has(name);
1292
-
1293
- if (!isAdvanced) {
1294
- // Basic mode: name chips
1295
- return renderFieldRow({
1296
- hasValue: false,
983
+ const advanced = advancedParamOpen.has(name);
984
+ const write = (next: CemParameter[]): void => {
985
+ transactDoc(activeTab.value, (t) =>
986
+ mutateUpdateDef(t, name, { parameters: next.length > 0 ? next : undefined }),
987
+ );
988
+ };
989
+ const plan: FieldPlan = {
990
+ addChip: (value: string) => {
991
+ const trimmed = value.trim();
992
+ if (trimmed) {
993
+ write([...params, { name: trimmed }]);
994
+ }
995
+ },
996
+ addRow: () => {
997
+ transactDoc(activeTab.value, (t) =>
998
+ mutateUpdateDef(t, name, { parameters: [...params, { name: "" }] }),
999
+ );
1000
+ },
1001
+ cell: (row: string, cell: string, value: string) => {
1002
+ const next = [...params];
1003
+ const current = next[Number(row)];
1004
+ if (!current) {
1005
+ return;
1006
+ }
1007
+ next[Number(row)] = writeCemField(current, cell, value);
1008
+ write(next);
1009
+ },
1010
+ cellCheck: (row: string, _cell: string, checked: boolean) => {
1011
+ const next = [...params];
1012
+ const current = next[Number(row)];
1013
+ if (!current) {
1014
+ return;
1015
+ }
1016
+ const { optional: _optional, ...rest } = current;
1017
+ next[Number(row)] = checked ? { ...rest, optional: true } : rest;
1018
+ write(next);
1019
+ },
1020
+ dropChip: (chip: string) => write(params.filter((_, j) => j !== Number(chip))),
1021
+ dropRow: (row: string) => write(params.filter((_, j) => j !== Number(row))),
1022
+ live: "none",
1023
+ press: (action: string) => {
1024
+ if (action !== "footer") {
1025
+ return;
1026
+ }
1027
+ if (advanced) {
1028
+ advancedParamOpen.delete(name);
1029
+ } else {
1030
+ advancedParamOpen.add(name);
1031
+ }
1032
+ repaint();
1033
+ },
1034
+ value: "",
1035
+ };
1036
+
1037
+ if (!advanced) {
1038
+ const chips: SignalChipView[] = params.map((p, i) => ({
1039
+ field: "parameters",
1040
+ key: String(i),
1041
+ label: p.name || "?",
1042
+ removeLabel: `Remove ${p.name || "parameter"}`,
1043
+ signal: name,
1044
+ }));
1045
+ return field(
1046
+ {
1047
+ addLabel: "Add a parameter",
1048
+ chips,
1049
+ footer: "▸ Advanced",
1050
+ hasFooter: true,
1051
+ key: "parameters",
1052
+ kind: "chips",
1053
+ label: "Parameters",
1054
+ prop: "parameters",
1055
+ signal: name,
1056
+ },
1057
+ plan,
1058
+ );
1059
+ }
1060
+ return field(
1061
+ {
1062
+ addLabel: "Add parameter",
1063
+ cellRows: params.map((p, i) =>
1064
+ cemRow(name, "parameters", String(i), p.name || "parameter", [
1065
+ cemCell("name", "name", p.name || "", "1"),
1066
+ cemCell("type", "type", cemTypeText(p.type), "1"),
1067
+ cemCell("description", "desc", p.description || "", "2"),
1068
+ {
1069
+ checked: Boolean(p.optional),
1070
+ grow: "0",
1071
+ key: "optional",
1072
+ kind: "checkbox",
1073
+ label: "Optional",
1074
+ placeholder: "",
1075
+ value: "",
1076
+ },
1077
+ ]),
1078
+ ),
1079
+ footer: "▾ Basic",
1080
+ hasFooter: true,
1081
+ key: "parameters",
1082
+ kind: "rows",
1297
1083
  label: "Parameters",
1298
1084
  prop: "parameters",
1299
- widget: html`
1300
- <div style="display:flex;flex-wrap:wrap;gap:4px;align-items:center">
1301
- ${params.map(
1302
- (p: CemParameter, i: number) => html`
1303
- <span
1304
- style="display:inline-flex;align-items:center;gap:2px;padding:1px 6px;border-radius:var(--radius);background:var(--hover-bg);font-size:var(--spectrum-font-size-50, 11px);font-family:var(--font-mono)"
1305
- >
1306
- ${p.name || "?"}
1307
- <span
1308
- style="cursor:pointer;opacity:0.5;margin-left:2px"
1309
- @click=${() => {
1310
- transactDoc(activeTab.value, (t) =>
1311
- mutateUpdateDef(t, name, {
1312
- parameters: params.some((_: unknown, j: number) => j !== i)
1313
- ? params.filter((_: unknown, j: number) => j !== i)
1314
- : undefined,
1315
- }),
1316
- );
1317
- }}
1318
- >×</span
1319
- >
1320
- </span>
1321
- `,
1322
- )}
1323
- <input
1324
- class="field-input"
1325
- style="width:60px;flex:0 0 auto;font-size:var(--spectrum-font-size-50, 11px)"
1326
- placeholder="+"
1327
- @keydown=${(e: KeyboardEvent) => {
1328
- if (e.key === "Enter" && (e.target as HTMLInputElement).value.trim()) {
1329
- transactDoc(activeTab.value, (t) =>
1330
- mutateUpdateDef(t, name, {
1331
- parameters: [...params, { name: (e.target as HTMLInputElement).value.trim() }],
1332
- }),
1333
- );
1334
- }
1335
- }}
1336
- />
1337
- </div>
1338
- <span
1339
- style="font-size:10px;color:var(--fg-dim);cursor:pointer;width:100%;margin-top:2px"
1340
- @click=${() => {
1341
- advancedParamOpen.add(name);
1342
- ctx.renderLeftPanel();
1343
- }}
1344
- >▸ Advanced</span
1345
- >
1346
- `,
1085
+ signal: name,
1086
+ span: true,
1087
+ },
1088
+ plan,
1089
+ );
1090
+ }
1091
+
1092
+ /** The CEM emits editor — the same table shape, over `emits`. */
1093
+ function emitsField(name: string, def: SignalDef): SignalFieldView {
1094
+ const emits = def.emits || ([] as CemEvent[]);
1095
+ const write = (next: CemEvent[]): void => {
1096
+ transactDoc(activeTab.value, (t) =>
1097
+ mutateUpdateDef(t, name, { emits: next.length > 0 ? next : undefined }),
1098
+ );
1099
+ };
1100
+ return field(
1101
+ {
1102
+ addLabel: "Add event",
1103
+ cellRows: emits.map((e, i) =>
1104
+ cemRow(name, "emits", String(i), e.name || "event", [
1105
+ cemCell("name", "event name", e.name || "", "1"),
1106
+ cemCell("type", "type", cemTypeText(e.type), "1"),
1107
+ cemCell("description", "description", e.description || "", "2"),
1108
+ ]),
1109
+ ),
1110
+ key: "emits",
1111
+ kind: "rows",
1112
+ label: "Emits",
1113
+ prop: "emits",
1114
+ signal: name,
1115
+ span: true,
1116
+ },
1117
+ {
1118
+ addRow: () => {
1119
+ transactDoc(activeTab.value, (t) =>
1120
+ mutateUpdateDef(t, name, { emits: [...emits, { name: "" }] }),
1121
+ );
1122
+ },
1123
+ cell: (row: string, cell: string, value: string) => {
1124
+ const next = [...emits];
1125
+ const current = next[Number(row)];
1126
+ if (!current) {
1127
+ return;
1128
+ }
1129
+ next[Number(row)] = writeCemField(current, cell, value);
1130
+ write(next);
1131
+ },
1132
+ dropRow: (row: string) => write(emits.filter((_, j) => j !== Number(row))),
1133
+ live: "none",
1134
+ value: "",
1135
+ },
1136
+ );
1137
+ }
1138
+
1139
+ /**
1140
+ * A function: what it is for, what it takes, what it emits, and its body.
1141
+ *
1142
+ * Structured bodies (spec §20) are a body MODE rather than a new entity: "Statements" is the
1143
+ * statement-card editor over `body: JxStatement[]`, "Code" is the text path over `body: string`.
1144
+ * Switching modes replaces the body with the other representation's empty seed — an explicit mode
1145
+ * change, nothing is converted.
1146
+ */
1147
+ function functionFields(S: SignalsPanelState, name: string, def: SignalDef): SignalFieldView[] {
1148
+ const patch = (value: Record<string, JsonValue | undefined>): void => {
1149
+ transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, value));
1150
+ };
1151
+ const out: SignalFieldView[] = [
1152
+ textField(name, "description", "Description", def.description || "", (v) =>
1153
+ patch({ description: v || undefined }),
1154
+ ),
1155
+ parameterField(name, def),
1156
+ ];
1157
+ if (isCustomElementDoc(S)) {
1158
+ out.push(emitsField(name, def));
1159
+ }
1160
+
1161
+ if (def.$src) {
1162
+ out.push(
1163
+ textField(name, "src", "Source", def.$src || "", (v) => patch({ $src: v || undefined })),
1164
+ textField(name, "export", "Export", def.$export || "", (v) =>
1165
+ patch({ $export: v || undefined }),
1166
+ ),
1167
+ );
1168
+ return out;
1169
+ }
1170
+
1171
+ const statements = Array.isArray(def.body);
1172
+ out.push(
1173
+ field(
1174
+ {
1175
+ buttons: statements
1176
+ ? []
1177
+ : [
1178
+ {
1179
+ field: "body",
1180
+ icon: "code",
1181
+ key: "editor",
1182
+ label: "Open in code editor",
1183
+ signal: name,
1184
+ },
1185
+ ],
1186
+ key: "body",
1187
+ kind: "bar",
1188
+ label: "Body",
1189
+ prop: "Body",
1190
+ segments: [
1191
+ {
1192
+ field: "body",
1193
+ key: "statements",
1194
+ label: "Statements",
1195
+ selected: statements,
1196
+ signal: name,
1197
+ },
1198
+ { field: "body", key: "code", label: "Code", selected: !statements, signal: name },
1199
+ ],
1200
+ signal: name,
1201
+ },
1202
+ {
1203
+ live: "none",
1204
+ press: (action: string) => {
1205
+ if (action === "statements" && !statements) {
1206
+ patch({ body: [] });
1207
+ repaint();
1208
+ } else if (action === "code" && statements) {
1209
+ patch({ body: "" });
1210
+ repaint();
1211
+ } else if (action === "editor") {
1212
+ // Target AND reveal, in one call. See the formula button below.
1213
+ openLogicTarget({ editing: { defName: name, type: "def" }, surface: "function" });
1214
+ }
1215
+ },
1216
+ value: "",
1217
+ },
1218
+ ),
1219
+ );
1220
+
1221
+ if (statements) {
1222
+ out.push(field({ key: "statements", kind: "slot", signal: name, slot: "statements" }));
1223
+ islands.set(`${name}/statements`, (host) => {
1224
+ mountStatementEditor(
1225
+ host,
1226
+ def.body as JxStatement[],
1227
+ (next) => {
1228
+ patch({ body: next as unknown as JsonValue });
1229
+ repaint();
1230
+ },
1231
+ {
1232
+ allowEventRef: true,
1233
+ emits: def.emits ?? [],
1234
+ // This editor is in the Navigator's Data panel; the Events tab's is not.
1235
+ region: NAVIGATOR_STATEMENTS_REGION,
1236
+ stateDefs: Object.keys(S.document.state || {}),
1237
+ stateEntries: S.document.state || {},
1238
+ },
1239
+ );
1347
1240
  });
1241
+ return out;
1348
1242
  }
1243
+ const source = typeof def.body === "string" ? def.body : "";
1244
+ out.push(
1245
+ field(
1246
+ {
1247
+ key: "code",
1248
+ kind: "code",
1249
+ label: "Body",
1250
+ prop: "Body",
1251
+ rows: "3",
1252
+ signal: name,
1253
+ value: source,
1254
+ },
1255
+ { live: "commit", value: source, write: (v: string) => patch({ body: v }) },
1256
+ ),
1257
+ );
1258
+ return out;
1259
+ }
1349
1260
 
1350
- // Advanced mode: full rows
1351
- return renderFieldRow({
1352
- hasValue: false,
1353
- label: "Parameters",
1354
- prop: "parameters",
1355
- widget: html`
1356
- <div style="display:flex;flex-direction:column;gap:4px">
1357
- ${params.map(
1358
- (p: CemParameter, i: number) => html`
1359
- <div style="display:flex;gap:4px;align-items:center">
1360
- <input
1361
- class="field-input"
1362
- .value=${p.name || ""}
1363
- placeholder="name"
1364
- style="flex:1"
1365
- @change=${(e: Event) => {
1366
- const next = [...params];
1367
- next[i] = {
1368
- ...next[i],
1369
- name: (e.target as HTMLInputElement).value,
1370
- };
1371
- transactDoc(activeTab.value, (t) =>
1372
- mutateUpdateDef(t, name, { parameters: next }),
1373
- );
1374
- }}
1375
- />
1376
- <input
1377
- class="field-input"
1378
- .value=${cemTypeText(p.type)}
1379
- placeholder="type"
1380
- style="flex:1"
1381
- @change=${(e: Event) => {
1382
- const next = [...params];
1383
- const val = (e.target as HTMLInputElement).value;
1384
- const { type: _t, ...rest } = next[i]!;
1385
- next[i] = val ? { ...rest, type: { text: val } } : rest;
1386
- transactDoc(activeTab.value, (t) =>
1387
- mutateUpdateDef(t, name, { parameters: next }),
1388
- );
1389
- }}
1390
- />
1391
- <input
1392
- class="field-input"
1393
- .value=${p.description || ""}
1394
- placeholder="desc"
1395
- style="flex:2"
1396
- @change=${(e: Event) => {
1397
- const next = [...params];
1398
- const val = (e.target as HTMLInputElement).value;
1399
- const { description: _d, ...rest } = next[i]!;
1400
- next[i] = val ? { ...rest, description: val } : rest;
1401
- transactDoc(activeTab.value, (t) =>
1402
- mutateUpdateDef(t, name, { parameters: next }),
1403
- );
1404
- }}
1405
- />
1406
- <input
1407
- type="checkbox"
1408
- title="optional"
1409
- .checked=${Boolean(p.optional)}
1410
- @change=${(e: Event) => {
1411
- const next = [...params];
1412
- const { checked } = e.target as HTMLInputElement;
1413
- const { optional: _o, ...rest } = next[i]!;
1414
- next[i] = checked ? { ...rest, optional: true } : rest;
1415
- transactDoc(activeTab.value, (t) =>
1416
- mutateUpdateDef(t, name, { parameters: next }),
1417
- );
1418
- }}
1419
- />
1420
- <span
1421
- style="cursor:pointer;opacity:0.5"
1422
- @click=${() => {
1423
- const next = params.filter((_: unknown, j: number) => j !== i);
1424
- transactDoc(activeTab.value, (t) =>
1425
- mutateUpdateDef(t, name, {
1426
- parameters: next.length > 0 ? next : undefined,
1427
- }),
1428
- );
1429
- }}
1430
- >×</span
1431
- >
1432
- </div>
1433
- `,
1434
- )}
1435
- <button
1436
- class="kv-add"
1437
- @click=${() =>
1438
- transactDoc(activeTab.value, (t) =>
1439
- mutateUpdateDef(t, name, {
1440
- parameters: [...params, { name: "" }],
1441
- }),
1442
- )}
1443
- >
1444
- + Add parameter
1445
- </button>
1446
- </div>
1447
- <span
1448
- style="font-size:10px;color:var(--fg-dim);cursor:pointer;width:100%;margin-top:2px"
1449
- @click=${() => {
1450
- advancedParamOpen.delete(name);
1451
- ctx.renderLeftPanel();
1452
- }}
1453
- >▾ Basic</span
1454
- >
1455
- `,
1261
+ /** An assignment or a call the page performs — edited as a formula tree. */
1262
+ function expressionFields(S: SignalsPanelState, name: string, def: SignalDef): SignalFieldView[] {
1263
+ const exprNode = def.$expression || { operator: "=", target: null };
1264
+ const out: SignalFieldView[] = [
1265
+ field(
1266
+ {
1267
+ buttons: [
1268
+ {
1269
+ field: "expression",
1270
+ icon: "align-bottom",
1271
+ key: "formula",
1272
+ label: "Open in formula workspace",
1273
+ signal: name,
1274
+ },
1275
+ ],
1276
+ key: "expression",
1277
+ kind: "bar",
1278
+ label: "Expression",
1279
+ prop: "Expression",
1280
+ signal: name,
1281
+ },
1282
+ {
1283
+ live: "none",
1284
+ /* One call, because a click is BOTH events: it names the target and it asks for the
1285
+ surface. Setting the field alone leaned on the dock's reveal effect, which fires at most
1286
+ once per target — so clicking this again after closing the dock was a dead click. It also
1287
+ left `editingFunction` set, and that one wins the tie. */
1288
+ press: () =>
1289
+ openLogicTarget({ editing: { defName: name, type: "def" }, surface: "formula" }),
1290
+ value: "",
1291
+ },
1292
+ ),
1293
+ field({ key: "editor", kind: "slot", signal: name, slot: "expression" }),
1294
+ ];
1295
+ islands.set(`${name}/expression`, (host) => {
1296
+ mountExpressionEditor(
1297
+ host,
1298
+ exprNode,
1299
+ (newNode: unknown) =>
1300
+ transactDoc(activeTab.value, (t) =>
1301
+ // Expression editors emit JSON expression nodes.
1302
+ mutateUpdateDef(t, name, { $expression: newNode as JsonValue }),
1303
+ ),
1304
+ {
1305
+ allowEventRef: false,
1306
+ // Live-context evaluation in the canvas iframe, snapshot fallback (M6). The panel lives
1307
+ // In the Navigator — re-render it when a fresh live result lands.
1308
+ preview: livePreviewExpression(activeTab.value, `def:${name}`, exprNode, null, () =>
1309
+ renderOnly("leftPanel"),
1310
+ ),
1311
+ onInsertDef: (defName, vendored) =>
1312
+ transactDoc(activeTab.value, (t) =>
1313
+ mutateAddDef(t, defName, vendored as Record<string, JsonValue>),
1314
+ ),
1315
+ stateDefs: Object.keys(S.document.state || {}),
1316
+ stateEntries: S.document.state || {},
1317
+ },
1318
+ );
1456
1319
  });
1320
+ return out;
1457
1321
  }
1458
1322
 
1459
- /** Render CEM emits editor for function state entries. */
1460
- function renderEmitsEditorTemplate(S: SignalsPanelState, name: string, def: SignalDef) {
1461
- const emits = def.emits || ([] as CemEvent[]);
1462
- if (emits.length === 0 && !isCustomElementDoc(S)) {
1463
- return nothing;
1464
- }
1465
-
1466
- return html`
1467
- <div
1468
- style="font-size:var(--spectrum-font-size-50, 11px);font-weight:600;color:var(--fg-dim);margin:8px 0 4px;text-transform:uppercase;letter-spacing:0.05em"
1469
- >
1470
- Emits
1471
- </div>
1472
- ${emits.map(
1473
- (ev: CemEvent, i: number) => html`
1474
- <div style="display:flex;gap:4px;align-items:center;margin-bottom:4px">
1475
- <input
1476
- class="field-input"
1477
- .value=${ev.name || ""}
1478
- placeholder="event name"
1479
- style="flex:1"
1480
- @change=${(e: Event) => {
1481
- const next = [...emits];
1482
- next[i] = {
1483
- ...next[i],
1484
- name: (e.target as HTMLInputElement).value,
1485
- };
1486
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { emits: next }));
1487
- }}
1488
- />
1489
- <input
1490
- class="field-input"
1491
- .value=${cemTypeText(ev.type)}
1492
- placeholder="type"
1493
- style="flex:1"
1494
- @change=${(e: Event) => {
1495
- const next = [...emits];
1496
- const val = (e.target as HTMLInputElement).value;
1497
- const { type: _t, ...rest } = next[i]!;
1498
- next[i] = val ? { ...rest, type: { text: val } } : rest;
1499
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { emits: next }));
1500
- }}
1501
- />
1502
- <input
1503
- class="field-input"
1504
- .value=${ev.description || ""}
1505
- placeholder="description"
1506
- style="flex:2"
1507
- @change=${(e: Event) => {
1508
- const next = [...emits];
1509
- const val = (e.target as HTMLInputElement).value;
1510
- const { description: _d, ...rest } = next[i]!;
1511
- next[i] = val ? { ...rest, description: val } : rest;
1512
- transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, { emits: next }));
1513
- }}
1514
- />
1515
- <span
1516
- style="cursor:pointer;opacity:0.5"
1517
- @click=${() => {
1518
- transactDoc(activeTab.value, (t) =>
1519
- mutateUpdateDef(t, name, {
1520
- emits: emits.some((_: unknown, j: number) => j !== i)
1521
- ? emits.filter((_: unknown, j: number) => j !== i)
1522
- : undefined,
1523
- }),
1524
- );
1525
- }}
1526
- >×</span
1527
- >
1528
- </div>
1529
- `,
1530
- )}
1531
- <button
1532
- class="kv-add"
1533
- @click=${() =>
1534
- transactDoc(activeTab.value, (t) =>
1535
- mutateUpdateDef(t, name, { emits: [...emits, { name: "" }] }),
1536
- )}
1537
- >
1538
- + Add event
1539
- </button>
1540
- `;
1323
+ /** Every field of one entry's editor, in the order they are read. */
1324
+ function editorFields(S: SignalsPanelState, name: string, def: SignalDef): SignalFieldView[] {
1325
+ const head = nameField(S, name);
1326
+ switch (defCategory(def)) {
1327
+ case "state": {
1328
+ return [head, ...stateFields(S, name, def)];
1329
+ }
1330
+ case "computed": {
1331
+ return [head, ...computedFields(name, def)];
1332
+ }
1333
+ case "data": {
1334
+ return [head, ...dataFields(S, name, def)];
1335
+ }
1336
+ case "function": {
1337
+ return [head, ...functionFields(S, name, def)];
1338
+ }
1339
+ case "expression": {
1340
+ return [head, ...expressionFields(S, name, def)];
1341
+ }
1342
+ default: {
1343
+ return [head];
1344
+ }
1345
+ }
1541
1346
  }
1542
1347
 
1543
- // ─── Plugin schema-driven form rendering ────────────────────────────────────
1348
+ // ─── Plugin schema-driven form ───────────────────────────────────────────────
1544
1349
 
1545
1350
  /**
1546
1351
  * Resolve a schema context pointer for signal config forms — a thin wrapper over the generic
@@ -1567,120 +1372,461 @@ function resolveSignalsContextPointer(pointer: string, scope?: Record<string, un
1567
1372
  }
1568
1373
 
1569
1374
  /**
1570
- * Render config form fields from a JSON Schema `properties` object — a thin wrapper over the shared
1571
- * schema-form engine. Skips studio-reserved keys, resolves enum/context refs against the project
1572
- * config, and commits every patch through transactDoc/mutateUpdateDef.
1375
+ * Put the shared schema form into the node the document made for it.
1376
+ *
1377
+ * `ui/schema-form.ts` is a document too, and a document CLEARS the host it is given — so what comes
1378
+ * back is the form's own host element rather than a template, and this function's whole job is to
1379
+ * place it. Calling `mountSchemaForm` again with the same key updates the standing form in place,
1380
+ * which is what keeps the caret in a field across the repaint a commit provokes.
1381
+ *
1382
+ * @param {HTMLElement} host - The empty node the document drew
1383
+ * @param {JsonSchema} schema - The plugin's own schema
1384
+ * @param {SignalDef} def - The entry being configured
1385
+ * @param {string} name - The entry's name
1386
+ * @param {SignalsPanelState} S - The focused document
1573
1387
  */
1574
- export function renderSchemaFieldsTemplate(
1575
- schema: JsonSchema | null | undefined,
1388
+ function placeSchemaForm(
1389
+ host: HTMLElement,
1390
+ schema: JsonSchema,
1576
1391
  def: SignalDef,
1577
1392
  name: string,
1578
1393
  S: SignalsPanelState,
1579
- ctx: SignalsPanelCtx | null = null,
1580
- ) {
1581
- if (!schema?.properties) {
1582
- return nothing;
1394
+ ): void {
1395
+ if (!schema.properties) {
1396
+ host.replaceChildren();
1397
+ return;
1583
1398
  }
1584
-
1585
1399
  const properties = Object.fromEntries(
1586
1400
  Object.entries(schema.properties).filter(([prop]) => !STUDIO_RESERVED_KEYS.has(prop)),
1587
1401
  );
1588
-
1589
- return renderForm({ ...schema, properties }, def as Record<string, unknown>, {
1590
- context: {
1591
- fieldKeyPrefix: name,
1592
- params: dynamicRouteParams(S.documentPath),
1593
- resolvePointer: resolveSignalsContextPointer,
1594
- // A config value may point at any signal but this one — a def that reads itself is a cycle.
1595
- signals: bindableSignalNames(S.document).filter((signal) => signal !== name),
1402
+ const form = mountSchemaForm(
1403
+ `signal:${name}`,
1404
+ { ...schema, properties },
1405
+ def as Record<string, unknown>,
1406
+ {
1407
+ context: {
1408
+ fieldKeyPrefix: name,
1409
+ params: dynamicRouteParams(S.documentPath),
1410
+ resolvePointer: resolveSignalsContextPointer,
1411
+ // A config value may point at any signal but this one — a def that reads itself is a cycle.
1412
+ signals: bindableSignalNames(S.document).filter((signal) => signal !== name),
1413
+ },
1414
+ onChange: (patch) => transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, patch)),
1415
+ rerender: repaint,
1596
1416
  },
1597
- onChange: (patch) => transactDoc(activeTab.value, (t) => mutateUpdateDef(t, name, patch)),
1598
- ...(ctx && { rerender: () => ctx.renderLeftPanel() }),
1599
- });
1417
+ );
1418
+ if (form.parentNode !== host) {
1419
+ host.replaceChildren(form);
1420
+ }
1600
1421
  }
1601
1422
 
1423
+ // ─── The projection ──────────────────────────────────────────────────────────
1424
+
1602
1425
  /**
1603
- * Render editor fields for an external $prototype + $src plugin. Shows $src/$export inputs plus
1604
- * schema-driven config fields.
1426
+ * ONE summary slot per row, and the resolved value wins it as soon as there is one.
1427
+ *
1428
+ * The row wants to say four things about an entry — its category, its name, how it is defined and
1429
+ * what it became — and a 240px Navigator fits three. Eliding both summaries to make room produced
1430
+ * "recentproje… C… Array(3)": two truncated descriptions and a truncated identity. So the
1431
+ * definition hint holds the slot until the canvas resolves a value and then steps aside for it, and
1432
+ * the full definition is one click down in fields — which is what the hint was abbreviating.
1433
+ *
1434
+ * The switch is whether the canvas has reported a scope AT ALL, not whether this entry appears in
1435
+ * it. An entry the canvas ran and did not produce is "pending", which is a fact about the value; a
1436
+ * panel opened before the canvas has rendered knows nothing about any of them, and guessing
1437
+ * "pending" for the whole list there would be a fact about the panel dressed up as one about data.
1438
+ *
1439
+ * ENTRIES THAT CANNOT HOLD A VALUE never get the column. A function and an assignment expression
1440
+ * are things the page DOES, not things it knows, and they are absent from the resolved scope for
1441
+ * exactly that reason — so the value column called all eight of a component's `setFilter` handlers
1442
+ * "pending", which reads as "still loading" for something that will never load.
1605
1443
  */
1606
- export function renderExternalPrototypeEditorTemplate(
1607
- S: SignalsPanelState,
1444
+ function summary(
1608
1445
  name: string,
1609
1446
  def: SignalDef,
1610
- ctx: SignalsPanelCtx,
1611
- ) {
1612
- // Schema-driven config fields (async with cache)
1613
- let schemaContent: TemplateResult | typeof nothing = nothing;
1614
- const importedPath = def.$prototype
1615
- ? projectState?.projectConfig?.imports?.[def.$prototype]
1616
- : null;
1617
- const resolvedSrc = def.$src || importedPath;
1618
- if (resolvedSrc && def.$prototype) {
1619
- const cacheKey = `${resolvedSrc}::${def.$prototype}`;
1620
- if (pluginSchemaCache.has(cacheKey)) {
1621
- const schema = pluginSchemaCache.get(cacheKey);
1622
- if (schema) {
1623
- schemaContent = html`
1624
- ${
1625
- schema.description
1626
- ? html`<div class="signal-hint" style="padding:4px 0 8px">${schema.description}</div>`
1627
- : nothing
1628
- }
1629
- ${renderSchemaFieldsTemplate(schema as JsonSchema, def, name, S, ctx)}
1630
- `;
1631
- }
1632
- } else {
1633
- // Trigger async load — will re-render when cached
1634
- schemaContent = html`<div
1635
- style="padding:4px 0;font-size:var(--spectrum-font-size-50, 11px);color:var(--fg-dim);font-style:italic"
1636
- >
1637
- Loading schema…
1638
- </div>`;
1639
- void fetchPluginSchema(def, {
1640
- ...(S.documentPath != null && { documentPath: S.documentPath }),
1641
- }).then((schema) => {
1642
- if (schema) {
1643
- ctx.renderLeftPanel();
1644
- }
1645
- });
1447
+ live: unknown,
1448
+ resolved: boolean,
1449
+ ): { summary: string; summaryTitle: string; summaryTone: string } {
1450
+ const holdsNoValue =
1451
+ defCategory(def) === "function" ||
1452
+ (def.$expression != null && isActionExpression(def.$expression));
1453
+ if (!resolved || holdsNoValue) {
1454
+ const hint = defHint(name, def);
1455
+ return { summary: hint, summaryTitle: hint, summaryTone: "hint" };
1456
+ }
1457
+ return {
1458
+ summary: dataTypeLabel(live),
1459
+ summaryTitle: "What this resolved to on the canvas",
1460
+ summaryTone: unwrapSignal(live) === null ? "pending" : "value",
1461
+ };
1462
+ }
1463
+
1464
+ /** The five categories, in the order the panel lists them. */
1465
+ const CATEGORY_LABELS: readonly { key: string; label: string }[] = [
1466
+ { key: "state", label: "State" },
1467
+ { key: "computed", label: "Computed" },
1468
+ { key: "data", label: "Data" },
1469
+ { key: "expression", label: "Expressions" },
1470
+ { key: "function", label: "Functions" },
1471
+ ];
1472
+
1473
+ /**
1474
+ * The picker's fixed rows.
1475
+ *
1476
+ * GROUPED rather than divided: `jx-select` draws both halves of a group's delimiter, so the three
1477
+ * unlabelled `sp-menu-divider`s this replaces are named runs a reader can aim at.
1478
+ */
1479
+ const ADD_GROUPS: readonly SignalOptionGroup[] = [
1480
+ {
1481
+ id: "values",
1482
+ label: "Values",
1483
+ rows: [
1484
+ { label: "Value", value: "state" },
1485
+ { label: "Computed", value: "computed" },
1486
+ ],
1487
+ },
1488
+ {
1489
+ id: "sources",
1490
+ label: "Data sources",
1491
+ rows: [
1492
+ { label: "Fetch from a URL", value: "request" },
1493
+ { label: "LocalStorage", value: "localStorage" },
1494
+ { label: "SessionStorage", value: "sessionStorage" },
1495
+ { label: "IndexedDB", value: "indexedDB" },
1496
+ { label: "Cookie", value: "cookie" },
1497
+ { label: "Set", value: "set" },
1498
+ { label: "Map", value: "map" },
1499
+ { label: "FormData", value: "formData" },
1500
+ { label: "From a module…", value: "external" },
1501
+ ],
1502
+ },
1503
+ {
1504
+ id: "logic",
1505
+ label: "Logic",
1506
+ rows: [
1507
+ { label: "Expression", value: "expression" },
1508
+ { label: "Function", value: "function" },
1509
+ ],
1510
+ },
1511
+ ];
1512
+
1513
+ /** Everything the surface should be showing, from the panel state it was last handed. */
1514
+ export function signalsView(S: SignalsPanelState, ctx: SignalsPanelCtx): SignalsView {
1515
+ plans.clear();
1516
+ islands.clear();
1517
+
1518
+ const defs = S.document.state || {};
1519
+ const entries = Object.entries(defs);
1520
+ /* What the canvas actually resolved these to. `S.canvas` is the session's canvas record, so this
1521
+ costs a property read — the panel already had the scope in hand. */
1522
+ const liveScope = (S.canvas?.scope ?? null) as Record<string, unknown> | null;
1523
+ const scope = liveScope ?? {};
1524
+ /* A Refresh is out and the canvas has not answered. Read off the tab, so it survives the repaints
1525
+ between the press and the `dataScope` that ends it. */
1526
+ const refreshing = S.canvas?.refreshing === true;
1527
+
1528
+ /* Warm the extensions payload so manifest state classes appear in the add picker (the panel
1529
+ re-renders constantly; loadExtensions memoizes, so this is a one-time fetch per project). */
1530
+ void loadExtensions();
1531
+
1532
+ const grouped = new Map<string, [string, SignalDef][]>(
1533
+ CATEGORY_LABELS.map(({ key }) => [key, []]),
1534
+ );
1535
+ for (const [name, raw] of entries) {
1536
+ grouped.get(defCategory(raw))?.push([name, asSignalDef(raw)]);
1537
+ }
1538
+
1539
+ S._collapsedSignalCats ||= new Set();
1540
+ const collapsed = S._collapsedSignalCats;
1541
+
1542
+ const categories: SignalCategoryView[] = [];
1543
+ for (const { key, label } of CATEGORY_LABELS) {
1544
+ const items = grouped.get(key) ?? [];
1545
+ if (items.length === 0) {
1546
+ continue;
1646
1547
  }
1548
+ const rows: SignalRowView[] = items.map(([name, def]) => {
1549
+ const expanded = isDataRowExpanded(name);
1550
+ const live = scope[name];
1551
+ if (expanded) {
1552
+ islands.set(`${name}/tree`, (host) => {
1553
+ paintDataTree(host, unwrapSignal(live), name, repaint);
1554
+ });
1555
+ }
1556
+ return {
1557
+ badge: defBadgeLabel(def),
1558
+ category: defCategory(def),
1559
+ categoryLabel: label,
1560
+ deleteLabel: `Delete ${name}`,
1561
+ expanded,
1562
+ fields: expanded ? editorFields(S, name, def) : [],
1563
+ key: name,
1564
+ liveLabel: "Resolved to",
1565
+ name,
1566
+ ...summary(name, def, live, liveScope !== null),
1567
+ };
1568
+ });
1569
+ categories.push({ key, label: `${label} (${items.length})`, open: !collapsed.has(key), rows });
1570
+ }
1571
+
1572
+ const groups = [...ADD_GROUPS];
1573
+ const imports = projectState?.projectConfig?.imports;
1574
+ if (imports) {
1575
+ groups.splice(2, 0, {
1576
+ id: "imports",
1577
+ label: "Project imports",
1578
+ rows: Object.keys(imports).map((k) => ({ label: k, value: `import:${k}` })),
1579
+ });
1580
+ }
1581
+ const classes = extensionStateClasses();
1582
+ if (classes.length > 0) {
1583
+ groups.splice(imports ? 3 : 2, 0, {
1584
+ id: "extensions",
1585
+ label: "Extensions",
1586
+ rows: classes.map((cls) => ({ label: cls.name, value: `ext:${cls.name}` })),
1587
+ });
1588
+ }
1589
+
1590
+ return {
1591
+ addGroups: groups,
1592
+ addOptions: [{ label: "+ Add…", value: "" }],
1593
+ addValue: "",
1594
+ categories,
1595
+ emptyMessage:
1596
+ "Data lives here — values this page can read, compute or fetch, " +
1597
+ "ready to bind to any element.",
1598
+ listState: entries.length === 0 ? "empty" : "listed",
1599
+ refreshState: ctx.refreshData && entries.length > 0 ? (refreshing ? "busy" : "idle") : "none",
1600
+ refreshing,
1601
+ };
1602
+ }
1603
+
1604
+ // ─── Adding an entry ─────────────────────────────────────────────────────────
1605
+
1606
+ /** The first free name of the form `<base>`, `<base>1`, `<base>2`… */
1607
+ function freeName(S: SignalsPanelState, base: string): string {
1608
+ let candidate = base;
1609
+ let i = 1;
1610
+ while (S.document.state && S.document.state[candidate]) {
1611
+ candidate = base + i;
1612
+ i += 1;
1613
+ }
1614
+ return candidate;
1615
+ }
1616
+
1617
+ /**
1618
+ * Add a def from one of the built-in templates under a free name, and expand it for editing. Shared
1619
+ * by the "+ Add…" picker and the panel's empty state, so both create the same thing.
1620
+ */
1621
+ function addTemplateDef(type: string, S: SignalsPanelState): void {
1622
+ const template = DEF_TEMPLATES[type];
1623
+ if (!template) {
1624
+ return;
1647
1625
  }
1626
+ const name = freeName(S, type === "function" ? "newFunction" : "$newSignal");
1627
+ transactDoc(activeTab.value, (t) =>
1628
+ mutateAddDef(t, name, structuredClone(template) as Record<string, JsonValue>),
1629
+ );
1630
+ setDataRowExpanded(name, true);
1631
+ repaint();
1632
+ }
1633
+
1634
+ /** The name a prototype-backed entry is created under: `$contentCollection` for `ContentCollection`. */
1635
+ function prototypeName(S: SignalsPanelState, proto: string): string {
1636
+ return freeName(S, `$${proto.charAt(0).toLowerCase()}${proto.slice(1)}`);
1637
+ }
1648
1638
 
1649
- return html`
1650
- ${
1651
- importedPath
1652
- ? html`<div
1653
- class="signal-hint"
1654
- style="padding:4px 0 2px;font-size:var(--spectrum-font-size-50, 11px);color:var(--fg-dim)"
1655
- >
1656
- ${def.$prototype}
1657
- </div>`
1658
- : html`
1659
- ${signalFieldRow("Source", def.$src || "", (v: string) => {
1660
- transactDoc(activeTab.value, (t) =>
1661
- mutateUpdateDef(t, name, { $src: v || undefined }),
1662
- );
1663
- pluginSchemaCache.delete(`${v}::${def.$prototype}`);
1664
- })}
1665
- ${signalFieldRow("Kind", def.$prototype || "", (v: string) => {
1666
- transactDoc(activeTab.value, (t) =>
1667
- mutateUpdateDef(t, name, { $prototype: v || undefined }),
1668
- );
1669
- pluginSchemaCache.delete(`${def.$src}::${v}`);
1670
- })}
1671
- `
1639
+ /** Create the entry the picker names — a template, an extension class, or a project import. */
1640
+ function addSignalOfType(type: string): void {
1641
+ const S = panelState;
1642
+ if (!S || !type) {
1643
+ return;
1644
+ }
1645
+ /* Extension-manifest state classes ("ext:Session"): no $src needed — the registry resolves them;
1646
+ the descriptor's stateDefaults seed the def (e.g. timing "client"). */
1647
+ if (type.startsWith("ext:")) {
1648
+ const proto = type.slice(4);
1649
+ const cls = extensionStateClasses().find((c) => c.name === proto);
1650
+ const name = prototypeName(S, proto);
1651
+ transactDoc(activeTab.value, (t) =>
1652
+ mutateAddDef(t, name, {
1653
+ $prototype: proto,
1654
+ ...cls?.stateDefaults,
1655
+ } as Record<string, JsonValue>),
1656
+ );
1657
+ setDataRowExpanded(name, true);
1658
+ repaint();
1659
+ return;
1660
+ }
1661
+ /* A project import ("import:ContentCollection") is the prototype name and nothing else — the
1662
+ registry resolves the path. The schema is NOT fetched here: the entry is opened, and an open
1663
+ entry's field list is what asks for it (see {@link externalFields}), which repaints when it
1664
+ lands. Fetching in both places sent the same request twice. */
1665
+ if (type.startsWith("import:")) {
1666
+ const proto = type.slice(7);
1667
+ const name = prototypeName(S, proto);
1668
+ transactDoc(activeTab.value, (t) =>
1669
+ mutateAddDef(t, name, { $prototype: proto } as Record<string, JsonValue>),
1670
+ );
1671
+ setDataRowExpanded(name, true);
1672
+ repaint();
1673
+ return;
1674
+ }
1675
+ addTemplateDef(type, S);
1676
+ }
1677
+
1678
+ // ─── The panel ───────────────────────────────────────────────────────────────
1679
+
1680
+ /** Ask the Navigator to paint again. Every action ends here or in a transaction. */
1681
+ function repaint(): void {
1682
+ panelCtx?.renderLeftPanel();
1683
+ }
1684
+
1685
+ /** The plan a control names, or `undefined` if the projection has moved on without it. */
1686
+ function planFor(signal: string, fieldKey: string): FieldPlan | undefined {
1687
+ return plans.get(planKey(signal, fieldKey));
1688
+ }
1689
+
1690
+ /** Write a value now, cancelling any debounce that was waiting to write the same field. */
1691
+ function commitNow(signal: string, fieldKey: string, value: string): void {
1692
+ const key = planKey(signal, fieldKey);
1693
+ clearTimeout(timers.get(key));
1694
+ timers.delete(key);
1695
+ const plan = plans.get(key);
1696
+ if (!plan?.write || value === plan.value) {
1697
+ return;
1698
+ }
1699
+ plan.write(value);
1700
+ }
1701
+
1702
+ /** Everything a control in the document may ask for. */
1703
+ const ACTIONS: SignalsActions = {
1704
+ addChip: (signal, fieldKey, value) => planFor(signal, fieldKey)?.addChip?.(value),
1705
+ addRow: (signal, fieldKey) => planFor(signal, fieldKey)?.addRow?.(),
1706
+ addSignal: (type) => addSignalOfType(type),
1707
+ checkCell: (signal, fieldKey, row, cell, checked) =>
1708
+ planFor(signal, fieldKey)?.cellCheck?.(row, cell, checked),
1709
+ checkField: (signal, fieldKey, checked) => planFor(signal, fieldKey)?.check?.(checked),
1710
+ commitField: (signal, fieldKey, value) => commitNow(signal, fieldKey, value),
1711
+ dropChip: (signal, fieldKey, chip) => planFor(signal, fieldKey)?.dropChip?.(chip),
1712
+ dropRow: (signal, fieldKey, row) => planFor(signal, fieldKey)?.dropRow?.(row),
1713
+ dropSignal: (name) => transactDoc(activeTab.value, (t) => mutateRemoveDef(t, name)),
1714
+ editCell: (signal, fieldKey, row, cell, value) =>
1715
+ planFor(signal, fieldKey)?.cell?.(row, cell, value),
1716
+ /*
1717
+ * What a KEYSTROKE is worth is the field's own answer, which is why the document asks the same
1718
+ * question of every control and this decides. A rename committed per keystroke would rename the
1719
+ * entry eight times on the way to a nine-letter name.
1720
+ */
1721
+ inputField: (signal, fieldKey, value) => {
1722
+ const key = planKey(signal, fieldKey);
1723
+ const plan = plans.get(key);
1724
+ if (!plan?.write || plan.live === "none") {
1725
+ return;
1726
+ }
1727
+ if (plan.live === "commit") {
1728
+ plan.write(value);
1729
+ return;
1730
+ }
1731
+ clearTimeout(timers.get(key));
1732
+ timers.set(
1733
+ key,
1734
+ setTimeout(() => {
1735
+ timers.delete(key);
1736
+ plans.get(key)?.write?.(value);
1737
+ }, DEBOUNCE_MS),
1738
+ );
1739
+ },
1740
+ pressField: (signal, fieldKey, action) => planFor(signal, fieldKey)?.press?.(action),
1741
+ refresh: () => {
1742
+ panelCtx?.refreshData?.();
1743
+ repaint();
1744
+ },
1745
+ toggleCategory: (key, open) => {
1746
+ const collapsed = panelState?._collapsedSignalCats;
1747
+ if (!collapsed) {
1748
+ return;
1672
1749
  }
1673
- ${
1674
- def.$export
1675
- ? signalFieldRow("Export", def.$export || "", (v: string) =>
1676
- transactDoc(activeTab.value, (t) =>
1677
- mutateUpdateDef(t, name, { $export: v || undefined }),
1678
- ),
1679
- )
1680
- : nothing
1750
+ if (open) {
1751
+ collapsed.delete(key);
1752
+ } else {
1753
+ collapsed.add(key);
1681
1754
  }
1682
- ${schemaContent}
1683
- `;
1755
+ repaint();
1756
+ },
1757
+ toggleRow: (name) => {
1758
+ setDataRowExpanded(name, !isDataRowExpanded(name));
1759
+ repaint();
1760
+ },
1761
+ };
1762
+
1763
+ /**
1764
+ * Fill one island: the four foreign surfaces an entry's editor embeds, plus its value tree.
1765
+ *
1766
+ * The painters are closures the projection built, so each is already holding the def that is on
1767
+ * screen; a host whose entry has since gone finds nothing and is emptied rather than being painted
1768
+ * from a stale closure.
1769
+ *
1770
+ * @param {HTMLElement} host - The empty node the document drew
1771
+ * @param {string} signal - The entry it belongs to
1772
+ * @param {string} slot - `expression`, `statements`, `media`, `schema` or `tree`
1773
+ */
1774
+ function paintIsland(host: HTMLElement, signal: string, slot: string): void {
1775
+ const painter = islands.get(`${signal}/${slot}`);
1776
+ if (painter) {
1777
+ painter(host);
1778
+ return;
1779
+ }
1780
+ host.replaceChildren();
1781
+ }
1782
+
1783
+ /** The mounted surface, and the container it is standing in. */
1784
+ let standing: { handle: SignalsSurfaceHandle; host: HTMLElement } | null = null;
1785
+
1786
+ /**
1787
+ * Draw the Data panel — mounting the document the first time, and projecting into it every time
1788
+ * after.
1789
+ *
1790
+ * The document goes into `.panel-content`, not into the `.panel-body` this is handed, for the
1791
+ * reason `panels/git-panel.ts` states: only one of them is the node lit renders this panel's body
1792
+ * into, and appending to the other would leave the panel drawn under whatever the Navigator paints
1793
+ * next.
1794
+ *
1795
+ * A container that has CHANGED means the Navigator rebuilt the panel, so the panel-local record of
1796
+ * which parameter editor is in its advanced view — and which rename was refused — starts over with
1797
+ * it. Neither belongs to the document, and neither should outlive the panel that was showing it.
1798
+ *
1799
+ * @param {HTMLElement} host - The painted `.panel-body`
1800
+ * @param {SignalsPanelState} S - The focused document, as the Navigator reads it
1801
+ * @param {SignalsPanelCtx} ctx - The repaint, and the Refresh verb
1802
+ */
1803
+ export function mountSignalsPanel(
1804
+ host: HTMLElement,
1805
+ S: SignalsPanelState,
1806
+ ctx: SignalsPanelCtx,
1807
+ ): void {
1808
+ const container = host.querySelector<HTMLElement>(".panel-content") ?? host;
1809
+ panelState = S;
1810
+ panelCtx = ctx;
1811
+ /* A row the reader collapsed took its tree's host out of the page, and the tree registry holds a
1812
+ host by reference — this is the one moment a closed tree can be taken down. */
1813
+ disposeDetachedDataTrees();
1814
+ if (standing && (standing.host !== container || !standing.handle.attached())) {
1815
+ standing.handle.dispose();
1816
+ standing = null;
1817
+ advancedParamOpen.clear();
1818
+ schemaRequests.clear();
1819
+ renameError = null;
1820
+ }
1821
+ const view = signalsView(S, ctx);
1822
+ if (standing) {
1823
+ standing.handle.update(view);
1824
+ return;
1825
+ }
1826
+ standing = {
1827
+ handle: mountSignalsSurface(container, view, ACTIONS, paintIsland),
1828
+ host: container,
1829
+ };
1684
1830
  }
1685
1831
 
1686
1832
  // ─── Commands ─────────────────────────────────────────────────────────────────
@@ -1800,6 +1946,6 @@ export function registerSignalsCommands(registry: CommandRegistry): void {
1800
1946
  It was registered here `rail: false`, waiting for plan §11.2's merge into Data, and the merge did
1801
1947
  not follow: the button was removed and the editor was left reachable only by typing "State" into
1802
1948
  the palette. Declaring a state variable — or a component property, which is a state entry with a
1803
- default — is not an advanced move to hide behind a search box, so `renderSignalsTemplate` is now
1804
- rendered by the Data panel and there is no second record. A stored `leftTab: "state"` migrates to
1949
+ default — is not an advanced move to hide behind a search box, so the entry list is now the Data
1950
+ panel's own document and there is no second record. A stored `leftTab: "state"` migrates to
1805
1951
  `data` in `shell.ts`. */