@jxsuite/studio 6.1.2 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/chunks/2020-5wtzce5g.js +4 -0
- package/dist/chunks/adapter-5tphg7xe.js +4 -0
- package/dist/chunks/add-new-flow-psym14sn.js +4 -0
- package/dist/chunks/cf-account-picker-haqazq5r.js +4 -0
- package/dist/chunks/cf-settings-h2wrh6hq.js +4 -0
- package/dist/chunks/{color-area-0fj8a4ga.js → color-area-6rbra5dy.js} +2 -2
- package/dist/chunks/{color-field-xch8kx2g.js → color-field-gy56c94g.js} +3 -3
- package/dist/chunks/color-m7r1vk54.js +4 -0
- package/dist/chunks/{color-slider-jxd8v350.js → color-slider-sqqdyf6c.js} +2 -2
- package/dist/chunks/{combobox-p0efwjke.js → combobox-72h32ejk.js} +3 -3
- package/dist/chunks/components-r22xca09.js +4 -0
- package/dist/chunks/content-source-mmppmcyg.js +4 -0
- package/dist/chunks/convert-to-repeater-he70fc2b.js +4 -0
- package/dist/chunks/editor-ccd9jwsa.js +4 -0
- package/dist/chunks/entry-commands-6qxz9e8p.js +4 -0
- package/dist/chunks/extension-sections-0egb88dm.js +4 -0
- package/dist/chunks/file-ops-qnab36ek.js +4 -0
- package/dist/chunks/files-rcn56qvm.js +4 -0
- package/dist/chunks/format-host-yxz0kjjs.js +4 -0
- package/dist/chunks/fs-events-d1qxq4z4.js +4 -0
- package/dist/chunks/grid-open-4z1g062t.js +4 -0
- package/dist/chunks/{grid-view-d2wq9ezg.js → grid-view-qg1b5eqx.js} +25 -25
- package/dist/chunks/{icons-pnfz478q.js → icons-63bjtbs7.js} +2 -2
- package/dist/chunks/{iframe-entry-jdcktez8.js → iframe-entry-2zggzajv.js} +4 -4
- package/dist/chunks/{iframe-entry-zap785cw.js → iframe-entry-76j8vwwx.js} +4 -4
- package/dist/chunks/iframe-entry-99nr5zfb.js +5 -0
- package/dist/chunks/{iframe-entry-x0ygf2yr.js → iframe-entry-pkgmcgp0.js} +4 -4
- package/dist/chunks/iframe-host-ssv7kyj0.js +4 -0
- package/dist/chunks/{index-zpjmzqfk.js → index-32eqzv1k.js} +3 -3
- package/dist/chunks/index-f0jn7nnp.js +4 -0
- package/dist/chunks/{javascript-m54mnwp6.js → javascript-pjp734q4.js} +3 -3
- package/dist/chunks/jsonMode-njkvsa02.js +13 -0
- package/dist/chunks/jx-validate-ttasrbh7.js +4 -0
- package/dist/chunks/layers-bgyr11tr.js +4 -0
- package/dist/chunks/layers-panel-7ez66zeq.js +4 -0
- package/dist/chunks/{listbox-e74qjyq6.js → listbox-c421cf8c.js} +2 -2
- package/dist/chunks/lit-html-v9gh687x.js +4 -0
- package/dist/chunks/media-library-sq468wts.js +4 -0
- package/dist/chunks/media-picker-shfe59pt.js +4 -0
- package/dist/chunks/{menu-n6yzswmy.js → menu-bdpzbfvs.js} +2 -2
- package/dist/chunks/{monaco-binding-7gx83x3r.js → monaco-binding-vq1hgnn0.js} +3 -3
- package/dist/chunks/monaco-buffer-3afgwvbp.js +4 -0
- package/dist/chunks/monaco-setup-xrd2cbkp.js +4 -0
- package/dist/chunks/notify-kzrjr3fv.js +4 -0
- package/dist/chunks/parse-v77xtwv7.js +4 -0
- package/dist/chunks/platform-7v4rtd7m.js +4 -0
- package/dist/chunks/popover-b89sfej7.js +4 -0
- package/dist/chunks/publish-panel-2z9gw8ee.js +4 -0
- package/dist/chunks/publish-quick-qx2455cj.js +4 -0
- package/dist/chunks/publish-workspace-wrc1dgwx.js +4 -0
- package/dist/chunks/quick-search-n0t0eqxb.js +4 -0
- package/dist/chunks/references-9547rg5s.js +4 -0
- package/dist/chunks/right-panel-wsgnr9pn.js +4 -0
- package/dist/chunks/source-control-kwktr9y6.js +4 -0
- package/dist/chunks/{split-n2gfpm5m.js → split-c9zfdgxf.js} +2 -2
- package/dist/chunks/studio-03ct6yfv.js +9 -0
- package/dist/chunks/studio-07rcfje3.js +5 -0
- package/dist/chunks/studio-3e54qh39.js +7 -0
- package/dist/chunks/studio-3exxhz5g.js +5 -0
- package/dist/chunks/studio-3q40e3rj.js +5 -0
- package/dist/chunks/studio-3qeh9t0h.js +6 -0
- package/dist/chunks/studio-3qqjeep5.js +6 -0
- package/dist/chunks/studio-43sd2mam.js +4 -0
- package/dist/chunks/studio-4hp8pkyt.js +5 -0
- package/dist/chunks/studio-58xg6j2q.js +9 -0
- package/dist/chunks/studio-5g0rprdt.js +7 -0
- package/dist/chunks/{studio-2j1cb95x.js → studio-6m380wp7.js} +4 -4
- package/dist/chunks/studio-80jyh1xh.js +4 -0
- package/dist/chunks/studio-83mk4bgg.js +9 -0
- package/dist/chunks/{studio-v0jzqdjm.js → studio-881z1xrx.js} +4 -4
- package/dist/chunks/studio-8m0y2qe7.js +5 -0
- package/dist/chunks/{studio-d81xfh1g.js → studio-8tr6cfrc.js} +4 -4
- package/dist/chunks/studio-8vcvfs7r.js +6 -0
- package/dist/chunks/studio-9tmebf0s.js +5 -0
- package/dist/chunks/studio-a2bvaptz.js +5 -0
- package/dist/chunks/studio-b7y5mr30.js +5 -0
- package/dist/chunks/studio-bavb31r0.js +5 -0
- package/dist/chunks/{studio-dgz8d0c1.js → studio-bbe4c90a.js} +4 -4
- package/dist/chunks/studio-c05wdnt3.js +5 -0
- package/dist/chunks/studio-ccv2svc6.js +5 -0
- package/dist/chunks/studio-d5grtb9e.js +16 -0
- package/dist/chunks/studio-df0baea9.js +5 -0
- package/dist/chunks/studio-dgk4dqtx.js +7 -0
- package/dist/chunks/studio-e1g64q43.js +5 -0
- package/dist/chunks/studio-ed1hp6zf.js +6 -0
- package/dist/chunks/studio-ed3xx40h.js +7 -0
- package/dist/chunks/studio-f14e1zdj.js +5 -0
- package/dist/chunks/studio-fbhxxc5f.js +5 -0
- package/dist/chunks/studio-fyh0jc96.js +5 -0
- package/dist/chunks/studio-gcxk20n7.js +5 -0
- package/dist/chunks/studio-jaaz8ce9.js +5 -0
- package/dist/chunks/{studio-90pq9km1.js → studio-jcnsdaep.js} +4 -4
- package/dist/chunks/studio-jsgf2q5k.js +7 -0
- package/dist/chunks/{studio-0q988f9w.js → studio-k3xxbagr.js} +3 -3
- package/dist/chunks/{studio-5ve6bbep.js → studio-k7evxqh4.js} +4 -4
- package/dist/chunks/studio-kt3pjkwd.js +5 -0
- package/dist/chunks/studio-m8g16bgz.js +5 -0
- package/dist/chunks/studio-mn0476ag.js +5 -0
- package/dist/chunks/studio-mx1smyq0.js +5 -0
- package/dist/chunks/studio-n0c5h6mf.js +5 -0
- package/dist/chunks/{studio-485046te.js → studio-nh6d342n.js} +7 -7
- package/dist/chunks/studio-nqxqf8p2.js +4 -0
- package/dist/chunks/studio-p4nrkvqr.js +5 -0
- package/dist/chunks/{studio-1fxkaaea.js → studio-pbxa5fpc.js} +4 -4
- package/dist/chunks/studio-pgmvdv7z.js +7 -0
- package/dist/chunks/studio-ph84avkf.js +5 -0
- package/dist/chunks/{studio-tq0rtfwj.js → studio-q0mvbaq6.js} +4 -4
- package/dist/chunks/studio-q0tccd43.js +5 -0
- package/dist/chunks/studio-q7rtx6zq.js +14 -0
- package/dist/chunks/{studio-a3g98wjr.js → studio-qgh13jtz.js} +4 -4
- package/dist/chunks/studio-qmn1xc24.js +5 -0
- package/dist/chunks/studio-qwdqwkj0.js +5 -0
- package/dist/chunks/studio-r5qy30vx.js +5 -0
- package/dist/chunks/studio-rj9eamf8.js +5 -0
- package/dist/chunks/studio-rptgsk96.js +5 -0
- package/dist/chunks/studio-rthyg42z.js +928 -0
- package/dist/chunks/studio-s19tp1xr.js +5 -0
- package/dist/chunks/studio-s32578pn.js +5 -0
- package/dist/chunks/studio-svb7yyve.js +5 -0
- package/dist/chunks/studio-syreb644.js +5 -0
- package/dist/chunks/studio-tf9jryb1.js +10 -0
- package/dist/chunks/studio-tfkznv39.js +5 -0
- package/dist/chunks/studio-tft6r105.js +5 -0
- package/dist/chunks/studio-txf2kysx.js +5 -0
- package/dist/chunks/studio-v8867ddv.js +12 -0
- package/dist/chunks/studio-vxq7fcvv.js +5 -0
- package/dist/chunks/studio-w4rz9hdk.js +5 -0
- package/dist/chunks/studio-wbcjbhy1.js +9 -0
- package/dist/chunks/studio-wc649wss.js +5 -0
- package/dist/chunks/studio-wys23mxz.js +5 -0
- package/dist/chunks/studio-x1x167jh.js +5 -0
- package/dist/chunks/studio-x9hsze61.js +5 -0
- package/dist/chunks/studio-y0n7yxea.js +9 -0
- package/dist/chunks/studio-y3ahhdpx.js +5 -0
- package/dist/chunks/studio-z97c48fk.js +5 -0
- package/dist/chunks/studio-zat62m0c.js +5 -0
- package/dist/chunks/studio-zcd2e7yz.js +5 -0
- package/dist/chunks/studio-zz9qe3rw.js +64 -0
- package/dist/chunks/toast-1gv0h53h.js +4 -0
- package/dist/chunks/{toast-host-ke5z4ng7.js → toast-host-1yg5ar8d.js} +3 -3
- package/dist/chunks/{tooltip-5v8ngwj2.js → tooltip-yypz28gq.js} +3 -3
- package/dist/chunks/{tree-mdq3v5cw.js → tree-9xa4hm7n.js} +2 -2
- package/dist/chunks/tsMode-epyfz1hv.js +14 -0
- package/dist/chunks/workspace-dz7nxfhv.js +4 -0
- package/dist/chunks/workspace-home-xsd7hkjw.js +4 -0
- package/dist/chunks/{ws-client-nze0k6mv.js → ws-client-bv552sfx.js} +3 -3
- package/dist/iframe-entry.js +20 -16
- package/dist/manifest.json +77 -63
- package/dist/studio.js +103 -104
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +12 -11
- package/src/browse/component-examples.ts +272 -0
- package/src/browse/library-commands.ts +64 -104
- package/src/browse/library-layouts.ts +263 -49
- package/src/browse/library-model.ts +181 -133
- package/src/browse/library-pane.ts +738 -446
- package/src/browse/library-preview.ts +149 -19
- package/src/browse/library-source.ts +308 -41
- package/src/browse/preview-root.ts +78 -0
- package/src/browse/preview-style.ts +155 -0
- package/src/canvas/asset-refs.ts +16 -1
- package/src/canvas/canvas-live-render.ts +222 -30
- package/src/canvas/canvas-origin.ts +37 -12
- package/src/canvas/canvas-page-context.ts +112 -0
- package/src/canvas/canvas-render.ts +113 -107
- package/src/canvas/canvas-utils.ts +57 -12
- package/src/canvas/iframe-drop.ts +107 -0
- package/src/canvas/iframe-entry.ts +24 -12
- package/src/canvas/iframe-host.ts +134 -62
- package/src/canvas/iframe-keys.ts +11 -0
- package/src/canvas/iframe-patch.ts +8 -7
- package/src/canvas/iframe-protocol.ts +24 -5
- package/src/canvas/iframe-render.ts +48 -1
- package/src/canvas/iframe-subtree.ts +5 -2
- package/src/canvas/npm-modules.ts +22 -0
- package/src/canvas/preview-navigate.ts +16 -0
- package/src/canvas/repeater-sources.ts +200 -0
- package/src/canvas/stage-scrollbar.ts +5 -1
- package/src/canvas/surface-registry.ts +6 -0
- package/src/commands/app-commands.ts +16 -6
- package/src/commands/budget.ts +10 -8
- package/src/commands/context.ts +35 -10
- package/src/commands/defaults.ts +69 -45
- package/src/commands/keymap.ts +9 -0
- package/src/commands/levels.ts +11 -5
- package/src/commands/live-context.ts +2 -0
- package/src/commands/registry.ts +10 -1
- package/src/content/collection-match.ts +6 -5
- package/src/content/collection-pages.ts +412 -0
- package/src/content/content-collections.ts +270 -0
- package/src/content/entry-commands.ts +51 -60
- package/src/content/entry-editor.ts +111 -18
- package/src/content/entry-fields.ts +13 -1
- package/src/content/entry-inspector.ts +225 -0
- package/src/content/entry-model.ts +17 -3
- package/src/content/entry-open.ts +80 -0
- package/src/content/entry-summaries.ts +440 -0
- package/src/content/file-record-ids.ts +80 -0
- package/src/content/file-records.ts +125 -0
- package/src/content/page-summaries.ts +129 -0
- package/src/content/record-entry.ts +201 -0
- package/src/content/writing-banner.ts +177 -0
- package/src/create/add-new-commands.ts +185 -0
- package/src/create/add-new-flow.ts +1116 -0
- package/src/create/add-new.ts +1014 -0
- package/src/create/element-name.ts +121 -0
- package/src/editor/convert-to-component.ts +67 -43
- package/src/editor/file-drop-action.ts +9 -1
- package/src/editor/inline-edit-apply.ts +44 -0
- package/src/editor/inline-edit.ts +10 -0
- package/src/editor/insert-component.ts +91 -0
- package/src/editor/insert-zone-action.ts +13 -4
- package/src/editor/shortcuts.ts +44 -22
- package/src/editor/slash-menu.ts +51 -8
- package/src/files/components.ts +74 -10
- package/src/files/file-ops.ts +15 -5
- package/src/files/files.ts +164 -150
- package/src/files/fs-events.ts +18 -7
- package/src/files/media-meta.ts +95 -1
- package/src/files/media-types.ts +55 -0
- package/src/files/media-upload.ts +209 -52
- package/src/files/new-file-name.ts +156 -0
- package/src/files/serialize-document.ts +13 -3
- package/src/format/format-host.ts +15 -0
- package/src/github/github-auth.ts +2 -2
- package/src/github/github-publish.ts +4 -3
- package/src/grid/grid-controller.ts +5 -1
- package/src/grid/grid-open.ts +0 -27
- package/src/grid/grid-panel.ts +54 -2
- package/src/grid/grid-reveal.ts +34 -0
- package/src/grid/grid-source.ts +3 -25
- package/src/grid/grid-view.ts +31 -6
- package/src/grid/sources/content-source.ts +75 -7
- package/src/grid/tabulator-tables.d.ts +6 -0
- package/src/hosting/document.ts +2 -2
- package/src/i18n/i18n-commands.ts +2 -1
- package/src/media/media-commands.ts +181 -0
- package/src/media/media-library-model.ts +232 -0
- package/src/media/media-library.ts +1283 -0
- package/src/media/media-pane.ts +47 -19
- package/src/packages/pull-package-sync.ts +1 -1
- package/src/panels/activity-panel.ts +21 -5
- package/src/panels/ai-chat/chat-markdown.ts +8 -8
- package/src/panels/block-action-bar.ts +11 -11
- package/src/panels/canvas-dnd-bridge.ts +5 -2
- package/src/panels/chat-panel.ts +6 -6
- package/src/panels/collections-panel.ts +306 -0
- package/src/panels/component-preview.ts +35 -13
- package/src/panels/data-explorer.ts +1 -1
- package/src/panels/data-grid.ts +4 -0
- package/src/panels/dnd.ts +17 -30
- package/src/panels/elements-panel.ts +9 -2
- package/src/panels/empty-state.ts +2 -2
- package/src/panels/formula-workspace.ts +2 -3
- package/src/panels/frontmatter-fields.ts +9 -9
- package/src/panels/git-diff-open.ts +9 -5
- package/src/panels/head-panel.ts +369 -103
- package/src/panels/i18n-panel.ts +11 -10
- package/src/panels/imports-panel.ts +2 -0
- package/src/panels/jump-bar.ts +27 -2
- package/src/panels/layers-panel.ts +36 -30
- package/src/panels/left-panel.ts +22 -8
- package/src/panels/navigator-panels.ts +23 -11
- package/src/panels/pane-context.ts +111 -29
- package/src/panels/pane-grid.ts +6 -6
- package/src/panels/panel-registry.ts +14 -9
- package/src/panels/properties-panel.ts +7 -3
- package/src/panels/right-panel.ts +199 -43
- package/src/panels/seo-modal.ts +41 -108
- package/src/panels/settings-menu.ts +52 -50
- package/src/panels/settings-pane.ts +268 -162
- package/src/panels/signals-panel.ts +3 -4
- package/src/panels/site-model.ts +367 -0
- package/src/panels/site-panel.ts +638 -0
- package/src/panels/style-panel.ts +37 -3
- package/src/panels/stylebook-doc.ts +142 -10
- package/src/panels/stylebook-layers-panel.ts +39 -80
- package/src/panels/stylebook-panel.ts +47 -10
- package/src/panels/styles-panel.ts +327 -0
- package/src/panels/tab-strip.ts +109 -25
- package/src/platform.ts +1 -1
- package/src/platforms/cloud.ts +28 -1
- package/src/platforms/default-platform.ts +4 -4
- package/src/platforms/devserver.ts +51 -25
- package/src/publish/deploy-checklist.ts +12 -7
- package/src/publish/pages-service.ts +38 -14
- package/src/publish/publish-changes.ts +491 -0
- package/src/publish/publish-commands.ts +83 -18
- package/src/publish/publish-ids.ts +49 -0
- package/src/publish/publish-quick.ts +148 -0
- package/src/publish/publish-workspace.ts +926 -0
- package/src/publish/source-control.ts +907 -0
- package/src/services/automation.ts +16 -20
- package/src/services/cf-settings.ts +4 -4
- package/src/services/connection.ts +2 -2
- package/src/services/directory-picker.ts +2 -2
- package/src/services/references.ts +0 -0
- package/src/services/render-critic.ts +21 -2
- package/src/services/settings/definitions.ts +25 -14
- package/src/settings/contributed-section.ts +49 -10
- package/src/settings/css-vars-editor.ts +93 -27
- package/src/settings/developer-panels.ts +43 -0
- package/src/settings/extension-sections.ts +20 -2
- package/src/settings/general-settings.ts +91 -21
- package/src/settings/preferences-dialog.ts +8 -1
- package/src/settings/project-sections.ts +27 -10
- package/src/settings/schema-field-ui.ts +3 -1
- package/src/settings/section-registry.ts +116 -11
- package/src/settings/settings-dialog.ts +192 -0
- package/src/settings/settings-document.ts +182 -162
- package/src/shell.ts +467 -431
- package/src/site-context.ts +64 -10
- package/src/state.ts +28 -9
- package/src/studio.ts +194 -41
- package/src/style/project-styles.ts +34 -0
- package/src/style/stylebook-specimens.ts +89 -0
- package/src/style/stylebook-target.ts +26 -0
- package/src/style/tag-inheritance.ts +105 -0
- package/src/style/variables-focus.ts +61 -0
- package/src/style/variables-view.ts +235 -0
- package/src/surfaces/add-new.json +1482 -0
- package/src/surfaces/add-new.ts +325 -0
- package/src/surfaces/ai-chat.ts +2 -2
- package/src/surfaces/boot-failure.json +4 -3
- package/src/surfaces/bottom-dock.json +1 -1
- package/src/surfaces/canvas-stage.json +30 -19
- package/src/surfaces/canvas-stage.ts +5 -5
- package/src/surfaces/collections-panel.json +615 -0
- package/src/surfaces/collections-panel.ts +168 -0
- package/src/surfaces/commandbar.json +241 -96
- package/src/surfaces/commandbar.ts +134 -99
- package/src/surfaces/empty-state.json +1 -1
- package/src/surfaces/entry-editor.json +136 -0
- package/src/surfaces/entry-editor.ts +21 -0
- package/src/surfaces/entry-inspector.json +371 -0
- package/src/surfaces/entry-inspector.ts +143 -0
- package/src/surfaces/inspector-dock.json +162 -14
- package/src/surfaces/inspector-dock.ts +46 -19
- package/src/surfaces/library-pane.json +670 -437
- package/src/surfaces/library-pane.ts +94 -59
- package/src/surfaces/media-browser.json +62 -6
- package/src/surfaces/media-browser.ts +15 -1
- package/src/surfaces/media-field.json +1 -1
- package/src/surfaces/media-library.json +1586 -0
- package/src/surfaces/media-library.ts +282 -0
- package/src/surfaces/media-pick-dialog.json +71 -0
- package/src/surfaces/media-pick-dialog.ts +132 -0
- package/src/surfaces/menu.ts +18 -0
- package/src/surfaces/pane-context.json +144 -77
- package/src/surfaces/pane-context.ts +24 -0
- package/src/surfaces/panel-deploy-checklist.json +12 -6
- package/src/surfaces/panel-deploy-checklist.ts +3 -1
- package/src/surfaces/panel-page.json +72 -2
- package/src/surfaces/panel-page.ts +38 -7
- package/src/surfaces/panel-signals.json +1 -1
- package/src/surfaces/panel-signals.ts +5 -5
- package/src/surfaces/panel-stylebook-layers.json +71 -204
- package/src/surfaces/panel-stylebook-layers.ts +0 -21
- package/src/surfaces/preferences.json +120 -48
- package/src/surfaces/preferences.ts +8 -0
- package/src/surfaces/publish-quick.json +428 -0
- package/src/surfaces/publish-quick.ts +165 -0
- package/src/surfaces/publish-workspace.json +2892 -0
- package/src/surfaces/publish-workspace.ts +391 -0
- package/src/surfaces/rail.json +1 -159
- package/src/surfaces/rail.ts +18 -75
- package/src/surfaces/schema-form.json +10 -2
- package/src/surfaces/schema-form.ts +2 -0
- package/src/surfaces/seo.json +1 -1
- package/src/surfaces/settings-contributed.json +5 -5
- package/src/surfaces/settings-contributed.ts +18 -2
- package/src/surfaces/settings-css-vars.json +44 -36
- package/src/surfaces/settings-css-vars.ts +77 -4
- package/src/surfaces/settings-deploy.json +2 -15
- package/src/surfaces/settings-deploy.ts +1 -1
- package/src/surfaces/settings-dialog.json +51 -0
- package/src/surfaces/settings-dialog.ts +169 -0
- package/src/surfaces/settings-overview.json +3 -3
- package/src/surfaces/settings-overview.ts +9 -2
- package/src/surfaces/settings-pane.json +269 -46
- package/src/surfaces/settings-pane.ts +85 -24
- package/src/surfaces/shell.json +2 -1
- package/src/surfaces/site-panel.json +1029 -0
- package/src/surfaces/site-panel.ts +233 -0
- package/src/surfaces/slash-menu.json +63 -21
- package/src/surfaces/slash-menu.ts +23 -0
- package/src/surfaces/statusbar.ts +39 -15
- package/src/surfaces/styles-panel.json +735 -0
- package/src/surfaces/styles-panel.ts +195 -0
- package/src/surfaces/styles-variables.json +149 -0
- package/src/surfaces/styles-variables.ts +145 -0
- package/src/surfaces/tab-strip.json +4 -4
- package/src/surfaces/welcome.ts +1 -1
- package/src/surfaces/writing-banner.json +94 -0
- package/src/surfaces/writing-banner.ts +80 -0
- package/src/tabs/bare-repeaters.ts +101 -0
- package/src/tabs/tab.ts +47 -7
- package/src/types.ts +21 -26
- package/src/ui/form-controls.ts +47 -8
- package/src/ui/layers.ts +13 -0
- package/src/ui/media-picker.ts +284 -40
- package/src/ui/regions.ts +7 -6
- package/src/ui/schema-form.ts +259 -27
- package/src/ui/virtual-window.ts +26 -1
- package/src/utils/base64.ts +3 -3
- package/src/utils/edit-display.ts +322 -67
- package/src/utils/local-day.ts +18 -0
- package/src/workspace/pane-derive.ts +4 -4
- package/src/workspace/session.ts +28 -7
- package/src/workspace/tab-workspace.ts +137 -0
- package/src/workspace/workspace-back.ts +189 -0
- package/src/workspace/workspace-home.ts +133 -0
- package/src/workspace/workspace-ids.ts +129 -0
- package/src/workspace/workspace-switch.ts +372 -0
- package/src/workspace/workspace.ts +125 -11
- package/styles/shell-frame.css +30 -7
- package/styles/shell-frame.json +15 -3
- package/dist/chunks/2020-f2gkpy8d.js +0 -4
- package/dist/chunks/adapter-4912g339.js +0 -4
- package/dist/chunks/cf-account-picker-wccaenh6.js +0 -4
- package/dist/chunks/cf-settings-br6105tr.js +0 -4
- package/dist/chunks/color-28ksk2qm.js +0 -4
- package/dist/chunks/components-db68vsn7.js +0 -4
- package/dist/chunks/content-source-1449qesq.js +0 -4
- package/dist/chunks/convert-to-repeater-5h457egj.js +0 -4
- package/dist/chunks/editor-x9jn86a7.js +0 -4
- package/dist/chunks/entry-commands-may24cpt.js +0 -4
- package/dist/chunks/extension-sections-pvr14vxr.js +0 -4
- package/dist/chunks/file-ops-x9v54pcj.js +0 -4
- package/dist/chunks/files-j3knqf0s.js +0 -4
- package/dist/chunks/format-host-tyre2trb.js +0 -4
- package/dist/chunks/fs-events-64ma9htc.js +0 -4
- package/dist/chunks/git-panel-84ps5dex.js +0 -4
- package/dist/chunks/iframe-entry-fvbk89gm.js +0 -5
- package/dist/chunks/iframe-host-ck6vejt5.js +0 -4
- package/dist/chunks/index-b8xw3vy9.js +0 -4
- package/dist/chunks/jsonMode-w15cmrcm.js +0 -13
- package/dist/chunks/jx-validate-at3zn8wa.js +0 -4
- package/dist/chunks/layers-h4xp1pss.js +0 -4
- package/dist/chunks/layers-panel-m5hw1d7g.js +0 -4
- package/dist/chunks/lit-html-8t8t3dnr.js +0 -4
- package/dist/chunks/media-picker-2cweexc5.js +0 -4
- package/dist/chunks/menu-ynw13y0s.js +0 -4
- package/dist/chunks/monaco-buffer-qzxn567r.js +0 -4
- package/dist/chunks/monaco-setup-gxf6656g.js +0 -4
- package/dist/chunks/notify-cj5zbrs2.js +0 -4
- package/dist/chunks/parse-b7ddhr8m.js +0 -4
- package/dist/chunks/platform-3dfws9js.js +0 -4
- package/dist/chunks/popover-jt2gxmzs.js +0 -4
- package/dist/chunks/publish-panel-1gvxbj21.js +0 -4
- package/dist/chunks/quick-search-0dajrwqg.js +0 -4
- package/dist/chunks/references-49qgt404.js +0 -4
- package/dist/chunks/right-panel-jm1bwe53.js +0 -4
- package/dist/chunks/section-registry-6kwc4fky.js +0 -4
- package/dist/chunks/studio-0rt52vpq.js +0 -5
- package/dist/chunks/studio-18vgbhs5.js +0 -5
- package/dist/chunks/studio-24mccdrj.js +0 -5
- package/dist/chunks/studio-2eh507zk.js +0 -9
- package/dist/chunks/studio-2hcbccfv.js +0 -5
- package/dist/chunks/studio-2jbs3rxv.js +0 -5
- package/dist/chunks/studio-4h8k4dw9.js +0 -5
- package/dist/chunks/studio-5f6n18kx.js +0 -5
- package/dist/chunks/studio-5gthpky6.js +0 -5
- package/dist/chunks/studio-6ep3js6b.js +0 -9
- package/dist/chunks/studio-6wkmf3wz.js +0 -9
- package/dist/chunks/studio-841rf3cg.js +0 -5
- package/dist/chunks/studio-8gmwgheg.js +0 -5
- package/dist/chunks/studio-8qm0ep50.js +0 -5
- package/dist/chunks/studio-924p1tm1.js +0 -6
- package/dist/chunks/studio-a3shpz9y.js +0 -5
- package/dist/chunks/studio-amsbaaar.js +0 -6
- package/dist/chunks/studio-at5spjbc.js +0 -7
- package/dist/chunks/studio-b2xv945e.js +0 -5
- package/dist/chunks/studio-bn5nkafw.js +0 -5
- package/dist/chunks/studio-bw6q5xjf.js +0 -9
- package/dist/chunks/studio-c7q0t9x1.js +0 -5
- package/dist/chunks/studio-camnhc96.js +0 -6
- package/dist/chunks/studio-cffqvnzd.js +0 -5
- package/dist/chunks/studio-efvfrsf8.js +0 -5
- package/dist/chunks/studio-er30bpcx.js +0 -5
- package/dist/chunks/studio-ezscgr64.js +0 -5
- package/dist/chunks/studio-ffrrncvz.js +0 -5
- package/dist/chunks/studio-fpphgn8y.js +0 -10
- package/dist/chunks/studio-j1kvhdjk.js +0 -4
- package/dist/chunks/studio-j430n16g.js +0 -7
- package/dist/chunks/studio-jjkg84nj.js +0 -5
- package/dist/chunks/studio-m0q32ars.js +0 -4
- package/dist/chunks/studio-m6bdst11.js +0 -5
- package/dist/chunks/studio-nv2cptd4.js +0 -64
- package/dist/chunks/studio-nwfrc98n.js +0 -5
- package/dist/chunks/studio-p1wgvp7r.js +0 -5
- package/dist/chunks/studio-pag2wncm.js +0 -5
- package/dist/chunks/studio-qvanvzfd.js +0 -5
- package/dist/chunks/studio-qwa64kt7.js +0 -5
- package/dist/chunks/studio-rk1zc4x3.js +0 -928
- package/dist/chunks/studio-se2r3ptc.js +0 -5
- package/dist/chunks/studio-sg2a0dh0.js +0 -5
- package/dist/chunks/studio-tjzjhe68.js +0 -5
- package/dist/chunks/studio-v2w7q8r1.js +0 -7
- package/dist/chunks/studio-v5vjq1h0.js +0 -4
- package/dist/chunks/studio-v8aga3vp.js +0 -14
- package/dist/chunks/studio-vfcdrfmv.js +0 -5
- package/dist/chunks/studio-vmfkgdjf.js +0 -5
- package/dist/chunks/studio-wt1816jj.js +0 -5
- package/dist/chunks/studio-wz73q5kq.js +0 -4
- package/dist/chunks/studio-y04qtxvp.js +0 -7
- package/dist/chunks/studio-y21g8zzg.js +0 -5
- package/dist/chunks/studio-y64hrt8p.js +0 -12
- package/dist/chunks/studio-ynn90f58.js +0 -16
- package/dist/chunks/studio-yxz682sx.js +0 -5
- package/dist/chunks/studio-zp7snvkq.js +0 -5
- package/dist/chunks/studio-zq4z6tm6.js +0 -5
- package/dist/chunks/toast-c9fn075p.js +0 -4
- package/dist/chunks/tsMode-sf49ee9x.js +0 -14
- package/dist/chunks/workspace-jtfahpee.js +0 -4
- package/src/panels/frontmatter-panel.ts +0 -711
- package/src/panels/git-panel.ts +0 -1108
- package/src/surfaces/doc-header.json +0 -776
- package/src/surfaces/doc-header.ts +0 -222
- package/src/surfaces/git-panel.json +0 -1880
- package/src/surfaces/git-panel.ts +0 -264
package/src/files/files.ts
CHANGED
|
@@ -80,6 +80,7 @@ import { runActiveReported, runReported } from "../commands/run-reported";
|
|
|
80
80
|
import { collectionOfPath } from "../content/entry-model";
|
|
81
81
|
import { confirmFileDelete, parseSourceForPath, renamePromptMessage } from "./file-ops";
|
|
82
82
|
import { parseJsonDocument } from "@jxsuite/schema/json-layout";
|
|
83
|
+
import { normalizeDocument } from "../tabs/bare-repeaters";
|
|
83
84
|
// The pure half of the differ: order-insensitive equality with no Yjs behind it.
|
|
84
85
|
import { deepEqual } from "@jxsuite/collab/diff-core";
|
|
85
86
|
import type { JsonLayout } from "@jxsuite/schema/json-layout";
|
|
@@ -94,14 +95,27 @@ import {
|
|
|
94
95
|
refreshFormats,
|
|
95
96
|
} from "../format/format-host";
|
|
96
97
|
import { convertTargets, creationFormats, knownDocumentExtensions } from "../format/format-choices";
|
|
98
|
+
import {
|
|
99
|
+
composeNewFileName,
|
|
100
|
+
extensionOfName,
|
|
101
|
+
OTHER_FORMAT,
|
|
102
|
+
validateNewFileName,
|
|
103
|
+
} from "./new-file-name";
|
|
104
|
+
import type { FormatChoice, NewFileNameCheck } from "./new-file-name";
|
|
97
105
|
import { collectionForDirectory } from "../content/collection-match";
|
|
98
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
markSessionRestored,
|
|
108
|
+
persistedSession,
|
|
109
|
+
persistedWorkspace,
|
|
110
|
+
resetProjectShell,
|
|
111
|
+
} from "../shell";
|
|
112
|
+
import type { WorkspaceId } from "../workspace/workspace-ids";
|
|
99
113
|
import { restoreSession } from "../workspace/session";
|
|
100
|
-
import {
|
|
114
|
+
import { cleanupSourceControl } from "../publish/source-control";
|
|
101
115
|
import { addRecentProject, trackRecentFile } from "../recent-projects";
|
|
102
116
|
import { listWindow, revealListRow, watchListWindow } from "../ui/virtual-window";
|
|
103
117
|
import type { JsonObject, JxMutableNode } from "@jxsuite/schema/types";
|
|
104
|
-
import {
|
|
118
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
105
119
|
import type { CommandArgValues } from "../commands/command-args";
|
|
106
120
|
import type { FormatFrontmatter } from "../format/format-host";
|
|
107
121
|
import type { ResolvedI18n } from "@jxsuite/schema/locale";
|
|
@@ -176,6 +190,29 @@ export async function loadProjectDirectories(): Promise<void> {
|
|
|
176
190
|
projectState.projectDirs = found;
|
|
177
191
|
}
|
|
178
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Re-read the project's directories when a write created a conventional one the list lacks.
|
|
195
|
+
*
|
|
196
|
+
* The list is read as the project opens, so a project that had no `pages/` or `content/` then does
|
|
197
|
+
* not grow one in the list when Add new writes its first page or collection, and the Content scan
|
|
198
|
+
* walks only what the list names. Resolves `true` when the list was re-read, so the caller knows a
|
|
199
|
+
* repaint is due; `false` (and no listing at all) when every written path was already covered.
|
|
200
|
+
*
|
|
201
|
+
* @param paths Project-relative paths just written.
|
|
202
|
+
*/
|
|
203
|
+
export async function refreshProjectDirectoriesFor(paths: readonly string[]): Promise<boolean> {
|
|
204
|
+
const known = new Set(projectState?.projectDirs);
|
|
205
|
+
const missing = paths.some((path) => {
|
|
206
|
+
const top = path.replaceAll("\\", "/").replace(/^\.\//, "").split("/")[0] ?? "";
|
|
207
|
+
return path.includes("/") && CONVENTIONAL_DIRS.has(top) && !known.has(top);
|
|
208
|
+
});
|
|
209
|
+
if (!missing) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
await loadProjectDirectories();
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
179
216
|
/**
|
|
180
217
|
* Whether the tree currently draws the files `.gitignore` masks.
|
|
181
218
|
*
|
|
@@ -256,13 +293,19 @@ export async function loadProject() {
|
|
|
256
293
|
* the window on the project it already had, and the caller announces the outcome — "Opening the
|
|
257
294
|
* project…" over a dialog the user just dismissed is a report of something that did not happen.
|
|
258
295
|
*
|
|
259
|
-
* @param
|
|
296
|
+
* @param ctx.renderLeftPanel Repaint the Navigator.
|
|
297
|
+
* @param ctx.resetProjectCaches Drop the caches panels above this module keep for the project being
|
|
298
|
+
* left (the Site panel's index, the media listing). Injected for the reason `fs-events.ts` takes
|
|
299
|
+
* `invalidateDerivedCaches`: those panels import from the file layer, so reaching up is a cycle,
|
|
300
|
+
* and `studio.ts`'s `openRecentProject` runs the same reset.
|
|
260
301
|
* @returns {Promise<boolean>} Whether the window changed project.
|
|
261
302
|
*/
|
|
262
303
|
export async function openProject({
|
|
263
304
|
renderLeftPanel,
|
|
305
|
+
resetProjectCaches,
|
|
264
306
|
}: {
|
|
265
307
|
renderLeftPanel: () => void;
|
|
308
|
+
resetProjectCaches?: () => void;
|
|
266
309
|
}): Promise<boolean> {
|
|
267
310
|
try {
|
|
268
311
|
const platform = getPlatform();
|
|
@@ -312,10 +355,13 @@ export async function openProject({
|
|
|
312
355
|
await loadComponentRegistry();
|
|
313
356
|
|
|
314
357
|
// Source control, the stylebook selection and the settings tab describe the project being
|
|
315
|
-
// Left behind. The poll timer goes with them; the
|
|
316
|
-
|
|
358
|
+
// Left behind. The poll timer goes with them; the Publish workspace re-arms it on its next mount.
|
|
359
|
+
cleanupSourceControl();
|
|
317
360
|
resetProjectShell();
|
|
318
|
-
|
|
361
|
+
resetProjectCaches?.();
|
|
362
|
+
/* The Navigator's panel is the workspace frame's, applied as the project opened (§22.3): Design
|
|
363
|
+
opens on the Site panel. Forcing Files here put a developer panel with no rail button on
|
|
364
|
+
screen and re-opened a Navigator the frame had hidden. */
|
|
319
365
|
addRecentProject(requireProjectState().name, requireProjectState().projectRoot);
|
|
320
366
|
renderLeftPanel();
|
|
321
367
|
// The project's name is permanent state in the status bar's PROJECT field now.
|
|
@@ -424,6 +470,31 @@ export async function openLastSessionOrHome(): Promise<boolean> {
|
|
|
424
470
|
// `projectState.projectRoot`, the recent-list entry. One reader of one field cannot disagree with
|
|
425
471
|
// The writer; three callers passing three spellings silently restore nothing.
|
|
426
472
|
const session = persistedSession(workspace.projectRoot);
|
|
473
|
+
/* The workspace it was left in, read BEFORE anything opens: every tab the restore activates
|
|
474
|
+
enters its own workspace, and that entry is persisted over the stored one. */
|
|
475
|
+
const left = persistedWorkspace(workspace.projectRoot);
|
|
476
|
+
const restored = await restoreSessionOrHome(session);
|
|
477
|
+
_showWorkspace?.(left);
|
|
478
|
+
return restored;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* How a project open shows the workspace it was left in, once its documents are open. Installed by
|
|
483
|
+
* the bootstrap (`workspace/workspace-switch.ts`'s `switchWorkspace`), because the switch opens
|
|
484
|
+
* homes this module must not import; unset, the restore leaves the workspace wherever its tabs
|
|
485
|
+
* did.
|
|
486
|
+
*/
|
|
487
|
+
let _showWorkspace: ((id: WorkspaceId) => void) | null = null;
|
|
488
|
+
|
|
489
|
+
/** Install (or with `null`, remove) what {@link openLastSessionOrHome} shows the workspace with. */
|
|
490
|
+
export function setSessionWorkspaceShow(show: ((id: WorkspaceId) => void) | null): void {
|
|
491
|
+
_showWorkspace = show;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** The body of {@link openLastSessionOrHome}: the session, or the home page. */
|
|
495
|
+
async function restoreSessionOrHome(
|
|
496
|
+
session: ReturnType<typeof persistedSession>,
|
|
497
|
+
): Promise<boolean> {
|
|
427
498
|
// Read first, THEN allow writes: the persist effect fires the moment `workspace.projectRoot` is
|
|
428
499
|
// Set, and an empty workspace captured at that instant would overwrite the very record this line
|
|
429
500
|
// Reads. See `markSessionRestored`.
|
|
@@ -1698,29 +1769,6 @@ const BLANK_DOCUMENT = JSON.stringify(
|
|
|
1698
1769
|
2,
|
|
1699
1770
|
);
|
|
1700
1771
|
|
|
1701
|
-
/**
|
|
1702
|
-
* The picker row meaning "I will type the whole name myself".
|
|
1703
|
-
*
|
|
1704
|
-
* It is what keeps the format picker from being a cage. `New File…` is the only generic
|
|
1705
|
-
* file-creation affordance in Studio and both backends create intermediate directories on write, so
|
|
1706
|
-
* without this row `styles/main.css`, `public/robots.txt`, `.gitignore` and a `credits.txt` beside
|
|
1707
|
-
* a collection's images all become uncreatable — and the picker would have taken away more than it
|
|
1708
|
-
* gave. The picker stays the authority on the extension; this is one of its answers.
|
|
1709
|
-
*/
|
|
1710
|
-
export const OTHER_FORMAT = "__other__";
|
|
1711
|
-
|
|
1712
|
-
/** How the extension of a new file is settled. */
|
|
1713
|
-
export type FormatChoice =
|
|
1714
|
-
/** Picker over every creatable format, plus Other…. The field is a NAME, taken verbatim. */
|
|
1715
|
-
| { kind: "choose"; defaultExt?: string; docKind?: "page" | "component" | "content" }
|
|
1716
|
-
/**
|
|
1717
|
-
* Picker holding ONE format, plus Other… — the collection's own extension. The field is a NAME.
|
|
1718
|
-
* `because` is the sentence a refusal quotes when Other… would smuggle a foreign document in.
|
|
1719
|
-
*/
|
|
1720
|
-
| { kind: "locked"; ext: string; because: string }
|
|
1721
|
-
/** No picker. The field is a DISPLAY NAME, slugified, and `ext` is appended. */
|
|
1722
|
-
| { kind: "fixed"; ext: string };
|
|
1723
|
-
|
|
1724
1772
|
/**
|
|
1725
1773
|
* One creation, named.
|
|
1726
1774
|
*
|
|
@@ -1742,34 +1790,12 @@ export interface NewFileRequest {
|
|
|
1742
1790
|
* (`i18n.createTranslation` composes `about.json` from the file it is translating).
|
|
1743
1791
|
*/
|
|
1744
1792
|
format?: FormatChoice;
|
|
1745
|
-
/**
|
|
1746
|
-
|
|
1747
|
-
* file name, which is the only way a caller can seed a body whose FORMAT — or whose own tag name
|
|
1748
|
-
* — it does not know until the dialog resolves. Defaults to the resolved format's
|
|
1749
|
-
* `newFileTemplate`.
|
|
1750
|
-
*/
|
|
1751
|
-
content?:
|
|
1752
|
-
| string
|
|
1753
|
-
| ((ext: string, fileName: string) => string | undefined | Promise<string | undefined>);
|
|
1793
|
+
/** Body to write. Defaults to the resolved format's `newFileTemplate`. */
|
|
1794
|
+
content?: string;
|
|
1754
1795
|
/** Who is creating, for the Problem's `source` line. Defaults to "Files". */
|
|
1755
1796
|
source?: string;
|
|
1756
1797
|
}
|
|
1757
1798
|
|
|
1758
|
-
/** A display name, reduced to a file stem. Lowercase by design: this is a slug. */
|
|
1759
|
-
function slugify(input: string): string {
|
|
1760
|
-
return input
|
|
1761
|
-
.trim()
|
|
1762
|
-
.toLowerCase()
|
|
1763
|
-
.replaceAll(/\s+/g, "-")
|
|
1764
|
-
.replaceAll(/[^a-z\d-]/g, "");
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
/** The extension of a file name, including the dot, or `""`. */
|
|
1768
|
-
function extensionOfName(name: string): string {
|
|
1769
|
-
const dot = name.lastIndexOf(".");
|
|
1770
|
-
return dot > 0 ? name.slice(dot) : "";
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
1799
|
/** The two picker modes — the ones where the field asks for a NAME and the picker owns the suffix. */
|
|
1774
1800
|
type PickerChoice = Extract<FormatChoice, { kind: "choose" } | { kind: "locked" }>;
|
|
1775
1801
|
|
|
@@ -1831,14 +1857,7 @@ export async function createFileIn(request: NewFileRequest): Promise<string | nu
|
|
|
1831
1857
|
await loadFormats();
|
|
1832
1858
|
|
|
1833
1859
|
// One listing, before the field opens: the names it must refuse are known while typing.
|
|
1834
|
-
|
|
1835
|
-
try {
|
|
1836
|
-
const listing = await getPlatform().listDirectory(dir);
|
|
1837
|
-
taken = new Set(listing.map((entry) => entry.name.toLowerCase()));
|
|
1838
|
-
} catch {
|
|
1839
|
-
// A directory that cannot be listed is usually one that does not exist yet — the write below
|
|
1840
|
-
// Is the authority on whether that is a problem, and it reports with the real reason.
|
|
1841
|
-
}
|
|
1860
|
+
const taken = await listTakenNames(dir);
|
|
1842
1861
|
|
|
1843
1862
|
const picker = choice !== undefined && choice.kind !== "fixed" ? choice : null;
|
|
1844
1863
|
const rows = picker === null ? null : formatRowsFor(picker);
|
|
@@ -1847,73 +1866,8 @@ export async function createFileIn(request: NewFileRequest): Promise<string | nu
|
|
|
1847
1866
|
? ""
|
|
1848
1867
|
: initialPickFor(picker, rows, request.suggestedName ?? "");
|
|
1849
1868
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
*
|
|
1853
|
-
* The verbatim STEM mode is not a convenience. The slugifier lowercases and strips everything
|
|
1854
|
-
* outside `[a-z0-9-]`, which turns `[slug]` into `slug` — and eight shipped starters carry
|
|
1855
|
-
* `pages/[slug].json`, `[sku].json`, `[...slug].json`. A picker that slugified would make a
|
|
1856
|
-
* dynamic route uncreatable and would say nothing about it.
|
|
1857
|
-
*/
|
|
1858
|
-
const fileNameFor = (input: string, picked: string) => {
|
|
1859
|
-
const trimmed = input.trim();
|
|
1860
|
-
if (choice === undefined || picked === OTHER_FORMAT) {
|
|
1861
|
-
return trimmed;
|
|
1862
|
-
}
|
|
1863
|
-
if (choice.kind === "fixed") {
|
|
1864
|
-
const slug = slugify(trimmed);
|
|
1865
|
-
return slug === "" ? "" : `${slug}${choice.ext}`;
|
|
1866
|
-
}
|
|
1867
|
-
return trimmed.toLowerCase().endsWith(picked.toLowerCase()) ? trimmed : `${trimmed}${picked}`;
|
|
1868
|
-
};
|
|
1869
|
-
|
|
1870
|
-
const known = knownDocumentExtensions();
|
|
1871
|
-
|
|
1872
|
-
const validate = (value: string, picked: string) => {
|
|
1873
|
-
const trimmed = value.trim();
|
|
1874
|
-
if (!trimmed) {
|
|
1875
|
-
return choice?.kind === "fixed" ? "Enter a name." : "Enter a file name.";
|
|
1876
|
-
}
|
|
1877
|
-
if (choice?.kind === "fixed" && slugify(trimmed) === "") {
|
|
1878
|
-
return "Enter at least one letter or number.";
|
|
1879
|
-
}
|
|
1880
|
-
const normalized = trimmed.replaceAll("\\", "/");
|
|
1881
|
-
if (normalized.startsWith("/") || normalized.endsWith("/")) {
|
|
1882
|
-
return "Enter a name, not a path that starts or ends with a slash.";
|
|
1883
|
-
}
|
|
1884
|
-
if (normalized.split("/").includes("..")) {
|
|
1885
|
-
return "A name cannot step outside the destination folder.";
|
|
1886
|
-
}
|
|
1887
|
-
// A slash is the tree's ONLY way to make a directory — both backends create them on write — so
|
|
1888
|
-
// It is allowed exactly where the field is a whole file name, and refused where it is a name.
|
|
1889
|
-
if (normalized.includes("/") && choice !== undefined && picked !== OTHER_FORMAT) {
|
|
1890
|
-
return `Pick Other… to create a file in a subfolder of ${dir === "." ? "the project root" : dir}.`;
|
|
1891
|
-
}
|
|
1892
|
-
/* A name whose extension MATCHES the pick is fine and is composed once, never doubled;
|
|
1893
|
-
refusing every trailing extension would reject this dialog's own `untitled.json` default.
|
|
1894
|
-
A name ending in an extension no format claims is a stem that happens to have a dot. */
|
|
1895
|
-
const typedExt = extensionOfName(normalized);
|
|
1896
|
-
if (
|
|
1897
|
-
picked !== "" &&
|
|
1898
|
-
picked !== OTHER_FORMAT &&
|
|
1899
|
-
typedExt !== "" &&
|
|
1900
|
-
typedExt.toLowerCase() !== picked.toLowerCase() &&
|
|
1901
|
-
known.has(typedExt.toLowerCase())
|
|
1902
|
-
) {
|
|
1903
|
-
const label = rows?.find((row) => row.value === picked)?.label ?? picked;
|
|
1904
|
-
return `You picked ${label}; this name ends in ${typedExt}.`;
|
|
1905
|
-
}
|
|
1906
|
-
if (choice?.kind === "locked" && picked === OTHER_FORMAT) {
|
|
1907
|
-
const foreign = typedExt !== "" && typedExt.toLowerCase() !== choice.ext.toLowerCase();
|
|
1908
|
-
if (foreign && known.has(typedExt.toLowerCase())) {
|
|
1909
|
-
return choice.because;
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
/* No emptiness check here: every way `fileNameFor` can answer "" is already refused above — a
|
|
1913
|
-
blank field by the first rule, a display name that slugifies to nothing by the second. */
|
|
1914
|
-
const candidate = fileNameFor(value, picked);
|
|
1915
|
-
return taken.has(candidate.toLowerCase()) ? `${candidate} already exists in ${dir}/.` : "";
|
|
1916
|
-
};
|
|
1869
|
+
const check: NewFileNameCheck = { choice, dir, known: knownDocumentExtensions(), rows, taken };
|
|
1870
|
+
const validate = (value: string, picked: string) => validateNewFileName(value, picked, check);
|
|
1917
1871
|
|
|
1918
1872
|
let picked = initialPick;
|
|
1919
1873
|
const promptOptions: PromptDialogOptions = {
|
|
@@ -1942,13 +1896,10 @@ export async function createFileIn(request: NewFileRequest): Promise<string | nu
|
|
|
1942
1896
|
return null;
|
|
1943
1897
|
}
|
|
1944
1898
|
|
|
1945
|
-
const fileName =
|
|
1899
|
+
const fileName = composeNewFileName(entered, picked, choice);
|
|
1946
1900
|
const path = dir === "." ? fileName : `${dir}/${fileName}`;
|
|
1947
|
-
markLocalMutation(path);
|
|
1948
1901
|
const format = formatForPath(fileName);
|
|
1949
|
-
const suppliedBody =
|
|
1950
|
-
? await request.content(extensionOfName(fileName), fileName)
|
|
1951
|
-
: request.content;
|
|
1902
|
+
const suppliedBody = request.content;
|
|
1952
1903
|
/*
|
|
1953
1904
|
* What a new file starts as, in precedence order.
|
|
1954
1905
|
*
|
|
@@ -1968,16 +1919,59 @@ export async function createFileIn(request: NewFileRequest): Promise<string | nu
|
|
|
1968
1919
|
? BLANK_DOCUMENT
|
|
1969
1920
|
: "{}\n"
|
|
1970
1921
|
: "");
|
|
1922
|
+
return writeNewFile(path, content, { dir, source });
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/**
|
|
1926
|
+
* The names already in `dir`, lower-cased — what a new name there must not collide with.
|
|
1927
|
+
*
|
|
1928
|
+
* A directory that cannot be listed answers empty: it is usually one that does not exist yet, and
|
|
1929
|
+
* the write that follows is the authority on whether that is a problem, reporting with the real
|
|
1930
|
+
* reason.
|
|
1931
|
+
*/
|
|
1932
|
+
export async function listTakenNames(dir: string): Promise<Set<string>> {
|
|
1933
|
+
try {
|
|
1934
|
+
const listing = await getPlatform().listDirectory(dir);
|
|
1935
|
+
return new Set(listing.map((entry) => entry.name.toLowerCase()));
|
|
1936
|
+
} catch {
|
|
1937
|
+
return new Set();
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
/**
|
|
1942
|
+
* Write one new file: the second half of every creation flow, after its name has been judged.
|
|
1943
|
+
*
|
|
1944
|
+
* The local mutation is marked first, so the watcher's echo of this write is not mistaken for an
|
|
1945
|
+
* edit made outside Studio. The destination is re-listed so the tree shows the file, the success is
|
|
1946
|
+
* a toast, and a failure is a Problem carrying the path, since the thing the author must do next is
|
|
1947
|
+
* about that path. It does NOT open the file: what a new file opens in is the caller's to say.
|
|
1948
|
+
*
|
|
1949
|
+
* @param path Project-relative path of the file to create.
|
|
1950
|
+
* @param content Its first bytes.
|
|
1951
|
+
* @param opts `dir` is re-listed afterwards (defaults to the path's parent); `source` names the
|
|
1952
|
+
* flow in the Problem; `quiet` skips the success toast for a write that is one step of a larger
|
|
1953
|
+
* creation that reports once for all of them.
|
|
1954
|
+
* @returns The path, or `null` when the write failed.
|
|
1955
|
+
*/
|
|
1956
|
+
export async function writeNewFile(
|
|
1957
|
+
path: string,
|
|
1958
|
+
content: string,
|
|
1959
|
+
opts: { dir?: string; source?: string; quiet?: boolean } = {},
|
|
1960
|
+
): Promise<string | null> {
|
|
1961
|
+
const dir = opts.dir ?? (path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : ".");
|
|
1962
|
+
markLocalMutation(path);
|
|
1971
1963
|
try {
|
|
1972
1964
|
await getPlatform().writeFile(path, content);
|
|
1973
1965
|
await loadDirectory(dir);
|
|
1974
|
-
|
|
1966
|
+
if (opts.quiet !== true) {
|
|
1967
|
+
notify.success(`Created ${path}`);
|
|
1968
|
+
}
|
|
1975
1969
|
return path;
|
|
1976
1970
|
} catch (error) {
|
|
1977
1971
|
notify.error(`Could not create ${path}.`, {
|
|
1978
1972
|
detail: errorMessage(error),
|
|
1979
1973
|
path,
|
|
1980
|
-
source,
|
|
1974
|
+
source: opts.source ?? "Files",
|
|
1981
1975
|
});
|
|
1982
1976
|
return null;
|
|
1983
1977
|
}
|
|
@@ -2241,6 +2235,13 @@ export interface OpenFileOpts {
|
|
|
2241
2235
|
preview?: boolean;
|
|
2242
2236
|
/** False leaves the keyboard where it is. Defaults to true. */
|
|
2243
2237
|
focus?: boolean;
|
|
2238
|
+
/**
|
|
2239
|
+
* The canvas mode the tab is in when it becomes active, for an open that is not an edit: a
|
|
2240
|
+
* comparison opens straight into `git-diff`. Set BEFORE activation, because a tab's mode decides
|
|
2241
|
+
* which workspace it belongs to (`workspace/tab-workspace.ts`) and activation enters that
|
|
2242
|
+
* workspace; a mode set afterwards would visit the edit mode's workspace on the way.
|
|
2243
|
+
*/
|
|
2244
|
+
canvasMode?: string;
|
|
2244
2245
|
}
|
|
2245
2246
|
|
|
2246
2247
|
/**
|
|
@@ -2293,6 +2294,9 @@ export async function openFileInTab(path: string, opts: OpenFileOpts = {}) {
|
|
|
2293
2294
|
const follows = opts.focus !== false;
|
|
2294
2295
|
for (const [id, tab] of workspace.tabs.entries()) {
|
|
2295
2296
|
if (tab.documentPath === path) {
|
|
2297
|
+
if (opts.canvasMode !== undefined) {
|
|
2298
|
+
tab.session.ui.canvasMode = opts.canvasMode;
|
|
2299
|
+
}
|
|
2296
2300
|
revealOpenTab(id, opts);
|
|
2297
2301
|
// The tree's cursor answers "where is the author", so a side-open that deliberately left the
|
|
2298
2302
|
// Keyboard behind must not move it.
|
|
@@ -2392,10 +2396,12 @@ export async function openFileInTab(path: string, opts: OpenFileOpts = {}) {
|
|
|
2392
2396
|
document,
|
|
2393
2397
|
...(frontmatter != null && { frontmatter }),
|
|
2394
2398
|
layout,
|
|
2399
|
+
source: format ? content : null,
|
|
2395
2400
|
sourceFormat: format?.name ?? null,
|
|
2396
2401
|
...(opts.paneId !== undefined && { paneId: opts.paneId }),
|
|
2397
2402
|
...(opts.preview === true && { preview: true }),
|
|
2398
2403
|
...(opts.focus === false && { focus: false }),
|
|
2404
|
+
...(opts.canvasMode !== undefined && { canvasMode: opts.canvasMode }),
|
|
2399
2405
|
});
|
|
2400
2406
|
if (follows) {
|
|
2401
2407
|
requireProjectState().selectedPath = path;
|
|
@@ -2508,16 +2514,21 @@ export async function reloadFileInTab(path: string, opts: { onlyIfChanged?: bool
|
|
|
2508
2514
|
}
|
|
2509
2515
|
tab.doc.document = document;
|
|
2510
2516
|
tab.doc.content.frontmatter = frontmatter;
|
|
2517
|
+
tab.doc.source = content;
|
|
2511
2518
|
} else if (path.endsWith(".json")) {
|
|
2512
2519
|
// The file changed under the tab, so its layout is whatever the new text says.
|
|
2513
2520
|
const parsed = parseJsonDocument(content);
|
|
2514
2521
|
tab.doc.layout = parsed.layout;
|
|
2515
|
-
if (opts.onlyIfChanged && deepEqual(parsed.document, tab.doc.document)) {
|
|
2516
|
-
return;
|
|
2517
|
-
}
|
|
2518
2522
|
/* SAFETY: this tab was opened on this `.json` path as a Jx document, and the file's schema
|
|
2519
2523
|
Makes its top-level object a node — the object `parseJsonDocument` states it returns. */
|
|
2520
|
-
|
|
2524
|
+
const document = parsed.document as JxMutableNode;
|
|
2525
|
+
// The same member form the tab was opened in, so the comparison and every edit after it
|
|
2526
|
+
// Read one shape, and the record of what the file wrote is the new text's.
|
|
2527
|
+
tab.doc.bareRepeaters = normalizeDocument(document);
|
|
2528
|
+
if (opts.onlyIfChanged && deepEqual(document, tab.doc.document)) {
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
tab.doc.document = document;
|
|
2521
2532
|
}
|
|
2522
2533
|
tab.doc.dirty = false;
|
|
2523
2534
|
} catch (error) {
|
|
@@ -2548,6 +2559,10 @@ export function registerFilesPanel(): void {
|
|
|
2548
2559
|
level: "project",
|
|
2549
2560
|
dock: "navigator",
|
|
2550
2561
|
icon: "folder",
|
|
2562
|
+
/* A developer panel (§5.1): the Site panel lists the project by what its files ARE, so the tree
|
|
2563
|
+
of where they are stored is on the Design rail only with **Show developer panels** on. With it
|
|
2564
|
+
off the panel is still `view.setActivity { tab: "files" }` and a palette row away. */
|
|
2565
|
+
rail: "developer",
|
|
2551
2566
|
// The body is a document, so lit draws nothing and the mount happens against the painted DOM.
|
|
2552
2567
|
// `afterRender` runs on every repaint; {@link mountFilesPanel} is idempotent.
|
|
2553
2568
|
render: () => nothing,
|
|
@@ -2603,16 +2618,15 @@ let _lastValues: FilesPanelValues | null = null;
|
|
|
2603
2618
|
* Draw the panel — mounting the document the first time, and re-projecting into it every time
|
|
2604
2619
|
* after.
|
|
2605
2620
|
*
|
|
2606
|
-
* The document goes into `.panel-content`, not into the `.panel-body` this is handed,
|
|
2607
|
-
*
|
|
2608
|
-
*
|
|
2609
|
-
* next.
|
|
2621
|
+
* The document goes into `.panel-content`, not into the `.panel-body` this is handed, because only
|
|
2622
|
+
* one of them is the node lit renders this panel's body into, and appending to the other would
|
|
2623
|
+
* leave the panel drawn under whatever the Navigator paints next.
|
|
2610
2624
|
*
|
|
2611
|
-
* The repaint stays the NAVIGATOR'S.
|
|
2612
|
-
* reactive `shell.git` record; the tree's inputs are a `Map` of directory listings, a `Set`
|
|
2613
|
-
* expanded paths and a roaming setting, none of which is reactive — so `ctx.rerender` is what
|
|
2614
|
-
* surface has always been redrawn by, and it stays that way rather than growing a second
|
|
2615
|
-
* that could disagree with it.
|
|
2625
|
+
* The repaint stays the NAVIGATOR'S. The Publish workspace owns an `effect()` because its inputs
|
|
2626
|
+
* are the reactive `shell.git` record; the tree's inputs are a `Map` of directory listings, a `Set`
|
|
2627
|
+
* of expanded paths and a roaming setting, none of which is reactive — so `ctx.rerender` is what
|
|
2628
|
+
* this surface has always been redrawn by, and it stays that way rather than growing a second
|
|
2629
|
+
* scheduler that could disagree with it.
|
|
2616
2630
|
*
|
|
2617
2631
|
* @param host - The painted `.panel-body`
|
|
2618
2632
|
* @param rerender - The Navigator's repaint
|
|
@@ -2647,7 +2661,7 @@ export function mountFilesPanel(host: HTMLElement, rerender: () => void): void {
|
|
|
2647
2661
|
* Take the panel down: the document, the drag registrations, the scroll watch and the row handles.
|
|
2648
2662
|
*
|
|
2649
2663
|
* Exported because a project close has to reach it — the tree it is showing belongs to the project
|
|
2650
|
-
* being left behind, exactly as `
|
|
2664
|
+
* being left behind, exactly as `cleanupSourceControl` is for the working tree.
|
|
2651
2665
|
*/
|
|
2652
2666
|
export function unmountFilesPanel(): void {
|
|
2653
2667
|
dismissFileContextMenu();
|
package/src/files/fs-events.ts
CHANGED
|
@@ -237,11 +237,11 @@ export async function relistLoadedDirs(
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
|
-
* Modes only a tab whose `document` is a placeholder offers: the media viewer
|
|
241
|
-
*
|
|
242
|
-
*
|
|
240
|
+
* Modes only a tab whose `document` is a placeholder offers: the media viewer and a CSV grid.
|
|
241
|
+
* Spelled out as strings, not imported, because each constant's module drags its whole editor in
|
|
242
|
+
* behind it. A workspace home has no file at all, so it never reaches the re-read.
|
|
243
243
|
*/
|
|
244
|
-
const STUB_DOCUMENT_MODES = new Set(["media", "grid"
|
|
244
|
+
const STUB_DOCUMENT_MODES = new Set(["media", "grid"]);
|
|
245
245
|
|
|
246
246
|
/**
|
|
247
247
|
* Whether a tab's `document` is a placeholder rather than its parsed file. Such a tab reads its
|
|
@@ -293,8 +293,16 @@ export interface FsSyncContext {
|
|
|
293
293
|
* and this module is imported BY the file layer those panels sit on top of — reaching up would be
|
|
294
294
|
* a cycle. The bootstrap owns the list because the bootstrap is where the panels are already in
|
|
295
295
|
* scope.
|
|
296
|
+
*
|
|
297
|
+
* Handed the project-relative paths the batch touched, so a cache that only one directory can
|
|
298
|
+
* change (the media listing, the component registry) can ignore the rest. `undefined` means the
|
|
299
|
+
* stream had a gap and any path may have changed.
|
|
300
|
+
*
|
|
301
|
+
* `structural` says whether the batch added, removed or renamed anything, as opposed to only
|
|
302
|
+
* rewriting files that already existed: a cache of the file SET can ignore a batch of plain
|
|
303
|
+
* `change` events, which is every save. It is `true` after a gap.
|
|
296
304
|
*/
|
|
297
|
-
invalidateDerivedCaches?: () => void;
|
|
305
|
+
invalidateDerivedCaches?: (paths?: readonly string[], structural?: boolean) => void;
|
|
298
306
|
/**
|
|
299
307
|
* After a reconnect, the clean, non-collab open tabs whose files may have changed while the
|
|
300
308
|
* stream was down. The handler re-reads them; it should leave a tab whose content did not change
|
|
@@ -363,7 +371,7 @@ export function startFsSync(ctx: FsSyncContext): () => void {
|
|
|
363
371
|
);
|
|
364
372
|
if (reason === "reconnect") {
|
|
365
373
|
invalidateUsages();
|
|
366
|
-
ctx.invalidateDerivedCaches?.();
|
|
374
|
+
ctx.invalidateDerivedCaches?.(undefined, true);
|
|
367
375
|
await Promise.all([relist, reloadIgnoreCache()]);
|
|
368
376
|
} else {
|
|
369
377
|
await relist;
|
|
@@ -413,7 +421,10 @@ export function startFsSync(ctx: FsSyncContext): () => void {
|
|
|
413
421
|
// Removed layout's `$head` to the open page, a `$paths` enumeration listing entries that are
|
|
414
422
|
// Gone.
|
|
415
423
|
invalidateUsages();
|
|
416
|
-
ctx.invalidateDerivedCaches?.(
|
|
424
|
+
ctx.invalidateDerivedCaches?.(
|
|
425
|
+
events.map((event) => event.path),
|
|
426
|
+
events.some((event) => event.type !== "change"),
|
|
427
|
+
);
|
|
417
428
|
// Content versions are derived state of the same kind, and Studio's own writes change bytes:
|
|
418
429
|
// A version kept past its echo would let an immutable cache answer with the old file.
|
|
419
430
|
forgetEventVersions(events);
|
package/src/files/media-meta.ts
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* renders anyway. {@link recordImageSize} is how that surface hands the number back, and until
|
|
12
12
|
* one does, the dimensions are `null` and the summary simply omits them. Downloading the file a
|
|
13
13
|
* second time to measure it would be a network request per thumbnail for a line of text.
|
|
14
|
+
* - **An SVG's dimensions are its own**, read from the file: `width`/`height` on the root element, or
|
|
15
|
+
* its `viewBox` when either is missing ({@link svgIntrinsicSize}). A vector image has no pixels
|
|
16
|
+
* to decode, and an `<img>` of one that declares no size reports the replaced element's default
|
|
17
|
+
* object size (150 × 150 in Chromium), a number about the browser rather than the file. So
|
|
18
|
+
* {@link recordImageSize} ignores SVGs, and {@link loadMediaMeta} reads the text instead, which
|
|
19
|
+
* for a vector is a few hundred bytes the thumbnail already fetched.
|
|
14
20
|
*
|
|
15
21
|
* A missing number is `null` everywhere, never `0`. `0 × 0` and `0 B` are things a real file can
|
|
16
22
|
* almost be, so they must not double as "we did not find out" — the same rule
|
|
@@ -63,6 +69,81 @@ const inFlight = new Map<string, Promise<MediaMeta>>();
|
|
|
63
69
|
*/
|
|
64
70
|
const measured = new Map<string, { width: number; height: number }>();
|
|
65
71
|
|
|
72
|
+
/**
|
|
73
|
+
* SVG paths whose own size has been read (or found unreadable), so {@link loadMediaMeta} reads each
|
|
74
|
+
* file once rather than on every paint that finds no dimensions: most SVGs declare none at all.
|
|
75
|
+
*/
|
|
76
|
+
const svgRead = new Set<string>();
|
|
77
|
+
|
|
78
|
+
function isSvg(path: string): boolean {
|
|
79
|
+
return extensionOf(path) === ".svg";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A `width`/`height` attribute in user units (`64`, `64px`), or null for anything relative. */
|
|
83
|
+
function svgLength(value: string | null): number | null {
|
|
84
|
+
const match = value === null ? null : /^\s*(\d+(?:\.\d+)?)\s*(?:px)?\s*$/.exec(value);
|
|
85
|
+
const length = match ? Number(match[1]) : Number.NaN;
|
|
86
|
+
return length > 0 ? length : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The size an SVG document declares for itself, or null when it declares none (the intrinsic
|
|
91
|
+
* dimensions of an outermost `<svg>` element, SVG 2 §8.2): `width` and `height` where both are
|
|
92
|
+
* absolute lengths; one of them with the `viewBox`'s aspect ratio supplying the other; or the
|
|
93
|
+
* `viewBox`'s own width and height when neither is given. A percentage or a font-relative length is
|
|
94
|
+
* a size relative to whatever embeds the file, which a file on its own does not have.
|
|
95
|
+
*/
|
|
96
|
+
export function svgIntrinsicSize(text: string): { width: number; height: number } | null {
|
|
97
|
+
const root = new DOMParser().parseFromString(text, "image/svg+xml").documentElement;
|
|
98
|
+
if (root.localName !== "svg") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const width = svgLength(root.getAttribute("width"));
|
|
102
|
+
const height = svgLength(root.getAttribute("height"));
|
|
103
|
+
const box = (root.getAttribute("viewBox") ?? "")
|
|
104
|
+
.trim()
|
|
105
|
+
.split(/[\s,]+/)
|
|
106
|
+
.map(Number);
|
|
107
|
+
const boxWidth = box[2] ?? Number.NaN;
|
|
108
|
+
const boxHeight = box[3] ?? Number.NaN;
|
|
109
|
+
const hasBox = box.length === 4 && boxWidth > 0 && boxHeight > 0;
|
|
110
|
+
let size: { width: number; height: number } | null = null;
|
|
111
|
+
if (width !== null && height !== null) {
|
|
112
|
+
size = { height, width };
|
|
113
|
+
} else if (hasBox && width !== null) {
|
|
114
|
+
size = { height: (width * boxHeight) / boxWidth, width };
|
|
115
|
+
} else if (hasBox && height !== null) {
|
|
116
|
+
size = { height, width: (height * boxWidth) / boxHeight };
|
|
117
|
+
} else if (hasBox) {
|
|
118
|
+
size = { height: boxHeight, width: boxWidth };
|
|
119
|
+
}
|
|
120
|
+
return size && { height: Math.round(size.height), width: Math.round(size.width) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Read an SVG's declared size into {@link measured}, once per path. */
|
|
124
|
+
async function readSvgSize(path: string): Promise<void> {
|
|
125
|
+
svgRead.add(path);
|
|
126
|
+
try {
|
|
127
|
+
const size = svgIntrinsicSize(await getPlatform().readFile(path));
|
|
128
|
+
if (size && size.width > 0 && size.height > 0) {
|
|
129
|
+
measured.set(path, size);
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Unreadable: the dimensions stay unknown, which the summary already says by omitting them.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whether {@link loadMediaMeta} has nothing left to learn about a file: its record is settled and,
|
|
138
|
+
* for an SVG, its own declared size has been read. A surface asks this, rather than whether
|
|
139
|
+
* {@link peekMediaMeta} is null, before starting a load: a listing seeds every SVG's record without
|
|
140
|
+
* the dimensions only the file itself can give.
|
|
141
|
+
*/
|
|
142
|
+
export function mediaMetaSettled(path: string): boolean {
|
|
143
|
+
const key = normalizeProjectPath(path);
|
|
144
|
+
return settled.has(key) && (!isSvg(key) || svgRead.has(key));
|
|
145
|
+
}
|
|
146
|
+
|
|
66
147
|
/** Build a meta record from a path and whatever the listing knew about it. */
|
|
67
148
|
function metaFrom(path: string, entry: DirEntry | undefined): MediaMeta {
|
|
68
149
|
const name = baseName(path);
|
|
@@ -118,7 +199,7 @@ export function seedMediaMeta(entries: Iterable<DirEntry>): void {
|
|
|
118
199
|
export async function loadMediaMeta(path: string): Promise<MediaMeta> {
|
|
119
200
|
const key = normalizeProjectPath(path);
|
|
120
201
|
const done = settled.get(key);
|
|
121
|
-
if (done) {
|
|
202
|
+
if (done && mediaMetaSettled(key)) {
|
|
122
203
|
return done;
|
|
123
204
|
}
|
|
124
205
|
const running = inFlight.get(key);
|
|
@@ -126,6 +207,14 @@ export async function loadMediaMeta(path: string): Promise<MediaMeta> {
|
|
|
126
207
|
return running;
|
|
127
208
|
}
|
|
128
209
|
const pending = (async (): Promise<MediaMeta> => {
|
|
210
|
+
if (isSvg(key) && !svgRead.has(key)) {
|
|
211
|
+
await readSvgSize(key);
|
|
212
|
+
}
|
|
213
|
+
if (done) {
|
|
214
|
+
// Seeded by a listing already: only the SVG's own size was missing.
|
|
215
|
+
const dims = measured.get(key);
|
|
216
|
+
return { ...done, height: dims?.height ?? null, width: dims?.width ?? null };
|
|
217
|
+
}
|
|
129
218
|
let entry: DirEntry | undefined;
|
|
130
219
|
try {
|
|
131
220
|
const listing = await notedListing(() => getPlatform().listDirectory(dirName(key)));
|
|
@@ -154,6 +243,10 @@ export function recordImageSize(path: string, width: number, height: number): bo
|
|
|
154
243
|
return false; // A broken or still-decoding image reports 0 — that is not a measurement.
|
|
155
244
|
}
|
|
156
245
|
const key = normalizeProjectPath(path);
|
|
246
|
+
if (isSvg(key)) {
|
|
247
|
+
// The decode of a vector measures the browser's default object size, not the file.
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
157
250
|
const known = measured.get(key);
|
|
158
251
|
if (known && known.width === width && known.height === height) {
|
|
159
252
|
return false;
|
|
@@ -170,6 +263,7 @@ export function recordImageSize(path: string, width: number, height: number): bo
|
|
|
170
263
|
export function invalidateMediaMeta(): void {
|
|
171
264
|
settled.clear();
|
|
172
265
|
measured.clear();
|
|
266
|
+
svgRead.clear();
|
|
173
267
|
}
|
|
174
268
|
|
|
175
269
|
/** `84 KB`, `1.2 MB`, `903 B` — one significant fraction, because this is a caption. */
|