@jxsuite/studio 6.1.2 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/chunks/2020-5wtzce5g.js +4 -0
- package/dist/chunks/adapter-5tphg7xe.js +4 -0
- package/dist/chunks/add-new-flow-psym14sn.js +4 -0
- package/dist/chunks/cf-account-picker-haqazq5r.js +4 -0
- package/dist/chunks/cf-settings-h2wrh6hq.js +4 -0
- package/dist/chunks/{color-area-0fj8a4ga.js → color-area-6rbra5dy.js} +2 -2
- package/dist/chunks/{color-field-xch8kx2g.js → color-field-gy56c94g.js} +3 -3
- package/dist/chunks/color-m7r1vk54.js +4 -0
- package/dist/chunks/{color-slider-jxd8v350.js → color-slider-sqqdyf6c.js} +2 -2
- package/dist/chunks/{combobox-p0efwjke.js → combobox-72h32ejk.js} +3 -3
- package/dist/chunks/components-r22xca09.js +4 -0
- package/dist/chunks/content-source-mmppmcyg.js +4 -0
- package/dist/chunks/convert-to-repeater-he70fc2b.js +4 -0
- package/dist/chunks/editor-ccd9jwsa.js +4 -0
- package/dist/chunks/entry-commands-6qxz9e8p.js +4 -0
- package/dist/chunks/extension-sections-0egb88dm.js +4 -0
- package/dist/chunks/file-ops-qnab36ek.js +4 -0
- package/dist/chunks/files-rcn56qvm.js +4 -0
- package/dist/chunks/format-host-yxz0kjjs.js +4 -0
- package/dist/chunks/fs-events-d1qxq4z4.js +4 -0
- package/dist/chunks/grid-open-4z1g062t.js +4 -0
- package/dist/chunks/{grid-view-d2wq9ezg.js → grid-view-qg1b5eqx.js} +25 -25
- package/dist/chunks/{icons-pnfz478q.js → icons-63bjtbs7.js} +2 -2
- package/dist/chunks/{iframe-entry-jdcktez8.js → iframe-entry-2zggzajv.js} +4 -4
- package/dist/chunks/{iframe-entry-zap785cw.js → iframe-entry-76j8vwwx.js} +4 -4
- package/dist/chunks/iframe-entry-99nr5zfb.js +5 -0
- package/dist/chunks/{iframe-entry-x0ygf2yr.js → iframe-entry-pkgmcgp0.js} +4 -4
- package/dist/chunks/iframe-host-ssv7kyj0.js +4 -0
- package/dist/chunks/{index-zpjmzqfk.js → index-32eqzv1k.js} +3 -3
- package/dist/chunks/index-f0jn7nnp.js +4 -0
- package/dist/chunks/{javascript-m54mnwp6.js → javascript-pjp734q4.js} +3 -3
- package/dist/chunks/jsonMode-njkvsa02.js +13 -0
- package/dist/chunks/jx-validate-ttasrbh7.js +4 -0
- package/dist/chunks/layers-bgyr11tr.js +4 -0
- package/dist/chunks/layers-panel-7ez66zeq.js +4 -0
- package/dist/chunks/{listbox-e74qjyq6.js → listbox-c421cf8c.js} +2 -2
- package/dist/chunks/lit-html-v9gh687x.js +4 -0
- package/dist/chunks/media-library-sq468wts.js +4 -0
- package/dist/chunks/media-picker-shfe59pt.js +4 -0
- package/dist/chunks/{menu-n6yzswmy.js → menu-bdpzbfvs.js} +2 -2
- package/dist/chunks/{monaco-binding-7gx83x3r.js → monaco-binding-vq1hgnn0.js} +3 -3
- package/dist/chunks/monaco-buffer-3afgwvbp.js +4 -0
- package/dist/chunks/monaco-setup-xrd2cbkp.js +4 -0
- package/dist/chunks/notify-kzrjr3fv.js +4 -0
- package/dist/chunks/parse-v77xtwv7.js +4 -0
- package/dist/chunks/platform-7v4rtd7m.js +4 -0
- package/dist/chunks/popover-b89sfej7.js +4 -0
- package/dist/chunks/publish-panel-2z9gw8ee.js +4 -0
- package/dist/chunks/publish-quick-qx2455cj.js +4 -0
- package/dist/chunks/publish-workspace-wrc1dgwx.js +4 -0
- package/dist/chunks/quick-search-n0t0eqxb.js +4 -0
- package/dist/chunks/references-9547rg5s.js +4 -0
- package/dist/chunks/right-panel-wsgnr9pn.js +4 -0
- package/dist/chunks/source-control-kwktr9y6.js +4 -0
- package/dist/chunks/{split-n2gfpm5m.js → split-c9zfdgxf.js} +2 -2
- package/dist/chunks/studio-03ct6yfv.js +9 -0
- package/dist/chunks/studio-07rcfje3.js +5 -0
- package/dist/chunks/studio-3e54qh39.js +7 -0
- package/dist/chunks/studio-3exxhz5g.js +5 -0
- package/dist/chunks/studio-3q40e3rj.js +5 -0
- package/dist/chunks/studio-3qeh9t0h.js +6 -0
- package/dist/chunks/studio-3qqjeep5.js +6 -0
- package/dist/chunks/studio-43sd2mam.js +4 -0
- package/dist/chunks/studio-4hp8pkyt.js +5 -0
- package/dist/chunks/studio-58xg6j2q.js +9 -0
- package/dist/chunks/studio-5g0rprdt.js +7 -0
- package/dist/chunks/{studio-2j1cb95x.js → studio-6m380wp7.js} +4 -4
- package/dist/chunks/studio-80jyh1xh.js +4 -0
- package/dist/chunks/studio-83mk4bgg.js +9 -0
- package/dist/chunks/{studio-v0jzqdjm.js → studio-881z1xrx.js} +4 -4
- package/dist/chunks/studio-8m0y2qe7.js +5 -0
- package/dist/chunks/{studio-d81xfh1g.js → studio-8tr6cfrc.js} +4 -4
- package/dist/chunks/studio-8vcvfs7r.js +6 -0
- package/dist/chunks/studio-9tmebf0s.js +5 -0
- package/dist/chunks/studio-a2bvaptz.js +5 -0
- package/dist/chunks/studio-b7y5mr30.js +5 -0
- package/dist/chunks/studio-bavb31r0.js +5 -0
- package/dist/chunks/{studio-dgz8d0c1.js → studio-bbe4c90a.js} +4 -4
- package/dist/chunks/studio-c05wdnt3.js +5 -0
- package/dist/chunks/studio-ccv2svc6.js +5 -0
- package/dist/chunks/studio-d5grtb9e.js +16 -0
- package/dist/chunks/studio-df0baea9.js +5 -0
- package/dist/chunks/studio-dgk4dqtx.js +7 -0
- package/dist/chunks/studio-e1g64q43.js +5 -0
- package/dist/chunks/studio-ed1hp6zf.js +6 -0
- package/dist/chunks/studio-ed3xx40h.js +7 -0
- package/dist/chunks/studio-f14e1zdj.js +5 -0
- package/dist/chunks/studio-fbhxxc5f.js +5 -0
- package/dist/chunks/studio-fyh0jc96.js +5 -0
- package/dist/chunks/studio-gcxk20n7.js +5 -0
- package/dist/chunks/studio-jaaz8ce9.js +5 -0
- package/dist/chunks/{studio-90pq9km1.js → studio-jcnsdaep.js} +4 -4
- package/dist/chunks/studio-jsgf2q5k.js +7 -0
- package/dist/chunks/{studio-0q988f9w.js → studio-k3xxbagr.js} +3 -3
- package/dist/chunks/{studio-5ve6bbep.js → studio-k7evxqh4.js} +4 -4
- package/dist/chunks/studio-kt3pjkwd.js +5 -0
- package/dist/chunks/studio-m8g16bgz.js +5 -0
- package/dist/chunks/studio-mn0476ag.js +5 -0
- package/dist/chunks/studio-mx1smyq0.js +5 -0
- package/dist/chunks/studio-n0c5h6mf.js +5 -0
- package/dist/chunks/{studio-485046te.js → studio-nh6d342n.js} +7 -7
- package/dist/chunks/studio-nqxqf8p2.js +4 -0
- package/dist/chunks/studio-p4nrkvqr.js +5 -0
- package/dist/chunks/{studio-1fxkaaea.js → studio-pbxa5fpc.js} +4 -4
- package/dist/chunks/studio-pgmvdv7z.js +7 -0
- package/dist/chunks/studio-ph84avkf.js +5 -0
- package/dist/chunks/{studio-tq0rtfwj.js → studio-q0mvbaq6.js} +4 -4
- package/dist/chunks/studio-q0tccd43.js +5 -0
- package/dist/chunks/studio-q7rtx6zq.js +14 -0
- package/dist/chunks/{studio-a3g98wjr.js → studio-qgh13jtz.js} +4 -4
- package/dist/chunks/studio-qmn1xc24.js +5 -0
- package/dist/chunks/studio-qwdqwkj0.js +5 -0
- package/dist/chunks/studio-r5qy30vx.js +5 -0
- package/dist/chunks/studio-rj9eamf8.js +5 -0
- package/dist/chunks/studio-rptgsk96.js +5 -0
- package/dist/chunks/studio-rthyg42z.js +928 -0
- package/dist/chunks/studio-s19tp1xr.js +5 -0
- package/dist/chunks/studio-s32578pn.js +5 -0
- package/dist/chunks/studio-svb7yyve.js +5 -0
- package/dist/chunks/studio-syreb644.js +5 -0
- package/dist/chunks/studio-tf9jryb1.js +10 -0
- package/dist/chunks/studio-tfkznv39.js +5 -0
- package/dist/chunks/studio-tft6r105.js +5 -0
- package/dist/chunks/studio-txf2kysx.js +5 -0
- package/dist/chunks/studio-v8867ddv.js +12 -0
- package/dist/chunks/studio-vxq7fcvv.js +5 -0
- package/dist/chunks/studio-w4rz9hdk.js +5 -0
- package/dist/chunks/studio-wbcjbhy1.js +9 -0
- package/dist/chunks/studio-wc649wss.js +5 -0
- package/dist/chunks/studio-wys23mxz.js +5 -0
- package/dist/chunks/studio-x1x167jh.js +5 -0
- package/dist/chunks/studio-x9hsze61.js +5 -0
- package/dist/chunks/studio-y0n7yxea.js +9 -0
- package/dist/chunks/studio-y3ahhdpx.js +5 -0
- package/dist/chunks/studio-z97c48fk.js +5 -0
- package/dist/chunks/studio-zat62m0c.js +5 -0
- package/dist/chunks/studio-zcd2e7yz.js +5 -0
- package/dist/chunks/studio-zz9qe3rw.js +64 -0
- package/dist/chunks/toast-1gv0h53h.js +4 -0
- package/dist/chunks/{toast-host-ke5z4ng7.js → toast-host-1yg5ar8d.js} +3 -3
- package/dist/chunks/{tooltip-5v8ngwj2.js → tooltip-yypz28gq.js} +3 -3
- package/dist/chunks/{tree-mdq3v5cw.js → tree-9xa4hm7n.js} +2 -2
- package/dist/chunks/tsMode-epyfz1hv.js +14 -0
- package/dist/chunks/workspace-dz7nxfhv.js +4 -0
- package/dist/chunks/workspace-home-xsd7hkjw.js +4 -0
- package/dist/chunks/{ws-client-nze0k6mv.js → ws-client-bv552sfx.js} +3 -3
- package/dist/iframe-entry.js +20 -16
- package/dist/manifest.json +77 -63
- package/dist/studio.js +103 -104
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +12 -11
- package/src/browse/component-examples.ts +272 -0
- package/src/browse/library-commands.ts +64 -104
- package/src/browse/library-layouts.ts +263 -49
- package/src/browse/library-model.ts +181 -133
- package/src/browse/library-pane.ts +738 -446
- package/src/browse/library-preview.ts +149 -19
- package/src/browse/library-source.ts +308 -41
- package/src/browse/preview-root.ts +78 -0
- package/src/browse/preview-style.ts +155 -0
- package/src/canvas/asset-refs.ts +16 -1
- package/src/canvas/canvas-live-render.ts +222 -30
- package/src/canvas/canvas-origin.ts +37 -12
- package/src/canvas/canvas-page-context.ts +112 -0
- package/src/canvas/canvas-render.ts +113 -107
- package/src/canvas/canvas-utils.ts +57 -12
- package/src/canvas/iframe-drop.ts +107 -0
- package/src/canvas/iframe-entry.ts +24 -12
- package/src/canvas/iframe-host.ts +134 -62
- package/src/canvas/iframe-keys.ts +11 -0
- package/src/canvas/iframe-patch.ts +8 -7
- package/src/canvas/iframe-protocol.ts +24 -5
- package/src/canvas/iframe-render.ts +48 -1
- package/src/canvas/iframe-subtree.ts +5 -2
- package/src/canvas/npm-modules.ts +22 -0
- package/src/canvas/preview-navigate.ts +16 -0
- package/src/canvas/repeater-sources.ts +200 -0
- package/src/canvas/stage-scrollbar.ts +5 -1
- package/src/canvas/surface-registry.ts +6 -0
- package/src/commands/app-commands.ts +16 -6
- package/src/commands/budget.ts +10 -8
- package/src/commands/context.ts +35 -10
- package/src/commands/defaults.ts +69 -45
- package/src/commands/keymap.ts +9 -0
- package/src/commands/levels.ts +11 -5
- package/src/commands/live-context.ts +2 -0
- package/src/commands/registry.ts +10 -1
- package/src/content/collection-match.ts +6 -5
- package/src/content/collection-pages.ts +412 -0
- package/src/content/content-collections.ts +270 -0
- package/src/content/entry-commands.ts +51 -60
- package/src/content/entry-editor.ts +111 -18
- package/src/content/entry-fields.ts +13 -1
- package/src/content/entry-inspector.ts +225 -0
- package/src/content/entry-model.ts +17 -3
- package/src/content/entry-open.ts +80 -0
- package/src/content/entry-summaries.ts +440 -0
- package/src/content/file-record-ids.ts +80 -0
- package/src/content/file-records.ts +125 -0
- package/src/content/page-summaries.ts +129 -0
- package/src/content/record-entry.ts +201 -0
- package/src/content/writing-banner.ts +177 -0
- package/src/create/add-new-commands.ts +185 -0
- package/src/create/add-new-flow.ts +1116 -0
- package/src/create/add-new.ts +1014 -0
- package/src/create/element-name.ts +121 -0
- package/src/editor/convert-to-component.ts +67 -43
- package/src/editor/file-drop-action.ts +9 -1
- package/src/editor/inline-edit-apply.ts +44 -0
- package/src/editor/inline-edit.ts +10 -0
- package/src/editor/insert-component.ts +91 -0
- package/src/editor/insert-zone-action.ts +13 -4
- package/src/editor/shortcuts.ts +44 -22
- package/src/editor/slash-menu.ts +51 -8
- package/src/files/components.ts +74 -10
- package/src/files/file-ops.ts +15 -5
- package/src/files/files.ts +164 -150
- package/src/files/fs-events.ts +18 -7
- package/src/files/media-meta.ts +95 -1
- package/src/files/media-types.ts +55 -0
- package/src/files/media-upload.ts +209 -52
- package/src/files/new-file-name.ts +156 -0
- package/src/files/serialize-document.ts +13 -3
- package/src/format/format-host.ts +15 -0
- package/src/github/github-auth.ts +2 -2
- package/src/github/github-publish.ts +4 -3
- package/src/grid/grid-controller.ts +5 -1
- package/src/grid/grid-open.ts +0 -27
- package/src/grid/grid-panel.ts +54 -2
- package/src/grid/grid-reveal.ts +34 -0
- package/src/grid/grid-source.ts +3 -25
- package/src/grid/grid-view.ts +31 -6
- package/src/grid/sources/content-source.ts +75 -7
- package/src/grid/tabulator-tables.d.ts +6 -0
- package/src/hosting/document.ts +2 -2
- package/src/i18n/i18n-commands.ts +2 -1
- package/src/media/media-commands.ts +181 -0
- package/src/media/media-library-model.ts +232 -0
- package/src/media/media-library.ts +1283 -0
- package/src/media/media-pane.ts +47 -19
- package/src/packages/pull-package-sync.ts +1 -1
- package/src/panels/activity-panel.ts +21 -5
- package/src/panels/ai-chat/chat-markdown.ts +8 -8
- package/src/panels/block-action-bar.ts +11 -11
- package/src/panels/canvas-dnd-bridge.ts +5 -2
- package/src/panels/chat-panel.ts +6 -6
- package/src/panels/collections-panel.ts +306 -0
- package/src/panels/component-preview.ts +35 -13
- package/src/panels/data-explorer.ts +1 -1
- package/src/panels/data-grid.ts +4 -0
- package/src/panels/dnd.ts +17 -30
- package/src/panels/elements-panel.ts +9 -2
- package/src/panels/empty-state.ts +2 -2
- package/src/panels/formula-workspace.ts +2 -3
- package/src/panels/frontmatter-fields.ts +9 -9
- package/src/panels/git-diff-open.ts +9 -5
- package/src/panels/head-panel.ts +369 -103
- package/src/panels/i18n-panel.ts +11 -10
- package/src/panels/imports-panel.ts +2 -0
- package/src/panels/jump-bar.ts +27 -2
- package/src/panels/layers-panel.ts +36 -30
- package/src/panels/left-panel.ts +22 -8
- package/src/panels/navigator-panels.ts +23 -11
- package/src/panels/pane-context.ts +111 -29
- package/src/panels/pane-grid.ts +6 -6
- package/src/panels/panel-registry.ts +14 -9
- package/src/panels/properties-panel.ts +7 -3
- package/src/panels/right-panel.ts +199 -43
- package/src/panels/seo-modal.ts +41 -108
- package/src/panels/settings-menu.ts +52 -50
- package/src/panels/settings-pane.ts +268 -162
- package/src/panels/signals-panel.ts +3 -4
- package/src/panels/site-model.ts +367 -0
- package/src/panels/site-panel.ts +638 -0
- package/src/panels/style-panel.ts +37 -3
- package/src/panels/stylebook-doc.ts +142 -10
- package/src/panels/stylebook-layers-panel.ts +39 -80
- package/src/panels/stylebook-panel.ts +47 -10
- package/src/panels/styles-panel.ts +327 -0
- package/src/panels/tab-strip.ts +109 -25
- package/src/platform.ts +1 -1
- package/src/platforms/cloud.ts +28 -1
- package/src/platforms/default-platform.ts +4 -4
- package/src/platforms/devserver.ts +51 -25
- package/src/publish/deploy-checklist.ts +12 -7
- package/src/publish/pages-service.ts +38 -14
- package/src/publish/publish-changes.ts +491 -0
- package/src/publish/publish-commands.ts +83 -18
- package/src/publish/publish-ids.ts +49 -0
- package/src/publish/publish-quick.ts +148 -0
- package/src/publish/publish-workspace.ts +926 -0
- package/src/publish/source-control.ts +907 -0
- package/src/services/automation.ts +16 -20
- package/src/services/cf-settings.ts +4 -4
- package/src/services/connection.ts +2 -2
- package/src/services/directory-picker.ts +2 -2
- package/src/services/references.ts +0 -0
- package/src/services/render-critic.ts +21 -2
- package/src/services/settings/definitions.ts +25 -14
- package/src/settings/contributed-section.ts +49 -10
- package/src/settings/css-vars-editor.ts +93 -27
- package/src/settings/developer-panels.ts +43 -0
- package/src/settings/extension-sections.ts +20 -2
- package/src/settings/general-settings.ts +91 -21
- package/src/settings/preferences-dialog.ts +8 -1
- package/src/settings/project-sections.ts +27 -10
- package/src/settings/schema-field-ui.ts +3 -1
- package/src/settings/section-registry.ts +116 -11
- package/src/settings/settings-dialog.ts +192 -0
- package/src/settings/settings-document.ts +182 -162
- package/src/shell.ts +467 -431
- package/src/site-context.ts +64 -10
- package/src/state.ts +28 -9
- package/src/studio.ts +194 -41
- package/src/style/project-styles.ts +34 -0
- package/src/style/stylebook-specimens.ts +89 -0
- package/src/style/stylebook-target.ts +26 -0
- package/src/style/tag-inheritance.ts +105 -0
- package/src/style/variables-focus.ts +61 -0
- package/src/style/variables-view.ts +235 -0
- package/src/surfaces/add-new.json +1482 -0
- package/src/surfaces/add-new.ts +325 -0
- package/src/surfaces/ai-chat.ts +2 -2
- package/src/surfaces/boot-failure.json +4 -3
- package/src/surfaces/bottom-dock.json +1 -1
- package/src/surfaces/canvas-stage.json +30 -19
- package/src/surfaces/canvas-stage.ts +5 -5
- package/src/surfaces/collections-panel.json +615 -0
- package/src/surfaces/collections-panel.ts +168 -0
- package/src/surfaces/commandbar.json +241 -96
- package/src/surfaces/commandbar.ts +134 -99
- package/src/surfaces/empty-state.json +1 -1
- package/src/surfaces/entry-editor.json +136 -0
- package/src/surfaces/entry-editor.ts +21 -0
- package/src/surfaces/entry-inspector.json +371 -0
- package/src/surfaces/entry-inspector.ts +143 -0
- package/src/surfaces/inspector-dock.json +162 -14
- package/src/surfaces/inspector-dock.ts +46 -19
- package/src/surfaces/library-pane.json +670 -437
- package/src/surfaces/library-pane.ts +94 -59
- package/src/surfaces/media-browser.json +62 -6
- package/src/surfaces/media-browser.ts +15 -1
- package/src/surfaces/media-field.json +1 -1
- package/src/surfaces/media-library.json +1586 -0
- package/src/surfaces/media-library.ts +282 -0
- package/src/surfaces/media-pick-dialog.json +71 -0
- package/src/surfaces/media-pick-dialog.ts +132 -0
- package/src/surfaces/menu.ts +18 -0
- package/src/surfaces/pane-context.json +144 -77
- package/src/surfaces/pane-context.ts +24 -0
- package/src/surfaces/panel-deploy-checklist.json +12 -6
- package/src/surfaces/panel-deploy-checklist.ts +3 -1
- package/src/surfaces/panel-page.json +72 -2
- package/src/surfaces/panel-page.ts +38 -7
- package/src/surfaces/panel-signals.json +1 -1
- package/src/surfaces/panel-signals.ts +5 -5
- package/src/surfaces/panel-stylebook-layers.json +71 -204
- package/src/surfaces/panel-stylebook-layers.ts +0 -21
- package/src/surfaces/preferences.json +120 -48
- package/src/surfaces/preferences.ts +8 -0
- package/src/surfaces/publish-quick.json +428 -0
- package/src/surfaces/publish-quick.ts +165 -0
- package/src/surfaces/publish-workspace.json +2892 -0
- package/src/surfaces/publish-workspace.ts +391 -0
- package/src/surfaces/rail.json +1 -159
- package/src/surfaces/rail.ts +18 -75
- package/src/surfaces/schema-form.json +10 -2
- package/src/surfaces/schema-form.ts +2 -0
- package/src/surfaces/seo.json +1 -1
- package/src/surfaces/settings-contributed.json +5 -5
- package/src/surfaces/settings-contributed.ts +18 -2
- package/src/surfaces/settings-css-vars.json +44 -36
- package/src/surfaces/settings-css-vars.ts +77 -4
- package/src/surfaces/settings-deploy.json +2 -15
- package/src/surfaces/settings-deploy.ts +1 -1
- package/src/surfaces/settings-dialog.json +51 -0
- package/src/surfaces/settings-dialog.ts +169 -0
- package/src/surfaces/settings-overview.json +3 -3
- package/src/surfaces/settings-overview.ts +9 -2
- package/src/surfaces/settings-pane.json +269 -46
- package/src/surfaces/settings-pane.ts +85 -24
- package/src/surfaces/shell.json +2 -1
- package/src/surfaces/site-panel.json +1029 -0
- package/src/surfaces/site-panel.ts +233 -0
- package/src/surfaces/slash-menu.json +63 -21
- package/src/surfaces/slash-menu.ts +23 -0
- package/src/surfaces/statusbar.ts +39 -15
- package/src/surfaces/styles-panel.json +735 -0
- package/src/surfaces/styles-panel.ts +195 -0
- package/src/surfaces/styles-variables.json +149 -0
- package/src/surfaces/styles-variables.ts +145 -0
- package/src/surfaces/tab-strip.json +4 -4
- package/src/surfaces/welcome.ts +1 -1
- package/src/surfaces/writing-banner.json +94 -0
- package/src/surfaces/writing-banner.ts +80 -0
- package/src/tabs/bare-repeaters.ts +101 -0
- package/src/tabs/tab.ts +47 -7
- package/src/types.ts +21 -26
- package/src/ui/form-controls.ts +47 -8
- package/src/ui/layers.ts +13 -0
- package/src/ui/media-picker.ts +284 -40
- package/src/ui/regions.ts +7 -6
- package/src/ui/schema-form.ts +259 -27
- package/src/ui/virtual-window.ts +26 -1
- package/src/utils/base64.ts +3 -3
- package/src/utils/edit-display.ts +322 -67
- package/src/utils/local-day.ts +18 -0
- package/src/workspace/pane-derive.ts +4 -4
- package/src/workspace/session.ts +28 -7
- package/src/workspace/tab-workspace.ts +137 -0
- package/src/workspace/workspace-back.ts +189 -0
- package/src/workspace/workspace-home.ts +133 -0
- package/src/workspace/workspace-ids.ts +129 -0
- package/src/workspace/workspace-switch.ts +372 -0
- package/src/workspace/workspace.ts +125 -11
- package/styles/shell-frame.css +30 -7
- package/styles/shell-frame.json +15 -3
- package/dist/chunks/2020-f2gkpy8d.js +0 -4
- package/dist/chunks/adapter-4912g339.js +0 -4
- package/dist/chunks/cf-account-picker-wccaenh6.js +0 -4
- package/dist/chunks/cf-settings-br6105tr.js +0 -4
- package/dist/chunks/color-28ksk2qm.js +0 -4
- package/dist/chunks/components-db68vsn7.js +0 -4
- package/dist/chunks/content-source-1449qesq.js +0 -4
- package/dist/chunks/convert-to-repeater-5h457egj.js +0 -4
- package/dist/chunks/editor-x9jn86a7.js +0 -4
- package/dist/chunks/entry-commands-may24cpt.js +0 -4
- package/dist/chunks/extension-sections-pvr14vxr.js +0 -4
- package/dist/chunks/file-ops-x9v54pcj.js +0 -4
- package/dist/chunks/files-j3knqf0s.js +0 -4
- package/dist/chunks/format-host-tyre2trb.js +0 -4
- package/dist/chunks/fs-events-64ma9htc.js +0 -4
- package/dist/chunks/git-panel-84ps5dex.js +0 -4
- package/dist/chunks/iframe-entry-fvbk89gm.js +0 -5
- package/dist/chunks/iframe-host-ck6vejt5.js +0 -4
- package/dist/chunks/index-b8xw3vy9.js +0 -4
- package/dist/chunks/jsonMode-w15cmrcm.js +0 -13
- package/dist/chunks/jx-validate-at3zn8wa.js +0 -4
- package/dist/chunks/layers-h4xp1pss.js +0 -4
- package/dist/chunks/layers-panel-m5hw1d7g.js +0 -4
- package/dist/chunks/lit-html-8t8t3dnr.js +0 -4
- package/dist/chunks/media-picker-2cweexc5.js +0 -4
- package/dist/chunks/menu-ynw13y0s.js +0 -4
- package/dist/chunks/monaco-buffer-qzxn567r.js +0 -4
- package/dist/chunks/monaco-setup-gxf6656g.js +0 -4
- package/dist/chunks/notify-cj5zbrs2.js +0 -4
- package/dist/chunks/parse-b7ddhr8m.js +0 -4
- package/dist/chunks/platform-3dfws9js.js +0 -4
- package/dist/chunks/popover-jt2gxmzs.js +0 -4
- package/dist/chunks/publish-panel-1gvxbj21.js +0 -4
- package/dist/chunks/quick-search-0dajrwqg.js +0 -4
- package/dist/chunks/references-49qgt404.js +0 -4
- package/dist/chunks/right-panel-jm1bwe53.js +0 -4
- package/dist/chunks/section-registry-6kwc4fky.js +0 -4
- package/dist/chunks/studio-0rt52vpq.js +0 -5
- package/dist/chunks/studio-18vgbhs5.js +0 -5
- package/dist/chunks/studio-24mccdrj.js +0 -5
- package/dist/chunks/studio-2eh507zk.js +0 -9
- package/dist/chunks/studio-2hcbccfv.js +0 -5
- package/dist/chunks/studio-2jbs3rxv.js +0 -5
- package/dist/chunks/studio-4h8k4dw9.js +0 -5
- package/dist/chunks/studio-5f6n18kx.js +0 -5
- package/dist/chunks/studio-5gthpky6.js +0 -5
- package/dist/chunks/studio-6ep3js6b.js +0 -9
- package/dist/chunks/studio-6wkmf3wz.js +0 -9
- package/dist/chunks/studio-841rf3cg.js +0 -5
- package/dist/chunks/studio-8gmwgheg.js +0 -5
- package/dist/chunks/studio-8qm0ep50.js +0 -5
- package/dist/chunks/studio-924p1tm1.js +0 -6
- package/dist/chunks/studio-a3shpz9y.js +0 -5
- package/dist/chunks/studio-amsbaaar.js +0 -6
- package/dist/chunks/studio-at5spjbc.js +0 -7
- package/dist/chunks/studio-b2xv945e.js +0 -5
- package/dist/chunks/studio-bn5nkafw.js +0 -5
- package/dist/chunks/studio-bw6q5xjf.js +0 -9
- package/dist/chunks/studio-c7q0t9x1.js +0 -5
- package/dist/chunks/studio-camnhc96.js +0 -6
- package/dist/chunks/studio-cffqvnzd.js +0 -5
- package/dist/chunks/studio-efvfrsf8.js +0 -5
- package/dist/chunks/studio-er30bpcx.js +0 -5
- package/dist/chunks/studio-ezscgr64.js +0 -5
- package/dist/chunks/studio-ffrrncvz.js +0 -5
- package/dist/chunks/studio-fpphgn8y.js +0 -10
- package/dist/chunks/studio-j1kvhdjk.js +0 -4
- package/dist/chunks/studio-j430n16g.js +0 -7
- package/dist/chunks/studio-jjkg84nj.js +0 -5
- package/dist/chunks/studio-m0q32ars.js +0 -4
- package/dist/chunks/studio-m6bdst11.js +0 -5
- package/dist/chunks/studio-nv2cptd4.js +0 -64
- package/dist/chunks/studio-nwfrc98n.js +0 -5
- package/dist/chunks/studio-p1wgvp7r.js +0 -5
- package/dist/chunks/studio-pag2wncm.js +0 -5
- package/dist/chunks/studio-qvanvzfd.js +0 -5
- package/dist/chunks/studio-qwa64kt7.js +0 -5
- package/dist/chunks/studio-rk1zc4x3.js +0 -928
- package/dist/chunks/studio-se2r3ptc.js +0 -5
- package/dist/chunks/studio-sg2a0dh0.js +0 -5
- package/dist/chunks/studio-tjzjhe68.js +0 -5
- package/dist/chunks/studio-v2w7q8r1.js +0 -7
- package/dist/chunks/studio-v5vjq1h0.js +0 -4
- package/dist/chunks/studio-v8aga3vp.js +0 -14
- package/dist/chunks/studio-vfcdrfmv.js +0 -5
- package/dist/chunks/studio-vmfkgdjf.js +0 -5
- package/dist/chunks/studio-wt1816jj.js +0 -5
- package/dist/chunks/studio-wz73q5kq.js +0 -4
- package/dist/chunks/studio-y04qtxvp.js +0 -7
- package/dist/chunks/studio-y21g8zzg.js +0 -5
- package/dist/chunks/studio-y64hrt8p.js +0 -12
- package/dist/chunks/studio-ynn90f58.js +0 -16
- package/dist/chunks/studio-yxz682sx.js +0 -5
- package/dist/chunks/studio-zp7snvkq.js +0 -5
- package/dist/chunks/studio-zq4z6tm6.js +0 -5
- package/dist/chunks/toast-c9fn075p.js +0 -4
- package/dist/chunks/tsMode-sf49ee9x.js +0 -14
- package/dist/chunks/workspace-jtfahpee.js +0 -4
- package/src/panels/frontmatter-panel.ts +0 -711
- package/src/panels/git-panel.ts +0 -1108
- package/src/surfaces/doc-header.json +0 -776
- package/src/surfaces/doc-header.ts +0 -222
- package/src/surfaces/git-panel.json +0 -1880
- package/src/surfaces/git-panel.ts +0 -264
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the Collections panel says about each entry while one is being written: its title, its
|
|
3
|
+
* status and a thumbnail (`specs/studio.md` §22.5, the Content-Entry board's list).
|
|
4
|
+
*
|
|
5
|
+
* The scan behind the Content home knows each entry's FILE (`browse/library-model.ts`); a title and
|
|
6
|
+
* a draft flag are inside it. So this reads each entry once — a JSON entry's root, a Markdown
|
|
7
|
+
* entry's frontmatter, through the same parse the editor uses — and keeps the answer until a file
|
|
8
|
+
* event drops it ({@link invalidateEntrySummaries}, wired beside the scan's own invalidation in
|
|
9
|
+
* `studio.ts`). A collection is read when its list is first shown, not before: the panel lists one
|
|
10
|
+
* collection's entries at a time.
|
|
11
|
+
*
|
|
12
|
+
* The read never rejects. An entry that cannot be read is listed under its file name with no
|
|
13
|
+
* status, which is what the file list would have said about it anyway.
|
|
14
|
+
*
|
|
15
|
+
* @docs studio/projects/browse
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
isBoolean,
|
|
20
|
+
isJsonObject,
|
|
21
|
+
isNonNullObject,
|
|
22
|
+
isNumber,
|
|
23
|
+
isString,
|
|
24
|
+
} from "@jxsuite/schema/guards";
|
|
25
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
26
|
+
import { isImage } from "../files/media-upload";
|
|
27
|
+
import { previewFileSrc } from "../files/media-paths";
|
|
28
|
+
import { hasDraftAxis, isDraftEntry } from "./draft-state";
|
|
29
|
+
import { entryCollection } from "./entry-model";
|
|
30
|
+
import { PAGES_COLLECTION, collectionKeyOfPath } from "./content-collections";
|
|
31
|
+
import type { EntryFields } from "./entry-fields";
|
|
32
|
+
import type { ContentTypeSchema, ContentTypeSchemaField } from "@jxsuite/schema/types";
|
|
33
|
+
|
|
34
|
+
/** One entry, as the Collections panel lists it. */
|
|
35
|
+
export interface EntrySummary {
|
|
36
|
+
path: string;
|
|
37
|
+
/** The entry's `title`, or its file name without the extension. */
|
|
38
|
+
title: string;
|
|
39
|
+
/** "Draft", "Published", or "" for a collection with no draft workflow. */
|
|
40
|
+
status: string;
|
|
41
|
+
/** A preview URL for the entry's first image field, or "" for none. */
|
|
42
|
+
thumb: string;
|
|
43
|
+
/** The entry's own date as `YYYY-MM-DD`, from its schema's date field, or "" for none. */
|
|
44
|
+
date: string;
|
|
45
|
+
/**
|
|
46
|
+
* Each field the collection's schema declares and the entry holds, as the text a list cell shows.
|
|
47
|
+
* Absent from a summary the read did not reach.
|
|
48
|
+
*/
|
|
49
|
+
fields?: EntryFieldTexts;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** An entry's declared fields as list text, keyed by field name. */
|
|
53
|
+
export type EntryFieldTexts = Readonly<Record<string, string>>;
|
|
54
|
+
|
|
55
|
+
/** Field names that mean "what this entry is called", in the order they are preferred. */
|
|
56
|
+
const TITLE_FIELD_NAMES = ["title", "name", "label", "heading"];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The field an entry is called by: `title` where there is one, else the first conventionally named
|
|
60
|
+
* field (`name`, `label`, `heading`) the schema declares or the entry holds. A menu item with a
|
|
61
|
+
* `name` and no `title` is "Espresso Tiramisu", not `tiramisu`.
|
|
62
|
+
*/
|
|
63
|
+
export function titleFieldOf(
|
|
64
|
+
schema: ContentTypeSchema | null | undefined,
|
|
65
|
+
fields: EntryFields = {},
|
|
66
|
+
): string | null {
|
|
67
|
+
// Whatever its capitalisation: a CSV's header row is often `Title`, and it is still the title.
|
|
68
|
+
const named = (keys: readonly string[]): string | null => {
|
|
69
|
+
for (const wanted of TITLE_FIELD_NAMES) {
|
|
70
|
+
const key = keys.find((candidate) => candidate.toLowerCase() === wanted);
|
|
71
|
+
if (key !== undefined) {
|
|
72
|
+
return key;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
return (
|
|
78
|
+
named(Object.keys(schema?.properties ?? {})) ??
|
|
79
|
+
named(Object.keys(fields).filter((key) => isString(fields[key])))
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A field value as list text: a scalar as written, a list of scalars joined, anything else "". */
|
|
84
|
+
export function fieldText(value: EntryFields[string] | Date | undefined): string {
|
|
85
|
+
if (value instanceof Date) {
|
|
86
|
+
return entryDay(value);
|
|
87
|
+
}
|
|
88
|
+
if (isString(value)) {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
if (isNumber(value) || isBoolean(value)) {
|
|
92
|
+
return String(value);
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
return value
|
|
96
|
+
.filter((item) => isString(item) || isNumber(item))
|
|
97
|
+
.map(String)
|
|
98
|
+
.join(", ");
|
|
99
|
+
}
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The text of every field the schema declares and the entry holds. */
|
|
104
|
+
function fieldTexts(
|
|
105
|
+
schema: ContentTypeSchema | null | undefined,
|
|
106
|
+
fields: EntryFields,
|
|
107
|
+
): EntryFieldTexts {
|
|
108
|
+
return Object.fromEntries(
|
|
109
|
+
Object.keys(schema?.properties ?? {})
|
|
110
|
+
.filter((name) => name in fields)
|
|
111
|
+
.map((name) => [name, fieldText(fields[name])]),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Field names that mean "when this entry is dated" when the schema types no field as a date. */
|
|
116
|
+
const DATE_FIELD_NAMES = ["date", "published", "publishedAt", "pubDate", "publishDate"];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The field an entry is dated by: the first schema property typed `format: date` or `date-time`,
|
|
120
|
+
* else the first conventionally named one present in the entry.
|
|
121
|
+
*/
|
|
122
|
+
export function dateFieldOf(
|
|
123
|
+
schema: ContentTypeSchema | null | undefined,
|
|
124
|
+
fields: EntryFields,
|
|
125
|
+
): string | null {
|
|
126
|
+
for (const [name, field] of Object.entries(schema?.properties ?? {})) {
|
|
127
|
+
if (field.format === "date" || field.format === "date-time") {
|
|
128
|
+
return name;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return DATE_FIELD_NAMES.find((name) => name in fields) ?? null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The field a collection's SCHEMA says its entries are dated by, before any entry is read: the
|
|
136
|
+
* first property typed `format: date` or `date-time`, else the first conventionally named property
|
|
137
|
+
* it declares (a `date` with no format is still the date). The Table draws that field in its Date
|
|
138
|
+
* column, so it is the one field column it must not draw a second time.
|
|
139
|
+
*/
|
|
140
|
+
export function declaredDateField(schema: ContentTypeSchema | null | undefined): string | null {
|
|
141
|
+
const properties = schema?.properties ?? {};
|
|
142
|
+
return dateFieldOf(schema, Object.fromEntries(Object.keys(properties).map((name) => [name, ""])));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A date value as `YYYY-MM-DD`, or "". A value that already starts with a calendar date is taken as
|
|
147
|
+
* written, so a date is never shifted a day by a time zone; anything else must parse.
|
|
148
|
+
*/
|
|
149
|
+
export function entryDay(value: EntryFields[string] | Date): string {
|
|
150
|
+
if (value instanceof Date) {
|
|
151
|
+
return Number.isNaN(value.getTime()) ? "" : value.toISOString().slice(0, 10);
|
|
152
|
+
}
|
|
153
|
+
if (!isString(value)) {
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
const written = /^(\d{4}-\d{2}-\d{2})/.exec(value.trim());
|
|
157
|
+
if (written) {
|
|
158
|
+
return written[1]!;
|
|
159
|
+
}
|
|
160
|
+
const parsed = new Date(value);
|
|
161
|
+
if (value.trim() === "" || Number.isNaN(parsed.getTime())) {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
if (!value.includes(":")) {
|
|
165
|
+
/* A date with no time of day (`6/3/2026`, `March 4, 2026`) parses as LOCAL midnight, so its
|
|
166
|
+
UTC day is the day before anywhere east of Greenwich: the day is read where it was parsed. */
|
|
167
|
+
const month = String(parsed.getMonth() + 1).padStart(2, "0");
|
|
168
|
+
const day = String(parsed.getDate()).padStart(2, "0");
|
|
169
|
+
return `${parsed.getFullYear()}-${month}-${day}`;
|
|
170
|
+
}
|
|
171
|
+
return parsed.toISOString().slice(0, 10);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The file name without its extension: `content/posts/hello.md` → `hello`. */
|
|
175
|
+
export function entryStem(path: string): string {
|
|
176
|
+
const base = path.split("/").pop() ?? path;
|
|
177
|
+
const dot = base.lastIndexOf(".");
|
|
178
|
+
return dot > 0 ? base.slice(0, dot) : base;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The project path an image field names, resolved the way the entry's own page resolves it: a
|
|
183
|
+
* root-relative URL is served from `public/`, anything else is relative to the entry's folder.
|
|
184
|
+
*/
|
|
185
|
+
export function imageFieldPath(entryPath: string, value: string): string {
|
|
186
|
+
if (value.startsWith("/")) {
|
|
187
|
+
return `public${value}`;
|
|
188
|
+
}
|
|
189
|
+
const dir = entryPath.includes("/") ? entryPath.slice(0, entryPath.lastIndexOf("/")) : "";
|
|
190
|
+
const parts = [...(dir === "" ? [] : dir.split("/")), ...value.split("/")];
|
|
191
|
+
const out: string[] = [];
|
|
192
|
+
for (const part of parts) {
|
|
193
|
+
if (part === "" || part === ".") {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (part === "..") {
|
|
197
|
+
out.pop();
|
|
198
|
+
} else {
|
|
199
|
+
out.push(part);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return out.join("/");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A URL with a scheme (`https://…`, `data:…`): a picture somewhere else, shown as it is. */
|
|
206
|
+
const ABSOLUTE_URL = /^[a-z][\d+.a-z-]*:/i;
|
|
207
|
+
|
|
208
|
+
/** An image field's value as the URL a thumbnail is drawn from, or "" for no picture. */
|
|
209
|
+
function thumbSrc(entryPath: string, value: string): string {
|
|
210
|
+
const trimmed = value.trim();
|
|
211
|
+
if (trimmed === "") {
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
return ABSOLUTE_URL.test(trimmed) ? trimmed : previewFileSrc(imageFieldPath(entryPath, trimmed));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The picture a field holds, when it is one: an image path, an image object's `src`
|
|
219
|
+
* (`featuredImage: { src, alt }`), or a list of them (its first).
|
|
220
|
+
*/
|
|
221
|
+
function pictureIn(value: EntryFields[string]): string {
|
|
222
|
+
if (isString(value)) {
|
|
223
|
+
// A CSV keeps a gallery as one cell, its URLs separated by commas.
|
|
224
|
+
return value.split(",")[0]?.trim() ?? "";
|
|
225
|
+
}
|
|
226
|
+
if (Array.isArray(value)) {
|
|
227
|
+
const [first] = value;
|
|
228
|
+
return first === undefined ? "" : pictureIn(first);
|
|
229
|
+
}
|
|
230
|
+
if (isJsonObject(value)) {
|
|
231
|
+
const { src } = value;
|
|
232
|
+
return isString(src) ? src : "";
|
|
233
|
+
}
|
|
234
|
+
return "";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Whether a schema property holds a picture: `format: image`, an image object, or a list of either. */
|
|
238
|
+
function isPictureField(field: ContentTypeSchemaField): boolean {
|
|
239
|
+
if (field.format === "image") {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
const src = field.properties?.["src"];
|
|
243
|
+
if (field.type === "object" && src?.type === "string") {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
return field.type === "array" && field.items !== undefined && isPictureField(field.items);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* An entry's thumbnail: the first field its SCHEMA declares as a picture that holds one, so a post
|
|
251
|
+
* is shown by its `cover` and not by whatever image path happens to come first; else the first
|
|
252
|
+
* field whose value is an image file. A preview URL, or "" for none.
|
|
253
|
+
*/
|
|
254
|
+
export function entryThumb(
|
|
255
|
+
entryPath: string,
|
|
256
|
+
fields: EntryFields,
|
|
257
|
+
schema: ContentTypeSchema | null | undefined,
|
|
258
|
+
): string {
|
|
259
|
+
for (const [name, field] of Object.entries(schema?.properties ?? {})) {
|
|
260
|
+
if (isPictureField(field)) {
|
|
261
|
+
const src = thumbSrc(entryPath, pictureIn(fields[name]));
|
|
262
|
+
if (src !== "") {
|
|
263
|
+
return src;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const value of Object.values(fields)) {
|
|
268
|
+
if (!isString(value) || ABSOLUTE_URL.test(value)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const dot = value.lastIndexOf(".");
|
|
272
|
+
if (dot > 0 && isImage(value.slice(dot))) {
|
|
273
|
+
return previewFileSrc(imageFieldPath(entryPath, value));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return "";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** One entry's summary, from its fields. Pure. */
|
|
280
|
+
export function summarize(path: string, collection: string, fields: EntryFields): EntrySummary {
|
|
281
|
+
const schema = entryCollection(collection)?.schema ?? null;
|
|
282
|
+
const titleField = titleFieldOf(schema, fields);
|
|
283
|
+
const title = titleField === null ? undefined : fields[titleField];
|
|
284
|
+
const draft = isDraftEntry(fields);
|
|
285
|
+
const dateField = dateFieldOf(schema, fields);
|
|
286
|
+
return {
|
|
287
|
+
date: dateField === null ? "" : entryDay(fields[dateField]),
|
|
288
|
+
fields: fieldTexts(schema, fields),
|
|
289
|
+
path,
|
|
290
|
+
status: hasDraftAxis(schema, fields) ? (draft ? "Draft" : "Published") : "",
|
|
291
|
+
thumb: entryThumb(path, fields, schema),
|
|
292
|
+
title: isString(title) && title.trim() !== "" ? title : entryStem(path),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Read one entry's fields: a JSON entry's root, any other entry's frontmatter. */
|
|
297
|
+
async function readFields(path: string): Promise<EntryFields> {
|
|
298
|
+
try {
|
|
299
|
+
const source = await getPlatform().readFile(path);
|
|
300
|
+
if (path.endsWith(".json")) {
|
|
301
|
+
const parsed: unknown = JSON.parse(source);
|
|
302
|
+
/* SAFETY: a JSON entry is an object of fields (its collection's schema says so); a value
|
|
303
|
+
that is not an object has no fields, which is what the fallback below says. */
|
|
304
|
+
return isNonNullObject(parsed) && !Array.isArray(parsed) ? (parsed as EntryFields) : {};
|
|
305
|
+
}
|
|
306
|
+
const { parseSourceForPath } = await import("../files/file-ops");
|
|
307
|
+
const parsed = await parseSourceForPath(path, source);
|
|
308
|
+
return parsed.frontmatter;
|
|
309
|
+
} catch {
|
|
310
|
+
return {};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── The cache ───────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
/** Entries read, keyed by collection and then by the paths listed, so a changed list re-reads. */
|
|
317
|
+
const _cache = new Map<string, { key: string; entries: EntrySummary[] }>();
|
|
318
|
+
const _loading = new Map<string, Promise<EntrySummary[]>>();
|
|
319
|
+
let _epoch = 0;
|
|
320
|
+
const _listeners = new Set<() => void>();
|
|
321
|
+
|
|
322
|
+
/** At most this many entries are read for one list; the rest are listed by file name. */
|
|
323
|
+
export const SUMMARY_READ_LIMIT = 200;
|
|
324
|
+
|
|
325
|
+
/** Read a collection's entries. */
|
|
326
|
+
async function readSummaries(
|
|
327
|
+
collection: string,
|
|
328
|
+
paths: readonly string[],
|
|
329
|
+
): Promise<EntrySummary[]> {
|
|
330
|
+
const read = paths.slice(0, SUMMARY_READ_LIMIT);
|
|
331
|
+
const summaries = await Promise.all(
|
|
332
|
+
read.map(async (path) => summarize(path, collection, await readFields(path))),
|
|
333
|
+
);
|
|
334
|
+
const rest = paths.slice(SUMMARY_READ_LIMIT).map((path): EntrySummary => ({
|
|
335
|
+
date: "",
|
|
336
|
+
path,
|
|
337
|
+
status: "",
|
|
338
|
+
thumb: "",
|
|
339
|
+
title: entryStem(path),
|
|
340
|
+
}));
|
|
341
|
+
return [...summaries, ...rest];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* A collection's entries as they stand: the summaries, or `null` while they are being read (which
|
|
346
|
+
* starts the read; listeners are told when it lands).
|
|
347
|
+
*
|
|
348
|
+
* @param collection - The collection's name.
|
|
349
|
+
* @param paths - Its entry files, in the order to list them.
|
|
350
|
+
*/
|
|
351
|
+
export function peekEntrySummaries(
|
|
352
|
+
collection: string,
|
|
353
|
+
paths: readonly string[],
|
|
354
|
+
): EntrySummary[] | null {
|
|
355
|
+
const key = paths.join("\n");
|
|
356
|
+
const cached = _cache.get(collection);
|
|
357
|
+
if (cached?.key === key) {
|
|
358
|
+
return cached.entries;
|
|
359
|
+
}
|
|
360
|
+
if (!hasPlatform()) {
|
|
361
|
+
return paths.map((path) => ({ date: "", path, status: "", thumb: "", title: entryStem(path) }));
|
|
362
|
+
}
|
|
363
|
+
const inFlight = `${collection}\n${key}`;
|
|
364
|
+
if (!_loading.has(inFlight)) {
|
|
365
|
+
const epoch = _epoch;
|
|
366
|
+
const read: Promise<EntrySummary[]> = readSummaries(collection, paths).then((entries) => {
|
|
367
|
+
/* Only this read's own record: an invalidation in between may have handed the key to a newer
|
|
368
|
+
read, and deleting THAT record would let the next peek start a duplicate of it. */
|
|
369
|
+
if (_loading.get(inFlight) === read) {
|
|
370
|
+
_loading.delete(inFlight);
|
|
371
|
+
}
|
|
372
|
+
if (epoch === _epoch) {
|
|
373
|
+
_cache.set(collection, { entries, key });
|
|
374
|
+
for (const listener of _listeners) {
|
|
375
|
+
listener();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return entries;
|
|
379
|
+
});
|
|
380
|
+
_loading.set(inFlight, read);
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Restate one entry from the fields Studio just saved, without reading the file back.
|
|
387
|
+
*
|
|
388
|
+
* A save is Studio's own write, so the fields it wrote are the answer, and re-reading them is both
|
|
389
|
+
* slower and wrong: the dev server lets a file read be cached for five seconds, and its watcher
|
|
390
|
+
* skips some trees outright, so the list kept the old title after an author renamed an entry and
|
|
391
|
+
* pressed Save. An entry no cached list holds is left to that list's own first read.
|
|
392
|
+
*
|
|
393
|
+
* @returns Whether a cached list held the entry and now states the saved fields.
|
|
394
|
+
*/
|
|
395
|
+
export function noteEntrySaved(path: string, collection: string, fields: EntryFields): boolean {
|
|
396
|
+
const cached = _cache.get(collection);
|
|
397
|
+
const index = cached?.entries.findIndex((entry) => entry.path === path) ?? -1;
|
|
398
|
+
if (!cached || index === -1) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
const entries = cached.entries.with(index, summarize(path, collection, fields));
|
|
402
|
+
_cache.set(collection, { entries, key: cached.key });
|
|
403
|
+
for (const listener of _listeners) {
|
|
404
|
+
listener();
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* After a document saved: restate its entry, when it is one, from the fields it saved.
|
|
411
|
+
*
|
|
412
|
+
* The caller says where the fields live (`entryFields(tab)`, the live record, not a copy), which
|
|
413
|
+
* keeps the editor's field store out of this data module's graph.
|
|
414
|
+
*
|
|
415
|
+
* @param path The saved document's path, or `null` for one with no file.
|
|
416
|
+
* @param fields The saved document's fields: a JSON entry's root, any other entry's frontmatter.
|
|
417
|
+
* @returns Whether a cached list changed, so the caller knows a repaint is due.
|
|
418
|
+
*/
|
|
419
|
+
export function noteSavedEntry(path: string | null, fields: EntryFields): boolean {
|
|
420
|
+
const collection = collectionKeyOfPath(path);
|
|
421
|
+
if (path === null || collection === null || collection === PAGES_COLLECTION) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
return noteEntrySaved(path, collection, fields);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Forget every summary. The next list re-reads its entries. */
|
|
428
|
+
export function invalidateEntrySummaries(): void {
|
|
429
|
+
_epoch += 1;
|
|
430
|
+
_cache.clear();
|
|
431
|
+
_loading.clear();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Be told when a read lands. Returns the function that stops it. */
|
|
435
|
+
export function onEntrySummariesRead(listener: () => void): () => void {
|
|
436
|
+
_listeners.add(listener);
|
|
437
|
+
return () => {
|
|
438
|
+
_listeners.delete(listener);
|
|
439
|
+
};
|
|
440
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ids of a file-backed collection's records, as the content loader gives them.
|
|
3
|
+
*
|
|
4
|
+
* A collection whose `source` names one file (`./content/projects.csv`) has one entry per record,
|
|
5
|
+
* and the build names each one: a CSV row by the collection's `idField`, else its `id`, `sku`,
|
|
6
|
+
* `slug` or `Slug` cell, else its row number from 0 (`coerceCSVRows` in `@jxsuite/parser`); an
|
|
7
|
+
* object of a JSON array by its `id`, else `<file stem>-<n>` (`loadJSONEntries`). The Site panel
|
|
8
|
+
* counts these and a reference picker offers them, so both must agree with the build.
|
|
9
|
+
*
|
|
10
|
+
* A leaf over the file's text: `grid/sources/content-source.ts` reads the file and hands it here.
|
|
11
|
+
* `content/file-records.ts` names the same records for the Content home, but it reaches the entry
|
|
12
|
+
* summaries, which reach the content source, so this module imports only the CSV codec.
|
|
13
|
+
*
|
|
14
|
+
* @docs studio/projects/browse
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { isNonNullObject, isNumber, isString } from "@jxsuite/schema/guards";
|
|
18
|
+
import { parseCsv } from "../grid/csv-codec";
|
|
19
|
+
import type { JsonValue } from "@jxsuite/schema/types";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The ids the CSV loader gives each data row; blank rows are skipped, as the Content home skips
|
|
23
|
+
* them.
|
|
24
|
+
*/
|
|
25
|
+
function csvIds(text: string, idField: string | undefined): string[] {
|
|
26
|
+
const { headers, rows } = parseCsv(text);
|
|
27
|
+
const candidates = [idField, "id", "sku", "slug", "Slug"].flatMap((name) => {
|
|
28
|
+
const at = name === undefined ? -1 : headers.indexOf(name);
|
|
29
|
+
return at === -1 ? [] : [at];
|
|
30
|
+
});
|
|
31
|
+
return rows
|
|
32
|
+
.filter((row) => row.some((cell) => cell.trim() !== ""))
|
|
33
|
+
.map((row, index) => {
|
|
34
|
+
const at = candidates.find((candidate) => (row[candidate] ?? "") !== "");
|
|
35
|
+
return at === undefined ? String(index) : row[at]!;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The ids the JSON loader gives: each object of an array, `<stem>-<n>` where it names none; a file
|
|
41
|
+
* that is ONE object (`docs/nav.json`) is one entry, its own `id` else the file's stem
|
|
42
|
+
* (`loadJSONEntries`). `[]` for text that is not JSON.
|
|
43
|
+
*/
|
|
44
|
+
function jsonIds(text: string, stem: string): string[] {
|
|
45
|
+
let parsed: JsonValue;
|
|
46
|
+
try {
|
|
47
|
+
// SAFETY: JSON.parse only ever yields JSON values; its `any` return type says nothing more.
|
|
48
|
+
parsed = JSON.parse(text) as JsonValue;
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const idOf = (item: JsonValue, fallback: string): string => {
|
|
53
|
+
const id = isNonNullObject(item) && !Array.isArray(item) ? item.id : undefined;
|
|
54
|
+
return isString(id) || isNumber(id) ? String(id) : fallback;
|
|
55
|
+
};
|
|
56
|
+
if (!Array.isArray(parsed)) {
|
|
57
|
+
return [idOf(parsed, stem)];
|
|
58
|
+
}
|
|
59
|
+
return parsed.map((item, index) => idOf(item, `${stem}-${index}`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The record ids a collection file holds.
|
|
64
|
+
*
|
|
65
|
+
* @param path The file's project-relative path; its extension says how to read it.
|
|
66
|
+
* @param text The file's text.
|
|
67
|
+
* @param idField The collection's declared `idField`, if any.
|
|
68
|
+
* @returns The ids in file order, or `[]` for a file kind that holds no records Studio reads.
|
|
69
|
+
*/
|
|
70
|
+
export function fileRecordIds(path: string, text: string, idField?: string): string[] {
|
|
71
|
+
const lower = path.toLowerCase();
|
|
72
|
+
if (lower.endsWith(".csv")) {
|
|
73
|
+
return csvIds(text, idField);
|
|
74
|
+
}
|
|
75
|
+
if (lower.endsWith(".json")) {
|
|
76
|
+
const stem = (path.split("/").pop() ?? path).replace(/\.json$/i, "");
|
|
77
|
+
return jsonIds(text, stem);
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entries of a file-backed collection: the records of the one file its `source` names.
|
|
3
|
+
*
|
|
4
|
+
* A collection whose `source` is a file (`content.pricing.source: "./content/pricing/prices.csv"`)
|
|
5
|
+
* has one entry per record, exactly as the parser's loader reads it: a CSV row under the header, or
|
|
6
|
+
* an object of a JSON array. The Content workspace lists those records, so a fifteen-row price list
|
|
7
|
+
* reads "Pricing 15" with a row per record rather than "Pricing 1" and the file's name
|
|
8
|
+
* (`specs/studio.md` §22.5). The records are written in the file's own grid, which opening any of
|
|
9
|
+
* them opens.
|
|
10
|
+
*
|
|
11
|
+
* Pure over the file's text: the scan (`browse/library-source.ts`) reads the file and hands it
|
|
12
|
+
* here.
|
|
13
|
+
*
|
|
14
|
+
* @docs studio/projects/browse
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { isNonNullObject } from "@jxsuite/schema/guards";
|
|
18
|
+
import { parseCsv } from "../grid/csv-codec";
|
|
19
|
+
import { fieldText, titleFieldOf } from "./entry-summaries";
|
|
20
|
+
import type { ContentTypeSchema, JsonValue } from "@jxsuite/schema/types";
|
|
21
|
+
|
|
22
|
+
/** One record of a file-backed collection. */
|
|
23
|
+
export interface FileRecord {
|
|
24
|
+
/** Its position among the file's records, from 0. */
|
|
25
|
+
index: number;
|
|
26
|
+
/**
|
|
27
|
+
* Its position among the file's data rows, from 0, blank rows counted: the row its file's grid
|
|
28
|
+
* shows it in, which a blank row above it makes differ from {@link index}.
|
|
29
|
+
*/
|
|
30
|
+
row: number;
|
|
31
|
+
/** What it is called: its title field, else its first text, else its row number. */
|
|
32
|
+
title: string;
|
|
33
|
+
/** Every field it holds, as list text, keyed by field name. */
|
|
34
|
+
fields: Readonly<Record<string, string>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The title of one record, as the Content home names it. */
|
|
38
|
+
function recordTitle(
|
|
39
|
+
schema: ContentTypeSchema | null | undefined,
|
|
40
|
+
fields: Readonly<Record<string, string>>,
|
|
41
|
+
index: number,
|
|
42
|
+
fallback: string,
|
|
43
|
+
): string {
|
|
44
|
+
const field = titleFieldOf(schema, fields);
|
|
45
|
+
const named = field === null ? "" : (fields[field] ?? "");
|
|
46
|
+
const first = Object.values(fields).find((value) => value.trim() !== "");
|
|
47
|
+
return (named || first || fallback || `Row ${index + 1}`).trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One record's fields, and the data row it was read from. */
|
|
51
|
+
interface RecordFields {
|
|
52
|
+
fields: Record<string, string>;
|
|
53
|
+
row: number;
|
|
54
|
+
/** The file is this one record, so it is called what the file is called when it says nothing. */
|
|
55
|
+
single?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The records of a CSV text: one per data row under the header, blank rows skipped. */
|
|
59
|
+
function csvFields(text: string): RecordFields[] {
|
|
60
|
+
const { headers, rows } = parseCsv(text);
|
|
61
|
+
return rows.flatMap((cells, row) =>
|
|
62
|
+
cells.some((cell) => cell.trim() !== "")
|
|
63
|
+
? [{ fields: Object.fromEntries(headers.map((header, i) => [header, cells[i] ?? ""])), row }]
|
|
64
|
+
: [],
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The records of a JSON text: one per object of an array, or ONE for a file that is a single object
|
|
70
|
+
* (`docs/nav.json`), which the loader reads as one entry named by the file. Null for text that is
|
|
71
|
+
* not JSON, which is then listed as the file it is.
|
|
72
|
+
*/
|
|
73
|
+
function jsonFields(text: string): RecordFields[] | null {
|
|
74
|
+
let parsed: JsonValue;
|
|
75
|
+
try {
|
|
76
|
+
// SAFETY: JSON.parse only ever yields JSON values; its `any` return type says nothing more.
|
|
77
|
+
parsed = JSON.parse(text) as JsonValue;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const fieldsOf = (item: JsonValue): Record<string, string> =>
|
|
82
|
+
isNonNullObject(item) && !Array.isArray(item)
|
|
83
|
+
? Object.fromEntries(Object.entries(item).map(([key, value]) => [key, fieldText(value)]))
|
|
84
|
+
: {};
|
|
85
|
+
if (!Array.isArray(parsed)) {
|
|
86
|
+
return [{ fields: fieldsOf(parsed), row: 0, single: true }];
|
|
87
|
+
}
|
|
88
|
+
return parsed.map((item, row) => ({ fields: fieldsOf(item), row }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The records a collection file holds, or null when its kind is not one the Content workspace reads
|
|
93
|
+
* records from (it is then listed as the file it is).
|
|
94
|
+
*
|
|
95
|
+
* @param path The file's project-relative path; its extension says how to read it.
|
|
96
|
+
* @param text The file's text.
|
|
97
|
+
* @param schema The collection's schema, which names the title field.
|
|
98
|
+
*/
|
|
99
|
+
export function fileRecords(
|
|
100
|
+
path: string,
|
|
101
|
+
text: string,
|
|
102
|
+
schema?: ContentTypeSchema | null,
|
|
103
|
+
): FileRecord[] | null {
|
|
104
|
+
const lower = path.toLowerCase();
|
|
105
|
+
const rows = lower.endsWith(".csv")
|
|
106
|
+
? csvFields(text)
|
|
107
|
+
: lower.endsWith(".json")
|
|
108
|
+
? jsonFields(text)
|
|
109
|
+
: null;
|
|
110
|
+
if (rows === null) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const stem = (path.split("/").pop() ?? path).replace(/\.[^.]+$/, "");
|
|
114
|
+
return rows.map(({ fields, row, single }, index) => ({
|
|
115
|
+
fields,
|
|
116
|
+
index,
|
|
117
|
+
row,
|
|
118
|
+
title: recordTitle(schema, fields, index, single ? stem : ""),
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The key a record's row is listed under: its file and its place in it, `prices.csv#3`. */
|
|
123
|
+
export function recordKey(path: string, index: number): string {
|
|
124
|
+
return `${path}#${index + 1}`;
|
|
125
|
+
}
|