@jxsuite/studio 2.0.1 → 2.1.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/canvas.html +21 -0
- package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
- package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
- package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
- package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
- package/dist/chunks/{components-799jpvhm.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-799jpvhm.js.map → components-2bpqp4mq.js.map} +1 -1
- package/dist/chunks/content-source-34gnkbnn.js +38 -0
- package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
- package/dist/chunks/extension-sections-6mqydz35.js +301 -0
- package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
- package/dist/chunks/files-8kn321h1.js +70 -0
- package/dist/chunks/files-8kn321h1.js.map +9 -0
- package/dist/chunks/{format-host-8jxwts2p.js → format-host-mkwsmt8d.js} +3 -3
- package/dist/chunks/git-panel-tjbr191d.js +51 -0
- package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
- package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
- package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
- package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
- package/dist/chunks/layers-panel-87pb25km.js +58 -0
- package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
- package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
- package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
- package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
- package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
- package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
- package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
- package/dist/chunks/right-panel-zpdxd98t.js +37 -0
- package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
- package/dist/chunks/section-registry-zmckaxf4.js +30 -0
- package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
- package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
- package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
- package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
- package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
- package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
- package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
- package/dist/chunks/studio-41exgzhq.js +6690 -0
- package/dist/chunks/studio-41exgzhq.js.map +38 -0
- package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
- package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
- package/dist/chunks/studio-50k77cca.js +1787 -0
- package/dist/chunks/studio-50k77cca.js.map +18 -0
- package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
- package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
- package/dist/chunks/studio-6f0am8f2.js +53 -0
- package/dist/chunks/studio-6f0am8f2.js.map +10 -0
- package/dist/chunks/studio-6nmjc5qk.js +7002 -0
- package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
- package/dist/chunks/studio-7afd21nw.js +67 -0
- package/dist/chunks/studio-7afd21nw.js.map +10 -0
- package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
- package/dist/chunks/studio-7bmdvzne.js.map +10 -0
- package/dist/chunks/studio-8jpzqk5s.js +128 -0
- package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
- package/dist/chunks/studio-94m77tgk.js +1482 -0
- package/dist/chunks/studio-94m77tgk.js.map +13 -0
- package/dist/chunks/studio-9je6w5sq.js +1000 -0
- package/dist/chunks/studio-9je6w5sq.js.map +16 -0
- package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
- package/dist/chunks/{studio-gs6kqdt1.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-gs6kqdt1.js.map → studio-adn0z1k3.js.map} +3 -3
- package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
- package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
- package/dist/chunks/studio-bw5dz6km.js +608 -0
- package/dist/chunks/studio-bw5dz6km.js.map +21 -0
- package/dist/chunks/studio-c6s5getc.js +4232 -0
- package/dist/chunks/studio-c6s5getc.js.map +39 -0
- package/dist/chunks/studio-cjm7fdjf.js +451 -0
- package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
- package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
- package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
- package/dist/chunks/studio-dg7zyt5x.js +28 -0
- package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
- package/dist/chunks/studio-dzcyvvvz.js +166 -0
- package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
- package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
- package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
- package/dist/chunks/studio-f12h6177.js +411 -0
- package/dist/chunks/studio-f12h6177.js.map +13 -0
- package/dist/chunks/studio-hbp1s8m9.js +1817 -0
- package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
- package/dist/chunks/studio-k2sxjph5.js +15295 -0
- package/dist/chunks/studio-k2sxjph5.js.map +235 -0
- package/dist/chunks/studio-myje51t3.js +385 -0
- package/dist/chunks/studio-myje51t3.js.map +12 -0
- package/dist/chunks/studio-na6rwzhw.js +353 -0
- package/dist/chunks/studio-na6rwzhw.js.map +11 -0
- package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
- package/dist/chunks/studio-p471gg5n.js.map +14 -0
- package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
- package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
- package/dist/chunks/{studio-esrewxap.js → studio-s519zc99.js} +3 -3
- package/dist/chunks/studio-sas7mt3k.js +75 -0
- package/dist/chunks/studio-sas7mt3k.js.map +10 -0
- package/dist/chunks/studio-t5wqwzdh.js +74 -0
- package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
- package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
- package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
- package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
- package/dist/chunks/studio-tr20m8g8.js.map +9 -0
- package/dist/chunks/{studio-v7jkjqng.js → studio-v96qfnww.js} +5514 -4291
- package/dist/chunks/studio-v96qfnww.js.map +54 -0
- package/dist/chunks/studio-vcetc91k.js +659 -0
- package/dist/chunks/studio-vcetc91k.js.map +10 -0
- package/dist/chunks/{studio-jm0z592w.js → studio-vmymjefp.js} +2972 -4937
- package/dist/chunks/studio-vmymjefp.js.map +98 -0
- package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
- package/dist/chunks/studio-x9hmy5kr.js +390 -0
- package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
- package/dist/chunks/studio-xa0ce0n7.js +335 -0
- package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
- package/dist/chunks/studio-xm3dbxvn.js +378 -0
- package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
- package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
- package/dist/chunks/studio-zv37yrfa.js +1054 -0
- package/dist/chunks/studio-zv37yrfa.js.map +13 -0
- package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
- package/dist/chunks/ws-client-654ts66j.js.map +11 -0
- package/dist/iframe-entry.js +668 -230
- package/dist/studio.js +21080 -32585
- package/index.html +35 -4685
- package/package.json +17 -15
- package/src/about/about-modal.ts +42 -11
- package/src/browse/library-commands.ts +240 -0
- package/src/browse/library-layouts.ts +369 -0
- package/src/browse/library-model.ts +396 -0
- package/src/browse/library-pane.ts +1049 -0
- package/src/browse/library-preview.ts +252 -0
- package/src/browse/library-source.ts +150 -0
- package/src/canvas/canvas-helpers.ts +33 -9
- package/src/canvas/canvas-live-render.ts +45 -29
- package/src/canvas/canvas-patcher.ts +151 -31
- package/src/canvas/canvas-perf.ts +46 -7
- package/src/canvas/canvas-render.ts +1092 -220
- package/src/canvas/canvas-surface.ts +240 -0
- package/src/canvas/canvas-utils.ts +1166 -124
- package/src/canvas/iframe-drop.ts +2 -1
- package/src/canvas/iframe-entry.ts +241 -4
- package/src/canvas/iframe-host.ts +1226 -143
- package/src/canvas/iframe-inline-edit.ts +31 -0
- package/src/canvas/iframe-interaction.ts +76 -16
- package/src/canvas/iframe-keys.ts +107 -45
- package/src/canvas/iframe-overlay.ts +38 -1
- package/src/canvas/iframe-protocol.ts +137 -2
- package/src/canvas/iframe-render.ts +70 -2
- package/src/canvas/iframe-slash.ts +6 -0
- package/src/canvas/path-mapping.ts +56 -6
- package/src/canvas/surface-registry.ts +292 -0
- package/src/collab/collab-commands.ts +244 -0
- package/src/collab/collab-session.ts +198 -32
- package/src/collab/collab-state.ts +28 -2
- package/src/collab/presence-chips.ts +100 -9
- package/src/commands/active-registry.ts +42 -0
- package/src/commands/app-commands.ts +176 -0
- package/src/commands/budget.ts +158 -0
- package/src/commands/command-args.ts +253 -0
- package/src/commands/context.ts +387 -0
- package/src/commands/defaults.ts +580 -0
- package/src/commands/keymap.ts +571 -0
- package/src/commands/levels.ts +345 -0
- package/src/commands/live-context.ts +193 -0
- package/src/commands/reference.ts +227 -0
- package/src/commands/registry.ts +293 -0
- package/src/component-props.ts +3 -2
- package/src/content/draft-state.ts +106 -0
- package/src/content/entry-commands.ts +282 -0
- package/src/content/entry-editor.ts +327 -0
- package/src/content/entry-fields.ts +116 -0
- package/src/content/entry-model.ts +175 -0
- package/src/editor/canvas-context-menu.ts +17 -11
- package/src/editor/canvas-slash-bridge.ts +11 -4
- package/src/editor/context-menu.ts +804 -204
- package/src/editor/convert-to-component.ts +31 -10
- package/src/editor/convert-to-repeater.ts +3 -2
- package/src/editor/inline-edit-apply.ts +5 -5
- package/src/editor/inline-edit.ts +86 -50
- package/src/editor/insert-zone-action.ts +1 -1
- package/src/editor/shortcuts.ts +1043 -329
- package/src/editor/slash-menu.ts +1 -0
- package/src/files/elements.ts +166 -0
- package/src/files/file-ops.ts +178 -31
- package/src/files/files.ts +898 -268
- package/src/files/fs-events.ts +21 -0
- package/src/files/media-meta.ts +203 -0
- package/src/files/media-paths.ts +141 -0
- package/src/files/media-upload.ts +7 -3
- package/src/files/media-usage.ts +189 -0
- package/src/github/github-auth.ts +235 -33
- package/src/github/github-publish.ts +80 -21
- package/src/grid/grid-controller.ts +152 -12
- package/src/grid/grid-layout.ts +225 -20
- package/src/grid/grid-open.ts +120 -0
- package/src/grid/grid-panel.ts +554 -39
- package/src/grid/grid-source.ts +27 -1
- package/src/grid/redirects-grid.ts +599 -0
- package/src/grid/redirects.ts +398 -0
- package/src/grid/sources/content-source.ts +108 -2
- package/src/i18n/i18n-commands.ts +322 -0
- package/src/new-project/add-repo-modal.ts +8 -10
- package/src/new-project/import-tab.ts +7 -0
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +221 -245
- package/src/packages/jxsuite-update.ts +97 -48
- package/src/packages/pull-package-sync.ts +18 -9
- package/src/page-params.ts +2 -1
- package/src/panels/activity-bar.ts +152 -91
- package/src/panels/activity-panel.ts +481 -0
- package/src/panels/ai-chat/chat-markdown.ts +8 -3
- package/src/panels/ai-chat/chat-view.ts +245 -18
- package/src/panels/ai-chat/composer.ts +31 -5
- package/src/panels/ai-chat/sessions-view.ts +17 -10
- package/src/panels/ai-panel.ts +279 -53
- package/src/panels/block-action-bar.ts +1115 -224
- package/src/panels/bottom-dock.ts +337 -0
- package/src/panels/canvas-dnd-bridge.ts +37 -8
- package/src/panels/chat-panel.ts +29 -12
- package/src/panels/data-explorer.ts +254 -112
- package/src/panels/data-grid.ts +10 -4
- package/src/panels/dnd.ts +28 -51
- package/src/panels/editors.ts +434 -107
- package/src/panels/elements-panel.ts +66 -2
- package/src/panels/empty-state.ts +112 -0
- package/src/panels/events-panel.ts +596 -82
- package/src/panels/formula-workspace.ts +408 -218
- package/src/panels/frontmatter-fields.ts +51 -10
- package/src/panels/frontmatter-panel.ts +357 -79
- package/src/panels/git-panel.ts +375 -164
- package/src/panels/head-panel.ts +791 -85
- package/src/panels/i18n-panel.ts +592 -0
- package/src/panels/imports-panel.ts +95 -85
- package/src/panels/jump-bar.ts +584 -0
- package/src/panels/layers-panel.ts +1106 -328
- package/src/panels/left-panel.ts +109 -204
- package/src/panels/navigator-panels.ts +131 -0
- package/src/panels/overlays.ts +7 -2
- package/src/panels/pane-context.ts +1477 -0
- package/src/panels/pane-grid.ts +406 -0
- package/src/panels/panel-registry.ts +340 -0
- package/src/panels/panel-scheduler.ts +75 -0
- package/src/panels/problems-panel.ts +234 -0
- package/src/panels/properties-panel.ts +946 -1139
- package/src/panels/provenance.ts +258 -0
- package/src/panels/quick-search.ts +791 -122
- package/src/panels/right-panel.ts +228 -102
- package/src/panels/seo-modal.ts +395 -0
- package/src/panels/settings-pane.ts +203 -0
- package/src/panels/signals-panel.ts +354 -137
- package/src/panels/statement-editor.ts +41 -25
- package/src/panels/statusbar.ts +342 -106
- package/src/panels/style-panel.ts +974 -463
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-doc.ts +4 -2
- package/src/panels/stylebook-layers-panel.ts +3 -2
- package/src/panels/stylebook-panel.ts +51 -26
- package/src/panels/tab-strip.ts +1058 -78
- package/src/panels/target-line.ts +274 -0
- package/src/panels/toolbar.ts +631 -484
- package/src/panels/welcome-screen.ts +301 -198
- package/src/platform-errors.ts +26 -3
- package/src/platform.ts +96 -2
- package/src/platforms/cloud.ts +71 -8
- package/src/platforms/devserver.ts +92 -29
- package/src/publish/deploy-checklist.ts +383 -0
- package/src/publish/pages-service.ts +10 -1
- package/src/publish/publish-commands.ts +218 -0
- package/src/publish/publish-panel.ts +110 -16
- package/src/recent-projects.ts +3 -2
- package/src/services/a11y-report.ts +505 -0
- package/src/services/ai-models.ts +30 -1
- package/src/services/ai-project-tools.ts +29 -10
- package/src/services/ai-system-prompt.ts +53 -14
- package/src/services/ai-tools.ts +39 -3
- package/src/services/ai-writes.ts +154 -0
- package/src/services/announce.ts +97 -0
- package/src/services/automation.ts +424 -175
- package/src/services/cem-export.ts +9 -4
- package/src/services/clock.ts +61 -0
- package/src/services/document-assistant.ts +36 -8
- package/src/services/idle.ts +179 -0
- package/src/services/import-client.ts +26 -0
- package/src/services/monaco-buffer.ts +627 -0
- package/src/services/monaco-lazy.ts +36 -0
- package/src/services/notify.ts +271 -0
- package/src/services/profile.ts +155 -0
- package/src/services/references.ts +0 -0
- package/src/services/token-lint.ts +3 -2
- package/src/services/tool-executor.ts +53 -7
- package/src/services/trusted-types.ts +123 -0
- package/src/settings/contexts-section.ts +427 -0
- package/src/settings/contributed-section.ts +149 -11
- package/src/settings/css-vars-editor.ts +306 -349
- package/src/settings/defs-editor.ts +93 -34
- package/src/settings/dependencies-editor.ts +7 -7
- package/src/settings/extension-sections.ts +37 -5
- package/src/settings/general-settings.ts +204 -121
- package/src/settings/locales-section.ts +347 -0
- package/src/settings/preferences-accounts.ts +140 -0
- package/src/settings/preferences-dialog.ts +620 -0
- package/src/settings/preferences-keymap.ts +270 -0
- package/src/settings/project-sections.ts +260 -0
- package/src/settings/section-registry.ts +169 -0
- package/src/settings/settings-document.ts +385 -0
- package/src/shell.ts +1549 -0
- package/src/site-context.ts +45 -20
- package/src/state.ts +14 -275
- package/src/store.ts +88 -43
- package/src/studio.ts +790 -346
- package/src/style/live-preview.ts +36 -0
- package/src/style/project-styles.ts +349 -0
- package/src/style/token-ref.ts +123 -0
- package/src/tabs/patch-ops.ts +16 -4
- package/src/tabs/project-config.ts +416 -0
- package/src/tabs/selection.ts +323 -0
- package/src/tabs/tab.ts +182 -47
- package/src/tabs/transact.ts +209 -28
- package/src/types.ts +68 -13
- package/src/ui/ai-credentials-form.ts +39 -35
- package/src/ui/ai-managed-connect.ts +7 -1
- package/src/ui/dynamic-slot.ts +224 -81
- package/src/ui/expression-editor.ts +71 -63
- package/src/ui/field-input.ts +4 -6
- package/src/ui/field-row.ts +215 -38
- package/src/ui/form-controls.ts +194 -7
- package/src/ui/layers.ts +492 -69
- package/src/ui/media-picker.ts +115 -25
- package/src/ui/panel-resize.ts +109 -91
- package/src/ui/progress-modal.ts +131 -50
- package/src/ui/regions.ts +462 -0
- package/src/ui/schema-form.ts +229 -45
- package/src/ui/spectrum.ts +24 -8
- package/src/ui/timing.ts +49 -0
- package/src/ui/value-selector.ts +20 -0
- package/src/ui/value-source.ts +348 -0
- package/src/ui/virtual-window.ts +400 -0
- package/src/utils/edit-display.ts +3 -2
- package/src/utils/inherited-style.ts +67 -37
- package/src/view.ts +64 -78
- package/src/workspace/pane-derive.ts +1258 -0
- package/src/workspace/session.ts +317 -0
- package/src/workspace/workspace.ts +1345 -23
- package/styles/canvas.css +653 -0
- package/styles/forced-colors.css +106 -0
- package/styles/inspector.css +1454 -0
- package/styles/overlays.css +2704 -0
- package/styles/panels.css +1701 -0
- package/styles/shell.css +1423 -0
- package/styles/tokens.css +151 -0
- package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
- package/dist/chunks/extension-sections-qr3drkkw.js +0 -531
- package/dist/chunks/extension-sections-qr3drkkw.js.map +0 -12
- package/dist/chunks/files-j8a1t06k.js +0 -50
- package/dist/chunks/files-j8a1t06k.js.map +0 -9
- package/dist/chunks/git-panel-xtq5z81x.js +0 -28
- package/dist/chunks/git-panel-xtq5z81x.js.map +0 -9
- package/dist/chunks/layers-panel-ancn2wpq.js +0 -26
- package/dist/chunks/layers-panel-ancn2wpq.js.map +0 -9
- package/dist/chunks/settings-modal-17ttr96g.js +0 -29
- package/dist/chunks/settings-modal-17ttr96g.js.map +0 -9
- package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
- package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
- package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
- package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
- package/dist/chunks/studio-0xgs7xrs.js +0 -429
- package/dist/chunks/studio-0xgs7xrs.js.map +0 -13
- package/dist/chunks/studio-4d8hzcz5.js +0 -3427
- package/dist/chunks/studio-4d8hzcz5.js.map +0 -26
- package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-jm0z592w.js.map +0 -115
- package/dist/chunks/studio-m2t4mdg5.js +0 -1854
- package/dist/chunks/studio-m2t4mdg5.js.map +0 -18
- package/dist/chunks/studio-n4p11dfy.js +0 -853
- package/dist/chunks/studio-n4p11dfy.js.map +0 -12
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-sjh1wt1a.js +0 -3744
- package/dist/chunks/studio-sjh1wt1a.js.map +0 -22
- package/dist/chunks/studio-v7jkjqng.js.map +0 -46
- package/dist/chunks/studio-xq0t7n5t.js +0 -140
- package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
- package/dist/chunks/studio-xze4zrs1.js.map +0 -10
- package/dist/chunks/studio-yfcstdzy.js +0 -399
- package/dist/chunks/studio-yfcstdzy.js.map +0 -12
- package/dist/chunks/studio-zp7jncs8.js +0 -469
- package/dist/chunks/studio-zp7jncs8.js.map +0 -14
- package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
- package/src/browse/browse-modal.ts +0 -69
- package/src/browse/browse.ts +0 -904
- package/src/canvas/canvas-diff.ts +0 -216
- package/src/canvas/nested-site-style.ts +0 -58
- package/src/new-project/design-fields.ts +0 -264
- package/src/new-project/templates.ts +0 -61
- package/src/panels/tab-bar.ts +0 -548
- package/src/settings/settings-modal.ts +0 -203
- /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
- /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
- /package/dist/chunks/{format-host-8jxwts2p.js.map → format-host-mkwsmt8d.js.map} +0 -0
- /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
- /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
- /package/dist/chunks/{studio-esrewxap.js.map → studio-s519zc99.js.map} +0 -0
- /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
- /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* The Library — an editor kind, drawn into the pane, over a {@link LibrarySource}.
|
|
4
|
+
*
|
|
5
|
+
* It replaces the Manage view, which was a full-screen MODAL over the whole shell. Everything that
|
|
6
|
+
* was wrong with that follows from the modal: it could not be a tab, so it could not be one of two
|
|
7
|
+
* panes, so it could not be open beside the page you were browsing FOR; it had no editor kind, so
|
|
8
|
+
* the status bar and the pane context bar had nothing to say about it; and its own filter bar was
|
|
9
|
+
* the only way to reach any of its state, so nothing it did was in the palette, on a chord, or
|
|
10
|
+
* available to the assistant. `manage` was already in `commands/context.ts`'s `EDITOR_KIND_BY_MODE`
|
|
11
|
+
* — the map just had nothing behind it.
|
|
12
|
+
*
|
|
13
|
+
* Four things this module is careful about:
|
|
14
|
+
*
|
|
15
|
+
* 1. **It never says "No files found" for two different reasons.** A scan that could not read a
|
|
16
|
+
* directory is INCOMPLETE, not empty; it raises a Problem carrying the directory and a Retry
|
|
17
|
+
* (`library.refresh`), and the surface says so. A filter that matched nothing says which
|
|
18
|
+
* filter.
|
|
19
|
+
* 2. **The rendered item count is proportional to the viewport, not to the project.** See
|
|
20
|
+
* `../ui/virtual-window.ts` and `library-preview.ts`; the acceptance case is 300 pages in
|
|
21
|
+
* "All".
|
|
22
|
+
* 3. **An upload has a named destination**, shown before the drop, asked for when the active category
|
|
23
|
+
* does not name one.
|
|
24
|
+
* 4. **Creation is somebody else's flow.** A page, layout or component is `files.ts`'s creation, so a
|
|
25
|
+
* new page from the Library and a new file from the tree refuse a name that is already taken in
|
|
26
|
+
* exactly the same way; a collection entry is `content/entry-commands.ts`'s `createEntry`, so it
|
|
27
|
+
* arrives with the collection's extension and a body seeded from that collection's schema.
|
|
28
|
+
*
|
|
29
|
+
* @docs studio/projects/browse
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
33
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
34
|
+
import { effect, effectScope, reactive } from "../reactivity";
|
|
35
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
36
|
+
import { notify } from "../services/notify";
|
|
37
|
+
import { beginActivity } from "../panels/activity-panel";
|
|
38
|
+
import { renderPopover, showPromptDialog } from "../ui/layers";
|
|
39
|
+
import { rectOf } from "../utils/geometry";
|
|
40
|
+
import { getPlatform } from "../platform";
|
|
41
|
+
import { invalidateUsages } from "../services/references";
|
|
42
|
+
import { confirmFileDelete, renamePromptMessage } from "../files/file-ops";
|
|
43
|
+
import { createFileIn, openFileInTab } from "../files/files";
|
|
44
|
+
import { createEntry } from "../content/entry-commands";
|
|
45
|
+
import { entryCollections } from "../content/entry-model";
|
|
46
|
+
import { UPLOAD_ACCEPT, uploadAssets } from "../files/media-upload";
|
|
47
|
+
import { localeLabel } from "@jxsuite/schema/locale";
|
|
48
|
+
import {
|
|
49
|
+
LIBRARY_CATEGORIES,
|
|
50
|
+
LIBRARY_LAYOUTS,
|
|
51
|
+
LIBRARY_LAYOUT_LABELS,
|
|
52
|
+
PREVIEW_LAYOUTS,
|
|
53
|
+
filterLibrary,
|
|
54
|
+
libraryCategory,
|
|
55
|
+
libraryLocales,
|
|
56
|
+
uploadDirForCategory,
|
|
57
|
+
} from "./library-model";
|
|
58
|
+
import { createLibrarySource, libraryColumns } from "./library-source";
|
|
59
|
+
import { createPreviewCache, createPreviewObserver, previewFor } from "./library-preview";
|
|
60
|
+
import { computeWindow } from "../ui/virtual-window";
|
|
61
|
+
import { paneRegion } from "../ui/regions";
|
|
62
|
+
import {
|
|
63
|
+
LAYOUT_METRICS,
|
|
64
|
+
boardTpl,
|
|
65
|
+
calendarTpl,
|
|
66
|
+
cardsTpl,
|
|
67
|
+
columnsAt,
|
|
68
|
+
mediaTpl,
|
|
69
|
+
tableHeadTpl,
|
|
70
|
+
tableRowsTpl,
|
|
71
|
+
} from "./library-layouts";
|
|
72
|
+
import type { ActivityHandle } from "../panels/activity-panel";
|
|
73
|
+
import type { EffectScope } from "@vue/reactivity";
|
|
74
|
+
import type { LibraryFile, LibraryLayout } from "./library-model";
|
|
75
|
+
import type { LibrarySource } from "./library-source";
|
|
76
|
+
import type { LayoutContext } from "./library-layouts";
|
|
77
|
+
import type { PreviewCache, PreviewObserver } from "./library-preview";
|
|
78
|
+
import type { WindowRange } from "../ui/virtual-window";
|
|
79
|
+
import type { Tab } from "../tabs/tab";
|
|
80
|
+
import type { CanvasSurface } from "../canvas/canvas-surface";
|
|
81
|
+
|
|
82
|
+
// ─── View state ──────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export interface LibraryViewState {
|
|
85
|
+
category: string;
|
|
86
|
+
layout: LibraryLayout;
|
|
87
|
+
query: string;
|
|
88
|
+
/**
|
|
89
|
+
* The language facet: a canonical tag, or "" for every language.
|
|
90
|
+
*
|
|
91
|
+
* A tag rather than a label, because two locales can display the same autonym and only the tag
|
|
92
|
+
* matches what {@link LibraryFile.locale} carries.
|
|
93
|
+
*/
|
|
94
|
+
locale: string;
|
|
95
|
+
/** Bumped to force a repaint from a non-reactive source (a scroll, a finished scan). */
|
|
96
|
+
revision: number;
|
|
97
|
+
/** A scan is running. Distinct from "scanned and empty", which is what the old view conflated. */
|
|
98
|
+
loading: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The Library's view state, reactive and module-scoped.
|
|
103
|
+
*
|
|
104
|
+
* Module-scoped rather than per-tab because there is one Library per window (its tab id carries no
|
|
105
|
+
* category — see `grid-source.ts`), and because the commands that write it must be able to do so
|
|
106
|
+
* whether or not the pane is currently mounted: `library.setCategory` run from the palette while
|
|
107
|
+
* another tab is focused sets the state the Library opens into.
|
|
108
|
+
*/
|
|
109
|
+
export const libraryView: LibraryViewState = reactive({
|
|
110
|
+
category: "all",
|
|
111
|
+
layout: "cards",
|
|
112
|
+
loading: false,
|
|
113
|
+
locale: "",
|
|
114
|
+
query: "",
|
|
115
|
+
revision: 0,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/** The window's Library source. Built lazily; discarded by {@link invalidateLibrary}. */
|
|
119
|
+
let source: LibrarySource | null = null;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Whether a scan has been STARTED for the current source.
|
|
123
|
+
*
|
|
124
|
+
* Distinct from `source.scanned()`, which only answers whether one SUCCEEDED. The pane's render
|
|
125
|
+
* effect kicks the first scan off, so without this a scan that failed would be retried on the very
|
|
126
|
+
* repaint its own failure caused — an infinite loop, and the reason a Retry has to be a command the
|
|
127
|
+
* reader presses rather than something the surface does on its own behalf.
|
|
128
|
+
*/
|
|
129
|
+
let scanAttempted = false;
|
|
130
|
+
|
|
131
|
+
/** The source, creating it on first use. */
|
|
132
|
+
export function librarySource(): LibrarySource {
|
|
133
|
+
source ??= createLibrarySource();
|
|
134
|
+
return source;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Repaint whatever is mounted. */
|
|
138
|
+
function bump() {
|
|
139
|
+
libraryView.revision += 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function setLibraryCategory(key: string): void {
|
|
143
|
+
libraryView.category = key;
|
|
144
|
+
bump();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function setLibraryLayout(layout: LibraryLayout): void {
|
|
148
|
+
libraryView.layout = layout;
|
|
149
|
+
bump();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function setLibrarySearch(query: string): void {
|
|
153
|
+
libraryView.query = query;
|
|
154
|
+
bump();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Show one language, or `""` for all of them. A setter, so "" is a value and not an absence. */
|
|
158
|
+
export function setLibraryLocale(locale: string): void {
|
|
159
|
+
libraryView.locale = locale;
|
|
160
|
+
bump();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Drop the scan and the rendered previews.
|
|
165
|
+
*
|
|
166
|
+
* Called by every surface that changes the project's file set — an upload, a creation, a rename, a
|
|
167
|
+
* delete — so the Library is never showing a file that is gone.
|
|
168
|
+
*/
|
|
169
|
+
export function invalidateLibrary(): void {
|
|
170
|
+
source = null;
|
|
171
|
+
scanAttempted = false;
|
|
172
|
+
for (const panel of _active.values()) {
|
|
173
|
+
panel.cache.clear();
|
|
174
|
+
panel.slots.clear();
|
|
175
|
+
}
|
|
176
|
+
bump();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Loading ─────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/** A scan slower than this earns an Activity row; a fast one must not litter the dock. */
|
|
182
|
+
export const SCAN_ACTIVITY_DELAY_MS = 600;
|
|
183
|
+
|
|
184
|
+
/** Deduplication key for the scan Problem, so a failing watcher is one row and not sixty. */
|
|
185
|
+
const SCAN_PROBLEM_KEY = "library.scan";
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Run the scan and report it honestly.
|
|
189
|
+
*
|
|
190
|
+
* No Cancel: `platform.listDirectory` has no abort, so a Cancel button here could only stop the
|
|
191
|
+
* WAITING, not the work — and a button that does not do what it says is the failure the Activity
|
|
192
|
+
* contract exists to end. The row is informational, and it only appears when the scan is slow
|
|
193
|
+
* enough that its absence would look like a hang.
|
|
194
|
+
*/
|
|
195
|
+
async function loadLibrary(): Promise<void> {
|
|
196
|
+
const current = librarySource();
|
|
197
|
+
scanAttempted = true;
|
|
198
|
+
libraryView.loading = true;
|
|
199
|
+
bump();
|
|
200
|
+
|
|
201
|
+
const slow: { handle: ActivityHandle | null } = { handle: null };
|
|
202
|
+
const timer = setTimeout(() => {
|
|
203
|
+
slow.handle = beginActivity({
|
|
204
|
+
source: "Library",
|
|
205
|
+
status: "Reading the project's directories…",
|
|
206
|
+
title: "Scan project files",
|
|
207
|
+
});
|
|
208
|
+
}, SCAN_ACTIVITY_DELAY_MS);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
await current.rows();
|
|
212
|
+
const failures = current.failures();
|
|
213
|
+
if (failures.length > 0) {
|
|
214
|
+
const detail = failures.map((f) => `${f.dir} — ${f.error}`).join("\n");
|
|
215
|
+
const summary = `Could not read ${failures.length} project ${
|
|
216
|
+
failures.length === 1 ? "directory" : "directories"
|
|
217
|
+
} — this list is incomplete.`;
|
|
218
|
+
// A Problem, not a toast: the list on screen is wrong until this is fixed, and the fix is a
|
|
219
|
+
// Command the row can run.
|
|
220
|
+
notify.error(summary, {
|
|
221
|
+
action: "library.refresh",
|
|
222
|
+
detail,
|
|
223
|
+
key: SCAN_PROBLEM_KEY,
|
|
224
|
+
path: failures[0]!.dir,
|
|
225
|
+
source: "Library",
|
|
226
|
+
tier: "problem",
|
|
227
|
+
});
|
|
228
|
+
slow.handle?.setStatus(summary);
|
|
229
|
+
}
|
|
230
|
+
slow.handle?.done(`${current.files().length} file(s)`);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
// `scanLibrary` does not reject, so reaching here means the platform itself is gone.
|
|
233
|
+
const message = "Could not scan the project's files.";
|
|
234
|
+
if (slow.handle) {
|
|
235
|
+
slow.handle.fail(message);
|
|
236
|
+
} else {
|
|
237
|
+
notify.error(message, {
|
|
238
|
+
action: "library.refresh",
|
|
239
|
+
detail: errorMessage(error),
|
|
240
|
+
key: SCAN_PROBLEM_KEY,
|
|
241
|
+
source: "Library",
|
|
242
|
+
tier: "problem",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
} finally {
|
|
246
|
+
clearTimeout(timer);
|
|
247
|
+
libraryView.loading = false;
|
|
248
|
+
bump();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Re-scan from scratch — the Retry behind the Problem, and the toolbar's Refresh. */
|
|
253
|
+
export async function refreshLibrary(): Promise<void> {
|
|
254
|
+
source = null;
|
|
255
|
+
scanAttempted = false;
|
|
256
|
+
for (const panel of _active.values()) {
|
|
257
|
+
panel.cache.clear();
|
|
258
|
+
}
|
|
259
|
+
await loadLibrary();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── The mounted pane ────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
interface ActiveLibraryPane {
|
|
265
|
+
/** The pane whose stage this Library is drawn on. The map key, held on the record too. */
|
|
266
|
+
paneId: string;
|
|
267
|
+
tabId: string;
|
|
268
|
+
scope: EffectScope;
|
|
269
|
+
wrap: HTMLElement;
|
|
270
|
+
cache: PreviewCache;
|
|
271
|
+
observer: PreviewObserver;
|
|
272
|
+
/** The scroller, once lit has created it. */
|
|
273
|
+
scroller: HTMLElement | null;
|
|
274
|
+
/** Slot elements awaiting a preview, keyed by the element itself. */
|
|
275
|
+
pending: WeakMap<Element, LibraryFile>;
|
|
276
|
+
/** The live preview slot for each path in the CURRENT window, so a resolved render finds it. */
|
|
277
|
+
slots: Map<string, Element>;
|
|
278
|
+
/**
|
|
279
|
+
* Paths whose preview is being built right now.
|
|
280
|
+
*
|
|
281
|
+
* Without this a repaint mid-load asks for the same document again: lit hands a fresh `ref`
|
|
282
|
+
* closure on every render, so the slot is re-registered before the first read has resolved, and a
|
|
283
|
+
* scroll through a long list would read each document several times over.
|
|
284
|
+
*/
|
|
285
|
+
pendingPaths: Set<string>;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The Library mounted in each pane, keyed by pane id.
|
|
290
|
+
*
|
|
291
|
+
* ONE instance per pane, not per window. It was a module-level `let active`, which is a fact about
|
|
292
|
+
* the shell having had one stage — and the failure it produced the moment two are drawn is not
|
|
293
|
+
* cosmetic: pane B mounting a Library called `detachLibraryPane()`, destroying pane A's
|
|
294
|
+
* `IntersectionObserver`, preview cache and effect scope while pane A's DOM was still on screen.
|
|
295
|
+
* `canvas-render.ts`'s `resetCanvasView` calls the detach UNCONDITIONALLY on every empty pane, so
|
|
296
|
+
* it did not even take a second Library to do it.
|
|
297
|
+
*/
|
|
298
|
+
const _active = new Map<string, ActiveLibraryPane>();
|
|
299
|
+
|
|
300
|
+
/** The Library mounted in a pane, or null. */
|
|
301
|
+
function activeIn(paneId: string): ActiveLibraryPane | null {
|
|
302
|
+
return _active.get(paneId) ?? null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Whether the Library is live in this pane for this tab — the canvas-render fast-path guard. */
|
|
306
|
+
export function libraryPaneMounted(paneId: string, tab: Tab): boolean {
|
|
307
|
+
const panel = activeIn(paneId);
|
|
308
|
+
return panel !== null && panel.tabId === tab.id && panel.wrap.isConnected;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Tear one pane's Library down (mode change, tab switch, project close). Idempotent. */
|
|
312
|
+
export function detachLibraryPane(paneId: string): void {
|
|
313
|
+
const panel = _active.get(paneId);
|
|
314
|
+
if (!panel) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
panel.observer.destroy();
|
|
318
|
+
panel.cache.clear();
|
|
319
|
+
panel.slots.clear();
|
|
320
|
+
panel.pendingPaths.clear();
|
|
321
|
+
panel.scope.stop();
|
|
322
|
+
_active.delete(paneId);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ─── Geometry ────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
/** The window for the current layout, or the whole list for a layout that does not window. */
|
|
328
|
+
function windowFor(panel: ActiveLibraryPane, layout: LibraryLayout, count: number): WindowRange {
|
|
329
|
+
const metric = LAYOUT_METRICS[layout];
|
|
330
|
+
const { scroller } = panel;
|
|
331
|
+
if (!metric.windowed || !scroller) {
|
|
332
|
+
return { end: count, padBottom: 0, padTop: 0, start: 0, totalRows: count };
|
|
333
|
+
}
|
|
334
|
+
return computeWindow({
|
|
335
|
+
columns: columnsAt(layout, scroller.clientWidth),
|
|
336
|
+
count,
|
|
337
|
+
rowHeight: metric.rowHeight,
|
|
338
|
+
scrollTop: scroller.scrollTop,
|
|
339
|
+
viewportHeight: scroller.clientHeight,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ─── Previews ────────────────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
/** Build a preview and attach it to whichever slot is currently showing that path. */
|
|
346
|
+
async function fillPreview(panel: ActiveLibraryPane, file: LibraryFile) {
|
|
347
|
+
// Only ever entered once per path at a time: `mountPreview` is the sole caller and refuses to
|
|
348
|
+
// Observe a slot whose path is already in flight.
|
|
349
|
+
panel.pendingPaths.add(file.path);
|
|
350
|
+
try {
|
|
351
|
+
const rendered = await previewFor(file.path, panel.cache);
|
|
352
|
+
if (!rendered || activeIn(panel.paneId) !== panel) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const slot = panel.slots.get(file.path);
|
|
356
|
+
if (slot?.isConnected && !slot.firstElementChild) {
|
|
357
|
+
slot.append(rendered);
|
|
358
|
+
}
|
|
359
|
+
} finally {
|
|
360
|
+
panel.pendingPaths.delete(file.path);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** The `ref` callback every previewable card carries. */
|
|
365
|
+
function mountPreview(panel: ActiveLibraryPane, element: Element | undefined, file: LibraryFile) {
|
|
366
|
+
if (!element) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
panel.slots.set(file.path, element);
|
|
370
|
+
// Read the cache BEFORE the "already filled" shortcut, because the read is what marks the entry
|
|
371
|
+
// As recently used. Skipping it let a preview that was on screen — and therefore never touched —
|
|
372
|
+
// Drift to the tail of the LRU, get evicted, get detached, and be re-rendered on the next
|
|
373
|
+
// Repaint: the 300-page measurement showed 560 reads for 300 documents until this line moved.
|
|
374
|
+
const cached = panel.cache.get(file.path);
|
|
375
|
+
if (element.firstElementChild) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (cached) {
|
|
379
|
+
element.append(cached);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (panel.pendingPaths.has(file.path)) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
panel.pending.set(element, file);
|
|
386
|
+
panel.observer.observe(element);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ─── Context menu ────────────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
let contextHandle: ReturnType<typeof renderPopover> | null = null;
|
|
392
|
+
|
|
393
|
+
function dismissLibraryContextMenu() {
|
|
394
|
+
contextHandle?.dismiss();
|
|
395
|
+
contextHandle = null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
interface MenuItem {
|
|
399
|
+
label: string;
|
|
400
|
+
action?: () => void | Promise<void>;
|
|
401
|
+
danger?: boolean;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function showLibraryContextMenu(event: MouseEvent, file: LibraryFile) {
|
|
405
|
+
event.preventDefault();
|
|
406
|
+
event.stopPropagation();
|
|
407
|
+
dismissLibraryContextMenu();
|
|
408
|
+
|
|
409
|
+
const items: MenuItem[] = [
|
|
410
|
+
{
|
|
411
|
+
action: () => {
|
|
412
|
+
void openFileInTab(file.path);
|
|
413
|
+
},
|
|
414
|
+
label: "Open",
|
|
415
|
+
},
|
|
416
|
+
{ label: "—" },
|
|
417
|
+
{ action: () => renameLibraryFile(file), label: "Rename…" },
|
|
418
|
+
{ action: () => duplicateLibraryFile(file), label: "Duplicate" },
|
|
419
|
+
{ label: "—" },
|
|
420
|
+
{ action: () => deleteLibraryFile(file), danger: true, label: "Delete" },
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
let x = event.clientX;
|
|
424
|
+
let y = event.clientY;
|
|
425
|
+
|
|
426
|
+
contextHandle = renderPopover(
|
|
427
|
+
html`<sp-popover
|
|
428
|
+
open
|
|
429
|
+
style="position:fixed;left:${x}px;top:${y}px"
|
|
430
|
+
${ref((el) => {
|
|
431
|
+
if (!el) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
requestAnimationFrame(() => {
|
|
435
|
+
const popover = el as HTMLElement;
|
|
436
|
+
const menuRect = rectOf(popover);
|
|
437
|
+
if (x + menuRect.width > window.innerWidth) {
|
|
438
|
+
x = window.innerWidth - menuRect.width - 4;
|
|
439
|
+
}
|
|
440
|
+
if (y + menuRect.height > window.innerHeight) {
|
|
441
|
+
y = window.innerHeight - menuRect.height - 4;
|
|
442
|
+
}
|
|
443
|
+
popover.style.left = `${x}px`;
|
|
444
|
+
popover.style.top = `${y}px`;
|
|
445
|
+
});
|
|
446
|
+
})}
|
|
447
|
+
>
|
|
448
|
+
<sp-menu>
|
|
449
|
+
${items.map((item) =>
|
|
450
|
+
item.label === "—"
|
|
451
|
+
? html`<sp-menu-divider></sp-menu-divider>`
|
|
452
|
+
: html`<sp-menu-item
|
|
453
|
+
style=${item.danger ? "color: var(--danger)" : ""}
|
|
454
|
+
@click=${() => {
|
|
455
|
+
dismissLibraryContextMenu();
|
|
456
|
+
void item.action?.();
|
|
457
|
+
}}
|
|
458
|
+
>${item.label}</sp-menu-item
|
|
459
|
+
>`,
|
|
460
|
+
)}
|
|
461
|
+
</sp-menu>
|
|
462
|
+
</sp-popover>`,
|
|
463
|
+
{
|
|
464
|
+
dismissOnOutsideClick: true,
|
|
465
|
+
layer: "dialog",
|
|
466
|
+
onDismiss: () => {
|
|
467
|
+
contextHandle = null;
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* The Library's rename dialog — the same copy, from the same helper, as the Files panel's.
|
|
475
|
+
*
|
|
476
|
+
* A rename here rewrites references exactly as one in the sidebar does, and two dialogs that
|
|
477
|
+
* disagreed about that is the divergence the shared helper exists to prevent.
|
|
478
|
+
*/
|
|
479
|
+
async function renameLibraryFile(file: LibraryFile) {
|
|
480
|
+
const message = await renamePromptMessage(file.path);
|
|
481
|
+
const newName = await showPromptDialog("Rename", {
|
|
482
|
+
confirmLabel: "Rename",
|
|
483
|
+
...(message === undefined ? {} : { message }),
|
|
484
|
+
select: "stem",
|
|
485
|
+
validate: (value) => (value.trim() ? "" : "Enter a file name."),
|
|
486
|
+
value: file.name,
|
|
487
|
+
});
|
|
488
|
+
if (!newName || newName === file.name) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const normalized = file.path.replaceAll("\\", "/");
|
|
492
|
+
const parent = normalized.includes("/") ? normalized.slice(0, normalized.lastIndexOf("/")) : ".";
|
|
493
|
+
const newPath = parent === "." ? newName : `${parent}/${newName}`;
|
|
494
|
+
try {
|
|
495
|
+
await getPlatform().renameFile(file.path, newPath);
|
|
496
|
+
invalidateUsages();
|
|
497
|
+
invalidateLibrary();
|
|
498
|
+
await loadLibrary();
|
|
499
|
+
notify.success(`Renamed to ${newName}`);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
notify.error(`Could not rename ${file.name}.`, {
|
|
502
|
+
detail: errorMessage(error),
|
|
503
|
+
path: file.path,
|
|
504
|
+
source: "Library",
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function duplicateLibraryFile(file: LibraryFile) {
|
|
510
|
+
const normalized = file.path.replaceAll("\\", "/");
|
|
511
|
+
const parent = normalized.includes("/") ? normalized.slice(0, normalized.lastIndexOf("/")) : ".";
|
|
512
|
+
const stem = file.name.replace(/(\.[^.]+)$/, "");
|
|
513
|
+
const copyName = `${stem}-copy${file.ext || ""}`;
|
|
514
|
+
const copyPath = parent === "." ? copyName : `${parent}/${copyName}`;
|
|
515
|
+
try {
|
|
516
|
+
const platform = getPlatform();
|
|
517
|
+
const content = await platform.readFile(file.path);
|
|
518
|
+
await platform.writeFile(copyPath, content);
|
|
519
|
+
invalidateLibrary();
|
|
520
|
+
await loadLibrary();
|
|
521
|
+
notify.success(`Duplicated as ${copyName}`);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
notify.error(`Could not duplicate ${file.name}.`, {
|
|
524
|
+
detail: errorMessage(error),
|
|
525
|
+
path: file.path,
|
|
526
|
+
source: "Library",
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function deleteLibraryFile(file: LibraryFile) {
|
|
532
|
+
const confirmed = await confirmFileDelete(file);
|
|
533
|
+
if (!confirmed) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
await getPlatform().deleteFile(file.path);
|
|
538
|
+
invalidateUsages();
|
|
539
|
+
invalidateLibrary();
|
|
540
|
+
await loadLibrary();
|
|
541
|
+
notify.success(`Deleted ${file.name}`);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
notify.error(`Could not delete ${file.name}.`, {
|
|
544
|
+
detail: errorMessage(error),
|
|
545
|
+
path: file.path,
|
|
546
|
+
source: "Library",
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ─── Creation ────────────────────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
/** What the New menu offers: the three document kinds, plus one row per creatable collection. */
|
|
554
|
+
export interface LibraryNewEntry {
|
|
555
|
+
key: string;
|
|
556
|
+
label: string;
|
|
557
|
+
dir: string;
|
|
558
|
+
/**
|
|
559
|
+
* The content collection this row creates an entry IN, or undefined for a document kind.
|
|
560
|
+
*
|
|
561
|
+
* Present is the whole difference between the two creation paths below — a collection row is
|
|
562
|
+
* `content/`'s job, because only that module knows the collection's extension and its schema.
|
|
563
|
+
*/
|
|
564
|
+
collection?: string;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The new-entity rows.
|
|
569
|
+
*
|
|
570
|
+
* Collections come from `content/entry-model.ts` rather than from `project.json` directly, so the
|
|
571
|
+
* menu offers exactly the collections an entry can be created in. The predecessor read the raw
|
|
572
|
+
* `content` map and derived a directory from the type NAME when `source` was absent, which produced
|
|
573
|
+
* a row for a collection that has no entry directory at all and a row for a single-file (CSV)
|
|
574
|
+
* catalogue, whose entries are rows and not files. Both created a file nothing would ever load.
|
|
575
|
+
*/
|
|
576
|
+
export function libraryNewEntries(): LibraryNewEntry[] {
|
|
577
|
+
const rows: LibraryNewEntry[] = [
|
|
578
|
+
{ dir: "pages", key: "page", label: "Page" },
|
|
579
|
+
{ dir: "layouts", key: "layout", label: "Layout" },
|
|
580
|
+
{ dir: "components", key: "component", label: "Component" },
|
|
581
|
+
];
|
|
582
|
+
for (const collection of entryCollections()) {
|
|
583
|
+
rows.push({
|
|
584
|
+
collection: collection.name,
|
|
585
|
+
dir: collection.dir,
|
|
586
|
+
key: `collection:${collection.name}`,
|
|
587
|
+
label: collection.name.charAt(0).toUpperCase() + collection.name.slice(1),
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
return rows;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Create one entity and open it.
|
|
595
|
+
*
|
|
596
|
+
* Two paths, because a collection entry is not a blank document. A **collection** row goes through
|
|
597
|
+
* `content/entry-commands.ts`'s `createEntry`, which supplies the collection's extension (so the
|
|
598
|
+
* field asks for a display name and the file is actually matched by the collection it was created
|
|
599
|
+
* in) and a body seeded from the collection's **schema defaults** — the entry is valid the moment
|
|
600
|
+
* it exists, rather than a pile of absent required fields — and then opens it in the entry FORM,
|
|
601
|
+
* which is the editor that collection's fields belong to. Both of those are the reason
|
|
602
|
+
* `createEntry` is called rather than reimplemented; a second seeder here would drift from the one
|
|
603
|
+
* the palette's `content.newEntry` uses within a release.
|
|
604
|
+
*
|
|
605
|
+
* A **document kind** keeps `files.ts`'s verbatim name field, deliberately: a page may be a `.md`
|
|
606
|
+
* as easily as a `.json`, so the extension the author types is a real choice and forcing one here
|
|
607
|
+
* would remove it. A collection's extension is not a choice — it is the collection's.
|
|
608
|
+
*/
|
|
609
|
+
export async function createLibraryEntry(key: string): Promise<string | null> {
|
|
610
|
+
const entry = libraryNewEntries().find((row) => row.key === key);
|
|
611
|
+
if (!entry) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
const created =
|
|
615
|
+
entry.collection === undefined
|
|
616
|
+
? await createFileIn({
|
|
617
|
+
dir: entry.dir,
|
|
618
|
+
source: "Library",
|
|
619
|
+
suggestedName: "untitled",
|
|
620
|
+
title: `New ${entry.label}`,
|
|
621
|
+
})
|
|
622
|
+
: await createEntry(entry.collection);
|
|
623
|
+
if (created === null) {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
invalidateLibrary();
|
|
627
|
+
await loadLibrary();
|
|
628
|
+
// `createEntry` has already opened the entry form; opening the file again here would replace that
|
|
629
|
+
// Editor with the generic one for the same tab.
|
|
630
|
+
if (entry.collection === undefined) {
|
|
631
|
+
void openFileInTab(created);
|
|
632
|
+
}
|
|
633
|
+
return created;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ─── Upload ──────────────────────────────────────────────────────────────────
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Where an upload lands, and how the author was told.
|
|
640
|
+
*
|
|
641
|
+
* The active category's directory when it has one — printed on the drop zone, so the destination is
|
|
642
|
+
* visible BEFORE the drop. "All" has none, and rather than falling back to a default nobody chose,
|
|
643
|
+
* it asks. That fallback is exactly the surprise §7 names: the file arrives, the toast says it
|
|
644
|
+
* worked, and it is in a directory the author never picked.
|
|
645
|
+
*/
|
|
646
|
+
export async function resolveUploadDir(): Promise<string | null> {
|
|
647
|
+
const named = uploadDirForCategory(libraryView.category);
|
|
648
|
+
if (named !== undefined) {
|
|
649
|
+
return named;
|
|
650
|
+
}
|
|
651
|
+
const chosen = await showPromptDialog("Upload files", {
|
|
652
|
+
confirmLabel: "Upload",
|
|
653
|
+
message: "The All view has no folder of its own. Choose where these files land.",
|
|
654
|
+
placeholder: "public",
|
|
655
|
+
validate: (value) => (value.trim() ? "" : "Enter a folder."),
|
|
656
|
+
value: "public",
|
|
657
|
+
});
|
|
658
|
+
return chosen === null ? null : chosen.trim().replace(/\/$/, "");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async function uploadIntoLibrary(files: FileList | File[]) {
|
|
662
|
+
const dir = await resolveUploadDir();
|
|
663
|
+
if (dir === null) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
await uploadAssets([...files], { dir });
|
|
667
|
+
invalidateLibrary();
|
|
668
|
+
await loadLibrary();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ─── Templates ───────────────────────────────────────────────────────────────
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* The language facet, drawn only where it can change the answer.
|
|
675
|
+
*
|
|
676
|
+
* Its options come from ALL scanned files, never from the filtered ones: a picker whose choices
|
|
677
|
+
* collapse to the choice just made cannot be used to make another. One locale — or none — means
|
|
678
|
+
* every file already agrees, and a permanently-selected chip is chrome that says nothing.
|
|
679
|
+
*/
|
|
680
|
+
function localeFilterTpl(files: readonly LibraryFile[]) {
|
|
681
|
+
const locales = libraryLocales(files);
|
|
682
|
+
if (locales.length < 2) {
|
|
683
|
+
return nothing;
|
|
684
|
+
}
|
|
685
|
+
const value = libraryView.locale === "" ? "all" : libraryView.locale;
|
|
686
|
+
return html`
|
|
687
|
+
<sp-picker
|
|
688
|
+
size="s"
|
|
689
|
+
quiet
|
|
690
|
+
class="library-locale-filter"
|
|
691
|
+
label="All languages"
|
|
692
|
+
.value=${value}
|
|
693
|
+
@change=${(e: Event) => {
|
|
694
|
+
const chosen = (e.target as HTMLElement & { value: string }).value;
|
|
695
|
+
setLibraryLocale(chosen === "all" ? "" : chosen);
|
|
696
|
+
}}
|
|
697
|
+
>
|
|
698
|
+
<sp-menu-item value="all">All languages</sp-menu-item>
|
|
699
|
+
${locales.map((tag) => html`<sp-menu-item value=${tag}>${localeLabel(tag)}</sp-menu-item>`)}
|
|
700
|
+
</sp-picker>
|
|
701
|
+
`;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function toolbarTpl(panel: ActiveLibraryPane, files: readonly LibraryFile[]) {
|
|
705
|
+
const destination = uploadDirForCategory(libraryView.category);
|
|
706
|
+
return html`
|
|
707
|
+
<div class="library-toolbar">
|
|
708
|
+
<sp-action-group selects="single" size="s" compact>
|
|
709
|
+
${LIBRARY_CATEGORIES.map(
|
|
710
|
+
(category) => html`
|
|
711
|
+
<sp-action-button
|
|
712
|
+
size="s"
|
|
713
|
+
?selected=${libraryView.category === category.key}
|
|
714
|
+
@click=${() => setLibraryCategory(category.key)}
|
|
715
|
+
>
|
|
716
|
+
${category.label}
|
|
717
|
+
</sp-action-button>
|
|
718
|
+
`,
|
|
719
|
+
)}
|
|
720
|
+
</sp-action-group>
|
|
721
|
+
${localeFilterTpl(files)}
|
|
722
|
+
<sp-search
|
|
723
|
+
size="s"
|
|
724
|
+
placeholder="Filter files…"
|
|
725
|
+
.value=${libraryView.query}
|
|
726
|
+
@input=${(e: Event) => setLibrarySearch((e.target as HTMLInputElement).value)}
|
|
727
|
+
@submit=${(e: Event) => e.preventDefault()}
|
|
728
|
+
></sp-search>
|
|
729
|
+
<overlay-trigger placement="bottom-start" triggered-by="click">
|
|
730
|
+
<sp-action-button size="s" slot="trigger">
|
|
731
|
+
<sp-icon-add slot="icon"></sp-icon-add> New
|
|
732
|
+
</sp-action-button>
|
|
733
|
+
<sp-popover slot="click-content" tip>
|
|
734
|
+
<sp-menu
|
|
735
|
+
@change=${(e: Event) => {
|
|
736
|
+
void createLibraryEntry((e.target as HTMLSelectElement).value);
|
|
737
|
+
}}
|
|
738
|
+
>
|
|
739
|
+
${libraryNewEntries().map(
|
|
740
|
+
(entry) =>
|
|
741
|
+
html`<sp-menu-item value=${entry.key}
|
|
742
|
+
>${entry.label} — ${entry.dir}/</sp-menu-item
|
|
743
|
+
>`,
|
|
744
|
+
)}
|
|
745
|
+
</sp-menu>
|
|
746
|
+
</sp-popover>
|
|
747
|
+
</overlay-trigger>
|
|
748
|
+
<sp-action-button
|
|
749
|
+
size="s"
|
|
750
|
+
title=${destination === undefined ? "Upload — asks for a folder" : `Upload into ${destination}/`}
|
|
751
|
+
@click=${() => {
|
|
752
|
+
const input = panel.wrap.querySelector(
|
|
753
|
+
".library-upload-input",
|
|
754
|
+
) as HTMLInputElement | null;
|
|
755
|
+
input?.click();
|
|
756
|
+
}}
|
|
757
|
+
>
|
|
758
|
+
<sp-icon-upload slot="icon"></sp-icon-upload> Upload
|
|
759
|
+
</sp-action-button>
|
|
760
|
+
<input
|
|
761
|
+
type="file"
|
|
762
|
+
multiple
|
|
763
|
+
accept=${UPLOAD_ACCEPT}
|
|
764
|
+
class="library-upload-input"
|
|
765
|
+
@change=${(e: Event) => {
|
|
766
|
+
const input = e.target as HTMLInputElement;
|
|
767
|
+
if (input.files?.length) {
|
|
768
|
+
void uploadIntoLibrary(input.files);
|
|
769
|
+
}
|
|
770
|
+
input.value = "";
|
|
771
|
+
}}
|
|
772
|
+
/>
|
|
773
|
+
<span class="library-spacer"></span>
|
|
774
|
+
<sp-action-group selects="single" size="s" compact class="library-layout-switch">
|
|
775
|
+
${LIBRARY_LAYOUTS.map(
|
|
776
|
+
(layout) => html`
|
|
777
|
+
<sp-action-button
|
|
778
|
+
size="s"
|
|
779
|
+
?selected=${libraryView.layout === layout}
|
|
780
|
+
title=${LIBRARY_LAYOUT_LABELS[layout]}
|
|
781
|
+
@click=${() => setLibraryLayout(layout)}
|
|
782
|
+
>
|
|
783
|
+
${LIBRARY_LAYOUT_LABELS[layout]}
|
|
784
|
+
</sp-action-button>
|
|
785
|
+
`,
|
|
786
|
+
)}
|
|
787
|
+
</sp-action-group>
|
|
788
|
+
</div>
|
|
789
|
+
`;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* The banner an INCOMPLETE listing carries.
|
|
794
|
+
*
|
|
795
|
+
* The Problem is the durable record; this is the same fact on the surface it is about, because a
|
|
796
|
+
* reader looking at a short list needs to know it is short for a reason without going to the dock.
|
|
797
|
+
*/
|
|
798
|
+
function failureBannerTpl(current: LibrarySource) {
|
|
799
|
+
const failures = current.failures();
|
|
800
|
+
if (failures.length === 0) {
|
|
801
|
+
return nothing;
|
|
802
|
+
}
|
|
803
|
+
return html`<div class="library-banner" role="status">
|
|
804
|
+
<span>
|
|
805
|
+
This list is incomplete — ${failures.length}
|
|
806
|
+
${failures.length === 1 ? "directory" : "directories"} could not be read
|
|
807
|
+
(${failures[0]!.dir}${failures.length > 1 ? ", …" : ""}).
|
|
808
|
+
</span>
|
|
809
|
+
<sp-button
|
|
810
|
+
size="s"
|
|
811
|
+
variant="secondary"
|
|
812
|
+
@click=${() => {
|
|
813
|
+
void refreshLibrary();
|
|
814
|
+
}}
|
|
815
|
+
>Retry</sp-button
|
|
816
|
+
>
|
|
817
|
+
</div>`;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* The empty state, which is FOUR states and not one.
|
|
822
|
+
*
|
|
823
|
+
* The view this replaces printed "No files found" for every one of them, including the one where
|
|
824
|
+
* the request had failed.
|
|
825
|
+
*/
|
|
826
|
+
function emptyTpl(current: LibrarySource, total: number) {
|
|
827
|
+
if (libraryView.loading || !current.scanned()) {
|
|
828
|
+
return html`<div class="library-empty">Scanning the project…</div>`;
|
|
829
|
+
}
|
|
830
|
+
if (current.failures().length > 0) {
|
|
831
|
+
return html`<div class="library-empty">
|
|
832
|
+
<p>Nothing to show, and the scan did not finish.</p>
|
|
833
|
+
<p class="library-empty-detail">
|
|
834
|
+
${current.failures().length}
|
|
835
|
+
${current.failures().length === 1 ? "directory" : "directories"} could not be read, so this
|
|
836
|
+
is not the same as an empty project.
|
|
837
|
+
</p>
|
|
838
|
+
<sp-button
|
|
839
|
+
size="s"
|
|
840
|
+
variant="accent"
|
|
841
|
+
@click=${() => {
|
|
842
|
+
void refreshLibrary();
|
|
843
|
+
}}
|
|
844
|
+
>Retry</sp-button
|
|
845
|
+
>
|
|
846
|
+
</div>`;
|
|
847
|
+
}
|
|
848
|
+
if (total === 0) {
|
|
849
|
+
return html`<div class="library-empty">
|
|
850
|
+
<p>This project has no files yet.</p>
|
|
851
|
+
<p class="library-empty-detail">Use New to create a page, layout, component or entry.</p>
|
|
852
|
+
</div>`;
|
|
853
|
+
}
|
|
854
|
+
const category = libraryCategory(libraryView.category);
|
|
855
|
+
// Both facets in one clause, so a reader who filtered twice is told about both rather than being
|
|
856
|
+
// Sent to clear one and find the list still empty.
|
|
857
|
+
const scopes = [
|
|
858
|
+
category && category.key !== "all" ? category.label : "",
|
|
859
|
+
libraryView.locale === "" ? "" : localeLabel(libraryView.locale),
|
|
860
|
+
].filter((scope) => scope !== "");
|
|
861
|
+
const where = scopes.length === 0 ? "" : ` in ${scopes.join(" and ")}`;
|
|
862
|
+
const term = libraryView.query.trim();
|
|
863
|
+
return html`<div class="library-empty">
|
|
864
|
+
<p>
|
|
865
|
+
No files match${term ? html` “${term}”` : nothing}${where}.
|
|
866
|
+
<span class="library-empty-detail">${total} file(s) in the project.</span>
|
|
867
|
+
</p>
|
|
868
|
+
<sp-button
|
|
869
|
+
size="s"
|
|
870
|
+
variant="secondary"
|
|
871
|
+
@click=${() => {
|
|
872
|
+
setLibrarySearch("");
|
|
873
|
+
setLibraryCategory("all");
|
|
874
|
+
setLibraryLocale("");
|
|
875
|
+
}}
|
|
876
|
+
>
|
|
877
|
+
Clear filters
|
|
878
|
+
</sp-button>
|
|
879
|
+
</div>`;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** The body for one layout, windowed where the layout windows. */
|
|
883
|
+
function bodyTpl(panel: ActiveLibraryPane, files: readonly LibraryFile[], ctx: LayoutContext) {
|
|
884
|
+
const { layout } = libraryView;
|
|
885
|
+
const range = windowFor(panel, layout, files.length);
|
|
886
|
+
const slice = files.slice(range.start, range.end);
|
|
887
|
+
|
|
888
|
+
switch (layout) {
|
|
889
|
+
case "board": {
|
|
890
|
+
return boardTpl(files, ctx);
|
|
891
|
+
}
|
|
892
|
+
case "calendar": {
|
|
893
|
+
return calendarTpl(files, ctx);
|
|
894
|
+
}
|
|
895
|
+
case "media": {
|
|
896
|
+
return html`<div
|
|
897
|
+
class="library-grid library-grid-media"
|
|
898
|
+
style="padding-top:${range.padTop}px;padding-bottom:${range.padBottom}px"
|
|
899
|
+
>
|
|
900
|
+
${mediaTpl(slice, ctx)}
|
|
901
|
+
</div>`;
|
|
902
|
+
}
|
|
903
|
+
case "table": {
|
|
904
|
+
return html`<div class="library-table" role="table">
|
|
905
|
+
${tableHeadTpl(ctx.columns)}
|
|
906
|
+
<div style="height:${range.padTop}px"></div>
|
|
907
|
+
${tableRowsTpl(slice, ctx)}
|
|
908
|
+
<div style="height:${range.padBottom}px"></div>
|
|
909
|
+
</div>`;
|
|
910
|
+
}
|
|
911
|
+
default: {
|
|
912
|
+
return html`<div
|
|
913
|
+
class="library-grid library-grid-cards"
|
|
914
|
+
style="padding-top:${range.padTop}px;padding-bottom:${range.padBottom}px"
|
|
915
|
+
>
|
|
916
|
+
${cardsTpl(slice, ctx)}
|
|
917
|
+
</div>`;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// ─── Mount ───────────────────────────────────────────────────────────────────
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Draw the Library into the pane. Re-entrant: a same-tab call while it is live is a no-op, because
|
|
926
|
+
* the pane owns its own reactivity from here (the grid/settings/stylebook pattern).
|
|
927
|
+
*/
|
|
928
|
+
export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
|
|
929
|
+
const { paneId, wrap: canvasWrap } = surface;
|
|
930
|
+
if (libraryPaneMounted(paneId, tab)) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
detachLibraryPane(paneId);
|
|
934
|
+
|
|
935
|
+
const scope = effectScope();
|
|
936
|
+
const panel: ActiveLibraryPane = {
|
|
937
|
+
cache: createPreviewCache(),
|
|
938
|
+
observer: createPreviewObserver(() => {
|
|
939
|
+
/* Replaced immediately below — the observer needs the panel to exist first. */
|
|
940
|
+
}),
|
|
941
|
+
paneId,
|
|
942
|
+
pending: new WeakMap(),
|
|
943
|
+
pendingPaths: new Set(),
|
|
944
|
+
scope,
|
|
945
|
+
scroller: null,
|
|
946
|
+
slots: new Map(),
|
|
947
|
+
tabId: tab.id,
|
|
948
|
+
wrap: canvasWrap,
|
|
949
|
+
};
|
|
950
|
+
panel.observer.destroy();
|
|
951
|
+
panel.observer = createPreviewObserver((element) => {
|
|
952
|
+
const file = panel.pending.get(element);
|
|
953
|
+
if (file) {
|
|
954
|
+
panel.pending.delete(element);
|
|
955
|
+
void fillPreview(panel, file);
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
_active.set(paneId, panel);
|
|
959
|
+
|
|
960
|
+
const ctx: LayoutContext = {
|
|
961
|
+
columns: libraryColumns(),
|
|
962
|
+
contextMenu: (event, file) => showLibraryContextMenu(event, file),
|
|
963
|
+
mountPreview: (element, file) => {
|
|
964
|
+
if (PREVIEW_LAYOUTS.has(libraryView.layout)) {
|
|
965
|
+
mountPreview(panel, element, file);
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
openFile: (path) => {
|
|
969
|
+
void openFileInTab(path);
|
|
970
|
+
},
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
const onScroller = (element: Element | undefined) => {
|
|
974
|
+
const next = (element as HTMLElement | undefined) ?? null;
|
|
975
|
+
if (next === panel.scroller) {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
panel.scroller = next;
|
|
979
|
+
next?.addEventListener("scroll", () => bump(), { passive: true });
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
scope.run(() => {
|
|
983
|
+
effect(() => {
|
|
984
|
+
if (activeIn(paneId) !== panel) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
// Everything the pane draws from.
|
|
988
|
+
void libraryView.revision;
|
|
989
|
+
void libraryView.category;
|
|
990
|
+
void libraryView.layout;
|
|
991
|
+
void libraryView.query;
|
|
992
|
+
void libraryView.locale;
|
|
993
|
+
void libraryView.loading;
|
|
994
|
+
|
|
995
|
+
const current = librarySource();
|
|
996
|
+
const files = filterLibrary(current.files(), {
|
|
997
|
+
category: libraryView.category,
|
|
998
|
+
locale: libraryView.locale,
|
|
999
|
+
query: libraryView.query,
|
|
1000
|
+
});
|
|
1001
|
+
const total = current.files().length;
|
|
1002
|
+
|
|
1003
|
+
// The slot table describes the CURRENT window only; the `ref` callbacks below refill it as
|
|
1004
|
+
// Lit commits, and a stale entry would hand a resolved preview to a detached node.
|
|
1005
|
+
panel.slots.clear();
|
|
1006
|
+
|
|
1007
|
+
litRender(
|
|
1008
|
+
html`
|
|
1009
|
+
<div class="library" data-jx-region=${paneRegion(paneId, "library")}>
|
|
1010
|
+
${toolbarTpl(panel, current.files())} ${failureBannerTpl(current)}
|
|
1011
|
+
<div
|
|
1012
|
+
class="library-body"
|
|
1013
|
+
data-jx-region=${paneRegion(paneId, "library/dropZone")}
|
|
1014
|
+
${ref(onScroller)}
|
|
1015
|
+
@dragover=${(e: DragEvent) => {
|
|
1016
|
+
e.preventDefault();
|
|
1017
|
+
(e.currentTarget as HTMLElement).classList.add("library-drop-active");
|
|
1018
|
+
}}
|
|
1019
|
+
@dragleave=${(e: DragEvent) => {
|
|
1020
|
+
(e.currentTarget as HTMLElement).classList.remove("library-drop-active");
|
|
1021
|
+
}}
|
|
1022
|
+
@drop=${(e: DragEvent) => {
|
|
1023
|
+
e.preventDefault();
|
|
1024
|
+
(e.currentTarget as HTMLElement).classList.remove("library-drop-active");
|
|
1025
|
+
const dropped = e.dataTransfer?.files;
|
|
1026
|
+
if (dropped?.length) {
|
|
1027
|
+
void uploadIntoLibrary(dropped);
|
|
1028
|
+
}
|
|
1029
|
+
}}
|
|
1030
|
+
>
|
|
1031
|
+
${files.length === 0 ? emptyTpl(current, total) : bodyTpl(panel, files, ctx)}
|
|
1032
|
+
</div>
|
|
1033
|
+
</div>
|
|
1034
|
+
`,
|
|
1035
|
+
canvasWrap,
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
// Lit has committed, so every card outside the new window is now detached. Hand those
|
|
1039
|
+
// Observations back HERE and nowhere else: this is the only moment the pane knows the window
|
|
1040
|
+
// Has moved, and the intersect callback cannot be the only release path — a card flicked past
|
|
1041
|
+
// Before it ever intersected is never reported, so it would be watched until the tab closed.
|
|
1042
|
+
panel.observer.releaseDetached();
|
|
1043
|
+
|
|
1044
|
+
if (!scanAttempted && !libraryView.loading) {
|
|
1045
|
+
void loadLibrary();
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
1049
|
+
}
|