@jxsuite/studio 6.1.1 → 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/{action-group-2mqby73c.js → action-group-4aan7g6g.js} +3 -3
- 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-6rbra5dy.js +4 -0
- package/dist/chunks/color-field-gy56c94g.js +4 -0
- package/dist/chunks/color-m7r1vk54.js +4 -0
- package/dist/chunks/color-slider-sqqdyf6c.js +4 -0
- package/dist/chunks/combobox-72h32ejk.js +4 -0
- 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/dialog-c4x05vyx.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-qg1b5eqx.js +51 -0
- package/dist/chunks/icons-63bjtbs7.js +4 -0
- package/dist/chunks/iframe-entry-2zggzajv.js +5 -0
- package/dist/chunks/iframe-entry-76j8vwwx.js +5 -0
- package/dist/chunks/iframe-entry-99nr5zfb.js +5 -0
- package/dist/chunks/iframe-entry-pkgmcgp0.js +5 -0
- package/dist/chunks/iframe-host-ssv7kyj0.js +4 -0
- package/dist/chunks/{index-2veqmnhr.js → index-32eqzv1k.js} +3 -3
- package/dist/chunks/index-f0jn7nnp.js +4 -0
- package/dist/chunks/{javascript-8nbvs12s.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-c421cf8c.js +4 -0
- 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-bdpzbfvs.js +4 -0
- package/dist/chunks/monaco-binding-vq1hgnn0.js +4 -0
- 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/number-field-y62xq0ty.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/select-rzds841e.js +4 -0
- package/dist/chunks/source-control-kwktr9y6.js +4 -0
- package/dist/chunks/split-c9zfdgxf.js +4 -0
- 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-6m380wp7.js +5 -0
- package/dist/chunks/studio-80jyh1xh.js +4 -0
- package/dist/chunks/studio-83mk4bgg.js +9 -0
- package/dist/chunks/{studio-kggey1jt.js → studio-881z1xrx.js} +4 -4
- package/dist/chunks/studio-8m0y2qe7.js +5 -0
- package/dist/chunks/studio-8tr6cfrc.js +5 -0
- 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-bbe4c90a.js +5 -0
- 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-8grwxtqk.js → studio-jcnsdaep.js} +4 -4
- package/dist/chunks/studio-jsgf2q5k.js +7 -0
- package/dist/chunks/{studio-1q1sh2ax.js → studio-k3xxbagr.js} +3 -3
- package/dist/chunks/studio-k7evxqh4.js +5 -0
- 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-rv11kfwa.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-h46vtzk8.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-wszjyh0j.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-pre0skq6.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-t6b6qe31.js → studio-r5qy30vx.js} +4 -4
- 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/swatch-group-ewh24n1f.js +4 -0
- package/dist/chunks/tabs-f5cw3tgp.js +4 -0
- package/dist/chunks/textfield-wewc8rq2.js +4 -0
- package/dist/chunks/toast-1gv0h53h.js +4 -0
- package/dist/chunks/toast-host-1yg5ar8d.js +4 -0
- package/dist/chunks/{toolbar-2nnzqhrj.js → toolbar-44cm5cta.js} +3 -3
- package/dist/chunks/tooltip-yypz28gq.js +4 -0
- package/dist/chunks/tree-9xa4hm7n.js +4 -0
- 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-bv552sfx.js +4 -0
- package/dist/iframe-entry.js +20 -16
- package/dist/manifest.json +77 -63
- package/dist/studio.js +106 -107
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +17 -16
- package/src/about/about-modal.ts +4 -3
- package/src/account/action-flow.ts +45 -38
- package/src/account-status.ts +7 -2
- package/src/browse/component-examples.ts +272 -0
- package/src/browse/library-commands.ts +64 -104
- package/src/browse/library-layouts.ts +270 -53
- package/src/browse/library-model.ts +194 -134
- package/src/browse/library-pane.ts +748 -440
- package/src/browse/library-preview.ts +161 -26
- 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 +40 -10
- package/src/canvas/asset-resolve.ts +19 -23
- package/src/canvas/canvas-helpers.ts +7 -6
- package/src/canvas/canvas-live-render.ts +260 -77
- package/src/canvas/canvas-origin.ts +37 -12
- package/src/canvas/canvas-page-context.ts +112 -0
- package/src/canvas/canvas-patcher.ts +30 -30
- package/src/canvas/canvas-perf.ts +11 -5
- package/src/canvas/canvas-render.ts +175 -148
- package/src/canvas/canvas-surface.ts +5 -3
- package/src/canvas/canvas-utils.ts +147 -61
- package/src/canvas/dialog-path.ts +9 -4
- package/src/canvas/dialog-state.ts +1 -2
- package/src/canvas/diff-marks.ts +9 -16
- package/src/canvas/diff-toolbar.ts +8 -7
- package/src/canvas/edit-width-drag.ts +4 -1
- package/src/canvas/edit-width.ts +1 -3
- package/src/canvas/iframe-channel.ts +27 -13
- package/src/canvas/iframe-drop.ts +120 -12
- package/src/canvas/iframe-editable-root.ts +29 -16
- package/src/canvas/iframe-entry.ts +104 -75
- package/src/canvas/iframe-eval.ts +73 -49
- package/src/canvas/iframe-host.ts +269 -158
- package/src/canvas/iframe-inline-edit.ts +49 -30
- package/src/canvas/iframe-insert.ts +5 -4
- package/src/canvas/iframe-interaction.ts +30 -25
- package/src/canvas/iframe-keys.ts +11 -0
- package/src/canvas/iframe-overlay.ts +1 -1
- package/src/canvas/iframe-patch.ts +25 -21
- package/src/canvas/iframe-position.ts +12 -6
- package/src/canvas/iframe-protocol.ts +36 -21
- package/src/canvas/iframe-render.ts +84 -35
- package/src/canvas/iframe-slash.ts +24 -11
- package/src/canvas/iframe-subtree.ts +10 -3
- package/src/canvas/npm-modules.ts +22 -0
- package/src/canvas/path-mapping.ts +19 -12
- package/src/canvas/popover-path.ts +7 -3
- package/src/canvas/popover-state.ts +1 -2
- package/src/canvas/preview-navigate.ts +16 -0
- package/src/canvas/repeater-sources.ts +200 -0
- package/src/canvas/serialize-scope.ts +13 -5
- package/src/canvas/stage-scrollbar.ts +7 -2
- package/src/canvas/surface-registry.ts +27 -3
- package/src/collab/collab-commands.ts +7 -2
- package/src/collab/collab-session.ts +84 -45
- package/src/collab/collab-state.ts +4 -3
- package/src/collab/monaco-binding.ts +16 -13
- package/src/collab/monaco-cursors.ts +5 -2
- package/src/commands/app-commands.ts +16 -6
- package/src/commands/budget.ts +13 -11
- package/src/commands/command-args.ts +103 -71
- package/src/commands/context.ts +55 -28
- package/src/commands/defaults.ts +69 -45
- package/src/commands/keymap.ts +56 -43
- package/src/commands/levels.ts +14 -8
- package/src/commands/live-context.ts +9 -7
- package/src/commands/reference.ts +2 -1
- package/src/commands/registry.ts +22 -11
- package/src/commands/run-reported.ts +15 -10
- package/src/component-props.ts +46 -30
- package/src/content/collection-match.ts +11 -9
- package/src/content/collection-pages.ts +412 -0
- package/src/content/content-collections.ts +270 -0
- package/src/content/draft-state.ts +4 -3
- package/src/content/entry-commands.ts +54 -62
- package/src/content/entry-editor.ts +137 -32
- package/src/content/entry-fields.ts +25 -8
- package/src/content/entry-inspector.ts +225 -0
- package/src/content/entry-model.ts +38 -15
- 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/canvas-context-menu.ts +1 -2
- package/src/editor/canvas-slash-bridge.ts +3 -6
- package/src/editor/content-slice.ts +16 -14
- package/src/editor/context-menu.ts +35 -22
- package/src/editor/convert-targets.ts +30 -35
- package/src/editor/convert-to-component.ts +78 -48
- package/src/editor/convert-to-repeater.ts +34 -22
- package/src/editor/editable-tags.ts +17 -2
- package/src/editor/file-drop-action.ts +29 -22
- package/src/editor/inline-edit-apply.ts +79 -17
- package/src/editor/inline-edit.ts +76 -50
- package/src/editor/inline-format.ts +59 -56
- package/src/editor/inline-link.ts +40 -15
- package/src/editor/insert-component.ts +91 -0
- package/src/editor/insert-zone-action.ts +14 -5
- package/src/editor/merge-tags.ts +58 -19
- package/src/editor/repeater-scope.ts +39 -28
- package/src/editor/shortcuts.ts +76 -42
- package/src/editor/slash-menu.ts +52 -14
- package/src/files/asset-versions.ts +4 -4
- package/src/files/components.ts +89 -20
- package/src/files/elements.ts +5 -6
- package/src/files/file-ops.ts +92 -41
- package/src/files/files.ts +248 -239
- package/src/files/fs-events.ts +18 -7
- package/src/files/media-meta.ts +97 -2
- package/src/files/media-paths.ts +3 -0
- 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 +15 -5
- package/src/format/constraints.ts +1 -1
- package/src/format/convert-file.ts +27 -13
- package/src/format/format-choices.ts +2 -1
- package/src/format/format-host.ts +91 -50
- package/src/github/github-auth-status.ts +27 -3
- package/src/github/github-auth.ts +21 -11
- package/src/github/github-publish.ts +28 -23
- package/src/grid/cell-editors.ts +46 -31
- package/src/grid/cell-popovers.ts +5 -3
- package/src/grid/edit-buffer.ts +10 -9
- package/src/grid/grid-controller.ts +25 -23
- package/src/grid/grid-layout.ts +21 -5
- package/src/grid/grid-open.ts +3 -33
- package/src/grid/grid-panel.ts +65 -6
- package/src/grid/grid-reveal.ts +34 -0
- package/src/grid/grid-source.ts +9 -31
- package/src/grid/grid-view.ts +45 -23
- package/src/grid/redirects-grid.ts +14 -5
- package/src/grid/redirects.ts +16 -14
- package/src/grid/schema-columns.ts +25 -19
- package/src/grid/sources/connector-source.ts +47 -20
- package/src/grid/sources/content-source.ts +100 -31
- package/src/grid/sources/csv-file-source.ts +2 -2
- package/src/grid/tabulator-tables.d.ts +61 -34
- package/src/hosting/document.ts +2 -2
- package/src/hosting/stage.ts +7 -7
- package/src/i18n/i18n-commands.ts +3 -3
- 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 +51 -20
- package/src/new-project/add-repo-modal.ts +20 -19
- package/src/new-project/import-tab.ts +10 -14
- package/src/new-project/location-fields.ts +2 -1
- package/src/new-project/new-project-modal.ts +12 -9
- package/src/packages/jxsuite-update.ts +4 -3
- package/src/packages/pull-package-sync.ts +26 -25
- package/src/page-params.ts +97 -68
- package/src/panels/activity-panel.ts +38 -14
- package/src/panels/ai-chat/attached-context.ts +13 -7
- package/src/panels/ai-chat/chat-markdown.ts +8 -8
- package/src/panels/ai-chat/chat-view.ts +80 -73
- package/src/panels/ai-chat/composer.ts +30 -33
- package/src/panels/ai-chat/sessions-view.ts +1 -2
- package/src/panels/ai-panel.ts +23 -20
- package/src/panels/block-action-bar.ts +57 -46
- package/src/panels/bottom-dock.ts +5 -3
- package/src/panels/canvas-dnd-bridge.ts +36 -26
- 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 +63 -26
- package/src/panels/data-grid.ts +18 -11
- package/src/panels/dnd.ts +115 -138
- package/src/panels/editors.ts +52 -21
- package/src/panels/elements-panel.ts +19 -5
- package/src/panels/empty-state.ts +2 -2
- package/src/panels/events-panel.ts +84 -69
- package/src/panels/formula-workspace.ts +78 -54
- package/src/panels/frontmatter-fields.ts +33 -28
- package/src/panels/git-diff-open.ts +10 -6
- package/src/panels/head-panel.ts +488 -202
- package/src/panels/i18n-panel.ts +20 -12
- package/src/panels/imports-panel.ts +25 -47
- package/src/panels/jump-bar.ts +75 -31
- package/src/panels/layers-panel.ts +130 -101
- package/src/panels/left-panel.ts +25 -11
- package/src/panels/navigator-panels.ts +23 -11
- package/src/panels/pane-context.ts +140 -49
- package/src/panels/pane-grid.ts +55 -26
- package/src/panels/panel-registry.ts +36 -46
- package/src/panels/properties-panel.ts +182 -124
- package/src/panels/quick-search.ts +53 -31
- package/src/panels/right-panel.ts +201 -45
- package/src/panels/seo-modal.ts +54 -125
- package/src/panels/settings-menu.ts +63 -61
- package/src/panels/settings-pane.ts +268 -162
- package/src/panels/signals-panel.ts +156 -135
- package/src/panels/site-model.ts +367 -0
- package/src/panels/site-panel.ts +638 -0
- package/src/panels/statement-editor.ts +230 -140
- package/src/panels/style-panel.ts +253 -206
- package/src/panels/style-utils.ts +114 -51
- package/src/panels/stylebook-doc.ts +195 -60
- package/src/panels/stylebook-layers-panel.ts +48 -89
- package/src/panels/stylebook-panel.ts +60 -27
- package/src/panels/styles-panel.ts +327 -0
- package/src/panels/tab-drop.ts +27 -11
- package/src/panels/tab-strip.ts +143 -49
- package/src/platform-errors.ts +57 -43
- package/src/platform.ts +59 -31
- package/src/platforms/cloud.ts +373 -176
- package/src/platforms/default-platform.ts +14 -8
- package/src/platforms/devserver.ts +180 -142
- package/src/platforms/read-batcher.ts +16 -8
- package/src/project-list.ts +2 -1
- package/src/publish/deploy-checklist.ts +20 -9
- package/src/publish/pages-service.ts +66 -27
- package/src/publish/publish-changes.ts +491 -0
- package/src/publish/publish-commands.ts +88 -22
- package/src/publish/publish-ids.ts +49 -0
- package/src/publish/publish-panel.ts +10 -6
- 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/recent-projects.ts +15 -8
- package/src/resize-edges.ts +26 -36
- package/src/services/a11y-report.ts +26 -19
- package/src/services/agent-seed.ts +23 -11
- package/src/services/ai-ask.ts +9 -12
- package/src/services/ai-command-tools.ts +68 -62
- package/src/services/ai-import-tools.ts +80 -67
- package/src/services/ai-models.ts +17 -13
- package/src/services/ai-project-tools.ts +68 -32
- package/src/services/ai-session-store.ts +19 -10
- package/src/services/ai-system-prompt.ts +46 -20
- package/src/services/ai-tools.ts +71 -27
- package/src/services/ai-write-report.ts +20 -12
- package/src/services/ai-writes.ts +4 -4
- package/src/services/automation.ts +74 -51
- package/src/services/cem-export.ts +149 -58
- package/src/services/cf-settings.ts +4 -4
- package/src/services/clock.ts +6 -1
- package/src/services/code-services.ts +37 -31
- package/src/services/connection.ts +2 -2
- package/src/services/context-manager.ts +14 -10
- package/src/services/context-resolver.ts +20 -8
- package/src/services/data-service.ts +19 -15
- package/src/services/directory-picker.ts +20 -14
- package/src/services/document-assistant.ts +39 -19
- package/src/services/gated-registry.ts +10 -6
- package/src/services/harness/chat-reducer.ts +23 -1
- package/src/services/import-client.ts +91 -38
- package/src/services/import-seed.ts +8 -2
- package/src/services/jx-validate.ts +28 -31
- package/src/services/live-preview.ts +0 -0
- package/src/services/live-surfaces.ts +6 -3
- package/src/services/model-uri.ts +10 -2
- package/src/services/module-preloads.ts +12 -8
- package/src/services/monaco-buffer.ts +43 -24
- package/src/services/monaco-lazy.ts +7 -8
- package/src/services/monaco-setup.ts +23 -16
- package/src/services/notify.ts +44 -17
- package/src/services/popover-report.ts +25 -10
- package/src/services/preview-eval.ts +20 -8
- package/src/services/profile.ts +13 -6
- package/src/services/project-path.ts +13 -10
- package/src/services/references.ts +0 -0
- package/src/services/render-critic.ts +29 -9
- package/src/services/settings/definitions.ts +25 -14
- package/src/services/settings/kernel.ts +13 -4
- package/src/services/settings/write-queue.ts +9 -9
- package/src/services/token-lint.ts +12 -10
- package/src/services/tool-executor.ts +17 -5
- package/src/services/tool-outcomes.ts +44 -24
- package/src/services/trusted-types.ts +24 -7
- package/src/settings/contexts-section.ts +11 -8
- package/src/settings/contributed-section.ts +128 -70
- package/src/settings/css-vars-editor.ts +119 -32
- package/src/settings/defs-editor.ts +38 -35
- package/src/settings/developer-panels.ts +43 -0
- package/src/settings/extension-rows.ts +16 -8
- package/src/settings/extension-sections.ts +42 -14
- package/src/settings/extensions-section.ts +4 -1
- package/src/settings/general-settings.ts +101 -28
- package/src/settings/locales-section.ts +20 -12
- package/src/settings/preferences-dialog.ts +17 -7
- package/src/settings/preferences-keymap.ts +7 -5
- package/src/settings/preferences-sections.ts +11 -5
- package/src/settings/project-sections.ts +28 -11
- package/src/settings/schema-field-ui.ts +12 -1
- package/src/settings/section-registry.ts +116 -11
- package/src/settings/settings-dialog.ts +192 -0
- package/src/settings/settings-document.ts +186 -166
- package/src/shell/tree.ts +6 -5
- package/src/shell.ts +536 -477
- package/src/site-context.ts +101 -32
- package/src/state.ts +68 -57
- package/src/store.ts +54 -34
- package/src/studio.ts +219 -116
- package/src/style/live-preview.ts +1 -1
- package/src/style/project-styles.ts +40 -7
- 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/token-ref.ts +5 -4
- package/src/style/variables-focus.ts +61 -0
- package/src/style/variables-view.ts +235 -0
- package/src/surfaces/about.ts +18 -19
- package/src/surfaces/add-new.json +1482 -0
- package/src/surfaces/add-new.ts +325 -0
- package/src/surfaces/add-repo.ts +17 -19
- package/src/surfaces/ai-chat.ts +33 -38
- package/src/surfaces/ai-credentials-form.ts +6 -16
- package/src/surfaces/ai-managed-connect.ts +3 -4
- package/src/surfaces/ai-model-picker.ts +3 -5
- package/src/surfaces/block-action-bar.ts +22 -11
- package/src/surfaces/boot-failure.json +4 -3
- package/src/surfaces/boot-failure.ts +4 -5
- package/src/surfaces/bottom-dock.json +1 -1
- package/src/surfaces/bottom-dock.ts +9 -18
- package/src/surfaces/canvas-stage.json +30 -19
- package/src/surfaces/canvas-stage.ts +17 -16
- package/src/surfaces/cf-account-picker.ts +17 -18
- 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 +145 -105
- package/src/surfaces/convert-repeater.ts +17 -19
- package/src/surfaces/data-actions.ts +9 -7
- package/src/surfaces/dialog.ts +26 -28
- package/src/surfaces/diff-toolbar.ts +9 -7
- package/src/surfaces/empty-state.json +1 -1
- package/src/surfaces/empty-state.ts +7 -10
- package/src/surfaces/entry-editor.json +136 -0
- package/src/surfaces/entry-editor.ts +35 -11
- package/src/surfaces/entry-inspector.json +371 -0
- package/src/surfaces/entry-inspector.ts +143 -0
- package/src/surfaces/expression-editor.ts +11 -25
- package/src/surfaces/files-panel.ts +27 -14
- package/src/surfaces/formula-palette.ts +4 -5
- package/src/surfaces/github-auth.ts +16 -20
- package/src/surfaces/github-publish.ts +17 -22
- package/src/surfaces/grid-cell.ts +30 -31
- package/src/surfaces/grid-open.ts +10 -14
- package/src/surfaces/grid-panel.ts +13 -9
- package/src/surfaces/grid-replace.ts +13 -13
- package/src/surfaces/grid-views.ts +15 -18
- package/src/surfaces/inspector-dock.json +162 -14
- package/src/surfaces/inspector-dock.ts +59 -31
- package/src/surfaces/jump-bar.ts +5 -6
- package/src/surfaces/jxsuite-update.ts +16 -22
- package/src/surfaces/library-pane.json +670 -437
- package/src/surfaces/library-pane.ts +111 -77
- package/src/surfaces/logic-panel.ts +17 -30
- package/src/surfaces/logic-workspace.ts +13 -15
- package/src/surfaces/media-browser.json +62 -6
- package/src/surfaces/media-browser.ts +26 -11
- package/src/surfaces/media-field.json +1 -1
- package/src/surfaces/media-field.ts +4 -5
- package/src/surfaces/media-library.json +1586 -0
- package/src/surfaces/media-library.ts +282 -0
- package/src/surfaces/media-pane.ts +8 -7
- package/src/surfaces/media-pick-dialog.json +71 -0
- package/src/surfaces/media-pick-dialog.ts +132 -0
- package/src/surfaces/menu.ts +47 -23
- package/src/surfaces/navigator-dock.ts +11 -12
- package/src/surfaces/new-project.ts +27 -26
- package/src/surfaces/pane-context.json +144 -77
- package/src/surfaces/pane-context.ts +44 -25
- package/src/surfaces/pane-grid.ts +15 -10
- package/src/surfaces/panel-activity.ts +5 -6
- package/src/surfaces/panel-data.ts +3 -4
- package/src/surfaces/panel-deploy-checklist.json +12 -6
- package/src/surfaces/panel-deploy-checklist.ts +8 -7
- package/src/surfaces/panel-elements.ts +5 -7
- package/src/surfaces/panel-i18n.ts +5 -7
- package/src/surfaces/panel-imports.ts +6 -13
- package/src/surfaces/panel-outline.ts +25 -19
- package/src/surfaces/panel-page.json +72 -2
- package/src/surfaces/panel-page.ts +49 -25
- package/src/surfaces/panel-problems.ts +5 -7
- package/src/surfaces/panel-signals.json +1 -1
- package/src/surfaces/panel-signals.ts +25 -39
- package/src/surfaces/panel-stylebook-layers.json +71 -204
- package/src/surfaces/panel-stylebook-layers.ts +5 -29
- package/src/surfaces/preferences.json +120 -48
- package/src/surfaces/preferences.ts +29 -11
- package/src/surfaces/progress-modal.ts +15 -9
- package/src/surfaces/properties-panel.json +1 -1
- package/src/surfaces/properties-panel.ts +20 -42
- 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/publish.ts +18 -18
- package/src/surfaces/push-plan.ts +7 -8
- package/src/surfaces/rail.json +1 -159
- package/src/surfaces/rail.ts +22 -79
- package/src/surfaces/reference-field.ts +11 -15
- package/src/surfaces/schema-builder.ts +16 -22
- package/src/surfaces/schema-form.json +10 -2
- package/src/surfaces/schema-form.ts +17 -25
- package/src/surfaces/secret-field.ts +9 -6
- package/src/surfaces/seo.json +1 -1
- package/src/surfaces/seo.ts +21 -15
- package/src/surfaces/settings-contexts.ts +9 -7
- package/src/surfaces/settings-contributed.json +5 -5
- package/src/surfaces/settings-contributed.ts +32 -12
- package/src/surfaces/settings-css-vars.json +44 -36
- package/src/surfaces/settings-css-vars.ts +82 -16
- package/src/surfaces/settings-defs.json +2 -2
- package/src/surfaces/settings-defs.ts +17 -21
- package/src/surfaces/settings-deploy.json +2 -15
- package/src/surfaces/settings-deploy.ts +9 -16
- package/src/surfaces/settings-dialog.json +51 -0
- package/src/surfaces/settings-dialog.ts +169 -0
- package/src/surfaces/settings-extensions.ts +5 -6
- package/src/surfaces/settings-head.ts +33 -39
- package/src/surfaces/settings-locales.ts +8 -20
- package/src/surfaces/settings-overview.json +3 -3
- package/src/surfaces/settings-overview.ts +14 -8
- package/src/surfaces/settings-packages.ts +8 -7
- package/src/surfaces/settings-pane.json +269 -46
- package/src/surfaces/settings-pane.ts +88 -36
- package/src/surfaces/settings-rawjson.ts +5 -6
- package/src/surfaces/shell.json +2 -1
- package/src/surfaces/shell.ts +1 -2
- 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 +42 -9
- package/src/surfaces/statements.ts +13 -18
- package/src/surfaces/statusbar.ts +56 -29
- package/src/surfaces/style-panel.ts +18 -31
- package/src/surfaces/stylebook-chrome.ts +3 -5
- 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/tab-strip.ts +14 -13
- package/src/surfaces/target-line.ts +6 -7
- package/src/surfaces/welcome.ts +6 -8
- 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/patch-ops.ts +4 -3
- package/src/tabs/project-config.ts +32 -21
- package/src/tabs/selection.ts +36 -8
- package/src/tabs/tab.ts +99 -48
- package/src/tabs/transact.ts +98 -53
- package/src/types.ts +53 -43
- package/src/ui/ai-credentials-form.ts +10 -13
- package/src/ui/ai-model-picker.ts +3 -3
- package/src/ui/cf-account-picker.ts +7 -9
- package/src/ui/color-selector.ts +9 -5
- package/src/ui/dynamic-slot.ts +7 -8
- package/src/ui/expression-editor.ts +112 -105
- package/src/ui/form-controls.ts +104 -56
- package/src/ui/formula-catalog.ts +239 -230
- package/src/ui/formula-chips.ts +33 -18
- package/src/ui/layers.ts +57 -31
- package/src/ui/media-picker.ts +300 -47
- package/src/ui/popover-surface.ts +20 -12
- package/src/ui/progress-modal.ts +13 -8
- package/src/ui/regions.ts +14 -12
- package/src/ui/schema-form.ts +372 -97
- package/src/ui/surface.ts +71 -8
- package/src/ui/timing.ts +9 -0
- package/src/ui/value-source.ts +44 -31
- package/src/ui/virtual-window.ts +29 -3
- package/src/utils/base64.ts +6 -4
- package/src/utils/edit-display.ts +366 -111
- package/src/utils/element-selectors.ts +3 -2
- package/src/utils/google-fonts.ts +15 -14
- package/src/utils/inherited-style.ts +18 -20
- package/src/utils/link-target.ts +1 -1
- package/src/utils/local-day.ts +18 -0
- package/src/utils/preview-format.ts +25 -3
- package/src/utils/strip-events.ts +49 -22
- package/src/utils/studio-utils.ts +29 -14
- package/src/view.ts +2 -3
- package/src/workspace/pane-derive.ts +14 -16
- package/src/workspace/session.ts +81 -50
- 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 +165 -47
- package/styles/shell-frame.css +30 -7
- package/styles/shell-frame.json +15 -3
- package/dist/chunks/2020-va2wwra4.js +0 -4
- package/dist/chunks/adapter-f93pafd7.js +0 -4
- package/dist/chunks/cf-account-picker-af1h381v.js +0 -4
- package/dist/chunks/cf-settings-125tdm0w.js +0 -4
- package/dist/chunks/color-area-jqhna8zy.js +0 -4
- package/dist/chunks/color-field-embq5ctg.js +0 -4
- package/dist/chunks/color-fpezvnkt.js +0 -4
- package/dist/chunks/color-slider-1qmw7wqy.js +0 -4
- package/dist/chunks/combobox-773hdzyj.js +0 -4
- package/dist/chunks/components-ax2f1fnn.js +0 -4
- package/dist/chunks/content-source-108aqtk1.js +0 -4
- package/dist/chunks/convert-to-repeater-ry5rmppr.js +0 -4
- package/dist/chunks/dialog-mtm9fq5t.js +0 -4
- package/dist/chunks/editor-pfgdmkb0.js +0 -4
- package/dist/chunks/entry-commands-t0k1q0kx.js +0 -4
- package/dist/chunks/extension-sections-6mwh2hf8.js +0 -4
- package/dist/chunks/file-ops-meej5mqf.js +0 -4
- package/dist/chunks/files-44vp8a2s.js +0 -4
- package/dist/chunks/format-host-7q1petb9.js +0 -4
- package/dist/chunks/fs-events-h2mwnxaz.js +0 -4
- package/dist/chunks/git-panel-zngbymvd.js +0 -4
- package/dist/chunks/grid-view-sx9vynsp.js +0 -51
- package/dist/chunks/icons-p44xhjf0.js +0 -4
- package/dist/chunks/iframe-entry-f7mqjwpm.js +0 -5
- package/dist/chunks/iframe-entry-haw7jkvz.js +0 -5
- package/dist/chunks/iframe-entry-z6qrytv3.js +0 -5
- package/dist/chunks/iframe-host-qk3ycq87.js +0 -4
- package/dist/chunks/index-wkyfjeqz.js +0 -4
- package/dist/chunks/jsonMode-s8kt9dsj.js +0 -13
- package/dist/chunks/jx-validate-psgd7vsx.js +0 -4
- package/dist/chunks/layers-panel-1kq2jyfk.js +0 -4
- package/dist/chunks/layers-xd92fg8d.js +0 -4
- package/dist/chunks/listbox-s8fkmnq8.js +0 -4
- package/dist/chunks/lit-html-ac4cwwr4.js +0 -4
- package/dist/chunks/media-picker-0svzkdyb.js +0 -4
- package/dist/chunks/menu-37d07z9m.js +0 -4
- package/dist/chunks/menu-5c55ea3d.js +0 -4
- package/dist/chunks/monaco-binding-z6r70pdt.js +0 -4
- package/dist/chunks/monaco-buffer-fz6gq5y7.js +0 -4
- package/dist/chunks/monaco-setup-q4axkgj7.js +0 -4
- package/dist/chunks/notify-0xgtn8q1.js +0 -4
- package/dist/chunks/number-field-gw7sq8rn.js +0 -4
- package/dist/chunks/parse-897fvqah.js +0 -4
- package/dist/chunks/platform-v1qctbe6.js +0 -4
- package/dist/chunks/popover-8htra4er.js +0 -4
- package/dist/chunks/publish-panel-ym1fmgq5.js +0 -4
- package/dist/chunks/quick-search-a54e6t5n.js +0 -4
- package/dist/chunks/references-gmqp82fs.js +0 -4
- package/dist/chunks/right-panel-v7t9awbk.js +0 -4
- package/dist/chunks/section-registry-vhyf3zsr.js +0 -4
- package/dist/chunks/select-n3v7gwas.js +0 -4
- package/dist/chunks/split-pff8f3ze.js +0 -4
- package/dist/chunks/studio-0bhx5hz6.js +0 -5
- package/dist/chunks/studio-0k1ddnyy.js +0 -6
- package/dist/chunks/studio-1b04pd0j.js +0 -64
- package/dist/chunks/studio-4dwbj6gb.js +0 -5
- package/dist/chunks/studio-4tanh828.js +0 -5
- package/dist/chunks/studio-55h4mstj.js +0 -4
- package/dist/chunks/studio-5tnvjkz4.js +0 -5
- package/dist/chunks/studio-60dqgb0c.js +0 -5
- package/dist/chunks/studio-61dp09qg.js +0 -9
- package/dist/chunks/studio-63epcrj2.js +0 -5
- package/dist/chunks/studio-74k29dy9.js +0 -5
- package/dist/chunks/studio-7vg6jcd3.js +0 -5
- package/dist/chunks/studio-7zx31knw.js +0 -9
- package/dist/chunks/studio-8bxk97km.js +0 -5
- package/dist/chunks/studio-933e30n4.js +0 -5
- package/dist/chunks/studio-98wnwhbg.js +0 -5
- package/dist/chunks/studio-9mccjwjh.js +0 -14
- package/dist/chunks/studio-a2ke1e4t.js +0 -5
- package/dist/chunks/studio-e3a2a6v3.js +0 -4
- package/dist/chunks/studio-e81q1vb9.js +0 -5
- package/dist/chunks/studio-epgrmqqm.js +0 -5
- package/dist/chunks/studio-ery42sqr.js +0 -4
- package/dist/chunks/studio-g1shz58g.js +0 -5
- package/dist/chunks/studio-gf0fz03c.js +0 -5
- package/dist/chunks/studio-gjtpe62q.js +0 -6
- package/dist/chunks/studio-gkskhg7m.js +0 -5
- package/dist/chunks/studio-gx4dnjzd.js +0 -5
- package/dist/chunks/studio-j03zga3y.js +0 -9
- package/dist/chunks/studio-jhpxeckz.js +0 -5
- package/dist/chunks/studio-k01r7ykp.js +0 -5
- package/dist/chunks/studio-kk7h30ne.js +0 -7
- package/dist/chunks/studio-kt1zqney.js +0 -5
- package/dist/chunks/studio-m3cwhvye.js +0 -5
- package/dist/chunks/studio-mcayt6hz.js +0 -5
- package/dist/chunks/studio-myjcghv3.js +0 -5
- package/dist/chunks/studio-n8bxwc3y.js +0 -4
- package/dist/chunks/studio-ngm8gvpb.js +0 -5
- package/dist/chunks/studio-nj5a01jy.js +0 -928
- package/dist/chunks/studio-pcgz8heb.js +0 -5
- package/dist/chunks/studio-qcsq0t15.js +0 -5
- package/dist/chunks/studio-qkstvve9.js +0 -5
- package/dist/chunks/studio-qv1pwd3c.js +0 -5
- package/dist/chunks/studio-rc6dvxjb.js +0 -7
- package/dist/chunks/studio-s8dmmz66.js +0 -5
- package/dist/chunks/studio-sp5xzbj4.js +0 -6
- package/dist/chunks/studio-szsjbj0g.js +0 -5
- package/dist/chunks/studio-t1xzawhr.js +0 -16
- package/dist/chunks/studio-tdcprbqb.js +0 -5
- package/dist/chunks/studio-vrmcjf9g.js +0 -7
- package/dist/chunks/studio-vyc0b4he.js +0 -10
- package/dist/chunks/studio-w0cgf6jg.js +0 -5
- package/dist/chunks/studio-w3hskzqk.js +0 -5
- package/dist/chunks/studio-xgrgcdyh.js +0 -9
- package/dist/chunks/studio-xz2yxfcb.js +0 -5
- package/dist/chunks/studio-y6n42tgs.js +0 -5
- package/dist/chunks/studio-yb560vxr.js +0 -5
- package/dist/chunks/studio-yn9f8vtx.js +0 -5
- package/dist/chunks/studio-yw05w8f0.js +0 -7
- package/dist/chunks/studio-z16sgavc.js +0 -5
- package/dist/chunks/studio-zey7zsdw.js +0 -12
- package/dist/chunks/studio-zyqn40w4.js +0 -5
- package/dist/chunks/swatch-group-8qh9a3bm.js +0 -4
- package/dist/chunks/tabs-jwthg40w.js +0 -4
- package/dist/chunks/textfield-spy3hc77.js +0 -4
- package/dist/chunks/toast-4d35zd64.js +0 -4
- package/dist/chunks/toast-host-8sjmfed3.js +0 -4
- package/dist/chunks/tooltip-fd6g9xy3.js +0 -4
- package/dist/chunks/tree-h19v1jfz.js +0 -4
- package/dist/chunks/tsMode-2frah6h1.js +0 -14
- package/dist/chunks/workspace-zt81g1ms.js +0 -4
- package/dist/chunks/ws-client-n2bh4hkf.js +0 -4
- package/src/panels/frontmatter-panel.ts +0 -701
- package/src/panels/git-panel.ts +0 -1120
- package/src/surfaces/doc-header.json +0 -776
- package/src/surfaces/doc-header.ts +0 -231
- package/src/surfaces/git-panel.json +0 -1880
- package/src/surfaces/git-panel.ts +0 -272
|
@@ -1,75 +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";
|
|
41
|
+
import type { PromptDialogOptions } from "../ui/layers";
|
|
38
42
|
import { openMenu } from "../surfaces/menu";
|
|
39
|
-
import { rectOf } from "../utils/geometry";
|
|
40
43
|
import { getPlatform } from "../platform";
|
|
44
|
+
import { projectState, renderOnly } from "../store";
|
|
41
45
|
import { invalidateUsages } from "../services/references";
|
|
42
46
|
import { confirmFileDelete, renamePromptMessage } from "../files/file-ops";
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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";
|
|
49
78
|
import {
|
|
50
|
-
LIBRARY_CATEGORIES,
|
|
51
79
|
LIBRARY_LAYOUTS,
|
|
52
80
|
LIBRARY_LAYOUT_LABELS,
|
|
53
81
|
PREVIEW_LAYOUTS,
|
|
54
82
|
filterLibrary,
|
|
55
|
-
libraryCategory,
|
|
56
83
|
libraryLocales,
|
|
57
|
-
|
|
84
|
+
projectLocales,
|
|
58
85
|
} from "./library-model";
|
|
59
|
-
import { createLibrarySource, libraryColumns } from "./library-source";
|
|
86
|
+
import { createLibrarySource, entryFieldColumns, libraryColumns } from "./library-source";
|
|
60
87
|
import { createPreviewCache, createPreviewObserver, previewFor } from "./library-preview";
|
|
88
|
+
import { mountPreview, shownPreview } from "./preview-root";
|
|
61
89
|
import { computeWindow } from "../ui/virtual-window";
|
|
62
90
|
import { paneRegion } from "../ui/regions";
|
|
63
|
-
import {
|
|
91
|
+
import {
|
|
92
|
+
LAYOUT_METRICS,
|
|
93
|
+
boardFieldOf,
|
|
94
|
+
boardView,
|
|
95
|
+
calendarView,
|
|
96
|
+
columnsAt,
|
|
97
|
+
entryFactsFrom,
|
|
98
|
+
fieldValues,
|
|
99
|
+
libraryRow,
|
|
100
|
+
} from "./library-layouts";
|
|
64
101
|
import { mountLibrarySurface } from "../surfaces/library-pane";
|
|
65
102
|
import type { ActivityHandle } from "../panels/activity-panel";
|
|
66
|
-
import type {
|
|
103
|
+
import type { ContentCollection } from "../content/content-collections";
|
|
104
|
+
import type { CollectionPage } from "../content/collection-pages";
|
|
67
105
|
import type { EffectScope } from "@vue/reactivity";
|
|
68
106
|
import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
|
|
69
107
|
import type { LibraryFile, LibraryLayout } from "./library-model";
|
|
108
|
+
import type { BoardField, EntryFacts, EntryFactsOf } from "./library-layouts";
|
|
70
109
|
import type { LibrarySource } from "./library-source";
|
|
110
|
+
import type { CommandArgValues } from "../commands/command-args";
|
|
71
111
|
import type {
|
|
72
112
|
LibraryChip,
|
|
113
|
+
LibraryPageLine,
|
|
73
114
|
LibrarySurfaceHandle,
|
|
74
115
|
LibraryView,
|
|
75
116
|
LibraryViewKind,
|
|
@@ -80,8 +121,8 @@ import type { CanvasSurface } from "../canvas/canvas-surface";
|
|
|
80
121
|
|
|
81
122
|
// ─── View state ──────────────────────────────────────────────────────────────
|
|
82
123
|
|
|
83
|
-
|
|
84
|
-
|
|
124
|
+
/** One collection's own view: how it is arranged, what it is filtered to. */
|
|
125
|
+
export interface CollectionViewState {
|
|
85
126
|
layout: LibraryLayout;
|
|
86
127
|
query: string;
|
|
87
128
|
/**
|
|
@@ -91,30 +132,86 @@ export interface LibraryViewState {
|
|
|
91
132
|
* matches what {@link LibraryFile.locale} carries.
|
|
92
133
|
*/
|
|
93
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>;
|
|
94
148
|
/** Bumped to force a repaint from a non-reactive source (a scroll, a finished scan). */
|
|
95
149
|
revision: number;
|
|
96
150
|
/** A scan is running. Distinct from "scanned and empty", which is what the old view conflated. */
|
|
97
151
|
loading: boolean;
|
|
98
152
|
}
|
|
99
153
|
|
|
154
|
+
/** The view a collection starts in: Table, the shape both Content mockups open on. */
|
|
155
|
+
const DEFAULT_LAYOUT: LibraryLayout = "table";
|
|
156
|
+
|
|
100
157
|
/**
|
|
101
|
-
* The
|
|
158
|
+
* The Content home's state, reactive and module-scoped.
|
|
102
159
|
*
|
|
103
|
-
* Module-scoped rather than per-tab because there is one
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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.
|
|
107
164
|
*/
|
|
108
|
-
export const
|
|
109
|
-
|
|
110
|
-
layout: "cards",
|
|
165
|
+
export const contentView: ContentViewState = reactive({
|
|
166
|
+
collection: PAGES_COLLECTION,
|
|
111
167
|
loading: false,
|
|
112
|
-
locale: "",
|
|
113
|
-
query: "",
|
|
114
168
|
revision: 0,
|
|
169
|
+
views: {},
|
|
115
170
|
});
|
|
116
171
|
|
|
117
|
-
/** 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}. */
|
|
118
215
|
let source: LibrarySource | null = null;
|
|
119
216
|
|
|
120
217
|
/**
|
|
@@ -129,33 +226,54 @@ let scanAttempted = false;
|
|
|
129
226
|
|
|
130
227
|
/** The source, creating it on first use. */
|
|
131
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
|
+
}
|
|
132
241
|
source ??= createLibrarySource();
|
|
133
242
|
return source;
|
|
134
243
|
}
|
|
135
244
|
|
|
136
|
-
/** Repaint whatever is mounted. */
|
|
245
|
+
/** Repaint whatever is mounted, the Collections panel's counts included. */
|
|
137
246
|
function bump() {
|
|
138
|
-
|
|
247
|
+
contentView.revision += 1;
|
|
139
248
|
}
|
|
140
249
|
|
|
141
|
-
|
|
142
|
-
|
|
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);
|
|
143
254
|
bump();
|
|
255
|
+
renderOnly("leftPanel");
|
|
144
256
|
}
|
|
145
257
|
|
|
146
|
-
export function setLibraryLayout(layout: LibraryLayout): void {
|
|
147
|
-
|
|
258
|
+
export function setLibraryLayout(layout: LibraryLayout, key: string = shownCollectionKey()): void {
|
|
259
|
+
collectionView(key).layout = layout;
|
|
148
260
|
bump();
|
|
149
261
|
}
|
|
150
262
|
|
|
151
|
-
export function setLibrarySearch(query: string): void {
|
|
152
|
-
|
|
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;
|
|
153
271
|
bump();
|
|
154
272
|
}
|
|
155
273
|
|
|
156
274
|
/** Show one language, or `""` for all of them. A setter, so "" is a value and not an absence. */
|
|
157
|
-
export function setLibraryLocale(locale: string): void {
|
|
158
|
-
|
|
275
|
+
export function setLibraryLocale(locale: string, key: string = shownCollectionKey()): void {
|
|
276
|
+
collectionView(key).locale = locale;
|
|
159
277
|
bump();
|
|
160
278
|
}
|
|
161
279
|
|
|
@@ -163,7 +281,7 @@ export function setLibraryLocale(locale: string): void {
|
|
|
163
281
|
* Drop the scan and the rendered previews.
|
|
164
282
|
*
|
|
165
283
|
* Called by every surface that changes the project's file set — an upload, a creation, a rename, a
|
|
166
|
-
* delete — so the
|
|
284
|
+
* delete — so the home is never showing a file that is gone.
|
|
167
285
|
*
|
|
168
286
|
* The SLOT table survives, and that is the difference between this and a teardown: clearing the
|
|
169
287
|
* cache detaches every rendered preview from the box it was in, and those boxes are still on
|
|
@@ -179,13 +297,45 @@ export function invalidateLibrary(): void {
|
|
|
179
297
|
bump();
|
|
180
298
|
}
|
|
181
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
|
+
|
|
182
324
|
// ─── Loading ─────────────────────────────────────────────────────────────────
|
|
183
325
|
|
|
184
326
|
/** A scan slower than this earns an Activity row; a fast one must not litter the dock. */
|
|
185
327
|
export const SCAN_ACTIVITY_DELAY_MS = 600;
|
|
186
328
|
|
|
187
329
|
/** Deduplication key for the scan Problem, so a failing watcher is one row and not sixty. */
|
|
188
|
-
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
|
+
}
|
|
189
339
|
|
|
190
340
|
/**
|
|
191
341
|
* Run the scan and report it honestly.
|
|
@@ -198,13 +348,13 @@ const SCAN_PROBLEM_KEY = "library.scan";
|
|
|
198
348
|
async function loadLibrary(): Promise<void> {
|
|
199
349
|
const current = librarySource();
|
|
200
350
|
scanAttempted = true;
|
|
201
|
-
|
|
351
|
+
contentView.loading = true;
|
|
202
352
|
bump();
|
|
203
353
|
|
|
204
|
-
const slow:
|
|
354
|
+
const slow: SlowScanActivity = { handle: null };
|
|
205
355
|
const timer = setTimeout(() => {
|
|
206
356
|
slow.handle = beginActivity({
|
|
207
|
-
source:
|
|
357
|
+
source: SOURCE,
|
|
208
358
|
status: "Reading the project's directories…",
|
|
209
359
|
title: "Scan project files",
|
|
210
360
|
});
|
|
@@ -221,11 +371,11 @@ async function loadLibrary(): Promise<void> {
|
|
|
221
371
|
// A Problem, not a toast: the list on screen is wrong until this is fixed, and the fix is a
|
|
222
372
|
// Command the row can run.
|
|
223
373
|
notify.error(summary, {
|
|
224
|
-
action: "
|
|
374
|
+
action: "content.refresh",
|
|
225
375
|
detail,
|
|
226
376
|
key: SCAN_PROBLEM_KEY,
|
|
227
377
|
path: failures[0]!.dir,
|
|
228
|
-
source:
|
|
378
|
+
source: SOURCE,
|
|
229
379
|
tier: "problem",
|
|
230
380
|
});
|
|
231
381
|
slow.handle?.setStatus(summary);
|
|
@@ -238,21 +388,23 @@ async function loadLibrary(): Promise<void> {
|
|
|
238
388
|
slow.handle.fail(message);
|
|
239
389
|
} else {
|
|
240
390
|
notify.error(message, {
|
|
241
|
-
action: "
|
|
391
|
+
action: "content.refresh",
|
|
242
392
|
detail: errorMessage(error),
|
|
243
393
|
key: SCAN_PROBLEM_KEY,
|
|
244
|
-
source:
|
|
394
|
+
source: SOURCE,
|
|
245
395
|
tier: "problem",
|
|
246
396
|
});
|
|
247
397
|
}
|
|
248
398
|
} finally {
|
|
249
399
|
clearTimeout(timer);
|
|
250
|
-
|
|
400
|
+
contentView.loading = false;
|
|
251
401
|
bump();
|
|
402
|
+
// The Collections panel draws the counts this scan just produced.
|
|
403
|
+
renderOnly("leftPanel");
|
|
252
404
|
}
|
|
253
405
|
}
|
|
254
406
|
|
|
255
|
-
/** Re-scan from scratch — the Retry behind the Problem
|
|
407
|
+
/** Re-scan from scratch — the Retry behind the Problem. */
|
|
256
408
|
export async function refreshLibrary(): Promise<void> {
|
|
257
409
|
source = null;
|
|
258
410
|
scanAttempted = false;
|
|
@@ -265,7 +417,7 @@ export async function refreshLibrary(): Promise<void> {
|
|
|
265
417
|
// ─── The mounted pane ────────────────────────────────────────────────────────
|
|
266
418
|
|
|
267
419
|
interface ActiveLibraryPane {
|
|
268
|
-
/** The pane whose stage this
|
|
420
|
+
/** The pane whose stage this home is drawn on. The map key, held on the record too. */
|
|
269
421
|
paneId: string;
|
|
270
422
|
tabId: string;
|
|
271
423
|
scope: EffectScope;
|
|
@@ -287,30 +439,26 @@ interface ActiveLibraryPane {
|
|
|
287
439
|
* and a scroll through a long list would read each document several times over.
|
|
288
440
|
*/
|
|
289
441
|
pendingPaths: Set<string>;
|
|
290
|
-
/** Whether a drag is over the body. Held here so a dragover does not recompute the window. */
|
|
291
|
-
dropActive: boolean;
|
|
292
442
|
/** The coalesced post-repaint pass; see {@link scheduleSettle}. */
|
|
293
443
|
settleTimer: ReturnType<typeof setTimeout> | null;
|
|
444
|
+
/** Stops listening for the collection-pages index. */
|
|
445
|
+
unlisten: () => void;
|
|
294
446
|
}
|
|
295
447
|
|
|
296
448
|
/**
|
|
297
|
-
* The
|
|
449
|
+
* The home mounted in each pane, keyed by pane id.
|
|
298
450
|
*
|
|
299
|
-
* ONE instance per pane, not per window
|
|
300
|
-
*
|
|
301
|
-
* cosmetic: pane B mounting a Library called `detachLibraryPane()`, destroying pane A's
|
|
302
|
-
* `IntersectionObserver`, preview cache and effect scope while pane A's DOM was still on screen.
|
|
303
|
-
* `canvas-render.ts`'s `resetCanvasView` calls the detach UNCONDITIONALLY on every empty pane, so
|
|
304
|
-
* 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.
|
|
305
453
|
*/
|
|
306
454
|
const _active = new Map<string, ActiveLibraryPane>();
|
|
307
455
|
|
|
308
|
-
/** The
|
|
456
|
+
/** The home mounted in a pane, or null. */
|
|
309
457
|
function activeIn(paneId: string): ActiveLibraryPane | null {
|
|
310
458
|
return _active.get(paneId) ?? null;
|
|
311
459
|
}
|
|
312
460
|
|
|
313
|
-
/** Whether the
|
|
461
|
+
/** Whether the home is live in this pane for this tab — the canvas-render fast-path guard. */
|
|
314
462
|
export function libraryPaneMounted(paneId: string, tab: Tab): boolean {
|
|
315
463
|
const panel = activeIn(paneId);
|
|
316
464
|
return (
|
|
@@ -318,7 +466,7 @@ export function libraryPaneMounted(paneId: string, tab: Tab): boolean {
|
|
|
318
466
|
);
|
|
319
467
|
}
|
|
320
468
|
|
|
321
|
-
/** Tear one pane's
|
|
469
|
+
/** Tear one pane's home down (mode change, tab switch, project close). Idempotent. */
|
|
322
470
|
export function detachLibraryPane(paneId: string): void {
|
|
323
471
|
const panel = _active.get(paneId);
|
|
324
472
|
if (!panel) {
|
|
@@ -328,6 +476,7 @@ export function detachLibraryPane(paneId: string): void {
|
|
|
328
476
|
clearTimeout(panel.settleTimer);
|
|
329
477
|
panel.settleTimer = null;
|
|
330
478
|
}
|
|
479
|
+
panel.unlisten();
|
|
331
480
|
panel.observer.destroy();
|
|
332
481
|
panel.cache.clear();
|
|
333
482
|
panel.slots.clear();
|
|
@@ -371,8 +520,8 @@ async function fillPreview(panel: ActiveLibraryPane, path: string) {
|
|
|
371
520
|
return;
|
|
372
521
|
}
|
|
373
522
|
const slot = panel.slots.get(path);
|
|
374
|
-
if (slot?.isConnected && !slot
|
|
375
|
-
slot
|
|
523
|
+
if (slot?.isConnected && !shownPreview(slot)) {
|
|
524
|
+
mountPreview(slot, rendered);
|
|
376
525
|
}
|
|
377
526
|
} finally {
|
|
378
527
|
panel.pendingPaths.delete(path);
|
|
@@ -386,11 +535,11 @@ function tendSlot(panel: ActiveLibraryPane, slot: HTMLElement, path: string): vo
|
|
|
386
535
|
// Drift to the tail of the LRU, get evicted, get detached, and be re-rendered on the next
|
|
387
536
|
// Repaint: the 300-page measurement showed 560 reads for 300 documents until this line moved.
|
|
388
537
|
const cached = panel.cache.get(path);
|
|
389
|
-
if (slot
|
|
538
|
+
if (shownPreview(slot)) {
|
|
390
539
|
return;
|
|
391
540
|
}
|
|
392
541
|
if (cached) {
|
|
393
|
-
slot
|
|
542
|
+
mountPreview(slot, cached);
|
|
394
543
|
return;
|
|
395
544
|
}
|
|
396
545
|
if (panel.pendingPaths.has(path)) {
|
|
@@ -414,20 +563,11 @@ function previewSlot(panel: ActiveLibraryPane, slot: HTMLElement, path: string):
|
|
|
414
563
|
/**
|
|
415
564
|
* The pass that runs once the document has reconciled, coalesced onto a task of its own.
|
|
416
565
|
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
* The first is releasing observations. A card outside the new window is now detached, and an
|
|
423
|
-
* observation of a detached node can never fire again: it is pure cost — the browser walks it on
|
|
424
|
-
* every scroll frame — and it pins the card's whole subtree. The intersect callback cannot be the
|
|
425
|
-
* only release path, because a card flicked past before it ever intersected is never reported.
|
|
426
|
-
*
|
|
427
|
-
* The second is tending the slots that ARE on screen. A row the reconciler kept keeps its preview
|
|
428
|
-
* box, so nothing re-announces it — and an LRU eviction detaches the render that was inside it. So
|
|
429
|
-
* every visible slot is touched here: the touch is what keeps it at the head of the LRU, and the
|
|
430
|
-
* 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.
|
|
431
571
|
*/
|
|
432
572
|
function scheduleSettle(panel: ActiveLibraryPane): void {
|
|
433
573
|
if (panel.settleTimer !== null) {
|
|
@@ -439,7 +579,7 @@ function scheduleSettle(panel: ActiveLibraryPane): void {
|
|
|
439
579
|
return;
|
|
440
580
|
}
|
|
441
581
|
panel.observer.releaseDetached();
|
|
442
|
-
const previews = PREVIEW_LAYOUTS.has(
|
|
582
|
+
const previews = PREVIEW_LAYOUTS.has(collectionView().layout);
|
|
443
583
|
for (const [path, slot] of panel.slots) {
|
|
444
584
|
if (!slot.isConnected) {
|
|
445
585
|
panel.slots.delete(path);
|
|
@@ -452,14 +592,34 @@ function scheduleSettle(panel: ActiveLibraryPane): void {
|
|
|
452
592
|
}, 0);
|
|
453
593
|
}
|
|
454
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
|
+
|
|
455
615
|
// ─── Context menu ────────────────────────────────────────────────────────────
|
|
456
616
|
|
|
457
617
|
/**
|
|
458
|
-
* The menu the
|
|
618
|
+
* The per-file menu the home has up, if any.
|
|
459
619
|
*
|
|
460
|
-
* One handle
|
|
461
|
-
*
|
|
462
|
-
*
|
|
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.
|
|
463
623
|
*/
|
|
464
624
|
let _menu: MenuHandle | null = null;
|
|
465
625
|
|
|
@@ -477,16 +637,11 @@ function fileAt(path: string): LibraryFile | null {
|
|
|
477
637
|
);
|
|
478
638
|
}
|
|
479
639
|
|
|
480
|
-
/**
|
|
481
|
-
* The per-file menu, opened at the pointer.
|
|
482
|
-
*
|
|
483
|
-
* The rows are the `menu` surface's own projections, so the Library's Delete is drawn, keyed and
|
|
484
|
-
* dismissed exactly as the element menu's is — including the edge clamping, which was a hand-rolled
|
|
485
|
-
* `requestAnimationFrame` measurement here and is the kit's popover's job.
|
|
486
|
-
*/
|
|
640
|
+
/** The per-file menu, opened at the pointer. Region `content`. */
|
|
487
641
|
function showLibraryContextMenu(path: string, x: number, y: number): void {
|
|
488
642
|
const file = fileAt(path);
|
|
489
|
-
|
|
643
|
+
// A record is a row of a file: renaming, duplicating or deleting "it" would act on the file.
|
|
644
|
+
if (!file || file.record) {
|
|
490
645
|
return;
|
|
491
646
|
}
|
|
492
647
|
closeLibraryMenu();
|
|
@@ -512,10 +667,10 @@ function showLibraryContextMenu(path: string, x: number, y: number): void {
|
|
|
512
667
|
}
|
|
513
668
|
},
|
|
514
669
|
origin: { x, y },
|
|
515
|
-
region: "
|
|
670
|
+
region: "content",
|
|
516
671
|
rows: [
|
|
517
672
|
row("open", "Open", () => {
|
|
518
|
-
|
|
673
|
+
openLibraryFile(file);
|
|
519
674
|
}),
|
|
520
675
|
row(
|
|
521
676
|
"rename",
|
|
@@ -540,57 +695,24 @@ function showLibraryContextMenu(path: string, x: number, y: number): void {
|
|
|
540
695
|
});
|
|
541
696
|
}
|
|
542
697
|
|
|
543
|
-
/** The New menu, opened under its own button. A second press closes it: the button is a toggle. */
|
|
544
|
-
function showLibraryNewMenu(opener: HTMLElement): void {
|
|
545
|
-
if (_menu) {
|
|
546
|
-
closeLibraryMenu();
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
const entries = libraryNewEntries();
|
|
550
|
-
_menu = openMenu({
|
|
551
|
-
label: "New",
|
|
552
|
-
onClosed: (handle) => {
|
|
553
|
-
if (_menu === handle) {
|
|
554
|
-
_menu = null;
|
|
555
|
-
}
|
|
556
|
-
},
|
|
557
|
-
opener,
|
|
558
|
-
place: () => {
|
|
559
|
-
const box = rectOf(opener);
|
|
560
|
-
return { x: box.left, y: box.bottom + 4 };
|
|
561
|
-
},
|
|
562
|
-
region: "library-new",
|
|
563
|
-
rows: entries.map((entry, index) => ({
|
|
564
|
-
destructive: false,
|
|
565
|
-
disabled: false,
|
|
566
|
-
/* The document kinds and the collections are two different creation flows, so the boundary
|
|
567
|
-
between them is drawn rather than left to be inferred from the folder names. */
|
|
568
|
-
dividerAbove:
|
|
569
|
-
entry.collection !== undefined && entries[index - 1]?.collection === undefined && index > 0,
|
|
570
|
-
id: entry.key,
|
|
571
|
-
title: `${entry.label} — ${entry.dir}/`,
|
|
572
|
-
})),
|
|
573
|
-
run: (id) => {
|
|
574
|
-
void createLibraryEntry(id);
|
|
575
|
-
},
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
|
|
579
698
|
/**
|
|
580
|
-
* The
|
|
699
|
+
* The rename dialog — the same copy, from the same helper, as the Files panel's.
|
|
581
700
|
*
|
|
582
701
|
* A rename here rewrites references exactly as one in the sidebar does, and two dialogs that
|
|
583
702
|
* disagreed about that is the divergence the shared helper exists to prevent.
|
|
584
703
|
*/
|
|
585
704
|
async function renameLibraryFile(file: LibraryFile) {
|
|
586
705
|
const message = await renamePromptMessage(file.path);
|
|
587
|
-
const
|
|
706
|
+
const prompt: PromptDialogOptions = {
|
|
588
707
|
confirmLabel: "Rename",
|
|
589
|
-
...(message === undefined ? {} : { message }),
|
|
590
708
|
select: "stem",
|
|
591
709
|
validate: (value) => (value.trim() ? "" : "Enter a file name."),
|
|
592
710
|
value: file.name,
|
|
593
|
-
}
|
|
711
|
+
};
|
|
712
|
+
if (message !== undefined) {
|
|
713
|
+
prompt.message = message;
|
|
714
|
+
}
|
|
715
|
+
const newName = await showPromptDialog("Rename", prompt);
|
|
594
716
|
if (!newName || newName === file.name) {
|
|
595
717
|
return;
|
|
596
718
|
}
|
|
@@ -607,7 +729,7 @@ async function renameLibraryFile(file: LibraryFile) {
|
|
|
607
729
|
notify.error(`Could not rename ${file.name}.`, {
|
|
608
730
|
detail: errorMessage(error),
|
|
609
731
|
path: file.path,
|
|
610
|
-
source:
|
|
732
|
+
source: SOURCE,
|
|
611
733
|
});
|
|
612
734
|
}
|
|
613
735
|
}
|
|
@@ -629,7 +751,7 @@ async function duplicateLibraryFile(file: LibraryFile) {
|
|
|
629
751
|
notify.error(`Could not duplicate ${file.name}.`, {
|
|
630
752
|
detail: errorMessage(error),
|
|
631
753
|
path: file.path,
|
|
632
|
-
source:
|
|
754
|
+
source: SOURCE,
|
|
633
755
|
});
|
|
634
756
|
}
|
|
635
757
|
}
|
|
@@ -649,181 +771,95 @@ async function deleteLibraryFile(file: LibraryFile) {
|
|
|
649
771
|
notify.error(`Could not delete ${file.name}.`, {
|
|
650
772
|
detail: errorMessage(error),
|
|
651
773
|
path: file.path,
|
|
652
|
-
source:
|
|
774
|
+
source: SOURCE,
|
|
653
775
|
});
|
|
654
776
|
}
|
|
655
777
|
}
|
|
656
778
|
|
|
657
|
-
// ───
|
|
658
|
-
|
|
659
|
-
/** What the New menu offers: the three document kinds, plus one row per creatable collection. */
|
|
660
|
-
export interface LibraryNewEntry {
|
|
661
|
-
key: string;
|
|
662
|
-
label: string;
|
|
663
|
-
dir: string;
|
|
664
|
-
/**
|
|
665
|
-
* The content collection this row creates an entry IN, or undefined for a document kind.
|
|
666
|
-
*
|
|
667
|
-
* Present is the whole difference between the two creation paths below — a collection row is
|
|
668
|
-
* `content/`'s job, because only that module knows the collection's extension and its schema.
|
|
669
|
-
*/
|
|
670
|
-
collection?: string;
|
|
671
|
-
}
|
|
779
|
+
// ─── The collection's page ───────────────────────────────────────────────────
|
|
672
780
|
|
|
673
|
-
/**
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
* menu offers exactly the collections an entry can be created in. The predecessor read the raw
|
|
678
|
-
* `content` map and derived a directory from the type NAME when `source` was absent, which produced
|
|
679
|
-
* a row for a collection that has no entry directory at all and a row for a single-file (CSV)
|
|
680
|
-
* catalogue, whose entries are rows and not files. Both created a file nothing would ever load.
|
|
681
|
-
*/
|
|
682
|
-
export function libraryNewEntries(): LibraryNewEntry[] {
|
|
683
|
-
const rows: LibraryNewEntry[] = [
|
|
684
|
-
{ dir: "pages", key: "page", label: "Page" },
|
|
685
|
-
{ dir: "layouts", key: "layout", label: "Layout" },
|
|
686
|
-
{ dir: "components", key: "component", label: "Component" },
|
|
687
|
-
];
|
|
688
|
-
for (const collection of entryCollections()) {
|
|
689
|
-
rows.push({
|
|
690
|
-
collection: collection.name,
|
|
691
|
-
dir: collection.dir,
|
|
692
|
-
key: `collection:${collection.name}`,
|
|
693
|
-
label: collection.name.charAt(0).toUpperCase() + collection.name.slice(1),
|
|
694
|
-
});
|
|
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";
|
|
695
785
|
}
|
|
696
|
-
|
|
786
|
+
const layout = page.layout ?? defaultLayout;
|
|
787
|
+
return layout ? `using ${templateDisplayName(layout)}` : "with no template";
|
|
697
788
|
}
|
|
698
789
|
|
|
699
790
|
/**
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* Two paths, because a collection entry is not a blank document. A **collection** row goes through
|
|
703
|
-
* `content/entry-commands.ts`'s `createEntry`, which supplies the collection's extension (so the
|
|
704
|
-
* field asks for a display name and the file is actually matched by the collection it was created
|
|
705
|
-
* in) and a body seeded from the collection's **schema defaults** — the entry is valid the moment
|
|
706
|
-
* it exists, rather than a pile of absent required fields — and then opens it in the entry FORM,
|
|
707
|
-
* which is the editor that collection's fields belong to. Both of those are the reason
|
|
708
|
-
* `createEntry` is called rather than reimplemented; a second seeder here would drift from the one
|
|
709
|
-
* the palette's `content.newEntry` uses within a release.
|
|
710
|
-
*
|
|
711
|
-
* A **document kind** gets the format PICKER, which is the same argument the verbatim name field
|
|
712
|
-
* used to make: a page may be a `.md` as easily as a `.json`, so the extension is a real choice —
|
|
713
|
-
* now offered rather than left to be typed from memory. A collection's extension is not a choice;
|
|
714
|
-
* it is the collection's, and `createEntry` supplies it.
|
|
715
|
-
*
|
|
716
|
-
* **A layout is not a choice either, and that is not a policy.** Both readers of a layout parse it
|
|
717
|
-
* as JSON and neither dispatches through the format registry — `site/layout-resolver.ts` throws and
|
|
718
|
-
* `site-context.ts` returns null — so a `.md` layout is a file the build cannot load. There is no
|
|
719
|
-
* `"layout"` document kind for a format to declare, so this is stated here rather than derived.
|
|
720
|
-
*
|
|
721
|
-
* A **component** keeps the picker: a format whose `$studio.documentMode.componentWhen` promotes a
|
|
722
|
-
* frontmatter `tagName` (Markdown's does) can express one perfectly well. What it cannot do is
|
|
723
|
-
* express one from a `newFileTemplate` that carries no `tagName`, so the seed below supplies it —
|
|
724
|
-
* through the format's own serializer, because this module knows no format's syntax.
|
|
725
|
-
*/
|
|
726
|
-
/** How each document kind's extension is settled. Anything else falls back to the full picker. */
|
|
727
|
-
const FORMAT_CHOICE_BY_KIND: Record<string, FormatChoice> = {
|
|
728
|
-
component: { defaultExt: ".json", docKind: "component", kind: "choose" },
|
|
729
|
-
layout: { ext: ".json", kind: "fixed" },
|
|
730
|
-
page: { defaultExt: ".json", docKind: "page", kind: "choose" },
|
|
731
|
-
};
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* The body a new component starts as, when the reader picked a format rather than `.json`.
|
|
791
|
+
* The header line: where a collection's entries are published (§22.5).
|
|
735
792
|
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
* format's syntax in the Library. Without the `tagName` the file satisfies neither
|
|
739
|
-
* `$studio.documentMode.componentWhen` nor the build's tag registration, and is a component nothing
|
|
740
|
-
* loads. A stem that is not a valid custom-element name (no hyphen) cannot carry one, so it falls
|
|
741
|
-
* back to the format's own template and the author names the tag themselves.
|
|
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.
|
|
742
795
|
*/
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
} catch {
|
|
759
|
-
// The serializer is the project's, so its failure is not a reason to refuse the creation —
|
|
760
|
-
// The format's own template still produces a file the author can finish by hand.
|
|
761
|
-
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;
|
|
762
811
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
export async function createLibraryEntry(key: string): Promise<string | null> {
|
|
766
|
-
const entry = libraryNewEntries().find((row) => row.key === key);
|
|
767
|
-
if (!entry) {
|
|
768
|
-
return null;
|
|
812
|
+
if (page === undefined) {
|
|
813
|
+
return { ...hidden, state: "loading", text: "Finding where these entries are published…" };
|
|
769
814
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
? await createFileIn({
|
|
773
|
-
...(entry.key === "component" ? { content: componentSeed } : {}),
|
|
774
|
-
dir: entry.dir,
|
|
775
|
-
format: FORMAT_CHOICE_BY_KIND[entry.key] ?? { defaultExt: ".json", kind: "choose" },
|
|
776
|
-
source: "Library",
|
|
777
|
-
suggestedName: "untitled",
|
|
778
|
-
title: `New ${entry.label}`,
|
|
779
|
-
})
|
|
780
|
-
: await createEntry(entry.collection);
|
|
781
|
-
if (created === null) {
|
|
782
|
-
return null;
|
|
815
|
+
if (page === null) {
|
|
816
|
+
return { ...hidden, state: "none", text: "Entries have no page of their own" };
|
|
783
817
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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`,
|
|
839
|
+
};
|
|
790
840
|
}
|
|
791
|
-
return
|
|
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
|
+
};
|
|
792
848
|
}
|
|
793
849
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
* visible BEFORE the drop. "All" has none, and rather than falling back to a default nobody chose,
|
|
801
|
-
* it asks. That fallback is exactly the surprise studio.md §16 exists to refuse: the file arrives,
|
|
802
|
-
* the toast says it worked, and it is in a directory the author never picked.
|
|
803
|
-
*/
|
|
804
|
-
export async function resolveUploadDir(): Promise<string | null> {
|
|
805
|
-
const named = uploadDirForCategory(libraryView.category);
|
|
806
|
-
if (named !== undefined) {
|
|
807
|
-
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);
|
|
808
856
|
}
|
|
809
|
-
const chosen = await showPromptDialog("Upload files", {
|
|
810
|
-
confirmLabel: "Upload",
|
|
811
|
-
message: "The All view has no folder of its own. Choose where these files land.",
|
|
812
|
-
placeholder: "public",
|
|
813
|
-
validate: (value) => (value.trim() ? "" : "Enter a folder."),
|
|
814
|
-
value: "public",
|
|
815
|
-
});
|
|
816
|
-
return chosen === null ? null : chosen.trim().replace(/\/$/, "");
|
|
817
857
|
}
|
|
818
858
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
await uploadAssets([...files], { dir });
|
|
825
|
-
invalidateLibrary();
|
|
826
|
-
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);
|
|
827
863
|
}
|
|
828
864
|
|
|
829
865
|
// ─── The projection ──────────────────────────────────────────────────────────
|
|
@@ -834,25 +870,27 @@ function chip(key: string, label: string, active: boolean): LibraryChip {
|
|
|
834
870
|
}
|
|
835
871
|
|
|
836
872
|
/** The sentence a filter that matched nothing prints, naming both facets rather than one. */
|
|
837
|
-
function noMatchMessage(): string {
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const where = scopes.length === 0 ? "" : ` in ${scopes.join(" and ")}`;
|
|
846
|
-
const term = libraryView.query.trim();
|
|
847
|
-
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}.`;
|
|
848
881
|
}
|
|
849
882
|
|
|
850
|
-
/** Which of the
|
|
851
|
-
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 {
|
|
852
890
|
if (matched > 0) {
|
|
853
|
-
return
|
|
891
|
+
return layout;
|
|
854
892
|
}
|
|
855
|
-
if (
|
|
893
|
+
if (contentView.loading || !current.scanned()) {
|
|
856
894
|
return "loading";
|
|
857
895
|
}
|
|
858
896
|
if (current.failures().length > 0) {
|
|
@@ -861,11 +899,329 @@ function viewKind(current: LibrarySource, matched: number, total: number): Libra
|
|
|
861
899
|
return total === 0 ? "empty" : "nomatch";
|
|
862
900
|
}
|
|
863
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
|
+
|
|
864
1220
|
// ─── Mount ───────────────────────────────────────────────────────────────────
|
|
865
1221
|
|
|
866
1222
|
/**
|
|
867
|
-
* Draw the
|
|
868
|
-
* 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).
|
|
869
1225
|
*/
|
|
870
1226
|
export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
871
1227
|
const { paneId, wrap: canvasWrap } = surface;
|
|
@@ -887,115 +1243,54 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
887
1243
|
}
|
|
888
1244
|
});
|
|
889
1245
|
|
|
890
|
-
const view = (): LibraryView => {
|
|
891
|
-
const current = librarySource();
|
|
892
|
-
const { layout } = libraryView;
|
|
893
|
-
const files = filterLibrary(current.files(), {
|
|
894
|
-
category: libraryView.category,
|
|
895
|
-
locale: libraryView.locale,
|
|
896
|
-
query: libraryView.query,
|
|
897
|
-
});
|
|
898
|
-
const total = current.files().length;
|
|
899
|
-
const kind = viewKind(current, files.length, total);
|
|
900
|
-
const range = windowFor(panel, layout, files.length);
|
|
901
|
-
const slice = files.slice(range.start, range.end);
|
|
902
|
-
const failures = current.failures();
|
|
903
|
-
const locales = libraryLocales(current.files());
|
|
904
|
-
const destination = uploadDirForCategory(libraryView.category);
|
|
905
|
-
/* Cards is the only layout that draws a live document render inside a card. A Media tile is a
|
|
906
|
-
thumbnail of an ASSET — `library-layouts.ts` says why — so it asks for the glyph instead. */
|
|
907
|
-
const live = kind === "cards";
|
|
908
|
-
const calendar = kind === "calendar" ? calendarView(files) : { groups: [], truncated: "" };
|
|
909
|
-
|
|
910
|
-
return {
|
|
911
|
-
bannerState: failures.length > 0 ? "shown" : "hidden",
|
|
912
|
-
bannerText:
|
|
913
|
-
failures.length === 0
|
|
914
|
-
? ""
|
|
915
|
-
: `This list is incomplete — ${failures.length} ${
|
|
916
|
-
failures.length === 1 ? "directory" : "directories"
|
|
917
|
-
} could not be read (${failures[0]!.dir}${failures.length > 1 ? ", …" : ""}).`,
|
|
918
|
-
categories: LIBRARY_CATEGORIES.map((category) =>
|
|
919
|
-
chip(category.key, category.label, libraryView.category === category.key),
|
|
920
|
-
),
|
|
921
|
-
columns:
|
|
922
|
-
kind === "table" ? libraryColumns().map((c) => ({ field: c.field, title: c.title })) : [],
|
|
923
|
-
dropActive: panel?.dropActive ? "true" : "false",
|
|
924
|
-
dropRegion: paneRegion(paneId, "library/dropZone"),
|
|
925
|
-
emptyDetail:
|
|
926
|
-
kind === "incomplete"
|
|
927
|
-
? `${failures.length} ${
|
|
928
|
-
failures.length === 1 ? "directory" : "directories"
|
|
929
|
-
} could not be read, so this is not the same as an empty project.`
|
|
930
|
-
: kind === "nomatch"
|
|
931
|
-
? `${total} file(s) in the project.`
|
|
932
|
-
: "",
|
|
933
|
-
emptyMessage: kind === "nomatch" ? noMatchMessage() : "",
|
|
934
|
-
groups: kind === "board" ? boardView(files) : calendar.groups,
|
|
935
|
-
layouts: LIBRARY_LAYOUTS.map((name) =>
|
|
936
|
-
chip(name, LIBRARY_LAYOUT_LABELS[name], libraryView.layout === name),
|
|
937
|
-
),
|
|
938
|
-
localeOptions: [
|
|
939
|
-
{ label: "All languages", value: "all" },
|
|
940
|
-
...locales.map((tag) => ({ label: localeLabel(tag), value: tag })),
|
|
941
|
-
],
|
|
942
|
-
/* The facet's options come from ALL scanned files, never from the filtered ones: a picker
|
|
943
|
-
whose choices collapse to the choice just made cannot be used to make another. One locale —
|
|
944
|
-
or none — means every file already agrees. */
|
|
945
|
-
localeState: locales.length < 2 ? "hidden" : "shown",
|
|
946
|
-
localeValue: libraryView.locale === "" ? "all" : libraryView.locale,
|
|
947
|
-
pad: `padding-top:${range.padTop}px;padding-bottom:${range.padBottom}px`,
|
|
948
|
-
padBottom: `height:${range.padBottom}px`,
|
|
949
|
-
padTop: `height:${range.padTop}px`,
|
|
950
|
-
query: libraryView.query,
|
|
951
|
-
region: paneRegion(paneId, "library"),
|
|
952
|
-
rows:
|
|
953
|
-
kind === "cards" || kind === "media" || kind === "table"
|
|
954
|
-
? slice.map((file) => libraryRow(file, kind === "table" ? libraryColumns() : [], live))
|
|
955
|
-
: [],
|
|
956
|
-
truncated: calendar.truncated,
|
|
957
|
-
truncatedState: calendar.truncated === "" ? "hidden" : "shown",
|
|
958
|
-
uploadAccept: uploadAccept(),
|
|
959
|
-
uploadHint:
|
|
960
|
-
destination === undefined ? "Upload — asks for a folder" : `Upload into ${destination}/`,
|
|
961
|
-
view: kind,
|
|
962
|
-
};
|
|
963
|
-
};
|
|
964
|
-
|
|
965
1246
|
const draw = () => {
|
|
966
1247
|
if (panel !== null && activeIn(paneId) === panel) {
|
|
967
|
-
panel.surface.update(
|
|
1248
|
+
panel.surface.update(homeView(panel, paneId));
|
|
968
1249
|
scheduleSettle(panel);
|
|
969
1250
|
}
|
|
970
1251
|
};
|
|
971
1252
|
|
|
972
|
-
const mounted = mountLibrarySurface(canvasWrap,
|
|
1253
|
+
const mounted = mountLibrarySurface(canvasWrap, homeView(null, paneId), {
|
|
1254
|
+
addPage: () => {
|
|
1255
|
+
void addCollectionPageFor(shownCollection().key);
|
|
1256
|
+
},
|
|
973
1257
|
contextMenu: (path, x, y) => showLibraryContextMenu(path, x, y),
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
panel.dropActive = false;
|
|
977
|
-
draw();
|
|
978
|
-
}
|
|
1258
|
+
designPage: () => {
|
|
1259
|
+
designCollectionPage(shownCollection().key);
|
|
979
1260
|
},
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1261
|
+
openFields: () => {
|
|
1262
|
+
void runActiveReported(
|
|
1263
|
+
"settings.open",
|
|
1264
|
+
{ entry: shownCollection().key, section: "content" },
|
|
1265
|
+
SOURCE,
|
|
1266
|
+
);
|
|
985
1267
|
},
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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;
|
|
993
1275
|
}
|
|
1276
|
+
void runActiveReported("collection.editInGrid", { name: collection.key }, SOURCE);
|
|
994
1277
|
},
|
|
995
1278
|
openFile: (path) => {
|
|
996
|
-
|
|
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);
|
|
997
1293
|
},
|
|
998
|
-
openNewMenu: (opener) => showLibraryNewMenu(opener),
|
|
999
1294
|
previewSlot: (element, path) => {
|
|
1000
1295
|
if (panel) {
|
|
1001
1296
|
previewSlot(panel, element, path);
|
|
@@ -1003,7 +1298,6 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
1003
1298
|
},
|
|
1004
1299
|
reset: () => {
|
|
1005
1300
|
setLibrarySearch("");
|
|
1006
|
-
setLibraryCategory("all");
|
|
1007
1301
|
setLibraryLocale("");
|
|
1008
1302
|
},
|
|
1009
1303
|
retry: () => {
|
|
@@ -1016,24 +1310,16 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
1016
1310
|
}
|
|
1017
1311
|
},
|
|
1018
1312
|
search: (value) => setLibrarySearch(value),
|
|
1019
|
-
|
|
1313
|
+
/* SAFETY: the surface's view buttons pass `$map.item.key`, and the `layouts` state they map is
|
|
1314
|
+
built from LIBRARY_LAYOUTS in the projection above, so every key is a LibraryLayout. */
|
|
1020
1315
|
setLayout: (key) => setLibraryLayout(key as LibraryLayout),
|
|
1316
|
+
setFilter: (value) => setLibraryFilter(value === "all" ? "" : value),
|
|
1021
1317
|
setLocale: (value) => setLibraryLocale(value === "all" ? "" : value),
|
|
1022
|
-
upload: (input) => {
|
|
1023
|
-
const chosen = input.files;
|
|
1024
|
-
// Cleared before the upload starts: a file input that keeps its value refuses the same file
|
|
1025
|
-
// Twice, and the picker is this surface's own control rather than a node it found.
|
|
1026
|
-
input.value = "";
|
|
1027
|
-
if (chosen?.length) {
|
|
1028
|
-
void uploadIntoLibrary(chosen);
|
|
1029
|
-
}
|
|
1030
|
-
},
|
|
1031
1318
|
});
|
|
1032
1319
|
|
|
1033
1320
|
const scope = effectScope();
|
|
1034
1321
|
panel = {
|
|
1035
1322
|
cache: createPreviewCache(),
|
|
1036
|
-
dropActive: false,
|
|
1037
1323
|
observer,
|
|
1038
1324
|
paneId,
|
|
1039
1325
|
pending: new WeakMap(),
|
|
@@ -1044,6 +1330,17 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
1044
1330
|
slots: new Map(),
|
|
1045
1331
|
surface: mounted,
|
|
1046
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
|
+
})(),
|
|
1047
1344
|
wrap: canvasWrap,
|
|
1048
1345
|
};
|
|
1049
1346
|
_active.set(paneId, panel);
|
|
@@ -1054,18 +1351,29 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
|
1054
1351
|
return;
|
|
1055
1352
|
}
|
|
1056
1353
|
// Everything the pane draws from.
|
|
1057
|
-
void
|
|
1058
|
-
void
|
|
1059
|
-
void
|
|
1060
|
-
|
|
1061
|
-
void
|
|
1062
|
-
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;
|
|
1063
1361
|
|
|
1064
1362
|
draw();
|
|
1065
1363
|
|
|
1066
|
-
if (!scanAttempted && !
|
|
1364
|
+
if (!scanAttempted && !contentView.loading) {
|
|
1067
1365
|
void loadLibrary();
|
|
1068
1366
|
}
|
|
1069
1367
|
});
|
|
1070
1368
|
});
|
|
1071
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
|
+
}
|