@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
|
@@ -0,0 +1,1477 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* The pane's own chrome — region ⑦ (context bar) and region ⑩ (the floating zoom pod).
|
|
4
|
+
*
|
|
5
|
+
* This replaces `#tab-bar`, which was a 28px band styled identically to the tab strip above it,
|
|
6
|
+
* holding **five unrelated axes** with no labels on any of them: a document-stack breadcrumb
|
|
7
|
+
* (navigation), a zoom widget (viewport), route params and component test props (document data), a
|
|
8
|
+
* colour-scheme switch and feature toggles (rendering state), and Export (a mode action). Plan §3.2
|
|
9
|
+
* ⑦ replaces them with three axes that each say what they are:
|
|
10
|
+
*
|
|
11
|
+
* - **Editor kind** — `Canvas ⌄`, offering only the kinds this document declares AND its pane can
|
|
12
|
+
* host ({@link hostableKindsOf}), so the control can never contain a permanently dead entry. A
|
|
13
|
+
* document with one kind renders the name as text rather than as a dropdown that cannot go
|
|
14
|
+
* anywhere.
|
|
15
|
+
* - **Canvas view** — `Edit │ Design` as a radio ({@link canvasBaseViewsFor}) with a **Preview**
|
|
16
|
+
* toggle beside it ({@link previewStateOf}), and none at all in a pane that may not host the
|
|
17
|
+
* Canvas. The two are drawn apart because they are two axes: preview is a flag over an edit or
|
|
18
|
+
* design base, so the radio marks the base throughout and the toggle says whether it is on.
|
|
19
|
+
* - **Rendering context** — `md ⌄ Light ⌄`, folding the size breakpoint, the colour scheme, the
|
|
20
|
+
* feature queries and the layout show/hide switch into one popover. Per §2 principle 5 this
|
|
21
|
+
* control **only selects**; its footer is "Manage contexts…", which routes to the definition site
|
|
22
|
+
* — Project Settings › Contexts (`settings/contexts-section.ts`), the one place a breakpoint, a
|
|
23
|
+
* colour scheme or a feature query is defined.
|
|
24
|
+
* - **Resolving with** — the document DATA a render resolves against: a page's route params, a
|
|
25
|
+
* component's test props. Its own popover beside the context one, headed "resolving with", with
|
|
26
|
+
* the fields in a vertical stack. A SECOND popover rather than a fourth group in the first,
|
|
27
|
+
* because these are values you type and everything in the rendering-context popover is something
|
|
28
|
+
* you pick — and because a row of text fields is what made the 28px band unreadable.
|
|
29
|
+
*
|
|
30
|
+
* **The bar is not a grid row.** It renders inside the pane's own cell (`#pane-chrome`, stacked
|
|
31
|
+
* over `#canvas-wrap`), because a per-pane surface cannot be a row of the application grid — the
|
|
32
|
+
* second pane would have no way to have one. The stage is offset by {@link PANE_CONTEXT_VAR} rather
|
|
33
|
+
* than by a track, and the zoom pod floats bottom-right over the canvas exactly as §3.2 ⑩ asks.
|
|
34
|
+
*
|
|
35
|
+
* **The read-only banner rides with the bar.** `collab/presence-chips.ts` wrote the sentence a
|
|
36
|
+
* read-only guest needs before their first keystroke (§7.4); the surface that owes it a home is
|
|
37
|
+
* this one, because it is the per-pane, per-document chrome that sits directly above the editing
|
|
38
|
+
* surface. It is stacked under the bar inside `.pc-band` and the offset is MEASURED from that band
|
|
39
|
+
* ({@link applyPaneContextOffset}), so a two-line banner pushes the stage down instead of covering
|
|
40
|
+
* the document it is warning you about.
|
|
41
|
+
*
|
|
42
|
+
* Module shape follows `tab-strip.ts`: mount(host, ctx) → effectScope/effect → render().
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
46
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
47
|
+
import { projectState, updateUi } from "../store";
|
|
48
|
+
import { effect, effectScope } from "../reactivity";
|
|
49
|
+
import { PRIMARY_PANE, focusPane, workspace } from "../workspace/workspace";
|
|
50
|
+
import {
|
|
51
|
+
activeMediaOfPane,
|
|
52
|
+
canvasModeOfPane,
|
|
53
|
+
canvasModeOfTab,
|
|
54
|
+
derivationOfPane,
|
|
55
|
+
surfaceForPane,
|
|
56
|
+
tabOfPane,
|
|
57
|
+
} from "../canvas/canvas-surface";
|
|
58
|
+
import {
|
|
59
|
+
DERIVE_PRESETS,
|
|
60
|
+
PRESET_LABELS,
|
|
61
|
+
declaredMedia,
|
|
62
|
+
deriveRefusal,
|
|
63
|
+
pinRefusal,
|
|
64
|
+
presetRefusal,
|
|
65
|
+
} from "../workspace/pane-derive";
|
|
66
|
+
import { renderPopover } from "../ui/layers";
|
|
67
|
+
import { rectOf } from "../utils/geometry";
|
|
68
|
+
import { paneRegion } from "../ui/regions";
|
|
69
|
+
import {
|
|
70
|
+
fitToScreen,
|
|
71
|
+
getFit,
|
|
72
|
+
resetZoom,
|
|
73
|
+
canvasBaseViewOf,
|
|
74
|
+
canvasBaseViewsFor,
|
|
75
|
+
previewStateOf,
|
|
76
|
+
setCanvasView,
|
|
77
|
+
setEditZoom,
|
|
78
|
+
setFit,
|
|
79
|
+
setUserZoom,
|
|
80
|
+
stageZoom,
|
|
81
|
+
} from "../canvas/canvas-utils";
|
|
82
|
+
import { editorKindOf, editorKindsOf, modeForEditorKind } from "../tabs/tab";
|
|
83
|
+
import { collabState } from "../collab/collab-state";
|
|
84
|
+
import { readOnlyBannerTemplate } from "../collab/presence-chips";
|
|
85
|
+
import { activeRegistry } from "../commands/active-registry";
|
|
86
|
+
import { getEffectiveLayoutPath, getEffectiveLocales, getEffectiveMedia } from "../site-context";
|
|
87
|
+
import { localeLabel, localeOfPath } from "@jxsuite/schema/locale";
|
|
88
|
+
import { isSchemeQuery } from "../utils/canvas-media";
|
|
89
|
+
import { dynamicRouteParams, loadParamValues, pagePathsDef } from "../page-params";
|
|
90
|
+
import { componentPropEntries, isComponentDoc } from "../component-props";
|
|
91
|
+
import { mediaDisplayName } from "./shared";
|
|
92
|
+
import type { CanvasView, FitMode } from "../canvas/canvas-utils";
|
|
93
|
+
import { EDITOR_KIND_LABELS } from "../commands/context";
|
|
94
|
+
import type { EditorKind } from "../commands/context";
|
|
95
|
+
import type { ParamValues } from "../page-params";
|
|
96
|
+
import type { Tab } from "../tabs/tab";
|
|
97
|
+
import type { ResolvedI18n } from "@jxsuite/schema/locale";
|
|
98
|
+
import type { JsonValue } from "../types";
|
|
99
|
+
import type { EffectScope } from "@vue/reactivity";
|
|
100
|
+
import type { TemplateResult } from "lit-html";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The CSS variable the stage is offset by while a context bar is on screen.
|
|
104
|
+
*
|
|
105
|
+
* The bar overlays the pane cell, so something has to keep the canvas out from under it. A variable
|
|
106
|
+
* written by one projection is the same shape `applyDockLayout()` uses for the dock widths — and it
|
|
107
|
+
* means "no tab open" costs the welcome screen no dead band.
|
|
108
|
+
*/
|
|
109
|
+
export const PANE_CONTEXT_VAR = "--pane-context-h";
|
|
110
|
+
|
|
111
|
+
/** The bar's height. Declared here because the offset projection and the stylesheet must agree. */
|
|
112
|
+
const PANE_CONTEXT_HEIGHT = 28;
|
|
113
|
+
|
|
114
|
+
export interface PaneContextCtx {
|
|
115
|
+
exportFile: () => void;
|
|
116
|
+
/*
|
|
117
|
+
* There is no `getCanvasMode` here, and there cannot be one.
|
|
118
|
+
*
|
|
119
|
+
* It answered for the FOCUSED pane — `studio.ts` composes it from `workspace.activePaneId` — and
|
|
120
|
+
* this bar is drawn once per pane, from `tabOfPane(paneId)`. One reader was left: `exportTpl`,
|
|
121
|
+
* which is why entering Code in EITHER pane put an Export button in BOTH bars. Every mode
|
|
122
|
+
* question this module asks is now asked of the pane it is drawing: `canvasModeOfTab(tab)` for
|
|
123
|
+
* the tab's own effective mode, `canvasModeOfPane(paneId)` for the stage's.
|
|
124
|
+
*/
|
|
125
|
+
/**
|
|
126
|
+
* Write the BASE mode of the tab it is GIVEN. The editor-kind dropdown and the view control both
|
|
127
|
+
* land here, and both are drawn per pane — so the tab is a parameter. It used to be `(mode:
|
|
128
|
+
* string) => void`, resolving `activeTab.value` inside `studio.ts`, which made the side pane's
|
|
129
|
+
* Editor picker a control over the primary's document.
|
|
130
|
+
*/
|
|
131
|
+
setCanvasMode: (tab: Tab, mode: string) => void;
|
|
132
|
+
parseMediaEntries: (media: Record<string, string> | null | undefined) => {
|
|
133
|
+
sizeBreakpoints: {
|
|
134
|
+
name: string;
|
|
135
|
+
query: string;
|
|
136
|
+
width: number;
|
|
137
|
+
type: string;
|
|
138
|
+
}[];
|
|
139
|
+
featureQueries: { name: string; query: string }[];
|
|
140
|
+
baseWidth: number;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Where each pane's chrome renders. One entry per drawn cell.
|
|
146
|
+
*
|
|
147
|
+
* A Map rather than a `let _host`, for the reason the whole grid exists: this bar states one pane's
|
|
148
|
+
* editor kind, canvas view, rendering context and zoom, and two panes have two of each.
|
|
149
|
+
* `panels/pane-grid.ts` attaches a cell's `.pane-chrome` as the cell is built.
|
|
150
|
+
*/
|
|
151
|
+
const _hosts = new Map<string, HTMLElement>();
|
|
152
|
+
|
|
153
|
+
let _ctx: PaneContextCtx | null = null;
|
|
154
|
+
|
|
155
|
+
let _scope: EffectScope | null = null;
|
|
156
|
+
|
|
157
|
+
/** Human names for the three Canvas views. */
|
|
158
|
+
const CANVAS_VIEW_LABELS: Readonly<Record<CanvasView, string>> = {
|
|
159
|
+
design: "Design",
|
|
160
|
+
edit: "Edit",
|
|
161
|
+
preview: "Preview",
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** The colour-scheme segment's values, labels and tooltips. */
|
|
165
|
+
const SCHEMES = [
|
|
166
|
+
["auto", "Auto", "Follow the OS color scheme"],
|
|
167
|
+
["light", "Light", "Force the light scheme"],
|
|
168
|
+
["dark", "Dark", "Force the dark scheme"],
|
|
169
|
+
] as const;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Give a pane's chrome somewhere to paint, or take it away.
|
|
173
|
+
*
|
|
174
|
+
* Called by `panels/pane-grid.ts` as a cell is built and as it is disposed.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} paneId
|
|
177
|
+
* @param {HTMLElement | null} host
|
|
178
|
+
*/
|
|
179
|
+
export function attachPaneChromeHost(paneId: string, host: HTMLElement | null): void {
|
|
180
|
+
const previous = _hosts.get(paneId);
|
|
181
|
+
if (previous === host) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (previous) {
|
|
185
|
+
litRender(nothing, previous);
|
|
186
|
+
applyPaneContextOffset(0, previous);
|
|
187
|
+
}
|
|
188
|
+
if (host) {
|
|
189
|
+
_hosts.set(paneId, host);
|
|
190
|
+
renderPane(paneId, host);
|
|
191
|
+
} else {
|
|
192
|
+
_hosts.delete(paneId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Mount the pane chrome. Idempotent.
|
|
198
|
+
*
|
|
199
|
+
* `host` is the PRIMARY pane's, the same bargain `panels/tab-strip.ts`'s `mount` makes: the
|
|
200
|
+
* bootstrap holds the primary's cell, and every other pane's host arrives from the grid through
|
|
201
|
+
* {@link attachPaneChromeHost}.
|
|
202
|
+
*
|
|
203
|
+
* @param {HTMLElement} host
|
|
204
|
+
* @param {PaneContextCtx} ctx
|
|
205
|
+
*/
|
|
206
|
+
export function mount(host: HTMLElement, ctx: PaneContextCtx) {
|
|
207
|
+
_ctx = ctx;
|
|
208
|
+
attachPaneChromeHost(PRIMARY_PANE, host);
|
|
209
|
+
_scope = effectScope();
|
|
210
|
+
_scope.run(() => {
|
|
211
|
+
effect(() => {
|
|
212
|
+
/* EVERY pane's tab. The bar states its own pane's editor kind, canvas view, rendering
|
|
213
|
+
context and zoom, so the unfocused one has to repaint when ITS document moves — tracking
|
|
214
|
+
`activeTab` alone left the side pane's bar frozen describing whatever it last drew. */
|
|
215
|
+
for (const pane of workspace.panes) {
|
|
216
|
+
/* NO DERIVATION READS HERE, and the reason is one rule rather than a tally. `render()`
|
|
217
|
+
runs inside this effect and draws EVERY attached pane, so whatever a template here reads
|
|
218
|
+
is ALREADY a dependency and restating it as a `void` line chooses nothing: `derived.kind`
|
|
219
|
+
picks the bar's shape, `activeMediaOfPane` reads `kind`/`preset`/`media` for the Context
|
|
220
|
+
axis, `zoomOf` reads `derived.zoom` for the pod, and {@link zoomPodTpl} asks
|
|
221
|
+
`canvasModeOfPane`, which for a lens answers `derived.mode` and decides whether the pod
|
|
222
|
+
is drawn at all — pinned by lens-chrome's "the pod is drawn from the LENS's mode".
|
|
223
|
+
An earlier version of this comment listed `mode` among the fields no template reads,
|
|
224
|
+
which was wrong about the fact and right about the deletion. `status` and `reason` really
|
|
225
|
+
are unread here, so tracking them only repainted this bar for a change it does not draw.
|
|
226
|
+
The stage is the surface that needs those two declared, because `renderCanvasImpl` runs
|
|
227
|
+
in a rAF rather than in an effect — see `studio.ts`. */
|
|
228
|
+
const tab = tabOfPane(pane.id);
|
|
229
|
+
if (!tab) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
// Read reactive properties to establish tracking — mirrors the toolbar's subset.
|
|
233
|
+
void tab.doc.document;
|
|
234
|
+
void tab.doc.document?.$layout;
|
|
235
|
+
void tab.doc.mode;
|
|
236
|
+
void tab.documentPath;
|
|
237
|
+
void tab.capabilities.modes;
|
|
238
|
+
void tab.session.ui.activeMedia;
|
|
239
|
+
void tab.session.ui.canvasMode;
|
|
240
|
+
void tab.session.ui.editZoom;
|
|
241
|
+
void tab.session.ui.featureToggles;
|
|
242
|
+
void tab.session.ui.preview;
|
|
243
|
+
void tab.session.ui.previewColorScheme;
|
|
244
|
+
void tab.session.ui.previewLocale;
|
|
245
|
+
void tab.session.ui.previewParams;
|
|
246
|
+
void tab.session.ui.previewProps;
|
|
247
|
+
void tab.session.ui.showLayout;
|
|
248
|
+
void tab.session.ui.zoom;
|
|
249
|
+
// The read-only banner (§7.4) is part of this chrome, so its two facts are tracked here
|
|
250
|
+
// Too: a peer downgrading you mid-session must make the sentence appear, not wait for the
|
|
251
|
+
// Next zoom.
|
|
252
|
+
void collabState(tab).active;
|
|
253
|
+
void collabState(tab).readOnly;
|
|
254
|
+
}
|
|
255
|
+
render();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function unmount() {
|
|
261
|
+
_scope?.stop();
|
|
262
|
+
_scope = null;
|
|
263
|
+
for (const host of _hosts.values()) {
|
|
264
|
+
applyPaneContextOffset(0, host);
|
|
265
|
+
}
|
|
266
|
+
_hosts.clear();
|
|
267
|
+
_ctx = null;
|
|
268
|
+
applyPaneContextOffset(0);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Keep the stage clear of the pane's top band.
|
|
273
|
+
*
|
|
274
|
+
* One projection, one variable — the canvas is offset only by what is actually rendered, so the
|
|
275
|
+
* welcome screen (no tab, no bar) does not open under a 28px gap nothing explains, and a read-only
|
|
276
|
+
* banner does not sit on top of the document it is warning you about.
|
|
277
|
+
*
|
|
278
|
+
* **On the PANE, not on `:root`**, for the reason `panels/jump-bar.ts` gives at the same seam: two
|
|
279
|
+
* cells have two bands of different heights, and a single document-level number offsets both stages
|
|
280
|
+
* by whichever pane painted last. A host outside a cell writes the root, which is what it meant
|
|
281
|
+
* when the shell had one bar.
|
|
282
|
+
*
|
|
283
|
+
* @param {number} height Band height in px. `0` when the pane has no chrome.
|
|
284
|
+
* @param {HTMLElement | null} [host] The bar's host. Its cell takes the variable when it has one.
|
|
285
|
+
*/
|
|
286
|
+
export function applyPaneContextOffset(height: number, host?: HTMLElement | null): void {
|
|
287
|
+
const target = host?.closest<HTMLElement>(".pane") ?? document.documentElement;
|
|
288
|
+
target.style.setProperty(PANE_CONTEXT_VAR, `${height}px`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* How tall the top band came out — the bar plus whatever banners rode with it.
|
|
293
|
+
*
|
|
294
|
+
* MEASURED rather than summed, because the banner's height is its wrapped text and only layout
|
|
295
|
+
* knows how many lines that is. `offsetHeight` is 0 in a DOM with no layout engine (every unit
|
|
296
|
+
* test), so the bar's declared height is the floor: the offset is then exactly what it was before
|
|
297
|
+
* banners existed, which is the honest answer when nothing has been laid out.
|
|
298
|
+
*/
|
|
299
|
+
function topBandHeight(host: HTMLElement): number {
|
|
300
|
+
const band = host.querySelector<HTMLElement>(".pc-band");
|
|
301
|
+
return Math.max(band?.offsetHeight ?? 0, PANE_CONTEXT_HEIGHT);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Whether this tab's editor has any of the bar's three axes to offer.
|
|
306
|
+
*
|
|
307
|
+
* Project Settings has none: it declares one editor kind, no canvas view, and no rendering context
|
|
308
|
+
* — and the one control the bar WOULD contribute, "Manage contexts…", routes to a section of the
|
|
309
|
+
* very document on screen. A bar of three inert controls above the definition site they point at is
|
|
310
|
+
* chrome that says nothing, so the stage takes the whole pane.
|
|
311
|
+
*
|
|
312
|
+
* @param {Tab} tab
|
|
313
|
+
* @returns {boolean}
|
|
314
|
+
*/
|
|
315
|
+
function wantsContextBar(tab: Tab): boolean {
|
|
316
|
+
return tab.session.ui.canvasMode !== "settings";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Paint every attached pane's chrome. */
|
|
320
|
+
export function render() {
|
|
321
|
+
for (const [paneId, host] of _hosts) {
|
|
322
|
+
renderPane(paneId, host);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Paint one pane's chrome into its own host, from its own tab. */
|
|
327
|
+
function renderPane(paneId: string, host: HTMLElement) {
|
|
328
|
+
if (!_ctx) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const tab = tabOfPane(paneId);
|
|
333
|
+
const show = Boolean(tab) && wantsContextBar(tab as Tab);
|
|
334
|
+
litRender(show ? paneChromeTemplate(tab as Tab, paneId, _ctx) : nothing, host);
|
|
335
|
+
applyPaneContextOffset(show ? topBandHeight(host) : 0, host);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.error("pane-context render error:", error);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ─── The preset menu · the first renderer of `context/pane` ──────────────────
|
|
342
|
+
|
|
343
|
+
/** The open preset menu, if any. One at a time, like every other menu in this shell. */
|
|
344
|
+
let _presetMenu: { dismiss: () => void } | null = null;
|
|
345
|
+
|
|
346
|
+
/** Close the preset menu, if it is open. Idempotent. */
|
|
347
|
+
export function dismissPresetMenu(): void {
|
|
348
|
+
_presetMenu?.dismiss();
|
|
349
|
+
_presetMenu = null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The `⟲` trigger, in the context bar's leading slot — the one that was empty.
|
|
354
|
+
*
|
|
355
|
+
* `.pc-spacer` existed to push the three axes right. It is where §18.4's preset menu goes because
|
|
356
|
+
* the menu is about THIS PANE and the bar is the pane's own chrome; the alternative homes (the tab
|
|
357
|
+
* strip, the jump bar) are about a document and an address respectively.
|
|
358
|
+
*/
|
|
359
|
+
function presetTriggerTpl(paneId: string): TemplateResult {
|
|
360
|
+
return html`<button
|
|
361
|
+
class="pc-derive-trigger"
|
|
362
|
+
aria-haspopup="menu"
|
|
363
|
+
title="Show something beside this pane"
|
|
364
|
+
@click=${(event: MouseEvent) => {
|
|
365
|
+
openPresetMenu(event, paneId);
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
<span aria-hidden="true">⟲</span>
|
|
369
|
+
</button>`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Open the preset menu.
|
|
374
|
+
*
|
|
375
|
+
* **There is no `pane.showDerivePresets`.** §13.5, quoted verbatim in `canvas/canvas-render.ts`:
|
|
376
|
+
* opening a menu to press an item names a CONTROL; the item is the command. Every row here runs
|
|
377
|
+
* `pane.derive`, `pane.pin` or `pane.unsplit` with its own arguments, and the screenshot manifest
|
|
378
|
+
* addresses those ids directly rather than driving this widget. Its region is
|
|
379
|
+
* `overlay.menu:derive-presets`, which `ui/layers.ts` derives from the slot key — no budget is
|
|
380
|
+
* spent.
|
|
381
|
+
*/
|
|
382
|
+
function openPresetMenu(event: MouseEvent, paneId: string): void {
|
|
383
|
+
event.preventDefault();
|
|
384
|
+
event.stopPropagation();
|
|
385
|
+
dismissPresetMenu();
|
|
386
|
+
const registry = activeRegistry();
|
|
387
|
+
if (!registry) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const anchor = rectOf(event.currentTarget as HTMLElement);
|
|
391
|
+
const left = Math.round(Math.min(anchor.left, window.innerWidth - 4));
|
|
392
|
+
const top = Math.round(anchor.bottom);
|
|
393
|
+
const rows = presetRows(paneId);
|
|
394
|
+
_presetMenu = renderPopover(
|
|
395
|
+
html`<sp-popover open style="position:fixed;z-index:10000;left:${left}px;top:${top}px">
|
|
396
|
+
<sp-menu role="menu" aria-label="Show beside this pane">
|
|
397
|
+
${rows.map(
|
|
398
|
+
(row) => html`<sp-menu-item
|
|
399
|
+
role="menuitem"
|
|
400
|
+
?disabled=${row.disabled !== null}
|
|
401
|
+
title=${row.disabled ? `requires ${row.disabled}` : row.label}
|
|
402
|
+
@click=${() => {
|
|
403
|
+
dismissPresetMenu();
|
|
404
|
+
if (row.disabled === null) {
|
|
405
|
+
/* THE PANE THE MENU IS ABOUT, before the verb that resolves the focus runs.
|
|
406
|
+
See {@link PresetRow.pane}: all three of these commands take the focused pane as
|
|
407
|
+
their subject, and a pointer gesture on this pane has already focused it — but a
|
|
408
|
+
keyboard activation has not, because `panels/pane-grid.ts` focuses on
|
|
409
|
+
pointerdown. Without this line the secondary pane's menu derived from the
|
|
410
|
+
primary and its Unsplit closed the wrong pane, by keyboard only. */
|
|
411
|
+
focusPane(row.pane);
|
|
412
|
+
void registry.run(row.command, row.args);
|
|
413
|
+
}
|
|
414
|
+
}}
|
|
415
|
+
>${row.label}</sp-menu-item
|
|
416
|
+
>`,
|
|
417
|
+
)}
|
|
418
|
+
</sp-menu>
|
|
419
|
+
</sp-popover>`,
|
|
420
|
+
{
|
|
421
|
+
dismissOnOutsideClick: true,
|
|
422
|
+
onDismiss: () => {
|
|
423
|
+
_presetMenu = null;
|
|
424
|
+
},
|
|
425
|
+
region: "derive-presets",
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** One row of the preset menu: a command, its arguments, and why it cannot run when it cannot. */
|
|
431
|
+
export interface PresetRow {
|
|
432
|
+
label: string;
|
|
433
|
+
command: string;
|
|
434
|
+
args: Record<string, unknown>;
|
|
435
|
+
/**
|
|
436
|
+
* The pane this row is ABOUT — the one whose ⟲ opened the menu.
|
|
437
|
+
*
|
|
438
|
+
* The previous round made the row's ANSWER a pure function of the pane and left its ACTION
|
|
439
|
+
* resolving the focus: every one of these three commands takes the focused pane as its subject
|
|
440
|
+
* (`pane.derive`'s `activePane()`/`sidePane()`, `pane.unsplit`'s `workspace.activePaneId`), so a
|
|
441
|
+
* row read off the secondary pane's menu derived from the primary. Reachable by keyboard and only
|
|
442
|
+
* by keyboard, which is why it survived a browser pass: `panels/pane-grid.ts` focuses a pane on
|
|
443
|
+
* POINTERDOWN, and a keyboard activation of a menu item fires `click` alone.
|
|
444
|
+
*
|
|
445
|
+
* Carried on the row rather than passed as a command argument. A pane id in `args` would make
|
|
446
|
+
* "which pane" a public input of `pane.derive` — a third property on a record the palette already
|
|
447
|
+
* cannot prompt for, and a value the AI tool would have to invent — to say something the focus
|
|
448
|
+
* already says. The menu moves the focus to the pane it is a menu of, which is what a pointer
|
|
449
|
+
* gesture on that pane does anyway, and all three verbs then agree with the row.
|
|
450
|
+
*/
|
|
451
|
+
pane: string;
|
|
452
|
+
/** The `requires` sentence, or null when the row can run. */
|
|
453
|
+
disabled: string | null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* The rows, built from the registry and from {@link presetRefusal} — one per projection, one per
|
|
458
|
+
* declared breakpoint, then the two exits.
|
|
459
|
+
*
|
|
460
|
+
* Exported because it is the whole content of the menu and it is a pure function of the pane: the
|
|
461
|
+
* widget is untestable in a DOM with no layout, and this is not.
|
|
462
|
+
*
|
|
463
|
+
* @param {string} paneId
|
|
464
|
+
* @returns {PresetRow[]}
|
|
465
|
+
*/
|
|
466
|
+
export function presetRows(paneId: string): PresetRow[] {
|
|
467
|
+
const registry = activeRegistry();
|
|
468
|
+
const tab = tabOfPane(paneId);
|
|
469
|
+
const derived = derivationOfPane(paneId);
|
|
470
|
+
/* {@link deriveRefusal}, not `registry.disabledReason("pane.derive")`.
|
|
471
|
+
The registry resolves its context from the FOCUS, so a menu built for a pane it was handed by
|
|
472
|
+
name got the answer for whichever pane the keyboard happened to be in: the same pane's rows
|
|
473
|
+
read "enabled" or "an open document in a pane that is not itself derived" depending on where
|
|
474
|
+
the author had last clicked. `scripts/check-pane-singletons.ts` rule 4 could not see it — the
|
|
475
|
+
focus read was a method call on a registry value, dispatched by a string id into a closure in
|
|
476
|
+
another module. It counts `disabledReason` and `isEnabled` themselves as focus reads now,
|
|
477
|
+
which names the shape without pretending to follow the dispatch. */
|
|
478
|
+
const deriveReason = deriveRefusal(paneId);
|
|
479
|
+
const rows: PresetRow[] = [];
|
|
480
|
+
for (const preset of DERIVE_PRESETS) {
|
|
481
|
+
if (preset === "breakpoint" || preset === "locale") {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
rows.push({
|
|
485
|
+
args: { preset },
|
|
486
|
+
command: "pane.derive",
|
|
487
|
+
disabled: deriveReason ?? presetRefusal(preset, paneId, null),
|
|
488
|
+
label: PRESET_LABELS[preset],
|
|
489
|
+
pane: paneId,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
// "Same page at ⟨breakpoint⟩" — one row per breakpoint the document declares, because the item IS
|
|
493
|
+
// The command and a submenu would be a second control between the author and it.
|
|
494
|
+
for (const media of tab && !derived ? declaredMedia(tab) : []) {
|
|
495
|
+
rows.push({
|
|
496
|
+
/* BASE omits `media` rather than passing `""`. `optionalStringArg` refuses an empty string
|
|
497
|
+
by design — "present but blank" is the shape that hides a missing value — so the base row
|
|
498
|
+
has to say nothing rather than say nothing loudly. Caught by opening the menu in a real
|
|
499
|
+
browser: every row ran, and the base one threw `expected a non-empty string, got ""`. */
|
|
500
|
+
args: media === null ? { preset: "breakpoint" } : { media, preset: "breakpoint" },
|
|
501
|
+
command: "pane.derive",
|
|
502
|
+
disabled: deriveReason ?? presetRefusal("breakpoint", paneId, media),
|
|
503
|
+
label: `${PRESET_LABELS.breakpoint} ${media ? mediaDisplayName(media) : "Base"}`,
|
|
504
|
+
pane: paneId,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
/* "Same page in ⟨language⟩" — one row per locale the PROJECT declares, in the same shape as the
|
|
508
|
+
breakpoint rows above and with one difference: there is no omit-the-argument row. A breakpoint
|
|
509
|
+
has a base size that is spelled by saying nothing; every locale is a real tag, including the
|
|
510
|
+
default one, whose file `translationPathFor` puts at the unprefixed path under
|
|
511
|
+
`prefix-except-default`. The label is the locale's own AUTONYM — "français", not "French" —
|
|
512
|
+
because that is what the person looking for their language scans a list for.
|
|
513
|
+
A project that declares no `i18n` block at all contributes no rows: `getEffectiveLocales()`
|
|
514
|
+
answers null, and the menu is the same menu it has always been. One that declares exactly one
|
|
515
|
+
locale gets its one row carrying {@link presetRefusal}'s sentence, which names Project
|
|
516
|
+
Settings › Locales — the author asked for languages, so the answer is where to add the second
|
|
517
|
+
one rather than silence. */
|
|
518
|
+
for (const locale of tab && !derived ? (getEffectiveLocales()?.locales ?? []) : []) {
|
|
519
|
+
rows.push({
|
|
520
|
+
args: { locale, preset: "locale" },
|
|
521
|
+
command: "pane.derive",
|
|
522
|
+
disabled: deriveReason ?? presetRefusal("locale", paneId, null, locale),
|
|
523
|
+
label: `${PRESET_LABELS.locale} ${localeLabel(locale)}`,
|
|
524
|
+
pane: paneId,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/* The two exits, and the reason only ONE of them takes a pane.
|
|
528
|
+
`pane.pin`'s subject is the grid's derived pane — the author reaches it from the palette while
|
|
529
|
+
the keyboard is in the page they are editing — and {@link pinRefusal} answers about a named
|
|
530
|
+
pane without reading the focus, so the menu can ask it about its own. `pane.unsplit`'s
|
|
531
|
+
enablement is `workspace.panes.length > 1`, a fact about the grid with no pane in it at all;
|
|
532
|
+
its `requires` is data on the record and the boolean is the same from anywhere. Its RUN is a
|
|
533
|
+
focus read, like `pane.derive`'s — which is what {@link PresetRow.pane} is for. */
|
|
534
|
+
const exits: [string, string | null][] = [
|
|
535
|
+
["pane.pin", pinRefusal(paneId)],
|
|
536
|
+
[
|
|
537
|
+
"pane.unsplit",
|
|
538
|
+
workspace.panes.length > 1 ? null : (registry?.get("pane.unsplit")?.requires ?? null),
|
|
539
|
+
],
|
|
540
|
+
];
|
|
541
|
+
for (const [id, disabled] of exits) {
|
|
542
|
+
const command = registry?.get(id);
|
|
543
|
+
if (command) {
|
|
544
|
+
rows.push({ args: {}, command: id, disabled, label: command.title, pane: paneId });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return rows;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** One labelled axis. The label is the point: five unlabelled controls is what this replaces. */
|
|
551
|
+
function axisTpl(label: string, control: TemplateResult): TemplateResult {
|
|
552
|
+
return html`
|
|
553
|
+
<div class="pc-axis">
|
|
554
|
+
<span class="pc-axis-label">${label}</span>
|
|
555
|
+
${control}
|
|
556
|
+
</div>
|
|
557
|
+
`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* The bar, always the same three axes.
|
|
562
|
+
*
|
|
563
|
+
* There is no takeover branch. Opening a function body or a formula reveals the dock's Logic tab
|
|
564
|
+
* (P8) and leaves the canvas standing underneath it, so the axes still describe the document on the
|
|
565
|
+
* stage and the zoom pod still has something to zoom. Suppressing them while the dock was open
|
|
566
|
+
* removed the controls for the very document the reader could still see.
|
|
567
|
+
*
|
|
568
|
+
* **There is no breadcrumb either.** The address is ⑥'s job — `panels/jump-bar.ts`, one row above —
|
|
569
|
+
* and the Logic tab's own header carries the Close. This bar drew a second Back and a second trail
|
|
570
|
+
* beside both of them.
|
|
571
|
+
*/
|
|
572
|
+
function paneChromeTemplate(tab: Tab, paneId: string, ctx: PaneContextCtx): TemplateResult {
|
|
573
|
+
const kind = editorKindOf(tab);
|
|
574
|
+
/* A LENS suppresses the two axes that WRITE. Editor kind and Canvas view both land in
|
|
575
|
+
`ctx.setCanvasMode(tab, …)`, and that tab belongs to the pane beside this one — so a control
|
|
576
|
+
drawn in the lens would flip the document the author is editing. The lens's own mode is the
|
|
577
|
+
derivation, chosen when it was created and changed by re-deriving. The Context axis stays as
|
|
578
|
+
a static summary for the same reason (it writes `updateUi(tab, …)`), and the zoom pod stays
|
|
579
|
+
because zoom is the one view fact a lens genuinely owns. */
|
|
580
|
+
const lens = derivationOfPane(paneId)?.kind === "lens";
|
|
581
|
+
|
|
582
|
+
return html`
|
|
583
|
+
<div class="pc-band">
|
|
584
|
+
<div class="pane-context" data-jx-region=${paneRegion(paneId, "context")}>
|
|
585
|
+
<div class="pc-spacer">
|
|
586
|
+
${
|
|
587
|
+
/* …and NO ⟲ trigger in a lens. Every projection row in the menu it opens is permanently
|
|
588
|
+
disabled from there (a derived pane cannot derive again) and the breakpoint rows are
|
|
589
|
+
suppressed outright, leaving one live row — Unsplit — which the derivation chip's ✕
|
|
590
|
+
in this pane's own strip already runs. A control that can do nothing from where it is
|
|
591
|
+
drawn is the class this phase has deleted three times. A COMPANION keeps it: "Keep
|
|
592
|
+
This Document" is live there and it is genuinely about that pane. The SPACER stays
|
|
593
|
+
either way: it is what pushes the axes right, and dropping it would move the one
|
|
594
|
+
control a lens does draw. */
|
|
595
|
+
lens ? nothing : presetTriggerTpl(paneId)
|
|
596
|
+
}
|
|
597
|
+
</div>
|
|
598
|
+
${
|
|
599
|
+
lens
|
|
600
|
+
? renderingSummaryTpl(paneId)
|
|
601
|
+
: html`${editorKindTpl(tab, ctx)} ${kind === "canvas" ? viewTpl(tab, ctx) : nothing}
|
|
602
|
+
${renderingContextTpl(tab, paneId, ctx)} ${exportTpl(tab, ctx)}`
|
|
603
|
+
}
|
|
604
|
+
</div>
|
|
605
|
+
${lens ? nothing : readOnlyBannerTemplate(tab)}
|
|
606
|
+
</div>
|
|
607
|
+
${zoomPodTpl(tab, paneId)}
|
|
608
|
+
`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* A lens's rendering context, stated rather than offered.
|
|
613
|
+
*
|
|
614
|
+
* Open question 1, answered "keep it, read-only": a lens drawn under different preview params than
|
|
615
|
+
* the pane it is a lens OF would be lying about what it is a lens of, so the summary has to be on
|
|
616
|
+
* screen — but the popover behind it writes the source tab's `session.ui`, which is the one thing a
|
|
617
|
+
* lens must never do.
|
|
618
|
+
*/
|
|
619
|
+
function renderingSummaryTpl(paneId: string): TemplateResult {
|
|
620
|
+
/* No `ctx.parseMediaEntries` here. It parsed the document's whole `$media` map, discarded the
|
|
621
|
+
result through `void sizeBreakpoints` and printed the media NAME — a parse per lens-bar render
|
|
622
|
+
for a value that was never read. `mediaDisplayName` is the only lookup this line needs. */
|
|
623
|
+
/* {@link activeMediaOfPane}, and it exists for exactly this. `session.ui.activeMedia` is per-TAB
|
|
624
|
+
and a lens shares its tab with the pane beside it, so this axis printed the breakpoint the
|
|
625
|
+
SOURCE pane was on: the stage drew the Tablet artboard and the line under it said "Base". The
|
|
626
|
+
docstring above says a lens drawn under different params "would be lying about what it is a
|
|
627
|
+
lens of" — and it lied about the one axis the preset is named after. */
|
|
628
|
+
const media = activeMediaOfPane(paneId);
|
|
629
|
+
/* A lens shares its TAB, so it renders under the tab's preview locale whether it asked to or not
|
|
630
|
+
— and a lens whose stage is mirrored while its own line says nothing is the same lie about the
|
|
631
|
+
rendering context this summary exists to prevent. Stated only when it differs from the
|
|
632
|
+
document's own language, for the reason the trigger beside it is: a pane drawing the file it
|
|
633
|
+
has open in the language that file is written in has nothing to report. */
|
|
634
|
+
const locale = localeOf(tabOfPane(paneId));
|
|
635
|
+
const stated = `${media ? mediaDisplayName(media) : "Base"}${
|
|
636
|
+
locale?.overridden ? ` · ${localeLabel(locale.effective)}` : ""
|
|
637
|
+
}`;
|
|
638
|
+
return axisTpl("Context", html`<span class="pc-static">${stated}</span>`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ─── Axis 1 · Editor kind ────────────────────────────────────────────────────
|
|
642
|
+
|
|
643
|
+
function editorKindTpl(tab: Tab, ctx: PaneContextCtx): TemplateResult {
|
|
644
|
+
/* What the DOCUMENT declares, and nothing else. This read `hostableKindsOf` — the declared kinds
|
|
645
|
+
narrowed by what the tab's pane was allowed to host — so a page in the side pane was offered
|
|
646
|
+
Code and not Design. The pane cap is gone; a control that cannot go anywhere is still the
|
|
647
|
+
defect this axis exists to remove, and one declared kind still renders as text. */
|
|
648
|
+
const kinds = editorKindsOf(tab);
|
|
649
|
+
const current = editorKindOf(tab);
|
|
650
|
+
if (kinds.length < 2) {
|
|
651
|
+
// One kind is not a choice. Rendering it as a dropdown would be a control that cannot move —
|
|
652
|
+
// Which is the defect this axis exists to remove, in miniature.
|
|
653
|
+
return axisTpl("Editor", html`<span class="pc-static">${EDITOR_KIND_LABELS[current]}</span>`);
|
|
654
|
+
}
|
|
655
|
+
return axisTpl(
|
|
656
|
+
"Editor",
|
|
657
|
+
html`
|
|
658
|
+
<sp-picker
|
|
659
|
+
size="s"
|
|
660
|
+
quiet
|
|
661
|
+
class="pc-editor-kind"
|
|
662
|
+
label="Editor"
|
|
663
|
+
value=${current}
|
|
664
|
+
@change=${(e: Event) => {
|
|
665
|
+
const kind = (e.target as HTMLInputElement).value as EditorKind;
|
|
666
|
+
const mode = modeForEditorKind(tab, kind);
|
|
667
|
+
if (!mode) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
tab.session.ui.preview = false;
|
|
671
|
+
ctx.setCanvasMode(tab, mode);
|
|
672
|
+
}}
|
|
673
|
+
>
|
|
674
|
+
${kinds.map(
|
|
675
|
+
(kind) => html`<sp-menu-item value=${kind}>${EDITOR_KIND_LABELS[kind]}</sp-menu-item>`,
|
|
676
|
+
)}
|
|
677
|
+
</sp-picker>
|
|
678
|
+
`,
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ─── Axis 2 · Canvas view ────────────────────────────────────────────────────
|
|
683
|
+
|
|
684
|
+
function viewTpl(tab: Tab, ctx: PaneContextCtx): TemplateResult | typeof nothing {
|
|
685
|
+
// Every pane draws a live Canvas, so this is no longer narrowed by WHERE the tab is — only by
|
|
686
|
+
// What the document declares. A document with no Canvas view still draws no view group.
|
|
687
|
+
const views = canvasBaseViewsFor(tab);
|
|
688
|
+
if (views.length === 0) {
|
|
689
|
+
return nothing;
|
|
690
|
+
}
|
|
691
|
+
const current = canvasBaseViewOf(tab);
|
|
692
|
+
const preview = previewStateOf(tab);
|
|
693
|
+
/*
|
|
694
|
+
* TWO CONTROLS, because they are two axes.
|
|
695
|
+
*
|
|
696
|
+
* `Edit │ Design │ Preview` was one three-way radio, and it lied about the state it was showing:
|
|
697
|
+
* preview is stored as a flag OVER an edit/design base, so while it was on the radio could not
|
|
698
|
+
* say which mode you were previewing — or which one Escape would return you to. Now the radio
|
|
699
|
+
* marks the base the whole time and the toggle sits beside it, pressed.
|
|
700
|
+
*/
|
|
701
|
+
return axisTpl(
|
|
702
|
+
"View",
|
|
703
|
+
html`
|
|
704
|
+
<sp-action-group compact size="s" class="pc-view" role="radiogroup" aria-label="Canvas view">
|
|
705
|
+
${views.map(
|
|
706
|
+
(value) => html`
|
|
707
|
+
<sp-action-button
|
|
708
|
+
size="s"
|
|
709
|
+
role="radio"
|
|
710
|
+
aria-checked=${value === current ? "true" : "false"}
|
|
711
|
+
?selected=${value === current}
|
|
712
|
+
title=${`Show this document in ${CANVAS_VIEW_LABELS[value]}`}
|
|
713
|
+
@click=${() => setCanvasView(tab, value, ctx.setCanvasMode)}
|
|
714
|
+
>
|
|
715
|
+
${CANVAS_VIEW_LABELS[value]}
|
|
716
|
+
</sp-action-button>
|
|
717
|
+
`,
|
|
718
|
+
)}
|
|
719
|
+
</sp-action-group>
|
|
720
|
+
${
|
|
721
|
+
preview.available
|
|
722
|
+
? html`
|
|
723
|
+
<sp-action-button
|
|
724
|
+
size="s"
|
|
725
|
+
class="pc-preview-toggle"
|
|
726
|
+
toggles
|
|
727
|
+
?selected=${preview.on}
|
|
728
|
+
aria-pressed=${preview.on ? "true" : "false"}
|
|
729
|
+
title=${
|
|
730
|
+
preview.on
|
|
731
|
+
? `Stop previewing — back to ${CANVAS_VIEW_LABELS[current ?? "edit"]}`
|
|
732
|
+
: "Preview: the page as it ships, with editing off"
|
|
733
|
+
}
|
|
734
|
+
@click=${() => {
|
|
735
|
+
setCanvasView(
|
|
736
|
+
tab,
|
|
737
|
+
preview.on ? (current ?? "edit") : "preview",
|
|
738
|
+
ctx.setCanvasMode,
|
|
739
|
+
);
|
|
740
|
+
}}
|
|
741
|
+
>
|
|
742
|
+
${CANVAS_VIEW_LABELS.preview}
|
|
743
|
+
</sp-action-button>
|
|
744
|
+
`
|
|
745
|
+
: nothing
|
|
746
|
+
}
|
|
747
|
+
`,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ─── Axis 3 · Rendering context ──────────────────────────────────────────────
|
|
752
|
+
|
|
753
|
+
/** Whether the open document is a page of a site project — pages get route params, others props. */
|
|
754
|
+
function isPageDoc(tab: Tab): boolean {
|
|
755
|
+
const path = tab.documentPath;
|
|
756
|
+
return Boolean(
|
|
757
|
+
path &&
|
|
758
|
+
projectState?.isSiteProject &&
|
|
759
|
+
(path.startsWith("pages/") || path.startsWith("./pages/")),
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** What a pane is rendering AS, once — the trigger, the popover and the lens summary all ask this. */
|
|
764
|
+
interface PaneLocale {
|
|
765
|
+
/** The project's declared locales, default first. */
|
|
766
|
+
i18n: ResolvedI18n;
|
|
767
|
+
/** The locale of the FILE this pane has open, when its directory names one — `pages/fr/…`. */
|
|
768
|
+
pathLocale: string | null;
|
|
769
|
+
/** The tag the artboard is drawn under. */
|
|
770
|
+
effective: string;
|
|
771
|
+
/** Whether that is not the document's own language, i.e. whether the bar has something to say. */
|
|
772
|
+
overridden: boolean;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* The language axis for a tab, or `null` when the project has no language question to ask.
|
|
777
|
+
*
|
|
778
|
+
* Called at RENDER time and never cached: `projectState` is replaced wholesale rather than mutated,
|
|
779
|
+
* so a locale added in Settings reaches this bar on the next paint and a module-level copy would
|
|
780
|
+
* still be describing the project that was open when this file loaded.
|
|
781
|
+
*
|
|
782
|
+
* A single declared locale is the same as none for this control — there is nothing to switch TO,
|
|
783
|
+
* and "groups a document declares nothing for are absent" is the rule the whole popover follows.
|
|
784
|
+
*
|
|
785
|
+
* The fallback chain is what makes the control honest about a file it has not been told about:
|
|
786
|
+
* `previewLocale` is the author's explicit choice, the path is the file's own language (§13.3 puts
|
|
787
|
+
* a translation in its own directory), and the default locale is what an unprefixed page renders
|
|
788
|
+
* as.
|
|
789
|
+
*/
|
|
790
|
+
function localeOf(tab: Tab | null): PaneLocale | null {
|
|
791
|
+
const i18n = getEffectiveLocales();
|
|
792
|
+
if (!tab || i18n === null || i18n.locales.length < 2) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
const pathLocale = localeOfPath(tab.documentPath ?? "", i18n);
|
|
796
|
+
const own = pathLocale ?? i18n.defaultLocale;
|
|
797
|
+
const effective = tab.session.ui.previewLocale ?? own;
|
|
798
|
+
return { effective, i18n, overridden: effective !== own, pathLocale };
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function renderingContextTpl(tab: Tab, paneId: string, ctx: PaneContextCtx): TemplateResult {
|
|
802
|
+
const { ui } = tab.session;
|
|
803
|
+
const { featureQueries, sizeBreakpoints } = ctx.parseMediaEntries(
|
|
804
|
+
getEffectiveMedia(tab.doc.document?.$media as Record<string, string> | undefined),
|
|
805
|
+
);
|
|
806
|
+
const schemeQueries = featureQueries.filter(({ query }) => isSchemeQuery(query));
|
|
807
|
+
const plainQueries = featureQueries.filter(({ query }) => !isSchemeQuery(query));
|
|
808
|
+
const scheme = (ui.previewColorScheme ?? "auto") as string;
|
|
809
|
+
const activeMedia = (ui.activeMedia ?? null) as string | null;
|
|
810
|
+
const sizeLabel = activeMedia ? mediaDisplayName(activeMedia) : "Base";
|
|
811
|
+
const schemeLabel = SCHEMES.find(([value]) => value === scheme)?.[1] ?? "Auto";
|
|
812
|
+
const locale = localeOf(tab);
|
|
813
|
+
/* The language joins the trigger ONLY when the pane is not drawing the document's own — a French
|
|
814
|
+
page open in a French pane is not a rendering context worth three more characters, and a bar
|
|
815
|
+
that grew a third term in every multilingual project would stop reading as a state. */
|
|
816
|
+
const summary = [
|
|
817
|
+
sizeLabel,
|
|
818
|
+
schemeQueries.length > 0 ? schemeLabel : null,
|
|
819
|
+
locale?.overridden ? localeLabel(locale.effective) : null,
|
|
820
|
+
]
|
|
821
|
+
.filter((part) => part !== null)
|
|
822
|
+
.join(" · ");
|
|
823
|
+
const hasLayout = isPageDoc(tab) && Boolean(getEffectiveLayoutPath(tab.doc.document?.$layout));
|
|
824
|
+
const resolving = isPageDoc(tab) ? paramPickersTpl(tab, paneId) : propFieldsTpl(tab, paneId);
|
|
825
|
+
/* How many of those fields carry a value. The trigger has to say something true at a glance, the
|
|
826
|
+
way the Context trigger says `Base · Auto` — a chevron with no reading is a control you have to
|
|
827
|
+
open in order to learn whether it was worth opening. */
|
|
828
|
+
const resolvingSet = isPageDoc(tab)
|
|
829
|
+
? Object.values(ui.previewParams ?? {}).filter((v) => v !== "" && v !== undefined).length
|
|
830
|
+
: Object.keys(ui.previewProps ?? {}).length;
|
|
831
|
+
|
|
832
|
+
return axisTpl(
|
|
833
|
+
"Context",
|
|
834
|
+
html`
|
|
835
|
+
${resolvingTpl(paneId, resolving, resolvingSet)}
|
|
836
|
+
<overlay-trigger placement="bottom-end" triggered-by="click">
|
|
837
|
+
<sp-action-button
|
|
838
|
+
slot="trigger"
|
|
839
|
+
size="s"
|
|
840
|
+
quiet
|
|
841
|
+
class="pc-context-trigger"
|
|
842
|
+
title="What this document is being rendered with"
|
|
843
|
+
>
|
|
844
|
+
${summary} ⌄
|
|
845
|
+
</sp-action-button>
|
|
846
|
+
<sp-popover slot="click-content" tip class="pc-context-popover">
|
|
847
|
+
<div class="pc-ctx">
|
|
848
|
+
${sizeGroupTpl(paneId, sizeBreakpoints, activeMedia)}
|
|
849
|
+
${
|
|
850
|
+
schemeQueries.length > 0
|
|
851
|
+
? groupTpl(
|
|
852
|
+
"Color scheme",
|
|
853
|
+
html`
|
|
854
|
+
<sp-action-group compact size="s">
|
|
855
|
+
${SCHEMES.map(
|
|
856
|
+
([value, label, title]) => html`
|
|
857
|
+
<sp-action-button
|
|
858
|
+
size="s"
|
|
859
|
+
role="radio"
|
|
860
|
+
aria-checked=${scheme === value ? "true" : "false"}
|
|
861
|
+
title=${title}
|
|
862
|
+
?selected=${scheme === value}
|
|
863
|
+
@click=${() =>
|
|
864
|
+
runContextCommand(paneId, "canvas.setColorScheme", {
|
|
865
|
+
scheme: value,
|
|
866
|
+
})}
|
|
867
|
+
>
|
|
868
|
+
${label}
|
|
869
|
+
</sp-action-button>
|
|
870
|
+
`,
|
|
871
|
+
)}
|
|
872
|
+
</sp-action-group>
|
|
873
|
+
`,
|
|
874
|
+
)
|
|
875
|
+
: nothing
|
|
876
|
+
}
|
|
877
|
+
${locale ? localeGroupTpl(paneId, locale) : nothing}
|
|
878
|
+
${
|
|
879
|
+
plainQueries.length > 0
|
|
880
|
+
? groupTpl(
|
|
881
|
+
"Features",
|
|
882
|
+
html`
|
|
883
|
+
<sp-action-group compact size="s">
|
|
884
|
+
${plainQueries.map(
|
|
885
|
+
({ name, query }) => html`
|
|
886
|
+
<sp-action-button
|
|
887
|
+
size="s"
|
|
888
|
+
toggles
|
|
889
|
+
title=${query}
|
|
890
|
+
?selected=${Boolean(ui.featureToggles[name])}
|
|
891
|
+
@click=${() => {
|
|
892
|
+
updateUi(tab, "featureToggles", {
|
|
893
|
+
...tab.session.ui.featureToggles,
|
|
894
|
+
[name]: !tab.session.ui.featureToggles[name],
|
|
895
|
+
});
|
|
896
|
+
}}
|
|
897
|
+
>
|
|
898
|
+
${mediaDisplayName(name)}
|
|
899
|
+
</sp-action-button>
|
|
900
|
+
`,
|
|
901
|
+
)}
|
|
902
|
+
</sp-action-group>
|
|
903
|
+
`,
|
|
904
|
+
)
|
|
905
|
+
: nothing
|
|
906
|
+
}
|
|
907
|
+
${
|
|
908
|
+
hasLayout
|
|
909
|
+
? groupTpl(
|
|
910
|
+
"Layout",
|
|
911
|
+
html`
|
|
912
|
+
<sp-switch
|
|
913
|
+
size="s"
|
|
914
|
+
class="pc-layout-switch"
|
|
915
|
+
?checked=${ui.showLayout !== false}
|
|
916
|
+
@change=${() =>
|
|
917
|
+
runContextCommand(paneId, "canvas.setLayoutVisible", {
|
|
918
|
+
visible: tab.session.ui.showLayout === false,
|
|
919
|
+
})}
|
|
920
|
+
>
|
|
921
|
+
Show layout elements
|
|
922
|
+
</sp-switch>
|
|
923
|
+
`,
|
|
924
|
+
)
|
|
925
|
+
: nothing
|
|
926
|
+
}
|
|
927
|
+
<button
|
|
928
|
+
class="pc-ctx-manage"
|
|
929
|
+
type="button"
|
|
930
|
+
title="Breakpoints, schemes and feature queries are defined in Project Settings › Contexts"
|
|
931
|
+
@click=${() => {
|
|
932
|
+
void activeRegistry()?.run("settings.open", { section: "contexts" });
|
|
933
|
+
}}
|
|
934
|
+
>
|
|
935
|
+
Manage contexts…
|
|
936
|
+
</button>
|
|
937
|
+
</div>
|
|
938
|
+
</sp-popover>
|
|
939
|
+
</overlay-trigger>
|
|
940
|
+
`,
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* The document DATA a render resolves against — route params, component test props — in a popover.
|
|
946
|
+
*
|
|
947
|
+
* §4.2 folds these in behind the words "resolving with…", and this is where that lands: the phrase
|
|
948
|
+
* is the popover's header and the fields are a vertical stack under it, the same shape as the
|
|
949
|
+
* rendering-context popover beside it.
|
|
950
|
+
*
|
|
951
|
+
* They used to sit OPEN on the bar, and the argument for that was the screenshot contract (§13.1):
|
|
952
|
+
* behind a click, the one first-hour flow the docs teach — typing a test prop on
|
|
953
|
+
* `start/first-component` — costs a second gesture, and the manifest's input budget may only
|
|
954
|
+
* ratchet down. The answer is not to keep n text fields on a 28px band that also carries the
|
|
955
|
+
* editor, the view and the rendering context; it is that a transient surface opens by COMMAND (plan
|
|
956
|
+
* §13.2), so the shot spends a `cmd` step rather than a selector and the input budget is untouched.
|
|
957
|
+
* `canvas.setResolvingOpen` is that command, and it is the same door the pointer uses.
|
|
958
|
+
*
|
|
959
|
+
* @param {string} paneId
|
|
960
|
+
* @param {TemplateResult | typeof nothing} body
|
|
961
|
+
* @param {number} setCount — how many fields carry a value, for the trigger's summary
|
|
962
|
+
*/
|
|
963
|
+
function resolvingTpl(
|
|
964
|
+
paneId: string,
|
|
965
|
+
body: TemplateResult | typeof nothing,
|
|
966
|
+
setCount: number,
|
|
967
|
+
): TemplateResult | typeof nothing {
|
|
968
|
+
if (body === nothing) {
|
|
969
|
+
return nothing;
|
|
970
|
+
}
|
|
971
|
+
return html`
|
|
972
|
+
<overlay-trigger
|
|
973
|
+
placement="bottom-end"
|
|
974
|
+
triggered-by="click"
|
|
975
|
+
${ref((el: Element | undefined) => {
|
|
976
|
+
if (el) {
|
|
977
|
+
_resolvingTrigger.set(paneId, el as HTMLElement & { open?: string | undefined });
|
|
978
|
+
}
|
|
979
|
+
})}
|
|
980
|
+
>
|
|
981
|
+
<sp-action-button
|
|
982
|
+
slot="trigger"
|
|
983
|
+
size="s"
|
|
984
|
+
quiet
|
|
985
|
+
class="pc-resolving-trigger"
|
|
986
|
+
title="The values this document is being rendered with"
|
|
987
|
+
>
|
|
988
|
+
${setCount > 0 ? `${setCount} set` : "Defaults"} ⌄
|
|
989
|
+
</sp-action-button>
|
|
990
|
+
<sp-popover slot="click-content" tip class="pc-context-popover">
|
|
991
|
+
<div class="pc-ctx">${groupTpl("resolving with", body as TemplateResult)}</div>
|
|
992
|
+
</sp-popover>
|
|
993
|
+
</overlay-trigger>
|
|
994
|
+
`;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Each pane's "resolving with" `overlay-trigger`, which IS the open state.
|
|
999
|
+
*
|
|
1000
|
+
* No parallel `Set`. The overlay owns whether it is showing — it opens on a pointer click without
|
|
1001
|
+
* telling anyone first — so a second record of the same fact could only ever be the stale one. The
|
|
1002
|
+
* element's `open` property is what the pointer writes and what the command writes, and reading it
|
|
1003
|
+
* back is how anything else asks.
|
|
1004
|
+
*/
|
|
1005
|
+
const _resolvingTrigger = new Map<string, HTMLElement & { open?: string | undefined }>();
|
|
1006
|
+
|
|
1007
|
+
/** Whether this pane's resolving popover is open. Exported for the tests. */
|
|
1008
|
+
export function isResolvingOpen(paneId: string): boolean {
|
|
1009
|
+
return _resolvingTrigger.get(paneId)?.open === "click";
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Open or close it.
|
|
1014
|
+
*
|
|
1015
|
+
* A named end state rather than a toggle, so `canvas.setResolvingOpen { open: false }` means the
|
|
1016
|
+
* same thing twice and a screenshot can photograph it (§13's R1).
|
|
1017
|
+
*
|
|
1018
|
+
* It writes the element and does NOT re-render the bar. `overlay-trigger` moves the popover into an
|
|
1019
|
+
* overlay portal while it is open and restores it on close; re-rendering this template in the
|
|
1020
|
+
* middle of that leaves the moved copy painted over the canvas with no way to dismiss it. The
|
|
1021
|
+
* overlay owns its own visibility — the only thing the bar redraws for is the trigger's summary,
|
|
1022
|
+
* and that changes when a VALUE changes, which already renders.
|
|
1023
|
+
*/
|
|
1024
|
+
export function setResolvingOpen(paneId: string, open: boolean): void {
|
|
1025
|
+
const trigger = _resolvingTrigger.get(paneId);
|
|
1026
|
+
if (trigger && isResolvingOpen(paneId) !== open) {
|
|
1027
|
+
trigger.open = open ? "click" : undefined;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** Forget the held triggers — a fresh window, and the tests. */
|
|
1032
|
+
export function resetResolvingOpen(): void {
|
|
1033
|
+
_resolvingTrigger.clear();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/** One labelled group inside the rendering-context popover. */
|
|
1037
|
+
function groupTpl(label: string, body: TemplateResult): TemplateResult {
|
|
1038
|
+
return html`
|
|
1039
|
+
<div class="pc-ctx-group">
|
|
1040
|
+
<span class="pc-ctx-label">${label}</span>
|
|
1041
|
+
${body}
|
|
1042
|
+
</div>
|
|
1043
|
+
`;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* The size segment: the base width plus every declared size breakpoint.
|
|
1048
|
+
*
|
|
1049
|
+
* It writes `ui.activeMedia`, the same field a canvas panel header click writes — one axis, one
|
|
1050
|
+
* field, two ways in. `null` is the base, which is why the list is not simply the breakpoints.
|
|
1051
|
+
*/
|
|
1052
|
+
/**
|
|
1053
|
+
* Run a rendering-context verb through the registry.
|
|
1054
|
+
*
|
|
1055
|
+
* These four controls wrote `session.ui` directly through `updateUi`, which is why none of the
|
|
1056
|
+
* three axes was a command: the popover WAS the capability, and the palette, the assistant and
|
|
1057
|
+
* `__jxAutomation` had no name for it. Going through the registry makes the control and the verb
|
|
1058
|
+
* one thing (§2, principle 1) and gets the breakpoint refusal for free.
|
|
1059
|
+
*/
|
|
1060
|
+
function runContextCommand(paneId: string, id: string, args: Record<string, unknown>): void {
|
|
1061
|
+
// THIS pane, named. The bar is drawn once per pane and the side bar's controls write the side
|
|
1062
|
+
// Pane's tab; a verb defaulting to the focused pane would have made the side bar edit the
|
|
1063
|
+
// Foreground document the moment its control became a command.
|
|
1064
|
+
void activeRegistry()?.run(id, { ...args, pane: paneId });
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function sizeGroupTpl(
|
|
1068
|
+
paneId: string,
|
|
1069
|
+
breakpoints: { name: string; width: number }[],
|
|
1070
|
+
activeMedia: string | null,
|
|
1071
|
+
): TemplateResult {
|
|
1072
|
+
return groupTpl(
|
|
1073
|
+
"Size",
|
|
1074
|
+
html`
|
|
1075
|
+
<sp-action-group compact size="s" class="pc-sizes">
|
|
1076
|
+
<sp-action-button
|
|
1077
|
+
size="s"
|
|
1078
|
+
role="radio"
|
|
1079
|
+
aria-checked=${activeMedia === null ? "true" : "false"}
|
|
1080
|
+
title="The base rendering, with no breakpoint applied"
|
|
1081
|
+
?selected=${activeMedia === null}
|
|
1082
|
+
@click=${() => runContextCommand(paneId, "canvas.setBreakpoint", { media: null })}
|
|
1083
|
+
>
|
|
1084
|
+
Base
|
|
1085
|
+
</sp-action-button>
|
|
1086
|
+
${breakpoints.map(
|
|
1087
|
+
({ name, width }) => html`
|
|
1088
|
+
<sp-action-button
|
|
1089
|
+
size="s"
|
|
1090
|
+
role="radio"
|
|
1091
|
+
aria-checked=${activeMedia === name ? "true" : "false"}
|
|
1092
|
+
title=${`${mediaDisplayName(name)} — ${width}px`}
|
|
1093
|
+
?selected=${activeMedia === name}
|
|
1094
|
+
@click=${() => runContextCommand(paneId, "canvas.setBreakpoint", { media: name })}
|
|
1095
|
+
>
|
|
1096
|
+
${mediaDisplayName(name)}
|
|
1097
|
+
</sp-action-button>
|
|
1098
|
+
`,
|
|
1099
|
+
)}
|
|
1100
|
+
</sp-action-group>
|
|
1101
|
+
`,
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* The language segment: which of the project's locales this pane renders AS.
|
|
1107
|
+
*
|
|
1108
|
+
* It is one radio group and one sentence, and the sentence is the load-bearing half. Jx has no
|
|
1109
|
+
* message catalogue — a translation is a different file in a different directory — so this control
|
|
1110
|
+
* cannot and must not claim to translate the page. What it does change is real: the artboard's
|
|
1111
|
+
* `lang` and its `dir`, and an RTL locale mirrors the layout on the stage. A control that let an
|
|
1112
|
+
* author believe it had translated anything would be worse than no control — which is also why the
|
|
1113
|
+
* sentence stops there: the build's `$page.locale` is not injected into the canvas render today, so
|
|
1114
|
+
* a tooltip promising it would be describing the site rather than the pane.
|
|
1115
|
+
*
|
|
1116
|
+
* Labelled with each locale's AUTONYM, for the reason `localeLabel` gives: a reader looking for
|
|
1117
|
+
* their own language scans for their own word for it.
|
|
1118
|
+
*
|
|
1119
|
+
* The document's own language is named in its tooltip rather than styled apart — a per-pane axis
|
|
1120
|
+
* has no room for a second visual state, and the author needs to know which entry is "no override"
|
|
1121
|
+
* before clicking, not after.
|
|
1122
|
+
*/
|
|
1123
|
+
function localeGroupTpl(paneId: string, locale: PaneLocale): TemplateResult {
|
|
1124
|
+
return groupTpl(
|
|
1125
|
+
"Language",
|
|
1126
|
+
html`
|
|
1127
|
+
<sp-action-group
|
|
1128
|
+
compact
|
|
1129
|
+
size="s"
|
|
1130
|
+
title="The language this pane renders as — its lang and direction only. The text is whatever file is open."
|
|
1131
|
+
>
|
|
1132
|
+
${locale.i18n.locales.map(
|
|
1133
|
+
(tag) => html`
|
|
1134
|
+
<sp-action-button
|
|
1135
|
+
size="s"
|
|
1136
|
+
role="radio"
|
|
1137
|
+
aria-checked=${locale.effective === tag ? "true" : "false"}
|
|
1138
|
+
title=${
|
|
1139
|
+
tag === locale.pathLocale
|
|
1140
|
+
? `${localeLabel(tag)} — the language of the file this pane has open`
|
|
1141
|
+
: `Render as ${localeLabel(tag)} (${tag})`
|
|
1142
|
+
}
|
|
1143
|
+
?selected=${locale.effective === tag}
|
|
1144
|
+
@click=${() => runContextCommand(paneId, "i18n.switchLocale", { locale: tag })}
|
|
1145
|
+
>
|
|
1146
|
+
${localeLabel(tag)}
|
|
1147
|
+
</sp-action-button>
|
|
1148
|
+
`,
|
|
1149
|
+
)}
|
|
1150
|
+
</sp-action-group>
|
|
1151
|
+
`,
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// ─── The mode action ─────────────────────────────────────────────────────────
|
|
1156
|
+
// Export is not an axis, and it is the one control in the old bar with nowhere to go yet: retiring
|
|
1157
|
+
// It without a command name, a chord and a residue would be deletion, not consolidation (§2
|
|
1158
|
+
// Principle 9). It stays in the Code view's trailing slot until the Code editor kind owns an action
|
|
1159
|
+
// Strip of its own.
|
|
1160
|
+
|
|
1161
|
+
function exportTpl(tab: Tab, ctx: PaneContextCtx): TemplateResult | typeof nothing {
|
|
1162
|
+
/* THIS tab's effective mode. `ctx.getCanvasMode()` answered for the focused pane, so a document
|
|
1163
|
+
opened as Code in either pane put an Export button in the OTHER pane's bar as well — over a
|
|
1164
|
+
document that is not the one the button exports. */
|
|
1165
|
+
if (canvasModeOfTab(tab) !== "source") {
|
|
1166
|
+
return nothing;
|
|
1167
|
+
}
|
|
1168
|
+
return html`
|
|
1169
|
+
<sp-action-button size="s" class="pc-export" @click=${ctx.exportFile}>
|
|
1170
|
+
<sp-icon-export slot="icon"></sp-icon-export>
|
|
1171
|
+
Export
|
|
1172
|
+
</sp-action-button>
|
|
1173
|
+
`;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// ─── ⑩ The floating zoom pod ─────────────────────────────────────────────────
|
|
1177
|
+
|
|
1178
|
+
/** The fit entries the pod offers, in menu order. `1` is "actual size", a numeric fit. */
|
|
1179
|
+
const FIT_CHOICES: readonly { value: string; fit: FitMode; label: string }[] = [
|
|
1180
|
+
{ fit: "page", label: "Fit page", value: "page" },
|
|
1181
|
+
{ fit: "width", label: "Fit width", value: "width" },
|
|
1182
|
+
{ fit: 1, label: "Actual size", value: "actual" },
|
|
1183
|
+
{ fit: "none", label: "No fit", value: "none" },
|
|
1184
|
+
];
|
|
1185
|
+
|
|
1186
|
+
/** Which entry the pod shows as chosen — a numeric fit reads as "actual size" only at 1. */
|
|
1187
|
+
function fitChoiceValue(fit: FitMode): string {
|
|
1188
|
+
if (typeof fit === "number") {
|
|
1189
|
+
return fit === 1 ? "actual" : "";
|
|
1190
|
+
}
|
|
1191
|
+
return fit;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Zoom and fit, floating over the canvas bottom-right.
|
|
1196
|
+
*
|
|
1197
|
+
* Two surfaces, one control: `edit` drives the content-reflow `editZoom`, while design / Stylebook
|
|
1198
|
+
* / git-diff render on the panzoom surface and drive `ui.zoom` — and only the panzoom surface has a
|
|
1199
|
+
* fit, because a fit is a statement about an artboard. Preview is deliberately absent: its frame is
|
|
1200
|
+
* a real viewport that scrolls its own document, so there is nothing to zoom.
|
|
1201
|
+
*/
|
|
1202
|
+
function zoomPodTpl(tab: Tab, paneId: string): TemplateResult | typeof nothing {
|
|
1203
|
+
/* THIS pane's mode. `ctx.getCanvasMode()` answers for the focused pane, so the unfocused pod
|
|
1204
|
+
offered an `editZoom` control over a stage drawing Design — and hid the fit that stage has. */
|
|
1205
|
+
const mode = canvasModeOfPane(paneId);
|
|
1206
|
+
/* And THIS pane's stage. Every zoom verb below takes a surface: without one they all defaulted to
|
|
1207
|
+
`activeCanvasSurface()`, so the side pane's `+` magnified the primary's document by a factor
|
|
1208
|
+
computed from the side pane's own zoom, and its "100%" button reset the primary. */
|
|
1209
|
+
const surface = surfaceForPane(paneId);
|
|
1210
|
+
if (mode === "edit") {
|
|
1211
|
+
const editZoom = tab.session.ui.editZoom ?? 1;
|
|
1212
|
+
return podTpl(
|
|
1213
|
+
paneId,
|
|
1214
|
+
html`
|
|
1215
|
+
${zoomButton("Zoom out (Ctrl+-)", "−", () =>
|
|
1216
|
+
setEditZoom((tab.session.ui.editZoom ?? 1) / 1.2, surface),
|
|
1217
|
+
)}
|
|
1218
|
+
<sp-action-button
|
|
1219
|
+
size="s"
|
|
1220
|
+
class="pc-zoom-label"
|
|
1221
|
+
title="Reset to 100% (Ctrl+0)"
|
|
1222
|
+
@click=${() => setEditZoom(1, surface)}
|
|
1223
|
+
>
|
|
1224
|
+
${Math.round(editZoom * 100)}%
|
|
1225
|
+
</sp-action-button>
|
|
1226
|
+
${zoomButton("Zoom in (Ctrl+=)", "+", () =>
|
|
1227
|
+
setEditZoom((tab.session.ui.editZoom ?? 1) * 1.2, surface),
|
|
1228
|
+
)}
|
|
1229
|
+
`,
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
if (mode !== "design" && mode !== "stylebook" && mode !== "git-diff") {
|
|
1233
|
+
return nothing;
|
|
1234
|
+
}
|
|
1235
|
+
const zoom = stageZoom(surface);
|
|
1236
|
+
const chosen = fitChoiceValue(getFit(surface));
|
|
1237
|
+
return podTpl(
|
|
1238
|
+
paneId,
|
|
1239
|
+
html`
|
|
1240
|
+
${zoomButton("Zoom out (Ctrl+-)", "−", () => setUserZoom(stageZoom(surface) / 1.2, surface))}
|
|
1241
|
+
<sp-action-button
|
|
1242
|
+
size="s"
|
|
1243
|
+
class="pc-zoom-label"
|
|
1244
|
+
title="Reset to 100% (Ctrl+0)"
|
|
1245
|
+
@click=${() => resetZoom(surface)}
|
|
1246
|
+
>
|
|
1247
|
+
${Math.round(zoom * 100)}%
|
|
1248
|
+
</sp-action-button>
|
|
1249
|
+
${zoomButton("Zoom in (Ctrl+=)", "+", () => setUserZoom(stageZoom(surface) * 1.2, surface))}
|
|
1250
|
+
<sp-picker
|
|
1251
|
+
size="s"
|
|
1252
|
+
quiet
|
|
1253
|
+
class="pc-fit"
|
|
1254
|
+
label="Fit"
|
|
1255
|
+
value=${chosen}
|
|
1256
|
+
title="How this document is framed"
|
|
1257
|
+
@change=${(e: Event) => {
|
|
1258
|
+
const { value } = e.target as HTMLInputElement;
|
|
1259
|
+
const choice = FIT_CHOICES.find((entry) => entry.value === value);
|
|
1260
|
+
if (!choice) {
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
// "Fit page" from the control may magnify a small artboard past life size; the fit APPLIED
|
|
1264
|
+
// On arrival never does. Same declared state, two caps — see fitToScreen's maxZoom.
|
|
1265
|
+
if (choice.fit === "page") {
|
|
1266
|
+
fitToScreen({ surface });
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
setFit(choice.fit, surface);
|
|
1270
|
+
}}
|
|
1271
|
+
>
|
|
1272
|
+
${FIT_CHOICES.map(
|
|
1273
|
+
({ value, label }) => html`<sp-menu-item value=${value}>${label}</sp-menu-item>`,
|
|
1274
|
+
)}
|
|
1275
|
+
</sp-picker>
|
|
1276
|
+
`,
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function podTpl(paneId: string, body: TemplateResult): TemplateResult {
|
|
1281
|
+
return html`
|
|
1282
|
+
<div class="pane-zoom" data-jx-region=${paneRegion(paneId, "zoom")}>
|
|
1283
|
+
<sp-action-group compact size="s">${body}</sp-action-group>
|
|
1284
|
+
</div>
|
|
1285
|
+
`;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function zoomButton(title: string, glyph: string, onClick: () => void): TemplateResult {
|
|
1289
|
+
return html`
|
|
1290
|
+
<sp-action-button size="s" title=${title} @click=${onClick}>${glyph}</sp-action-button>
|
|
1291
|
+
`;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// ─── Dynamic route-param pickers ─────────────────────────────────────────────
|
|
1295
|
+
// Candidate values load asynchronously (ContentCollection resolution / data-file read); the module
|
|
1296
|
+
// Caches the last result per (documentPath, $paths) and re-renders when it lands — the same lazy
|
|
1297
|
+
// Fill pattern as head-panel's loadLayoutEntries. When values arrive, any param without a chosen
|
|
1298
|
+
// Value auto-selects the first candidate (matching the compiler, whose first expanded route is the
|
|
1299
|
+
// First path entry).
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Candidate values per `(documentPath, $paths)` key, and the keys whose load is in flight.
|
|
1303
|
+
*
|
|
1304
|
+
* **A Map, not one slot, and that is a livelock fix rather than a cache-size preference.** It was
|
|
1305
|
+
* `_paramValues` + `_paramValuesKey`, holding exactly ONE result, while {@link render} loops every
|
|
1306
|
+
* attached pane. Two panes on documents with different keys evicted each other on every pass: pane
|
|
1307
|
+
* A's render stored A's key and cleared the value, pane B's render in the same loop stored B's, and
|
|
1308
|
+
* whichever load landed found its key gone — or, once the "still shown somewhere" guard let it
|
|
1309
|
+
* through, stored its value and called `render()`, which re-issued both loads again. An unbounded
|
|
1310
|
+
* microtask chain: no rAF, no paint, no input. `⌘\` with two pages under dynamic routes was enough,
|
|
1311
|
+
* and the probe never returned.
|
|
1312
|
+
*
|
|
1313
|
+
* `_paramLoading` is what stops a re-render re-issuing a load that has not landed yet; the value
|
|
1314
|
+
* map is what stops one pane's answer erasing the other's.
|
|
1315
|
+
*/
|
|
1316
|
+
const _paramValues = new Map<string, ParamValues>();
|
|
1317
|
+
|
|
1318
|
+
const _paramLoading = new Set<string>();
|
|
1319
|
+
|
|
1320
|
+
/** Drop every cached candidate list — a project switch, and the tests. */
|
|
1321
|
+
export function resetParamValues(): void {
|
|
1322
|
+
_paramValues.clear();
|
|
1323
|
+
_paramLoading.clear();
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* @param {Tab} tab
|
|
1328
|
+
* @returns {ParamValues | null} — null while loading (or when the doc declares no params)
|
|
1329
|
+
*/
|
|
1330
|
+
function paramValuesFor(tab: Tab): ParamValues | null {
|
|
1331
|
+
const pathsDef = pagePathsDef({
|
|
1332
|
+
document: tab.doc.document,
|
|
1333
|
+
frontmatter: tab.doc.content.frontmatter,
|
|
1334
|
+
});
|
|
1335
|
+
if (!pathsDef && dynamicRouteParams(tab.documentPath).length === 0) {
|
|
1336
|
+
return null;
|
|
1337
|
+
}
|
|
1338
|
+
const key = `${tab.documentPath ?? ""}::${JSON.stringify(pathsDef)}`;
|
|
1339
|
+
const cached = _paramValues.get(key);
|
|
1340
|
+
if (cached) {
|
|
1341
|
+
return cached;
|
|
1342
|
+
}
|
|
1343
|
+
if (_paramLoading.has(key)) {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
_paramLoading.add(key);
|
|
1347
|
+
void loadParamValues(tab.documentPath, pathsDef).then((values) => {
|
|
1348
|
+
_paramLoading.delete(key);
|
|
1349
|
+
/* Still SHOWN somewhere, rather than still focused. The candidate values fill a picker in one
|
|
1350
|
+
pane's bar; a load that landed while the keyboard was in the other pane used to be discarded,
|
|
1351
|
+
leaving the picker permanently empty for whoever was not looking at it. */
|
|
1352
|
+
if (!workspace.panes.some((pane) => pane.activeTabId === tab.id)) {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
_paramValues.set(key, values);
|
|
1356
|
+
autoSelectParams(tab, values);
|
|
1357
|
+
render();
|
|
1358
|
+
});
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* @param {Tab} tab
|
|
1364
|
+
* @param {ParamValues} values
|
|
1365
|
+
*/
|
|
1366
|
+
function autoSelectParams(tab: Tab, values: ParamValues) {
|
|
1367
|
+
const current = tab.session.ui.previewParams ?? {};
|
|
1368
|
+
const additions: Record<string, string> = {};
|
|
1369
|
+
for (const [name, list] of Object.entries(values)) {
|
|
1370
|
+
if (!current[name] && list.length > 0) {
|
|
1371
|
+
additions[name] = list[0]!;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (Object.keys(additions).length > 0) {
|
|
1375
|
+
updateUi(tab, "previewParams", { ...current, ...additions });
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* @param {Tab} tab
|
|
1381
|
+
* @returns {TemplateResult | typeof nothing}
|
|
1382
|
+
*/
|
|
1383
|
+
function paramPickersTpl(tab: Tab, paneId: string): TemplateResult | typeof nothing {
|
|
1384
|
+
const values = paramValuesFor(tab);
|
|
1385
|
+
const names = new Set(dynamicRouteParams(tab.documentPath));
|
|
1386
|
+
for (const name of Object.keys(values ?? {})) {
|
|
1387
|
+
names.add(name);
|
|
1388
|
+
}
|
|
1389
|
+
if (names.size === 0) {
|
|
1390
|
+
return nothing;
|
|
1391
|
+
}
|
|
1392
|
+
const { previewParams } = tab.session.ui;
|
|
1393
|
+
return html`
|
|
1394
|
+
${[...names].map(
|
|
1395
|
+
(name) => html`
|
|
1396
|
+
<sp-picker
|
|
1397
|
+
size="s"
|
|
1398
|
+
quiet
|
|
1399
|
+
class="pc-param"
|
|
1400
|
+
label=${name}
|
|
1401
|
+
title=${`Preview value for [${name}]`}
|
|
1402
|
+
value=${previewParams?.[name] ?? ""}
|
|
1403
|
+
@change=${(e: Event) => {
|
|
1404
|
+
const { value } = e.target as HTMLInputElement;
|
|
1405
|
+
// Through the registry, like every control in the popover beside this one.
|
|
1406
|
+
runContextCommand(paneId, "canvas.setRouteParam", { name, value });
|
|
1407
|
+
}}
|
|
1408
|
+
>
|
|
1409
|
+
${(values?.[name] ?? []).map(
|
|
1410
|
+
(v: string) => html`<sp-menu-item value=${v}>${v}</sp-menu-item>`,
|
|
1411
|
+
)}
|
|
1412
|
+
</sp-picker>
|
|
1413
|
+
`,
|
|
1414
|
+
)}
|
|
1415
|
+
`;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// ─── Component test-prop fields ──────────────────────────────────────────────
|
|
1419
|
+
// The previewParams mirror for component docs: one small field per prop entry (the doc's
|
|
1420
|
+
// Plain-data state entries), committed on change so typing never re-renders the bar mid-edit. A
|
|
1421
|
+
// Value parses as JSON when it can (numbers, booleans, arrays) and falls back to the raw string;
|
|
1422
|
+
// Clearing a field removes the override so the prop returns to its authored default.
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* @param {string} raw
|
|
1426
|
+
* @returns {JsonValue}
|
|
1427
|
+
*/
|
|
1428
|
+
function parsePropValue(raw: string): JsonValue {
|
|
1429
|
+
try {
|
|
1430
|
+
return JSON.parse(raw) as JsonValue;
|
|
1431
|
+
} catch {
|
|
1432
|
+
return raw;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* @param {Tab} tab
|
|
1438
|
+
* @param {string} paneId
|
|
1439
|
+
* @returns {TemplateResult | typeof nothing}
|
|
1440
|
+
*/
|
|
1441
|
+
function propFieldsTpl(tab: Tab, paneId: string): TemplateResult | typeof nothing {
|
|
1442
|
+
const doc = tab.doc.document;
|
|
1443
|
+
if (!isComponentDoc(doc)) {
|
|
1444
|
+
return nothing;
|
|
1445
|
+
}
|
|
1446
|
+
const entries = componentPropEntries(doc);
|
|
1447
|
+
if (entries.length === 0) {
|
|
1448
|
+
return nothing;
|
|
1449
|
+
}
|
|
1450
|
+
const { previewProps } = tab.session.ui;
|
|
1451
|
+
const display = (v: JsonValue | undefined) =>
|
|
1452
|
+
v === undefined ? "" : typeof v === "string" ? v : JSON.stringify(v);
|
|
1453
|
+
return html`
|
|
1454
|
+
${entries.map(
|
|
1455
|
+
({ name }) => html`
|
|
1456
|
+
<sp-textfield
|
|
1457
|
+
size="s"
|
|
1458
|
+
quiet
|
|
1459
|
+
class="pc-prop"
|
|
1460
|
+
data-jx-region=${paneRegion(paneId, `prop:${name}`)}
|
|
1461
|
+
placeholder=${name}
|
|
1462
|
+
title=${`Test value for ${name}`}
|
|
1463
|
+
.value=${display(previewProps?.[name])}
|
|
1464
|
+
@change=${(e: Event) => {
|
|
1465
|
+
const raw = (e.target as HTMLInputElement).value;
|
|
1466
|
+
// The command owns the write; this control owns only the parse, because "what a typed
|
|
1467
|
+
// String means" is a fact about a text field and not about the value.
|
|
1468
|
+
runContextCommand(paneId, "canvas.setTestProp", {
|
|
1469
|
+
name,
|
|
1470
|
+
value: raw === "" ? null : parsePropValue(raw),
|
|
1471
|
+
});
|
|
1472
|
+
}}
|
|
1473
|
+
></sp-textfield>
|
|
1474
|
+
`,
|
|
1475
|
+
)}
|
|
1476
|
+
`;
|
|
1477
|
+
}
|