@jxsuite/studio 6.1.2 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which page each collection's entries are published at — read from the pages, never stored.
|
|
3
|
+
*
|
|
4
|
+
* `project.json` records no URL pattern and no template for a collection. The route lives in the
|
|
5
|
+
* template PAGE: a dynamic file under `pages/` whose `$paths.contentType` names the collection
|
|
6
|
+
* (`pages/blog/[slug].json`, `$paths: { contentType: "posts", param: "slug" }`), or whose
|
|
7
|
+
* `$paths.values` enumerate the ids of a `ContentEntry` it reads by its route parameter (a taxonomy
|
|
8
|
+
* page, `pages/author/[slug].json`), and the layout an entry is drawn in is that page's `$layout`.
|
|
9
|
+
* A second record of either in `project.json` would be free to disagree with the router, so the
|
|
10
|
+
* Content workspace's header line ("Each post has a page at `/blog/:slug` using Post") and the
|
|
11
|
+
* canvas's entry wrapping (`canvas/canvas-live-render.ts`) both read this index (`specs/studio.md`
|
|
12
|
+
* §22.5).
|
|
13
|
+
*
|
|
14
|
+
* One read per invalidation: a walk of `pages/`, then a parse of each dynamic page, cached until a
|
|
15
|
+
* file event drops it ({@link invalidateCollectionPages}, wired beside the Site panel's index in
|
|
16
|
+
* `studio.ts`). The read never rejects; a page that cannot be read or parsed is a page with no
|
|
17
|
+
* collection.
|
|
18
|
+
*
|
|
19
|
+
* @docs studio/projects/browse
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { isJsonObject, isNumber, isString } from "@jxsuite/schema/guards";
|
|
23
|
+
import { fileToRoute, isRoutedPageFile } from "@jxsuite/site/routes";
|
|
24
|
+
import {
|
|
25
|
+
canonicalizeLocale,
|
|
26
|
+
localeOfPath,
|
|
27
|
+
resolveI18n,
|
|
28
|
+
servedLocaleOfPath,
|
|
29
|
+
} from "@jxsuite/schema/locale";
|
|
30
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
31
|
+
import { projectState } from "../store";
|
|
32
|
+
import { pagePathsDef } from "../page-params";
|
|
33
|
+
import type { FormatFrontmatter } from "../format/format-host";
|
|
34
|
+
import type { JxDocumentValue, JxMutableNode } from "@jxsuite/schema/types";
|
|
35
|
+
import type { EntryFields } from "./entry-fields";
|
|
36
|
+
import type { DirEntry } from "../types";
|
|
37
|
+
|
|
38
|
+
/** What a page under `pages/` says about collections and layouts. */
|
|
39
|
+
export interface PageFacts {
|
|
40
|
+
/** The collection its `$paths` expands over, or `null` for a page of its own. */
|
|
41
|
+
contentType: string | null;
|
|
42
|
+
/** The route parameter an entry fills (`slug`), when `$paths` names one. */
|
|
43
|
+
param: string | null;
|
|
44
|
+
/** The entry field that fills it (`sku`), when `$paths` names one other than `id`. */
|
|
45
|
+
field: string | null;
|
|
46
|
+
/** Its `$layout`: a path, `false` (no layout), or `undefined` (the project's default). */
|
|
47
|
+
layout: string | false | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* The ids a `$paths.values` page enumerates (`["chad-beiler", "kris-bucher"]`): the only entries
|
|
50
|
+
* it publishes a URL for. `null` for a `$paths.contentType` page, which publishes every entry,
|
|
51
|
+
* and for a `values` that is not a list of ids written in the page.
|
|
52
|
+
*/
|
|
53
|
+
values?: readonly string[] | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A collection's template page. */
|
|
57
|
+
export interface CollectionPage {
|
|
58
|
+
/** The page file, project-relative (`pages/blog/[slug].json`). */
|
|
59
|
+
path: string;
|
|
60
|
+
/** Its URL pattern (`/blog/:slug`). */
|
|
61
|
+
pattern: string;
|
|
62
|
+
/** The parameter an entry's id fills, or `null` when the page names none. */
|
|
63
|
+
param: string | null;
|
|
64
|
+
/** The entry field the parameter is read from, as {@link PageFacts.field}. */
|
|
65
|
+
field: string | null;
|
|
66
|
+
/** The page's `$layout`, as {@link PageFacts.layout}. */
|
|
67
|
+
layout: string | false | undefined;
|
|
68
|
+
/** The only entry ids it publishes, as {@link PageFacts.values}; null or absent for every one. */
|
|
69
|
+
values?: readonly string[] | null;
|
|
70
|
+
/**
|
|
71
|
+
* The language the page is served in (`pages/ar/exhibitions/[slug].json` → `ar`), or `null` in a
|
|
72
|
+
* project without `i18n` and for a page outside every locale tree.
|
|
73
|
+
*/
|
|
74
|
+
locale: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The collection a page enumerating its own `$paths.values` publishes one entry of per URL: the
|
|
79
|
+
* `contentType` of its `ContentEntry` state entry whose `id` is the route parameter (`{ "$ref":
|
|
80
|
+
* "#/$params/slug" }`). A taxonomy page (`pages/author/[slug].json`, listing the author slugs and
|
|
81
|
+
* reading `state.author` from the `author` collection by that slug) states its collection that way
|
|
82
|
+
* rather than with `$paths.contentType`, and is the collection's page all the same.
|
|
83
|
+
*/
|
|
84
|
+
function entryCollectionOf(
|
|
85
|
+
document: JxMutableNode | null,
|
|
86
|
+
frontmatter: FormatFrontmatter | null,
|
|
87
|
+
param: string,
|
|
88
|
+
): string | null {
|
|
89
|
+
const state = document?.state ?? frontmatter?.state;
|
|
90
|
+
const target = `#/$params/${param}`;
|
|
91
|
+
for (const entry of Object.values(isJsonObject(state) ? state : {})) {
|
|
92
|
+
if (!isJsonObject(entry) || entry.$prototype !== "ContentEntry") {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const { contentType, id } = entry;
|
|
96
|
+
if (isJsonObject(id) && id.$ref === target && isString(contentType) && contentType !== "") {
|
|
97
|
+
return contentType;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The facts a page document states, from its parsed tree and (for a format page) frontmatter. */
|
|
104
|
+
export function pageFactsOf(
|
|
105
|
+
document: JxMutableNode | null,
|
|
106
|
+
frontmatter: FormatFrontmatter | null,
|
|
107
|
+
): PageFacts {
|
|
108
|
+
const paths = pagePathsDef({ document, frontmatter });
|
|
109
|
+
let contentType: string | null = null;
|
|
110
|
+
let param: string | null = null;
|
|
111
|
+
let field: string | null = null;
|
|
112
|
+
let values: string[] | null = null;
|
|
113
|
+
if (paths && !Array.isArray(paths) && "contentType" in paths) {
|
|
114
|
+
const type = paths.contentType;
|
|
115
|
+
contentType = isString(type) && type !== "" ? type : null;
|
|
116
|
+
const named = "param" in paths ? paths.param : undefined;
|
|
117
|
+
param = isString(named) && named !== "" ? named : null;
|
|
118
|
+
const from = "field" in paths ? paths.field : undefined;
|
|
119
|
+
field = isString(from) && from !== "" && from !== "id" ? from : null;
|
|
120
|
+
} else if (paths && !Array.isArray(paths) && "values" in paths) {
|
|
121
|
+
const named = "param" in paths ? paths.param : undefined;
|
|
122
|
+
const enumerated = isString(named) && named !== "" ? named : null;
|
|
123
|
+
contentType = enumerated === null ? null : entryCollectionOf(document, frontmatter, enumerated);
|
|
124
|
+
param = contentType === null ? null : enumerated;
|
|
125
|
+
// The ids it lists are the entries it publishes; any other entry of the collection has no page.
|
|
126
|
+
const listed = paths.values;
|
|
127
|
+
values =
|
|
128
|
+
contentType !== null && Array.isArray(listed)
|
|
129
|
+
? listed.flatMap((value) => {
|
|
130
|
+
const id = segmentOf(value);
|
|
131
|
+
return id === null ? [] : [id];
|
|
132
|
+
})
|
|
133
|
+
: null;
|
|
134
|
+
}
|
|
135
|
+
const own = document?.$layout ?? frontmatter?.$layout;
|
|
136
|
+
const layout = isString(own) || own === false ? own : undefined;
|
|
137
|
+
return { contentType, field, layout, param, ...(values !== null && { values }) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Read one page's facts from disk. A file that cannot be read or parsed states none.
|
|
142
|
+
*
|
|
143
|
+
* @param path - Project-relative page path.
|
|
144
|
+
*/
|
|
145
|
+
export async function readPageFacts(path: string): Promise<PageFacts> {
|
|
146
|
+
const none: PageFacts = { contentType: null, field: null, layout: undefined, param: null };
|
|
147
|
+
try {
|
|
148
|
+
const source = await getPlatform().readFile(path);
|
|
149
|
+
if (path.endsWith(".json")) {
|
|
150
|
+
/* SAFETY: a `.json` file under `pages/` is a page document by the project layout; a file
|
|
151
|
+
that does not parse throws into the catch below, which is "no facts". */
|
|
152
|
+
return pageFactsOf(JSON.parse(source) as JxMutableNode, null);
|
|
153
|
+
}
|
|
154
|
+
const { parseSourceForPath } = await import("../files/file-ops");
|
|
155
|
+
const parsed = await parseSourceForPath(path, source);
|
|
156
|
+
/* SAFETY: a format class's parser returns its file as a Jx element tree (the format
|
|
157
|
+
contract); `pageFactsOf` reads `$paths` and `$layout` off it and nothing else. */
|
|
158
|
+
return pageFactsOf(parsed.document as JxMutableNode, parsed.frontmatter);
|
|
159
|
+
} catch {
|
|
160
|
+
return none;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── The index ───────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/** Every template page of each collection, in path order. A multilingual site has one per language. */
|
|
167
|
+
type CollectionPageIndex = Map<string, CollectionPage[]>;
|
|
168
|
+
|
|
169
|
+
let _index: CollectionPageIndex | null = null;
|
|
170
|
+
let _loading: Promise<CollectionPageIndex> | null = null;
|
|
171
|
+
/** Bumped by every invalidation, so a read that started before one cannot land after it. */
|
|
172
|
+
let _epoch = 0;
|
|
173
|
+
/** Told when a read lands, so a surface showing a stale answer can repaint. */
|
|
174
|
+
const _listeners = new Set<() => void>();
|
|
175
|
+
|
|
176
|
+
/** Every file under `pages/`, recursively. An unreadable directory contributes nothing. */
|
|
177
|
+
async function listPages(dir: string, out: string[]): Promise<void> {
|
|
178
|
+
let entries: DirEntry[];
|
|
179
|
+
try {
|
|
180
|
+
entries = await getPlatform().listDirectory(dir);
|
|
181
|
+
} catch {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
await Promise.all(
|
|
185
|
+
entries.map(async (entry) => {
|
|
186
|
+
if (entry.type === "directory") {
|
|
187
|
+
await listPages(entry.path, out);
|
|
188
|
+
} else {
|
|
189
|
+
out.push(entry.path);
|
|
190
|
+
}
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** One walk of `pages/`, then the facts of every dynamic page in it. */
|
|
196
|
+
async function readIndex(): Promise<CollectionPageIndex> {
|
|
197
|
+
const files: string[] = [];
|
|
198
|
+
await listPages("pages", files);
|
|
199
|
+
/* Only routed pages: an underscore-prefixed file or folder is private to its directory and
|
|
200
|
+
publishes nothing, and it would otherwise win the path-order tie below (`_` sorts first). */
|
|
201
|
+
const dynamic = files.filter((path) => path.includes("[") && isRoutedPageFile(path)).toSorted();
|
|
202
|
+
const facts = await Promise.all(
|
|
203
|
+
dynamic.map(async (path) => [path, await readPageFacts(path)] as const),
|
|
204
|
+
);
|
|
205
|
+
const { i18n } = resolveI18n(projectState?.projectConfig ?? {});
|
|
206
|
+
const index: CollectionPageIndex = new Map();
|
|
207
|
+
for (const [path, fact] of facts) {
|
|
208
|
+
if (fact.contentType) {
|
|
209
|
+
const page: CollectionPage = {
|
|
210
|
+
field: fact.field,
|
|
211
|
+
layout: fact.layout,
|
|
212
|
+
locale: servedLocaleOfPath(path, i18n),
|
|
213
|
+
param: fact.param,
|
|
214
|
+
path,
|
|
215
|
+
pattern: fileToRoute(path).urlPattern,
|
|
216
|
+
...(fact.values && { values: fact.values }),
|
|
217
|
+
};
|
|
218
|
+
index.set(fact.contentType, [...(index.get(fact.contentType) ?? []), page]);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return index;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The index, read once per invalidation.
|
|
226
|
+
*
|
|
227
|
+
* With no platform or no project there is nothing to read, and the answer is an empty map rather
|
|
228
|
+
* than a rejection.
|
|
229
|
+
*/
|
|
230
|
+
export function collectionPages(): Promise<CollectionPageIndex> {
|
|
231
|
+
if (_index) {
|
|
232
|
+
return Promise.resolve(_index);
|
|
233
|
+
}
|
|
234
|
+
if (!hasPlatform() || !projectState) {
|
|
235
|
+
return Promise.resolve(new Map());
|
|
236
|
+
}
|
|
237
|
+
if (!_loading) {
|
|
238
|
+
const epoch = _epoch;
|
|
239
|
+
_loading = readIndex().then((index) => {
|
|
240
|
+
if (epoch === _epoch) {
|
|
241
|
+
_index = index;
|
|
242
|
+
_loading = null;
|
|
243
|
+
for (const listener of _listeners) {
|
|
244
|
+
listener();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return index;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return _loading;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The one of a collection's template pages that serves `locale`.
|
|
255
|
+
*
|
|
256
|
+
* A multilingual site gives a collection one template page per language
|
|
257
|
+
* (`pages/exhibitions/[slug].json`, `pages/ar/exhibitions/[slug].json`, …), and "the" page is the
|
|
258
|
+
* one in the language asked about: an Arabic entry is drawn in the Arabic page's layout, and the
|
|
259
|
+
* header for the English view names the English URL. With no language asked, or none of the pages
|
|
260
|
+
* in it, the answer is the site's default language's page, then a page outside every locale tree,
|
|
261
|
+
* then the first in path order, so two pages over one collection still answer deterministically.
|
|
262
|
+
*/
|
|
263
|
+
export function pageForLocale(
|
|
264
|
+
pages: readonly CollectionPage[] | undefined,
|
|
265
|
+
locale: string | null | undefined,
|
|
266
|
+
): CollectionPage | null {
|
|
267
|
+
if (!pages || pages.length === 0) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const wanted = canonicalizeLocale(locale);
|
|
271
|
+
const fallback = resolveI18n(projectState?.projectConfig ?? {}).i18n?.defaultLocale ?? null;
|
|
272
|
+
return (
|
|
273
|
+
(wanted === null ? undefined : pages.find((page) => page.locale === wanted)) ??
|
|
274
|
+
(fallback === null ? undefined : pages.find((page) => page.locale === fallback)) ??
|
|
275
|
+
pages.find((page) => page.locale === null) ??
|
|
276
|
+
pages[0]!
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The language a collection entry is written in, read from its path (`content/x/ar/a.md` → `ar`). */
|
|
281
|
+
export function entryLocale(path: string | null | undefined): string | null {
|
|
282
|
+
if (!path) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return localeOfPath(path, resolveI18n(projectState?.projectConfig ?? {}).i18n);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* A collection's template page as the index stands: the page, `null` when the collection has none,
|
|
290
|
+
* or `undefined` while the index has not been read yet (which starts the read).
|
|
291
|
+
*
|
|
292
|
+
* @param locale - The language asked about, as {@link pageForLocale} reads it.
|
|
293
|
+
*/
|
|
294
|
+
export function peekCollectionPage(
|
|
295
|
+
name: string,
|
|
296
|
+
locale?: string | null,
|
|
297
|
+
): CollectionPage | null | undefined {
|
|
298
|
+
if (!_index) {
|
|
299
|
+
void collectionPages();
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
return pageForLocale(_index.get(name), locale);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Every template page of a collection, one per language in a multilingual site, as the index
|
|
307
|
+
* stands: `[]` for none, `undefined` while the index has not been read (which starts the read).
|
|
308
|
+
*/
|
|
309
|
+
export function peekCollectionPages(name: string): readonly CollectionPage[] | undefined {
|
|
310
|
+
if (!_index) {
|
|
311
|
+
void collectionPages();
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
return _index.get(name) ?? [];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** A collection's template page for a language, reading the index if it has to. */
|
|
318
|
+
export async function collectionPage(
|
|
319
|
+
name: string,
|
|
320
|
+
locale?: string | null,
|
|
321
|
+
): Promise<CollectionPage | null> {
|
|
322
|
+
const index = await collectionPages();
|
|
323
|
+
return pageForLocale(index.get(name), locale);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Forget the index. The next read walks `pages/` again. */
|
|
327
|
+
export function invalidateCollectionPages(): void {
|
|
328
|
+
_epoch += 1;
|
|
329
|
+
_index = null;
|
|
330
|
+
_loading = null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Be told when a read of the index lands. Returns the function that stops it. */
|
|
334
|
+
export function onCollectionPagesRead(listener: () => void): () => void {
|
|
335
|
+
_listeners.add(listener);
|
|
336
|
+
return () => {
|
|
337
|
+
_listeners.delete(listener);
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** A field value as a route segment: text or a number, else nothing. */
|
|
342
|
+
function segmentOf(value: JxDocumentValue | undefined): string | null {
|
|
343
|
+
if (isString(value) && value !== "") {
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
return isNumber(value) && Number.isFinite(value) ? String(value) : null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The URL one entry opens at, from its collection's page pattern, filled as the build fills it
|
|
351
|
+
* (`Content.resolvePaths`): the page's `$paths.field` read from the entry's fields when it names
|
|
352
|
+
* one (`/products/:sku` with the entry's `sku`), else the entry's id (a JSON entry's `id` field,
|
|
353
|
+
* else its file stem). `null` when the collection has no page, or when its page enumerates the ids
|
|
354
|
+
* it publishes (`$paths.values`) and this entry is not among them.
|
|
355
|
+
*
|
|
356
|
+
* @param fields - The entry's fields (frontmatter, or a JSON entry's root), when the caller has
|
|
357
|
+
* them.
|
|
358
|
+
*/
|
|
359
|
+
export function entryUrl(
|
|
360
|
+
page: CollectionPage | null,
|
|
361
|
+
entryPath: string,
|
|
362
|
+
fields?: Readonly<EntryFields> | null,
|
|
363
|
+
): string | null {
|
|
364
|
+
if (!page) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const base = entryPath.split("/").pop() ?? entryPath;
|
|
368
|
+
const dot = base.lastIndexOf(".");
|
|
369
|
+
const stem = dot > 0 ? base.slice(0, dot) : base;
|
|
370
|
+
const own = page.field === null ? null : segmentOf(fields?.[page.field]);
|
|
371
|
+
// Only a JSON entry's own `id` names it (the loader's `jsonEntry`); a Markdown entry is its file.
|
|
372
|
+
const named = entryPath.endsWith(".json") ? segmentOf(fields?.id) : null;
|
|
373
|
+
const id = own ?? named ?? stem;
|
|
374
|
+
// A page that lists the ids it publishes has no URL for an entry it does not list.
|
|
375
|
+
if (page.values && !page.values.includes(id)) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
if (page.param) {
|
|
379
|
+
return page.pattern.replace(`:${page.param}`, id);
|
|
380
|
+
}
|
|
381
|
+
return page.pattern.replace(/:[A-Za-z_]\w*/, id);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The layout an entry is drawn in (§22.5): its own `$layout`, else its collection's template page's
|
|
386
|
+
* `$layout`, else the project's `defaults.layout`. `null` when none applies, or when the first of
|
|
387
|
+
* them to speak says `false`.
|
|
388
|
+
*/
|
|
389
|
+
export function entryLayoutPath(
|
|
390
|
+
own: string | false | undefined,
|
|
391
|
+
page: CollectionPage | null,
|
|
392
|
+
): string | null {
|
|
393
|
+
if (own === false) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
if (isString(own) && own !== "") {
|
|
397
|
+
return own;
|
|
398
|
+
}
|
|
399
|
+
if (page?.layout === false) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
if (isString(page?.layout) && page.layout !== "") {
|
|
403
|
+
return page.layout;
|
|
404
|
+
}
|
|
405
|
+
return projectState?.projectConfig?.defaults?.layout ?? null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Forget everything — tests, and a project switch. */
|
|
409
|
+
export function resetCollectionPages(): void {
|
|
410
|
+
invalidateCollectionPages();
|
|
411
|
+
_listeners.clear();
|
|
412
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The collections the Content workspace lists: a built-in **Pages** collection, then every
|
|
3
|
+
* collection `project.json` declares, in declaration order (`specs/studio.md` §9.1.2, §22.5).
|
|
4
|
+
*
|
|
5
|
+
* One answer to "which collection is this file in", and it is `content/collection-match.ts`'s. A
|
|
6
|
+
* routed file under `pages/` is a page; any other file is in the collection `collectionForFile`
|
|
7
|
+
* names, or in none. The Library this replaces had a third matcher (`contentTypeFor`, a source
|
|
8
|
+
* prefix test) and folder categories beside it, and the two disagreed about the same entry; both
|
|
9
|
+
* are gone, and nothing here guesses from a folder name.
|
|
10
|
+
*
|
|
11
|
+
* Pure over the open project's configuration: no DOM, no platform, so the commands that name a
|
|
12
|
+
* collection can build their `enum` in a bare Bun process.
|
|
13
|
+
*
|
|
14
|
+
* @docs studio/projects/browse
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { isRoutedPageFile } from "@jxsuite/site/routes";
|
|
18
|
+
import { projectState } from "../store";
|
|
19
|
+
import { MEDIA_EXTENSIONS, extensionOf } from "../files/media-types";
|
|
20
|
+
import { collectionForFile } from "./collection-match";
|
|
21
|
+
import type { ContentSectionEntry } from "../types";
|
|
22
|
+
import type { ContentTypeSchema } from "@jxsuite/schema/types";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Collection names whose singular is the word itself. The suffix rules below would make `news` a
|
|
26
|
+
* `new`, `series` a `sery` and `species` a `specy`, and the result is visible copy ("New new").
|
|
27
|
+
*/
|
|
28
|
+
const INVARIANT_NOUNS = new Set(["data", "media", "metadata", "news", "series", "species"]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Collections named for the WHOLE rather than its parts: a `menu` holds dishes, a `gallery`
|
|
32
|
+
* pictures. Their item is "<name> item", the noun English already uses ("menu item", "catalog
|
|
33
|
+
* item"); "New menu" would promise a second menu.
|
|
34
|
+
*/
|
|
35
|
+
const COLLECTIVE_NOUNS = new Set([
|
|
36
|
+
"archive",
|
|
37
|
+
"catalog",
|
|
38
|
+
"catalogue",
|
|
39
|
+
"collection",
|
|
40
|
+
"directory",
|
|
41
|
+
"faq",
|
|
42
|
+
"gallery",
|
|
43
|
+
"inventory",
|
|
44
|
+
"library",
|
|
45
|
+
"menu",
|
|
46
|
+
"portfolio",
|
|
47
|
+
"showcase",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A storage name in words, as a person reads it: `project_tag` → "project tag", `service-type` →
|
|
52
|
+
* "service type", `docsNav` → "docs nav". Lower case; {@link capitalise} makes a heading of it.
|
|
53
|
+
*/
|
|
54
|
+
export function humanName(name: string): string {
|
|
55
|
+
return name
|
|
56
|
+
.replaceAll(/[_-]+/g, " ")
|
|
57
|
+
.replaceAll(/([a-z\d])([A-Z])/g, "$1 $2")
|
|
58
|
+
.trim()
|
|
59
|
+
.replaceAll(/\s+/g, " ")
|
|
60
|
+
.toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A collection's item, said in the singular: `recipes` → `recipe`, `stories` → `story`. */
|
|
64
|
+
export function itemNoun(collection: string): string {
|
|
65
|
+
// Only the last word is a noun to singularise: `post_tags` is a "post tag".
|
|
66
|
+
// A one-word name keeps its own spelling (`News`); a compound one is said in lower-case words.
|
|
67
|
+
const words = /[_-]|[a-z\d][A-Z]/.test(collection) ? humanName(collection) : collection;
|
|
68
|
+
const at = words.lastIndexOf(" ");
|
|
69
|
+
const last = words.slice(at + 1);
|
|
70
|
+
if (COLLECTIVE_NOUNS.has(last.toLowerCase())) {
|
|
71
|
+
return `${words} item`;
|
|
72
|
+
}
|
|
73
|
+
return `${words.slice(0, at + 1)}${singular(last)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One word in the singular. */
|
|
77
|
+
function singular(collection: string): string {
|
|
78
|
+
if (INVARIANT_NOUNS.has(collection.toLowerCase())) {
|
|
79
|
+
return collection;
|
|
80
|
+
}
|
|
81
|
+
if (/[^aeiou]ies$/.test(collection)) {
|
|
82
|
+
return `${collection.slice(0, -3)}y`;
|
|
83
|
+
}
|
|
84
|
+
// `classes` → `class`, `boxes` → `box`, `matches` → `match`: the plural added `es`.
|
|
85
|
+
if (/(?:ss|x|ch|sh|z)es$/.test(collection)) {
|
|
86
|
+
return collection.slice(0, -2);
|
|
87
|
+
}
|
|
88
|
+
// `status`, `campus`, `analysis`, `glass` are already singular.
|
|
89
|
+
if (/(?:us|is|ss)$/.test(collection)) {
|
|
90
|
+
return collection;
|
|
91
|
+
}
|
|
92
|
+
if (collection.endsWith("s")) {
|
|
93
|
+
return collection.slice(0, -1);
|
|
94
|
+
}
|
|
95
|
+
return collection;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A word with its first letter in capitals: `posts` → `Posts`. */
|
|
99
|
+
export function capitalise(word: string): string {
|
|
100
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A template or page file's display name, from its file name: `./layouts/post.json` → `Post`,
|
|
105
|
+
* `./layouts/blog-post.json` → `Blog Post`. The one spelling every surface that names a template
|
|
106
|
+
* uses, so the header line, the writing banner and the layout picker never disagree.
|
|
107
|
+
*/
|
|
108
|
+
export function templateDisplayName(path: string): string {
|
|
109
|
+
const base = path.replaceAll("\\", "/").split("/").pop() ?? path;
|
|
110
|
+
return base
|
|
111
|
+
.replace(/\.[^.]+$/, "")
|
|
112
|
+
.replaceAll(/[-_]+/g, " ")
|
|
113
|
+
.replaceAll(/\b\w/g, (c: string) => c.toUpperCase());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The key of the built-in collection every site has: the routed files under `pages/`. */
|
|
117
|
+
export const PAGES_COLLECTION = "pages";
|
|
118
|
+
|
|
119
|
+
/** One collection, as the Content workspace names it. */
|
|
120
|
+
export interface ContentCollection {
|
|
121
|
+
/** `pages`, or the collection's name in `project.json`. The value every command names. */
|
|
122
|
+
key: string;
|
|
123
|
+
/** What the heading says: "Pages", or the collection's name capitalised. */
|
|
124
|
+
title: string;
|
|
125
|
+
/** One of its items, in the singular and lower case: "page", "post", "story". */
|
|
126
|
+
item: string;
|
|
127
|
+
/** `pages` for the built-in collection, `collection` for a declared one. */
|
|
128
|
+
kind: "pages" | "collection";
|
|
129
|
+
/** True for a collection whose `source` is one file (a CSV catalogue): its entries are rows. */
|
|
130
|
+
fileBacked: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The declared collections, name → entry, in declaration order. */
|
|
134
|
+
function contentSection(): Record<string, ContentSectionEntry> {
|
|
135
|
+
/* SAFETY: `content` is the parser extension's section, collection name → entry object; this
|
|
136
|
+
module reads only the optional `source`, and only to ask whether it names one file. */
|
|
137
|
+
return (projectState?.projectConfig?.content ?? {}) as Record<string, ContentSectionEntry>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Whether a `source` names a single file rather than a directory. */
|
|
141
|
+
function namesFile(source: string | undefined): boolean {
|
|
142
|
+
return source !== undefined && /\.[\da-z]+$/i.test(source.replace(/\/+$/, ""));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The built-in Pages collection. */
|
|
146
|
+
const PAGES: ContentCollection = {
|
|
147
|
+
fileBacked: false,
|
|
148
|
+
item: "page",
|
|
149
|
+
key: PAGES_COLLECTION,
|
|
150
|
+
kind: "pages",
|
|
151
|
+
title: "Pages",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** Every collection the Content workspace lists: Pages first, then the declared ones. */
|
|
155
|
+
export function contentCollections(): ContentCollection[] {
|
|
156
|
+
const declared = Object.entries(contentSection()).map(([name, def]): ContentCollection => ({
|
|
157
|
+
fileBacked: namesFile(def.source),
|
|
158
|
+
item: itemNoun(name),
|
|
159
|
+
key: name,
|
|
160
|
+
kind: "collection",
|
|
161
|
+
// In words, as the Table names a field: `project_tag` is "Project tag", not "Project_tag".
|
|
162
|
+
title: capitalise(humanName(name)),
|
|
163
|
+
}));
|
|
164
|
+
return [PAGES, ...declared];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Every collection key, for a command's `enum`: `pages` first, then the declared names. */
|
|
168
|
+
export function contentCollectionKeys(): string[] {
|
|
169
|
+
return contentCollections().map((collection) => collection.key);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The project-relative file a file-backed collection's entries are rows of (`./content/p.csv` →
|
|
174
|
+
* `content/p.csv`), or `null` for a collection whose source is a directory, or none at all.
|
|
175
|
+
*/
|
|
176
|
+
export function collectionSourceFile(key: string): string | null {
|
|
177
|
+
const source = contentSection()[key]?.source;
|
|
178
|
+
return source !== undefined && namesFile(source) ? source.replace(/^\.\//, "") : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The schema a collection declares, file-backed or not, or `null` for none. */
|
|
182
|
+
export function declaredCollectionSchema(key: string): ContentTypeSchema | null {
|
|
183
|
+
return contentSection()[key]?.schema ?? null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The collection with this key, or `null` when the project declares none by that name. */
|
|
187
|
+
export function contentCollection(key: string): ContentCollection | null {
|
|
188
|
+
return contentCollections().find((collection) => collection.key === key) ?? null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The collection a file is in, or `null`.
|
|
193
|
+
*
|
|
194
|
+
* Media files are in none: an image beside an entry is the entry's asset, which the Media workspace
|
|
195
|
+
* lists. A file under `pages/` is a page; any other is `collectionForFile`'s answer.
|
|
196
|
+
*/
|
|
197
|
+
export function collectionKeyOfPath(path: string | null | undefined): string | null {
|
|
198
|
+
if (!path) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const normalized = path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
202
|
+
if (MEDIA_EXTENSIONS.has(extensionOf(normalized))) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (normalized.startsWith("pages/")) {
|
|
206
|
+
// An underscore-prefixed file or folder is a page directory's private part, not a route.
|
|
207
|
+
return isRoutedPageFile(normalized) ? PAGES_COLLECTION : null;
|
|
208
|
+
}
|
|
209
|
+
return collectionForFile(normalized)?.name ?? null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Whether an entry is written on the canvas rather than in the form editor.
|
|
214
|
+
*
|
|
215
|
+
* An entry with a body (a Markdown file, or any format with prose) is written on the canvas inside
|
|
216
|
+
* its template's layout (§22.5). A JSON entry IS its fields, with no body to write, and a
|
|
217
|
+
* file-backed collection's entries are rows: both keep the form-only `entry` editor.
|
|
218
|
+
*/
|
|
219
|
+
export function writesEntryOnCanvas(path: string | null | undefined): boolean {
|
|
220
|
+
if (!path) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
const match = collectionForFile(path);
|
|
224
|
+
if (!match || match.fileBacked) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
const ext = extensionOf(path);
|
|
228
|
+
return ext !== ".json" && ext !== ".csv";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Which of the Content workspace's three derived caches a batch of file events can make wrong. */
|
|
232
|
+
export interface ContentCacheDrops {
|
|
233
|
+
/** The scan the collections are filtered from: only the file SET, so adds, removes and renames. */
|
|
234
|
+
scan: boolean;
|
|
235
|
+
/** The pages that say where each collection is published: any write under `pages/`. */
|
|
236
|
+
pages: boolean;
|
|
237
|
+
/** The titles and statuses the Collections panel lists: any write to a collection's entry. */
|
|
238
|
+
entries: boolean;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Which Content caches a file-event batch drops.
|
|
243
|
+
*
|
|
244
|
+
* A plain content `change` cannot change which files exist, so it leaves the scan alone; that is
|
|
245
|
+
* every save, Studio's own echoes included, and dropping the scan on each one blanked the
|
|
246
|
+
* Collections panel and re-walked the project after every keystroke-and-save. It can still change a
|
|
247
|
+
* template page's `$paths` (the index) and an entry's title or status (the summaries), so those are
|
|
248
|
+
* dropped by path.
|
|
249
|
+
*
|
|
250
|
+
* @param paths The project-relative paths the batch touched, or `undefined` after a gap, when any
|
|
251
|
+
* path may have changed.
|
|
252
|
+
* @param structural Whether the batch added, removed or renamed anything.
|
|
253
|
+
*/
|
|
254
|
+
export function contentCacheDrops(
|
|
255
|
+
paths: readonly string[] | undefined,
|
|
256
|
+
structural: boolean,
|
|
257
|
+
): ContentCacheDrops {
|
|
258
|
+
if (paths === undefined || structural) {
|
|
259
|
+
return { entries: true, pages: true, scan: true };
|
|
260
|
+
}
|
|
261
|
+
const normalized = paths.map((path) => path.replaceAll("\\", "/").replace(/^\.\//, ""));
|
|
262
|
+
return {
|
|
263
|
+
entries: normalized.some((path) => {
|
|
264
|
+
const key = collectionKeyOfPath(path);
|
|
265
|
+
return key !== null && key !== PAGES_COLLECTION;
|
|
266
|
+
}),
|
|
267
|
+
pages: normalized.some((path) => path.startsWith("pages/")),
|
|
268
|
+
scan: false,
|
|
269
|
+
};
|
|
270
|
+
}
|