@jxsuite/studio 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/canvas.html +21 -0
- package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
- package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
- package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
- package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
- package/dist/chunks/{components-799jpvhm.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-799jpvhm.js.map → components-2bpqp4mq.js.map} +1 -1
- package/dist/chunks/content-source-34gnkbnn.js +38 -0
- package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
- package/dist/chunks/extension-sections-6mqydz35.js +301 -0
- package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
- package/dist/chunks/files-8kn321h1.js +70 -0
- package/dist/chunks/files-8kn321h1.js.map +9 -0
- package/dist/chunks/{format-host-8jxwts2p.js → format-host-mkwsmt8d.js} +3 -3
- package/dist/chunks/git-panel-tjbr191d.js +51 -0
- package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
- package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
- package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
- package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
- package/dist/chunks/layers-panel-87pb25km.js +58 -0
- package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
- package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
- package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
- package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
- package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
- package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
- package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
- package/dist/chunks/right-panel-zpdxd98t.js +37 -0
- package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
- package/dist/chunks/section-registry-zmckaxf4.js +30 -0
- package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
- package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
- package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
- package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
- package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
- package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
- package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
- package/dist/chunks/studio-41exgzhq.js +6690 -0
- package/dist/chunks/studio-41exgzhq.js.map +38 -0
- package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
- package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
- package/dist/chunks/studio-50k77cca.js +1787 -0
- package/dist/chunks/studio-50k77cca.js.map +18 -0
- package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
- package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
- package/dist/chunks/studio-6f0am8f2.js +53 -0
- package/dist/chunks/studio-6f0am8f2.js.map +10 -0
- package/dist/chunks/studio-6nmjc5qk.js +7002 -0
- package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
- package/dist/chunks/studio-7afd21nw.js +67 -0
- package/dist/chunks/studio-7afd21nw.js.map +10 -0
- package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
- package/dist/chunks/studio-7bmdvzne.js.map +10 -0
- package/dist/chunks/studio-8jpzqk5s.js +128 -0
- package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
- package/dist/chunks/studio-94m77tgk.js +1482 -0
- package/dist/chunks/studio-94m77tgk.js.map +13 -0
- package/dist/chunks/studio-9je6w5sq.js +1000 -0
- package/dist/chunks/studio-9je6w5sq.js.map +16 -0
- package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
- package/dist/chunks/{studio-gs6kqdt1.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-gs6kqdt1.js.map → studio-adn0z1k3.js.map} +3 -3
- package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
- package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
- package/dist/chunks/studio-bw5dz6km.js +608 -0
- package/dist/chunks/studio-bw5dz6km.js.map +21 -0
- package/dist/chunks/studio-c6s5getc.js +4232 -0
- package/dist/chunks/studio-c6s5getc.js.map +39 -0
- package/dist/chunks/studio-cjm7fdjf.js +451 -0
- package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
- package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
- package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
- package/dist/chunks/studio-dg7zyt5x.js +28 -0
- package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
- package/dist/chunks/studio-dzcyvvvz.js +166 -0
- package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
- package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
- package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
- package/dist/chunks/studio-f12h6177.js +411 -0
- package/dist/chunks/studio-f12h6177.js.map +13 -0
- package/dist/chunks/studio-hbp1s8m9.js +1817 -0
- package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
- package/dist/chunks/studio-k2sxjph5.js +15295 -0
- package/dist/chunks/studio-k2sxjph5.js.map +235 -0
- package/dist/chunks/studio-myje51t3.js +385 -0
- package/dist/chunks/studio-myje51t3.js.map +12 -0
- package/dist/chunks/studio-na6rwzhw.js +353 -0
- package/dist/chunks/studio-na6rwzhw.js.map +11 -0
- package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
- package/dist/chunks/studio-p471gg5n.js.map +14 -0
- package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
- package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
- package/dist/chunks/{studio-esrewxap.js → studio-s519zc99.js} +3 -3
- package/dist/chunks/studio-sas7mt3k.js +75 -0
- package/dist/chunks/studio-sas7mt3k.js.map +10 -0
- package/dist/chunks/studio-t5wqwzdh.js +74 -0
- package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
- package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
- package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
- package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
- package/dist/chunks/studio-tr20m8g8.js.map +9 -0
- package/dist/chunks/{studio-v7jkjqng.js → studio-v96qfnww.js} +5514 -4291
- package/dist/chunks/studio-v96qfnww.js.map +54 -0
- package/dist/chunks/studio-vcetc91k.js +659 -0
- package/dist/chunks/studio-vcetc91k.js.map +10 -0
- package/dist/chunks/{studio-jm0z592w.js → studio-vmymjefp.js} +2972 -4937
- package/dist/chunks/studio-vmymjefp.js.map +98 -0
- package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
- package/dist/chunks/studio-x9hmy5kr.js +390 -0
- package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
- package/dist/chunks/studio-xa0ce0n7.js +335 -0
- package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
- package/dist/chunks/studio-xm3dbxvn.js +378 -0
- package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
- package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
- package/dist/chunks/studio-zv37yrfa.js +1054 -0
- package/dist/chunks/studio-zv37yrfa.js.map +13 -0
- package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
- package/dist/chunks/ws-client-654ts66j.js.map +11 -0
- package/dist/iframe-entry.js +668 -230
- package/dist/studio.js +21080 -32585
- package/index.html +35 -4685
- package/package.json +17 -15
- package/src/about/about-modal.ts +42 -11
- package/src/browse/library-commands.ts +240 -0
- package/src/browse/library-layouts.ts +369 -0
- package/src/browse/library-model.ts +396 -0
- package/src/browse/library-pane.ts +1049 -0
- package/src/browse/library-preview.ts +252 -0
- package/src/browse/library-source.ts +150 -0
- package/src/canvas/canvas-helpers.ts +33 -9
- package/src/canvas/canvas-live-render.ts +45 -29
- package/src/canvas/canvas-patcher.ts +151 -31
- package/src/canvas/canvas-perf.ts +46 -7
- package/src/canvas/canvas-render.ts +1092 -220
- package/src/canvas/canvas-surface.ts +240 -0
- package/src/canvas/canvas-utils.ts +1166 -124
- package/src/canvas/iframe-drop.ts +2 -1
- package/src/canvas/iframe-entry.ts +241 -4
- package/src/canvas/iframe-host.ts +1226 -143
- package/src/canvas/iframe-inline-edit.ts +31 -0
- package/src/canvas/iframe-interaction.ts +76 -16
- package/src/canvas/iframe-keys.ts +107 -45
- package/src/canvas/iframe-overlay.ts +38 -1
- package/src/canvas/iframe-protocol.ts +137 -2
- package/src/canvas/iframe-render.ts +70 -2
- package/src/canvas/iframe-slash.ts +6 -0
- package/src/canvas/path-mapping.ts +56 -6
- package/src/canvas/surface-registry.ts +292 -0
- package/src/collab/collab-commands.ts +244 -0
- package/src/collab/collab-session.ts +198 -32
- package/src/collab/collab-state.ts +28 -2
- package/src/collab/presence-chips.ts +100 -9
- package/src/commands/active-registry.ts +42 -0
- package/src/commands/app-commands.ts +176 -0
- package/src/commands/budget.ts +158 -0
- package/src/commands/command-args.ts +253 -0
- package/src/commands/context.ts +387 -0
- package/src/commands/defaults.ts +580 -0
- package/src/commands/keymap.ts +571 -0
- package/src/commands/levels.ts +345 -0
- package/src/commands/live-context.ts +193 -0
- package/src/commands/reference.ts +227 -0
- package/src/commands/registry.ts +293 -0
- package/src/component-props.ts +3 -2
- package/src/content/draft-state.ts +106 -0
- package/src/content/entry-commands.ts +282 -0
- package/src/content/entry-editor.ts +327 -0
- package/src/content/entry-fields.ts +116 -0
- package/src/content/entry-model.ts +175 -0
- package/src/editor/canvas-context-menu.ts +17 -11
- package/src/editor/canvas-slash-bridge.ts +11 -4
- package/src/editor/context-menu.ts +804 -204
- package/src/editor/convert-to-component.ts +31 -10
- package/src/editor/convert-to-repeater.ts +3 -2
- package/src/editor/inline-edit-apply.ts +5 -5
- package/src/editor/inline-edit.ts +86 -50
- package/src/editor/insert-zone-action.ts +1 -1
- package/src/editor/shortcuts.ts +1043 -329
- package/src/editor/slash-menu.ts +1 -0
- package/src/files/elements.ts +166 -0
- package/src/files/file-ops.ts +178 -31
- package/src/files/files.ts +898 -268
- package/src/files/fs-events.ts +21 -0
- package/src/files/media-meta.ts +203 -0
- package/src/files/media-paths.ts +141 -0
- package/src/files/media-upload.ts +7 -3
- package/src/files/media-usage.ts +189 -0
- package/src/github/github-auth.ts +235 -33
- package/src/github/github-publish.ts +80 -21
- package/src/grid/grid-controller.ts +152 -12
- package/src/grid/grid-layout.ts +225 -20
- package/src/grid/grid-open.ts +120 -0
- package/src/grid/grid-panel.ts +554 -39
- package/src/grid/grid-source.ts +27 -1
- package/src/grid/redirects-grid.ts +599 -0
- package/src/grid/redirects.ts +398 -0
- package/src/grid/sources/content-source.ts +108 -2
- package/src/i18n/i18n-commands.ts +322 -0
- package/src/new-project/add-repo-modal.ts +8 -10
- package/src/new-project/import-tab.ts +7 -0
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +221 -245
- package/src/packages/jxsuite-update.ts +97 -48
- package/src/packages/pull-package-sync.ts +18 -9
- package/src/page-params.ts +2 -1
- package/src/panels/activity-bar.ts +152 -91
- package/src/panels/activity-panel.ts +481 -0
- package/src/panels/ai-chat/chat-markdown.ts +8 -3
- package/src/panels/ai-chat/chat-view.ts +245 -18
- package/src/panels/ai-chat/composer.ts +31 -5
- package/src/panels/ai-chat/sessions-view.ts +17 -10
- package/src/panels/ai-panel.ts +279 -53
- package/src/panels/block-action-bar.ts +1115 -224
- package/src/panels/bottom-dock.ts +337 -0
- package/src/panels/canvas-dnd-bridge.ts +37 -8
- package/src/panels/chat-panel.ts +29 -12
- package/src/panels/data-explorer.ts +254 -112
- package/src/panels/data-grid.ts +10 -4
- package/src/panels/dnd.ts +28 -51
- package/src/panels/editors.ts +434 -107
- package/src/panels/elements-panel.ts +66 -2
- package/src/panels/empty-state.ts +112 -0
- package/src/panels/events-panel.ts +596 -82
- package/src/panels/formula-workspace.ts +408 -218
- package/src/panels/frontmatter-fields.ts +51 -10
- package/src/panels/frontmatter-panel.ts +357 -79
- package/src/panels/git-panel.ts +375 -164
- package/src/panels/head-panel.ts +791 -85
- package/src/panels/i18n-panel.ts +592 -0
- package/src/panels/imports-panel.ts +95 -85
- package/src/panels/jump-bar.ts +584 -0
- package/src/panels/layers-panel.ts +1106 -328
- package/src/panels/left-panel.ts +109 -204
- package/src/panels/navigator-panels.ts +131 -0
- package/src/panels/overlays.ts +7 -2
- package/src/panels/pane-context.ts +1477 -0
- package/src/panels/pane-grid.ts +406 -0
- package/src/panels/panel-registry.ts +340 -0
- package/src/panels/panel-scheduler.ts +75 -0
- package/src/panels/problems-panel.ts +234 -0
- package/src/panels/properties-panel.ts +946 -1139
- package/src/panels/provenance.ts +258 -0
- package/src/panels/quick-search.ts +791 -122
- package/src/panels/right-panel.ts +228 -102
- package/src/panels/seo-modal.ts +395 -0
- package/src/panels/settings-pane.ts +203 -0
- package/src/panels/signals-panel.ts +354 -137
- package/src/panels/statement-editor.ts +41 -25
- package/src/panels/statusbar.ts +342 -106
- package/src/panels/style-panel.ts +974 -463
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-doc.ts +4 -2
- package/src/panels/stylebook-layers-panel.ts +3 -2
- package/src/panels/stylebook-panel.ts +51 -26
- package/src/panels/tab-strip.ts +1058 -78
- package/src/panels/target-line.ts +274 -0
- package/src/panels/toolbar.ts +631 -484
- package/src/panels/welcome-screen.ts +301 -198
- package/src/platform-errors.ts +26 -3
- package/src/platform.ts +96 -2
- package/src/platforms/cloud.ts +71 -8
- package/src/platforms/devserver.ts +92 -29
- package/src/publish/deploy-checklist.ts +383 -0
- package/src/publish/pages-service.ts +10 -1
- package/src/publish/publish-commands.ts +218 -0
- package/src/publish/publish-panel.ts +110 -16
- package/src/recent-projects.ts +3 -2
- package/src/services/a11y-report.ts +505 -0
- package/src/services/ai-models.ts +30 -1
- package/src/services/ai-project-tools.ts +29 -10
- package/src/services/ai-system-prompt.ts +53 -14
- package/src/services/ai-tools.ts +39 -3
- package/src/services/ai-writes.ts +154 -0
- package/src/services/announce.ts +97 -0
- package/src/services/automation.ts +424 -175
- package/src/services/cem-export.ts +9 -4
- package/src/services/clock.ts +61 -0
- package/src/services/document-assistant.ts +36 -8
- package/src/services/idle.ts +179 -0
- package/src/services/import-client.ts +26 -0
- package/src/services/monaco-buffer.ts +627 -0
- package/src/services/monaco-lazy.ts +36 -0
- package/src/services/notify.ts +271 -0
- package/src/services/profile.ts +155 -0
- package/src/services/references.ts +0 -0
- package/src/services/token-lint.ts +3 -2
- package/src/services/tool-executor.ts +53 -7
- package/src/services/trusted-types.ts +123 -0
- package/src/settings/contexts-section.ts +427 -0
- package/src/settings/contributed-section.ts +149 -11
- package/src/settings/css-vars-editor.ts +306 -349
- package/src/settings/defs-editor.ts +93 -34
- package/src/settings/dependencies-editor.ts +7 -7
- package/src/settings/extension-sections.ts +37 -5
- package/src/settings/general-settings.ts +204 -121
- package/src/settings/locales-section.ts +347 -0
- package/src/settings/preferences-accounts.ts +140 -0
- package/src/settings/preferences-dialog.ts +620 -0
- package/src/settings/preferences-keymap.ts +270 -0
- package/src/settings/project-sections.ts +260 -0
- package/src/settings/section-registry.ts +169 -0
- package/src/settings/settings-document.ts +385 -0
- package/src/shell.ts +1549 -0
- package/src/site-context.ts +45 -20
- package/src/state.ts +14 -275
- package/src/store.ts +88 -43
- package/src/studio.ts +790 -346
- package/src/style/live-preview.ts +36 -0
- package/src/style/project-styles.ts +349 -0
- package/src/style/token-ref.ts +123 -0
- package/src/tabs/patch-ops.ts +16 -4
- package/src/tabs/project-config.ts +416 -0
- package/src/tabs/selection.ts +323 -0
- package/src/tabs/tab.ts +182 -47
- package/src/tabs/transact.ts +209 -28
- package/src/types.ts +68 -13
- package/src/ui/ai-credentials-form.ts +39 -35
- package/src/ui/ai-managed-connect.ts +7 -1
- package/src/ui/dynamic-slot.ts +224 -81
- package/src/ui/expression-editor.ts +71 -63
- package/src/ui/field-input.ts +4 -6
- package/src/ui/field-row.ts +215 -38
- package/src/ui/form-controls.ts +194 -7
- package/src/ui/layers.ts +492 -69
- package/src/ui/media-picker.ts +115 -25
- package/src/ui/panel-resize.ts +109 -91
- package/src/ui/progress-modal.ts +131 -50
- package/src/ui/regions.ts +462 -0
- package/src/ui/schema-form.ts +229 -45
- package/src/ui/spectrum.ts +24 -8
- package/src/ui/timing.ts +49 -0
- package/src/ui/value-selector.ts +20 -0
- package/src/ui/value-source.ts +348 -0
- package/src/ui/virtual-window.ts +400 -0
- package/src/utils/edit-display.ts +3 -2
- package/src/utils/inherited-style.ts +67 -37
- package/src/view.ts +64 -78
- package/src/workspace/pane-derive.ts +1258 -0
- package/src/workspace/session.ts +317 -0
- package/src/workspace/workspace.ts +1345 -23
- package/styles/canvas.css +653 -0
- package/styles/forced-colors.css +106 -0
- package/styles/inspector.css +1454 -0
- package/styles/overlays.css +2704 -0
- package/styles/panels.css +1701 -0
- package/styles/shell.css +1423 -0
- package/styles/tokens.css +151 -0
- package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
- package/dist/chunks/extension-sections-qr3drkkw.js +0 -531
- package/dist/chunks/extension-sections-qr3drkkw.js.map +0 -12
- package/dist/chunks/files-j8a1t06k.js +0 -50
- package/dist/chunks/files-j8a1t06k.js.map +0 -9
- package/dist/chunks/git-panel-xtq5z81x.js +0 -28
- package/dist/chunks/git-panel-xtq5z81x.js.map +0 -9
- package/dist/chunks/layers-panel-ancn2wpq.js +0 -26
- package/dist/chunks/layers-panel-ancn2wpq.js.map +0 -9
- package/dist/chunks/settings-modal-17ttr96g.js +0 -29
- package/dist/chunks/settings-modal-17ttr96g.js.map +0 -9
- package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
- package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
- package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
- package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
- package/dist/chunks/studio-0xgs7xrs.js +0 -429
- package/dist/chunks/studio-0xgs7xrs.js.map +0 -13
- package/dist/chunks/studio-4d8hzcz5.js +0 -3427
- package/dist/chunks/studio-4d8hzcz5.js.map +0 -26
- package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-jm0z592w.js.map +0 -115
- package/dist/chunks/studio-m2t4mdg5.js +0 -1854
- package/dist/chunks/studio-m2t4mdg5.js.map +0 -18
- package/dist/chunks/studio-n4p11dfy.js +0 -853
- package/dist/chunks/studio-n4p11dfy.js.map +0 -12
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-sjh1wt1a.js +0 -3744
- package/dist/chunks/studio-sjh1wt1a.js.map +0 -22
- package/dist/chunks/studio-v7jkjqng.js.map +0 -46
- package/dist/chunks/studio-xq0t7n5t.js +0 -140
- package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
- package/dist/chunks/studio-xze4zrs1.js.map +0 -10
- package/dist/chunks/studio-yfcstdzy.js +0 -399
- package/dist/chunks/studio-yfcstdzy.js.map +0 -12
- package/dist/chunks/studio-zp7jncs8.js +0 -469
- package/dist/chunks/studio-zp7jncs8.js.map +0 -14
- package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
- package/src/browse/browse-modal.ts +0 -69
- package/src/browse/browse.ts +0 -904
- package/src/canvas/canvas-diff.ts +0 -216
- package/src/canvas/nested-site-style.ts +0 -58
- package/src/new-project/design-fields.ts +0 -264
- package/src/new-project/templates.ts +0 -61
- package/src/panels/tab-bar.ts +0 -548
- package/src/settings/settings-modal.ts +0 -203
- /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
- /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
- /package/dist/chunks/{format-host-8jxwts2p.js.map → format-host-mkwsmt8d.js.map} +0 -0
- /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
- /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
- /package/dist/chunks/{studio-esrewxap.js.map → studio-s519zc99.js.map} +0 -0
- /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
- /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keymap.ts — chords in, command ids out.
|
|
3
|
+
*
|
|
4
|
+
* Three jobs, deliberately in one file because they must agree on one canonical chord string:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Normalisation.** `"Cmd+Shift+P"`, `"meta+shift+p"` and `"mod+shift+P"` are the same chord.
|
|
7
|
+
* Everything is stored as `"mod+shift+p"` — modifiers in a fixed order, key lowercase.
|
|
8
|
+
* 2. **Formatting.** ONE function turns a chord into what the user reads: `⌘⇧P` on mac, `Ctrl+Shift+P`
|
|
9
|
+
* everywhere else. Today `toolbar.ts:301` and `:555` hardcode `⌘P` and show it to Windows and
|
|
10
|
+
* Linux users; `formatChord` is what kills that everywhere at once.
|
|
11
|
+
* 3. **Resolution through a scope stack.** `caret > grid/code engine > focused dock > global`. A chord
|
|
12
|
+
* bound in a narrower scope shadows the same chord in a wider one — that is the mechanism, not
|
|
13
|
+
* an accident, and it is why `keyScope` exists as a field separate from `level`.
|
|
14
|
+
* {@link overlappingScopes} is the same fact read the other way round: which scopes can be live
|
|
15
|
+
* at one instant, and therefore which pairs cannot both answer one chord.
|
|
16
|
+
*
|
|
17
|
+
* Conflicts fail loudly at registration: two commands claiming one chord in ONE scope throws. That
|
|
18
|
+
* is the class of bug that let ⌘W (`editor/shortcuts.ts:192`, refuses to close the last tab) and
|
|
19
|
+
* the tab strip's × (`panels/tab-strip.ts:182`, closes it happily) disagree for a year.
|
|
20
|
+
*
|
|
21
|
+
* 4. **The user's layer.** A rebinding is an OVERRIDE (`setOverrides`), never an edit to a record:
|
|
22
|
+
* `declaredFor(id)` keeps answering what the registry declared, which is what a reset restores
|
|
23
|
+
* and what `specs/studio.md` §15 rule 2 means by the Keyboard sheet being a projection. The two
|
|
24
|
+
* layers meet by one rule — **an override outranks a default, and a default never evicts an
|
|
25
|
+
* override** — so the outcome does not depend on registration order, and a stored override that
|
|
26
|
+
* a later release's new default happens to collide with cannot throw the bootstrap. Two defaults
|
|
27
|
+
* colliding still throws, because that one is a bug in the app and nobody's preference.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { KEY_SCOPES } from "./levels";
|
|
31
|
+
import type { KeyScope } from "./levels";
|
|
32
|
+
|
|
33
|
+
/** Canonical modifier order. `mod` is ⌘ on mac and Ctrl elsewhere — one token, one meaning. */
|
|
34
|
+
const MODIFIER_ORDER = ["mod", "ctrl", "alt", "shift"] as const;
|
|
35
|
+
|
|
36
|
+
type Modifier = (typeof MODIFIER_ORDER)[number];
|
|
37
|
+
|
|
38
|
+
/** Spellings people actually write, mapped onto the canonical four. */
|
|
39
|
+
const MODIFIER_ALIASES: Readonly<Record<string, Modifier>> = {
|
|
40
|
+
mod: "mod",
|
|
41
|
+
cmd: "mod",
|
|
42
|
+
command: "mod",
|
|
43
|
+
meta: "mod",
|
|
44
|
+
super: "mod",
|
|
45
|
+
ctrl: "ctrl",
|
|
46
|
+
control: "ctrl",
|
|
47
|
+
alt: "alt",
|
|
48
|
+
opt: "alt",
|
|
49
|
+
option: "alt",
|
|
50
|
+
shift: "shift",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Key aliases — arrow/space spellings and the `KeyboardEvent.key` values that differ. */
|
|
54
|
+
const KEY_ALIASES: Readonly<Record<string, string>> = {
|
|
55
|
+
up: "arrowup",
|
|
56
|
+
down: "arrowdown",
|
|
57
|
+
left: "arrowleft",
|
|
58
|
+
right: "arrowright",
|
|
59
|
+
esc: "escape",
|
|
60
|
+
space: " ",
|
|
61
|
+
spacebar: " ",
|
|
62
|
+
del: "delete",
|
|
63
|
+
plus: "=",
|
|
64
|
+
return: "enter",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** How each modifier prints, per platform. */
|
|
68
|
+
const MAC_GLYPHS: Readonly<Record<Modifier, string>> = {
|
|
69
|
+
mod: "⌘",
|
|
70
|
+
ctrl: "⌃",
|
|
71
|
+
alt: "⌥",
|
|
72
|
+
shift: "⇧",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const PC_LABELS: Readonly<Record<Modifier, string>> = {
|
|
76
|
+
mod: "Ctrl",
|
|
77
|
+
ctrl: "Ctrl",
|
|
78
|
+
alt: "Alt",
|
|
79
|
+
shift: "Shift",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Keys whose printed name is not just the uppercased token. */
|
|
83
|
+
const KEY_LABELS: Readonly<Record<string, string>> = {
|
|
84
|
+
arrowup: "↑",
|
|
85
|
+
arrowdown: "↓",
|
|
86
|
+
arrowleft: "←",
|
|
87
|
+
arrowright: "→",
|
|
88
|
+
" ": "Space",
|
|
89
|
+
escape: "Esc",
|
|
90
|
+
enter: "Enter",
|
|
91
|
+
tab: "Tab",
|
|
92
|
+
delete: "Delete",
|
|
93
|
+
backspace: "Backspace",
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The five fields the keymap reads off a keyboard event.
|
|
98
|
+
*
|
|
99
|
+
* Declared structurally rather than as `KeyboardEvent` so this module — and the registry that
|
|
100
|
+
* imports it — type without the DOM lib. That is what lets `scripts/check-command-levels.ts` load
|
|
101
|
+
* the command set in a bare Bun process, and it costs nothing: a real `KeyboardEvent` satisfies
|
|
102
|
+
* it.
|
|
103
|
+
*/
|
|
104
|
+
export interface KeyChordEvent {
|
|
105
|
+
key: string;
|
|
106
|
+
/**
|
|
107
|
+
* The physical key, when the event carries one.
|
|
108
|
+
*
|
|
109
|
+
* Optional, and read for exactly one purpose: with ⇧ held, `key` is the SHIFTED glyph, so ⌘⇧2
|
|
110
|
+
* arrives as `"@"` on a US layout, `"\""` on a UK one and `"é"` on a French one. A chord table
|
|
111
|
+
* that spelled `mod+shift+2` would then never fire — which is precisely the run of chords ⌘⇧1–4
|
|
112
|
+
* (the Inspector tabs) needs. `code` is `"Digit2"` on all three, so the digit row resolves by
|
|
113
|
+
* position, the way a user counting tabs means it.
|
|
114
|
+
*/
|
|
115
|
+
code?: string;
|
|
116
|
+
metaKey: boolean;
|
|
117
|
+
ctrlKey: boolean;
|
|
118
|
+
altKey: boolean;
|
|
119
|
+
shiftKey: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Enough of `navigator` to tell a mac from everything else. */
|
|
123
|
+
export interface PlatformInfo {
|
|
124
|
+
platform?: string;
|
|
125
|
+
userAgent?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A chord broken into its parts. */
|
|
129
|
+
export interface ParsedChord {
|
|
130
|
+
mod: boolean;
|
|
131
|
+
ctrl: boolean;
|
|
132
|
+
alt: boolean;
|
|
133
|
+
shift: boolean;
|
|
134
|
+
/** Lowercased key token, e.g. "p", "arrowup", "," or a lone space for Space. */
|
|
135
|
+
key: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The subset of a command record the keymap indexes. */
|
|
139
|
+
export interface BindableRecord {
|
|
140
|
+
id: string;
|
|
141
|
+
keybinding?: string | readonly string[] | undefined;
|
|
142
|
+
keyScope?: KeyScope | undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Parse `"Cmd+Shift+P"` into its parts. Throws on an empty or modifier-only chord. */
|
|
146
|
+
export function parseChord(chord: string): ParsedChord {
|
|
147
|
+
const parsed: ParsedChord = { mod: false, ctrl: false, alt: false, shift: false, key: "" };
|
|
148
|
+
const tokens = chord.split("+");
|
|
149
|
+
for (const [index, token] of tokens.entries()) {
|
|
150
|
+
if (token === "") {
|
|
151
|
+
// "mod++".split("+") is ["mod", "", ""] — a pair of empty tokens is a literal "+" key, and
|
|
152
|
+
// A lone empty token is just the slack in "mod + s".
|
|
153
|
+
if (index > 0 && tokens[index - 1] === "") {
|
|
154
|
+
parsed.key = "+";
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// A whitespace-only token is the Space key; everything else is trimmed so "Cmd + Shift + P"
|
|
159
|
+
// Parses the same as "Cmd+Shift+P".
|
|
160
|
+
const trimmed = token.trim();
|
|
161
|
+
const lower = trimmed === "" ? " " : trimmed.toLowerCase();
|
|
162
|
+
const modifier = MODIFIER_ALIASES[lower];
|
|
163
|
+
if (modifier) {
|
|
164
|
+
parsed[modifier] = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
parsed.key = KEY_ALIASES[lower] ?? lower;
|
|
168
|
+
}
|
|
169
|
+
if (!parsed.key) {
|
|
170
|
+
throw new Error(`keybinding "${chord}" has no key — a chord needs more than modifiers`);
|
|
171
|
+
}
|
|
172
|
+
return parsed;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Render a parsed chord back to its canonical string. */
|
|
176
|
+
export function serializeChord(parsed: ParsedChord): string {
|
|
177
|
+
const parts = MODIFIER_ORDER.filter((modifier) => parsed[modifier]);
|
|
178
|
+
return [...parts, parsed.key].join("+");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** `"Cmd+Shift+P"` → `"mod+shift+p"`. Idempotent; the storage form for every binding. */
|
|
182
|
+
export function normalizeChord(chord: string): string {
|
|
183
|
+
return serializeChord(parseChord(chord));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Whether the current platform is a mac. `navigator` is injectable so tests never touch globals. */
|
|
187
|
+
export function isMacPlatform(
|
|
188
|
+
nav: PlatformInfo = (globalThis as { navigator?: PlatformInfo }).navigator ?? {},
|
|
189
|
+
): boolean {
|
|
190
|
+
return /mac/i.test(nav.platform ?? nav.userAgent ?? "");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The chord a keyboard event represents, in canonical form — or `""` for a bare modifier press.
|
|
195
|
+
*
|
|
196
|
+
* `mod` is the platform's primary modifier: ⌘ on mac (where a separate ⌃ can still be held and
|
|
197
|
+
* reads as `ctrl`), Ctrl elsewhere (where Ctrl is `mod` and never also `ctrl`).
|
|
198
|
+
*/
|
|
199
|
+
/** `"Digit4"` → `"4"`; anything else → `undefined`. See {@link KeyChordEvent.code}. */
|
|
200
|
+
function digitFromCode(code: string | undefined): string | undefined {
|
|
201
|
+
const match = /^Digit(\d)$/.exec(code ?? "");
|
|
202
|
+
return match?.[1];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function chordFromEvent(event: KeyChordEvent, mac: boolean): string {
|
|
206
|
+
const key = digitFromCode(event.code) ?? event.key.toLowerCase();
|
|
207
|
+
/* A modifier ALONE is not a chord, and holding one is how every chord starts. Serializing the
|
|
208
|
+
keydown for a lone Ctrl produced "mod+control", which `parseChord` refuses by contract ("a
|
|
209
|
+
chord needs more than modifiers") — so every bare modifier press threw out of `resolveEvent`,
|
|
210
|
+
into the keydown listener, and onto the console. `""` is the honest answer, and
|
|
211
|
+
{@link Keymap.resolveEvent} reads it as "no chord yet". */
|
|
212
|
+
if (MODIFIER_ALIASES[key]) {
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
const parsed: ParsedChord = {
|
|
216
|
+
mod: mac ? event.metaKey : event.ctrlKey,
|
|
217
|
+
ctrl: mac ? event.ctrlKey : false,
|
|
218
|
+
alt: event.altKey,
|
|
219
|
+
// Shift is implied by an uppercase letter but a chord table cannot rely on that: with ⇧ held
|
|
220
|
+
// `e.key` is "Z" for ⌘⇧Z and "?" for ⇧/, so the flag is the only stable signal.
|
|
221
|
+
shift: event.shiftKey,
|
|
222
|
+
key: KEY_ALIASES[key] ?? key,
|
|
223
|
+
};
|
|
224
|
+
return serializeChord(parsed);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** `"mod+shift+p"` → `⌘⇧P` on mac, `Ctrl+Shift+P` elsewhere. The only place a chord is styled. */
|
|
228
|
+
export function formatChord(chord: string, mac: boolean): string {
|
|
229
|
+
const parsed = parseChord(chord);
|
|
230
|
+
const held = MODIFIER_ORDER.filter((modifier) => parsed[modifier]);
|
|
231
|
+
const key =
|
|
232
|
+
KEY_LABELS[parsed.key] ??
|
|
233
|
+
(parsed.key.length === 1
|
|
234
|
+
? parsed.key.toUpperCase()
|
|
235
|
+
: parsed.key.charAt(0).toUpperCase() + parsed.key.slice(1));
|
|
236
|
+
return mac
|
|
237
|
+
? held.map((modifier) => MAC_GLYPHS[modifier]).join("") + key
|
|
238
|
+
: [...held.map((modifier) => PC_LABELS[modifier]), key].join("+");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Raised when two commands claim one chord in one scope. Carries both ids for the message. */
|
|
242
|
+
export class KeybindingConflictError extends Error {
|
|
243
|
+
readonly chord: string;
|
|
244
|
+
readonly scope: KeyScope;
|
|
245
|
+
readonly existingId: string;
|
|
246
|
+
readonly incomingId: string;
|
|
247
|
+
|
|
248
|
+
constructor(chord: string, scope: KeyScope, existingId: string, incomingId: string) {
|
|
249
|
+
super(
|
|
250
|
+
`keybinding conflict: "${chord}" in scope "${scope}" is already bound to "${existingId}"; ` +
|
|
251
|
+
`"${incomingId}" cannot claim it. Rebind one, or move one to a narrower keyScope.`,
|
|
252
|
+
);
|
|
253
|
+
this.name = "KeybindingConflictError";
|
|
254
|
+
this.chord = chord;
|
|
255
|
+
this.scope = scope;
|
|
256
|
+
this.existingId = existingId;
|
|
257
|
+
this.incomingId = incomingId;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The overlay's stack, and the only one `palette` appears in. */
|
|
262
|
+
const PALETTE_SCOPES: readonly KeyScope[] = ["palette"];
|
|
263
|
+
|
|
264
|
+
/** Every scope a dispatch stack can reach outside an overlay, `global` last-resort but named first. */
|
|
265
|
+
const DISPATCHABLE_SCOPES: readonly KeyScope[] = [
|
|
266
|
+
"global",
|
|
267
|
+
...KEY_SCOPES.filter((scope) => scope !== "global" && scope !== "palette"),
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Every scope that can be live at the same instant as `scope` — including `scope` itself.
|
|
272
|
+
*
|
|
273
|
+
* Scopes are NOT independent namespaces, and treating them as one is how a rebinding steals a
|
|
274
|
+
* chord. Dispatch walks a stack (`commands/context.ts` `keyScopeStack`), and every stack Studio can
|
|
275
|
+
* be in is either the overlay's `["palette"]` or `[<one engine scope>, "global"]`. So:
|
|
276
|
+
*
|
|
277
|
+
* - **`global` overlaps every engine scope.** Binding a canvas command to ⌘S does not "also" bind ⌘S
|
|
278
|
+
* — it SHADOWS Save for as long as the canvas has focus, and Save stops responding with no trace.
|
|
279
|
+
* Whichever of the two is the newcomer, the pair cannot both answer that chord.
|
|
280
|
+
* - **Two engine scopes never overlap.** `caret` and `grid` are never on one stack, so one chord may
|
|
281
|
+
* mean two things there for ever; refusing that would forbid the shadowing ladder's whole point.
|
|
282
|
+
* - **`palette` overlaps nothing but itself.** An overlay owns the keyboard outright: nothing else
|
|
283
|
+
* resolves while one is up, and a palette binding resolves nowhere else.
|
|
284
|
+
*
|
|
285
|
+
* Ordered so that {@link Keymap.resolveChord} over the result names the most direct holder first:
|
|
286
|
+
* the same scope, then the wider one.
|
|
287
|
+
*/
|
|
288
|
+
export function overlappingScopes(scope: KeyScope): readonly KeyScope[] {
|
|
289
|
+
if (scope === "palette") {
|
|
290
|
+
return PALETTE_SCOPES;
|
|
291
|
+
}
|
|
292
|
+
if (scope === "global") {
|
|
293
|
+
return DISPATCHABLE_SCOPES;
|
|
294
|
+
}
|
|
295
|
+
return [scope, "global"];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** One resolved chord. */
|
|
299
|
+
export interface KeymapMatch {
|
|
300
|
+
commandId: string;
|
|
301
|
+
chord: string;
|
|
302
|
+
scope: KeyScope;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The user's keybinding layer: command id → the chords it holds INSTEAD of the ones it declared.
|
|
307
|
+
*
|
|
308
|
+
* An entry with an empty list is a command the user unbound. An id with no entry is a command that
|
|
309
|
+
* still has whatever the registry declared — absence is what makes a default a default.
|
|
310
|
+
*/
|
|
311
|
+
export type KeybindingOverrides = ReadonlyMap<string, readonly string[]>;
|
|
312
|
+
|
|
313
|
+
export interface Keymap {
|
|
314
|
+
/**
|
|
315
|
+
* Whether chords print with mac glyphs.
|
|
316
|
+
*
|
|
317
|
+
* Exposed because the platform decision is made ONCE, here, and every surface that prints a chord
|
|
318
|
+
* reads it rather than re-detecting: the hardcoded `⌘P` that `toolbar.ts` showed Windows and
|
|
319
|
+
* Linux users existed because there was nowhere else to ask.
|
|
320
|
+
*/
|
|
321
|
+
readonly mac: boolean;
|
|
322
|
+
/** Format an arbitrary chord for this platform. {@link formatBinding} is this over a command's. */
|
|
323
|
+
format: (chord: string) => string;
|
|
324
|
+
/** Index a record's bindings. Throws {@link KeybindingConflictError} on a same-scope clash. */
|
|
325
|
+
add: (record: BindableRecord) => void;
|
|
326
|
+
/** Drop a record's bindings — used when a registry is rebuilt, not in normal operation. */
|
|
327
|
+
remove: (id: string) => void;
|
|
328
|
+
/** Canonical chords LIVE for `id` — the user's, when they have overridden it. */
|
|
329
|
+
bindingsFor: (id: string) => readonly string[];
|
|
330
|
+
/**
|
|
331
|
+
* Canonical chords `id` DECLARED, whatever the user layer says.
|
|
332
|
+
*
|
|
333
|
+
* This is what a reset restores, and it is why a rebinding is a layer rather than an edit: the
|
|
334
|
+
* registry stays the one place a default chord is written down.
|
|
335
|
+
*/
|
|
336
|
+
declaredFor: (id: string) => readonly string[];
|
|
337
|
+
/** Replace the user's layer and re-index every record against it. */
|
|
338
|
+
setOverrides: (overrides: KeybindingOverrides) => void;
|
|
339
|
+
/** The user's layer, as applied. A copy: the layer is replaced, never mutated in place. */
|
|
340
|
+
overrides: () => KeybindingOverrides;
|
|
341
|
+
/** The first binding of `id`, formatted for this platform, or `undefined` if unbound. */
|
|
342
|
+
formatBinding: (id: string) => string | undefined;
|
|
343
|
+
/** Walk `scopeStack` narrowest-first and return the first scope that binds `chord`. */
|
|
344
|
+
resolveChord: (chord: string, scopeStack: readonly KeyScope[]) => KeymapMatch | undefined;
|
|
345
|
+
/** {@link resolveChord} over a live keyboard event. */
|
|
346
|
+
resolveEvent: (event: KeyChordEvent, scopeStack: readonly KeyScope[]) => KeymapMatch | undefined;
|
|
347
|
+
/** Every (scope, chord, id) triple — the generated shortcut sheet reads this. */
|
|
348
|
+
entries: () => readonly KeymapMatch[];
|
|
349
|
+
/**
|
|
350
|
+
* Subscribe to "the live bindings changed". Returns an unsubscribe.
|
|
351
|
+
*
|
|
352
|
+
* One listener exists today and it is the reason this hook does: the canvas iframe holds a COPY
|
|
353
|
+
* of the chord table (`canvas/iframe-keys.ts`), and a copy with no invalidation is a second
|
|
354
|
+
* authority that drifts the moment someone rebinds a key. Fired by {@link setOverrides}, which is
|
|
355
|
+
* the only operation that changes what is live after boot.
|
|
356
|
+
*/
|
|
357
|
+
onChange: (listener: () => void) => () => void;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Build an empty keymap.
|
|
362
|
+
*
|
|
363
|
+
* @param options.mac Whether to format chords with mac glyphs. Defaults to platform detection.
|
|
364
|
+
*/
|
|
365
|
+
/** Which layer a live claim came from — the whole of the precedence rule is this one field. */
|
|
366
|
+
interface Claim {
|
|
367
|
+
id: string;
|
|
368
|
+
source: "default" | "override";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function createKeymap(options: { mac?: boolean } = {}): Keymap {
|
|
372
|
+
const mac = options.mac ?? isMacPlatform();
|
|
373
|
+
/** Scope → chord → who holds it. */
|
|
374
|
+
const byScope = new Map<KeyScope, Map<string, Claim>>();
|
|
375
|
+
/** Command id → the chords it holds LIVE, in declaration order. */
|
|
376
|
+
const byId = new Map<string, string[]>();
|
|
377
|
+
/** Command id → what it declared. Never written by a rebinding. */
|
|
378
|
+
const declared = new Map<string, { chords: string[]; scope: KeyScope }>();
|
|
379
|
+
/** The user's layer. Replaced wholesale by `setOverrides`; empty until one is applied. */
|
|
380
|
+
let userLayer = new Map<string, readonly string[]>();
|
|
381
|
+
/** Who to tell when the live bindings change. */
|
|
382
|
+
const listeners = new Set<() => void>();
|
|
383
|
+
|
|
384
|
+
function chordsOf(record: BindableRecord): string[] {
|
|
385
|
+
const raw = record.keybinding;
|
|
386
|
+
if (!raw) {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
return (typeof raw === "string" ? [raw] : [...raw]).map((chord) => normalizeChord(chord));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function tableFor(scope: KeyScope): Map<string, Claim> {
|
|
393
|
+
let table = byScope.get(scope);
|
|
394
|
+
if (!table) {
|
|
395
|
+
table = new Map<string, Claim>();
|
|
396
|
+
byScope.set(scope, table);
|
|
397
|
+
}
|
|
398
|
+
return table;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Index one record's LIVE chords.
|
|
403
|
+
*
|
|
404
|
+
* `strict` is registration: two defaults colliding is an app bug and throws, and the throw
|
|
405
|
+
* happens before anything is written so a rejected record leaves nothing behind. A rebuild is not
|
|
406
|
+
* strict — by then every collision involves the user's layer, which has an answer.
|
|
407
|
+
*/
|
|
408
|
+
function claim(id: string, strict: boolean): void {
|
|
409
|
+
const record = declared.get(id);
|
|
410
|
+
if (!record) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const override = userLayer.get(id);
|
|
414
|
+
const source: Claim["source"] = override === undefined ? "default" : "override";
|
|
415
|
+
const chords = override ?? record.chords;
|
|
416
|
+
const table = tableFor(record.scope);
|
|
417
|
+
if (strict) {
|
|
418
|
+
for (const chord of chords) {
|
|
419
|
+
const held = table.get(chord);
|
|
420
|
+
if (held && held.id !== id && held.source === "default" && source === "default") {
|
|
421
|
+
throw new KeybindingConflictError(chord, record.scope, held.id, id);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const taken: string[] = [];
|
|
426
|
+
for (const chord of chords) {
|
|
427
|
+
const held = table.get(chord);
|
|
428
|
+
if (held && held.id !== id) {
|
|
429
|
+
if (source === "default") {
|
|
430
|
+
// A default never evicts an override: the user asked for this chord by name, and a
|
|
431
|
+
// Command that ships with it later is the one that goes unbound.
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
// An override always evicts. (Two overrides on one chord can only come from a hand-edited
|
|
435
|
+
// Store — `rebindCommand` refuses it — and then the later record wins, deterministically.)
|
|
436
|
+
byId.set(
|
|
437
|
+
held.id,
|
|
438
|
+
(byId.get(held.id) ?? []).filter((bound) => bound !== chord),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
table.set(chord, { id, source });
|
|
442
|
+
taken.push(chord);
|
|
443
|
+
}
|
|
444
|
+
byId.set(id, taken);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Re-index everything: overridden records first, so their claims are the ones that survive. */
|
|
448
|
+
function rebuild(): void {
|
|
449
|
+
byScope.clear();
|
|
450
|
+
byId.clear();
|
|
451
|
+
for (const id of declared.keys()) {
|
|
452
|
+
if (userLayer.has(id)) {
|
|
453
|
+
claim(id, false);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const id of declared.keys()) {
|
|
457
|
+
if (!userLayer.has(id)) {
|
|
458
|
+
claim(id, false);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
mac,
|
|
465
|
+
format(chord) {
|
|
466
|
+
return formatChord(chord, mac);
|
|
467
|
+
},
|
|
468
|
+
add(record) {
|
|
469
|
+
const scope: KeyScope = record.keyScope ?? "global";
|
|
470
|
+
declared.set(record.id, { chords: chordsOf(record), scope });
|
|
471
|
+
try {
|
|
472
|
+
claim(record.id, true);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
declared.delete(record.id);
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
remove(id) {
|
|
479
|
+
declared.delete(id);
|
|
480
|
+
// A rebuild rather than a targeted delete, so anything this record was displacing gets its
|
|
481
|
+
// Chord back — the same reason `setOverrides` rebuilds.
|
|
482
|
+
rebuild();
|
|
483
|
+
},
|
|
484
|
+
bindingsFor(id) {
|
|
485
|
+
return byId.get(id) ?? [];
|
|
486
|
+
},
|
|
487
|
+
declaredFor(id) {
|
|
488
|
+
return declared.get(id)?.chords ?? [];
|
|
489
|
+
},
|
|
490
|
+
setOverrides(overrides) {
|
|
491
|
+
userLayer = new Map(
|
|
492
|
+
[...overrides].map(([id, chords]) => [id, chords.map((chord) => normalizeChord(chord))]),
|
|
493
|
+
);
|
|
494
|
+
rebuild();
|
|
495
|
+
for (const listener of listeners) {
|
|
496
|
+
listener();
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
onChange(listener) {
|
|
500
|
+
listeners.add(listener);
|
|
501
|
+
return () => listeners.delete(listener);
|
|
502
|
+
},
|
|
503
|
+
overrides() {
|
|
504
|
+
return new Map(userLayer);
|
|
505
|
+
},
|
|
506
|
+
formatBinding(id) {
|
|
507
|
+
const first = byId.get(id)?.[0];
|
|
508
|
+
return first === undefined ? undefined : formatChord(first, mac);
|
|
509
|
+
},
|
|
510
|
+
resolveChord(chord, scopeStack) {
|
|
511
|
+
const normalized = normalizeChord(chord);
|
|
512
|
+
// Narrowest scope first: the stack IS the precedence order, so the first hit wins and a
|
|
513
|
+
// Caret binding shadows the same chord bound globally.
|
|
514
|
+
const scope = scopeStack.find((candidate) => byScope.get(candidate)?.has(normalized));
|
|
515
|
+
const held = scope === undefined ? undefined : byScope.get(scope)?.get(normalized);
|
|
516
|
+
return scope === undefined || held === undefined
|
|
517
|
+
? undefined
|
|
518
|
+
: { chord: normalized, commandId: held.id, scope };
|
|
519
|
+
},
|
|
520
|
+
resolveEvent(event, scopeStack) {
|
|
521
|
+
const chord = chordFromEvent(event, mac);
|
|
522
|
+
// A bare modifier press resolves to nothing rather than throwing on a keyless chord.
|
|
523
|
+
return chord === "" ? undefined : this.resolveChord(chord, scopeStack);
|
|
524
|
+
},
|
|
525
|
+
entries() {
|
|
526
|
+
const all: KeymapMatch[] = [];
|
|
527
|
+
for (const [scope, table] of byScope) {
|
|
528
|
+
for (const [chord, held] of table) {
|
|
529
|
+
all.push({ chord, commandId: held.id, scope });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return all;
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Every live (chord, scope) pair in `scopes`, deduplicated — the table a second realm can resolve
|
|
539
|
+
* against.
|
|
540
|
+
*
|
|
541
|
+
* Written for the canvas iframe, which cannot see the registry at all: it is handed this and
|
|
542
|
+
* forwards a keystroke iff some scope on its own stack claims the chord (`canvas/iframe-keys.ts`).
|
|
543
|
+
* The command id is deliberately dropped — the frame decides whether to forward, never what runs —
|
|
544
|
+
* and the projection is a pure function of the keymap so the host can recompute and repost it
|
|
545
|
+
* whenever a rebinding lands.
|
|
546
|
+
*/
|
|
547
|
+
export function chordsInScopes<S extends KeyScope>(
|
|
548
|
+
keymap: Keymap,
|
|
549
|
+
scopes: readonly S[],
|
|
550
|
+
): { chord: string; scope: S }[] {
|
|
551
|
+
const seen = new Set<string>();
|
|
552
|
+
const out: { chord: string; scope: S }[] = [];
|
|
553
|
+
for (const entry of keymap.entries()) {
|
|
554
|
+
/* `find` rather than a `Set.has` test: the narrowed member is what makes the result carry the
|
|
555
|
+
CALLER's scope union. The canvas frame's table is three scopes wide (`FRAME_KEY_SCOPES`) and
|
|
556
|
+
has to stay that way across the bridge, where `SyncedChord` declares exactly those three —
|
|
557
|
+
widening back to every `KeyScope` here would push a `grid` chord into a message that cannot
|
|
558
|
+
hold one. */
|
|
559
|
+
const scope = scopes.find((candidate) => candidate === entry.scope);
|
|
560
|
+
if (scope === undefined) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const key = `${scope} ${entry.chord}`;
|
|
564
|
+
if (seen.has(key)) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
seen.add(key);
|
|
568
|
+
out.push({ chord: entry.chord, scope });
|
|
569
|
+
}
|
|
570
|
+
return out;
|
|
571
|
+
}
|