@jxsuite/studio 6.1.2 → 8.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 (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,1014 @@
1
+ /**
2
+ * Add new, as a pure derivation: what a kind, a name and that kind's options WRITE.
3
+ *
4
+ * `specs/studio.md` §9.1.1. One function, {@link deriveCreation}, answers the directory, the file
5
+ * name, the extension, the route, the tag and the initial content of whatever the dialog is about
6
+ * to create, and {@link creationProblem} answers whether it may. The dialog prints the first as its
7
+ * "Saved as …" hint and refuses with the second; the flow (`create/add-new-flow.ts`) writes the
8
+ * first once the second is empty. What the dialog says is what it writes BY CONSTRUCTION, because
9
+ * there is no second place a path is composed.
10
+ *
11
+ * The refusals a file name earns are New File's (`files/new-file-name.ts`): blank, a slug of
12
+ * nothing, a `..`, a collision in the destination. A component's tag is HTML's valid custom element
13
+ * name (`create/element-name.ts`). Neither is restated here.
14
+ *
15
+ * Nothing here touches the platform, the DOM or a store. The caller hands in a
16
+ * {@link CreationContext} — the layouts, collections and components the project has — and a `taken`
17
+ * lookup over the directory listings it made, so every branch is a unit test.
18
+ *
19
+ * @docs studio/projects/pages-layouts-components
20
+ */
21
+
22
+ import { fileToRoute } from "@jxsuite/site/routes";
23
+ import { canonicalizeLocale, translationPathFor } from "@jxsuite/schema/locale";
24
+ import { LOCALE_PLACEHOLDER } from "@jxsuite/schema/asset-paths";
25
+ import { isJsonObject } from "@jxsuite/schema/guards";
26
+ import { seedEntry } from "../content/entry-model";
27
+ import { slugify, validateNewFileName } from "../files/new-file-name";
28
+ import { customElementNameProblem, deriveTagName } from "./element-name";
29
+ import { DEFAULT_MEDIA_DIR } from "../files/media-upload";
30
+ import { itemNoun } from "../content/content-collections";
31
+ import type {
32
+ ContentTypeSchema,
33
+ ContentTypeSchemaField,
34
+ JsonObject,
35
+ JxMutableNode,
36
+ } from "@jxsuite/schema/types";
37
+
38
+ // ─── Kinds ───────────────────────────────────────────────────────────────────
39
+
40
+ /** What Add new creates, in the order its list shows them. */
41
+ export const CREATION_KINDS = [
42
+ "page",
43
+ "collection",
44
+ "template",
45
+ "component",
46
+ "media",
47
+ "entry",
48
+ ] as const;
49
+
50
+ export type CreationKind = (typeof CREATION_KINDS)[number];
51
+
52
+ /** How a kind is named and introduced: the list row, the field label and the button. */
53
+ export interface KindInfo {
54
+ id: CreationKind;
55
+ label: string;
56
+ /** The muted line under the row's label. */
57
+ hint: string;
58
+ icon: string;
59
+ /** The primary button. */
60
+ cta: string;
61
+ /** The name field's label. Media has no name. */
62
+ nameLabel: string;
63
+ placeholder: string;
64
+ }
65
+
66
+ /** The copy the approved Add new board carries, one row per kind. */
67
+ export const KIND_INFO: Readonly<Record<CreationKind, KindInfo>> = {
68
+ collection: {
69
+ cta: "Create collection",
70
+ hint: "Posts, products, team… with a page each",
71
+ icon: "stack",
72
+ id: "collection",
73
+ label: "Collection",
74
+ nameLabel: "Collection name",
75
+ placeholder: "Recipes",
76
+ },
77
+ component: {
78
+ cta: "Create component",
79
+ hint: "A reusable block for any page",
80
+ icon: "cube",
81
+ id: "component",
82
+ label: "Component",
83
+ nameLabel: "Component name",
84
+ placeholder: "Pricing table",
85
+ },
86
+ entry: {
87
+ cta: "Create entry",
88
+ hint: "A new item in a collection",
89
+ icon: "file-text",
90
+ id: "entry",
91
+ label: "Entry",
92
+ nameLabel: "Entry name",
93
+ placeholder: "My first post",
94
+ },
95
+ media: {
96
+ cta: "Upload",
97
+ hint: "Images, video, documents",
98
+ icon: "image",
99
+ id: "media",
100
+ label: "Media",
101
+ nameLabel: "",
102
+ placeholder: "",
103
+ },
104
+ page: {
105
+ cta: "Create page",
106
+ hint: "A new URL on your site",
107
+ icon: "file",
108
+ id: "page",
109
+ label: "Page",
110
+ nameLabel: "Page name",
111
+ placeholder: "Pricing",
112
+ },
113
+ template: {
114
+ cta: "Create template",
115
+ hint: "Shared frame that pages sit inside",
116
+ icon: "frame-corners",
117
+ id: "template",
118
+ label: "Template",
119
+ nameLabel: "Template name",
120
+ placeholder: "Landing",
121
+ },
122
+ };
123
+
124
+ // ─── Options ─────────────────────────────────────────────────────────────────
125
+
126
+ /** A page's Start from: no template at all. */
127
+ export const START_BLANK = "blank";
128
+ /** A page's Start from: prose, saved in the project's writing format (Markdown). */
129
+ export const START_ARTICLE = "article";
130
+ /** A component's Start from: lift the canvas selection out (Convert to Component). */
131
+ export const START_SELECTION = "selection";
132
+ /** A component's Start from: a copy of an existing component. */
133
+ export const START_COPY = "copy";
134
+
135
+ /** The field presets a new collection can start with, in the board's order. */
136
+ export const FIELD_PRESETS = [
137
+ "title",
138
+ "image",
139
+ "richText",
140
+ "date",
141
+ "tags",
142
+ "number",
143
+ "link",
144
+ ] as const;
145
+
146
+ export type FieldPreset = (typeof FIELD_PRESETS)[number];
147
+
148
+ /** What a preset is called, and what it adds to the schema. `richText` is the entry's BODY. */
149
+ export const FIELD_PRESET_INFO: Readonly<
150
+ Record<FieldPreset, { label: string; key: string | null; schema: ContentTypeSchemaField | null }>
151
+ > = {
152
+ date: { key: "date", label: "Date", schema: { format: "date", type: "string" } },
153
+ image: { key: "cover", label: "Image", schema: { format: "image", type: "string" } },
154
+ link: { key: "link", label: "Link", schema: { format: "uri", type: "string" } },
155
+ number: { key: "number", label: "Number", schema: { type: "number" } },
156
+ richText: { key: null, label: "Rich text", schema: null },
157
+ tags: { key: "tags", label: "Tags", schema: { items: { type: "string" }, type: "array" } },
158
+ title: { key: "title", label: "Title", schema: { type: "string" } },
159
+ };
160
+
161
+ /** The presets a new collection starts with checked. */
162
+ export const DEFAULT_FIELD_PRESETS: readonly FieldPreset[] = ["title", "image", "richText"];
163
+
164
+ /** What the reader chose beyond the name. Each kind reads the options it has. */
165
+ export interface CreationOptions {
166
+ /**
167
+ * Page: {@link START_BLANK}, {@link START_ARTICLE} or a layout path. Template: a layout path to
168
+ * wrap in, or `""` for nothing. Component: `"blank"`, {@link START_SELECTION} or
169
+ * {@link START_COPY}.
170
+ */
171
+ startFrom?: string | undefined;
172
+ /** Page: the URL, when the reader edited it. Derived from the name otherwise. */
173
+ url?: string | undefined;
174
+ /** Collection: the field presets. */
175
+ fields?: readonly FieldPreset[] | undefined;
176
+ /** Collection: give each entry a page of its own. */
177
+ ownPage?: boolean | undefined;
178
+ /** Collection: the own page's template — a layout path, or {@link START_BLANK} for none. */
179
+ pageTemplate?: string | undefined;
180
+ /** Entry: the collection it is created in. */
181
+ collection?: string | undefined;
182
+ /**
183
+ * Page and Entry: the language it is written in, a declared locale tag. Ignored by a project that
184
+ * declares no `i18n`, and by an entry of a collection that is not localized.
185
+ */
186
+ locale?: string | undefined;
187
+ /** Collection: one folder per language (`content/<name>/{locale}`), in a project with `i18n`. */
188
+ localized?: boolean | undefined;
189
+ /** Component: the tag of the component a copy is made of. */
190
+ copyOf?: string | undefined;
191
+ /** Media: how many files are waiting to upload. */
192
+ files?: number | undefined;
193
+ }
194
+
195
+ // ─── Context ─────────────────────────────────────────────────────────────────
196
+
197
+ /** A layout the project has, as the Page panel's picker lists it. */
198
+ export interface LayoutChoice {
199
+ name: string;
200
+ /** The `$layout` reference: project-root relative, `./layouts/base.json`. */
201
+ ref: string;
202
+ }
203
+
204
+ /** A collection an entry can be created in. */
205
+ export interface CollectionChoice {
206
+ name: string;
207
+ /** Source directory, without a trailing slash. May hold a `{locale}` placeholder. */
208
+ dir: string;
209
+ ext: string;
210
+ schema: ContentTypeSchema | null;
211
+ /** The format class that writes its entries, or `null` for native JSON. */
212
+ formatName: string | null;
213
+ /**
214
+ * For a localized collection: each declared locale's directory as it is spelled on disk (`fr-CA`
215
+ * → `content/exhibitions/fr-CA`). A locale with no directory yet is absent, and gets the
216
+ * lowercase spelling Studio writes everywhere else.
217
+ */
218
+ localeDirs?: Readonly<Record<string, string>> | undefined;
219
+ }
220
+
221
+ /** The project's languages, as `@jxsuite/schema/locale` resolves its `i18n` block. */
222
+ export interface CreationI18n {
223
+ defaultLocale: string;
224
+ /** Canonical tags, default first. */
225
+ locales: readonly string[];
226
+ routing: "prefix-except-default" | "prefix-always";
227
+ }
228
+
229
+ /** A format the derivation can write a body-bearing document in. */
230
+ export interface WritingFormat {
231
+ /** The format class's import name, `Markdown`. */
232
+ name: string;
233
+ /** The extension it writes, `.md`. */
234
+ ext: string;
235
+ }
236
+
237
+ /** What the project has, read once by the flow and handed in. */
238
+ export interface CreationContext {
239
+ layouts: readonly LayoutChoice[];
240
+ /** `project.json` `defaults.layout`, as written, or `null`. */
241
+ defaultLayout: string | null;
242
+ collections: readonly CollectionChoice[];
243
+ /** Every `content` name `project.json` declares, file-backed ones included. */
244
+ declaredContent: readonly string[];
245
+ /** The tags the component registry knows. */
246
+ componentTags: readonly string[];
247
+ /** The tags a copy can be made of: the project's own JSON definitions, npm packages excluded. */
248
+ copyableTags?: readonly string[] | undefined;
249
+ /** Component definitions a copy can be made of, by tag. */
250
+ componentDocs?: ReadonlyMap<string, JxMutableNode> | undefined;
251
+ /** Whether the canvas has an element a conversion could lift out. */
252
+ hasSelection?: boolean | undefined;
253
+ /** The format an Article page is written in, or `null` when the project has none. */
254
+ article: WritingFormat | null;
255
+ /** The format a new collection's entries are written in, or `null` for native JSON. */
256
+ contentFormat: WritingFormat | null;
257
+ /** The project's languages, or `null`/absent when it declares no `i18n`. */
258
+ i18n?: CreationI18n | null | undefined;
259
+ }
260
+
261
+ /**
262
+ * Names already in a directory, lower-cased, or `undefined` while it has not been listed.
263
+ *
264
+ * `undefined` is "not known yet" and is never read as "free": the flow lists every destination it
265
+ * is about to write before it writes, and judges again.
266
+ */
267
+ export type TakenLookup = (dir: string) => ReadonlySet<string> | undefined;
268
+
269
+ // ─── The result ──────────────────────────────────────────────────────────────
270
+
271
+ /** A document to write, and the format that writes it (`null` is native JSON). */
272
+ export interface CreationBody {
273
+ doc: JsonObject;
274
+ format: string | null;
275
+ /** Write a frontmatter block. Only a format reads it. */
276
+ frontmatter?: boolean;
277
+ }
278
+
279
+ /** One more file a creation writes beside its main one. */
280
+ export interface CreationWrite {
281
+ path: string;
282
+ body: CreationBody;
283
+ }
284
+
285
+ /** What one Add new writes, derived. */
286
+ export interface Creation {
287
+ kind: CreationKind;
288
+ dir: string;
289
+ fileName: string;
290
+ /** The main file's extension, `.json`, `.md`, or `""` for Media. */
291
+ ext: string;
292
+ /** `dir/fileName`, or the upload directory for Media. */
293
+ path: string;
294
+ /** A component's custom element name. */
295
+ tag?: string;
296
+ /** The URL a page, or a collection's own page, opens at. */
297
+ route?: string;
298
+ content: CreationBody;
299
+ /** The files written beside the main one: a collection's first entry and its own page. */
300
+ extraWrites: CreationWrite[];
301
+ /** A collection's `project.json` `content` entry. */
302
+ config?: { name: string; entry: JsonObject };
303
+ /** The one-line "Saved as …" hint. */
304
+ saved: string;
305
+ }
306
+
307
+ // ─── Small derivations ───────────────────────────────────────────────────────
308
+
309
+ /** `dir/name`, with the project root spelled as no prefix. */
310
+ function join(dir: string, name: string): string {
311
+ return dir === "." || dir === "" ? name : `${dir}/${name}`;
312
+ }
313
+
314
+ /** `./layouts/base.json` → `layouts/base.json`: the spelling a path comparison needs. */
315
+ export function normalizeLayoutPath(path: string): string {
316
+ return path.replace(/^\.\//, "");
317
+ }
318
+
319
+ /**
320
+ * The `$layout` a document should carry for a chosen template.
321
+ *
322
+ * `undefined` is "say nothing": the project's default already applies, so writing it again would
323
+ * freeze this document to today's default. {@link START_BLANK} is `false` when a default exists to
324
+ * opt out of, and nothing when there is none.
325
+ */
326
+ export function layoutValueFor(choice: string, ctx: CreationContext): string | false | undefined {
327
+ const fallback = ctx.defaultLayout;
328
+ if (choice === START_BLANK || choice === "") {
329
+ return fallback ? false : undefined;
330
+ }
331
+ if (fallback && normalizeLayoutPath(fallback) === normalizeLayoutPath(choice)) {
332
+ return undefined;
333
+ }
334
+ return choice;
335
+ }
336
+
337
+ /** The template a new page, or a collection's page, starts from unless the reader picks another. */
338
+ export function defaultTemplateChoice(ctx: CreationContext): string {
339
+ const fallback = ctx.defaultLayout;
340
+ if (fallback) {
341
+ const match = ctx.layouts.find(
342
+ (layout) => normalizeLayoutPath(layout.ref) === normalizeLayoutPath(fallback),
343
+ );
344
+ return match?.ref ?? fallback;
345
+ }
346
+ return ctx.layouts[0]?.ref ?? START_BLANK;
347
+ }
348
+
349
+ /**
350
+ * The template a page in `locale` starts in: the layout named for that language
351
+ * (`layouts/fr-ca.json`, or one in `layouts/fr-ca/`), as a multilingual site keeps one frame per
352
+ * language with its own navigation and direction; else the project's default ({@link
353
+ * defaultTemplateChoice}). The default language usually has no layout of its own name, and is drawn
354
+ * in the default.
355
+ */
356
+ export function templateForLocale(ctx: CreationContext, locale: string | null | undefined): string {
357
+ const wanted = canonicalizeLocale(locale ?? "");
358
+ if (wanted !== null) {
359
+ const named = ctx.layouts.find((layout) => {
360
+ const parts = normalizeLayoutPath(layout.ref)
361
+ .replace(/\.json$/i, "")
362
+ .split("/");
363
+ return parts.some((part) => canonicalizeLocale(part) === wanted);
364
+ });
365
+ if (named) {
366
+ return named.ref;
367
+ }
368
+ }
369
+ return defaultTemplateChoice(ctx);
370
+ }
371
+
372
+ /** A collection's item, said in the singular. Defined with the collections it names. */
373
+ export { itemNoun } from "../content/content-collections";
374
+
375
+ /**
376
+ * A page URL as the reader typed it, reduced to path segments: each one slugified, empty ones
377
+ * dropped. `"/Docs/Getting Started/"` → `["docs", "getting-started"]`, `"/"` → `[]`.
378
+ */
379
+ export function urlSegments(url: string): string[] {
380
+ return url
381
+ .trim()
382
+ .replaceAll("\\", "/")
383
+ .split("/")
384
+ .map((segment) => slugify(segment))
385
+ .filter((segment) => segment !== "");
386
+ }
387
+
388
+ /** The schema a collection's presets add up to. Title, when chosen, is required. */
389
+ export function presetSchema(fields: readonly FieldPreset[]): ContentTypeSchema {
390
+ const properties: Record<string, ContentTypeSchemaField> = {};
391
+ for (const preset of FIELD_PRESETS) {
392
+ const info = FIELD_PRESET_INFO[preset];
393
+ if (fields.includes(preset) && info.key !== null && info.schema !== null) {
394
+ properties[info.key] = { ...info.schema };
395
+ }
396
+ }
397
+ const schema: ContentTypeSchema = { properties, type: "object" };
398
+ if (fields.includes("title")) {
399
+ schema.required = ["title"];
400
+ }
401
+ return schema;
402
+ }
403
+
404
+ /**
405
+ * The presets an EXISTING collection's schema amounts to, which is what its page binds: each preset
406
+ * whose key the schema declares, and Rich text when the entries are written in a format with a body
407
+ * (anything but JSON). The inverse of {@link presetSchema}, for a collection Add new did not make.
408
+ */
409
+ export function schemaPresets(
410
+ schema: ContentTypeSchema | null | undefined,
411
+ hasBody: boolean,
412
+ ): FieldPreset[] {
413
+ const properties = schema?.properties ?? {};
414
+ return FIELD_PRESETS.filter((preset) => {
415
+ const { key } = FIELD_PRESET_INFO[preset];
416
+ return key === null ? hasBody : Object.hasOwn(properties, key);
417
+ });
418
+ }
419
+
420
+ /**
421
+ * A seeded entry body: the schema's defaults and required keys, the title set to the name when the
422
+ * schema has one, written in the collection's format.
423
+ *
424
+ * The shape `content/entry-commands.ts` has always seeded with — `{ ...seed, children: [] }` with a
425
+ * frontmatter block when the seed has keys — so the New File route into a collection folder and Add
426
+ * new write the same bytes.
427
+ */
428
+ export function entryBody(
429
+ collection: Pick<CollectionChoice, "schema" | "formatName" | "ext">,
430
+ title?: string,
431
+ ): CreationBody {
432
+ const seed = seedEntry(collection.schema);
433
+ if (title !== undefined && title !== "" && collection.schema?.properties?.title !== undefined) {
434
+ seed.title = title;
435
+ }
436
+ if (collection.ext === ".json" || collection.formatName === null) {
437
+ return { doc: seed, format: null };
438
+ }
439
+ return {
440
+ doc: { ...seed, children: [] },
441
+ format: collection.formatName,
442
+ frontmatter: Object.keys(seed).length > 0,
443
+ };
444
+ }
445
+
446
+ /**
447
+ * The page every entry of a collection opens at: the blog starter's `pages/[slug].json`, field for
448
+ * field — `$paths` over the `slug` param, a `ContentEntry` named `entry`, the chosen `$layout` —
449
+ * with a body bound to whichever of title, image and rich text the collection has.
450
+ */
451
+ export function collectionPageBody(
452
+ name: string,
453
+ fields: readonly FieldPreset[],
454
+ layout: string | false | undefined,
455
+ ctx: Pick<CreationContext, "contentFormat">,
456
+ ): JsonObject {
457
+ const children: JsonObject[] = [];
458
+ if (fields.includes("title")) {
459
+ children.push({ tagName: "h1", textContent: "${state.entry.data.title}" });
460
+ }
461
+ if (fields.includes("image")) {
462
+ children.push({
463
+ attributes: {
464
+ alt: fields.includes("title") ? "${state.entry.data.title}" : "",
465
+ loading: "lazy",
466
+ src: "${state.entry.data.cover}",
467
+ },
468
+ tagName: "img",
469
+ });
470
+ }
471
+ if (fields.includes("richText") && ctx.contentFormat !== null) {
472
+ children.push({ children: "${state.entry.$children}", tagName: "div" });
473
+ }
474
+ const page: JsonObject = {};
475
+ if (layout !== undefined) {
476
+ page.$layout = layout;
477
+ }
478
+ page.$paths = { contentType: name, field: "id", param: "slug" };
479
+ page.title = fields.includes("title") ? "${state.entry.data.title}" : itemNoun(name);
480
+ page.state = {
481
+ entry: {
482
+ $prototype: "ContentEntry",
483
+ $src: "@jxsuite/parser/ContentEntry.class.json",
484
+ contentType: name,
485
+ field: "id",
486
+ id: { $ref: "#/$params/slug" },
487
+ },
488
+ };
489
+ page.tagName = "article";
490
+ page.children = children;
491
+ return page;
492
+ }
493
+
494
+ /** A declared locale, canonical, or `null` when `tag` is not one of the project's. */
495
+ export function declaredLocale(tag: string | undefined, ctx: CreationContext): string | null {
496
+ const i18n = ctx.i18n ?? null;
497
+ const canonical = canonicalizeLocale(tag);
498
+ return i18n !== null && canonical !== null && i18n.locales.includes(canonical) ? canonical : null;
499
+ }
500
+
501
+ /** Whether a collection keeps one folder per language. */
502
+ export function isLocalizedCollection(collection: Pick<CollectionChoice, "dir">): boolean {
503
+ return collection.dir.includes(LOCALE_PLACEHOLDER);
504
+ }
505
+
506
+ /**
507
+ * A localized collection's directory for one language: the spelling already on disk, or the
508
+ * lowercase tag `translationPathFor` writes for a language that has no folder yet.
509
+ */
510
+ export function localeDirOf(
511
+ collection: Pick<CollectionChoice, "dir" | "localeDirs">,
512
+ locale: string,
513
+ ): string {
514
+ return (
515
+ collection.localeDirs?.[locale] ??
516
+ collection.dir.replace(LOCALE_PLACEHOLDER, locale.toLowerCase())
517
+ );
518
+ }
519
+
520
+ /** A path moved into a language's tree (`pages/x.json` → `pages/fr-ca/x.json`), or unchanged. */
521
+ function inLocale(path: string, locale: string | null, ctx: CreationContext): string {
522
+ const i18n = ctx.i18n ?? null;
523
+ if (locale === null || i18n === null) {
524
+ return path;
525
+ }
526
+ return translationPathFor(path, locale, { ...i18n, locales: [...i18n.locales] }) ?? path;
527
+ }
528
+
529
+ /** `pages/fr-ca/x.json` → `["pages/fr-ca", "x.json"]`. */
530
+ function splitPath(path: string): [string, string] {
531
+ const slash = path.lastIndexOf("/");
532
+ return slash === -1 ? [".", path] : [path.slice(0, slash), path.slice(slash + 1)];
533
+ }
534
+
535
+ // ─── The derivation ──────────────────────────────────────────────────────────
536
+
537
+ /** A blank component's whole definition. */
538
+ function componentDoc(tagName: string, options: CreationOptions, ctx: CreationContext): JsonObject {
539
+ if (options.startFrom === START_COPY && options.copyOf !== undefined) {
540
+ const source = ctx.componentDocs?.get(options.copyOf);
541
+ if (source) {
542
+ // SAFETY: a definition read from disk is a JSON document, and this clone is a fresh copy.
543
+ // oxlint-disable-next-line unicorn/prefer-structured-clone
544
+ const copy = JSON.parse(JSON.stringify(source)) as JsonObject;
545
+ delete copy.$id;
546
+ copy.tagName = tagName;
547
+ return copy;
548
+ }
549
+ }
550
+ return { children: [], tagName };
551
+ }
552
+
553
+ /** A page: its file, its URL, and its body. */
554
+ function derivePage(name: string, options: CreationOptions, ctx: CreationContext): Creation {
555
+ const startFrom = options.startFrom ?? defaultTemplateChoice(ctx);
556
+ const article = startFrom === START_ARTICLE && ctx.article !== null ? ctx.article : null;
557
+ const ext = article?.ext ?? ".json";
558
+ const segments =
559
+ options.url === undefined ? [slugify(name)].filter((s) => s !== "") : urlSegments(options.url);
560
+ const stem =
561
+ segments.length === 0 ? (options.url === undefined ? "" : "index") : segments.at(-1)!;
562
+ const plainDir = ["pages", ...segments.slice(0, -1)].join("/");
563
+ const fileName = stem === "" ? "" : `${stem}${ext}`;
564
+ /* A page in another language lives in that language's tree, which is where its URL comes from
565
+ (`site-architecture.md` §13.1). Moved after the name is composed, so the hint and the write
566
+ are the same path. */
567
+ const locale = declaredLocale(options.locale, ctx);
568
+ const path =
569
+ fileName === "" ? join(plainDir, fileName) : inLocale(join(plainDir, fileName), locale, ctx);
570
+ const dir = fileName === "" ? plainDir : splitPath(path)[0];
571
+ const title = name.trim();
572
+ let content: CreationBody;
573
+ if (article) {
574
+ content = {
575
+ doc: { children: [{ tagName: "h1", textContent: title }], title },
576
+ format: article.name,
577
+ frontmatter: true,
578
+ };
579
+ } else {
580
+ const doc: JsonObject = {};
581
+ const layout = layoutValueFor(startFrom === START_ARTICLE ? START_BLANK : startFrom, ctx);
582
+ if (layout !== undefined) {
583
+ doc.$layout = layout;
584
+ }
585
+ doc.title = title;
586
+ doc.tagName = "main";
587
+ doc.children = [{ tagName: "h1", textContent: title }];
588
+ content = { doc, format: null };
589
+ }
590
+ const route = fileName === "" ? "" : fileToRoute(path).urlPattern;
591
+ return {
592
+ content,
593
+ dir,
594
+ ext,
595
+ extraWrites: [],
596
+ fileName,
597
+ kind: "page",
598
+ path,
599
+ route,
600
+ saved: fileName === "" ? "" : `Saved as ${path} · opens at ${route}`,
601
+ };
602
+ }
603
+
604
+ /** A collection: its `project.json` entry, its folder's first entry, and optionally its page. */
605
+ function deriveCollection(name: string, options: CreationOptions, ctx: CreationContext): Creation {
606
+ const slug = slugify(name);
607
+ const fields = options.fields ?? DEFAULT_FIELD_PRESETS;
608
+ const i18n = ctx.i18n ?? null;
609
+ // One folder per language, the first entry written in the default one (§13.3).
610
+ const localized = i18n !== null && options.localized !== false;
611
+ const root = `content/${slug}`;
612
+ const dir = localized ? `${root}/${i18n.defaultLocale.toLowerCase()}` : root;
613
+ const format = ctx.contentFormat;
614
+ const ext = format?.ext ?? ".json";
615
+ const item = itemNoun(slug);
616
+ // The noun in words (`menu item`) is a file name in a slug's spelling (`first-menu-item.md`).
617
+ const fileName = slug === "" ? "" : `first-${item.replaceAll(" ", "-")}${ext}`;
618
+ const schema = presetSchema(fields);
619
+ // The schema as the JSON `project.json` stores: a copy the guard proves is a JSON object.
620
+ const stored: unknown = structuredClone(schema);
621
+ const entry: JsonObject = {
622
+ format: format?.name ?? "json",
623
+ schema: isJsonObject(stored) ? stored : {},
624
+ source: localized ? `./${root}/${LOCALE_PLACEHOLDER}` : `./${root}`,
625
+ };
626
+ const first = entryBody({ ext, formatName: format?.name ?? null, schema }, `First ${item}`);
627
+ const extraWrites: CreationWrite[] = [];
628
+ let route: string | undefined;
629
+ let pageNote = "";
630
+ if (options.ownPage !== false && slug !== "") {
631
+ const chosen = options.pageTemplate ?? defaultTemplateChoice(ctx);
632
+ /* A localized collection is served in every language, so it gets a template page in each, the
633
+ way a multilingual site keeps one per language (`pages/fr-ca/<slug>/[slug].json`): one page
634
+ in the default language alone published every other language's entries at no URL. The
635
+ default language's page is first, and names the route the summary states. A language with a
636
+ frame of its own (`layouts/fr-ca.json`) is drawn in it unless a template other than the
637
+ default was chosen, which is then every language's. */
638
+ const locales = localized
639
+ ? [i18n.defaultLocale, ...i18n.locales.filter((tag) => tag !== i18n.defaultLocale)]
640
+ : [null];
641
+ const written: string[] = [];
642
+ for (const locale of locales) {
643
+ const pagePath = inLocale(`pages/${slug}/[slug].json`, locale, ctx);
644
+ const template =
645
+ locale === null || chosen !== defaultTemplateChoice(ctx)
646
+ ? chosen
647
+ : templateForLocale(ctx, locale);
648
+ route ??= fileToRoute(pagePath).urlPattern;
649
+ extraWrites.push({
650
+ body: {
651
+ doc: collectionPageBody(slug, fields, layoutValueFor(template, ctx), ctx),
652
+ format: null,
653
+ },
654
+ path: pagePath,
655
+ });
656
+ written.push(pagePath);
657
+ }
658
+ pageNote =
659
+ written.length === 1
660
+ ? `, and ${written[0]} at ${route}`
661
+ : `, and a page in each language (${written.join(", ")})`;
662
+ }
663
+ const result: Creation = {
664
+ config: { entry, name: slug },
665
+ content: first,
666
+ dir,
667
+ ext,
668
+ extraWrites,
669
+ fileName,
670
+ kind: "collection",
671
+ path: join(dir, fileName),
672
+ saved:
673
+ slug === ""
674
+ ? ""
675
+ : localized
676
+ ? `Adds a “${slug}” collection to project.json with one folder per language under ${root}/, a first entry in ${dir}/${pageNote}.`
677
+ : `Adds a “${slug}” collection to project.json, a ${dir}/ folder with a first entry${pageNote}.`,
678
+ };
679
+ if (route !== undefined) {
680
+ result.route = route;
681
+ }
682
+ return result;
683
+ }
684
+
685
+ /** A template: always `.json`, because both readers of a layout parse it as JSON only. */
686
+ function deriveTemplate(name: string, options: CreationOptions): Creation {
687
+ const slug = slugify(name);
688
+ const fileName = slug === "" ? "" : `${slug}.json`;
689
+ const path = join("layouts", fileName);
690
+ const doc: JsonObject = {};
691
+ const wrap = options.startFrom ?? "";
692
+ if (wrap !== "" && wrap !== START_BLANK) {
693
+ doc.$layout = wrap;
694
+ }
695
+ doc.tagName = "div";
696
+ doc.children = [{ children: [{ tagName: "slot" }], tagName: "main" }];
697
+ return {
698
+ content: { doc, format: null },
699
+ dir: "layouts",
700
+ ext: ".json",
701
+ extraWrites: [],
702
+ fileName,
703
+ kind: "template",
704
+ path,
705
+ saved: fileName === "" ? "" : `Saved as ${path} with one slot.`,
706
+ };
707
+ }
708
+
709
+ /** A component: the tag the name derives to, and a definition that carries it. */
710
+ function deriveComponent(name: string, options: CreationOptions, ctx: CreationContext): Creation {
711
+ const tagName = deriveTagName(name);
712
+ const fileName = tagName === "" ? "" : `${tagName}.json`;
713
+ const path = join("components", fileName);
714
+ return {
715
+ content: { doc: componentDoc(tagName, options, ctx), format: null },
716
+ dir: "components",
717
+ ext: ".json",
718
+ extraWrites: [],
719
+ fileName,
720
+ kind: "component",
721
+ path,
722
+ saved: fileName === "" ? "" : `Saved as ${path} with tag <${tagName}>.`,
723
+ tag: tagName,
724
+ };
725
+ }
726
+
727
+ /** An entry: a seeded file in the collection's source root, in its fixed format. */
728
+ function deriveEntry(name: string, options: CreationOptions, ctx: CreationContext): Creation {
729
+ const collection = ctx.collections.find((c) => c.name === options.collection) ?? null;
730
+ const slug = slugify(name);
731
+ const ext = collection?.ext ?? ".json";
732
+ /* A localized collection's `dir` names no directory anybody has: the entry goes in the folder of
733
+ the language it is written in, and with no language there is nowhere to put it. */
734
+ const locale = declaredLocale(options.locale, ctx);
735
+ const placeless = collection !== null && isLocalizedCollection(collection) && locale === null;
736
+ const dir =
737
+ collection === null
738
+ ? "content"
739
+ : isLocalizedCollection(collection) && locale !== null
740
+ ? localeDirOf(collection, locale)
741
+ : collection.dir;
742
+ const fileName = slug === "" || collection === null || placeless ? "" : `${slug}${ext}`;
743
+ const path = join(dir, fileName);
744
+ return {
745
+ content: collection ? entryBody(collection, name.trim()) : { doc: {}, format: null },
746
+ dir,
747
+ ext,
748
+ extraWrites: [],
749
+ fileName,
750
+ kind: "entry",
751
+ path,
752
+ saved: fileName === "" ? "" : `Saved as ${path} in ${collection!.name}.`,
753
+ };
754
+ }
755
+
756
+ /**
757
+ * Everything one Add new would write, from the kind, the name and the options.
758
+ *
759
+ * Total: an empty or unusable name derives an empty `fileName` and an empty `saved`, never a throw,
760
+ * so the dialog can call this on every keystroke and let {@link creationProblem} say why.
761
+ */
762
+ export function deriveCreation(
763
+ kind: CreationKind,
764
+ name: string,
765
+ options: CreationOptions,
766
+ ctx: CreationContext,
767
+ ): Creation {
768
+ switch (kind) {
769
+ case "page": {
770
+ return derivePage(name, options, ctx);
771
+ }
772
+ case "collection": {
773
+ return deriveCollection(name, options, ctx);
774
+ }
775
+ case "template": {
776
+ return deriveTemplate(name, options);
777
+ }
778
+ case "component": {
779
+ return deriveComponent(name, options, ctx);
780
+ }
781
+ case "entry": {
782
+ return deriveEntry(name, options, ctx);
783
+ }
784
+ default: {
785
+ const files = options.files ?? 0;
786
+ return {
787
+ content: { doc: {}, format: null },
788
+ dir: DEFAULT_MEDIA_DIR,
789
+ ext: "",
790
+ extraWrites: [],
791
+ fileName: "",
792
+ kind,
793
+ path: DEFAULT_MEDIA_DIR,
794
+ saved:
795
+ files === 0
796
+ ? `Saved under ${DEFAULT_MEDIA_DIR}/. No folder to choose.`
797
+ : `${files === 1 ? "1 file" : `${files} files`} will be saved under ${DEFAULT_MEDIA_DIR}/.`,
798
+ };
799
+ }
800
+ }
801
+ }
802
+
803
+ // ─── The refusal ─────────────────────────────────────────────────────────────
804
+
805
+ /** New File's refusal for one path, judged against its directory's listing when there is one. */
806
+ function fileProblem(path: string, taken: TakenLookup): string {
807
+ const slash = path.lastIndexOf("/");
808
+ const dir = slash === -1 ? "." : path.slice(0, slash);
809
+ const listing = taken(dir);
810
+ return validateNewFileName(path.slice(slash + 1), "", {
811
+ dir,
812
+ taken: listing ?? new Set<string>(),
813
+ });
814
+ }
815
+
816
+ /**
817
+ * The directories that could hold another page at a page's route, beside the one it is written in.
818
+ *
819
+ * `pages/about.json` opens where `pages/about.md` and `pages/about/index.json` do, and
820
+ * `pages/about/index.json` where `pages/about.json` does, so the refusal lists the neighbour too.
821
+ */
822
+ function routeNeighbours(creation: Creation): { dir: string; stem: string }[] {
823
+ if (creation.kind !== "page" || creation.fileName === "") {
824
+ return [];
825
+ }
826
+ const stem = creation.fileName.slice(0, creation.fileName.length - creation.ext.length);
827
+ if (stem !== "index") {
828
+ // `<dir>/<stem>.*` in its own directory, and `<dir>/<stem>/index.*` one down.
829
+ return [
830
+ { dir: creation.dir, stem },
831
+ { dir: join(creation.dir, stem), stem: "index" },
832
+ ];
833
+ }
834
+ const slash = creation.dir.lastIndexOf("/");
835
+ if (slash === -1) {
836
+ return [{ dir: creation.dir, stem }];
837
+ }
838
+ // `<parent>/<dir>.*` claims the route `<dir>/index.*` opens at.
839
+ return [
840
+ { dir: creation.dir, stem },
841
+ { dir: creation.dir.slice(0, slash), stem: creation.dir.slice(slash + 1) },
842
+ ];
843
+ }
844
+
845
+ /** Another page file that already opens at this page's route, or `null`. */
846
+ function routeClaimant(creation: Creation, taken: TakenLookup): string | null {
847
+ const own = creation.fileName.toLowerCase();
848
+ for (const { dir, stem } of routeNeighbours(creation)) {
849
+ const prefix = `${stem.toLowerCase()}.`;
850
+ for (const name of taken(dir) ?? []) {
851
+ if (name.startsWith(prefix) && !(dir === creation.dir && name === own)) {
852
+ return join(dir, name);
853
+ }
854
+ }
855
+ }
856
+ return null;
857
+ }
858
+
859
+ /** The name refusals every named kind shares: blank, then a slug of nothing. */
860
+ function nameProblem(name: string): string {
861
+ return validateNewFileName(name, "", {
862
+ choice: { ext: "", kind: "fixed" },
863
+ dir: ".",
864
+ taken: new Set(),
865
+ });
866
+ }
867
+
868
+ /**
869
+ * Why this creation may not be made, or `""` when it may.
870
+ *
871
+ * The first refusal that applies, in the order a reader fixes them: the name, then what the kind
872
+ * needs (a valid tag, a collection, a selection, a URL), then a collision on disk or in the
873
+ * project's declarations.
874
+ */
875
+ export function creationProblem(
876
+ creation: Creation,
877
+ name: string,
878
+ options: CreationOptions,
879
+ ctx: CreationContext,
880
+ taken: TakenLookup,
881
+ ): string {
882
+ if (creation.kind === "media") {
883
+ return (options.files ?? 0) === 0 ? "Drop files here, or browse for them." : "";
884
+ }
885
+ const named = nameProblem(name);
886
+ if (named !== "") {
887
+ return named;
888
+ }
889
+ switch (creation.kind) {
890
+ case "page": {
891
+ if (options.url !== undefined) {
892
+ if (options.url.replaceAll("\\", "/").split("/").includes("..")) {
893
+ return "A URL cannot step outside the site.";
894
+ }
895
+ if (!options.url.trim().startsWith("/")) {
896
+ return "A URL starts with a slash, such as /pricing.";
897
+ }
898
+ }
899
+ const claimed = routeClaimant(creation, taken);
900
+ if (claimed !== null) {
901
+ return `${claimed} already opens at ${creation.route ?? "/"}.`;
902
+ }
903
+ break;
904
+ }
905
+ case "component": {
906
+ const tag = creation.tag ?? "";
907
+ const problem = customElementNameProblem(tag);
908
+ if (problem !== "") {
909
+ return problem;
910
+ }
911
+ if (ctx.componentTags.includes(tag)) {
912
+ return `A component named <${tag}> already exists.`;
913
+ }
914
+ if (options.startFrom === START_SELECTION && ctx.hasSelection !== true) {
915
+ return "Select an element on the canvas to make a component from it.";
916
+ }
917
+ if (
918
+ options.startFrom === START_COPY &&
919
+ (options.copyOf === undefined || ctx.componentDocs?.get(options.copyOf) === undefined)
920
+ ) {
921
+ return "Choose the component to copy.";
922
+ }
923
+ break;
924
+ }
925
+ case "collection": {
926
+ const slug = creation.config?.name ?? "";
927
+ if (ctx.declaredContent.includes(slug)) {
928
+ return `A collection named “${slug}” already exists.`;
929
+ }
930
+ if (taken("content")?.has(slug) === true) {
931
+ return `content/${slug}/ already exists.`;
932
+ }
933
+ break;
934
+ }
935
+ case "entry": {
936
+ const collection = ctx.collections.find((c) => c.name === options.collection);
937
+ if (!collection) {
938
+ return "Choose the collection this entry belongs to.";
939
+ }
940
+ if (isLocalizedCollection(collection) && declaredLocale(options.locale, ctx) === null) {
941
+ return `Choose the language this ${itemNoun(collection.name)} is written in.`;
942
+ }
943
+ break;
944
+ }
945
+ default: {
946
+ break;
947
+ }
948
+ }
949
+ const paths = [
950
+ ...(creation.kind === "collection" ? [] : [creation.path]),
951
+ ...creation.extraWrites.map((write) => write.path),
952
+ ];
953
+ for (const path of paths) {
954
+ const problem = fileProblem(path, taken);
955
+ if (problem !== "") {
956
+ return problem;
957
+ }
958
+ }
959
+ return "";
960
+ }
961
+
962
+ /** Every directory a creation's refusal reads, so the flow lists exactly those. */
963
+ export function directoriesToList(creation: Creation): string[] {
964
+ const dirs = new Set<string>();
965
+ const add = (path: string) => {
966
+ const slash = path.lastIndexOf("/");
967
+ dirs.add(slash === -1 ? "." : path.slice(0, slash));
968
+ };
969
+ if (creation.kind === "media") {
970
+ return [];
971
+ }
972
+ if (creation.kind === "collection") {
973
+ dirs.add("content");
974
+ } else if (creation.fileName !== "") {
975
+ add(creation.path);
976
+ }
977
+ for (const write of creation.extraWrites) {
978
+ add(write.path);
979
+ }
980
+ for (const { dir } of routeNeighbours(creation)) {
981
+ dirs.add(dir);
982
+ }
983
+ return [...dirs];
984
+ }
985
+
986
+ /** Directories grouped by depth, shallowest group first: `.`, then `pages`, then `pages/a`. */
987
+ export function byDepth(dirs: readonly string[]): string[][] {
988
+ const depthOf = (dir: string) => (dir === "." ? 0 : dir.split("/").length);
989
+ const levels = new Map<number, string[]>();
990
+ for (const dir of dirs) {
991
+ const depth = depthOf(dir);
992
+ levels.set(depth, [...(levels.get(depth) ?? []), dir]);
993
+ }
994
+ return [...levels.entries()].toSorted(([a], [b]) => a - b).map(([, group]) => group);
995
+ }
996
+
997
+ /**
998
+ * Whether a directory is known not to exist because its parent has been listed and does not name
999
+ * it. A name in the listing (file or folder) is not proof of a folder, so it is listed as usual.
1000
+ *
1001
+ * @param dir Project-relative directory.
1002
+ * @param taken Lower-cased names per listed directory.
1003
+ */
1004
+ export function absentByParent(
1005
+ dir: string,
1006
+ taken: ReadonlyMap<string, ReadonlySet<string>>,
1007
+ ): boolean {
1008
+ if (dir === ".") {
1009
+ return false;
1010
+ }
1011
+ const slash = dir.lastIndexOf("/");
1012
+ const parent = taken.get(slash === -1 ? "." : dir.slice(0, slash));
1013
+ return parent !== undefined && !parent.has(dir.slice(slash + 1).toLowerCase());
1014
+ }