@jxsuite/studio 4.0.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 (613) 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-xp8vnwma.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-nj44jpzj.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-rws7k4br.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-hbtp0eh1.js → studio-25hkdrkw.js} +356 -85
  62. package/dist/chunks/studio-26d7myd1.js +258 -0
  63. package/dist/chunks/{studio-p8j86wag.js → studio-38emantv.js} +22 -5
  64. package/dist/chunks/studio-3gxv60tn.js +422 -0
  65. package/dist/chunks/{studio-8swrjbny.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-gv8vvshc.js → studio-5523bpzt.js} +11 -11
  69. package/dist/chunks/{studio-5q69gfag.js → studio-57mf66w2.js} +1753 -2484
  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-eenfkt4e.js → studio-79y5sz51.js} +52 -38
  78. package/dist/chunks/{studio-4q0wznzf.js → studio-7e11jpa9.js} +461 -145
  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-arfpe389.js +195 -0
  88. package/dist/chunks/{studio-2zkz8evn.js → studio-az3zd49t.js} +473 -281
  89. package/dist/chunks/studio-cpvb2ed1.js +1171 -0
  90. package/dist/chunks/{studio-9r6j5ev1.js → studio-d0300636.js} +5078 -19
  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-4pqqxbrt.js → studio-em9nw8eh.js} +301 -112
  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-hhvs4sdg.js +195 -0
  100. package/dist/chunks/{studio-68n67094.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-kjjsjak4.js → studio-rkstz940.js} +3661 -6136
  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-yxrkyv8a.js → studio-s6z853aq.js} +12108 -11766
  113. package/dist/chunks/{studio-sf7zjgv1.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-htsf0w6m.js → studio-vz68yn9e.js} +31 -154
  118. package/dist/chunks/studio-wqpngpsk.js +174 -0
  119. package/dist/chunks/{studio-eesa1myv.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 +1812 -698
  135. package/dist/manifest.json +7 -0
  136. package/dist/studio.js +52900 -36573
  137. package/dist/workers/editor.worker.js +1040 -1085
  138. package/dist/workers/json.worker.js +1946 -1991
  139. package/dist/workers/ts.worker.js +4960 -5005
  140. package/favicon.ico +0 -0
  141. package/index.html +2 -0
  142. package/package.json +22 -55
  143. package/src/about/about-modal.ts +73 -123
  144. package/src/browse/library-commands.ts +20 -49
  145. package/src/browse/library-layouts.ts +87 -218
  146. package/src/browse/library-pane.ts +406 -438
  147. package/src/canvas/canvas-helpers.ts +0 -49
  148. package/src/canvas/canvas-live-render.ts +25 -2
  149. package/src/canvas/canvas-render.ts +666 -231
  150. package/src/canvas/canvas-surface.ts +0 -1
  151. package/src/canvas/canvas-utils.ts +310 -117
  152. package/src/canvas/dialog-path.ts +76 -0
  153. package/src/canvas/dialog-state.ts +67 -0
  154. package/src/canvas/diff-marks.ts +252 -0
  155. package/src/canvas/diff-toolbar.ts +349 -0
  156. package/src/canvas/diff-view.ts +123 -0
  157. package/src/canvas/edit-width-drag.ts +187 -0
  158. package/src/canvas/edit-width.ts +150 -0
  159. package/src/canvas/iframe-entry.ts +125 -8
  160. package/src/canvas/iframe-host.ts +271 -15
  161. package/src/canvas/iframe-interaction.ts +133 -13
  162. package/src/canvas/iframe-patch.ts +7 -1
  163. package/src/canvas/iframe-position.ts +5 -6
  164. package/src/canvas/iframe-protocol.ts +106 -5
  165. package/src/canvas/iframe-render.ts +373 -5
  166. package/src/canvas/iframe-slash.ts +2 -2
  167. package/src/canvas/iframe-subtree.ts +5 -8
  168. package/src/canvas/path-mapping.ts +25 -3
  169. package/src/canvas/popover-path.ts +106 -0
  170. package/src/canvas/popover-state.ts +155 -0
  171. package/src/canvas/surface-registry.ts +21 -10
  172. package/src/collab/presence-chips.ts +51 -73
  173. package/src/commands/app-commands.ts +10 -0
  174. package/src/commands/budget.ts +12 -0
  175. package/src/commands/command-args.ts +369 -6
  176. package/src/commands/defaults.ts +44 -14
  177. package/src/commands/live-context.ts +11 -5
  178. package/src/commands/reference.ts +12 -2
  179. package/src/commands/registry.ts +165 -8
  180. package/src/content/entry-commands.ts +16 -23
  181. package/src/content/entry-editor.ts +144 -101
  182. package/src/editor/canvas-slash-bridge.ts +2 -2
  183. package/src/editor/context-menu.ts +49 -200
  184. package/src/editor/convert-to-component.ts +42 -82
  185. package/src/editor/convert-to-repeater.ts +132 -173
  186. package/src/editor/inline-edit.ts +11 -0
  187. package/src/editor/shortcuts.ts +65 -26
  188. package/src/editor/slash-menu.ts +150 -157
  189. package/src/files/file-ops.ts +117 -11
  190. package/src/files/files.ts +949 -664
  191. package/src/files/json-layout.ts +474 -0
  192. package/src/files/media-paths.ts +13 -42
  193. package/src/files/media-usage.ts +41 -131
  194. package/src/files/serialize-document.ts +8 -1
  195. package/src/format/convert-file.ts +16 -10
  196. package/src/format/format-host.ts +50 -1
  197. package/src/github/github-auth.ts +71 -29
  198. package/src/github/github-publish.ts +57 -65
  199. package/src/grid/cell-editors.ts +46 -4
  200. package/src/grid/cell-popovers.ts +65 -62
  201. package/src/grid/grid-open.ts +86 -64
  202. package/src/grid/grid-panel.ts +390 -469
  203. package/src/grid/redirects-grid.ts +40 -44
  204. package/src/hosting/document.ts +2 -0
  205. package/src/hosting/layout.ts +10 -0
  206. package/src/i18n/i18n-commands.ts +127 -62
  207. package/src/media/media-pane.ts +83 -133
  208. package/src/new-project/add-repo-modal.ts +125 -176
  209. package/src/new-project/import-tab.ts +201 -250
  210. package/src/new-project/location-fields.ts +101 -165
  211. package/src/new-project/new-project-modal.ts +376 -367
  212. package/src/packages/jxsuite-update.ts +76 -18
  213. package/src/panels/activity-panel.ts +137 -104
  214. package/src/panels/ai-chat/chat-markdown.ts +15 -13
  215. package/src/panels/ai-chat/chat-view.ts +307 -472
  216. package/src/panels/ai-chat/composer.ts +175 -153
  217. package/src/panels/ai-chat/sessions-view.ts +31 -62
  218. package/src/panels/ai-panel.ts +210 -185
  219. package/src/panels/block-action-bar.ts +437 -643
  220. package/src/panels/bottom-dock.ts +112 -47
  221. package/src/panels/chat-panel.ts +19 -14
  222. package/src/panels/data-explorer.ts +141 -102
  223. package/src/panels/data-grid.ts +183 -150
  224. package/src/panels/dnd.ts +75 -20
  225. package/src/panels/editors.ts +26 -19
  226. package/src/panels/elements-panel.ts +243 -186
  227. package/src/panels/empty-state.ts +26 -42
  228. package/src/panels/events-panel.ts +950 -729
  229. package/src/panels/formula-workspace.ts +434 -207
  230. package/src/panels/frontmatter-fields.ts +185 -153
  231. package/src/panels/frontmatter-panel.ts +411 -187
  232. package/src/panels/git-diff-open.ts +148 -0
  233. package/src/panels/git-panel.ts +679 -605
  234. package/src/panels/head-panel.ts +617 -435
  235. package/src/panels/i18n-panel.ts +153 -133
  236. package/src/panels/imports-panel.ts +388 -414
  237. package/src/panels/jump-bar.ts +174 -147
  238. package/src/panels/layers-panel.ts +785 -671
  239. package/src/panels/left-panel.ts +166 -94
  240. package/src/panels/navigator-panels.ts +1 -1
  241. package/src/panels/pane-context.ts +529 -677
  242. package/src/panels/pane-grid.ts +265 -241
  243. package/src/panels/panel-registry.ts +15 -4
  244. package/src/panels/problems-panel.ts +177 -99
  245. package/src/panels/properties-panel.ts +1525 -966
  246. package/src/panels/provenance.ts +31 -113
  247. package/src/panels/quick-search.ts +217 -172
  248. package/src/panels/right-panel.ts +135 -169
  249. package/src/panels/seo-modal.ts +346 -167
  250. package/src/panels/settings-menu.ts +109 -646
  251. package/src/panels/settings-pane.ts +102 -86
  252. package/src/panels/signals-panel.ts +1318 -1172
  253. package/src/panels/statement-editor.ts +556 -428
  254. package/src/panels/style-panel.ts +1367 -630
  255. package/src/panels/style-utils.ts +36 -32
  256. package/src/panels/stylebook-doc.ts +30 -73
  257. package/src/panels/stylebook-layers-panel.ts +202 -109
  258. package/src/panels/stylebook-panel.ts +124 -88
  259. package/src/panels/tab-strip.ts +385 -287
  260. package/src/platforms/cloud.ts +347 -86
  261. package/src/platforms/devserver.ts +13 -0
  262. package/src/publish/deploy-checklist.ts +124 -78
  263. package/src/publish/publish-commands.ts +4 -12
  264. package/src/publish/publish-panel.ts +165 -299
  265. package/src/services/a11y-report.ts +100 -43
  266. package/src/services/ai-command-tools.ts +468 -0
  267. package/src/services/ai-import-tools.ts +50 -12
  268. package/src/services/ai-models.ts +108 -1
  269. package/src/services/ai-session-store.ts +6 -0
  270. package/src/services/ai-system-prompt.ts +110 -18
  271. package/src/services/ai-tools.ts +28 -143
  272. package/src/services/ai-write-report.ts +174 -0
  273. package/src/services/context-manager.ts +12 -1
  274. package/src/services/document-assistant.ts +50 -2
  275. package/src/services/idle.ts +16 -14
  276. package/src/services/live-surfaces.ts +237 -0
  277. package/src/services/model-uri.ts +75 -0
  278. package/src/services/notify.ts +63 -6
  279. package/src/services/popover-report.ts +325 -0
  280. package/src/services/surface-registry.ts +56 -0
  281. package/src/services/tool-executor.ts +17 -0
  282. package/src/settings/contexts-section.ts +301 -299
  283. package/src/settings/contributed-section.ts +361 -222
  284. package/src/settings/css-vars-editor.ts +320 -325
  285. package/src/settings/defs-editor.ts +572 -535
  286. package/src/settings/dependencies-editor.ts +79 -107
  287. package/src/settings/extension-commands.ts +379 -0
  288. package/src/settings/extension-rows.ts +113 -0
  289. package/src/settings/extensions-section.ts +287 -0
  290. package/src/settings/general-settings.ts +130 -181
  291. package/src/settings/head-editor.ts +10 -305
  292. package/src/settings/locales-section.ts +146 -196
  293. package/src/settings/preferences-accounts.ts +193 -12
  294. package/src/settings/preferences-dialog.ts +384 -307
  295. package/src/settings/project-sections.ts +108 -181
  296. package/src/settings/schema-field-ui.ts +11 -399
  297. package/src/settings/section-registry.ts +8 -1
  298. package/src/settings/settings-document.ts +31 -29
  299. package/src/shell/tree.ts +42 -87
  300. package/src/shell.ts +48 -36
  301. package/src/studio.ts +90 -67
  302. package/src/style/token-ref.ts +0 -39
  303. package/src/surfaces/about.json +263 -0
  304. package/src/surfaces/about.ts +163 -0
  305. package/src/surfaces/add-repo.json +486 -0
  306. package/src/surfaces/add-repo.ts +236 -0
  307. package/src/surfaces/ai-chat.json +2185 -0
  308. package/src/surfaces/ai-chat.ts +549 -0
  309. package/src/surfaces/ai-credentials-form.json +332 -0
  310. package/src/surfaces/ai-credentials-form.ts +151 -0
  311. package/src/surfaces/ai-managed-connect.json +108 -0
  312. package/src/surfaces/ai-managed-connect.ts +123 -0
  313. package/src/surfaces/ai-model-picker.json +114 -0
  314. package/src/surfaces/ai-model-picker.ts +139 -0
  315. package/src/surfaces/block-action-bar.json +640 -0
  316. package/src/surfaces/block-action-bar.ts +368 -0
  317. package/src/surfaces/bottom-dock.json +154 -0
  318. package/src/surfaces/bottom-dock.ts +125 -0
  319. package/src/surfaces/canvas-stage.json +444 -0
  320. package/src/surfaces/canvas-stage.ts +253 -0
  321. package/src/surfaces/cf-account-picker.json +300 -0
  322. package/src/surfaces/cf-account-picker.ts +214 -0
  323. package/src/surfaces/commandbar.json +984 -0
  324. package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +414 -389
  325. package/src/surfaces/convert-repeater.json +309 -0
  326. package/src/surfaces/convert-repeater.ts +276 -0
  327. package/src/surfaces/data-actions.json +157 -0
  328. package/src/surfaces/data-actions.ts +115 -0
  329. package/src/surfaces/dialog.json +289 -0
  330. package/src/surfaces/dialog.ts +343 -0
  331. package/src/surfaces/diff-toolbar.json +266 -0
  332. package/src/surfaces/diff-toolbar.ts +139 -0
  333. package/src/surfaces/doc-header.json +739 -0
  334. package/src/surfaces/doc-header.ts +223 -0
  335. package/src/surfaces/empty-state.json +152 -0
  336. package/src/surfaces/empty-state.ts +173 -0
  337. package/src/surfaces/entry-editor.json +281 -0
  338. package/src/surfaces/entry-editor.ts +191 -0
  339. package/src/surfaces/expression-editor.json +977 -0
  340. package/src/surfaces/expression-editor.ts +266 -0
  341. package/src/surfaces/files-panel.json +592 -0
  342. package/src/surfaces/files-panel.ts +301 -0
  343. package/src/surfaces/formula-palette.json +364 -0
  344. package/src/surfaces/formula-palette.ts +340 -0
  345. package/src/surfaces/git-panel.json +1822 -0
  346. package/src/surfaces/git-panel.ts +272 -0
  347. package/src/surfaces/github-auth.json +112 -0
  348. package/src/surfaces/github-auth.ts +139 -0
  349. package/src/surfaces/github-publish.json +206 -0
  350. package/src/surfaces/github-publish.ts +169 -0
  351. package/src/surfaces/grid-cell.json +218 -0
  352. package/src/surfaces/grid-cell.ts +120 -0
  353. package/src/surfaces/grid-open.json +147 -0
  354. package/src/surfaces/grid-open.ts +87 -0
  355. package/src/surfaces/grid-panel.json +706 -0
  356. package/src/surfaces/grid-panel.ts +206 -0
  357. package/src/surfaces/grid-replace.json +179 -0
  358. package/src/surfaces/grid-replace.ts +89 -0
  359. package/src/surfaces/grid-views.json +446 -0
  360. package/src/surfaces/grid-views.ts +172 -0
  361. package/src/surfaces/inspector-dock.json +187 -0
  362. package/src/surfaces/inspector-dock.ts +161 -0
  363. package/src/surfaces/jump-bar.json +243 -0
  364. package/src/surfaces/jump-bar.ts +129 -0
  365. package/src/surfaces/jxsuite-update.json +146 -0
  366. package/src/surfaces/jxsuite-update.ts +149 -0
  367. package/src/surfaces/library-pane.json +1509 -0
  368. package/src/surfaces/library-pane.ts +363 -0
  369. package/src/surfaces/logic-panel.json +1949 -0
  370. package/src/surfaces/logic-panel.ts +369 -0
  371. package/src/surfaces/logic-workspace.json +685 -0
  372. package/src/surfaces/logic-workspace.ts +238 -0
  373. package/src/surfaces/media-browser.json +303 -0
  374. package/src/surfaces/media-browser.ts +145 -0
  375. package/src/surfaces/media-field.json +163 -0
  376. package/src/surfaces/media-field.ts +116 -0
  377. package/src/surfaces/media-pane.json +513 -0
  378. package/src/surfaces/media-pane.ts +197 -0
  379. package/src/surfaces/menu.json +232 -0
  380. package/src/surfaces/menu.ts +375 -0
  381. package/src/surfaces/navigator-dock.json +113 -0
  382. package/src/surfaces/navigator-dock.ts +130 -0
  383. package/src/surfaces/new-project.json +1654 -0
  384. package/src/surfaces/new-project.ts +411 -0
  385. package/src/surfaces/palette.json +535 -0
  386. package/src/surfaces/pane-context.json +1497 -0
  387. package/src/surfaces/pane-context.ts +513 -0
  388. package/src/surfaces/pane-grid.json +188 -0
  389. package/src/surfaces/pane-grid.ts +250 -0
  390. package/src/surfaces/panel-activity.json +563 -0
  391. package/src/surfaces/panel-activity.ts +187 -0
  392. package/src/surfaces/panel-data.json +141 -0
  393. package/src/surfaces/panel-data.ts +156 -0
  394. package/src/surfaces/panel-deploy-checklist.json +283 -0
  395. package/src/surfaces/panel-deploy-checklist.ts +175 -0
  396. package/src/surfaces/panel-elements.json +367 -0
  397. package/src/surfaces/panel-elements.ts +175 -0
  398. package/src/surfaces/panel-i18n.json +569 -0
  399. package/src/surfaces/panel-i18n.ts +212 -0
  400. package/src/surfaces/panel-imports.json +780 -0
  401. package/src/surfaces/panel-imports.ts +209 -0
  402. package/src/surfaces/panel-outline.json +734 -0
  403. package/src/surfaces/panel-outline.ts +326 -0
  404. package/src/surfaces/panel-page.json +1017 -0
  405. package/src/surfaces/panel-page.ts +240 -0
  406. package/src/surfaces/panel-problems.json +497 -0
  407. package/src/surfaces/panel-problems.ts +157 -0
  408. package/src/surfaces/panel-signals.json +1973 -0
  409. package/src/surfaces/panel-signals.ts +436 -0
  410. package/src/surfaces/panel-stylebook-layers.json +317 -0
  411. package/src/surfaces/panel-stylebook-layers.ts +155 -0
  412. package/src/surfaces/preferences.json +850 -0
  413. package/src/surfaces/preferences.ts +332 -0
  414. package/src/surfaces/progress-modal.json +110 -0
  415. package/src/surfaces/progress-modal.ts +150 -0
  416. package/src/surfaces/properties-panel.json +1578 -0
  417. package/src/surfaces/properties-panel.ts +337 -0
  418. package/src/surfaces/publish.json +945 -0
  419. package/src/surfaces/publish.ts +392 -0
  420. package/src/surfaces/push-plan.json +110 -0
  421. package/src/surfaces/push-plan.ts +87 -0
  422. package/src/surfaces/rail.json +255 -0
  423. package/src/surfaces/rail.ts +206 -0
  424. package/src/surfaces/reference-field.json +191 -0
  425. package/src/surfaces/reference-field.ts +168 -0
  426. package/src/surfaces/schema-builder.json +1011 -0
  427. package/src/surfaces/schema-builder.ts +181 -0
  428. package/src/surfaces/schema-form.json +1013 -0
  429. package/src/surfaces/schema-form.ts +287 -0
  430. package/src/surfaces/secret-field.json +60 -0
  431. package/src/surfaces/secret-field.ts +146 -0
  432. package/src/surfaces/seo.json +932 -0
  433. package/src/surfaces/seo.ts +334 -0
  434. package/src/surfaces/settings-contexts.json +538 -0
  435. package/src/surfaces/settings-contexts.ts +196 -0
  436. package/src/surfaces/settings-contributed.json +489 -0
  437. package/src/surfaces/settings-contributed.ts +199 -0
  438. package/src/surfaces/settings-css-vars.json +820 -0
  439. package/src/surfaces/settings-css-vars.ts +199 -0
  440. package/src/surfaces/settings-defs.json +1363 -0
  441. package/src/surfaces/settings-defs.ts +232 -0
  442. package/src/surfaces/settings-deploy.json +108 -0
  443. package/src/surfaces/settings-deploy.ts +135 -0
  444. package/src/surfaces/settings-extensions.json +486 -0
  445. package/src/surfaces/settings-extensions.ts +201 -0
  446. package/src/surfaces/settings-head.json +779 -0
  447. package/src/surfaces/settings-head.ts +397 -0
  448. package/src/surfaces/settings-locales.json +420 -0
  449. package/src/surfaces/settings-locales.ts +137 -0
  450. package/src/surfaces/settings-overview.json +423 -0
  451. package/src/surfaces/settings-overview.ts +218 -0
  452. package/src/surfaces/settings-packages.json +549 -0
  453. package/src/surfaces/settings-packages.ts +158 -0
  454. package/src/surfaces/settings-pane.json +147 -0
  455. package/src/surfaces/settings-pane.ts +150 -0
  456. package/src/surfaces/settings-rawjson.json +104 -0
  457. package/src/surfaces/settings-rawjson.ts +107 -0
  458. package/src/surfaces/shell.json +99 -0
  459. package/src/surfaces/shell.ts +30 -0
  460. package/src/surfaces/slash-menu.json +180 -0
  461. package/src/surfaces/slash-menu.ts +272 -0
  462. package/src/surfaces/statements.json +607 -0
  463. package/src/surfaces/statements.ts +238 -0
  464. package/src/surfaces/statusbar.json +94 -0
  465. package/src/{panels → surfaces}/statusbar.ts +163 -90
  466. package/src/surfaces/style-panel.json +1643 -0
  467. package/src/surfaces/style-panel.ts +338 -0
  468. package/src/surfaces/stylebook-chrome.json +122 -0
  469. package/src/surfaces/stylebook-chrome.ts +122 -0
  470. package/src/surfaces/tab-strip.json +448 -0
  471. package/src/surfaces/tab-strip.ts +200 -0
  472. package/src/surfaces/target-line.json +419 -0
  473. package/src/surfaces/target-line.ts +349 -0
  474. package/src/surfaces/toasts.json +189 -0
  475. package/src/surfaces/welcome.json +571 -0
  476. package/src/surfaces/welcome.ts +338 -0
  477. package/src/tabs/project-config.ts +71 -15
  478. package/src/tabs/tab.ts +52 -1
  479. package/src/types.ts +49 -3
  480. package/src/ui/ai-credentials-form.ts +104 -100
  481. package/src/ui/ai-managed-connect.ts +116 -49
  482. package/src/ui/ai-model-picker.ts +102 -47
  483. package/src/ui/cf-account-picker.ts +135 -0
  484. package/src/ui/color-selector.ts +80 -301
  485. package/src/ui/dynamic-slot.ts +43 -283
  486. package/src/ui/expression-editor.ts +797 -576
  487. package/src/ui/field-input.ts +8 -181
  488. package/src/ui/form-controls.ts +838 -366
  489. package/src/ui/formula-chips.ts +80 -85
  490. package/src/ui/kit.ts +33 -0
  491. package/src/ui/layers.ts +293 -593
  492. package/src/ui/media-picker.ts +203 -284
  493. package/src/ui/panel-resize.ts +251 -64
  494. package/src/ui/popover-surface.ts +153 -0
  495. package/src/ui/progress-modal.ts +22 -60
  496. package/src/ui/regions.ts +13 -7
  497. package/src/ui/schema-form.ts +906 -440
  498. package/src/ui/surface.ts +122 -0
  499. package/src/ui/unit-selector.ts +15 -131
  500. package/src/ui/value-source.ts +14 -0
  501. package/src/ui/virtual-window.ts +88 -9
  502. package/src/utils/canvas-media.ts +72 -0
  503. package/src/utils/edit-display.ts +7 -2
  504. package/src/utils/element-selectors.ts +69 -0
  505. package/src/utils/link-target.ts +2 -2
  506. package/src/utils/studio-utils.ts +0 -10
  507. package/src/view.ts +15 -0
  508. package/src/workspace/pane-derive.ts +36 -14
  509. package/src/workspace/workspace.ts +15 -6
  510. package/styles/canvas.css +72 -467
  511. package/styles/forced-colors.css +48 -82
  512. package/styles/forced-colors.json +41 -0
  513. package/styles/inspector.css +2 -1301
  514. package/styles/overlays.css +96 -2823
  515. package/styles/panels.css +6 -1423
  516. package/styles/shell-frame.css +272 -0
  517. package/styles/shell-frame.json +227 -0
  518. package/styles/shell.css +0 -1532
  519. package/styles/tokens.css +65 -136
  520. package/styles/tokens.json +84 -0
  521. package/dist/chunks/Overlay.dev-723bv0df.js +0 -23
  522. package/dist/chunks/adapter-jkcyhgtr.js +0 -14
  523. package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
  524. package/dist/chunks/components-zmx10t62.js +0 -21
  525. package/dist/chunks/content-source-t88vb2dr.js +0 -49
  526. package/dist/chunks/convert-to-repeater-dhw1nrz9.js +0 -268
  527. package/dist/chunks/editor-cptq5jp1.js +0 -37
  528. package/dist/chunks/entry-commands-6z6bhxg0.js +0 -46
  529. package/dist/chunks/extension-sections-3ydgjbdy.js +0 -311
  530. package/dist/chunks/file-ops-rskmhxyq.js +0 -48
  531. package/dist/chunks/files-0cratsr0.js +0 -92
  532. package/dist/chunks/flow-qbfgrpeh.js +0 -634
  533. package/dist/chunks/focus-trap.esm-ggawf1hy.js +0 -1024
  534. package/dist/chunks/focus-visible-6eg70z18.js +0 -167
  535. package/dist/chunks/format-host-tgcmfs4p.js +0 -43
  536. package/dist/chunks/fs-events-hrrg328g.js +0 -21
  537. package/dist/chunks/git-panel-0yywyk76.js +0 -58
  538. package/dist/chunks/index-zqxmqxmh.js +0 -109
  539. package/dist/chunks/jx-validate-8q2g05x1.js +0 -19
  540. package/dist/chunks/layers-panel-z3tqe5cd.js +0 -67
  541. package/dist/chunks/layers-q75tj2mf.js +0 -45
  542. package/dist/chunks/lit-html-j055mkh8.js +0 -22
  543. package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
  544. package/dist/chunks/notify-yjm79n4s.js +0 -30
  545. package/dist/chunks/parse-24d0gay3.js +0 -19
  546. package/dist/chunks/platform-ba1j1gbh.js +0 -16
  547. package/dist/chunks/publish-panel-54jvhskq.js +0 -28
  548. package/dist/chunks/quick-search-cgfmjwq5.js +0 -65
  549. package/dist/chunks/references-3qx8fkyp.js +0 -29
  550. package/dist/chunks/right-panel-cqp8bc4v.js +0 -46
  551. package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
  552. package/dist/chunks/sp-overlay.dev-yztmtrs1.js +0 -18
  553. package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
  554. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
  555. package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +0 -15
  556. package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
  557. package/dist/chunks/studio-0a5qrh01.js +0 -3
  558. package/dist/chunks/studio-18j3bmcz.js +0 -14
  559. package/dist/chunks/studio-1hbqnt19.js +0 -1823
  560. package/dist/chunks/studio-4rzyrtjz.js +0 -426
  561. package/dist/chunks/studio-5rywrkxz.js +0 -1903
  562. package/dist/chunks/studio-7afd21nw.js +0 -67
  563. package/dist/chunks/studio-7nte2hj4.js +0 -613
  564. package/dist/chunks/studio-8jpzqk5s.js +0 -128
  565. package/dist/chunks/studio-8q517dfr.js +0 -24
  566. package/dist/chunks/studio-9c4k4vhv.js +0 -357
  567. package/dist/chunks/studio-9n4mrmnb.js +0 -184
  568. package/dist/chunks/studio-a0tswy0w.js +0 -58
  569. package/dist/chunks/studio-ahjfdy6e.js +0 -387
  570. package/dist/chunks/studio-ahnzd9rg.js +0 -23
  571. package/dist/chunks/studio-bw5dz6km.js +0 -608
  572. package/dist/chunks/studio-cb12yz9w.js +0 -15055
  573. package/dist/chunks/studio-d1ks410h.js +0 -175
  574. package/dist/chunks/studio-eqq1w638.js +0 -113
  575. package/dist/chunks/studio-ex8zyp98.js +0 -1813
  576. package/dist/chunks/studio-h4rxscfc.js +0 -10
  577. package/dist/chunks/studio-kyh9avvv.js +0 -28
  578. package/dist/chunks/studio-kyk2v92a.js +0 -659
  579. package/dist/chunks/studio-m3hwn7y1.js +0 -120
  580. package/dist/chunks/studio-mmv72a27.js +0 -4344
  581. package/dist/chunks/studio-n8b9wqwy.js +0 -291
  582. package/dist/chunks/studio-pdfjntbk.js +0 -6939
  583. package/dist/chunks/studio-qq6jym04.js +0 -53
  584. package/dist/chunks/studio-r1r7afxx.js +0 -1066
  585. package/dist/chunks/studio-rdx8jcxy.js +0 -83
  586. package/dist/chunks/studio-rn9ta2mz.js +0 -111
  587. package/dist/chunks/studio-t7qb9db8.js +0 -665
  588. package/dist/chunks/studio-tmc5emrw.js +0 -8
  589. package/dist/chunks/studio-txwkm6p9.js +0 -12
  590. package/dist/chunks/studio-v38hmvx1.js +0 -390
  591. package/dist/chunks/studio-vadzgkrz.js +0 -318
  592. package/dist/chunks/studio-wx5609xd.js +0 -341
  593. package/dist/chunks/studio-z6nw2ek2.js +0 -18
  594. package/dist/chunks/studio-zczps70q.js +0 -378
  595. package/dist/chunks/workspace-xpres40v.js +0 -89
  596. package/src/panels/activity-bar.ts +0 -251
  597. package/src/panels/panel-scheduler.ts +0 -219
  598. package/src/panels/style-inputs.ts +0 -237
  599. package/src/panels/target-line.ts +0 -274
  600. package/src/panels/welcome-screen.ts +0 -346
  601. package/src/ui/button-group.ts +0 -102
  602. package/src/ui/choice-field.ts +0 -72
  603. package/src/ui/field-row.ts +0 -249
  604. package/src/ui/formula-palette.ts +0 -213
  605. package/src/ui/icons.ts +0 -52
  606. package/src/ui/jx-theme.ts +0 -130
  607. package/src/ui/spectrum.ts +0 -340
  608. package/src/ui/value-selector.ts +0 -174
  609. package/src/ui/widgets.ts +0 -172
  610. /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
  611. /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
  612. /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
  613. /package/dist/chunks/{tsMode-ef187txx.css → tsMode-hnxxb73h.css} +0 -0
@@ -1,616 +1,653 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Definitions — the visual editor for project-level `$defs` (JSON Schema type definitions).
3
+ * Data Shapes — the editor for project-level `$defs` (JSON Schema type definitions).
4
4
  *
5
5
  * Manages entries in project.json `$defs` — reusable type schemas for external datasets, API
6
6
  * responses, CMS payloads, etc. Same concept as component-level `$defs` but scoped to the entire
7
- * project. The on-disk key stays `$defs` and the section key stays `definitions`.
7
+ * project. The on-disk key stays `$defs` and the section key stays `definitions`; the surface is
8
+ * named for neither, because a surface is named for what it draws.
8
9
  *
9
- * **The reference field type is complete here for the first time.** `schema-field-ui.ts` has always
10
- * been able to draw the target picker, and `ui/form-controls.ts` (the content-types builder) has
11
- * always passed it the available content types — this editor never did, so choosing "reference"
12
- * emitted a bare `#/content/` that pointed at nothing and offered no way to say what it pointed at.
13
- * The list is read straight off the live config's `content` map, which is where a
14
- * `#/content/<type>` pointer resolves.
10
+ * **The reference field type is complete here.** `schema-field-ui.ts` has always been able to draw
11
+ * the target picker, and `ui/form-controls.ts` (the content-types builder) has always passed it the
12
+ * available content types — this editor never did, so choosing "reference" emitted a bare
13
+ * `#/content/` that pointed at nothing and offered no way to say what it pointed at. The list is
14
+ * read straight off the live config's `content` map, which is where a `#/content/<type>` pointer
15
+ * resolves.
16
+ *
17
+ * **The markup left.** The section is the `settings-defs` surface (`surfaces/settings-defs.json`),
18
+ * mounted by `surfaces/settings-defs.ts`; what is here is the section itself — what a shape is,
19
+ * what a field may be renamed to, and what reaches `project.json`. `renderDefsEditor` is unchanged
20
+ * as a contract: the registry hands a container to a `render`, and this one mounts a document into
21
+ * it instead of rendering lit.
22
+ *
23
+ * Three things the conversion settled, and each was a defect rather than a translation:
24
+ *
25
+ * - **The field cards are this section's own now.** They were `schema-field-ui.ts`'s lit templates,
26
+ * shared with the content-types builder, and that sharing is what made every card a Spectrum one.
27
+ * That module is untouched and still draws the builder's cards; this section draws its own out of
28
+ * the kit, so the two surfaces can move one at a time. Nothing was deleted from it.
29
+ * - **The nested add row no longer reads itself.** It found its own name field and its own picker
30
+ * with `closest()` and `querySelector` at click time — a node found by selector is real only
31
+ * until the next render — so what the reader had typed lived in the DOM and nowhere else. Each
32
+ * object field now carries its draft in this section's own state ({@link DefsUi.drafts}), which
33
+ * is what lets a redraw arrive mid-typing without taking the half-typed name away.
34
+ * - **A refused rename snaps back because the scope moves, not because a handler writes to an
35
+ * input.** The lit version assigned `target.value = fieldName` from inside the change handler. A
36
+ * document's binding only writes when the scope value CHANGES, and after a refusal the scope
37
+ * still holds what is on disk — so every setter says what the control now holds first
38
+ * ({@link say}), and the snap-back is then a real move.
39
+ *
40
+ * @docs studio/projects/settings
15
41
  */
16
42
 
17
- import { html, render as litRender } from "lit-html";
18
43
  import { projectState } from "../store";
19
44
  import { commitProjectConfig } from "../tabs/project-config";
20
- import { addFieldFormTpl, detectFieldFormat, fieldCardTpl, schemaForType } from "./schema-field-ui";
21
-
22
- import type { FieldHandlers, SchemaProperty } from "./schema-field-ui.js";
45
+ import { camelToLabel } from "../utils/studio-utils";
46
+ import {
47
+ FIELD_TYPES,
48
+ FORMAT_OPTIONS,
49
+ detectFieldFormat,
50
+ detectFieldType,
51
+ schemaForType,
52
+ } from "./schema-field-ui";
53
+ import { mountDefsSurface } from "../surfaces/settings-defs";
54
+
55
+ import type { SchemaProperty } from "./schema-field-ui";
56
+ import type {
57
+ DefsActions,
58
+ DefsChoice,
59
+ DefsFieldView,
60
+ DefsSurfaceHandle,
61
+ DefsView,
62
+ NestedFieldView,
63
+ } from "../surfaces/settings-defs";
23
64
  import type { ContentTypeSchema, ContentTypeSchemaField } from "@jxsuite/schema/types";
24
65
 
25
- // ─── Module state ─────────────────────────────────────────────────────────────
66
+ // ─── The pickers ──────────────────────────────────────────────────────────────
26
67
 
27
- let selectedDef: string | null = null;
28
- let showAddField = false;
29
- let newFieldState = { format: "", name: "", required: false, type: "string" };
30
- let showNewDef = false;
31
- let newDefName = "";
68
+ /** The field types, as picker rows. One array, made once: an equal write re-runs no binding. */
69
+ const TYPE_ROWS: DefsChoice[] = FIELD_TYPES.map((type) => ({ label: type, value: type }));
32
70
 
33
- // ─── Persistence ──────────────────────────────────────────────────────────────
71
+ /** The formats, with the empty one named: a blank row says nothing about what choosing it does. */
72
+ const FORMAT_ROWS: DefsChoice[] = FORMAT_OPTIONS.map((format) => ({
73
+ label: format || "(none)",
74
+ value: format,
75
+ }));
34
76
 
35
- /**
36
- * Commit the configuration this editor has just mutated in place.
37
- *
38
- * Every handler below edits `projectState.projectConfig.$defs` directly and then says so here. The
39
- * predecessor was this module's own writer — `JSON.stringify(config, null, "\t")` straight to
40
- * `platform.writeFile`, called as `void saveProjectConfig()` at fourteen sites. It re-indented the
41
- * whole file (every `project.json` on disk uses two spaces), it recorded nothing an undo could
42
- * reach, and a rejected write became an unhandled rejection while the form went on showing a data
43
- * shape that was never saved.
44
- *
45
- * `commitProjectConfig` transacts the mutation onto the configuration document and reports its own
46
- * failures as Problems, so there is nothing left here to await or to swallow.
47
- */
48
- function persist(): void {
49
- void commitProjectConfig();
50
- }
77
+ /** What a new field starts as. */
78
+ const BLANK_FIELD = { format: "", name: "", required: false, type: "string" };
51
79
 
52
- // ─── Helpers ─────────────────────────────────────────────────────────────────
80
+ // ─── Per-container state ──────────────────────────────────────────────────────
53
81
 
54
- /** Get the selected $def schema object. */
55
- function getSelectedDef(): ContentTypeSchema | undefined {
56
- const config = projectState?.projectConfig;
57
- return config?.$defs?.[selectedDef as string] as ContentTypeSchema | undefined;
82
+ /** One object field's unsubmitted nested field. */
83
+ interface Draft {
84
+ name: string;
85
+ type: string;
58
86
  }
59
87
 
60
88
  /**
61
- * The content types a `reference` field can point at — the same list the content-types builder
62
- * resolves through `#/$context/content`, read here without a schema-form context to go through.
89
+ * What the reader is part-way through, per rendered container.
63
90
  *
64
- * @returns {string[]}
91
+ * Per container rather than per module, for the reason `general-settings.ts` and
92
+ * `contexts-section.ts` keep their errors that way: Project Settings is a pane document and a pane
93
+ * can be split, so two containers may be showing this section at once. Module state would make the
94
+ * second one take the first one's selection away the moment either was redrawn.
65
95
  */
66
- function contentTypeNames(): string[] {
67
- const content = (projectState?.projectConfig as Record<string, unknown> | null | undefined)
68
- ?.content;
69
- return content && typeof content === "object" && !Array.isArray(content)
70
- ? Object.keys(content)
71
- : [];
96
+ interface DefsUi {
97
+ selected: string | null;
98
+ newOpen: boolean;
99
+ newName: string;
100
+ addOpen: boolean;
101
+ add: { format: string; name: string; required: boolean; type: string };
102
+ /** Each object field's nested add row, by the field's name on disk. */
103
+ drafts: Map<string, Draft>;
104
+ /**
105
+ * The ONE control whose value has parted from what is on disk, and what it holds.
106
+ *
107
+ * One at a time is the whole truth of it: a `change` commits one control, and the decision about
108
+ * it is synchronous, so there is never a second control in flight. See {@link say}.
109
+ */
110
+ echo: { id: string; value: string | boolean } | null;
72
111
  }
73
112
 
74
- // ─── Handlers ─────────────────────────────────────────────────────────────────
75
-
76
- /** @param {() => void} rerender */
77
- function handleNewDef(rerender: () => void) {
78
- const name = newDefName.trim();
79
- if (!name) {
80
- return;
81
- }
82
-
83
- const config = projectState?.projectConfig;
84
- if (!config) {
85
- return;
86
- }
87
- if (!config.$defs) {
88
- config.$defs = {};
113
+ const uis = new WeakMap<HTMLElement, DefsUi>();
114
+
115
+ /** The mounted surface, per container — see {@link renderDefsEditor}. */
116
+ const mounts = new WeakMap<HTMLElement, DefsSurfaceHandle>();
117
+
118
+ /** This container's state, made on first use. */
119
+ function uiFor(container: HTMLElement): DefsUi {
120
+ let ui = uis.get(container);
121
+ if (!ui) {
122
+ ui = {
123
+ add: { ...BLANK_FIELD },
124
+ addOpen: false,
125
+ drafts: new Map(),
126
+ echo: null,
127
+ newName: "",
128
+ newOpen: false,
129
+ selected: null,
130
+ };
131
+ uis.set(container, ui);
89
132
  }
90
- if (config.$defs[name]) {
91
- return;
92
- } // Already exists
93
-
94
- config.$defs[name] = {
95
- properties: {},
96
- required: [],
97
- type: "object",
98
- };
99
-
100
- selectedDef = name;
101
- showNewDef = false;
102
- newDefName = "";
103
- rerender();
104
- persist();
133
+ return ui;
105
134
  }
106
135
 
107
- /** @param {() => void} rerender */
108
- function handleAddField(rerender: () => void) {
109
- const name = newFieldState.name.trim();
110
- if (!name || !selectedDef) {
111
- return;
112
- }
113
-
114
- const def = getSelectedDef();
115
- if (!def) {
116
- return;
117
- }
118
-
119
- if (!def.properties) {
120
- def.properties = {};
121
- }
122
- def.properties[name] = schemaForType(newFieldState.type, newFieldState.format || undefined);
123
-
124
- if (newFieldState.required) {
125
- if (!def.required) {
126
- def.required = [];
127
- }
128
- if (!def.required.includes(name)) {
129
- def.required.push(name);
130
- }
131
- }
132
-
133
- showAddField = false;
134
- newFieldState = { format: "", name: "", required: false, type: "string" };
135
- rerender();
136
- persist();
136
+ /** The id of a field's name control. */
137
+ function nameId(key: string): string {
138
+ return `name:${key}`;
137
139
  }
138
140
 
139
- /**
140
- * @param {string} fieldName
141
- * @param {() => void} rerender
142
- */
143
- function handleDeleteField(fieldName: string, rerender: () => void) {
144
- const def = getSelectedDef();
145
- if (!def?.properties) {
146
- return;
147
- }
148
-
149
- delete def.properties[fieldName];
150
- if (def.required) {
151
- def.required = def.required.filter((r: string) => r !== fieldName);
152
- }
153
-
154
- rerender();
155
- persist();
141
+ /** The id of a field's required switch. */
142
+ function requiredId(key: string): string {
143
+ return `required:${key}`;
156
144
  }
157
145
 
158
- /**
159
- * @param {string} fieldName
160
- * @param {() => void} rerender
161
- */
162
- function handleToggleRequired(fieldName: string, rerender: () => void) {
163
- const def = getSelectedDef();
164
- if (!def) {
165
- return;
166
- }
167
- if (!def.required) {
168
- def.required = [];
169
- }
170
-
171
- const idx = def.required.indexOf(fieldName);
172
- if (idx !== -1) {
173
- def.required.splice(idx, 1);
174
- } else {
175
- def.required.push(fieldName);
176
- }
177
-
178
- rerender();
179
- persist();
146
+ /** A child is addressed by both halves, because two objects may hold the same child name. */
147
+ function nestedId(parent: string, key: string): string {
148
+ return `${parent} ${key}`;
180
149
  }
181
150
 
182
151
  /**
183
- * @param {string} oldName
184
- * @param {string} newName
185
- * @param {() => void} rerender
152
+ * Say what a control now holds, before anything is decided about it.
153
+ *
154
+ * This is what `live()` did for the lit templates. A document's binding writes only when the SCOPE
155
+ * value changes, and after a refusal the scope still holds the value on disk — so without this
156
+ * nothing is written back and the refused text stays in the field. Echoing first makes the
157
+ * projection that follows a real change.
186
158
  */
187
- function handleRenameField(oldName: string, newName: string, rerender: () => void) {
188
- const def = getSelectedDef();
189
- if (!def?.properties || !newName || def.properties[newName]) {
190
- return;
191
- }
192
-
193
- const newProps: Record<string, ContentTypeSchemaField> = {};
194
- for (const [key, val] of Object.entries(def.properties)) {
195
- newProps[key === oldName ? newName : key] = val;
196
- }
197
- def.properties = newProps;
198
-
199
- if (def.required) {
200
- def.required = def.required.map((r: string) => (r === oldName ? newName : r));
201
- }
202
-
203
- rerender();
204
- persist();
159
+ function say(container: HTMLElement, id: string, value: string | boolean): void {
160
+ uiFor(container).echo = { id, value };
161
+ renderDefsEditor(container);
205
162
  }
206
163
 
207
- /**
208
- * @param {string} fieldName
209
- * @param {string} newType
210
- * @param {() => void} rerender
211
- */
212
- function handleChangeType(fieldName: string, newType: string, rerender: () => void) {
213
- const def = getSelectedDef();
214
- if (!def?.properties?.[fieldName]) {
215
- return;
216
- }
217
-
218
- const oldFormat =
219
- newType === "string" || newType === "array"
220
- ? detectFieldFormat(def.properties[fieldName])
221
- : undefined;
222
- def.properties[fieldName] = schemaForType(newType, oldFormat || undefined);
223
- rerender();
224
- persist();
164
+ /** Forget the echo: every control shows what the file says again. */
165
+ function hush(container: HTMLElement): void {
166
+ uiFor(container).echo = null;
225
167
  }
226
168
 
227
- /**
228
- * Point a reference field at a content type. The pointer form is `#/content/<type>` — the same one
229
- * `ui/form-controls.ts` writes, so a reference authored here and one authored in the content-types
230
- * builder are the same value.
231
- *
232
- * @param {string} fieldName
233
- * @param {string} target
234
- * @param {() => void} rerender
235
- */
236
- function handleChangeRefTarget(fieldName: string, target: string, rerender: () => void) {
237
- const def = getSelectedDef();
238
- if (!def?.properties?.[fieldName]) {
239
- return;
240
- }
241
- def.properties[fieldName] = { $ref: `#/content/${target}` } as ContentTypeSchemaField;
242
- rerender();
243
- persist();
169
+ /** What a control shows: what it was last told it holds, or what is on disk. */
170
+ function shown<T extends string | boolean>(ui: DefsUi, id: string, disk: T): T {
171
+ return ui.echo?.id === id ? (ui.echo.value as T) : disk;
244
172
  }
245
173
 
174
+ // ─── Persistence ──────────────────────────────────────────────────────────────
175
+
246
176
  /**
247
- * @param {string} fieldName
248
- * @param {string} format
249
- * @param {() => void} rerender
177
+ * Commit the configuration this editor has just mutated in place.
178
+ *
179
+ * Every handler below edits `projectState.projectConfig.$defs` directly and then says so here. The
180
+ * predecessor was this module's own writer — `JSON.stringify(config, null, "\t")` straight to
181
+ * `platform.writeFile`, called as `void saveProjectConfig()` at fourteen sites. It re-indented the
182
+ * whole file (every `project.json` on disk uses two spaces), it recorded nothing an undo could
183
+ * reach, and a rejected write became an unhandled rejection while the form went on showing a data
184
+ * shape that was never saved.
185
+ *
186
+ * `commitProjectConfig` transacts the mutation onto the configuration document and reports its own
187
+ * failures as Problems, so there is nothing left here to await or to swallow.
250
188
  */
251
- function handleChangeFormat(fieldName: string, format: string, rerender: () => void) {
252
- const def = getSelectedDef();
253
- if (!def?.properties?.[fieldName]) {
254
- return;
255
- }
256
-
257
- const prop = def.properties[fieldName];
258
- const type = prop.type || "string";
259
- def.properties[fieldName] = schemaForType(type, format || undefined);
260
- rerender();
261
- persist();
189
+ function persist(): void {
190
+ void commitProjectConfig();
262
191
  }
263
192
 
264
- // ─── Nested field handlers ───────────────────────────────────────────────────
193
+ // ─── The project, as it stands now ────────────────────────────────────────────
265
194
 
266
195
  /**
267
- * @param {string} parentName
268
- * @param {{ name: string; type: string; required: boolean }} fieldState
269
- * @param {() => void} rerender
196
+ * The live `$defs`, read at the moment it is needed.
197
+ *
198
+ * The lit version closed over the config at render time and rebuilt every handler on every render,
199
+ * so a stale closure was impossible only because there were no long-lived ones. The handlers now
200
+ * outlive a projection, which makes reading through to the store the thing that keeps them honest.
270
201
  */
271
- function handleAddNestedField(
272
- parentName: string,
273
- fieldState: { name: string; type: string; required: boolean },
274
- rerender: () => void,
275
- ) {
276
- const def = getSelectedDef();
277
- const parent = def?.properties?.[parentName];
278
- if (!parent) {
279
- return;
280
- }
281
-
282
- if (!parent.properties) {
283
- parent.properties = {};
284
- }
285
- parent.properties[fieldState.name] = schemaForType(fieldState.type);
286
-
287
- if (fieldState.required) {
288
- if (!parent.required) {
289
- parent.required = [];
290
- }
291
- if (!parent.required.includes(fieldState.name)) {
292
- parent.required.push(fieldState.name);
293
- }
294
- }
202
+ function currentDefs(): Record<string, ContentTypeSchema> | undefined {
203
+ return projectState?.projectConfig?.$defs as Record<string, ContentTypeSchema> | undefined;
204
+ }
295
205
 
296
- rerender();
297
- persist();
206
+ /** The selected shape's schema, or `undefined` when nothing is selected or it has gone. */
207
+ function selectedDef(ui: DefsUi): ContentTypeSchema | undefined {
208
+ return ui.selected === null ? undefined : currentDefs()?.[ui.selected];
298
209
  }
299
210
 
300
211
  /**
301
- * @param {string} parentName
302
- * @param {string} childName
303
- * @param {() => void} rerender
212
+ * The content types a `reference` field can point at — the same list the content-types builder
213
+ * resolves through `#/$context/content`, read here without a schema-form context to go through.
304
214
  */
305
- function handleDeleteNested(parentName: string, childName: string, rerender: () => void) {
306
- const def = getSelectedDef();
307
- const parent = def?.properties?.[parentName];
308
- if (!parent?.properties) {
309
- return;
310
- }
311
-
312
- delete parent.properties[childName];
313
- if (parent.required) {
314
- parent.required = parent.required.filter((r: string) => r !== childName);
315
- }
316
-
317
- rerender();
318
- persist();
215
+ function contentTypeNames(): string[] {
216
+ const content = (projectState?.projectConfig as Record<string, unknown> | null | undefined)
217
+ ?.content;
218
+ return content && typeof content === "object" && !Array.isArray(content)
219
+ ? Object.keys(content)
220
+ : [];
319
221
  }
320
222
 
321
- /**
322
- * @param {string} parentName
323
- * @param {string} childName
324
- * @param {() => void} rerender
325
- */
326
- function handleToggleNestedRequired(parentName: string, childName: string, rerender: () => void) {
327
- const def = getSelectedDef();
328
- const parent = def?.properties?.[parentName];
329
- if (!parent) {
330
- return;
331
- }
332
- if (!parent.required) {
333
- parent.required = [];
334
- }
223
+ /** The last target list, so an unchanged one is the same array and re-runs no binding. */
224
+ let targetRows: DefsChoice[] = [];
225
+ let targetNames = " ";
335
226
 
336
- const idx = parent.required.indexOf(childName);
337
- if (idx !== -1) {
338
- parent.required.splice(idx, 1);
339
- } else {
340
- parent.required.push(childName);
227
+ /** The reference targets, as picker rows. */
228
+ function targets(): DefsChoice[] {
229
+ const names = contentTypeNames();
230
+ const id = names.join(" ");
231
+ if (id !== targetNames) {
232
+ targetNames = id;
233
+ targetRows = names.map((name) => ({ label: name, value: name }));
341
234
  }
342
-
343
- rerender();
344
- persist();
235
+ return targetRows;
345
236
  }
346
237
 
347
- /**
348
- * @param {string} parentName
349
- * @param {string} oldChild
350
- * @param {string} newChild
351
- * @param {() => void} rerender
352
- */
353
- function handleRenameNested(
354
- parentName: string,
355
- oldChild: string,
356
- newChild: string,
357
- rerender: () => void,
358
- ) {
359
- const def = getSelectedDef();
360
- const parent = def?.properties?.[parentName];
361
- if (!parent?.properties || !newChild || parent.properties[newChild]) {
362
- return;
363
- }
238
+ // ─── Projection ───────────────────────────────────────────────────────────────
364
239
 
365
- const newProps: Record<string, ContentTypeSchemaField> = {};
366
- for (const [key, val] of Object.entries(parent.properties)) {
367
- newProps[key === oldChild ? newChild : key] = val;
368
- }
369
- parent.properties = newProps;
370
-
371
- if (parent.required) {
372
- parent.required = parent.required.map((r: string) => (r === oldChild ? newChild : r));
373
- }
240
+ /** Whether a type carries a format at all — only `string` and `array` do. */
241
+ function takesFormat(type: string): boolean {
242
+ return type === "string" || type === "array";
243
+ }
374
244
 
375
- rerender();
376
- persist();
245
+ /** One child of an object field, as the document draws it. */
246
+ function nestedView(
247
+ ui: DefsUi,
248
+ parent: string,
249
+ key: string,
250
+ schema: SchemaProperty,
251
+ required: boolean,
252
+ ): NestedFieldView {
253
+ const id = nestedId(parent, key);
254
+ const type = detectFieldType(schema);
255
+ return {
256
+ format: detectFieldFormat(schema),
257
+ hasFormat: takesFormat(type),
258
+ key,
259
+ name: shown(ui, nameId(id), key),
260
+ parent,
261
+ required: shown(ui, requiredId(id), required),
262
+ type,
263
+ };
377
264
  }
378
265
 
379
- /**
380
- * @param {string} parentName
381
- * @param {string} childName
382
- * @param {string} newType
383
- * @param {() => void} rerender
384
- */
385
- function handleChangeNestedType(
386
- parentName: string,
387
- childName: string,
388
- newType: string,
389
- rerender: () => void,
390
- ) {
391
- const def = getSelectedDef();
392
- const parent = def?.properties?.[parentName];
393
- if (!parent?.properties?.[childName]) {
394
- return;
395
- }
266
+ /** One field of the selected shape, as the document draws it. */
267
+ function fieldView(
268
+ ui: DefsUi,
269
+ key: string,
270
+ schema: SchemaProperty,
271
+ required: boolean,
272
+ hasTargets: boolean,
273
+ ): DefsFieldView {
274
+ const type = detectFieldType(schema);
275
+ const nested = type === "object";
276
+ const draft = ui.drafts.get(key);
277
+ const childRequired = schema.required ?? [];
278
+ return {
279
+ children: nested
280
+ ? Object.entries(schema.properties ?? {}).map(([child, sub]) =>
281
+ nestedView(ui, key, child, sub, childRequired.includes(child)),
282
+ )
283
+ : [],
284
+ draftName: draft?.name ?? "",
285
+ draftType: draft?.type ?? "string",
286
+ /* A reference with nothing to point at draws no picker: a control whose list is empty offers
287
+ the reader no answer, which is why the lit card gated on the same thing. */
288
+ extra: type === "reference" && hasTargets ? "reference" : nested ? "nested" : "none",
289
+ format: detectFieldFormat(schema),
290
+ hasFormat: takesFormat(type),
291
+ key,
292
+ label: camelToLabel(key),
293
+ name: shown(ui, nameId(key), key),
294
+ refTarget: schema.$ref ? schema.$ref.replace(/^#\/[^/]+\//, "") : "",
295
+ required: shown(ui, requiredId(key), required),
296
+ type,
297
+ };
298
+ }
396
299
 
397
- const oldFormat =
398
- newType === "string" || newType === "array"
399
- ? detectFieldFormat(parent.properties[childName])
400
- : undefined;
401
- parent.properties[childName] = schemaForType(newType, oldFormat || undefined);
402
- rerender();
403
- persist();
300
+ /** The whole section, as one value the document can be handed. */
301
+ function project(container: HTMLElement): DefsView {
302
+ const ui = uiFor(container);
303
+ const defs = currentDefs() ?? {};
304
+ const def = selectedDef(ui);
305
+ const rows = targets();
306
+ const properties = def?.properties ?? {};
307
+ const required = def?.required ?? [];
308
+ return {
309
+ addFormat: ui.add.format,
310
+ addHasFormat: takesFormat(ui.add.type),
311
+ addName: ui.add.name,
312
+ addRequired: ui.add.required,
313
+ addState: ui.addOpen && def ? "open" : "closed",
314
+ addType: ui.add.type,
315
+ editorState: def ? "editing" : "empty",
316
+ fields: Object.entries(properties).map(([key, schema]) =>
317
+ fieldView(ui, key, schema as SchemaProperty, required.includes(key), rows.length > 0),
318
+ ),
319
+ formatOptions: FORMAT_ROWS,
320
+ newName: ui.newName,
321
+ newState: ui.newOpen ? "open" : "closed",
322
+ selected: def && ui.selected !== null ? ui.selected : "",
323
+ shapes: Object.keys(defs).map((name) => ({ name, selected: name === ui.selected })),
324
+ targets: rows,
325
+ typeOptions: TYPE_ROWS,
326
+ };
404
327
  }
405
328
 
329
+ // ─── The decisions ────────────────────────────────────────────────────────────
330
+
406
331
  /**
407
- * @param {string} parentName
408
- * @param {string} childName
409
- * @param {string} format
410
- * @param {() => void} rerender
332
+ * What every control in one container does. Built once, when that container's surface is mounted,
333
+ * and read through to the store on every call.
411
334
  */
412
- function handleChangeNestedFormat(
413
- parentName: string,
414
- childName: string,
415
- format: string,
416
- rerender: () => void,
417
- ) {
418
- const def = getSelectedDef();
419
- const parent = def?.properties?.[parentName];
420
- if (!parent?.properties?.[childName]) {
421
- return;
422
- }
335
+ function actionsFor(container: HTMLElement): DefsActions {
336
+ const ui = uiFor(container);
337
+ const redraw = (): void => renderDefsEditor(container);
338
+
339
+ /** The parent object a nested edit is about, or `undefined` when it has gone underneath. */
340
+ const parentOf = (parent: string): SchemaProperty | undefined =>
341
+ selectedDef(ui)?.properties?.[parent] as SchemaProperty | undefined;
342
+
343
+ /** One object field's add-row draft, made on first use. */
344
+ const draftOf = (parent: string): Draft => {
345
+ let draft = ui.drafts.get(parent);
346
+ if (!draft) {
347
+ draft = { name: "", type: "string" };
348
+ ui.drafts.set(parent, draft);
349
+ }
350
+ return draft;
351
+ };
423
352
 
424
- const prop = parent.properties[childName];
425
- const type = prop.type || "string";
426
- parent.properties[childName] = schemaForType(type, format || undefined);
427
- rerender();
428
- persist();
429
- }
353
+ /** A property map with one key renamed, rebuilt in place so a rename never reorders the list. */
354
+ const renameKey = <T>(properties: Record<string, T>, from: string, to: string): void => {
355
+ const next: Record<string, T> = {};
356
+ for (const [key, value] of Object.entries(properties)) {
357
+ next[key === from ? to : key] = value;
358
+ delete properties[key];
359
+ }
360
+ Object.assign(properties, next);
361
+ };
430
362
 
431
- /** @param {() => void} rerender */
432
- function handleDeleteDef(rerender: () => void) {
433
- if (!selectedDef) {
434
- return;
435
- }
436
- const config = projectState?.projectConfig;
437
- if (!config?.$defs?.[selectedDef]) {
438
- return;
439
- }
363
+ /** Add or remove a name from a `required` list, made on first use. */
364
+ const markRequired = (
365
+ holder: { required?: string[] | undefined },
366
+ key: string,
367
+ required: boolean,
368
+ ): void => {
369
+ if (!holder.required) {
370
+ holder.required = [];
371
+ }
372
+ const at = holder.required.indexOf(key);
373
+ if (required && at === -1) {
374
+ holder.required.push(key);
375
+ }
376
+ if (!required && at !== -1) {
377
+ holder.required.splice(at, 1);
378
+ }
379
+ };
380
+
381
+ /** A refusal: the control goes back to what the file says, and nothing is written. */
382
+ const refuse = (): void => {
383
+ hush(container);
384
+ redraw();
385
+ };
440
386
 
441
- delete config.$defs[selectedDef];
442
- selectedDef = null;
387
+ /** A write went through: forget the echo, redraw from the file, and commit it. */
388
+ const done = (): void => {
389
+ hush(container);
390
+ redraw();
391
+ persist();
392
+ };
443
393
 
444
- rerender();
445
- persist();
394
+ return {
395
+ addNested(parent) {
396
+ const draft = draftOf(parent);
397
+ const name = draft.name.trim();
398
+ const holder = parentOf(parent);
399
+ if (!name || !holder) {
400
+ return;
401
+ }
402
+ if (!holder.properties) {
403
+ holder.properties = {};
404
+ }
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 case,
407
+ and re-choosing the type each time would be the editor asking what it just heard. */
408
+ draft.name = "";
409
+ done();
410
+ },
411
+ cancelAdd() {
412
+ ui.addOpen = false;
413
+ ui.add = { ...BLANK_FIELD };
414
+ redraw();
415
+ },
416
+ cancelNew() {
417
+ ui.newOpen = false;
418
+ ui.newName = "";
419
+ redraw();
420
+ },
421
+ confirmAdd() {
422
+ const name = ui.add.name.trim();
423
+ const def = selectedDef(ui);
424
+ if (!name || !def) {
425
+ return;
426
+ }
427
+ if (!def.properties) {
428
+ def.properties = {};
429
+ }
430
+ def.properties[name] = schemaForType(ui.add.type, ui.add.format || undefined);
431
+ if (ui.add.required) {
432
+ markRequired(def, name, true);
433
+ }
434
+ ui.addOpen = false;
435
+ ui.add = { ...BLANK_FIELD };
436
+ done();
437
+ },
438
+ createNew() {
439
+ const name = ui.newName.trim();
440
+ const config = projectState?.projectConfig;
441
+ if (!name || !config) {
442
+ return;
443
+ }
444
+ if (!config.$defs) {
445
+ config.$defs = {};
446
+ }
447
+ if (config.$defs[name]) {
448
+ return;
449
+ }
450
+ config.$defs[name] = { properties: {}, required: [], type: "object" };
451
+ ui.selected = name;
452
+ ui.newOpen = false;
453
+ ui.newName = "";
454
+ done();
455
+ },
456
+ editAddFormat(value) {
457
+ ui.add.format = value;
458
+ redraw();
459
+ },
460
+ editAddName(value) {
461
+ ui.add.name = value;
462
+ redraw();
463
+ },
464
+ editAddRequired(required) {
465
+ ui.add.required = required;
466
+ redraw();
467
+ },
468
+ editAddType(value) {
469
+ ui.add.type = value;
470
+ redraw();
471
+ },
472
+ editDraftName(parent, value) {
473
+ draftOf(parent).name = value;
474
+ redraw();
475
+ },
476
+ editDraftType(parent, value) {
477
+ draftOf(parent).type = value;
478
+ redraw();
479
+ },
480
+ editNew(value) {
481
+ ui.newName = value;
482
+ redraw();
483
+ },
484
+ openAdd() {
485
+ ui.addOpen = true;
486
+ ui.add = { ...BLANK_FIELD };
487
+ redraw();
488
+ },
489
+ openNew() {
490
+ ui.newOpen = true;
491
+ ui.newName = "";
492
+ redraw();
493
+ },
494
+ removeField(key) {
495
+ const def = selectedDef(ui);
496
+ if (!def?.properties) {
497
+ return;
498
+ }
499
+ delete def.properties[key];
500
+ if (def.required) {
501
+ def.required = def.required.filter((name) => name !== key);
502
+ }
503
+ done();
504
+ },
505
+ removeNested(parent, key) {
506
+ const holder = parentOf(parent);
507
+ if (!holder?.properties) {
508
+ return;
509
+ }
510
+ delete holder.properties[key];
511
+ if (holder.required) {
512
+ holder.required = holder.required.filter((name) => name !== key);
513
+ }
514
+ done();
515
+ },
516
+ removeShape() {
517
+ const defs = currentDefs();
518
+ if (ui.selected === null || !defs?.[ui.selected]) {
519
+ return;
520
+ }
521
+ delete defs[ui.selected];
522
+ ui.selected = null;
523
+ ui.addOpen = false;
524
+ done();
525
+ },
526
+ renameField(key, value) {
527
+ say(container, nameId(key), value);
528
+ const def = selectedDef(ui);
529
+ const name = value.trim();
530
+ if (!def?.properties || !name || name === key || def.properties[name]) {
531
+ refuse();
532
+ return;
533
+ }
534
+ renameKey(def.properties, key, name);
535
+ if (def.required) {
536
+ def.required = def.required.map((entry) => (entry === key ? name : entry));
537
+ }
538
+ done();
539
+ },
540
+ renameNested(parent, key, value) {
541
+ say(container, nameId(nestedId(parent, key)), value);
542
+ const holder = parentOf(parent);
543
+ const name = value.trim();
544
+ if (!holder?.properties || !name || name === key || holder.properties[name]) {
545
+ refuse();
546
+ return;
547
+ }
548
+ renameKey(holder.properties, key, name);
549
+ if (holder.required) {
550
+ holder.required = holder.required.map((entry) => (entry === key ? name : entry));
551
+ }
552
+ done();
553
+ },
554
+ select(name) {
555
+ ui.selected = name;
556
+ ui.addOpen = false;
557
+ redraw();
558
+ },
559
+ setFormat(key, value) {
560
+ const def = selectedDef(ui);
561
+ const property = def?.properties?.[key];
562
+ if (!def?.properties || !property) {
563
+ return;
564
+ }
565
+ def.properties[key] = schemaForType(property.type || "string", value || undefined);
566
+ done();
567
+ },
568
+ setNestedFormat(parent, key, value) {
569
+ const holder = parentOf(parent);
570
+ const property = holder?.properties?.[key];
571
+ if (!holder?.properties || !property) {
572
+ return;
573
+ }
574
+ holder.properties[key] = schemaForType(property.type || "string", value || undefined);
575
+ done();
576
+ },
577
+ setNestedRequired(parent, key, required) {
578
+ say(container, requiredId(nestedId(parent, key)), required);
579
+ const holder = parentOf(parent);
580
+ if (!holder) {
581
+ refuse();
582
+ return;
583
+ }
584
+ markRequired(holder, key, required);
585
+ done();
586
+ },
587
+ setNestedType(parent, key, value) {
588
+ const holder = parentOf(parent);
589
+ const property = holder?.properties?.[key];
590
+ if (!holder?.properties || !property) {
591
+ return;
592
+ }
593
+ /* A format survives a move between the two types that carry one, and is dropped by any type
594
+ that does not: `schemaForType` is handed the old format only when the new type takes it. */
595
+ const format = takesFormat(value) ? detectFieldFormat(property) : undefined;
596
+ holder.properties[key] = schemaForType(value, format || undefined);
597
+ done();
598
+ },
599
+ setRequired(key, required) {
600
+ say(container, requiredId(key), required);
601
+ const def = selectedDef(ui);
602
+ if (!def) {
603
+ refuse();
604
+ return;
605
+ }
606
+ markRequired(def, key, required);
607
+ done();
608
+ },
609
+ setTarget(key, value) {
610
+ const def = selectedDef(ui);
611
+ if (!def?.properties?.[key]) {
612
+ return;
613
+ }
614
+ /* The pointer form is `#/content/<type>` — the same one `ui/form-controls.ts` writes, so a
615
+ reference authored here and one authored in the content-types builder are the same value. */
616
+ def.properties[key] = { $ref: `#/content/${value}` } as ContentTypeSchemaField;
617
+ done();
618
+ },
619
+ setType(key, value) {
620
+ const def = selectedDef(ui);
621
+ const property = def?.properties?.[key];
622
+ if (!def?.properties || !property) {
623
+ return;
624
+ }
625
+ const format = takesFormat(value) ? detectFieldFormat(property) : undefined;
626
+ def.properties[key] = schemaForType(value, format || undefined);
627
+ done();
628
+ },
629
+ };
446
630
  }
447
631
 
448
632
  // ─── Render ───────────────────────────────────────────────────────────────────
449
633
 
450
634
  /**
451
- * Render the Data Shapes editor.
635
+ * Render Project Settings › Data Shapes into `container`.
636
+ *
637
+ * One mount per container, re-used for every subsequent call. The host calls this again for any
638
+ * change it notices, and a section whose container has meanwhile been given to a DIFFERENT section
639
+ * finds its document gone — `litRender` into the same node replaces everything in it — so the mount
640
+ * is rebuilt exactly when it has actually been evicted, and disposed on the way out rather than
641
+ * left running over detached nodes.
452
642
  *
453
643
  * @param {HTMLElement} container
454
644
  */
455
- export function renderDefsEditor(container: HTMLElement) {
456
- const rerender = () => renderDefsEditor(container);
457
- const config = projectState?.projectConfig;
458
- const defs = config?.$defs || {};
459
- const defNames = Object.keys(defs);
460
-
461
- // Left column — def list
462
- const listTpl = html`
463
- <div class="settings-list-panel">
464
- ${defNames.map(
465
- (name) => html`
466
- <sp-action-button
467
- size="s"
468
- ?selected=${selectedDef === name}
469
- @click=${() => {
470
- selectedDef = name;
471
- showAddField = false;
472
- rerender();
473
- }}
474
- >
475
- ${name}
476
- </sp-action-button>
477
- `,
478
- )}
479
- ${
480
- showNewDef
481
- ? html`
482
- <div class="settings-inline-form">
483
- <sp-textfield
484
- size="s"
485
- placeholder="ProductReview"
486
- .value=${newDefName}
487
- @input=${(e: Event) => {
488
- newDefName = (e.target as HTMLInputElement).value;
489
- }}
490
- @keydown=${(e: KeyboardEvent) => {
491
- if (e.key === "Enter") {
492
- handleNewDef(rerender);
493
- }
494
- if (e.key === "Escape") {
495
- showNewDef = false;
496
- rerender();
497
- }
498
- }}
499
- ></sp-textfield>
500
- <sp-action-button size="s" @click=${() => handleNewDef(rerender)}>
501
- Create
502
- </sp-action-button>
503
- </div>
504
- `
505
- : html`
506
- <sp-action-button
507
- size="s"
508
- quiet
509
- @click=${() => {
510
- showNewDef = true;
511
- rerender();
512
- }}
513
- >
514
- <sp-icon-add slot="icon"></sp-icon-add> New Data Shape
515
- </sp-action-button>
516
- `
517
- }
518
- </div>
519
- `;
520
-
521
- // Right column — schema editor
522
- let editorTpl;
523
- if (!selectedDef || !defs[selectedDef]) {
524
- editorTpl = html`<div class="settings-empty-state">
525
- Pick a data shape on the left, or create one.
526
- </div>`;
527
- } else {
528
- const def = defs[selectedDef] as ContentTypeSchema;
529
- const properties = def.properties || {};
530
- const required = def.required || [];
531
-
532
- const handlers: FieldHandlers = {
533
- onAddNestedField: (p: string, s: { name: string; type: string; required: boolean }) =>
534
- handleAddNestedField(p, s, rerender),
535
- onChangeFormat: (n: string, f: string) => handleChangeFormat(n, f, rerender),
536
- onChangeNestedFormat: (p: string, c: string, f: string) =>
537
- handleChangeNestedFormat(p, c, f, rerender),
538
- onChangeNestedType: (p: string, c: string, t: string) =>
539
- handleChangeNestedType(p, c, t, rerender),
540
- onChangeRefTarget: (n: string, t: string) => handleChangeRefTarget(n, t, rerender),
541
- onChangeType: (n: string, t: string) => handleChangeType(n, t, rerender),
542
- onDelete: (n: string) => handleDeleteField(n, rerender),
543
- onDeleteNested: (p: string, c: string) => handleDeleteNested(p, c, rerender),
544
- onRename: (oldN: string, newN: string) => handleRenameField(oldN, newN, rerender),
545
- onRenameNested: (p: string, o: string, n: string) => handleRenameNested(p, o, n, rerender),
546
- onToggleNestedRequired: (p: string, c: string) => handleToggleNestedRequired(p, c, rerender),
547
- onToggleRequired: (n: string) => handleToggleRequired(n, rerender),
548
- };
549
-
550
- const targets = contentTypeNames();
551
- const fieldCards = Object.entries(properties).map(([name, fieldDef]) =>
552
- fieldCardTpl(name, fieldDef as SchemaProperty, required.includes(name), handlers, targets),
553
- );
554
-
555
- editorTpl = html`
556
- <div class="settings-editor-panel">
557
- <div class="settings-editor-header">
558
- <h3>${selectedDef}</h3>
559
- <sp-action-button
560
- size="xs"
561
- quiet
562
- title="Delete data shape"
563
- @click=${() => handleDeleteDef(rerender)}
564
- >
565
- <sp-icon-delete slot="icon"></sp-icon-delete>
566
- </sp-action-button>
567
- </div>
568
- <div class="schema-field-list">${fieldCards}</div>
569
- ${
570
- showAddField
571
- ? addFieldFormTpl(newFieldState, {
572
- onCancel: () => {
573
- showAddField = false;
574
- newFieldState = {
575
- format: "",
576
- name: "",
577
- required: false,
578
- type: "string",
579
- };
580
- rerender();
581
- },
582
- onConfirm: () => handleAddField(rerender),
583
- onInput: (field, value) => {
584
- newFieldState = { ...newFieldState, [field]: value };
585
- rerender();
586
- },
587
- })
588
- : html`
589
- <sp-action-button
590
- size="s"
591
- quiet
592
- @click=${() => {
593
- showAddField = true;
594
- rerender();
595
- }}
596
- >
597
- <sp-icon-add slot="icon"></sp-icon-add> Add Field
598
- </sp-action-button>
599
- `
600
- }
601
- </div>
602
- `;
603
- }
604
-
605
- // Every section names itself in an <h3> matching its nav entry (Overview, Contexts, Head,
606
- // Packages, Extensions, Deploy, Raw JSON). This one did not, so it was the only section whose
607
- // Body never said what the reader had clicked.
608
- const tpl = html`
609
- <div class="settings-section">
610
- <h3 class="settings-section-title">Data Shapes</h3>
611
- <div class="settings-two-col">${listTpl} ${editorTpl}</div>
612
- </div>
613
- `;
614
-
615
- litRender(tpl, container);
645
+ export function renderDefsEditor(container: HTMLElement): void {
646
+ let handle = mounts.get(container);
647
+ if (!handle?.attached()) {
648
+ handle?.dispose();
649
+ handle = mountDefsSurface(container, actionsFor(container));
650
+ mounts.set(container, handle);
651
+ }
652
+ handle.update(project(container));
616
653
  }