@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
@@ -1,32 +1,82 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Built-in schema-form controls (specs/extensions.md §9.1): "schema-builder" (visual JSON-Schema
4
- * field editor wrapping settings/schema-field-ui), "secret" (value committed via the host's secret
5
- * store, never project.json) and "reference" (an entry of another content collection). The fourth
6
- * built-in, "binding", registers from panels/signals-panel.ts because it owns panel-local ephemeral
7
- * UI state (custom-ref mode) and the route-param picker semantics.
3
+ * Built-in schema-form controls (specs/extensions.md §9.1): "schema-builder" (the visual
4
+ * JSON-Schema field editor), "secret" (value committed via the host's secret store, never
5
+ * project.json) and "reference" (an entry of another content collection).
6
+ *
7
+ * Three, not the four the spec still lists: `"binding"` was the signal/route-param picker, and P5
8
+ * replaced it with the value-source ladder every scalar field now carries (`ui/dynamic-slot.ts`,
9
+ * `ui/value-source.ts`). Nothing has registered it since, so a descriptor naming it falls through
10
+ * to the control its type would have had anyway.
11
+ *
12
+ * **All three are Jx documents, and the registry has one shape again.** Each is registered as a
13
+ * MOUNT (`ui/schema-form.ts`'s {@link SchemaFormMountedControl}): the form's own document announces
14
+ * an empty `[part="control-host"]` per field, a document clears the host it is given, and the flow
15
+ * for each control lives here while its markup lives beside it in `surfaces/schema-builder.json`,
16
+ * `surfaces/secret-field.json` and `surfaces/reference-field.json`. The template half of the
17
+ * registry went with the last of them — this module was its only producer, and `ui/schema-form.ts`
18
+ * imported lit for nothing else.
8
19
  *
9
20
  * Imported once for side effects from studio startup.
10
21
  */
11
22
 
12
- import { html, nothing } from "lit-html";
13
- import { repeat } from "lit-html/directives/repeat.js";
14
- import { until } from "lit-html/directives/until.js";
15
23
  import { errorMessage } from "@jxsuite/schema/parse";
16
24
  import { referenceTarget, registerFormControl } from "./schema-form";
17
25
  import {
18
- addFieldFormTpl,
26
+ FIELD_TYPES,
27
+ FORMAT_OPTIONS,
19
28
  detectFieldFormat,
20
- fieldCardTpl,
29
+ detectFieldType,
21
30
  schemaForType,
22
31
  } from "../settings/schema-field-ui";
23
- import { toCamelCase } from "../utils/studio-utils";
24
-
25
- import type { FieldHandlers, SchemaProperty } from "../settings/schema-field-ui";
26
- import type { SchemaFormControlArgs } from "./schema-form";
32
+ import { mountSchemaBuilderSurface } from "../surfaces/schema-builder";
33
+ import { mountReferenceFieldSurface } from "../surfaces/reference-field";
34
+ import { mountSecretFieldSurface } from "../surfaces/secret-field";
35
+ import { camelToLabel, toCamelCase } from "../utils/studio-utils";
36
+
37
+ import type { SchemaProperty } from "../settings/schema-field-ui";
38
+ import type {
39
+ NestedSchemaFieldView,
40
+ SchemaBuilderView,
41
+ SchemaFieldChoice,
42
+ SchemaFieldView,
43
+ } from "../surfaces/schema-builder";
44
+ import type { ReferenceChoice, ReferenceFieldView } from "../surfaces/reference-field";
45
+ import type {
46
+ SchemaFormContext,
47
+ SchemaFormControlArgs,
48
+ SchemaFormControlHandle,
49
+ SchemaFormMountedControl,
50
+ } from "./schema-form";
27
51
 
28
52
  // ─── Schema-builder control ──────────────────────────────────────────────────
29
53
 
54
+ /**
55
+ * The visual JSON-Schema field editor: `surfaces/schema-builder.json` draws it, and everything
56
+ * below decides it.
57
+ *
58
+ * It is a MOUNTED control (`ui/schema-form.ts`'s {@link SchemaFormMountedControl}) rather than a
59
+ * template one, which is the whole of this conversion: the form's own document announces an empty
60
+ * `[part="control-host"]` per field, a Jx document clears the host it is given, and the two facts
61
+ * together are what let the last lit consumer of the registry become a document. Three things the
62
+ * move settled, and each was a defect rather than a translation:
63
+ *
64
+ * - **The add rows no longer read themselves.** The nested add row found its own name field and its
65
+ * own picker with `closest()` and `querySelector` at click time — a node found by selector is
66
+ * real only until the next render — so what the reader had typed lived in the DOM and nowhere
67
+ * else, and an outside repaint took it away. Each object field carries its draft in
68
+ * {@link BuilderUi.drafts} now.
69
+ * - **A refused rename snaps back because the value moves, not because a handler writes to an
70
+ * input.** The lit card assigned `target.value = fieldName` from inside its change handler. A
71
+ * document's binding only writes when the scope value CHANGES, and after a refusal the scope
72
+ * still holds what the schema says — so every rename says what the control now holds first
73
+ * ({@link say}), and the snap-back is then a real move.
74
+ * - **A vanished field is a no-op, not an empty commit.** Every handler used to clone the value, run
75
+ * a mutation that returned early, and commit the clone anyway — so an edit to a card whose field
76
+ * had gone underneath it wrote the schema back minus nothing, and the host recorded an edit that
77
+ * was not one. A refusal now commits nothing at all.
78
+ */
79
+
30
80
  /** The JSON-Schema object shape the schema-builder edits. */
31
81
  interface BuilderSchema {
32
82
  type?: string;
@@ -34,293 +84,622 @@ interface BuilderSchema {
34
84
  required?: string[];
35
85
  }
36
86
 
37
- interface AddFieldState {
38
- format: string;
87
+ /** A draft with its three lists filled in, which is what a mutation is handed. */
88
+ interface BuilderDraft extends BuilderSchema {
89
+ properties: Record<string, SchemaProperty>;
90
+ required: string[];
91
+ }
92
+
93
+ /** One object field's unsubmitted nested field. */
94
+ interface NestedDraft {
39
95
  name: string;
40
- required: boolean;
41
96
  type: string;
42
97
  }
43
98
 
44
- const blankAddFieldState = (): AddFieldState => ({
45
- format: "",
46
- name: "",
47
- required: false,
48
- type: "string",
49
- });
99
+ /**
100
+ * What the reader is part-way through, per mounted control.
101
+ *
102
+ * Per mount rather than per module, which is what the host element buys: two panes may each be
103
+ * showing a content type's schema, and the module-level `Map` keyed on `${fieldKeyPrefix}.${key}`
104
+ * this replaced gave the second one the first one's open add form.
105
+ */
106
+ interface BuilderUi {
107
+ addOpen: boolean;
108
+ add: { format: string; name: string; required: boolean; type: string };
109
+ /** Each object field's nested add row, by the field's name in the schema. */
110
+ drafts: Map<string, NestedDraft>;
111
+ /**
112
+ * The ONE control whose value has parted from the schema, and what it holds.
113
+ *
114
+ * One at a time is the whole truth of it: a `change` commits one control, and the decision about
115
+ * it is synchronous, so there is never a second control in flight. See {@link say}.
116
+ */
117
+ echo: { id: string; value: string | boolean } | null;
118
+ }
119
+
120
+ /** The field types, as picker rows. One array, made once: an equal write re-runs no binding. */
121
+ const TYPE_ROWS: SchemaFieldChoice[] = FIELD_TYPES.map((type) => ({ label: type, value: type }));
122
+
123
+ /** The formats, with the empty one named: a blank row says nothing about what choosing it does. */
124
+ const FORMAT_ROWS: SchemaFieldChoice[] = FORMAT_OPTIONS.map((format) => ({
125
+ label: format || "(none)",
126
+ value: format,
127
+ }));
128
+
129
+ /** What a new field starts as. */
130
+ const BLANK_FIELD = { format: "", name: "", required: false, type: "string" };
50
131
 
51
- /** Per-field add-field form state, keyed by `${fieldKeyPrefix}.${key}`. */
52
- const addFieldOpen = new Set<string>();
53
- const addFieldStates = new Map<string, AddFieldState>();
132
+ /** The last target list, so an unchanged one is the same array and re-runs no binding. */
133
+ let targetRows: SchemaFieldChoice[] = [];
134
+ let targetNames = " ";
54
135
 
55
- /** Reset schema-builder ephemeral UI state (test hook). */
136
+ /**
137
+ * Forget the memoised reference-target rows (test hook).
138
+ *
139
+ * The builder's own ephemeral state — which add form is open, what each add row holds — lives with
140
+ * the mount now and goes when the control does, so this is what is left of a module-level reset.
141
+ */
56
142
  export function resetFormControlUiState(): void {
57
- addFieldOpen.clear();
58
- addFieldStates.clear();
143
+ targetRows = [];
144
+ targetNames = " ";
145
+ }
146
+
147
+ /**
148
+ * The content types a `reference` field can point at, through the host's context.
149
+ *
150
+ * The same list `settings/defs-editor.ts` reads straight off the live config: this one goes through
151
+ * `#/$context/content` because a form control is given a context and never the project.
152
+ */
153
+ function targetsFor(ctx: SchemaFormContext): SchemaFieldChoice[] {
154
+ const content = ctx.resolvePointer("#/$context/content");
155
+ const names =
156
+ content && typeof content === "object" && !Array.isArray(content) ? Object.keys(content) : [];
157
+ const id = names.join(" ");
158
+ if (id !== targetNames) {
159
+ targetNames = id;
160
+ targetRows = names.map((name) => ({ label: name, value: name }));
161
+ }
162
+ return targetRows;
163
+ }
164
+
165
+ /** Whether a type carries a format at all — only `string` and `array` do. */
166
+ function takesFormat(type: string): boolean {
167
+ return type === "string" || type === "array";
168
+ }
169
+
170
+ /** The value as a schema object: anything else is an object with no fields in it yet. */
171
+ function readSchema(value: unknown): BuilderSchema {
172
+ return value && typeof value === "object" && !Array.isArray(value)
173
+ ? (value as BuilderSchema)
174
+ : { properties: {}, required: [], type: "object" };
59
175
  }
60
176
 
61
177
  /** Working clone of the schema value — JSON round-trip, as values may be reactive proxies. */
62
- function cloneSchema(value: unknown): BuilderSchema {
63
- const base =
64
- value && typeof value === "object" && !Array.isArray(value)
65
- ? value
66
- : { properties: {}, required: [], type: "object" };
178
+ function cloneSchema(value: unknown): BuilderDraft {
67
179
  // oxlint-disable-next-line unicorn/prefer-structured-clone
68
- return JSON.parse(JSON.stringify(base)) as BuilderSchema;
180
+ const draft = JSON.parse(JSON.stringify(readSchema(value))) as BuilderSchema;
181
+ draft.type ??= "object";
182
+ draft.properties ??= {};
183
+ draft.required ??= [];
184
+ return draft as BuilderDraft;
185
+ }
186
+
187
+ /** The id of a field's name control. */
188
+ function nameId(key: string): string {
189
+ return `name:${key}`;
190
+ }
191
+
192
+ /** The id of a field's required switch. */
193
+ function requiredId(key: string): string {
194
+ return `required:${key}`;
195
+ }
196
+
197
+ /** A child is addressed by both halves, because two objects may hold the same child name. */
198
+ function nestedId(parent: string, key: string): string {
199
+ return `${parent} ${key}`;
69
200
  }
70
201
 
71
- /** Visual JSON-Schema field editor wrapping the shared schema-field-ui templates. */
72
- function schemaBuilderControl({ key, value, onChange, ctx, rerender }: SchemaFormControlArgs) {
73
- const schema =
74
- value && typeof value === "object" && !Array.isArray(value)
75
- ? (value as BuilderSchema)
76
- : ({ properties: {}, required: [], type: "object" } as BuilderSchema);
77
- const properties = schema.properties ?? {};
78
- const required = schema.required ?? [];
79
- const stateKey = `${ctx.fieldKeyPrefix ?? ""}.${key}`;
80
-
81
- const update = (mutate: (draft: BuilderSchema) => void) => {
82
- const draft = cloneSchema(value);
83
- draft.type ??= "object";
84
- draft.properties ??= {};
85
- draft.required ??= [];
86
- mutate(draft);
87
- onChange(draft);
88
- rerender?.();
202
+ /** What a control shows: what it was last told it holds, or what the schema says. */
203
+ function shown<T extends string | boolean>(ui: BuilderUi, id: string, held: T): T {
204
+ return ui.echo?.id === id ? (ui.echo.value as T) : held;
205
+ }
206
+
207
+ /** One child of an object field, as the document draws it. */
208
+ function nestedView(
209
+ ui: BuilderUi,
210
+ parent: string,
211
+ key: string,
212
+ schema: SchemaProperty,
213
+ required: boolean,
214
+ ): NestedSchemaFieldView {
215
+ const id = nestedId(parent, key);
216
+ const type = detectFieldType(schema);
217
+ return {
218
+ format: detectFieldFormat(schema),
219
+ hasFormat: takesFormat(type),
220
+ key,
221
+ name: shown(ui, nameId(id), key),
222
+ parent,
223
+ required: shown(ui, requiredId(id), required),
224
+ type,
89
225
  };
226
+ }
90
227
 
91
- // Content types available as reference targets, resolved through the host context
92
- const contentTypesValue = ctx.resolvePointer("#/$context/content");
93
- const contentTypeNames =
94
- contentTypesValue && typeof contentTypesValue === "object"
95
- ? Object.keys(contentTypesValue)
96
- : [];
97
-
98
- const handlers: FieldHandlers = {
99
- onAddNestedField: (parentName, fieldState) =>
100
- update((draft) => {
101
- const parent = draft.properties?.[parentName];
102
- const name = toCamelCase(fieldState.name);
103
- if (!parent || !name) {
104
- return;
105
- }
106
- parent.properties ??= {};
107
- parent.properties[name] = schemaForType(fieldState.type);
108
- if (fieldState.required) {
109
- parent.required ??= [];
110
- if (!parent.required.includes(name)) {
111
- parent.required.push(name);
112
- }
113
- }
114
- }),
115
- onChangeFormat: (name, format) =>
116
- update((draft) => {
117
- const prop = draft.properties?.[name];
118
- if (!prop) {
119
- return;
228
+ /** One field of the object, as the document draws it. */
229
+ function fieldView(
230
+ ui: BuilderUi,
231
+ key: string,
232
+ schema: SchemaProperty,
233
+ required: boolean,
234
+ hasTargets: boolean,
235
+ ): SchemaFieldView {
236
+ const type = detectFieldType(schema);
237
+ const nested = type === "object";
238
+ const draft = ui.drafts.get(key);
239
+ const childRequired = schema.required ?? [];
240
+ return {
241
+ children: nested
242
+ ? Object.entries(schema.properties ?? {}).map(([child, sub]) =>
243
+ nestedView(ui, key, child, sub, childRequired.includes(child)),
244
+ )
245
+ : [],
246
+ draftName: draft?.name ?? "",
247
+ draftType: draft?.type ?? "string",
248
+ /* A reference with nothing to point at draws no picker: a control whose list is empty offers
249
+ the reader no answer, which is why the lit card gated on the same thing. */
250
+ extra: type === "reference" && hasTargets ? "reference" : nested ? "nested" : "none",
251
+ format: detectFieldFormat(schema),
252
+ hasFormat: takesFormat(type),
253
+ key,
254
+ label: camelToLabel(key),
255
+ name: shown(ui, nameId(key), key),
256
+ refTarget: schema.$ref ? schema.$ref.replace(/^#\/[^/]+\//, "") : "",
257
+ required: shown(ui, requiredId(key), required),
258
+ type,
259
+ };
260
+ }
261
+
262
+ /** A property map with one key renamed, rebuilt in place so a rename never reorders the list. */
263
+ function renameKey<T>(properties: Record<string, T>, from: string, to: string): void {
264
+ const next: Record<string, T> = {};
265
+ for (const [key, value] of Object.entries(properties)) {
266
+ next[key === from ? to : key] = value;
267
+ delete properties[key];
268
+ }
269
+ Object.assign(properties, next);
270
+ }
271
+
272
+ /** Add or remove a name from a `required` list, made on first use. */
273
+ function markRequired(
274
+ holder: { required?: string[] | undefined },
275
+ key: string,
276
+ required: boolean,
277
+ ): void {
278
+ holder.required ??= [];
279
+ const at = holder.required.indexOf(key);
280
+ if (required && at === -1) {
281
+ holder.required.push(key);
282
+ }
283
+ if (!required && at !== -1) {
284
+ holder.required.splice(at, 1);
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Mount the schema-builder into the host the form drew for it.
290
+ *
291
+ * @param {HTMLElement} host - The field's `[part="control-host"]`
292
+ * @param {SchemaFormControlArgs} args - The value, the context and the commit hook
293
+ * @returns {SchemaFormControlHandle}
294
+ */
295
+ function mountSchemaBuilder(
296
+ host: HTMLElement,
297
+ args: SchemaFormControlArgs,
298
+ ): SchemaFormControlHandle {
299
+ let latest = args;
300
+ /**
301
+ * The schema this control last committed, until the host hands one back.
302
+ *
303
+ * Every host that draws this control redraws on a commit, and the redraw arrives as an `update`
304
+ * carrying the new value — but the frontmatter renderer passes no `rerender` at all, and a host
305
+ * that never came back would leave the cards showing the schema as it was before the edit. The
306
+ * commit is what is shown until the value itself moves.
307
+ */
308
+ let committed: BuilderSchema | null = null;
309
+ const ui: BuilderUi = {
310
+ add: { ...BLANK_FIELD },
311
+ addOpen: false,
312
+ drafts: new Map(),
313
+ echo: null,
314
+ };
315
+
316
+ /** What the cards are drawn from. */
317
+ const source = (): BuilderSchema => committed ?? readSchema(latest.value);
318
+
319
+ const project = (): SchemaBuilderView => {
320
+ const schema = source();
321
+ const required = schema.required ?? [];
322
+ const rows = targetsFor(latest.ctx);
323
+ return {
324
+ addFormat: ui.add.format,
325
+ addHasFormat: takesFormat(ui.add.type),
326
+ addName: ui.add.name,
327
+ addRequired: ui.add.required,
328
+ addState: ui.addOpen ? "open" : "closed",
329
+ addType: ui.add.type,
330
+ fields: Object.entries(schema.properties ?? {}).map(([key, property]) =>
331
+ fieldView(ui, key, property, required.includes(key), rows.length > 0),
332
+ ),
333
+ formatOptions: FORMAT_ROWS,
334
+ targets: rows,
335
+ typeOptions: TYPE_ROWS,
336
+ };
337
+ };
338
+
339
+ const redraw = (): void => surface.update(project());
340
+
341
+ /**
342
+ * Say what a control now holds, before anything is decided about it.
343
+ *
344
+ * This is what `live()` did for the lit card. A document's binding writes only when the SCOPE
345
+ * value changes, and after a refusal the scope still holds what the schema says — so without this
346
+ * nothing is written back and the refused text stays in the field.
347
+ */
348
+ const say = (id: string, value: string | boolean): void => {
349
+ ui.echo = { id, value };
350
+ redraw();
351
+ };
352
+
353
+ /** A refusal: every control goes back to what the schema says, and nothing is committed. */
354
+ const refuse = (): void => {
355
+ ui.echo = null;
356
+ redraw();
357
+ };
358
+
359
+ /**
360
+ * Edit the schema and commit the result.
361
+ *
362
+ * The draft is a clone, so a mutation that answers `false` costs nothing: it is a REFUSAL, and
363
+ * the host hears nothing at all. A mutation that answers `true` is what the field now holds, and
364
+ * it is shown from this moment rather than when the host gets round to redrawing.
365
+ */
366
+ const write = (mutate: (draft: BuilderDraft) => boolean): void => {
367
+ const draft = cloneSchema(source());
368
+ if (!mutate(draft)) {
369
+ refuse();
370
+ return;
371
+ }
372
+ committed = draft;
373
+ ui.echo = null;
374
+ latest.onChange(draft);
375
+ redraw();
376
+ };
377
+
378
+ /** The parent object a nested edit is about, or `undefined` when it has gone underneath. */
379
+ const parentOf = (draft: BuilderDraft, parent: string): SchemaProperty | undefined =>
380
+ draft.properties[parent];
381
+
382
+ /** One object field's add-row draft, made on first use. */
383
+ const draftOf = (parent: string): NestedDraft => {
384
+ let draft = ui.drafts.get(parent);
385
+ if (!draft) {
386
+ draft = { name: "", type: "string" };
387
+ ui.drafts.set(parent, draft);
388
+ }
389
+ return draft;
390
+ };
391
+
392
+ const surface = mountSchemaBuilderSurface(host, {
393
+ addNested(parent) {
394
+ const draft = draftOf(parent);
395
+ const name = toCamelCase(draft.name.trim());
396
+ if (!name) {
397
+ return;
398
+ }
399
+ write((next) => {
400
+ const holder = parentOf(next, parent);
401
+ if (!holder) {
402
+ return false;
120
403
  }
121
- draft.properties![name] = schemaForType(prop.type || "string", format || undefined);
122
- }),
123
- onChangeNestedFormat: (parentName, childName, format) =>
124
- update((draft) => {
125
- const parent = draft.properties?.[parentName];
126
- const prop = parent?.properties?.[childName];
127
- if (!prop) {
128
- return;
404
+ holder.properties ??= {};
405
+ holder.properties[name] = schemaForType(draft.type || "string");
406
+ /* The name clears and the type does not: adding three strings in a row is the ordinary
407
+ case, and re-choosing the type each time would be the editor asking what it just heard. */
408
+ draft.name = "";
409
+ return true;
410
+ });
411
+ },
412
+ cancelAdd() {
413
+ ui.addOpen = false;
414
+ ui.add = { ...BLANK_FIELD };
415
+ redraw();
416
+ },
417
+ confirmAdd() {
418
+ const name = toCamelCase(ui.add.name.trim());
419
+ if (!name) {
420
+ return;
421
+ }
422
+ const { format, required, type } = ui.add;
423
+ ui.addOpen = false;
424
+ ui.add = { ...BLANK_FIELD };
425
+ write((next) => {
426
+ next.properties[name] = schemaForType(type, format || undefined);
427
+ if (required) {
428
+ markRequired(next, name, true);
129
429
  }
130
- parent!.properties![childName] = schemaForType(prop.type || "string", format || undefined);
131
- }),
132
- onChangeNestedType: (parentName, childName, newType) =>
133
- update((draft) => {
134
- const parent = draft.properties?.[parentName];
135
- const prop = parent?.properties?.[childName];
136
- if (!prop) {
137
- return;
430
+ return true;
431
+ });
432
+ },
433
+ editAddFormat(value) {
434
+ ui.add.format = value;
435
+ redraw();
436
+ },
437
+ editAddName(value) {
438
+ ui.add.name = value;
439
+ redraw();
440
+ },
441
+ editAddRequired(required) {
442
+ ui.add.required = required;
443
+ redraw();
444
+ },
445
+ editAddType(value) {
446
+ ui.add.type = value;
447
+ redraw();
448
+ },
449
+ editDraftName(parent, value) {
450
+ draftOf(parent).name = value;
451
+ redraw();
452
+ },
453
+ editDraftType(parent, value) {
454
+ draftOf(parent).type = value;
455
+ redraw();
456
+ },
457
+ openAdd() {
458
+ ui.addOpen = true;
459
+ ui.add = { ...BLANK_FIELD };
460
+ redraw();
461
+ },
462
+ removeField(key) {
463
+ write((next) => {
464
+ if (!next.properties[key]) {
465
+ return false;
138
466
  }
139
- const oldFormat =
140
- newType === "string" || newType === "array" ? detectFieldFormat(prop) : undefined;
141
- parent!.properties![childName] = schemaForType(newType, oldFormat || undefined);
142
- }),
143
- onChangeRefTarget: (name, target) =>
144
- update((draft) => {
145
- if (!draft.properties?.[name]) {
146
- return;
467
+ delete next.properties[key];
468
+ next.required = next.required.filter((name) => name !== key);
469
+ return true;
470
+ });
471
+ },
472
+ removeNested(parent, key) {
473
+ write((next) => {
474
+ const holder = parentOf(next, parent);
475
+ if (!holder?.properties?.[key]) {
476
+ return false;
147
477
  }
148
- draft.properties[name] = { $ref: `#/content/${target}` };
149
- }),
150
- onChangeType: (name, newType) =>
151
- update((draft) => {
152
- const prop = draft.properties?.[name];
153
- if (!prop) {
154
- return;
478
+ delete holder.properties[key];
479
+ if (holder.required) {
480
+ holder.required = holder.required.filter((name) => name !== key);
155
481
  }
156
- const oldFormat =
157
- newType === "string" || newType === "array" ? detectFieldFormat(prop) : undefined;
158
- draft.properties![name] = schemaForType(newType, oldFormat || undefined);
159
- }),
160
- onDelete: (name) =>
161
- update((draft) => {
162
- delete draft.properties?.[name];
163
- draft.required = (draft.required ?? []).filter((r) => r !== name);
164
- }),
165
- onDeleteNested: (parentName, childName) =>
166
- update((draft) => {
167
- const parent = draft.properties?.[parentName];
168
- if (!parent?.properties) {
169
- return;
482
+ return true;
483
+ });
484
+ },
485
+ renameField(key, value) {
486
+ say(nameId(key), value);
487
+ const name = toCamelCase(value.trim());
488
+ write((next) => {
489
+ if (!next.properties[key] || !name || name === key || next.properties[name]) {
490
+ return false;
170
491
  }
171
- delete parent.properties[childName];
172
- if (parent.required) {
173
- parent.required = parent.required.filter((r) => r !== childName);
492
+ renameKey(next.properties, key, name);
493
+ next.required = next.required.map((entry) => (entry === key ? name : entry));
494
+ return true;
495
+ });
496
+ },
497
+ renameNested(parent, key, value) {
498
+ say(nameId(nestedId(parent, key)), value);
499
+ const name = toCamelCase(value.trim());
500
+ write((next) => {
501
+ const holder = parentOf(next, parent);
502
+ if (!holder?.properties?.[key] || !name || name === key || holder.properties[name]) {
503
+ return false;
174
504
  }
175
- }),
176
- onRename: (oldName, newName) =>
177
- update((draft) => {
178
- const normalized = toCamelCase(newName);
179
- if (!draft.properties || !normalized || draft.properties[normalized]) {
180
- return;
505
+ renameKey(holder.properties, key, name);
506
+ if (holder.required) {
507
+ holder.required = holder.required.map((entry) => (entry === key ? name : entry));
181
508
  }
182
- const next: Record<string, SchemaProperty> = {};
183
- for (const [k, v] of Object.entries(draft.properties)) {
184
- next[k === oldName ? normalized : k] = v;
509
+ return true;
510
+ });
511
+ },
512
+ setFormat(key, value) {
513
+ write((next) => {
514
+ const property = next.properties[key];
515
+ if (!property) {
516
+ return false;
185
517
  }
186
- draft.properties = next;
187
- draft.required = (draft.required ?? []).map((r) => (r === oldName ? normalized : r));
188
- }),
189
- onRenameNested: (parentName, oldChild, newChild) =>
190
- update((draft) => {
191
- const parent = draft.properties?.[parentName];
192
- const normalized = toCamelCase(newChild);
193
- if (!parent?.properties || !normalized || parent.properties[normalized]) {
194
- return;
518
+ next.properties[key] = schemaForType(property.type || "string", value || undefined);
519
+ return true;
520
+ });
521
+ },
522
+ setNestedFormat(parent, key, value) {
523
+ write((next) => {
524
+ const holder = parentOf(next, parent);
525
+ const property = holder?.properties?.[key];
526
+ if (!holder?.properties || !property) {
527
+ return false;
195
528
  }
196
- const next: Record<string, SchemaProperty> = {};
197
- for (const [k, v] of Object.entries(parent.properties)) {
198
- next[k === oldChild ? normalized : k] = v;
529
+ holder.properties[key] = schemaForType(property.type || "string", value || undefined);
530
+ return true;
531
+ });
532
+ },
533
+ setNestedRequired(parent, key, required) {
534
+ say(requiredId(nestedId(parent, key)), required);
535
+ write((next) => {
536
+ const holder = parentOf(next, parent);
537
+ if (!holder) {
538
+ return false;
199
539
  }
200
- parent.properties = next;
201
- if (parent.required) {
202
- parent.required = parent.required.map((r) => (r === oldChild ? normalized : r));
540
+ markRequired(holder, key, required);
541
+ return true;
542
+ });
543
+ },
544
+ setNestedType(parent, key, value) {
545
+ write((next) => {
546
+ const holder = parentOf(next, parent);
547
+ const property = holder?.properties?.[key];
548
+ if (!holder?.properties || !property) {
549
+ return false;
203
550
  }
204
- }),
205
- onToggleNestedRequired: (parentName, childName) =>
206
- update((draft) => {
207
- const parent = draft.properties?.[parentName];
208
- if (!parent) {
209
- return;
551
+ /* A format survives a move between the two types that carry one, and is dropped by any type
552
+ that does not: `schemaForType` is handed the old format only when the new type takes it. */
553
+ const format = takesFormat(value) ? detectFieldFormat(property) : undefined;
554
+ holder.properties[key] = schemaForType(value, format || undefined);
555
+ return true;
556
+ });
557
+ },
558
+ setRequired(key, required) {
559
+ say(requiredId(key), required);
560
+ write((next) => {
561
+ if (!next.properties[key]) {
562
+ return false;
210
563
  }
211
- parent.required ??= [];
212
- const idx = parent.required.indexOf(childName);
213
- if (idx === -1) {
214
- parent.required.push(childName);
215
- } else {
216
- parent.required.splice(idx, 1);
564
+ markRequired(next, key, required);
565
+ return true;
566
+ });
567
+ },
568
+ setTarget(key, value) {
569
+ write((next) => {
570
+ if (!next.properties[key]) {
571
+ return false;
217
572
  }
218
- }),
219
- onToggleRequired: (name) =>
220
- update((draft) => {
221
- draft.required ??= [];
222
- const idx = draft.required.indexOf(name);
223
- if (idx === -1) {
224
- draft.required.push(name);
225
- } else {
226
- draft.required.splice(idx, 1);
573
+ /* The pointer form is `#/content/<type>` — the same one `settings/defs-editor.ts` writes,
574
+ so a reference authored here and one authored in Data Shapes are the same value. */
575
+ next.properties[key] = { $ref: `#/content/${value}` };
576
+ return true;
577
+ });
578
+ },
579
+ setType(key, value) {
580
+ write((next) => {
581
+ const property = next.properties[key];
582
+ if (!property) {
583
+ return false;
227
584
  }
228
- }),
585
+ const format = takesFormat(value) ? detectFieldFormat(property) : undefined;
586
+ next.properties[key] = schemaForType(value, format || undefined);
587
+ return true;
588
+ });
589
+ },
590
+ });
591
+
592
+ redraw();
593
+
594
+ return {
595
+ dispose: () => surface.dispose(),
596
+ update(next) {
597
+ latest = next;
598
+ /* The host has spoken: what it says the value is wins over what this control last committed,
599
+ and any control that had parted from it is put back. */
600
+ committed = null;
601
+ ui.echo = null;
602
+ redraw();
603
+ },
229
604
  };
230
-
231
- const fieldCards = repeat(
232
- Object.entries(properties),
233
- ([name]) => name,
234
- ([name, def]) => fieldCardTpl(name, def, required.includes(name), handlers, contentTypeNames),
235
- );
236
-
237
- const addFieldState = addFieldStates.get(stateKey) ?? blankAddFieldState();
238
-
239
- return html`
240
- <div class="schema-builder">
241
- <div class="schema-field-list">${fieldCards}</div>
242
- ${
243
- addFieldOpen.has(stateKey)
244
- ? addFieldFormTpl(addFieldState, {
245
- onCancel: () => {
246
- addFieldOpen.delete(stateKey);
247
- addFieldStates.delete(stateKey);
248
- rerender?.();
249
- },
250
- onConfirm: () => {
251
- const raw = addFieldState.name.trim();
252
- const name = toCamelCase(raw);
253
- if (!name) {
254
- return;
255
- }
256
- // Close the form before update() rerenders with the committed value
257
- addFieldOpen.delete(stateKey);
258
- addFieldStates.delete(stateKey);
259
- update((draft) => {
260
- draft.properties![name] = schemaForType(
261
- addFieldState.type,
262
- addFieldState.format || undefined,
263
- );
264
- if (addFieldState.required && !draft.required!.includes(name)) {
265
- draft.required!.push(name);
266
- }
267
- });
268
- },
269
- onInput: (field, val) => {
270
- addFieldStates.set(stateKey, { ...addFieldState, [field]: val });
271
- rerender?.();
272
- },
273
- })
274
- : html`
275
- <sp-action-button
276
- size="s"
277
- quiet
278
- @click=${() => {
279
- addFieldOpen.add(stateKey);
280
- addFieldStates.set(stateKey, blankAddFieldState());
281
- rerender?.();
282
- }}
283
- >
284
- <sp-icon-add slot="icon"></sp-icon-add> Add Field
285
- </sp-action-button>
286
- `
287
- }
288
- </div>
289
- `;
290
605
  }
291
606
 
607
+ /**
608
+ * The schema-builder as the registry holds it — a MOUNT, because it is a Jx document
609
+ * (`src/surfaces/schema-builder.json`) and a document needs a host of its own rather than a
610
+ * template slot. Named rather than written inline at the registration below so a test can drive the
611
+ * control the way the engine does, without the registry growing a second public lookup for it.
612
+ */
613
+ export const schemaBuilderControl: SchemaFormMountedControl = { mount: mountSchemaBuilder };
614
+
292
615
  registerFormControl("schema-builder", schemaBuilderControl);
293
616
 
294
617
  // ─── Secret control ──────────────────────────────────────────────────────────
295
618
 
296
- // Secret VALUES commit through the host's secret store (ctx.commitSecret → platform setSecrets),
297
- // Never project.json; the field itself persists only the derived env-var NAME commitSecret
298
- // Returns. Hosts without a secrets surface leave the control disabled.
299
- registerFormControl("secret", ({ key, value, onChange, ctx, rerender }) => {
300
- const { commitSecret } = ctx;
301
- const envName = typeof value === "string" && value ? value : null;
302
- return html`<sp-textfield
303
- size="s"
304
- type="password"
305
- class="secret-field"
306
- placeholder=${envName ? `Stored as ${envName}` : "Not set"}
307
- ?disabled=${!commitSecret}
308
- @change=${async (e: Event) => {
309
- const target = e.target as HTMLInputElement;
310
- const secretValue = target.value;
311
- if (!commitSecret || !secretValue) {
619
+ /**
620
+ * The env-var name a field currently points at, or `null` when it points at nothing.
621
+ *
622
+ * The field persists a NAME — the value itself went to the platform's secret store — so a non-empty
623
+ * string here is the whole of what a screen may say about the secret.
624
+ */
625
+ function envNameOf(value: unknown): string | null {
626
+ return typeof value === "string" && value ? value : null;
627
+ }
628
+
629
+ /**
630
+ * Mount the secret field into the host the form drew for it.
631
+ *
632
+ * Secret VALUES commit through the host's secret store (`ctx.commitSecret` → the platform's
633
+ * `setSecrets`), never project.json; the field itself persists only the derived env-var NAME that
634
+ * call answers with. A host with no secrets surface leaves the control disabled, because there is
635
+ * nowhere for what the reader types to go.
636
+ *
637
+ * **The field is emptied the moment the value leaves it, not when the commit lands.** The commit is
638
+ * asynchronous and the clear is not, so ordering them the other way round would leave the
639
+ * credential on screen for the length of a platform round trip — and would leave it there for good
640
+ * if the call refused. This way a failure costs the reader their typing, which is the direction a
641
+ * mistake in a secret field should fail in.
642
+ *
643
+ * @param {HTMLElement} host - The field's `[part="control-host"]`
644
+ * @param {SchemaFormControlArgs} args - The value, the context and the commit hook
645
+ * @returns {SchemaFormControlHandle}
646
+ */
647
+ function mountSecret(host: HTMLElement, args: SchemaFormControlArgs): SchemaFormControlHandle {
648
+ let latest = args;
649
+
650
+ const redraw = (): void => {
651
+ const envName = envNameOf(latest.value);
652
+ surface.update({
653
+ disabled: !latest.ctx.commitSecret,
654
+ /* The FIELD's key, exactly as the row above shows it: `surfaces/schema-form.json` draws a
655
+ `jx-field` whose visible label is the raw property name, and the kit names a slotted
656
+ control from that label — but this control is mounted into a plain host, which a label
657
+ cannot point at. So the control carries the accessible name itself, and it is the same
658
+ string a sighted reader sees rather than a prettier one (WCAG 2.5.3). */
659
+ label: latest.key,
660
+ placeholder: envName ? `Stored as ${envName}` : "Not set",
661
+ });
662
+ };
663
+
664
+ const surface = mountSecretFieldSurface(host, {
665
+ commit(value) {
666
+ const { commitSecret } = latest.ctx;
667
+ if (!commitSecret || !value) {
312
668
  return;
313
669
  }
314
- const name = await commitSecret(key, secretValue);
315
- target.value = "";
316
- if (typeof name === "string" && name && name !== envName) {
317
- onChange(name);
318
- } else {
319
- rerender?.();
320
- }
321
- }}
322
- ></sp-textfield>`;
323
- });
670
+ surface.clear();
671
+ const { key, onChange } = latest;
672
+ const envName = envNameOf(latest.value);
673
+ void Promise.resolve(commitSecret(key, value)).then((name) => {
674
+ /* A name equal to the one the field already holds is not an edit: the same secret was
675
+ rotated in place, the store answered with the same env var, and a patch would rewrite
676
+ project.json with the bytes it already has. */
677
+ if (typeof name === "string" && name && name !== envName) {
678
+ onChange(name);
679
+ }
680
+ });
681
+ },
682
+ });
683
+
684
+ redraw();
685
+
686
+ return {
687
+ dispose: () => surface.dispose(),
688
+ update(next) {
689
+ latest = next;
690
+ redraw();
691
+ },
692
+ };
693
+ }
694
+
695
+ /**
696
+ * The secret control as the registry holds it. Named rather than written inline at the registration
697
+ * below so a test can drive it the way the engine does, without the registry growing a second
698
+ * public lookup for it.
699
+ */
700
+ export const secretControl: SchemaFormMountedControl = { mount: mountSecret };
701
+
702
+ registerFormControl("secret", secretControl);
324
703
 
325
704
  // ─── Reference control ───────────────────────────────────────────────────────
326
705
 
@@ -336,8 +715,10 @@ registerFormControl("secret", ({ key, value, onChange, ctx, rerender }) => {
336
715
  *
337
716
  * Three states, and none of them lies:
338
717
  *
339
- * - **loading** — a disabled picker saying so, replaced when the read lands (which is why the control
340
- * needs `rerender`; without it there is no second frame and "Loading…" is forever);
718
+ * - **loading** — a disabled picker saying so, replaced when the read lands. The control redraws
719
+ * ITSELF when that happens now: it used to need the host's `rerender`, and the frontmatter
720
+ * renderer passes none, so lit's `until` was standing in for a second frame the control could not
721
+ * ask for;
341
722
  * - **failed** — the current value stays EDITABLE as text, with the reason and a Retry beside it.
342
723
  * Swapping a failed read for an empty dropdown would present "no entries" and "could not find
343
724
  * out" as the same screen, which is §16.1's complaint in miniature;
@@ -352,13 +733,26 @@ const entryIdCache = new Map<string, Promise<string[]>>();
352
733
  /**
353
734
  * What the read ENDED as, per collection.
354
735
  *
355
- * Kept beside the promise so a settled collection renders synchronously. Without it every repaint
356
- * of the enclosing form hands `until` a fresh unsettled promise, `until` falls back to its
357
- * placeholder, and a picker that has had its answer for ten minutes flashes "Loading…" on every
358
- * keystroke in the field beside it.
736
+ * Kept beside the promise so a settled collection is answered without waiting. Without it every
737
+ * repaint of the enclosing form would go back to a promise, and a picker that has had its answer
738
+ * for ten minutes would flash "Loading…" on every keystroke in the field beside it.
359
739
  */
360
740
  const entryIdResult = new Map<string, { ids: string[] } | { error: string }>();
361
741
 
742
+ /**
743
+ * Who is waiting for a collection's read to land, by collection. Emptied when it does.
744
+ *
745
+ * A SET rather than a flag, and the difference is a field that never finishes loading. It was one
746
+ * boolean per collection — "a settle handler is already attached" — so the first control to ask got
747
+ * the answer and every later one was told nothing at all. That was survivable while the only asker
748
+ * was a lit template (`until` held a promise of its own) or a whole panel repainting on the first
749
+ * control's behalf; with three independently mounted documents on one collection, the second and
750
+ * third would sit on "Loading…" for the life of the pane. Repeated asks from one waiter dedupe by
751
+ * identity, and the set is dropped whole the moment the read settles, so it cannot grow past the
752
+ * length of one listing.
753
+ */
754
+ const entryIdWaiting = new Map<string, Set<() => void>>();
755
+
362
756
  /**
363
757
  * Forget cached entry ids — for one collection, or all of them.
364
758
  *
@@ -370,9 +764,11 @@ export function invalidateReferenceEntries(collection?: string): void {
370
764
  if (collection === undefined) {
371
765
  entryIdCache.clear();
372
766
  entryIdResult.clear();
767
+ entryIdWaiting.clear();
373
768
  } else {
374
769
  entryIdCache.delete(collection);
375
770
  entryIdResult.delete(collection);
771
+ entryIdWaiting.delete(collection);
376
772
  }
377
773
  }
378
774
 
@@ -392,120 +788,196 @@ function entryIdsFor(collection: string): Promise<string[]> {
392
788
  return pending;
393
789
  }
394
790
 
395
- /** Plain text editing of the reference id — the fallback when the choices cannot be listed. */
396
- function referenceTextField(current: string, onChange: (next: unknown) => void) {
397
- return html`<sp-textfield
398
- size="s"
399
- class="reference-field"
400
- .value=${current}
401
- @change=${(e: Event) => onChange((e.target as HTMLInputElement).value.trim() || undefined)}
402
- ></sp-textfield>`;
403
- }
404
-
405
- /** The picker, once the ids are in. */
406
- function referencePicker(
791
+ /**
792
+ * How listing a collection's entry ids ENDED, for a surface that draws its own picker.
793
+ *
794
+ * `null` means the read is still in flight, and `onSettled` is called when it lands — which is the
795
+ * second frame a document has to be GIVEN, since nothing repaints it on the read's behalf.
796
+ *
797
+ * Exported because two surfaces draw the choices and neither is this control: the Document Header
798
+ * card and the Navigator's Page panel project a `$ref` field as a picker row of their own
799
+ * (`panels/frontmatter-fields.ts`), and what they need is not the widget but the answer behind it.
800
+ * Taking it from here rather than calling `listCollectionEntryIds` directly is what keeps ONE cache
801
+ * and ONE invalidation: a collection listed for the Document Header card is not listed again for
802
+ * the entry editor, and {@link invalidateReferenceEntries} after an entry is created is still the
803
+ * single event that forgets it.
804
+ *
805
+ * @param {string} collection
806
+ * @param {() => void} [onSettled]
807
+ * @returns {{ ids: string[] } | { error: string } | null}
808
+ */
809
+ export function referenceEntryState(
407
810
  collection: string,
408
- ids: string[],
409
- current: string,
410
- onChange: (next: unknown) => void,
411
- ) {
412
- const dangling = current !== "" && !ids.includes(current);
413
- return html`
414
- <sp-picker
415
- size="s"
416
- class="reference-field"
417
- value=${current || "__none__"}
418
- @change=${(e: Event) => {
419
- const chosen = (e.target as HTMLInputElement).value;
420
- onChange(chosen === "__none__" ? undefined : chosen);
421
- }}
422
- >
423
- <sp-menu-item value="__none__">—</sp-menu-item>
424
- ${
425
- dangling
426
- ? html`<sp-menu-item class="reference-missing" value=${current}
427
- >${current} — not found</sp-menu-item
428
- >`
429
- : nothing
430
- }
431
- ${ids.map((id) => html`<sp-menu-item value=${id}>${id}</sp-menu-item>`)}
432
- </sp-picker>
433
- ${ids.length === 0 ? html`<span class="reference-note">No ${collection} entries yet.</span>` : nothing}
434
- `;
811
+ onSettled?: () => void,
812
+ ): { ids: string[] } | { error: string } | null {
813
+ const done = entryIdResult.get(collection);
814
+ if (done) {
815
+ return done;
816
+ }
817
+ const pending = entryIdsFor(collection);
818
+ const waiting = entryIdWaiting.get(collection);
819
+ if (waiting) {
820
+ if (onSettled) {
821
+ waiting.add(onSettled);
822
+ }
823
+ return null;
824
+ }
825
+ entryIdWaiting.set(collection, new Set(onSettled ? [onSettled] : []));
826
+ /**
827
+ * Record how the read ended and tell everyone who asked — unless this is not the read anyone is
828
+ * waiting for any more.
829
+ *
830
+ * A Retry invalidates the collection and starts a second read while the first is still in flight,
831
+ * so without the identity check the loser writes its stale answer over the winner's and notifies
832
+ * a set of waiters that belongs to the other read. An invalidated read settles into nothing,
833
+ * which is what "forget this collection" has to mean.
834
+ */
835
+ const settle = (result: { ids: string[] } | { error: string }): void => {
836
+ if (entryIdCache.get(collection) !== pending) {
837
+ return;
838
+ }
839
+ const listeners = entryIdWaiting.get(collection);
840
+ entryIdWaiting.delete(collection);
841
+ entryIdResult.set(collection, result);
842
+ for (const run of listeners ?? []) {
843
+ run();
844
+ }
845
+ };
846
+ void pending.then(
847
+ (ids) => settle({ ids }),
848
+ (error: unknown) => settle({ error: errorMessage(error) }),
849
+ );
850
+ return null;
435
851
  }
436
852
 
437
- registerFormControl("reference", ({ schema, value, onChange, rerender }: SchemaFormControlArgs) => {
438
- const collection = referenceTarget(schema);
439
- const current = typeof value === "string" ? value : "";
853
+ /**
854
+ * The view a reference field is drawn from, given what the flow knows right now.
855
+ *
856
+ * Every branch answers with a whole view rather than patching one, so the three states cannot leak
857
+ * into each other: a picker that failed to list is a TEXT field, and a text field's `options` is
858
+ * empty rather than stale.
859
+ */
860
+ function referenceView(args: SchemaFormControlArgs, onSettled: () => void): ReferenceFieldView {
861
+ const current = typeof args.value === "string" ? args.value : "";
862
+ const base = {
863
+ canRetry: false,
864
+ disabled: false,
865
+ hasNote: false,
866
+ /* The row's own visible label, verbatim — see the secret control's note on why the control
867
+ carries its accessible name rather than inheriting one from the field around it. */
868
+ label: args.key,
869
+ note: "",
870
+ noteTone: "dim",
871
+ options: [] as ReferenceChoice[],
872
+ value: current,
873
+ } satisfies Omit<ReferenceFieldView, "kind">;
874
+
875
+ const collection = referenceTarget(args.schema);
440
876
  if (collection === null) {
441
877
  /* The control was named by a `ui.control` override on a property that references nothing. The
442
878
  field is still editable — refusing to draw it would lose the value — and the note says which
443
879
  half of the declaration is missing. */
444
- return html`<div class="reference-control">
445
- ${referenceTextField(current, onChange)}
446
- <span class="reference-note"
447
- >No collection referenced — add <code>"$ref": "#/content/&lt;type&gt;"</code> to this
448
- field.</span
449
- >
450
- </div>`;
880
+ return {
881
+ ...base,
882
+ hasNote: true,
883
+ kind: "text",
884
+ note: 'No collection referenced — add "$ref": "#/content/<type>" to this field.',
885
+ };
451
886
  }
452
887
 
453
- const failedTpl = (error: string) => html`
454
- ${referenceTextField(current, onChange)}
455
- <span class="reference-note reference-note--failed"
456
- >Could not list ${collection} entries — ${error}</span
457
- >
458
- ${
459
- /* Retry only where a repaint is possible. A button that cannot do the thing it is named
460
- after is worse than its absence — the field beside it still edits the value. */
461
- rerender
462
- ? html`<sp-action-button
463
- size="s"
464
- quiet
465
- @click=${() => {
466
- invalidateReferenceEntries(collection);
467
- rerender();
468
- }}
469
- >Retry</sp-action-button
470
- >`
471
- : nothing
888
+ const state = referenceEntryState(collection, onSettled);
889
+ if (state === null) {
890
+ // Only the FIRST paint of a collection waits; a settled one is answered from the cache above.
891
+ return {
892
+ ...base,
893
+ disabled: true,
894
+ kind: "picker",
895
+ options: [{ label: "Loading…", value: current }],
896
+ };
897
+ }
898
+ if ("error" in state) {
899
+ return {
900
+ ...base,
901
+ canRetry: true,
902
+ hasNote: true,
903
+ kind: "text",
904
+ note: `Could not list ${collection} entries — ${state.error}`,
905
+ noteTone: "danger",
906
+ };
907
+ }
908
+
909
+ const dangling = current !== "" && !state.ids.includes(current);
910
+ return {
911
+ ...base,
912
+ hasNote: state.ids.length === 0,
913
+ kind: "picker",
914
+ note: state.ids.length === 0 ? `No ${collection} entries yet.` : "",
915
+ options: [
916
+ { label: "—", value: "" },
917
+ ...(dangling ? [{ label: `${current} — not found`, value: current }] : []),
918
+ ...state.ids.map((id) => ({ label: id, value: id })),
919
+ ],
920
+ };
921
+ }
922
+
923
+ /**
924
+ * Mount the reference picker into the host the form drew for it.
925
+ *
926
+ * @param {HTMLElement} host - The field's `[part="control-host"]`, or a row cell's own host
927
+ * @param {SchemaFormControlArgs} args - The value, the schema and the commit hook
928
+ * @returns {SchemaFormControlHandle}
929
+ */
930
+ function mountReference(host: HTMLElement, args: SchemaFormControlArgs): SchemaFormControlHandle {
931
+ let latest = args;
932
+ let disposed = false;
933
+
934
+ /* The read lands turns after the control asked for it, and the field may be gone by then — a
935
+ cell whose row was removed, a form the pane took down. A redraw after that would update a scope
936
+ nothing is mounted on. */
937
+ const redraw = (): void => {
938
+ if (!disposed) {
939
+ surface.update(referenceView(latest, redraw));
472
940
  }
473
- `;
941
+ };
474
942
 
475
- // Already answered: render it now. Only the FIRST paint of a collection waits.
476
- const done = entryIdResult.get(collection);
477
- if (done) {
478
- return html`<div class="reference-control">
479
- ${
480
- "ids" in done
481
- ? referencePicker(collection, done.ids, current, onChange)
482
- : failedTpl(done.error)
943
+ const surface = mountReferenceFieldSurface(host, {
944
+ choose(value) {
945
+ latest.onChange(value === "" ? undefined : value);
946
+ },
947
+ edit(value) {
948
+ latest.onChange(value.trim() || undefined);
949
+ },
950
+ retry() {
951
+ const collection = referenceTarget(latest.schema);
952
+ if (collection !== null) {
953
+ invalidateReferenceEntries(collection);
483
954
  }
484
- </div>`;
485
- }
955
+ redraw();
956
+ },
957
+ });
486
958
 
487
- /* `until` rather than a host repaint: a form control cannot ask its host to draw a second frame,
488
- and the frontmatter renderer passes no `rerender` at all — with one, this field would have said
489
- "Loading…" for the life of the panel. */
490
- const resolved = entryIdsFor(collection).then(
491
- (ids) => {
492
- entryIdResult.set(collection, { ids });
493
- return referencePicker(collection, ids, current, onChange);
959
+ redraw();
960
+
961
+ return {
962
+ dispose() {
963
+ disposed = true;
964
+ surface.dispose();
494
965
  },
495
- (error: unknown) => {
496
- const message = errorMessage(error);
497
- entryIdResult.set(collection, { error: message });
498
- return failedTpl(message);
966
+ update(next) {
967
+ latest = next;
968
+ redraw();
499
969
  },
500
- );
970
+ };
971
+ }
972
+
973
+ /**
974
+ * The reference control as the registry holds it. Named rather than written inline at the
975
+ * registration below so a test can drive it the way the engine does, without the registry growing a
976
+ * second public lookup for it.
977
+ */
978
+ export const referenceControl: SchemaFormMountedControl = { mount: mountReference };
501
979
 
502
- return html`<div class="reference-control">
503
- ${until(
504
- resolved,
505
- html`<sp-picker size="s" class="reference-field" disabled label="Loading…"></sp-picker>`,
506
- )}
507
- </div>`;
508
- });
980
+ registerFormControl("reference", referenceControl);
509
981
 
510
982
  // Keep an explicit export so hosts can assert the built-ins module loaded
511
983
  export const builtinFormControls = ["schema-builder", "secret", "reference"] as const;