@jxsuite/studio 6.1.1 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/chunks/2020-5wtzce5g.js +4 -0
- package/dist/chunks/{action-group-2mqby73c.js → action-group-4aan7g6g.js} +3 -3
- package/dist/chunks/adapter-5tphg7xe.js +4 -0
- package/dist/chunks/add-new-flow-psym14sn.js +4 -0
- package/dist/chunks/cf-account-picker-haqazq5r.js +4 -0
- package/dist/chunks/cf-settings-h2wrh6hq.js +4 -0
- package/dist/chunks/color-area-6rbra5dy.js +4 -0
- package/dist/chunks/color-field-gy56c94g.js +4 -0
- package/dist/chunks/color-m7r1vk54.js +4 -0
- package/dist/chunks/color-slider-sqqdyf6c.js +4 -0
- package/dist/chunks/combobox-72h32ejk.js +4 -0
- package/dist/chunks/components-r22xca09.js +4 -0
- package/dist/chunks/content-source-mmppmcyg.js +4 -0
- package/dist/chunks/convert-to-repeater-he70fc2b.js +4 -0
- package/dist/chunks/dialog-c4x05vyx.js +4 -0
- package/dist/chunks/editor-ccd9jwsa.js +4 -0
- package/dist/chunks/entry-commands-6qxz9e8p.js +4 -0
- package/dist/chunks/extension-sections-0egb88dm.js +4 -0
- package/dist/chunks/file-ops-qnab36ek.js +4 -0
- package/dist/chunks/files-rcn56qvm.js +4 -0
- package/dist/chunks/format-host-yxz0kjjs.js +4 -0
- package/dist/chunks/fs-events-d1qxq4z4.js +4 -0
- package/dist/chunks/grid-open-4z1g062t.js +4 -0
- package/dist/chunks/grid-view-qg1b5eqx.js +51 -0
- package/dist/chunks/icons-63bjtbs7.js +4 -0
- package/dist/chunks/iframe-entry-2zggzajv.js +5 -0
- package/dist/chunks/iframe-entry-76j8vwwx.js +5 -0
- package/dist/chunks/iframe-entry-99nr5zfb.js +5 -0
- package/dist/chunks/iframe-entry-pkgmcgp0.js +5 -0
- package/dist/chunks/iframe-host-ssv7kyj0.js +4 -0
- package/dist/chunks/{index-2veqmnhr.js → index-32eqzv1k.js} +3 -3
- package/dist/chunks/index-f0jn7nnp.js +4 -0
- package/dist/chunks/{javascript-8nbvs12s.js → javascript-pjp734q4.js} +3 -3
- package/dist/chunks/jsonMode-njkvsa02.js +13 -0
- package/dist/chunks/jx-validate-ttasrbh7.js +4 -0
- package/dist/chunks/layers-bgyr11tr.js +4 -0
- package/dist/chunks/layers-panel-7ez66zeq.js +4 -0
- package/dist/chunks/listbox-c421cf8c.js +4 -0
- package/dist/chunks/lit-html-v9gh687x.js +4 -0
- package/dist/chunks/media-library-sq468wts.js +4 -0
- package/dist/chunks/media-picker-shfe59pt.js +4 -0
- package/dist/chunks/menu-bdpzbfvs.js +4 -0
- package/dist/chunks/monaco-binding-vq1hgnn0.js +4 -0
- package/dist/chunks/monaco-buffer-3afgwvbp.js +4 -0
- package/dist/chunks/monaco-setup-xrd2cbkp.js +4 -0
- package/dist/chunks/notify-kzrjr3fv.js +4 -0
- package/dist/chunks/number-field-y62xq0ty.js +4 -0
- package/dist/chunks/parse-v77xtwv7.js +4 -0
- package/dist/chunks/platform-7v4rtd7m.js +4 -0
- package/dist/chunks/popover-b89sfej7.js +4 -0
- package/dist/chunks/publish-panel-2z9gw8ee.js +4 -0
- package/dist/chunks/publish-quick-qx2455cj.js +4 -0
- package/dist/chunks/publish-workspace-wrc1dgwx.js +4 -0
- package/dist/chunks/quick-search-n0t0eqxb.js +4 -0
- package/dist/chunks/references-9547rg5s.js +4 -0
- package/dist/chunks/right-panel-wsgnr9pn.js +4 -0
- package/dist/chunks/select-rzds841e.js +4 -0
- package/dist/chunks/source-control-kwktr9y6.js +4 -0
- package/dist/chunks/split-c9zfdgxf.js +4 -0
- package/dist/chunks/studio-03ct6yfv.js +9 -0
- package/dist/chunks/studio-07rcfje3.js +5 -0
- package/dist/chunks/studio-3e54qh39.js +7 -0
- package/dist/chunks/studio-3exxhz5g.js +5 -0
- package/dist/chunks/studio-3q40e3rj.js +5 -0
- package/dist/chunks/studio-3qeh9t0h.js +6 -0
- package/dist/chunks/studio-3qqjeep5.js +6 -0
- package/dist/chunks/studio-43sd2mam.js +4 -0
- package/dist/chunks/studio-4hp8pkyt.js +5 -0
- package/dist/chunks/studio-58xg6j2q.js +9 -0
- package/dist/chunks/studio-5g0rprdt.js +7 -0
- package/dist/chunks/studio-6m380wp7.js +5 -0
- package/dist/chunks/studio-80jyh1xh.js +4 -0
- package/dist/chunks/studio-83mk4bgg.js +9 -0
- package/dist/chunks/{studio-kggey1jt.js → studio-881z1xrx.js} +4 -4
- package/dist/chunks/studio-8m0y2qe7.js +5 -0
- package/dist/chunks/studio-8tr6cfrc.js +5 -0
- package/dist/chunks/studio-8vcvfs7r.js +6 -0
- package/dist/chunks/studio-9tmebf0s.js +5 -0
- package/dist/chunks/studio-a2bvaptz.js +5 -0
- package/dist/chunks/studio-b7y5mr30.js +5 -0
- package/dist/chunks/studio-bavb31r0.js +5 -0
- package/dist/chunks/studio-bbe4c90a.js +5 -0
- package/dist/chunks/studio-c05wdnt3.js +5 -0
- package/dist/chunks/studio-ccv2svc6.js +5 -0
- package/dist/chunks/studio-d5grtb9e.js +16 -0
- package/dist/chunks/studio-df0baea9.js +5 -0
- package/dist/chunks/studio-dgk4dqtx.js +7 -0
- package/dist/chunks/studio-e1g64q43.js +5 -0
- package/dist/chunks/studio-ed1hp6zf.js +6 -0
- package/dist/chunks/studio-ed3xx40h.js +7 -0
- package/dist/chunks/studio-f14e1zdj.js +5 -0
- package/dist/chunks/studio-fbhxxc5f.js +5 -0
- package/dist/chunks/studio-fyh0jc96.js +5 -0
- package/dist/chunks/studio-gcxk20n7.js +5 -0
- package/dist/chunks/studio-jaaz8ce9.js +5 -0
- package/dist/chunks/{studio-8grwxtqk.js → studio-jcnsdaep.js} +4 -4
- package/dist/chunks/studio-jsgf2q5k.js +7 -0
- package/dist/chunks/{studio-1q1sh2ax.js → studio-k3xxbagr.js} +3 -3
- package/dist/chunks/studio-k7evxqh4.js +5 -0
- package/dist/chunks/studio-kt3pjkwd.js +5 -0
- package/dist/chunks/studio-m8g16bgz.js +5 -0
- package/dist/chunks/studio-mn0476ag.js +5 -0
- package/dist/chunks/studio-mx1smyq0.js +5 -0
- package/dist/chunks/studio-n0c5h6mf.js +5 -0
- package/dist/chunks/{studio-rv11kfwa.js → studio-nh6d342n.js} +7 -7
- package/dist/chunks/studio-nqxqf8p2.js +4 -0
- package/dist/chunks/studio-p4nrkvqr.js +5 -0
- package/dist/chunks/{studio-h46vtzk8.js → studio-pbxa5fpc.js} +4 -4
- package/dist/chunks/studio-pgmvdv7z.js +7 -0
- package/dist/chunks/studio-ph84avkf.js +5 -0
- package/dist/chunks/{studio-wszjyh0j.js → studio-q0mvbaq6.js} +4 -4
- package/dist/chunks/studio-q0tccd43.js +5 -0
- package/dist/chunks/studio-q7rtx6zq.js +14 -0
- package/dist/chunks/{studio-pre0skq6.js → studio-qgh13jtz.js} +4 -4
- package/dist/chunks/studio-qmn1xc24.js +5 -0
- package/dist/chunks/studio-qwdqwkj0.js +5 -0
- package/dist/chunks/{studio-t6b6qe31.js → studio-r5qy30vx.js} +4 -4
- package/dist/chunks/studio-rj9eamf8.js +5 -0
- package/dist/chunks/studio-rptgsk96.js +5 -0
- package/dist/chunks/studio-rthyg42z.js +928 -0
- package/dist/chunks/studio-s19tp1xr.js +5 -0
- package/dist/chunks/studio-s32578pn.js +5 -0
- package/dist/chunks/studio-svb7yyve.js +5 -0
- package/dist/chunks/studio-syreb644.js +5 -0
- package/dist/chunks/studio-tf9jryb1.js +10 -0
- package/dist/chunks/studio-tfkznv39.js +5 -0
- package/dist/chunks/studio-tft6r105.js +5 -0
- package/dist/chunks/studio-txf2kysx.js +5 -0
- package/dist/chunks/studio-v8867ddv.js +12 -0
- package/dist/chunks/studio-vxq7fcvv.js +5 -0
- package/dist/chunks/studio-w4rz9hdk.js +5 -0
- package/dist/chunks/studio-wbcjbhy1.js +9 -0
- package/dist/chunks/studio-wc649wss.js +5 -0
- package/dist/chunks/studio-wys23mxz.js +5 -0
- package/dist/chunks/studio-x1x167jh.js +5 -0
- package/dist/chunks/studio-x9hsze61.js +5 -0
- package/dist/chunks/studio-y0n7yxea.js +9 -0
- package/dist/chunks/studio-y3ahhdpx.js +5 -0
- package/dist/chunks/studio-z97c48fk.js +5 -0
- package/dist/chunks/studio-zat62m0c.js +5 -0
- package/dist/chunks/studio-zcd2e7yz.js +5 -0
- package/dist/chunks/studio-zz9qe3rw.js +64 -0
- package/dist/chunks/swatch-group-ewh24n1f.js +4 -0
- package/dist/chunks/tabs-f5cw3tgp.js +4 -0
- package/dist/chunks/textfield-wewc8rq2.js +4 -0
- package/dist/chunks/toast-1gv0h53h.js +4 -0
- package/dist/chunks/toast-host-1yg5ar8d.js +4 -0
- package/dist/chunks/{toolbar-2nnzqhrj.js → toolbar-44cm5cta.js} +3 -3
- package/dist/chunks/tooltip-yypz28gq.js +4 -0
- package/dist/chunks/tree-9xa4hm7n.js +4 -0
- package/dist/chunks/tsMode-epyfz1hv.js +14 -0
- package/dist/chunks/workspace-dz7nxfhv.js +4 -0
- package/dist/chunks/workspace-home-xsd7hkjw.js +4 -0
- package/dist/chunks/ws-client-bv552sfx.js +4 -0
- package/dist/iframe-entry.js +20 -16
- package/dist/manifest.json +77 -63
- package/dist/studio.js +106 -107
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +17 -16
- package/src/about/about-modal.ts +4 -3
- package/src/account/action-flow.ts +45 -38
- package/src/account-status.ts +7 -2
- package/src/browse/component-examples.ts +272 -0
- package/src/browse/library-commands.ts +64 -104
- package/src/browse/library-layouts.ts +270 -53
- package/src/browse/library-model.ts +194 -134
- package/src/browse/library-pane.ts +748 -440
- package/src/browse/library-preview.ts +161 -26
- package/src/browse/library-source.ts +308 -41
- package/src/browse/preview-root.ts +78 -0
- package/src/browse/preview-style.ts +155 -0
- package/src/canvas/asset-refs.ts +40 -10
- package/src/canvas/asset-resolve.ts +19 -23
- package/src/canvas/canvas-helpers.ts +7 -6
- package/src/canvas/canvas-live-render.ts +260 -77
- package/src/canvas/canvas-origin.ts +37 -12
- package/src/canvas/canvas-page-context.ts +112 -0
- package/src/canvas/canvas-patcher.ts +30 -30
- package/src/canvas/canvas-perf.ts +11 -5
- package/src/canvas/canvas-render.ts +175 -148
- package/src/canvas/canvas-surface.ts +5 -3
- package/src/canvas/canvas-utils.ts +147 -61
- package/src/canvas/dialog-path.ts +9 -4
- package/src/canvas/dialog-state.ts +1 -2
- package/src/canvas/diff-marks.ts +9 -16
- package/src/canvas/diff-toolbar.ts +8 -7
- package/src/canvas/edit-width-drag.ts +4 -1
- package/src/canvas/edit-width.ts +1 -3
- package/src/canvas/iframe-channel.ts +27 -13
- package/src/canvas/iframe-drop.ts +120 -12
- package/src/canvas/iframe-editable-root.ts +29 -16
- package/src/canvas/iframe-entry.ts +104 -75
- package/src/canvas/iframe-eval.ts +73 -49
- package/src/canvas/iframe-host.ts +269 -158
- package/src/canvas/iframe-inline-edit.ts +49 -30
- package/src/canvas/iframe-insert.ts +5 -4
- package/src/canvas/iframe-interaction.ts +30 -25
- package/src/canvas/iframe-keys.ts +11 -0
- package/src/canvas/iframe-overlay.ts +1 -1
- package/src/canvas/iframe-patch.ts +25 -21
- package/src/canvas/iframe-position.ts +12 -6
- package/src/canvas/iframe-protocol.ts +36 -21
- package/src/canvas/iframe-render.ts +84 -35
- package/src/canvas/iframe-slash.ts +24 -11
- package/src/canvas/iframe-subtree.ts +10 -3
- package/src/canvas/npm-modules.ts +22 -0
- package/src/canvas/path-mapping.ts +19 -12
- package/src/canvas/popover-path.ts +7 -3
- package/src/canvas/popover-state.ts +1 -2
- package/src/canvas/preview-navigate.ts +16 -0
- package/src/canvas/repeater-sources.ts +200 -0
- package/src/canvas/serialize-scope.ts +13 -5
- package/src/canvas/stage-scrollbar.ts +7 -2
- package/src/canvas/surface-registry.ts +27 -3
- package/src/collab/collab-commands.ts +7 -2
- package/src/collab/collab-session.ts +84 -45
- package/src/collab/collab-state.ts +4 -3
- package/src/collab/monaco-binding.ts +16 -13
- package/src/collab/monaco-cursors.ts +5 -2
- package/src/commands/app-commands.ts +16 -6
- package/src/commands/budget.ts +13 -11
- package/src/commands/command-args.ts +103 -71
- package/src/commands/context.ts +55 -28
- package/src/commands/defaults.ts +69 -45
- package/src/commands/keymap.ts +56 -43
- package/src/commands/levels.ts +14 -8
- package/src/commands/live-context.ts +9 -7
- package/src/commands/reference.ts +2 -1
- package/src/commands/registry.ts +22 -11
- package/src/commands/run-reported.ts +15 -10
- package/src/component-props.ts +46 -30
- package/src/content/collection-match.ts +11 -9
- package/src/content/collection-pages.ts +412 -0
- package/src/content/content-collections.ts +270 -0
- package/src/content/draft-state.ts +4 -3
- package/src/content/entry-commands.ts +54 -62
- package/src/content/entry-editor.ts +137 -32
- package/src/content/entry-fields.ts +25 -8
- package/src/content/entry-inspector.ts +225 -0
- package/src/content/entry-model.ts +38 -15
- package/src/content/entry-open.ts +80 -0
- package/src/content/entry-summaries.ts +440 -0
- package/src/content/file-record-ids.ts +80 -0
- package/src/content/file-records.ts +125 -0
- package/src/content/page-summaries.ts +129 -0
- package/src/content/record-entry.ts +201 -0
- package/src/content/writing-banner.ts +177 -0
- package/src/create/add-new-commands.ts +185 -0
- package/src/create/add-new-flow.ts +1116 -0
- package/src/create/add-new.ts +1014 -0
- package/src/create/element-name.ts +121 -0
- package/src/editor/canvas-context-menu.ts +1 -2
- package/src/editor/canvas-slash-bridge.ts +3 -6
- package/src/editor/content-slice.ts +16 -14
- package/src/editor/context-menu.ts +35 -22
- package/src/editor/convert-targets.ts +30 -35
- package/src/editor/convert-to-component.ts +78 -48
- package/src/editor/convert-to-repeater.ts +34 -22
- package/src/editor/editable-tags.ts +17 -2
- package/src/editor/file-drop-action.ts +29 -22
- package/src/editor/inline-edit-apply.ts +79 -17
- package/src/editor/inline-edit.ts +76 -50
- package/src/editor/inline-format.ts +59 -56
- package/src/editor/inline-link.ts +40 -15
- package/src/editor/insert-component.ts +91 -0
- package/src/editor/insert-zone-action.ts +14 -5
- package/src/editor/merge-tags.ts +58 -19
- package/src/editor/repeater-scope.ts +39 -28
- package/src/editor/shortcuts.ts +76 -42
- package/src/editor/slash-menu.ts +52 -14
- package/src/files/asset-versions.ts +4 -4
- package/src/files/components.ts +89 -20
- package/src/files/elements.ts +5 -6
- package/src/files/file-ops.ts +92 -41
- package/src/files/files.ts +248 -239
- package/src/files/fs-events.ts +18 -7
- package/src/files/media-meta.ts +97 -2
- package/src/files/media-paths.ts +3 -0
- package/src/files/media-types.ts +55 -0
- package/src/files/media-upload.ts +209 -52
- package/src/files/new-file-name.ts +156 -0
- package/src/files/serialize-document.ts +15 -5
- package/src/format/constraints.ts +1 -1
- package/src/format/convert-file.ts +27 -13
- package/src/format/format-choices.ts +2 -1
- package/src/format/format-host.ts +91 -50
- package/src/github/github-auth-status.ts +27 -3
- package/src/github/github-auth.ts +21 -11
- package/src/github/github-publish.ts +28 -23
- package/src/grid/cell-editors.ts +46 -31
- package/src/grid/cell-popovers.ts +5 -3
- package/src/grid/edit-buffer.ts +10 -9
- package/src/grid/grid-controller.ts +25 -23
- package/src/grid/grid-layout.ts +21 -5
- package/src/grid/grid-open.ts +3 -33
- package/src/grid/grid-panel.ts +65 -6
- package/src/grid/grid-reveal.ts +34 -0
- package/src/grid/grid-source.ts +9 -31
- package/src/grid/grid-view.ts +45 -23
- package/src/grid/redirects-grid.ts +14 -5
- package/src/grid/redirects.ts +16 -14
- package/src/grid/schema-columns.ts +25 -19
- package/src/grid/sources/connector-source.ts +47 -20
- package/src/grid/sources/content-source.ts +100 -31
- package/src/grid/sources/csv-file-source.ts +2 -2
- package/src/grid/tabulator-tables.d.ts +61 -34
- package/src/hosting/document.ts +2 -2
- package/src/hosting/stage.ts +7 -7
- package/src/i18n/i18n-commands.ts +3 -3
- package/src/media/media-commands.ts +181 -0
- package/src/media/media-library-model.ts +232 -0
- package/src/media/media-library.ts +1283 -0
- package/src/media/media-pane.ts +51 -20
- package/src/new-project/add-repo-modal.ts +20 -19
- package/src/new-project/import-tab.ts +10 -14
- package/src/new-project/location-fields.ts +2 -1
- package/src/new-project/new-project-modal.ts +12 -9
- package/src/packages/jxsuite-update.ts +4 -3
- package/src/packages/pull-package-sync.ts +26 -25
- package/src/page-params.ts +97 -68
- package/src/panels/activity-panel.ts +38 -14
- package/src/panels/ai-chat/attached-context.ts +13 -7
- package/src/panels/ai-chat/chat-markdown.ts +8 -8
- package/src/panels/ai-chat/chat-view.ts +80 -73
- package/src/panels/ai-chat/composer.ts +30 -33
- package/src/panels/ai-chat/sessions-view.ts +1 -2
- package/src/panels/ai-panel.ts +23 -20
- package/src/panels/block-action-bar.ts +57 -46
- package/src/panels/bottom-dock.ts +5 -3
- package/src/panels/canvas-dnd-bridge.ts +36 -26
- package/src/panels/chat-panel.ts +6 -6
- package/src/panels/collections-panel.ts +306 -0
- package/src/panels/component-preview.ts +35 -13
- package/src/panels/data-explorer.ts +63 -26
- package/src/panels/data-grid.ts +18 -11
- package/src/panels/dnd.ts +115 -138
- package/src/panels/editors.ts +52 -21
- package/src/panels/elements-panel.ts +19 -5
- package/src/panels/empty-state.ts +2 -2
- package/src/panels/events-panel.ts +84 -69
- package/src/panels/formula-workspace.ts +78 -54
- package/src/panels/frontmatter-fields.ts +33 -28
- package/src/panels/git-diff-open.ts +10 -6
- package/src/panels/head-panel.ts +488 -202
- package/src/panels/i18n-panel.ts +20 -12
- package/src/panels/imports-panel.ts +25 -47
- package/src/panels/jump-bar.ts +75 -31
- package/src/panels/layers-panel.ts +130 -101
- package/src/panels/left-panel.ts +25 -11
- package/src/panels/navigator-panels.ts +23 -11
- package/src/panels/pane-context.ts +140 -49
- package/src/panels/pane-grid.ts +55 -26
- package/src/panels/panel-registry.ts +36 -46
- package/src/panels/properties-panel.ts +182 -124
- package/src/panels/quick-search.ts +53 -31
- package/src/panels/right-panel.ts +201 -45
- package/src/panels/seo-modal.ts +54 -125
- package/src/panels/settings-menu.ts +63 -61
- package/src/panels/settings-pane.ts +268 -162
- package/src/panels/signals-panel.ts +156 -135
- package/src/panels/site-model.ts +367 -0
- package/src/panels/site-panel.ts +638 -0
- package/src/panels/statement-editor.ts +230 -140
- package/src/panels/style-panel.ts +253 -206
- package/src/panels/style-utils.ts +114 -51
- package/src/panels/stylebook-doc.ts +195 -60
- package/src/panels/stylebook-layers-panel.ts +48 -89
- package/src/panels/stylebook-panel.ts +60 -27
- package/src/panels/styles-panel.ts +327 -0
- package/src/panels/tab-drop.ts +27 -11
- package/src/panels/tab-strip.ts +143 -49
- package/src/platform-errors.ts +57 -43
- package/src/platform.ts +59 -31
- package/src/platforms/cloud.ts +373 -176
- package/src/platforms/default-platform.ts +14 -8
- package/src/platforms/devserver.ts +180 -142
- package/src/platforms/read-batcher.ts +16 -8
- package/src/project-list.ts +2 -1
- package/src/publish/deploy-checklist.ts +20 -9
- package/src/publish/pages-service.ts +66 -27
- package/src/publish/publish-changes.ts +491 -0
- package/src/publish/publish-commands.ts +88 -22
- package/src/publish/publish-ids.ts +49 -0
- package/src/publish/publish-panel.ts +10 -6
- package/src/publish/publish-quick.ts +148 -0
- package/src/publish/publish-workspace.ts +926 -0
- package/src/publish/source-control.ts +907 -0
- package/src/recent-projects.ts +15 -8
- package/src/resize-edges.ts +26 -36
- package/src/services/a11y-report.ts +26 -19
- package/src/services/agent-seed.ts +23 -11
- package/src/services/ai-ask.ts +9 -12
- package/src/services/ai-command-tools.ts +68 -62
- package/src/services/ai-import-tools.ts +80 -67
- package/src/services/ai-models.ts +17 -13
- package/src/services/ai-project-tools.ts +68 -32
- package/src/services/ai-session-store.ts +19 -10
- package/src/services/ai-system-prompt.ts +46 -20
- package/src/services/ai-tools.ts +71 -27
- package/src/services/ai-write-report.ts +20 -12
- package/src/services/ai-writes.ts +4 -4
- package/src/services/automation.ts +74 -51
- package/src/services/cem-export.ts +149 -58
- package/src/services/cf-settings.ts +4 -4
- package/src/services/clock.ts +6 -1
- package/src/services/code-services.ts +37 -31
- package/src/services/connection.ts +2 -2
- package/src/services/context-manager.ts +14 -10
- package/src/services/context-resolver.ts +20 -8
- package/src/services/data-service.ts +19 -15
- package/src/services/directory-picker.ts +20 -14
- package/src/services/document-assistant.ts +39 -19
- package/src/services/gated-registry.ts +10 -6
- package/src/services/harness/chat-reducer.ts +23 -1
- package/src/services/import-client.ts +91 -38
- package/src/services/import-seed.ts +8 -2
- package/src/services/jx-validate.ts +28 -31
- package/src/services/live-preview.ts +0 -0
- package/src/services/live-surfaces.ts +6 -3
- package/src/services/model-uri.ts +10 -2
- package/src/services/module-preloads.ts +12 -8
- package/src/services/monaco-buffer.ts +43 -24
- package/src/services/monaco-lazy.ts +7 -8
- package/src/services/monaco-setup.ts +23 -16
- package/src/services/notify.ts +44 -17
- package/src/services/popover-report.ts +25 -10
- package/src/services/preview-eval.ts +20 -8
- package/src/services/profile.ts +13 -6
- package/src/services/project-path.ts +13 -10
- package/src/services/references.ts +0 -0
- package/src/services/render-critic.ts +29 -9
- package/src/services/settings/definitions.ts +25 -14
- package/src/services/settings/kernel.ts +13 -4
- package/src/services/settings/write-queue.ts +9 -9
- package/src/services/token-lint.ts +12 -10
- package/src/services/tool-executor.ts +17 -5
- package/src/services/tool-outcomes.ts +44 -24
- package/src/services/trusted-types.ts +24 -7
- package/src/settings/contexts-section.ts +11 -8
- package/src/settings/contributed-section.ts +128 -70
- package/src/settings/css-vars-editor.ts +119 -32
- package/src/settings/defs-editor.ts +38 -35
- package/src/settings/developer-panels.ts +43 -0
- package/src/settings/extension-rows.ts +16 -8
- package/src/settings/extension-sections.ts +42 -14
- package/src/settings/extensions-section.ts +4 -1
- package/src/settings/general-settings.ts +101 -28
- package/src/settings/locales-section.ts +20 -12
- package/src/settings/preferences-dialog.ts +17 -7
- package/src/settings/preferences-keymap.ts +7 -5
- package/src/settings/preferences-sections.ts +11 -5
- package/src/settings/project-sections.ts +28 -11
- package/src/settings/schema-field-ui.ts +12 -1
- package/src/settings/section-registry.ts +116 -11
- package/src/settings/settings-dialog.ts +192 -0
- package/src/settings/settings-document.ts +186 -166
- package/src/shell/tree.ts +6 -5
- package/src/shell.ts +536 -477
- package/src/site-context.ts +101 -32
- package/src/state.ts +68 -57
- package/src/store.ts +54 -34
- package/src/studio.ts +219 -116
- package/src/style/live-preview.ts +1 -1
- package/src/style/project-styles.ts +40 -7
- package/src/style/stylebook-specimens.ts +89 -0
- package/src/style/stylebook-target.ts +26 -0
- package/src/style/tag-inheritance.ts +105 -0
- package/src/style/token-ref.ts +5 -4
- package/src/style/variables-focus.ts +61 -0
- package/src/style/variables-view.ts +235 -0
- package/src/surfaces/about.ts +18 -19
- package/src/surfaces/add-new.json +1482 -0
- package/src/surfaces/add-new.ts +325 -0
- package/src/surfaces/add-repo.ts +17 -19
- package/src/surfaces/ai-chat.ts +33 -38
- package/src/surfaces/ai-credentials-form.ts +6 -16
- package/src/surfaces/ai-managed-connect.ts +3 -4
- package/src/surfaces/ai-model-picker.ts +3 -5
- package/src/surfaces/block-action-bar.ts +22 -11
- package/src/surfaces/boot-failure.json +4 -3
- package/src/surfaces/boot-failure.ts +4 -5
- package/src/surfaces/bottom-dock.json +1 -1
- package/src/surfaces/bottom-dock.ts +9 -18
- package/src/surfaces/canvas-stage.json +30 -19
- package/src/surfaces/canvas-stage.ts +17 -16
- package/src/surfaces/cf-account-picker.ts +17 -18
- package/src/surfaces/collections-panel.json +615 -0
- package/src/surfaces/collections-panel.ts +168 -0
- package/src/surfaces/commandbar.json +241 -96
- package/src/surfaces/commandbar.ts +145 -105
- package/src/surfaces/convert-repeater.ts +17 -19
- package/src/surfaces/data-actions.ts +9 -7
- package/src/surfaces/dialog.ts +26 -28
- package/src/surfaces/diff-toolbar.ts +9 -7
- package/src/surfaces/empty-state.json +1 -1
- package/src/surfaces/empty-state.ts +7 -10
- package/src/surfaces/entry-editor.json +136 -0
- package/src/surfaces/entry-editor.ts +35 -11
- package/src/surfaces/entry-inspector.json +371 -0
- package/src/surfaces/entry-inspector.ts +143 -0
- package/src/surfaces/expression-editor.ts +11 -25
- package/src/surfaces/files-panel.ts +27 -14
- package/src/surfaces/formula-palette.ts +4 -5
- package/src/surfaces/github-auth.ts +16 -20
- package/src/surfaces/github-publish.ts +17 -22
- package/src/surfaces/grid-cell.ts +30 -31
- package/src/surfaces/grid-open.ts +10 -14
- package/src/surfaces/grid-panel.ts +13 -9
- package/src/surfaces/grid-replace.ts +13 -13
- package/src/surfaces/grid-views.ts +15 -18
- package/src/surfaces/inspector-dock.json +162 -14
- package/src/surfaces/inspector-dock.ts +59 -31
- package/src/surfaces/jump-bar.ts +5 -6
- package/src/surfaces/jxsuite-update.ts +16 -22
- package/src/surfaces/library-pane.json +670 -437
- package/src/surfaces/library-pane.ts +111 -77
- package/src/surfaces/logic-panel.ts +17 -30
- package/src/surfaces/logic-workspace.ts +13 -15
- package/src/surfaces/media-browser.json +62 -6
- package/src/surfaces/media-browser.ts +26 -11
- package/src/surfaces/media-field.json +1 -1
- package/src/surfaces/media-field.ts +4 -5
- package/src/surfaces/media-library.json +1586 -0
- package/src/surfaces/media-library.ts +282 -0
- package/src/surfaces/media-pane.ts +8 -7
- package/src/surfaces/media-pick-dialog.json +71 -0
- package/src/surfaces/media-pick-dialog.ts +132 -0
- package/src/surfaces/menu.ts +47 -23
- package/src/surfaces/navigator-dock.ts +11 -12
- package/src/surfaces/new-project.ts +27 -26
- package/src/surfaces/pane-context.json +144 -77
- package/src/surfaces/pane-context.ts +44 -25
- package/src/surfaces/pane-grid.ts +15 -10
- package/src/surfaces/panel-activity.ts +5 -6
- package/src/surfaces/panel-data.ts +3 -4
- package/src/surfaces/panel-deploy-checklist.json +12 -6
- package/src/surfaces/panel-deploy-checklist.ts +8 -7
- package/src/surfaces/panel-elements.ts +5 -7
- package/src/surfaces/panel-i18n.ts +5 -7
- package/src/surfaces/panel-imports.ts +6 -13
- package/src/surfaces/panel-outline.ts +25 -19
- package/src/surfaces/panel-page.json +72 -2
- package/src/surfaces/panel-page.ts +49 -25
- package/src/surfaces/panel-problems.ts +5 -7
- package/src/surfaces/panel-signals.json +1 -1
- package/src/surfaces/panel-signals.ts +25 -39
- package/src/surfaces/panel-stylebook-layers.json +71 -204
- package/src/surfaces/panel-stylebook-layers.ts +5 -29
- package/src/surfaces/preferences.json +120 -48
- package/src/surfaces/preferences.ts +29 -11
- package/src/surfaces/progress-modal.ts +15 -9
- package/src/surfaces/properties-panel.json +1 -1
- package/src/surfaces/properties-panel.ts +20 -42
- package/src/surfaces/publish-quick.json +428 -0
- package/src/surfaces/publish-quick.ts +165 -0
- package/src/surfaces/publish-workspace.json +2892 -0
- package/src/surfaces/publish-workspace.ts +391 -0
- package/src/surfaces/publish.ts +18 -18
- package/src/surfaces/push-plan.ts +7 -8
- package/src/surfaces/rail.json +1 -159
- package/src/surfaces/rail.ts +22 -79
- package/src/surfaces/reference-field.ts +11 -15
- package/src/surfaces/schema-builder.ts +16 -22
- package/src/surfaces/schema-form.json +10 -2
- package/src/surfaces/schema-form.ts +17 -25
- package/src/surfaces/secret-field.ts +9 -6
- package/src/surfaces/seo.json +1 -1
- package/src/surfaces/seo.ts +21 -15
- package/src/surfaces/settings-contexts.ts +9 -7
- package/src/surfaces/settings-contributed.json +5 -5
- package/src/surfaces/settings-contributed.ts +32 -12
- package/src/surfaces/settings-css-vars.json +44 -36
- package/src/surfaces/settings-css-vars.ts +82 -16
- package/src/surfaces/settings-defs.json +2 -2
- package/src/surfaces/settings-defs.ts +17 -21
- package/src/surfaces/settings-deploy.json +2 -15
- package/src/surfaces/settings-deploy.ts +9 -16
- package/src/surfaces/settings-dialog.json +51 -0
- package/src/surfaces/settings-dialog.ts +169 -0
- package/src/surfaces/settings-extensions.ts +5 -6
- package/src/surfaces/settings-head.ts +33 -39
- package/src/surfaces/settings-locales.ts +8 -20
- package/src/surfaces/settings-overview.json +3 -3
- package/src/surfaces/settings-overview.ts +14 -8
- package/src/surfaces/settings-packages.ts +8 -7
- package/src/surfaces/settings-pane.json +269 -46
- package/src/surfaces/settings-pane.ts +88 -36
- package/src/surfaces/settings-rawjson.ts +5 -6
- package/src/surfaces/shell.json +2 -1
- package/src/surfaces/shell.ts +1 -2
- package/src/surfaces/site-panel.json +1029 -0
- package/src/surfaces/site-panel.ts +233 -0
- package/src/surfaces/slash-menu.json +63 -21
- package/src/surfaces/slash-menu.ts +42 -9
- package/src/surfaces/statements.ts +13 -18
- package/src/surfaces/statusbar.ts +56 -29
- package/src/surfaces/style-panel.ts +18 -31
- package/src/surfaces/stylebook-chrome.ts +3 -5
- package/src/surfaces/styles-panel.json +735 -0
- package/src/surfaces/styles-panel.ts +195 -0
- package/src/surfaces/styles-variables.json +149 -0
- package/src/surfaces/styles-variables.ts +145 -0
- package/src/surfaces/tab-strip.json +4 -4
- package/src/surfaces/tab-strip.ts +14 -13
- package/src/surfaces/target-line.ts +6 -7
- package/src/surfaces/welcome.ts +6 -8
- package/src/surfaces/writing-banner.json +94 -0
- package/src/surfaces/writing-banner.ts +80 -0
- package/src/tabs/bare-repeaters.ts +101 -0
- package/src/tabs/patch-ops.ts +4 -3
- package/src/tabs/project-config.ts +32 -21
- package/src/tabs/selection.ts +36 -8
- package/src/tabs/tab.ts +99 -48
- package/src/tabs/transact.ts +98 -53
- package/src/types.ts +53 -43
- package/src/ui/ai-credentials-form.ts +10 -13
- package/src/ui/ai-model-picker.ts +3 -3
- package/src/ui/cf-account-picker.ts +7 -9
- package/src/ui/color-selector.ts +9 -5
- package/src/ui/dynamic-slot.ts +7 -8
- package/src/ui/expression-editor.ts +112 -105
- package/src/ui/form-controls.ts +104 -56
- package/src/ui/formula-catalog.ts +239 -230
- package/src/ui/formula-chips.ts +33 -18
- package/src/ui/layers.ts +57 -31
- package/src/ui/media-picker.ts +300 -47
- package/src/ui/popover-surface.ts +20 -12
- package/src/ui/progress-modal.ts +13 -8
- package/src/ui/regions.ts +14 -12
- package/src/ui/schema-form.ts +372 -97
- package/src/ui/surface.ts +71 -8
- package/src/ui/timing.ts +9 -0
- package/src/ui/value-source.ts +44 -31
- package/src/ui/virtual-window.ts +29 -3
- package/src/utils/base64.ts +6 -4
- package/src/utils/edit-display.ts +366 -111
- package/src/utils/element-selectors.ts +3 -2
- package/src/utils/google-fonts.ts +15 -14
- package/src/utils/inherited-style.ts +18 -20
- package/src/utils/link-target.ts +1 -1
- package/src/utils/local-day.ts +18 -0
- package/src/utils/preview-format.ts +25 -3
- package/src/utils/strip-events.ts +49 -22
- package/src/utils/studio-utils.ts +29 -14
- package/src/view.ts +2 -3
- package/src/workspace/pane-derive.ts +14 -16
- package/src/workspace/session.ts +81 -50
- package/src/workspace/tab-workspace.ts +137 -0
- package/src/workspace/workspace-back.ts +189 -0
- package/src/workspace/workspace-home.ts +133 -0
- package/src/workspace/workspace-ids.ts +129 -0
- package/src/workspace/workspace-switch.ts +372 -0
- package/src/workspace/workspace.ts +165 -47
- package/styles/shell-frame.css +30 -7
- package/styles/shell-frame.json +15 -3
- package/dist/chunks/2020-va2wwra4.js +0 -4
- package/dist/chunks/adapter-f93pafd7.js +0 -4
- package/dist/chunks/cf-account-picker-af1h381v.js +0 -4
- package/dist/chunks/cf-settings-125tdm0w.js +0 -4
- package/dist/chunks/color-area-jqhna8zy.js +0 -4
- package/dist/chunks/color-field-embq5ctg.js +0 -4
- package/dist/chunks/color-fpezvnkt.js +0 -4
- package/dist/chunks/color-slider-1qmw7wqy.js +0 -4
- package/dist/chunks/combobox-773hdzyj.js +0 -4
- package/dist/chunks/components-ax2f1fnn.js +0 -4
- package/dist/chunks/content-source-108aqtk1.js +0 -4
- package/dist/chunks/convert-to-repeater-ry5rmppr.js +0 -4
- package/dist/chunks/dialog-mtm9fq5t.js +0 -4
- package/dist/chunks/editor-pfgdmkb0.js +0 -4
- package/dist/chunks/entry-commands-t0k1q0kx.js +0 -4
- package/dist/chunks/extension-sections-6mwh2hf8.js +0 -4
- package/dist/chunks/file-ops-meej5mqf.js +0 -4
- package/dist/chunks/files-44vp8a2s.js +0 -4
- package/dist/chunks/format-host-7q1petb9.js +0 -4
- package/dist/chunks/fs-events-h2mwnxaz.js +0 -4
- package/dist/chunks/git-panel-zngbymvd.js +0 -4
- package/dist/chunks/grid-view-sx9vynsp.js +0 -51
- package/dist/chunks/icons-p44xhjf0.js +0 -4
- package/dist/chunks/iframe-entry-f7mqjwpm.js +0 -5
- package/dist/chunks/iframe-entry-haw7jkvz.js +0 -5
- package/dist/chunks/iframe-entry-z6qrytv3.js +0 -5
- package/dist/chunks/iframe-host-qk3ycq87.js +0 -4
- package/dist/chunks/index-wkyfjeqz.js +0 -4
- package/dist/chunks/jsonMode-s8kt9dsj.js +0 -13
- package/dist/chunks/jx-validate-psgd7vsx.js +0 -4
- package/dist/chunks/layers-panel-1kq2jyfk.js +0 -4
- package/dist/chunks/layers-xd92fg8d.js +0 -4
- package/dist/chunks/listbox-s8fkmnq8.js +0 -4
- package/dist/chunks/lit-html-ac4cwwr4.js +0 -4
- package/dist/chunks/media-picker-0svzkdyb.js +0 -4
- package/dist/chunks/menu-37d07z9m.js +0 -4
- package/dist/chunks/menu-5c55ea3d.js +0 -4
- package/dist/chunks/monaco-binding-z6r70pdt.js +0 -4
- package/dist/chunks/monaco-buffer-fz6gq5y7.js +0 -4
- package/dist/chunks/monaco-setup-q4axkgj7.js +0 -4
- package/dist/chunks/notify-0xgtn8q1.js +0 -4
- package/dist/chunks/number-field-gw7sq8rn.js +0 -4
- package/dist/chunks/parse-897fvqah.js +0 -4
- package/dist/chunks/platform-v1qctbe6.js +0 -4
- package/dist/chunks/popover-8htra4er.js +0 -4
- package/dist/chunks/publish-panel-ym1fmgq5.js +0 -4
- package/dist/chunks/quick-search-a54e6t5n.js +0 -4
- package/dist/chunks/references-gmqp82fs.js +0 -4
- package/dist/chunks/right-panel-v7t9awbk.js +0 -4
- package/dist/chunks/section-registry-vhyf3zsr.js +0 -4
- package/dist/chunks/select-n3v7gwas.js +0 -4
- package/dist/chunks/split-pff8f3ze.js +0 -4
- package/dist/chunks/studio-0bhx5hz6.js +0 -5
- package/dist/chunks/studio-0k1ddnyy.js +0 -6
- package/dist/chunks/studio-1b04pd0j.js +0 -64
- package/dist/chunks/studio-4dwbj6gb.js +0 -5
- package/dist/chunks/studio-4tanh828.js +0 -5
- package/dist/chunks/studio-55h4mstj.js +0 -4
- package/dist/chunks/studio-5tnvjkz4.js +0 -5
- package/dist/chunks/studio-60dqgb0c.js +0 -5
- package/dist/chunks/studio-61dp09qg.js +0 -9
- package/dist/chunks/studio-63epcrj2.js +0 -5
- package/dist/chunks/studio-74k29dy9.js +0 -5
- package/dist/chunks/studio-7vg6jcd3.js +0 -5
- package/dist/chunks/studio-7zx31knw.js +0 -9
- package/dist/chunks/studio-8bxk97km.js +0 -5
- package/dist/chunks/studio-933e30n4.js +0 -5
- package/dist/chunks/studio-98wnwhbg.js +0 -5
- package/dist/chunks/studio-9mccjwjh.js +0 -14
- package/dist/chunks/studio-a2ke1e4t.js +0 -5
- package/dist/chunks/studio-e3a2a6v3.js +0 -4
- package/dist/chunks/studio-e81q1vb9.js +0 -5
- package/dist/chunks/studio-epgrmqqm.js +0 -5
- package/dist/chunks/studio-ery42sqr.js +0 -4
- package/dist/chunks/studio-g1shz58g.js +0 -5
- package/dist/chunks/studio-gf0fz03c.js +0 -5
- package/dist/chunks/studio-gjtpe62q.js +0 -6
- package/dist/chunks/studio-gkskhg7m.js +0 -5
- package/dist/chunks/studio-gx4dnjzd.js +0 -5
- package/dist/chunks/studio-j03zga3y.js +0 -9
- package/dist/chunks/studio-jhpxeckz.js +0 -5
- package/dist/chunks/studio-k01r7ykp.js +0 -5
- package/dist/chunks/studio-kk7h30ne.js +0 -7
- package/dist/chunks/studio-kt1zqney.js +0 -5
- package/dist/chunks/studio-m3cwhvye.js +0 -5
- package/dist/chunks/studio-mcayt6hz.js +0 -5
- package/dist/chunks/studio-myjcghv3.js +0 -5
- package/dist/chunks/studio-n8bxwc3y.js +0 -4
- package/dist/chunks/studio-ngm8gvpb.js +0 -5
- package/dist/chunks/studio-nj5a01jy.js +0 -928
- package/dist/chunks/studio-pcgz8heb.js +0 -5
- package/dist/chunks/studio-qcsq0t15.js +0 -5
- package/dist/chunks/studio-qkstvve9.js +0 -5
- package/dist/chunks/studio-qv1pwd3c.js +0 -5
- package/dist/chunks/studio-rc6dvxjb.js +0 -7
- package/dist/chunks/studio-s8dmmz66.js +0 -5
- package/dist/chunks/studio-sp5xzbj4.js +0 -6
- package/dist/chunks/studio-szsjbj0g.js +0 -5
- package/dist/chunks/studio-t1xzawhr.js +0 -16
- package/dist/chunks/studio-tdcprbqb.js +0 -5
- package/dist/chunks/studio-vrmcjf9g.js +0 -7
- package/dist/chunks/studio-vyc0b4he.js +0 -10
- package/dist/chunks/studio-w0cgf6jg.js +0 -5
- package/dist/chunks/studio-w3hskzqk.js +0 -5
- package/dist/chunks/studio-xgrgcdyh.js +0 -9
- package/dist/chunks/studio-xz2yxfcb.js +0 -5
- package/dist/chunks/studio-y6n42tgs.js +0 -5
- package/dist/chunks/studio-yb560vxr.js +0 -5
- package/dist/chunks/studio-yn9f8vtx.js +0 -5
- package/dist/chunks/studio-yw05w8f0.js +0 -7
- package/dist/chunks/studio-z16sgavc.js +0 -5
- package/dist/chunks/studio-zey7zsdw.js +0 -12
- package/dist/chunks/studio-zyqn40w4.js +0 -5
- package/dist/chunks/swatch-group-8qh9a3bm.js +0 -4
- package/dist/chunks/tabs-jwthg40w.js +0 -4
- package/dist/chunks/textfield-spy3hc77.js +0 -4
- package/dist/chunks/toast-4d35zd64.js +0 -4
- package/dist/chunks/toast-host-8sjmfed3.js +0 -4
- package/dist/chunks/tooltip-fd6g9xy3.js +0 -4
- package/dist/chunks/tree-h19v1jfz.js +0 -4
- package/dist/chunks/tsMode-2frah6h1.js +0 -14
- package/dist/chunks/workspace-zt81g1ms.js +0 -4
- package/dist/chunks/ws-client-n2bh4hkf.js +0 -4
- package/src/panels/frontmatter-panel.ts +0 -701
- package/src/panels/git-panel.ts +0 -1120
- package/src/surfaces/doc-header.json +0 -776
- package/src/surfaces/doc-header.ts +0 -231
- package/src/surfaces/git-panel.json +0 -1880
- package/src/surfaces/git-panel.ts +0 -272
|
@@ -0,0 +1,1283 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* The Media workspace's library: the project's media, filtered, searched, sorted and acted on.
|
|
4
|
+
*
|
|
5
|
+
* One flow, two modes (studio.md §22.6). In **manage** mode it is the Media home tab: filters on
|
|
6
|
+
* the left, a grid of tiles in the middle, and the selected file's detail on the right, with
|
|
7
|
+
* **Replace**, **Open in viewer** and **Delete**. In **pick** mode it is the full-size media picker
|
|
8
|
+
* (§9.3), the same surface inside a `jx-dialog` whose footer answers Cancel or Use <file>; the
|
|
9
|
+
* compact popover's **Browse all media…** opens it (`ui/media-picker.ts`).
|
|
10
|
+
*
|
|
11
|
+
* **What it lists is `ui/media-picker.ts`'s listing**, `public/**` and every collection's
|
|
12
|
+
* `images/**`, so the workspace, the pickers and the Site panel cannot disagree about what exists.
|
|
13
|
+
* Metadata comes from the listing (`files/media-meta.ts`), and usage from the reference index
|
|
14
|
+
* (`files/media-usage.ts`), asked about the selected file when it is selected and about every file
|
|
15
|
+
* only when the reader asks for **Unused**.
|
|
16
|
+
*
|
|
17
|
+
* **Unused is asked for, never assumed.** The sweep starts the first time the filter is chosen,
|
|
18
|
+
* runs one query at a time, stops when no library shows the filter any more, and the count stays
|
|
19
|
+
* blank until the last answer is in. A file whose query failed is never counted as unused, and a
|
|
20
|
+
* host with no reference index draws no Unused filter at all: a confident zero is the one answer a
|
|
21
|
+
* delete must never be prompted by.
|
|
22
|
+
*
|
|
23
|
+
* **Uploads with no context land in `public/media/`** (`DEFAULT_MEDIA_DIR`): the Media home has no
|
|
24
|
+
* document, so it always does. The pick dialog answers a field of the active document, so it
|
|
25
|
+
* uploads where that document's own uploads go (`uploadDirFor`: a collection entry keeps its media
|
|
26
|
+
* in `content/<c>/images/`). Either way the destination is named on the Upload label before the
|
|
27
|
+
* drop, only files the listing will show are uploaded, and the first upload is selected at once, so
|
|
28
|
+
* the detail pane describes it, and its tile scrolls into view, the moment the re-read listing has
|
|
29
|
+
* it. **Replace** is the one overwrite (`replaceAsset`), offered only files of the replaced one's
|
|
30
|
+
* own type; **Delete** goes through the same confirmation as every delete, which states the usage
|
|
31
|
+
* line (`files/file-ops.ts`), and a delete that empties an upload destination (`public/media/`, a
|
|
32
|
+
* collection's `images/`) takes the folder the upload created with it.
|
|
33
|
+
*
|
|
34
|
+
* **This module is the flow; `surfaces/media-library.json` is the markup.** Every decision is made
|
|
35
|
+
* here and handed to the document as strings and discriminants.
|
|
36
|
+
*
|
|
37
|
+
* @docs studio/projects/media
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
41
|
+
import { getPlatform } from "../platform";
|
|
42
|
+
import { notify } from "../services/notify";
|
|
43
|
+
import { invalidateUsages, onUsagesInvalidated, usagesSupported } from "../services/references";
|
|
44
|
+
import { overlayRegion, paneRegion } from "../ui/regions";
|
|
45
|
+
import { layerHost } from "../ui/layers";
|
|
46
|
+
import { rectOf } from "../utils/geometry";
|
|
47
|
+
import { activeTab, closeTab, workspace } from "../workspace/workspace";
|
|
48
|
+
import { createPreviewObserver } from "../browse/library-preview";
|
|
49
|
+
import {
|
|
50
|
+
DEFAULT_MEDIA_DIR,
|
|
51
|
+
announceMediaChanged,
|
|
52
|
+
isUploadDestination,
|
|
53
|
+
replaceAccept,
|
|
54
|
+
replaceAsset,
|
|
55
|
+
uploadAccept,
|
|
56
|
+
uploadAssets,
|
|
57
|
+
uploadDirFor,
|
|
58
|
+
} from "../files/media-upload";
|
|
59
|
+
import {
|
|
60
|
+
loadMediaMeta,
|
|
61
|
+
mediaMetaSettled,
|
|
62
|
+
mediaMetaSummary,
|
|
63
|
+
peekMediaMeta,
|
|
64
|
+
recordImageSize,
|
|
65
|
+
} from "../files/media-meta";
|
|
66
|
+
import { dirName, previewFileSrc } from "../files/media-paths";
|
|
67
|
+
import { forgetVersions } from "../files/asset-versions";
|
|
68
|
+
import { loadMediaUsages, peekMediaUsages } from "../files/media-usage";
|
|
69
|
+
import { onProjectMediaChange, projectMediaFiles } from "../ui/media-picker";
|
|
70
|
+
import { openMediaTab } from "./media-open";
|
|
71
|
+
import { factsLine, mediaStageFor, specimenOf, usageOf } from "./media-pane";
|
|
72
|
+
import {
|
|
73
|
+
MEDIA_FILTERS,
|
|
74
|
+
MEDIA_SORTS,
|
|
75
|
+
filterCounts,
|
|
76
|
+
gridColumns,
|
|
77
|
+
gridStep,
|
|
78
|
+
isMediaFilterKey,
|
|
79
|
+
isMediaSortKey,
|
|
80
|
+
matchesQuery,
|
|
81
|
+
mediaFilterLabel,
|
|
82
|
+
passesFilter,
|
|
83
|
+
visibleMedia,
|
|
84
|
+
} from "./media-library-model";
|
|
85
|
+
import { mountMediaLibrarySurface } from "../surfaces/media-library";
|
|
86
|
+
import { MEDIA_PICK_SLOT, openMediaPickDialogSurface } from "../surfaces/media-pick-dialog";
|
|
87
|
+
import type { LibraryMediaFile, MediaFilterKey, MediaSortKey } from "./media-library-model";
|
|
88
|
+
import type { MediaFileKind } from "../files/media-upload";
|
|
89
|
+
import type { ProjectMediaFile } from "../ui/media-picker";
|
|
90
|
+
import type {
|
|
91
|
+
MediaLibrarySurfaceHandle,
|
|
92
|
+
MediaLibraryView,
|
|
93
|
+
MediaTileRow,
|
|
94
|
+
} from "../surfaces/media-library";
|
|
95
|
+
import type { MediaPickDialogHandle } from "../surfaces/media-pick-dialog";
|
|
96
|
+
import type { PreviewObserver } from "../browse/library-preview";
|
|
97
|
+
import type { Tab } from "../tabs/tab";
|
|
98
|
+
import type { CanvasSurface } from "../canvas/canvas-surface";
|
|
99
|
+
import type * as FileOpsModule from "../files/file-ops";
|
|
100
|
+
import type * as FilesModule from "../files/files";
|
|
101
|
+
|
|
102
|
+
/** The canvas mode the Media home tab draws under (`workspace/workspace-ids.ts`). */
|
|
103
|
+
export const MEDIA_HOME_MODE = "media-home";
|
|
104
|
+
|
|
105
|
+
/** The kit glyph a file with no picture of its own is drawn with. */
|
|
106
|
+
const KIND_GLYPH: Readonly<Record<MediaFileKind, string>> = {
|
|
107
|
+
audio: "play",
|
|
108
|
+
document: "file-text",
|
|
109
|
+
font: "text-aa",
|
|
110
|
+
image: "image",
|
|
111
|
+
video: "play",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** The word a tile says about a file nothing has been measured about yet. */
|
|
115
|
+
const KIND_WORD: Readonly<Record<MediaFileKind, string>> = {
|
|
116
|
+
audio: "Audio",
|
|
117
|
+
document: "Document",
|
|
118
|
+
font: "Font",
|
|
119
|
+
image: "Image",
|
|
120
|
+
video: "Video",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ─── What the reader chose ───────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/** A library's own choices: the filter, the search, the sort and the selection. */
|
|
126
|
+
interface LibraryChoice {
|
|
127
|
+
filter: MediaFilterKey;
|
|
128
|
+
query: string;
|
|
129
|
+
sort: MediaSortKey;
|
|
130
|
+
selected: string | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The Media home's choices, shared by every pane that shows it and written by `media.browse`, so a
|
|
135
|
+
* command can say "show me the unused images" without knowing which pane will draw the answer.
|
|
136
|
+
*/
|
|
137
|
+
const homeChoice: LibraryChoice = { filter: "all", query: "", sort: "newest", selected: null };
|
|
138
|
+
|
|
139
|
+
/** A file the commands asked to open the picker for, once the input that opens it exists. */
|
|
140
|
+
interface PendingPicker {
|
|
141
|
+
role: "upload" | "replace";
|
|
142
|
+
/** For a replace: the file it replaces, which must be the selection when the input appears. */
|
|
143
|
+
file: string | null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let _pendingPicker: PendingPicker | null = null;
|
|
147
|
+
|
|
148
|
+
// ─── Unused ──────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/** The files nobody references, once every file's question has settled; `null` before. */
|
|
151
|
+
let _unused: Set<string> | null = null;
|
|
152
|
+
/** How far the sweep has got, for the line the grid shows while it runs. */
|
|
153
|
+
let _sweepDone = 0;
|
|
154
|
+
let _sweepTotal = 0;
|
|
155
|
+
let _sweepRunning = false;
|
|
156
|
+
/** Bumped when the listing moves, so a sweep over the old listing cannot land. */
|
|
157
|
+
let _sweepEpoch = 0;
|
|
158
|
+
|
|
159
|
+
/** Forget the sweep: the listing it was over has changed. */
|
|
160
|
+
function resetUnused(): void {
|
|
161
|
+
_unused = null;
|
|
162
|
+
_sweepRunning = false;
|
|
163
|
+
_sweepDone = 0;
|
|
164
|
+
_sweepTotal = 0;
|
|
165
|
+
_sweepEpoch += 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Whether a live library still shows Unused: the one reader the sweep is running for. */
|
|
169
|
+
function unusedWanted(): boolean {
|
|
170
|
+
return [..._mounts].some((mount) => !mount.disposed && mount.choice.filter === "unused");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Ask the usage index about every file, one at a time, and settle {@link _unused}.
|
|
175
|
+
*
|
|
176
|
+
* One query in flight at a time. The backend reads the project's references once and answers each
|
|
177
|
+
* question from that index (`@jxsuite/server/refactor`), so the first question pays for the sweep
|
|
178
|
+
* and the rest are filters over it; a burst of concurrent first questions would only queue behind
|
|
179
|
+
* it. The sweep stops the moment no library shows Unused any more: a reader who left the filter is
|
|
180
|
+
* not owed the rest of the answer, and every question already answered stays cached for the next
|
|
181
|
+
* time the filter is chosen.
|
|
182
|
+
*/
|
|
183
|
+
async function sweepUnused(files: readonly ProjectMediaFile[]): Promise<void> {
|
|
184
|
+
if (_sweepRunning || _unused !== null || !usagesSupported()) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const epoch = _sweepEpoch;
|
|
188
|
+
_sweepRunning = true;
|
|
189
|
+
_sweepDone = 0;
|
|
190
|
+
_sweepTotal = files.length;
|
|
191
|
+
const unused = new Set<string>();
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
const state = await loadMediaUsages(file.file);
|
|
194
|
+
if (epoch !== _sweepEpoch) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!unusedWanted()) {
|
|
198
|
+
resetUnused();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (state.status === "ready" && state.result.files.length === 0) {
|
|
202
|
+
unused.add(file.file);
|
|
203
|
+
}
|
|
204
|
+
_sweepDone += 1;
|
|
205
|
+
redrawAll();
|
|
206
|
+
}
|
|
207
|
+
_unused = unused;
|
|
208
|
+
_sweepRunning = false;
|
|
209
|
+
redrawAll();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Mounts ──────────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
interface LibraryMount {
|
|
215
|
+
mode: "manage" | "pick";
|
|
216
|
+
/** Pick mode for a field that holds a picture: only images are offered. */
|
|
217
|
+
images: boolean;
|
|
218
|
+
choice: LibraryChoice;
|
|
219
|
+
/** The pane id, or `null` for the pick dialog. */
|
|
220
|
+
paneId: string | null;
|
|
221
|
+
region: string;
|
|
222
|
+
handle: MediaLibrarySurfaceHandle | null;
|
|
223
|
+
dropActive: boolean;
|
|
224
|
+
/** Tiles that have scrolled into view, whose thumbnails may load. */
|
|
225
|
+
visible: Set<string>;
|
|
226
|
+
tiles: Map<string, HTMLElement>;
|
|
227
|
+
grid: HTMLElement | null;
|
|
228
|
+
/**
|
|
229
|
+
* Every file input the document has created for each role, pruned of the ones it has dropped.
|
|
230
|
+
* Upload has two placements (the toolbar's and the empty state's), and the empty state's is gone
|
|
231
|
+
* the moment the first file lands: a single slot would keep pointing at it.
|
|
232
|
+
*/
|
|
233
|
+
inputs: { upload: Set<HTMLInputElement>; replace: Set<HTMLInputElement> };
|
|
234
|
+
/** Where this mount's uploads land: `public/media/` at home, the field's document's in a pick. */
|
|
235
|
+
uploadDir: string;
|
|
236
|
+
/** A pending sweep of nodes the document dropped (detached tiles, old inputs). */
|
|
237
|
+
sweepTimer: ReturnType<typeof setTimeout> | null;
|
|
238
|
+
observer: PreviewObserver;
|
|
239
|
+
/** Pick mode: what a use commits. */
|
|
240
|
+
onPick: ((file: ProjectMediaFile) => void) | null;
|
|
241
|
+
/** Pick mode: tell the dialog the selection moved, so its footer can say what Use will use. */
|
|
242
|
+
onSelect: (() => void) | null;
|
|
243
|
+
/**
|
|
244
|
+
* A file to scroll into view once its tile is drawn: an upload, which the re-read listing may not
|
|
245
|
+
* hold yet when it is selected, and which a sort can place anywhere in the grid.
|
|
246
|
+
*/
|
|
247
|
+
reveal: string | null;
|
|
248
|
+
/**
|
|
249
|
+
* Whether the keyboard is owed to this library's tab stop ({@link focusMediaHome}) and no tile
|
|
250
|
+
* has been drawn to take it yet: a home mounted before its listing has arrived has no tiles.
|
|
251
|
+
*/
|
|
252
|
+
focusOwed: boolean;
|
|
253
|
+
disposed: boolean;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Every live library, so a listing, a sweep or a measurement can repaint them all. */
|
|
257
|
+
const _mounts = new Set<LibraryMount>();
|
|
258
|
+
|
|
259
|
+
/** The library mounted in each pane, keyed by pane id, as `media/media-pane.ts` keys its viewers. */
|
|
260
|
+
const _panes = new Map<string, { tabId: string; wrap: HTMLElement; mount: LibraryMount }>();
|
|
261
|
+
|
|
262
|
+
/** Panes whose next library takes the keyboard on its tab stop ({@link focusMediaHome}). */
|
|
263
|
+
const _focusOnMount = new Set<string>();
|
|
264
|
+
|
|
265
|
+
let _unsubscribe: (() => void) | null = null;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Listen while any library is mounted, and stop when the last one goes: to the listing, and to the
|
|
269
|
+
* usage index. Either moving invalidates the Unused set. A document saved with a new `<img src>`
|
|
270
|
+
* moves only the second, so listening to the listing alone left a referenced file under Unused, the
|
|
271
|
+
* one list a reader deletes from.
|
|
272
|
+
*/
|
|
273
|
+
function listen(): void {
|
|
274
|
+
if (_unsubscribe !== null) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const forget = (): void => {
|
|
278
|
+
resetUnused();
|
|
279
|
+
redrawAll();
|
|
280
|
+
};
|
|
281
|
+
const stopListing = onProjectMediaChange(forget);
|
|
282
|
+
const stopUsages = onUsagesInvalidated(forget);
|
|
283
|
+
_unsubscribe = () => {
|
|
284
|
+
stopListing();
|
|
285
|
+
stopUsages();
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function unlistenIfIdle(): void {
|
|
290
|
+
if (_mounts.size === 0 && _unsubscribe) {
|
|
291
|
+
_unsubscribe();
|
|
292
|
+
_unsubscribe = null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function redraw(mount: LibraryMount): void {
|
|
297
|
+
if (!mount.disposed) {
|
|
298
|
+
mount.handle?.update(libraryView(mount));
|
|
299
|
+
// A pick dialog's footer names what Use will use, which a search or a filter can take away.
|
|
300
|
+
mount.onSelect?.();
|
|
301
|
+
revealPending(mount);
|
|
302
|
+
// On the frame's next turn: the projection just assigned is what draws the tiles.
|
|
303
|
+
if (mount.focusOwed) {
|
|
304
|
+
queueMicrotask(() => serveOwedFocus(mount));
|
|
305
|
+
}
|
|
306
|
+
scheduleSweep(mount);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Scroll the pending {@link LibraryMount.reveal} file's tile into view, once it is in the document.
|
|
312
|
+
* Like the focus move, it reaches only for the tile the surface announced.
|
|
313
|
+
*/
|
|
314
|
+
function revealPending(mount: LibraryMount): void {
|
|
315
|
+
const file = mount.reveal;
|
|
316
|
+
const tile = file === null ? undefined : mount.tiles.get(file);
|
|
317
|
+
if (tile?.isConnected) {
|
|
318
|
+
mount.reveal = null;
|
|
319
|
+
tile.scrollIntoView({ block: "nearest" });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Once a repaint has settled, give back what the document dropped: tiles a filter, a search or a
|
|
325
|
+
* sort removed (the preview observer still watches every one that never scrolled into view, the
|
|
326
|
+
* leak `browse/library-preview.ts` documents), and inputs a `$switch` took away.
|
|
327
|
+
*/
|
|
328
|
+
function scheduleSweep(mount: LibraryMount): void {
|
|
329
|
+
// A disposed mount has nothing left to give back, and `disposeMount` clears a pending sweep.
|
|
330
|
+
if (mount.sweepTimer !== null || mount.disposed) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
mount.sweepTimer = setTimeout(() => {
|
|
334
|
+
mount.sweepTimer = null;
|
|
335
|
+
mount.observer.releaseDetached();
|
|
336
|
+
for (const [file, tile] of mount.tiles) {
|
|
337
|
+
if (!tile.isConnected) {
|
|
338
|
+
mount.tiles.delete(file);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
for (const inputs of [mount.inputs.upload, mount.inputs.replace]) {
|
|
342
|
+
for (const input of inputs) {
|
|
343
|
+
if (!input.isConnected) {
|
|
344
|
+
inputs.delete(input);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}, 0);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** The first of a role's inputs still in the document, or `null`. */
|
|
352
|
+
function connectedInput(inputs: ReadonlySet<HTMLInputElement>): HTMLInputElement | null {
|
|
353
|
+
return [...inputs].find((input) => input.isConnected) ?? null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function redrawAll(): void {
|
|
357
|
+
for (const mount of _mounts) {
|
|
358
|
+
redraw(mount);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function disposeMount(mount: LibraryMount): void {
|
|
363
|
+
mount.disposed = true;
|
|
364
|
+
if (mount.sweepTimer !== null) {
|
|
365
|
+
clearTimeout(mount.sweepTimer);
|
|
366
|
+
mount.sweepTimer = null;
|
|
367
|
+
}
|
|
368
|
+
mount.inputs.upload.clear();
|
|
369
|
+
mount.inputs.replace.clear();
|
|
370
|
+
mount.observer.destroy();
|
|
371
|
+
mount.handle?.dispose();
|
|
372
|
+
mount.handle = null;
|
|
373
|
+
mount.tiles.clear();
|
|
374
|
+
_mounts.delete(mount);
|
|
375
|
+
unlistenIfIdle();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ─── The projection ──────────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
/** The listing a mount offers, as read, and as the model reads it. */
|
|
381
|
+
interface LibraryListing {
|
|
382
|
+
/** `null` while the listing is still being read. */
|
|
383
|
+
listed: readonly ProjectMediaFile[] | null;
|
|
384
|
+
files: LibraryMediaFile[];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** The listing as the model reads it, narrowed to images for an image picker. */
|
|
388
|
+
function libraryFiles(mount: LibraryMount): LibraryListing {
|
|
389
|
+
const listing = projectMediaFiles();
|
|
390
|
+
const listed =
|
|
391
|
+
listing === null ? null : mount.images ? listing.filter((f) => f.isImage) : listing;
|
|
392
|
+
const files = (listed ?? []).map((file) => {
|
|
393
|
+
const meta = peekMediaMeta(file.file);
|
|
394
|
+
return {
|
|
395
|
+
bytes: meta?.bytes ?? null,
|
|
396
|
+
file: file.file,
|
|
397
|
+
kind: file.kind,
|
|
398
|
+
modified: meta?.modified ?? null,
|
|
399
|
+
name: file.name,
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
return { files, listed };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** The filters a mount offers: an image picker has only images to filter. */
|
|
406
|
+
function offeredFilters(mount: LibraryMount): typeof MEDIA_FILTERS {
|
|
407
|
+
return MEDIA_FILTERS.filter(
|
|
408
|
+
(filter) =>
|
|
409
|
+
(filter.key !== "unused" || usagesSupported()) &&
|
|
410
|
+
(!mount.images || filter.key === "image" || filter.key === "unused"),
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The selected file, when the grid shows it: the listing still has it, the mount offers it, and the
|
|
416
|
+
* filter and the search keep it.
|
|
417
|
+
*
|
|
418
|
+
* The selection itself is kept (clearing the search brings the file back, still selected), but the
|
|
419
|
+
* detail pane and a pick's **Use** follow the grid: describing, or committing, a file the reader's
|
|
420
|
+
* own search has just hidden is acting on something no longer on screen.
|
|
421
|
+
*/
|
|
422
|
+
export function selectedEntry(mount: {
|
|
423
|
+
choice: LibraryChoice;
|
|
424
|
+
images: boolean;
|
|
425
|
+
}): ProjectMediaFile | null {
|
|
426
|
+
const { filter, query, selected } = mount.choice;
|
|
427
|
+
if (selected === null) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
const entry = projectMediaFiles()?.find((file) => file.file === selected) ?? null;
|
|
431
|
+
if (entry === null || (mount.images && !entry.isImage)) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const modelled: LibraryMediaFile = {
|
|
435
|
+
bytes: null,
|
|
436
|
+
file: entry.file,
|
|
437
|
+
kind: entry.kind,
|
|
438
|
+
modified: null,
|
|
439
|
+
name: entry.name,
|
|
440
|
+
};
|
|
441
|
+
return passesFilter(modelled, filter, _unused) && matchesQuery(modelled, query) ? entry : null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** What the grid says in place of tiles. */
|
|
445
|
+
interface EmptyTexts {
|
|
446
|
+
emptyTitle: string;
|
|
447
|
+
emptyDetail: string;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function emptyTexts(mount: LibraryMount, total: number): EmptyTexts {
|
|
451
|
+
if (total === 0) {
|
|
452
|
+
return {
|
|
453
|
+
emptyDetail: `Upload files or drop them anywhere here. They are saved under ${mount.uploadDir}/, and anything already under public/ or a collection's images/ folder is listed here too.`,
|
|
454
|
+
emptyTitle: mount.images ? "No images yet" : "No media yet",
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const { filter, query } = mount.choice;
|
|
458
|
+
if (query.trim() !== "") {
|
|
459
|
+
return {
|
|
460
|
+
emptyDetail: `Nothing in ${mediaFilterLabel(filter)} matches it.`,
|
|
461
|
+
emptyTitle: `No matches for “${query.trim()}”`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (filter === "unused") {
|
|
465
|
+
return {
|
|
466
|
+
emptyDetail: "Every file is referenced by at least one document.",
|
|
467
|
+
emptyTitle: "No unused media",
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
emptyDetail: `${total} ${total === 1 ? "file" : "files"} in the project, none of them ${mediaFilterLabel(filter).toLowerCase()}.`,
|
|
472
|
+
emptyTitle: `No ${mediaFilterLabel(filter).toLowerCase()}`,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Everything the document draws for one mount. */
|
|
477
|
+
function libraryView(mount: LibraryMount): MediaLibraryView {
|
|
478
|
+
const { choice } = mount;
|
|
479
|
+
const { files, listed } = libraryFiles(mount);
|
|
480
|
+
const counts = filterCounts(files, _unused);
|
|
481
|
+
const filters = offeredFilters(mount);
|
|
482
|
+
// A filter the mount does not offer (a stored "video" in an image picker) reads as its first.
|
|
483
|
+
if (!filters.some((f) => f.key === choice.filter)) {
|
|
484
|
+
choice.filter = filters[0]?.key ?? "all";
|
|
485
|
+
}
|
|
486
|
+
const visible = visibleMedia(files, {
|
|
487
|
+
filter: choice.filter,
|
|
488
|
+
query: choice.query,
|
|
489
|
+
sort: choice.sort,
|
|
490
|
+
unused: _unused,
|
|
491
|
+
});
|
|
492
|
+
/* The sweep is over the WHOLE listing, never this mount's: its answer is shared by every mount,
|
|
493
|
+
and an image picker's narrowed list would leave the Media home's Unused without its videos. */
|
|
494
|
+
const listing = projectMediaFiles();
|
|
495
|
+
if (choice.filter === "unused" && listing !== null) {
|
|
496
|
+
void sweepUnused(listing);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let gridState: MediaLibraryView["gridState"] = "listed";
|
|
500
|
+
if (listed === null) {
|
|
501
|
+
gridState = "loading";
|
|
502
|
+
} else if (choice.filter === "unused" && _unused === null) {
|
|
503
|
+
gridState = "counting";
|
|
504
|
+
} else if (files.length === 0 || visible.length === 0) {
|
|
505
|
+
gridState = files.length === 0 ? "empty" : "nomatch";
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const entry = selectedEntry(mount);
|
|
509
|
+
const stop = visible.some((file) => file.file === choice.selected)
|
|
510
|
+
? choice.selected
|
|
511
|
+
: (visible[0]?.file ?? null);
|
|
512
|
+
const tiles: MediaTileRow[] = visible.map((file) => {
|
|
513
|
+
const isImage = file.kind === "image";
|
|
514
|
+
return {
|
|
515
|
+
caption: mediaMetaSummary(peekMediaMeta(file.file)) || KIND_WORD[file.kind],
|
|
516
|
+
file: file.file,
|
|
517
|
+
glyph: KIND_GLYPH[file.kind],
|
|
518
|
+
key: file.file,
|
|
519
|
+
name: file.name,
|
|
520
|
+
selected: file.file === choice.selected ? "true" : "false",
|
|
521
|
+
tabindex: file.file === stop ? "0" : "-1",
|
|
522
|
+
thumbSrc: isImage && mount.visible.has(file.file) ? previewFileSrc(file.file) : "",
|
|
523
|
+
thumbState: isImage ? (mount.visible.has(file.file) ? "image" : "pending") : "glyph",
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const title =
|
|
528
|
+
mount.images && choice.filter === "image" ? "Images" : mediaFilterLabel(choice.filter);
|
|
529
|
+
return {
|
|
530
|
+
countingLabel:
|
|
531
|
+
_sweepTotal > 0
|
|
532
|
+
? `Counting references… ${_sweepDone} of ${_sweepTotal}`
|
|
533
|
+
: "Counting references…",
|
|
534
|
+
dropActive: mount.dropActive ? "true" : "false",
|
|
535
|
+
filters: filters.map((filter) => {
|
|
536
|
+
const count = counts[filter.key];
|
|
537
|
+
return {
|
|
538
|
+
count: count === null ? "" : String(count),
|
|
539
|
+
key: filter.key,
|
|
540
|
+
label: mount.images && filter.key === "image" ? "All images" : filter.label,
|
|
541
|
+
pressed: filter.key === choice.filter ? "true" : "false",
|
|
542
|
+
};
|
|
543
|
+
}),
|
|
544
|
+
gridState,
|
|
545
|
+
mode: mount.mode,
|
|
546
|
+
query: choice.query,
|
|
547
|
+
region: mount.region,
|
|
548
|
+
sort: choice.sort,
|
|
549
|
+
sortOptions: MEDIA_SORTS.map((sort) => ({ label: sort.label, value: sort.key })),
|
|
550
|
+
tiles,
|
|
551
|
+
title,
|
|
552
|
+
titleId: `media-library-title-${mount.region.replaceAll(/[^\w-]/g, "-")}`,
|
|
553
|
+
uploadAccept: mount.images ? "image/*" : uploadAccept(),
|
|
554
|
+
uploadHint: `Upload into ${mount.uploadDir}/`,
|
|
555
|
+
...emptyTexts(mount, files.length),
|
|
556
|
+
...detailView(mount, entry),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** The detail pane for the selected file, or the hint that stands in for one. */
|
|
561
|
+
function detailView(
|
|
562
|
+
mount: LibraryMount,
|
|
563
|
+
entry: ProjectMediaFile | null,
|
|
564
|
+
): Pick<
|
|
565
|
+
MediaLibraryView,
|
|
566
|
+
| "detailState"
|
|
567
|
+
| "detailHint"
|
|
568
|
+
| "selectedFile"
|
|
569
|
+
| "detailName"
|
|
570
|
+
| "detailFacts"
|
|
571
|
+
| "detailRef"
|
|
572
|
+
| "previewKind"
|
|
573
|
+
| "previewSrc"
|
|
574
|
+
| "previewGlyph"
|
|
575
|
+
| "previewFontFace"
|
|
576
|
+
| "previewSpecimen"
|
|
577
|
+
| "previewSpecimenFamily"
|
|
578
|
+
| "replaceAccept"
|
|
579
|
+
| "usageState"
|
|
580
|
+
| "usageHeadline"
|
|
581
|
+
| "usageFiles"
|
|
582
|
+
> {
|
|
583
|
+
if (entry === null) {
|
|
584
|
+
return {
|
|
585
|
+
detailFacts: "",
|
|
586
|
+
detailHint:
|
|
587
|
+
mount.mode === "pick" ? "Choose a file to use it." : "Select a file to see its details.",
|
|
588
|
+
detailName: "",
|
|
589
|
+
detailRef: "",
|
|
590
|
+
detailState: "none",
|
|
591
|
+
previewFontFace: "",
|
|
592
|
+
previewGlyph: "",
|
|
593
|
+
previewKind: "glyph",
|
|
594
|
+
previewSpecimen: "",
|
|
595
|
+
previewSpecimenFamily: "",
|
|
596
|
+
previewSrc: "",
|
|
597
|
+
replaceAccept: "",
|
|
598
|
+
selectedFile: "",
|
|
599
|
+
usageFiles: [],
|
|
600
|
+
usageHeadline: "",
|
|
601
|
+
usageState: "hidden",
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const meta = peekMediaMeta(entry.file);
|
|
605
|
+
const usage = peekMediaUsages(entry.file);
|
|
606
|
+
/* Asked on the first paint that finds them missing: both loaders cache and de-duplicate in
|
|
607
|
+
flight, and both repaint when they land. */
|
|
608
|
+
if (!mediaMetaSettled(entry.file)) {
|
|
609
|
+
void loadMediaMeta(entry.file).then(redrawAll);
|
|
610
|
+
}
|
|
611
|
+
if (usage === null || usage.status === "pending") {
|
|
612
|
+
void loadMediaUsages(entry.file).then(redrawAll);
|
|
613
|
+
}
|
|
614
|
+
/* The viewer's own decision about which element shows a file (`media/media-pane.ts`), so the two
|
|
615
|
+
cannot drift: a font is set as a specimen in both. A PDF's embed wants the viewer's full pane,
|
|
616
|
+
so the detail pane draws its glyph and Open in viewer shows it. */
|
|
617
|
+
const stage = mediaStageFor(entry.file);
|
|
618
|
+
const previewKind =
|
|
619
|
+
stage === "image" || stage === "video" || stage === "audio" || stage === "font"
|
|
620
|
+
? stage
|
|
621
|
+
: "glyph";
|
|
622
|
+
const specimen =
|
|
623
|
+
previewKind === "font"
|
|
624
|
+
? specimenOf(entry.file)
|
|
625
|
+
: { fontFace: "", specimen: "", specimenFamily: "" };
|
|
626
|
+
return {
|
|
627
|
+
detailFacts: factsLine(entry.file, meta),
|
|
628
|
+
detailHint: "",
|
|
629
|
+
detailName: entry.name,
|
|
630
|
+
detailRef: entry.ref,
|
|
631
|
+
detailState: "shown",
|
|
632
|
+
previewFontFace: specimen.fontFace,
|
|
633
|
+
previewGlyph: KIND_GLYPH[entry.kind],
|
|
634
|
+
previewKind,
|
|
635
|
+
previewSpecimen: specimen.specimen,
|
|
636
|
+
previewSpecimenFamily: specimen.specimenFamily,
|
|
637
|
+
previewSrc: previewKind === "glyph" ? "" : previewFileSrc(entry.file),
|
|
638
|
+
replaceAccept: replaceAccept(entry.file),
|
|
639
|
+
selectedFile: entry.file,
|
|
640
|
+
...usageOf(usage),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ─── Actions ─────────────────────────────────────────────────────────────────
|
|
645
|
+
|
|
646
|
+
/** `files/files.ts`, for a usage link. Dynamic, for the reason `media/media-pane.ts` gives. */
|
|
647
|
+
let _files: Promise<typeof FilesModule> | null = null;
|
|
648
|
+
/** `files/file-ops.ts`, for the delete confirmation: it reaches the canvas and the collab layer. */
|
|
649
|
+
let _fileOps: Promise<typeof FileOpsModule> | null = null;
|
|
650
|
+
|
|
651
|
+
function filesModule(): Promise<typeof FilesModule> {
|
|
652
|
+
_files ??= import("../files/files");
|
|
653
|
+
return _files;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function fileOpsModule(): Promise<typeof FileOpsModule> {
|
|
657
|
+
_fileOps ??= import("../files/file-ops");
|
|
658
|
+
return _fileOps;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Open the picker an input stands for, with HTML's `showPicker()`.
|
|
663
|
+
*
|
|
664
|
+
* It throws without transient user activation (a command run from a script rather than a key or a
|
|
665
|
+
* click) and where a host has not implemented it; either way the control is focused instead, so
|
|
666
|
+
* Enter opens it.
|
|
667
|
+
*/
|
|
668
|
+
function openPicker(input: HTMLInputElement): void {
|
|
669
|
+
try {
|
|
670
|
+
input.showPicker();
|
|
671
|
+
} catch {
|
|
672
|
+
input.focus();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Select `file` in a mount; the repaint says so to a pick dialog's footer. */
|
|
677
|
+
function select(mount: LibraryMount, file: string | null): void {
|
|
678
|
+
mount.choice.selected = file;
|
|
679
|
+
redraw(mount);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function uploadInto(mount: LibraryMount, files: FileList | File[]): Promise<void> {
|
|
683
|
+
/* Only files this library will list (a drop ignores `accept`): an image picker takes pictures, and
|
|
684
|
+
the home takes what `MEDIA_EXTENSIONS` names, so what is selected next is always on screen. */
|
|
685
|
+
const uploaded = await uploadAssets([...files], {
|
|
686
|
+
dir: mount.uploadDir,
|
|
687
|
+
only: mount.images ? "image" : "media",
|
|
688
|
+
});
|
|
689
|
+
const [first] = uploaded;
|
|
690
|
+
if (!first) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
/* The listing is being re-read (the upload core announced the change), so the new file may not be
|
|
694
|
+
in it yet: select it now, and the detail pane describes it the moment the listing lands. */
|
|
695
|
+
if (mount.choice.filter !== "all" && mount.choice.filter !== first.kind) {
|
|
696
|
+
mount.choice.filter = mount.images ? "image" : "all";
|
|
697
|
+
}
|
|
698
|
+
mount.choice.query = "";
|
|
699
|
+
mount.reveal = first.path;
|
|
700
|
+
select(mount, first.path);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Delete a media file, after the confirmation every delete goes through (studio.md §9.1.1): its
|
|
705
|
+
* usage line is stated before the button can be pressed.
|
|
706
|
+
*
|
|
707
|
+
* @returns Whether it was deleted.
|
|
708
|
+
*/
|
|
709
|
+
export async function deleteMediaFile(path: string): Promise<boolean> {
|
|
710
|
+
const name = path.split("/").pop() ?? path;
|
|
711
|
+
const { confirmFileDelete } = await fileOpsModule();
|
|
712
|
+
if (!(await confirmFileDelete({ name, path }))) {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
const { markLocalMutation } = await import("../files/fs-events");
|
|
716
|
+
try {
|
|
717
|
+
markLocalMutation(path);
|
|
718
|
+
await getPlatform().deleteFile(path);
|
|
719
|
+
} catch (error) {
|
|
720
|
+
notify.error(`Could not delete ${name}.`, {
|
|
721
|
+
detail: errorMessage(error),
|
|
722
|
+
path,
|
|
723
|
+
source: "Media",
|
|
724
|
+
});
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
forgetVersions([path]);
|
|
728
|
+
invalidateUsages();
|
|
729
|
+
// A viewer tab over a file that is gone would show a broken image; it holds no edits to lose.
|
|
730
|
+
if (workspace.tabs.has(path)) {
|
|
731
|
+
closeTab(path);
|
|
732
|
+
}
|
|
733
|
+
for (const mount of _mounts) {
|
|
734
|
+
if (mount.choice.selected === path) {
|
|
735
|
+
mount.choice.selected = null;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (homeChoice.selected === path) {
|
|
739
|
+
homeChoice.selected = null;
|
|
740
|
+
}
|
|
741
|
+
const dir = dirName(path);
|
|
742
|
+
const pruned = await pruneEmptiedDestination(dir, markLocalMutation);
|
|
743
|
+
// A pruned folder is gone, so the Files tree re-reads the folder that held it.
|
|
744
|
+
await announceMediaChanged(pruned ? dirName(dir) : dir);
|
|
745
|
+
notify.success(`Deleted ${name}`);
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Take away a folder an upload created once a delete has emptied it, and say whether it went.
|
|
751
|
+
*
|
|
752
|
+
* Only an upload destination (`public/media`, a collection's `images`): those exist because a
|
|
753
|
+
* Studio upload made them, and an author who uploads one file and deletes it again should not be
|
|
754
|
+
* left a folder nobody asked for. A folder the author made is theirs, emptied or not. A backend
|
|
755
|
+
* that refuses (a dotfile the listing does not show, a host with no folder delete) leaves it, which
|
|
756
|
+
* costs nothing a reader can see: an empty folder lists no media.
|
|
757
|
+
*/
|
|
758
|
+
async function pruneEmptiedDestination(
|
|
759
|
+
dir: string,
|
|
760
|
+
markLocalMutation: (path: string) => void,
|
|
761
|
+
): Promise<boolean> {
|
|
762
|
+
if (!isUploadDestination(dir)) {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
const platform = getPlatform();
|
|
766
|
+
try {
|
|
767
|
+
const left = await platform.listDirectory(dir);
|
|
768
|
+
if (left.length > 0) {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
markLocalMutation(dir);
|
|
772
|
+
await platform.deleteFile(dir);
|
|
773
|
+
return true;
|
|
774
|
+
} catch {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function actionsFor(mount: LibraryMount) {
|
|
780
|
+
return {
|
|
781
|
+
activate: (file: string) => {
|
|
782
|
+
select(mount, file);
|
|
783
|
+
activate(mount, file);
|
|
784
|
+
},
|
|
785
|
+
activateSelected: () => {
|
|
786
|
+
const { selected } = mount.choice;
|
|
787
|
+
if (selected !== null) {
|
|
788
|
+
activate(mount, selected);
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
copyRef: () => {
|
|
792
|
+
const entry = selectedEntry(mount);
|
|
793
|
+
// `navigator.clipboard` is [SecureContext]: a plain-http host has none, whatever lib.dom says.
|
|
794
|
+
if (entry && "clipboard" in navigator) {
|
|
795
|
+
void navigator.clipboard.writeText(entry.ref);
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
dragOut: () => {
|
|
799
|
+
if (mount.dropActive) {
|
|
800
|
+
mount.dropActive = false;
|
|
801
|
+
redraw(mount);
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
dragOver: () => {
|
|
805
|
+
if (!mount.dropActive) {
|
|
806
|
+
mount.dropActive = true;
|
|
807
|
+
redraw(mount);
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
drop: (files: FileList | null) => {
|
|
811
|
+
mount.dropActive = false;
|
|
812
|
+
redraw(mount);
|
|
813
|
+
if (files?.length) {
|
|
814
|
+
void uploadInto(mount, files);
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
focusTile: (file: string) => {
|
|
818
|
+
/* Selection follows focus (the listbox pattern): a Tab onto the grid lands on the roving stop
|
|
819
|
+
AND selects it, so the first Enter opens (or, in a pick, uses) the tile the ring is on. */
|
|
820
|
+
if (mount.choice.selected !== file) {
|
|
821
|
+
select(mount, file);
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
gridHost: (element: HTMLElement) => {
|
|
825
|
+
mount.grid = element;
|
|
826
|
+
},
|
|
827
|
+
inputHost: (element: HTMLInputElement, role: "upload" | "replace") => {
|
|
828
|
+
mount.inputs[role].add(element);
|
|
829
|
+
scheduleSweep(mount);
|
|
830
|
+
const pending = _pendingPicker;
|
|
831
|
+
if (
|
|
832
|
+
mount.mode === "manage" &&
|
|
833
|
+
pending?.role === role &&
|
|
834
|
+
(role === "upload" || pending.file === mount.choice.selected)
|
|
835
|
+
) {
|
|
836
|
+
_pendingPicker = null;
|
|
837
|
+
openPicker(element);
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
key: (key: string, ctrl: boolean) => moveSelection(mount, key, ctrl),
|
|
841
|
+
measure: (file: string, width: number, height: number) => {
|
|
842
|
+
if (recordImageSize(file, width, height)) {
|
|
843
|
+
redrawAll();
|
|
844
|
+
} else if (!mediaMetaSettled(file)) {
|
|
845
|
+
// An SVG's decode is not its size: the file says what it is, read once.
|
|
846
|
+
void loadMediaMeta(file).then(redrawAll);
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
openDocument: (path: string) => {
|
|
850
|
+
void filesModule().then(({ openFileInTab }) => openFileInTab(path));
|
|
851
|
+
},
|
|
852
|
+
openViewer: () => {
|
|
853
|
+
const { selected } = mount.choice;
|
|
854
|
+
if (selected !== null) {
|
|
855
|
+
openMediaTab(selected);
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
remove: () => {
|
|
859
|
+
const { selected } = mount.choice;
|
|
860
|
+
if (selected !== null) {
|
|
861
|
+
void deleteMediaFile(selected);
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
replace: (input: HTMLInputElement) => {
|
|
865
|
+
const [chosen] = input.files ?? [];
|
|
866
|
+
// Cleared first: a file input that keeps its value refuses the same file twice.
|
|
867
|
+
input.value = "";
|
|
868
|
+
const { selected } = mount.choice;
|
|
869
|
+
if (chosen && selected !== null) {
|
|
870
|
+
/* No metadata invalidation here: `replaceAsset` announces the change, whose handler drops
|
|
871
|
+
the listing AND its metadata and re-reads them in that order. A second drop after it
|
|
872
|
+
lands would wipe the fresh facts of every other tile, and nothing would re-seed them. */
|
|
873
|
+
void replaceAsset(selected, chosen).then(redrawAll);
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
reset: () => {
|
|
877
|
+
mount.choice.filter = mount.images ? "image" : "all";
|
|
878
|
+
mount.choice.query = "";
|
|
879
|
+
redraw(mount);
|
|
880
|
+
},
|
|
881
|
+
select: (file: string) => select(mount, file),
|
|
882
|
+
setFilter: (key: string) => {
|
|
883
|
+
if (isMediaFilterKey(key)) {
|
|
884
|
+
mount.choice.filter = key;
|
|
885
|
+
redraw(mount);
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
setQuery: (value: string) => {
|
|
889
|
+
mount.choice.query = value;
|
|
890
|
+
redraw(mount);
|
|
891
|
+
},
|
|
892
|
+
setSort: (value: string) => {
|
|
893
|
+
if (isMediaSortKey(value)) {
|
|
894
|
+
mount.choice.sort = value;
|
|
895
|
+
redraw(mount);
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
tileHost: (element: HTMLElement, file: string) => {
|
|
899
|
+
mount.tiles.set(file, element);
|
|
900
|
+
tileFiles.set(element, file);
|
|
901
|
+
mount.observer.observe(element);
|
|
902
|
+
},
|
|
903
|
+
upload: (input: HTMLInputElement) => {
|
|
904
|
+
// Copied before the reset: `input.files` is LIVE, so clearing the value (which lets the same
|
|
905
|
+
// File be chosen twice) would empty the list this reads.
|
|
906
|
+
const chosen = [...(input.files ?? [])];
|
|
907
|
+
input.value = "";
|
|
908
|
+
if (chosen.length > 0) {
|
|
909
|
+
void uploadInto(mount, chosen);
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** Enter or a double-click: open the viewer, or in pick mode, use the file. */
|
|
916
|
+
function activate(mount: LibraryMount, file: string): void {
|
|
917
|
+
if (mount.mode === "pick") {
|
|
918
|
+
const entry = selectedEntry(mount);
|
|
919
|
+
if (entry) {
|
|
920
|
+
mount.onPick?.(entry);
|
|
921
|
+
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
openMediaTab(file);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** An arrow, Home or End on the grid: move the selection and the focus with it. */
|
|
928
|
+
function moveSelection(mount: LibraryMount, key: string, ctrl: boolean): void {
|
|
929
|
+
const { files } = libraryFiles(mount);
|
|
930
|
+
const visible = visibleMedia(files, {
|
|
931
|
+
filter: mount.choice.filter,
|
|
932
|
+
query: mount.choice.query,
|
|
933
|
+
sort: mount.choice.sort,
|
|
934
|
+
unused: _unused,
|
|
935
|
+
});
|
|
936
|
+
const index = visible.findIndex((file) => file.file === mount.choice.selected);
|
|
937
|
+
const columns = mount.grid ? gridColumns(rectOf(mount.grid).width) : 1;
|
|
938
|
+
const next =
|
|
939
|
+
index === -1
|
|
940
|
+
? visible.length > 0
|
|
941
|
+
? 0
|
|
942
|
+
: null
|
|
943
|
+
: gridStep(index, key, columns, visible.length, ctrl);
|
|
944
|
+
const target = next === null ? undefined : visible[next];
|
|
945
|
+
if (!target) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
select(mount, target.file);
|
|
949
|
+
/* A focus move is the one thing a flow may reach into its document for; the tile is the one the
|
|
950
|
+
surface announced, not a node found by selector. */
|
|
951
|
+
mount.tiles.get(target.file)?.focus();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** The file each tile node stands for, as the surface announced it. */
|
|
955
|
+
const tileFiles = new WeakMap<Element, string>();
|
|
956
|
+
|
|
957
|
+
/** Build a mount and wire its observer. */
|
|
958
|
+
function createMount(
|
|
959
|
+
mode: "manage" | "pick",
|
|
960
|
+
choice: LibraryChoice,
|
|
961
|
+
region: string,
|
|
962
|
+
paneId: string | null,
|
|
963
|
+
images: boolean,
|
|
964
|
+
uploadDir: string,
|
|
965
|
+
): LibraryMount {
|
|
966
|
+
const mount: LibraryMount = {
|
|
967
|
+
choice,
|
|
968
|
+
disposed: false,
|
|
969
|
+
dropActive: false,
|
|
970
|
+
grid: null,
|
|
971
|
+
handle: null,
|
|
972
|
+
images,
|
|
973
|
+
inputs: { replace: new Set(), upload: new Set() },
|
|
974
|
+
mode,
|
|
975
|
+
observer: createPreviewObserver((element) => {
|
|
976
|
+
/* The file the surface announced with the node, not an attribute read back off it: the
|
|
977
|
+
observer may report a node before the runtime has written its attributes. */
|
|
978
|
+
const file = tileFiles.get(element);
|
|
979
|
+
if (file !== undefined && !mount.visible.has(file)) {
|
|
980
|
+
mount.visible.add(file);
|
|
981
|
+
scheduleRedraw(mount);
|
|
982
|
+
}
|
|
983
|
+
}),
|
|
984
|
+
onPick: null,
|
|
985
|
+
onSelect: null,
|
|
986
|
+
paneId,
|
|
987
|
+
reveal: null,
|
|
988
|
+
focusOwed: false,
|
|
989
|
+
region,
|
|
990
|
+
sweepTimer: null,
|
|
991
|
+
tiles: new Map(),
|
|
992
|
+
uploadDir,
|
|
993
|
+
visible: new Set(),
|
|
994
|
+
};
|
|
995
|
+
return mount;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/** Tiles enter view in bursts (a scroll, the first paint); one repaint per burst is enough. */
|
|
999
|
+
const _scheduled = new Set<LibraryMount>();
|
|
1000
|
+
|
|
1001
|
+
function scheduleRedraw(mount: LibraryMount): void {
|
|
1002
|
+
if (_scheduled.has(mount)) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
_scheduled.add(mount);
|
|
1006
|
+
queueMicrotask(() => {
|
|
1007
|
+
_scheduled.delete(mount);
|
|
1008
|
+
redraw(mount);
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// ─── The Media home ──────────────────────────────────────────────────────────
|
|
1013
|
+
|
|
1014
|
+
/** Whether this tab's library is already mounted in this pane and still in the document. */
|
|
1015
|
+
export function mediaLibraryMounted(paneId: string, tab: Tab): boolean {
|
|
1016
|
+
const pane = _panes.get(paneId);
|
|
1017
|
+
return (
|
|
1018
|
+
pane !== undefined &&
|
|
1019
|
+
pane.tabId === tab.id &&
|
|
1020
|
+
pane.wrap.isConnected &&
|
|
1021
|
+
pane.mount.handle !== null &&
|
|
1022
|
+
pane.mount.handle.attached()
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** Tear one pane's library down (mode change, tab switch, project close). Idempotent. */
|
|
1027
|
+
export function detachMediaLibrary(paneId: string): void {
|
|
1028
|
+
const pane = _panes.get(paneId);
|
|
1029
|
+
if (!pane) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
disposeMount(pane.mount);
|
|
1033
|
+
_panes.delete(paneId);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Draw the Media home into a pane's stage.
|
|
1038
|
+
*
|
|
1039
|
+
* @param {CanvasSurface} surface
|
|
1040
|
+
* @param {Tab} tab
|
|
1041
|
+
*/
|
|
1042
|
+
export function renderMediaLibrary(surface: CanvasSurface, tab: Tab): void {
|
|
1043
|
+
const { paneId, wrap } = surface;
|
|
1044
|
+
if (mediaLibraryMounted(paneId, tab)) {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
detachMediaLibrary(paneId);
|
|
1048
|
+
const mount = createMount(
|
|
1049
|
+
"manage",
|
|
1050
|
+
homeChoice,
|
|
1051
|
+
paneRegion(paneId, "media-library"),
|
|
1052
|
+
paneId,
|
|
1053
|
+
false,
|
|
1054
|
+
// The home has no document, so its uploads take the default.
|
|
1055
|
+
DEFAULT_MEDIA_DIR,
|
|
1056
|
+
);
|
|
1057
|
+
_mounts.add(mount);
|
|
1058
|
+
listen();
|
|
1059
|
+
mount.handle = mountMediaLibrarySurface(wrap, libraryView(mount), actionsFor(mount), {
|
|
1060
|
+
clear: true,
|
|
1061
|
+
});
|
|
1062
|
+
_panes.set(paneId, { mount, tabId: tab.id, wrap });
|
|
1063
|
+
if (_focusOnMount.delete(paneId)) {
|
|
1064
|
+
void focusTabStop(mount);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Put the keyboard on a library's tab stop once it is drawn: the selected tile, or the tile the
|
|
1070
|
+
* roving tabindex leaves reachable when nothing is selected. Like the arrow keys' focus move, it
|
|
1071
|
+
* reaches only for a tile the surface announced. With no such tile in the document yet (the listing
|
|
1072
|
+
* is still being read), the focus stays owed and the next repaint serves it.
|
|
1073
|
+
*/
|
|
1074
|
+
async function focusTabStop(mount: LibraryMount): Promise<void> {
|
|
1075
|
+
await mount.handle?.ready;
|
|
1076
|
+
serveOwedFocus(mount, true);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/** Serve an owed focus, when {@link focusTabStop} is owed one and a tab-stop tile is drawn. */
|
|
1080
|
+
function serveOwedFocus(mount: LibraryMount, owe = mount.focusOwed): void {
|
|
1081
|
+
if (!owe || mount.disposed) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const { selected } = mount.choice;
|
|
1085
|
+
const target =
|
|
1086
|
+
(selected === null ? undefined : mount.tiles.get(selected)) ??
|
|
1087
|
+
[...mount.tiles.values()].find((tile) => tile.getAttribute("tabindex") === "0");
|
|
1088
|
+
if (target?.isConnected) {
|
|
1089
|
+
mount.focusOwed = false;
|
|
1090
|
+
target.focus();
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
mount.focusOwed = true;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Give the keyboard back to the Media home in `paneId`: what leaving a viewer with Back does
|
|
1098
|
+
* (`workspace/workspace-back.ts`), so a keyboard user lands on the tile they opened rather than on
|
|
1099
|
+
* the page body. Served now when the home is standing in the pane, or by its next mount there.
|
|
1100
|
+
*
|
|
1101
|
+
* @param {string} paneId
|
|
1102
|
+
*/
|
|
1103
|
+
export function focusMediaHome(paneId: string): void {
|
|
1104
|
+
const pane = _panes.get(paneId);
|
|
1105
|
+
if (pane?.wrap.isConnected && pane.mount.handle?.attached() === true) {
|
|
1106
|
+
void focusTabStop(pane.mount);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
_focusOnMount.add(paneId);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** A request for the Media home: what `media.browse` passes. */
|
|
1113
|
+
export interface MediaBrowseRequest {
|
|
1114
|
+
filter?: MediaFilterKey;
|
|
1115
|
+
query?: string;
|
|
1116
|
+
/** A file to select: its project path. */
|
|
1117
|
+
select?: string;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Apply a request to the Media home's choices and repaint every pane showing it. The caller opens
|
|
1122
|
+
* the home tab (`media/media-commands.ts`); this module does not know where homes come from.
|
|
1123
|
+
*/
|
|
1124
|
+
export function applyMediaBrowse(request: MediaBrowseRequest): void {
|
|
1125
|
+
if (request.filter !== undefined) {
|
|
1126
|
+
homeChoice.filter = request.filter;
|
|
1127
|
+
}
|
|
1128
|
+
if (request.query !== undefined) {
|
|
1129
|
+
homeChoice.query = request.query;
|
|
1130
|
+
}
|
|
1131
|
+
if (request.select !== undefined) {
|
|
1132
|
+
homeChoice.selected = request.select;
|
|
1133
|
+
// Shown whatever filter was in force: a file asked for by name must be on screen.
|
|
1134
|
+
if (homeChoice.filter !== "all") {
|
|
1135
|
+
homeChoice.filter = "all";
|
|
1136
|
+
}
|
|
1137
|
+
homeChoice.query = "";
|
|
1138
|
+
}
|
|
1139
|
+
redrawAll();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Open the Upload picker of the Media home: now, when a home is mounted, or as soon as its input is
|
|
1144
|
+
* created. The caller has opened the home.
|
|
1145
|
+
*/
|
|
1146
|
+
export function requestMediaUpload(): void {
|
|
1147
|
+
/* The first input still in the document, across panes: the empty state's own Upload is dropped
|
|
1148
|
+
when the first file lands, and the toolbar's is the one that stays. */
|
|
1149
|
+
const input =
|
|
1150
|
+
[..._panes.values()]
|
|
1151
|
+
.map((pane) => connectedInput(pane.mount.inputs.upload))
|
|
1152
|
+
.find((found) => found !== null) ?? null;
|
|
1153
|
+
if (input) {
|
|
1154
|
+
openPicker(input);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
_pendingPicker = { file: null, role: "upload" };
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Select `path` in the Media home and open its Replace picker, now or once the detail pane draws
|
|
1162
|
+
* it. The caller has opened the home.
|
|
1163
|
+
*/
|
|
1164
|
+
export function requestMediaReplace(path: string): void {
|
|
1165
|
+
applyMediaBrowse({ select: path });
|
|
1166
|
+
let input: HTMLInputElement | null = null;
|
|
1167
|
+
for (const pane of _panes.values()) {
|
|
1168
|
+
if (pane.mount.choice.selected === path) {
|
|
1169
|
+
input ??= connectedInput(pane.mount.inputs.replace);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (input) {
|
|
1173
|
+
openPicker(input);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
_pendingPicker = { file: path, role: "replace" };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// ─── Pick mode ───────────────────────────────────────────────────────────────
|
|
1180
|
+
|
|
1181
|
+
export interface MediaPickRequest {
|
|
1182
|
+
/** Only images, for a field that holds a picture. */
|
|
1183
|
+
images: boolean;
|
|
1184
|
+
/** What a use commits: the file's site URL. */
|
|
1185
|
+
onPick: (ref: string) => void;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** The open pick dialog, or `null`. One at a time: it answers one field. */
|
|
1189
|
+
let _pick: { dialog: MediaPickDialogHandle; mount: LibraryMount } | null = null;
|
|
1190
|
+
|
|
1191
|
+
/** What the dialog's footer says for the selection standing now. */
|
|
1192
|
+
function pickDialogView(mount: LibraryMount) {
|
|
1193
|
+
const entry = selectedEntry(mount);
|
|
1194
|
+
return {
|
|
1195
|
+
blocked: entry === null,
|
|
1196
|
+
confirmLabel: entry ? `Use ${entry.name}` : mount.images ? "Use image" : "Use file",
|
|
1197
|
+
headline: mount.images ? "Choose an image" : "Choose media",
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/** Close the pick dialog, if one is open. Idempotent. */
|
|
1202
|
+
export function closeMediaPickDialog(): void {
|
|
1203
|
+
const open = _pick;
|
|
1204
|
+
_pick = null;
|
|
1205
|
+
if (open) {
|
|
1206
|
+
disposeMount(open.mount);
|
|
1207
|
+
open.dialog.close();
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Open the full-size picker: the library in pick mode, in a `jx-dialog` in the dialog layer. A
|
|
1213
|
+
* double-click on a tile, Enter on the grid, or **Use <file>** commits the selection.
|
|
1214
|
+
*/
|
|
1215
|
+
export function openMediaPickDialog(request: MediaPickRequest): void {
|
|
1216
|
+
closeMediaPickDialog();
|
|
1217
|
+
const choice: LibraryChoice = {
|
|
1218
|
+
filter: request.images ? "image" : "all",
|
|
1219
|
+
query: "",
|
|
1220
|
+
selected: null,
|
|
1221
|
+
sort: "newest",
|
|
1222
|
+
};
|
|
1223
|
+
/* A region under the dialog slot's own (`overlay.dialog:media-picker`), so a node inside the
|
|
1224
|
+
library still resolves to a grammar-valid id rather than shadowing the slot's with a bare one. */
|
|
1225
|
+
const mount = createMount(
|
|
1226
|
+
"pick",
|
|
1227
|
+
choice,
|
|
1228
|
+
`${overlayRegion("dialog", MEDIA_PICK_SLOT)}/library`,
|
|
1229
|
+
null,
|
|
1230
|
+
request.images,
|
|
1231
|
+
/* A pick answers a field of the active document (the dialog belongs to no pane), so it lands
|
|
1232
|
+
where that document's other uploads do, exactly as the field's own Upload does. */
|
|
1233
|
+
uploadDirFor(activeTab.value?.documentPath ?? null),
|
|
1234
|
+
);
|
|
1235
|
+
const use = (entry: ProjectMediaFile): void => {
|
|
1236
|
+
closeMediaPickDialog();
|
|
1237
|
+
request.onPick(entry.ref);
|
|
1238
|
+
};
|
|
1239
|
+
mount.onPick = use;
|
|
1240
|
+
_mounts.add(mount);
|
|
1241
|
+
listen();
|
|
1242
|
+
const dialog = openMediaPickDialogSurface({
|
|
1243
|
+
body: (host) => {
|
|
1244
|
+
mount.handle = mountMediaLibrarySurface(host, libraryView(mount), actionsFor(mount), {
|
|
1245
|
+
clear: false,
|
|
1246
|
+
});
|
|
1247
|
+
},
|
|
1248
|
+
/* Escape, the backdrop or Cancel: the platform closed the dialog, so take its slot and the
|
|
1249
|
+
library down with it. `close()` is idempotent, so the flow's own close running first is fine. */
|
|
1250
|
+
closed: () => {
|
|
1251
|
+
if (_pick?.mount === mount) {
|
|
1252
|
+
closeMediaPickDialog();
|
|
1253
|
+
} else {
|
|
1254
|
+
disposeMount(mount);
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
confirm: () => {
|
|
1258
|
+
const entry = selectedEntry(mount);
|
|
1259
|
+
if (entry) {
|
|
1260
|
+
use(entry);
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
layer: layerHost("dialog"),
|
|
1264
|
+
view: pickDialogView(mount),
|
|
1265
|
+
});
|
|
1266
|
+
mount.onSelect = () => dialog.update(pickDialogView(mount));
|
|
1267
|
+
_pick = { dialog, mount };
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/** Forget every choice and mount: a project switch, or a test. */
|
|
1271
|
+
export function resetMediaLibrary(): void {
|
|
1272
|
+
closeMediaPickDialog();
|
|
1273
|
+
for (const paneId of _panes.keys()) {
|
|
1274
|
+
detachMediaLibrary(paneId);
|
|
1275
|
+
}
|
|
1276
|
+
homeChoice.filter = "all";
|
|
1277
|
+
homeChoice.query = "";
|
|
1278
|
+
homeChoice.selected = null;
|
|
1279
|
+
homeChoice.sort = "newest";
|
|
1280
|
+
_pendingPicker = null;
|
|
1281
|
+
_focusOnMount.clear();
|
|
1282
|
+
resetUnused();
|
|
1283
|
+
}
|