@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.
- package/README.md +5 -5
- package/dist/chunks/2020-5wtzce5g.js +4 -0
- package/dist/chunks/adapter-5tphg7xe.js +4 -0
- package/dist/chunks/add-new-flow-psym14sn.js +4 -0
- package/dist/chunks/cf-account-picker-haqazq5r.js +4 -0
- package/dist/chunks/cf-settings-h2wrh6hq.js +4 -0
- package/dist/chunks/{color-area-0fj8a4ga.js → color-area-6rbra5dy.js} +2 -2
- package/dist/chunks/{color-field-xch8kx2g.js → color-field-gy56c94g.js} +3 -3
- package/dist/chunks/color-m7r1vk54.js +4 -0
- package/dist/chunks/{color-slider-jxd8v350.js → color-slider-sqqdyf6c.js} +2 -2
- package/dist/chunks/{combobox-p0efwjke.js → combobox-72h32ejk.js} +3 -3
- package/dist/chunks/components-r22xca09.js +4 -0
- package/dist/chunks/content-source-mmppmcyg.js +4 -0
- package/dist/chunks/convert-to-repeater-he70fc2b.js +4 -0
- package/dist/chunks/editor-ccd9jwsa.js +4 -0
- package/dist/chunks/entry-commands-6qxz9e8p.js +4 -0
- package/dist/chunks/extension-sections-0egb88dm.js +4 -0
- package/dist/chunks/file-ops-qnab36ek.js +4 -0
- package/dist/chunks/files-rcn56qvm.js +4 -0
- package/dist/chunks/format-host-yxz0kjjs.js +4 -0
- package/dist/chunks/fs-events-d1qxq4z4.js +4 -0
- package/dist/chunks/grid-open-4z1g062t.js +4 -0
- package/dist/chunks/{grid-view-d2wq9ezg.js → grid-view-qg1b5eqx.js} +25 -25
- package/dist/chunks/{icons-pnfz478q.js → icons-63bjtbs7.js} +2 -2
- package/dist/chunks/{iframe-entry-jdcktez8.js → iframe-entry-2zggzajv.js} +4 -4
- package/dist/chunks/{iframe-entry-zap785cw.js → iframe-entry-76j8vwwx.js} +4 -4
- package/dist/chunks/iframe-entry-99nr5zfb.js +5 -0
- package/dist/chunks/{iframe-entry-x0ygf2yr.js → iframe-entry-pkgmcgp0.js} +4 -4
- package/dist/chunks/iframe-host-ssv7kyj0.js +4 -0
- package/dist/chunks/{index-zpjmzqfk.js → index-32eqzv1k.js} +3 -3
- package/dist/chunks/index-f0jn7nnp.js +4 -0
- package/dist/chunks/{javascript-m54mnwp6.js → javascript-pjp734q4.js} +3 -3
- package/dist/chunks/jsonMode-njkvsa02.js +13 -0
- package/dist/chunks/jx-validate-ttasrbh7.js +4 -0
- package/dist/chunks/layers-bgyr11tr.js +4 -0
- package/dist/chunks/layers-panel-7ez66zeq.js +4 -0
- package/dist/chunks/{listbox-e74qjyq6.js → listbox-c421cf8c.js} +2 -2
- package/dist/chunks/lit-html-v9gh687x.js +4 -0
- package/dist/chunks/media-library-sq468wts.js +4 -0
- package/dist/chunks/media-picker-shfe59pt.js +4 -0
- package/dist/chunks/{menu-n6yzswmy.js → menu-bdpzbfvs.js} +2 -2
- package/dist/chunks/{monaco-binding-7gx83x3r.js → monaco-binding-vq1hgnn0.js} +3 -3
- package/dist/chunks/monaco-buffer-3afgwvbp.js +4 -0
- package/dist/chunks/monaco-setup-xrd2cbkp.js +4 -0
- package/dist/chunks/notify-kzrjr3fv.js +4 -0
- package/dist/chunks/parse-v77xtwv7.js +4 -0
- package/dist/chunks/platform-7v4rtd7m.js +4 -0
- package/dist/chunks/popover-b89sfej7.js +4 -0
- package/dist/chunks/publish-panel-2z9gw8ee.js +4 -0
- package/dist/chunks/publish-quick-qx2455cj.js +4 -0
- package/dist/chunks/publish-workspace-wrc1dgwx.js +4 -0
- package/dist/chunks/quick-search-n0t0eqxb.js +4 -0
- package/dist/chunks/references-9547rg5s.js +4 -0
- package/dist/chunks/right-panel-wsgnr9pn.js +4 -0
- package/dist/chunks/source-control-kwktr9y6.js +4 -0
- package/dist/chunks/{split-n2gfpm5m.js → split-c9zfdgxf.js} +2 -2
- package/dist/chunks/studio-03ct6yfv.js +9 -0
- package/dist/chunks/studio-07rcfje3.js +5 -0
- package/dist/chunks/studio-3e54qh39.js +7 -0
- package/dist/chunks/studio-3exxhz5g.js +5 -0
- package/dist/chunks/studio-3q40e3rj.js +5 -0
- package/dist/chunks/studio-3qeh9t0h.js +6 -0
- package/dist/chunks/studio-3qqjeep5.js +6 -0
- package/dist/chunks/studio-43sd2mam.js +4 -0
- package/dist/chunks/studio-4hp8pkyt.js +5 -0
- package/dist/chunks/studio-58xg6j2q.js +9 -0
- package/dist/chunks/studio-5g0rprdt.js +7 -0
- package/dist/chunks/{studio-2j1cb95x.js → studio-6m380wp7.js} +4 -4
- package/dist/chunks/studio-80jyh1xh.js +4 -0
- package/dist/chunks/studio-83mk4bgg.js +9 -0
- package/dist/chunks/{studio-v0jzqdjm.js → studio-881z1xrx.js} +4 -4
- package/dist/chunks/studio-8m0y2qe7.js +5 -0
- package/dist/chunks/{studio-d81xfh1g.js → studio-8tr6cfrc.js} +4 -4
- package/dist/chunks/studio-8vcvfs7r.js +6 -0
- package/dist/chunks/studio-9tmebf0s.js +5 -0
- package/dist/chunks/studio-a2bvaptz.js +5 -0
- package/dist/chunks/studio-b7y5mr30.js +5 -0
- package/dist/chunks/studio-bavb31r0.js +5 -0
- package/dist/chunks/{studio-dgz8d0c1.js → studio-bbe4c90a.js} +4 -4
- package/dist/chunks/studio-c05wdnt3.js +5 -0
- package/dist/chunks/studio-ccv2svc6.js +5 -0
- package/dist/chunks/studio-d5grtb9e.js +16 -0
- package/dist/chunks/studio-df0baea9.js +5 -0
- package/dist/chunks/studio-dgk4dqtx.js +7 -0
- package/dist/chunks/studio-e1g64q43.js +5 -0
- package/dist/chunks/studio-ed1hp6zf.js +6 -0
- package/dist/chunks/studio-ed3xx40h.js +7 -0
- package/dist/chunks/studio-f14e1zdj.js +5 -0
- package/dist/chunks/studio-fbhxxc5f.js +5 -0
- package/dist/chunks/studio-fyh0jc96.js +5 -0
- package/dist/chunks/studio-gcxk20n7.js +5 -0
- package/dist/chunks/studio-jaaz8ce9.js +5 -0
- package/dist/chunks/{studio-90pq9km1.js → studio-jcnsdaep.js} +4 -4
- package/dist/chunks/studio-jsgf2q5k.js +7 -0
- package/dist/chunks/{studio-0q988f9w.js → studio-k3xxbagr.js} +3 -3
- package/dist/chunks/{studio-5ve6bbep.js → studio-k7evxqh4.js} +4 -4
- package/dist/chunks/studio-kt3pjkwd.js +5 -0
- package/dist/chunks/studio-m8g16bgz.js +5 -0
- package/dist/chunks/studio-mn0476ag.js +5 -0
- package/dist/chunks/studio-mx1smyq0.js +5 -0
- package/dist/chunks/studio-n0c5h6mf.js +5 -0
- package/dist/chunks/{studio-485046te.js → studio-nh6d342n.js} +7 -7
- package/dist/chunks/studio-nqxqf8p2.js +4 -0
- package/dist/chunks/studio-p4nrkvqr.js +5 -0
- package/dist/chunks/{studio-1fxkaaea.js → studio-pbxa5fpc.js} +4 -4
- package/dist/chunks/studio-pgmvdv7z.js +7 -0
- package/dist/chunks/studio-ph84avkf.js +5 -0
- package/dist/chunks/{studio-tq0rtfwj.js → studio-q0mvbaq6.js} +4 -4
- package/dist/chunks/studio-q0tccd43.js +5 -0
- package/dist/chunks/studio-q7rtx6zq.js +14 -0
- package/dist/chunks/{studio-a3g98wjr.js → studio-qgh13jtz.js} +4 -4
- package/dist/chunks/studio-qmn1xc24.js +5 -0
- package/dist/chunks/studio-qwdqwkj0.js +5 -0
- package/dist/chunks/studio-r5qy30vx.js +5 -0
- package/dist/chunks/studio-rj9eamf8.js +5 -0
- package/dist/chunks/studio-rptgsk96.js +5 -0
- package/dist/chunks/studio-rthyg42z.js +928 -0
- package/dist/chunks/studio-s19tp1xr.js +5 -0
- package/dist/chunks/studio-s32578pn.js +5 -0
- package/dist/chunks/studio-svb7yyve.js +5 -0
- package/dist/chunks/studio-syreb644.js +5 -0
- package/dist/chunks/studio-tf9jryb1.js +10 -0
- package/dist/chunks/studio-tfkznv39.js +5 -0
- package/dist/chunks/studio-tft6r105.js +5 -0
- package/dist/chunks/studio-txf2kysx.js +5 -0
- package/dist/chunks/studio-v8867ddv.js +12 -0
- package/dist/chunks/studio-vxq7fcvv.js +5 -0
- package/dist/chunks/studio-w4rz9hdk.js +5 -0
- package/dist/chunks/studio-wbcjbhy1.js +9 -0
- package/dist/chunks/studio-wc649wss.js +5 -0
- package/dist/chunks/studio-wys23mxz.js +5 -0
- package/dist/chunks/studio-x1x167jh.js +5 -0
- package/dist/chunks/studio-x9hsze61.js +5 -0
- package/dist/chunks/studio-y0n7yxea.js +9 -0
- package/dist/chunks/studio-y3ahhdpx.js +5 -0
- package/dist/chunks/studio-z97c48fk.js +5 -0
- package/dist/chunks/studio-zat62m0c.js +5 -0
- package/dist/chunks/studio-zcd2e7yz.js +5 -0
- package/dist/chunks/studio-zz9qe3rw.js +64 -0
- package/dist/chunks/toast-1gv0h53h.js +4 -0
- package/dist/chunks/{toast-host-ke5z4ng7.js → toast-host-1yg5ar8d.js} +3 -3
- package/dist/chunks/{tooltip-5v8ngwj2.js → tooltip-yypz28gq.js} +3 -3
- package/dist/chunks/{tree-mdq3v5cw.js → tree-9xa4hm7n.js} +2 -2
- package/dist/chunks/tsMode-epyfz1hv.js +14 -0
- package/dist/chunks/workspace-dz7nxfhv.js +4 -0
- package/dist/chunks/workspace-home-xsd7hkjw.js +4 -0
- package/dist/chunks/{ws-client-nze0k6mv.js → ws-client-bv552sfx.js} +3 -3
- package/dist/iframe-entry.js +20 -16
- package/dist/manifest.json +77 -63
- package/dist/studio.js +103 -104
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +12 -11
- package/src/browse/component-examples.ts +272 -0
- package/src/browse/library-commands.ts +64 -104
- package/src/browse/library-layouts.ts +263 -49
- package/src/browse/library-model.ts +181 -133
- package/src/browse/library-pane.ts +738 -446
- package/src/browse/library-preview.ts +149 -19
- package/src/browse/library-source.ts +308 -41
- package/src/browse/preview-root.ts +78 -0
- package/src/browse/preview-style.ts +155 -0
- package/src/canvas/asset-refs.ts +16 -1
- package/src/canvas/canvas-live-render.ts +222 -30
- package/src/canvas/canvas-origin.ts +37 -12
- package/src/canvas/canvas-page-context.ts +112 -0
- package/src/canvas/canvas-render.ts +113 -107
- package/src/canvas/canvas-utils.ts +57 -12
- package/src/canvas/iframe-drop.ts +107 -0
- package/src/canvas/iframe-entry.ts +24 -12
- package/src/canvas/iframe-host.ts +134 -62
- package/src/canvas/iframe-keys.ts +11 -0
- package/src/canvas/iframe-patch.ts +8 -7
- package/src/canvas/iframe-protocol.ts +24 -5
- package/src/canvas/iframe-render.ts +48 -1
- package/src/canvas/iframe-subtree.ts +5 -2
- package/src/canvas/npm-modules.ts +22 -0
- package/src/canvas/preview-navigate.ts +16 -0
- package/src/canvas/repeater-sources.ts +200 -0
- package/src/canvas/stage-scrollbar.ts +5 -1
- package/src/canvas/surface-registry.ts +6 -0
- package/src/commands/app-commands.ts +16 -6
- package/src/commands/budget.ts +10 -8
- package/src/commands/context.ts +35 -10
- package/src/commands/defaults.ts +69 -45
- package/src/commands/keymap.ts +9 -0
- package/src/commands/levels.ts +11 -5
- package/src/commands/live-context.ts +2 -0
- package/src/commands/registry.ts +10 -1
- package/src/content/collection-match.ts +6 -5
- package/src/content/collection-pages.ts +412 -0
- package/src/content/content-collections.ts +270 -0
- package/src/content/entry-commands.ts +51 -60
- package/src/content/entry-editor.ts +111 -18
- package/src/content/entry-fields.ts +13 -1
- package/src/content/entry-inspector.ts +225 -0
- package/src/content/entry-model.ts +17 -3
- package/src/content/entry-open.ts +80 -0
- package/src/content/entry-summaries.ts +440 -0
- package/src/content/file-record-ids.ts +80 -0
- package/src/content/file-records.ts +125 -0
- package/src/content/page-summaries.ts +129 -0
- package/src/content/record-entry.ts +201 -0
- package/src/content/writing-banner.ts +177 -0
- package/src/create/add-new-commands.ts +185 -0
- package/src/create/add-new-flow.ts +1116 -0
- package/src/create/add-new.ts +1014 -0
- package/src/create/element-name.ts +121 -0
- package/src/editor/convert-to-component.ts +67 -43
- package/src/editor/file-drop-action.ts +9 -1
- package/src/editor/inline-edit-apply.ts +44 -0
- package/src/editor/inline-edit.ts +10 -0
- package/src/editor/insert-component.ts +91 -0
- package/src/editor/insert-zone-action.ts +13 -4
- package/src/editor/shortcuts.ts +44 -22
- package/src/editor/slash-menu.ts +51 -8
- package/src/files/components.ts +74 -10
- package/src/files/file-ops.ts +15 -5
- package/src/files/files.ts +164 -150
- package/src/files/fs-events.ts +18 -7
- package/src/files/media-meta.ts +95 -1
- package/src/files/media-types.ts +55 -0
- package/src/files/media-upload.ts +209 -52
- package/src/files/new-file-name.ts +156 -0
- package/src/files/serialize-document.ts +13 -3
- package/src/format/format-host.ts +15 -0
- package/src/github/github-auth.ts +2 -2
- package/src/github/github-publish.ts +4 -3
- package/src/grid/grid-controller.ts +5 -1
- package/src/grid/grid-open.ts +0 -27
- package/src/grid/grid-panel.ts +54 -2
- package/src/grid/grid-reveal.ts +34 -0
- package/src/grid/grid-source.ts +3 -25
- package/src/grid/grid-view.ts +31 -6
- package/src/grid/sources/content-source.ts +75 -7
- package/src/grid/tabulator-tables.d.ts +6 -0
- package/src/hosting/document.ts +2 -2
- package/src/i18n/i18n-commands.ts +2 -1
- package/src/media/media-commands.ts +181 -0
- package/src/media/media-library-model.ts +232 -0
- package/src/media/media-library.ts +1283 -0
- package/src/media/media-pane.ts +47 -19
- package/src/packages/pull-package-sync.ts +1 -1
- package/src/panels/activity-panel.ts +21 -5
- package/src/panels/ai-chat/chat-markdown.ts +8 -8
- package/src/panels/block-action-bar.ts +11 -11
- package/src/panels/canvas-dnd-bridge.ts +5 -2
- package/src/panels/chat-panel.ts +6 -6
- package/src/panels/collections-panel.ts +306 -0
- package/src/panels/component-preview.ts +35 -13
- package/src/panels/data-explorer.ts +1 -1
- package/src/panels/data-grid.ts +4 -0
- package/src/panels/dnd.ts +17 -30
- package/src/panels/elements-panel.ts +9 -2
- package/src/panels/empty-state.ts +2 -2
- package/src/panels/formula-workspace.ts +2 -3
- package/src/panels/frontmatter-fields.ts +9 -9
- package/src/panels/git-diff-open.ts +9 -5
- package/src/panels/head-panel.ts +369 -103
- package/src/panels/i18n-panel.ts +11 -10
- package/src/panels/imports-panel.ts +2 -0
- package/src/panels/jump-bar.ts +27 -2
- package/src/panels/layers-panel.ts +36 -30
- package/src/panels/left-panel.ts +22 -8
- package/src/panels/navigator-panels.ts +23 -11
- package/src/panels/pane-context.ts +111 -29
- package/src/panels/pane-grid.ts +6 -6
- package/src/panels/panel-registry.ts +14 -9
- package/src/panels/properties-panel.ts +7 -3
- package/src/panels/right-panel.ts +199 -43
- package/src/panels/seo-modal.ts +41 -108
- package/src/panels/settings-menu.ts +52 -50
- package/src/panels/settings-pane.ts +268 -162
- package/src/panels/signals-panel.ts +3 -4
- package/src/panels/site-model.ts +367 -0
- package/src/panels/site-panel.ts +638 -0
- package/src/panels/style-panel.ts +37 -3
- package/src/panels/stylebook-doc.ts +142 -10
- package/src/panels/stylebook-layers-panel.ts +39 -80
- package/src/panels/stylebook-panel.ts +47 -10
- package/src/panels/styles-panel.ts +327 -0
- package/src/panels/tab-strip.ts +109 -25
- package/src/platform.ts +1 -1
- package/src/platforms/cloud.ts +28 -1
- package/src/platforms/default-platform.ts +4 -4
- package/src/platforms/devserver.ts +51 -25
- package/src/publish/deploy-checklist.ts +12 -7
- package/src/publish/pages-service.ts +38 -14
- package/src/publish/publish-changes.ts +491 -0
- package/src/publish/publish-commands.ts +83 -18
- package/src/publish/publish-ids.ts +49 -0
- package/src/publish/publish-quick.ts +148 -0
- package/src/publish/publish-workspace.ts +926 -0
- package/src/publish/source-control.ts +907 -0
- package/src/services/automation.ts +16 -20
- package/src/services/cf-settings.ts +4 -4
- package/src/services/connection.ts +2 -2
- package/src/services/directory-picker.ts +2 -2
- package/src/services/references.ts +0 -0
- package/src/services/render-critic.ts +21 -2
- package/src/services/settings/definitions.ts +25 -14
- package/src/settings/contributed-section.ts +49 -10
- package/src/settings/css-vars-editor.ts +93 -27
- package/src/settings/developer-panels.ts +43 -0
- package/src/settings/extension-sections.ts +20 -2
- package/src/settings/general-settings.ts +91 -21
- package/src/settings/preferences-dialog.ts +8 -1
- package/src/settings/project-sections.ts +27 -10
- package/src/settings/schema-field-ui.ts +3 -1
- package/src/settings/section-registry.ts +116 -11
- package/src/settings/settings-dialog.ts +192 -0
- package/src/settings/settings-document.ts +182 -162
- package/src/shell.ts +467 -431
- package/src/site-context.ts +64 -10
- package/src/state.ts +28 -9
- package/src/studio.ts +194 -41
- package/src/style/project-styles.ts +34 -0
- package/src/style/stylebook-specimens.ts +89 -0
- package/src/style/stylebook-target.ts +26 -0
- package/src/style/tag-inheritance.ts +105 -0
- package/src/style/variables-focus.ts +61 -0
- package/src/style/variables-view.ts +235 -0
- package/src/surfaces/add-new.json +1482 -0
- package/src/surfaces/add-new.ts +325 -0
- package/src/surfaces/ai-chat.ts +2 -2
- package/src/surfaces/boot-failure.json +4 -3
- package/src/surfaces/bottom-dock.json +1 -1
- package/src/surfaces/canvas-stage.json +30 -19
- package/src/surfaces/canvas-stage.ts +5 -5
- package/src/surfaces/collections-panel.json +615 -0
- package/src/surfaces/collections-panel.ts +168 -0
- package/src/surfaces/commandbar.json +241 -96
- package/src/surfaces/commandbar.ts +134 -99
- package/src/surfaces/empty-state.json +1 -1
- package/src/surfaces/entry-editor.json +136 -0
- package/src/surfaces/entry-editor.ts +21 -0
- package/src/surfaces/entry-inspector.json +371 -0
- package/src/surfaces/entry-inspector.ts +143 -0
- package/src/surfaces/inspector-dock.json +162 -14
- package/src/surfaces/inspector-dock.ts +46 -19
- package/src/surfaces/library-pane.json +670 -437
- package/src/surfaces/library-pane.ts +94 -59
- package/src/surfaces/media-browser.json +62 -6
- package/src/surfaces/media-browser.ts +15 -1
- package/src/surfaces/media-field.json +1 -1
- package/src/surfaces/media-library.json +1586 -0
- package/src/surfaces/media-library.ts +282 -0
- package/src/surfaces/media-pick-dialog.json +71 -0
- package/src/surfaces/media-pick-dialog.ts +132 -0
- package/src/surfaces/menu.ts +18 -0
- package/src/surfaces/pane-context.json +144 -77
- package/src/surfaces/pane-context.ts +24 -0
- package/src/surfaces/panel-deploy-checklist.json +12 -6
- package/src/surfaces/panel-deploy-checklist.ts +3 -1
- package/src/surfaces/panel-page.json +72 -2
- package/src/surfaces/panel-page.ts +38 -7
- package/src/surfaces/panel-signals.json +1 -1
- package/src/surfaces/panel-signals.ts +5 -5
- package/src/surfaces/panel-stylebook-layers.json +71 -204
- package/src/surfaces/panel-stylebook-layers.ts +0 -21
- package/src/surfaces/preferences.json +120 -48
- package/src/surfaces/preferences.ts +8 -0
- package/src/surfaces/publish-quick.json +428 -0
- package/src/surfaces/publish-quick.ts +165 -0
- package/src/surfaces/publish-workspace.json +2892 -0
- package/src/surfaces/publish-workspace.ts +391 -0
- package/src/surfaces/rail.json +1 -159
- package/src/surfaces/rail.ts +18 -75
- package/src/surfaces/schema-form.json +10 -2
- package/src/surfaces/schema-form.ts +2 -0
- package/src/surfaces/seo.json +1 -1
- package/src/surfaces/settings-contributed.json +5 -5
- package/src/surfaces/settings-contributed.ts +18 -2
- package/src/surfaces/settings-css-vars.json +44 -36
- package/src/surfaces/settings-css-vars.ts +77 -4
- package/src/surfaces/settings-deploy.json +2 -15
- package/src/surfaces/settings-deploy.ts +1 -1
- package/src/surfaces/settings-dialog.json +51 -0
- package/src/surfaces/settings-dialog.ts +169 -0
- package/src/surfaces/settings-overview.json +3 -3
- package/src/surfaces/settings-overview.ts +9 -2
- package/src/surfaces/settings-pane.json +269 -46
- package/src/surfaces/settings-pane.ts +85 -24
- package/src/surfaces/shell.json +2 -1
- package/src/surfaces/site-panel.json +1029 -0
- package/src/surfaces/site-panel.ts +233 -0
- package/src/surfaces/slash-menu.json +63 -21
- package/src/surfaces/slash-menu.ts +23 -0
- package/src/surfaces/statusbar.ts +39 -15
- package/src/surfaces/styles-panel.json +735 -0
- package/src/surfaces/styles-panel.ts +195 -0
- package/src/surfaces/styles-variables.json +149 -0
- package/src/surfaces/styles-variables.ts +145 -0
- package/src/surfaces/tab-strip.json +4 -4
- package/src/surfaces/welcome.ts +1 -1
- package/src/surfaces/writing-banner.json +94 -0
- package/src/surfaces/writing-banner.ts +80 -0
- package/src/tabs/bare-repeaters.ts +101 -0
- package/src/tabs/tab.ts +47 -7
- package/src/types.ts +21 -26
- package/src/ui/form-controls.ts +47 -8
- package/src/ui/layers.ts +13 -0
- package/src/ui/media-picker.ts +284 -40
- package/src/ui/regions.ts +7 -6
- package/src/ui/schema-form.ts +259 -27
- package/src/ui/virtual-window.ts +26 -1
- package/src/utils/base64.ts +3 -3
- package/src/utils/edit-display.ts +322 -67
- package/src/utils/local-day.ts +18 -0
- package/src/workspace/pane-derive.ts +4 -4
- package/src/workspace/session.ts +28 -7
- package/src/workspace/tab-workspace.ts +137 -0
- package/src/workspace/workspace-back.ts +189 -0
- package/src/workspace/workspace-home.ts +133 -0
- package/src/workspace/workspace-ids.ts +129 -0
- package/src/workspace/workspace-switch.ts +372 -0
- package/src/workspace/workspace.ts +125 -11
- package/styles/shell-frame.css +30 -7
- package/styles/shell-frame.json +15 -3
- package/dist/chunks/2020-f2gkpy8d.js +0 -4
- package/dist/chunks/adapter-4912g339.js +0 -4
- package/dist/chunks/cf-account-picker-wccaenh6.js +0 -4
- package/dist/chunks/cf-settings-br6105tr.js +0 -4
- package/dist/chunks/color-28ksk2qm.js +0 -4
- package/dist/chunks/components-db68vsn7.js +0 -4
- package/dist/chunks/content-source-1449qesq.js +0 -4
- package/dist/chunks/convert-to-repeater-5h457egj.js +0 -4
- package/dist/chunks/editor-x9jn86a7.js +0 -4
- package/dist/chunks/entry-commands-may24cpt.js +0 -4
- package/dist/chunks/extension-sections-pvr14vxr.js +0 -4
- package/dist/chunks/file-ops-x9v54pcj.js +0 -4
- package/dist/chunks/files-j3knqf0s.js +0 -4
- package/dist/chunks/format-host-tyre2trb.js +0 -4
- package/dist/chunks/fs-events-64ma9htc.js +0 -4
- package/dist/chunks/git-panel-84ps5dex.js +0 -4
- package/dist/chunks/iframe-entry-fvbk89gm.js +0 -5
- package/dist/chunks/iframe-host-ck6vejt5.js +0 -4
- package/dist/chunks/index-b8xw3vy9.js +0 -4
- package/dist/chunks/jsonMode-w15cmrcm.js +0 -13
- package/dist/chunks/jx-validate-at3zn8wa.js +0 -4
- package/dist/chunks/layers-h4xp1pss.js +0 -4
- package/dist/chunks/layers-panel-m5hw1d7g.js +0 -4
- package/dist/chunks/lit-html-8t8t3dnr.js +0 -4
- package/dist/chunks/media-picker-2cweexc5.js +0 -4
- package/dist/chunks/menu-ynw13y0s.js +0 -4
- package/dist/chunks/monaco-buffer-qzxn567r.js +0 -4
- package/dist/chunks/monaco-setup-gxf6656g.js +0 -4
- package/dist/chunks/notify-cj5zbrs2.js +0 -4
- package/dist/chunks/parse-b7ddhr8m.js +0 -4
- package/dist/chunks/platform-3dfws9js.js +0 -4
- package/dist/chunks/popover-jt2gxmzs.js +0 -4
- package/dist/chunks/publish-panel-1gvxbj21.js +0 -4
- package/dist/chunks/quick-search-0dajrwqg.js +0 -4
- package/dist/chunks/references-49qgt404.js +0 -4
- package/dist/chunks/right-panel-jm1bwe53.js +0 -4
- package/dist/chunks/section-registry-6kwc4fky.js +0 -4
- package/dist/chunks/studio-0rt52vpq.js +0 -5
- package/dist/chunks/studio-18vgbhs5.js +0 -5
- package/dist/chunks/studio-24mccdrj.js +0 -5
- package/dist/chunks/studio-2eh507zk.js +0 -9
- package/dist/chunks/studio-2hcbccfv.js +0 -5
- package/dist/chunks/studio-2jbs3rxv.js +0 -5
- package/dist/chunks/studio-4h8k4dw9.js +0 -5
- package/dist/chunks/studio-5f6n18kx.js +0 -5
- package/dist/chunks/studio-5gthpky6.js +0 -5
- package/dist/chunks/studio-6ep3js6b.js +0 -9
- package/dist/chunks/studio-6wkmf3wz.js +0 -9
- package/dist/chunks/studio-841rf3cg.js +0 -5
- package/dist/chunks/studio-8gmwgheg.js +0 -5
- package/dist/chunks/studio-8qm0ep50.js +0 -5
- package/dist/chunks/studio-924p1tm1.js +0 -6
- package/dist/chunks/studio-a3shpz9y.js +0 -5
- package/dist/chunks/studio-amsbaaar.js +0 -6
- package/dist/chunks/studio-at5spjbc.js +0 -7
- package/dist/chunks/studio-b2xv945e.js +0 -5
- package/dist/chunks/studio-bn5nkafw.js +0 -5
- package/dist/chunks/studio-bw6q5xjf.js +0 -9
- package/dist/chunks/studio-c7q0t9x1.js +0 -5
- package/dist/chunks/studio-camnhc96.js +0 -6
- package/dist/chunks/studio-cffqvnzd.js +0 -5
- package/dist/chunks/studio-efvfrsf8.js +0 -5
- package/dist/chunks/studio-er30bpcx.js +0 -5
- package/dist/chunks/studio-ezscgr64.js +0 -5
- package/dist/chunks/studio-ffrrncvz.js +0 -5
- package/dist/chunks/studio-fpphgn8y.js +0 -10
- package/dist/chunks/studio-j1kvhdjk.js +0 -4
- package/dist/chunks/studio-j430n16g.js +0 -7
- package/dist/chunks/studio-jjkg84nj.js +0 -5
- package/dist/chunks/studio-m0q32ars.js +0 -4
- package/dist/chunks/studio-m6bdst11.js +0 -5
- package/dist/chunks/studio-nv2cptd4.js +0 -64
- package/dist/chunks/studio-nwfrc98n.js +0 -5
- package/dist/chunks/studio-p1wgvp7r.js +0 -5
- package/dist/chunks/studio-pag2wncm.js +0 -5
- package/dist/chunks/studio-qvanvzfd.js +0 -5
- package/dist/chunks/studio-qwa64kt7.js +0 -5
- package/dist/chunks/studio-rk1zc4x3.js +0 -928
- package/dist/chunks/studio-se2r3ptc.js +0 -5
- package/dist/chunks/studio-sg2a0dh0.js +0 -5
- package/dist/chunks/studio-tjzjhe68.js +0 -5
- package/dist/chunks/studio-v2w7q8r1.js +0 -7
- package/dist/chunks/studio-v5vjq1h0.js +0 -4
- package/dist/chunks/studio-v8aga3vp.js +0 -14
- package/dist/chunks/studio-vfcdrfmv.js +0 -5
- package/dist/chunks/studio-vmfkgdjf.js +0 -5
- package/dist/chunks/studio-wt1816jj.js +0 -5
- package/dist/chunks/studio-wz73q5kq.js +0 -4
- package/dist/chunks/studio-y04qtxvp.js +0 -7
- package/dist/chunks/studio-y21g8zzg.js +0 -5
- package/dist/chunks/studio-y64hrt8p.js +0 -12
- package/dist/chunks/studio-ynn90f58.js +0 -16
- package/dist/chunks/studio-yxz682sx.js +0 -5
- package/dist/chunks/studio-zp7snvkq.js +0 -5
- package/dist/chunks/studio-zq4z6tm6.js +0 -5
- package/dist/chunks/toast-c9fn075p.js +0 -4
- package/dist/chunks/tsMode-sf49ee9x.js +0 -14
- package/dist/chunks/workspace-jtfahpee.js +0 -4
- package/src/panels/frontmatter-panel.ts +0 -711
- package/src/panels/git-panel.ts +0 -1108
- package/src/surfaces/doc-header.json +0 -776
- package/src/surfaces/doc-header.ts +0 -222
- package/src/surfaces/git-panel.json +0 -1880
- 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
|
+
}
|