@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
|
@@ -5,40 +5,111 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { html, nothing } from "lit-html";
|
|
8
|
-
import type { CanvasPanel } from "../types";
|
|
8
|
+
import type { CanvasPanel, JsonValue } from "../types";
|
|
9
9
|
import { ref } from "lit-html/directives/ref.js";
|
|
10
10
|
import { classMap } from "lit-html/directives/class-map.js";
|
|
11
11
|
import { styleMap } from "lit-html/directives/style-map.js";
|
|
12
12
|
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
import { renderOnly } from "../store";
|
|
15
|
+
import {
|
|
16
|
+
activeCanvasSurface,
|
|
17
|
+
activeMediaOfPane,
|
|
18
|
+
canvasModeOfPane,
|
|
19
|
+
derivationOfPane,
|
|
20
|
+
tabOfMountedPanel,
|
|
21
|
+
tabOfPane,
|
|
22
|
+
} from "./canvas-surface";
|
|
23
|
+
import type { CanvasSurface } from "./canvas-surface";
|
|
24
|
+
import { activeTab, workspace } from "../workspace/workspace";
|
|
25
|
+
import {
|
|
26
|
+
findCanvasElement,
|
|
27
|
+
getActivePanel,
|
|
28
|
+
panelMediaToActiveMedia,
|
|
29
|
+
panelOfSurface,
|
|
30
|
+
} from "./canvas-helpers";
|
|
18
31
|
import { rectOf } from "../utils/geometry";
|
|
32
|
+
import { getEffectiveLocales, getEffectiveMedia } from "../site-context";
|
|
33
|
+
import { dynamicRouteParams } from "../page-params";
|
|
34
|
+
import {
|
|
35
|
+
argsSchema,
|
|
36
|
+
booleanArg,
|
|
37
|
+
booleanProperty,
|
|
38
|
+
boundedNumberArg,
|
|
39
|
+
enumArg,
|
|
40
|
+
enumProperty,
|
|
41
|
+
numberProperty,
|
|
42
|
+
stringArg,
|
|
43
|
+
stringProperty,
|
|
44
|
+
} from "../commands/command-args";
|
|
19
45
|
import type { TemplateResult } from "lit-html";
|
|
46
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
47
|
+
import type { CommandArgValues } from "../commands/command-args";
|
|
48
|
+
import type { Tab } from "../tabs/tab";
|
|
20
49
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
50
|
+
/*
|
|
51
|
+
* There is no `initCanvasUtils`, and no `_ctx`.
|
|
52
|
+
*
|
|
53
|
+
* It injected three functions — `getCanvasMode`, `getZoom`, `setZoomDirect` — and all three were
|
|
54
|
+
* spelled `activeTab.value`, which is the FOCUSED pane's tab. Every geometry function here already
|
|
55
|
+
* takes a `CanvasSurface`, so the pan/zoom offsets and the pan-zoom wrap moved onto it while the
|
|
56
|
+
* SCALE stayed behind the injection, and the two disagreed in four visible ways:
|
|
57
|
+
*
|
|
58
|
+
* 1 · the unfocused pane drew at the focused tab's scale — `scale(1)` for a document at 2×;
|
|
59
|
+
* 2 · the side pane's `+` zoomed the PRIMARY's document, by a factor computed from the side's
|
|
60
|
+
* own zoom (`b.zoom * 1.2` written to `a`);
|
|
61
|
+
* 3 · `getFit()` keyed on `activeTab`, so the unfocused pod reported the focused pane's fit;
|
|
62
|
+
* 4 · the side pane entering Design ran `fitOnCanvasEntry` → `setZoomDirect(0.163)` and the
|
|
63
|
+
* PRIMARY snapped to 16%, because the other pane had fitted itself.
|
|
64
|
+
*
|
|
65
|
+
* `scripts/check-pane-singletons.ts` could not see any of it: it bans `view.<field>` by name and
|
|
66
|
+
* the zoom was never on `view`. The rule it enforces is now the general one — per-stage state is
|
|
67
|
+
* reached through a surface — and this module reads `activeTab` in exactly one place, the command
|
|
68
|
+
* verbs, where "the active pane" is the whole meaning of the request.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/** The tab a stage is showing. The one route from a surface to the state its geometry needs. */
|
|
72
|
+
function tabOfSurface(surface: CanvasSurface): Tab | null {
|
|
73
|
+
return tabOfPane(surface.paneId);
|
|
74
|
+
}
|
|
26
75
|
|
|
27
76
|
/**
|
|
28
|
-
*
|
|
77
|
+
* A stage's own pan-zoom scale. 1 when its pane shows nothing.
|
|
29
78
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* setZoomDirect: (zoom: number) => void;
|
|
34
|
-
* }} ctx
|
|
79
|
+
* **`session.ui.zoom` is per-TAB, and a LENS shares its tab with the pane beside it** — so under a
|
|
80
|
+
* lens the two stages would zoom together, which is exactly what "the same page at another
|
|
81
|
+
* breakpoint" must not do. The derivation carries the lens's own scale.
|
|
35
82
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
83
|
+
function zoomOf(surface: CanvasSurface): number {
|
|
84
|
+
const derived = derivationOfPane(surface.paneId);
|
|
85
|
+
if (derived?.kind === "lens") {
|
|
86
|
+
return derived.zoom;
|
|
87
|
+
}
|
|
88
|
+
return tabOfSurface(surface)?.session.ui.zoom ?? 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A stage's own pan-zoom scale, for the CONTROL that reports it.
|
|
93
|
+
*
|
|
94
|
+
* The zoom pod printed `tab.session.ui.zoom`, which is the source pane's number under a lens — so a
|
|
95
|
+
* breakpoint lens zoomed to 40% reported whatever the desktop pane was at, and its `-` divided the
|
|
96
|
+
* wrong figure. Every zoom verb already takes a surface; the readout has to as well.
|
|
97
|
+
*/
|
|
98
|
+
export function stageZoom(surface: CanvasSurface = activeCanvasSurface()): number {
|
|
99
|
+
return zoomOf(surface);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Write a stage's own pan-zoom scale. A no-op when its pane shows nothing. See {@link zoomOf}. */
|
|
103
|
+
function setZoomOf(surface: CanvasSurface, zoom: number): void {
|
|
104
|
+
const derived = derivationOfPane(surface.paneId);
|
|
105
|
+
if (derived?.kind === "lens") {
|
|
106
|
+
derived.zoom = zoom;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const tab = tabOfSurface(surface);
|
|
110
|
+
if (tab) {
|
|
111
|
+
tab.session.ui.zoom = zoom;
|
|
112
|
+
}
|
|
42
113
|
}
|
|
43
114
|
|
|
44
115
|
/**
|
|
@@ -83,7 +154,15 @@ export function canvasPanelTemplate(
|
|
|
83
154
|
<div
|
|
84
155
|
class="canvas-panel-header"
|
|
85
156
|
@click=${() => {
|
|
86
|
-
|
|
157
|
+
// The breakpoint belongs to the tab of the pane that MOUNTED this artboard — the
|
|
158
|
+
// Parent-side twin of the iframe's `hit` message, resolved the same way.
|
|
159
|
+
// `updateUi` writes to `activeTab`, which is the FOCUSED pane's tab, so clicking
|
|
160
|
+
// A header in an unfocused pane set another document's breakpoint and the Style
|
|
161
|
+
// Panel then edited a compound block the person never opened.
|
|
162
|
+
const tab = tabOfMountedPanel(panel);
|
|
163
|
+
if (tab) {
|
|
164
|
+
tab.session.ui.activeMedia = panelMediaToActiveMedia(mediaName);
|
|
165
|
+
}
|
|
87
166
|
}}
|
|
88
167
|
>
|
|
89
168
|
${label}
|
|
@@ -115,54 +194,64 @@ export function canvasPanelTemplate(
|
|
|
115
194
|
return { panel, tpl };
|
|
116
195
|
}
|
|
117
196
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
197
|
+
/*
|
|
198
|
+
* Every geometry function below takes its surface EXPLICITLY, and none of them defaults to
|
|
199
|
+
* `activeCanvasSurface()`.
|
|
200
|
+
*
|
|
201
|
+
* A default is how one pane's fit lands on the other's transform: the fit that runs on a mode
|
|
202
|
+
* transition, the observer that re-centres until content settles and the wheel that pans are all
|
|
203
|
+
* scheduled from a render, and a render belongs to the pane it was scheduled for — which is the
|
|
204
|
+
* focused pane only by coincidence. Every call site already holds a surface or a tab.
|
|
205
|
+
*/
|
|
206
|
+
|
|
207
|
+
/** Center a stage's content horizontally in its viewport, top-aligned vertically. */
|
|
208
|
+
export function centerCanvas(surface: CanvasSurface) {
|
|
209
|
+
if (!surface.panzoomWrap) {
|
|
121
210
|
return;
|
|
122
211
|
}
|
|
123
|
-
const wrapWidth =
|
|
124
|
-
const contentWidth =
|
|
125
|
-
const zoom =
|
|
212
|
+
const wrapWidth = surface.wrap.clientWidth;
|
|
213
|
+
const contentWidth = surface.panzoomWrap.scrollWidth;
|
|
214
|
+
const zoom = zoomOf(surface);
|
|
126
215
|
const scaledWidth = contentWidth * zoom;
|
|
127
|
-
|
|
128
|
-
|
|
216
|
+
surface.panX = Math.max(16, (wrapWidth - scaledWidth) / 2);
|
|
217
|
+
surface.panY = 0;
|
|
129
218
|
}
|
|
130
219
|
|
|
131
220
|
/**
|
|
132
221
|
* Attach a ResizeObserver to view.panzoomWrap that re-centers until the user pans. Handles async
|
|
133
222
|
* content (runtime rendering, data fetching) that changes layout after initial paint.
|
|
134
223
|
*/
|
|
135
|
-
export function observeCenterUntilStable() {
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
224
|
+
export function observeCenterUntilStable(surface: CanvasSurface) {
|
|
225
|
+
if (surface.centerObserver) {
|
|
226
|
+
surface.centerObserver.disconnect();
|
|
227
|
+
surface.centerObserver = null;
|
|
139
228
|
}
|
|
140
|
-
if (!
|
|
229
|
+
if (!surface.panzoomWrap) {
|
|
141
230
|
return;
|
|
142
231
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (!
|
|
146
|
-
|
|
147
|
-
|
|
232
|
+
surface.needsCenter = true;
|
|
233
|
+
surface.centerObserver = new ResizeObserver(() => {
|
|
234
|
+
if (!surface.needsCenter) {
|
|
235
|
+
surface.centerObserver?.disconnect();
|
|
236
|
+
surface.centerObserver = null;
|
|
148
237
|
return;
|
|
149
238
|
}
|
|
150
|
-
centerCanvas();
|
|
151
|
-
applyTransform();
|
|
239
|
+
centerCanvas(surface);
|
|
240
|
+
applyTransform(surface);
|
|
152
241
|
});
|
|
153
|
-
|
|
154
|
-
centerCanvas();
|
|
242
|
+
surface.centerObserver.observe(surface.panzoomWrap);
|
|
243
|
+
centerCanvas(surface);
|
|
155
244
|
}
|
|
156
245
|
|
|
157
246
|
/** Apply the current zoom + pan transform to the panzoom wrapper. */
|
|
158
|
-
export function applyTransform() {
|
|
159
|
-
if (!
|
|
247
|
+
export function applyTransform(surface: CanvasSurface) {
|
|
248
|
+
if (!surface.panzoomWrap) {
|
|
160
249
|
return;
|
|
161
250
|
}
|
|
162
|
-
const zoom =
|
|
163
|
-
|
|
251
|
+
const zoom = zoomOf(surface);
|
|
252
|
+
surface.panzoomWrap.style.transform = `translate(${surface.panX}px, ${surface.panY}px) scale(${zoom})`;
|
|
164
253
|
// Overlays live INSIDE the scaled panzoom-wrap (iframe hosts draw there), so no per-mode redraw
|
|
165
|
-
// Is needed here — the flush only re-anchors the fixed block-action-bar. The
|
|
254
|
+
// Is needed here — the flush only re-anchors the fixed block-action-bar. The floating zoom pod
|
|
166
255
|
// Tracks `ui.zoom` reactively, so no explicit indicator refresh is needed either.
|
|
167
256
|
renderOnly("overlays");
|
|
168
257
|
}
|
|
@@ -200,17 +289,19 @@ export function clampEditZoom(zoom: number): number {
|
|
|
200
289
|
* bare style writes only; the iframe's own ResizeObserver re-posts `contentHeight` after the
|
|
201
290
|
* reflow, which also finalizes the viewport height.
|
|
202
291
|
*/
|
|
203
|
-
export function applyEditZoom() {
|
|
204
|
-
|
|
292
|
+
export function applyEditZoom(surface: CanvasSurface = activeCanvasSurface()) {
|
|
293
|
+
// THIS pane's effective mode. It was the focused pane's, so a side pane in Edit was refused its
|
|
294
|
+
// Own content zoom whenever the primary happened to be in Design.
|
|
295
|
+
if (canvasModeOfPane(surface.paneId) !== "edit") {
|
|
205
296
|
return;
|
|
206
297
|
}
|
|
207
|
-
const [panel] =
|
|
298
|
+
const [panel] = surface.panels;
|
|
208
299
|
if (!panel?.canvas || !panel.viewport) {
|
|
209
300
|
return;
|
|
210
301
|
}
|
|
211
302
|
const canvasEl = panel.canvas;
|
|
212
303
|
const iframe = canvasEl.querySelector("iframe");
|
|
213
|
-
const editZoom =
|
|
304
|
+
const editZoom = tabOfSurface(surface)?.session.ui.editZoom ?? 1;
|
|
214
305
|
if (editZoom === 1) {
|
|
215
306
|
// Exactly today's fluid behavior — no inline width, no transform, auto viewport height.
|
|
216
307
|
panel._width = null;
|
|
@@ -222,7 +313,7 @@ export function applyEditZoom() {
|
|
|
222
313
|
}
|
|
223
314
|
panel.viewport.style.height = "";
|
|
224
315
|
} else {
|
|
225
|
-
const column =
|
|
316
|
+
const column = surface.wrap.querySelector<HTMLElement>(".content-edit-column");
|
|
226
317
|
if (!column) {
|
|
227
318
|
return;
|
|
228
319
|
}
|
|
@@ -250,126 +341,404 @@ export function applyEditZoom() {
|
|
|
250
341
|
renderOnly("overlays");
|
|
251
342
|
}
|
|
252
343
|
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
344
|
+
/**
|
|
345
|
+
* Set a stage's edit zoom (clamped) and apply it synchronously.
|
|
346
|
+
*
|
|
347
|
+
* The SURFACE decides whose zoom this is, not the focus. Both this and {@link requestEditZoom}
|
|
348
|
+
* opened `activeTab.value`, so the side pane's `+` and the wheel over the side pane's stage — which
|
|
349
|
+
* had already resolved the right surface and the right tab — wrote the primary's `editZoom`.
|
|
350
|
+
*/
|
|
351
|
+
export function setEditZoom(zoom: number, surface: CanvasSurface = activeCanvasSurface()) {
|
|
352
|
+
const tab = tabOfSurface(surface);
|
|
256
353
|
if (!tab) {
|
|
257
354
|
return;
|
|
258
355
|
}
|
|
259
356
|
tab.session.ui.editZoom = clampEditZoom(zoom);
|
|
260
|
-
applyEditZoom();
|
|
357
|
+
applyEditZoom(surface);
|
|
261
358
|
}
|
|
262
359
|
|
|
263
|
-
|
|
360
|
+
/**
|
|
361
|
+
* One pending edit-zoom frame PER STAGE.
|
|
362
|
+
*
|
|
363
|
+
* A single `let _editZoomRaf` was a shared slot: a wheel burst over one pane suppressed the other
|
|
364
|
+
* pane's coalesced reflow entirely, and the frame that did run applied whichever surface won the
|
|
365
|
+
* race. Keyed by pane id, for the reason `canvas-render.ts` keys its own render frames that way.
|
|
366
|
+
*/
|
|
367
|
+
const _editZoomRafs = new Map<string, number>();
|
|
264
368
|
|
|
265
369
|
/**
|
|
266
|
-
* Wheel-rate edit-zoom setter: the reactive `editZoom` write lands immediately (the
|
|
267
|
-
* tracks it), but the DOM work — an iframe width resize, i.e. a real reflow — is coalesced to
|
|
268
|
-
* `applyEditZoom()` per animation frame so a fast ctrl+scroll burst doesn't thrash layout.
|
|
370
|
+
* Wheel-rate edit-zoom setter: the reactive `editZoom` write lands immediately (the zoom pod's
|
|
371
|
+
* label tracks it), but the DOM work — an iframe width resize, i.e. a real reflow — is coalesced to
|
|
372
|
+
* one `applyEditZoom()` per animation frame so a fast ctrl+scroll burst doesn't thrash layout.
|
|
269
373
|
*/
|
|
270
|
-
export function requestEditZoom(zoom: number) {
|
|
271
|
-
const tab =
|
|
374
|
+
export function requestEditZoom(zoom: number, surface: CanvasSurface = activeCanvasSurface()) {
|
|
375
|
+
const tab = tabOfSurface(surface);
|
|
272
376
|
if (!tab) {
|
|
273
377
|
return;
|
|
274
378
|
}
|
|
275
379
|
tab.session.ui.editZoom = clampEditZoom(zoom);
|
|
276
|
-
if (
|
|
380
|
+
if (_editZoomRafs.has(surface.paneId)) {
|
|
277
381
|
return;
|
|
278
382
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
383
|
+
_editZoomRafs.set(
|
|
384
|
+
surface.paneId,
|
|
385
|
+
requestAnimationFrame(() => {
|
|
386
|
+
_editZoomRafs.delete(surface.paneId);
|
|
387
|
+
applyEditZoom(surface);
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
283
390
|
}
|
|
284
391
|
|
|
285
|
-
/**
|
|
286
|
-
|
|
287
|
-
|
|
392
|
+
/**
|
|
393
|
+
* Compute and apply a geometric fit. Pure arithmetic — it declares nothing.
|
|
394
|
+
*
|
|
395
|
+
* `axis` is the fit's own meaning: `"page"` fits both axes (the whole artboard, in view), `"width"`
|
|
396
|
+
* fits the horizontal axis and lets the page run off the bottom, which is what you want on a long
|
|
397
|
+
* document you are about to scroll. `maxZoom` caps the result.
|
|
398
|
+
*/
|
|
399
|
+
function applyGeometricFit(surface: CanvasSurface, axis: "width" | "page", maxZoom: number): void {
|
|
400
|
+
if (!surface.panzoomWrap) {
|
|
288
401
|
return;
|
|
289
402
|
}
|
|
290
|
-
const wrapWidth =
|
|
291
|
-
const wrapHeight =
|
|
403
|
+
const wrapWidth = surface.wrap.clientWidth;
|
|
404
|
+
const wrapHeight = surface.wrap.clientHeight;
|
|
292
405
|
const gap = 24;
|
|
293
406
|
const padding = 32;
|
|
294
407
|
let totalPanelWidth = 0;
|
|
295
|
-
|
|
408
|
+
const { panels } = surface;
|
|
409
|
+
for (const p of panels) {
|
|
296
410
|
totalPanelWidth += p._width || 800;
|
|
297
411
|
}
|
|
298
|
-
totalPanelWidth += gap * Math.max(0,
|
|
412
|
+
totalPanelWidth += gap * Math.max(0, panels.length - 1) + padding;
|
|
299
413
|
|
|
300
|
-
const zoom =
|
|
301
|
-
const wrapRect = rectOf(
|
|
414
|
+
const zoom = zoomOf(surface);
|
|
415
|
+
const wrapRect = rectOf(surface.panzoomWrap);
|
|
302
416
|
const unscaledHeight = wrapRect.height / zoom;
|
|
303
417
|
const maxPanelHeight = unscaledHeight + padding;
|
|
304
418
|
|
|
305
419
|
const fitZoomW = wrapWidth / totalPanelWidth;
|
|
306
420
|
const fitZoomH = wrapHeight / maxPanelHeight;
|
|
307
|
-
const
|
|
421
|
+
const wanted = axis === "width" ? fitZoomW : Math.min(fitZoomW, fitZoomH);
|
|
422
|
+
const fitZoom = Math.min(maxZoom, Math.max(PAN_ZOOM_MIN, wanted));
|
|
308
423
|
|
|
309
|
-
|
|
424
|
+
setZoomOf(surface, fitZoom);
|
|
310
425
|
|
|
311
426
|
const scaledWidth = totalPanelWidth * fitZoom;
|
|
312
427
|
const scaledHeight = maxPanelHeight * fitZoom;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
applyTransform();
|
|
428
|
+
surface.panX = Math.max(0, (wrapWidth - scaledWidth) / 2);
|
|
429
|
+
surface.panY = Math.max(0, (wrapHeight - scaledHeight) / 2);
|
|
430
|
+
applyTransform(surface);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Fit all panels within the viewport, and declare that as the document's fit.
|
|
435
|
+
*
|
|
436
|
+
* `maxZoom` caps the result. The Fit control passes the full {@link PAN_ZOOM_MAX} (asking to fit is
|
|
437
|
+
* asking to magnify a small artboard too); the automatic fit on entering a panzoom mode passes 1,
|
|
438
|
+
* because arriving at a document should never blow it up past life size.
|
|
439
|
+
*/
|
|
440
|
+
export function fitToScreen({
|
|
441
|
+
maxZoom = PAN_ZOOM_MAX,
|
|
442
|
+
surface = activeCanvasSurface(),
|
|
443
|
+
}: { maxZoom?: number; surface?: CanvasSurface } = {}) {
|
|
444
|
+
// Declared before the guard: "frame the whole page" is a preference the author expressed, and it
|
|
445
|
+
// Stays true whether or not there is a laid-out artboard to apply it to this instant.
|
|
446
|
+
declareFit("page", surface);
|
|
447
|
+
applyGeometricFit(surface, "page", maxZoom);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ─── Pan-zoom (design / stylebook / git-diff artboards) ──────────────────────
|
|
451
|
+
|
|
452
|
+
export const PAN_ZOOM_MIN = 0.05;
|
|
453
|
+
|
|
454
|
+
export const PAN_ZOOM_MAX = 5;
|
|
455
|
+
|
|
456
|
+
/** Clamp a pan-zoom value to the supported range. */
|
|
457
|
+
export function clampPanZoom(zoom: number): number {
|
|
458
|
+
return Math.min(PAN_ZOOM_MAX, Math.max(PAN_ZOOM_MIN, zoom));
|
|
316
459
|
}
|
|
317
460
|
|
|
318
|
-
/**
|
|
319
|
-
|
|
320
|
-
|
|
461
|
+
/**
|
|
462
|
+
* How a document is framed when you arrive at it — **declared state**, per tab + document.
|
|
463
|
+
*
|
|
464
|
+
* - `"page"` — the whole artboard in view (the default: arriving at a document should show it).
|
|
465
|
+
* - `"width"` — fit the horizontal axis; the page runs off the bottom, which is what a long document
|
|
466
|
+
* you are about to scroll actually wants.
|
|
467
|
+
* - `"none"` — frame nothing; whatever the zoom is, is the zoom.
|
|
468
|
+
* - A number — that exact pan-zoom.
|
|
469
|
+
*
|
|
470
|
+
* This replaces an IMPLICIT fit-on-entry plus a `Set` of "documents the author has zoomed by hand",
|
|
471
|
+
* which had three problems. It could not be read, so the zoom control could not show it. It could
|
|
472
|
+
* not be written, so a preference users expect to persist silently reset. And because entering a
|
|
473
|
+
* mode applied a transform nobody had asked for, anything measuring the canvas from outside had to
|
|
474
|
+
* guess whether a fit had happened — which is precisely how a screen coordinate ends up wrong.
|
|
475
|
+
*
|
|
476
|
+
* "The author chose 84%" is not a special case here: it is the fit `0.84`.
|
|
477
|
+
*/
|
|
478
|
+
export type FitMode = "width" | "page" | "none" | number;
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* The fit a document gets when it has not declared one.
|
|
482
|
+
*
|
|
483
|
+
* `"page"` because a 1280px artboard used to land clipped mid-word in a ~700px viewport, and
|
|
484
|
+
* arriving at a document should never require a zoom-out before you can read it.
|
|
485
|
+
*/
|
|
486
|
+
export const DEFAULT_FIT: FitMode = "page";
|
|
487
|
+
|
|
488
|
+
/** The three named fits, as the `canvas.setFit` schema and its coercion both read them. */
|
|
489
|
+
export const FIT_WORDS: readonly string[] = ["width", "page", "none"];
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Coerce a `canvas.setFit` argument, refusing anything that is neither a named fit nor a scale.
|
|
493
|
+
*
|
|
494
|
+
* Written here rather than in `commands/command-args.ts` because the value space is this module's:
|
|
495
|
+
* {@link FitMode} is declared three lines up, and a second copy of "which words are fits" in the
|
|
496
|
+
* shared helpers is exactly the drift the registry projection exists to prevent.
|
|
497
|
+
*/
|
|
498
|
+
function fitArg(commandId: string, args: Record<string, unknown>, key: string): FitMode {
|
|
499
|
+
const value = args[key];
|
|
500
|
+
if (typeof value === "string" && FIT_WORDS.includes(value)) {
|
|
501
|
+
return value as FitMode;
|
|
502
|
+
}
|
|
503
|
+
if (typeof value === "number" && value >= PAN_ZOOM_MIN && value <= PAN_ZOOM_MAX) {
|
|
504
|
+
return value;
|
|
505
|
+
}
|
|
506
|
+
throw new RangeError(
|
|
507
|
+
`command "${commandId}" argument "${key}": expected ${FIT_WORDS.map((w) => `"${w}"`).join(" | ")} ` +
|
|
508
|
+
`or a number in [${PAN_ZOOM_MIN}, ${PAN_ZOOM_MAX}], got ${JSON.stringify(value)}`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Declared fits, keyed per tab + document. */
|
|
513
|
+
const _fits = new Map<string, FitMode>();
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* The registry key for the document a STAGE is showing (null when its pane shows none).
|
|
517
|
+
*
|
|
518
|
+
* It read `activeTab`, which is why the unfocused pane's zoom pod reported the focused pane's fit —
|
|
519
|
+
* and why "Fit page" chosen in one pane changed the label in the other.
|
|
520
|
+
*/
|
|
521
|
+
function fitKey(surface: CanvasSurface): string | null {
|
|
522
|
+
const tab = tabOfSurface(surface);
|
|
523
|
+
if (!tab) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
/* A LENS shares the tab with the pane it derives from, so the tab alone stops being a key: the
|
|
527
|
+
mobile lens and the desktop pane would declare ONE fit between them and re-frame each other on
|
|
528
|
+
every mode transition. Same reason `zoomOf` reads the derivation. */
|
|
529
|
+
const lens = derivationOfPane(surface.paneId)?.kind === "lens" ? `::${surface.paneId}` : "";
|
|
530
|
+
return `${tab.id}::${tab.documentPath ?? ""}${lens}`;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Write a stage's document's fit without applying it — the internal half of {@link setFit}. */
|
|
534
|
+
function declareFit(fit: FitMode, surface: CanvasSurface): void {
|
|
535
|
+
const key = fitKey(surface);
|
|
536
|
+
if (key) {
|
|
537
|
+
_fits.set(key, fit);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** A stage's declared fit, or {@link DEFAULT_FIT}. Readable by that pane's zoom control. */
|
|
542
|
+
export function getFit(surface: CanvasSurface = activeCanvasSurface()): FitMode {
|
|
543
|
+
const key = fitKey(surface);
|
|
544
|
+
return (key === null ? undefined : _fits.get(key)) ?? DEFAULT_FIT;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Whether a stage's document has declared a fit of its own. */
|
|
548
|
+
export function hasDeclaredFit(surface: CanvasSurface = activeCanvasSurface()): boolean {
|
|
549
|
+
const key = fitKey(surface);
|
|
550
|
+
return key !== null && _fits.has(key);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Drop every declared fit — a fresh session, and the tests. */
|
|
554
|
+
export function resetFits(): void {
|
|
555
|
+
_fits.clear();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Honour a fit right now. Defaults to the STAGE's own declared one. */
|
|
559
|
+
export function applyFit(fit?: FitMode, surface: CanvasSurface = activeCanvasSurface()): void {
|
|
560
|
+
/* Resolved in the body rather than as a parameter default: a default may only read parameters
|
|
561
|
+
declared BEFORE it, and the fit belongs to the surface that comes after. Reading `getFit()`
|
|
562
|
+
with no surface is exactly what made the default answer for the FOCUSED pane. */
|
|
563
|
+
const chosen = fit ?? getFit(surface);
|
|
564
|
+
if (chosen === "none") {
|
|
565
|
+
applyTransform(surface);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (typeof chosen === "number") {
|
|
569
|
+
setZoomOf(surface, clampPanZoom(chosen));
|
|
570
|
+
applyTransform(surface);
|
|
321
571
|
return;
|
|
322
572
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
573
|
+
// Capped at life size: arriving at a document should never blow it up past 100%.
|
|
574
|
+
applyGeometricFit(surface, chosen, 1);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Declare the active document's fit AND honour it. The one writer every control routes through. */
|
|
578
|
+
export function setFit(fit: FitMode, surface: CanvasSurface = activeCanvasSurface()): void {
|
|
579
|
+
declareFit(fit, surface);
|
|
580
|
+
applyFit(fit, surface);
|
|
326
581
|
}
|
|
327
582
|
|
|
328
583
|
/**
|
|
329
|
-
*
|
|
584
|
+
* Record the active document's CURRENT pan-zoom as its declared fit.
|
|
330
585
|
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
animatePanBy(offsetY);
|
|
586
|
+
* For the gesture paths that move the zoom themselves and then have to say so — ctrl+scroll writes
|
|
587
|
+
* `ui.zoom` directly, pinch will too. An author-chosen zoom is a numeric fit, so re-entering the
|
|
588
|
+
* mode restores it instead of re-framing over the top of it.
|
|
589
|
+
*/
|
|
590
|
+
export function markExplicitZoom(surface: CanvasSurface = activeCanvasSurface()): void {
|
|
591
|
+
declareFit(clampPanZoom(zoomOf(surface)), surface);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Set the active tab's pan-zoom on the author's behalf: clamped, declared as this document's fit,
|
|
596
|
+
* and applied. Every author-facing pan-zoom control routes through here.
|
|
597
|
+
*/
|
|
598
|
+
export function setUserZoom(zoom: number, surface: CanvasSurface = activeCanvasSurface()): void {
|
|
599
|
+
const tab = tabOfSurface(surface);
|
|
600
|
+
if (!tab) {
|
|
601
|
+
return;
|
|
348
602
|
}
|
|
603
|
+
// The fit is declared BEFORE the reactive write, not after: `ui.zoom` is tracked, effects run
|
|
604
|
+
// Synchronously on assignment, and the zoom pod reads `getFit()` while it renders — so declaring
|
|
605
|
+
// Second means the control repaints one interaction behind the state it is reporting.
|
|
606
|
+
const next = clampPanZoom(zoom);
|
|
607
|
+
declareFit(next, surface);
|
|
608
|
+
setZoomOf(surface, next);
|
|
609
|
+
applyTransform(surface);
|
|
349
610
|
}
|
|
350
611
|
|
|
351
612
|
/**
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
613
|
+
* Honour the declared fit when a pane enters a panzoom mode (Design, Stylebook).
|
|
614
|
+
*
|
|
615
|
+
* Called on the mode transition only. At that point the panels exist with their declared widths but
|
|
616
|
+
* the iframes have not painted, which is why the geometry is driven by the panel widths rather than
|
|
617
|
+
* by anything measured from content.
|
|
355
618
|
*/
|
|
356
|
-
export function
|
|
357
|
-
|
|
619
|
+
export function fitOnCanvasEntry(surface: CanvasSurface): void {
|
|
620
|
+
/* An unmeasurable viewport would fit to the 5% floor, which is worse than the clipping this
|
|
621
|
+
replaces. Leave the zoom alone.
|
|
622
|
+
This guard is load-bearing rather than defensive now: a zoomed pane grid hides the other cell
|
|
623
|
+
with `display:none`, so its stage really does measure zero — and unzooming re-fits for free
|
|
624
|
+
through {@link observeCenterUntilStable}'s observer, which fires on the restored layout. */
|
|
625
|
+
if (surface.wrap.clientWidth <= 0) {
|
|
626
|
+
applyTransform(surface);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
applyFit(getFit(surface), surface);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Reset zoom to 100% and re-center horizontally, declaring 100% as this document's fit. */
|
|
633
|
+
export function resetZoom(surface: CanvasSurface = activeCanvasSurface()) {
|
|
634
|
+
declareFit(1, surface);
|
|
635
|
+
if (!surface.panzoomWrap) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
setZoomOf(surface, 1);
|
|
639
|
+
centerCanvas(surface);
|
|
640
|
+
applyTransform(surface);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* How far, in parent-viewport px, the pane must move down-screen to centre `rect`.
|
|
645
|
+
*
|
|
646
|
+
* Both inputs are `getBoundingClientRect` space, which is also the space the canvas host answers in
|
|
647
|
+
* (`iframe-host.ts`'s `pointForRect` composes the frame offset and the empirical scale before it
|
|
648
|
+
* hands a rect back), so one arithmetic serves a rect measured from a parent DOM element and a rect
|
|
649
|
+
* measured inside a canvas iframe alike.
|
|
650
|
+
*/
|
|
651
|
+
function centeringOffset(surface: CanvasSurface, rect: { top: number; height: number }): number {
|
|
652
|
+
const wrapRect = rectOf(surface.wrap);
|
|
358
653
|
const elCenterY = rect.top + rect.height / 2 - wrapRect.top;
|
|
359
|
-
|
|
654
|
+
return wrapRect.height / 2 - elCenterY;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* The element a reveal actually MOVES, or `null` when the panzoom transform is the only mover.
|
|
659
|
+
*
|
|
660
|
+
* This is the whole Edit-vs-Design distinction, in one place. Design, Stylebook and git-diff render
|
|
661
|
+
* a `.panzoom-wrap` and are moved by writing `view.panY`; **Edit renders no panzoom wrap at all** —
|
|
662
|
+
* it is an ordinary scrolling column (`.content-edit-canvas`, the edit branch of `renderCanvas`),
|
|
663
|
+
* and the only thing that brings a node below the fold into view there is its scroll container.
|
|
664
|
+
*
|
|
665
|
+
* Reading it off the stage's own active panel is what keeps the two callers from disagreeing. It is
|
|
666
|
+
* the discriminator `_panToEl` has always used, and `renderCanvas` re-establishes it on every
|
|
667
|
+
* render: a surface's `panels` is emptied and its panzoom wrap nulled before any branch runs, so
|
|
668
|
+
* the panel that answers here is this mode's, never the last mode's.
|
|
669
|
+
*
|
|
670
|
+
* **It took its surface as a parameter only after the rule grew a second hop.** It was zero-arity
|
|
671
|
+
* and called `getActivePanel()`, which is the FOCUSED pane's — so `revealBy(surface, …)` was handed
|
|
672
|
+
* one stage and scrolled another's. Benign in the tree, because both callers happened to pass the
|
|
673
|
+
* focused surface, and invisible to the one-hop rule for exactly the wrong reason: a forwarder
|
|
674
|
+
* whose whole body is `getActivePanel()?.scrollContainer` launders a focus read behind a name.
|
|
675
|
+
*
|
|
676
|
+
* @param {CanvasSurface} surface
|
|
677
|
+
* @returns {HTMLElement | null}
|
|
678
|
+
*/
|
|
679
|
+
export function revealScroller(surface: CanvasSurface): HTMLElement | null {
|
|
680
|
+
return panelOfSurface(surface)?.scrollContainer ?? null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Move the pane by `offsetY`, on whichever surface this mode actually moves.
|
|
685
|
+
*
|
|
686
|
+
* `smooth` is the caller's intent, not a property of the surface: the Outline's jump-to-node is a
|
|
687
|
+
* gesture an author watches, so it eases. The panzoom branch always eases — its 250ms tween is the
|
|
688
|
+
* one {@link import("./iframe-host").revealCanvasPath} waits out frame by frame — but a scroll has
|
|
689
|
+
* no such settle protocol, so an un-eased caller gets a scroll that is finished on return.
|
|
690
|
+
*/
|
|
691
|
+
function revealBy(surface: CanvasSurface, offsetY: number, smooth: boolean): void {
|
|
692
|
+
const scroller = revealScroller(surface);
|
|
693
|
+
if (!scroller) {
|
|
694
|
+
animatePanBy(surface, offsetY);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const top = scroller.scrollTop - offsetY;
|
|
698
|
+
if (smooth) {
|
|
699
|
+
scroller.scrollTo({ behavior: "smooth", top });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
// No clamp: the browser clamps `scrollTop` into `[0, scrollHeight - clientHeight]` itself, and
|
|
703
|
+
// Clamping here would only make a unit test agree with arithmetic the DOM never performs.
|
|
704
|
+
scroller.scrollTop = top;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Smoothly pan/scroll the canvas vertically to center the given DOM element.
|
|
709
|
+
*
|
|
710
|
+
* @param {HTMLElement} el
|
|
711
|
+
*/
|
|
712
|
+
function _panToEl(surface: CanvasSurface, el: HTMLElement) {
|
|
713
|
+
revealBy(surface, centeringOffset(surface, rectOf(el)), true);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Centre a PARENT-VIEWPORT rect in the pane — for callers whose target lives inside an iframe (no
|
|
718
|
+
* parent DOM element to measure; the host converts the measured iframe rect and passes it here).
|
|
719
|
+
*
|
|
720
|
+
* Was `animatePanBy` unconditionally, which made it a NO-OP on the Edit surface: `view.panY` only
|
|
721
|
+
* reaches the screen through a `.panzoom-wrap` transform, and Edit has none — so `revealCanvasPath`
|
|
722
|
+
* reported a node's unchanged off-screen point and the click that followed selected nothing.
|
|
723
|
+
*/
|
|
724
|
+
export function panToParentRect(
|
|
725
|
+
rect: { top: number; height: number },
|
|
726
|
+
surface: CanvasSurface = activeCanvasSurface(),
|
|
727
|
+
) {
|
|
728
|
+
revealBy(surface, centeringOffset(surface, rect), false);
|
|
360
729
|
}
|
|
361
730
|
|
|
362
|
-
/** Animate `
|
|
363
|
-
function animatePanBy(offsetY: number) {
|
|
364
|
-
const startY =
|
|
731
|
+
/** Animate a stage's `panY` by `offsetY` with the shared 250ms ease-out. */
|
|
732
|
+
function animatePanBy(surface: CanvasSurface, offsetY: number) {
|
|
733
|
+
const startY = surface.panY;
|
|
365
734
|
const targetY = startY + offsetY;
|
|
366
735
|
const start = performance.now();
|
|
367
736
|
const duration = 250;
|
|
368
737
|
const step = (now: number) => {
|
|
369
738
|
const t = Math.min((now - start) / duration, 1);
|
|
370
739
|
const ease = t * (2 - t);
|
|
371
|
-
|
|
372
|
-
applyTransform();
|
|
740
|
+
surface.panY = startY + (targetY - startY) * ease;
|
|
741
|
+
applyTransform(surface);
|
|
373
742
|
if (t < 1) {
|
|
374
743
|
requestAnimationFrame(step);
|
|
375
744
|
}
|
|
@@ -383,6 +752,7 @@ function animatePanBy(offsetY: number) {
|
|
|
383
752
|
* @param {(string | number)[]} path
|
|
384
753
|
*/
|
|
385
754
|
export function panToElement(path: (string | number)[]) {
|
|
755
|
+
const surface = activeCanvasSurface();
|
|
386
756
|
const panel = getActivePanel();
|
|
387
757
|
if (!panel?.canvas) {
|
|
388
758
|
return;
|
|
@@ -391,13 +761,685 @@ export function panToElement(path: (string | number)[]) {
|
|
|
391
761
|
if (!el) {
|
|
392
762
|
return;
|
|
393
763
|
}
|
|
394
|
-
_panToEl(
|
|
764
|
+
_panToEl(surface, el);
|
|
395
765
|
}
|
|
396
766
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
767
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Every canvas mode a pane can render, in the order the mode switcher lists them.
|
|
771
|
+
*
|
|
772
|
+
* `"preview"` is in the set even though it is not a base mode: `getCanvasMode()` composes the
|
|
773
|
+
* per-tab `ui.preview` flag with an `edit`/`design` base and presents "preview" to every downstream
|
|
774
|
+
* gate, and `tab.capabilities.modes` lists it exactly the same way. A caller naming a mode is
|
|
775
|
+
* naming the mode it will observe, so the enum is the observable set, not the storage set.
|
|
776
|
+
*/
|
|
777
|
+
export const CANVAS_MODES = [
|
|
778
|
+
"edit",
|
|
779
|
+
"design",
|
|
780
|
+
"preview",
|
|
781
|
+
"source",
|
|
782
|
+
"stylebook",
|
|
783
|
+
"grid",
|
|
784
|
+
"git-diff",
|
|
785
|
+
] as const;
|
|
786
|
+
|
|
787
|
+
export type CanvasMode = (typeof CANVAS_MODES)[number];
|
|
788
|
+
|
|
789
|
+
/** The base modes the preview flag composes with — the two the Canvas view axis shows it beside. */
|
|
790
|
+
const PREVIEWABLE_BASE_MODES = new Set(["edit", "design"]);
|
|
791
|
+
|
|
792
|
+
// ─── §4.2 axis 2 — the Canvas view ───────────────────────────────────────────
|
|
793
|
+
// `Edit │ Design` is a radio and `Preview` is a TOGGLE beside it, because that is what the storage
|
|
794
|
+
// Has always been: `preview` is not a base mode, it is a flag that composes with one.
|
|
795
|
+
//
|
|
796
|
+
// This was argued the other way, and the argument is worth keeping because it is half right. One
|
|
797
|
+
// Three-value control, it said, because a switcher showing "Design" selected while the pane was
|
|
798
|
+
// Previewing reports a state the app is not in. But the app IS in Design — with preview composed
|
|
799
|
+
// Over it — and a three-value radio could only ever show one of the two facts. It picked the
|
|
800
|
+
// Composed one and hid the base, so while previewing nothing said what you were previewing or what
|
|
801
|
+
// Leaving would return you to. A pressed toggle beside a marked radio shows both, which is the
|
|
802
|
+
// State, and neither half is silent.
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Every value of the Canvas view axis. `preview` composes with the other two rather than replacing
|
|
806
|
+
* them.
|
|
807
|
+
*/
|
|
808
|
+
export const CANVAS_VIEWS = ["edit", "design", "preview"] as const;
|
|
809
|
+
|
|
810
|
+
export type CanvasView = (typeof CANVAS_VIEWS)[number];
|
|
811
|
+
|
|
812
|
+
/** A tab's view state, as the axis reads it — its base mode and whether preview composes over it. */
|
|
813
|
+
interface ViewableTab {
|
|
814
|
+
capabilities: { modes: string[] };
|
|
815
|
+
session: { ui: { canvasMode: string; preview?: boolean } };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/*
|
|
819
|
+
* `canvasViewOf` lived here: it collapsed the base and the preview flag into ONE effective view, so
|
|
820
|
+
* a three-value radio could mark exactly one of Edit/Design/Preview. Splitting that radio into a
|
|
821
|
+
* two-value base plus a preview toggle took its last caller — the two halves are read separately
|
|
822
|
+
* now, by `canvasBaseViewOf` and `previewStateOf` below. `canvasModeOfPane` still composes the two
|
|
823
|
+
* for the RENDERER, which does want one answer.
|
|
824
|
+
*/
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* The views this document supports, in axis order — the control's entries.
|
|
828
|
+
*
|
|
829
|
+
* Filtered by `capabilities.modes` for the same reason the editor-kind dropdown is (§4.2): a
|
|
830
|
+
* permanently dead segment is a question the app refuses to answer.
|
|
831
|
+
*/
|
|
832
|
+
export function canvasViewsFor(tab: ViewableTab): CanvasView[] {
|
|
833
|
+
return CANVAS_VIEWS.filter((value) => tab.capabilities.modes.includes(value));
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* The BASE views a document supports — the radio's entries. Preview is not among them.
|
|
838
|
+
*
|
|
839
|
+
* Preview was the third value of a three-way radio, which made it read as a mode you leave Design
|
|
840
|
+
* to enter: while previewing, the control could not tell you which mode you would come back to. It
|
|
841
|
+
* has always been stored as a per-tab flag over an edit/design base, so the radio was describing
|
|
842
|
+
* the storage inaccurately. Split so the two axes are drawn as what they are.
|
|
843
|
+
*/
|
|
844
|
+
export function canvasBaseViewsFor(tab: ViewableTab): CanvasView[] {
|
|
845
|
+
return canvasViewsFor(tab).filter((value) => value !== "preview");
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** The base view the tab returns to — what the radio marks while preview is on. */
|
|
849
|
+
export function canvasBaseViewOf(tab: ViewableTab): CanvasView | null {
|
|
850
|
+
const base = tab.session.ui.canvasMode;
|
|
851
|
+
return PREVIEWABLE_BASE_MODES.has(base) ? (base as CanvasView) : null;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Whether this document can be previewed at all, and whether it is being.
|
|
856
|
+
*
|
|
857
|
+
* `available` is a fact about the DOCUMENT (its declared modes) and about the base it is currently
|
|
858
|
+
* in — `source` composes with no preview, so the toggle is not offered there.
|
|
859
|
+
*/
|
|
860
|
+
export function previewStateOf(tab: ViewableTab): { available: boolean; on: boolean } {
|
|
861
|
+
return {
|
|
862
|
+
available:
|
|
863
|
+
tab.capabilities.modes.includes("preview") &&
|
|
864
|
+
PREVIEWABLE_BASE_MODES.has(tab.session.ui.canvasMode),
|
|
865
|
+
on: tab.session.ui.preview === true,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Move a pane onto one of the three views. The one writer the segmented control routes through.
|
|
871
|
+
*
|
|
872
|
+
* Idempotent in both directions, which is what `canvas.togglePreview` could not be: arriving at
|
|
873
|
+
* Edit or Design clears the preview flag, so "Design" means Design from any starting state.
|
|
874
|
+
*
|
|
875
|
+
* @param tab The pane's tab.
|
|
876
|
+
* @param value The view to land on.
|
|
877
|
+
* @param setCanvasMode Writes the BASE mode of the tab it is GIVEN — `studio.ts`'s own, injected
|
|
878
|
+
* because this module does not own the canvas render loop. It took only a mode, which is how the
|
|
879
|
+
* side pane's `Edit │ Design │ Preview` control cleared its own tab's preview flag and then moved
|
|
880
|
+
* the FOCUSED pane's tab to `design`.
|
|
881
|
+
*/
|
|
882
|
+
export function setCanvasView<T extends ViewableTab>(
|
|
883
|
+
tab: T,
|
|
884
|
+
value: CanvasView,
|
|
885
|
+
setCanvasMode: (tab: T, mode: string) => void,
|
|
886
|
+
): void {
|
|
887
|
+
if (value === "preview") {
|
|
888
|
+
if (PREVIEWABLE_BASE_MODES.has(tab.session.ui.canvasMode)) {
|
|
889
|
+
tab.session.ui.preview = true;
|
|
890
|
+
}
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
tab.session.ui.preview = false;
|
|
894
|
+
if (tab.session.ui.canvasMode !== value) {
|
|
895
|
+
setCanvasMode(tab, value);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** What the canvas view verbs need that this module does not own. */
|
|
900
|
+
export interface CanvasCommandDeps {
|
|
901
|
+
/** The effective mode, `ui.preview` already composed in — `studio.ts`'s `getCanvasMode`. */
|
|
902
|
+
getCanvasMode: () => string;
|
|
903
|
+
/** Write the BASE mode (`ui.canvasMode`) of the tab it is given — `studio.ts`'s `setCanvasMode`. */
|
|
904
|
+
setCanvasMode: (tab: Tab | null, mode: string) => void;
|
|
905
|
+
/** Open or close a pane's resolving-with popover — `pane-context.ts`'s `setResolvingOpen`. */
|
|
906
|
+
setResolvingOpen: (paneId: string, open: boolean) => void;
|
|
907
|
+
/**
|
|
908
|
+
* Repaint ONE pane's stage — `studio.ts`'s `renderCanvas`, taking the pane.
|
|
909
|
+
*
|
|
910
|
+
* Injected rather than imported: `canvas-render.ts` imports this module, so reaching back for
|
|
911
|
+
* `renderCanvas` would close a cycle. It takes the pane id because the rendering-context verbs
|
|
912
|
+
* do: `renderOnly("canvas")` resolves the FOCUSED pane, so the side bar's size switcher wrote the
|
|
913
|
+
* side pane's tab and repainted the primary, leaving the stage it changed showing the old width.
|
|
914
|
+
*/
|
|
915
|
+
renderPane: (paneId: string) => void;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** A document is open in a pane — every verb here writes that pane's own view state. */
|
|
919
|
+
/* The pan-zoom surface, as one set rather than three spellings of it.
|
|
920
|
+
`canvas.setZoom` and `canvas.setFit` used to gate on `document.open` alone while their peers
|
|
921
|
+
`canvas.zoomIn`/`zoomOut`/`zoomReset` gate on `document.open && editor.kind === "canvas"` — so
|
|
922
|
+
driving Studio with a Library pane focused, `setZoom` and `setFit` both SUCCEEDED and wrote a
|
|
923
|
+
zoom the surface they name was not showing, in the same state where the three verbs that mean
|
|
924
|
+
the same thing refused with "requires an open document". `pane-context.ts`'s zoom pod already
|
|
925
|
+
draws itself for exactly these three modes; this is that rule, written once. */
|
|
926
|
+
const PANZOOM_MODES = new Set(["design", "stylebook", "git-diff"]);
|
|
927
|
+
|
|
928
|
+
const documentOpen = (ctx: { document: { open: boolean } }) => ctx.document.open;
|
|
929
|
+
|
|
930
|
+
/** The three values `session.ui.previewColorScheme` may hold (spec §9.5). */
|
|
931
|
+
const COLOR_SCHEMES = ["auto", "light", "dark"] as const;
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* The canvas view verbs — `setMode`, `setZoom`, `setEditZoom`.
|
|
935
|
+
*
|
|
936
|
+
* **These are the setters that retire `canvas.togglePreview`** (plan §13.3 clause 3). A toggle
|
|
937
|
+
* cannot say which state it ends in, so six screenshots taken through it photographed whichever way
|
|
938
|
+
* the default happened to point that week; `canvas.setMode { mode: "preview" }` means the same
|
|
939
|
+
* thing twice in a row and from any starting state.
|
|
940
|
+
*
|
|
941
|
+
* Three refusals, each a wrong picture that used to be accepted silently:
|
|
942
|
+
*
|
|
943
|
+
* - A mode the open document cannot render (`tab.capabilities.modes`) — a `.csv` has no `design`.
|
|
944
|
+
* - `"preview"` over a base mode it does not compose with — `ui.preview` is ignored in `source`, so
|
|
945
|
+
* setting it there would report a state the app is not in.
|
|
946
|
+
* - A zoom outside the supported range, which the interactive controls clamp and a caller may not.
|
|
947
|
+
*
|
|
948
|
+
* @param {CanvasCommandDeps} deps
|
|
949
|
+
* @returns {AnyCommand[]}
|
|
950
|
+
*/
|
|
951
|
+
export function canvasViewCommands(deps: CanvasCommandDeps): AnyCommand[] {
|
|
952
|
+
/** The open tab, or a refusal naming why there is nothing to act on. */
|
|
953
|
+
function requireTab(commandId: string) {
|
|
954
|
+
const tab = activeTab.value;
|
|
955
|
+
if (!tab) {
|
|
956
|
+
throw new RangeError(`command "${commandId}" needs an open document; no tab is active`);
|
|
957
|
+
}
|
|
958
|
+
return tab;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* The tab a rendering-context verb addresses: the named pane's, or the focused one's.
|
|
963
|
+
*
|
|
964
|
+
* The bar is drawn once PER PANE (§18), and the side pane's controls have always written the side
|
|
965
|
+
* pane's tab — so a verb that could only reach the focused one would be a narrower capability
|
|
966
|
+
* than the control it replaced, which is the opposite of the point.
|
|
967
|
+
*/
|
|
968
|
+
function contextTab(commandId: string, args: CommandArgValues) {
|
|
969
|
+
const { pane } = args as { pane?: unknown };
|
|
970
|
+
if (pane === undefined) {
|
|
971
|
+
return requireTab(commandId);
|
|
972
|
+
}
|
|
973
|
+
const paneId = stringArg(commandId, args, "pane");
|
|
974
|
+
const tab = tabOfPane(paneId);
|
|
975
|
+
if (!tab) {
|
|
976
|
+
throw new RangeError(
|
|
977
|
+
`command "${commandId}" argument "pane": "${paneId}" has no open document`,
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
return tab;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/** The optional pane selector every rendering-context verb accepts. */
|
|
984
|
+
const paneArg = {
|
|
985
|
+
pane: stringProperty("Which pane to render this way. Defaults to the focused one."),
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Repaint the pane a rendering-context verb just wrote — not the focused one.
|
|
990
|
+
*
|
|
991
|
+
* `renderOnly("canvas")` calls `renderCanvas()` with no pane, which resolves the FOCUSED one.
|
|
992
|
+
* These verbs take a `pane` precisely because the side bar addresses the side pane, so the two
|
|
993
|
+
* resolutions disagree exactly when it matters.
|
|
994
|
+
*/
|
|
995
|
+
function repaint(args: CommandArgValues): void {
|
|
996
|
+
const { pane } = args as { pane?: unknown };
|
|
997
|
+
deps.renderPane(typeof pane === "string" ? pane : workspace.activePaneId);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return [
|
|
1001
|
+
{
|
|
1002
|
+
args: argsSchema({
|
|
1003
|
+
mode: enumProperty(CANVAS_MODES, "The canvas mode to switch the active pane to."),
|
|
1004
|
+
}),
|
|
1005
|
+
category: "View",
|
|
1006
|
+
id: "canvas.setMode",
|
|
1007
|
+
level: "document",
|
|
1008
|
+
menus: ["palette"],
|
|
1009
|
+
group: "3_canvas",
|
|
1010
|
+
requires: "an open document",
|
|
1011
|
+
when: documentOpen,
|
|
1012
|
+
aiTool: {
|
|
1013
|
+
description:
|
|
1014
|
+
"Switch the active pane's canvas to a mode: edit, design, preview, source, stylebook, " +
|
|
1015
|
+
"grid or git-diff. Fails when the open document does not support that mode.",
|
|
1016
|
+
name: "set_canvas_mode",
|
|
1017
|
+
},
|
|
1018
|
+
run: (_commandCtx, args) => {
|
|
1019
|
+
const mode = enumArg("canvas.setMode", args, "mode", CANVAS_MODES);
|
|
1020
|
+
const tab = requireTab("canvas.setMode");
|
|
1021
|
+
const { modes } = tab.capabilities;
|
|
1022
|
+
if (!modes.includes(mode)) {
|
|
1023
|
+
throw new RangeError(
|
|
1024
|
+
`command "canvas.setMode" argument "mode": "${mode}" is not a mode this document ` +
|
|
1025
|
+
`supports — it declares: ${modes.join(", ")}`,
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
/* There is no second refusal here. The pane used to be asked too — the side pane hosted
|
|
1029
|
+
Code, Diff, Config, Entry, Grid and Library only, so a Canvas mode was refused by
|
|
1030
|
+
NAME with a sentence telling you to unsplit first. Both panes draw a live Canvas now
|
|
1031
|
+
(`panels/pane-grid.ts`), so the document's own declared modes are the whole answer. */
|
|
1032
|
+
if (mode === "preview") {
|
|
1033
|
+
const base = tab.session.ui.canvasMode;
|
|
1034
|
+
if (!PREVIEWABLE_BASE_MODES.has(base)) {
|
|
1035
|
+
throw new RangeError(
|
|
1036
|
+
`command "canvas.setMode" argument "mode": "preview" composes with the edit and ` +
|
|
1037
|
+
`design base modes; this pane is in "${base}"`,
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
tab.session.ui.preview = true;
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
// Idempotent in both directions: leaving preview is part of arriving anywhere else, so a
|
|
1044
|
+
// Shot that says "design" gets design whether or not the previous step turned preview on.
|
|
1045
|
+
tab.session.ui.preview = false;
|
|
1046
|
+
// The tab `requireTab()` already resolved, not "whatever is focused when the write lands".
|
|
1047
|
+
deps.setCanvasMode(tab, mode);
|
|
1048
|
+
},
|
|
1049
|
+
title: "Set Canvas Mode",
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
args: argsSchema({
|
|
1053
|
+
zoom: numberProperty("Pan-zoom scale, 1 = 100%.", {
|
|
1054
|
+
maximum: PAN_ZOOM_MAX,
|
|
1055
|
+
minimum: PAN_ZOOM_MIN,
|
|
1056
|
+
}),
|
|
1057
|
+
}),
|
|
1058
|
+
category: "View",
|
|
1059
|
+
id: "canvas.setZoom",
|
|
1060
|
+
level: "document",
|
|
1061
|
+
menus: ["palette"],
|
|
1062
|
+
group: "3_canvas",
|
|
1063
|
+
requires: "a document on the pan-zoom surface",
|
|
1064
|
+
when: documentOpen,
|
|
1065
|
+
enablement: () => PANZOOM_MODES.has(deps.getCanvasMode()),
|
|
1066
|
+
run: (_commandCtx, args) => {
|
|
1067
|
+
requireTab("canvas.setZoom");
|
|
1068
|
+
setUserZoom(boundedNumberArg("canvas.setZoom", args, "zoom", PAN_ZOOM_MIN, PAN_ZOOM_MAX));
|
|
1069
|
+
},
|
|
1070
|
+
title: "Set Zoom",
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
args: argsSchema({
|
|
1074
|
+
fit: {
|
|
1075
|
+
description:
|
|
1076
|
+
'How the document is framed: "width" | "page" | "none", or a numeric pan-zoom scale.',
|
|
1077
|
+
oneOf: [
|
|
1078
|
+
{ enum: [...FIT_WORDS], type: "string" },
|
|
1079
|
+
{ maximum: PAN_ZOOM_MAX, minimum: PAN_ZOOM_MIN, type: "number" },
|
|
1080
|
+
],
|
|
1081
|
+
},
|
|
1082
|
+
}),
|
|
1083
|
+
category: "View",
|
|
1084
|
+
id: "canvas.setFit",
|
|
1085
|
+
level: "document",
|
|
1086
|
+
menus: ["palette"],
|
|
1087
|
+
group: "3_canvas",
|
|
1088
|
+
// The sentence has to match the gate, or the refusal teaches the wrong thing.
|
|
1089
|
+
requires: "a document on the pan-zoom surface",
|
|
1090
|
+
when: documentOpen,
|
|
1091
|
+
enablement: () => PANZOOM_MODES.has(deps.getCanvasMode()),
|
|
1092
|
+
run: (_commandCtx, args) => {
|
|
1093
|
+
requireTab("canvas.setFit");
|
|
1094
|
+
setFit(fitArg("canvas.setFit", args, "fit"));
|
|
1095
|
+
},
|
|
1096
|
+
title: "Set Fit",
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
args: argsSchema({
|
|
1100
|
+
zoom: numberProperty("Edit-mode content zoom, 1 = 100%.", {
|
|
1101
|
+
maximum: EDIT_ZOOM_MAX,
|
|
1102
|
+
minimum: EDIT_ZOOM_MIN,
|
|
1103
|
+
}),
|
|
1104
|
+
}),
|
|
1105
|
+
category: "View",
|
|
1106
|
+
id: "canvas.setEditZoom",
|
|
1107
|
+
level: "document",
|
|
1108
|
+
menus: ["palette"],
|
|
1109
|
+
group: "3_canvas",
|
|
1110
|
+
requires: "a document in edit mode",
|
|
1111
|
+
when: documentOpen,
|
|
1112
|
+
enablement: () => deps.getCanvasMode() === "edit",
|
|
1113
|
+
run: (_commandCtx, args) => {
|
|
1114
|
+
requireTab("canvas.setEditZoom");
|
|
1115
|
+
setEditZoom(
|
|
1116
|
+
boundedNumberArg("canvas.setEditZoom", args, "zoom", EDIT_ZOOM_MIN, EDIT_ZOOM_MAX),
|
|
1117
|
+
);
|
|
1118
|
+
},
|
|
1119
|
+
title: "Set Edit Zoom",
|
|
1120
|
+
},
|
|
1121
|
+
/*
|
|
1122
|
+
* ── The rendering context (§4.2's control ③) ──────────────────────────────────────────────
|
|
1123
|
+
*
|
|
1124
|
+
* Three setters for the three axes the Context popover offers. The popover wrote
|
|
1125
|
+
* `session.ui.activeMedia` / `previewColorScheme` / `showLayout` through `updateUi` directly, so
|
|
1126
|
+
* none of them was a command: not in the palette, not scriptable, not addressable by the
|
|
1127
|
+
* assistant, and not bindable — in a design whose first principle is that a capability exists
|
|
1128
|
+
* as a `Command` record and every surface projects it.
|
|
1129
|
+
*
|
|
1130
|
+
* SETTERS, not cycles. §5.3's keymap declares `⌘⌥↑`/`⌘⌥↓` and `⌘⌥⇧S` to cycle the size and
|
|
1131
|
+
* scheme axes, and a chord carries no argument, so those chords need `next`/`prev` records of
|
|
1132
|
+
* their own — a delta each, which is what §13's R1 forbids a screenshot from naming. Naming the
|
|
1133
|
+
* state you end in works from every surface; the chords are a separate decision and are not
|
|
1134
|
+
* made here.
|
|
1135
|
+
*/
|
|
1136
|
+
{
|
|
1137
|
+
args: {
|
|
1138
|
+
additionalProperties: false,
|
|
1139
|
+
properties: {
|
|
1140
|
+
...paneArg,
|
|
1141
|
+
media: {
|
|
1142
|
+
description:
|
|
1143
|
+
"A breakpoint key the document renders under, or null for the base rendering.",
|
|
1144
|
+
type: ["string", "null"],
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
required: ["media"],
|
|
1148
|
+
type: "object",
|
|
1149
|
+
},
|
|
1150
|
+
category: "View",
|
|
1151
|
+
id: "canvas.setBreakpoint",
|
|
1152
|
+
level: "document",
|
|
1153
|
+
menus: ["palette"],
|
|
1154
|
+
group: "3_canvas",
|
|
1155
|
+
requires: "an open document",
|
|
1156
|
+
when: documentOpen,
|
|
1157
|
+
run: (_commandCtx, args) => {
|
|
1158
|
+
const tab = contextTab("canvas.setBreakpoint", args);
|
|
1159
|
+
const { media } = args as { media?: unknown };
|
|
1160
|
+
if (media !== null && typeof media !== "string") {
|
|
1161
|
+
throw new RangeError(
|
|
1162
|
+
`command "canvas.setBreakpoint" argument "media": expected a breakpoint key or null`,
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
/* REFUSES a key the document cannot render under, rather than rendering at a breakpoint
|
|
1166
|
+
that does not exist — the rule `data.expandRow` applies to a state entry's name.
|
|
1167
|
+
`getEffectiveMedia` and not `document.$media`: the popover offers the SITE's breakpoints
|
|
1168
|
+
too, and a command that refused what the control offers would be worse than no command. */
|
|
1169
|
+
const declared = Object.keys(
|
|
1170
|
+
getEffectiveMedia(tab.doc.document?.$media as Record<string, string> | undefined),
|
|
1171
|
+
);
|
|
1172
|
+
if (media !== null && !declared.includes(media)) {
|
|
1173
|
+
throw new RangeError(
|
|
1174
|
+
`command "canvas.setBreakpoint" argument "media": "${media}" is not a breakpoint this ` +
|
|
1175
|
+
`document defines — it defines: ${declared.length > 0 ? declared.join(", ") : "nothing"}`,
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
tab.session.ui.activeMedia = media;
|
|
1179
|
+
repaint(args);
|
|
1180
|
+
},
|
|
1181
|
+
title: "Set Breakpoint",
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
args: argsSchema({
|
|
1185
|
+
...paneArg,
|
|
1186
|
+
scheme: enumProperty(COLOR_SCHEMES, "Which color scheme the canvas renders in."),
|
|
1187
|
+
}),
|
|
1188
|
+
category: "View",
|
|
1189
|
+
id: "canvas.setColorScheme",
|
|
1190
|
+
level: "document",
|
|
1191
|
+
menus: ["palette"],
|
|
1192
|
+
group: "3_canvas",
|
|
1193
|
+
requires: "an open document",
|
|
1194
|
+
when: documentOpen,
|
|
1195
|
+
run: (_commandCtx, args) => {
|
|
1196
|
+
const tab = contextTab("canvas.setColorScheme", args);
|
|
1197
|
+
tab.session.ui.previewColorScheme = enumArg(
|
|
1198
|
+
"canvas.setColorScheme",
|
|
1199
|
+
args,
|
|
1200
|
+
"scheme",
|
|
1201
|
+
COLOR_SCHEMES,
|
|
1202
|
+
) as "auto" | "dark" | "light";
|
|
1203
|
+
repaint(args);
|
|
1204
|
+
},
|
|
1205
|
+
title: "Set Color Scheme",
|
|
1206
|
+
},
|
|
1207
|
+
/*
|
|
1208
|
+
* The language a pane RENDERS AS — and only that.
|
|
1209
|
+
*
|
|
1210
|
+
* Jx has no message catalogue: a translation is a different file in a different directory
|
|
1211
|
+
* (site-architecture §13.3), so "show this page in French" is a navigation and the locale
|
|
1212
|
+
* preset owns it. What is left over is genuinely a rendering context, and it is the half of
|
|
1213
|
+
* §13.4 the canvas can show today: the artboard root's `lang` and `dir`. That is why this is a
|
|
1214
|
+
* verb and not a label — an RTL locale mirrors the layout on screen. (§13.4's other half,
|
|
1215
|
+
* `$page.locale` in the rendered state, is injected by the compiler and not by the canvas's
|
|
1216
|
+
* `substitutePreviewParams`; previewing it is a separate change to the render path.)
|
|
1217
|
+
*
|
|
1218
|
+
* It lives in this file rather than in `i18n/i18n-commands.ts` because `contextTab`, `paneArg`
|
|
1219
|
+
* and `repaint` are local closures here, and every rendering-context verb needs all three: the
|
|
1220
|
+
* bar is drawn per pane, so a verb that could only reach the focused one would repaint the
|
|
1221
|
+
* wrong stage. `insert.data` sits outside its namespace's module for the same reason.
|
|
1222
|
+
*/
|
|
1223
|
+
{
|
|
1224
|
+
args: argsSchema(
|
|
1225
|
+
{
|
|
1226
|
+
...paneArg,
|
|
1227
|
+
locale: stringProperty("BCP 47 tag this pane renders as."),
|
|
1228
|
+
},
|
|
1229
|
+
["locale"],
|
|
1230
|
+
),
|
|
1231
|
+
category: "View",
|
|
1232
|
+
id: "i18n.switchLocale",
|
|
1233
|
+
level: "document",
|
|
1234
|
+
menus: ["palette"],
|
|
1235
|
+
group: "3_canvas",
|
|
1236
|
+
requires: "an open document in a project that declares more than one locale",
|
|
1237
|
+
when: (ctx) => ctx.document.open && ctx.project.isMultilingual,
|
|
1238
|
+
run: (_commandCtx, args) => {
|
|
1239
|
+
const tab = contextTab("i18n.switchLocale", args);
|
|
1240
|
+
const locale = stringArg("i18n.switchLocale", args, "locale");
|
|
1241
|
+
/* Read at RUN time. `projectState` is replaced wholesale rather than mutated, so a value
|
|
1242
|
+
cached at module scope answers for whichever project was open when this file loaded. */
|
|
1243
|
+
const i18n = getEffectiveLocales();
|
|
1244
|
+
/* REFUSES a tag the project does not declare, rather than clamping to the default — the
|
|
1245
|
+
rule `canvas.setBreakpoint` applies to a breakpoint key. A locale is a URL prefix and an
|
|
1246
|
+
`hreflang` at the same time, so rendering "as" a language the site does not have would
|
|
1247
|
+
preview a page that cannot exist. */
|
|
1248
|
+
if (i18n === null || !i18n.locales.includes(locale)) {
|
|
1249
|
+
throw new RangeError(
|
|
1250
|
+
`command "i18n.switchLocale" argument "locale": "${locale}" is not a locale this ` +
|
|
1251
|
+
`project declares — it declares: ${i18n === null ? "nothing" : i18n.locales.join(", ")}`,
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
tab.session.ui.previewLocale = locale;
|
|
1255
|
+
repaint(args);
|
|
1256
|
+
},
|
|
1257
|
+
title: "Set Rendering Language",
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
args: argsSchema({
|
|
1261
|
+
...paneArg,
|
|
1262
|
+
visible: booleanProperty("True to draw the page's layout elements, false to hide them."),
|
|
1263
|
+
}),
|
|
1264
|
+
category: "View",
|
|
1265
|
+
id: "canvas.setLayoutVisible",
|
|
1266
|
+
level: "document",
|
|
1267
|
+
menus: ["palette"],
|
|
1268
|
+
group: "3_canvas",
|
|
1269
|
+
requires: "an open document",
|
|
1270
|
+
when: documentOpen,
|
|
1271
|
+
run: (_commandCtx, args) => {
|
|
1272
|
+
const tab = contextTab("canvas.setLayoutVisible", args);
|
|
1273
|
+
tab.session.ui.showLayout = booleanArg("canvas.setLayoutVisible", args, "visible");
|
|
1274
|
+
repaint(args);
|
|
1275
|
+
},
|
|
1276
|
+
title: "Show Layout Elements",
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
args: {
|
|
1280
|
+
additionalProperties: false,
|
|
1281
|
+
properties: {
|
|
1282
|
+
...paneArg,
|
|
1283
|
+
open: {
|
|
1284
|
+
default: true,
|
|
1285
|
+
description: "True to open the resolving-with popover, false to close it.",
|
|
1286
|
+
type: "boolean",
|
|
1287
|
+
},
|
|
1288
|
+
},
|
|
1289
|
+
required: [],
|
|
1290
|
+
type: "object",
|
|
1291
|
+
},
|
|
1292
|
+
category: "View",
|
|
1293
|
+
id: "canvas.setResolvingOpen",
|
|
1294
|
+
level: "document",
|
|
1295
|
+
menus: ["palette"],
|
|
1296
|
+
group: "3_canvas",
|
|
1297
|
+
requires: "an open document",
|
|
1298
|
+
when: documentOpen,
|
|
1299
|
+
/*
|
|
1300
|
+
* The route params and component test props live in a popover now, and a transient surface
|
|
1301
|
+
* opens by COMMAND rather than by clicking (plan §13.2) — otherwise the one shot that types a
|
|
1302
|
+
* test value would need a CSS selector to reach it, which §13's contract forbids outright.
|
|
1303
|
+
* The pointer and the camera use the same door.
|
|
1304
|
+
*/
|
|
1305
|
+
run: (_commandCtx, args) => {
|
|
1306
|
+
// Resolve the tab first, so a pane with no document refuses before anything opens.
|
|
1307
|
+
contextTab("canvas.setResolvingOpen", args);
|
|
1308
|
+
const { open, pane } = args as { open?: unknown; pane?: unknown };
|
|
1309
|
+
// `open` defaults to true and is never COERCED: `{ open: "no" }` would otherwise read as a
|
|
1310
|
+
// Close, the class of silent wrong answer this setter family exists to stop.
|
|
1311
|
+
deps.setResolvingOpen(
|
|
1312
|
+
typeof pane === "string" ? pane : workspace.activePaneId,
|
|
1313
|
+
open === undefined || booleanArg("canvas.setResolvingOpen", args, "open"),
|
|
1314
|
+
);
|
|
1315
|
+
},
|
|
1316
|
+
title: "Show Resolving Values",
|
|
1317
|
+
},
|
|
1318
|
+
/*
|
|
1319
|
+
* ── The values a render resolves WITH ─────────────────────────────────────────────────────
|
|
1320
|
+
*
|
|
1321
|
+
* The two controls in that popover wrote `session.ui` through `updateUi` directly, while every
|
|
1322
|
+
* control in the rendering-context popover beside them runs a command — and `runContextCommand`
|
|
1323
|
+
* says why in as many words: "the popover WAS the capability, and the palette, the assistant and
|
|
1324
|
+
* `__jxAutomation` had no name for it." Moving these behind a click without naming them would
|
|
1325
|
+
* have reintroduced that, one layer deeper: a value you can now only reach by opening a popover
|
|
1326
|
+
* and typing.
|
|
1327
|
+
*
|
|
1328
|
+
* The screenshot budget agrees. `counter-test-prop` typed into `pane.primary/prop:count`; with
|
|
1329
|
+
* a verb it spends a `cmd` instead, so `inputSteps` falls 14 → 13 and the region leaves the
|
|
1330
|
+
* manifest, dropping `nonDerivedRegions` 11 → 10. Both budgets may only ratchet down, and this
|
|
1331
|
+
* ratchets both.
|
|
1332
|
+
*/
|
|
1333
|
+
{
|
|
1334
|
+
args: {
|
|
1335
|
+
additionalProperties: false,
|
|
1336
|
+
properties: {
|
|
1337
|
+
...paneArg,
|
|
1338
|
+
name: stringProperty("The prop's name, as the component's state declares it."),
|
|
1339
|
+
value: {
|
|
1340
|
+
description:
|
|
1341
|
+
"The test value — any JSON. Null clears it and the prop returns to its default.",
|
|
1342
|
+
type: ["string", "number", "boolean", "array", "object", "null"],
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
required: ["name"],
|
|
1346
|
+
type: "object",
|
|
1347
|
+
},
|
|
1348
|
+
category: "View",
|
|
1349
|
+
id: "canvas.setTestProp",
|
|
1350
|
+
level: "document",
|
|
1351
|
+
menus: ["palette"],
|
|
1352
|
+
group: "3_canvas",
|
|
1353
|
+
requires: "an open component document",
|
|
1354
|
+
when: documentOpen,
|
|
1355
|
+
run: (_commandCtx, args) => {
|
|
1356
|
+
const tab = contextTab("canvas.setTestProp", args);
|
|
1357
|
+
const name = stringArg("canvas.setTestProp", args, "name");
|
|
1358
|
+
// REFUSES a prop the component does not declare, rather than seeding a value nothing reads
|
|
1359
|
+
// — the rule `data.expandRow` and `canvas.setBreakpoint` both apply to their own names.
|
|
1360
|
+
const declared = Object.keys(
|
|
1361
|
+
(tab.doc.document?.state as Record<string, unknown> | undefined) ?? {},
|
|
1362
|
+
);
|
|
1363
|
+
if (!declared.includes(name)) {
|
|
1364
|
+
throw new RangeError(
|
|
1365
|
+
`command "canvas.setTestProp" argument "name": "${name}" is not a prop this component ` +
|
|
1366
|
+
`declares — it declares: ${declared.length > 0 ? declared.join(", ") : "nothing"}`,
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
const { value } = args as { value?: unknown };
|
|
1370
|
+
const next = { ...tab.session.ui.previewProps };
|
|
1371
|
+
if (value === null || value === undefined) {
|
|
1372
|
+
delete next[name];
|
|
1373
|
+
} else {
|
|
1374
|
+
next[name] = value as JsonValue;
|
|
1375
|
+
}
|
|
1376
|
+
tab.session.ui.previewProps = Object.keys(next).length > 0 ? next : null;
|
|
1377
|
+
repaint(args);
|
|
1378
|
+
},
|
|
1379
|
+
title: "Set Test Value",
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
args: {
|
|
1383
|
+
additionalProperties: false,
|
|
1384
|
+
properties: {
|
|
1385
|
+
...paneArg,
|
|
1386
|
+
name: stringProperty("The route parameter's name, without brackets."),
|
|
1387
|
+
value: stringProperty("The value to preview the route with. Empty clears it."),
|
|
1388
|
+
},
|
|
1389
|
+
required: ["name", "value"],
|
|
1390
|
+
type: "object",
|
|
1391
|
+
},
|
|
1392
|
+
category: "View",
|
|
1393
|
+
id: "canvas.setRouteParam",
|
|
1394
|
+
level: "document",
|
|
1395
|
+
menus: ["palette"],
|
|
1396
|
+
group: "3_canvas",
|
|
1397
|
+
requires: "an open page with a dynamic route",
|
|
1398
|
+
when: documentOpen,
|
|
1399
|
+
run: (_commandCtx, args) => {
|
|
1400
|
+
const tab = contextTab("canvas.setRouteParam", args);
|
|
1401
|
+
const name = stringArg("canvas.setRouteParam", args, "name");
|
|
1402
|
+
const declared = dynamicRouteParams(tab.documentPath);
|
|
1403
|
+
if (!declared.includes(name)) {
|
|
1404
|
+
throw new RangeError(
|
|
1405
|
+
`command "canvas.setRouteParam" argument "name": "${name}" is not a parameter of this ` +
|
|
1406
|
+
`route — it has: ${declared.length > 0 ? declared.join(", ") : "none"}`,
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
tab.session.ui.previewParams = {
|
|
1410
|
+
...tab.session.ui.previewParams,
|
|
1411
|
+
[name]: stringArg("canvas.setRouteParam", args, "value"),
|
|
1412
|
+
};
|
|
1413
|
+
repaint(args);
|
|
1414
|
+
},
|
|
1415
|
+
title: "Set Route Parameter",
|
|
1416
|
+
},
|
|
1417
|
+
];
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Register the canvas view verbs. Called from the bootstrap, beside the transforms they drive.
|
|
1422
|
+
*
|
|
1423
|
+
* @param {CommandRegistry} registry
|
|
1424
|
+
* @param {CanvasCommandDeps} deps
|
|
1425
|
+
*/
|
|
1426
|
+
export function registerCanvasViewCommands(
|
|
1427
|
+
registry: CommandRegistry,
|
|
1428
|
+
deps: CanvasCommandDeps,
|
|
1429
|
+
): void {
|
|
1430
|
+
registry.registerAll(canvasViewCommands(deps));
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Toggle the "active" class on a stage's artboard headers, from that stage's own `activeMedia`.
|
|
1435
|
+
*
|
|
1436
|
+
* Surface-taking for the same reason everything else here is: the breakpoint a header reports is a
|
|
1437
|
+
* fact about the document that stage is drawing, and reading `activeTab` drew the focused pane's
|
|
1438
|
+
* breakpoint onto the other pane's artboards.
|
|
1439
|
+
*/
|
|
1440
|
+
export function updateActivePanelHeaders(surface: CanvasSurface = activeCanvasSurface()) {
|
|
1441
|
+
const activeMedia = activeMediaOfPane(surface.paneId);
|
|
1442
|
+
for (const p of surface.panels) {
|
|
401
1443
|
const header = p.element?.querySelector(".canvas-panel-header");
|
|
402
1444
|
if (header) {
|
|
403
1445
|
const isActive =
|