@jxsuite/studio 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (607) hide show
  1. package/README.md +1 -1
  2. package/data/elements-meta.json +5 -15
  3. package/data/html-meta.json +51 -6
  4. package/dist/chunks/{2020-n6tfwnda.js → 2020-g79n01v9.js} +14 -14
  5. package/dist/chunks/action-group-sp6tna8v.js +118 -0
  6. package/dist/chunks/adapter-nj7tnjfn.js +13 -0
  7. package/dist/chunks/cf-account-picker-fyf63pmy.js +495 -0
  8. package/dist/chunks/cf-settings-71bv5zn9.js +20 -0
  9. package/dist/chunks/color-area-v17df7b8.js +154 -0
  10. package/dist/chunks/color-field-anwybnkn.js +208 -0
  11. package/dist/chunks/color-slider-44ez96fc.js +79 -0
  12. package/dist/chunks/color-xgg3kfw7.js +35 -0
  13. package/dist/chunks/combobox-5e7jev23.js +236 -0
  14. package/dist/chunks/components-2w1f35qs.js +18 -0
  15. package/dist/chunks/content-source-3wqeas45.js +46 -0
  16. package/dist/chunks/convert-to-repeater-d0cgnb6d.js +639 -0
  17. package/dist/chunks/dialog-s890b918.js +85 -0
  18. package/dist/chunks/editor-3qhbg51x.js +35 -0
  19. package/dist/chunks/entry-commands-d6g50j7s.js +42 -0
  20. package/dist/chunks/extension-sections-fy7q4fe9.js +651 -0
  21. package/dist/chunks/file-ops-3btj6st0.js +47 -0
  22. package/dist/chunks/files-ef8fgs3w.js +72 -0
  23. package/dist/chunks/format-host-3c33nwr8.js +40 -0
  24. package/dist/chunks/fs-events-13tj6sex.js +18 -0
  25. package/dist/chunks/git-panel-4wp2rn9b.js +49 -0
  26. package/dist/chunks/icons-yy10wk9r.js +402 -0
  27. package/dist/chunks/iframe-entry-15g3n6xv.js +114 -0
  28. package/dist/chunks/iframe-entry-b27vq81n.js +203 -0
  29. package/dist/chunks/iframe-entry-nbyxee1y.js +154 -0
  30. package/dist/chunks/iframe-host-jgesd924.js +115 -0
  31. package/dist/chunks/{index-wcnt3r9g.js → index-mdmyx6s2.js} +11 -11
  32. package/dist/chunks/index-n4j7qxyr.js +109 -0
  33. package/dist/chunks/{javascript-q8kpmfah.js → javascript-xvrfrpkb.js} +8 -9
  34. package/dist/chunks/{jsonMode-dkr040ga.js → jsonMode-tyx2kj6p.js} +526 -527
  35. package/dist/chunks/jx-validate-j0wxk72w.js +15 -0
  36. package/dist/chunks/layers-1nq5evsh.js +29 -0
  37. package/dist/chunks/layers-panel-ar2gh90g.js +40 -0
  38. package/dist/chunks/listbox-6v2ac08j.js +44 -0
  39. package/dist/chunks/lit-html-m244w9nh.js +17 -0
  40. package/dist/chunks/media-picker-2qbetq73.js +33 -0
  41. package/dist/chunks/menu-q99sy9rf.js +285 -0
  42. package/dist/chunks/menu-rf7n72mf.js +14 -0
  43. package/dist/chunks/{monaco-binding-rj6xpx9y.js → monaco-binding-sw6en4dw.js} +12 -15
  44. package/dist/chunks/monaco-buffer-bhy3hb6m.js +24 -0
  45. package/dist/chunks/{monaco-setup-vx9bhp1d.js → monaco-setup-8r62fj6y.js} +27 -37
  46. package/dist/chunks/notify-628t0anh.js +21 -0
  47. package/dist/chunks/number-field-5cndyft8.js +72 -0
  48. package/dist/chunks/parse-0zx14y15.js +10 -0
  49. package/dist/chunks/platform-ts9cg1a4.js +15 -0
  50. package/dist/chunks/popover-47z3ytva.js +25 -0
  51. package/dist/chunks/publish-panel-98f7sr3g.js +27 -0
  52. package/dist/chunks/quick-search-srzz83k2.js +45 -0
  53. package/dist/chunks/references-n553f0e1.js +26 -0
  54. package/dist/chunks/right-panel-rrgx2dn3.js +45 -0
  55. package/dist/chunks/section-registry-5pq32t0q.js +25 -0
  56. package/dist/chunks/select-2r16ez41.js +49 -0
  57. package/dist/chunks/split-g7nm1rgb.js +191 -0
  58. package/dist/chunks/studio-0hqbpbd0.js +62 -0
  59. package/dist/chunks/studio-18avnhjp.js +2430 -0
  60. package/dist/chunks/{studio-kryvsw6j.js → studio-1k325pwf.js} +48 -48
  61. package/dist/chunks/{studio-msd847zn.js → studio-25hkdrkw.js} +357 -86
  62. package/dist/chunks/studio-26d7myd1.js +258 -0
  63. package/dist/chunks/{studio-4r1pv6zf.js → studio-38emantv.js} +22 -5
  64. package/dist/chunks/studio-3gxv60tn.js +422 -0
  65. package/dist/chunks/{studio-fbf92ckf.js → studio-3hxkd92t.js} +275 -163
  66. package/dist/chunks/studio-4m0haseg.js +185 -0
  67. package/dist/chunks/{studio-ynxwkrzd.js → studio-4wbb9ct0.js} +1953 -1785
  68. package/dist/chunks/{studio-1wagb550.js → studio-5523bpzt.js} +11 -11
  69. package/dist/chunks/{studio-x4qkdat5.js → studio-57mf66w2.js} +1749 -2481
  70. package/dist/chunks/studio-5jy81t3s.js +17463 -0
  71. package/dist/chunks/studio-5rt7sqa0.js +1168 -0
  72. package/dist/chunks/studio-5te5tqng.js +81 -0
  73. package/dist/chunks/{studio-p4ngc046.js → studio-6ewqv390.js} +9 -9
  74. package/dist/chunks/{studio-9ab8gqsm.js → studio-6nzaktvy.js} +7 -7
  75. package/dist/chunks/studio-6xa21ej9.js +28 -0
  76. package/dist/chunks/{studio-5ehhackm.js → studio-727xh0e3.js} +19 -19
  77. package/dist/chunks/{studio-ahn3pdgx.js → studio-79y5sz51.js} +52 -38
  78. package/dist/chunks/{studio-gcye7qka.js → studio-7e11jpa9.js} +21 -21
  79. package/dist/chunks/{studio-q7v10cg2.js → studio-7r42c6ca.js} +17 -7
  80. package/dist/chunks/studio-7xptrg0j.js +954 -0
  81. package/dist/chunks/{studio-cft9r4r7.js → studio-81390n6f.js} +22 -22
  82. package/dist/chunks/{studio-q43a5y9h.js → studio-8jn5yhyn.js} +16 -16
  83. package/dist/chunks/studio-8qtrpzr3.js +839 -0
  84. package/dist/chunks/{studio-dg7zyt5x.js → studio-98b3z07n.js} +1 -1
  85. package/dist/chunks/studio-9xxz1q69.js +1356 -0
  86. package/dist/chunks/studio-akbww0j5.js +13615 -0
  87. package/dist/chunks/{studio-x813zp48.js → studio-arfpe389.js} +30 -30
  88. package/dist/chunks/{studio-trxrabqf.js → studio-az3zd49t.js} +475 -283
  89. package/dist/chunks/studio-cpvb2ed1.js +1171 -0
  90. package/dist/chunks/{studio-f3wqjtk5.js → studio-d0300636.js} +5076 -18
  91. package/dist/chunks/{studio-bgxt93ga.js → studio-d055c37e.js} +13 -13
  92. package/dist/chunks/studio-d1ja2x9r.js +362 -0
  93. package/dist/chunks/{studio-b9b6t1dg.js → studio-e1f24eff.js} +5664 -5666
  94. package/dist/chunks/{studio-3bqpr0zh.js → studio-em9nw8eh.js} +299 -110
  95. package/dist/chunks/studio-erdatzvp.js +1190 -0
  96. package/dist/chunks/{studio-g1wdche0.js → studio-esrxgwh6.js} +80 -80
  97. package/dist/chunks/studio-fabxk6r9.js +2811 -0
  98. package/dist/chunks/{studio-3n4ndrzh.js → studio-h4nvnjvz.js} +303 -303
  99. package/dist/chunks/{studio-dm19q43r.js → studio-hhvs4sdg.js} +10 -10
  100. package/dist/chunks/{studio-q9jd1a60.js → studio-kcgff4r0.js} +15 -18
  101. package/dist/chunks/{studio-685s0ytq.js → studio-nqgawfks.js} +572 -572
  102. package/dist/chunks/{studio-xrapxwwc.js → studio-p1dw8wfw.js} +55 -29
  103. package/dist/chunks/studio-pm6hgy7r.js +4763 -0
  104. package/dist/chunks/studio-q0a3yk4n.js +33 -0
  105. package/dist/chunks/{studio-wcy8trfx.js → studio-q3x06vmj.js} +23 -22
  106. package/dist/chunks/studio-qahxvnan.js +111 -0
  107. package/dist/chunks/{studio-53n8bp04.js → studio-r6xmmtfh.js} +1 -1
  108. package/dist/chunks/{studio-2b946z26.js → studio-rkstz940.js} +3658 -6134
  109. package/dist/chunks/{studio-tvgyw9xw.js → studio-rnyf3f8e.js} +83 -250
  110. package/dist/chunks/studio-rpyah2ew.js +2805 -0
  111. package/dist/chunks/studio-rs76k67r.js +12 -0
  112. package/dist/chunks/{studio-jkftxyvz.js → studio-s6z853aq.js} +5858 -2152
  113. package/dist/chunks/{studio-6yw4y3my.js → studio-sgdc5pfc.js} +8 -8
  114. package/dist/chunks/studio-tb3ev01j.js +763 -0
  115. package/dist/chunks/{studio-t5wqwzdh.js → studio-tjjk7jvk.js} +1 -1
  116. package/dist/chunks/studio-vhv5tgxm.js +51 -0
  117. package/dist/chunks/{studio-qjy2z9bv.js → studio-vz68yn9e.js} +32 -137
  118. package/dist/chunks/studio-wqpngpsk.js +174 -0
  119. package/dist/chunks/{studio-b55y6ewx.js → studio-x3rty6tr.js} +58 -58
  120. package/dist/chunks/studio-xsykpvsh.js +8448 -0
  121. package/dist/chunks/{studio-92ab2jqe.js → studio-y9zedyjn.js} +14 -14
  122. package/dist/chunks/studio-yxa81f45.js +12 -0
  123. package/dist/chunks/swatch-group-t0n6h2rn.js +139 -0
  124. package/dist/chunks/tabs-wbp1cdws.js +156 -0
  125. package/dist/chunks/textfield-abk6qyee.js +73 -0
  126. package/dist/chunks/toast-780pvfbv.js +27 -0
  127. package/dist/chunks/toast-host-3sqv46sn.js +116 -0
  128. package/dist/chunks/toolbar-xbh5bhtr.js +194 -0
  129. package/dist/chunks/tooltip-eptb614w.js +369 -0
  130. package/dist/chunks/tree-a73p0pqm.js +367 -0
  131. package/dist/chunks/{tsMode-76kr53q0.js → tsMode-jy4m86rv.js} +83 -85
  132. package/dist/chunks/workspace-wrew8zk4.js +69 -0
  133. package/dist/chunks/{ws-client-6shc5rze.js → ws-client-ny4zc51r.js} +20 -22
  134. package/dist/iframe-entry.js +1780 -667
  135. package/dist/studio.js +52819 -36480
  136. package/dist/workers/editor.worker.js +1040 -1085
  137. package/dist/workers/json.worker.js +1946 -1991
  138. package/dist/workers/ts.worker.js +4960 -5005
  139. package/index.html +1 -0
  140. package/package.json +18 -52
  141. package/src/about/about-modal.ts +72 -123
  142. package/src/browse/library-commands.ts +20 -49
  143. package/src/browse/library-layouts.ts +87 -218
  144. package/src/browse/library-pane.ts +406 -438
  145. package/src/canvas/canvas-helpers.ts +0 -49
  146. package/src/canvas/canvas-live-render.ts +25 -2
  147. package/src/canvas/canvas-render.ts +666 -231
  148. package/src/canvas/canvas-surface.ts +0 -1
  149. package/src/canvas/canvas-utils.ts +310 -117
  150. package/src/canvas/dialog-path.ts +76 -0
  151. package/src/canvas/dialog-state.ts +67 -0
  152. package/src/canvas/diff-marks.ts +252 -0
  153. package/src/canvas/diff-toolbar.ts +349 -0
  154. package/src/canvas/diff-view.ts +123 -0
  155. package/src/canvas/edit-width-drag.ts +187 -0
  156. package/src/canvas/edit-width.ts +150 -0
  157. package/src/canvas/iframe-entry.ts +125 -8
  158. package/src/canvas/iframe-host.ts +271 -15
  159. package/src/canvas/iframe-interaction.ts +133 -13
  160. package/src/canvas/iframe-patch.ts +7 -1
  161. package/src/canvas/iframe-position.ts +5 -6
  162. package/src/canvas/iframe-protocol.ts +106 -5
  163. package/src/canvas/iframe-render.ts +373 -5
  164. package/src/canvas/iframe-slash.ts +2 -2
  165. package/src/canvas/iframe-subtree.ts +5 -8
  166. package/src/canvas/path-mapping.ts +25 -3
  167. package/src/canvas/popover-path.ts +106 -0
  168. package/src/canvas/popover-state.ts +155 -0
  169. package/src/canvas/surface-registry.ts +21 -10
  170. package/src/collab/presence-chips.ts +51 -73
  171. package/src/commands/app-commands.ts +10 -0
  172. package/src/commands/budget.ts +12 -0
  173. package/src/commands/command-args.ts +369 -6
  174. package/src/commands/defaults.ts +44 -14
  175. package/src/commands/live-context.ts +11 -5
  176. package/src/commands/reference.ts +12 -2
  177. package/src/commands/registry.ts +165 -8
  178. package/src/content/entry-commands.ts +16 -23
  179. package/src/content/entry-editor.ts +144 -101
  180. package/src/editor/canvas-slash-bridge.ts +2 -2
  181. package/src/editor/context-menu.ts +49 -200
  182. package/src/editor/convert-to-component.ts +42 -82
  183. package/src/editor/convert-to-repeater.ts +132 -173
  184. package/src/editor/inline-edit.ts +11 -0
  185. package/src/editor/shortcuts.ts +65 -26
  186. package/src/editor/slash-menu.ts +150 -157
  187. package/src/files/file-ops.ts +117 -11
  188. package/src/files/files.ts +943 -660
  189. package/src/files/json-layout.ts +474 -0
  190. package/src/files/serialize-document.ts +8 -1
  191. package/src/format/convert-file.ts +16 -10
  192. package/src/format/format-host.ts +50 -1
  193. package/src/github/github-auth.ts +71 -29
  194. package/src/github/github-publish.ts +57 -65
  195. package/src/grid/cell-editors.ts +46 -4
  196. package/src/grid/cell-popovers.ts +65 -62
  197. package/src/grid/grid-open.ts +86 -64
  198. package/src/grid/grid-panel.ts +390 -469
  199. package/src/grid/redirects-grid.ts +40 -44
  200. package/src/hosting/layout.ts +2 -0
  201. package/src/i18n/i18n-commands.ts +127 -62
  202. package/src/media/media-pane.ts +83 -133
  203. package/src/new-project/add-repo-modal.ts +125 -176
  204. package/src/new-project/import-tab.ts +181 -252
  205. package/src/new-project/location-fields.ts +93 -160
  206. package/src/new-project/new-project-modal.ts +376 -367
  207. package/src/packages/jxsuite-update.ts +76 -18
  208. package/src/panels/activity-panel.ts +137 -104
  209. package/src/panels/ai-chat/chat-markdown.ts +15 -13
  210. package/src/panels/ai-chat/chat-view.ts +307 -472
  211. package/src/panels/ai-chat/composer.ts +175 -163
  212. package/src/panels/ai-chat/sessions-view.ts +31 -62
  213. package/src/panels/ai-panel.ts +210 -185
  214. package/src/panels/block-action-bar.ts +437 -643
  215. package/src/panels/bottom-dock.ts +112 -47
  216. package/src/panels/chat-panel.ts +19 -14
  217. package/src/panels/data-explorer.ts +141 -102
  218. package/src/panels/data-grid.ts +183 -150
  219. package/src/panels/dnd.ts +75 -20
  220. package/src/panels/editors.ts +26 -19
  221. package/src/panels/elements-panel.ts +243 -186
  222. package/src/panels/empty-state.ts +26 -42
  223. package/src/panels/events-panel.ts +950 -729
  224. package/src/panels/formula-workspace.ts +434 -207
  225. package/src/panels/frontmatter-fields.ts +185 -153
  226. package/src/panels/frontmatter-panel.ts +411 -187
  227. package/src/panels/git-diff-open.ts +148 -0
  228. package/src/panels/git-panel.ts +679 -605
  229. package/src/panels/head-panel.ts +617 -435
  230. package/src/panels/i18n-panel.ts +153 -133
  231. package/src/panels/imports-panel.ts +388 -414
  232. package/src/panels/jump-bar.ts +174 -147
  233. package/src/panels/layers-panel.ts +785 -671
  234. package/src/panels/left-panel.ts +166 -94
  235. package/src/panels/navigator-panels.ts +1 -1
  236. package/src/panels/pane-context.ts +529 -677
  237. package/src/panels/pane-grid.ts +265 -241
  238. package/src/panels/panel-registry.ts +15 -4
  239. package/src/panels/problems-panel.ts +177 -99
  240. package/src/panels/properties-panel.ts +1525 -966
  241. package/src/panels/provenance.ts +31 -113
  242. package/src/panels/quick-search.ts +217 -172
  243. package/src/panels/right-panel.ts +135 -169
  244. package/src/panels/seo-modal.ts +346 -167
  245. package/src/panels/settings-menu.ts +109 -646
  246. package/src/panels/settings-pane.ts +102 -86
  247. package/src/panels/signals-panel.ts +1318 -1172
  248. package/src/panels/statement-editor.ts +556 -428
  249. package/src/panels/style-panel.ts +1367 -630
  250. package/src/panels/style-utils.ts +36 -32
  251. package/src/panels/stylebook-doc.ts +30 -73
  252. package/src/panels/stylebook-layers-panel.ts +202 -109
  253. package/src/panels/stylebook-panel.ts +124 -88
  254. package/src/panels/tab-strip.ts +385 -287
  255. package/src/platforms/cloud.ts +106 -16
  256. package/src/platforms/devserver.ts +13 -0
  257. package/src/publish/deploy-checklist.ts +124 -78
  258. package/src/publish/publish-commands.ts +4 -12
  259. package/src/publish/publish-panel.ts +134 -328
  260. package/src/services/a11y-report.ts +100 -43
  261. package/src/services/ai-command-tools.ts +468 -0
  262. package/src/services/ai-session-store.ts +6 -0
  263. package/src/services/ai-system-prompt.ts +110 -18
  264. package/src/services/ai-tools.ts +28 -143
  265. package/src/services/ai-write-report.ts +174 -0
  266. package/src/services/document-assistant.ts +50 -2
  267. package/src/services/idle.ts +16 -14
  268. package/src/services/live-surfaces.ts +237 -0
  269. package/src/services/model-uri.ts +75 -0
  270. package/src/services/notify.ts +63 -6
  271. package/src/services/popover-report.ts +325 -0
  272. package/src/services/surface-registry.ts +56 -0
  273. package/src/services/tool-executor.ts +6 -0
  274. package/src/settings/contexts-section.ts +301 -299
  275. package/src/settings/contributed-section.ts +361 -222
  276. package/src/settings/css-vars-editor.ts +320 -325
  277. package/src/settings/defs-editor.ts +572 -535
  278. package/src/settings/dependencies-editor.ts +79 -107
  279. package/src/settings/extension-commands.ts +379 -0
  280. package/src/settings/extension-rows.ts +113 -0
  281. package/src/settings/extensions-section.ts +287 -0
  282. package/src/settings/general-settings.ts +130 -181
  283. package/src/settings/head-editor.ts +10 -305
  284. package/src/settings/locales-section.ts +146 -196
  285. package/src/settings/preferences-accounts.ts +1 -1
  286. package/src/settings/preferences-dialog.ts +350 -330
  287. package/src/settings/project-sections.ts +108 -181
  288. package/src/settings/schema-field-ui.ts +11 -399
  289. package/src/settings/section-registry.ts +8 -1
  290. package/src/settings/settings-document.ts +31 -29
  291. package/src/shell/tree.ts +42 -87
  292. package/src/shell.ts +48 -36
  293. package/src/studio.ts +90 -67
  294. package/src/style/token-ref.ts +0 -39
  295. package/src/surfaces/about.json +263 -0
  296. package/src/surfaces/about.ts +163 -0
  297. package/src/surfaces/add-repo.json +486 -0
  298. package/src/surfaces/add-repo.ts +236 -0
  299. package/src/surfaces/ai-chat.json +2185 -0
  300. package/src/surfaces/ai-chat.ts +549 -0
  301. package/src/surfaces/ai-credentials-form.json +332 -0
  302. package/src/surfaces/ai-credentials-form.ts +151 -0
  303. package/src/surfaces/ai-managed-connect.json +108 -0
  304. package/src/surfaces/ai-managed-connect.ts +123 -0
  305. package/src/surfaces/ai-model-picker.json +114 -0
  306. package/src/surfaces/ai-model-picker.ts +139 -0
  307. package/src/surfaces/block-action-bar.json +640 -0
  308. package/src/surfaces/block-action-bar.ts +368 -0
  309. package/src/surfaces/bottom-dock.json +154 -0
  310. package/src/surfaces/bottom-dock.ts +125 -0
  311. package/src/surfaces/canvas-stage.json +444 -0
  312. package/src/surfaces/canvas-stage.ts +253 -0
  313. package/src/surfaces/cf-account-picker.json +300 -0
  314. package/src/surfaces/cf-account-picker.ts +214 -0
  315. package/src/surfaces/commandbar.json +984 -0
  316. package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +414 -389
  317. package/src/surfaces/convert-repeater.json +309 -0
  318. package/src/surfaces/convert-repeater.ts +276 -0
  319. package/src/surfaces/data-actions.json +157 -0
  320. package/src/surfaces/data-actions.ts +115 -0
  321. package/src/surfaces/dialog.json +289 -0
  322. package/src/surfaces/dialog.ts +343 -0
  323. package/src/surfaces/diff-toolbar.json +266 -0
  324. package/src/surfaces/diff-toolbar.ts +139 -0
  325. package/src/surfaces/doc-header.json +739 -0
  326. package/src/surfaces/doc-header.ts +223 -0
  327. package/src/surfaces/empty-state.json +152 -0
  328. package/src/surfaces/empty-state.ts +173 -0
  329. package/src/surfaces/entry-editor.json +281 -0
  330. package/src/surfaces/entry-editor.ts +191 -0
  331. package/src/surfaces/expression-editor.json +977 -0
  332. package/src/surfaces/expression-editor.ts +266 -0
  333. package/src/surfaces/files-panel.json +592 -0
  334. package/src/surfaces/files-panel.ts +301 -0
  335. package/src/surfaces/formula-palette.json +364 -0
  336. package/src/surfaces/formula-palette.ts +340 -0
  337. package/src/surfaces/git-panel.json +1822 -0
  338. package/src/surfaces/git-panel.ts +272 -0
  339. package/src/surfaces/github-auth.json +112 -0
  340. package/src/surfaces/github-auth.ts +139 -0
  341. package/src/surfaces/github-publish.json +206 -0
  342. package/src/surfaces/github-publish.ts +169 -0
  343. package/src/surfaces/grid-cell.json +218 -0
  344. package/src/surfaces/grid-cell.ts +120 -0
  345. package/src/surfaces/grid-open.json +147 -0
  346. package/src/surfaces/grid-open.ts +87 -0
  347. package/src/surfaces/grid-panel.json +706 -0
  348. package/src/surfaces/grid-panel.ts +206 -0
  349. package/src/surfaces/grid-replace.json +179 -0
  350. package/src/surfaces/grid-replace.ts +89 -0
  351. package/src/surfaces/grid-views.json +446 -0
  352. package/src/surfaces/grid-views.ts +172 -0
  353. package/src/surfaces/inspector-dock.json +187 -0
  354. package/src/surfaces/inspector-dock.ts +161 -0
  355. package/src/surfaces/jump-bar.json +243 -0
  356. package/src/surfaces/jump-bar.ts +129 -0
  357. package/src/surfaces/jxsuite-update.json +146 -0
  358. package/src/surfaces/jxsuite-update.ts +149 -0
  359. package/src/surfaces/library-pane.json +1509 -0
  360. package/src/surfaces/library-pane.ts +363 -0
  361. package/src/surfaces/logic-panel.json +1949 -0
  362. package/src/surfaces/logic-panel.ts +369 -0
  363. package/src/surfaces/logic-workspace.json +685 -0
  364. package/src/surfaces/logic-workspace.ts +238 -0
  365. package/src/surfaces/media-browser.json +303 -0
  366. package/src/surfaces/media-browser.ts +145 -0
  367. package/src/surfaces/media-field.json +163 -0
  368. package/src/surfaces/media-field.ts +116 -0
  369. package/src/surfaces/media-pane.json +513 -0
  370. package/src/surfaces/media-pane.ts +197 -0
  371. package/src/surfaces/menu.json +232 -0
  372. package/src/surfaces/menu.ts +375 -0
  373. package/src/surfaces/navigator-dock.json +113 -0
  374. package/src/surfaces/navigator-dock.ts +130 -0
  375. package/src/surfaces/new-project.json +1654 -0
  376. package/src/surfaces/new-project.ts +411 -0
  377. package/src/surfaces/palette.json +535 -0
  378. package/src/surfaces/pane-context.json +1497 -0
  379. package/src/surfaces/pane-context.ts +513 -0
  380. package/src/surfaces/pane-grid.json +188 -0
  381. package/src/surfaces/pane-grid.ts +250 -0
  382. package/src/surfaces/panel-activity.json +563 -0
  383. package/src/surfaces/panel-activity.ts +187 -0
  384. package/src/surfaces/panel-data.json +141 -0
  385. package/src/surfaces/panel-data.ts +156 -0
  386. package/src/surfaces/panel-deploy-checklist.json +283 -0
  387. package/src/surfaces/panel-deploy-checklist.ts +175 -0
  388. package/src/surfaces/panel-elements.json +367 -0
  389. package/src/surfaces/panel-elements.ts +175 -0
  390. package/src/surfaces/panel-i18n.json +569 -0
  391. package/src/surfaces/panel-i18n.ts +212 -0
  392. package/src/surfaces/panel-imports.json +780 -0
  393. package/src/surfaces/panel-imports.ts +209 -0
  394. package/src/surfaces/panel-outline.json +734 -0
  395. package/src/surfaces/panel-outline.ts +326 -0
  396. package/src/surfaces/panel-page.json +1017 -0
  397. package/src/surfaces/panel-page.ts +240 -0
  398. package/src/surfaces/panel-problems.json +497 -0
  399. package/src/surfaces/panel-problems.ts +157 -0
  400. package/src/surfaces/panel-signals.json +1973 -0
  401. package/src/surfaces/panel-signals.ts +436 -0
  402. package/src/surfaces/panel-stylebook-layers.json +317 -0
  403. package/src/surfaces/panel-stylebook-layers.ts +155 -0
  404. package/src/surfaces/preferences.json +850 -0
  405. package/src/surfaces/preferences.ts +332 -0
  406. package/src/surfaces/progress-modal.json +110 -0
  407. package/src/surfaces/progress-modal.ts +150 -0
  408. package/src/surfaces/properties-panel.json +1578 -0
  409. package/src/surfaces/properties-panel.ts +337 -0
  410. package/src/surfaces/publish.json +945 -0
  411. package/src/surfaces/publish.ts +392 -0
  412. package/src/surfaces/push-plan.json +110 -0
  413. package/src/surfaces/push-plan.ts +87 -0
  414. package/src/surfaces/rail.json +255 -0
  415. package/src/surfaces/rail.ts +206 -0
  416. package/src/surfaces/reference-field.json +191 -0
  417. package/src/surfaces/reference-field.ts +168 -0
  418. package/src/surfaces/schema-builder.json +1011 -0
  419. package/src/surfaces/schema-builder.ts +181 -0
  420. package/src/surfaces/schema-form.json +1013 -0
  421. package/src/surfaces/schema-form.ts +287 -0
  422. package/src/surfaces/secret-field.json +60 -0
  423. package/src/surfaces/secret-field.ts +146 -0
  424. package/src/surfaces/seo.json +932 -0
  425. package/src/surfaces/seo.ts +334 -0
  426. package/src/surfaces/settings-contexts.json +538 -0
  427. package/src/surfaces/settings-contexts.ts +196 -0
  428. package/src/surfaces/settings-contributed.json +489 -0
  429. package/src/surfaces/settings-contributed.ts +199 -0
  430. package/src/surfaces/settings-css-vars.json +820 -0
  431. package/src/surfaces/settings-css-vars.ts +199 -0
  432. package/src/surfaces/settings-defs.json +1363 -0
  433. package/src/surfaces/settings-defs.ts +232 -0
  434. package/src/surfaces/settings-deploy.json +108 -0
  435. package/src/surfaces/settings-deploy.ts +135 -0
  436. package/src/surfaces/settings-extensions.json +486 -0
  437. package/src/surfaces/settings-extensions.ts +201 -0
  438. package/src/surfaces/settings-head.json +779 -0
  439. package/src/surfaces/settings-head.ts +397 -0
  440. package/src/surfaces/settings-locales.json +420 -0
  441. package/src/surfaces/settings-locales.ts +137 -0
  442. package/src/surfaces/settings-overview.json +423 -0
  443. package/src/surfaces/settings-overview.ts +218 -0
  444. package/src/surfaces/settings-packages.json +549 -0
  445. package/src/surfaces/settings-packages.ts +158 -0
  446. package/src/surfaces/settings-pane.json +147 -0
  447. package/src/surfaces/settings-pane.ts +150 -0
  448. package/src/surfaces/settings-rawjson.json +104 -0
  449. package/src/surfaces/settings-rawjson.ts +107 -0
  450. package/src/surfaces/shell.json +99 -0
  451. package/src/surfaces/shell.ts +30 -0
  452. package/src/surfaces/slash-menu.json +180 -0
  453. package/src/surfaces/slash-menu.ts +272 -0
  454. package/src/surfaces/statements.json +607 -0
  455. package/src/surfaces/statements.ts +238 -0
  456. package/src/surfaces/statusbar.json +94 -0
  457. package/src/{panels → surfaces}/statusbar.ts +163 -90
  458. package/src/surfaces/style-panel.json +1643 -0
  459. package/src/surfaces/style-panel.ts +338 -0
  460. package/src/surfaces/stylebook-chrome.json +122 -0
  461. package/src/surfaces/stylebook-chrome.ts +122 -0
  462. package/src/surfaces/tab-strip.json +448 -0
  463. package/src/surfaces/tab-strip.ts +200 -0
  464. package/src/surfaces/target-line.json +419 -0
  465. package/src/surfaces/target-line.ts +349 -0
  466. package/src/surfaces/toasts.json +189 -0
  467. package/src/surfaces/welcome.json +571 -0
  468. package/src/surfaces/welcome.ts +338 -0
  469. package/src/tabs/project-config.ts +71 -15
  470. package/src/tabs/tab.ts +52 -1
  471. package/src/types.ts +22 -0
  472. package/src/ui/ai-credentials-form.ts +104 -100
  473. package/src/ui/ai-managed-connect.ts +54 -36
  474. package/src/ui/ai-model-picker.ts +75 -47
  475. package/src/ui/cf-account-picker.ts +76 -141
  476. package/src/ui/color-selector.ts +80 -301
  477. package/src/ui/dynamic-slot.ts +43 -283
  478. package/src/ui/expression-editor.ts +797 -576
  479. package/src/ui/field-input.ts +8 -181
  480. package/src/ui/form-controls.ts +838 -366
  481. package/src/ui/formula-chips.ts +80 -85
  482. package/src/ui/kit.ts +33 -0
  483. package/src/ui/layers.ts +293 -593
  484. package/src/ui/media-picker.ts +203 -284
  485. package/src/ui/panel-resize.ts +251 -64
  486. package/src/ui/popover-surface.ts +153 -0
  487. package/src/ui/progress-modal.ts +22 -60
  488. package/src/ui/regions.ts +13 -7
  489. package/src/ui/schema-form.ts +906 -440
  490. package/src/ui/surface.ts +122 -0
  491. package/src/ui/unit-selector.ts +15 -131
  492. package/src/ui/value-source.ts +14 -0
  493. package/src/ui/virtual-window.ts +88 -9
  494. package/src/utils/canvas-media.ts +72 -0
  495. package/src/utils/edit-display.ts +7 -2
  496. package/src/utils/element-selectors.ts +69 -0
  497. package/src/utils/link-target.ts +2 -2
  498. package/src/utils/studio-utils.ts +0 -10
  499. package/src/view.ts +15 -0
  500. package/src/workspace/pane-derive.ts +36 -14
  501. package/src/workspace/workspace.ts +15 -6
  502. package/styles/canvas.css +72 -467
  503. package/styles/forced-colors.css +48 -82
  504. package/styles/forced-colors.json +41 -0
  505. package/styles/inspector.css +2 -1301
  506. package/styles/overlays.css +96 -2823
  507. package/styles/panels.css +6 -1423
  508. package/styles/shell-frame.css +272 -0
  509. package/styles/shell-frame.json +227 -0
  510. package/styles/shell.css +0 -1568
  511. package/styles/tokens.css +65 -136
  512. package/styles/tokens.json +84 -0
  513. package/dist/chunks/Overlay.dev-etqjwyf9.js +0 -23
  514. package/dist/chunks/adapter-jkcyhgtr.js +0 -14
  515. package/dist/chunks/cf-account-picker-en86v7x7.js +0 -163
  516. package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
  517. package/dist/chunks/components-zmx10t62.js +0 -21
  518. package/dist/chunks/content-source-48xg86wj.js +0 -49
  519. package/dist/chunks/convert-to-repeater-43tfn7cs.js +0 -268
  520. package/dist/chunks/editor-cptq5jp1.js +0 -37
  521. package/dist/chunks/entry-commands-e63f1xc0.js +0 -46
  522. package/dist/chunks/extension-sections-txv08xxg.js +0 -311
  523. package/dist/chunks/file-ops-wbh4hhsx.js +0 -48
  524. package/dist/chunks/files-3kfkwhk4.js +0 -93
  525. package/dist/chunks/flow-qbfgrpeh.js +0 -634
  526. package/dist/chunks/focus-trap.esm-9t2drp0g.js +0 -1025
  527. package/dist/chunks/focus-visible-6eg70z18.js +0 -167
  528. package/dist/chunks/format-host-w16q81tf.js +0 -43
  529. package/dist/chunks/fs-events-hrrg328g.js +0 -21
  530. package/dist/chunks/git-panel-532whr1y.js +0 -59
  531. package/dist/chunks/index-zqxmqxmh.js +0 -109
  532. package/dist/chunks/jx-validate-8er6gbxy.js +0 -19
  533. package/dist/chunks/layers-panel-07hm7g5j.js +0 -67
  534. package/dist/chunks/layers-q75tj2mf.js +0 -45
  535. package/dist/chunks/lit-html-j055mkh8.js +0 -22
  536. package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
  537. package/dist/chunks/notify-yjm79n4s.js +0 -30
  538. package/dist/chunks/parse-24d0gay3.js +0 -19
  539. package/dist/chunks/platform-ba1j1gbh.js +0 -16
  540. package/dist/chunks/publish-panel-v9st96ja.js +0 -29
  541. package/dist/chunks/quick-search-zccjp6gv.js +0 -66
  542. package/dist/chunks/references-3qx8fkyp.js +0 -29
  543. package/dist/chunks/right-panel-vm3b6kwy.js +0 -46
  544. package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
  545. package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +0 -18
  546. package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
  547. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
  548. package/dist/chunks/sp-tooltip.dev-4sastdnw.js +0 -15
  549. package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
  550. package/dist/chunks/studio-0a5qrh01.js +0 -3
  551. package/dist/chunks/studio-0bq6eh7v.js +0 -12
  552. package/dist/chunks/studio-18j3bmcz.js +0 -14
  553. package/dist/chunks/studio-1ngbejft.js +0 -181
  554. package/dist/chunks/studio-4b5z1x43.js +0 -424
  555. package/dist/chunks/studio-4n2b5gzb.js +0 -357
  556. package/dist/chunks/studio-4rzyrtjz.js +0 -426
  557. package/dist/chunks/studio-5fx3g9jg.js +0 -4325
  558. package/dist/chunks/studio-5myknm0a.js +0 -1903
  559. package/dist/chunks/studio-7afd21nw.js +0 -67
  560. package/dist/chunks/studio-7nte2hj4.js +0 -613
  561. package/dist/chunks/studio-8jpzqk5s.js +0 -128
  562. package/dist/chunks/studio-8q517dfr.js +0 -24
  563. package/dist/chunks/studio-950k0dx6.js +0 -6939
  564. package/dist/chunks/studio-9n4mrmnb.js +0 -184
  565. package/dist/chunks/studio-a0tswy0w.js +0 -58
  566. package/dist/chunks/studio-ahnzd9rg.js +0 -23
  567. package/dist/chunks/studio-cmb03bg5.js +0 -13890
  568. package/dist/chunks/studio-d1ks410h.js +0 -175
  569. package/dist/chunks/studio-ef49pnzx.js +0 -387
  570. package/dist/chunks/studio-eqq1w638.js +0 -113
  571. package/dist/chunks/studio-ew0djmhg.js +0 -341
  572. package/dist/chunks/studio-gfgb8pe6.js +0 -1652
  573. package/dist/chunks/studio-h4rxscfc.js +0 -10
  574. package/dist/chunks/studio-jc994dnr.js +0 -1066
  575. package/dist/chunks/studio-kyh9avvv.js +0 -28
  576. package/dist/chunks/studio-m3hwn7y1.js +0 -120
  577. package/dist/chunks/studio-mtfnaqs9.js +0 -659
  578. package/dist/chunks/studio-n6j9t10q.js +0 -318
  579. package/dist/chunks/studio-n8b9wqwy.js +0 -291
  580. package/dist/chunks/studio-qcy05dp8.js +0 -1743
  581. package/dist/chunks/studio-qq6jym04.js +0 -53
  582. package/dist/chunks/studio-rdx8jcxy.js +0 -83
  583. package/dist/chunks/studio-rn9ta2mz.js +0 -111
  584. package/dist/chunks/studio-t7qb9db8.js +0 -665
  585. package/dist/chunks/studio-tmc5emrw.js +0 -8
  586. package/dist/chunks/studio-z6nw2ek2.js +0 -18
  587. package/dist/chunks/studio-zczps70q.js +0 -378
  588. package/dist/chunks/studio-zqazsct2.js +0 -1823
  589. package/dist/chunks/workspace-fjx52ryv.js +0 -89
  590. package/src/panels/activity-bar.ts +0 -251
  591. package/src/panels/panel-scheduler.ts +0 -219
  592. package/src/panels/style-inputs.ts +0 -237
  593. package/src/panels/target-line.ts +0 -274
  594. package/src/panels/welcome-screen.ts +0 -346
  595. package/src/ui/button-group.ts +0 -102
  596. package/src/ui/choice-field.ts +0 -72
  597. package/src/ui/field-row.ts +0 -249
  598. package/src/ui/formula-palette.ts +0 -213
  599. package/src/ui/icons.ts +0 -52
  600. package/src/ui/jx-theme.ts +0 -130
  601. package/src/ui/spectrum.ts +0 -340
  602. package/src/ui/value-selector.ts +0 -174
  603. package/src/ui/widgets.ts +0 -172
  604. /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
  605. /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
  606. /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
  607. /package/dist/chunks/{tsMode-ef187txx.css → tsMode-hnxxb73h.css} +0 -0
@@ -1,34 +1,72 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Schema-form — reusable JSON-Schema → form rendering engine, extracted from the signals panel.
3
+ * Schema-form — the reusable JSON-Schema → form engine, and the FLOW half of it.
4
4
  *
5
- * Maps schema property types to Spectrum controls (enum → picker, boolean → checkbox,
6
- * number/integer → number-field, `json-schema` format → multiline JSON editor, array-of-objects →
7
- * multi-row inline form, other array/object → JSON text field, default → textfield). Hosts commit
8
- * edits through a single `onChange(patch)` callback; dynamic enum choices resolve through a
9
- * {@link SchemaFormContext}. Custom controls register by name via {@link registerFormControl} and
10
- * are consulted first for `ui` overrides.
5
+ * Maps schema property types to controls (enum → select, boolean → checkbox, number/integer →
6
+ * number field, `json-schema` format → JSON editor, array-of-objects → a row per item, other
7
+ * array/object → JSON text, default → text field); hosts commit edits through a single
8
+ * `onChange(patch)` callback; dynamic enum choices resolve through a {@link SchemaFormContext}.
9
+ * Custom controls register by name via {@link registerFormControl} and are consulted first for `ui`
10
+ * overrides.
11
+ *
12
+ * **This module decides; `surfaces/schema-form.json` draws.** Every question with an answer — which
13
+ * control a property gets, what its choices are, which rungs of the value ladder the position
14
+ * permits, what a keystroke is worth and whether a committed value is refused — is answered here
15
+ * and handed over as a list of already-decided rows. The document holds the markup, the ARIA and
16
+ * the style, and holds no schema knowledge at all.
17
+ *
18
+ * **Why a host element and not a template.** A Jx document CLEARS the host it is given, so a
19
+ * document and a lit template can never share a container. {@link mountSchemaForm} therefore hands
20
+ * back the element the form lives in; a caller that is still lit puts that element in its own tree
21
+ * exactly where it used to interpolate a template (studio-ui-guidelines.md §9.3, §9.4). Calling it
22
+ * again with the same `formKey` updates the standing form rather than building a second one, which
23
+ * is what keeps the caret in a field across the repaint a commit provokes.
11
24
  *
12
25
  * **Binding is the same ladder as everywhere else** (§6.6). A host that names somewhere a value can
13
26
  * come from — route params, the document's signals — gets a Value Source chip on every scalar
14
- * field, drawn by `ui/dynamic-slot.ts` and named by `ui/value-source.ts`. It replaces two things: a
15
- * private `Static value / $params/… / Custom…` picker that mounted only when the field ALREADY held
16
- * a `$ref` (so no form had a gesture for starting a binding at all), and a link button beside the
17
- * textfield that silently committed the first route param. A host that names no source — the
18
- * project settings forms — draws no chip and edits fixed values, exactly as before.
27
+ * field, named by `ui/value-source.ts` and switched by `ui/dynamic-slot.ts`'s per-field, per-rung
28
+ * memory, which is shared rather than reimplemented: only the DRAWING differs between a form row
29
+ * and an inspector row. A host that names no source — the project settings forms — draws no chip
30
+ * and edits fixed values.
31
+ *
32
+ * **A registered control is MOUNTED into a host, and that is the whole contract now.** It used to
33
+ * be two: a function returning a lit template, and beside it a `mount` for the one built-in that
34
+ * had become a Jx document. The other two followed (`surfaces/secret-field.json`,
35
+ * `surfaces/reference-field.json`), and a contract with no producers is not a contract — so the
36
+ * template half went with them, and with it the last `lit-html` import in the form engine. What a
37
+ * control is handed is the empty `[part="control-host"]` this form's own document already draws for
38
+ * its field, or the `[part="cell-control-host"]` a row cell draws; what it hands back is a standing
39
+ * mount, updated rather than rebuilt on every repaint (specs/extensions.md §9.1).
19
40
  */
20
41
 
21
- import { html, nothing } from "lit-html";
22
- import { ifDefined } from "lit-html/directives/if-defined.js";
23
- import { live } from "lit-html/directives/live.js";
24
- import { styleMap } from "lit-html/directives/style-map.js";
25
42
  import { isRef } from "@jxsuite/schema/guards";
26
- import { renderFieldRow } from "./field-row";
27
- import { renderDynamicSlot } from "./dynamic-slot";
28
- import { configFieldSchema } from "./value-source";
29
- import type { TemplateResult } from "lit-html";
43
+ import { createSchemaFormSurface } from "../surfaces/schema-form";
44
+ import { openMenu } from "../surfaces/menu";
45
+ import type { MenuHandle } from "../surfaces/menu";
46
+ import { cloneValue } from "../tabs/doc-op-apply";
47
+ import {
48
+ effectiveSlotMode,
49
+ hasStashedSlotValue,
50
+ slotModeSeed,
51
+ stashSlotValue,
52
+ switchSlotMode,
53
+ } from "./dynamic-slot";
54
+ import {
55
+ VALUE_SOURCE_HINTS,
56
+ VALUE_SOURCE_LABELS,
57
+ configFieldSchema,
58
+ slotCaps,
59
+ } from "./value-source";
60
+ import type { SlotMode } from "./value-source";
30
61
  import type { SignalOption } from "./dynamic-slot";
31
62
  import type { JsonValue } from "../types";
63
+ import type {
64
+ SchemaFormCellView,
65
+ SchemaFormFieldView,
66
+ SchemaFormOption,
67
+ SchemaFormRowView,
68
+ SchemaFormSurface,
69
+ } from "../surfaces/schema-form";
32
70
 
33
71
  /** A (possibly nested) JSON Schema node, covering both object and property level keys. */
34
72
  export interface JsonSchema {
@@ -81,12 +119,38 @@ export interface SchemaFormControlArgs {
81
119
  value: unknown;
82
120
  onChange: (next: unknown) => void;
83
121
  ctx: SchemaFormContext;
122
+ /**
123
+ * Ask the HOST to draw the form again, where it offered a way to.
124
+ *
125
+ * No built-in needs it any more, and that is the mount's doing rather than an omission: a control
126
+ * that owns its host draws its own second frame, so the `reference` picker's read landing and its
127
+ * Retry are its own business now. What it is still for is a commit that changes something OUTSIDE
128
+ * this field — a value another row's choices are derived from — which only the host can redraw.
129
+ */
84
130
  rerender?: (() => void) | undefined;
85
131
  }
86
132
 
87
- export type SchemaFormControl = (args: SchemaFormControlArgs) => TemplateResult;
133
+ /**
134
+ * A control that OWNS a host element.
135
+ *
136
+ * The registry's whole contract, and the one a Jx document can satisfy: a document clears the host
137
+ * it is given and cannot be interpolated into a lit template (specs/studio-ui-guidelines.md §9.4),
138
+ * so a control is handed the empty host this form already draws for it and keeps the mount for the
139
+ * life of the field. `mount` is called ONCE per host; every later repaint of the form reaches the
140
+ * control through {@link SchemaFormControlHandle.update} instead, which is what keeps the caret in a
141
+ * name the reader is part-way through typing when the value beside it commits.
142
+ */
143
+ export interface SchemaFormMountedControl {
144
+ mount: (host: HTMLElement, args: SchemaFormControlArgs) => SchemaFormControlHandle;
145
+ }
146
+
147
+ /** A standing mounted control: brought up to date, and taken down with the form. */
148
+ export interface SchemaFormControlHandle {
149
+ update: (args: SchemaFormControlArgs) => void;
150
+ dispose: () => void;
151
+ }
88
152
 
89
- /** Options for {@link renderForm}. */
153
+ /** Options for {@link mountSchemaForm}. */
90
154
  export interface RenderFormOptions {
91
155
  onChange: (patch: Record<string, unknown>) => void;
92
156
  context?: SchemaFormContext | undefined;
@@ -101,9 +165,8 @@ export interface RenderFormOptions {
101
165
  * Externally-produced diagnostics, keyed by property name — §7.1's inline tier, sourced.
102
166
  *
103
167
  * This is how a validator that runs over the WHOLE document reaches the one field it is about:
104
- * `jx-validate`'s `project.json` errors (until now wired only to the AI's `write_project_config`,
105
- * so a human editing the same file through Settings got no validation at all), Monaco's markers
106
- * for the same file open in the code view, and a host's own commit-time rejection.
168
+ * `jx-validate`'s `project.json` errors, Monaco's markers for the same file open in the code
169
+ * view, and a host's own commit-time rejection.
107
170
  *
108
171
  * A host message wins over {@link validateFieldValue}'s intrinsic check, because the host knows
109
172
  * things the property schema alone does not — that this enum value names a connector that was
@@ -117,26 +180,29 @@ export interface RenderFormOptions {
117
180
  * Report required-but-empty fields inline. Off by default, and that default is §7.1's rule
118
181
  * literally applied: a form the user has not touched yet has not committed anything, so painting
119
182
  * every required field red the moment it renders is telling them they got something wrong before
120
- * they did anything. Required-ness is already shown — the label carries a `*`. A host that
121
- * validates on submit turns this on for the render that follows the rejected submit.
183
+ * they did anything. Required-ness is already shown — the row carries the required mark. A host
184
+ * that validates on submit turns this on for the render that follows the rejected submit.
122
185
  */
123
186
  showRequired?: boolean | undefined;
124
187
  }
125
188
 
126
189
  // ─── Control registry ────────────────────────────────────────────────────────
127
190
 
128
- const controlRegistry = new Map<string, SchemaFormControl>();
191
+ const controlRegistry = new Map<string, SchemaFormMountedControl>();
129
192
 
130
- /** Register (or replace) a named form control. */
131
- export function registerFormControl(name: string, control: SchemaFormControl): void {
193
+ /**
194
+ * Register (or replace) a named form control: a {@link SchemaFormMountedControl} that draws into the
195
+ * host it is given.
196
+ *
197
+ * There is no lookup beside this. `getFormControl` existed to hand a TEMPLATE control to a lit
198
+ * caller that would interpolate it, and every such caller is gone — `panels/frontmatter-fields.ts`
199
+ * projects a row rather than borrowing a widget, and both frontmatter surfaces are documents. A
200
+ * control needs a HOST, which only this engine has, so the dispatch below is the only reader.
201
+ */
202
+ export function registerFormControl(name: string, control: SchemaFormMountedControl): void {
132
203
  controlRegistry.set(name, control);
133
204
  }
134
205
 
135
- /** Look up a registered form control by name. */
136
- export function getFormControl(name: string): SchemaFormControl | undefined {
137
- return controlRegistry.get(name);
138
- }
139
-
140
206
  // ─── References between collections ──────────────────────────────────────────
141
207
 
142
208
  /** `#/content/<type>` — a property whose value is the id of an entry in another collection. */
@@ -219,368 +285,10 @@ export function parseNumericField(raw: string, integer: boolean): number {
219
285
  return integer ? Math.trunc(Number(raw)) : Number(raw);
220
286
  }
221
287
 
222
- /** Plain textfield editing a `{ $ref }` value directly — the fallback when no binding control. */
223
- function refTextField(key: string, refVal: string, onChange: (next: unknown) => void) {
224
- return html`<sp-textfield
225
- size="s"
226
- label=${key}
227
- placeholder=${key}
228
- .value=${live(refVal)}
229
- @change=${(e: Event) => {
230
- const v = (e.target as HTMLInputElement).value.trim();
231
- onChange(v ? { $ref: v } : undefined);
232
- }}
233
- ></sp-textfield>`;
234
- }
235
-
236
- /**
237
- * Render a single inline field within an array-of-objects row. Dispatches by schema type: enum →
238
- * picker, boolean → switch, number → number-field, else → textfield.
239
- *
240
- * @param {string} key
241
- * @param {JsonSchema} schema
242
- * @param {unknown} value
243
- * @param {(val: unknown) => void} onChange
244
- * @param {SchemaFormContext | undefined} ctx
245
- * @param {Record<string, unknown>} [scope] - Scope for dependent enum refs (the parent form value)
246
- * @param {() => void} [rerender] - Repaint hook, threaded so an inline reference can show its
247
- * choices once they load — an async control with no way to ask for a second frame renders its
248
- * loading state forever.
249
- */
250
- export function renderInlineField(
251
- key: string,
252
- schema: JsonSchema,
253
- value: unknown,
254
- onChange: (val: unknown) => void,
255
- ctx?: SchemaFormContext,
256
- scope?: Record<string, unknown>,
257
- rerender?: () => void,
258
- ) {
259
- if (isRef(value)) {
260
- return refTextField(key, value.$ref, onChange);
261
- }
262
- const referenceControl =
263
- referenceTarget(schema) === null ? undefined : controlRegistry.get("reference");
264
- if (referenceControl) {
265
- return referenceControl({
266
- ctx: ctx ?? NULL_FORM_CONTEXT,
267
- key,
268
- onChange,
269
- rerender,
270
- schema,
271
- value,
272
- });
273
- }
274
- const enumValues = resolveFormEnum(schema.enum, ctx, scope);
275
-
276
- if (enumValues) {
277
- return html`<sp-picker
278
- size="s"
279
- label=${key}
280
- value=${value !== undefined ? String(value) : "__none__"}
281
- @change=${(e: Event) =>
282
- onChange(
283
- (e.target as HTMLInputElement).value === "__none__"
284
- ? undefined
285
- : (e.target as HTMLInputElement).value,
286
- )}
287
- >
288
- <sp-menu-item value="__none__">—</sp-menu-item>
289
- ${enumValues.map((v: string) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`)}
290
- </sp-picker>`;
291
- }
292
- if (schema.type === "boolean") {
293
- return html`<sp-switch
294
- size="s"
295
- ?checked=${Boolean(value)}
296
- @change=${(e: Event) => onChange((e.target as HTMLInputElement).checked)}
297
- >${key}</sp-switch
298
- >`;
299
- }
300
- if (schema.type === "integer" || schema.type === "number") {
301
- return html`<sp-number-field
302
- size="s"
303
- label=${key}
304
- .value=${value !== undefined ? value : nothing}
305
- step=${schema.type === "integer" ? "1" : nothing}
306
- @change=${(e: Event) => {
307
- const parsed = parseNumericField(
308
- (e.target as HTMLInputElement).value,
309
- schema.type === "integer",
310
- );
311
- onChange(Number.isNaN(parsed) ? undefined : parsed);
312
- }}
313
- ></sp-number-field>`;
314
- }
315
- return html`<sp-textfield
316
- size="s"
317
- label=${key}
318
- placeholder=${key}
319
- .value=${value ?? ""}
320
- @input=${(e: Event) => onChange((e.target as HTMLInputElement).value || undefined)}
321
- ></sp-textfield>`;
322
- }
323
-
324
- /** Render a debounced multiline JSON text field for array/object schema properties. */
325
- function renderJsonTextField(currentValue: unknown, ps: JsonSchema, commit: (v: unknown) => void) {
326
- /** @type {ReturnType<typeof setTimeout> | undefined} */
327
- let debounce: ReturnType<typeof setTimeout> | undefined;
328
- return html`<sp-textfield
329
- multiline
330
- size="s"
331
- style="min-height:40px"
332
- .value=${currentValue !== undefined ? JSON.stringify(currentValue, null, 2) : ""}
333
- placeholder=${ps.default !== undefined ? JSON.stringify(ps.default) : nothing}
334
- @input=${(e: Event) => {
335
- clearTimeout(debounce);
336
- debounce = setTimeout(() => {
337
- try {
338
- commit(JSON.parse((e.target as HTMLInputElement).value) as unknown);
339
- } catch {}
340
- }, 500);
341
- }}
342
- ></sp-textfield>`;
343
- }
344
-
345
- // ─── Per-property control dispatch ───────────────────────────────────────────
346
-
347
- /** Render the widget for one schema property, honoring registered-control overrides. */
348
- function renderPropertyControl(
349
- prop: string,
350
- ps: JsonSchema,
351
- value: Record<string, unknown>,
352
- required: Set<string>,
353
- opts: RenderFormOptions,
354
- ctx: SchemaFormContext,
355
- ): TemplateResult {
356
- const currentValue = value[prop];
357
- const commit = (next: unknown) => opts.onChange({ [prop]: next });
358
- const controlArgs: SchemaFormControlArgs = {
359
- ctx,
360
- key: prop,
361
- onChange: commit,
362
- rerender: opts.rerender,
363
- schema: ps,
364
- value: currentValue,
365
- };
366
-
367
- // Explicit ui override → consult the control registry first
368
- const overrideName = opts.ui?.[prop]?.control;
369
- if (overrideName) {
370
- const custom = controlRegistry.get(overrideName);
371
- if (custom) {
372
- return custom(controlArgs);
373
- }
374
- }
375
-
376
- /* A ref left in a form whose host named nowhere to bind: no ladder is drawn, so the pointer is
377
- edited as the string it is rather than rendered as "[object Object]" in a typed widget. */
378
- if (isRef(currentValue) && isBindableField(ps)) {
379
- return refTextField(prop, currentValue.$ref, commit);
380
- }
381
-
382
- /* A relationship to another collection (`$ref: "#/content/<type>"`) is the registered `reference`
383
- control, wherever the form is drawn — §9.2's "one picker" is this dispatch plus the single
384
- `registerFormControl("reference", …)` in `ui/form-controls.ts`. It is deliberately NOT an enum:
385
- the choices are entry files on disk, so they are read asynchronously and can be stale, and a
386
- schema `enum` is a closed set the document itself declares. When the control is not registered
387
- (a bare-Bun import of this engine), the field falls through to the plain textfield below rather
388
- than rendering nothing. */
389
- if (referenceTarget(ps) !== null) {
390
- const referenceControl = controlRegistry.get("reference");
391
- if (referenceControl) {
392
- return referenceControl(controlArgs);
393
- }
394
- }
395
-
396
- const enumValues = resolveFormEnum(opts.ui?.[prop]?.enum ?? ps.enum, ctx, value);
397
- if (enumValues) {
398
- return html`
399
- <sp-picker
400
- size="s"
401
- value=${
402
- currentValue !== undefined
403
- ? String(currentValue)
404
- : ps.default !== undefined
405
- ? String(ps.default)
406
- : "__none__"
407
- }
408
- @change=${(e: Event) =>
409
- commit(
410
- (e.target as HTMLInputElement).value === "__none__"
411
- ? undefined
412
- : (e.target as HTMLInputElement).value,
413
- )}
414
- >
415
- ${!required.has(prop) ? html`<sp-menu-item value="__none__">—</sp-menu-item>` : nothing}
416
- ${enumValues.map((val: string) => html`<sp-menu-item value=${val}>${val}</sp-menu-item>`)}
417
- </sp-picker>
418
- `;
419
- }
420
- if (ps.type === "boolean") {
421
- return html`<sp-checkbox
422
- ?checked=${currentValue ?? ps.default ?? false}
423
- @change=${(e: Event) => commit((e.target as HTMLInputElement).checked)}
424
- ></sp-checkbox>`;
425
- }
426
- if (ps.type === "integer" || ps.type === "number") {
427
- /** @type {ReturnType<typeof setTimeout> | undefined} */
428
- let debounce: ReturnType<typeof setTimeout> | undefined;
429
- return html`<sp-number-field
430
- size="s"
431
- min=${ifDefined(ps.minimum)}
432
- max=${ifDefined(ps.maximum)}
433
- step=${ps.type === "integer" ? "1" : nothing}
434
- .value=${currentValue !== undefined ? currentValue : nothing}
435
- placeholder=${ps.default != null ? String(ps.default) : nothing}
436
- @change=${(e: Event) => {
437
- clearTimeout(debounce);
438
- debounce = setTimeout(() => {
439
- const parsed = parseNumericField(
440
- (e.target as HTMLInputElement).value,
441
- ps.type === "integer",
442
- );
443
- commit(Number.isNaN(parsed) ? undefined : parsed);
444
- }, 400);
445
- }}
446
- ></sp-number-field>`;
447
- }
448
- if (ps.format === "json-schema") {
449
- const hasValue =
450
- currentValue && typeof currentValue === "object" && Object.keys(currentValue).length > 0;
451
- const cv = currentValue as Record<string, unknown>;
452
- const isSchemaRef = hasValue && cv.$ref;
453
- /** @type {ReturnType<typeof setTimeout> | undefined} */
454
- let debounce: ReturnType<typeof setTimeout> | undefined;
455
- return html`
456
- <div class="schema-param-editor">
457
- ${
458
- hasValue && !isSchemaRef && cv.properties
459
- ? html`
460
- <div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:4px">
461
- ${Object.entries(cv.properties as Record<string, Record<string, unknown>>).map(
462
- ([k, v]) => html`
463
- <span
464
- style="background:var(--bg);padding:1px 6px;border-radius:var(--radius);font-size:10px;color:var(--fg-dim)"
465
- >${k}: ${v.type ?? "any"}</span
466
- >
467
- `,
468
- )}
469
- </div>
470
- `
471
- : nothing
472
- }
473
- <sp-textfield
474
- multiline
475
- size="s"
476
- style=${styleMap({
477
- fontFamily: "monospace",
478
- fontSize: "11px",
479
- minHeight: hasValue ? "80px" : "40px",
480
- })}
481
- .value=${currentValue !== undefined ? JSON.stringify(currentValue, null, 2) : ""}
482
- placeholder=${ps.description ?? "JSON Schema defining the data shape…"}
483
- @input=${(e: Event) => {
484
- clearTimeout(debounce);
485
- debounce = setTimeout(() => {
486
- try {
487
- commit(JSON.parse((e.target as HTMLInputElement).value) as unknown);
488
- } catch {}
489
- }, 500);
490
- }}
491
- ></sp-textfield>
492
- </div>
493
- `;
494
- }
495
- if (ps.type === "array" && ps.items?.type === "object" && ps.items?.properties) {
496
- // Array of objects with defined schema → multi-row inline form
497
- const rows: Record<string, unknown>[] = Array.isArray(currentValue)
498
- ? (currentValue as Record<string, unknown>[])
499
- : [];
500
- const itemProps = ps.items.properties;
501
- return html`
502
- <div class="array-object-field">
503
- ${rows.map(
504
- (row: Record<string, unknown>, idx: number) => html`
505
- <div
506
- class="array-object-row"
507
- style="display:flex;gap:4px;align-items:center;margin-bottom:4px"
508
- >
509
- ${Object.entries(itemProps).map(([propKey, propSchema]) =>
510
- renderInlineField(
511
- propKey,
512
- propSchema,
513
- row[propKey],
514
- (val) => {
515
- const updated = [...rows];
516
- updated[idx] = { ...updated[idx], [propKey]: val };
517
- commit(updated);
518
- },
519
- ctx,
520
- value,
521
- opts.rerender,
522
- ),
523
- )}
524
- <sp-action-button
525
- quiet
526
- size="s"
527
- @click=${() => {
528
- const updated = rows.filter((_: unknown, i: number) => i !== idx);
529
- commit(updated.length > 0 ? updated : undefined);
530
- opts.rerender?.();
531
- }}
532
- >
533
- <sp-icon-delete slot="icon"></sp-icon-delete>
534
- </sp-action-button>
535
- </div>
536
- `,
537
- )}
538
- <sp-action-button
539
- quiet
540
- size="s"
541
- @click=${(e: Event) => {
542
- e.stopPropagation();
543
- const newRow: Record<string, unknown> = {};
544
- for (const [k, v] of Object.entries(itemProps)) {
545
- if (v.default !== undefined) {
546
- newRow[k] = v.default;
547
- }
548
- }
549
- commit([...rows, newRow]);
550
- opts.rerender?.();
551
- }}
552
- >+ Add</sp-action-button
553
- >
554
- </div>
555
- `;
556
- }
557
- if (ps.type === "array" || ps.type === "object") {
558
- return renderJsonTextField(currentValue, ps, commit) as TemplateResult;
559
- }
560
-
561
- /** @type {ReturnType<typeof setTimeout> | undefined} */
562
- let debounce: ReturnType<typeof setTimeout> | undefined;
563
- const ph = ps.default !== undefined ? String(ps.default) : (ps.examples?.[0] ?? "");
564
- return html`<sp-textfield
565
- size="s"
566
- style="flex:1"
567
- .value=${currentValue ?? ""}
568
- placeholder=${ph || nothing}
569
- title=${ps.description || nothing}
570
- @input=${(e: Event) => {
571
- clearTimeout(debounce);
572
- debounce = setTimeout(() => commit((e.target as HTMLInputElement).value || undefined), 400);
573
- }}
574
- ></sp-textfield>`;
575
- }
576
-
577
- // ─── The Value Source ladder (§6.6) ──────────────────────────────────────────
578
-
579
288
  /**
580
289
  * Whether a property is edited as a single value at all. The three that are not — a nested JSON
581
- * Schema, an object and an array — are edited as raw JSON or as a multi-row sub-form, and a chip
582
- * offering to replace that editor with a signal pointer would be offering to delete the user's
583
- * work.
290
+ * Schema, an object and an array — are edited as raw JSON or as a row per item, and a chip offering
291
+ * to replace that editor with a signal pointer would be offering to delete the user's work.
584
292
  */
585
293
  function isBindableField(ps: JsonSchema): boolean {
586
294
  return ps.format !== "json-schema" && ps.type !== "object" && ps.type !== "array";
@@ -594,6 +302,17 @@ function refSourcesFor(ctx: SchemaFormContext): SignalOption[] {
594
302
  ];
595
303
  }
596
304
 
305
+ /** The empty row's value in a select — an option list cannot offer "absent" as a value. */
306
+ const NONE = "__none__";
307
+
308
+ /** A value as a control reads it: a string, and never `"[object Object]"`. */
309
+ function asText(value: unknown): string {
310
+ if (value === undefined || value === null) {
311
+ return "";
312
+ }
313
+ return typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
314
+ }
315
+
597
316
  // ─── Field validation (§7.1, inline tier) ────────────────────────────────────
598
317
 
599
318
  /**
@@ -655,60 +374,807 @@ export function validateFieldValue(
655
374
  return "";
656
375
  }
657
376
 
658
- // ─── Form rendering ──────────────────────────────────────────────────────────
377
+ // ─── What one property turned out to be ──────────────────────────────────────
378
+
379
+ /**
380
+ * How this module reads a gesture back. The document reports `commit("port", "8080")` and nothing
381
+ * else; everything needed to turn that into a patch was decided when the row was derived, so the
382
+ * plan is where it is kept rather than re-derived from the schema on every keystroke.
383
+ */
384
+ interface FieldPlan {
385
+ prop: string;
386
+ schema: JsonSchema;
387
+ /** How a committed string becomes a document value. */
388
+ commit: "text" | "ref" | "template" | "select" | "number" | "json" | "none";
389
+ /** Commit a keystroke after this long; `0` means only on change. */
390
+ debounceMs: number;
391
+ /** Item-property schemas, for an array-of-objects field. */
392
+ itemProps?: Record<string, JsonSchema> | undefined;
393
+ /** The rows currently held, so a cell edit can rebuild the array. */
394
+ rows?: Record<string, unknown>[] | undefined;
395
+ /** The ladder, when this position offers one. */
396
+ ladder?: { fieldKey: string; mode: SlotMode; offered: SlotMode[]; sources: SignalOption[] };
397
+ /** A registered control owns the whole field; args are rebuilt for it on every repaint. */
398
+ control?: SchemaFormMountedControl | undefined;
399
+ /**
400
+ * Registered controls owning one cell, keyed by the cell's host id.
401
+ *
402
+ * The same kind of entry a field's own control is, and reached by the same dispatch: a cell host
403
+ * is a host like any other. The one control a cell is ever given is `reference` (see
404
+ * {@link deriveCell}).
405
+ */
406
+ cellControls?: Map<
407
+ string,
408
+ { control: SchemaFormMountedControl; schema: JsonSchema; row: string; key: string }
409
+ >;
410
+ /**
411
+ * Cells that currently hold a pointer, by cell id.
412
+ *
413
+ * A cell edits the pointer STRING it is showing, so what it commits has to go back as `{ $ref }`
414
+ * — which the item property's declared type cannot say, because the property is typed as what the
415
+ * pointer resolves to.
416
+ */
417
+ refCells?: Set<string>;
418
+ }
419
+
420
+ /** The debounce a JSON editor gets: long, because a half-typed object is not a refusal. */
421
+ const JSON_COMMIT_MS = 500;
422
+ /** The debounce a scalar gets, so the canvas follows a value being typed. */
423
+ const TEXT_COMMIT_MS = 400;
424
+
425
+ // ─── The controller ──────────────────────────────────────────────────────────
426
+
427
+ interface FormController {
428
+ readonly host: HTMLElement;
429
+ readonly update: (
430
+ schema: JsonSchema,
431
+ value: Record<string, unknown>,
432
+ opts: RenderFormOptions,
433
+ ) => void;
434
+ readonly dispose: () => void;
435
+ }
436
+
437
+ /** Every standing form, by the key its caller named it with. */
438
+ const forms = new Map<string, FormController>();
439
+
440
+ /** Test hook: take every standing form down, so one test's mount never outlives it. */
441
+ export function resetSchemaForms(): void {
442
+ for (const controller of forms.values()) {
443
+ controller.dispose();
444
+ }
445
+ forms.clear();
446
+ }
659
447
 
660
448
  /**
661
- * Render form field rows for a JSON Schema's `properties`, committing edits through `opts.onChange`
662
- * as single-key patches (`undefined` values mean "unset the key").
449
+ * Mount (or update) a schema-driven form and hand back the element it lives in.
663
450
  *
451
+ * @param {string} formKey Stable identity for this form — the same key updates the standing mount
452
+ * instead of building a second one, which is what keeps the caret in a field across a repaint.
664
453
  * @param {JsonSchema} schema
665
- * @param {Record<string, unknown>} value - The record being edited
454
+ * @param {Record<string, unknown>} value The record being edited
666
455
  * @param {RenderFormOptions} opts
667
- * @returns {TemplateResult}
456
+ * @returns {HTMLElement} The form's own host, for a lit caller to place in its tree
668
457
  */
669
- export function renderForm(
458
+ export function mountSchemaForm(
459
+ formKey: string,
670
460
  schema: JsonSchema,
671
461
  value: Record<string, unknown>,
672
462
  opts: RenderFormOptions,
673
- ): TemplateResult {
674
- const required = new Set(schema.required);
675
- const ctx = opts.context ?? NULL_FORM_CONTEXT;
676
- const refSources = refSourcesFor(ctx);
677
-
678
- const propertyFields = Object.entries(schema.properties ?? {}).map(([prop, ps]) => {
679
- const labelText = prop + (required.has(prop) ? " *" : "");
680
- // Host diagnostics win over the intrinsic check — see RenderFormOptions.errors.
681
- const error =
682
- opts.errors?.[prop] ||
683
- validateFieldValue(ps, value[prop], Boolean(opts.showRequired) && required.has(prop));
684
- const count = opts.errorCounts?.[prop];
685
- const staticWidget = renderPropertyControl(prop, ps, value, required, opts, ctx);
686
- /* A `ui.control` override owns its whole field — the secret control writes an env-var NAME
687
- rather than the value it was handed, so a rung switch above it would be editing a different
688
- thing than the one on screen. */
689
- const laddered =
690
- refSources.length > 0 && isBindableField(ps) && !opts.ui?.[prop]?.control
691
- ? renderDynamicSlot({
692
- allowCustomRef: true,
693
- caps: { schema: configFieldSchema(ps) },
694
- extraSignals: refSources,
695
- fieldKey: `${ctx.fieldKeyPrefix ?? ""}.${prop}`,
696
- onChange: (v?: JsonValue) => opts.onChange({ [prop]: v }),
697
- staticWidget,
698
- stateDefs: [],
699
- value: value[prop],
700
- })
701
- : null;
702
- return renderFieldRow({
703
- hasValue: false,
704
- label: labelText,
705
- prop: ps.name || prop,
706
- widget: laddered ? laddered.widget : staticWidget,
707
- ...(laddered ? { labelExtra: laddered.modeButton } : {}),
708
- ...(error ? { error } : {}),
709
- ...(count === undefined ? {} : { errorCount: count }),
463
+ ): HTMLElement {
464
+ sweep(formKey);
465
+ let controller = forms.get(formKey);
466
+ if (!controller) {
467
+ controller = createController();
468
+ forms.set(formKey, controller);
469
+ }
470
+ controller.update(schema, value, opts);
471
+ return controller.host;
472
+ }
473
+
474
+ /**
475
+ * Drop forms whose host has been taken out of the page.
476
+ *
477
+ * A host is created here and inserted by its caller one lit render later, so "not connected" is the
478
+ * normal state of the form being mounted right now — which is why the key being asked for is exempt
479
+ * rather than the sweep being conditional on a count.
480
+ */
481
+ function sweep(exceptKey: string): void {
482
+ for (const [key, controller] of forms) {
483
+ if (key !== exceptKey && !controller.host.isConnected) {
484
+ controller.dispose();
485
+ forms.delete(key);
486
+ }
487
+ }
488
+ }
489
+
490
+ function createController(): FormController {
491
+ let schema: JsonSchema = {};
492
+ let value: Record<string, unknown> = {};
493
+ let opts: RenderFormOptions = { onChange: () => {} };
494
+ let ctx: SchemaFormContext = NULL_FORM_CONTEXT;
495
+ const plans = new Map<string, FieldPlan>();
496
+ const timers = new Map<string, ReturnType<typeof setTimeout>>();
497
+ const controlHosts = new Map<string, HTMLElement>();
498
+ /** Standing mounted controls, by host id, with the element each was mounted into. */
499
+ const mountedControls = new Map<string, { host: HTMLElement; handle: SchemaFormControlHandle }>();
500
+ /** The rung picker, while one is up. One at a time, and it goes down with the form. */
501
+ let sourceMenu: MenuHandle | null = null;
502
+
503
+ const later = (key: string, ms: number, run: () => void): void => {
504
+ clearTimeout(timers.get(key));
505
+ if (ms <= 0) {
506
+ run();
507
+ return;
508
+ }
509
+ timers.set(key, setTimeout(run, ms));
510
+ };
511
+ const now = (key: string, run: () => void): void => {
512
+ clearTimeout(timers.get(key));
513
+ timers.delete(key);
514
+ run();
515
+ };
516
+
517
+ const patch = (prop: string, next: unknown): void => {
518
+ opts.onChange({ [prop]: next });
519
+ };
520
+
521
+ /** Turn a committed string into the document value this plan says it is. */
522
+ const write = (plan: FieldPlan, raw: string): void => {
523
+ if (plan.commit === "none") {
524
+ // The value is not a string this module writes: a control or a row editor owns it.
525
+ return;
526
+ }
527
+ switch (plan.commit) {
528
+ case "ref": {
529
+ const trimmed = raw.trim();
530
+ patch(plan.prop, trimmed ? { $ref: trimmed } : undefined);
531
+ return;
532
+ }
533
+ case "template": {
534
+ patch(plan.prop, raw);
535
+ return;
536
+ }
537
+ case "select": {
538
+ patch(plan.prop, raw === NONE ? undefined : raw);
539
+ return;
540
+ }
541
+ case "number": {
542
+ const parsed = parseNumericField(raw, plan.schema.type === "integer");
543
+ patch(plan.prop, Number.isNaN(parsed) ? undefined : parsed);
544
+ return;
545
+ }
546
+ case "json": {
547
+ try {
548
+ patch(plan.prop, JSON.parse(raw) as unknown);
549
+ } catch {
550
+ /* A half-typed object is not a refusal: the last parsable text is what the document
551
+ keeps, and the field goes on showing what is actually in it. */
552
+ }
553
+ return;
554
+ }
555
+ default: {
556
+ patch(plan.prop, raw || undefined);
557
+ }
558
+ }
559
+ };
560
+
561
+ /** The field's items, each shallow-copied so a cell edit never writes through to the document. */
562
+ const cellRows = (plan: FieldPlan): Record<string, unknown>[] => {
563
+ const copies: Record<string, unknown>[] = [];
564
+ for (const item of plan.rows ?? []) {
565
+ copies.push({ ...item });
566
+ }
567
+ return copies;
568
+ };
569
+
570
+ const actions = {
571
+ addRow: (field: string) => {
572
+ const plan = plans.get(field);
573
+ if (!plan?.itemProps) {
574
+ return;
575
+ }
576
+ const seed: Record<string, unknown> = {};
577
+ for (const [k, v] of Object.entries(plan.itemProps)) {
578
+ if (v.default !== undefined) {
579
+ seed[k] = v.default;
580
+ }
581
+ }
582
+ patch(field, [...cellRows(plan), seed]);
583
+ opts.rerender?.();
584
+ },
585
+ commit: (key: string, raw: string) => {
586
+ const plan = plans.get(key);
587
+ if (plan) {
588
+ now(key, () => write(plan, raw));
589
+ }
590
+ },
591
+ commitChecked: (key: string, checked: boolean) => {
592
+ const plan = plans.get(key);
593
+ if (plan) {
594
+ patch(plan.prop, checked);
595
+ }
596
+ },
597
+ edit: (key: string, raw: string) => {
598
+ const plan = plans.get(key);
599
+ if (plan && plan.debounceMs > 0) {
600
+ later(key, plan.debounceMs, () => write(plan, raw));
601
+ }
602
+ },
603
+ editCell: (field: string, row: string, cell: string, raw: string) => {
604
+ writeCell(field, row, cell, raw);
605
+ },
606
+ editCellChecked: (field: string, row: string, cell: string, checked: boolean) => {
607
+ writeCell(field, row, cell, checked);
608
+ },
609
+ editJson: (key: string, raw: string) => {
610
+ const plan = plans.get(key);
611
+ if (plan) {
612
+ later(key, JSON_COMMIT_MS, () => write(plan, raw));
613
+ }
614
+ },
615
+ pickSource: (key: string, anchor: HTMLElement) => {
616
+ openSourceMenu(key, anchor);
617
+ },
618
+ removeRow: (field: string, row: string) => {
619
+ const plan = plans.get(field);
620
+ if (!plan) {
621
+ return;
622
+ }
623
+ const idx = Number(row);
624
+ const kept = cellRows(plan).filter((_, i) => i !== idx);
625
+ patch(field, kept.length > 0 ? kept : undefined);
626
+ opts.rerender?.();
627
+ },
628
+ };
629
+
630
+ function writeCell(field: string, row: string, cell: string, next: unknown): void {
631
+ const plan = plans.get(field);
632
+ if (!plan?.itemProps) {
633
+ return;
634
+ }
635
+ const idx = Number(row);
636
+ const rows = cellRows(plan);
637
+ const target = rows[idx];
638
+ if (!target) {
639
+ return;
640
+ }
641
+ const cellSchema = plan.itemProps[cell];
642
+ rows[idx] = {
643
+ ...target,
644
+ [cell]: coerceCell(cellSchema, next, plan.refCells?.has(`${field}/${row}/${cell}`) ?? false),
645
+ };
646
+ patch(field, rows);
647
+ }
648
+
649
+ /** A cell's committed string, as the item property's declared type. */
650
+ function coerceCell(
651
+ cellSchema: JsonSchema | undefined,
652
+ next: unknown,
653
+ isPointer: boolean,
654
+ ): unknown {
655
+ if (typeof next === "boolean") {
656
+ return next;
657
+ }
658
+ const raw = String(next);
659
+ if (isPointer) {
660
+ const trimmed = raw.trim();
661
+ return trimmed ? { $ref: trimmed } : undefined;
662
+ }
663
+ if (cellSchema?.type === "integer" || cellSchema?.type === "number") {
664
+ const parsed = parseNumericField(raw, cellSchema.type === "integer");
665
+ return Number.isNaN(parsed) ? undefined : parsed;
666
+ }
667
+ if (raw === NONE || raw === "") {
668
+ return undefined;
669
+ }
670
+ return raw;
671
+ }
672
+
673
+ /** The rung picker: every source this position permits, one action away (§6.3). */
674
+ function openSourceMenu(key: string, anchor: HTMLElement): void {
675
+ const plan = plans.get(key);
676
+ if (!plan?.ladder) {
677
+ return;
678
+ }
679
+ const { fieldKey, mode, offered, sources } = plan.ladder;
680
+ sourceMenu?.close();
681
+ sourceMenu = openMenu({
682
+ label: "Value source",
683
+ onClosed: () => {
684
+ sourceMenu = null;
685
+ },
686
+ opener: anchor,
687
+ region: "value-source",
688
+ rows: offered.map((rung) => ({
689
+ checked: (rung === mode ? "true" : "false") as "true" | "false",
690
+ destructive: false,
691
+ disabled: false,
692
+ dividerAbove: false,
693
+ id: rung,
694
+ title: VALUE_SOURCE_LABELS[rung],
695
+ })),
696
+ run: (id) => {
697
+ const next = id as SlotMode;
698
+ if (next === mode) {
699
+ return;
700
+ }
701
+ /* Leaving From data… seeds an empty Fixed value stash with the property's declared default,
702
+ so unbind-restores-default survives a detour through Mixed text. */
703
+ const literalDefault = plan.schema.default as JsonValue | undefined;
704
+ if (
705
+ mode === "ref" &&
706
+ literalDefault !== undefined &&
707
+ !hasStashedSlotValue(fieldKey, "literal")
708
+ ) {
709
+ stashSlotValue(fieldKey, "literal", literalDefault);
710
+ }
711
+ patch(
712
+ plan.prop,
713
+ switchSlotMode(
714
+ fieldKey,
715
+ mode,
716
+ next,
717
+ cloneValue(value[plan.prop] as JsonValue | undefined),
718
+ slotModeSeed(next, { extraSignals: sources, literalDefault, stateDefs: [] }),
719
+ ),
720
+ );
721
+ },
710
722
  });
711
- });
723
+ }
724
+
725
+ /**
726
+ * The control a host id names, and the arguments it is drawn from right now.
727
+ *
728
+ * One answer for both kinds of host, because a cell host is a host: a field's id is its property
729
+ * and a cell's is its `field/row/property` triple, and what differs between them is only where
730
+ * the value comes from and where a commit goes.
731
+ */
732
+ function controlFor(
733
+ id: string,
734
+ ): { control: SchemaFormMountedControl; args: SchemaFormControlArgs } | undefined {
735
+ const plan = plans.get(id);
736
+ if (plan?.control) {
737
+ return {
738
+ args: {
739
+ ctx,
740
+ key: plan.prop,
741
+ onChange: (next) => patch(plan.prop, next),
742
+ rerender: opts.rerender,
743
+ schema: plan.schema,
744
+ value: value[plan.prop],
745
+ },
746
+ control: plan.control,
747
+ };
748
+ }
749
+ for (const owner of plans.values()) {
750
+ const entry = owner.cellControls?.get(id);
751
+ if (!entry) {
752
+ continue;
753
+ }
754
+ const row = (owner.rows ?? [])[Number(entry.row)] ?? {};
755
+ return {
756
+ args: {
757
+ ctx,
758
+ key: entry.key,
759
+ onChange: (next) => writeCell(owner.prop, entry.row, entry.key, next),
760
+ rerender: opts.rerender,
761
+ schema: entry.schema,
762
+ value: row[entry.key],
763
+ },
764
+ control: entry.control,
765
+ };
766
+ }
767
+ return undefined;
768
+ }
769
+
770
+ /**
771
+ * Draw one registered control into the host the document announced for it.
772
+ *
773
+ * Connectedness is deliberately NOT consulted. A host is announced as it is CREATED, which is one
774
+ * reconcile step before it is in the page — so "is it connected" answers no for exactly the node
775
+ * that needs drawing, and a check there is how the control came out empty on first mount.
776
+ */
777
+ function paintControl(id: string, host: HTMLElement): void {
778
+ const found = controlFor(id);
779
+ if (!found) {
780
+ return;
781
+ }
782
+ /* A control is built once per host and updated after that. The host element is what identifies
783
+ it: the reconciler may replace the node, and a handle left pointing at the old one would go
784
+ on updating a document nobody can see. */
785
+ const standing = mountedControls.get(id);
786
+ if (standing?.host === host) {
787
+ standing.handle.update(found.args);
788
+ return;
789
+ }
790
+ standing?.handle.dispose();
791
+ mountedControls.set(id, { handle: found.control.mount(host, found.args), host });
792
+ }
793
+
794
+ /** Take down a mounted control whose field is no longer one — or is no longer here at all. */
795
+ function sweepMountedControls(): void {
796
+ for (const [id, standing] of mountedControls) {
797
+ if (!controlFor(id)) {
798
+ standing.handle.dispose();
799
+ mountedControls.delete(id);
800
+ }
801
+ }
802
+ }
803
+
804
+ /** Redraw every control this form has been handed a host for. */
805
+ function paintControls(): void {
806
+ sweepMountedControls();
807
+ for (const [id, host] of controlHosts) {
808
+ paintControl(id, host);
809
+ }
810
+ }
811
+
812
+ const surface: SchemaFormSurface = createSchemaFormSurface(
813
+ { fields: [] },
814
+ actions,
815
+ (id, host) => {
816
+ controlHosts.set(id, host);
817
+ paintControl(id, host);
818
+ },
819
+ );
820
+
821
+ return {
822
+ dispose() {
823
+ for (const timer of timers.values()) {
824
+ clearTimeout(timer);
825
+ }
826
+ timers.clear();
827
+ sourceMenu?.close();
828
+ sourceMenu = null;
829
+ for (const standing of mountedControls.values()) {
830
+ standing.handle.dispose();
831
+ }
832
+ mountedControls.clear();
833
+ controlHosts.clear();
834
+ plans.clear();
835
+ surface.dispose();
836
+ },
837
+ host: surface.host,
838
+ update(nextSchema, nextValue, nextOpts) {
839
+ schema = nextSchema;
840
+ value = nextValue;
841
+ opts = nextOpts;
842
+ ctx = nextOpts.context ?? NULL_FORM_CONTEXT;
843
+ plans.clear();
844
+ surface.update({ fields: deriveFields(schema, value, opts, ctx, plans) });
845
+ paintControls();
846
+ },
847
+ };
848
+ }
849
+
850
+ // ─── Derivation: one schema property becomes one already-decided row ─────────
851
+
852
+ /** Every row the document draws, and the plan that reads each one's gestures back. */
853
+ function deriveFields(
854
+ schema: JsonSchema,
855
+ value: Record<string, unknown>,
856
+ opts: RenderFormOptions,
857
+ ctx: SchemaFormContext,
858
+ plans: Map<string, FieldPlan>,
859
+ ): SchemaFormFieldView[] {
860
+ const required = new Set(schema.required);
861
+ const sources = refSourcesFor(ctx);
862
+ return Object.entries(schema.properties ?? {}).map(([prop, ps]) =>
863
+ deriveField(prop, ps, {
864
+ ctx,
865
+ opts,
866
+ plans,
867
+ required: required.has(prop),
868
+ sources,
869
+ value,
870
+ }),
871
+ );
872
+ }
873
+
874
+ interface DeriveArgs {
875
+ ctx: SchemaFormContext;
876
+ opts: RenderFormOptions;
877
+ plans: Map<string, FieldPlan>;
878
+ required: boolean;
879
+ sources: SignalOption[];
880
+ value: Record<string, unknown>;
881
+ }
712
882
 
713
- return html`${propertyFields}`;
883
+ function deriveField(prop: string, ps: JsonSchema, args: DeriveArgs): SchemaFormFieldView {
884
+ const { ctx, opts, plans, required, sources, value } = args;
885
+ const current = value[prop];
886
+ const plan: FieldPlan = { commit: "text", debounceMs: TEXT_COMMIT_MS, prop, schema: ps };
887
+ plans.set(prop, plan);
888
+
889
+ // Host diagnostics win over the intrinsic check — see RenderFormOptions.errors.
890
+ const error =
891
+ opts.errors?.[prop] || validateFieldValue(ps, current, Boolean(opts.showRequired) && required);
892
+ const count = opts.errorCounts?.[prop];
893
+
894
+ /* A `ui.control` override owns its whole field — the secret control writes an env-var NAME rather
895
+ than the value it was handed, so a rung switch above it would be editing a different thing than
896
+ the one on screen. */
897
+ const overrideName = opts.ui?.[prop]?.control;
898
+ const override = overrideName ? controlRegistry.get(overrideName) : undefined;
899
+ const laddered = sources.length > 0 && isBindableField(ps) && !overrideName;
900
+
901
+ const row: SchemaFormFieldView = {
902
+ addLabel: `Add ${prop}`,
903
+ checked: false,
904
+ chips: [],
905
+ description: ps.description ?? "",
906
+ error,
907
+ errorCount: count !== undefined && count > 1 ? `×${count}` : "",
908
+ hasChip: false,
909
+ hasChips: false,
910
+ hasCount: count !== undefined && count > 1,
911
+ hasError: error !== "",
912
+ hasOptions: false,
913
+ invalid: error !== "",
914
+ key: prop,
915
+ kind: "text",
916
+ label: prop,
917
+ max: "",
918
+ min: "",
919
+ mono: false,
920
+ multiline: false,
921
+ options: [],
922
+ placeholder: "",
923
+ prop: ps.name || prop,
924
+ required,
925
+ rows: [],
926
+ source: "fixed",
927
+ sourceHint: "",
928
+ sourceLabel: "",
929
+ sourceLocked: false,
930
+ sourceName: "",
931
+ step: "",
932
+ value: "",
933
+ };
934
+
935
+ if (laddered) {
936
+ const fieldKey = `${ctx.fieldKeyPrefix ?? ""}.${prop}`;
937
+ const mode = effectiveSlotMode(fieldKey, current);
938
+ /* A From data… rung with nothing to point at is a dead end — but this engine only ladders a
939
+ field once the host has named a source, so the rung always has something in it. */
940
+ const offered = slotCaps({ schema: configFieldSchema(ps) });
941
+ plan.ladder = { fieldKey, mode, offered, sources };
942
+ row.hasChip = true;
943
+ row.sourceLabel = VALUE_SOURCE_LABELS[mode];
944
+ row.sourceHint = VALUE_SOURCE_HINTS[mode];
945
+ row.source = mode === "literal" ? "fixed" : "bound";
946
+ row.sourceLocked = !offered.some((m) => m !== mode);
947
+ row.sourceName = row.sourceLocked
948
+ ? `Value source: ${row.sourceLabel} (no other source available here)`
949
+ : `Value source: ${row.sourceLabel} — click to change`;
950
+ if (mode === "ref") {
951
+ plan.commit = "ref";
952
+ plan.debounceMs = 0;
953
+ row.kind = "pointer";
954
+ row.value = isRef(current) ? current.$ref : "";
955
+ row.options = sources.map((s) => ({ label: s.label, value: s.value }));
956
+ row.hasOptions = row.options.length > 0;
957
+ return row;
958
+ }
959
+ if (mode === "template") {
960
+ plan.commit = "template";
961
+ plan.debounceMs = 0;
962
+ row.kind = "template";
963
+ row.value = String(current ?? "");
964
+ return row;
965
+ }
966
+ }
967
+
968
+ if (override) {
969
+ plan.commit = "none";
970
+ plan.control = override;
971
+ row.kind = "control";
972
+ return row;
973
+ }
974
+
975
+ /* A ref left in a form whose host named nowhere to bind: no ladder is drawn, so the pointer is
976
+ edited as the string it is rather than rendered as "[object Object]" in a typed widget. */
977
+ if (isRef(current) && isBindableField(ps)) {
978
+ plan.commit = "ref";
979
+ plan.debounceMs = 0;
980
+ row.kind = "text";
981
+ row.mono = true;
982
+ row.value = current.$ref;
983
+ row.placeholder = prop;
984
+ return row;
985
+ }
986
+
987
+ /* A relationship to another collection (`$ref: "#/content/<type>"`) is the registered `reference`
988
+ control, wherever the form is drawn — §9.2's "one picker" is this dispatch plus the single
989
+ `registerFormControl("reference", …)` in `ui/form-controls.ts`. It is deliberately NOT an enum:
990
+ the choices are entry files on disk, so they are read asynchronously and can be stale, and a
991
+ schema `enum` is a closed set the document itself declares. When the control is not registered
992
+ (a bare-Bun import of this engine), the field falls through to the plain text control below. */
993
+ if (referenceTarget(ps) !== null) {
994
+ const reference = controlRegistry.get("reference");
995
+ if (reference) {
996
+ plan.commit = "none";
997
+ plan.control = reference;
998
+ row.kind = "control";
999
+ return row;
1000
+ }
1001
+ }
1002
+
1003
+ const enumValues = resolveFormEnum(opts.ui?.[prop]?.enum ?? ps.enum, ctx, value);
1004
+ if (enumValues) {
1005
+ plan.commit = "select";
1006
+ plan.debounceMs = 0;
1007
+ row.kind = "select";
1008
+ row.value =
1009
+ current !== undefined
1010
+ ? String(current)
1011
+ : ps.default !== undefined
1012
+ ? String(ps.default)
1013
+ : NONE;
1014
+ row.options = [
1015
+ ...(required ? [] : [{ label: "—", value: NONE }]),
1016
+ ...enumValues.map((v) => ({ label: v, value: v })),
1017
+ ];
1018
+ return row;
1019
+ }
1020
+ if (ps.type === "boolean") {
1021
+ plan.commit = "none";
1022
+ row.kind = "checkbox";
1023
+ row.checked = Boolean(current ?? ps.default ?? false);
1024
+ return row;
1025
+ }
1026
+ if (ps.type === "integer" || ps.type === "number") {
1027
+ plan.commit = "number";
1028
+ plan.debounceMs = 0;
1029
+ row.kind = "number";
1030
+ row.value = current !== undefined ? String(current) : "";
1031
+ row.placeholder = ps.default != null ? String(ps.default) : "";
1032
+ row.min = ps.minimum !== undefined ? String(ps.minimum) : "";
1033
+ row.max = ps.maximum !== undefined ? String(ps.maximum) : "";
1034
+ row.step = ps.type === "integer" ? "1" : "";
1035
+ return row;
1036
+ }
1037
+ if (ps.format === "json-schema") {
1038
+ plan.commit = "json";
1039
+ row.kind = "json";
1040
+ row.value = current !== undefined ? JSON.stringify(current, null, 2) : "";
1041
+ row.placeholder = ps.description ?? "JSON Schema defining the data shape…";
1042
+ row.chips = shapeChips(current);
1043
+ row.hasChips = row.chips.length > 0;
1044
+ return row;
1045
+ }
1046
+ if (ps.type === "array" && ps.items?.type === "object" && ps.items.properties) {
1047
+ return arrayOfObjects(prop, ps.items.properties, args, plan, row);
1048
+ }
1049
+ if (ps.type === "array" || ps.type === "object") {
1050
+ plan.commit = "json";
1051
+ row.kind = "json";
1052
+ row.value = current !== undefined ? JSON.stringify(current, null, 2) : "";
1053
+ row.placeholder = ps.default !== undefined ? JSON.stringify(ps.default) : "";
1054
+ return row;
1055
+ }
1056
+
1057
+ row.kind = "text";
1058
+ row.value = asText(current);
1059
+ row.placeholder = ps.default !== undefined ? String(ps.default) : (ps.examples?.[0] ?? "");
1060
+ return row;
1061
+ }
1062
+
1063
+ /** The property chips above a JSON-Schema field: what shape is in there right now. */
1064
+ function shapeChips(current: unknown): { key: string; label: string }[] {
1065
+ if (!current || typeof current !== "object") {
1066
+ return [];
1067
+ }
1068
+ const held = current as Record<string, unknown>;
1069
+ if (held.$ref || typeof held.properties !== "object" || held.properties === null) {
1070
+ return [];
1071
+ }
1072
+ return Object.entries(held.properties as Record<string, Record<string, unknown>>).map(
1073
+ ([key, spec]) => ({ key, label: `${key}: ${String(spec.type ?? "any")}` }),
1074
+ );
1075
+ }
1076
+
1077
+ /** An array whose items have a declared shape: one row per item, one cell per property. */
1078
+ function arrayOfObjects(
1079
+ prop: string,
1080
+ itemProps: Record<string, JsonSchema>,
1081
+ args: DeriveArgs,
1082
+ plan: FieldPlan,
1083
+ row: SchemaFormFieldView,
1084
+ ): SchemaFormFieldView {
1085
+ const held = args.value[prop];
1086
+ const rows = Array.isArray(held) ? (held as Record<string, unknown>[]) : [];
1087
+ plan.commit = "none";
1088
+ plan.itemProps = itemProps;
1089
+ plan.rows = rows;
1090
+ plan.cellControls = new Map();
1091
+ plan.refCells = new Set();
1092
+ row.kind = "rows";
1093
+ row.rows = rows.map((item, idx) => deriveRow(prop, String(idx), item, itemProps, args, plan));
1094
+ return row;
1095
+ }
1096
+
1097
+ function deriveRow(
1098
+ prop: string,
1099
+ key: string,
1100
+ item: Record<string, unknown>,
1101
+ itemProps: Record<string, JsonSchema>,
1102
+ args: DeriveArgs,
1103
+ plan: FieldPlan,
1104
+ ): SchemaFormRowView {
1105
+ return {
1106
+ cells: Object.entries(itemProps).map(([cellKey, cellSchema]) =>
1107
+ deriveCell(prop, key, cellKey, cellSchema, item[cellKey], args, plan),
1108
+ ),
1109
+ field: prop,
1110
+ key,
1111
+ removeLabel: `Remove ${prop} ${Number(key) + 1}`,
1112
+ };
1113
+ }
1114
+
1115
+ function deriveCell(
1116
+ prop: string,
1117
+ rowKey: string,
1118
+ cellKey: string,
1119
+ cellSchema: JsonSchema,
1120
+ held: unknown,
1121
+ args: DeriveArgs,
1122
+ plan: FieldPlan,
1123
+ ): SchemaFormCellView {
1124
+ const id = `${prop}/${rowKey}/${cellKey}`;
1125
+ const cell: SchemaFormCellView = {
1126
+ checked: false,
1127
+ field: prop,
1128
+ id,
1129
+ key: cellKey,
1130
+ kind: "text",
1131
+ label: cellKey,
1132
+ options: [],
1133
+ row: rowKey,
1134
+ value: "",
1135
+ };
1136
+ if (isRef(held)) {
1137
+ cell.value = held.$ref;
1138
+ plan.refCells?.add(id);
1139
+ return cell;
1140
+ }
1141
+ if (referenceTarget(cellSchema) !== null) {
1142
+ /* A CELL's control is mounted into a host of the cell's own, exactly as a field's is: the one
1143
+ control a cell is ever given is the reference picker, and that is a document now
1144
+ (`surfaces/reference-field.json`). What the cell host must never become is a second writer of
1145
+ a node the document already owns. */
1146
+ const reference = controlRegistry.get("reference");
1147
+ if (reference) {
1148
+ cell.kind = "control";
1149
+ plan.cellControls?.set(id, {
1150
+ control: reference,
1151
+ key: cellKey,
1152
+ row: rowKey,
1153
+ schema: cellSchema,
1154
+ });
1155
+ return cell;
1156
+ }
1157
+ }
1158
+ const enumValues = resolveFormEnum(cellSchema.enum, args.ctx, args.value);
1159
+ if (enumValues) {
1160
+ cell.kind = "select";
1161
+ cell.value = held !== undefined ? String(held) : NONE;
1162
+ cell.options = [
1163
+ { label: "—", value: NONE },
1164
+ ...enumValues.map((v: string): SchemaFormOption => ({ label: v, value: v })),
1165
+ ];
1166
+ return cell;
1167
+ }
1168
+ if (cellSchema.type === "boolean") {
1169
+ cell.kind = "checkbox";
1170
+ cell.checked = Boolean(held);
1171
+ return cell;
1172
+ }
1173
+ if (cellSchema.type === "integer" || cellSchema.type === "number") {
1174
+ cell.kind = "number";
1175
+ cell.value = held !== undefined ? String(held) : "";
1176
+ return cell;
1177
+ }
1178
+ cell.value = asText(held);
1179
+ return cell;
714
1180
  }