@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
|
@@ -1,28 +1,65 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
// ─── Clipboard & Context Menu ─────────────────────────────────────────────────
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The element context menu is a RENDERING of the command registry (UX-REDESIGN-PLAN §5.5).
|
|
5
|
+
*
|
|
6
|
+
* This file used to hold an 18-item literal of `{ label, action, danger }` records — a fourth
|
|
7
|
+
* hand-maintained copy of verbs that also exist in the toolbar, the block action bar and the
|
|
8
|
+
* keymap, with bare labels that taught no chords and hand-set danger styling. It now holds the
|
|
9
|
+
* command RECORDS (next to their implementations, which have always lived here) and a renderer that
|
|
10
|
+
* asks `registry.forPlacement("context/element")` what to draw.
|
|
11
|
+
*
|
|
12
|
+
* Everything a row prints is derived:
|
|
13
|
+
*
|
|
14
|
+
* - The `title` — the record is the only place the action is named;
|
|
15
|
+
* - The chord, via `keymap.formatBinding(id)`, so ⌘C is finally taught where it is used;
|
|
16
|
+
* - `destructive` styling, from the record rather than a per-row `danger: true`;
|
|
17
|
+
* - `disabledReason(id)` as a greyed subtitle, so an inapplicable verb explains itself instead of
|
|
18
|
+
* vanishing and leaving the author to guess the menu is state-dependent at all.
|
|
19
|
+
*
|
|
20
|
+
* Group ordering and `when` filtering are the registry's; dividers fall where `group` changes. What
|
|
21
|
+
* is left here is positioning, popover rendering, and the menu keyboard contract.
|
|
22
|
+
*/
|
|
23
|
+
import { html, nothing } from "lit-html";
|
|
24
|
+
import { displayTagName } from "@jxsuite/schema/guards";
|
|
4
25
|
import { jsonClone } from "../utils/studio-utils";
|
|
5
26
|
import { ref } from "lit-html/directives/ref.js";
|
|
6
27
|
import { htmlToJx } from "@jxsuite/markup/html-to-jx";
|
|
7
28
|
import { childIndex, getNodeAtPath, parentElementPath } from "../store";
|
|
8
29
|
import { activeTab, workspace } from "../workspace/workspace";
|
|
30
|
+
import { isSelected, isSpliceablePath, primarySelection } from "../tabs/selection";
|
|
9
31
|
import {
|
|
10
32
|
mutateDuplicateNode,
|
|
11
33
|
mutateInsertNode,
|
|
12
34
|
mutateRemoveNode,
|
|
35
|
+
mutateRemoveNodes,
|
|
13
36
|
mutateReplaceStyle,
|
|
14
37
|
mutateWrapNode,
|
|
15
38
|
transactDoc,
|
|
16
39
|
} from "../tabs/transact";
|
|
17
|
-
import {
|
|
18
|
-
import { convertToComponent } from "./convert-to-component";
|
|
19
|
-
import { convertToRepeater } from "./convert-to-repeater";
|
|
40
|
+
import { notify } from "../services/notify";
|
|
20
41
|
import { componentRegistry } from "../files/components";
|
|
21
42
|
import { renderPopover } from "../ui/layers";
|
|
22
43
|
import { rectOf } from "../utils/geometry";
|
|
44
|
+
import { createCommandRegistry } from "../commands/registry";
|
|
45
|
+
import { registerSelectionCommands } from "../panels/block-action-bar";
|
|
46
|
+
import { convertToComponent } from "./convert-to-component";
|
|
47
|
+
import { activeRegistry } from "../commands/active-registry";
|
|
48
|
+
import { defaultCommands, noopCommandDeps } from "../commands/defaults";
|
|
49
|
+
import { inspectorCommands } from "../panels/properties-panel";
|
|
50
|
+
import { usagesSupported } from "../services/references";
|
|
51
|
+
import {
|
|
52
|
+
editorKindForMode,
|
|
53
|
+
hasElementSelection,
|
|
54
|
+
hasSelection,
|
|
55
|
+
makeContext,
|
|
56
|
+
} from "../commands/context";
|
|
23
57
|
|
|
58
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
59
|
+
import type { CommandContext } from "../commands/context";
|
|
60
|
+
import type { Placement } from "../commands/levels";
|
|
24
61
|
import type { JxPath } from "../state";
|
|
25
|
-
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
62
|
+
import type { JxMutableNode, JxStyle } from "@jxsuite/schema/types";
|
|
26
63
|
|
|
27
64
|
type JxNode = JxMutableNode;
|
|
28
65
|
|
|
@@ -35,7 +72,10 @@ function nodeToHtml(node: JxNode | string): string {
|
|
|
35
72
|
if (typeof node === "string") {
|
|
36
73
|
return node;
|
|
37
74
|
}
|
|
38
|
-
|
|
75
|
+
/* A chosen tag cannot be serialised to one HTML string, so the copy takes the branch the
|
|
76
|
+
candidates list first — clipboard HTML is a lossy export by nature (it drops bindings too),
|
|
77
|
+
and emitting `<[object Object]>` would be worse than lossy. */
|
|
78
|
+
const tag = displayTagName(node.tagName).split("|")[0] || "div";
|
|
39
79
|
let attrs = "";
|
|
40
80
|
if (node.attributes) {
|
|
41
81
|
for (const [k, v] of Object.entries(node.attributes)) {
|
|
@@ -144,32 +184,43 @@ async function readFromClipboard() {
|
|
|
144
184
|
|
|
145
185
|
export async function copyNode() {
|
|
146
186
|
const tab = activeTab.value;
|
|
147
|
-
|
|
187
|
+
const selected = primarySelection(tab?.session.selection);
|
|
188
|
+
if (!tab || !selected) {
|
|
148
189
|
return;
|
|
149
190
|
}
|
|
150
|
-
const node = getNodeAtPath(tab.doc.document,
|
|
191
|
+
const node = getNodeAtPath(tab.doc.document, selected);
|
|
151
192
|
if (!node) {
|
|
152
193
|
return;
|
|
153
194
|
}
|
|
154
195
|
const json = jsonClone(node);
|
|
155
196
|
await writeToClipboard(json);
|
|
156
|
-
|
|
197
|
+
notify.success("Copied", { key: "clipboard" });
|
|
157
198
|
}
|
|
158
199
|
|
|
159
200
|
export async function cutNode() {
|
|
160
201
|
const tab = activeTab.value;
|
|
161
|
-
|
|
202
|
+
const sel = primarySelection(tab?.session.selection);
|
|
203
|
+
if (!tab || !sel || sel.length < 2) {
|
|
162
204
|
return;
|
|
163
205
|
}
|
|
164
|
-
const sel = tab.session.selection;
|
|
165
206
|
const node = getNodeAtPath(tab.doc.document, sel);
|
|
166
207
|
if (!node) {
|
|
167
208
|
return;
|
|
168
209
|
}
|
|
169
210
|
const json = jsonClone(node);
|
|
170
211
|
await writeToClipboard(json);
|
|
171
|
-
|
|
172
|
-
|
|
212
|
+
// The clipboard holds the primary node; the cut removes everything selected, in one undo step.
|
|
213
|
+
const cutting = tab.session.selection.filter((path) => path.length >= 2);
|
|
214
|
+
/* AND THE REMOVAL CAN BE REFUSED, which makes this a copy rather than a cut.
|
|
215
|
+
`transactDoc` consults the collab gate, which pauses structural editing for the whole room
|
|
216
|
+
while source is canonical. Reporting "Cut" anyway claims a removal that did not happen — and
|
|
217
|
+
the toast's action is `edit.undo`, so the one recovery it offered was aimed at whatever edit
|
|
218
|
+
came BEFORE this one. The gate raises its own keyed toast saying why; this one stays silent
|
|
219
|
+
rather than adding a false claim beside it. */
|
|
220
|
+
if (!transactDoc(tab, (t) => mutateRemoveNodes(t, cutting))) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
notify.success("Cut", { action: "edit.undo", key: "clipboard" });
|
|
173
224
|
}
|
|
174
225
|
|
|
175
226
|
export async function pasteNode() {
|
|
@@ -183,15 +234,16 @@ export async function pasteNode() {
|
|
|
183
234
|
return;
|
|
184
235
|
}
|
|
185
236
|
|
|
186
|
-
const
|
|
237
|
+
const selected = primarySelection(tab.session.selection);
|
|
238
|
+
const pPath = selected ?? [];
|
|
187
239
|
const parent = getNodeAtPath(tab.doc.document, pPath);
|
|
188
240
|
if (!parent) {
|
|
189
241
|
return;
|
|
190
242
|
}
|
|
191
243
|
|
|
192
|
-
if (
|
|
193
|
-
const pp = parentElementPath(
|
|
194
|
-
const idx = childIndex(
|
|
244
|
+
if (selected && selected.length >= 2) {
|
|
245
|
+
const pp = parentElementPath(selected) as JxPath;
|
|
246
|
+
const idx = childIndex(selected) as number;
|
|
195
247
|
transactDoc(tab, (t) => {
|
|
196
248
|
for (let i = 0; i < nodes.length; i++) {
|
|
197
249
|
mutateInsertNode(t, pp, idx + 1 + i, nodes[i]!);
|
|
@@ -205,20 +257,21 @@ export async function pasteNode() {
|
|
|
205
257
|
}
|
|
206
258
|
});
|
|
207
259
|
}
|
|
208
|
-
|
|
260
|
+
notify.success("Pasted", { action: "edit.undo", key: "clipboard" });
|
|
209
261
|
}
|
|
210
262
|
|
|
211
263
|
export function copyStyles() {
|
|
212
264
|
const tab = activeTab.value;
|
|
213
|
-
|
|
265
|
+
const selected = primarySelection(tab?.session.selection);
|
|
266
|
+
if (!tab || !selected) {
|
|
214
267
|
return;
|
|
215
268
|
}
|
|
216
|
-
const node = getNodeAtPath(tab.doc.document,
|
|
269
|
+
const node = getNodeAtPath(tab.doc.document, selected);
|
|
217
270
|
if (!node?.style) {
|
|
218
271
|
return;
|
|
219
272
|
}
|
|
220
273
|
workspace.styleClipboard = jsonClone(node.style);
|
|
221
|
-
|
|
274
|
+
notify.success("Styles copied", { key: "clipboard" });
|
|
222
275
|
}
|
|
223
276
|
|
|
224
277
|
export function pasteStyles() {
|
|
@@ -226,38 +279,735 @@ export function pasteStyles() {
|
|
|
226
279
|
return;
|
|
227
280
|
}
|
|
228
281
|
const tab = activeTab.value;
|
|
229
|
-
|
|
282
|
+
const targets = tab?.session.selection ?? [];
|
|
283
|
+
if (!tab || targets.length === 0) {
|
|
230
284
|
return;
|
|
231
285
|
}
|
|
232
286
|
const style = jsonClone(workspace.styleClipboard);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
287
|
+
// Pasting one style onto six cards is the §6.5 example, and it is one undo step: `transactDoc`
|
|
288
|
+
// Records everything its callback mutates as a single history entry.
|
|
289
|
+
transactDoc(tab, (t) => {
|
|
290
|
+
for (const sel of targets) {
|
|
291
|
+
mutateReplaceStyle(t, sel, jsonClone(style));
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
notify.success(
|
|
295
|
+
targets.length === 1 ? "Styles pasted" : `Styles pasted to ${targets.length} elements`,
|
|
296
|
+
{ action: "edit.undo", key: "clipboard" },
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ─── The menu target ─────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The node an open element menu addresses, plus the per-invocation hooks its rows need.
|
|
304
|
+
*
|
|
305
|
+
* The registry is module-level and long-lived; the target is per-right-click. Every record's `when`
|
|
306
|
+
* / `enablement` / `run` reaches the node through {@link ElementCommandDeps.target}, which is what
|
|
307
|
+
* lets one set of records serve every right-click without being rebuilt.
|
|
308
|
+
*/
|
|
309
|
+
export interface ElementMenuTarget {
|
|
310
|
+
path: JxPath;
|
|
311
|
+
node: JxMutableNode;
|
|
312
|
+
/* `onEditComponent` was here, read only by this file's own `selection.editComponent`. That verb
|
|
313
|
+
is `block-action-bar.ts`'s now, and it navigates through the same `navigateToComponent` both
|
|
314
|
+
hosts were already passing in — so the per-target hook was one more way to say one thing. */
|
|
315
|
+
/** Re-render the surface that opened the menu — the outline's inline title editor needs it. */
|
|
316
|
+
rerender?: (() => void) | undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** What the element command records read from the rest of Studio. */
|
|
320
|
+
export interface ElementCommandDeps {
|
|
321
|
+
/** The node the open menu addresses, or `null` when no menu is open. */
|
|
322
|
+
target: () => ElementMenuTarget | null;
|
|
323
|
+
/** The style payload available to paste, or `null` when nothing has been copied. */
|
|
324
|
+
styleClipboard: () => JxStyle | null;
|
|
325
|
+
/** The component document's path when `tagName` names a registered component, else `null`. */
|
|
326
|
+
componentPathFor: (tagName: string) => string | null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Whether the node is a repeater (an `Array` prototype node), whose one child is its template. */
|
|
330
|
+
function isArrayNode(node: JxMutableNode): boolean {
|
|
331
|
+
return node.$prototype === "Array";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ─── The element command records ─────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The commands the element context menu contributes.
|
|
338
|
+
*
|
|
339
|
+
* `selection.duplicate`, `selection.delete` and `selection.repeat` are deliberately NOT here: they
|
|
340
|
+
* are defined in `commands/defaults.ts` and already declare `context/element`, and a second
|
|
341
|
+
* definition site is the exact failure the registry exists to prevent.
|
|
342
|
+
*
|
|
343
|
+
* `selection.repeat` was here, in this file's PRIVATE registry, which is why the two
|
|
344
|
+
* `repeat-dialog` shots were quarantined: the app registry never held it, so the palette could not
|
|
345
|
+
* list it, no chord could reach it, the AI could not call it, and
|
|
346
|
+
* `__jxAutomation.run("selection.repeat")` answered "unknown command" — the Repeat dialog was
|
|
347
|
+
* reachable by right-click and by nothing else.
|
|
348
|
+
*/
|
|
349
|
+
export function elementCommands(deps: ElementCommandDeps): AnyCommand[] {
|
|
350
|
+
/** A menu is open over some node — the precondition every row in this file shares. */
|
|
351
|
+
const hasTarget = () => deps.target() !== null;
|
|
352
|
+
/**
|
|
353
|
+
* The document under the menu is one whose ELEMENT TREE may be restructured.
|
|
354
|
+
*
|
|
355
|
+
* `liveContext` below already computes the honest editor kind, and its own comment says a verb
|
|
356
|
+
* reading it "would have been dead in this menu and live over a settings document, which is
|
|
357
|
+
* exactly backwards" — but no record here read it, so the menu got the backwards half. The menu
|
|
358
|
+
* opens from the canvas AND from the Outline, and the Outline renders whatever the active tab's
|
|
359
|
+
* document is: with Project Settings open that is `project.json` drawn as a layer tree. Delete
|
|
360
|
+
* and Duplicate, inherited from `commands/defaults.ts`, carry `editor.kind === "canvas"` and were
|
|
361
|
+
* correctly filtered out there — while Cut, Paste after, Paste inside, Insert before, Insert
|
|
362
|
+
* after, Wrap, Set Title and Convert to Component all rendered and ran, each transacting an
|
|
363
|
+
* element splice into the file that defines the project. One menu, one target, two rules, and the
|
|
364
|
+
* loose ones were the mutating majority.
|
|
365
|
+
*/
|
|
366
|
+
const structurallyEditable = () => {
|
|
367
|
+
const tab = activeTab.value;
|
|
368
|
+
return Boolean(tab) && editorKindForMode(tab!.session.ui.canvasMode) === "canvas";
|
|
369
|
+
};
|
|
370
|
+
/** The target sits in a splice coordinate, so structural verbs can address it. */
|
|
371
|
+
const spliceable = () => {
|
|
372
|
+
const target = deps.target();
|
|
373
|
+
return target !== null && isSpliceablePath(target.path) && structurallyEditable();
|
|
374
|
+
};
|
|
375
|
+
/** Not the repeater itself: its content is the single `map` template, not a child list. */
|
|
376
|
+
const notRepeater = () => {
|
|
377
|
+
const target = deps.target();
|
|
378
|
+
return target !== null && !isArrayNode(target.node);
|
|
379
|
+
};
|
|
380
|
+
const NOT_STRUCTURAL =
|
|
381
|
+
"an element with a sibling position — not the page root or a repeater item";
|
|
382
|
+
|
|
383
|
+
return [
|
|
384
|
+
/* COPY AND CUT LIVE IN `editor/shortcuts.ts`, which owns the chord table.
|
|
385
|
+
Two records with this id existed — same title, same `mod+c`, both calling the `copyNode()`
|
|
386
|
+
exported from THIS file — because the menu's registry and the app's never met. They meet now,
|
|
387
|
+
and the menu inherits them by `menus: ["context/element"]` like Duplicate and Delete. */
|
|
388
|
+
{
|
|
389
|
+
id: "edit.pasteAfter",
|
|
390
|
+
title: "Paste after",
|
|
391
|
+
category: "Edit",
|
|
392
|
+
level: "selection",
|
|
393
|
+
keyScope: "canvas",
|
|
394
|
+
/* NO CHORD. ⌘V is `edit.paste`'s (`editor/shortcuts.ts`), and `pasteNode()` — the function it
|
|
395
|
+
runs — already inserts after the selection when there is one. This record claimed the same
|
|
396
|
+
chord in the same scope, which the keymap refuses; the two registries never met, so the
|
|
397
|
+
conflict was latent until the element family joined the app's. The row is kept because it
|
|
398
|
+
is the specific verb with the specific refusal sentence, but the key belongs to the general
|
|
399
|
+
one, and printing a chord this record does not own would be a lie in the menu. */
|
|
400
|
+
menus: ["context/element", "palette"],
|
|
401
|
+
group: "1_clipboard",
|
|
402
|
+
undo: "document",
|
|
403
|
+
when: () => hasTarget() && structurallyEditable(),
|
|
404
|
+
enablement: () => spliceable() && notRepeater(),
|
|
405
|
+
requires: NOT_STRUCTURAL,
|
|
406
|
+
run: async () => {
|
|
407
|
+
const target = deps.target();
|
|
408
|
+
const nodes = await readFromClipboard();
|
|
409
|
+
if (!target || !nodes || nodes.length === 0) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const parent = parentElementPath(target.path) as JxPath;
|
|
413
|
+
const idx = childIndex(target.path) as number;
|
|
414
|
+
transactDoc(activeTab.value, (t) => {
|
|
415
|
+
for (const [offset, node] of nodes.entries()) {
|
|
416
|
+
mutateInsertNode(t, parent, idx + 1 + offset, node);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
notify.success("Pasted", { action: "edit.undo", key: "clipboard" });
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
id: "edit.pasteInside",
|
|
424
|
+
title: "Paste inside",
|
|
425
|
+
category: "Edit",
|
|
426
|
+
level: "selection",
|
|
427
|
+
menus: ["context/element", "palette"],
|
|
428
|
+
group: "1_clipboard",
|
|
429
|
+
undo: "document",
|
|
430
|
+
when: () => hasTarget() && structurallyEditable(),
|
|
431
|
+
// A repeater has no child list to paste into, and the document root is addressed by the
|
|
432
|
+
// Canvas itself rather than by an element menu.
|
|
433
|
+
enablement: () => {
|
|
434
|
+
const target = deps.target();
|
|
435
|
+
return target !== null && target.path.length >= 2 && notRepeater();
|
|
436
|
+
},
|
|
437
|
+
requires: "a container element that is not a repeater",
|
|
438
|
+
run: async () => {
|
|
439
|
+
const target = deps.target();
|
|
440
|
+
const nodes = await readFromClipboard();
|
|
441
|
+
if (!target || !nodes || nodes.length === 0) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const idx = Array.isArray(target.node.children) ? target.node.children.length : 0;
|
|
445
|
+
transactDoc(activeTab.value, (t) => {
|
|
446
|
+
for (const [offset, node] of nodes.entries()) {
|
|
447
|
+
mutateInsertNode(t, target.path, idx + offset, node);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
notify.success("Pasted", { action: "edit.undo", key: "clipboard" });
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
// ── Styles ──
|
|
455
|
+
{
|
|
456
|
+
id: "edit.copyStyles",
|
|
457
|
+
title: "Copy styles",
|
|
458
|
+
category: "Edit",
|
|
459
|
+
level: "selection",
|
|
460
|
+
menus: ["context/element", "palette"],
|
|
461
|
+
group: "2_styles",
|
|
462
|
+
undo: "none",
|
|
463
|
+
when: hasTarget,
|
|
464
|
+
enablement: () => Boolean(deps.target()?.node.style),
|
|
465
|
+
requires: "styles on the selected element",
|
|
466
|
+
run: () => {
|
|
467
|
+
const style = deps.target()?.node.style;
|
|
468
|
+
if (!style) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
workspace.styleClipboard = jsonClone(style);
|
|
472
|
+
notify.success("Styles copied", { key: "clipboard" });
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
id: "edit.pasteStyles",
|
|
477
|
+
title: "Paste styles",
|
|
478
|
+
category: "Edit",
|
|
479
|
+
level: "selection",
|
|
480
|
+
menus: ["context/element", "palette"],
|
|
481
|
+
group: "2_styles",
|
|
482
|
+
undo: "document",
|
|
483
|
+
when: () => hasTarget() && structurallyEditable(),
|
|
484
|
+
enablement: () => deps.styleClipboard() !== null,
|
|
485
|
+
requires: "a copied style set",
|
|
486
|
+
run: () => {
|
|
487
|
+
const target = deps.target();
|
|
488
|
+
const style = deps.styleClipboard();
|
|
489
|
+
if (!target || !style) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const clone = jsonClone(style);
|
|
493
|
+
transactDoc(activeTab.value, (t) => mutateReplaceStyle(t, target.path, clone));
|
|
494
|
+
notify.success("Styles pasted", { action: "edit.undo", key: "clipboard" });
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
// ── Structure ── (shares "3_structure" with the default set's Duplicate)
|
|
499
|
+
{
|
|
500
|
+
id: "selection.insertBefore",
|
|
501
|
+
title: "Insert before",
|
|
502
|
+
category: "Insert",
|
|
503
|
+
level: "selection",
|
|
504
|
+
menus: ["context/element", "palette"],
|
|
505
|
+
group: "3_structure",
|
|
506
|
+
undo: "document",
|
|
507
|
+
when: hasTarget,
|
|
508
|
+
enablement: spliceable,
|
|
509
|
+
requires: NOT_STRUCTURAL,
|
|
510
|
+
run: () => insertSiblingParagraph(deps.target(), 0),
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
id: "selection.insertAfter",
|
|
514
|
+
title: "Insert after",
|
|
515
|
+
category: "Insert",
|
|
516
|
+
level: "selection",
|
|
517
|
+
menus: ["context/element", "palette"],
|
|
518
|
+
group: "3_structure",
|
|
519
|
+
undo: "document",
|
|
520
|
+
when: hasTarget,
|
|
521
|
+
enablement: spliceable,
|
|
522
|
+
requires: NOT_STRUCTURAL,
|
|
523
|
+
run: () => insertSiblingParagraph(deps.target(), 1),
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
id: "selection.wrap",
|
|
527
|
+
title: "Wrap in Div",
|
|
528
|
+
category: "Selection",
|
|
529
|
+
level: "selection",
|
|
530
|
+
menus: ["context/element", "palette"],
|
|
531
|
+
group: "3_structure",
|
|
532
|
+
undo: "document",
|
|
533
|
+
when: hasTarget,
|
|
534
|
+
enablement: spliceable,
|
|
535
|
+
requires: NOT_STRUCTURAL,
|
|
536
|
+
run: () => {
|
|
537
|
+
const target = deps.target();
|
|
538
|
+
if (target) {
|
|
539
|
+
transactDoc(activeTab.value, (t) => mutateWrapNode(t, target.path));
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
|
|
544
|
+
// ── Identity ──
|
|
545
|
+
{
|
|
546
|
+
id: "selection.setTitle",
|
|
547
|
+
title: "Set Title",
|
|
548
|
+
category: "Selection",
|
|
549
|
+
level: "selection",
|
|
550
|
+
menus: ["context/element", "palette"],
|
|
551
|
+
group: "4_identity",
|
|
552
|
+
undo: "document",
|
|
553
|
+
// The inline title editor lives in the surface that opened the menu; with no `rerender` hook
|
|
554
|
+
// There is nothing to edit into, and the row used to render as a silent no-op.
|
|
555
|
+
when: () => Boolean(deps.target()?.rerender),
|
|
556
|
+
enablement: spliceable,
|
|
557
|
+
requires: NOT_STRUCTURAL,
|
|
558
|
+
run: async () => {
|
|
559
|
+
const target = deps.target();
|
|
560
|
+
if (!target?.rerender) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
// Lazy import breaks the context-menu ↔ layers-panel module cycle
|
|
564
|
+
const { startLayerTitleEdit } = await import("../panels/layers-panel");
|
|
565
|
+
startLayerTitleEdit(target.path, target.rerender);
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
/* EDIT COMPONENT AND CONVERT TO COMPONENT LIVE IN `panels/block-action-bar.ts`, beside the
|
|
569
|
+
block bar's other selection verbs, and are inherited here by `menus`. Both were defined twice
|
|
570
|
+
with different `when` predicates, so the menu row and the block-bar button could disagree
|
|
571
|
+
about whether the same selection was a component instance. */
|
|
572
|
+
];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Insert an empty paragraph `offset` positions from the target (0 = before, 1 = after). */
|
|
576
|
+
function insertSiblingParagraph(target: ElementMenuTarget | null, offset: number): void {
|
|
577
|
+
if (!target) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const parent = parentElementPath(target.path) as JxPath;
|
|
581
|
+
const idx = childIndex(target.path) as number;
|
|
582
|
+
transactDoc(activeTab.value, (t) =>
|
|
583
|
+
mutateInsertNode(t, parent, idx + offset, { children: [], tagName: "p" }),
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Copy and Cut — the two clipboard verbs, defined ONCE, beside the functions they call.
|
|
589
|
+
*
|
|
590
|
+
* They were defined twice: here, as part of the element menu's family, and in
|
|
591
|
+
* `editor/shortcuts.ts`'s chord table — same ids, same chords, both calling the `copyNode()` and
|
|
592
|
+
* `cutNode()` exported from this file. The two registries never met, so the duplicate ids were a
|
|
593
|
+
* debt rather than a crash; composing the element family into the app registry collected it.
|
|
594
|
+
*
|
|
595
|
+
* `edit.cut` is VISIBLE on the document root and disabled there with its `requires` sentence,
|
|
596
|
+
* rather than hidden — a greyed row that explains itself teaches, an absent one does not.
|
|
597
|
+
*/
|
|
598
|
+
export function clipboardCommands(): AnyCommand[] {
|
|
599
|
+
return [
|
|
600
|
+
{
|
|
601
|
+
category: "Edit",
|
|
602
|
+
group: "1_clipboard",
|
|
603
|
+
id: "edit.copy",
|
|
604
|
+
keybinding: "mod+c",
|
|
605
|
+
keyScope: "canvas",
|
|
606
|
+
level: "selection",
|
|
607
|
+
menus: ["context/element", "palette"],
|
|
608
|
+
requires: "an element selection",
|
|
609
|
+
run: () => {
|
|
610
|
+
void copyNode();
|
|
611
|
+
},
|
|
612
|
+
title: "Copy",
|
|
613
|
+
undo: "none",
|
|
614
|
+
when: hasSelection,
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
category: "Edit",
|
|
618
|
+
destructive: true,
|
|
619
|
+
enablement: hasElementSelection,
|
|
620
|
+
group: "1_clipboard",
|
|
621
|
+
id: "edit.cut",
|
|
622
|
+
keybinding: "mod+x",
|
|
623
|
+
keyScope: "canvas",
|
|
624
|
+
level: "selection",
|
|
625
|
+
menus: ["context/element", "palette"],
|
|
626
|
+
requires: "an element selection that is not the document root",
|
|
627
|
+
run: () => {
|
|
628
|
+
void cutNode();
|
|
629
|
+
},
|
|
630
|
+
title: "Cut",
|
|
631
|
+
undo: "document",
|
|
632
|
+
when: hasSelection,
|
|
633
|
+
},
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Define the element menu's commands on `registry`. Throws on a duplicate id or chord clash. */
|
|
638
|
+
export function registerElementCommands(registry: CommandRegistry, deps: ElementCommandDeps): void {
|
|
639
|
+
registry.registerAll(elementCommands(deps));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ─── The live registry ───────────────────────────────────────────────────────
|
|
643
|
+
|
|
644
|
+
/** The node the currently open menu addresses. */
|
|
645
|
+
let _target: ElementMenuTarget | null = null;
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* How the FALLBACK registry opens a component document.
|
|
649
|
+
*
|
|
650
|
+
* Set by `setContextMenuNavigate` from the bootstrap. In the running app `contextMenuRegistry()`
|
|
651
|
+
* returns the app's registry and this is never read; it exists so the fallback the tests and the
|
|
652
|
+
* pre-bootstrap window use is not a registry with a lying no-op in it.
|
|
653
|
+
*/
|
|
654
|
+
let _navigate: ((path: string) => void | Promise<void>) | null = null;
|
|
655
|
+
|
|
656
|
+
/** Publish how the fallback registry opens a component document. */
|
|
657
|
+
export function setContextMenuNavigate(
|
|
658
|
+
navigate: ((path: string) => void | Promise<void>) | null,
|
|
659
|
+
): void {
|
|
660
|
+
_navigate = navigate;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* The node these verbs act on: the open menu's target, or — when no menu is open — the SELECTION.
|
|
665
|
+
*
|
|
666
|
+
* The fallback is what makes these eight records reachable from anywhere but a right-click. Every
|
|
667
|
+
* one of them declares `menus: ["context/element", "palette"]` and the palette never listed one,
|
|
668
|
+
* because `target()` returned `null` outside the menu and each record's `when` reads it. "Paste
|
|
669
|
+
* Style" and "Convert to Component" were pointer-only verbs advertising themselves as palette
|
|
670
|
+
* entries, in a plan whose first principle is that a capability has exactly one definition site and
|
|
671
|
+
* every surface projects it.
|
|
672
|
+
*
|
|
673
|
+
* A right-click already writes the selection it targets (see `openElementMenu`), so the two agree
|
|
674
|
+
* by construction rather than by coincidence.
|
|
675
|
+
*/
|
|
676
|
+
function commandTarget(): ElementMenuTarget | null {
|
|
677
|
+
if (_target) {
|
|
678
|
+
return _target;
|
|
679
|
+
}
|
|
680
|
+
const tab = activeTab.value;
|
|
681
|
+
const path = primarySelection(tab?.session.selection);
|
|
682
|
+
if (!tab || !path) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
const node = getNodeAtPath(tab.doc.document, path) as JxMutableNode | undefined;
|
|
686
|
+
return node ? { node, path } : null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Deps bound to the live modules. */
|
|
690
|
+
const liveDeps: ElementCommandDeps = {
|
|
691
|
+
componentPathFor: (tagName) =>
|
|
692
|
+
// A custom-element tag is the only thing that CAN name a component; the registry lookup is
|
|
693
|
+
// What decides whether it does.
|
|
694
|
+
(tagName.includes("-") && componentRegistry.find((c) => c.tagName === tagName)?.path) || null,
|
|
695
|
+
styleClipboard: () => workspace.styleClipboard,
|
|
696
|
+
target: commandTarget,
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* The eight element records, bound to the live modules, for the APP registry.
|
|
701
|
+
*
|
|
702
|
+
* `contextMenuRegistry()` below still builds its own — the menu needs a context whose selection
|
|
703
|
+
* facts describe the right-clicked node rather than the app's ambient one — but the records are
|
|
704
|
+
* these, so the palette row and the menu row are one definition rather than two that drift.
|
|
705
|
+
*/
|
|
706
|
+
export function liveElementCommands(): AnyCommand[] {
|
|
707
|
+
return elementCommands(liveDeps);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* The facts the predicates read, derived from the open menu's target.
|
|
712
|
+
*
|
|
713
|
+
* `selection.isRoot` is the context's "this selection has no removable position" flag — it is what
|
|
714
|
+
* `selection.delete` gates on — so it reports true for a repeater template as well as for the
|
|
715
|
+
* document root. Both are unspliceable, and treating only the literal root as root would let Delete
|
|
716
|
+
* splice a template at NaN (see {@link isSpliceablePath}).
|
|
717
|
+
*/
|
|
718
|
+
function liveContext(): CommandContext {
|
|
719
|
+
const target = commandTarget();
|
|
720
|
+
const tab = activeTab.value;
|
|
721
|
+
// One read, two consumers — a chosen tag reads as `a|div`, which matches no component id.
|
|
722
|
+
const targetTag = displayTagName(target?.node.tagName);
|
|
723
|
+
|
|
724
|
+
return makeContext({
|
|
725
|
+
// The menu's context has to carry capabilities too, now that it renders a record gated on one:
|
|
726
|
+
// `makeContext` defaults every capability to false, which would hide Find Usages on every host.
|
|
727
|
+
capability: { findReferences: usagesSupported() },
|
|
728
|
+
document: { dirty: Boolean(tab?.doc.dirty), open: Boolean(tab) },
|
|
729
|
+
/* The editor the menu was opened over. `makeContext` defaults this to "none", which was a
|
|
730
|
+
falsehood the records here got away with only because every one of them gates on the menu's
|
|
731
|
+
own target instead: a verb that reads `editor.kind` — as the canvas verbs in `shortcuts.ts`
|
|
732
|
+
now do — would have been dead in this menu and live over a settings document, which is
|
|
733
|
+
exactly backwards. The menu opens from the canvas AND from the Outline, so the kind comes
|
|
734
|
+
from the tab rather than being asserted. */
|
|
735
|
+
editor: { kind: tab ? editorKindForMode(tab.session.ui.canvasMode) : "none" },
|
|
736
|
+
selection: {
|
|
737
|
+
count: target ? 1 : 0,
|
|
738
|
+
isComponentInstance: Boolean(target && liveDeps.componentPathFor(targetTag)),
|
|
739
|
+
isRepeater: target ? isArrayNode(target.node) : false,
|
|
740
|
+
isRoot: target ? !isSpliceablePath(target.path) : false,
|
|
741
|
+
kind: targetTag,
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
let _registry: CommandRegistry | null = null;
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* The registry the menu renders.
|
|
750
|
+
*
|
|
751
|
+
* It carries this file's records PLUS every `context/element` record from `commands/defaults.ts`
|
|
752
|
+
* (Duplicate and Delete). Those two have their one definition site there; until a bootstrap
|
|
753
|
+
* composes every contribution point into a single app-wide registry, the menu builds its own and
|
|
754
|
+
* supplies real implementations for exactly the verbs it renders.
|
|
755
|
+
*/
|
|
756
|
+
export function contextMenuRegistry(): CommandRegistry {
|
|
757
|
+
/*
|
|
758
|
+
* THE APP'S REGISTRY, when there is one — §5.5's "the file keeps positioning and popover
|
|
759
|
+
* rendering only".
|
|
760
|
+
*
|
|
761
|
+
* The private registry below existed because the menu's context had to describe the RIGHT-CLICKED
|
|
762
|
+
* node while the app's described the ambient selection. Those are the same node now:
|
|
763
|
+
* `openElementMenu` writes the selection it targets before opening, and `commandTarget()` reads
|
|
764
|
+
* the selection when no menu is open. So the divergence the second registry was built to survive
|
|
765
|
+
* no longer exists, and keeping it meant the menu rendered a different `edit.copy` from the one
|
|
766
|
+
* ⌘C runs.
|
|
767
|
+
*
|
|
768
|
+
* It also means the menu renders every `context/element` record in the app — including ones
|
|
769
|
+
* contributed after this file loads — instead of the four families it happened to import.
|
|
770
|
+
*/
|
|
771
|
+
const app = activeRegistry();
|
|
772
|
+
if (app) {
|
|
773
|
+
return app;
|
|
774
|
+
}
|
|
775
|
+
if (_registry) {
|
|
776
|
+
return _registry;
|
|
777
|
+
}
|
|
778
|
+
const registry = createCommandRegistry({ getContext: liveContext });
|
|
779
|
+
const inherited = defaultCommands({
|
|
780
|
+
...noopCommandDeps(),
|
|
781
|
+
// Neither implementation re-checks spliceability. `selection.delete` and `selection.duplicate`
|
|
782
|
+
// Both declare `enablement: structurallyEditable`, which reads `selection.isRoot` — and
|
|
783
|
+
// {@link liveContext} sets that from the very predicate a hand-guard here would repeat.
|
|
784
|
+
// `registry.run` throws `CommandUnavailableError` before a disabled record's `run` is reached,
|
|
785
|
+
// So the guard was unreachable, and a second copy of the invariant is how the two come to
|
|
786
|
+
// Disagree.
|
|
787
|
+
deleteSelection: () => {
|
|
788
|
+
const target = _target;
|
|
789
|
+
if (target) {
|
|
790
|
+
transactDoc(activeTab.value, (t) => mutateRemoveNode(t, target.path));
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
duplicateSelection: () => {
|
|
794
|
+
const target = _target;
|
|
795
|
+
if (target) {
|
|
796
|
+
transactDoc(activeTab.value, (t) => mutateDuplicateNode(t, target.path));
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
}).filter((command) => (command.menus ?? []).includes("context/element"));
|
|
800
|
+
registry.registerAll(inherited);
|
|
801
|
+
// Find Usages has its one definition site in `panels/properties-panel.ts`, beside the section it
|
|
802
|
+
// Opens. Pulling its `context/element` records in here — the same move `defaultCommands` gets
|
|
803
|
+
// Above — is what keeps the menu row and the palette entry a single record rather than two that
|
|
804
|
+
// Drift.
|
|
805
|
+
registry.registerAll(
|
|
806
|
+
inspectorCommands().filter((command) => (command.menus ?? []).includes("context/element")),
|
|
807
|
+
);
|
|
808
|
+
/* The clipboard pair and the two component verbs, from the modules that own them. Each was
|
|
809
|
+
DEFINED a second time in this file until composing the element family into the app registry
|
|
810
|
+
turned the duplicate ids into a hard failure — which is what a "debt with a name" becomes when
|
|
811
|
+
the thing it was waiting for arrives. */
|
|
812
|
+
registry.registerAll(clipboardCommands());
|
|
813
|
+
registerSelectionCommands(registry, {
|
|
814
|
+
convertToComponent: () => convertToComponent(),
|
|
815
|
+
// The same seam `panels/block-action-bar.ts`'s own fallback uses: the host that opened the menu
|
|
816
|
+
// Supplies navigation, and in the real app this whole registry is bypassed for the app's.
|
|
817
|
+
navigateToComponent: (path) => {
|
|
818
|
+
void _navigate?.(path);
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
registerElementCommands(registry, liveDeps);
|
|
822
|
+
_registry = registry;
|
|
823
|
+
return registry;
|
|
236
824
|
}
|
|
237
825
|
|
|
238
826
|
// ─── Context menu ─────────────────────────────────────────────────────────────
|
|
239
827
|
|
|
828
|
+
/** One rendered row: a record plus everything the registry says about it right now. */
|
|
829
|
+
interface MenuRow {
|
|
830
|
+
command: AnyCommand;
|
|
831
|
+
enabled: boolean;
|
|
832
|
+
/** The `requires` sentence, printed as a greyed subtitle when the row is disabled. */
|
|
833
|
+
reason: string | undefined;
|
|
834
|
+
chord: string | undefined;
|
|
835
|
+
/** Whether a divider opens this row's group. */
|
|
836
|
+
dividerAbove: boolean;
|
|
837
|
+
}
|
|
838
|
+
|
|
240
839
|
let _ctxHandle: ReturnType<typeof renderPopover> | null = null;
|
|
840
|
+
let _rows: MenuRow[] = [];
|
|
841
|
+
let _activeIdx = 0;
|
|
842
|
+
/** Whatever owned the keyboard before the menu took it, so it can be handed back. */
|
|
843
|
+
let _opener: HTMLElement | null = null;
|
|
844
|
+
|
|
845
|
+
/** The rendered `sp-menu-item` elements, in row order. */
|
|
846
|
+
function itemElements(): HTMLElement[] {
|
|
847
|
+
return _ctxHandle
|
|
848
|
+
? [..._ctxHandle.host.querySelectorAll<HTMLElement>("sp-menu-item[data-command-id]")]
|
|
849
|
+
: [];
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Move the roving tabindex (and the caret) to `index`, wrapping at both ends. */
|
|
853
|
+
function focusItem(index: number): void {
|
|
854
|
+
const items = itemElements();
|
|
855
|
+
if (items.length === 0) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const next = ((index % items.length) + items.length) % items.length;
|
|
859
|
+
for (const [at, item] of items.entries()) {
|
|
860
|
+
item.tabIndex = at === next ? 0 : -1;
|
|
861
|
+
item.toggleAttribute("focused", at === next);
|
|
862
|
+
}
|
|
863
|
+
_activeIdx = next;
|
|
864
|
+
items[next]?.focus();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** Run the row's command, then close. Disabled rows explain themselves and do nothing. */
|
|
868
|
+
function activateRow(index: number): void {
|
|
869
|
+
const row = _rows[index];
|
|
870
|
+
if (!row?.enabled) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
// Run BEFORE dismissing: every `run` reads `deps.target()` synchronously on entry, and dismissal
|
|
874
|
+
// Clears the target.
|
|
875
|
+
const result = contextMenuRegistry().run(row.command.id);
|
|
876
|
+
dismissContextMenu();
|
|
877
|
+
void result;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* The menu keyboard contract: Up/Down/Home/End move, Enter/Space activate, Escape and Tab dismiss.
|
|
882
|
+
*
|
|
883
|
+
* Bound on `document` in the CAPTURE phase, the same shape `editor/slash-menu.ts` uses: the app's
|
|
884
|
+
* own Escape (which walks the selection ladder) and the canvas arrow-key nudges must not also fire,
|
|
885
|
+
* and a bubble-phase handler on the popover would race Spectrum's own menu key handling.
|
|
886
|
+
*/
|
|
887
|
+
function onMenuKeydown(e: KeyboardEvent): void {
|
|
888
|
+
if (!_ctxHandle) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
switch (e.key) {
|
|
892
|
+
case "ArrowDown": {
|
|
893
|
+
focusItem(_activeIdx + 1);
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
case "ArrowUp": {
|
|
897
|
+
focusItem(_activeIdx - 1);
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
case "Home": {
|
|
901
|
+
focusItem(0);
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
case "End": {
|
|
905
|
+
focusItem(_rows.length - 1);
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
case "Enter":
|
|
909
|
+
case " ": {
|
|
910
|
+
activateRow(_activeIdx);
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
case "Escape":
|
|
914
|
+
case "Tab": {
|
|
915
|
+
dismissContextMenu();
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
default: {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
e.preventDefault();
|
|
923
|
+
e.stopPropagation();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** Drop the menu's global state and hand the keyboard back to whatever opened it. */
|
|
927
|
+
function teardownMenu(): void {
|
|
928
|
+
document.removeEventListener("keydown", onMenuKeydown, true);
|
|
929
|
+
const opener = _opener;
|
|
930
|
+
_target = null;
|
|
931
|
+
_rows = [];
|
|
932
|
+
_activeIdx = 0;
|
|
933
|
+
_opener = null;
|
|
934
|
+
// Only when the menu still held the keyboard: an outside CLICK has already moved focus somewhere
|
|
935
|
+
// The author chose, and yanking it back to the opener would fight them.
|
|
936
|
+
const active = document.activeElement;
|
|
937
|
+
if (opener?.isConnected && (!active || active === document.body)) {
|
|
938
|
+
opener.focus();
|
|
939
|
+
}
|
|
940
|
+
}
|
|
241
941
|
|
|
242
942
|
/** Dismiss the context menu if open. */
|
|
243
943
|
export function dismissContextMenu() {
|
|
244
|
-
if (_ctxHandle) {
|
|
245
|
-
|
|
246
|
-
_ctxHandle = null;
|
|
944
|
+
if (!_ctxHandle) {
|
|
945
|
+
return;
|
|
247
946
|
}
|
|
947
|
+
const handle = _ctxHandle;
|
|
948
|
+
_ctxHandle = null;
|
|
949
|
+
handle.dismiss();
|
|
950
|
+
teardownMenu();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** Ask the registry what belongs in `placement` right now and decorate each record for rendering. */
|
|
954
|
+
function buildRows(placement: Placement): MenuRow[] {
|
|
955
|
+
const registry = contextMenuRegistry();
|
|
956
|
+
let group: string | undefined;
|
|
957
|
+
return registry.forPlacement(placement).map((command, index) => {
|
|
958
|
+
const dividerAbove = index > 0 && command.group !== group;
|
|
959
|
+
({ group } = command);
|
|
960
|
+
const enabled = registry.isEnabled(command.id);
|
|
961
|
+
const chord = registry.keymap.formatBinding(command.id);
|
|
962
|
+
return {
|
|
963
|
+
// A chord that just restates the row's own name ("Delete" bound to Delete) teaches nothing
|
|
964
|
+
// And reads as a stutter, so it is not printed.
|
|
965
|
+
chord: chord?.toLowerCase() === command.title.toLowerCase() ? undefined : chord,
|
|
966
|
+
command,
|
|
967
|
+
dividerAbove,
|
|
968
|
+
enabled,
|
|
969
|
+
reason: enabled ? undefined : registry.disabledReason(command.id),
|
|
970
|
+
};
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** One row. Everything it prints comes off the record — nothing is passed in per call site. */
|
|
975
|
+
function rowTemplate(row: MenuRow, index: number) {
|
|
976
|
+
return html`${
|
|
977
|
+
row.dividerAbove ? html`<sp-menu-divider role="separator"></sp-menu-divider>` : nothing
|
|
978
|
+
}<sp-menu-item
|
|
979
|
+
role="menuitem"
|
|
980
|
+
data-command-id=${row.command.id}
|
|
981
|
+
tabindex=${index === 0 ? 0 : -1}
|
|
982
|
+
?disabled=${!row.enabled}
|
|
983
|
+
aria-disabled=${row.enabled ? "false" : "true"}
|
|
984
|
+
style=${row.command.destructive && row.enabled ? "color: var(--danger)" : ""}
|
|
985
|
+
@click=${() => activateRow(index)}
|
|
986
|
+
>${row.command.title}${
|
|
987
|
+
row.chord
|
|
988
|
+
? html`<kbd slot="value" style="color: var(--fg-dim)">${row.chord}</kbd>`
|
|
989
|
+
: nothing
|
|
990
|
+
}${
|
|
991
|
+
row.reason ? html`<span slot="description">Needs ${row.reason}</span>` : nothing
|
|
992
|
+
}</sp-menu-item
|
|
993
|
+
>`;
|
|
248
994
|
}
|
|
249
995
|
|
|
250
996
|
/**
|
|
251
997
|
* @param {MouseEvent} e
|
|
252
998
|
* @param {JxPath} path
|
|
253
|
-
* @param {{
|
|
999
|
+
* @param {{
|
|
1000
|
+
* rerender?: () => void;
|
|
1001
|
+
* placement?: Placement;
|
|
1002
|
+
* }} [opts]
|
|
254
1003
|
*/
|
|
255
1004
|
export function showContextMenu(
|
|
256
1005
|
e: MouseEvent,
|
|
257
1006
|
path: JxPath,
|
|
258
1007
|
opts: {
|
|
259
|
-
onEditComponent?: (path: string) => void;
|
|
260
1008
|
rerender?: () => void;
|
|
1009
|
+
/** Which registry placement to render. The canvas and the outline are both element menus. */
|
|
1010
|
+
placement?: Placement;
|
|
261
1011
|
} = {},
|
|
262
1012
|
) {
|
|
263
1013
|
e.preventDefault();
|
|
@@ -272,169 +1022,24 @@ export function showContextMenu(
|
|
|
272
1022
|
return;
|
|
273
1023
|
}
|
|
274
1024
|
|
|
275
|
-
// Select the node
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const idxIsNumber = typeof childIndex(path) === "number";
|
|
282
|
-
|
|
283
|
-
const items: { label: string; action?: () => void | Promise<void>; danger?: boolean }[] = [
|
|
284
|
-
{ action: () => copyNode(), label: "Copy" },
|
|
285
|
-
];
|
|
286
|
-
|
|
287
|
-
if (path.length >= 2 && idxIsNumber) {
|
|
288
|
-
items.push(
|
|
289
|
-
{ action: () => cutNode(), label: "Cut" },
|
|
290
|
-
{
|
|
291
|
-
action: () => transactDoc(activeTab.value, (t) => mutateDuplicateNode(t, path)),
|
|
292
|
-
label: "Duplicate",
|
|
293
|
-
},
|
|
294
|
-
);
|
|
295
|
-
if (node.style) {
|
|
296
|
-
const nodeStyle = node.style;
|
|
297
|
-
items.push({
|
|
298
|
-
action: () => {
|
|
299
|
-
workspace.styleClipboard = jsonClone(nodeStyle);
|
|
300
|
-
statusMessage("Styles copied");
|
|
301
|
-
},
|
|
302
|
-
label: "Copy styles",
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
if (workspace.styleClipboard) {
|
|
306
|
-
items.push({
|
|
307
|
-
action: () => {
|
|
308
|
-
if (!workspace.styleClipboard) {
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
const style = jsonClone(workspace.styleClipboard);
|
|
312
|
-
transactDoc(activeTab.value, (t) => mutateReplaceStyle(t, path, style));
|
|
313
|
-
statusMessage("Styles pasted");
|
|
314
|
-
},
|
|
315
|
-
label: "Paste styles",
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
// Separator
|
|
319
|
-
items.push(
|
|
320
|
-
{ label: "—" },
|
|
321
|
-
{
|
|
322
|
-
action: () => {
|
|
323
|
-
const pp = parentElementPath(path) as JxPath;
|
|
324
|
-
const idx = childIndex(path) as number;
|
|
325
|
-
transactDoc(activeTab.value, (t) =>
|
|
326
|
-
mutateInsertNode(t, pp, idx, { children: [], tagName: "p" }),
|
|
327
|
-
);
|
|
328
|
-
},
|
|
329
|
-
label: "Insert before",
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
action: () => {
|
|
333
|
-
const pp = parentElementPath(path) as JxPath;
|
|
334
|
-
const idx = childIndex(path) as number;
|
|
335
|
-
transactDoc(activeTab.value, (t) =>
|
|
336
|
-
mutateInsertNode(t, pp, idx + 1, { children: [], tagName: "p" }),
|
|
337
|
-
);
|
|
338
|
-
},
|
|
339
|
-
label: "Insert after",
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
action: () => transactDoc(activeTab.value, (t) => mutateWrapNode(t, path)),
|
|
343
|
-
label: "Wrap in Div",
|
|
344
|
-
},
|
|
345
|
-
);
|
|
346
|
-
// Don't offer Repeat on a repeater template (path tail "map") or on an array node itself.
|
|
347
|
-
if (path.at(-1) !== "map" && (node as JxMutableNode).$prototype !== "Array") {
|
|
348
|
-
items.push({
|
|
349
|
-
action: () => convertToRepeater(),
|
|
350
|
-
label: "Repeat...",
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
items.push({
|
|
354
|
-
action: async () => {
|
|
355
|
-
if (opts.rerender) {
|
|
356
|
-
// Lazy import breaks the context-menu ↔ layers-panel module cycle
|
|
357
|
-
const { startLayerTitleEdit } = await import("../panels/layers-panel");
|
|
358
|
-
startLayerTitleEdit(path, opts.rerender);
|
|
359
|
-
}
|
|
360
|
-
},
|
|
361
|
-
label: "Set Title",
|
|
362
|
-
});
|
|
363
|
-
if (node.tagName) {
|
|
364
|
-
const isComponent =
|
|
365
|
-
node.tagName.includes("-") &&
|
|
366
|
-
componentRegistry.some(
|
|
367
|
-
(/** @type {{ tagName: string }} */ c) => c.tagName === node.tagName,
|
|
368
|
-
);
|
|
369
|
-
if (isComponent && opts.onEditComponent) {
|
|
370
|
-
const comp = componentRegistry.find(
|
|
371
|
-
(/** @type {{ tagName: string; path: string }} */ c) => c.tagName === node.tagName,
|
|
372
|
-
);
|
|
373
|
-
items.push({
|
|
374
|
-
action: () => opts.onEditComponent?.(comp?.path as string),
|
|
375
|
-
label: "Edit Component",
|
|
376
|
-
});
|
|
377
|
-
} else if (!isComponent) {
|
|
378
|
-
items.push({
|
|
379
|
-
action: () => convertToComponent(),
|
|
380
|
-
label: "Convert to Component",
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
// Separator
|
|
385
|
-
items.push(
|
|
386
|
-
{ label: "—" },
|
|
387
|
-
{
|
|
388
|
-
action: () => transactDoc(activeTab.value, (t) => mutateRemoveNode(t, path)),
|
|
389
|
-
danger: true,
|
|
390
|
-
label: "Delete",
|
|
391
|
-
},
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
// Paste targets — never into/after an array node (its content is the single map template).
|
|
395
|
-
if (path.length >= 2 && (node as JxMutableNode).$prototype !== "Array") {
|
|
396
|
-
items.push(
|
|
397
|
-
{ label: "—" },
|
|
398
|
-
{
|
|
399
|
-
action: async () => {
|
|
400
|
-
const nodes = await readFromClipboard();
|
|
401
|
-
if (!nodes || nodes.length === 0) {
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const idx = Array.isArray(node.children) ? node.children.length : 0;
|
|
405
|
-
transactDoc(activeTab.value, (t) => {
|
|
406
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
407
|
-
mutateInsertNode(t, path, idx + i, nodes[i]!);
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
statusMessage("Pasted");
|
|
411
|
-
},
|
|
412
|
-
label: "Paste inside",
|
|
413
|
-
},
|
|
414
|
-
);
|
|
415
|
-
if (idxIsNumber) {
|
|
416
|
-
items.push({
|
|
417
|
-
action: async () => {
|
|
418
|
-
const nodes = await readFromClipboard();
|
|
419
|
-
if (!nodes || nodes.length === 0) {
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
const pp = parentElementPath(path) as JxPath;
|
|
423
|
-
const idx = childIndex(path) as number;
|
|
424
|
-
transactDoc(activeTab.value, (t) => {
|
|
425
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
426
|
-
mutateInsertNode(t, pp, idx + 1 + i, nodes[i]!);
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
statusMessage("Pasted");
|
|
430
|
-
},
|
|
431
|
-
label: "Paste after",
|
|
432
|
-
});
|
|
433
|
-
}
|
|
1025
|
+
// Select the node — the menu addresses it, and so does everything the commands read. A
|
|
1026
|
+
// Right-click INSIDE an existing multi-selection keeps that selection, the way every list does:
|
|
1027
|
+
// Collapsing six selected cards to one because the user aimed at one of them would silently
|
|
1028
|
+
// Retarget the very batch commands the menu is about to offer.
|
|
1029
|
+
if (!isSelected(tab.session.selection, path)) {
|
|
1030
|
+
tab.session.selection = [path];
|
|
434
1031
|
}
|
|
1032
|
+
_target = { node, path, rerender: opts.rerender };
|
|
1033
|
+
_rows = buildRows(opts.placement ?? "context/element");
|
|
1034
|
+
if (_rows.length === 0) {
|
|
1035
|
+
_target = null;
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
_activeIdx = 0;
|
|
1039
|
+
_opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
435
1040
|
|
|
436
|
-
let x = e.clientX
|
|
437
|
-
|
|
1041
|
+
let x = e.clientX;
|
|
1042
|
+
let y = e.clientY;
|
|
438
1043
|
|
|
439
1044
|
_ctxHandle = renderPopover(
|
|
440
1045
|
html`<sp-popover
|
|
@@ -458,26 +1063,21 @@ export function showContextMenu(
|
|
|
458
1063
|
});
|
|
459
1064
|
})}
|
|
460
1065
|
>
|
|
461
|
-
<sp-menu>
|
|
462
|
-
${
|
|
463
|
-
item.label === "—"
|
|
464
|
-
? html`<sp-menu-divider></sp-menu-divider>`
|
|
465
|
-
: html`<sp-menu-item
|
|
466
|
-
style=${item.danger ? "color: var(--danger)" : ""}
|
|
467
|
-
@click=${() => {
|
|
468
|
-
dismissContextMenu();
|
|
469
|
-
void item.action?.();
|
|
470
|
-
}}
|
|
471
|
-
>${item.label}</sp-menu-item
|
|
472
|
-
>`,
|
|
473
|
-
)}
|
|
1066
|
+
<sp-menu role="menu" aria-label="Element actions">
|
|
1067
|
+
${_rows.map((row, index) => rowTemplate(row, index))}
|
|
474
1068
|
</sp-menu>
|
|
475
1069
|
</sp-popover>`,
|
|
476
1070
|
{
|
|
477
1071
|
dismissOnOutsideClick: true,
|
|
478
1072
|
onDismiss: () => {
|
|
479
1073
|
_ctxHandle = null;
|
|
1074
|
+
teardownMenu();
|
|
480
1075
|
},
|
|
481
1076
|
},
|
|
482
1077
|
);
|
|
1078
|
+
|
|
1079
|
+
document.addEventListener("keydown", onMenuKeydown, true);
|
|
1080
|
+
// A frame later: lit has committed the items, and focusing the first row is what makes the whole
|
|
1081
|
+
// Keyboard contract reachable at all (the menu used to open with focus left behind it).
|
|
1082
|
+
requestAnimationFrame(() => focusItem(0));
|
|
483
1083
|
}
|