@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,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
  }