@jxsuite/studio 6.1.2 → 8.0.1

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 (522) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/2020-5wtzce5g.js +4 -0
  3. package/dist/chunks/adapter-5tphg7xe.js +4 -0
  4. package/dist/chunks/add-new-flow-psym14sn.js +4 -0
  5. package/dist/chunks/cf-account-picker-haqazq5r.js +4 -0
  6. package/dist/chunks/cf-settings-h2wrh6hq.js +4 -0
  7. package/dist/chunks/{color-area-0fj8a4ga.js → color-area-6rbra5dy.js} +2 -2
  8. package/dist/chunks/{color-field-xch8kx2g.js → color-field-gy56c94g.js} +3 -3
  9. package/dist/chunks/color-m7r1vk54.js +4 -0
  10. package/dist/chunks/{color-slider-jxd8v350.js → color-slider-sqqdyf6c.js} +2 -2
  11. package/dist/chunks/{combobox-p0efwjke.js → combobox-72h32ejk.js} +3 -3
  12. package/dist/chunks/components-r22xca09.js +4 -0
  13. package/dist/chunks/content-source-mmppmcyg.js +4 -0
  14. package/dist/chunks/convert-to-repeater-he70fc2b.js +4 -0
  15. package/dist/chunks/editor-ccd9jwsa.js +4 -0
  16. package/dist/chunks/entry-commands-6qxz9e8p.js +4 -0
  17. package/dist/chunks/extension-sections-0egb88dm.js +4 -0
  18. package/dist/chunks/file-ops-qnab36ek.js +4 -0
  19. package/dist/chunks/files-rcn56qvm.js +4 -0
  20. package/dist/chunks/format-host-yxz0kjjs.js +4 -0
  21. package/dist/chunks/fs-events-d1qxq4z4.js +4 -0
  22. package/dist/chunks/grid-open-4z1g062t.js +4 -0
  23. package/dist/chunks/{grid-view-d2wq9ezg.js → grid-view-qg1b5eqx.js} +25 -25
  24. package/dist/chunks/{icons-pnfz478q.js → icons-63bjtbs7.js} +2 -2
  25. package/dist/chunks/{iframe-entry-jdcktez8.js → iframe-entry-2zggzajv.js} +4 -4
  26. package/dist/chunks/{iframe-entry-zap785cw.js → iframe-entry-76j8vwwx.js} +4 -4
  27. package/dist/chunks/iframe-entry-99nr5zfb.js +5 -0
  28. package/dist/chunks/{iframe-entry-x0ygf2yr.js → iframe-entry-pkgmcgp0.js} +4 -4
  29. package/dist/chunks/iframe-host-ssv7kyj0.js +4 -0
  30. package/dist/chunks/{index-zpjmzqfk.js → index-32eqzv1k.js} +3 -3
  31. package/dist/chunks/index-f0jn7nnp.js +4 -0
  32. package/dist/chunks/{javascript-m54mnwp6.js → javascript-pjp734q4.js} +3 -3
  33. package/dist/chunks/jsonMode-njkvsa02.js +13 -0
  34. package/dist/chunks/jx-validate-ttasrbh7.js +4 -0
  35. package/dist/chunks/layers-bgyr11tr.js +4 -0
  36. package/dist/chunks/layers-panel-7ez66zeq.js +4 -0
  37. package/dist/chunks/{listbox-e74qjyq6.js → listbox-c421cf8c.js} +2 -2
  38. package/dist/chunks/lit-html-v9gh687x.js +4 -0
  39. package/dist/chunks/media-library-sq468wts.js +4 -0
  40. package/dist/chunks/media-picker-shfe59pt.js +4 -0
  41. package/dist/chunks/{menu-n6yzswmy.js → menu-bdpzbfvs.js} +2 -2
  42. package/dist/chunks/{monaco-binding-7gx83x3r.js → monaco-binding-vq1hgnn0.js} +3 -3
  43. package/dist/chunks/monaco-buffer-3afgwvbp.js +4 -0
  44. package/dist/chunks/monaco-setup-xrd2cbkp.js +4 -0
  45. package/dist/chunks/notify-kzrjr3fv.js +4 -0
  46. package/dist/chunks/parse-v77xtwv7.js +4 -0
  47. package/dist/chunks/platform-7v4rtd7m.js +4 -0
  48. package/dist/chunks/popover-b89sfej7.js +4 -0
  49. package/dist/chunks/publish-panel-2z9gw8ee.js +4 -0
  50. package/dist/chunks/publish-quick-qx2455cj.js +4 -0
  51. package/dist/chunks/publish-workspace-wrc1dgwx.js +4 -0
  52. package/dist/chunks/quick-search-n0t0eqxb.js +4 -0
  53. package/dist/chunks/references-9547rg5s.js +4 -0
  54. package/dist/chunks/right-panel-wsgnr9pn.js +4 -0
  55. package/dist/chunks/source-control-kwktr9y6.js +4 -0
  56. package/dist/chunks/{split-n2gfpm5m.js → split-c9zfdgxf.js} +2 -2
  57. package/dist/chunks/studio-03ct6yfv.js +9 -0
  58. package/dist/chunks/studio-07rcfje3.js +5 -0
  59. package/dist/chunks/studio-3e54qh39.js +7 -0
  60. package/dist/chunks/studio-3exxhz5g.js +5 -0
  61. package/dist/chunks/studio-3q40e3rj.js +5 -0
  62. package/dist/chunks/studio-3qeh9t0h.js +6 -0
  63. package/dist/chunks/studio-3qqjeep5.js +6 -0
  64. package/dist/chunks/studio-43sd2mam.js +4 -0
  65. package/dist/chunks/studio-4hp8pkyt.js +5 -0
  66. package/dist/chunks/studio-58xg6j2q.js +9 -0
  67. package/dist/chunks/studio-5g0rprdt.js +7 -0
  68. package/dist/chunks/{studio-2j1cb95x.js → studio-6m380wp7.js} +4 -4
  69. package/dist/chunks/studio-80jyh1xh.js +4 -0
  70. package/dist/chunks/studio-83mk4bgg.js +9 -0
  71. package/dist/chunks/{studio-v0jzqdjm.js → studio-881z1xrx.js} +4 -4
  72. package/dist/chunks/studio-8m0y2qe7.js +5 -0
  73. package/dist/chunks/{studio-d81xfh1g.js → studio-8tr6cfrc.js} +4 -4
  74. package/dist/chunks/studio-8vcvfs7r.js +6 -0
  75. package/dist/chunks/studio-9tmebf0s.js +5 -0
  76. package/dist/chunks/studio-a2bvaptz.js +5 -0
  77. package/dist/chunks/studio-b7y5mr30.js +5 -0
  78. package/dist/chunks/studio-bavb31r0.js +5 -0
  79. package/dist/chunks/{studio-dgz8d0c1.js → studio-bbe4c90a.js} +4 -4
  80. package/dist/chunks/studio-c05wdnt3.js +5 -0
  81. package/dist/chunks/studio-ccv2svc6.js +5 -0
  82. package/dist/chunks/studio-d5grtb9e.js +16 -0
  83. package/dist/chunks/studio-df0baea9.js +5 -0
  84. package/dist/chunks/studio-dgk4dqtx.js +7 -0
  85. package/dist/chunks/studio-e1g64q43.js +5 -0
  86. package/dist/chunks/studio-ed1hp6zf.js +6 -0
  87. package/dist/chunks/studio-ed3xx40h.js +7 -0
  88. package/dist/chunks/studio-f14e1zdj.js +5 -0
  89. package/dist/chunks/studio-fbhxxc5f.js +5 -0
  90. package/dist/chunks/studio-fyh0jc96.js +5 -0
  91. package/dist/chunks/studio-gcxk20n7.js +5 -0
  92. package/dist/chunks/studio-jaaz8ce9.js +5 -0
  93. package/dist/chunks/{studio-90pq9km1.js → studio-jcnsdaep.js} +4 -4
  94. package/dist/chunks/studio-jsgf2q5k.js +7 -0
  95. package/dist/chunks/{studio-0q988f9w.js → studio-k3xxbagr.js} +3 -3
  96. package/dist/chunks/{studio-5ve6bbep.js → studio-k7evxqh4.js} +4 -4
  97. package/dist/chunks/studio-kt3pjkwd.js +5 -0
  98. package/dist/chunks/studio-m8g16bgz.js +5 -0
  99. package/dist/chunks/studio-mn0476ag.js +5 -0
  100. package/dist/chunks/studio-mx1smyq0.js +5 -0
  101. package/dist/chunks/studio-n0c5h6mf.js +5 -0
  102. package/dist/chunks/{studio-485046te.js → studio-nh6d342n.js} +7 -7
  103. package/dist/chunks/studio-nqxqf8p2.js +4 -0
  104. package/dist/chunks/studio-p4nrkvqr.js +5 -0
  105. package/dist/chunks/{studio-1fxkaaea.js → studio-pbxa5fpc.js} +4 -4
  106. package/dist/chunks/studio-pgmvdv7z.js +7 -0
  107. package/dist/chunks/studio-ph84avkf.js +5 -0
  108. package/dist/chunks/{studio-tq0rtfwj.js → studio-q0mvbaq6.js} +4 -4
  109. package/dist/chunks/studio-q0tccd43.js +5 -0
  110. package/dist/chunks/studio-q7rtx6zq.js +14 -0
  111. package/dist/chunks/{studio-a3g98wjr.js → studio-qgh13jtz.js} +4 -4
  112. package/dist/chunks/studio-qmn1xc24.js +5 -0
  113. package/dist/chunks/studio-qwdqwkj0.js +5 -0
  114. package/dist/chunks/studio-r5qy30vx.js +5 -0
  115. package/dist/chunks/studio-rj9eamf8.js +5 -0
  116. package/dist/chunks/studio-rptgsk96.js +5 -0
  117. package/dist/chunks/studio-rthyg42z.js +928 -0
  118. package/dist/chunks/studio-s19tp1xr.js +5 -0
  119. package/dist/chunks/studio-s32578pn.js +5 -0
  120. package/dist/chunks/studio-svb7yyve.js +5 -0
  121. package/dist/chunks/studio-syreb644.js +5 -0
  122. package/dist/chunks/studio-tf9jryb1.js +10 -0
  123. package/dist/chunks/studio-tfkznv39.js +5 -0
  124. package/dist/chunks/studio-tft6r105.js +5 -0
  125. package/dist/chunks/studio-txf2kysx.js +5 -0
  126. package/dist/chunks/studio-v8867ddv.js +12 -0
  127. package/dist/chunks/studio-vxq7fcvv.js +5 -0
  128. package/dist/chunks/studio-w4rz9hdk.js +5 -0
  129. package/dist/chunks/studio-wbcjbhy1.js +9 -0
  130. package/dist/chunks/studio-wc649wss.js +5 -0
  131. package/dist/chunks/studio-wys23mxz.js +5 -0
  132. package/dist/chunks/studio-x1x167jh.js +5 -0
  133. package/dist/chunks/studio-x9hsze61.js +5 -0
  134. package/dist/chunks/studio-y0n7yxea.js +9 -0
  135. package/dist/chunks/studio-y3ahhdpx.js +5 -0
  136. package/dist/chunks/studio-z97c48fk.js +5 -0
  137. package/dist/chunks/studio-zat62m0c.js +5 -0
  138. package/dist/chunks/studio-zcd2e7yz.js +5 -0
  139. package/dist/chunks/studio-zz9qe3rw.js +64 -0
  140. package/dist/chunks/toast-1gv0h53h.js +4 -0
  141. package/dist/chunks/{toast-host-ke5z4ng7.js → toast-host-1yg5ar8d.js} +3 -3
  142. package/dist/chunks/{tooltip-5v8ngwj2.js → tooltip-yypz28gq.js} +3 -3
  143. package/dist/chunks/{tree-mdq3v5cw.js → tree-9xa4hm7n.js} +2 -2
  144. package/dist/chunks/tsMode-epyfz1hv.js +14 -0
  145. package/dist/chunks/workspace-dz7nxfhv.js +4 -0
  146. package/dist/chunks/workspace-home-xsd7hkjw.js +4 -0
  147. package/dist/chunks/{ws-client-nze0k6mv.js → ws-client-bv552sfx.js} +3 -3
  148. package/dist/iframe-entry.js +20 -16
  149. package/dist/manifest.json +77 -63
  150. package/dist/studio.js +103 -104
  151. package/dist/workers/ts.worker.js +3 -3
  152. package/package.json +12 -11
  153. package/src/browse/component-examples.ts +272 -0
  154. package/src/browse/library-commands.ts +64 -104
  155. package/src/browse/library-layouts.ts +263 -49
  156. package/src/browse/library-model.ts +181 -133
  157. package/src/browse/library-pane.ts +738 -446
  158. package/src/browse/library-preview.ts +149 -19
  159. package/src/browse/library-source.ts +308 -41
  160. package/src/browse/preview-root.ts +78 -0
  161. package/src/browse/preview-style.ts +155 -0
  162. package/src/canvas/asset-refs.ts +16 -1
  163. package/src/canvas/canvas-live-render.ts +222 -30
  164. package/src/canvas/canvas-origin.ts +37 -12
  165. package/src/canvas/canvas-page-context.ts +112 -0
  166. package/src/canvas/canvas-render.ts +113 -107
  167. package/src/canvas/canvas-utils.ts +57 -12
  168. package/src/canvas/iframe-drop.ts +107 -0
  169. package/src/canvas/iframe-entry.ts +24 -12
  170. package/src/canvas/iframe-host.ts +134 -62
  171. package/src/canvas/iframe-keys.ts +11 -0
  172. package/src/canvas/iframe-patch.ts +8 -7
  173. package/src/canvas/iframe-protocol.ts +24 -5
  174. package/src/canvas/iframe-render.ts +48 -1
  175. package/src/canvas/iframe-subtree.ts +5 -2
  176. package/src/canvas/npm-modules.ts +22 -0
  177. package/src/canvas/preview-navigate.ts +16 -0
  178. package/src/canvas/repeater-sources.ts +200 -0
  179. package/src/canvas/stage-scrollbar.ts +5 -1
  180. package/src/canvas/surface-registry.ts +6 -0
  181. package/src/commands/app-commands.ts +16 -6
  182. package/src/commands/budget.ts +10 -8
  183. package/src/commands/context.ts +35 -10
  184. package/src/commands/defaults.ts +69 -45
  185. package/src/commands/keymap.ts +9 -0
  186. package/src/commands/levels.ts +11 -5
  187. package/src/commands/live-context.ts +2 -0
  188. package/src/commands/registry.ts +10 -1
  189. package/src/content/collection-match.ts +6 -5
  190. package/src/content/collection-pages.ts +412 -0
  191. package/src/content/content-collections.ts +270 -0
  192. package/src/content/entry-commands.ts +51 -60
  193. package/src/content/entry-editor.ts +111 -18
  194. package/src/content/entry-fields.ts +13 -1
  195. package/src/content/entry-inspector.ts +225 -0
  196. package/src/content/entry-model.ts +17 -3
  197. package/src/content/entry-open.ts +80 -0
  198. package/src/content/entry-summaries.ts +440 -0
  199. package/src/content/file-record-ids.ts +80 -0
  200. package/src/content/file-records.ts +125 -0
  201. package/src/content/page-summaries.ts +129 -0
  202. package/src/content/record-entry.ts +201 -0
  203. package/src/content/writing-banner.ts +177 -0
  204. package/src/create/add-new-commands.ts +185 -0
  205. package/src/create/add-new-flow.ts +1116 -0
  206. package/src/create/add-new.ts +1014 -0
  207. package/src/create/element-name.ts +121 -0
  208. package/src/editor/convert-to-component.ts +67 -43
  209. package/src/editor/file-drop-action.ts +9 -1
  210. package/src/editor/inline-edit-apply.ts +44 -0
  211. package/src/editor/inline-edit.ts +10 -0
  212. package/src/editor/insert-component.ts +91 -0
  213. package/src/editor/insert-zone-action.ts +13 -4
  214. package/src/editor/shortcuts.ts +44 -22
  215. package/src/editor/slash-menu.ts +51 -8
  216. package/src/files/components.ts +74 -10
  217. package/src/files/file-ops.ts +15 -5
  218. package/src/files/files.ts +164 -150
  219. package/src/files/fs-events.ts +18 -7
  220. package/src/files/media-meta.ts +95 -1
  221. package/src/files/media-types.ts +55 -0
  222. package/src/files/media-upload.ts +209 -52
  223. package/src/files/new-file-name.ts +156 -0
  224. package/src/files/serialize-document.ts +13 -3
  225. package/src/format/format-host.ts +15 -0
  226. package/src/github/github-auth.ts +2 -2
  227. package/src/github/github-publish.ts +4 -3
  228. package/src/grid/grid-controller.ts +5 -1
  229. package/src/grid/grid-open.ts +0 -27
  230. package/src/grid/grid-panel.ts +54 -2
  231. package/src/grid/grid-reveal.ts +34 -0
  232. package/src/grid/grid-source.ts +3 -25
  233. package/src/grid/grid-view.ts +31 -6
  234. package/src/grid/sources/content-source.ts +75 -7
  235. package/src/grid/tabulator-tables.d.ts +6 -0
  236. package/src/hosting/document.ts +2 -2
  237. package/src/i18n/i18n-commands.ts +2 -1
  238. package/src/media/media-commands.ts +181 -0
  239. package/src/media/media-library-model.ts +232 -0
  240. package/src/media/media-library.ts +1283 -0
  241. package/src/media/media-pane.ts +47 -19
  242. package/src/packages/pull-package-sync.ts +1 -1
  243. package/src/panels/activity-panel.ts +21 -5
  244. package/src/panels/ai-chat/chat-markdown.ts +8 -8
  245. package/src/panels/block-action-bar.ts +11 -11
  246. package/src/panels/canvas-dnd-bridge.ts +5 -2
  247. package/src/panels/chat-panel.ts +6 -6
  248. package/src/panels/collections-panel.ts +306 -0
  249. package/src/panels/component-preview.ts +35 -13
  250. package/src/panels/data-explorer.ts +1 -1
  251. package/src/panels/data-grid.ts +4 -0
  252. package/src/panels/dnd.ts +17 -30
  253. package/src/panels/elements-panel.ts +9 -2
  254. package/src/panels/empty-state.ts +2 -2
  255. package/src/panels/formula-workspace.ts +2 -3
  256. package/src/panels/frontmatter-fields.ts +9 -9
  257. package/src/panels/git-diff-open.ts +9 -5
  258. package/src/panels/head-panel.ts +369 -103
  259. package/src/panels/i18n-panel.ts +11 -10
  260. package/src/panels/imports-panel.ts +2 -0
  261. package/src/panels/jump-bar.ts +27 -2
  262. package/src/panels/layers-panel.ts +36 -30
  263. package/src/panels/left-panel.ts +22 -8
  264. package/src/panels/navigator-panels.ts +23 -11
  265. package/src/panels/pane-context.ts +111 -29
  266. package/src/panels/pane-grid.ts +6 -6
  267. package/src/panels/panel-registry.ts +14 -9
  268. package/src/panels/properties-panel.ts +7 -3
  269. package/src/panels/right-panel.ts +199 -43
  270. package/src/panels/seo-modal.ts +41 -108
  271. package/src/panels/settings-menu.ts +52 -50
  272. package/src/panels/settings-pane.ts +268 -162
  273. package/src/panels/signals-panel.ts +3 -4
  274. package/src/panels/site-model.ts +367 -0
  275. package/src/panels/site-panel.ts +638 -0
  276. package/src/panels/style-panel.ts +37 -3
  277. package/src/panels/stylebook-doc.ts +142 -10
  278. package/src/panels/stylebook-layers-panel.ts +39 -80
  279. package/src/panels/stylebook-panel.ts +47 -10
  280. package/src/panels/styles-panel.ts +327 -0
  281. package/src/panels/tab-strip.ts +109 -25
  282. package/src/platform.ts +1 -1
  283. package/src/platforms/cloud.ts +28 -1
  284. package/src/platforms/default-platform.ts +4 -4
  285. package/src/platforms/devserver.ts +51 -25
  286. package/src/publish/deploy-checklist.ts +12 -7
  287. package/src/publish/pages-service.ts +38 -14
  288. package/src/publish/publish-changes.ts +491 -0
  289. package/src/publish/publish-commands.ts +83 -18
  290. package/src/publish/publish-ids.ts +49 -0
  291. package/src/publish/publish-quick.ts +148 -0
  292. package/src/publish/publish-workspace.ts +926 -0
  293. package/src/publish/source-control.ts +907 -0
  294. package/src/services/automation.ts +16 -20
  295. package/src/services/cf-settings.ts +4 -4
  296. package/src/services/connection.ts +2 -2
  297. package/src/services/directory-picker.ts +2 -2
  298. package/src/services/references.ts +0 -0
  299. package/src/services/render-critic.ts +21 -2
  300. package/src/services/settings/definitions.ts +25 -14
  301. package/src/settings/contributed-section.ts +49 -10
  302. package/src/settings/css-vars-editor.ts +93 -27
  303. package/src/settings/developer-panels.ts +43 -0
  304. package/src/settings/extension-sections.ts +20 -2
  305. package/src/settings/general-settings.ts +91 -21
  306. package/src/settings/preferences-dialog.ts +8 -1
  307. package/src/settings/project-sections.ts +27 -10
  308. package/src/settings/schema-field-ui.ts +3 -1
  309. package/src/settings/section-registry.ts +116 -11
  310. package/src/settings/settings-dialog.ts +192 -0
  311. package/src/settings/settings-document.ts +182 -162
  312. package/src/shell.ts +467 -431
  313. package/src/site-context.ts +64 -10
  314. package/src/state.ts +28 -9
  315. package/src/studio.ts +194 -41
  316. package/src/style/project-styles.ts +34 -0
  317. package/src/style/stylebook-specimens.ts +89 -0
  318. package/src/style/stylebook-target.ts +26 -0
  319. package/src/style/tag-inheritance.ts +105 -0
  320. package/src/style/variables-focus.ts +61 -0
  321. package/src/style/variables-view.ts +235 -0
  322. package/src/surfaces/add-new.json +1482 -0
  323. package/src/surfaces/add-new.ts +325 -0
  324. package/src/surfaces/ai-chat.ts +2 -2
  325. package/src/surfaces/boot-failure.json +4 -3
  326. package/src/surfaces/bottom-dock.json +1 -1
  327. package/src/surfaces/canvas-stage.json +30 -19
  328. package/src/surfaces/canvas-stage.ts +5 -5
  329. package/src/surfaces/collections-panel.json +615 -0
  330. package/src/surfaces/collections-panel.ts +168 -0
  331. package/src/surfaces/commandbar.json +241 -96
  332. package/src/surfaces/commandbar.ts +134 -99
  333. package/src/surfaces/empty-state.json +1 -1
  334. package/src/surfaces/entry-editor.json +136 -0
  335. package/src/surfaces/entry-editor.ts +21 -0
  336. package/src/surfaces/entry-inspector.json +371 -0
  337. package/src/surfaces/entry-inspector.ts +143 -0
  338. package/src/surfaces/inspector-dock.json +162 -14
  339. package/src/surfaces/inspector-dock.ts +46 -19
  340. package/src/surfaces/library-pane.json +670 -437
  341. package/src/surfaces/library-pane.ts +94 -59
  342. package/src/surfaces/media-browser.json +62 -6
  343. package/src/surfaces/media-browser.ts +15 -1
  344. package/src/surfaces/media-field.json +1 -1
  345. package/src/surfaces/media-library.json +1586 -0
  346. package/src/surfaces/media-library.ts +282 -0
  347. package/src/surfaces/media-pick-dialog.json +71 -0
  348. package/src/surfaces/media-pick-dialog.ts +132 -0
  349. package/src/surfaces/menu.ts +18 -0
  350. package/src/surfaces/pane-context.json +144 -77
  351. package/src/surfaces/pane-context.ts +24 -0
  352. package/src/surfaces/panel-deploy-checklist.json +12 -6
  353. package/src/surfaces/panel-deploy-checklist.ts +3 -1
  354. package/src/surfaces/panel-page.json +72 -2
  355. package/src/surfaces/panel-page.ts +38 -7
  356. package/src/surfaces/panel-signals.json +1 -1
  357. package/src/surfaces/panel-signals.ts +5 -5
  358. package/src/surfaces/panel-stylebook-layers.json +71 -204
  359. package/src/surfaces/panel-stylebook-layers.ts +0 -21
  360. package/src/surfaces/preferences.json +120 -48
  361. package/src/surfaces/preferences.ts +8 -0
  362. package/src/surfaces/publish-quick.json +428 -0
  363. package/src/surfaces/publish-quick.ts +165 -0
  364. package/src/surfaces/publish-workspace.json +2892 -0
  365. package/src/surfaces/publish-workspace.ts +391 -0
  366. package/src/surfaces/rail.json +1 -159
  367. package/src/surfaces/rail.ts +18 -75
  368. package/src/surfaces/schema-form.json +10 -2
  369. package/src/surfaces/schema-form.ts +2 -0
  370. package/src/surfaces/seo.json +1 -1
  371. package/src/surfaces/settings-contributed.json +5 -5
  372. package/src/surfaces/settings-contributed.ts +18 -2
  373. package/src/surfaces/settings-css-vars.json +44 -36
  374. package/src/surfaces/settings-css-vars.ts +77 -4
  375. package/src/surfaces/settings-deploy.json +2 -15
  376. package/src/surfaces/settings-deploy.ts +1 -1
  377. package/src/surfaces/settings-dialog.json +51 -0
  378. package/src/surfaces/settings-dialog.ts +169 -0
  379. package/src/surfaces/settings-overview.json +3 -3
  380. package/src/surfaces/settings-overview.ts +9 -2
  381. package/src/surfaces/settings-pane.json +269 -46
  382. package/src/surfaces/settings-pane.ts +85 -24
  383. package/src/surfaces/shell.json +2 -1
  384. package/src/surfaces/site-panel.json +1029 -0
  385. package/src/surfaces/site-panel.ts +233 -0
  386. package/src/surfaces/slash-menu.json +63 -21
  387. package/src/surfaces/slash-menu.ts +23 -0
  388. package/src/surfaces/statusbar.ts +39 -15
  389. package/src/surfaces/styles-panel.json +735 -0
  390. package/src/surfaces/styles-panel.ts +195 -0
  391. package/src/surfaces/styles-variables.json +149 -0
  392. package/src/surfaces/styles-variables.ts +145 -0
  393. package/src/surfaces/tab-strip.json +4 -4
  394. package/src/surfaces/welcome.ts +1 -1
  395. package/src/surfaces/writing-banner.json +94 -0
  396. package/src/surfaces/writing-banner.ts +80 -0
  397. package/src/tabs/bare-repeaters.ts +101 -0
  398. package/src/tabs/tab.ts +47 -7
  399. package/src/types.ts +21 -26
  400. package/src/ui/form-controls.ts +47 -8
  401. package/src/ui/layers.ts +13 -0
  402. package/src/ui/media-picker.ts +284 -40
  403. package/src/ui/regions.ts +7 -6
  404. package/src/ui/schema-form.ts +259 -27
  405. package/src/ui/virtual-window.ts +26 -1
  406. package/src/utils/base64.ts +3 -3
  407. package/src/utils/edit-display.ts +322 -67
  408. package/src/utils/local-day.ts +18 -0
  409. package/src/workspace/pane-derive.ts +4 -4
  410. package/src/workspace/session.ts +28 -7
  411. package/src/workspace/tab-workspace.ts +137 -0
  412. package/src/workspace/workspace-back.ts +189 -0
  413. package/src/workspace/workspace-home.ts +133 -0
  414. package/src/workspace/workspace-ids.ts +129 -0
  415. package/src/workspace/workspace-switch.ts +372 -0
  416. package/src/workspace/workspace.ts +125 -11
  417. package/styles/shell-frame.css +30 -7
  418. package/styles/shell-frame.json +15 -3
  419. package/dist/chunks/2020-f2gkpy8d.js +0 -4
  420. package/dist/chunks/adapter-4912g339.js +0 -4
  421. package/dist/chunks/cf-account-picker-wccaenh6.js +0 -4
  422. package/dist/chunks/cf-settings-br6105tr.js +0 -4
  423. package/dist/chunks/color-28ksk2qm.js +0 -4
  424. package/dist/chunks/components-db68vsn7.js +0 -4
  425. package/dist/chunks/content-source-1449qesq.js +0 -4
  426. package/dist/chunks/convert-to-repeater-5h457egj.js +0 -4
  427. package/dist/chunks/editor-x9jn86a7.js +0 -4
  428. package/dist/chunks/entry-commands-may24cpt.js +0 -4
  429. package/dist/chunks/extension-sections-pvr14vxr.js +0 -4
  430. package/dist/chunks/file-ops-x9v54pcj.js +0 -4
  431. package/dist/chunks/files-j3knqf0s.js +0 -4
  432. package/dist/chunks/format-host-tyre2trb.js +0 -4
  433. package/dist/chunks/fs-events-64ma9htc.js +0 -4
  434. package/dist/chunks/git-panel-84ps5dex.js +0 -4
  435. package/dist/chunks/iframe-entry-fvbk89gm.js +0 -5
  436. package/dist/chunks/iframe-host-ck6vejt5.js +0 -4
  437. package/dist/chunks/index-b8xw3vy9.js +0 -4
  438. package/dist/chunks/jsonMode-w15cmrcm.js +0 -13
  439. package/dist/chunks/jx-validate-at3zn8wa.js +0 -4
  440. package/dist/chunks/layers-h4xp1pss.js +0 -4
  441. package/dist/chunks/layers-panel-m5hw1d7g.js +0 -4
  442. package/dist/chunks/lit-html-8t8t3dnr.js +0 -4
  443. package/dist/chunks/media-picker-2cweexc5.js +0 -4
  444. package/dist/chunks/menu-ynw13y0s.js +0 -4
  445. package/dist/chunks/monaco-buffer-qzxn567r.js +0 -4
  446. package/dist/chunks/monaco-setup-gxf6656g.js +0 -4
  447. package/dist/chunks/notify-cj5zbrs2.js +0 -4
  448. package/dist/chunks/parse-b7ddhr8m.js +0 -4
  449. package/dist/chunks/platform-3dfws9js.js +0 -4
  450. package/dist/chunks/popover-jt2gxmzs.js +0 -4
  451. package/dist/chunks/publish-panel-1gvxbj21.js +0 -4
  452. package/dist/chunks/quick-search-0dajrwqg.js +0 -4
  453. package/dist/chunks/references-49qgt404.js +0 -4
  454. package/dist/chunks/right-panel-jm1bwe53.js +0 -4
  455. package/dist/chunks/section-registry-6kwc4fky.js +0 -4
  456. package/dist/chunks/studio-0rt52vpq.js +0 -5
  457. package/dist/chunks/studio-18vgbhs5.js +0 -5
  458. package/dist/chunks/studio-24mccdrj.js +0 -5
  459. package/dist/chunks/studio-2eh507zk.js +0 -9
  460. package/dist/chunks/studio-2hcbccfv.js +0 -5
  461. package/dist/chunks/studio-2jbs3rxv.js +0 -5
  462. package/dist/chunks/studio-4h8k4dw9.js +0 -5
  463. package/dist/chunks/studio-5f6n18kx.js +0 -5
  464. package/dist/chunks/studio-5gthpky6.js +0 -5
  465. package/dist/chunks/studio-6ep3js6b.js +0 -9
  466. package/dist/chunks/studio-6wkmf3wz.js +0 -9
  467. package/dist/chunks/studio-841rf3cg.js +0 -5
  468. package/dist/chunks/studio-8gmwgheg.js +0 -5
  469. package/dist/chunks/studio-8qm0ep50.js +0 -5
  470. package/dist/chunks/studio-924p1tm1.js +0 -6
  471. package/dist/chunks/studio-a3shpz9y.js +0 -5
  472. package/dist/chunks/studio-amsbaaar.js +0 -6
  473. package/dist/chunks/studio-at5spjbc.js +0 -7
  474. package/dist/chunks/studio-b2xv945e.js +0 -5
  475. package/dist/chunks/studio-bn5nkafw.js +0 -5
  476. package/dist/chunks/studio-bw6q5xjf.js +0 -9
  477. package/dist/chunks/studio-c7q0t9x1.js +0 -5
  478. package/dist/chunks/studio-camnhc96.js +0 -6
  479. package/dist/chunks/studio-cffqvnzd.js +0 -5
  480. package/dist/chunks/studio-efvfrsf8.js +0 -5
  481. package/dist/chunks/studio-er30bpcx.js +0 -5
  482. package/dist/chunks/studio-ezscgr64.js +0 -5
  483. package/dist/chunks/studio-ffrrncvz.js +0 -5
  484. package/dist/chunks/studio-fpphgn8y.js +0 -10
  485. package/dist/chunks/studio-j1kvhdjk.js +0 -4
  486. package/dist/chunks/studio-j430n16g.js +0 -7
  487. package/dist/chunks/studio-jjkg84nj.js +0 -5
  488. package/dist/chunks/studio-m0q32ars.js +0 -4
  489. package/dist/chunks/studio-m6bdst11.js +0 -5
  490. package/dist/chunks/studio-nv2cptd4.js +0 -64
  491. package/dist/chunks/studio-nwfrc98n.js +0 -5
  492. package/dist/chunks/studio-p1wgvp7r.js +0 -5
  493. package/dist/chunks/studio-pag2wncm.js +0 -5
  494. package/dist/chunks/studio-qvanvzfd.js +0 -5
  495. package/dist/chunks/studio-qwa64kt7.js +0 -5
  496. package/dist/chunks/studio-rk1zc4x3.js +0 -928
  497. package/dist/chunks/studio-se2r3ptc.js +0 -5
  498. package/dist/chunks/studio-sg2a0dh0.js +0 -5
  499. package/dist/chunks/studio-tjzjhe68.js +0 -5
  500. package/dist/chunks/studio-v2w7q8r1.js +0 -7
  501. package/dist/chunks/studio-v5vjq1h0.js +0 -4
  502. package/dist/chunks/studio-v8aga3vp.js +0 -14
  503. package/dist/chunks/studio-vfcdrfmv.js +0 -5
  504. package/dist/chunks/studio-vmfkgdjf.js +0 -5
  505. package/dist/chunks/studio-wt1816jj.js +0 -5
  506. package/dist/chunks/studio-wz73q5kq.js +0 -4
  507. package/dist/chunks/studio-y04qtxvp.js +0 -7
  508. package/dist/chunks/studio-y21g8zzg.js +0 -5
  509. package/dist/chunks/studio-y64hrt8p.js +0 -12
  510. package/dist/chunks/studio-ynn90f58.js +0 -16
  511. package/dist/chunks/studio-yxz682sx.js +0 -5
  512. package/dist/chunks/studio-zp7snvkq.js +0 -5
  513. package/dist/chunks/studio-zq4z6tm6.js +0 -5
  514. package/dist/chunks/toast-c9fn075p.js +0 -4
  515. package/dist/chunks/tsMode-sf49ee9x.js +0 -14
  516. package/dist/chunks/workspace-jtfahpee.js +0 -4
  517. package/src/panels/frontmatter-panel.ts +0 -711
  518. package/src/panels/git-panel.ts +0 -1108
  519. package/src/surfaces/doc-header.json +0 -776
  520. package/src/surfaces/doc-header.ts +0 -222
  521. package/src/surfaces/git-panel.json +0 -1880
  522. package/src/surfaces/git-panel.ts +0 -264
@@ -0,0 +1,1116 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Add new, as a flow: read the project, open the dialog, keep the answer, write it, open it.
4
+ *
5
+ * `specs/studio.md` §9.1.1. The derivation is `create/add-new.ts`'s and the markup is
6
+ * `surfaces/add-new.json`'s; this module is everything between them that touches the app — the
7
+ * {@link CreationContext} read off the stores, the directory listings the refusals need, the writes
8
+ * through the one file writer (`files/files.ts` `writeNewFile`) and the configuration chokepoint
9
+ * (`tabs/project-config.ts`), and what each kind opens afterwards.
10
+ *
11
+ * Two entry points, one write path. {@link openAddNew} is the dialog (`file.new` with no name, the
12
+ * Site panel's **+**, the Content home's **New**); {@link createDirect} is the same derivation,
13
+ * refusal and write with no dialog at all (`file.new` with a kind and a name — the assistant's way
14
+ * in, and a script's). Both end in {@link perform}, so a page made by a person and a page made by
15
+ * the assistant are the same bytes.
16
+ *
17
+ * @docs studio/projects/pages-layouts-components
18
+ */
19
+
20
+ import { errorMessage } from "@jxsuite/schema/parse";
21
+ import { serializeJson } from "@jxsuite/schema/json-layout";
22
+ import { isJsonObject, isString } from "@jxsuite/schema/guards";
23
+ import { getPlatform } from "../platform";
24
+ import { projectState, renderOnly } from "../store";
25
+ import { notify } from "../services/notify";
26
+ import { layerHost } from "../ui/layers";
27
+ import { activeRegistry } from "../commands/active-registry";
28
+ import { runActiveReported } from "../commands/run-reported";
29
+ import {
30
+ defaultContentFormat,
31
+ formatByExtension,
32
+ formatSerialize,
33
+ loadFormats,
34
+ } from "../format/format-host";
35
+ import { creationFormats } from "../format/format-choices";
36
+ import { collectionNames, entryCollections } from "../content/entry-model";
37
+ import { componentRegistry, noteComponentSaved } from "../files/components";
38
+ import {
39
+ listTakenNames,
40
+ openFileInTab,
41
+ refreshProjectDirectoriesFor,
42
+ writeNewFile,
43
+ } from "../files/files";
44
+ import { slugify } from "../files/new-file-name";
45
+ import { DEFAULT_MEDIA_DIR, uploadAccept, uploadAssets } from "../files/media-upload";
46
+ import { commitProjectConfig } from "../tabs/project-config";
47
+ import { convertibleSelection, convertSelectionToComponent } from "../editor/convert-to-component";
48
+ import { invalidateLayoutPickerCache, listLayoutEntries } from "../panels/head-panel";
49
+ import { invalidateReferenceEntries } from "../ui/form-controls";
50
+ import { openEntry } from "../content/entry-open";
51
+ import { invalidateCollectionPages } from "../content/collection-pages";
52
+ import { invalidatePageSummaries } from "../content/page-summaries";
53
+ import { entryFormatName } from "../content/entry-commands";
54
+ import { invalidateSiteIndex } from "../panels/site-panel";
55
+ import { invalidateLibrary } from "../browse/library-pane";
56
+ import { collectionSourceDirs } from "../grid/sources/content-source";
57
+ import { humanize } from "../panels/site-model";
58
+ import { canonicalizeLocale, localeLabel, localeOfPath, resolveI18n } from "@jxsuite/schema/locale";
59
+ import { openAddNewSurface } from "../surfaces/add-new";
60
+ import {
61
+ collectionPageBody,
62
+ creationProblem,
63
+ defaultTemplateChoice,
64
+ templateForLocale,
65
+ deriveCreation,
66
+ absentByParent,
67
+ byDepth,
68
+ directoriesToList,
69
+ DEFAULT_FIELD_PRESETS,
70
+ FIELD_PRESET_INFO,
71
+ FIELD_PRESETS,
72
+ itemNoun,
73
+ CREATION_KINDS,
74
+ KIND_INFO,
75
+ layoutValueFor,
76
+ normalizeLayoutPath,
77
+ schemaPresets,
78
+ START_ARTICLE,
79
+ START_BLANK,
80
+ START_COPY,
81
+ START_SELECTION,
82
+ declaredLocale,
83
+ isLocalizedCollection,
84
+ } from "./add-new";
85
+ import type {
86
+ CollectionChoice,
87
+ Creation,
88
+ CreationBody,
89
+ CreationContext,
90
+ CreationI18n,
91
+ CreationKind,
92
+ CreationOptions,
93
+ FieldPreset,
94
+ LayoutChoice,
95
+ WritingFormat,
96
+ } from "./add-new";
97
+ import type {
98
+ AddNewChoiceRow,
99
+ AddNewKindRow,
100
+ AddNewSurfaceHandle,
101
+ AddNewView,
102
+ } from "../surfaces/add-new";
103
+ import type { JsonObject, JxMutableNode } from "@jxsuite/schema/types";
104
+
105
+ /** The `source` every Problem this flow files carries. */
106
+ const SOURCE = "Add new";
107
+
108
+ /** What one creation produced, for the command's report and for a caller that opens it. */
109
+ export interface AddNewResult {
110
+ kind: CreationKind;
111
+ /** The main file, or the upload directory for Media. */
112
+ path: string;
113
+ /** Every path written, `project.json` included for a collection. */
114
+ wrote: string[];
115
+ route?: string;
116
+ /** A component's custom element name. */
117
+ tag?: string;
118
+ }
119
+
120
+ /** What `file.new` and the dialog's openers ask for. Every field is optional. */
121
+ export interface AddNewRequest {
122
+ kind?: CreationKind | undefined;
123
+ name?: string | undefined;
124
+ collection?: string | undefined;
125
+ /** A layout (path, `layouts/x.json` or the stem `x`), `article`, or `blank`. */
126
+ template?: string | undefined;
127
+ /** For a page or an entry: the language it is written in, a declared locale tag. */
128
+ locale?: string | undefined;
129
+ }
130
+
131
+ // ─── Reading the project ─────────────────────────────────────────────────────
132
+
133
+ /**
134
+ * The project's layouts, listed once per dialog through the Page panel's own listing
135
+ * (`panels/head-panel.ts` {@link listLayoutEntries}), so both pickers name the same templates and
136
+ * one invalidation forgets both.
137
+ */
138
+ async function readLayouts(): Promise<LayoutChoice[]> {
139
+ const entries = await listLayoutEntries();
140
+ return entries
141
+ .map((entry) => ({ name: entry.name, ref: entry.path }))
142
+ .toSorted((a, b) => a.name.localeCompare(b.name));
143
+ }
144
+
145
+ /** The first page format that writes prose: what an Article page is saved in. */
146
+ function articleFormat(): WritingFormat | null {
147
+ for (const row of creationFormats("page")) {
148
+ if (row.ext === ".json") {
149
+ continue;
150
+ }
151
+ const format = formatByExtension(row.ext, "serialize");
152
+ if (format) {
153
+ return { ext: row.ext, name: format.name };
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
159
+ /** The format a new collection's entries are written in, or `null` for native JSON. */
160
+ function contentWritingFormat(): WritingFormat | null {
161
+ const format = defaultContentFormat();
162
+ const ext = format?.extensions[0];
163
+ return format && ext ? { ext, name: format.name } : null;
164
+ }
165
+
166
+ /** The project's languages, or `null` when it declares none. */
167
+ function readI18n(): CreationI18n | null {
168
+ return resolveI18n(projectState?.projectConfig ?? {}).i18n;
169
+ }
170
+
171
+ /**
172
+ * A localized collection's language folders as they are spelled on disk, keyed by canonical tag:
173
+ * the museum starter writes `content/exhibitions/fr-CA`, and an entry created in French belongs in
174
+ * that folder rather than a second `fr-ca` beside it.
175
+ */
176
+ async function readLocaleDirs(collection: { dir: string; name: string }) {
177
+ const dirs: Record<string, string> = {};
178
+ const { i18n } = resolveI18n(projectState?.projectConfig ?? {});
179
+ for (const dir of await collectionSourceDirs(collection)) {
180
+ const tag = canonicalizeLocale(localeOfPath(dir, i18n));
181
+ if (tag !== null) {
182
+ dirs[tag] = dir;
183
+ }
184
+ }
185
+ return dirs;
186
+ }
187
+
188
+ /** Everything the derivation reads about the project, read now. */
189
+ export async function readCreationContext(): Promise<CreationContext> {
190
+ await loadFormats().catch(() => {
191
+ /* An unreachable format registry leaves JSON, which is the one shape that needs none. */
192
+ });
193
+ const collections: CollectionChoice[] = await Promise.all(
194
+ entryCollections().map(async (collection) => {
195
+ const choice: CollectionChoice = {
196
+ dir: collection.dir,
197
+ ext: collection.ext,
198
+ formatName: entryFormatName(collection),
199
+ name: collection.name,
200
+ schema: collection.schema,
201
+ };
202
+ if (isLocalizedCollection(collection)) {
203
+ choice.localeDirs = await readLocaleDirs(collection);
204
+ }
205
+ return choice;
206
+ }),
207
+ );
208
+ return {
209
+ i18n: readI18n(),
210
+ article: articleFormat(),
211
+ collections,
212
+ componentTags: componentRegistry.map((component) => component.tagName),
213
+ copyableTags: componentRegistry
214
+ .filter((component) => isCopyable(component))
215
+ .map((component) => component.tagName),
216
+ contentFormat: contentWritingFormat(),
217
+ declaredContent: collectionNames(),
218
+ defaultLayout: projectState?.projectConfig?.defaults?.layout ?? null,
219
+ hasSelection: convertibleSelection() !== null,
220
+ layouts: await readLayouts(),
221
+ };
222
+ }
223
+
224
+ /** Whether a copy can be made of a registered component: a JSON definition the project owns. */
225
+ function isCopyable(component: (typeof componentRegistry)[number]): boolean {
226
+ const { path } = component;
227
+ return isString(path) && component.source !== "npm" && path.endsWith(".json");
228
+ }
229
+
230
+ /** Project component definitions a copy can be made of: JSON files the project owns. */
231
+ async function readComponentDocs(): Promise<Map<string, JxMutableNode>> {
232
+ const docs = new Map<string, JxMutableNode>();
233
+ for (const component of componentRegistry) {
234
+ const { path } = component;
235
+ if (!isCopyable(component) || !isString(path)) {
236
+ continue;
237
+ }
238
+ try {
239
+ const parsed: unknown = JSON.parse(await getPlatform().readFile(path));
240
+ if (isJsonObject(parsed)) {
241
+ /* SAFETY: a component definition is a JSON object whose root is the component's element,
242
+ which is a JxMutableNode by the document model's own definition. */
243
+ docs.set(component.tagName, parsed as JxMutableNode);
244
+ }
245
+ } catch {
246
+ // A definition that cannot be read is not offered to copy; the registry still lists it.
247
+ }
248
+ }
249
+ return docs;
250
+ }
251
+
252
+ // ─── Arguments ───────────────────────────────────────────────────────────────
253
+
254
+ /** A `template` argument, resolved against the project's layouts; `null` when it names none. */
255
+ export function resolveTemplateArg(value: string, layouts: readonly LayoutChoice[]): string | null {
256
+ const wanted = value.trim();
257
+ if (wanted === START_BLANK || wanted === "none" || wanted === "") {
258
+ return START_BLANK;
259
+ }
260
+ if (wanted === START_ARTICLE) {
261
+ return START_ARTICLE;
262
+ }
263
+ const normalized = normalizeLayoutPath(wanted);
264
+ const match = layouts.find(
265
+ (layout) =>
266
+ normalizeLayoutPath(layout.ref) === normalized ||
267
+ normalizeLayoutPath(layout.ref) === `layouts/${normalized}` ||
268
+ normalizeLayoutPath(layout.ref) === `layouts/${normalized}.json` ||
269
+ layout.name.toLowerCase() === wanted.toLowerCase(),
270
+ );
271
+ return match?.ref ?? null;
272
+ }
273
+
274
+ /** The options a request implies for its kind. */
275
+ function optionsForRequest(request: AddNewRequest, ctx: CreationContext): CreationOptions {
276
+ const options: CreationOptions = {};
277
+ if (request.collection !== undefined) {
278
+ options.collection = request.collection;
279
+ }
280
+ if (request.locale !== undefined) {
281
+ if (declaredLocale(request.locale, ctx) === null) {
282
+ const declared = ctx.i18n?.locales ?? [];
283
+ throw new RangeError(
284
+ `command "file.new" argument "locale": "${request.locale}" is not a language this ` +
285
+ `project declares — declared: ${declared.length === 0 ? "none" : declared.join(", ")}`,
286
+ );
287
+ }
288
+ options.locale = request.locale;
289
+ }
290
+ if (request.template !== undefined) {
291
+ const resolved = resolveTemplateArg(request.template, ctx.layouts);
292
+ if (resolved === null) {
293
+ const names = ctx.layouts.map((layout) => normalizeLayoutPath(layout.ref));
294
+ throw new RangeError(
295
+ `command "file.new" argument "template": "${request.template}" names no template — ` +
296
+ `declared: ${[START_BLANK, START_ARTICLE, ...names].join(", ")}`,
297
+ );
298
+ }
299
+ if (request.kind === "collection") {
300
+ options.pageTemplate = resolved === START_ARTICLE ? START_BLANK : resolved;
301
+ } else if (request.kind === "template") {
302
+ options.startFrom = resolved === START_BLANK || resolved === START_ARTICLE ? "" : resolved;
303
+ } else {
304
+ options.startFrom = resolved;
305
+ }
306
+ }
307
+ return options;
308
+ }
309
+
310
+ // ─── Writing ─────────────────────────────────────────────────────────────────
311
+
312
+ /** A body's bytes: native JSON through the save serializer, anything else through its format. */
313
+ async function serializeBody(body: CreationBody): Promise<string> {
314
+ if (body.format === null) {
315
+ return serializeJson(body.doc, null);
316
+ }
317
+ /* SAFETY: a creation body is a document the derivation built — an element-shaped object, which is
318
+ what a format's serializer reads. */
319
+ return formatSerialize(body.format, body.doc as JxMutableNode, {
320
+ frontmatter: body.frontmatter ?? true,
321
+ mode: "roundtrip",
322
+ });
323
+ }
324
+
325
+ /** List every directory a creation would write into, so its refusal is judged against disk. */
326
+ async function listFor(creation: Creation, taken: Map<string, Set<string>>, fresh = false) {
327
+ /* Shallowest first, so a directory whose parent's listing does not name it is known to be absent
328
+ without asking. Asking was a failed request per keystroke: typing a page's name probes the
329
+ folder its index would live in (`pages/pri`, `pages/pric`, …), and none of those exist. */
330
+ for (const level of byDepth(directoriesToList(creation))) {
331
+ await Promise.all(
332
+ level.map(async (dir) => {
333
+ if (!fresh && taken.has(dir)) {
334
+ return;
335
+ }
336
+ taken.set(dir, absentByParent(dir, taken) ? new Set() : await listTakenNames(dir));
337
+ }),
338
+ );
339
+ }
340
+ }
341
+
342
+ /** Tell everything that lists files that some just appeared. */
343
+ function announce(paths: readonly string[]): void {
344
+ invalidateSiteIndex(paths);
345
+ invalidateLibrary();
346
+ if (paths.some((path) => path.startsWith("pages/"))) {
347
+ invalidateCollectionPages();
348
+ invalidatePageSummaries();
349
+ }
350
+ /* A first page or a first collection creates `pages/` or `content/`, which the project's
351
+ directory list (read at open) does not name yet; the Content scan walks that list. */
352
+ void refreshProjectDirectoriesFor(paths).then((reread) => {
353
+ if (reread) {
354
+ invalidateLibrary();
355
+ renderOnly("leftPanel");
356
+ }
357
+ });
358
+ }
359
+
360
+ /** One file of a creation, written; throws the reason it could not be. */
361
+ async function writeOne(path: string, body: CreationBody, quiet: boolean): Promise<void> {
362
+ let text: string;
363
+ try {
364
+ text = await serializeBody(body);
365
+ } catch (error) {
366
+ notify.error(`Could not prepare ${path}.`, {
367
+ detail: errorMessage(error),
368
+ path,
369
+ source: SOURCE,
370
+ });
371
+ throw new Error(`could not prepare ${path}`, { cause: error });
372
+ }
373
+ if ((await writeNewFile(path, text, { quiet, source: SOURCE })) === null) {
374
+ throw new Error(`could not write ${path}`);
375
+ }
376
+ }
377
+
378
+ /** Add a collection's `project.json` entry, then its first entry and its own page. */
379
+ async function performCollection(creation: Creation): Promise<AddNewResult | null> {
380
+ const config = creation.config!;
381
+ const existing = projectState?.projectConfig?.content;
382
+ const content: JsonObject = isJsonObject(existing) ? { ...existing } : {};
383
+ content[config.name] = config.entry;
384
+ const committed = await commitProjectConfig({ content });
385
+ if (!committed.ok) {
386
+ return null;
387
+ }
388
+ const wrote = ["project.json"];
389
+ const missed: string[] = [];
390
+ for (const write of [{ body: creation.content, path: creation.path }, ...creation.extraWrites]) {
391
+ try {
392
+ await writeOne(write.path, write.body, true);
393
+ wrote.push(write.path);
394
+ } catch {
395
+ // Already a Problem naming its path; what was written stays written, and the toast says so.
396
+ missed.push(write.path);
397
+ }
398
+ }
399
+ if (missed.length === 0) {
400
+ notify.success(`Created the ${config.name} collection`);
401
+ } else {
402
+ notify.warn(
403
+ `Created the ${config.name} collection, but ${missed.join(" and ")} could not be written.`,
404
+ { source: SOURCE },
405
+ );
406
+ }
407
+ announce(wrote);
408
+ invalidateReferenceEntries(config.name);
409
+ // Then: the Content workspace, showing the collection just made (§9.1.1).
410
+ void runActiveReported("content.showCollection", { collection: config.name }, SOURCE);
411
+ const result: AddNewResult = { kind: "collection", path: `content/${config.name}`, wrote };
412
+ if (creation.route !== undefined) {
413
+ result.route = creation.route;
414
+ }
415
+ return result;
416
+ }
417
+
418
+ /**
419
+ * Write a creation and open what it made. The refusal has already been judged.
420
+ *
421
+ * @returns What was written, or `null` when a write failed (already reported).
422
+ */
423
+ async function perform(
424
+ creation: Creation,
425
+ options: CreationOptions,
426
+ files: readonly File[],
427
+ ): Promise<AddNewResult | null> {
428
+ if (creation.kind === "media") {
429
+ // Only what the Media workspace lists: the browse that follows selects the first file by path.
430
+ const uploaded = await uploadAssets(files, { dir: DEFAULT_MEDIA_DIR, only: "media" });
431
+ if (uploaded.length === 0) {
432
+ return null;
433
+ }
434
+ const wrote = uploaded.map((asset) => asset.path);
435
+ announce(wrote);
436
+ // Then: the Media workspace, with what arrived selected so its detail pane describes it (§9.1.1).
437
+ void runActiveReported("media.browse", { select: uploaded[0]!.path }, SOURCE);
438
+ return { kind: "media", path: DEFAULT_MEDIA_DIR, wrote };
439
+ }
440
+ if (creation.kind === "collection") {
441
+ return performCollection(creation);
442
+ }
443
+ const result: AddNewResult = { kind: creation.kind, path: creation.path, wrote: [creation.path] };
444
+ if (creation.route !== undefined) {
445
+ result.route = creation.route;
446
+ }
447
+ if (creation.tag !== undefined) {
448
+ result.tag = creation.tag;
449
+ }
450
+
451
+ if (creation.kind === "component" && options.startFrom === START_SELECTION) {
452
+ // The transaction first, the file only if the document took the reference (§9.1.1).
453
+ const written = await convertSelectionToComponent(creation.tag!);
454
+ if (written === null) {
455
+ return null;
456
+ }
457
+ announce([written]);
458
+ await openFileInTab(written);
459
+ return result;
460
+ }
461
+
462
+ try {
463
+ await writeOne(creation.path, creation.content, false);
464
+ } catch {
465
+ return null;
466
+ }
467
+ announce([creation.path]);
468
+ switch (creation.kind) {
469
+ case "component": {
470
+ /* SAFETY: a component body is the element the derivation built, which is the document the
471
+ registry's extractor reads. */
472
+ noteComponentSaved(creation.path, creation.content.doc as JxMutableNode);
473
+ await openFileInTab(creation.path);
474
+ break;
475
+ }
476
+ case "template": {
477
+ invalidateLayoutPickerCache();
478
+ await openFileInTab(creation.path);
479
+ break;
480
+ }
481
+ case "entry": {
482
+ if (options.collection !== undefined) {
483
+ invalidateReferenceEntries(options.collection);
484
+ }
485
+ await openEntry(creation.path);
486
+ break;
487
+ }
488
+ default: {
489
+ await openFileInTab(creation.path);
490
+ }
491
+ }
492
+ return result;
493
+ }
494
+
495
+ // ─── Without the dialog ──────────────────────────────────────────────────────
496
+
497
+ /**
498
+ * Create one thing from a kind and a name, with no dialog — `file.new { kind, name }`.
499
+ *
500
+ * Refuses by THROWING (`studio-ui-guidelines.md` §12.4): every refusal the dialog would print under
501
+ * its field is a `RangeError` here, with the same sentence. Media is refused, because an upload
502
+ * needs files and an argument cannot carry one.
503
+ */
504
+ export async function createDirect(
505
+ request: AddNewRequest & { name: string },
506
+ ): Promise<AddNewResult> {
507
+ const kind = request.kind ?? "page";
508
+ if (kind === "media") {
509
+ throw new RangeError(
510
+ 'command "file.new": Media is uploaded, not named; run file.new { kind: "media" } with no ' +
511
+ "name to open the upload drop zone.",
512
+ );
513
+ }
514
+ const ctx = await readCreationContext();
515
+ const options = optionsForRequest({ ...request, kind }, ctx);
516
+ if (kind === "entry" && options.collection === undefined && ctx.collections.length === 1) {
517
+ options.collection = ctx.collections[0]!.name;
518
+ }
519
+ const creation = deriveCreation(kind, request.name, options, ctx);
520
+ const taken = new Map<string, Set<string>>();
521
+ await listFor(creation, taken, true);
522
+ const problem = creationProblem(creation, request.name, options, ctx, (dir) => taken.get(dir));
523
+ if (problem !== "") {
524
+ throw new RangeError(`command "file.new": ${problem}`);
525
+ }
526
+ const result = await perform(creation, options, []);
527
+ if (result === null) {
528
+ throw new Error(`file.new could not create ${creation.path}; the reason is in Problems.`);
529
+ }
530
+ return result;
531
+ }
532
+
533
+ // ─── A page for an existing collection ───────────────────────────────────────
534
+
535
+ /**
536
+ * Give an existing collection a page of its own: `pages/<name>/[slug].json`, written exactly as Add
537
+ * new writes it for a new collection with **Give each item its own page** on (the blog starter's
538
+ * shape, bound to whichever of title, image and body the collection has, in the default template).
539
+ * The Content home's **Add one** (`specs/studio.md` §22.5).
540
+ *
541
+ * @returns The page's path, or `null` when it was refused or could not be written (said already).
542
+ */
543
+ export async function addCollectionPage(name: string): Promise<string | null> {
544
+ const ctx = await readCreationContext();
545
+ const collection = ctx.collections.find((choice) => choice.name === name);
546
+ if (!collection) {
547
+ notify.error(`"${name}" is not a collection this project declares.`, { source: SOURCE });
548
+ return null;
549
+ }
550
+ const dir = `pages/${slugify(name) || name}`;
551
+ const path = `${dir}/[slug].json`;
552
+ const taken = await listTakenNames(dir);
553
+ if (taken.has("[slug].json")) {
554
+ notify.error(`${path} already exists.`, {
555
+ detail: "Open it in Design and point its $paths at this collection, or rename it first.",
556
+ path,
557
+ source: SOURCE,
558
+ });
559
+ return null;
560
+ }
561
+ const fields = schemaPresets(collection.schema, collection.formatName !== null);
562
+ const doc = collectionPageBody(
563
+ name,
564
+ fields,
565
+ layoutValueFor(defaultTemplateChoice(ctx), ctx),
566
+ ctx,
567
+ );
568
+ try {
569
+ await writeOne(path, { doc, format: null }, false);
570
+ } catch {
571
+ return null;
572
+ }
573
+ announce([path]);
574
+ return path;
575
+ }
576
+
577
+ // ─── The dialog ──────────────────────────────────────────────────────────────
578
+
579
+ /** What the reader has chosen so far, per kind, so switching kinds does not lose an answer. */
580
+ interface DialogState {
581
+ kind: CreationKind;
582
+ name: string;
583
+ /** The reader has typed: refusals are said from now on, not before. */
584
+ touched: boolean;
585
+ /**
586
+ * The page's template was chosen (by the request or a card), so a later change of language leaves
587
+ * it alone; until then the language chooses it ({@link templateForLocale}).
588
+ */
589
+ templateChosen: boolean;
590
+ url: string;
591
+ editingUrl: boolean;
592
+ pageStart: string;
593
+ wrap: string;
594
+ componentStart: string;
595
+ copyOf: string;
596
+ fields: FieldPreset[];
597
+ ownPage: boolean;
598
+ pageTemplate: string;
599
+ collection: string;
600
+ /** Page and Entry: the language, a declared tag; `""` in a project without `i18n`. */
601
+ locale: string;
602
+ /** Collection: one folder per language. */
603
+ localized: boolean;
604
+ files: File[];
605
+ dropActive: boolean;
606
+ busy: boolean;
607
+ }
608
+
609
+ /** The options the derivation reads for the chosen kind. */
610
+ function optionsOf(state: DialogState): CreationOptions {
611
+ switch (state.kind) {
612
+ case "page": {
613
+ const options: CreationOptions = { startFrom: state.pageStart };
614
+ if (state.editingUrl) {
615
+ options.url = state.url;
616
+ }
617
+ if (state.locale !== "") {
618
+ options.locale = state.locale;
619
+ }
620
+ return options;
621
+ }
622
+ case "template": {
623
+ return { startFrom: state.wrap };
624
+ }
625
+ case "component": {
626
+ const options: CreationOptions = { startFrom: state.componentStart };
627
+ if (state.componentStart === START_COPY && state.copyOf !== "") {
628
+ options.copyOf = state.copyOf;
629
+ }
630
+ return options;
631
+ }
632
+ case "collection": {
633
+ return {
634
+ fields: state.fields,
635
+ localized: state.localized,
636
+ ownPage: state.ownPage,
637
+ pageTemplate: state.pageTemplate,
638
+ };
639
+ }
640
+ case "entry": {
641
+ const options: CreationOptions = {};
642
+ if (state.collection !== "") {
643
+ options.collection = state.collection;
644
+ }
645
+ if (state.locale !== "") {
646
+ options.locale = state.locale;
647
+ }
648
+ return options;
649
+ }
650
+ default: {
651
+ return { files: state.files.length };
652
+ }
653
+ }
654
+ }
655
+
656
+ /** A card row. */
657
+ function card(
658
+ value: string,
659
+ label: string,
660
+ note: string,
661
+ chosen: string,
662
+ reason = "",
663
+ ): AddNewChoiceRow {
664
+ return {
665
+ checked: value === chosen ? "true" : "false",
666
+ disabled: reason !== "",
667
+ label,
668
+ note,
669
+ reason,
670
+ value,
671
+ };
672
+ }
673
+
674
+ /**
675
+ * The template cards for a page: every layout, then Article when the project writes prose, then
676
+ * Blank.
677
+ */
678
+ function startChoices(state: DialogState, ctx: CreationContext): AddNewChoiceRow[] {
679
+ const rows = ctx.layouts.map((layout) =>
680
+ card(
681
+ layout.ref,
682
+ layout.name,
683
+ ctx.defaultLayout !== null &&
684
+ normalizeLayoutPath(layout.ref) === normalizeLayoutPath(ctx.defaultLayout)
685
+ ? "the site's default"
686
+ : "a template",
687
+ state.pageStart,
688
+ ),
689
+ );
690
+ if (ctx.article !== null) {
691
+ rows.push(card(START_ARTICLE, "Article", "mostly writing", state.pageStart));
692
+ }
693
+ rows.push(card(START_BLANK, "Blank", "no template", state.pageStart));
694
+ return rows;
695
+ }
696
+
697
+ /** How many elements the canvas selection lifts out, said on its card. */
698
+ function selectionNote(): string {
699
+ const subject = convertibleSelection();
700
+ if (!subject) {
701
+ return "nothing selected";
702
+ }
703
+ const children = Array.isArray(subject.node.children) ? subject.node.children.length : 0;
704
+ const count = 1 + children;
705
+ return count === 1 ? "1 element" : `${count} elements`;
706
+ }
707
+
708
+ /** The chords `file.new` answers to, as this platform prints them. */
709
+ function chordNote(): string {
710
+ const keymap = activeRegistry()?.keymap;
711
+ const chords = keymap?.bindingsFor("file.new") ?? [];
712
+ if (!keymap || chords.length === 0) {
713
+ return "";
714
+ }
715
+ const [first, ...rest] = chords.map((chord) => keymap.format(chord));
716
+ /* The first chord is `⌘N`, which only the desktop app is handed: a browser keeps it for a new
717
+ window. Every other chord is delivered everywhere (add-new-commands.ts). */
718
+ return rest.length === 0
719
+ ? `${first} opens this anywhere.`
720
+ : `${first} opens this in the desktop app, ${rest.join(" or ")} anywhere.`;
721
+ }
722
+
723
+ /** The view the surface draws, from the state, the context and the derived result. */
724
+ function viewOf(
725
+ state: DialogState,
726
+ ctx: CreationContext,
727
+ creation: Creation,
728
+ problem: string,
729
+ ): AddNewView {
730
+ const info = KIND_INFO[state.kind];
731
+ const kinds = CREATION_KINDS.filter((id) => id !== "entry" || ctx.collections.length > 0).map(
732
+ (id): AddNewKindRow => ({
733
+ checked: id === state.kind ? "true" : "false",
734
+ hint: KIND_INFO[id].hint,
735
+ icon: KIND_INFO[id].icon,
736
+ id,
737
+ label: KIND_INFO[id].label,
738
+ }),
739
+ );
740
+ const slug = creation.config?.name ?? "";
741
+ const item = itemNoun(slug === "" ? "items" : slug);
742
+ const languages = ctx.i18n?.locales ?? [];
743
+ const chosen = ctx.collections.find((c) => c.name === state.collection);
744
+ /* A page always has a language in a multilingual site; an entry only when its collection keeps
745
+ one folder per language (an unlocalized collection is served under every locale, §13.3). */
746
+ const localeShown =
747
+ languages.length > 0 &&
748
+ (state.kind === "page" ||
749
+ (state.kind === "entry" && chosen !== undefined && isLocalizedCollection(chosen)));
750
+ const layoutOptions = [
751
+ ...ctx.layouts.map((layout) => ({ label: layout.name, value: layout.ref })),
752
+ { label: "No template", value: START_BLANK },
753
+ ];
754
+ return {
755
+ canConfirm: problem === "" && !state.busy,
756
+ chordNote: chordNote(),
757
+ collection: state.collection,
758
+ collectionOptions: ctx.collections.map((c) => ({ label: humanize(c.name), value: c.name })),
759
+ componentChoices: [
760
+ card("blank", "Blank", "an empty block", state.componentStart),
761
+ card(
762
+ START_SELECTION,
763
+ "Current selection",
764
+ selectionNote(),
765
+ state.componentStart,
766
+ ctx.hasSelection === true ? "" : "Select an element on the canvas first.",
767
+ ),
768
+ card(
769
+ START_COPY,
770
+ "Copy of…",
771
+ "an existing one",
772
+ state.componentStart,
773
+ (ctx.copyableTags ?? []).length > 0
774
+ ? ""
775
+ : "This project has no components of its own to copy yet.",
776
+ ),
777
+ ],
778
+ copyOf: state.copyOf,
779
+ copyOptions: [...(ctx.componentDocs?.keys() ?? [])].map((tag) => ({
780
+ label: `<${tag}>`,
781
+ value: tag,
782
+ })),
783
+ copying: state.componentStart === START_COPY,
784
+ cta: info.cta,
785
+ dropActive: state.dropActive ? "true" : "false",
786
+ editingUrl: state.editingUrl,
787
+ error: state.touched || state.kind === "media" ? (state.kind === "media" ? "" : problem) : "",
788
+ fileSummary: state.files.length === 0 ? "" : state.files.map((file) => file.name).join(", "),
789
+ itemNoun: item,
790
+ kind: state.kind,
791
+ kindHint: info.hint,
792
+ kindLabel: info.label,
793
+ kinds,
794
+ locale: state.locale,
795
+ localeOptions: languages.map((tag) => ({ label: localeLabel(tag), value: tag })),
796
+ localeShown,
797
+ localeSummary: languages.join(", "),
798
+ localizable: languages.length > 0,
799
+ localized: state.localized,
800
+ name: state.name,
801
+ nameLabel: info.nameLabel,
802
+ ownPage: state.ownPage,
803
+ pageTemplate: state.pageTemplate,
804
+ pattern: creation.route ?? `/${slug === "" ? "items" : slug}/:slug`,
805
+ placeholder: info.placeholder,
806
+ presets: FIELD_PRESETS.filter(
807
+ (preset) => preset !== "richText" || ctx.contentFormat !== null,
808
+ ).map((preset) => {
809
+ const selected = state.fields.includes(preset);
810
+ return {
811
+ icon: selected ? "check" : "plus",
812
+ id: preset,
813
+ label: FIELD_PRESET_INFO[preset].label,
814
+ selected,
815
+ };
816
+ }),
817
+ route: creation.route === undefined || creation.route === "" ? "/" : creation.route,
818
+ saved: creation.saved,
819
+ startChoices: startChoices(state, ctx),
820
+ templateOptions: layoutOptions,
821
+ uploadAccept: uploadAccept(),
822
+ url: state.url,
823
+ wrapChoices: [
824
+ ...ctx.layouts.map((layout) =>
825
+ card(layout.ref, layout.name, "keeps its frame around this one", state.wrap),
826
+ ),
827
+ card("", "Nothing", "a fresh frame", state.wrap),
828
+ ],
829
+ };
830
+ }
831
+
832
+ /** The kind a dialog opens on: the request's, else Page. Entry needs a collection to exist. */
833
+ function initialKind(request: AddNewRequest, ctx: CreationContext): CreationKind {
834
+ const kind = request.kind ?? "page";
835
+ return kind === "entry" && ctx.collections.length === 0 ? "page" : kind;
836
+ }
837
+
838
+ /** The dialog that is showing, if one is: its handle once mounted, and what it resolves with. */
839
+ interface OpenDialog {
840
+ handle: AddNewSurfaceHandle | null;
841
+ done: Promise<AddNewResult | null>;
842
+ }
843
+
844
+ let _open: OpenDialog | null = null;
845
+
846
+ /**
847
+ * Open Add new, and resolve with what it created, or `null` when it was dismissed.
848
+ *
849
+ * A second open while one is showing re-uses it: the dialog is modal, so a second request can only
850
+ * come from a chord or a script, and stacking two would leave the first with no way out.
851
+ */
852
+ export function openAddNew(request: AddNewRequest = {}): Promise<AddNewResult | null> {
853
+ if (_open) {
854
+ return _open.done;
855
+ }
856
+ let resolveDone!: (result: AddNewResult | null) => void;
857
+ const done = new Promise<AddNewResult | null>((resolve) => {
858
+ resolveDone = resolve;
859
+ });
860
+ const open: OpenDialog = { done, handle: null };
861
+ _open = open;
862
+ void start(request, resolveDone, open);
863
+ return done;
864
+ }
865
+
866
+ /** The dialog's whole life: read the project, mount, answer events until it closes. */
867
+ async function start(
868
+ request: AddNewRequest,
869
+ resolveDone: (result: AddNewResult | null) => void,
870
+ slot: OpenDialog,
871
+ ): Promise<void> {
872
+ const ctx = await readCreationContext();
873
+ const taken = new Map<string, Set<string>>();
874
+ const state: DialogState = {
875
+ busy: false,
876
+ collection: request.collection ?? ctx.collections[0]?.name ?? "",
877
+ // The language the request names (Content's language filter), else the site's default.
878
+ locale: declaredLocale(request.locale, ctx) ?? ctx.i18n?.defaultLocale ?? "",
879
+ localized: ctx.i18n !== null && ctx.i18n !== undefined,
880
+ // Blank even with a selection: converting edits the open page, so it is chosen, never assumed.
881
+ componentStart: "blank",
882
+ copyOf: "",
883
+ dropActive: false,
884
+ editingUrl: false,
885
+ fields: [...DEFAULT_FIELD_PRESETS],
886
+ files: [],
887
+ kind: initialKind(request, ctx),
888
+ name: request.name ?? "",
889
+ ownPage: true,
890
+ // The frame of the language the page is written in: `layouts/fr-ca.json` for a French page.
891
+ pageStart: templateForLocale(
892
+ ctx,
893
+ declaredLocale(request.locale, ctx) ?? ctx.i18n?.defaultLocale ?? "",
894
+ ),
895
+ pageTemplate: defaultTemplateChoice(ctx),
896
+ templateChosen: request.template !== undefined,
897
+ touched: (request.name ?? "") !== "",
898
+ url: "",
899
+ // The site's frame, as the board shows it: a layout nests inside a layout.
900
+ wrap: defaultTemplateChoice(ctx) === START_BLANK ? "" : defaultTemplateChoice(ctx),
901
+ };
902
+ if (request.template !== undefined) {
903
+ const resolved = resolveTemplateArg(request.template, ctx.layouts);
904
+ if (resolved !== null) {
905
+ state.pageStart = resolved;
906
+ state.pageTemplate = resolved === START_ARTICLE ? START_BLANK : resolved;
907
+ state.wrap = resolved === START_BLANK || resolved === START_ARTICLE ? "" : resolved;
908
+ }
909
+ }
910
+ if (ctx.contentFormat === null) {
911
+ // Rich text is a body, and a JSON collection has none: do not start with a chip it cannot honour.
912
+ state.fields = state.fields.filter((f) => f !== "richText");
913
+ }
914
+
915
+ let finished = false;
916
+ let result: AddNewResult | null = null;
917
+
918
+ const derive = () => deriveCreation(state.kind, state.name, optionsOf(state), ctx);
919
+ const judge = (creation: Creation) =>
920
+ creationProblem(creation, state.name, optionsOf(state), ctx, (dir) => taken.get(dir));
921
+ const render = () => {
922
+ const creation = derive();
923
+ slot.handle?.update(viewOf(state, ctx, creation, judge(creation)));
924
+ // A destination nobody has listed yet is listed now, and judged again when it lands.
925
+ if (directoriesToList(creation).some((dir) => !taken.has(dir))) {
926
+ void listFor(creation, taken).then(() => {
927
+ if (!finished) {
928
+ const again = derive();
929
+ slot.handle?.update(viewOf(state, ctx, again, judge(again)));
930
+ }
931
+ });
932
+ }
933
+ };
934
+ const change = (mutate: () => void) => {
935
+ mutate();
936
+ render();
937
+ };
938
+
939
+ const confirm = async () => {
940
+ if (state.busy) {
941
+ return;
942
+ }
943
+ state.touched = true;
944
+ const creation = derive();
945
+ await listFor(creation, taken, true);
946
+ const problem = judge(creation);
947
+ if (problem !== "") {
948
+ render();
949
+ return;
950
+ }
951
+ state.busy = true;
952
+ render();
953
+ try {
954
+ result = await perform(creation, optionsOf(state), state.files);
955
+ } catch (error) {
956
+ notify.error("Could not create it.", { detail: errorMessage(error), source: SOURCE });
957
+ result = null;
958
+ }
959
+ state.busy = false;
960
+ if (result === null) {
961
+ render();
962
+ return;
963
+ }
964
+ slot.handle?.close();
965
+ };
966
+
967
+ const first = derive();
968
+ const handle = openAddNewSurface({
969
+ actions: {
970
+ choose: (input) => {
971
+ const chosen = [...(input.files ?? [])];
972
+ input.value = "";
973
+ change(() => {
974
+ state.files = [...state.files, ...chosen];
975
+ });
976
+ },
977
+ closed: () => {
978
+ if (finished) {
979
+ return;
980
+ }
981
+ finished = true;
982
+ _open = null;
983
+ // Dismissed by the platform (Cancel, Escape): take the slot down too. Idempotent.
984
+ slot.handle?.close();
985
+ resolveDone(result);
986
+ },
987
+ confirm: () => {
988
+ void confirm();
989
+ },
990
+ dragOut: () => {
991
+ change(() => {
992
+ state.dropActive = false;
993
+ });
994
+ },
995
+ dragOver: () => {
996
+ if (!state.dropActive) {
997
+ change(() => {
998
+ state.dropActive = true;
999
+ });
1000
+ }
1001
+ },
1002
+ drop: (files) => {
1003
+ change(() => {
1004
+ state.dropActive = false;
1005
+ const dropped = files ? [...files] : [];
1006
+ if (dropped.length > 0) {
1007
+ // A file dropped anywhere on the dialog is an upload, whatever kind was showing.
1008
+ state.kind = "media";
1009
+ }
1010
+ state.files = [...state.files, ...dropped];
1011
+ });
1012
+ },
1013
+ pickCollection: (value) => {
1014
+ change(() => {
1015
+ state.collection = value;
1016
+ });
1017
+ },
1018
+ pickComponentStart: (value) => {
1019
+ change(() => {
1020
+ state.componentStart = value;
1021
+ });
1022
+ if (value === START_COPY && ctx.componentDocs === undefined) {
1023
+ void readComponentDocs().then((docs) => {
1024
+ ctx.componentDocs = docs;
1025
+ if (state.copyOf === "") {
1026
+ state.copyOf = docs.keys().next().value ?? "";
1027
+ }
1028
+ if (!finished) {
1029
+ render();
1030
+ }
1031
+ });
1032
+ }
1033
+ },
1034
+ pickLocale: (value) => {
1035
+ change(() => {
1036
+ state.locale = value;
1037
+ // Until a template is chosen, the page is framed in its language's own.
1038
+ if (!state.templateChosen) {
1039
+ state.pageStart = templateForLocale(ctx, value);
1040
+ }
1041
+ });
1042
+ },
1043
+ setLocalized: (checked) => {
1044
+ change(() => {
1045
+ state.localized = checked;
1046
+ });
1047
+ },
1048
+ pickCopy: (value) => {
1049
+ change(() => {
1050
+ state.copyOf = value;
1051
+ });
1052
+ },
1053
+ pickKind: (kind) => {
1054
+ change(() => {
1055
+ state.kind = kind;
1056
+ });
1057
+ },
1058
+ pickPageTemplate: (value) => {
1059
+ change(() => {
1060
+ state.pageTemplate = value;
1061
+ });
1062
+ },
1063
+ pickStart: (value) => {
1064
+ change(() => {
1065
+ state.pageStart = value;
1066
+ state.templateChosen = true;
1067
+ });
1068
+ },
1069
+ pickWrap: (value) => {
1070
+ change(() => {
1071
+ state.wrap = value;
1072
+ });
1073
+ },
1074
+ setName: (value) => {
1075
+ change(() => {
1076
+ state.name = value;
1077
+ state.touched = true;
1078
+ });
1079
+ },
1080
+ setOwnPage: (checked) => {
1081
+ change(() => {
1082
+ state.ownPage = checked;
1083
+ });
1084
+ },
1085
+ setUrl: (value) => {
1086
+ change(() => {
1087
+ state.url = value;
1088
+ state.touched = true;
1089
+ });
1090
+ },
1091
+ toggleUrl: () => {
1092
+ change(() => {
1093
+ if (!state.editingUrl) {
1094
+ state.url = derive().route ?? "/";
1095
+ }
1096
+ state.editingUrl = !state.editingUrl;
1097
+ });
1098
+ },
1099
+ togglePreset: (id) => {
1100
+ change(() => {
1101
+ const preset = FIELD_PRESETS.find((p) => p === id);
1102
+ if (preset === undefined) {
1103
+ return;
1104
+ }
1105
+ state.fields = state.fields.includes(preset)
1106
+ ? state.fields.filter((f) => f !== preset)
1107
+ : [...state.fields, preset];
1108
+ });
1109
+ },
1110
+ },
1111
+ layer: layerHost("dialog"),
1112
+ view: viewOf(state, ctx, first, judge(first)),
1113
+ });
1114
+ slot.handle = handle;
1115
+ render();
1116
+ }