@jxsuite/studio 2.0.0 → 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-0hzentwq.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-0hzentwq.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-2c188n41.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-97qcgvdn.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-97qcgvdn.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-hhj92gcr.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-ygzav7z5.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-n9zmpxjg.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 +23711 -35185
- 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 +34 -9
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +270 -262
- 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 -120
- 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 +75 -2
- 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 +113 -0
- 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-qct0gt8f.js +0 -531
- package/dist/chunks/extension-sections-qct0gt8f.js.map +0 -12
- package/dist/chunks/files-02zn5y24.js +0 -50
- package/dist/chunks/files-02zn5y24.js.map +0 -9
- package/dist/chunks/git-panel-mbb0y0hz.js +0 -28
- package/dist/chunks/git-panel-mbb0y0hz.js.map +0 -9
- package/dist/chunks/layers-panel-9qekmqk1.js +0 -26
- package/dist/chunks/layers-panel-9qekmqk1.js.map +0 -9
- package/dist/chunks/settings-modal-gj0hrah4.js +0 -29
- package/dist/chunks/settings-modal-gj0hrah4.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-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-568nx4t5.js +0 -469
- package/dist/chunks/studio-568nx4t5.js.map +0 -14
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-asvf0j9e.js +0 -429
- package/dist/chunks/studio-asvf0j9e.js.map +0 -13
- package/dist/chunks/studio-d77qm3pk.js +0 -3427
- package/dist/chunks/studio-d77qm3pk.js.map +0 -26
- package/dist/chunks/studio-j6gqyjf4.js +0 -1854
- package/dist/chunks/studio-j6gqyjf4.js.map +0 -18
- package/dist/chunks/studio-n9zmpxjg.js.map +0 -115
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-rq0vxb19.js +0 -3744
- package/dist/chunks/studio-rq0vxb19.js.map +0 -22
- package/dist/chunks/studio-wd6ewdvd.js +0 -853
- package/dist/chunks/studio-wd6ewdvd.js.map +0 -12
- 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-ygzav7z5.js.map +0 -46
- package/dist/chunks/studio-zmy7241e.js +0 -399
- package/dist/chunks/studio-zmy7241e.js.map +0 -12
- 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-2c188n41.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-hhj92gcr.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
package/src/editor/shortcuts.ts
CHANGED
|
@@ -1,55 +1,983 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* Shortcuts.
|
|
3
|
+
* Shortcuts.ts — the pointer surface of the canvas, and the keyboard's one dispatcher.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Two unrelated jobs share this file because they share the canvas element:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Wheel, middle-mouse pan and resize.** Unchanged: these are gestures over `canvasWrap`, not
|
|
8
|
+
* commands, and there is no chord to register them under.
|
|
9
|
+
* 2. **Keyboard.** Formerly a 403-line `keydown` switch — twelve modifier chords and seven bare keys
|
|
10
|
+
* hand-matched against the canvas mode, with three blanket guards in front. It is now a
|
|
11
|
+
* dispatcher of about thirty lines: build the scope stack from the command context, hand the
|
|
12
|
+
* event to the registry, and `preventDefault()` iff a command claimed it.
|
|
13
|
+
*
|
|
14
|
+
* The three guards did not move, they were replaced by mechanisms that generalise:
|
|
15
|
+
*
|
|
16
|
+
* | Old guard | Now |
|
|
17
|
+
* | ------------------------------------------------------ | --------------------------------------- |
|
|
18
|
+
* | `if (isModalOpen()) return` | `modal.open` → the `palette`-only stack |
|
|
19
|
+
* | `canvasMode === "grid" && !["o","p","s","w","z","Z"]…` | the `grid` stack (see `keyScopeStack`) |
|
|
20
|
+
* | the caret / text-input early return | the `caret` stack, which drops `canvas` |
|
|
21
|
+
*
|
|
22
|
+
* A chord whose command's `when` is false is deliberately NOT swallowed: `handleKeyEvent` returns
|
|
23
|
+
* `undefined`, nothing calls `preventDefault`, and the key falls through to the browser. That is
|
|
24
|
+
* how ↑/↓ still scroll a surface with no selection, and how ⌘Z reaches a text field's native undo
|
|
25
|
+
* when no document is open.
|
|
26
|
+
*
|
|
27
|
+
* This file is also the definition site for the commands it implements — the clipboard trio, the
|
|
28
|
+
* three zoom verbs and the four selection-navigation verbs — plus the implementations behind the
|
|
29
|
+
* document- and selection-level records declared in `commands/defaults.ts`. Registration lives next
|
|
30
|
+
* to the code that runs, so a chord and its behaviour cannot drift apart again.
|
|
7
31
|
*/
|
|
8
32
|
|
|
9
|
-
import {
|
|
10
|
-
import { activeTab,
|
|
33
|
+
import { childIndex, childList, getNodeAtPath, parentElementPath, projectState } from "../store";
|
|
34
|
+
import { activeTab, workspace } from "../workspace/workspace";
|
|
35
|
+
import {
|
|
36
|
+
STAGE_CLASS,
|
|
37
|
+
allCanvasSurfaces,
|
|
38
|
+
canvasModeOfPane,
|
|
39
|
+
surfaceForPane,
|
|
40
|
+
tabOfPane,
|
|
41
|
+
} from "../canvas/canvas-surface";
|
|
42
|
+
import type { CanvasSurface } from "../canvas/canvas-surface";
|
|
43
|
+
import { primarySelection } from "../tabs/selection";
|
|
11
44
|
import {
|
|
12
|
-
|
|
45
|
+
mutateDuplicateNodes,
|
|
13
46
|
mutateInsertNode,
|
|
14
|
-
|
|
47
|
+
mutateRemoveNodes,
|
|
15
48
|
redo as tabRedo,
|
|
16
49
|
undo as tabUndo,
|
|
17
50
|
transactDoc,
|
|
18
51
|
} from "../tabs/transact";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
52
|
+
import {
|
|
53
|
+
applyEditZoom,
|
|
54
|
+
markExplicitZoom,
|
|
55
|
+
requestEditZoom,
|
|
56
|
+
setEditZoom,
|
|
57
|
+
} from "../canvas/canvas-utils";
|
|
22
58
|
import { openQuickSearch } from "../panels/quick-search";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
59
|
+
import { inspectorTab } from "../panels/right-panel";
|
|
60
|
+
import { requestClose } from "../panels/tab-strip";
|
|
61
|
+
import { showDialog } from "../ui/layers";
|
|
25
62
|
import { rectOf } from "../utils/geometry";
|
|
63
|
+
import {
|
|
64
|
+
DOCK_IDS,
|
|
65
|
+
requireNavigatorPanelId,
|
|
66
|
+
setActivityTab,
|
|
67
|
+
setDockCollapsed,
|
|
68
|
+
shell,
|
|
69
|
+
} from "../shell";
|
|
70
|
+
import { REGION_FOR_FOCUS, resolveRegion } from "../ui/regions";
|
|
71
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
72
|
+
import { notify } from "../services/notify";
|
|
73
|
+
import { panelFocusRoster } from "../panels/navigator-panels";
|
|
74
|
+
import { clipboardCommands, pasteNode } from "./context-menu";
|
|
26
75
|
|
|
76
|
+
import { hasElementSelection, hasSelection, inCanvas, keyScopeStack } from "../commands/context";
|
|
77
|
+
import { defaultCommands } from "../commands/defaults";
|
|
78
|
+
import { setActiveRegistry } from "../commands/active-registry";
|
|
79
|
+
import { CommandUnavailableError } from "../commands/registry";
|
|
80
|
+
import { html } from "lit-html";
|
|
81
|
+
import type { CommandContext } from "../commands/context";
|
|
82
|
+
import type { DockId as CommandDockId } from "../commands/defaults";
|
|
83
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
84
|
+
import type { DockId, FocusRegion } from "../shell";
|
|
27
85
|
import type { JxPath } from "../state";
|
|
28
86
|
|
|
87
|
+
/** What the pointer handlers need from `studio.ts` on every gesture. */
|
|
88
|
+
export interface ShortcutPointerContext {
|
|
89
|
+
canvasMode: string;
|
|
90
|
+
panX: number;
|
|
91
|
+
panY: number;
|
|
92
|
+
setPan: (x: number, y: number) => void;
|
|
93
|
+
applyTransform: () => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Read a live pointer context for one pane's stage. */
|
|
97
|
+
export type StageContext = (surface: CanvasSurface) => ShortcutPointerContext;
|
|
98
|
+
|
|
99
|
+
/** Where a project the user is about to pick should open. */
|
|
100
|
+
export type ProjectOpenTarget = "thisWindow" | "newWindow";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* What Open Project actually did — which is not always what was asked for. The picker can be
|
|
104
|
+
* cancelled, and a project chosen for a new window may already be open in one, in which case that
|
|
105
|
+
* window is raised rather than a second one made. The flow reports THIS, never the target.
|
|
106
|
+
*/
|
|
107
|
+
export type ProjectOpenOutcome = "opened" | "newWindow" | "focused" | "cancelled";
|
|
108
|
+
|
|
109
|
+
/** The verbs the default command set needs that are not implemented in this file. */
|
|
110
|
+
export interface StudioCommandHooks {
|
|
111
|
+
saveDocument: () => void | Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Open a project, in the window the user chose, and answer with what happened.
|
|
114
|
+
*
|
|
115
|
+
* The TARGET is the new half. `project.open` with a project already open used to route silently
|
|
116
|
+
* to `platform.openProjectInNewWindow` and return, so the click looked like it did nothing when
|
|
117
|
+
* the new window opened behind this one. The choice is now asked for ({@link openProjectFlow})
|
|
118
|
+
* and reported; honouring it is the bootstrap's side of the contract.
|
|
119
|
+
*
|
|
120
|
+
* The OUTCOME is what makes the report honest. A hook that returned nothing left this file
|
|
121
|
+
* announcing the target it had asked for, so a dismissed file dialog still said the project was
|
|
122
|
+
* opening — see {@link ProjectOpenOutcome}.
|
|
123
|
+
*/
|
|
124
|
+
openProject: (target: ProjectOpenTarget) => Promise<ProjectOpenOutcome>;
|
|
125
|
+
openInBrowser: () => void | Promise<void>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Shared predicates ────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The artboard is the editor on screen — the precondition every predicate below shares.
|
|
132
|
+
*
|
|
133
|
+
* `keyScope: "canvas"` gates the KEYBOARD and nothing else: the palette, `__jxAutomation` and the
|
|
134
|
+
* assistant all reach a command through `when` / `enablement`, so a scope is not an availability
|
|
135
|
+
* rule. `edit.paste` declared only `ctx.document.open`, which is true of a Code pane, a Grid pane
|
|
136
|
+
* and Project Settings alike — and running it over Project Settings inserted an element node at the
|
|
137
|
+
* root of `project.json`. Every verb below addresses a NODE IN A DOCUMENT TREE, and the editor
|
|
138
|
+
* showing one is the Canvas.
|
|
139
|
+
*/
|
|
140
|
+
/* `inCanvas`, `hasSelection` and `hasElementSelection` are `commands/context.ts`'s now — the
|
|
141
|
+
element menu gates on the same three, and one of the two modules had to stop declaring them. */
|
|
142
|
+
|
|
143
|
+
/** A document is open, on the canvas. */
|
|
144
|
+
const inDocument = (ctx: CommandContext) => ctx.document.open && inCanvas(ctx);
|
|
145
|
+
|
|
146
|
+
// ─── Selection verbs ──────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Move the selection to the previous (`-1`) or next (`+1`) sibling.
|
|
150
|
+
*
|
|
151
|
+
* With nothing selected, either direction selects the document element: the first arrow press from
|
|
152
|
+
* a cold canvas has to put the cursor somewhere, and the root is the only node guaranteed to
|
|
153
|
+
* exist.
|
|
154
|
+
*/
|
|
155
|
+
function navigateSelection(direction: -1 | 1): void {
|
|
156
|
+
const tab = activeTab.value;
|
|
157
|
+
if (!tab) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const selected = primarySelection(tab.session.selection);
|
|
161
|
+
if (!selected) {
|
|
162
|
+
tab.session.selection = [[]];
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (selected.length < 2) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const parentPath = parentElementPath(selected) as JxPath;
|
|
169
|
+
const parent = getNodeAtPath(tab.doc.document, parentPath);
|
|
170
|
+
const newIndex = (childIndex(selected) as number) + direction;
|
|
171
|
+
if (newIndex >= 0 && newIndex < childList(parent).length) {
|
|
172
|
+
tab.session.selection = [[...parentPath, "children", newIndex]];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Select every sibling of the selection — or the root's children when nothing is selected.
|
|
178
|
+
*
|
|
179
|
+
* Reads the same three helpers `navigateSelection` does, so "sibling" means one thing in this file.
|
|
180
|
+
* A parent whose children are strings (a text-only block) yields the element children alone: a raw
|
|
181
|
+
* string is not a node a verb can act on, and selecting one would put a path in `session.selection`
|
|
182
|
+
* that `getNodeAtPath` answers with a string.
|
|
183
|
+
*/
|
|
184
|
+
function selectSiblings(): void {
|
|
185
|
+
const tab = activeTab.value;
|
|
186
|
+
if (!tab) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const selected = primarySelection(tab.session.selection);
|
|
190
|
+
const parentPath = (
|
|
191
|
+
selected && selected.length >= 2 ? parentElementPath(selected) : []
|
|
192
|
+
) as JxPath;
|
|
193
|
+
const parent = getNodeAtPath(tab.doc.document, parentPath);
|
|
194
|
+
const paths = childList(parent).flatMap((child, index) =>
|
|
195
|
+
typeof child === "object" && child !== null
|
|
196
|
+
? [[...parentPath, "children", index] as JxPath]
|
|
197
|
+
: [],
|
|
198
|
+
);
|
|
199
|
+
if (paths.length > 0) {
|
|
200
|
+
tab.session.selection = paths;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Descend into the first child of the selected node. */
|
|
205
|
+
function selectFirstChild(): void {
|
|
206
|
+
const tab = activeTab.value;
|
|
207
|
+
const selected = primarySelection(tab?.session.selection);
|
|
208
|
+
if (!tab || !selected) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const node = getNodeAtPath(tab.doc.document, selected);
|
|
212
|
+
if (childList(node).length > 0) {
|
|
213
|
+
tab.session.selection = [[...selected, "children", 0]];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Walk out of the selection by one rung.
|
|
219
|
+
*
|
|
220
|
+
* This is the Escape ladder (plan §5.3), and it is one rung short of complete: the block action bar
|
|
221
|
+
* is not on the registry yet, so "Escape returns to the bar's selection" has nowhere to read focus
|
|
222
|
+
* from. What ships is the rest of it — a nested selection selects its parent, the document element
|
|
223
|
+
* clears — which replaces an Escape that always cleared regardless of depth and so threw away the
|
|
224
|
+
* whole path to get out of one node.
|
|
225
|
+
*/
|
|
226
|
+
function selectParent(): void {
|
|
227
|
+
const tab = activeTab.value;
|
|
228
|
+
const selected = primarySelection(tab?.session.selection);
|
|
229
|
+
if (!tab || !selected) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// Escape collapses a multi-selection to the primary's parent, exactly as it collapses one — the
|
|
233
|
+
// Ladder is about depth, and walking out of a batch of siblings lands on the one thing above it.
|
|
234
|
+
const parent = parentElementPath(selected);
|
|
235
|
+
tab.session.selection = selected.length >= 2 && parent ? [parent] : [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Insert an empty paragraph after the selection and select it. */
|
|
239
|
+
function insertSiblingParagraph(): void {
|
|
240
|
+
const tab = activeTab.value;
|
|
241
|
+
const selected = primarySelection(tab?.session.selection);
|
|
242
|
+
if (!tab || !selected || selected.length < 2) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const parentPath = parentElementPath(selected) as JxPath;
|
|
246
|
+
const index = childIndex(selected) as number;
|
|
247
|
+
const newPath = [...parentPath, "children", index + 1];
|
|
248
|
+
transactDoc(tab, (t) => {
|
|
249
|
+
mutateInsertNode(t, parentPath, index + 1, { tagName: "p", textContent: "" });
|
|
250
|
+
t.session.selection = [newPath];
|
|
251
|
+
});
|
|
252
|
+
// The iframe canvas re-enters inline edit for the freshly-selected node via its own posted
|
|
253
|
+
// EnterEdit flow, so no parent-side enterEditOnPath is needed here.
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Duplicate the whole selection in ONE transaction, so a batch is one undo step (§6.5).
|
|
258
|
+
*
|
|
259
|
+
* With one path selected this is `mutateDuplicateNode` on that path and nothing else — the batch
|
|
260
|
+
* form calls the single form, it does not reimplement it.
|
|
261
|
+
*/
|
|
262
|
+
function duplicateSelection(): void {
|
|
263
|
+
const tab = activeTab.value;
|
|
264
|
+
const selection = tab?.session.selection ?? [];
|
|
265
|
+
if (tab && selection.length > 0) {
|
|
266
|
+
transactDoc(tab, (t) => mutateDuplicateNodes(t, selection));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Delete the whole selection in ONE transaction. The document element is never deletable. */
|
|
271
|
+
function deleteSelection(): void {
|
|
272
|
+
const tab = activeTab.value;
|
|
273
|
+
const deletable = (tab?.session.selection ?? []).filter((path) => path.length >= 2);
|
|
274
|
+
if (tab && deletable.length > 0) {
|
|
275
|
+
transactDoc(tab, (t) => mutateRemoveNodes(t, deletable));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── Document verbs ───────────────────────────────────────────────────────────
|
|
280
|
+
|
|
29
281
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* }} getContext
|
|
282
|
+
* Close the focused document — the ⌘W half of the bug this registry exists to prevent.
|
|
283
|
+
*
|
|
284
|
+
* The old chord refused to close the last tab (`shortcuts.ts:192`) while the tab strip's × closed
|
|
285
|
+
* it happily (`tab-strip.ts:182`): two implementations of one action, disagreeing for a release
|
|
286
|
+
* cycle. It was then rewritten to MATCH the ×, by copying the ×'s wording into this file — which is
|
|
287
|
+
* the same defect wearing a shirt, and it duly went stale the moment the × grew a Save button. It
|
|
288
|
+
* calls the × now. One implementation, so there is nothing left to disagree.
|
|
289
|
+
*
|
|
290
|
+
* The tab id is read once, before anything can await: `requestClose` captures it as an argument, so
|
|
291
|
+
* confirming after switching tabs still closes the document the chord was pressed on.
|
|
41
292
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
293
|
+
function closeDocument(): void {
|
|
294
|
+
const id = workspace.activeTabId;
|
|
295
|
+
if (id) {
|
|
296
|
+
void requestClose(id);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function undoDocument(): void {
|
|
301
|
+
const tab = activeTab.value;
|
|
302
|
+
if (tab) {
|
|
303
|
+
tabUndo(tab);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function redoDocument(): void {
|
|
308
|
+
const tab = activeTab.value;
|
|
309
|
+
if (tab) {
|
|
310
|
+
tabRedo(tab);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── Canvas zoom ──────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
/** Design-mode zoom bounds. Edit mode clamps inside `canvas-utils`. */
|
|
317
|
+
const ZOOM_MIN = 0.05;
|
|
318
|
+
const ZOOM_MAX = 5;
|
|
319
|
+
const ZOOM_STEP = 1.2;
|
|
320
|
+
|
|
321
|
+
/** Whether the zoom verbs address the reflowing content zoom rather than the artboard transform. */
|
|
322
|
+
function isContentZoom(ctx: CommandContext): boolean {
|
|
323
|
+
return ctx.canvas.view === "edit";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function zoomReset(ctx: CommandContext, resetPan: () => void): void {
|
|
327
|
+
if (isContentZoom(ctx)) {
|
|
328
|
+
setEditZoom(1);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const tab = activeTab.value;
|
|
332
|
+
if (!tab) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
tab.session.ui.zoom = 1;
|
|
336
|
+
resetPan();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function zoomBy(ctx: CommandContext, factor: number, redraw: () => void): void {
|
|
340
|
+
const tab = activeTab.value;
|
|
341
|
+
if (!tab) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (isContentZoom(ctx)) {
|
|
345
|
+
setEditZoom((tab.session.ui.editZoom ?? 1) * factor);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const next = (tab.session.ui.zoom ?? 1) * factor;
|
|
349
|
+
tab.session.ui.zoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, next));
|
|
350
|
+
redraw();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ─── Shell verbs ──────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The Command Bar's dock toggles, mapped onto the two docks that exist.
|
|
357
|
+
*
|
|
358
|
+
* `"bottom"` is the plan's Problems/Terminal dock (§3.1) and has no shell row yet, so it is absent
|
|
359
|
+
* here and {@link toggleShellDock} routes ⌘J to the Assistant instead — which is no longer a dock at
|
|
360
|
+
* all but the Inspector's fourth tab, and is still the third thing that chord could mean. When the
|
|
361
|
+
* bottom dock lands it gets a row here and that branch goes away.
|
|
362
|
+
*/
|
|
363
|
+
const DOCK_FOR_COMMAND: Readonly<Record<Exclude<CommandDockId, "bottom">, DockId>> = {
|
|
364
|
+
inspector: "right",
|
|
365
|
+
navigator: "left",
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Flip one of the Command Bar's three toggles.
|
|
370
|
+
*
|
|
371
|
+
* ⌘J is the odd one: the assistant it addresses is a TAB, so "toggle" means select it or step off
|
|
372
|
+
* it, and it runs through `view.setAssistant` rather than through a dock flag so the registry stays
|
|
373
|
+
* the one place that behaviour is written down.
|
|
374
|
+
*/
|
|
375
|
+
function toggleShellDock(registry: CommandRegistry, dock: CommandDockId): void {
|
|
376
|
+
if (dock === "bottom") {
|
|
377
|
+
const showing = !shell.docks.right.collapsed && inspectorTab() === "assistant";
|
|
378
|
+
runIfPresent(registry, "view.setAssistant", { open: !showing });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const target = DOCK_FOR_COMMAND[dock];
|
|
382
|
+
setDockCollapsed(target, !shell.docks[target].collapsed);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Run a record from ANOTHER module's registration, if this registry has it.
|
|
387
|
+
*
|
|
388
|
+
* `view.setAssistant` and `view.setRightTab` are `shell.ts`'s, composed into the app's registry by
|
|
389
|
+
* the bootstrap — a reduced registry (a test, a future second window kind) may not carry them, and
|
|
390
|
+
* a missing id must be inert rather than an exception thrown out of a keydown handler.
|
|
391
|
+
*/
|
|
392
|
+
function runIfPresent(registry: CommandRegistry, id: string, args: Record<string, unknown>): void {
|
|
393
|
+
if (registry.get(id) && registry.isEnabled(id)) {
|
|
394
|
+
void registry.run(id, args);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** The dock state Zen collapsed, or null when Zen is off. */
|
|
399
|
+
let zenRestore: Partial<Record<DockId, boolean>> | null = null;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Collapse every dock; the same chord puts them back exactly as they were.
|
|
403
|
+
*
|
|
404
|
+
* "Reversible by the same key" (plan §5.3) is the whole requirement, and it is why the previous
|
|
405
|
+
* state is snapshotted rather than assumed: an author who had the assistant closed does not want
|
|
406
|
+
* leaving Zen to open it.
|
|
407
|
+
*/
|
|
408
|
+
function toggleZen(): void {
|
|
409
|
+
if (zenRestore) {
|
|
410
|
+
const restore = zenRestore;
|
|
411
|
+
zenRestore = null;
|
|
412
|
+
for (const id of DOCK_IDS) {
|
|
413
|
+
setDockCollapsed(id, restore[id] ?? false);
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const snapshot: Partial<Record<DockId, boolean>> = {};
|
|
418
|
+
for (const id of DOCK_IDS) {
|
|
419
|
+
snapshot[id] = shell.docks[id].collapsed;
|
|
420
|
+
setDockCollapsed(id, true);
|
|
421
|
+
}
|
|
422
|
+
zenRestore = snapshot;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ─── Region focus (F6) ────────────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The F6 ring, in reading order: rail → navigator → pane → inspector → dock → status (plan §5.3).
|
|
429
|
+
*
|
|
430
|
+
* `shell.focusRegion` has enumerated these six values since the shell record landed and nothing
|
|
431
|
+
* could act on it, because there was no map from the enum to the DOM. `ui/regions.ts` is that map;
|
|
432
|
+
* this is its consumer, and the reason `REGION_FOR_FOCUS` exists.
|
|
433
|
+
*/
|
|
434
|
+
export const REGION_CYCLE: readonly FocusRegion[] = [
|
|
435
|
+
"rail",
|
|
436
|
+
"navigator",
|
|
437
|
+
"pane",
|
|
438
|
+
"inspector",
|
|
439
|
+
"dock",
|
|
440
|
+
"status",
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The next region in the ring that is actually on screen, or `null` when none is.
|
|
445
|
+
*
|
|
446
|
+
* Absent regions are SKIPPED rather than focused-and-lost: the bottom dock does not exist until P4
|
|
447
|
+
* and a collapsed Navigator has no host, so a ring that did not skip would strand the caret every
|
|
448
|
+
* second press. Pure, and injectable, so the walk is testable without a shell.
|
|
449
|
+
*/
|
|
450
|
+
export function nextRegion(
|
|
451
|
+
current: FocusRegion,
|
|
452
|
+
direction: 1 | -1,
|
|
453
|
+
isPresent: (region: FocusRegion) => boolean,
|
|
454
|
+
): FocusRegion | null {
|
|
455
|
+
const start = REGION_CYCLE.indexOf(current);
|
|
456
|
+
const size = REGION_CYCLE.length;
|
|
457
|
+
for (let step = 1; step <= size; step++) {
|
|
458
|
+
const candidate = REGION_CYCLE[(start + direction * step + size * size) % size]!;
|
|
459
|
+
if (isPresent(candidate)) {
|
|
460
|
+
return candidate;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The first element inside a region that can take the caret. */
|
|
467
|
+
const REGION_FOCUSABLE =
|
|
468
|
+
'a[href], button, input, textarea, select, sp-action-button, sp-tab, sp-textfield, sp-picker, [tabindex]:not([tabindex="-1"])';
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Move focus into a region and record that it moved.
|
|
472
|
+
*
|
|
473
|
+
* The record and the DOM are written together, deliberately: `focus.region` is a command-context
|
|
474
|
+
* key that the keyboard's own scope stack reads (`keyScopeStack` returns the dock stack for
|
|
475
|
+
* anything but the pane), so a focus move the record did not hear about would silently change which
|
|
476
|
+
* chords are live.
|
|
477
|
+
*/
|
|
478
|
+
export function focusShellRegion(region: FocusRegion): boolean {
|
|
479
|
+
const host = resolveRegion(REGION_FOR_FOCUS[region]);
|
|
480
|
+
if (!host) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
shell.focusRegion = region;
|
|
484
|
+
const inner = host.querySelector<HTMLElement>(REGION_FOCUSABLE);
|
|
485
|
+
if (inner) {
|
|
486
|
+
inner.focus();
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
// A bare `<div id>` shell host is not focusable on its own. Making it programmatically focusable
|
|
490
|
+
// Is the standard fix and costs nothing: -1 keeps it out of the Tab order, which is the point —
|
|
491
|
+
// F6 is the way in, Tab is the way around inside.
|
|
492
|
+
host.tabIndex = -1;
|
|
493
|
+
host.focus();
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function cycleRegion(direction: 1 | -1): void {
|
|
498
|
+
const target = nextRegion(shell.focusRegion, direction, (region) =>
|
|
499
|
+
Boolean(resolveRegion(REGION_FOR_FOCUS[region])),
|
|
500
|
+
);
|
|
501
|
+
if (target) {
|
|
502
|
+
focusShellRegion(target);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ─── Panel and inspector focus (⌘1–8, ⌘⇧1–4) ─────────────────────────────────
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Toggle-FOCUS, not toggle-visible (plan §5.3).
|
|
510
|
+
*
|
|
511
|
+
* ⌘1 from the canvas reveals Files and puts the caret in it; ⌘1 again — with Files already focused
|
|
512
|
+
* — collapses the dock and hands the caret back to the pane. The distinction matters because the
|
|
513
|
+
* common case is "take me there", and a toggle-visible binding makes that a coin flip on whichever
|
|
514
|
+
* panel happened to be showing.
|
|
515
|
+
*
|
|
516
|
+
* Every panel this can be handed lives in the Navigator. The roster is the rail plus the rail-less
|
|
517
|
+
* NAVIGATOR panels (`panels/navigator-panels.ts`), so a Bottom-dock record cannot reach here — the
|
|
518
|
+
* branch that used to open the Bottom dock existed for Problems alone, and Problems is off the rail
|
|
519
|
+
* and addressed by `view.setBottomTab` now. Keeping a dead branch would leave a second, silent door
|
|
520
|
+
* into a dock nothing here can name.
|
|
521
|
+
*/
|
|
522
|
+
function focusPanel(panelId: string): void {
|
|
523
|
+
/* THE DOOR. `shell.ts`'s `NAVIGATOR_PANEL_IDS` docstring says the command is what refuses an
|
|
524
|
+
undeclared id, "at the one door a caller comes through" — this is that door for ⌘1–8, and it
|
|
525
|
+
was the only one of three doors that had no lock. `setActivityTab` now takes a
|
|
526
|
+
`NavigatorPanelId`, so the roster's `string` has to be narrowed somewhere; narrowing HERE keeps
|
|
527
|
+
`commands/defaults.ts` free of shell types, which is the property that lets three CI checks
|
|
528
|
+
load the command set in a bare Bun process.
|
|
529
|
+
|
|
530
|
+
A throw rather than a silent return: `tests/navigator-panels.test.ts` asserts the enum and the
|
|
531
|
+
registry agree, so this is unreachable while that passes, and a chord that quietly does nothing
|
|
532
|
+
is precisely the defect this change is closing. */
|
|
533
|
+
const id = requireNavigatorPanelId(panelId, "panel.focus");
|
|
534
|
+
const alreadyThere =
|
|
535
|
+
shell.leftTab === id && !shell.docks.left.collapsed && shell.focusRegion === "navigator";
|
|
536
|
+
if (alreadyThere) {
|
|
537
|
+
setDockCollapsed("left", true);
|
|
538
|
+
focusShellRegion("pane");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
setActivityTab(id);
|
|
542
|
+
focusShellRegion("navigator");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* ⌘⇧1–4 — show an Inspector tab and focus the dock.
|
|
547
|
+
*
|
|
548
|
+
* All four are tabs of the same dock now. `"assistant"` still routes to its own record because that
|
|
549
|
+
* record is application-level and works with no document open — `view.setRightTab` is
|
|
550
|
+
* document-level and correctly refuses when there is nothing to inspect.
|
|
551
|
+
*/
|
|
552
|
+
function focusInspectorTab(registry: CommandRegistry, tabId: string): void {
|
|
553
|
+
setDockCollapsed("right", false);
|
|
554
|
+
if (tabId === "assistant") {
|
|
555
|
+
runIfPresent(registry, "view.setAssistant", { open: true });
|
|
556
|
+
} else {
|
|
557
|
+
runIfPresent(registry, "view.setRightTab", { tab: tabId });
|
|
558
|
+
}
|
|
559
|
+
focusShellRegion("inspector");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ─── Project: Open… ───────────────────────────────────────────────────────────
|
|
563
|
+
|
|
564
|
+
/** What the flow says once the answer is in — one line per outcome that isn't a cancel. */
|
|
565
|
+
const OPEN_PROJECT_REPORT: Record<Exclude<ProjectOpenOutcome, "cancelled">, string> = {
|
|
566
|
+
focused: "That project was already open — bringing its window to the front.",
|
|
567
|
+
newWindow: "Opened the project in a new window.",
|
|
568
|
+
opened: "Opened the project in this window.",
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Ask where the project should open, then say what happened.
|
|
573
|
+
*
|
|
574
|
+
* With no project open, or on a platform with one window, there is no choice to make and none is
|
|
575
|
+
* offered. With one open there IS a choice, and it was previously being made silently in the user's
|
|
576
|
+
* name: `openRecentProject` saw a live `projectState`, called `openProjectInNewWindow` and
|
|
577
|
+
* returned, so a window opened behind this one and the click read as a no-op.
|
|
578
|
+
*
|
|
579
|
+
* A window to open into is not enough to make the choice real — the platform must also be able to
|
|
580
|
+
* ASK WHICH PROJECT without binding this window to the answer ({@link StudioPlatform.pickProject}).
|
|
581
|
+
* Where the only picker is `openProject()`, which re-roots the asking window as it picks, "New
|
|
582
|
+
* Window" cannot be honoured however the dialog is answered, so the dialog is not shown.
|
|
583
|
+
*/
|
|
584
|
+
export async function openProjectFlow(hooks: StudioCommandHooks): Promise<void> {
|
|
585
|
+
const platform = hasPlatform() ? getPlatform() : null;
|
|
586
|
+
const canOpenElsewhere =
|
|
587
|
+
typeof platform?.openProjectInNewWindow === "function" &&
|
|
588
|
+
typeof platform?.pickProject === "function";
|
|
589
|
+
if (!projectState || !canOpenElsewhere) {
|
|
590
|
+
await hooks.openProject("thisWindow");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const openName = projectState.name;
|
|
594
|
+
const choice = await showDialog<ProjectOpenTarget | "cancel">(
|
|
595
|
+
(done) => html`
|
|
596
|
+
<sp-dialog-wrapper
|
|
597
|
+
open
|
|
598
|
+
underlay
|
|
599
|
+
headline="Open Project"
|
|
600
|
+
confirm-label="New Window"
|
|
601
|
+
secondary-label="This Window"
|
|
602
|
+
cancel-label="Cancel"
|
|
603
|
+
size="s"
|
|
604
|
+
@confirm=${() => done("newWindow")}
|
|
605
|
+
@secondary=${() => done("thisWindow")}
|
|
606
|
+
@cancel=${() => done("cancel")}
|
|
607
|
+
@close=${() => done("cancel")}
|
|
608
|
+
>
|
|
609
|
+
<p>${openName} is open in this window. Where should the project you pick open?</p>
|
|
610
|
+
</sp-dialog-wrapper>
|
|
611
|
+
`,
|
|
612
|
+
);
|
|
613
|
+
if (choice === "cancel") {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const outcome = await hooks.openProject(choice);
|
|
617
|
+
if (outcome === "cancelled") {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
notify.info(OPEN_PROJECT_REPORT[outcome], { key: "project.open", source: "Project" });
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ─── Command records owned by this file ───────────────────────────────────────
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* The nine verbs the old switch implemented inline and no other surface had a name for.
|
|
627
|
+
*
|
|
628
|
+
* All nine are `keyScope: "canvas"`: they act on a node in the artboard, so they must not fire
|
|
629
|
+
* while a caret owns the keyboard, while the grid engine does, or while Preview is showing a page
|
|
630
|
+
* with no overlays to aim at. That is one field per record instead of two ad-hoc refusal sets.
|
|
631
|
+
*
|
|
632
|
+
* Each is ALSO gated on {@link inCanvas}, through the three shared predicates. The `keyScope`
|
|
633
|
+
* answers "may this chord fire here"; the `when` answers "does this verb exist here at all", which
|
|
634
|
+
* is the question the palette, `__jxAutomation` and the assistant ask instead.
|
|
635
|
+
*/
|
|
636
|
+
export function canvasCommands(pointer: () => ShortcutPointerContext): AnyCommand[] {
|
|
637
|
+
const resetPan = () => {
|
|
638
|
+
pointer().setPan(16, 16);
|
|
639
|
+
pointer().applyTransform();
|
|
640
|
+
};
|
|
641
|
+
const redraw = () => pointer().applyTransform();
|
|
642
|
+
return [
|
|
643
|
+
// Copy and Cut are `editor/context-menu.ts`'s, beside the `copyNode`/`cutNode` this file
|
|
644
|
+
// Already imports from there — one import direction, and no cycle.
|
|
645
|
+
...clipboardCommands(),
|
|
646
|
+
{
|
|
647
|
+
category: "Edit",
|
|
648
|
+
group: "1_clipboard",
|
|
649
|
+
id: "edit.paste",
|
|
650
|
+
keybinding: "mod+v",
|
|
651
|
+
keyScope: "canvas",
|
|
652
|
+
level: "document",
|
|
653
|
+
requires: "an open document",
|
|
654
|
+
run: () => {
|
|
655
|
+
void pasteNode();
|
|
656
|
+
},
|
|
657
|
+
title: "Paste",
|
|
658
|
+
undo: "document",
|
|
659
|
+
when: inDocument,
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
category: "Selection",
|
|
663
|
+
group: "3_structure",
|
|
664
|
+
id: "selection.insertSibling",
|
|
665
|
+
keybinding: "enter",
|
|
666
|
+
keyScope: "canvas",
|
|
667
|
+
level: "selection",
|
|
668
|
+
requires: "an element selection that is not the document root",
|
|
669
|
+
run: () => insertSiblingParagraph(),
|
|
670
|
+
title: "Insert Paragraph After",
|
|
671
|
+
undo: "document",
|
|
672
|
+
when: hasElementSelection,
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
category: "Selection",
|
|
676
|
+
group: "2_navigate",
|
|
677
|
+
id: "selection.selectPrevious",
|
|
678
|
+
keybinding: "arrowup",
|
|
679
|
+
keyScope: "canvas",
|
|
680
|
+
level: "selection",
|
|
681
|
+
requires: "an open document",
|
|
682
|
+
run: () => navigateSelection(-1),
|
|
683
|
+
title: "Select Previous Sibling",
|
|
684
|
+
undo: "none",
|
|
685
|
+
when: inDocument,
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
category: "Selection",
|
|
689
|
+
group: "2_navigate",
|
|
690
|
+
id: "selection.selectNext",
|
|
691
|
+
keybinding: "arrowdown",
|
|
692
|
+
keyScope: "canvas",
|
|
693
|
+
level: "selection",
|
|
694
|
+
requires: "an open document",
|
|
695
|
+
run: () => navigateSelection(1),
|
|
696
|
+
title: "Select Next Sibling",
|
|
697
|
+
undo: "none",
|
|
698
|
+
when: inDocument,
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
category: "Selection",
|
|
702
|
+
group: "2_navigate",
|
|
703
|
+
/**
|
|
704
|
+
* ⌘A — every sibling of the selection, in one decision.
|
|
705
|
+
*
|
|
706
|
+
* The chord did nothing for two phases, and did it twice over: no record bound it, while
|
|
707
|
+
* `canvas/iframe-keys.ts` forwarded it anyway and called `preventDefault()` first, so the
|
|
708
|
+
* browser's own select-all was suppressed on the way to a host that had nothing to run. The
|
|
709
|
+
* keymap sync fixed the second half by construction — the frame forwards what the registry
|
|
710
|
+
* binds — and this record is the first half.
|
|
711
|
+
*
|
|
712
|
+
* `canvas` scope, so with a caret in text the stack is `["caret", "global"]`, nothing claims
|
|
713
|
+
* ⌘A, the frame does not forward it, and select-all means the SENTENCE. Structural select-all
|
|
714
|
+
* is what you get when no caret owns the keyboard, which is the same rule that decides the
|
|
715
|
+
* clipboard trio.
|
|
716
|
+
*
|
|
717
|
+
* Siblings rather than "every node in the document": the verbs a multi-selection feeds —
|
|
718
|
+
* delete, duplicate, a style paste — take a batch of peers, and a selection spanning depths
|
|
719
|
+
* makes half of them meaningless. From the root (which has no siblings) it selects the root's
|
|
720
|
+
* children, because that is the set a reader means by "all of it".
|
|
721
|
+
*/
|
|
722
|
+
id: "selection.selectAll",
|
|
723
|
+
keybinding: "mod+a",
|
|
724
|
+
keyScope: "canvas",
|
|
725
|
+
level: "selection",
|
|
726
|
+
requires: "an open document",
|
|
727
|
+
run: () => selectSiblings(),
|
|
728
|
+
title: "Select All",
|
|
729
|
+
undo: "none",
|
|
730
|
+
when: inDocument,
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
category: "Selection",
|
|
734
|
+
group: "2_navigate",
|
|
735
|
+
id: "selection.selectFirstChild",
|
|
736
|
+
keybinding: "arrowright",
|
|
737
|
+
keyScope: "canvas",
|
|
738
|
+
level: "selection",
|
|
739
|
+
requires: "an element selection with children",
|
|
740
|
+
run: () => selectFirstChild(),
|
|
741
|
+
title: "Select First Child",
|
|
742
|
+
undo: "none",
|
|
743
|
+
when: hasSelection,
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
category: "View",
|
|
747
|
+
group: "6_zoom",
|
|
748
|
+
id: "canvas.zoomReset",
|
|
749
|
+
/* No chord. `⌘0` is `pane.focusPrimary`, pairing with `pane.focusSecondary`'s `⌘⌥0` —
|
|
750
|
+
the re-bind `workspace/workspace.ts` deferred to this workstream by name. Resetting the
|
|
751
|
+
zoom keeps its button in the floating zoom pod (`panels/pane-context.ts`), which is where
|
|
752
|
+
the whole zoom cluster lives; focusing a pane has no control at all and needs the key. */
|
|
753
|
+
keyScope: "canvas",
|
|
754
|
+
level: "document",
|
|
755
|
+
requires: "an open document",
|
|
756
|
+
run: (ctx) => zoomReset(ctx, resetPan),
|
|
757
|
+
title: "Reset Zoom",
|
|
758
|
+
undo: "none",
|
|
759
|
+
when: inDocument,
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
category: "View",
|
|
763
|
+
group: "6_zoom",
|
|
764
|
+
id: "canvas.zoomIn",
|
|
765
|
+
// ⌘+ needs Shift on most layouts, and `KeyboardEvent.key` is then "+" with `shiftKey` set —
|
|
766
|
+
// Three spellings of one gesture, which is why the old switch had `case "=": case "+":`.
|
|
767
|
+
keybinding: ["mod+=", "mod++", "mod+shift++"],
|
|
768
|
+
keyScope: "canvas",
|
|
769
|
+
level: "document",
|
|
770
|
+
requires: "an open document",
|
|
771
|
+
run: (ctx) => zoomBy(ctx, ZOOM_STEP, redraw),
|
|
772
|
+
title: "Zoom In",
|
|
773
|
+
undo: "none",
|
|
774
|
+
when: inDocument,
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
category: "View",
|
|
778
|
+
group: "6_zoom",
|
|
779
|
+
id: "canvas.zoomOut",
|
|
780
|
+
keybinding: "mod+-",
|
|
781
|
+
keyScope: "canvas",
|
|
782
|
+
level: "document",
|
|
783
|
+
requires: "an open document",
|
|
784
|
+
run: (ctx) => zoomBy(ctx, 1 / ZOOM_STEP, redraw),
|
|
785
|
+
title: "Zoom Out",
|
|
786
|
+
undo: "none",
|
|
787
|
+
when: inDocument,
|
|
788
|
+
},
|
|
789
|
+
];
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Register every command the keyboard dispatches, with its real implementation.
|
|
794
|
+
*
|
|
795
|
+
* The default set's `CommandDeps` are wired here rather than in the bootstrap because nine of the
|
|
796
|
+
* twelve verbs are implemented in this file; the three that reach outside it arrive as
|
|
797
|
+
* {@link StudioCommandHooks}.
|
|
798
|
+
*
|
|
799
|
+
* **This is the KEYBOARD's registry, not yet the app's.** Three other contribution points landed in
|
|
800
|
+
* the same wave and each built its own registry over its own context — `editor/context-menu.ts`
|
|
801
|
+
* (the element menu, over the node the open MENU addresses), `panels/block-action-bar.ts` and
|
|
802
|
+
* `workspace/workspace.ts`. Composing all four into one app-wide registry is the follow-up PR, and
|
|
803
|
+
* these four records are what it has to reconcile:
|
|
804
|
+
*
|
|
805
|
+
* | Here | There | Keep |
|
|
806
|
+
* | ------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
807
|
+
* | `edit.copy` / `edit.cut` | same ids in `context-menu.ts` | THEIRS — the implementation lives there; it needs a selection-derived `target()` |
|
|
808
|
+
* | `edit.paste` | `edit.pasteAfter` / `edit.pasteInside` | theirs, once one of them covers "paste into the root with nothing selected", which ⌘V does today |
|
|
809
|
+
* | `selection.insertSibling` | `selection.insertAfter` (unbound, no chord) | theirs, once it SELECTS the inserted node — the canvas re-enters inline edit off that selection, and Enter-to-add-a-paragraph is dead without it |
|
|
810
|
+
*
|
|
811
|
+
* PAID, for the four that were literal duplicates. `edit.copy` and `edit.cut` are defined here and
|
|
812
|
+
* nowhere else (the menu inherits them by `menus`), and `selection.editComponent` /
|
|
813
|
+
* `selection.convertToComponent` are defined once in `panels/block-action-bar.ts`. The remaining
|
|
814
|
+
* rows above are DIFFERENT verbs that happen to be spelled alike, and each names its own
|
|
815
|
+
* resolution.
|
|
816
|
+
*/
|
|
817
|
+
export function registerStudioCommands(
|
|
818
|
+
registry: CommandRegistry,
|
|
819
|
+
hooks: StudioCommandHooks,
|
|
820
|
+
stageContext: StageContext,
|
|
821
|
+
): void {
|
|
822
|
+
/* A COMMAND runs in the focused pane, and that is the one place resolving through focus is the
|
|
823
|
+
right answer: the person just pressed a key or picked a palette row, so "the canvas" means the
|
|
824
|
+
one they are in. Every other reader — the wheel, the drag, the render — is answering for a
|
|
825
|
+
stage the pointer or the pass names, and takes its surface explicitly. */
|
|
826
|
+
const pointer = () => stageContext(surfaceForPane(workspace.activePaneId));
|
|
827
|
+
registry.registerAll(
|
|
828
|
+
defaultCommands({
|
|
829
|
+
closeDocument,
|
|
830
|
+
cycleRegion,
|
|
831
|
+
deleteSelection,
|
|
832
|
+
duplicateSelection,
|
|
833
|
+
focusInspectorTab: (tabId) => focusInspectorTab(registry, tabId),
|
|
834
|
+
focusPanel,
|
|
835
|
+
openInBrowser: () => hooks.openInBrowser(),
|
|
836
|
+
openPalette: (mode) => {
|
|
837
|
+
openQuickSearch(mode);
|
|
838
|
+
},
|
|
839
|
+
panelRoster: panelFocusRoster(),
|
|
840
|
+
openProject: () => openProjectFlow(hooks),
|
|
841
|
+
redo: redoDocument,
|
|
842
|
+
saveDocument: () => hooks.saveDocument(),
|
|
843
|
+
selectParent,
|
|
844
|
+
toggleDock: (dock) => {
|
|
845
|
+
toggleShellDock(registry, dock);
|
|
846
|
+
},
|
|
847
|
+
toggleZen,
|
|
848
|
+
undo: undoDocument,
|
|
849
|
+
}),
|
|
850
|
+
);
|
|
851
|
+
registry.registerAll(canvasCommands(pointer));
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// ─── The dispatcher ───────────────────────────────────────────────────────────
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Resolve one keydown through the registry.
|
|
858
|
+
*
|
|
859
|
+
* `preventDefault()` iff a command claimed the chord. A command that is VISIBLE but disabled — ⌘Z
|
|
860
|
+
* with nothing to undo, Delete on the document element — still counts as claiming it: the registry
|
|
861
|
+
* throws {@link CommandUnavailableError} from `run`, and swallowing the key is the honest outcome,
|
|
862
|
+
* because the chord is spoken for and letting the browser act on it instead would be a surprise.
|
|
863
|
+
*
|
|
864
|
+
* HANDOFF: `registry.handleKeyEvent` reports the hit and runs it in one step, so a refusal can only
|
|
865
|
+
* be observed by catching. It would be better for it to consult `isEnabled` itself and return the
|
|
866
|
+
* id without running; `commands/registry.ts` is another workstream's file this wave.
|
|
867
|
+
*/
|
|
868
|
+
function dispatchKey(registry: CommandRegistry, event: KeyboardEvent): string | undefined {
|
|
869
|
+
const commandId = claimChord(registry, event);
|
|
870
|
+
if (commandId) {
|
|
871
|
+
event.preventDefault();
|
|
872
|
+
}
|
|
873
|
+
return commandId;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** The id of the command that claimed the chord, whether it ran or refused. */
|
|
877
|
+
function claimChord(registry: CommandRegistry, event: KeyboardEvent): string | undefined {
|
|
878
|
+
const stack = keyScopeStack(registry.context());
|
|
879
|
+
try {
|
|
880
|
+
return registry.handleKeyEvent(event, stack);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
if (error instanceof CommandUnavailableError) {
|
|
883
|
+
return error.commandId;
|
|
884
|
+
}
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// ─── Pointer and wheel gestures ───────────────────────────────────────────────
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Install the canvas gestures and the keyboard dispatcher.
|
|
893
|
+
*
|
|
894
|
+
* @param registry The app's command registry, already populated.
|
|
895
|
+
* @param getContext Live pointer/pan state, read fresh on every gesture.
|
|
896
|
+
*/
|
|
897
|
+
export function initShortcuts(registry: CommandRegistry, stageContext: StageContext): void {
|
|
898
|
+
// Publish the composed registry to the chrome. `studio.ts` mounts the Command Bar and the Palette
|
|
899
|
+
// At the top of its body and builds the registry at the bottom, so this — the last bootstrap call
|
|
900
|
+
// Before the automation hook — is the point at which "the registry" exists to be rendered.
|
|
901
|
+
setActiveRegistry(registry);
|
|
902
|
+
_stageContext = stageContext;
|
|
903
|
+
|
|
904
|
+
// Re-fit the edit-mode content zoom on resize: its layout width derives from the LIVE column
|
|
905
|
+
// Width, which tracks the studio window.
|
|
906
|
+
window.addEventListener("resize", () => {
|
|
907
|
+
for (const surface of allCanvasSurfaces()) {
|
|
908
|
+
if (canvasModeOfPane(surface.paneId) === "edit") {
|
|
909
|
+
applyEditZoom(surface);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
document.addEventListener("keydown", (e) => {
|
|
915
|
+
dispatchKey(registry, e);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
/* Block ctrl+scroll (browser zoom) everywhere that is not A STAGE.
|
|
919
|
+
`!canvasWrap.contains(target)` was the single-stage spelling of this, and with a grid it would
|
|
920
|
+
have blocked the browser-zoom guard's own exemption for every pane but one. `closest()` asks
|
|
921
|
+
the question the rule actually means. */
|
|
922
|
+
document.addEventListener(
|
|
923
|
+
"wheel",
|
|
924
|
+
(e: WheelEvent) => {
|
|
925
|
+
if ((e.ctrlKey || e.metaKey) && !(e.target as Element | null)?.closest(`.${STAGE_CLASS}`)) {
|
|
926
|
+
e.preventDefault();
|
|
927
|
+
}
|
|
928
|
+
},
|
|
929
|
+
{ passive: false },
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Canvas modes whose stage scrolls ITSELF, so the wheel is never a pan.
|
|
935
|
+
*
|
|
936
|
+
* None of these mounts a panzoom wrap — `canvas/canvas-render.ts` nulls `surface.panzoomWrap` on
|
|
937
|
+
* the way into every one of them — so the pan branch below could only ever have preventDefaulted a
|
|
938
|
+
* wheel on behalf of a transform that does not exist: `applyTransform` returns at its own
|
|
939
|
+
* `!panzoomWrap` guard, while `setPan` still wrote offsets (and cleared `needsCenter`) that the
|
|
940
|
+
* next Design render would inherit. The visible cost is the whole of it: the surface could not be
|
|
941
|
+
* scrolled with the wheel at all.
|
|
942
|
+
*
|
|
943
|
+
* - `grid` — a Tabulator viewport with its own virtual scroller.
|
|
944
|
+
* - `manage` — the Library's ordinary overflow container.
|
|
945
|
+
* - `settings` — the Project Settings document (`panels/settings-pane.ts`): an `overflow-y: auto`
|
|
946
|
+
* section column, plus the Raw JSON `<pre>`'s own box inside it.
|
|
947
|
+
* - `entry` — the Entry editor's `overflow-y: auto` form (`content/entry-editor.ts`).
|
|
948
|
+
* - `preview` — the fidelity surface: ONE frame at the pane's own height over its own document, which
|
|
949
|
+
* is what lets `position: sticky`, scroll-driven animation and `IntersectionObserver` reveals
|
|
950
|
+
* fire at all. It is in this set for the same reason as the other four and for one of its own —
|
|
951
|
+
* there is no artboard here to zoom, either.
|
|
952
|
+
*/
|
|
953
|
+
const SELF_SCROLLING_MODES: ReadonlySet<string> = new Set([
|
|
954
|
+
"entry",
|
|
955
|
+
"grid",
|
|
956
|
+
"manage",
|
|
957
|
+
"preview",
|
|
958
|
+
"settings",
|
|
959
|
+
]);
|
|
960
|
+
|
|
961
|
+
/** The stage-context reader, published by {@link initShortcuts} for {@link installStageGestures}. */
|
|
962
|
+
let _stageContext: StageContext | null = null;
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Install one pane's stage gestures: wheel zoom/pan, middle-mouse drag pan, background deselect.
|
|
966
|
+
*
|
|
967
|
+
* Per CELL, with the surface in a closure, because every one of these gestures reads or writes
|
|
968
|
+
* state that belongs to the stage the pointer is over — its pan offsets, its tab's zoom, its tab's
|
|
969
|
+
* selection. Resolved through focus instead (which is what `getContext()` did) a wheel over the
|
|
970
|
+
* side pane panned the primary and set the primary tab's zoom.
|
|
971
|
+
*
|
|
972
|
+
* @param {CanvasSurface} surface
|
|
973
|
+
* @returns {() => void} Disposer — called by the grid when the cell is removed.
|
|
974
|
+
*/
|
|
975
|
+
export function installStageGestures(surface: CanvasSurface): () => void {
|
|
976
|
+
const canvasWrap = surface.wrap;
|
|
977
|
+
const getContext = () => _stageContext?.(surface) ?? stageless();
|
|
978
|
+
const controller = new AbortController();
|
|
979
|
+
const { signal } = controller;
|
|
980
|
+
|
|
53
981
|
// Wheel handler: Ctrl+Scroll = zoom (cursor-centered), plain scroll = pan
|
|
54
982
|
canvasWrap.addEventListener(
|
|
55
983
|
"wheel",
|
|
@@ -60,11 +988,12 @@ export function initShortcuts(
|
|
|
60
988
|
// Ourselves. The canvas iframe is sized to its content (no internal scroll) and a cross-origin
|
|
61
989
|
// OOPIF doesn't bubble wheel to the parent, so the wheel reaches us forwarded (or over the
|
|
62
990
|
// Canvas chrome) but never triggers native scroll.
|
|
991
|
+
const paneTab = tabOfPane(surface.paneId);
|
|
63
992
|
if (canvasMode === "edit") {
|
|
64
993
|
if (e.ctrlKey || e.metaKey) {
|
|
65
994
|
e.preventDefault();
|
|
66
|
-
const editZoom =
|
|
67
|
-
requestEditZoom(editZoom * (1 + -e.deltaY * 0.005));
|
|
995
|
+
const editZoom = paneTab?.session.ui.editZoom ?? 1;
|
|
996
|
+
requestEditZoom(editZoom * (1 + -e.deltaY * 0.005), surface);
|
|
68
997
|
return;
|
|
69
998
|
}
|
|
70
999
|
const sc = canvasWrap.querySelector<HTMLElement>(".content-edit-canvas");
|
|
@@ -75,8 +1004,19 @@ export function initShortcuts(
|
|
|
75
1004
|
}
|
|
76
1005
|
return;
|
|
77
1006
|
}
|
|
78
|
-
|
|
79
|
-
|
|
1007
|
+
/* Surfaces that scroll themselves — see {@link SELF_SCROLLING_MODES}. The plain wheel is the
|
|
1008
|
+
scroll container's under the pointer, and nothing here takes it.
|
|
1009
|
+
|
|
1010
|
+
Ctrl/⌘ is the other half, and it is not the same question: the browser reads it as PAGE
|
|
1011
|
+
ZOOM (so does a trackpad pinch, which arrives as exactly this event), and the guard in
|
|
1012
|
+
{@link initShortcuts} declines to block it inside a stage because a stage is expected to
|
|
1013
|
+
answer the gesture with a zoom of its own. These five have none to give — no artboard, no
|
|
1014
|
+
transform — so the gesture would scale the whole of Studio around a table or a form. Block
|
|
1015
|
+
it here, which is what every other surface in the app already does. */
|
|
1016
|
+
if (SELF_SCROLLING_MODES.has(canvasMode)) {
|
|
1017
|
+
if (e.ctrlKey || e.metaKey) {
|
|
1018
|
+
e.preventDefault();
|
|
1019
|
+
}
|
|
80
1020
|
return;
|
|
81
1021
|
}
|
|
82
1022
|
e.preventDefault();
|
|
@@ -85,13 +1025,20 @@ export function initShortcuts(
|
|
|
85
1025
|
const rect = rectOf(canvasWrap);
|
|
86
1026
|
const cursorX = e.clientX - rect.left;
|
|
87
1027
|
const cursorY = e.clientY - rect.top;
|
|
88
|
-
|
|
1028
|
+
/* THIS pane's tab, not the focused pane's. Both reads were `activeTab`, so a wheel over
|
|
1029
|
+
the unfocused stage magnified a document on the other side of the splitter. */
|
|
1030
|
+
const oldZoom = paneTab?.session.ui.zoom ?? 1;
|
|
89
1031
|
const delta = -e.deltaY * 0.005;
|
|
90
|
-
const newZoom = Math.min(
|
|
1032
|
+
const newZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, oldZoom * (1 + delta)));
|
|
91
1033
|
const ratio = newZoom / oldZoom;
|
|
92
1034
|
// Adjust pan so the point under cursor stays stationary
|
|
93
1035
|
setPan(cursorX - (cursorX - panX) * ratio, cursorY - (cursorY - panY) * ratio);
|
|
94
|
-
|
|
1036
|
+
if (paneTab) {
|
|
1037
|
+
paneTab.session.ui.zoom = newZoom;
|
|
1038
|
+
}
|
|
1039
|
+
// The author chose this zoom, so re-entering Design keeps it instead of auto-fitting.
|
|
1040
|
+
// On THIS stage: the declared fit is a fact about the document under the cursor.
|
|
1041
|
+
markExplicitZoom(surface);
|
|
95
1042
|
} else if (e.shiftKey) {
|
|
96
1043
|
// Shift+scroll = horizontal pan
|
|
97
1044
|
setPan(panX - e.deltaY, panY);
|
|
@@ -101,303 +1048,70 @@ export function initShortcuts(
|
|
|
101
1048
|
}
|
|
102
1049
|
applyTransform();
|
|
103
1050
|
},
|
|
104
|
-
{ passive: false },
|
|
1051
|
+
{ passive: false, signal },
|
|
105
1052
|
);
|
|
106
1053
|
|
|
107
1054
|
// Middle-mouse drag panning
|
|
108
|
-
canvasWrap.addEventListener(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (e.button !== 1) {
|
|
114
|
-
return;
|
|
115
|
-
} // Middle button only
|
|
116
|
-
e.preventDefault();
|
|
117
|
-
canvasWrap.setPointerCapture(e.pointerId);
|
|
118
|
-
let lastX = e.clientX,
|
|
119
|
-
lastY = e.clientY;
|
|
120
|
-
const onMove = (ev: PointerEvent) => {
|
|
121
|
-
const { panX, panY, setPan, applyTransform } = getContext();
|
|
122
|
-
setPan(panX + (ev.clientX - lastX), panY + (ev.clientY - lastY));
|
|
123
|
-
lastX = ev.clientX;
|
|
124
|
-
lastY = ev.clientY;
|
|
125
|
-
applyTransform();
|
|
126
|
-
};
|
|
127
|
-
const onUp = () => {
|
|
128
|
-
canvasWrap.releasePointerCapture(e.pointerId);
|
|
129
|
-
canvasWrap.removeEventListener("pointermove", onMove);
|
|
130
|
-
canvasWrap.removeEventListener("pointerup", onUp);
|
|
131
|
-
};
|
|
132
|
-
canvasWrap.addEventListener("pointermove", onMove);
|
|
133
|
-
canvasWrap.addEventListener("pointerup", onUp);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Re-fit the edit-mode content zoom on resize: its layout width derives from the LIVE column
|
|
137
|
-
// Width, which tracks the studio window.
|
|
138
|
-
window.addEventListener("resize", () => {
|
|
139
|
-
if (getContext().canvasMode === "edit") {
|
|
140
|
-
applyEditZoom();
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
document.addEventListener("keydown", (e) => {
|
|
145
|
-
// A modal surface owns the keyboard while it is up. Its underlay already swallows every click,
|
|
146
|
-
// So leaving these live let Delete/Enter mutate the document — and ⌘S/⌘W/⌘Z drive the app —
|
|
147
|
-
// Behind a dialog the author cannot reach (the dialog's own keys are handled inside its layer).
|
|
148
|
-
if (isModalOpen()) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const { canvasMode, setPan, applyTransform, saveFile, openProject } = getContext();
|
|
152
|
-
const tab = activeTab.value;
|
|
153
|
-
const mod = e.ctrlKey || e.metaKey;
|
|
154
|
-
|
|
155
|
-
// Don't intercept when typing in inputs or contenteditable
|
|
156
|
-
if (
|
|
157
|
-
e.target instanceof HTMLElement &&
|
|
158
|
-
e.target.matches(
|
|
159
|
-
"input, textarea, select, sp-textfield, sp-search, sp-number-field, sp-picker",
|
|
160
|
-
)
|
|
161
|
-
) {
|
|
162
|
-
if (mod && e.key === "s") {
|
|
163
|
-
e.preventDefault();
|
|
164
|
-
saveFile();
|
|
165
|
-
}
|
|
166
|
-
if (mod && e.key === "w") {
|
|
167
|
-
e.preventDefault();
|
|
168
|
-
}
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (isEditing()) {
|
|
172
|
-
if (mod && e.key === "s") {
|
|
173
|
-
e.preventDefault();
|
|
174
|
-
// SaveFile flushes the canvas frames itself; ending the local session here would be a
|
|
175
|
-
// Second, racing commit path.
|
|
176
|
-
saveFile();
|
|
177
|
-
}
|
|
178
|
-
if (mod && e.key === "w") {
|
|
179
|
-
e.preventDefault();
|
|
180
|
-
}
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (mod) {
|
|
184
|
-
// Grid mode: copy/paste/duplicate/zoom belong to the grid engine (Tabulator clipboard
|
|
185
|
-
// Needs the native events); only tab/app-level chords pass through.
|
|
186
|
-
if (canvasMode === "grid" && !["o", "p", "s", "w", "z", "Z"].includes(e.key)) {
|
|
1055
|
+
canvasWrap.addEventListener(
|
|
1056
|
+
"pointerdown",
|
|
1057
|
+
(e: PointerEvent) => {
|
|
1058
|
+
const ctx = getContext();
|
|
1059
|
+
if (ctx.canvasMode === "edit" || ctx.canvasMode === "preview") {
|
|
187
1060
|
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
openProject();
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
case "p": {
|
|
217
|
-
e.preventDefault();
|
|
218
|
-
openQuickSearch();
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
case "s": {
|
|
222
|
-
e.preventDefault();
|
|
223
|
-
saveFile();
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
// With Shift held e.key is "Z", so redo needs the uppercase case too.
|
|
227
|
-
case "z":
|
|
228
|
-
case "Z": {
|
|
229
|
-
e.preventDefault();
|
|
230
|
-
if (e.shiftKey) {
|
|
231
|
-
tabRedo(activeTab.value!);
|
|
232
|
-
} else {
|
|
233
|
-
tabUndo(activeTab.value!);
|
|
234
|
-
}
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
case "d": {
|
|
238
|
-
e.preventDefault();
|
|
239
|
-
if (tab?.session.selection) {
|
|
240
|
-
const sel = tab.session.selection;
|
|
241
|
-
transactDoc(tab, (t) => mutateDuplicateNode(t, sel));
|
|
242
|
-
}
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
case "c": {
|
|
246
|
-
e.preventDefault();
|
|
247
|
-
void copyNode();
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
case "x": {
|
|
251
|
-
e.preventDefault();
|
|
252
|
-
void cutNode();
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
case "v": {
|
|
256
|
-
e.preventDefault();
|
|
257
|
-
void pasteNode();
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
case "0": {
|
|
261
|
-
e.preventDefault();
|
|
262
|
-
if (canvasMode === "edit") {
|
|
263
|
-
setEditZoom(1);
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
activeTab.value!.session.ui.zoom = 1;
|
|
267
|
-
setPan(16, 16);
|
|
268
|
-
applyTransform();
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
case "=":
|
|
272
|
-
case "+": {
|
|
273
|
-
e.preventDefault();
|
|
274
|
-
if (canvasMode === "edit") {
|
|
275
|
-
setEditZoom((tab?.session.ui.editZoom ?? 1) * 1.2);
|
|
276
|
-
break;
|
|
277
|
-
}
|
|
278
|
-
activeTab.value!.session.ui.zoom = Math.min(5, (tab?.session.ui.zoom ?? 1) * 1.2);
|
|
279
|
-
applyTransform();
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
case "-": {
|
|
283
|
-
e.preventDefault();
|
|
284
|
-
if (canvasMode === "edit") {
|
|
285
|
-
setEditZoom((tab?.session.ui.editZoom ?? 1) / 1.2);
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
activeTab.value!.session.ui.zoom = Math.max(0.05, (tab?.session.ui.zoom ?? 1) / 1.2);
|
|
289
|
-
applyTransform();
|
|
290
|
-
break;
|
|
291
|
-
}
|
|
292
|
-
default: {
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Grid mode: Delete/Escape/Enter/arrows drive the grid's own range clearing and cell
|
|
300
|
-
// Navigation — never the canvas document.
|
|
301
|
-
if (canvasMode === "grid") {
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
1061
|
+
} // No panning in edit mode, and preview scrolls its own frame rather than panning
|
|
1062
|
+
if (e.button !== 1) {
|
|
1063
|
+
return;
|
|
1064
|
+
} // Middle button only
|
|
1065
|
+
e.preventDefault();
|
|
1066
|
+
canvasWrap.setPointerCapture(e.pointerId);
|
|
1067
|
+
let lastX = e.clientX;
|
|
1068
|
+
let lastY = e.clientY;
|
|
1069
|
+
const onMove = (ev: PointerEvent) => {
|
|
1070
|
+
const { panX, panY, setPan, applyTransform } = getContext();
|
|
1071
|
+
setPan(panX + (ev.clientX - lastX), panY + (ev.clientY - lastY));
|
|
1072
|
+
lastX = ev.clientX;
|
|
1073
|
+
lastY = ev.clientY;
|
|
1074
|
+
applyTransform();
|
|
1075
|
+
};
|
|
1076
|
+
const onUp = () => {
|
|
1077
|
+
canvasWrap.releasePointerCapture(e.pointerId);
|
|
1078
|
+
canvasWrap.removeEventListener("pointermove", onMove);
|
|
1079
|
+
canvasWrap.removeEventListener("pointerup", onUp);
|
|
1080
|
+
};
|
|
1081
|
+
canvasWrap.addEventListener("pointermove", onMove);
|
|
1082
|
+
canvasWrap.addEventListener("pointerup", onUp);
|
|
1083
|
+
},
|
|
1084
|
+
{ signal },
|
|
1085
|
+
);
|
|
304
1086
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
case "Escape": {
|
|
316
|
-
activeTab.value!.session.selection = null;
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
case "Enter": {
|
|
320
|
-
if (tab?.session.selection && tab.session.selection.length >= 2) {
|
|
321
|
-
e.preventDefault();
|
|
322
|
-
const pp = parentElementPath(tab.session.selection) as JxPath;
|
|
323
|
-
const idx = childIndex(tab.session.selection) as number;
|
|
324
|
-
const newPath = [...pp, "children", idx + 1];
|
|
325
|
-
transactDoc(tab, (t) => {
|
|
326
|
-
mutateInsertNode(t, pp, idx + 1, { tagName: "p", textContent: "" });
|
|
327
|
-
t.session.selection = newPath;
|
|
328
|
-
});
|
|
329
|
-
// The iframe canvas re-enters inline edit for the freshly-selected node via its own
|
|
330
|
-
// Posted enterEdit flow, so no parent-side enterEditOnPath is needed here.
|
|
331
|
-
}
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
334
|
-
case "ArrowUp": {
|
|
335
|
-
e.preventDefault();
|
|
336
|
-
navigateSelection(-1);
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
case "ArrowDown": {
|
|
340
|
-
e.preventDefault();
|
|
341
|
-
navigateSelection(1);
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
case "ArrowLeft": {
|
|
345
|
-
e.preventDefault();
|
|
346
|
-
if (tab?.session.selection && tab.session.selection.length >= 2) {
|
|
347
|
-
activeTab.value!.session.selection = parentElementPath(tab.session.selection);
|
|
348
|
-
}
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
case "ArrowRight": {
|
|
352
|
-
e.preventDefault();
|
|
353
|
-
if (tab?.session.selection) {
|
|
354
|
-
const node = getNodeAtPath(tab.doc.document, tab.session.selection);
|
|
355
|
-
if (childList(node).length > 0) {
|
|
356
|
-
activeTab.value!.session.selection = [...tab.session.selection, "children", 0];
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
default: {
|
|
362
|
-
break;
|
|
1087
|
+
/* Clicking the stage background (outside any artboard) deselects — and clears THIS pane's tab.
|
|
1088
|
+
It was a bare listener in the bootstrap over `canvasWrap` and `view.panzoomWrap`, comparing
|
|
1089
|
+
against the app's one stage and writing `activeTab`'s selection. Both halves are per-pane. */
|
|
1090
|
+
canvasWrap.addEventListener(
|
|
1091
|
+
"click",
|
|
1092
|
+
(e: MouseEvent) => {
|
|
1093
|
+
if (e.target !== canvasWrap && e.target !== surface.panzoomWrap) {
|
|
1094
|
+
return;
|
|
363
1095
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
// Block ctrl+scroll (browser zoom) on all non-canvas areas
|
|
368
|
-
document.addEventListener(
|
|
369
|
-
"wheel",
|
|
370
|
-
(e: WheelEvent) => {
|
|
371
|
-
if ((e.ctrlKey || e.metaKey) && !canvasWrap.contains(e.target as Node)) {
|
|
372
|
-
e.preventDefault();
|
|
1096
|
+
const tab = tabOfPane(surface.paneId);
|
|
1097
|
+
if (!tab?.session.selection.length) {
|
|
1098
|
+
return;
|
|
373
1099
|
}
|
|
1100
|
+
tab.session.selection = [];
|
|
374
1101
|
},
|
|
375
|
-
{
|
|
1102
|
+
{ signal },
|
|
376
1103
|
);
|
|
377
|
-
}
|
|
378
1104
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const tab = activeTab.value;
|
|
382
|
-
if (!tab?.session.selection) {
|
|
383
|
-
activeTab.value!.session.selection = [];
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
if (tab.session.selection.length < 2) {
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
1105
|
+
return () => controller.abort();
|
|
1106
|
+
}
|
|
389
1107
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
"children",
|
|
400
|
-
newIdx,
|
|
401
|
-
];
|
|
402
|
-
}
|
|
1108
|
+
/** The context a stage answers with before the bootstrap has published a reader. */
|
|
1109
|
+
function stageless(): ShortcutPointerContext {
|
|
1110
|
+
return {
|
|
1111
|
+
applyTransform: () => {},
|
|
1112
|
+
canvasMode: "design",
|
|
1113
|
+
panX: 0,
|
|
1114
|
+
panY: 0,
|
|
1115
|
+
setPan: () => {},
|
|
1116
|
+
};
|
|
403
1117
|
}
|