@jxsuite/studio 4.1.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (612) 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-qwkm9zrf.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-vj72gqtq.js +20 -0
  15. package/dist/chunks/content-source-5mex4x1h.js +46 -0
  16. package/dist/chunks/convert-to-repeater-n64h8znc.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-234hjnqw.js +42 -0
  20. package/dist/chunks/extension-sections-3hpcambp.js +651 -0
  21. package/dist/chunks/file-ops-8yhd83xr.js +47 -0
  22. package/dist/chunks/files-hnxqxtsg.js +72 -0
  23. package/dist/chunks/format-host-779sfp2n.js +40 -0
  24. package/dist/chunks/fs-events-b88z7g4h.js +18 -0
  25. package/dist/chunks/git-panel-xwj8fpab.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-syr0f9sb.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-ff81wthn.js +30 -0
  37. package/dist/chunks/layers-panel-pnnghw12.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-cqhhxhhe.js +33 -0
  41. package/dist/chunks/menu-9p2w46gj.js +15 -0
  42. package/dist/chunks/menu-q99sy9rf.js +285 -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-5zt840sy.js +27 -0
  52. package/dist/chunks/quick-search-3748564r.js +45 -0
  53. package/dist/chunks/references-qvvxej7h.js +26 -0
  54. package/dist/chunks/right-panel-eccz92qt.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-kryvsw6j.js → studio-1k325pwf.js} +48 -48
  60. package/dist/chunks/{studio-b55y6ewx.js → studio-1knfqgy7.js} +58 -58
  61. package/dist/chunks/{studio-2b946z26.js → studio-1z1kcjv4.js} +3628 -6095
  62. package/dist/chunks/studio-3773086z.js +111 -0
  63. package/dist/chunks/{studio-4r1pv6zf.js → studio-38emantv.js} +22 -5
  64. package/dist/chunks/{studio-x813zp48.js → studio-3rz5md06.js} +30 -30
  65. package/dist/chunks/{studio-ynxwkrzd.js → studio-4wbb9ct0.js} +1953 -1785
  66. package/dist/chunks/studio-5te5tqng.js +81 -0
  67. package/dist/chunks/studio-5wt9nvhd.js +4763 -0
  68. package/dist/chunks/{studio-p4ngc046.js → studio-6ewqv390.js} +9 -9
  69. package/dist/chunks/{studio-9ab8gqsm.js → studio-6nzaktvy.js} +7 -7
  70. package/dist/chunks/studio-6xa21ej9.js +28 -0
  71. package/dist/chunks/{studio-5ehhackm.js → studio-727xh0e3.js} +19 -19
  72. package/dist/chunks/{studio-gcye7qka.js → studio-7e11jpa9.js} +21 -21
  73. package/dist/chunks/{studio-q7v10cg2.js → studio-7r42c6ca.js} +17 -7
  74. package/dist/chunks/studio-7xnmgv66.js +13615 -0
  75. package/dist/chunks/studio-8scdew0e.js +258 -0
  76. package/dist/chunks/{studio-fbf92ckf.js → studio-93zafjcm.js} +275 -163
  77. package/dist/chunks/{studio-dg7zyt5x.js → studio-98b3z07n.js} +1 -1
  78. package/dist/chunks/studio-a79k90n9.js +1162 -0
  79. package/dist/chunks/studio-aj37eer3.js +422 -0
  80. package/dist/chunks/studio-b0k5tqgp.js +2432 -0
  81. package/dist/chunks/studio-b4s9eykd.js +2811 -0
  82. package/dist/chunks/{studio-f3wqjtk5.js → studio-d0300636.js} +5076 -18
  83. package/dist/chunks/{studio-b9b6t1dg.js → studio-e1f24eff.js} +5664 -5666
  84. package/dist/chunks/studio-e1pgga3k.js +839 -0
  85. package/dist/chunks/{studio-g1wdche0.js → studio-esrxgwh6.js} +80 -80
  86. package/dist/chunks/{studio-q43a5y9h.js → studio-g3jevyjt.js} +16 -16
  87. package/dist/chunks/studio-gb13095a.js +2806 -0
  88. package/dist/chunks/{studio-3n4ndrzh.js → studio-h4nvnjvz.js} +303 -303
  89. package/dist/chunks/{studio-dm19q43r.js → studio-hhvs4sdg.js} +10 -10
  90. package/dist/chunks/{studio-x4qkdat5.js → studio-js8nv53n.js} +1749 -2481
  91. package/dist/chunks/{studio-qjy2z9bv.js → studio-jxp12rnj.js} +32 -137
  92. package/dist/chunks/{studio-trxrabqf.js → studio-k75n7vhp.js} +527 -283
  93. package/dist/chunks/{studio-q9jd1a60.js → studio-kcgff4r0.js} +15 -18
  94. package/dist/chunks/studio-kkqkcp3d.js +17668 -0
  95. package/dist/chunks/studio-mhhyamxf.js +763 -0
  96. package/dist/chunks/{studio-cft9r4r7.js → studio-nq5yf5mp.js} +22 -22
  97. package/dist/chunks/{studio-685s0ytq.js → studio-nqgawfks.js} +572 -572
  98. package/dist/chunks/studio-nzzpvzx9.js +185 -0
  99. package/dist/chunks/{studio-xrapxwwc.js → studio-p1dw8wfw.js} +55 -29
  100. package/dist/chunks/{studio-3bqpr0zh.js → studio-pxbgypmg.js} +299 -110
  101. package/dist/chunks/studio-q0a3yk4n.js +33 -0
  102. package/dist/chunks/{studio-wcy8trfx.js → studio-q3x06vmj.js} +23 -22
  103. package/dist/chunks/{studio-6yw4y3my.js → studio-qhhm6hbt.js} +8 -8
  104. package/dist/chunks/studio-qk4bfsq2.js +362 -0
  105. package/dist/chunks/{studio-msd847zn.js → studio-qk90fgqm.js} +361 -90
  106. package/dist/chunks/{studio-jkftxyvz.js → studio-r6nvacqs.js} +5861 -2154
  107. package/dist/chunks/{studio-53n8bp04.js → studio-r6xmmtfh.js} +1 -1
  108. package/dist/chunks/{studio-tvgyw9xw.js → studio-rnyf3f8e.js} +83 -250
  109. package/dist/chunks/studio-rs76k67r.js +12 -0
  110. package/dist/chunks/studio-sbjyc8bt.js +954 -0
  111. package/dist/chunks/{studio-1wagb550.js → studio-syg2tadj.js} +11 -11
  112. package/dist/chunks/{studio-t5wqwzdh.js → studio-tjjk7jvk.js} +1 -1
  113. package/dist/chunks/studio-tqgryxwg.js +1171 -0
  114. package/dist/chunks/studio-try13h8c.js +8448 -0
  115. package/dist/chunks/studio-vfq8qpjt.js +1219 -0
  116. package/dist/chunks/studio-vhv5tgxm.js +51 -0
  117. package/dist/chunks/{studio-ahn3pdgx.js → studio-vw7mzp6z.js} +52 -38
  118. package/dist/chunks/studio-wqpngpsk.js +174 -0
  119. package/dist/chunks/studio-wqz6dggm.js +1356 -0
  120. package/dist/chunks/{studio-92ab2jqe.js → studio-y9zedyjn.js} +14 -14
  121. package/dist/chunks/studio-yxa81f45.js +12 -0
  122. package/dist/chunks/studio-zdn2g6dt.js +148 -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-t55jdjbx.js +69 -0
  133. package/dist/chunks/{ws-client-6shc5rze.js → ws-client-ny4zc51r.js} +20 -22
  134. package/dist/iframe-entry.js +1873 -673
  135. package/dist/studio.js +52757 -36535
  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 +333 -113
  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 +404 -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 +177 -10
  178. package/src/commands/run-reported.ts +120 -0
  179. package/src/content/entry-commands.ts +16 -23
  180. package/src/content/entry-editor.ts +144 -101
  181. package/src/editor/canvas-slash-bridge.ts +2 -2
  182. package/src/editor/context-menu.ts +49 -200
  183. package/src/editor/convert-to-component.ts +42 -82
  184. package/src/editor/convert-to-repeater.ts +132 -173
  185. package/src/editor/inline-edit.ts +11 -0
  186. package/src/editor/shortcuts.ts +76 -49
  187. package/src/editor/slash-menu.ts +150 -157
  188. package/src/files/components.ts +39 -0
  189. package/src/files/file-ops.ts +117 -11
  190. package/src/files/files.ts +944 -660
  191. package/src/files/json-layout.ts +474 -0
  192. package/src/files/serialize-document.ts +8 -1
  193. package/src/format/convert-file.ts +16 -10
  194. package/src/format/format-host.ts +50 -1
  195. package/src/github/github-auth.ts +71 -29
  196. package/src/github/github-publish.ts +57 -65
  197. package/src/grid/cell-editors.ts +46 -4
  198. package/src/grid/cell-popovers.ts +65 -62
  199. package/src/grid/grid-open.ts +86 -64
  200. package/src/grid/grid-panel.ts +390 -469
  201. package/src/grid/redirects-grid.ts +40 -44
  202. package/src/hosting/layout.ts +2 -0
  203. package/src/i18n/i18n-commands.ts +137 -62
  204. package/src/media/media-pane.ts +83 -133
  205. package/src/new-project/add-repo-modal.ts +125 -176
  206. package/src/new-project/import-tab.ts +181 -252
  207. package/src/new-project/location-fields.ts +93 -160
  208. package/src/new-project/new-project-modal.ts +376 -367
  209. package/src/packages/jxsuite-update.ts +76 -18
  210. package/src/packages/pull-package-sync.ts +5 -1
  211. package/src/panels/activity-panel.ts +137 -104
  212. package/src/panels/ai-chat/chat-markdown.ts +15 -13
  213. package/src/panels/ai-chat/chat-view.ts +307 -472
  214. package/src/panels/ai-chat/composer.ts +175 -163
  215. package/src/panels/ai-chat/sessions-view.ts +31 -62
  216. package/src/panels/ai-panel.ts +213 -185
  217. package/src/panels/block-action-bar.ts +444 -645
  218. package/src/panels/bottom-dock.ts +112 -47
  219. package/src/panels/chat-panel.ts +19 -14
  220. package/src/panels/data-explorer.ts +141 -102
  221. package/src/panels/data-grid.ts +183 -150
  222. package/src/panels/dnd.ts +75 -20
  223. package/src/panels/editors.ts +26 -19
  224. package/src/panels/elements-panel.ts +243 -186
  225. package/src/panels/empty-state.ts +26 -42
  226. package/src/panels/events-panel.ts +950 -729
  227. package/src/panels/formula-workspace.ts +434 -207
  228. package/src/panels/frontmatter-fields.ts +185 -153
  229. package/src/panels/frontmatter-panel.ts +411 -187
  230. package/src/panels/git-diff-open.ts +148 -0
  231. package/src/panels/git-panel.ts +679 -605
  232. package/src/panels/head-panel.ts +617 -435
  233. package/src/panels/i18n-panel.ts +148 -135
  234. package/src/panels/imports-panel.ts +388 -414
  235. package/src/panels/jump-bar.ts +175 -147
  236. package/src/panels/layers-panel.ts +785 -671
  237. package/src/panels/left-panel.ts +166 -94
  238. package/src/panels/navigator-panels.ts +1 -1
  239. package/src/panels/pane-context.ts +529 -677
  240. package/src/panels/pane-grid.ts +265 -241
  241. package/src/panels/panel-registry.ts +15 -4
  242. package/src/panels/problems-panel.ts +181 -99
  243. package/src/panels/properties-panel.ts +1525 -966
  244. package/src/panels/provenance.ts +31 -113
  245. package/src/panels/quick-search.ts +223 -184
  246. package/src/panels/right-panel.ts +135 -169
  247. package/src/panels/seo-modal.ts +346 -167
  248. package/src/panels/settings-menu.ts +114 -661
  249. package/src/panels/settings-pane.ts +102 -86
  250. package/src/panels/signals-panel.ts +1318 -1172
  251. package/src/panels/statement-editor.ts +556 -428
  252. package/src/panels/style-panel.ts +1367 -630
  253. package/src/panels/style-utils.ts +36 -32
  254. package/src/panels/stylebook-doc.ts +30 -73
  255. package/src/panels/stylebook-layers-panel.ts +202 -109
  256. package/src/panels/stylebook-panel.ts +124 -88
  257. package/src/panels/tab-strip.ts +386 -288
  258. package/src/platforms/cloud.ts +106 -16
  259. package/src/platforms/devserver.ts +13 -0
  260. package/src/publish/deploy-checklist.ts +124 -78
  261. package/src/publish/publish-commands.ts +4 -12
  262. package/src/publish/publish-panel.ts +134 -328
  263. package/src/services/a11y-report.ts +100 -43
  264. package/src/services/ai-command-tools.ts +538 -0
  265. package/src/services/ai-project-tools.ts +8 -2
  266. package/src/services/ai-session-store.ts +6 -0
  267. package/src/services/ai-system-prompt.ts +112 -24
  268. package/src/services/ai-tools.ts +36 -212
  269. package/src/services/ai-write-report.ts +174 -0
  270. package/src/services/document-assistant.ts +59 -8
  271. package/src/services/idle.ts +16 -14
  272. package/src/services/live-surfaces.ts +237 -0
  273. package/src/services/model-uri.ts +75 -0
  274. package/src/services/notify.ts +63 -6
  275. package/src/services/popover-report.ts +325 -0
  276. package/src/services/surface-registry.ts +56 -0
  277. package/src/services/tool-executor.ts +6 -0
  278. package/src/settings/contexts-section.ts +301 -299
  279. package/src/settings/contributed-section.ts +361 -222
  280. package/src/settings/css-vars-editor.ts +320 -325
  281. package/src/settings/defs-editor.ts +572 -535
  282. package/src/settings/dependencies-editor.ts +79 -107
  283. package/src/settings/extension-commands.ts +379 -0
  284. package/src/settings/extension-rows.ts +113 -0
  285. package/src/settings/extensions-section.ts +287 -0
  286. package/src/settings/general-settings.ts +130 -181
  287. package/src/settings/head-editor.ts +10 -305
  288. package/src/settings/locales-section.ts +146 -196
  289. package/src/settings/preferences-accounts.ts +1 -1
  290. package/src/settings/preferences-dialog.ts +350 -330
  291. package/src/settings/project-sections.ts +108 -181
  292. package/src/settings/schema-field-ui.ts +11 -399
  293. package/src/settings/section-registry.ts +8 -1
  294. package/src/settings/settings-document.ts +31 -29
  295. package/src/shell/tree.ts +42 -87
  296. package/src/shell.ts +48 -36
  297. package/src/studio.ts +104 -69
  298. package/src/style/token-ref.ts +0 -39
  299. package/src/surfaces/about.json +263 -0
  300. package/src/surfaces/about.ts +163 -0
  301. package/src/surfaces/add-repo.json +486 -0
  302. package/src/surfaces/add-repo.ts +236 -0
  303. package/src/surfaces/ai-chat.json +2185 -0
  304. package/src/surfaces/ai-chat.ts +549 -0
  305. package/src/surfaces/ai-credentials-form.json +332 -0
  306. package/src/surfaces/ai-credentials-form.ts +151 -0
  307. package/src/surfaces/ai-managed-connect.json +108 -0
  308. package/src/surfaces/ai-managed-connect.ts +123 -0
  309. package/src/surfaces/ai-model-picker.json +114 -0
  310. package/src/surfaces/ai-model-picker.ts +139 -0
  311. package/src/surfaces/block-action-bar.json +640 -0
  312. package/src/surfaces/block-action-bar.ts +368 -0
  313. package/src/surfaces/bottom-dock.json +154 -0
  314. package/src/surfaces/bottom-dock.ts +125 -0
  315. package/src/surfaces/canvas-stage.json +444 -0
  316. package/src/surfaces/canvas-stage.ts +253 -0
  317. package/src/surfaces/cf-account-picker.json +300 -0
  318. package/src/surfaces/cf-account-picker.ts +214 -0
  319. package/src/surfaces/commandbar.json +985 -0
  320. package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +417 -391
  321. package/src/surfaces/convert-repeater.json +309 -0
  322. package/src/surfaces/convert-repeater.ts +276 -0
  323. package/src/surfaces/data-actions.json +157 -0
  324. package/src/surfaces/data-actions.ts +115 -0
  325. package/src/surfaces/dialog.json +289 -0
  326. package/src/surfaces/dialog.ts +343 -0
  327. package/src/surfaces/diff-toolbar.json +266 -0
  328. package/src/surfaces/diff-toolbar.ts +139 -0
  329. package/src/surfaces/doc-header.json +739 -0
  330. package/src/surfaces/doc-header.ts +223 -0
  331. package/src/surfaces/empty-state.json +152 -0
  332. package/src/surfaces/empty-state.ts +173 -0
  333. package/src/surfaces/entry-editor.json +281 -0
  334. package/src/surfaces/entry-editor.ts +191 -0
  335. package/src/surfaces/expression-editor.json +977 -0
  336. package/src/surfaces/expression-editor.ts +266 -0
  337. package/src/surfaces/files-panel.json +592 -0
  338. package/src/surfaces/files-panel.ts +301 -0
  339. package/src/surfaces/formula-palette.json +364 -0
  340. package/src/surfaces/formula-palette.ts +340 -0
  341. package/src/surfaces/git-panel.json +1822 -0
  342. package/src/surfaces/git-panel.ts +272 -0
  343. package/src/surfaces/github-auth.json +112 -0
  344. package/src/surfaces/github-auth.ts +139 -0
  345. package/src/surfaces/github-publish.json +206 -0
  346. package/src/surfaces/github-publish.ts +169 -0
  347. package/src/surfaces/grid-cell.json +218 -0
  348. package/src/surfaces/grid-cell.ts +120 -0
  349. package/src/surfaces/grid-open.json +147 -0
  350. package/src/surfaces/grid-open.ts +87 -0
  351. package/src/surfaces/grid-panel.json +706 -0
  352. package/src/surfaces/grid-panel.ts +206 -0
  353. package/src/surfaces/grid-replace.json +179 -0
  354. package/src/surfaces/grid-replace.ts +89 -0
  355. package/src/surfaces/grid-views.json +446 -0
  356. package/src/surfaces/grid-views.ts +172 -0
  357. package/src/surfaces/inspector-dock.json +187 -0
  358. package/src/surfaces/inspector-dock.ts +161 -0
  359. package/src/surfaces/jump-bar.json +243 -0
  360. package/src/surfaces/jump-bar.ts +129 -0
  361. package/src/surfaces/jxsuite-update.json +146 -0
  362. package/src/surfaces/jxsuite-update.ts +149 -0
  363. package/src/surfaces/library-pane.json +1509 -0
  364. package/src/surfaces/library-pane.ts +363 -0
  365. package/src/surfaces/logic-panel.json +1949 -0
  366. package/src/surfaces/logic-panel.ts +369 -0
  367. package/src/surfaces/logic-workspace.json +685 -0
  368. package/src/surfaces/logic-workspace.ts +238 -0
  369. package/src/surfaces/media-browser.json +303 -0
  370. package/src/surfaces/media-browser.ts +145 -0
  371. package/src/surfaces/media-field.json +163 -0
  372. package/src/surfaces/media-field.ts +116 -0
  373. package/src/surfaces/media-pane.json +513 -0
  374. package/src/surfaces/media-pane.ts +197 -0
  375. package/src/surfaces/menu.json +232 -0
  376. package/src/surfaces/menu.ts +375 -0
  377. package/src/surfaces/navigator-dock.json +113 -0
  378. package/src/surfaces/navigator-dock.ts +130 -0
  379. package/src/surfaces/new-project.json +1654 -0
  380. package/src/surfaces/new-project.ts +411 -0
  381. package/src/surfaces/palette.json +535 -0
  382. package/src/surfaces/pane-context.json +1497 -0
  383. package/src/surfaces/pane-context.ts +513 -0
  384. package/src/surfaces/pane-grid.json +188 -0
  385. package/src/surfaces/pane-grid.ts +250 -0
  386. package/src/surfaces/panel-activity.json +563 -0
  387. package/src/surfaces/panel-activity.ts +187 -0
  388. package/src/surfaces/panel-data.json +141 -0
  389. package/src/surfaces/panel-data.ts +156 -0
  390. package/src/surfaces/panel-deploy-checklist.json +283 -0
  391. package/src/surfaces/panel-deploy-checklist.ts +175 -0
  392. package/src/surfaces/panel-elements.json +367 -0
  393. package/src/surfaces/panel-elements.ts +175 -0
  394. package/src/surfaces/panel-i18n.json +569 -0
  395. package/src/surfaces/panel-i18n.ts +212 -0
  396. package/src/surfaces/panel-imports.json +780 -0
  397. package/src/surfaces/panel-imports.ts +209 -0
  398. package/src/surfaces/panel-outline.json +734 -0
  399. package/src/surfaces/panel-outline.ts +326 -0
  400. package/src/surfaces/panel-page.json +1017 -0
  401. package/src/surfaces/panel-page.ts +240 -0
  402. package/src/surfaces/panel-problems.json +497 -0
  403. package/src/surfaces/panel-problems.ts +157 -0
  404. package/src/surfaces/panel-signals.json +1973 -0
  405. package/src/surfaces/panel-signals.ts +436 -0
  406. package/src/surfaces/panel-stylebook-layers.json +317 -0
  407. package/src/surfaces/panel-stylebook-layers.ts +155 -0
  408. package/src/surfaces/preferences.json +850 -0
  409. package/src/surfaces/preferences.ts +332 -0
  410. package/src/surfaces/progress-modal.json +110 -0
  411. package/src/surfaces/progress-modal.ts +150 -0
  412. package/src/surfaces/properties-panel.json +1578 -0
  413. package/src/surfaces/properties-panel.ts +337 -0
  414. package/src/surfaces/publish.json +945 -0
  415. package/src/surfaces/publish.ts +392 -0
  416. package/src/surfaces/push-plan.json +110 -0
  417. package/src/surfaces/push-plan.ts +87 -0
  418. package/src/surfaces/rail.json +255 -0
  419. package/src/surfaces/rail.ts +206 -0
  420. package/src/surfaces/reference-field.json +191 -0
  421. package/src/surfaces/reference-field.ts +168 -0
  422. package/src/surfaces/schema-builder.json +1011 -0
  423. package/src/surfaces/schema-builder.ts +181 -0
  424. package/src/surfaces/schema-form.json +1013 -0
  425. package/src/surfaces/schema-form.ts +287 -0
  426. package/src/surfaces/secret-field.json +60 -0
  427. package/src/surfaces/secret-field.ts +146 -0
  428. package/src/surfaces/seo.json +932 -0
  429. package/src/surfaces/seo.ts +334 -0
  430. package/src/surfaces/settings-contexts.json +538 -0
  431. package/src/surfaces/settings-contexts.ts +196 -0
  432. package/src/surfaces/settings-contributed.json +489 -0
  433. package/src/surfaces/settings-contributed.ts +199 -0
  434. package/src/surfaces/settings-css-vars.json +820 -0
  435. package/src/surfaces/settings-css-vars.ts +199 -0
  436. package/src/surfaces/settings-defs.json +1363 -0
  437. package/src/surfaces/settings-defs.ts +232 -0
  438. package/src/surfaces/settings-deploy.json +108 -0
  439. package/src/surfaces/settings-deploy.ts +135 -0
  440. package/src/surfaces/settings-extensions.json +486 -0
  441. package/src/surfaces/settings-extensions.ts +201 -0
  442. package/src/surfaces/settings-head.json +779 -0
  443. package/src/surfaces/settings-head.ts +397 -0
  444. package/src/surfaces/settings-locales.json +420 -0
  445. package/src/surfaces/settings-locales.ts +137 -0
  446. package/src/surfaces/settings-overview.json +423 -0
  447. package/src/surfaces/settings-overview.ts +218 -0
  448. package/src/surfaces/settings-packages.json +549 -0
  449. package/src/surfaces/settings-packages.ts +158 -0
  450. package/src/surfaces/settings-pane.json +147 -0
  451. package/src/surfaces/settings-pane.ts +150 -0
  452. package/src/surfaces/settings-rawjson.json +104 -0
  453. package/src/surfaces/settings-rawjson.ts +107 -0
  454. package/src/surfaces/shell.json +99 -0
  455. package/src/surfaces/shell.ts +30 -0
  456. package/src/surfaces/slash-menu.json +180 -0
  457. package/src/surfaces/slash-menu.ts +272 -0
  458. package/src/surfaces/statements.json +607 -0
  459. package/src/surfaces/statements.ts +238 -0
  460. package/src/surfaces/statusbar.json +94 -0
  461. package/src/{panels → surfaces}/statusbar.ts +164 -90
  462. package/src/surfaces/style-panel.json +1643 -0
  463. package/src/surfaces/style-panel.ts +338 -0
  464. package/src/surfaces/stylebook-chrome.json +122 -0
  465. package/src/surfaces/stylebook-chrome.ts +122 -0
  466. package/src/surfaces/tab-strip.json +448 -0
  467. package/src/surfaces/tab-strip.ts +200 -0
  468. package/src/surfaces/target-line.json +419 -0
  469. package/src/surfaces/target-line.ts +349 -0
  470. package/src/surfaces/toasts.json +189 -0
  471. package/src/surfaces/welcome.json +571 -0
  472. package/src/surfaces/welcome.ts +338 -0
  473. package/src/tabs/project-config.ts +111 -21
  474. package/src/tabs/tab.ts +52 -1
  475. package/src/types.ts +22 -0
  476. package/src/ui/ai-credentials-form.ts +104 -100
  477. package/src/ui/ai-managed-connect.ts +54 -36
  478. package/src/ui/ai-model-picker.ts +75 -47
  479. package/src/ui/cf-account-picker.ts +76 -141
  480. package/src/ui/color-selector.ts +80 -301
  481. package/src/ui/dynamic-slot.ts +43 -283
  482. package/src/ui/expression-editor.ts +797 -576
  483. package/src/ui/field-input.ts +8 -181
  484. package/src/ui/form-controls.ts +838 -366
  485. package/src/ui/formula-chips.ts +80 -85
  486. package/src/ui/kit.ts +33 -0
  487. package/src/ui/layers.ts +296 -593
  488. package/src/ui/media-picker.ts +203 -284
  489. package/src/ui/panel-resize.ts +251 -64
  490. package/src/ui/popover-surface.ts +153 -0
  491. package/src/ui/progress-modal.ts +22 -60
  492. package/src/ui/regions.ts +13 -7
  493. package/src/ui/schema-form.ts +906 -440
  494. package/src/ui/surface.ts +122 -0
  495. package/src/ui/unit-selector.ts +15 -131
  496. package/src/ui/value-source.ts +14 -0
  497. package/src/ui/virtual-window.ts +88 -9
  498. package/src/utils/canvas-media.ts +72 -0
  499. package/src/utils/edit-display.ts +7 -2
  500. package/src/utils/element-selectors.ts +69 -0
  501. package/src/utils/link-target.ts +2 -2
  502. package/src/utils/studio-utils.ts +0 -10
  503. package/src/view.ts +15 -0
  504. package/src/workspace/pane-derive.ts +36 -14
  505. package/src/workspace/workspace.ts +75 -6
  506. package/styles/canvas.css +72 -467
  507. package/styles/forced-colors.css +48 -82
  508. package/styles/forced-colors.json +41 -0
  509. package/styles/inspector.css +2 -1301
  510. package/styles/overlays.css +96 -2823
  511. package/styles/panels.css +6 -1423
  512. package/styles/shell-frame.css +272 -0
  513. package/styles/shell-frame.json +227 -0
  514. package/styles/shell.css +0 -1568
  515. package/styles/tokens.css +65 -136
  516. package/styles/tokens.json +84 -0
  517. package/dist/chunks/Overlay.dev-etqjwyf9.js +0 -23
  518. package/dist/chunks/adapter-jkcyhgtr.js +0 -14
  519. package/dist/chunks/cf-account-picker-en86v7x7.js +0 -163
  520. package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
  521. package/dist/chunks/components-zmx10t62.js +0 -21
  522. package/dist/chunks/content-source-48xg86wj.js +0 -49
  523. package/dist/chunks/convert-to-repeater-43tfn7cs.js +0 -268
  524. package/dist/chunks/editor-cptq5jp1.js +0 -37
  525. package/dist/chunks/entry-commands-e63f1xc0.js +0 -46
  526. package/dist/chunks/extension-sections-txv08xxg.js +0 -311
  527. package/dist/chunks/file-ops-wbh4hhsx.js +0 -48
  528. package/dist/chunks/files-3kfkwhk4.js +0 -93
  529. package/dist/chunks/flow-qbfgrpeh.js +0 -634
  530. package/dist/chunks/focus-trap.esm-9t2drp0g.js +0 -1025
  531. package/dist/chunks/focus-visible-6eg70z18.js +0 -167
  532. package/dist/chunks/format-host-w16q81tf.js +0 -43
  533. package/dist/chunks/fs-events-hrrg328g.js +0 -21
  534. package/dist/chunks/git-panel-532whr1y.js +0 -59
  535. package/dist/chunks/index-zqxmqxmh.js +0 -109
  536. package/dist/chunks/jx-validate-8er6gbxy.js +0 -19
  537. package/dist/chunks/layers-panel-07hm7g5j.js +0 -67
  538. package/dist/chunks/layers-q75tj2mf.js +0 -45
  539. package/dist/chunks/lit-html-j055mkh8.js +0 -22
  540. package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
  541. package/dist/chunks/notify-yjm79n4s.js +0 -30
  542. package/dist/chunks/parse-24d0gay3.js +0 -19
  543. package/dist/chunks/platform-ba1j1gbh.js +0 -16
  544. package/dist/chunks/publish-panel-v9st96ja.js +0 -29
  545. package/dist/chunks/quick-search-zccjp6gv.js +0 -66
  546. package/dist/chunks/references-3qx8fkyp.js +0 -29
  547. package/dist/chunks/right-panel-vm3b6kwy.js +0 -46
  548. package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
  549. package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +0 -18
  550. package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
  551. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
  552. package/dist/chunks/sp-tooltip.dev-4sastdnw.js +0 -15
  553. package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
  554. package/dist/chunks/studio-0a5qrh01.js +0 -3
  555. package/dist/chunks/studio-0bq6eh7v.js +0 -12
  556. package/dist/chunks/studio-18j3bmcz.js +0 -14
  557. package/dist/chunks/studio-1ngbejft.js +0 -181
  558. package/dist/chunks/studio-4b5z1x43.js +0 -424
  559. package/dist/chunks/studio-4n2b5gzb.js +0 -357
  560. package/dist/chunks/studio-4rzyrtjz.js +0 -426
  561. package/dist/chunks/studio-5fx3g9jg.js +0 -4325
  562. package/dist/chunks/studio-5myknm0a.js +0 -1903
  563. package/dist/chunks/studio-7afd21nw.js +0 -67
  564. package/dist/chunks/studio-7nte2hj4.js +0 -613
  565. package/dist/chunks/studio-8jpzqk5s.js +0 -128
  566. package/dist/chunks/studio-8q517dfr.js +0 -24
  567. package/dist/chunks/studio-950k0dx6.js +0 -6939
  568. package/dist/chunks/studio-9n4mrmnb.js +0 -184
  569. package/dist/chunks/studio-a0tswy0w.js +0 -58
  570. package/dist/chunks/studio-ahnzd9rg.js +0 -23
  571. package/dist/chunks/studio-bgxt93ga.js +0 -62
  572. package/dist/chunks/studio-cmb03bg5.js +0 -13890
  573. package/dist/chunks/studio-d1ks410h.js +0 -175
  574. package/dist/chunks/studio-ef49pnzx.js +0 -387
  575. package/dist/chunks/studio-eqq1w638.js +0 -113
  576. package/dist/chunks/studio-ew0djmhg.js +0 -341
  577. package/dist/chunks/studio-gfgb8pe6.js +0 -1652
  578. package/dist/chunks/studio-h4rxscfc.js +0 -10
  579. package/dist/chunks/studio-jc994dnr.js +0 -1066
  580. package/dist/chunks/studio-kyh9avvv.js +0 -28
  581. package/dist/chunks/studio-m3hwn7y1.js +0 -120
  582. package/dist/chunks/studio-mtfnaqs9.js +0 -659
  583. package/dist/chunks/studio-n6j9t10q.js +0 -318
  584. package/dist/chunks/studio-n8b9wqwy.js +0 -291
  585. package/dist/chunks/studio-qcy05dp8.js +0 -1743
  586. package/dist/chunks/studio-qq6jym04.js +0 -53
  587. package/dist/chunks/studio-rdx8jcxy.js +0 -83
  588. package/dist/chunks/studio-rn9ta2mz.js +0 -111
  589. package/dist/chunks/studio-t7qb9db8.js +0 -665
  590. package/dist/chunks/studio-tmc5emrw.js +0 -8
  591. package/dist/chunks/studio-z6nw2ek2.js +0 -18
  592. package/dist/chunks/studio-zczps70q.js +0 -378
  593. package/dist/chunks/studio-zqazsct2.js +0 -1823
  594. package/dist/chunks/workspace-fjx52ryv.js +0 -89
  595. package/src/panels/activity-bar.ts +0 -251
  596. package/src/panels/panel-scheduler.ts +0 -219
  597. package/src/panels/style-inputs.ts +0 -237
  598. package/src/panels/target-line.ts +0 -274
  599. package/src/panels/welcome-screen.ts +0 -346
  600. package/src/ui/button-group.ts +0 -102
  601. package/src/ui/choice-field.ts +0 -72
  602. package/src/ui/field-row.ts +0 -249
  603. package/src/ui/formula-palette.ts +0 -213
  604. package/src/ui/icons.ts +0 -52
  605. package/src/ui/jx-theme.ts +0 -130
  606. package/src/ui/spectrum.ts +0 -340
  607. package/src/ui/value-selector.ts +0 -174
  608. package/src/ui/widgets.ts +0 -172
  609. /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
  610. /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
  611. /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
  612. /package/dist/chunks/{tsMode-ef187txx.css → tsMode-hnxxb73h.css} +0 -0
@@ -2,30 +2,58 @@
2
2
  /**
3
3
  * The expression editor — one operator row plus an operand editor per slot, nested to any depth.
4
4
  *
5
- * **The layout is CSS, in `styles/inspector.css` (§ "The expression editor").** Every row here used
6
- * to be an inline `display:flex;…` attribute with a hard `min-width` on the pickers, and no rung of
7
- * that chain could set `min-width: 0` — so at Inspector width the operand controls refused to
8
- * shrink and Operator, Target and Value were clipped by the edge of the window. The editor is drawn
9
- * in the Inspector (~280px), in the Navigator, and in the Bottom dock's Logic tab (very wide), so
10
- * every row wraps to a stack rather than sizing itself in pixels.
5
+ * **This is the flow; `surfaces/expression-editor.json` is the surface.** This module decides what
6
+ * an operator needs, which rung of the value ladder an operand occupies, what a literal's type is,
7
+ * which refs a position may bind, what the live preview says at each node, and what every gesture
8
+ * commits. It draws nothing: {@link flattenExpression} walks the tree once and hands the document a
9
+ * FLAT list of rows that already carry their own indent, and {@link mountExpressionEditor} is the
10
+ * seam between the two.
11
+ *
12
+ * **There is no Monaco here, and never was.** The editor that hosts Monaco is the Logic dock's code
13
+ * surface (`panels/formula-workspace.ts`); this one is a form, and every part of it — the chip
14
+ * strip, the operator picker, the operand ladder, the positional args, the `switch` cases, the live
15
+ * badges — is markup a document can own. So nothing in this conversion is an island, and the four
16
+ * hosts that used to `litRender` into an empty node now mount a document into it instead.
17
+ *
18
+ * **Recursion is a property of the WALK.** An operand may be another expression, an aggregate's
19
+ * per-item body is one, and either may hold more of both — while a document's one repeater is
20
+ * `$prototype: "Array"` over a list. So the walk emits rows in reading order and each says how far
21
+ * in it sits, exactly as `panels/statement-editor.ts` does for the statement tree. A row's key is
22
+ * its node's own path plus the slot it fills, which is unique by construction: two rows of one
23
+ * expression cannot occupy one position in it.
24
+ *
25
+ * **A layout that could not shrink is what sent this editor to a stylesheet, and then to a
26
+ * document.** Every row here used to be an inline `display:flex;…` attribute with a hard
27
+ * `min-width` on the pickers, and no rung of that chain could set `min-width: 0` — so at Inspector
28
+ * width the operand controls refused to shrink and Operator, Target and Value were clipped by the
29
+ * edge of the window. The editor is drawn in the Inspector (~280px), in the Navigator, and in the
30
+ * Bottom dock's Logic tab (very wide), so every row wraps to a stack rather than sizing itself in
31
+ * pixels. The document's `style` block owns that now, and it emits no class at all.
32
+ *
33
+ * @docs studio/logic/formulas
11
34
  */
12
- import { html, nothing } from "lit-html";
13
- import { live } from "lit-html/directives/live.js";
14
35
  import { PURE_METHOD_OPS } from "@jxsuite/runtime/expression";
15
36
  import { isJsonObject, isRef } from "@jxsuite/schema/guards";
16
- import { renderFieldRow } from "./field-row";
17
- import { renderFormulaChips } from "./formula-chips";
37
+ import { formulaChipStrip } from "./formula-chips";
18
38
  import { applyCatalogPick, calleeEntry, formulaCatalog } from "./formula-catalog";
19
- import { openFormulaPalette } from "./formula-palette";
39
+ import { openFormulaPalette } from "../surfaces/formula-palette";
20
40
  import { VALUE_SOURCE_LABELS } from "./value-source";
21
- import { renderEmptyState } from "../panels/empty-state";
41
+ import {
42
+ disposeDetachedExpressionEditors,
43
+ renderExpressionEditorSurface,
44
+ } from "../surfaces/expression-editor";
22
45
 
46
+ import type {
47
+ ExprOption,
48
+ ExprRowView,
49
+ ExprSelectGroup,
50
+ ExpressionActions,
51
+ } from "../surfaces/expression-editor";
23
52
  import type {
24
53
  JxExpressionNode,
25
54
  JxExpressionOperand,
26
55
  JxStateDefinition,
27
56
  } from "@jxsuite/schema/types";
28
- import type { TemplateResult } from "lit-html";
29
57
 
30
58
  // ─── Operator Categories ────────────────────────────────────────────────────
31
59
 
@@ -129,6 +157,70 @@ const OPERATOR_GROUPS = [
129
157
  { label: "Function", ops: ["call"] },
130
158
  ];
131
159
 
160
+ /**
161
+ * The operator picker's rows, in the kit's own `optgroup` shape.
162
+ *
163
+ * Built once: the list is closed, and a delimited run is what the kit draws for it — the label
164
+ * names the group in the accessibility tree and the legend inside it is what a reader sees, which
165
+ * is the pair `<sp-menu-divider>` plus a header span was standing in for.
166
+ */
167
+ const OPERATOR_SELECT_GROUPS: ExprSelectGroup[] = OPERATOR_GROUPS.map((group) => ({
168
+ id: group.label,
169
+ label: group.label,
170
+ rows: group.ops.map((op) => ({ label: op, value: op })),
171
+ }));
172
+
173
+ /**
174
+ * The same rows, narrowed to the operators a position's grammar admits, with empty groups dropped.
175
+ *
176
+ * The full table is what it filters, rather than a second list written out per position: an
177
+ * operator that gains a group, or moves between two, keeps its label and its neighbours here
178
+ * without anybody remembering to say so twice.
179
+ */
180
+ function restrictGroups(operators: readonly string[]): ExprSelectGroup[] {
181
+ const allowed = new Set(operators);
182
+ return OPERATOR_SELECT_GROUPS.map((group) => ({
183
+ ...group,
184
+ rows: group.rows.filter((row) => allowed.has(row.value)),
185
+ })).filter((group) => group.rows.length > 0);
186
+ }
187
+
188
+ /** The three rungs of the value ladder, spelled the way `ui/value-source.ts` spells them (§6.3). */
189
+ const SOURCE_OPTIONS: ExprOption[] = [
190
+ { label: VALUE_SOURCE_LABELS.literal, value: "literal" },
191
+ { label: VALUE_SOURCE_LABELS.ref, value: "ref" },
192
+ { label: VALUE_SOURCE_LABELS.expression, value: "expression" },
193
+ ];
194
+
195
+ /** What a literal may be. Abbreviated because the picker sits beside the value it types. */
196
+ const TYPE_OPTIONS: ExprOption[] = [
197
+ { label: "str", value: "string" },
198
+ { label: "num", value: "number" },
199
+ { label: "bool", value: "boolean" },
200
+ { label: "null", value: "null" },
201
+ ];
202
+
203
+ /**
204
+ * Which control types a literal of each type.
205
+ *
206
+ * A map rather than a chain of ternaries because the two vocabularies are genuinely different: the
207
+ * type is what the value IS, and the control is what edits it — `null` is a type with no control at
208
+ * all, drawn as an inert word.
209
+ */
210
+ const LITERAL_CONTROLS: Record<string, string> = {
211
+ boolean: "bool",
212
+ null: "null",
213
+ number: "number",
214
+ string: "text",
215
+ };
216
+
217
+ /** Where an event handler's operand may bind besides the document's own signals. */
218
+ const EVENT_REFS = ["event#/detail", "event#/target/value"];
219
+
220
+ /** What a slot with nothing to bind to says, in the shell's one empty-state voice. */
221
+ const NO_BINDINGS_MESSAGE = "A binding points at a value this page holds.";
222
+ const NO_BINDINGS_DETAIL = "Add one in the State panel and it shows up here.";
223
+
132
224
  interface OperatorInfo {
133
225
  needsValue: boolean;
134
226
  needsInitial: boolean;
@@ -329,7 +421,7 @@ export function expressionHint(node: unknown) {
329
421
  return `${targetLabel} ${op} …`;
330
422
  }
331
423
 
332
- // ─── Live Value Badge (spec §19.9) ──────────────────────────────────────────
424
+ // ─── What the editor is handed ──────────────────────────────────────────────
333
425
 
334
426
  /** Preview data computed by services/preview-eval.ts — display strings keyed by node path. */
335
427
  export interface EditorPreview {
@@ -338,615 +430,744 @@ export interface EditorPreview {
338
430
  mutating: boolean;
339
431
  }
340
432
 
341
- function renderValueBadge(preview: EditorPreview | null | undefined, pathKey: string) {
342
- const text = preview?.values.get(pathKey);
343
- if (text === undefined) {
344
- return nothing;
345
- }
346
- return html`<span class="expr-live-badge" title=${text}>${text}</span>`;
433
+ export interface ExpressionEditorOpts {
434
+ stateDefs: string[];
435
+ allowEventRef: boolean;
436
+ depth?: number;
437
+ preview?: EditorPreview | null;
438
+ path?: (string | number)[];
439
+ /** Full state defs map — resolves named-formula catalog entries (call labels, palette). */
440
+ stateEntries?: Record<string, JxStateDefinition> | null;
441
+ /** Chip-strip click hook (depth 0). No-op when absent. */
442
+ onChipSelect?: (path: (string | number)[]) => void;
443
+ /** Vendors a packaged formula's state entry into the document on catalog pick. */
444
+ onInsertDef?: (name: string, def: JxStateDefinition) => void;
445
+ /**
446
+ * The operators the POSITION's own grammar admits, when it admits fewer than all of them.
447
+ *
448
+ * Most positions take an `ExpressionEntry`, whose grammar is the whole operator table, and leave
449
+ * this absent. An element's `tagName` does not: `defs/tag-expression.schema.ts` admits `?:` and
450
+ * `switch` and nothing else, because every branch there must resolve to a literal tag the
451
+ * pipeline can enumerate without evaluating. The panel used to seed that shape correctly and then
452
+ * hand it to this editor unrestricted — so the next click could reoperate it to `capitalize`, or
453
+ * pick `toUpperCase` out of the formula catalog, and write a document its own validator rejects.
454
+ * A whole-document projection then read `cases` off a node that had none.
455
+ *
456
+ * **Depth 0 only.** The restriction is the position's, and a tag choice's `target` is an ordinary
457
+ * `ExpressionOperand` — the discriminant may be any expression at all. Restricting the nested
458
+ * walk too would have taken that away.
459
+ */
460
+ operators?: readonly string[];
347
461
  }
348
462
 
349
- // ─── Ref Picker ─────────────────────────────────────────────────────────────
463
+ /** Everything one gesture on one row may ask for. Each row registers only what it offers. */
464
+ interface RowPlan {
465
+ setOperator?: (value: string) => void;
466
+ browse?: (anchor: HTMLElement) => void;
467
+ setSource?: (value: string) => void;
468
+ setLiteralType?: (value: string) => void;
469
+ setText?: (value: string) => void;
470
+ setNumber?: (value: string) => void;
471
+ setBool?: (checked: boolean) => void;
472
+ setRef?: (value: string) => void;
473
+ renameCase?: (value: string) => void;
474
+ remove?: () => void;
475
+ act?: () => void;
476
+ }
350
477
 
351
478
  /**
352
- * @param {string} refVal
353
- * @param {(newRef: string) => void} onRefChange
354
- * @param {{ stateDefs: string[]; allowEventRef: boolean }} opts
355
- * @returns {import("lit-html").TemplateResult}
479
+ * The whole projection: what the document draws, and what each key in it commits.
480
+ *
481
+ * Exported for its own test — the projection IS the editor now, so what it contains and what a key
482
+ * addresses are the contract rather than an implementation detail of a template.
356
483
  */
357
- function renderRefPicker(
358
- refVal: string,
359
- onRefChange: (newRef: string) => void,
360
- opts: { stateDefs: string[]; allowEventRef: boolean },
361
- ) {
362
- const stateRefs = (opts.stateDefs || []).map((k) => `#/state/${k}`);
363
- const eventRefs = opts.allowEventRef ? ["event#/detail", "event#/target/value"] : [];
364
- const allRefs = [...stateRefs, ...eventRefs];
365
- const isCustom = refVal && !allRefs.includes(refVal);
366
-
367
- // Nothing to pick and nothing already picked: a picker whose only entry is a disabled "No state
368
- // Defined" is a dead end. Say what a binding is instead, in the shell's one empty-state voice.
369
- if (allRefs.length === 0 && !refVal) {
370
- return renderEmptyState({
371
- compact: true,
372
- detail: "Add one in the State panel and it shows up here.",
373
- message: "A binding points at a value this page holds.",
374
- });
375
- }
484
+ export interface ExpressionProjection {
485
+ rows: ExprRowView[];
486
+ plans: Map<string, RowPlan>;
487
+ /** A chip's joined path back to the path it stands for. */
488
+ chipPaths: Map<string, (string | number)[]>;
489
+ }
376
490
 
377
- return html`
378
- <sp-picker
379
- size="s"
380
- class="expr-ref"
381
- placeholder="Select…"
382
- .value=${live(isCustom ? "__custom__" : refVal || "")}
383
- @change=${(e: Event) => {
384
- const val = (e.target as HTMLInputElement).value;
385
- if (val === "__custom__") {
386
- return;
387
- }
388
- onRefChange(val);
389
- }}
390
- >
391
- ${stateRefs.map(
392
- (r) => html`<sp-menu-item value=${r}>${r.replace("#/state/", "")}</sp-menu-item>`,
393
- )}
394
- ${
395
- eventRefs.length > 0
396
- ? html`
397
- <sp-menu-divider></sp-menu-divider>
398
- ${eventRefs.map((r) => html`<sp-menu-item value=${r}>${r}</sp-menu-item>`)}
399
- `
400
- : nothing
401
- }
402
- </sp-picker>
403
- `;
491
+ /** The walk's own state: the options it was given, plus what it has emitted so far. */
492
+ interface Walk {
493
+ opts: ExpressionEditorOpts;
494
+ preview: EditorPreview | null;
495
+ rows: ExprRowView[];
496
+ plans: Map<string, RowPlan>;
497
+ chipPaths: Map<string, (string | number)[]>;
404
498
  }
405
499
 
406
- // ─── Literal Editor ─────────────────────────────────────────────────────────
500
+ /**
501
+ * A row under construction: every field optional, and an explicit `undefined` allowed.
502
+ *
503
+ * `Partial<ExprRowView>` cannot be it. `exactOptionalPropertyTypes` is on, so a spread of one is
504
+ * typed `string | undefined` at a key the target declares as `string` — which is a real distinction
505
+ * everywhere a key means "absent", and a false one here, where every default is supplied below.
506
+ */
507
+ type RowPatch = { [K in keyof ExprRowView]?: ExprRowView[K] | undefined };
407
508
 
408
509
  /**
409
- * @param {unknown} operand
410
- * @param {(newVal: JxExpressionOperand) => void} onChange
411
- * @returns {import("lit-html").TemplateResult}
510
+ * A row with every field answered.
511
+ *
512
+ * The document asks no question an answer is missing for: a `$switch` on an absent field resolves
513
+ * to no case at all, so a partial row is a row that silently draws nothing.
412
514
  */
413
- function renderLiteralEditor(operand: unknown, onChange: (newVal: JxExpressionOperand) => void) {
414
- const type = literalType(operand);
415
- return html`
416
- <div class="expr-literal">
417
- <sp-picker
418
- size="s"
419
- class="expr-literal-type"
420
- .value=${live(type)}
421
- @change=${(e: Event) => {
422
- const newType = (e.target as HTMLInputElement).value;
423
- onChange(defaultForLiteralType(newType));
424
- }}
425
- >
426
- <sp-menu-item value="string">str</sp-menu-item>
427
- <sp-menu-item value="number">num</sp-menu-item>
428
- <sp-menu-item value="boolean">bool</sp-menu-item>
429
- <sp-menu-item value="null">null</sp-menu-item>
430
- </sp-picker>
431
- ${
432
- type === "string"
433
- ? html`<sp-textfield
434
- size="s"
435
- class="expr-literal-value"
436
- .value=${live(String(operand ?? ""))}
437
- @input=${(e: Event) => onChange((e.target as HTMLInputElement).value)}
438
- ></sp-textfield>`
439
- : type === "number"
440
- ? html`<sp-number-field
441
- size="s"
442
- class="expr-literal-value"
443
- .value=${live(Number(operand ?? 0))}
444
- @change=${(e: Event) => onChange(Number((e.target as HTMLInputElement).value))}
445
- ></sp-number-field>`
446
- : type === "boolean"
447
- ? html`<sp-checkbox
448
- size="s"
449
- class="expr-literal-bool"
450
- ?checked=${Boolean(operand)}
451
- @change=${(e: Event) => onChange((e.target as HTMLInputElement).checked)}
452
- >true</sp-checkbox
453
- >`
454
- : html`<span class="expr-literal-null">null</span>`
455
- }
456
- </div>
457
- `;
515
+ function blankRow(row: RowPatch & { key: string; kind: ExprRowView["kind"] }) {
516
+ return {
517
+ badge: "",
518
+ catalog: false,
519
+ checked: false,
520
+ chips: [],
521
+ chooser: false,
522
+ emptyDetail: "",
523
+ emptyMessage: "",
524
+ groups: [],
525
+ hasBadge: false,
526
+ indent: "0",
527
+ label: "",
528
+ lead: "none",
529
+ leadLabel: "",
530
+ leadValue: "",
531
+ literal: "",
532
+ literalType: "",
533
+ nested: "false",
534
+ prop: "",
535
+ refGroups: [],
536
+ refOptions: [],
537
+ removable: false,
538
+ source: "literal",
539
+ sourceOptions: [],
540
+ title: "",
541
+ typeOptions: [],
542
+ value: "",
543
+ ...row,
544
+ } as ExprRowView;
545
+ }
546
+
547
+ /** The indent one depth reads as. `.expression-editor--nested`'s 4px margin, said per row. */
548
+ function indentAt(depth: number): string {
549
+ return `${depth * 4}px`;
550
+ }
551
+
552
+ /** The live value at a node path, and whether the preview holds one at all. */
553
+ function badgeAt(walk: Walk, key: string): { badge: string; hasBadge: boolean } {
554
+ const text = walk.preview?.values.get(key);
555
+ return { badge: text ?? "", hasBadge: text !== undefined };
458
556
  }
459
557
 
460
- // ─── Operand Editor ─────────────────────────────────────────────────────────
558
+ // ─── The walk ───────────────────────────────────────────────────────────────
559
+
560
+ /** Everything one operand slot needs to know about itself before its rung is decided. */
561
+ interface OperandSlot {
562
+ /** The row's own key, unique within the editor. */
563
+ key: string;
564
+ /** The row's visible label, and what `inspector/field:<prop>` addresses. */
565
+ label: string;
566
+ prop: string;
567
+ depth: number;
568
+ path: (string | number)[];
569
+ /** The position admits a pointer and nothing else — an assignment target, a callee. */
570
+ mustBeRef: boolean;
571
+ /** The node path whose live value annotates this row. Empty asks for no badge. */
572
+ badgeKey: string;
573
+ lead?: ExprRowView["lead"];
574
+ leadLabel?: string;
575
+ leadValue?: string;
576
+ removable?: boolean;
577
+ title?: string;
578
+ /** Renaming a `switch` case: the key IS the matched value. */
579
+ renameCase?: (value: string) => void;
580
+ remove?: () => void;
581
+ }
461
582
 
462
583
  /**
463
- * Single-operand editor (mode picker + literal/ref/nested-expression widget). Exported for
464
- * statement-position operand slots (spec §20: `if` tests, `$switch` discriminants, `dispatchEvent`
465
- * detail).
584
+ * Emit one operand row, and — when it holds an expression — the rows of that expression under it.
466
585
  *
467
- * @param {unknown} operand
468
- * @param {(newOperand: unknown) => void} onChange
469
- * @param {{
470
- * stateDefs: string[];
471
- * allowEventRef: boolean;
472
- * depth: number;
473
- * mustBeRef?: boolean;
474
- * }} opts
475
- * @returns {import("lit-html").TemplateResult}
586
+ * @param walk The walk in progress.
587
+ * @param operand What the slot currently holds.
588
+ * @param commit What replacing it means.
589
+ * @param slot Everything about the slot itself.
476
590
  */
477
- export function renderOperandEditor(
591
+ function walkOperand(
592
+ walk: Walk,
478
593
  operand: unknown,
479
- onChange: (newOperand: unknown) => void,
480
- opts: {
481
- stateDefs: string[];
482
- allowEventRef: boolean;
483
- depth: number;
484
- mustBeRef?: boolean;
485
- preview?: EditorPreview | null;
486
- path?: (string | number)[];
487
- stateEntries?: Record<string, JxStateDefinition> | null;
488
- },
489
- ): TemplateResult {
490
- if (opts.mustBeRef) {
491
- const refVal = ((operand as Record<string, unknown> | null)?.$ref as string) ?? "";
492
- return html`
493
- <div class="expr-operand">${renderRefPicker(refVal, (r) => onChange({ $ref: r }), opts)}</div>
494
- `;
594
+ commit: (next: unknown) => void,
595
+ slot: OperandSlot,
596
+ ): void {
597
+ const { allowEventRef, stateDefs } = walk.opts;
598
+ const stateRefs = (stateDefs || []).map((k) => `#/state/${k}`);
599
+ const refValue = ((operand as Record<string, unknown> | null)?.$ref as string) ?? "";
600
+ const plan: RowPlan = {
601
+ setRef: (value) => commit({ $ref: value }),
602
+ };
603
+ if (slot.renameCase) {
604
+ plan.renameCase = slot.renameCase;
605
+ }
606
+ if (slot.remove) {
607
+ plan.remove = slot.remove;
608
+ }
609
+
610
+ const base = {
611
+ ...badgeAt(walk, slot.badgeKey),
612
+ indent: indentAt(slot.depth),
613
+ label: slot.label,
614
+ lead: slot.lead ?? "none",
615
+ leadLabel: slot.leadLabel ?? "",
616
+ leadValue: slot.leadValue ?? "",
617
+ nested: slot.depth > 0 ? "true" : "false",
618
+ prop: slot.prop,
619
+ refGroups: allowEventRef
620
+ ? [{ id: "event", label: "Event", rows: EVENT_REFS.map((r) => ({ label: r, value: r })) }]
621
+ : [],
622
+ refOptions: stateRefs.map((r) => ({ label: r.replace("#/state/", ""), value: r })),
623
+ removable: Boolean(slot.removable),
624
+ title: slot.title ?? "",
625
+ value: refValue,
626
+ };
627
+
628
+ /* Nothing to pick and nothing already picked: a picker whose only entry is a disabled "No state
629
+ defined" is a dead end. Say what a binding IS instead, in the shell's one empty-state voice. */
630
+ const nothingToBind = stateRefs.length === 0 && !allowEventRef && !refValue;
631
+
632
+ if (slot.mustBeRef) {
633
+ walk.plans.set(slot.key, plan);
634
+ walk.rows.push(
635
+ blankRow({
636
+ ...base,
637
+ emptyDetail: nothingToBind ? NO_BINDINGS_DETAIL : "",
638
+ emptyMessage: nothingToBind ? NO_BINDINGS_MESSAGE : "",
639
+ key: slot.key,
640
+ kind: "operand",
641
+ source: nothingToBind ? "empty" : "ref",
642
+ }),
643
+ );
644
+ return;
495
645
  }
496
646
 
497
647
  const mode = operandMode(operand);
498
- return html`
499
- <div class="expr-operand">
500
- <sp-picker
501
- size="s"
502
- class="expr-operand-mode"
503
- label="Value source"
504
- .value=${live(mode)}
505
- @change=${(e: Event) => {
506
- const newMode = (e.target as HTMLInputElement).value;
507
- onChange(defaultForMode(newMode));
508
- }}
509
- >
510
- <sp-menu-item value="literal">${VALUE_SOURCE_LABELS.literal}</sp-menu-item>
511
- <sp-menu-item value="ref">${VALUE_SOURCE_LABELS.ref}</sp-menu-item>
512
- <sp-menu-item value="expression">${VALUE_SOURCE_LABELS.expression}</sp-menu-item>
513
- </sp-picker>
514
- ${
515
- mode === "literal"
516
- ? renderLiteralEditor(operand, onChange)
517
- : mode === "ref"
518
- ? renderRefPicker(
519
- ((operand as Record<string, unknown> | null)?.$ref as string) ?? "",
520
- (r) => onChange({ $ref: r }),
521
- opts,
522
- )
523
- : renderExpressionEditor(operand, onChange, {
524
- ...opts,
525
- depth: opts.depth + 1,
526
- })
527
- }
528
- </div>
529
- `;
648
+ plan.setSource = (value) => commit(defaultForMode(value));
649
+ if (mode === "literal") {
650
+ const type = literalType(operand);
651
+ const asNumber = Number(operand ?? 0);
652
+ plan.setLiteralType = (value) => commit(defaultForLiteralType(value));
653
+ plan.setText = (value) => commit(value);
654
+ plan.setNumber = (value) => commit(Number(value));
655
+ plan.setBool = (checked) => commit(checked);
656
+ walk.plans.set(slot.key, plan);
657
+ walk.rows.push(
658
+ blankRow({
659
+ ...base,
660
+ checked: Boolean(operand),
661
+ chooser: true,
662
+ key: slot.key,
663
+ kind: "operand",
664
+ literal: LITERAL_CONTROLS[type],
665
+ literalType: type,
666
+ source: "literal",
667
+ sourceOptions: SOURCE_OPTIONS,
668
+ typeOptions: TYPE_OPTIONS,
669
+ value: type === "number" ? String(asNumber) : String(operand ?? ""),
670
+ }),
671
+ );
672
+ return;
673
+ }
674
+
675
+ walk.plans.set(slot.key, plan);
676
+ walk.rows.push(
677
+ blankRow({
678
+ ...base,
679
+ chooser: true,
680
+ emptyDetail: mode === "ref" && nothingToBind ? NO_BINDINGS_DETAIL : "",
681
+ emptyMessage: mode === "ref" && nothingToBind ? NO_BINDINGS_MESSAGE : "",
682
+ key: slot.key,
683
+ kind: "operand",
684
+ source: mode === "ref" && nothingToBind ? "empty" : mode,
685
+ sourceOptions: SOURCE_OPTIONS,
686
+ }),
687
+ );
688
+ if (mode === "expression") {
689
+ walkExpression(walk, operand, commit, slot.depth + 1, slot.path);
690
+ }
530
691
  }
531
692
 
532
- // ─── Positional Args Editor (splice / call) ─────────────────────────────────
693
+ /** The rows of a positional-args list (`splice`, `call`), plus the row that grows it. */
694
+ function walkArgs(
695
+ walk: Walk,
696
+ args: unknown,
697
+ commit: (next: unknown[]) => void,
698
+ depth: number,
699
+ path: (string | number)[],
700
+ naming: { labels: string[]; fallbackLabel: string },
701
+ ): void {
702
+ /* `Array.isArray` narrows an `unknown` to `any[]`, so every spread of it reads as unsafe to
703
+ the type-aware lint. The elements ARE unknown — each is an operand this walk discriminates
704
+ — so saying so restores the check rather than silencing it. */
705
+ const safeArgs: unknown[] = Array.isArray(args) ? args : [];
706
+ for (const [idx, arg] of safeArgs.entries()) {
707
+ const argPath = [...path, "value", idx];
708
+ const label = naming.labels[idx] ?? naming.fallbackLabel;
709
+ walkOperand(
710
+ walk,
711
+ arg,
712
+ (next) => {
713
+ const updated = [...safeArgs];
714
+ updated[idx] = next;
715
+ commit(updated);
716
+ },
717
+ {
718
+ badgeKey: "",
719
+ depth,
720
+ key: argPath.join("/"),
721
+ label,
722
+ leadLabel: label,
723
+ lead: "arg",
724
+ mustBeRef: false,
725
+ path: argPath,
726
+ prop: "value",
727
+ remove: () => {
728
+ const updated = safeArgs.filter((_, i) => i !== idx);
729
+ commit(updated.length > 0 ? updated : [null]);
730
+ },
731
+ removable: true,
732
+ title: `Remove ${label}`,
733
+ },
734
+ );
735
+ }
736
+ const addKey = `${path.join("/")}::add-arg`;
737
+ walk.plans.set(addKey, { act: () => commit([...safeArgs, null]) });
738
+ walk.rows.push(
739
+ blankRow({
740
+ indent: indentAt(depth),
741
+ key: addKey,
742
+ kind: "action",
743
+ label: "+ Add arg",
744
+ nested: depth > 0 ? "true" : "false",
745
+ }),
746
+ );
747
+ }
533
748
 
534
- /**
535
- * @param {unknown[]} args
536
- * @param {(newArgs: unknown[]) => void} onChange
537
- * @param {{ stateDefs: string[]; allowEventRef: boolean; depth: number }} opts
538
- * @param {{ labels?: string[]; fallbackLabel?: string }} [naming]
539
- * @returns {import("lit-html").TemplateResult}
540
- */
541
- function renderSpliceArgsEditor(
542
- args: unknown[],
543
- onChange: (newArgs: unknown[]) => void,
544
- opts: { stateDefs: string[]; allowEventRef: boolean; depth: number },
545
- naming: { labels?: string[]; fallbackLabel?: string } = {},
546
- ): TemplateResult {
547
- const safeArgs = Array.isArray(args) ? args : [];
548
- const labels = naming.labels ?? ["start", "del", "item"];
549
- const fallbackLabel = naming.fallbackLabel ?? "item";
550
-
551
- return html`
552
- <div class="array-object-field">
553
- ${safeArgs.map(
554
- (arg, idx) => html`
555
- <div class="array-object-row">
556
- <span class="array-object-label">${labels[idx] ?? fallbackLabel}</span>
557
- ${renderOperandEditor(
558
- arg,
559
- (newArg) => {
560
- const updated = [...safeArgs];
561
- updated[idx] = newArg;
562
- onChange(updated);
563
- },
564
- { ...opts, mustBeRef: false },
565
- )}
566
- <sp-action-button
567
- quiet
568
- size="xs"
569
- @click=${() => {
570
- const updated = safeArgs.filter((_, i) => i !== idx);
571
- onChange(updated.length > 0 ? updated : [null]);
572
- }}
573
- >
574
- <sp-icon-delete slot="icon"></sp-icon-delete>
575
- </sp-action-button>
576
- </div>
577
- `,
578
- )}
579
- <sp-action-button quiet size="s" @click=${() => onChange([...safeArgs, null])}>
580
- + Add arg
581
- </sp-action-button>
582
- </div>
583
- `;
584
- }
585
-
586
- // ─── Operator Picker Menu ───────────────────────────────────────────────────
587
-
588
- const _operatorMenuCache = OPERATOR_GROUPS.map(
589
- (group, i) => html`
590
- ${i > 0 ? html`<sp-menu-divider></sp-menu-divider>` : nothing}
591
- <sp-menu-group>
592
- <span slot="header">${group.label}</span>
593
- ${group.ops.map((op) => html`<sp-menu-item value=${op}>${op}</sp-menu-item>`)}
594
- </sp-menu-group>
595
- `,
596
- );
597
-
598
- // ─── Main Expression Editor ─────────────────────────────────────────────────
749
+ /** The case rows of a `switch` node: matched value → result operand, the default, and `+ Add case`. */
750
+ function walkSwitchCases(
751
+ walk: Walk,
752
+ safeNode: Record<string, unknown>,
753
+ onChange: (node: unknown) => void,
754
+ depth: number,
755
+ path: (string | number)[],
756
+ ): void {
757
+ const cases = isJsonObject(safeNode.cases)
758
+ ? (safeNode.cases as Record<string, unknown>)
759
+ : ({} as Record<string, unknown>);
760
+ const entries = Object.entries(cases);
761
+ const setCases = (next: Record<string, unknown>) => onChange({ ...safeNode, cases: next });
599
762
 
600
- export interface ExpressionEditorOpts {
601
- stateDefs: string[];
602
- allowEventRef: boolean;
603
- depth?: number;
604
- preview?: EditorPreview | null;
605
- path?: (string | number)[];
606
- /** Full state defs map — resolves named-formula catalog entries (call labels, palette). */
607
- stateEntries?: Record<string, JxStateDefinition> | null;
608
- /** Chip-strip click hook (depth 0). No-op when absent. */
609
- onChipSelect?: (path: (string | number)[]) => void;
610
- /** Vendors a packaged formula's state entry into the document on catalog pick. */
611
- onInsertDef?: (name: string, def: JxStateDefinition) => void;
763
+ for (const [caseKey, operand] of entries) {
764
+ const casePath = [...path, "cases", caseKey];
765
+ walkOperand(walk, operand, (v) => setCases({ ...cases, [caseKey]: v }), {
766
+ badgeKey: casePath.join("/"),
767
+ depth,
768
+ key: casePath.join("/"),
769
+ label: `Case ${caseKey}`,
770
+ lead: "case",
771
+ leadValue: caseKey,
772
+ mustBeRef: false,
773
+ path: casePath,
774
+ prop: "cases",
775
+ remove: () => {
776
+ const next = { ...cases };
777
+ delete next[caseKey];
778
+ setCases(next);
779
+ },
780
+ removable: true,
781
+ renameCase: (newKey) => {
782
+ if (newKey === caseKey) {
783
+ return;
784
+ }
785
+ const next: Record<string, unknown> = {};
786
+ for (const [k, v] of entries) {
787
+ next[k === caseKey ? newKey : k] = v;
788
+ }
789
+ setCases(next);
790
+ },
791
+ title: `Remove case ${caseKey}`,
792
+ });
793
+ }
794
+
795
+ const defaultPath = [...path, "default"];
796
+ walkOperand(walk, safeNode.default, (v) => onChange({ ...safeNode, default: v }), {
797
+ badgeKey: defaultPath.join("/"),
798
+ depth,
799
+ key: defaultPath.join("/"),
800
+ label: "Default",
801
+ lead: "default",
802
+ leadLabel: "default",
803
+ mustBeRef: false,
804
+ path: defaultPath,
805
+ prop: "default",
806
+ });
807
+
808
+ const addKey = `${path.join("/")}::add-case`;
809
+ walk.plans.set(addKey, {
810
+ act: () => {
811
+ let n = entries.length + 1;
812
+ let key = `case ${n}`;
813
+ while (Object.hasOwn(cases, key)) {
814
+ n += 1;
815
+ key = `case ${n}`;
816
+ }
817
+ setCases({ ...cases, [key]: null });
818
+ },
819
+ });
820
+ walk.rows.push(
821
+ blankRow({
822
+ indent: indentAt(depth),
823
+ key: addKey,
824
+ kind: "action",
825
+ label: "+ Add case",
826
+ nested: depth > 0 ? "true" : "false",
827
+ }),
828
+ );
612
829
  }
613
830
 
614
- /**
615
- * @param {unknown} node
616
- * @param {(node: unknown) => void} onChange
617
- * @param {ExpressionEditorOpts} opts
618
- * @returns {import("lit-html").TemplateResult}
619
- */
620
- export function renderExpressionEditor(
831
+ /** What replacing the operator means for the rest of the node. */
832
+ function reoperate(safeNode: Record<string, unknown>, newOp: string): Record<string, unknown> {
833
+ const newInfo = operatorInfo(newOp);
834
+ const updated: Record<string, unknown> = { operator: newOp, target: safeNode.target };
835
+ if (newInfo.targetMustBeRef && operandMode(safeNode.target) !== "ref") {
836
+ updated.target = { $ref: "" };
837
+ }
838
+ if (newInfo.needsValue) {
839
+ if (newInfo.valueIsNode) {
840
+ const val = safeNode.value as Record<string, unknown> | null;
841
+ updated.value = val?.operator ? safeNode.value : { operator: "!", target: null };
842
+ } else if (newInfo.spliceArray) {
843
+ updated.value = Array.isArray(safeNode.value) ? safeNode.value : [null];
844
+ } else {
845
+ updated.value = safeNode.value ?? null;
846
+ }
847
+ }
848
+ if (newInfo.needsInitial) {
849
+ updated.initial = safeNode.initial ?? (newOp === "?:" ? null : 0);
850
+ }
851
+ if (newInfo.switchCases) {
852
+ updated.cases = isJsonObject(safeNode.cases) ? safeNode.cases : {};
853
+ if ("default" in safeNode) {
854
+ updated.default = safeNode.default;
855
+ }
856
+ }
857
+ if (newInfo.callArgs) {
858
+ updated.value = Array.isArray(safeNode.value) ? safeNode.value : [];
859
+ }
860
+ return updated;
861
+ }
862
+
863
+ /** Emit every row one expression node reads as, deepest last. */
864
+ function walkExpression(
865
+ walk: Walk,
621
866
  node: unknown,
622
867
  onChange: (node: unknown) => void,
623
- opts: ExpressionEditorOpts,
624
- ): TemplateResult {
625
- const depth = opts.depth ?? 0;
868
+ depth: number,
869
+ path: (string | number)[],
870
+ ): void {
626
871
  const safeNode: Record<string, unknown> =
627
872
  node && typeof node === "object"
628
873
  ? (node as Record<string, unknown>)
629
874
  : { operator: "=", target: null };
630
875
  const op = (safeNode.operator as string) || "=";
631
876
  const info = operatorInfo(op);
632
- const preview = opts.preview ?? null;
633
- const path = opts.path ?? [];
634
877
  const pathKey = path.join("/");
635
878
  const sub = (...segs: (string | number)[]) => [...path, ...segs].join("/");
636
- /** Wrap an operand widget with its live value badge. */
637
- const withBadge = (widget: unknown, key: string) =>
638
- html`<div class="expr-widget">${widget}${renderValueBadge(preview, key)}</div>`;
879
+ const { opts, preview } = walk;
880
+
881
+ if (depth === 0) {
882
+ const chips = formulaChipStrip(safeNode, { path, preview });
883
+ if (chips.length > 0) {
884
+ for (const chip of chips) {
885
+ walk.chipPaths.set(chip.key, chip.path);
886
+ }
887
+ walk.rows.push(
888
+ blankRow({
889
+ chips: chips.map((chip) => ({
890
+ badge: chip.badge,
891
+ group: chip.group ? "true" : "false",
892
+ hasBadge: chip.hasBadge,
893
+ key: chip.key,
894
+ label: chip.label,
895
+ })),
896
+ key: "::chips",
897
+ kind: "chips",
898
+ }),
899
+ );
900
+ }
901
+ if (preview?.error) {
902
+ walk.rows.push(blankRow({ key: "::error", kind: "error", label: preview.error }));
903
+ }
904
+ }
639
905
 
640
906
  // The root badge: pure roots show their result; mutating roots' effect shows on target/value.
641
907
  const rootBadge =
642
- depth === 0 && preview && !preview.mutating ? renderValueBadge(preview, pathKey) : nothing;
643
-
644
- return html`
645
- <div class=${depth > 0 ? "expression-editor expression-editor--nested" : "expression-editor"}>
646
- ${
647
- depth === 0
648
- ? renderFormulaChips(safeNode, opts.onChipSelect ?? (() => {}), { path, preview })
649
- : nothing
650
- }
651
- ${
652
- depth === 0 && preview?.error
653
- ? html`<div class="expr-error">${preview.error}</div>`
654
- : nothing
655
- }
656
- ${renderFieldRow({
657
- hasValue: false,
658
- label: "Operator",
659
- prop: "operator",
660
- widget: html`
661
- <div class="expr-widget">
662
- <sp-picker
663
- size="s"
664
- class="expr-operator"
665
- .value=${live(op)}
666
- @change=${(e: Event) => {
667
- const newOp = (e.target as HTMLInputElement).value;
668
- const newInfo = operatorInfo(newOp);
669
- const updated: Record<string, unknown> = {
670
- operator: newOp,
671
- target: safeNode.target,
672
- };
673
- if (newInfo.targetMustBeRef && operandMode(safeNode.target) !== "ref") {
674
- updated.target = { $ref: "" };
675
- }
676
- if (newInfo.needsValue) {
677
- if (newInfo.valueIsNode) {
678
- const val = safeNode.value as Record<string, unknown> | null;
679
- updated.value = val?.operator
680
- ? safeNode.value
681
- : { operator: "!", target: null };
682
- } else if (newInfo.spliceArray) {
683
- updated.value = Array.isArray(safeNode.value) ? safeNode.value : [null];
684
- } else {
685
- updated.value = safeNode.value ?? null;
686
- }
687
- }
688
- if (newInfo.needsInitial) {
689
- updated.initial = safeNode.initial ?? (newOp === "?:" ? null : 0);
690
- }
691
- if (newInfo.switchCases) {
692
- updated.cases = isJsonObject(safeNode.cases) ? safeNode.cases : {};
693
- if ("default" in safeNode) {
694
- updated.default = safeNode.default;
695
- }
696
- }
697
- if (newInfo.callArgs) {
698
- updated.value = Array.isArray(safeNode.value) ? safeNode.value : [];
699
- }
700
- onChange(updated);
701
- }}
702
- >
703
- ${_operatorMenuCache}
704
- </sp-picker>
705
- <sp-action-button
706
- quiet
707
- size="s"
708
- class="expr-browse-catalog"
709
- title="Browse catalog"
710
- @click=${(e: Event) =>
711
- openFormulaPalette({
712
- anchor: e.currentTarget as HTMLElement,
713
- entries: formulaCatalog(opts.stateEntries),
714
- onPick: (entry) =>
715
- applyCatalogPick(entry, onChange, {
716
- onInsertDef: opts.onInsertDef,
717
- stateEntries: opts.stateEntries,
718
- }),
719
- })}
720
- >
721
- <sp-icon-brackets slot="icon"></sp-icon-brackets>
722
- </sp-action-button>
723
- ${rootBadge}
724
- </div>
725
- `,
726
- })}
727
- ${renderFieldRow({
728
- hasValue: false,
729
- label: op === "?:" ? "If" : op === "switch" ? "On" : op === "call" ? "Callee" : "Target",
730
- prop: "target",
731
- widget: withBadge(
732
- renderOperandEditor(safeNode.target, (t) => onChange({ ...safeNode, target: t }), {
733
- ...opts,
734
- depth,
735
- mustBeRef: info.targetMustBeRef,
736
- path: [...path, "target"],
737
- }),
738
- sub("target"),
739
- ),
740
- })}
741
- ${
742
- info.needsValue && !info.valueIsNode && !info.spliceArray
743
- ? renderFieldRow({
744
- hasValue: false,
745
- label: op === "?:" ? "Then" : "Value",
746
- prop: "value",
747
- widget: withBadge(
748
- renderOperandEditor(safeNode.value, (v) => onChange({ ...safeNode, value: v }), {
749
- ...opts,
750
- depth,
751
- mustBeRef: false,
752
- path: [...path, "value"],
908
+ depth === 0 && preview && !preview.mutating
909
+ ? badgeAt(walk, pathKey)
910
+ : { badge: "", hasBadge: false };
911
+
912
+ /* The position's own grammar, and only at its own root — see `ExpressionEditorOpts.operators`.
913
+ The catalog goes with it: every entry in it inserts a whole node, so a palette offered here
914
+ would be a second door onto the operators the select has just stopped offering. */
915
+ const grammar = depth === 0 ? opts.operators : undefined;
916
+ const operatorKey = `${pathKey}::operator`;
917
+ walk.plans.set(operatorKey, {
918
+ ...(grammar
919
+ ? {}
920
+ : {
921
+ browse: (anchor: HTMLElement) =>
922
+ openFormulaPalette({
923
+ anchor,
924
+ entries: formulaCatalog(opts.stateEntries),
925
+ onPick: (entry) =>
926
+ applyCatalogPick(entry, onChange, {
927
+ onInsertDef: opts.onInsertDef,
928
+ stateEntries: opts.stateEntries,
753
929
  }),
754
- sub("value"),
755
- ),
756
- })
757
- : nothing
758
- }
759
- ${
760
- info.needsValue && info.valueIsNode
761
- ? html`
762
- <div class="expr-subsection">
763
- ${renderFieldRow({
764
- hasValue: false,
765
- label: "Per-item",
766
- prop: "value",
767
- widget: nothing,
768
- })}
769
- ${renderExpressionEditor(
770
- (safeNode.value as Record<string, unknown> | null)?.operator
771
- ? safeNode.value
772
- : { operator: "!", target: null },
773
- (v) => onChange({ ...safeNode, value: v }),
774
- { ...opts, depth: depth + 1, path: [...path, "value"] },
775
- )}
776
- </div>
777
- `
778
- : nothing
779
- }
780
- ${
781
- info.spliceArray
782
- ? html`
783
- <div class="expr-subsection">
784
- ${renderFieldRow({
785
- hasValue: false,
786
- label: "Args",
787
- prop: "value",
788
- widget: nothing,
789
- })}
790
- ${renderSpliceArgsEditor(
791
- safeNode.value as unknown[],
792
- (v) => onChange({ ...safeNode, value: v }),
793
- {
794
- ...opts,
795
- depth,
796
- },
797
- )}
798
- </div>
799
- `
800
- : nothing
801
- }
802
- ${
803
- info.callArgs
804
- ? html`
805
- <div class="expr-subsection">
806
- ${renderFieldRow({
807
- hasValue: false,
808
- label: "Args",
809
- prop: "value",
810
- widget: nothing,
811
- })}
812
- ${renderSpliceArgsEditor(
813
- safeNode.value as unknown[],
814
- (v) => onChange({ ...safeNode, value: v }),
815
- { ...opts, depth },
816
- {
817
- fallbackLabel: "arg",
818
- labels: calleeParamLabels(safeNode.target, opts.stateEntries),
819
- },
820
- )}
821
- </div>
822
- `
823
- : nothing
824
- }
825
- ${
826
- info.switchCases
827
- ? renderSwitchCasesEditor(safeNode, onChange, { ...opts, depth, path }, withBadge)
828
- : nothing
829
- }
830
- ${
831
- info.needsInitial
832
- ? renderFieldRow({
833
- hasValue: false,
834
- label: op === "?:" ? "Else" : "Initial",
835
- prop: "initial",
836
- widget: withBadge(
837
- renderOperandEditor(
838
- safeNode.initial,
839
- (v) => onChange({ ...safeNode, initial: v }),
840
- {
841
- ...opts,
842
- depth,
843
- mustBeRef: false,
844
- path: [...path, "initial"],
845
- },
846
- ),
847
- sub("initial"),
848
- ),
849
- })
850
- : nothing
930
+ }),
931
+ }),
932
+ setOperator: (newOp) => onChange(reoperate(safeNode, newOp)),
933
+ });
934
+ walk.rows.push(
935
+ blankRow({
936
+ ...rootBadge,
937
+ catalog: grammar === undefined,
938
+ groups: grammar ? restrictGroups(grammar) : OPERATOR_SELECT_GROUPS,
939
+ indent: indentAt(depth),
940
+ key: operatorKey,
941
+ kind: "operator",
942
+ label: "Operator",
943
+ nested: depth > 0 ? "true" : "false",
944
+ prop: "operator",
945
+ title: "Browse catalog",
946
+ value: op,
947
+ }),
948
+ );
949
+
950
+ walkOperand(walk, safeNode.target, (t) => onChange({ ...safeNode, target: t }), {
951
+ badgeKey: sub("target"),
952
+ depth,
953
+ key: `${pathKey}::target`,
954
+ label: op === "?:" ? "If" : op === "switch" ? "On" : op === "call" ? "Callee" : "Target",
955
+ mustBeRef: info.targetMustBeRef,
956
+ path: [...path, "target"],
957
+ prop: "target",
958
+ });
959
+
960
+ if (info.needsValue && !info.valueIsNode && !info.spliceArray) {
961
+ walkOperand(walk, safeNode.value, (v) => onChange({ ...safeNode, value: v }), {
962
+ badgeKey: sub("value"),
963
+ depth,
964
+ key: `${pathKey}::value`,
965
+ label: op === "?:" ? "Then" : "Value",
966
+ mustBeRef: false,
967
+ path: [...path, "value"],
968
+ prop: "value",
969
+ });
970
+ }
971
+
972
+ if (info.needsValue && info.valueIsNode) {
973
+ walk.rows.push(
974
+ blankRow({
975
+ indent: indentAt(depth),
976
+ key: `${pathKey}::per-item`,
977
+ kind: "label",
978
+ label: "Per-item",
979
+ nested: depth > 0 ? "true" : "false",
980
+ prop: "value",
981
+ }),
982
+ );
983
+ walkExpression(
984
+ walk,
985
+ (safeNode.value as Record<string, unknown> | null)?.operator
986
+ ? safeNode.value
987
+ : { operator: "!", target: null },
988
+ (v) => onChange({ ...safeNode, value: v }),
989
+ depth + 1,
990
+ [...path, "value"],
991
+ );
992
+ }
993
+
994
+ if (info.spliceArray || info.callArgs) {
995
+ walk.rows.push(
996
+ blankRow({
997
+ indent: indentAt(depth),
998
+ key: `${pathKey}::args`,
999
+ kind: "label",
1000
+ label: "Args",
1001
+ nested: depth > 0 ? "true" : "false",
1002
+ prop: "value",
1003
+ }),
1004
+ );
1005
+ walkArgs(
1006
+ walk,
1007
+ safeNode.value,
1008
+ (v) => onChange({ ...safeNode, value: v }),
1009
+ depth,
1010
+ path,
1011
+ info.callArgs
1012
+ ? {
1013
+ fallbackLabel: "arg",
1014
+ labels: calleeParamLabels(safeNode.target, opts.stateEntries),
1015
+ }
1016
+ : { fallbackLabel: "item", labels: ["start", "del", "item"] },
1017
+ );
1018
+ }
1019
+
1020
+ if (info.switchCases) {
1021
+ walkSwitchCases(walk, safeNode, onChange, depth, path);
1022
+ }
1023
+
1024
+ if (info.needsInitial) {
1025
+ walkOperand(walk, safeNode.initial, (v) => onChange({ ...safeNode, initial: v }), {
1026
+ badgeKey: sub("initial"),
1027
+ depth,
1028
+ key: `${pathKey}::initial`,
1029
+ label: op === "?:" ? "Else" : "Initial",
1030
+ mustBeRef: false,
1031
+ path: [...path, "initial"],
1032
+ prop: "initial",
1033
+ });
1034
+ }
1035
+ }
1036
+
1037
+ /** Start a walk with nothing emitted yet. */
1038
+ function newWalk(opts: ExpressionEditorOpts): Walk {
1039
+ return {
1040
+ chipPaths: new Map(),
1041
+ opts,
1042
+ plans: new Map(),
1043
+ preview: opts.preview ?? null,
1044
+ rows: [],
1045
+ };
1046
+ }
1047
+
1048
+ /** The projection one expression reads as: the rows, and what each key commits. */
1049
+ export function flattenExpression(
1050
+ node: unknown,
1051
+ onChange: (node: unknown) => void,
1052
+ opts: ExpressionEditorOpts,
1053
+ ): ExpressionProjection {
1054
+ const walk = newWalk(opts);
1055
+ walkExpression(walk, node, onChange, opts.depth ?? 0, opts.path ?? []);
1056
+ return { chipPaths: walk.chipPaths, plans: walk.plans, rows: walk.rows };
1057
+ }
1058
+
1059
+ /**
1060
+ * The projection ONE operand slot reads as — a statement's `if` test, a `$switch` discriminant, a
1061
+ * `dispatchEvent` detail (spec §20). The same rows, with no operator row above them.
1062
+ */
1063
+ export function flattenOperand(
1064
+ operand: unknown,
1065
+ onChange: (next: unknown) => void,
1066
+ opts: ExpressionEditorOpts & { label?: string; prop?: string },
1067
+ ): ExpressionProjection {
1068
+ const walk = newWalk(opts);
1069
+ const path = opts.path ?? [];
1070
+ walkOperand(walk, operand, onChange, {
1071
+ badgeKey: path.join("/"),
1072
+ depth: opts.depth ?? 0,
1073
+ key: `${path.join("/")}::operand`,
1074
+ label: opts.label ?? "Value",
1075
+ mustBeRef: false,
1076
+ path,
1077
+ prop: opts.prop ?? "operand",
1078
+ });
1079
+ return { chipPaths: walk.chipPaths, plans: walk.plans, rows: walk.rows };
1080
+ }
1081
+
1082
+ // ─── The mount seam ─────────────────────────────────────────────────────────
1083
+
1084
+ /** What one standing editor is currently showing, and what its keys currently commit. */
1085
+ interface Held {
1086
+ projection: ExpressionProjection;
1087
+ opts: ExpressionEditorOpts;
1088
+ }
1089
+
1090
+ /**
1091
+ * The latest projection per host — the indirection that keeps a gesture honest.
1092
+ *
1093
+ * A mount reads its actions ONCE (`surfaces/expression-editor.ts` assigns rows on a repaint and
1094
+ * never rebuilds the scope, which is what keeps a reader's caret in a field). So an action that
1095
+ * closed over the projection it was built beside would go on committing the tree that projection
1096
+ * was walked from, and every edit after the first would be written against a stale node: the Logic
1097
+ * dock selects a sub-node, re-projects, and the operator picker then rewrites the node that used to
1098
+ * be selected. Closing over the HOST instead and reading this map at call time is what makes "which
1099
+ * node does this row address" a question answered when the row is used.
1100
+ */
1101
+ const held = new Map<HTMLElement, Held>();
1102
+
1103
+ /** The actions for a host, resolved through whatever that host is showing right now. */
1104
+ function actionsFor(host: HTMLElement): ExpressionActions {
1105
+ const plan = (key: string) => held.get(host)?.projection.plans.get(key);
1106
+ return {
1107
+ act: (key) => plan(key)?.act?.(),
1108
+ browse: (key, anchor) => plan(key)?.browse?.(anchor),
1109
+ pickChip: (key) => {
1110
+ const current = held.get(host);
1111
+ const path = current?.projection.chipPaths.get(key);
1112
+ if (path) {
1113
+ current?.opts.onChipSelect?.(path);
851
1114
  }
852
- </div>
853
- `;
1115
+ },
1116
+ remove: (key) => plan(key)?.remove?.(),
1117
+ renameCase: (key, value) => plan(key)?.renameCase?.(value),
1118
+ setBool: (key, checked) => plan(key)?.setBool?.(checked),
1119
+ setLiteralType: (key, value) => plan(key)?.setLiteralType?.(value),
1120
+ setNumber: (key, value) => plan(key)?.setNumber?.(value),
1121
+ setOperator: (key, value) => plan(key)?.setOperator?.(value),
1122
+ setRef: (key, value) => plan(key)?.setRef?.(value),
1123
+ setSource: (key, value) => plan(key)?.setSource?.(value),
1124
+ setText: (key, value) => plan(key)?.setText?.(value),
1125
+ };
854
1126
  }
855
1127
 
856
- // ─── Switch Cases Editor ────────────────────────────────────────────────────
1128
+ /**
1129
+ * Hand a fresh projection to `host` — mounting the document the first time, assigning after that.
1130
+ *
1131
+ * A slot the reader unbound takes its host out of the page and nothing else in the chain hears
1132
+ * about it, because the panel around it simply renders something else. Sweeping here is the one
1133
+ * moment this module is guaranteed to run.
1134
+ */
1135
+ function paint(host: HTMLElement, projection: ExpressionProjection, opts: ExpressionEditorOpts) {
1136
+ for (const other of held.keys()) {
1137
+ if (!other.isConnected) {
1138
+ held.delete(other);
1139
+ }
1140
+ }
1141
+ disposeDetachedExpressionEditors();
1142
+ held.set(host, { opts, projection });
1143
+ renderExpressionEditorSurface(host, projection.rows, actionsFor(host));
1144
+ }
857
1145
 
858
1146
  /**
859
- * Case rows for the `switch` operator: matched value → result operand, plus the default operand.
860
- * Mirrors the element-level `$switch`/`cases` model (spec §19.4b).
1147
+ * Draw one expression editor into `host`, or bring the one already there up to date.
1148
+ *
1149
+ * This is the seam every caller uses. The document CLEARS its host, so the host must be one the
1150
+ * caller left empty — which is what all five of them already drew for the lit version.
861
1151
  */
862
- function renderSwitchCasesEditor(
863
- safeNode: Record<string, unknown>,
1152
+ export function mountExpressionEditor(
1153
+ host: HTMLElement,
1154
+ node: unknown,
864
1155
  onChange: (node: unknown) => void,
865
- opts: {
866
- stateDefs: string[];
867
- allowEventRef: boolean;
868
- depth: number;
869
- preview?: EditorPreview | null;
870
- path: (string | number)[];
871
- },
872
- withBadge: (widget: unknown, key: string) => TemplateResult,
873
- ): TemplateResult {
874
- const cases = isJsonObject(safeNode.cases)
875
- ? (safeNode.cases as Record<string, unknown>)
876
- : ({} as Record<string, unknown>);
877
- const entries = Object.entries(cases);
878
- const setCases = (next: Record<string, unknown>) => onChange({ ...safeNode, cases: next });
1156
+ opts: ExpressionEditorOpts,
1157
+ ): void {
1158
+ paint(host, flattenExpression(node, onChange, opts), opts);
1159
+ }
879
1160
 
880
- return html`
881
- <div class="switch-cases">
882
- ${entries.map(
883
- ([key, operand]) => html`
884
- <div class="switch-case-row">
885
- <sp-textfield
886
- size="s"
887
- class="switch-case-key"
888
- placeholder="value"
889
- .value=${live(key)}
890
- @change=${(e: Event) => {
891
- const newKey = (e.target as HTMLInputElement).value;
892
- if (newKey === key) {
893
- return;
894
- }
895
- const next: Record<string, unknown> = {};
896
- for (const [k, v] of entries) {
897
- next[k === key ? newKey : k] = v;
898
- }
899
- setCases(next);
900
- }}
901
- ></sp-textfield>
902
- ${withBadge(
903
- renderOperandEditor(operand, (v) => setCases({ ...cases, [key]: v }), {
904
- ...opts,
905
- mustBeRef: false,
906
- path: [...opts.path, "cases", key],
907
- }),
908
- [...opts.path, "cases", key].join("/"),
909
- )}
910
- <sp-action-button
911
- quiet
912
- size="xs"
913
- @click=${() => {
914
- const next = { ...cases };
915
- delete next[key];
916
- setCases(next);
917
- }}
918
- >
919
- <sp-icon-delete slot="icon"></sp-icon-delete>
920
- </sp-action-button>
921
- </div>
922
- `,
923
- )}
924
- <div class="switch-case-row">
925
- <span class="switch-case-key switch-case-default">default</span>
926
- ${withBadge(
927
- renderOperandEditor(safeNode.default, (v) => onChange({ ...safeNode, default: v }), {
928
- ...opts,
929
- mustBeRef: false,
930
- path: [...opts.path, "default"],
931
- }),
932
- [...opts.path, "default"].join("/"),
933
- )}
934
- </div>
935
- <sp-action-button
936
- quiet
937
- size="s"
938
- @click=${() => {
939
- let n = entries.length + 1;
940
- let key = `case ${n}`;
941
- while (Object.hasOwn(cases, key)) {
942
- n += 1;
943
- key = `case ${n}`;
944
- }
945
- setCases({ ...cases, [key]: null });
946
- }}
947
- >
948
- + Add case
949
- </sp-action-button>
950
- </div>
951
- `;
1161
+ /**
1162
+ * Draw ONE operand slot into `host` — statement position, where there is no operator to pick.
1163
+ *
1164
+ * @param opts The editor's options, plus what to call the slot in its own label.
1165
+ */
1166
+ export function mountOperandEditor(
1167
+ host: HTMLElement,
1168
+ operand: unknown,
1169
+ onChange: (next: unknown) => void,
1170
+ opts: ExpressionEditorOpts & { label?: string; prop?: string },
1171
+ ): void {
1172
+ paint(host, flattenOperand(operand, onChange, opts), opts);
952
1173
  }