@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
|
@@ -1,76 +1,116 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
3
|
+
* The Content home: one collection, browsed, in the Content workspace's home tab (`specs/studio.md`
|
|
4
|
+
* §9.1.2, §22.5).
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
11
|
-
* — the map just had nothing behind it.
|
|
6
|
+
* The Library this replaces answered "where is everything" with one tab of every project file in
|
|
7
|
+
* folder categories. That question has better answers now (pages, templates and components in the
|
|
8
|
+
* Site panel, media in the Media workspace), and what was left is the Library's real job: browsing
|
|
9
|
+
* and writing a COLLECTION. The home shows one at a time, the built-in **Pages** or a collection
|
|
10
|
+
* the project declares, chosen in the Collections panel (`panels/collections-panel.ts`) or by
|
|
11
|
+
* `content.showCollection`.
|
|
12
12
|
*
|
|
13
13
|
* The MARKUP is `surfaces/library-pane.json`, a Jx document over the kit; this module is the
|
|
14
|
-
* projection and the decisions.
|
|
14
|
+
* projection and the decisions. Five things it is careful about:
|
|
15
15
|
*
|
|
16
16
|
* 1. **It never says "No files found" for two different reasons.** A scan that could not read a
|
|
17
17
|
* directory is INCOMPLETE, not empty; it raises a Problem carrying the directory and a Retry
|
|
18
|
-
* (`
|
|
18
|
+
* (`content.refresh`), and the surface says so. A filter that matched nothing says which filter.
|
|
19
19
|
* The four states are one word on the scope, so no two of them can be true at once.
|
|
20
20
|
* 2. **The rendered item count is proportional to the viewport, not to the project.** See
|
|
21
|
-
* `../ui/virtual-window.ts` and `library-preview.ts`; the acceptance case is 300 pages
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* 4. **
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
21
|
+
* `../ui/virtual-window.ts` and `library-preview.ts`; the acceptance case is 300 pages.
|
|
22
|
+
* 3. **Every collection keeps its own view.** Its view switch, search and language are its own
|
|
23
|
+
* ({@link contentView}`.views`), so Pages in Table and Posts in Cards is a state the home can be
|
|
24
|
+
* in, and coming back to a collection finds it as it was left.
|
|
25
|
+
* 4. **Where a collection is published is read from the pages.** The header line ("Each post has a
|
|
26
|
+
* page at `/blog/:slug` using Post") is derived from the page whose `$paths.contentType` names
|
|
27
|
+
* the collection (`content/collection-pages.ts`), never stored beside it.
|
|
28
|
+
* 5. **Creation is somebody else's flow.** **New <item>** runs `file.new` (Add new, §9.1.1) on the
|
|
29
|
+
* collection's kind, and **Add one** writes the collection's page exactly as Add new's own-page
|
|
30
|
+
* option does (`create/add-new-flow.ts` `addCollectionPage`).
|
|
29
31
|
*
|
|
30
32
|
* @docs studio/projects/browse
|
|
31
33
|
*/
|
|
32
34
|
|
|
33
35
|
import { effect, effectScope, reactive } from "../reactivity";
|
|
34
36
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
37
|
+
import { localeLabel, resolveI18n } from "@jxsuite/schema/locale";
|
|
35
38
|
import { notify } from "../services/notify";
|
|
36
39
|
import { beginActivity } from "../panels/activity-panel";
|
|
37
40
|
import { showPromptDialog } from "../ui/layers";
|
|
38
41
|
import type { PromptDialogOptions } from "../ui/layers";
|
|
39
42
|
import { openMenu } from "../surfaces/menu";
|
|
40
|
-
import { rectOf } from "../utils/geometry";
|
|
41
43
|
import { getPlatform } from "../platform";
|
|
44
|
+
import { projectState, renderOnly } from "../store";
|
|
42
45
|
import { invalidateUsages } from "../services/references";
|
|
43
46
|
import { confirmFileDelete, renamePromptMessage } from "../files/file-ops";
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
import { openFileInTab } from "../files/files";
|
|
48
|
+
import { runActiveReported } from "../commands/run-reported";
|
|
49
|
+
import {
|
|
50
|
+
PAGES_COLLECTION,
|
|
51
|
+
collectionSourceFile,
|
|
52
|
+
declaredCollectionSchema,
|
|
53
|
+
contentCollection,
|
|
54
|
+
templateDisplayName,
|
|
55
|
+
} from "../content/content-collections";
|
|
56
|
+
import {
|
|
57
|
+
onCollectionPagesRead,
|
|
58
|
+
peekCollectionPage,
|
|
59
|
+
peekCollectionPages,
|
|
60
|
+
} from "../content/collection-pages";
|
|
61
|
+
import { onPageSummariesRead, peekPageSummaries } from "../content/page-summaries";
|
|
62
|
+
import { localeMarked, pageLabel, singularLabel } from "../panels/site-model";
|
|
63
|
+
import { compareRoutes, fileToRoute } from "@jxsuite/site/routes";
|
|
64
|
+
import {
|
|
65
|
+
dateFieldOf,
|
|
66
|
+
declaredDateField,
|
|
67
|
+
entryDay,
|
|
68
|
+
entryThumb,
|
|
69
|
+
onEntrySummariesRead,
|
|
70
|
+
peekEntrySummaries,
|
|
71
|
+
titleFieldOf,
|
|
72
|
+
} from "../content/entry-summaries";
|
|
73
|
+
import { entryCollection } from "../content/entry-model";
|
|
74
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
75
|
+
import type { ContentTypeSchema } from "@jxsuite/schema/types";
|
|
76
|
+
import { openEntry } from "../content/entry-open";
|
|
77
|
+
import { openRecordEntry } from "../content/record-entry";
|
|
50
78
|
import {
|
|
51
|
-
LIBRARY_CATEGORIES,
|
|
52
79
|
LIBRARY_LAYOUTS,
|
|
53
80
|
LIBRARY_LAYOUT_LABELS,
|
|
54
81
|
PREVIEW_LAYOUTS,
|
|
55
82
|
filterLibrary,
|
|
56
|
-
libraryCategory,
|
|
57
83
|
libraryLocales,
|
|
58
|
-
|
|
84
|
+
projectLocales,
|
|
59
85
|
} from "./library-model";
|
|
60
|
-
import { createLibrarySource, libraryColumns } from "./library-source";
|
|
86
|
+
import { createLibrarySource, entryFieldColumns, libraryColumns } from "./library-source";
|
|
61
87
|
import { createPreviewCache, createPreviewObserver, previewFor } from "./library-preview";
|
|
88
|
+
import { mountPreview, shownPreview } from "./preview-root";
|
|
62
89
|
import { computeWindow } from "../ui/virtual-window";
|
|
63
90
|
import { paneRegion } from "../ui/regions";
|
|
64
|
-
import {
|
|
91
|
+
import {
|
|
92
|
+
LAYOUT_METRICS,
|
|
93
|
+
boardFieldOf,
|
|
94
|
+
boardView,
|
|
95
|
+
calendarView,
|
|
96
|
+
columnsAt,
|
|
97
|
+
entryFactsFrom,
|
|
98
|
+
fieldValues,
|
|
99
|
+
libraryRow,
|
|
100
|
+
} from "./library-layouts";
|
|
65
101
|
import { mountLibrarySurface } from "../surfaces/library-pane";
|
|
66
102
|
import type { ActivityHandle } from "../panels/activity-panel";
|
|
67
|
-
import type {
|
|
103
|
+
import type { ContentCollection } from "../content/content-collections";
|
|
104
|
+
import type { CollectionPage } from "../content/collection-pages";
|
|
68
105
|
import type { EffectScope } from "@vue/reactivity";
|
|
69
106
|
import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
|
|
70
107
|
import type { LibraryFile, LibraryLayout } from "./library-model";
|
|
108
|
+
import type { BoardField, EntryFacts, EntryFactsOf } from "./library-layouts";
|
|
71
109
|
import type { LibrarySource } from "./library-source";
|
|
110
|
+
import type { CommandArgValues } from "../commands/command-args";
|
|
72
111
|
import type {
|
|
73
112
|
LibraryChip,
|
|
113
|
+
LibraryPageLine,
|
|
74
114
|
LibrarySurfaceHandle,
|
|
75
115
|
LibraryView,
|
|
76
116
|
LibraryViewKind,
|
|
@@ -81,8 +121,8 @@ import type { CanvasSurface } from "../canvas/canvas-surface";
|
|
|
81
121
|
|
|
82
122
|
// ─── View state ──────────────────────────────────────────────────────────────
|
|
83
123
|
|
|
84
|
-
|
|
85
|
-
|
|
124
|
+
/** One collection's own view: how it is arranged, what it is filtered to. */
|
|
125
|
+
export interface CollectionViewState {
|
|
86
126
|
layout: LibraryLayout;
|
|
87
127
|
query: string;
|
|
88
128
|
/**
|
|
@@ -92,30 +132,86 @@ export interface LibraryViewState {
|
|
|
92
132
|
* matches what {@link LibraryFile.locale} carries.
|
|
93
133
|
*/
|
|
94
134
|
locale: string;
|
|
135
|
+
/**
|
|
136
|
+
* The Filter: one value of the collection's filter field ({@link filterFieldOf}: its status, else
|
|
137
|
+
* the field its Board groups by), or "" for every entry. A value the field no longer has filters
|
|
138
|
+
* nothing, rather than hiding every entry behind a choice the picker cannot show.
|
|
139
|
+
*/
|
|
140
|
+
filter: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ContentViewState {
|
|
144
|
+
/** The collection the home shows: `pages`, or a declared collection's name. */
|
|
145
|
+
collection: string;
|
|
146
|
+
/** Each collection's own view, created the first time it is shown. */
|
|
147
|
+
views: Record<string, CollectionViewState>;
|
|
95
148
|
/** Bumped to force a repaint from a non-reactive source (a scroll, a finished scan). */
|
|
96
149
|
revision: number;
|
|
97
150
|
/** A scan is running. Distinct from "scanned and empty", which is what the old view conflated. */
|
|
98
151
|
loading: boolean;
|
|
99
152
|
}
|
|
100
153
|
|
|
154
|
+
/** The view a collection starts in: Table, the shape both Content mockups open on. */
|
|
155
|
+
const DEFAULT_LAYOUT: LibraryLayout = "table";
|
|
156
|
+
|
|
101
157
|
/**
|
|
102
|
-
* The
|
|
158
|
+
* The Content home's state, reactive and module-scoped.
|
|
103
159
|
*
|
|
104
|
-
* Module-scoped rather than per-tab because there is one
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
160
|
+
* Module-scoped rather than per-tab because there is one Content home per window, and because the
|
|
161
|
+
* commands that write it must be able to do so whether or not the home is mounted:
|
|
162
|
+
* `content.setLayout` run from the palette sets the view the home opens into. Per COLLECTION
|
|
163
|
+
* inside, so one collection's view never leaks into another's.
|
|
108
164
|
*/
|
|
109
|
-
export const
|
|
110
|
-
|
|
111
|
-
layout: "cards",
|
|
165
|
+
export const contentView: ContentViewState = reactive({
|
|
166
|
+
collection: PAGES_COLLECTION,
|
|
112
167
|
loading: false,
|
|
113
|
-
locale: "",
|
|
114
|
-
query: "",
|
|
115
168
|
revision: 0,
|
|
169
|
+
views: {},
|
|
116
170
|
});
|
|
117
171
|
|
|
118
|
-
/** The
|
|
172
|
+
/** The view state of a collection, created on first read. */
|
|
173
|
+
export function collectionView(key: string = shownCollectionKey()): CollectionViewState {
|
|
174
|
+
const existing = contentView.views[key];
|
|
175
|
+
if (existing) {
|
|
176
|
+
return existing;
|
|
177
|
+
}
|
|
178
|
+
const created: CollectionViewState = {
|
|
179
|
+
filter: "",
|
|
180
|
+
layout: DEFAULT_LAYOUT,
|
|
181
|
+
locale: "",
|
|
182
|
+
query: "",
|
|
183
|
+
};
|
|
184
|
+
contentView.views[key] = created;
|
|
185
|
+
// Read back through the reactive record, so a later write to the returned object is tracked.
|
|
186
|
+
return contentView.views[key] ?? created;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The collection the home shows, falling back to Pages when its name has gone from the project. */
|
|
190
|
+
export function shownCollection(): ContentCollection {
|
|
191
|
+
return (
|
|
192
|
+
contentCollection(contentView.collection) ?? {
|
|
193
|
+
fileBacked: false,
|
|
194
|
+
item: "page",
|
|
195
|
+
key: PAGES_COLLECTION,
|
|
196
|
+
kind: "pages",
|
|
197
|
+
title: "Pages",
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The key of the collection the home actually shows.
|
|
204
|
+
*
|
|
205
|
+
* `contentView.collection` is what was last ASKED for, and it can outlive its collection (a rename
|
|
206
|
+
* or a removal in Settings › Content types). Every default that means "the shown collection" reads
|
|
207
|
+
* this instead, so the view switch, search, language picker and preview tending all act on the
|
|
208
|
+
* collection on screen rather than on a stale key.
|
|
209
|
+
*/
|
|
210
|
+
export function shownCollectionKey(): string {
|
|
211
|
+
return shownCollection().key;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The window's scan source. Built lazily; discarded by {@link invalidateLibrary}. */
|
|
119
215
|
let source: LibrarySource | null = null;
|
|
120
216
|
|
|
121
217
|
/**
|
|
@@ -130,33 +226,54 @@ let scanAttempted = false;
|
|
|
130
226
|
|
|
131
227
|
/** The source, creating it on first use. */
|
|
132
228
|
export function librarySource(): LibrarySource {
|
|
229
|
+
/* A scan of a directory list the project has since outgrown is dropped here, on the next ask,
|
|
230
|
+
rather than by every door that can change the list: the project's directories loading after a
|
|
231
|
+
window restored into Content, or a collection being declared. Each of those repaints, and the
|
|
232
|
+
repaint is what asks. Without it the first answer, a walk of no directories, was cached as
|
|
233
|
+
"this project has no pages" until a reload. */
|
|
234
|
+
if (source?.stale() === true) {
|
|
235
|
+
source = null;
|
|
236
|
+
scanAttempted = false;
|
|
237
|
+
for (const panel of _active.values()) {
|
|
238
|
+
panel.cache.clear();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
133
241
|
source ??= createLibrarySource();
|
|
134
242
|
return source;
|
|
135
243
|
}
|
|
136
244
|
|
|
137
|
-
/** Repaint whatever is mounted. */
|
|
245
|
+
/** Repaint whatever is mounted, the Collections panel's counts included. */
|
|
138
246
|
function bump() {
|
|
139
|
-
|
|
247
|
+
contentView.revision += 1;
|
|
140
248
|
}
|
|
141
249
|
|
|
142
|
-
|
|
143
|
-
|
|
250
|
+
/** Show a collection in the home. The home itself is opened by `content.showCollection`. */
|
|
251
|
+
export function setContentCollection(key: string): void {
|
|
252
|
+
contentView.collection = key;
|
|
253
|
+
collectionView(key);
|
|
144
254
|
bump();
|
|
255
|
+
renderOnly("leftPanel");
|
|
145
256
|
}
|
|
146
257
|
|
|
147
|
-
export function setLibraryLayout(layout: LibraryLayout): void {
|
|
148
|
-
|
|
258
|
+
export function setLibraryLayout(layout: LibraryLayout, key: string = shownCollectionKey()): void {
|
|
259
|
+
collectionView(key).layout = layout;
|
|
149
260
|
bump();
|
|
150
261
|
}
|
|
151
262
|
|
|
152
|
-
export function setLibrarySearch(query: string): void {
|
|
153
|
-
|
|
263
|
+
export function setLibrarySearch(query: string, key: string = shownCollectionKey()): void {
|
|
264
|
+
collectionView(key).query = query;
|
|
265
|
+
bump();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Show the entries with one value of the filter field, or `""` for all of them. A setter. */
|
|
269
|
+
export function setLibraryFilter(value: string, key: string = shownCollectionKey()): void {
|
|
270
|
+
collectionView(key).filter = value;
|
|
154
271
|
bump();
|
|
155
272
|
}
|
|
156
273
|
|
|
157
274
|
/** Show one language, or `""` for all of them. A setter, so "" is a value and not an absence. */
|
|
158
|
-
export function setLibraryLocale(locale: string): void {
|
|
159
|
-
|
|
275
|
+
export function setLibraryLocale(locale: string, key: string = shownCollectionKey()): void {
|
|
276
|
+
collectionView(key).locale = locale;
|
|
160
277
|
bump();
|
|
161
278
|
}
|
|
162
279
|
|
|
@@ -164,7 +281,7 @@ export function setLibraryLocale(locale: string): void {
|
|
|
164
281
|
* Drop the scan and the rendered previews.
|
|
165
282
|
*
|
|
166
283
|
* Called by every surface that changes the project's file set — an upload, a creation, a rename, a
|
|
167
|
-
* delete — so the
|
|
284
|
+
* delete — so the home is never showing a file that is gone.
|
|
168
285
|
*
|
|
169
286
|
* The SLOT table survives, and that is the difference between this and a teardown: clearing the
|
|
170
287
|
* cache detaches every rendered preview from the box it was in, and those boxes are still on
|
|
@@ -180,13 +297,45 @@ export function invalidateLibrary(): void {
|
|
|
180
297
|
bump();
|
|
181
298
|
}
|
|
182
299
|
|
|
300
|
+
/**
|
|
301
|
+
* How many files each collection holds, from the scan as it stands. Starts the scan when none has
|
|
302
|
+
* been attempted, so the Collections panel can ask before the home has ever been shown; the panel
|
|
303
|
+
* repaints when it lands.
|
|
304
|
+
*
|
|
305
|
+
* @returns Collection key → count, or `null` while no scan has finished.
|
|
306
|
+
*/
|
|
307
|
+
export function collectionCounts(): Map<string, number> | null {
|
|
308
|
+
const current = librarySource();
|
|
309
|
+
if (!scanAttempted && !contentView.loading) {
|
|
310
|
+
void loadLibrary();
|
|
311
|
+
}
|
|
312
|
+
if (!current.scanned()) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
const counts = new Map<string, number>();
|
|
316
|
+
for (const file of current.files()) {
|
|
317
|
+
if (file.collection !== null) {
|
|
318
|
+
counts.set(file.collection, (counts.get(file.collection) ?? 0) + 1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return counts;
|
|
322
|
+
}
|
|
323
|
+
|
|
183
324
|
// ─── Loading ─────────────────────────────────────────────────────────────────
|
|
184
325
|
|
|
185
326
|
/** A scan slower than this earns an Activity row; a fast one must not litter the dock. */
|
|
186
327
|
export const SCAN_ACTIVITY_DELAY_MS = 600;
|
|
187
328
|
|
|
188
329
|
/** Deduplication key for the scan Problem, so a failing watcher is one row and not sixty. */
|
|
189
|
-
const SCAN_PROBLEM_KEY = "
|
|
330
|
+
const SCAN_PROBLEM_KEY = "content.scan";
|
|
331
|
+
|
|
332
|
+
/** The `source` every Problem and toast this module files carries. */
|
|
333
|
+
const SOURCE = "Content";
|
|
334
|
+
|
|
335
|
+
/** The scan's Activity row, filled in only once the scan outlasts {@link SCAN_ACTIVITY_DELAY_MS}. */
|
|
336
|
+
interface SlowScanActivity {
|
|
337
|
+
handle: ActivityHandle | null;
|
|
338
|
+
}
|
|
190
339
|
|
|
191
340
|
/**
|
|
192
341
|
* Run the scan and report it honestly.
|
|
@@ -196,21 +345,16 @@ const SCAN_PROBLEM_KEY = "library.scan";
|
|
|
196
345
|
* contract exists to end. The row is informational, and it only appears when the scan is slow
|
|
197
346
|
* enough that its absence would look like a hang.
|
|
198
347
|
*/
|
|
199
|
-
/** The scan's Activity row, filled in only once the scan outlasts {@link SCAN_ACTIVITY_DELAY_MS}. */
|
|
200
|
-
interface SlowScanActivity {
|
|
201
|
-
handle: ActivityHandle | null;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
348
|
async function loadLibrary(): Promise<void> {
|
|
205
349
|
const current = librarySource();
|
|
206
350
|
scanAttempted = true;
|
|
207
|
-
|
|
351
|
+
contentView.loading = true;
|
|
208
352
|
bump();
|
|
209
353
|
|
|
210
354
|
const slow: SlowScanActivity = { handle: null };
|
|
211
355
|
const timer = setTimeout(() => {
|
|
212
356
|
slow.handle = beginActivity({
|
|
213
|
-
source:
|
|
357
|
+
source: SOURCE,
|
|
214
358
|
status: "Reading the project's directories…",
|
|
215
359
|
title: "Scan project files",
|
|
216
360
|
});
|
|
@@ -227,11 +371,11 @@ async function loadLibrary(): Promise<void> {
|
|
|
227
371
|
// A Problem, not a toast: the list on screen is wrong until this is fixed, and the fix is a
|
|
228
372
|
// Command the row can run.
|
|
229
373
|
notify.error(summary, {
|
|
230
|
-
action: "
|
|
374
|
+
action: "content.refresh",
|
|
231
375
|
detail,
|
|
232
376
|
key: SCAN_PROBLEM_KEY,
|
|
233
377
|
path: failures[0]!.dir,
|
|
234
|
-
source:
|
|
378
|
+
source: SOURCE,
|
|
235
379
|
tier: "problem",
|
|
236
380
|
});
|
|
237
381
|
slow.handle?.setStatus(summary);
|
|
@@ -244,21 +388,23 @@ async function loadLibrary(): Promise<void> {
|
|
|
244
388
|
slow.handle.fail(message);
|
|
245
389
|
} else {
|
|
246
390
|
notify.error(message, {
|
|
247
|
-
action: "
|
|
391
|
+
action: "content.refresh",
|
|
248
392
|
detail: errorMessage(error),
|
|
249
393
|
key: SCAN_PROBLEM_KEY,
|
|
250
|
-
source:
|
|
394
|
+
source: SOURCE,
|
|
251
395
|
tier: "problem",
|
|
252
396
|
});
|
|
253
397
|
}
|
|
254
398
|
} finally {
|
|
255
399
|
clearTimeout(timer);
|
|
256
|
-
|
|
400
|
+
contentView.loading = false;
|
|
257
401
|
bump();
|
|
402
|
+
// The Collections panel draws the counts this scan just produced.
|
|
403
|
+
renderOnly("leftPanel");
|
|
258
404
|
}
|
|
259
405
|
}
|
|
260
406
|
|
|
261
|
-
/** Re-scan from scratch — the Retry behind the Problem
|
|
407
|
+
/** Re-scan from scratch — the Retry behind the Problem. */
|
|
262
408
|
export async function refreshLibrary(): Promise<void> {
|
|
263
409
|
source = null;
|
|
264
410
|
scanAttempted = false;
|
|
@@ -271,7 +417,7 @@ export async function refreshLibrary(): Promise<void> {
|
|
|
271
417
|
// ─── The mounted pane ────────────────────────────────────────────────────────
|
|
272
418
|
|
|
273
419
|
interface ActiveLibraryPane {
|
|
274
|
-
/** The pane whose stage this
|
|
420
|
+
/** The pane whose stage this home is drawn on. The map key, held on the record too. */
|
|
275
421
|
paneId: string;
|
|
276
422
|
tabId: string;
|
|
277
423
|
scope: EffectScope;
|
|
@@ -293,30 +439,26 @@ interface ActiveLibraryPane {
|
|
|
293
439
|
* and a scroll through a long list would read each document several times over.
|
|
294
440
|
*/
|
|
295
441
|
pendingPaths: Set<string>;
|
|
296
|
-
/** Whether a drag is over the body. Held here so a dragover does not recompute the window. */
|
|
297
|
-
dropActive: boolean;
|
|
298
442
|
/** The coalesced post-repaint pass; see {@link scheduleSettle}. */
|
|
299
443
|
settleTimer: ReturnType<typeof setTimeout> | null;
|
|
444
|
+
/** Stops listening for the collection-pages index. */
|
|
445
|
+
unlisten: () => void;
|
|
300
446
|
}
|
|
301
447
|
|
|
302
448
|
/**
|
|
303
|
-
* The
|
|
449
|
+
* The home mounted in each pane, keyed by pane id.
|
|
304
450
|
*
|
|
305
|
-
* ONE instance per pane, not per window
|
|
306
|
-
*
|
|
307
|
-
* cosmetic: pane B mounting a Library called `detachLibraryPane()`, destroying pane A's
|
|
308
|
-
* `IntersectionObserver`, preview cache and effect scope while pane A's DOM was still on screen.
|
|
309
|
-
* `canvas-render.ts`'s `resetCanvasView` calls the detach UNCONDITIONALLY on every empty pane, so
|
|
310
|
-
* it did not even take a second Library to do it.
|
|
451
|
+
* ONE instance per pane, not per window: pane B mounting the home must not destroy pane A's
|
|
452
|
+
* `IntersectionObserver`, preview cache and effect scope while pane A's DOM is still on screen.
|
|
311
453
|
*/
|
|
312
454
|
const _active = new Map<string, ActiveLibraryPane>();
|
|
313
455
|
|
|
314
|
-
/** The
|
|
456
|
+
/** The home mounted in a pane, or null. */
|
|
315
457
|
function activeIn(paneId: string): ActiveLibraryPane | null {
|
|
316
458
|
return _active.get(paneId) ?? null;
|
|
317
459
|
}
|
|
318
460
|
|
|
319
|
-
/** Whether the
|
|
461
|
+
/** Whether the home is live in this pane for this tab — the canvas-render fast-path guard. */
|
|
320
462
|
export function libraryPaneMounted(paneId: string, tab: Tab): boolean {
|
|
321
463
|
const panel = activeIn(paneId);
|
|
322
464
|
return (
|
|
@@ -324,7 +466,7 @@ export function libraryPaneMounted(paneId: string, tab: Tab): boolean {
|
|
|
324
466
|
);
|
|
325
467
|
}
|
|
326
468
|
|
|
327
|
-
/** Tear one pane's
|
|
469
|
+
/** Tear one pane's home down (mode change, tab switch, project close). Idempotent. */
|
|
328
470
|
export function detachLibraryPane(paneId: string): void {
|
|
329
471
|
const panel = _active.get(paneId);
|
|
330
472
|
if (!panel) {
|
|
@@ -334,6 +476,7 @@ export function detachLibraryPane(paneId: string): void {
|
|
|
334
476
|
clearTimeout(panel.settleTimer);
|
|
335
477
|
panel.settleTimer = null;
|
|
336
478
|
}
|
|
479
|
+
panel.unlisten();
|
|
337
480
|
panel.observer.destroy();
|
|
338
481
|
panel.cache.clear();
|
|
339
482
|
panel.slots.clear();
|
|
@@ -377,8 +520,8 @@ async function fillPreview(panel: ActiveLibraryPane, path: string) {
|
|
|
377
520
|
return;
|
|
378
521
|
}
|
|
379
522
|
const slot = panel.slots.get(path);
|
|
380
|
-
if (slot?.isConnected && !slot
|
|
381
|
-
slot
|
|
523
|
+
if (slot?.isConnected && !shownPreview(slot)) {
|
|
524
|
+
mountPreview(slot, rendered);
|
|
382
525
|
}
|
|
383
526
|
} finally {
|
|
384
527
|
panel.pendingPaths.delete(path);
|
|
@@ -392,11 +535,11 @@ function tendSlot(panel: ActiveLibraryPane, slot: HTMLElement, path: string): vo
|
|
|
392
535
|
// Drift to the tail of the LRU, get evicted, get detached, and be re-rendered on the next
|
|
393
536
|
// Repaint: the 300-page measurement showed 560 reads for 300 documents until this line moved.
|
|
394
537
|
const cached = panel.cache.get(path);
|
|
395
|
-
if (slot
|
|
538
|
+
if (shownPreview(slot)) {
|
|
396
539
|
return;
|
|
397
540
|
}
|
|
398
541
|
if (cached) {
|
|
399
|
-
slot
|
|
542
|
+
mountPreview(slot, cached);
|
|
400
543
|
return;
|
|
401
544
|
}
|
|
402
545
|
if (panel.pendingPaths.has(path)) {
|
|
@@ -420,20 +563,11 @@ function previewSlot(panel: ActiveLibraryPane, slot: HTMLElement, path: string):
|
|
|
420
563
|
/**
|
|
421
564
|
* The pass that runs once the document has reconciled, coalesced onto a task of its own.
|
|
422
565
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* The first is releasing observations. A card outside the new window is now detached, and an
|
|
429
|
-
* observation of a detached node can never fire again: it is pure cost — the browser walks it on
|
|
430
|
-
* every scroll frame — and it pins the card's whole subtree. The intersect callback cannot be the
|
|
431
|
-
* only release path, because a card flicked past before it ever intersected is never reported.
|
|
432
|
-
*
|
|
433
|
-
* The second is tending the slots that ARE on screen. A row the reconciler kept keeps its preview
|
|
434
|
-
* box, so nothing re-announces it — and an LRU eviction detaches the render that was inside it. So
|
|
435
|
-
* every visible slot is touched here: the touch is what keeps it at the head of the LRU, and the
|
|
436
|
-
* refill is what puts a preview back into a box eviction emptied.
|
|
566
|
+
* A document repaint is not a moment this module is present at: writing the projection returns
|
|
567
|
+
* immediately and the runtime's keyed arrays reconcile a microtask later. Two things happen then:
|
|
568
|
+
* observations of cards that left the window are released (an observation of a detached node can
|
|
569
|
+
* never fire again, and it pins the card's subtree), and every visible slot is tended, which keeps
|
|
570
|
+
* it at the head of the LRU and refills a box an eviction emptied.
|
|
437
571
|
*/
|
|
438
572
|
function scheduleSettle(panel: ActiveLibraryPane): void {
|
|
439
573
|
if (panel.settleTimer !== null) {
|
|
@@ -445,7 +579,7 @@ function scheduleSettle(panel: ActiveLibraryPane): void {
|
|
|
445
579
|
return;
|
|
446
580
|
}
|
|
447
581
|
panel.observer.releaseDetached();
|
|
448
|
-
const previews = PREVIEW_LAYOUTS.has(
|
|
582
|
+
const previews = PREVIEW_LAYOUTS.has(collectionView().layout);
|
|
449
583
|
for (const [path, slot] of panel.slots) {
|
|
450
584
|
if (!slot.isConnected) {
|
|
451
585
|
panel.slots.delete(path);
|
|
@@ -458,14 +592,34 @@ function scheduleSettle(panel: ActiveLibraryPane): void {
|
|
|
458
592
|
}, 0);
|
|
459
593
|
}
|
|
460
594
|
|
|
595
|
+
// ─── Opening ─────────────────────────────────────────────────────────────────
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Open a row: a page in Design, an entry where it is written (`content/entry-open.ts`).
|
|
599
|
+
*
|
|
600
|
+
* The same answer whichever collection drew the row, because the file says what it is: a page is
|
|
601
|
+
* Design's, and activating it enters Design (§22.1).
|
|
602
|
+
*/
|
|
603
|
+
export function openLibraryFile(file: Pick<LibraryFile, "path" | "collection" | "record">): void {
|
|
604
|
+
if (file.record) {
|
|
605
|
+
/* A record of a file-backed collection opens in its entry form: that one row's fields, typed
|
|
606
|
+
by the collection's schema, kept in its file's grid (`content/record-entry.ts`). */
|
|
607
|
+
void openRecordEntry(file.record.source, file.record.row);
|
|
608
|
+
} else if (file.collection === null || file.collection === PAGES_COLLECTION) {
|
|
609
|
+
void openFileInTab(file.path);
|
|
610
|
+
} else {
|
|
611
|
+
void openEntry(file.path);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
461
615
|
// ─── Context menu ────────────────────────────────────────────────────────────
|
|
462
616
|
|
|
463
617
|
/**
|
|
464
|
-
* The menu the
|
|
618
|
+
* The per-file menu the home has up, if any.
|
|
465
619
|
*
|
|
466
|
-
* One handle
|
|
467
|
-
*
|
|
468
|
-
*
|
|
620
|
+
* One handle, because only one can be open: the kit's menu is a popover on the platform's own top
|
|
621
|
+
* layer, and opening a second while the first is up is how two of them end up light-dismissing each
|
|
622
|
+
* other.
|
|
469
623
|
*/
|
|
470
624
|
let _menu: MenuHandle | null = null;
|
|
471
625
|
|
|
@@ -483,16 +637,11 @@ function fileAt(path: string): LibraryFile | null {
|
|
|
483
637
|
);
|
|
484
638
|
}
|
|
485
639
|
|
|
486
|
-
/**
|
|
487
|
-
* The per-file menu, opened at the pointer.
|
|
488
|
-
*
|
|
489
|
-
* The rows are the `menu` surface's own projections, so the Library's Delete is drawn, keyed and
|
|
490
|
-
* dismissed exactly as the element menu's is — including the edge clamping, which was a hand-rolled
|
|
491
|
-
* `requestAnimationFrame` measurement here and is the kit's popover's job.
|
|
492
|
-
*/
|
|
640
|
+
/** The per-file menu, opened at the pointer. Region `content`. */
|
|
493
641
|
function showLibraryContextMenu(path: string, x: number, y: number): void {
|
|
494
642
|
const file = fileAt(path);
|
|
495
|
-
|
|
643
|
+
// A record is a row of a file: renaming, duplicating or deleting "it" would act on the file.
|
|
644
|
+
if (!file || file.record) {
|
|
496
645
|
return;
|
|
497
646
|
}
|
|
498
647
|
closeLibraryMenu();
|
|
@@ -518,10 +667,10 @@ function showLibraryContextMenu(path: string, x: number, y: number): void {
|
|
|
518
667
|
}
|
|
519
668
|
},
|
|
520
669
|
origin: { x, y },
|
|
521
|
-
region: "
|
|
670
|
+
region: "content",
|
|
522
671
|
rows: [
|
|
523
672
|
row("open", "Open", () => {
|
|
524
|
-
|
|
673
|
+
openLibraryFile(file);
|
|
525
674
|
}),
|
|
526
675
|
row(
|
|
527
676
|
"rename",
|
|
@@ -546,44 +695,8 @@ function showLibraryContextMenu(path: string, x: number, y: number): void {
|
|
|
546
695
|
});
|
|
547
696
|
}
|
|
548
697
|
|
|
549
|
-
/** The New menu, opened under its own button. A second press closes it: the button is a toggle. */
|
|
550
|
-
function showLibraryNewMenu(opener: HTMLElement): void {
|
|
551
|
-
if (_menu) {
|
|
552
|
-
closeLibraryMenu();
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
const entries = libraryNewEntries();
|
|
556
|
-
_menu = openMenu({
|
|
557
|
-
label: "New",
|
|
558
|
-
onClosed: (handle) => {
|
|
559
|
-
if (_menu === handle) {
|
|
560
|
-
_menu = null;
|
|
561
|
-
}
|
|
562
|
-
},
|
|
563
|
-
opener,
|
|
564
|
-
place: () => {
|
|
565
|
-
const box = rectOf(opener);
|
|
566
|
-
return { x: box.left, y: box.bottom + 4 };
|
|
567
|
-
},
|
|
568
|
-
region: "library-new",
|
|
569
|
-
rows: entries.map((entry, index) => ({
|
|
570
|
-
destructive: false,
|
|
571
|
-
disabled: false,
|
|
572
|
-
/* The document kinds and the collections are two different creation flows, so the boundary
|
|
573
|
-
between them is drawn rather than left to be inferred from the folder names. */
|
|
574
|
-
dividerAbove:
|
|
575
|
-
entry.collection !== undefined && entries[index - 1]?.collection === undefined && index > 0,
|
|
576
|
-
id: entry.key,
|
|
577
|
-
title: `${entry.label} — ${entry.dir}/`,
|
|
578
|
-
})),
|
|
579
|
-
run: (id) => {
|
|
580
|
-
void createLibraryEntry(id);
|
|
581
|
-
},
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
|
|
585
698
|
/**
|
|
586
|
-
* The
|
|
699
|
+
* The rename dialog — the same copy, from the same helper, as the Files panel's.
|
|
587
700
|
*
|
|
588
701
|
* A rename here rewrites references exactly as one in the sidebar does, and two dialogs that
|
|
589
702
|
* disagreed about that is the divergence the shared helper exists to prevent.
|
|
@@ -616,7 +729,7 @@ async function renameLibraryFile(file: LibraryFile) {
|
|
|
616
729
|
notify.error(`Could not rename ${file.name}.`, {
|
|
617
730
|
detail: errorMessage(error),
|
|
618
731
|
path: file.path,
|
|
619
|
-
source:
|
|
732
|
+
source: SOURCE,
|
|
620
733
|
});
|
|
621
734
|
}
|
|
622
735
|
}
|
|
@@ -638,7 +751,7 @@ async function duplicateLibraryFile(file: LibraryFile) {
|
|
|
638
751
|
notify.error(`Could not duplicate ${file.name}.`, {
|
|
639
752
|
detail: errorMessage(error),
|
|
640
753
|
path: file.path,
|
|
641
|
-
source:
|
|
754
|
+
source: SOURCE,
|
|
642
755
|
});
|
|
643
756
|
}
|
|
644
757
|
}
|
|
@@ -658,186 +771,95 @@ async function deleteLibraryFile(file: LibraryFile) {
|
|
|
658
771
|
notify.error(`Could not delete ${file.name}.`, {
|
|
659
772
|
detail: errorMessage(error),
|
|
660
773
|
path: file.path,
|
|
661
|
-
source:
|
|
774
|
+
source: SOURCE,
|
|
662
775
|
});
|
|
663
776
|
}
|
|
664
777
|
}
|
|
665
778
|
|
|
666
|
-
// ───
|
|
779
|
+
// ─── The collection's page ───────────────────────────────────────────────────
|
|
667
780
|
|
|
668
|
-
/**
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
dir: string;
|
|
673
|
-
/**
|
|
674
|
-
* The content collection this row creates an entry IN, or undefined for a document kind.
|
|
675
|
-
*
|
|
676
|
-
* Present is the whole difference between the two creation paths below — a collection row is
|
|
677
|
-
* `content/`'s job, because only that module knows the collection's extension and its schema.
|
|
678
|
-
*/
|
|
679
|
-
collection?: string;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* The new-entity rows.
|
|
684
|
-
*
|
|
685
|
-
* Collections come from `content/entry-model.ts` rather than from `project.json` directly, so the
|
|
686
|
-
* menu offers exactly the collections an entry can be created in. The predecessor read the raw
|
|
687
|
-
* `content` map and derived a directory from the type NAME when `source` was absent, which produced
|
|
688
|
-
* a row for a collection that has no entry directory at all and a row for a single-file (CSV)
|
|
689
|
-
* catalogue, whose entries are rows and not files. Both created a file nothing would ever load.
|
|
690
|
-
*/
|
|
691
|
-
export function libraryNewEntries(): LibraryNewEntry[] {
|
|
692
|
-
const rows: LibraryNewEntry[] = [
|
|
693
|
-
{ dir: "pages", key: "page", label: "Page" },
|
|
694
|
-
{ dir: "layouts", key: "layout", label: "Layout" },
|
|
695
|
-
{ dir: "components", key: "component", label: "Component" },
|
|
696
|
-
];
|
|
697
|
-
for (const collection of entryCollections()) {
|
|
698
|
-
rows.push({
|
|
699
|
-
collection: collection.name,
|
|
700
|
-
dir: collection.dir,
|
|
701
|
-
key: `collection:${collection.name}`,
|
|
702
|
-
label: collection.name.charAt(0).toUpperCase() + collection.name.slice(1),
|
|
703
|
-
});
|
|
781
|
+
/** The template a collection's page is drawn in, as the header line names it. */
|
|
782
|
+
function templateClause(page: CollectionPage, defaultLayout: string | null): string {
|
|
783
|
+
if (page.layout === false) {
|
|
784
|
+
return "with no template";
|
|
704
785
|
}
|
|
705
|
-
|
|
786
|
+
const layout = page.layout ?? defaultLayout;
|
|
787
|
+
return layout ? `using ${templateDisplayName(layout)}` : "with no template";
|
|
706
788
|
}
|
|
707
789
|
|
|
708
790
|
/**
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
* Two paths, because a collection entry is not a blank document. A **collection** row goes through
|
|
712
|
-
* `content/entry-commands.ts`'s `createEntry`, which supplies the collection's extension (so the
|
|
713
|
-
* field asks for a display name and the file is actually matched by the collection it was created
|
|
714
|
-
* in) and a body seeded from the collection's **schema defaults** — the entry is valid the moment
|
|
715
|
-
* it exists, rather than a pile of absent required fields — and then opens it in the entry FORM,
|
|
716
|
-
* which is the editor that collection's fields belong to. Both of those are the reason
|
|
717
|
-
* `createEntry` is called rather than reimplemented; a second seeder here would drift from the one
|
|
718
|
-
* the palette's `content.newEntry` uses within a release.
|
|
791
|
+
* The header line: where a collection's entries are published (§22.5).
|
|
719
792
|
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
* now offered rather than left to be typed from memory. A collection's extension is not a choice;
|
|
723
|
-
* it is the collection's, and `createEntry` supplies it.
|
|
724
|
-
*
|
|
725
|
-
* **A layout is not a choice either, and that is not a policy.** Both readers of a layout parse it
|
|
726
|
-
* as JSON and neither dispatches through the format registry — `site/layout-resolver.ts` throws and
|
|
727
|
-
* `site-context.ts` returns null — so a `.md` layout is a file the build cannot load. There is no
|
|
728
|
-
* `"layout"` document kind for a format to declare, so this is stated here rather than derived.
|
|
729
|
-
*
|
|
730
|
-
* A **component** keeps the picker: a format whose `$studio.documentMode.componentWhen` promotes a
|
|
731
|
-
* frontmatter `tagName` (Markdown's does) can express one perfectly well. What it cannot do is
|
|
732
|
-
* express one from a `newFileTemplate` that carries no `tagName`, so the seed below supplies it —
|
|
733
|
-
* through the format's own serializer, because this module knows no format's syntax.
|
|
793
|
+
* Pages have no line (a page is its own page), a collection whose index has not been read says
|
|
794
|
+
* nothing yet, and a collection with no page says so with the way to add one.
|
|
734
795
|
*/
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
* back to the format's own template and the author names the tag themselves.
|
|
751
|
-
*/
|
|
752
|
-
async function componentSeed(ext: string, fileName: string): Promise<string | undefined> {
|
|
753
|
-
const format = formatByExtension(ext);
|
|
754
|
-
if (!format?.capabilities.serialize) {
|
|
755
|
-
return undefined;
|
|
756
|
-
}
|
|
757
|
-
const tagName = fileName.slice(0, fileName.length - ext.length);
|
|
758
|
-
if (!/^[a-z][\d.a-z]*-[\d.a-z-]*$/.test(tagName)) {
|
|
759
|
-
return undefined;
|
|
796
|
+
export function pageLineFor(
|
|
797
|
+
collection: ContentCollection,
|
|
798
|
+
page: CollectionPage | null | undefined,
|
|
799
|
+
defaultLayout: string | null,
|
|
800
|
+
every: readonly CollectionPage[] = [],
|
|
801
|
+
): LibraryPageLine {
|
|
802
|
+
const hidden: LibraryPageLine = {
|
|
803
|
+
designLabel: "",
|
|
804
|
+
pattern: "",
|
|
805
|
+
state: "hidden",
|
|
806
|
+
template: "",
|
|
807
|
+
text: "",
|
|
808
|
+
};
|
|
809
|
+
if (collection.kind === "pages") {
|
|
810
|
+
return hidden;
|
|
760
811
|
}
|
|
761
|
-
|
|
762
|
-
return
|
|
763
|
-
format.name,
|
|
764
|
-
{ children: [], tagName },
|
|
765
|
-
{ frontmatter: true, mode: "roundtrip" },
|
|
766
|
-
);
|
|
767
|
-
} catch {
|
|
768
|
-
// The serializer is the project's, so its failure is not a reason to refuse the creation —
|
|
769
|
-
// The format's own template still produces a file the author can finish by hand.
|
|
770
|
-
return undefined;
|
|
812
|
+
if (page === undefined) {
|
|
813
|
+
return { ...hidden, state: "loading", text: "Finding where these entries are published…" };
|
|
771
814
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
export async function createLibraryEntry(key: string): Promise<string | null> {
|
|
775
|
-
const entry = libraryNewEntries().find((row) => row.key === key);
|
|
776
|
-
if (!entry) {
|
|
777
|
-
return null;
|
|
815
|
+
if (page === null) {
|
|
816
|
+
return { ...hidden, state: "none", text: "Entries have no page of their own" };
|
|
778
817
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
818
|
+
/* Every language, when the view shows every language and the collection has a page in more than
|
|
819
|
+
one: naming only the default language's page under "All" says the others have none. Each is
|
|
820
|
+
named by its language, in the order the index holds them; one template clause when they share
|
|
821
|
+
a template, else each its own. */
|
|
822
|
+
// In the order the project declares its languages, the default first, as every language list is.
|
|
823
|
+
const order = projectLocales();
|
|
824
|
+
const rank = (locale: string | null) => {
|
|
825
|
+
const at = locale === null ? -1 : order.indexOf(locale);
|
|
826
|
+
return at === -1 ? order.length : at;
|
|
827
|
+
};
|
|
828
|
+
const localized = every
|
|
829
|
+
.filter((each) => each.locale !== null)
|
|
830
|
+
.toSorted((a, b) => rank(a.locale) - rank(b.locale));
|
|
831
|
+
if (localized.length > 1) {
|
|
832
|
+
const clauses = new Set(localized.map((each) => templateClause(each, defaultLayout)));
|
|
833
|
+
return {
|
|
834
|
+
designLabel: `Design the ${collection.item} page`,
|
|
835
|
+
pattern: localized.map((each) => `${each.pattern} (${each.locale})`).join(", "),
|
|
836
|
+
state: "page",
|
|
837
|
+
template: clauses.size === 1 ? [...clauses][0]! : "using a template per language",
|
|
838
|
+
text: `Each ${collection.item} has a page in each language at`,
|
|
787
839
|
};
|
|
788
|
-
if (entry.key === "component") {
|
|
789
|
-
request.content = componentSeed;
|
|
790
|
-
}
|
|
791
|
-
created = await createFileIn(request);
|
|
792
|
-
} else {
|
|
793
|
-
created = await createEntry(entry.collection);
|
|
794
|
-
}
|
|
795
|
-
if (created === null) {
|
|
796
|
-
return null;
|
|
797
840
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
return created;
|
|
841
|
+
return {
|
|
842
|
+
designLabel: `Design the ${collection.item} page`,
|
|
843
|
+
pattern: page.pattern,
|
|
844
|
+
state: "page",
|
|
845
|
+
template: templateClause(page, defaultLayout),
|
|
846
|
+
text: `Each ${collection.item} has a page at`,
|
|
847
|
+
};
|
|
806
848
|
}
|
|
807
849
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
* visible BEFORE the drop. "All" has none, and rather than falling back to a default nobody chose,
|
|
815
|
-
* it asks. That fallback is exactly the surprise studio.md §16 exists to refuse: the file arrives,
|
|
816
|
-
* the toast says it worked, and it is in a directory the author never picked.
|
|
817
|
-
*/
|
|
818
|
-
export async function resolveUploadDir(): Promise<string | null> {
|
|
819
|
-
const named = uploadDirForCategory(libraryView.category);
|
|
820
|
-
if (named !== undefined) {
|
|
821
|
-
return named;
|
|
850
|
+
/** Open the collection's page in Design: activating it enters Design (§22.1). */
|
|
851
|
+
function designCollectionPage(name: string): void {
|
|
852
|
+
// The page in the language the view is filtered to: the one the reader is looking at.
|
|
853
|
+
const page = peekCollectionPage(name, collectionView(name).locale);
|
|
854
|
+
if (page) {
|
|
855
|
+
void openFileInTab(page.path);
|
|
822
856
|
}
|
|
823
|
-
const chosen = await showPromptDialog("Upload files", {
|
|
824
|
-
confirmLabel: "Upload",
|
|
825
|
-
message: "The All view has no folder of its own. Choose where these files land.",
|
|
826
|
-
placeholder: "public",
|
|
827
|
-
validate: (value) => (value.trim() ? "" : "Enter a folder."),
|
|
828
|
-
value: "public",
|
|
829
|
-
});
|
|
830
|
-
return chosen === null ? null : chosen.trim().replace(/\/$/, "");
|
|
831
857
|
}
|
|
832
858
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
await uploadAssets([...files], { dir });
|
|
839
|
-
invalidateLibrary();
|
|
840
|
-
await loadLibrary();
|
|
859
|
+
/** Write the collection's page, as Add new's own-page option does, then show the new line. */
|
|
860
|
+
async function addCollectionPageFor(name: string): Promise<void> {
|
|
861
|
+
const { addCollectionPage } = await import("../create/add-new-flow");
|
|
862
|
+
await addCollectionPage(name);
|
|
841
863
|
}
|
|
842
864
|
|
|
843
865
|
// ─── The projection ──────────────────────────────────────────────────────────
|
|
@@ -848,25 +870,27 @@ function chip(key: string, label: string, active: boolean): LibraryChip {
|
|
|
848
870
|
}
|
|
849
871
|
|
|
850
872
|
/** The sentence a filter that matched nothing prints, naming both facets rather than one. */
|
|
851
|
-
function noMatchMessage(): string {
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const where = scopes.length === 0 ? "" : ` in ${scopes.join(" and ")}`;
|
|
860
|
-
const term = libraryView.query.trim();
|
|
861
|
-
return `No files match${term ? ` “${term}”` : ""}${where}.`;
|
|
873
|
+
function noMatchMessage(collection: ContentCollection, view: CollectionViewState): string {
|
|
874
|
+
const where = `${view.locale === "" ? "" : ` in ${localeLabel(view.locale)}`}${
|
|
875
|
+
view.filter === "" ? "" : ` filtered to ${view.filter}`
|
|
876
|
+
}`;
|
|
877
|
+
const term = view.query.trim();
|
|
878
|
+
return `No ${collection.kind === "pages" ? "pages" : collection.title.toLowerCase()} match${
|
|
879
|
+
term ? ` “${term}”` : ""
|
|
880
|
+
}${where}.`;
|
|
862
881
|
}
|
|
863
882
|
|
|
864
|
-
/** Which of the
|
|
865
|
-
function viewKind(
|
|
883
|
+
/** Which of the eight things the body draws right now. */
|
|
884
|
+
function viewKind(
|
|
885
|
+
current: LibrarySource,
|
|
886
|
+
layout: LibraryLayout,
|
|
887
|
+
matched: number,
|
|
888
|
+
total: number,
|
|
889
|
+
): LibraryViewKind {
|
|
866
890
|
if (matched > 0) {
|
|
867
|
-
return
|
|
891
|
+
return layout;
|
|
868
892
|
}
|
|
869
|
-
if (
|
|
893
|
+
if (contentView.loading || !current.scanned()) {
|
|
870
894
|
return "loading";
|
|
871
895
|
}
|
|
872
896
|
if (current.failures().length > 0) {
|
|
@@ -875,11 +899,329 @@ function viewKind(current: LibrarySource, matched: number, total: number): Libra
|
|
|
875
899
|
return total === 0 ? "empty" : "nomatch";
|
|
876
900
|
}
|
|
877
901
|
|
|
902
|
+
/**
|
|
903
|
+
* A collection's entry files, in scan order. The ONE list both the Content home and the Collections
|
|
904
|
+
* panel read entry summaries for: the summary cache keeps one list per collection, so two
|
|
905
|
+
* differently-ordered lists would each evict the other's read.
|
|
906
|
+
*/
|
|
907
|
+
export function collectionEntryPaths(key: string): string[] {
|
|
908
|
+
return librarySource()
|
|
909
|
+
.files()
|
|
910
|
+
.filter((file) => file.collection === key)
|
|
911
|
+
.map((file) => file.path);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* What each entry of a declared collection says about itself (title, status, date), read once
|
|
916
|
+
* through the Collections panel's summaries. Pages and a file-backed collection (whose entries are
|
|
917
|
+
* rows of one file) answer nothing, and so does every entry until the read lands.
|
|
918
|
+
*/
|
|
919
|
+
function entryFactsFor(collection: ContentCollection, layout: LibraryLayout): EntryFactsOf {
|
|
920
|
+
if (collection.kind === "pages") {
|
|
921
|
+
return librarySource().scanned() ? pageFactsFor(layout === "table") : entryFactsFrom(new Map());
|
|
922
|
+
}
|
|
923
|
+
if (collection.fileBacked) {
|
|
924
|
+
/* A record's facts are its fields, read with the file by the scan; it is dated by its own date
|
|
925
|
+
field, as an entry file is, so the Calendar never falls back to the one file's mtime. */
|
|
926
|
+
const schema = collectionSchema(collection.key);
|
|
927
|
+
const records = librarySource()
|
|
928
|
+
.files()
|
|
929
|
+
.filter((file) => file.collection === collection.key && file.record);
|
|
930
|
+
return entryFactsFrom(
|
|
931
|
+
new Map(
|
|
932
|
+
records.map((file) => {
|
|
933
|
+
const { fields } = file.record!;
|
|
934
|
+
const dateField = dateFieldOf(schema, fields);
|
|
935
|
+
const date = dateField === null ? "" : entryDay(fields[dateField] ?? "");
|
|
936
|
+
// A record's picture is its image field, relative to the file that holds it.
|
|
937
|
+
const thumb = entryThumb(file.record!.source, fields, schema);
|
|
938
|
+
return [file.path, { date, fields, status: "", thumb, title: file.name }];
|
|
939
|
+
}),
|
|
940
|
+
),
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
if (!librarySource().scanned()) {
|
|
944
|
+
return entryFactsFrom(new Map());
|
|
945
|
+
}
|
|
946
|
+
const summaries = peekEntrySummaries(collection.key, collectionEntryPaths(collection.key));
|
|
947
|
+
const byPath = new Map<string, EntryFacts>(
|
|
948
|
+
(summaries ?? []).map((entry) => [
|
|
949
|
+
entry.path,
|
|
950
|
+
{
|
|
951
|
+
date: entry.date,
|
|
952
|
+
fields: entry.fields,
|
|
953
|
+
status: entry.status,
|
|
954
|
+
thumb: entry.thumb,
|
|
955
|
+
title: entry.title,
|
|
956
|
+
},
|
|
957
|
+
]),
|
|
958
|
+
);
|
|
959
|
+
return entryFactsFrom(byPath);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* What each page says about itself in the Pages table: its name, as the Site panel gives it (Home,
|
|
964
|
+
* About, "Post page" for a collection's page), its template, and Draft for a page marked one.
|
|
965
|
+
*
|
|
966
|
+
* The name comes from the route, so it is there before any file is read; the template and the draft
|
|
967
|
+
* flag arrive with the page summaries. A page's Status reads Published only when some page in the
|
|
968
|
+
* project is a draft: with none, pages have no draft workflow, and a column of "Published" would be
|
|
969
|
+
* a state nobody defined. Only the Table draws a template or a status, so only the Table reads the
|
|
970
|
+
* pages: Cards, Calendar and Board name a page by its route and read nothing.
|
|
971
|
+
*/
|
|
972
|
+
function pageFactsFor(readPages: boolean): EntryFactsOf {
|
|
973
|
+
const paths = collectionEntryPaths(PAGES_COLLECTION);
|
|
974
|
+
const summaries = readPages ? peekPageSummaries(paths) : null;
|
|
975
|
+
const declared = new Set(Object.keys(projectState?.projectConfig?.content ?? {}));
|
|
976
|
+
const fallback = defaultLayoutPath();
|
|
977
|
+
const { i18n } = resolveI18n(projectState?.projectConfig ?? {});
|
|
978
|
+
const anyDraft = [...(summaries?.values() ?? [])].some((page) => page.draft);
|
|
979
|
+
const byPath = new Map<string, EntryFacts>(
|
|
980
|
+
paths.map((path) => {
|
|
981
|
+
const summary = summaries?.get(path);
|
|
982
|
+
const collection = summary?.contentType ?? null;
|
|
983
|
+
const layout = summary === undefined ? undefined : (summary.layout ?? fallback ?? false);
|
|
984
|
+
return [
|
|
985
|
+
path,
|
|
986
|
+
{
|
|
987
|
+
date: "",
|
|
988
|
+
status: summary === undefined || !anyDraft ? "" : summary.draft ? "Draft" : "Published",
|
|
989
|
+
template:
|
|
990
|
+
layout === undefined ? "" : layout === false ? "None" : templateDisplayName(layout),
|
|
991
|
+
title:
|
|
992
|
+
collection !== null && declared.has(collection)
|
|
993
|
+
? localeMarked(`${singularLabel(collection)} page`, path, i18n)
|
|
994
|
+
: pageLabel(fileToRoute(path), path, i18n),
|
|
995
|
+
},
|
|
996
|
+
];
|
|
997
|
+
}),
|
|
998
|
+
);
|
|
999
|
+
return entryFactsFrom(byPath);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** Pages in the order the router matches them, the order the Site panel lists them in. */
|
|
1003
|
+
function inRouteOrder(files: readonly LibraryFile[]): readonly LibraryFile[] {
|
|
1004
|
+
return files
|
|
1005
|
+
.map((file) => ({ file, route: fileToRoute(file.path) }))
|
|
1006
|
+
.toSorted((a, b) => compareRoutes(a.route, b.route) || a.file.path.localeCompare(b.file.path))
|
|
1007
|
+
.map(({ file }) => file);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/** A declared collection's schema, or null for one that declares none. */
|
|
1011
|
+
function collectionSchema(key: string): ContentTypeSchema | null {
|
|
1012
|
+
return entryCollection(key)?.schema ?? declaredCollectionSchema(key);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/** The fields the Table names in a column of their own already: what an entry is called, its date. */
|
|
1016
|
+
function listedElsewhere(schema: ContentTypeSchema | null): ReadonlySet<string> {
|
|
1017
|
+
const skip = new Set<string>();
|
|
1018
|
+
const title = titleFieldOf(schema);
|
|
1019
|
+
const date = declaredDateField(schema);
|
|
1020
|
+
for (const name of [title, date]) {
|
|
1021
|
+
if (name !== null) {
|
|
1022
|
+
skip.add(name);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return skip;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/** Each field's declared string `enum`, the order a Board grouped by it draws its columns in. */
|
|
1029
|
+
function fieldEnums(schema: ContentTypeSchema | null): ReadonlyMap<string, readonly string[]> {
|
|
1030
|
+
const enums = new Map<string, readonly string[]>();
|
|
1031
|
+
for (const [name, field] of Object.entries(schema?.properties ?? {})) {
|
|
1032
|
+
if (Array.isArray(field.enum)) {
|
|
1033
|
+
enums.set(
|
|
1034
|
+
name,
|
|
1035
|
+
field.enum.filter((value) => isString(value)),
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return enums;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/** The field the Filter narrows a collection by, and the values it offers. */
|
|
1043
|
+
export interface FilterField {
|
|
1044
|
+
/** "Status", or the field's own title. */
|
|
1045
|
+
title: string;
|
|
1046
|
+
/** The values offered, in the order the Board stands its columns in. */
|
|
1047
|
+
values: readonly string[];
|
|
1048
|
+
/** Whether an entry with these facts has `value`. */
|
|
1049
|
+
matches: (facts: EntryFacts | undefined, value: string) => boolean;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* What the Filter narrows by: an entry's **status** where the collection has a draft workflow, else
|
|
1054
|
+
* the field its Board groups by (`boardFieldOf`: the first field with an `enum`, else the first
|
|
1055
|
+
* whose values repeat; a list field by each of its items). The same choice the Board makes, so the
|
|
1056
|
+
* Filter offers exactly the columns the Board draws. `null` when nothing groups the entries.
|
|
1057
|
+
*/
|
|
1058
|
+
export function filterFieldOf(
|
|
1059
|
+
facts: readonly (EntryFacts | undefined)[],
|
|
1060
|
+
field: BoardField | null,
|
|
1061
|
+
): FilterField | null {
|
|
1062
|
+
const statuses = [...new Set(facts.flatMap((entry) => (entry?.status ? [entry.status] : [])))];
|
|
1063
|
+
if (statuses.length > 0) {
|
|
1064
|
+
return {
|
|
1065
|
+
matches: (entry, value) => (entry?.status ?? "") === value,
|
|
1066
|
+
title: "Status",
|
|
1067
|
+
values: statuses.toSorted((a, b) =>
|
|
1068
|
+
a === "Draft" ? -1 : b === "Draft" ? 1 : a.localeCompare(b),
|
|
1069
|
+
),
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
if (!field) {
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
const held = new Set(
|
|
1076
|
+
facts.flatMap((entry) => fieldValues(entry?.fields?.[field.name], field.list)),
|
|
1077
|
+
);
|
|
1078
|
+
const values =
|
|
1079
|
+
field.order.length > 0
|
|
1080
|
+
? [...field.order, ...[...held].filter((value) => !field.order.includes(value)).toSorted()]
|
|
1081
|
+
: [...held].toSorted((a, b) => a.localeCompare(b));
|
|
1082
|
+
return {
|
|
1083
|
+
matches: (entry, value) => fieldValues(entry?.fields?.[field.name], field.list).includes(value),
|
|
1084
|
+
title: field.title,
|
|
1085
|
+
values,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/** Everything the home draws in a pane, as one value. */
|
|
1090
|
+
function homeView(panel: ActiveLibraryPane | null, paneId: string): LibraryView {
|
|
1091
|
+
const current = librarySource();
|
|
1092
|
+
const collection = shownCollection();
|
|
1093
|
+
const view = collectionView(collection.key);
|
|
1094
|
+
const { layout } = view;
|
|
1095
|
+
const all = filterLibrary(current.files(), { collection: collection.key, query: "" });
|
|
1096
|
+
const factsOf = entryFactsFor(collection, layout);
|
|
1097
|
+
const facts = all.map((file) => factsOf(file));
|
|
1098
|
+
const schema = collection.kind === "collection" ? collectionSchema(collection.key) : null;
|
|
1099
|
+
const fieldColumns = entryFieldColumns(schema, listedElsewhere(schema));
|
|
1100
|
+
const boardField = boardFieldOf(fieldColumns, fieldEnums(schema), facts);
|
|
1101
|
+
// Pages are listed by route; the Filter is a collection's, over what its entries say.
|
|
1102
|
+
const filterField = collection.kind === "collection" ? filterFieldOf(facts, boardField) : null;
|
|
1103
|
+
const filterValue =
|
|
1104
|
+
filterField !== null && filterField.values.includes(view.filter) ? view.filter : "";
|
|
1105
|
+
const searched = filterLibrary(current.files(), {
|
|
1106
|
+
collection: collection.key,
|
|
1107
|
+
locale: view.locale,
|
|
1108
|
+
query: view.query,
|
|
1109
|
+
});
|
|
1110
|
+
const filtered =
|
|
1111
|
+
filterField === null || filterValue === ""
|
|
1112
|
+
? searched
|
|
1113
|
+
: searched.filter((file) => filterField.matches(factsOf(file), filterValue));
|
|
1114
|
+
const files = collection.kind === "pages" ? inRouteOrder(filtered) : filtered;
|
|
1115
|
+
const total = all.length;
|
|
1116
|
+
const kind = viewKind(current, layout, files.length, total);
|
|
1117
|
+
const range = windowFor(panel, layout, files.length);
|
|
1118
|
+
const slice = files.slice(range.start, range.end);
|
|
1119
|
+
const failures = current.failures();
|
|
1120
|
+
const locales = libraryLocales(all);
|
|
1121
|
+
const columns = libraryColumns(collection.key, {
|
|
1122
|
+
// A date the schema declares is a column even before an entry fills it in.
|
|
1123
|
+
date: declaredDateField(schema) !== null || facts.some((entry) => (entry?.date ?? "") !== ""),
|
|
1124
|
+
fields: fieldColumns,
|
|
1125
|
+
locale: locales.length > 1,
|
|
1126
|
+
status: facts.some((entry) => (entry?.status ?? "") !== ""),
|
|
1127
|
+
});
|
|
1128
|
+
const live = kind === "cards";
|
|
1129
|
+
const calendar =
|
|
1130
|
+
kind === "calendar" ? calendarView(files, factsOf) : { groups: [], truncated: "" };
|
|
1131
|
+
const pages = collection.kind === "pages";
|
|
1132
|
+
const plural = pages ? "pages" : collection.title.toLowerCase();
|
|
1133
|
+
|
|
1134
|
+
return {
|
|
1135
|
+
bannerState: failures.length > 0 ? "shown" : "hidden",
|
|
1136
|
+
bannerText:
|
|
1137
|
+
failures.length === 0
|
|
1138
|
+
? ""
|
|
1139
|
+
: `This list is incomplete — ${failures.length} ${
|
|
1140
|
+
failures.length === 1 ? "directory" : "directories"
|
|
1141
|
+
} could not be read (${failures[0]!.dir}${failures.length > 1 ? ", …" : ""}).`,
|
|
1142
|
+
columns: kind === "table" ? columns.map((c) => ({ field: c.field, title: c.title })) : [],
|
|
1143
|
+
emptyDetail:
|
|
1144
|
+
kind === "incomplete"
|
|
1145
|
+
? `${failures.length} ${
|
|
1146
|
+
failures.length === 1 ? "directory" : "directories"
|
|
1147
|
+
} could not be read, so this is not the same as an empty collection.`
|
|
1148
|
+
: kind === "nomatch"
|
|
1149
|
+
? `${total} ${total === 1 ? collection.item : plural} in all.`
|
|
1150
|
+
: kind === "empty"
|
|
1151
|
+
? `New ${collection.item} adds the first one.`
|
|
1152
|
+
: "",
|
|
1153
|
+
emptyMessage:
|
|
1154
|
+
kind === "nomatch"
|
|
1155
|
+
? noMatchMessage(collection, view)
|
|
1156
|
+
: kind === "empty"
|
|
1157
|
+
? `No ${plural} yet.`
|
|
1158
|
+
: "",
|
|
1159
|
+
fieldsState: collection.kind === "collection" ? "shown" : "hidden",
|
|
1160
|
+
groups: kind === "board" ? boardView(files, factsOf, boardField) : calendar.groups,
|
|
1161
|
+
layouts: LIBRARY_LAYOUTS.map((name) =>
|
|
1162
|
+
chip(name, LIBRARY_LAYOUT_LABELS[name], layout === name),
|
|
1163
|
+
),
|
|
1164
|
+
localeOptions: [
|
|
1165
|
+
{ label: "All languages", value: "all" },
|
|
1166
|
+
...locales.map((tag) => ({ label: localeLabel(tag), value: tag })),
|
|
1167
|
+
],
|
|
1168
|
+
/* The facet's options come from ALL of the collection's files, never from the filtered ones: a
|
|
1169
|
+
picker whose choices collapse to the choice just made cannot be used to make another. One
|
|
1170
|
+
locale — or none — means every file already agrees. */
|
|
1171
|
+
filterOptions:
|
|
1172
|
+
filterField === null
|
|
1173
|
+
? []
|
|
1174
|
+
: [
|
|
1175
|
+
{ label: `Any ${filterField.title.toLowerCase()}`, value: "all" },
|
|
1176
|
+
...filterField.values.map((value) => ({ label: value, value })),
|
|
1177
|
+
],
|
|
1178
|
+
filterState: filterField !== null && filterField.values.length > 1 ? "shown" : "hidden",
|
|
1179
|
+
filterValue: filterValue === "" ? "all" : filterValue,
|
|
1180
|
+
localeState: locales.length < 2 ? "hidden" : "shown",
|
|
1181
|
+
localeValue: view.locale === "" ? "all" : view.locale,
|
|
1182
|
+
newLabel: `New ${collection.item}`,
|
|
1183
|
+
// A file-backed collection's entries are rows of its file, added in the file's grid.
|
|
1184
|
+
newState: collection.fileBacked ? "hidden" : "shown",
|
|
1185
|
+
/* ADDED to the grid's own padding rather than replacing it: a bare `padding-top:0px` took the
|
|
1186
|
+
gap under the header away, and the first row of cards sat with its top border under it. */
|
|
1187
|
+
pad: `padding-top:calc(var(--jx-space-4) + ${range.padTop}px);padding-bottom:calc(var(--jx-space-4) + ${range.padBottom}px)`,
|
|
1188
|
+
padBottom: `height:${range.padBottom}px`,
|
|
1189
|
+
padTop: `height:${range.padTop}px`,
|
|
1190
|
+
page: pageLineFor(
|
|
1191
|
+
collection,
|
|
1192
|
+
pages ? null : peekCollectionPage(collection.key, view.locale),
|
|
1193
|
+
defaultLayoutPath(),
|
|
1194
|
+
// Under every language, the page of each; filtered to one, that language's alone.
|
|
1195
|
+
pages || view.locale !== "" ? [] : (peekCollectionPages(collection.key) ?? []),
|
|
1196
|
+
),
|
|
1197
|
+
query: view.query,
|
|
1198
|
+
region: paneRegion(paneId, "content"),
|
|
1199
|
+
rows:
|
|
1200
|
+
kind === "cards" || kind === "table"
|
|
1201
|
+
? slice.map((file) =>
|
|
1202
|
+
libraryRow(file, kind === "table" ? columns : [], live, factsOf(file)),
|
|
1203
|
+
)
|
|
1204
|
+
: [],
|
|
1205
|
+
searchLabel: `Search ${plural}`,
|
|
1206
|
+
subtitle: pages ? "Every URL on your site." : "",
|
|
1207
|
+
title: collection.title,
|
|
1208
|
+
truncated: calendar.truncated,
|
|
1209
|
+
truncatedState: calendar.truncated === "" ? "hidden" : "shown",
|
|
1210
|
+
view: kind,
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/** The project's default layout, which a page with no `$layout` of its own is drawn in. */
|
|
1215
|
+
function defaultLayoutPath(): string | null {
|
|
1216
|
+
// Read at call time: `projectState` is replaced wholesale on a project switch.
|
|
1217
|
+
return projectState?.projectConfig?.defaults?.layout ?? null;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
878
1220
|
// ─── Mount ───────────────────────────────────────────────────────────────────
|
|
879
1221
|
|
|
880
1222
|
/**
|
|
881
|
-
* Draw the
|
|
882
|
-
* the pane owns its own reactivity from here (the grid/settings/stylebook pattern).
|
|
1223
|
+
* Draw the Content home into the pane. Re-entrant: a same-tab call while it is live is a no-op,
|
|
1224
|
+
* because the pane owns its own reactivity from here (the grid/settings/stylebook pattern).
|
|
883
1225
|
*/
|
|
884
1226
|
export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
885
1227
|
const { paneId, wrap: canvasWrap } = surface;
|
|
@@ -901,115 +1243,54 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
901
1243
|
}
|
|
902
1244
|
});
|
|
903
1245
|
|
|
904
|
-
const view = (): LibraryView => {
|
|
905
|
-
const current = librarySource();
|
|
906
|
-
const { layout } = libraryView;
|
|
907
|
-
const files = filterLibrary(current.files(), {
|
|
908
|
-
category: libraryView.category,
|
|
909
|
-
locale: libraryView.locale,
|
|
910
|
-
query: libraryView.query,
|
|
911
|
-
});
|
|
912
|
-
const total = current.files().length;
|
|
913
|
-
const kind = viewKind(current, files.length, total);
|
|
914
|
-
const range = windowFor(panel, layout, files.length);
|
|
915
|
-
const slice = files.slice(range.start, range.end);
|
|
916
|
-
const failures = current.failures();
|
|
917
|
-
const locales = libraryLocales(current.files());
|
|
918
|
-
const destination = uploadDirForCategory(libraryView.category);
|
|
919
|
-
/* Cards is the only layout that draws a live document render inside a card. A Media tile is a
|
|
920
|
-
thumbnail of an ASSET — `library-layouts.ts` says why — so it asks for the glyph instead. */
|
|
921
|
-
const live = kind === "cards";
|
|
922
|
-
const calendar = kind === "calendar" ? calendarView(files) : { groups: [], truncated: "" };
|
|
923
|
-
|
|
924
|
-
return {
|
|
925
|
-
bannerState: failures.length > 0 ? "shown" : "hidden",
|
|
926
|
-
bannerText:
|
|
927
|
-
failures.length === 0
|
|
928
|
-
? ""
|
|
929
|
-
: `This list is incomplete — ${failures.length} ${
|
|
930
|
-
failures.length === 1 ? "directory" : "directories"
|
|
931
|
-
} could not be read (${failures[0]!.dir}${failures.length > 1 ? ", …" : ""}).`,
|
|
932
|
-
categories: LIBRARY_CATEGORIES.map((category) =>
|
|
933
|
-
chip(category.key, category.label, libraryView.category === category.key),
|
|
934
|
-
),
|
|
935
|
-
columns:
|
|
936
|
-
kind === "table" ? libraryColumns().map((c) => ({ field: c.field, title: c.title })) : [],
|
|
937
|
-
dropActive: panel?.dropActive ? "true" : "false",
|
|
938
|
-
dropRegion: paneRegion(paneId, "library/dropZone"),
|
|
939
|
-
emptyDetail:
|
|
940
|
-
kind === "incomplete"
|
|
941
|
-
? `${failures.length} ${
|
|
942
|
-
failures.length === 1 ? "directory" : "directories"
|
|
943
|
-
} could not be read, so this is not the same as an empty project.`
|
|
944
|
-
: kind === "nomatch"
|
|
945
|
-
? `${total} file(s) in the project.`
|
|
946
|
-
: "",
|
|
947
|
-
emptyMessage: kind === "nomatch" ? noMatchMessage() : "",
|
|
948
|
-
groups: kind === "board" ? boardView(files) : calendar.groups,
|
|
949
|
-
layouts: LIBRARY_LAYOUTS.map((name) =>
|
|
950
|
-
chip(name, LIBRARY_LAYOUT_LABELS[name], libraryView.layout === name),
|
|
951
|
-
),
|
|
952
|
-
localeOptions: [
|
|
953
|
-
{ label: "All languages", value: "all" },
|
|
954
|
-
...locales.map((tag) => ({ label: localeLabel(tag), value: tag })),
|
|
955
|
-
],
|
|
956
|
-
/* The facet's options come from ALL scanned files, never from the filtered ones: a picker
|
|
957
|
-
whose choices collapse to the choice just made cannot be used to make another. One locale —
|
|
958
|
-
or none — means every file already agrees. */
|
|
959
|
-
localeState: locales.length < 2 ? "hidden" : "shown",
|
|
960
|
-
localeValue: libraryView.locale === "" ? "all" : libraryView.locale,
|
|
961
|
-
pad: `padding-top:${range.padTop}px;padding-bottom:${range.padBottom}px`,
|
|
962
|
-
padBottom: `height:${range.padBottom}px`,
|
|
963
|
-
padTop: `height:${range.padTop}px`,
|
|
964
|
-
query: libraryView.query,
|
|
965
|
-
region: paneRegion(paneId, "library"),
|
|
966
|
-
rows:
|
|
967
|
-
kind === "cards" || kind === "media" || kind === "table"
|
|
968
|
-
? slice.map((file) => libraryRow(file, kind === "table" ? libraryColumns() : [], live))
|
|
969
|
-
: [],
|
|
970
|
-
truncated: calendar.truncated,
|
|
971
|
-
truncatedState: calendar.truncated === "" ? "hidden" : "shown",
|
|
972
|
-
uploadAccept: uploadAccept(),
|
|
973
|
-
uploadHint:
|
|
974
|
-
destination === undefined ? "Upload — asks for a folder" : `Upload into ${destination}/`,
|
|
975
|
-
view: kind,
|
|
976
|
-
};
|
|
977
|
-
};
|
|
978
|
-
|
|
979
1246
|
const draw = () => {
|
|
980
1247
|
if (panel !== null && activeIn(paneId) === panel) {
|
|
981
|
-
panel.surface.update(
|
|
1248
|
+
panel.surface.update(homeView(panel, paneId));
|
|
982
1249
|
scheduleSettle(panel);
|
|
983
1250
|
}
|
|
984
1251
|
};
|
|
985
1252
|
|
|
986
|
-
const mounted = mountLibrarySurface(canvasWrap,
|
|
1253
|
+
const mounted = mountLibrarySurface(canvasWrap, homeView(null, paneId), {
|
|
1254
|
+
addPage: () => {
|
|
1255
|
+
void addCollectionPageFor(shownCollection().key);
|
|
1256
|
+
},
|
|
987
1257
|
contextMenu: (path, x, y) => showLibraryContextMenu(path, x, y),
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
panel.dropActive = false;
|
|
991
|
-
draw();
|
|
992
|
-
}
|
|
1258
|
+
designPage: () => {
|
|
1259
|
+
designCollectionPage(shownCollection().key);
|
|
993
1260
|
},
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1261
|
+
openFields: () => {
|
|
1262
|
+
void runActiveReported(
|
|
1263
|
+
"settings.open",
|
|
1264
|
+
{ entry: shownCollection().key, section: "content" },
|
|
1265
|
+
SOURCE,
|
|
1266
|
+
);
|
|
999
1267
|
},
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1268
|
+
openGrid: () => {
|
|
1269
|
+
const collection = shownCollection();
|
|
1270
|
+
// A file-backed collection's grid is its file's own: `collection.editInGrid` grids folders.
|
|
1271
|
+
const file = collection.fileBacked ? collectionSourceFile(collection.key) : null;
|
|
1272
|
+
if (file) {
|
|
1273
|
+
void openFileInTab(file);
|
|
1274
|
+
return;
|
|
1007
1275
|
}
|
|
1276
|
+
void runActiveReported("collection.editInGrid", { name: collection.key }, SOURCE);
|
|
1008
1277
|
},
|
|
1009
1278
|
openFile: (path) => {
|
|
1010
|
-
|
|
1279
|
+
openLibraryFile(fileAt(path) ?? { collection: shownCollectionKey(), path });
|
|
1280
|
+
},
|
|
1281
|
+
openNew: () => {
|
|
1282
|
+
const collection = shownCollection();
|
|
1283
|
+
// The language the view is filtered to is the one the reader is writing in.
|
|
1284
|
+
const { locale } = collectionView(collection.key);
|
|
1285
|
+
const args: CommandArgValues =
|
|
1286
|
+
collection.kind === "pages"
|
|
1287
|
+
? { kind: "page" }
|
|
1288
|
+
: { collection: collection.key, kind: "entry" };
|
|
1289
|
+
if (locale !== "") {
|
|
1290
|
+
args.locale = locale;
|
|
1291
|
+
}
|
|
1292
|
+
void runActiveReported("file.new", args, SOURCE);
|
|
1011
1293
|
},
|
|
1012
|
-
openNewMenu: (opener) => showLibraryNewMenu(opener),
|
|
1013
1294
|
previewSlot: (element, path) => {
|
|
1014
1295
|
if (panel) {
|
|
1015
1296
|
previewSlot(panel, element, path);
|
|
@@ -1017,7 +1298,6 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
1017
1298
|
},
|
|
1018
1299
|
reset: () => {
|
|
1019
1300
|
setLibrarySearch("");
|
|
1020
|
-
setLibraryCategory("all");
|
|
1021
1301
|
setLibraryLocale("");
|
|
1022
1302
|
},
|
|
1023
1303
|
retry: () => {
|
|
@@ -1030,26 +1310,16 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
1030
1310
|
}
|
|
1031
1311
|
},
|
|
1032
1312
|
search: (value) => setLibrarySearch(value),
|
|
1033
|
-
|
|
1034
|
-
/* SAFETY: the surface's layout buttons pass `$map.item.key`, and the `layouts` state they map is
|
|
1313
|
+
/* SAFETY: the surface's view buttons pass `$map.item.key`, and the `layouts` state they map is
|
|
1035
1314
|
built from LIBRARY_LAYOUTS in the projection above, so every key is a LibraryLayout. */
|
|
1036
1315
|
setLayout: (key) => setLibraryLayout(key as LibraryLayout),
|
|
1316
|
+
setFilter: (value) => setLibraryFilter(value === "all" ? "" : value),
|
|
1037
1317
|
setLocale: (value) => setLibraryLocale(value === "all" ? "" : value),
|
|
1038
|
-
upload: (input) => {
|
|
1039
|
-
const chosen = input.files;
|
|
1040
|
-
// Cleared before the upload starts: a file input that keeps its value refuses the same file
|
|
1041
|
-
// Twice, and the picker is this surface's own control rather than a node it found.
|
|
1042
|
-
input.value = "";
|
|
1043
|
-
if (chosen?.length) {
|
|
1044
|
-
void uploadIntoLibrary(chosen);
|
|
1045
|
-
}
|
|
1046
|
-
},
|
|
1047
1318
|
});
|
|
1048
1319
|
|
|
1049
1320
|
const scope = effectScope();
|
|
1050
1321
|
panel = {
|
|
1051
1322
|
cache: createPreviewCache(),
|
|
1052
|
-
dropActive: false,
|
|
1053
1323
|
observer,
|
|
1054
1324
|
paneId,
|
|
1055
1325
|
pending: new WeakMap(),
|
|
@@ -1060,6 +1330,17 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
1060
1330
|
slots: new Map(),
|
|
1061
1331
|
surface: mounted,
|
|
1062
1332
|
tabId: tab.id,
|
|
1333
|
+
unlisten: (() => {
|
|
1334
|
+
// The collection's page and its entries' titles are both read after the first paint.
|
|
1335
|
+
const stopPages = onCollectionPagesRead(() => bump());
|
|
1336
|
+
const stopEntries = onEntrySummariesRead(() => bump());
|
|
1337
|
+
const stopPageFacts = onPageSummariesRead(() => bump());
|
|
1338
|
+
return () => {
|
|
1339
|
+
stopPages();
|
|
1340
|
+
stopEntries();
|
|
1341
|
+
stopPageFacts();
|
|
1342
|
+
};
|
|
1343
|
+
})(),
|
|
1063
1344
|
wrap: canvasWrap,
|
|
1064
1345
|
};
|
|
1065
1346
|
_active.set(paneId, panel);
|
|
@@ -1070,18 +1351,29 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
1070
1351
|
return;
|
|
1071
1352
|
}
|
|
1072
1353
|
// Everything the pane draws from.
|
|
1073
|
-
void
|
|
1074
|
-
void
|
|
1075
|
-
void
|
|
1076
|
-
|
|
1077
|
-
void
|
|
1078
|
-
void
|
|
1354
|
+
void contentView.revision;
|
|
1355
|
+
void contentView.collection;
|
|
1356
|
+
void contentView.loading;
|
|
1357
|
+
const view = collectionView(shownCollectionKey());
|
|
1358
|
+
void view.layout;
|
|
1359
|
+
void view.query;
|
|
1360
|
+
void view.locale;
|
|
1079
1361
|
|
|
1080
1362
|
draw();
|
|
1081
1363
|
|
|
1082
|
-
if (!scanAttempted && !
|
|
1364
|
+
if (!scanAttempted && !contentView.loading) {
|
|
1083
1365
|
void loadLibrary();
|
|
1084
1366
|
}
|
|
1085
1367
|
});
|
|
1086
1368
|
});
|
|
1087
1369
|
}
|
|
1370
|
+
|
|
1371
|
+
/** Forget every collection's view and the scan — tests, and a project switch. */
|
|
1372
|
+
export function resetContentView(): void {
|
|
1373
|
+
contentView.collection = PAGES_COLLECTION;
|
|
1374
|
+
contentView.views = {};
|
|
1375
|
+
contentView.loading = false;
|
|
1376
|
+
source = null;
|
|
1377
|
+
scanAttempted = false;
|
|
1378
|
+
bump();
|
|
1379
|
+
}
|