@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
|
@@ -7,34 +7,81 @@
|
|
|
7
7
|
import { html, render as litRender, nothing } from "lit-html";
|
|
8
8
|
import { ref } from "lit-html/directives/ref.js";
|
|
9
9
|
import type * as monaco from "monaco-editor";
|
|
10
|
-
import { loadedMonaco, loadMonaco } from "../services/monaco-lazy";
|
|
10
|
+
import { loadedMonaco, loadMonaco, mountStillWanted } from "../services/monaco-lazy";
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
import { activeTab } from "../workspace/workspace";
|
|
12
|
+
import { getNodeAtPath, updateCanvas } from "../store";
|
|
13
|
+
import { activeTab, tabIsLive, workspace } from "../workspace/workspace";
|
|
14
|
+
import {
|
|
15
|
+
activeMediaOfPane,
|
|
16
|
+
canvasModeOfPane,
|
|
17
|
+
derivationOfPane,
|
|
18
|
+
nextRenderGeneration,
|
|
19
|
+
setSurfaceTeardown,
|
|
20
|
+
surfaceForPane,
|
|
21
|
+
tabOfPane,
|
|
22
|
+
} from "./canvas-surface";
|
|
23
|
+
import type { CanvasSurface } from "./canvas-surface";
|
|
24
|
+
import { primarySelection, uniquePaths } from "../tabs/selection";
|
|
25
|
+
import {
|
|
26
|
+
argsSchema,
|
|
27
|
+
nullablePathArg,
|
|
28
|
+
pathListArg,
|
|
29
|
+
pathListProperty,
|
|
30
|
+
pathProperty,
|
|
31
|
+
stringArg,
|
|
32
|
+
stringProperty,
|
|
33
|
+
} from "../commands/command-args";
|
|
14
34
|
import { collabSourceContext } from "../collab/collab-session";
|
|
15
35
|
import { attachCursorStyles } from "../collab/monaco-cursors";
|
|
16
36
|
import type { AwarenessLike } from "../collab/monaco-cursors";
|
|
17
|
-
import {
|
|
37
|
+
import { shell } from "../shell";
|
|
18
38
|
import { parseSourceForPath, serializeDocument } from "../files/file-ops";
|
|
19
39
|
import { detachGridPanel, gridPanelMounted, renderGridMode } from "../grid/grid-panel";
|
|
40
|
+
import { detachLibraryPane, libraryPaneMounted, renderLibraryMode } from "../browse/library-pane";
|
|
41
|
+
import { detachEntryPane, entryPaneMounted, renderEntryMode } from "../content/entry-editor";
|
|
42
|
+
import {
|
|
43
|
+
detachSettingsPane,
|
|
44
|
+
renderSettingsPane,
|
|
45
|
+
settingsPaneMounted,
|
|
46
|
+
} from "../panels/settings-pane";
|
|
20
47
|
import { formatByName, formatForPath } from "../format/format-host";
|
|
48
|
+
import {
|
|
49
|
+
BUFFER_COMMIT,
|
|
50
|
+
bufferIsLive,
|
|
51
|
+
bufferMovedOn,
|
|
52
|
+
bufferWrites,
|
|
53
|
+
commitBufferWrites,
|
|
54
|
+
} from "../services/monaco-buffer";
|
|
21
55
|
import { modelUriFor } from "../services/model-uri";
|
|
22
56
|
import { renderWelcome } from "../panels/welcome-screen";
|
|
57
|
+
import { renderEmptyState } from "../panels/empty-state";
|
|
58
|
+
import {
|
|
59
|
+
attachDocumentHeaderHost,
|
|
60
|
+
documentHeaderHost,
|
|
61
|
+
hasDocumentHeader,
|
|
62
|
+
} from "../panels/frontmatter-panel";
|
|
23
63
|
import { projectState } from "../state";
|
|
24
64
|
import {
|
|
25
65
|
applyEditZoom,
|
|
26
66
|
applyTransform,
|
|
27
67
|
canvasPanelTemplate,
|
|
68
|
+
fitOnCanvasEntry,
|
|
28
69
|
observeCenterUntilStable,
|
|
29
70
|
updateActivePanelHeaders,
|
|
30
71
|
} from "./canvas-utils";
|
|
31
72
|
import { parseMediaEntries } from "../utils/canvas-media";
|
|
32
73
|
import { getEffectiveMedia, getEffectiveStyle } from "../site-context";
|
|
33
74
|
import {
|
|
75
|
+
adoptCanvasPreviewMode,
|
|
34
76
|
commitActiveEditSession,
|
|
77
|
+
getEditSnapshot,
|
|
35
78
|
mountIframeCanvas,
|
|
79
|
+
postApplyFormat,
|
|
80
|
+
postOpenSlash,
|
|
36
81
|
postStyleUpdateToStylebookHosts,
|
|
82
|
+
releaseCanvasHosts,
|
|
37
83
|
} from "./iframe-host";
|
|
84
|
+
import { findEnclosingRepeater } from "../editor/repeater-scope";
|
|
38
85
|
import {
|
|
39
86
|
canvasPerf,
|
|
40
87
|
SPAN_FULL_RENDER,
|
|
@@ -47,19 +94,21 @@ import { transposeStylebookStyle } from "../panels/stylebook-doc";
|
|
|
47
94
|
import { dismissBlockActionBar, dismissLinkPopover } from "../panels/block-action-bar";
|
|
48
95
|
import { dismissContextMenu } from "../editor/context-menu";
|
|
49
96
|
import { dismissSlashMenu } from "../editor/slash-menu";
|
|
50
|
-
import { renderFunctionEditor } from "../panels/editors";
|
|
51
|
-
import { renderFormulaWorkspace } from "../panels/formula-workspace";
|
|
52
97
|
import { mediaDisplayName } from "../panels/shared";
|
|
53
|
-
import {
|
|
98
|
+
import { tabLabel } from "../panels/tab-strip";
|
|
99
|
+
import { activeRegistry } from "../commands/active-registry";
|
|
100
|
+
import { notify } from "../services/notify";
|
|
54
101
|
import * as overlaysPanel from "../panels/overlays";
|
|
55
102
|
|
|
103
|
+
import type { TemplateResult } from "lit-html";
|
|
56
104
|
import type { CanvasPanel, GitDiffState } from "../types";
|
|
105
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
57
106
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
58
107
|
import type { Tab } from "../tabs/tab.js";
|
|
108
|
+
import { mediaTypeEssence } from "@jxsuite/schema/media-type";
|
|
59
109
|
|
|
60
110
|
interface CanvasRenderCtx {
|
|
61
|
-
|
|
62
|
-
setCanvasMode: (mode: string) => void;
|
|
111
|
+
setCanvasMode: (tab: Tab | null, mode: string) => void;
|
|
63
112
|
openFileFromTree: (path: string) => void;
|
|
64
113
|
gitDiffState: GitDiffState | null;
|
|
65
114
|
setGitDiffState: (state: GitDiffState | null) => void;
|
|
@@ -67,9 +116,6 @@ interface CanvasRenderCtx {
|
|
|
67
116
|
|
|
68
117
|
let _ctx: CanvasRenderCtx | null = null;
|
|
69
118
|
|
|
70
|
-
let _prevStylebookFilter = "";
|
|
71
|
-
let _prevStylebookCustomizedOnly = false;
|
|
72
|
-
|
|
73
119
|
/**
|
|
74
120
|
* Initialize the canvas render module.
|
|
75
121
|
*
|
|
@@ -79,26 +125,29 @@ export function initCanvasRender(ctx: CanvasRenderCtx) {
|
|
|
79
125
|
_ctx = ctx;
|
|
80
126
|
}
|
|
81
127
|
|
|
82
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Monaco language for the source view of a tab's document.
|
|
130
|
+
*
|
|
131
|
+
* A document tab is a format tab or it is JSON — every opener (`files.ts`, `file-ops.ts`, the
|
|
132
|
+
* project-URL path in `studio.ts`) refuses an extension no format class claims, with
|
|
133
|
+
* `noFormatError`. This used to answer `"javascript"` for a `.js` path, which no tab can have, and
|
|
134
|
+
* the language flowed on into a source commit that marked such a tab dirty for text the document
|
|
135
|
+
* cannot hold.
|
|
136
|
+
*/
|
|
83
137
|
function sourceLang(tab: Tab) {
|
|
84
138
|
const format = formatByName(tab.doc.sourceFormat);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
return (tab.documentPath || "").endsWith(".js") ? "javascript" : "json";
|
|
139
|
+
// The essence: splitting the declared type would yield `markdown; variant=GFM` as a language id.
|
|
140
|
+
return format ? (mediaTypeEssence(format.mediaType)?.split("/").pop() ?? "plaintext") : "json";
|
|
89
141
|
}
|
|
90
142
|
|
|
91
143
|
/**
|
|
92
144
|
* The full source text for the source view. Format-class files serialize to their on-disk form
|
|
93
145
|
* (e.g. frontmatter YAML plus the body for markdown), not just the JSON of the body tree.
|
|
94
146
|
*/
|
|
95
|
-
async function sourceContent(tab: Tab
|
|
147
|
+
async function sourceContent(tab: Tab) {
|
|
96
148
|
if (formatByName(tab.doc.sourceFormat)) {
|
|
97
149
|
return serializeDocument(tab);
|
|
98
150
|
}
|
|
99
|
-
if (lang === "javascript") {
|
|
100
|
-
return tab.doc.document?.toString?.() || "";
|
|
101
|
-
}
|
|
102
151
|
return JSON.stringify(tab.doc.document, null, 2);
|
|
103
152
|
}
|
|
104
153
|
|
|
@@ -108,34 +157,101 @@ async function sourceContent(tab: Tab, lang: string) {
|
|
|
108
157
|
Panels have since grown their own rAF scheduler (see panel-scheduler.ts), so the second frame
|
|
109
158
|
bought nothing and put a hard ~32 ms floor under every escalated edit — canvas-patcher's
|
|
110
159
|
escalateToFullRender routes through here. */
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
160
|
+
/*
|
|
161
|
+
* One pending frame PER PANE. A shared id would have let the pane that scheduled first swallow the
|
|
162
|
+
* other pane's render for that frame — the dedupe has to be scoped to the surface it dedupes.
|
|
163
|
+
*/
|
|
164
|
+
const _canvasRafIds = new Map<string, number>();
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Schedule a full render of one pane's stage.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} [paneId] — defaults to the focused pane
|
|
170
|
+
*/
|
|
171
|
+
export function scheduleCanvasRender(paneId: string = workspace.activePaneId) {
|
|
172
|
+
if (_canvasRafIds.has(paneId)) {
|
|
114
173
|
return;
|
|
115
174
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
175
|
+
_canvasRafIds.set(
|
|
176
|
+
paneId,
|
|
177
|
+
requestAnimationFrame(() => {
|
|
178
|
+
_canvasRafIds.delete(paneId);
|
|
179
|
+
try {
|
|
180
|
+
renderCanvas(paneId);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error("renderCanvas error:", error);
|
|
183
|
+
}
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
124
186
|
}
|
|
125
187
|
|
|
188
|
+
/* `handOverCanvasStage` is gone with §18.3's single stage.
|
|
189
|
+
It existed because both render effects key on the active TAB while a handover changes the PANE,
|
|
190
|
+
so nothing else would have repainted the stage that had just changed hands. With a cell per pane
|
|
191
|
+
nothing changes hands: `panels/pane-grid.ts` registers a surface when it builds a cell and
|
|
192
|
+
disposes it when it removes one, and each pane's render is scheduled by its own arrival. */
|
|
193
|
+
|
|
126
194
|
/**
|
|
127
|
-
* Eject
|
|
195
|
+
* Eject a stage's DOM and Lit render part together. Setting textContent/innerHTML removes the
|
|
128
196
|
* comment nodes Lit uses as a ChildPart's markers; if the private `_$litPart$` reference is left
|
|
129
197
|
* behind, the next litRender() reuses a part whose markers are detached from the DOM and throws
|
|
130
198
|
* "This `ChildPart` has no `parentNode`…". Always eject the markers through this helper so the two
|
|
131
199
|
* operations can never drift apart.
|
|
132
200
|
*/
|
|
133
|
-
function hardClearCanvasWrap() {
|
|
201
|
+
function hardClearCanvasWrap(canvasWrap: HTMLElement) {
|
|
202
|
+
/* The frames go FIRST, while they are still reachable from the stage.
|
|
203
|
+
`textContent = ""` detaches every iframe in one statement, and a detached host is only ever
|
|
204
|
+
noticed lazily — by whichever `liveHosts` walk next happens to run — so its channel's `window`
|
|
205
|
+
"message" listener, its overlay and its pending reqIds outlive the DOM until then. Releasing
|
|
206
|
+
here makes the teardown synchronous with the clear that causes it. */
|
|
207
|
+
releaseCanvasHosts(canvasWrap);
|
|
134
208
|
canvasWrap.textContent = "";
|
|
135
209
|
// @ts-expect-error -- _$litPart$ is Lit's private render-part marker, not in the DOM types
|
|
136
210
|
delete canvasWrap["_$litPart$"];
|
|
137
211
|
}
|
|
138
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Hand the Document Header card the node the stage just made for it.
|
|
215
|
+
*
|
|
216
|
+
* One stable callback, so Lit invokes it once per host element rather than on every render. The
|
|
217
|
+
* `undefined` branch is Lit reporting a removal WITHOUT saying which node — and the two placements
|
|
218
|
+
* share this callback, so an Edit→Design swap can report the outgoing host after the incoming one
|
|
219
|
+
* has already been bound. Connectivity is the fact that settles it: only a host that really left
|
|
220
|
+
* the document releases the card.
|
|
221
|
+
*/
|
|
222
|
+
const _docHeaderRefs = new Map<string, (el: Element | undefined) => void>();
|
|
223
|
+
|
|
224
|
+
function docHeaderRef(paneId: string): (el: Element | undefined) => void {
|
|
225
|
+
let bind = _docHeaderRefs.get(paneId);
|
|
226
|
+
if (!bind) {
|
|
227
|
+
bind = (el: Element | undefined) => {
|
|
228
|
+
if (el) {
|
|
229
|
+
attachDocumentHeaderHost(paneId, el as HTMLElement);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!documentHeaderHost(paneId)?.isConnected) {
|
|
233
|
+
attachDocumentHeaderHost(paneId, null);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
_docHeaderRefs.set(paneId, bind);
|
|
237
|
+
}
|
|
238
|
+
return bind;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The stage's slot for the Document Header card.
|
|
243
|
+
*
|
|
244
|
+
* `in-column` is Edit: the card is a block of the artefact's own column and scrolls with it.
|
|
245
|
+
* `pinned` is Design: the artboards are drawn under a pan/zoom transform, so the card sits above
|
|
246
|
+
* the surface at 1:1 and keeps its own scroll.
|
|
247
|
+
*
|
|
248
|
+
* @param {"in-column" | "pinned"} placement
|
|
249
|
+
* @returns {TemplateResult}
|
|
250
|
+
*/
|
|
251
|
+
function docHeaderSlot(placement: "in-column" | "pinned", paneId: string): TemplateResult {
|
|
252
|
+
return html`<div class="doc-header-host ${placement}" ${ref(docHeaderRef(paneId))}></div>`;
|
|
253
|
+
}
|
|
254
|
+
|
|
139
255
|
/**
|
|
140
256
|
* Tear the canvas all the way back down to a pristine state. Invoked whenever there is no active
|
|
141
257
|
* tab (e.g. every tab was closed) so the canvas can never get wedged in a half-initialized state:
|
|
@@ -144,12 +260,14 @@ function hardClearCanvasWrap() {
|
|
|
144
260
|
* the very next render is treated as a fresh mode transition and rebuilds the surface from
|
|
145
261
|
* scratch.
|
|
146
262
|
*/
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
263
|
+
/* The source-mode collab teardown is a field of the STAGE, not of the module.
|
|
264
|
+
`disposeSourceCollab()` released whichever binding was last created, so with two Code panes one
|
|
265
|
+
pane leaving source mode dropped the OTHER pane's y-monaco binding and handed back a canonical
|
|
266
|
+
lock it did not hold. */
|
|
267
|
+
|
|
268
|
+
function disposeSourceCollab(surface: CanvasSurface): void {
|
|
269
|
+
surface.sourceCollabCleanup?.();
|
|
270
|
+
surface.sourceCollabCleanup = null;
|
|
153
271
|
}
|
|
154
272
|
|
|
155
273
|
/**
|
|
@@ -180,39 +298,92 @@ async function createSourceCollabBinding(
|
|
|
180
298
|
};
|
|
181
299
|
}
|
|
182
300
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
301
|
+
/*
|
|
302
|
+
* There is no `view.functionEditor` teardown in here, and that is deliberate.
|
|
303
|
+
*
|
|
304
|
+
* Every other disposal below is a surface this module CREATED — the grid panel, the source-mode
|
|
305
|
+
* Monaco and its collab binding, the centering observer. The function editor is not: the Bottom
|
|
306
|
+
* dock's Logic tab creates it and `panels/editors.ts`'s `syncFunctionEditor` disposes it, from an
|
|
307
|
+
* `afterRender` that `panels/bottom-dock.ts` runs for every registered tab whether or not it is
|
|
308
|
+
* showing — precisely so a surface that outlives its own markup can release it. Losing the last tab
|
|
309
|
+
* is one of the state changes that hook already covers.
|
|
310
|
+
*
|
|
311
|
+
* Reaching in from here was also wrong per PANE. `resetCanvasView` runs for a pane with no tab
|
|
312
|
+
* while `view.functionEditor` is app-wide, so a stage-holding pane going empty disposed an editor
|
|
313
|
+
* the dock was still showing for another pane's tab — and, having cleared the handle, left nothing
|
|
314
|
+
* to rebuild it until the next dock repaint.
|
|
315
|
+
*/
|
|
316
|
+
/**
|
|
317
|
+
* Drop the source-mode Monaco, its model, and the debounced work armed over it.
|
|
318
|
+
*
|
|
319
|
+
* The three sites below used to inline the same four lines, and the four lines were incomplete in
|
|
320
|
+
* the same way at all three: they never cancelled the 600ms sync timer. That timer reads
|
|
321
|
+
* `getValue()` off the editor it closes over, a disposed Monaco answers the empty string, and for a
|
|
322
|
+
* format-backed document the callback then runs `parseSourceForPath(path, "")` and assigns the
|
|
323
|
+
* result — **the page's body replaced with an empty parse, 600ms after the user left Code view,
|
|
324
|
+
* with the tab marked dirty so the next ⌘S writes it to disk.** For a `.json` tab the same path
|
|
325
|
+
* threw inside `JSON.parse("")` and was swallowed by the catch, which is luck rather than design
|
|
326
|
+
* and is why only one of the two shapes was ever reported.
|
|
327
|
+
*
|
|
328
|
+
* One function, so there is one place for the teardown to be and no fourth site can omit it.
|
|
329
|
+
*
|
|
330
|
+
* **And it commits before it cancels.** Cancelling alone traded a dead buffer's `""` for the user's
|
|
331
|
+
* last 600ms of typing, which is still lost work and is now silent: `doc.dirty` stays `false`, so
|
|
332
|
+
* nothing says the edit went missing. {@link commitBufferWrites} runs the armed parse WHILE the
|
|
333
|
+
* model is still attached — `getValue()` is read before its first `await` — and only then drops
|
|
334
|
+
* what is left, so leaving Code view saves the edit instead of choosing which way to discard it.
|
|
335
|
+
*
|
|
336
|
+
* **And the parse is allowed not to land, which is the third way out.** Unparseable source
|
|
337
|
+
* deliberately keeps the buffer rather than resyncing over a half-typed heading — the right call
|
|
338
|
+
* while the surface is standing, and a deletion the moment it is not, because the next two lines
|
|
339
|
+
* detach the model and the text existed nowhere else. `commitBufferWrites` says so; it cannot say
|
|
340
|
+
* it synchronously, because the parse runs through the format host, so the answer arrives with the
|
|
341
|
+
* promise. Refusing the teardown instead is not available to any of the three callers: they are a
|
|
342
|
+
* pane reset, a mode transition and a model-URI swap, each of which has already taken the container
|
|
343
|
+
* or the URI this editor needs, and a Monaco kept alive over either is unreachable rather than
|
|
344
|
+
* rescued.
|
|
345
|
+
*/
|
|
346
|
+
/*
|
|
347
|
+
* **And it is the STAGE's editor, which is the data-loss half.**
|
|
348
|
+
*
|
|
349
|
+
* `view.monacoEditor` was app-wide, so this disposed whichever Code buffer was open — not this
|
|
350
|
+
* pane's. With two Code panes, leaving source mode in one committed and destroyed the other's
|
|
351
|
+
* model, and `commitBufferWrites` parses `getValue()` against the tab THAT editor was mounted for,
|
|
352
|
+
* so the surviving pane's document took a write built from a buffer belonging to a different file.
|
|
353
|
+
*/
|
|
354
|
+
function disposeSourceEditor(surface: CanvasSurface): void {
|
|
355
|
+
const editor = surface.monacoEditor;
|
|
356
|
+
if (!editor) {
|
|
357
|
+
return;
|
|
201
358
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
359
|
+
commitBufferWrites(editor, "source");
|
|
360
|
+
editor.getModel()?.dispose();
|
|
361
|
+
editor.dispose();
|
|
362
|
+
surface.monacoEditor = null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function resetCanvasView(surface: CanvasSurface) {
|
|
366
|
+
const canvasWrap = surface.wrap;
|
|
367
|
+
const { paneId } = surface;
|
|
368
|
+
detachGridPanel(paneId);
|
|
369
|
+
detachLibraryPane(paneId);
|
|
370
|
+
detachEntryPane(paneId);
|
|
371
|
+
detachSettingsPane(paneId);
|
|
372
|
+
disposeSourceCollab(surface);
|
|
373
|
+
disposeSourceEditor(surface);
|
|
374
|
+
if (surface.centerObserver) {
|
|
375
|
+
surface.centerObserver.disconnect();
|
|
376
|
+
surface.centerObserver = null;
|
|
205
377
|
}
|
|
206
|
-
|
|
207
|
-
for (const p of canvasPanels) {
|
|
378
|
+
for (const p of surface.panels) {
|
|
208
379
|
p.renderScope?.stop();
|
|
209
380
|
p.renderScope = null;
|
|
210
381
|
}
|
|
211
|
-
|
|
382
|
+
surface.panels.length = 0;
|
|
212
383
|
|
|
213
|
-
hardClearCanvasWrap();
|
|
384
|
+
hardClearCanvasWrap(canvasWrap);
|
|
214
385
|
|
|
215
|
-
|
|
386
|
+
surface.panzoomWrap = null;
|
|
216
387
|
canvasWrap.style.padding = "";
|
|
217
388
|
canvasWrap.style.alignItems = "";
|
|
218
389
|
canvasWrap.style.flexDirection = "";
|
|
@@ -222,7 +393,7 @@ function resetCanvasView() {
|
|
|
222
393
|
dismissLinkPopover();
|
|
223
394
|
dismissContextMenu();
|
|
224
395
|
dismissSlashMenu();
|
|
225
|
-
|
|
396
|
+
surface.prevCanvasMode = null;
|
|
226
397
|
}
|
|
227
398
|
|
|
228
399
|
/**
|
|
@@ -232,14 +403,34 @@ function resetCanvasView() {
|
|
|
232
403
|
* before this runs, and every continuation below re-checks `view.monacoEditor`, so a teardown or a
|
|
233
404
|
* mode switch while the module loads is handled the same way a teardown mid-y-monaco-load already
|
|
234
405
|
* was.
|
|
406
|
+
*
|
|
407
|
+
* **The mount ITSELF owed the same re-check and did not make it.** This was the fourth Monaco mount
|
|
408
|
+
* path and the only one with no post-await guard: `renderCanvasImpl` sets `surface.prevCanvasMode`
|
|
409
|
+
* BEFORE it reaches here, so a second synchronous `renderCanvas()` inside the cold load sees
|
|
410
|
+
* `modeChanged === false` and a still-null `view.monacoEditor`, skips the source fast path and
|
|
411
|
+
* falls through to mount again. `store.ts`'s `render()`/`renderOnly()` coalesce nothing, so two
|
|
412
|
+
* mounts is a plain sequence of two renders. {@link mountStillWanted} is asked before `createModel`
|
|
413
|
+
* rather than after, because the duplicate is not just a wasted editor — the second `createModel`
|
|
414
|
+
* claims a URI the first already registered, which real Monaco throws on.
|
|
235
415
|
*/
|
|
236
416
|
async function mountSourceEditor(
|
|
237
417
|
tab: Tab,
|
|
418
|
+
surface: CanvasSurface,
|
|
238
419
|
editorContainer: Element,
|
|
239
420
|
filePath: string,
|
|
240
421
|
lang: string,
|
|
241
422
|
): Promise<void> {
|
|
423
|
+
const { paneId } = surface;
|
|
242
424
|
const monaco = await loadMonaco();
|
|
425
|
+
if (
|
|
426
|
+
!mountStillWanted(
|
|
427
|
+
editorContainer,
|
|
428
|
+
surface.monacoEditor,
|
|
429
|
+
() => tabOfPane(paneId) === tab && canvasModeOfPane(paneId) === "source",
|
|
430
|
+
)
|
|
431
|
+
) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
243
434
|
const modelUri = monaco.Uri.parse(modelUriFor(filePath));
|
|
244
435
|
const model = monaco.editor.createModel("", lang, modelUri);
|
|
245
436
|
// Co-edited tabs bind the buffer to the shared Y.Text instead of a local serialization: the
|
|
@@ -250,95 +441,172 @@ async function mountSourceEditor(
|
|
|
250
441
|
void collabCtx
|
|
251
442
|
.enter()
|
|
252
443
|
.then(async () => {
|
|
253
|
-
const editor =
|
|
444
|
+
const editor = surface.monacoEditor;
|
|
254
445
|
if (!editor || editor.getModel() !== model) {
|
|
446
|
+
// The lock was taken FOR a surface that is no longer here, and `leave()` reaches this
|
|
447
|
+
// Mount from exactly one place: the cleanup below, which this path never gets. See the
|
|
448
|
+
// Catch for what a lock nobody releases now costs.
|
|
449
|
+
collabCtx.leave();
|
|
255
450
|
return;
|
|
256
451
|
}
|
|
257
452
|
const cleanup = await createSourceCollabBinding(model, editor, collabCtx);
|
|
258
453
|
// The editor may have been torn down while y-monaco loaded — unbind immediately.
|
|
259
|
-
if (
|
|
454
|
+
if (surface.monacoEditor !== editor || editor.getModel() !== model) {
|
|
260
455
|
cleanup();
|
|
261
456
|
return;
|
|
262
457
|
}
|
|
263
|
-
sourceCollabCleanup = cleanup;
|
|
458
|
+
surface.sourceCollabCleanup = cleanup;
|
|
459
|
+
/* AND THE BUFFER SAYS SO, because from this instant it is not ours.
|
|
460
|
+
The binding is two-way: peers' keystrokes arrive in the model with nothing declared, and
|
|
461
|
+
anything WE write into the model is published to all of them. The repaint's fast path
|
|
462
|
+
below would happily serialize the structure tree over it — and the structure tree is what
|
|
463
|
+
the source reconciler PARSED OUT of this same shared text, so every round trip that is
|
|
464
|
+
not byte-stable was a whole-document replace pushed to every peer, on every repaint, with
|
|
465
|
+
`hasTextFocus()` protecting only the person currently typing. Clause 5 refuses it. */
|
|
466
|
+
editor._writes?.markShared();
|
|
264
467
|
if (collabCtx.readOnly) {
|
|
265
468
|
editor.updateOptions({ readOnly: true });
|
|
266
469
|
}
|
|
267
470
|
})
|
|
268
471
|
.catch(() => {
|
|
269
|
-
|
|
472
|
+
/* A BINDING FAILURE HOLDS A LOCK AND CARRIES NOTHING, and both halves have to be said.
|
|
473
|
+
`enter()` flips the room's canonical lock to "source" before y-monaco is even imported,
|
|
474
|
+
and `ctx.leave()` lives in exactly one place — the cleanup `createSourceCollabBinding`
|
|
475
|
+
returns. So a failure past that point left the lock held by a client with no binding, and
|
|
476
|
+
the freeze is now honest enough to include that client too (`collab-session.ts`'s transact
|
|
477
|
+
gate no longer exempts the lock holder): nobody in the room could edit the structure, and
|
|
478
|
+
nobody could edit the source either, until someone reloaded. Hand it back.
|
|
479
|
+
And say what the buffer is, because it is not an editing surface. The mount returns before
|
|
480
|
+
installing its change handler on a co-edited tab, so on this path a keystroke arms no
|
|
481
|
+
commit, marks nothing, leaves `tabBufferUnsaved` false — and the next repaint replaces it,
|
|
482
|
+
with clause 5 unable to help because `markShared` is what never ran. `readOnly` is what
|
|
483
|
+
"degrades to a read-only-ish local buffer" was always trying to say. */
|
|
484
|
+
collabCtx.leave();
|
|
485
|
+
const editor = surface.monacoEditor;
|
|
486
|
+
if (editor && editor.getModel() === model) {
|
|
487
|
+
editor.updateOptions({ readOnly: true });
|
|
488
|
+
}
|
|
270
489
|
});
|
|
271
490
|
} else {
|
|
272
|
-
sourceContent(tab
|
|
491
|
+
sourceContent(tab)
|
|
273
492
|
.then((content) => {
|
|
274
|
-
const editor =
|
|
275
|
-
|
|
493
|
+
const editor = surface.monacoEditor;
|
|
494
|
+
// The model identity says "this load is still for this buffer"; `bufferMovedOn` says "and
|
|
495
|
+
// The buffer is still the empty one it was created as". Serializing a document is a round
|
|
496
|
+
// Trip through the format host, and a user who starts typing into the empty box before it
|
|
497
|
+
// Returns had their first words replaced by the file they were already looking at.
|
|
498
|
+
if (editor && editor.getModel() === model && !bufferMovedOn(editor, "")) {
|
|
276
499
|
editor._ignoreNextChange = true;
|
|
277
500
|
model.setValue(content);
|
|
501
|
+
// The buffer took the DOCUMENT's own text, so the two agree — stated, because clause 3 is
|
|
502
|
+
// A fact each programmatic write owns rather than something a timer can be read for.
|
|
503
|
+
editor._writes?.markSettled();
|
|
278
504
|
}
|
|
279
505
|
})
|
|
280
506
|
.catch(() => {
|
|
281
507
|
// Serialization unavailable — leave the buffer empty rather than crash the render
|
|
282
508
|
});
|
|
283
509
|
}
|
|
284
|
-
view
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
510
|
+
// Typed as the surface `view` declares (the editor plus `_ignoreNextChange` / `_writes`), because
|
|
511
|
+
// The instance is HELD here now rather than re-read off `view.monacoEditor` in every callback.
|
|
512
|
+
const sourceEditor: NonNullable<typeof surface.monacoEditor> = monaco.editor.create(
|
|
513
|
+
editorContainer as unknown as HTMLElement,
|
|
514
|
+
{
|
|
515
|
+
automaticLayout: true,
|
|
516
|
+
fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', 'Consolas', monospace",
|
|
517
|
+
fontSize: 12,
|
|
518
|
+
lineNumbers: "on",
|
|
519
|
+
minimap: { enabled: false },
|
|
520
|
+
model,
|
|
521
|
+
scrollBeyondLastLine: false,
|
|
522
|
+
tabSize: 2,
|
|
523
|
+
theme: "vs-dark",
|
|
524
|
+
wordWrap: "on",
|
|
525
|
+
},
|
|
526
|
+
);
|
|
527
|
+
surface.monacoEditor = sourceEditor;
|
|
528
|
+
/* WHOSE BUFFER THIS IS, in the one spelling the dock's editor already used.
|
|
529
|
+
`tab` was named only by the closures below, so a caller holding a `Tab` — the ⌘W gate, the quit
|
|
530
|
+
gate — could ask the function editor whether the buffer was its tab's and had no way to ask
|
|
531
|
+
this one at all. Half an answer is what let a source tab close over unprompted typing. */
|
|
532
|
+
sourceEditor._editingTab = tab;
|
|
533
|
+
|
|
534
|
+
// The debounce holder goes on the editor, not in this closure. A closure variable is reachable
|
|
535
|
+
// Only from inside `mountSourceEditor`, which is why `disposeSourceEditor` had nothing to cancel
|
|
536
|
+
// And a timer outlived every teardown; hanging it off the instance gives it the instance's
|
|
537
|
+
// Lifetime. Installed before the collab early-return so the canceller exists either way.
|
|
538
|
+
const writes = bufferWrites(sourceEditor);
|
|
296
539
|
|
|
297
540
|
// Debounced sync back to state (solo tabs only: co-edited buffers flow through the shared
|
|
298
541
|
// Y.Text and the source reconciler's parse mirror instead of a whole-doc replace).
|
|
299
542
|
if (collabCtx) {
|
|
300
543
|
return;
|
|
301
544
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
545
|
+
sourceEditor.onDidChangeModelContent(() => {
|
|
546
|
+
// Identity, not existence: a change fired after this instance stopped being the mounted one has
|
|
547
|
+
// Nothing to say, and asking `view.monacoEditor` for the editor instead of holding it is how
|
|
548
|
+
// The callback below came to address a tab it was never mounted for.
|
|
549
|
+
if (surface.monacoEditor !== sourceEditor) {
|
|
306
550
|
return;
|
|
307
551
|
}
|
|
308
|
-
if (
|
|
309
|
-
|
|
552
|
+
if (sourceEditor._ignoreNextChange) {
|
|
553
|
+
sourceEditor._ignoreNextChange = false;
|
|
310
554
|
return;
|
|
311
555
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
556
|
+
// A keystroke: the buffer is ahead of the document until the commit lands, and the text is the
|
|
557
|
+
// User's own — which is the narrower fact the close and quit gates read.
|
|
558
|
+
writes.markTyped();
|
|
559
|
+
writes.arm(BUFFER_COMMIT, 600, async () => {
|
|
560
|
+
/* `tab` is the tab this editor was MOUNTED for, captured above and never re-read. It used to
|
|
561
|
+
be `activeTab.value` — whatever was focused when the timer fired — which is the same
|
|
562
|
+
cross-document write the dock's commit had, surviving here only because the model-URI swap
|
|
563
|
+
happens to dispose this editor on a source→source tab change. A coincidence in one caller
|
|
564
|
+
is not a rule, and the tab still has to be OPEN: a closed tab is not in `workspace.tabs`,
|
|
565
|
+
so a parse written into it is a parse nothing will ever read. */
|
|
566
|
+
if (!tabIsLive(tab) || !bufferIsLive(sourceEditor)) {
|
|
316
567
|
return;
|
|
317
568
|
}
|
|
318
|
-
if (formatByName(
|
|
569
|
+
if (formatByName(tab.doc.sourceFormat) && tab.documentPath) {
|
|
319
570
|
try {
|
|
320
571
|
// Parse the full source back into body + frontmatter (title, $head, etc.).
|
|
321
572
|
const { document, frontmatter } = await parseSourceForPath(
|
|
322
|
-
|
|
323
|
-
|
|
573
|
+
tab.documentPath,
|
|
574
|
+
sourceEditor.getValue(),
|
|
324
575
|
);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
576
|
+
/* AND THE TAB IS ASKED AGAIN, because the parse is an await and the check above it is
|
|
577
|
+
not. The whole reason `tabIsLive` is here is that a parse written into a closed tab is
|
|
578
|
+
a parse nothing will ever read — and the window that closes the tab is precisely this
|
|
579
|
+
one: `commitTabBuffers` is called by two destroyers (⌘W / the × via `requestClose`,
|
|
580
|
+
and the preview slot's replacement in `openTab`), and the second cannot wait for a
|
|
581
|
+
promise. Checked before the await as well as after, because a flush that starts on a
|
|
582
|
+
dead tab should not pay for a format round trip to find out. */
|
|
583
|
+
if (!tabIsLive(tab)) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
tab.doc.document = document as JxMutableNode;
|
|
587
|
+
tab.doc.content.frontmatter = frontmatter;
|
|
588
|
+
tab.doc.dirty = true;
|
|
589
|
+
writes.markSettled();
|
|
328
590
|
} catch {
|
|
329
|
-
// Unparseable source — don't update state
|
|
591
|
+
// Unparseable source — don't update state. And do NOT settle: the buffer really is ahead
|
|
592
|
+
// Of the document, so a repaint must go on refusing to overwrite the half-typed heading.
|
|
330
593
|
}
|
|
331
594
|
} else if (lang === "json") {
|
|
332
595
|
try {
|
|
333
|
-
|
|
334
|
-
|
|
596
|
+
tab.doc.document = JSON.parse(sourceEditor.getValue()) as JxMutableNode;
|
|
597
|
+
tab.doc.dirty = true;
|
|
598
|
+
writes.markSettled();
|
|
335
599
|
} catch {
|
|
336
|
-
// Invalid JSON —
|
|
600
|
+
// Invalid JSON — same: unparseable is a reason to keep the buffer, not to resync over it.
|
|
337
601
|
}
|
|
338
|
-
} else {
|
|
339
|
-
tabNow.doc.dirty = true;
|
|
340
602
|
}
|
|
341
|
-
|
|
603
|
+
/* There is no third branch, and the one that used to be here could not have worked.
|
|
604
|
+
It marked the tab DIRTY for text the document has no representation of — `sourceContent`
|
|
605
|
+
answered `doc.document?.toString?.()`, i.e. `"[object Object]"` — so ⌘S would have written
|
|
606
|
+
that string to disk. It only ever ran for a `.js` document tab, and nothing opens one:
|
|
607
|
+
`files.ts`, `file-ops.ts` and the project-URL open all end at `noFormatError` for an
|
|
608
|
+
extension no format class claims, so a document tab is a format tab or it is JSON. */
|
|
609
|
+
});
|
|
342
610
|
});
|
|
343
611
|
}
|
|
344
612
|
|
|
@@ -347,17 +615,97 @@ async function mountSourceEditor(
|
|
|
347
615
|
* several early returns, so the span is closed by {@link timeSpan}'s `finally` rather than by
|
|
348
616
|
* hand.
|
|
349
617
|
*/
|
|
350
|
-
export function renderCanvas() {
|
|
351
|
-
|
|
618
|
+
export function renderCanvas(paneId: string = workspace.activePaneId) {
|
|
619
|
+
const surface = surfaceForPane(paneId);
|
|
620
|
+
timeSpan(SPAN_FULL_RENDER, () => {
|
|
621
|
+
renderCanvasImpl(surface);
|
|
622
|
+
});
|
|
352
623
|
}
|
|
353
624
|
|
|
354
|
-
|
|
355
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Say, on this stage, why a derived pane has nothing to draw.
|
|
627
|
+
*
|
|
628
|
+
* **`derived.reason` had a writer, a render-input tracker and no reader at all.** It is written by
|
|
629
|
+
* `applyDerivation`, tracked by `panels/pane-context.ts`, and `grep -rn "\.reason" src/` found
|
|
630
|
+
* nothing that printed it — so choosing Layout on a page with no layout gave a pane with a
|
|
631
|
+
* derivation, no tabs, no strip chip, no context bar and a blank stage that `paneIsEmpty` would not
|
|
632
|
+
* collapse. The sentence written for the honest empty state was dead code.
|
|
633
|
+
*
|
|
634
|
+
* Through {@link renderEmptyState} rather than a block of its own: the copy rules are inherited,
|
|
635
|
+
* and a derived pane with nothing to show is the same kind of region as any other.
|
|
636
|
+
*
|
|
637
|
+
* **`held` is the document this pane still OWNS, and it is the difference between a notice and a
|
|
638
|
+
* riddle.** A companion that resolved once has a real tab, so `panels/tab-strip.ts` draws a real
|
|
639
|
+
* chip with a real ✕ — and when the rule then dies, the strip says `base.json` while the stage says
|
|
640
|
+
* "This page has no layout." The sentence is true and it is about something else; nothing on screen
|
|
641
|
+
* says why the file named in the strip is not drawn, or how to get it back. Naming it, and putting
|
|
642
|
+
* the one verb that ends the follow beside it, is what makes the two halves agree. A LENS never
|
|
643
|
+
* passes one: it owns no document, `tabOfPane` would hand back the SOURCE pane's, and `pane.pin` is
|
|
644
|
+
* refused for a projection (§14.1).
|
|
645
|
+
*
|
|
646
|
+
* @param {CanvasSurface} surface
|
|
647
|
+
* @param {string} message
|
|
648
|
+
* @param {Tab | null} [held]
|
|
649
|
+
*/
|
|
650
|
+
function renderDerivationNotice(
|
|
651
|
+
surface: CanvasSurface,
|
|
652
|
+
message: string,
|
|
653
|
+
held: Tab | null = null,
|
|
654
|
+
): void {
|
|
655
|
+
surface.wrap.style.display = "block";
|
|
656
|
+
const registry = activeRegistry();
|
|
657
|
+
const pin = registry?.get("pane.pin");
|
|
658
|
+
litRender(
|
|
659
|
+
renderEmptyState({
|
|
660
|
+
message,
|
|
661
|
+
...(held && { detail: `${tabLabel(held)} is still open here.` }),
|
|
662
|
+
...(held &&
|
|
663
|
+
pin && {
|
|
664
|
+
actions: [
|
|
665
|
+
{
|
|
666
|
+
label: pin.title,
|
|
667
|
+
run: () => {
|
|
668
|
+
void registry?.run(pin.id);
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
],
|
|
672
|
+
}),
|
|
673
|
+
}),
|
|
674
|
+
surface.wrap,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function renderCanvasImpl(surface: CanvasSurface) {
|
|
679
|
+
/* Every read below is scoped to ONE pane. The stage, the panels it mounted and the mode it draws
|
|
680
|
+
are the surface's; the document is whatever tab that pane is showing, which is `activeTab` only
|
|
681
|
+
when the pane is the focused one. */
|
|
682
|
+
const canvasWrap = surface.wrap;
|
|
683
|
+
/* A pane with no stage renders nothing — it is not an error and it is not a fallback to some
|
|
684
|
+
other pane's stage. Two schedulers can name a stage-less pane: a frame queued for a pane that
|
|
685
|
+
lost the shell's single stage between the schedule and the rAF, and `escalateToFullRender` on
|
|
686
|
+
a tab whose pane is not the one on screen. Both mean the same thing — there is no surface to
|
|
687
|
+
repaint — and rendering "the canvas" instead is how one pane's escalation used to redraw the
|
|
688
|
+
other. Left unguarded this threw on Lit's render part, which is what `⌘\` did. */
|
|
689
|
+
if (!canvasWrap) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const canvasPanels = surface.panels;
|
|
693
|
+
const tab = tabOfPane(surface.paneId);
|
|
356
694
|
if (!tab) {
|
|
357
695
|
// No active tab — reset every piece of canvas view state so reopening a file can never inherit
|
|
358
696
|
// A stale Lit part, a dead Monaco editor, or a mismatched prevCanvasMode (the toxic states that
|
|
359
697
|
// Previously left the canvas unrenderable until a full reload).
|
|
360
|
-
|
|
698
|
+
attachDocumentHeaderHost(surface.paneId, null);
|
|
699
|
+
resetCanvasView(surface);
|
|
700
|
+
/* A DERIVED pane with nothing to draw explains itself. It is not the welcome screen and it is
|
|
701
|
+
not a blank stage: it is a standing rule that has not resolved — "Select an element inside a
|
|
702
|
+
component to see its definition" — and until this branch existed that sentence was written,
|
|
703
|
+
tracked as a render input and never printed anywhere. */
|
|
704
|
+
const derived = derivationOfPane(surface.paneId);
|
|
705
|
+
if (derived) {
|
|
706
|
+
renderDerivationNotice(surface, derived.reason || "Looking for something to show here…");
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
361
709
|
// Nothing is open. A dev-server root that is only a workspace/monorepo (no project.json) still
|
|
362
710
|
// Populates projectState, but it is not an open project — the file tree prompts to open one and
|
|
363
711
|
// The canvas must keep showing the welcome screen rather than going blank.
|
|
@@ -366,24 +714,75 @@ function renderCanvasImpl() {
|
|
|
366
714
|
}
|
|
367
715
|
return;
|
|
368
716
|
}
|
|
717
|
+
/* A derivation that HAS a document and has gone stale says so too.
|
|
718
|
+
The notice above lives under `if (!tab)`, and a pane that already opened something keeps its
|
|
719
|
+
tab — so a layout companion whose page dropped its `$layout`, and a Code lens whose source
|
|
720
|
+
pane switched to Code beside it, both had `status: "unavailable"` and a sentence written by
|
|
721
|
+
`applyDerivation`, tracked as a render input by `panels/pane-context.ts`, and drawn nowhere.
|
|
722
|
+
The companion went on showing a file it was no longer about; the lens mounted a SECOND Monaco
|
|
723
|
+
model on the source pane's URI, which real Monaco throws on.
|
|
724
|
+
`reason` and not just `status`, because a derivation is `unavailable` for one frame before the
|
|
725
|
+
follow has run and a stage that blanked on that would flicker on every retarget. */
|
|
726
|
+
const staleDerivation = derivationOfPane(surface.paneId);
|
|
727
|
+
if (staleDerivation?.status === "unavailable" && staleDerivation.reason) {
|
|
728
|
+
attachDocumentHeaderHost(surface.paneId, null);
|
|
729
|
+
resetCanvasView(surface);
|
|
730
|
+
/* …and for a COMPANION, what it is still holding. `tab` here is the pane's OWN document for a
|
|
731
|
+
companion and the SOURCE pane's for a lens — the hop `tabOfPane` takes — so the kind decides
|
|
732
|
+
whether there is anything to name. See {@link renderDerivationNotice}. */
|
|
733
|
+
renderDerivationNotice(
|
|
734
|
+
surface,
|
|
735
|
+
staleDerivation.reason,
|
|
736
|
+
staleDerivation.kind === "companion" ? tab : null,
|
|
737
|
+
);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
369
740
|
const ctx = _ctx as CanvasRenderCtx;
|
|
370
741
|
const S = {
|
|
371
742
|
document: tab.doc.document,
|
|
372
743
|
mode: tab.doc.mode,
|
|
373
744
|
ui: tab.session.ui,
|
|
374
745
|
};
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
746
|
+
// The EFFECTIVE mode drives the host surface (panel structure, panzoom vs centered column vs the
|
|
747
|
+
// Preview stage). Preview is its own surface — a real, viewport-sized iframe that scrolls its own
|
|
748
|
+
// Document — so flipping the toggle IS a mode transition and rebuilds the surface, rather than
|
|
749
|
+
// Re-rendering the previous mode's panels in place.
|
|
750
|
+
const canvasMode = canvasModeOfPane(surface.paneId);
|
|
751
|
+
|
|
752
|
+
/* The Document Header card (§3.2 ⑧) is drawn by the STAGE, not by a band above it. Two surfaces
|
|
753
|
+
draw a page you can author: the centered Edit column, where the card goes INSIDE the document
|
|
754
|
+
column and scrolls with the artefact, and the Design artboards, where it is pinned above the
|
|
755
|
+
panzoom surface because a form drawn at the artboard's scale is a picture of a form, not a
|
|
756
|
+
control. `hasDocumentHeader` is the only remaining predicate and it is a fact about the
|
|
757
|
+
DOCUMENT — the one condition left, because the mode is the only other thing that decides
|
|
758
|
+
whether a page is being authored. It used to carry two more clauses, suppressing the card while
|
|
759
|
+
a function body or a formula was open on the grounds that those sub-editors took the whole
|
|
760
|
+
stage. They open in the dock's Logic tab now (P8) and the page is still on screen behind it, so
|
|
761
|
+
those clauses only DETACHED the visible card: it stopped re-rendering and quietly showed
|
|
762
|
+
frontmatter from before the edit. */
|
|
763
|
+
/* …and NOT in a lens. The Document Header is an editing surface — Title, Route, SEO, the raw
|
|
764
|
+
frontmatter — over a document this pane does not own, and two cards editing one document's
|
|
765
|
+
frontmatter side by side is two writers for one field. A lens is a view; the card belongs to
|
|
766
|
+
the pane that owns the tab. */
|
|
767
|
+
const wantsDocHeader =
|
|
768
|
+
(canvasMode === "edit" || canvasMode === "design") &&
|
|
769
|
+
derivationOfPane(surface.paneId)?.kind !== "lens" &&
|
|
770
|
+
hasDocumentHeader(tab);
|
|
771
|
+
if (!wantsDocHeader) {
|
|
772
|
+
attachDocumentHeaderHost(surface.paneId, null);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/* Advance THIS stage's render generation so stale async renders from its previous pass bail out.
|
|
776
|
+
Per-surface, because the comparison at the end of `renderCanvasIntoPanel` asks "is my pass
|
|
777
|
+
still the current one" — of this pane. App-wide, pane A opening a pass invalidated pane B's
|
|
778
|
+
in-flight one, so B's deferred artboards never reached `ready` and `classifyOps`' every-panel
|
|
779
|
+
gate escalated every subsequent edit in B to a full render, for good. The VALUE is still
|
|
780
|
+
globally unique, because the iframe host caches a pass's resolved document by it. */
|
|
781
|
+
surface.renderGeneration = nextRenderGeneration();
|
|
383
782
|
canvasPerf.fullRenders += 1;
|
|
384
783
|
|
|
385
784
|
// Detect whether this is a mode transition or a content-only re-render
|
|
386
|
-
const modeChanged = canvasMode !==
|
|
785
|
+
const modeChanged = canvasMode !== surface.prevCanvasMode;
|
|
387
786
|
|
|
388
787
|
// Only clear Lit's internal state on mode transitions (structural panel changes).
|
|
389
788
|
// For content re-renders in the same mode, Lit's template diffing preserves
|
|
@@ -391,27 +790,16 @@ function renderCanvasImpl() {
|
|
|
391
790
|
// RenderCanvasLive uses atomic clear (innerHTML = "" right before appendChild).
|
|
392
791
|
// @ts-expect-error -- _$litPart$ is Lit's private render-part marker, not in the DOM types
|
|
393
792
|
if (modeChanged && canvasWrap["_$litPart$"]) {
|
|
394
|
-
hardClearCanvasWrap();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Function editor mode: editing a function body in Monaco (JS)
|
|
398
|
-
if (S.ui.editingFunction) {
|
|
399
|
-
renderFunctionEditor();
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Dispose function editor if switching away
|
|
404
|
-
if (view.functionEditor) {
|
|
405
|
-
view.functionEditor.dispose();
|
|
406
|
-
view.functionEditor = null;
|
|
793
|
+
hardClearCanvasWrap(canvasWrap);
|
|
407
794
|
}
|
|
408
795
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
796
|
+
/* There are no `editingFunction` / `editingFormula` branches here, and that is the point.
|
|
797
|
+
Both used to RETURN from this function, which is what made the logic editors a takeover: the
|
|
798
|
+
stage kept whatever DOM it was last painted with and stopped tracking the document. P8 moved
|
|
799
|
+
both into the dock's Logic tab, `panels/bottom-dock.ts` reveals it from its own effect, and
|
|
800
|
+
`panels/editors.ts`'s `syncFunctionEditor` owns the Monaco instance from the panel's
|
|
801
|
+
`afterRender` — including disposing it. Nothing about the canvas render depends on a logic
|
|
802
|
+
target any more, so the page underneath the dock keeps rendering while you edit a formula. */
|
|
415
803
|
|
|
416
804
|
/* Source mode across a TAB switch: the buffer swap below reuses the existing model, and a model
|
|
417
805
|
carries the file identity Monaco validates against — its URI is what the JSON language service
|
|
@@ -420,32 +808,53 @@ function renderCanvasImpl() {
|
|
|
420
808
|
folder (`./project.schema.json` → `file:///pages/project.schema.json`, unregistered, back to
|
|
421
809
|
"No schema request service available"). Tear down instead and let the creation path below build
|
|
422
810
|
a model with the right URI. */
|
|
423
|
-
const loadedForSourceSwap =
|
|
424
|
-
|
|
811
|
+
const loadedForSourceSwap =
|
|
812
|
+
canvasMode === "source" && surface.monacoEditor ? loadedMonaco() : null;
|
|
813
|
+
if (loadedForSourceSwap && surface.monacoEditor) {
|
|
425
814
|
const expectedUri = loadedForSourceSwap.Uri.parse(
|
|
426
815
|
modelUriFor(tab.documentPath || "document.json"),
|
|
427
816
|
);
|
|
428
|
-
if (
|
|
429
|
-
disposeSourceCollab();
|
|
430
|
-
|
|
431
|
-
view.monacoEditor.dispose();
|
|
432
|
-
view.monacoEditor = null;
|
|
817
|
+
if (surface.monacoEditor.getModel()?.uri.toString() !== expectedUri.toString()) {
|
|
818
|
+
disposeSourceCollab(surface);
|
|
819
|
+
disposeSourceEditor(surface);
|
|
433
820
|
}
|
|
434
821
|
}
|
|
435
822
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
823
|
+
/* THE FAST PATHS, and the one condition every one of them owes.
|
|
824
|
+
Each says "the structure this mode needs is already standing on the stage, so there is nothing
|
|
825
|
+
to build". None of them can see the stage: they ask a MODULE whether it mounted (`view
|
|
826
|
+
.monacoEditor`, `gridPanelMounted`, …), and that answer outlives the DOM. `modeChanged` is
|
|
827
|
+
exactly "the structure on this stage is not mine" — a real mode transition, or a surface whose
|
|
828
|
+
`prevCanvasMode` was released because the stage changed hands — and the mode-transition clear
|
|
829
|
+
ABOVE has already emptied the wrap by the time any of them is reached.
|
|
830
|
+
Unguarded, this is what survived the Unsplit fix: the stage came back to the primary pane,
|
|
831
|
+
`prevCanvasMode` was null, the wrap was cleared, and the source fast path then returned on the
|
|
832
|
+
strength of a Monaco editor whose container had just been thrown away. An empty stage, a live
|
|
833
|
+
editor nobody could see, and only a reload to get it back. */
|
|
834
|
+
|
|
835
|
+
/* Source mode: update the existing Monaco editor without recreating it. Don't replace the buffer
|
|
836
|
+
while it has moved on — that would reformat under the cursor (the source view is the editing
|
|
837
|
+
surface here, mirroring the panel draft-state behaviour). This branch is where the rule was
|
|
838
|
+
first written, as a bare `!editor.hasTextFocus()`; `services/monaco-buffer.ts` is where it
|
|
839
|
+
lives now, and it gained the clauses this spelling was missing — a buffer holding text the
|
|
840
|
+
document has not been given (the user typed, then clicked away), an editor disposed inside the
|
|
841
|
+
round trip, and clause 5: a CO-EDITED buffer, which this write does not merely overwrite but
|
|
842
|
+
PUBLISHES over, to every peer in the room. The text below is `serializeDocument` of a structure
|
|
843
|
+
tree the source reconciler parsed out of that same shared text, so on a co-edited tab this was
|
|
844
|
+
the round trip asserting itself over its own input on every repaint. */
|
|
845
|
+
if (canvasMode === "source" && surface.monacoEditor && !modeChanged) {
|
|
846
|
+
const editor = surface.monacoEditor;
|
|
847
|
+
sourceContent(tab)
|
|
442
848
|
.then((newVal) => {
|
|
443
|
-
if (
|
|
849
|
+
if (surface.monacoEditor !== editor) {
|
|
444
850
|
return;
|
|
445
851
|
}
|
|
446
|
-
if (!editor
|
|
852
|
+
if (!bufferMovedOn(editor) && editor.getValue() !== newVal) {
|
|
447
853
|
editor._ignoreNextChange = true;
|
|
448
854
|
editor.setValue(newVal);
|
|
855
|
+
// Buffer and document agree again, by the document's text winning. Same declaration the
|
|
856
|
+
// Dock's re-sync makes, for the same reason: clause 3 belongs to whoever writes.
|
|
857
|
+
editor._writes?.markSettled();
|
|
449
858
|
}
|
|
450
859
|
})
|
|
451
860
|
.catch(() => {
|
|
@@ -456,7 +865,25 @@ function renderCanvasImpl() {
|
|
|
456
865
|
|
|
457
866
|
// Grid fast-path: the grid panel runs its own effect scope (toolbar + engine stay live), so a
|
|
458
867
|
// Same-tab re-render while the panel is mounted needs nothing from the canvas pipeline.
|
|
459
|
-
if (canvasMode === "grid" && gridPanelMounted(tab)) {
|
|
868
|
+
if (canvasMode === "grid" && gridPanelMounted(surface.paneId, tab) && !modeChanged) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Library fast-path, same shape: the Library pane owns its own reactivity (view state, scan,
|
|
873
|
+
// Window), so a same-tab re-render while it is mounted needs nothing from the canvas pipeline.
|
|
874
|
+
if (canvasMode === "manage" && libraryPaneMounted(surface.paneId, tab) && !modeChanged) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Entry fast-path, same shape: the entry form owns its own effect over the tab's frontmatter, so
|
|
879
|
+
// A field commit repaints the form and nothing reaches the canvas pipeline.
|
|
880
|
+
if (canvasMode === "entry" && entryPaneMounted(surface.paneId, tab) && !modeChanged) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Settings fast-path, same shape: the Project Settings editor subscribes to the section registry
|
|
885
|
+
// And to its own chosen section, so a mounted editor needs nothing from the canvas pipeline.
|
|
886
|
+
if (canvasMode === "settings" && settingsPaneMounted(surface) && !modeChanged) {
|
|
460
887
|
return;
|
|
461
888
|
}
|
|
462
889
|
|
|
@@ -465,10 +892,15 @@ function renderCanvasImpl() {
|
|
|
465
892
|
// Filter/Customized changes fall through to the full rebuild (they change which specimens exist),
|
|
466
893
|
// As does a zero-host post (no stylebook iframe live yet).
|
|
467
894
|
if (canvasMode === "stylebook" && !modeChanged) {
|
|
468
|
-
|
|
469
|
-
|
|
895
|
+
/* THIS stage's last catalogue, not the module's. Both panes render in the same frame when
|
|
896
|
+
`shell.stylebook.filter` changes, so a single slot was advanced by whichever stage went
|
|
897
|
+
first and the second one then saw `filterChanged === false` — returning through the cheap
|
|
898
|
+
style-post path with a catalogue built for the PREVIOUS filter. */
|
|
899
|
+
const curFilter = shell.stylebook.filter;
|
|
900
|
+
const curCustomized = shell.stylebook.customizedOnly;
|
|
470
901
|
const filterChanged =
|
|
471
|
-
curFilter !==
|
|
902
|
+
curFilter !== surface.prevStylebookFilter ||
|
|
903
|
+
curCustomized !== surface.prevStylebookCustomizedOnly;
|
|
472
904
|
if (!filterChanged) {
|
|
473
905
|
const style = transposeStylebookStyle(getEffectiveStyle(tab.doc.document?.style));
|
|
474
906
|
if (postStyleUpdateToStylebookHosts(style as Record<string, unknown>) > 0) {
|
|
@@ -478,7 +910,7 @@ function renderCanvasImpl() {
|
|
|
478
910
|
}
|
|
479
911
|
|
|
480
912
|
// Detect whether this is a mode transition or a content-only re-render
|
|
481
|
-
|
|
913
|
+
surface.prevCanvasMode = canvasMode;
|
|
482
914
|
|
|
483
915
|
// Best-effort commit of a live inline-edit session BEFORE lit rebuilds the panel DOM. Covers
|
|
484
916
|
// Keyboard-driven tab switches / mode changes where no parent pointerdown preceded the render —
|
|
@@ -486,19 +918,11 @@ function renderCanvasImpl() {
|
|
|
486
918
|
// Still routes to the tab the session belonged to (the host's tabId flips only on renderComplete).
|
|
487
919
|
commitActiveEditSession();
|
|
488
920
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
view.canvasDndCleanups = [];
|
|
495
|
-
|
|
496
|
-
// Panel event handlers (click, dblclick, etc.) capture closures over panel references.
|
|
497
|
-
// Always re-register to keep closures fresh across document switches.
|
|
498
|
-
for (const fn of view.canvasEventCleanups) {
|
|
499
|
-
fn();
|
|
500
|
-
}
|
|
501
|
-
view.canvasEventCleanups = [];
|
|
921
|
+
/* There is no `canvasDndCleanups` / `canvasEventCleanups` sweep here any more, and there never
|
|
922
|
+
needed to be one. Both arrays were written by nothing in `src/` — the DnD and panel handlers
|
|
923
|
+
they were built for moved inside the canvas iframe, whose overlay owns their lifetime — so the
|
|
924
|
+
two loops ran over an empty array on every render and the two fields existed only to be
|
|
925
|
+
cleared. Six test assertions were their only producers. */
|
|
502
926
|
|
|
503
927
|
// Panel JS objects are cheap — always clear and repopulate from templates.
|
|
504
928
|
// The actual DOM elements are preserved by Lit's diffing on content-only re-renders.
|
|
@@ -512,24 +936,33 @@ function renderCanvasImpl() {
|
|
|
512
936
|
|
|
513
937
|
if (modeChanged) {
|
|
514
938
|
// Full teardown on mode transitions — new panel structure needed
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
939
|
+
if (surface.centerObserver) {
|
|
940
|
+
surface.centerObserver.disconnect();
|
|
941
|
+
surface.centerObserver = null;
|
|
518
942
|
}
|
|
519
943
|
|
|
520
944
|
// Destroy the grid panel if switching away from grid mode
|
|
521
|
-
detachGridPanel();
|
|
945
|
+
detachGridPanel(surface.paneId);
|
|
946
|
+
|
|
947
|
+
// The Library holds an IntersectionObserver and an LRU of live runtime subtrees; leaving the
|
|
948
|
+
// Mode without releasing them is exactly the unbounded retention P7.1 exists to remove.
|
|
949
|
+
detachLibraryPane(surface.paneId);
|
|
950
|
+
|
|
951
|
+
// Same for the entry form, whose effect scope subscribes to the document's frontmatter
|
|
952
|
+
detachEntryPane(surface.paneId);
|
|
953
|
+
|
|
954
|
+
// Same for the Project Settings editor, which holds a registry subscription
|
|
955
|
+
detachSettingsPane(surface.paneId);
|
|
522
956
|
|
|
523
957
|
// Dispose Monaco editor if switching away from source mode
|
|
524
|
-
disposeSourceCollab();
|
|
525
|
-
|
|
526
|
-
view.monacoEditor.getModel()?.dispose();
|
|
527
|
-
view.monacoEditor.dispose();
|
|
528
|
-
view.monacoEditor = null;
|
|
529
|
-
}
|
|
958
|
+
disposeSourceCollab(surface);
|
|
959
|
+
disposeSourceEditor(surface);
|
|
530
960
|
|
|
961
|
+
// Same reason as `hardClearCanvasWrap`: lit is about to detach every artboard's iframe, and a
|
|
962
|
+
// Detached host is otherwise only noticed by whichever lazy `liveHosts` walk runs next.
|
|
963
|
+
releaseCanvasHosts(canvasWrap);
|
|
531
964
|
litRender(nothing, canvasWrap);
|
|
532
|
-
|
|
965
|
+
surface.panzoomWrap = null;
|
|
533
966
|
// Reset inline style overrides from other modes
|
|
534
967
|
canvasWrap.style.padding = "";
|
|
535
968
|
canvasWrap.style.alignItems = "";
|
|
@@ -547,14 +980,89 @@ function renderCanvasImpl() {
|
|
|
547
980
|
|
|
548
981
|
// Stylebook mode: render element catalog with panzoom surface
|
|
549
982
|
if (canvasMode === "stylebook") {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
renderStylebookMode({
|
|
983
|
+
surface.prevStylebookFilter = shell.stylebook.filter;
|
|
984
|
+
surface.prevStylebookCustomizedOnly = shell.stylebook.customizedOnly;
|
|
985
|
+
renderStylebookMode(surface, {
|
|
553
986
|
applyTransform,
|
|
554
987
|
canvasPanelTemplate,
|
|
555
988
|
observeCenterUntilStable,
|
|
556
989
|
updateActivePanelHeaders,
|
|
557
990
|
});
|
|
991
|
+
if (modeChanged) {
|
|
992
|
+
fitOnCanvasEntry(surface);
|
|
993
|
+
}
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/* Preview — the fidelity surface. One stage, one iframe, sized to the pane and scrolling its own
|
|
998
|
+
document, with no panzoom-wrap: the pan transform is precisely what stopped `position:sticky`,
|
|
999
|
+
scroll-driven animation and IntersectionObserver reveals from ever firing in the one mode whose
|
|
1000
|
+
job is to show the page as it ships. The host keeps the iframe at its CSS height for the same
|
|
1001
|
+
reason (see the `contentHeight` case in iframe-host.ts), and gates every editing affordance —
|
|
1002
|
+
hits, hover, the insertion "+", the Jx context menu and drops — off this render. */
|
|
1003
|
+
if (canvasMode === "preview") {
|
|
1004
|
+
if (modeChanged) {
|
|
1005
|
+
canvasWrap.style.padding = "0";
|
|
1006
|
+
canvasWrap.style.display = "block";
|
|
1007
|
+
canvasWrap.style.overflow = "hidden";
|
|
1008
|
+
}
|
|
1009
|
+
/*
|
|
1010
|
+
* PREVIEW IS AS WIDE AS THE CHOSEN BREAKPOINT, for the reason Edit is (see that branch).
|
|
1011
|
+
*
|
|
1012
|
+
* It filled the pane at every size: `--sm` selected, 640px in Edit, 924px here. The size
|
|
1013
|
+
* switcher is one per-pane setting and preview is the mode whose entire job is showing the page
|
|
1014
|
+
* as it will be seen — a control that changes the rendering context has to change the
|
|
1015
|
+
* rendering. Same expression as Edit's column, so the same page at `md` is the same page
|
|
1016
|
+
* whichever of the two the toggle is over.
|
|
1017
|
+
*
|
|
1018
|
+
* The HEIGHT is untouched: the frame stays the pane's height and the document scrolls itself,
|
|
1019
|
+
* which is what makes sticky, scroll-driven animation and IntersectionObserver fire here.
|
|
1020
|
+
*/
|
|
1021
|
+
const { baseWidth: previewBase, sizeBreakpoints: previewBreakpoints } = parseMediaEntries(
|
|
1022
|
+
getEffectiveMedia(S.document.$media),
|
|
1023
|
+
);
|
|
1024
|
+
const previewMedia = activeMediaOfPane(surface.paneId);
|
|
1025
|
+
const previewWidth =
|
|
1026
|
+
previewBreakpoints.find((bp) => bp.name === previewMedia)?.width ?? previewBase;
|
|
1027
|
+
const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
|
|
1028
|
+
litRender(
|
|
1029
|
+
html`<div class="preview-stage" style="max-width:${previewWidth}px">${panelTpl}</div>`,
|
|
1030
|
+
canvasWrap,
|
|
1031
|
+
);
|
|
1032
|
+
canvasPanels.push(panel as unknown as CanvasPanel);
|
|
1033
|
+
renderCanvasIntoPanel(surface, panel as unknown as CanvasPanel, S.ui.featureToggles);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/* Settings mode: the Project Settings document (plan §9.3). Same non-iframe-editor pattern as
|
|
1038
|
+
grid and stylebook — the panel owns its own reactivity from here — and the reason the seven
|
|
1039
|
+
settings screenshots now crop `pane.primary` instead of `overlay.dialog:settings` without one
|
|
1040
|
+
manifest step changing. */
|
|
1041
|
+
if (canvasMode === "settings") {
|
|
1042
|
+
canvasWrap.style.padding = "0";
|
|
1043
|
+
canvasWrap.style.display = "block";
|
|
1044
|
+
renderSettingsPane(surface);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/* Library mode: the project's own contents, over a GridSource, in one of five layouts
|
|
1049
|
+
(`browse/library-pane.ts`). `commands/context.ts` already mapped `manage` to the `library`
|
|
1050
|
+
editor kind — this is the surface that map was pointing at. */
|
|
1051
|
+
if (canvasMode === "manage") {
|
|
1052
|
+
canvasWrap.style.padding = "0";
|
|
1053
|
+
canvasWrap.style.display = "block";
|
|
1054
|
+
renderLibraryMode(surface, tab);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/* Entry mode: one content entry's fields, typed by its collection's schema
|
|
1059
|
+
(`content/entry-editor.ts`). Same non-iframe-editor pattern as the Library and Settings — and
|
|
1060
|
+
the reason `entry` is in `commands/context.ts`'s mode map, without which a form over a
|
|
1061
|
+
frontmatter record would have resolved into the CANVAS key scope. */
|
|
1062
|
+
if (canvasMode === "entry") {
|
|
1063
|
+
canvasWrap.style.padding = "0";
|
|
1064
|
+
canvasWrap.style.display = "block";
|
|
1065
|
+
renderEntryMode(surface, tab);
|
|
558
1066
|
return;
|
|
559
1067
|
}
|
|
560
1068
|
|
|
@@ -563,7 +1071,7 @@ function renderCanvasImpl() {
|
|
|
563
1071
|
if (canvasMode === "grid") {
|
|
564
1072
|
canvasWrap.style.padding = "0";
|
|
565
1073
|
canvasWrap.style.display = "block";
|
|
566
|
-
renderGridMode(
|
|
1074
|
+
renderGridMode(surface, tab);
|
|
567
1075
|
return;
|
|
568
1076
|
}
|
|
569
1077
|
|
|
@@ -588,15 +1096,30 @@ function renderCanvasImpl() {
|
|
|
588
1096
|
|
|
589
1097
|
const filePath = tab.documentPath || "document.json";
|
|
590
1098
|
const lang = sourceLang(tab);
|
|
591
|
-
void mountSourceEditor(tab, editorContainer as unknown as Element, filePath, lang);
|
|
1099
|
+
void mountSourceEditor(tab, surface, editorContainer as unknown as Element, filePath, lang);
|
|
592
1100
|
return;
|
|
593
1101
|
}
|
|
594
1102
|
|
|
595
1103
|
// Git diff mode — render original (left) and current (right) side-by-side on panzoom surface
|
|
596
1104
|
if (canvasMode === "git-diff") {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
1105
|
+
/* THIS PANE's diff. `shell.git.diffState` is one app-level slot, so with a diff lens open
|
|
1106
|
+
beside a page the two stages would draw the same comparison — and changing the primary's
|
|
1107
|
+
mode would clear the diff the side pane is still showing. A diff lens carries its own. */
|
|
1108
|
+
const paneDiff = derivationOfPane(surface.paneId);
|
|
1109
|
+
const lensDiff = paneDiff?.kind === "lens" && paneDiff.preset === "diff";
|
|
1110
|
+
const gitDiffState = lensDiff ? paneDiff.diff : ctx.gitDiffState;
|
|
1111
|
+
if (!gitDiffState) {
|
|
1112
|
+
/* A LENS NEVER TAKES THE FALLBACK. `setCanvasMode(tab, "design")` writes the SOURCE pane's
|
|
1113
|
+
tab — the one thing a lens must not do — so a diff lens whose comparison had not landed
|
|
1114
|
+
yet flipped the document the author was editing out of whatever mode they had it in. Its
|
|
1115
|
+
own comparison is still loading (or could not be read): say so on this stage and leave the
|
|
1116
|
+
tab alone. */
|
|
1117
|
+
if (lensDiff) {
|
|
1118
|
+
renderDerivationNotice(surface, paneDiff.reason || "Loading this file's changes…");
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
ctx.setCanvasMode(tab, "design");
|
|
1122
|
+
renderCanvas(surface.paneId);
|
|
600
1123
|
return;
|
|
601
1124
|
}
|
|
602
1125
|
|
|
@@ -605,7 +1128,6 @@ function renderCanvasImpl() {
|
|
|
605
1128
|
canvasWrap.style.overflow = "hidden";
|
|
606
1129
|
}
|
|
607
1130
|
|
|
608
|
-
const { gitDiffState } = ctx;
|
|
609
1131
|
const panelWidth = 800;
|
|
610
1132
|
|
|
611
1133
|
const { tpl: origTpl, panel: origPanel } = canvasPanelTemplate(
|
|
@@ -628,7 +1150,7 @@ function renderCanvasImpl() {
|
|
|
628
1150
|
style="transform-origin:0 0"
|
|
629
1151
|
${ref((el) => {
|
|
630
1152
|
if (el) {
|
|
631
|
-
|
|
1153
|
+
surface.panzoomWrap = el as HTMLDivElement;
|
|
632
1154
|
}
|
|
633
1155
|
})}
|
|
634
1156
|
>
|
|
@@ -659,27 +1181,34 @@ function renderCanvasImpl() {
|
|
|
659
1181
|
};
|
|
660
1182
|
|
|
661
1183
|
const { featureToggles } = S.ui;
|
|
1184
|
+
// Both sides mount after an await, so the pass's generation is captured now — see the note on
|
|
1185
|
+
// `passGen` at the design-mode artboard loop.
|
|
1186
|
+
const diffGen = surface.renderGeneration;
|
|
662
1187
|
void Promise.all([
|
|
663
1188
|
parseContent(gitDiffState.originalContent || ""),
|
|
664
1189
|
parseContent(gitDiffState.currentContent || ""),
|
|
665
1190
|
]).then(([originalDoc, currentDoc]) => {
|
|
666
1191
|
renderCanvasIntoPanel(
|
|
1192
|
+
surface,
|
|
667
1193
|
origPanel as unknown as CanvasPanel,
|
|
668
1194
|
featureToggles,
|
|
669
1195
|
originalDoc,
|
|
670
1196
|
gitDiffState,
|
|
1197
|
+
diffGen,
|
|
671
1198
|
);
|
|
672
1199
|
renderCanvasIntoPanel(
|
|
1200
|
+
surface,
|
|
673
1201
|
currPanel as unknown as CanvasPanel,
|
|
674
1202
|
featureToggles,
|
|
675
1203
|
currentDoc,
|
|
676
1204
|
gitDiffState,
|
|
1205
|
+
diffGen,
|
|
677
1206
|
);
|
|
678
1207
|
});
|
|
679
1208
|
|
|
680
|
-
applyTransform();
|
|
1209
|
+
applyTransform(surface);
|
|
681
1210
|
if (modeChanged) {
|
|
682
|
-
observeCenterUntilStable();
|
|
1211
|
+
observeCenterUntilStable(surface);
|
|
683
1212
|
}
|
|
684
1213
|
return;
|
|
685
1214
|
}
|
|
@@ -692,7 +1221,23 @@ function renderCanvasImpl() {
|
|
|
692
1221
|
canvasWrap.style.overflow = "hidden";
|
|
693
1222
|
}
|
|
694
1223
|
|
|
695
|
-
|
|
1224
|
+
/*
|
|
1225
|
+
* THE COLUMN IS AS WIDE AS THE CHOSEN BREAKPOINT.
|
|
1226
|
+
*
|
|
1227
|
+
* It was always `baseWidth`, so the Context bar's size switcher did nothing here: it wrote
|
|
1228
|
+
* `session.ui.activeMedia`, Design used that to highlight one of the artboards it already draws
|
|
1229
|
+
* side by side, and Edit — which draws ONE column — ignored it. A control that changes the
|
|
1230
|
+
* rendering context has to change the rendering, or it is a control over a label.
|
|
1231
|
+
*
|
|
1232
|
+
* The width is the artboard width Design would give that breakpoint, so the same page at `md`
|
|
1233
|
+
* is the same page in both modes; the iframe is that wide, so the document's own media queries
|
|
1234
|
+
* evaluate against it and the content reflows for real rather than being scaled.
|
|
1235
|
+
*/
|
|
1236
|
+
const { baseWidth, sizeBreakpoints: editBreakpoints } = parseMediaEntries(
|
|
1237
|
+
getEffectiveMedia(S.document.$media),
|
|
1238
|
+
);
|
|
1239
|
+
const editMedia = activeMediaOfPane(surface.paneId);
|
|
1240
|
+
const columnWidth = editBreakpoints.find((bp) => bp.name === editMedia)?.width ?? baseWidth;
|
|
696
1241
|
const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
|
|
697
1242
|
// A component-definition doc (root tag is a custom element) is a fragment, not a page: it should
|
|
698
1243
|
// Hug its content rather than have the column fill+stretch to the viewport (dead scroll space).
|
|
@@ -706,16 +1251,18 @@ function renderCanvasImpl() {
|
|
|
706
1251
|
panel.scrollContainer = (el as HTMLElement) || null;
|
|
707
1252
|
})}
|
|
708
1253
|
>
|
|
709
|
-
<div class=${columnClass} style="max-width:${
|
|
1254
|
+
<div class=${columnClass} style="max-width:${columnWidth}px">
|
|
1255
|
+
${wantsDocHeader ? docHeaderSlot("in-column", surface.paneId) : nothing}${panelTpl}
|
|
1256
|
+
</div>
|
|
710
1257
|
</div>
|
|
711
1258
|
`;
|
|
712
1259
|
litRender(editTpl, canvasWrap);
|
|
713
1260
|
canvasPanels.push(panel as unknown as CanvasPanel);
|
|
714
|
-
renderCanvasIntoPanel(panel as unknown as CanvasPanel, S.ui.featureToggles);
|
|
1261
|
+
renderCanvasIntoPanel(surface, panel as unknown as CanvasPanel, S.ui.featureToggles);
|
|
715
1262
|
// The column must exist in the DOM before the zoom's live width measurement — so the zoom is
|
|
716
1263
|
// Applied after the render rather than baked into the template (the panel mounts fluid and is
|
|
717
1264
|
// Immediately re-fitted; the iframe hasn't painted yet, so nothing visibly jumps).
|
|
718
|
-
applyEditZoom();
|
|
1265
|
+
applyEditZoom(surface);
|
|
719
1266
|
return;
|
|
720
1267
|
}
|
|
721
1268
|
|
|
@@ -724,6 +1271,13 @@ function renderCanvasImpl() {
|
|
|
724
1271
|
canvasWrap.style.padding = "0";
|
|
725
1272
|
canvasWrap.style.overflow = "hidden";
|
|
726
1273
|
}
|
|
1274
|
+
/* The stage stacks when it carries the card: header first, artboards below. Set on every render
|
|
1275
|
+
rather than on the transition, because whether a document HAS a header changes with the tab and
|
|
1276
|
+
not with the mode. `editors.ts` and `formula-workspace.ts` already claim the column this way —
|
|
1277
|
+
`#canvas-wrap` is a row by default and each surface states its own axis. */
|
|
1278
|
+
canvasWrap.style.flexDirection = wantsDocHeader ? "column" : "";
|
|
1279
|
+
canvasWrap.style.alignItems = wantsDocHeader ? "stretch" : "";
|
|
1280
|
+
const designHeaderTpl = wantsDocHeader ? docHeaderSlot("pinned", surface.paneId) : nothing;
|
|
727
1281
|
|
|
728
1282
|
const {
|
|
729
1283
|
sizeBreakpoints,
|
|
@@ -747,12 +1301,13 @@ function renderCanvasImpl() {
|
|
|
747
1301
|
);
|
|
748
1302
|
litRender(
|
|
749
1303
|
html`
|
|
1304
|
+
${designHeaderTpl}
|
|
750
1305
|
<div
|
|
751
1306
|
class="panzoom-wrap"
|
|
752
1307
|
style="transform-origin:0 0"
|
|
753
1308
|
${ref((el) => {
|
|
754
1309
|
if (el) {
|
|
755
|
-
|
|
1310
|
+
surface.panzoomWrap = el as HTMLDivElement;
|
|
756
1311
|
}
|
|
757
1312
|
})}
|
|
758
1313
|
>
|
|
@@ -762,10 +1317,14 @@ function renderCanvasImpl() {
|
|
|
762
1317
|
canvasWrap,
|
|
763
1318
|
);
|
|
764
1319
|
canvasPanels.push(panel as unknown as CanvasPanel);
|
|
765
|
-
renderCanvasIntoPanel(panel as unknown as CanvasPanel, featureToggles);
|
|
766
|
-
applyTransform();
|
|
1320
|
+
renderCanvasIntoPanel(surface, panel as unknown as CanvasPanel, featureToggles);
|
|
1321
|
+
applyTransform(surface);
|
|
767
1322
|
if (modeChanged) {
|
|
768
|
-
|
|
1323
|
+
// Fit BEFORE centering: the fit picks the zoom, centerCanvas then places the artboard at that
|
|
1324
|
+
// Zoom (and top-aligns it, which beats fit's vertical centering for a page taller than the
|
|
1325
|
+
// Viewport).
|
|
1326
|
+
fitOnCanvasEntry(surface);
|
|
1327
|
+
observeCenterUntilStable(surface);
|
|
769
1328
|
}
|
|
770
1329
|
return;
|
|
771
1330
|
}
|
|
@@ -787,7 +1346,20 @@ function renderCanvasImpl() {
|
|
|
787
1346
|
});
|
|
788
1347
|
}
|
|
789
1348
|
|
|
790
|
-
|
|
1349
|
+
/* A BREAKPOINT LENS is one artboard, not a second copy of the design board.
|
|
1350
|
+
Design mode already draws every declared breakpoint side by side on one panzoom surface, so
|
|
1351
|
+
"the same page at two breakpoints" was never a rendering problem — it was that a tab lives in
|
|
1352
|
+
one pane. The lens draws exactly the one artboard it names, filling its own pane at its own
|
|
1353
|
+
zoom; the fallback to the whole set is for a media the document has since stopped declaring,
|
|
1354
|
+
which `derivedTarget` also reports as `unavailable`. */
|
|
1355
|
+
const lens = derivationOfPane(surface.paneId);
|
|
1356
|
+
const lensMedia =
|
|
1357
|
+
lens?.kind === "lens" && lens.preset === "breakpoint" ? (lens.media ?? "base") : null;
|
|
1358
|
+
const chosen =
|
|
1359
|
+
lensMedia === null ? allPanelDefs : allPanelDefs.filter((d) => d.name === lensMedia);
|
|
1360
|
+
const panelDefs = chosen.length > 0 ? chosen : allPanelDefs;
|
|
1361
|
+
|
|
1362
|
+
const panelEntries = panelDefs.map((def) => {
|
|
791
1363
|
const label = `${def.displayName} (${def.width}px)`;
|
|
792
1364
|
const { tpl, panel } = canvasPanelTemplate(def.name, label, false, def.width);
|
|
793
1365
|
return { panel, tpl };
|
|
@@ -795,12 +1367,13 @@ function renderCanvasImpl() {
|
|
|
795
1367
|
|
|
796
1368
|
litRender(
|
|
797
1369
|
html`
|
|
1370
|
+
${designHeaderTpl}
|
|
798
1371
|
<div
|
|
799
1372
|
class="panzoom-wrap"
|
|
800
1373
|
style="transform-origin:0 0"
|
|
801
1374
|
${ref((el) => {
|
|
802
1375
|
if (el) {
|
|
803
|
-
|
|
1376
|
+
surface.panzoomWrap = el as HTMLDivElement;
|
|
804
1377
|
}
|
|
805
1378
|
})}
|
|
806
1379
|
>
|
|
@@ -810,24 +1383,35 @@ function renderCanvasImpl() {
|
|
|
810
1383
|
canvasWrap,
|
|
811
1384
|
);
|
|
812
1385
|
|
|
1386
|
+
/* Every artboard of this pass mounts under the generation the pass opened with, captured HERE
|
|
1387
|
+
rather than read inside each deferred callback. Two things depend on it. The iframe drops a
|
|
1388
|
+
render whose gen is older than the one it last accepted, so a pass that has been superseded
|
|
1389
|
+
while its artboards were still queued is discarded by the frame instead of posting a duplicate
|
|
1390
|
+
render under the NEW pass's number. And the host resolves the document once per generation
|
|
1391
|
+
(`preparePassRender`), which only fans out if the pass has one. */
|
|
1392
|
+
const passGen = surface.renderGeneration;
|
|
813
1393
|
for (let i = 0; i < panelEntries.length; i++) {
|
|
814
1394
|
const { panel } = panelEntries[i]!;
|
|
815
1395
|
const p = panel as CanvasPanel;
|
|
816
1396
|
canvasPanels.push(p);
|
|
817
1397
|
if (i === 0) {
|
|
818
|
-
renderCanvasIntoPanel(p, featureToggles);
|
|
1398
|
+
renderCanvasIntoPanel(surface, p, featureToggles);
|
|
819
1399
|
} else {
|
|
820
|
-
|
|
1400
|
+
// Yield between artboards so the first one paints before the rest mount. They no longer pay
|
|
1401
|
+
// For the document — the pass already resolved it — but each is still a live iframe render.
|
|
1402
|
+
setTimeout(() => renderCanvasIntoPanel(surface, p, featureToggles, null, null, passGen), 0);
|
|
821
1403
|
}
|
|
822
1404
|
}
|
|
823
1405
|
|
|
824
|
-
// Highlight active panel header
|
|
825
|
-
updateActivePanelHeaders();
|
|
1406
|
+
// Highlight active panel header — this pass's stage, not the focused pane's.
|
|
1407
|
+
updateActivePanelHeaders(surface);
|
|
826
1408
|
|
|
827
1409
|
// Apply current zoom + pan transform
|
|
828
|
-
applyTransform();
|
|
1410
|
+
applyTransform(surface);
|
|
829
1411
|
if (modeChanged) {
|
|
830
|
-
|
|
1412
|
+
// See the single-panel path: fit picks the zoom, centerCanvas then places the artboards.
|
|
1413
|
+
fitOnCanvasEntry(surface);
|
|
1414
|
+
observeCenterUntilStable(surface);
|
|
831
1415
|
}
|
|
832
1416
|
}
|
|
833
1417
|
|
|
@@ -837,6 +1421,8 @@ function renderCanvasImpl() {
|
|
|
837
1421
|
* `data-jx-path`/`data-jx-layout`, and draws its own overlays). Git-diff/preview overrides render
|
|
838
1422
|
* but stay un-patchable (`ready` only flips for the real tab document).
|
|
839
1423
|
*
|
|
1424
|
+
* @param {CanvasSurface} surface - The pane's stage this panel belongs to. The document rendered
|
|
1425
|
+
* and the preview flag both come from THAT pane, never from "the active tab".
|
|
840
1426
|
* @param {CanvasPanel} panel
|
|
841
1427
|
* @param {Record<string, boolean>} _featureToggles - Accepted for call-site symmetry (the iframe
|
|
842
1428
|
* render needs no structural-preview fallback); unused.
|
|
@@ -844,21 +1430,33 @@ function renderCanvasImpl() {
|
|
|
844
1430
|
* active tab doc if not provided.
|
|
845
1431
|
* @param {GitDiffState | null} [_gitDiffState] - Accepted for call-site symmetry; the iframe path
|
|
846
1432
|
* does not apply parent-side diff highlighting.
|
|
1433
|
+
* @param {number | null} [passGen] - The generation of the render pass this panel belongs to.
|
|
1434
|
+
* Passed explicitly by a DEFERRED mount, whose callback would otherwise read whatever generation
|
|
1435
|
+
* is current when the timer fires — a later pass's number, stamped on an earlier pass's
|
|
1436
|
+
* artboard.
|
|
847
1437
|
*/
|
|
848
1438
|
function renderCanvasIntoPanel(
|
|
1439
|
+
surface: CanvasSurface,
|
|
849
1440
|
panel: CanvasPanel,
|
|
850
1441
|
_featureToggles: Record<string, boolean>,
|
|
851
1442
|
docOverride: JxMutableNode | null = null,
|
|
852
1443
|
_gitDiffState: GitDiffState | null = null,
|
|
1444
|
+
passGen: number | null = null,
|
|
853
1445
|
) {
|
|
854
|
-
const gen =
|
|
855
|
-
const tab =
|
|
1446
|
+
const gen = passGen ?? surface.renderGeneration;
|
|
1447
|
+
const tab = tabOfPane(surface.paneId);
|
|
856
1448
|
const docToRender = docOverride || (tab?.doc.document as JxMutableNode);
|
|
857
1449
|
const canvas = panel.canvas as HTMLElement;
|
|
858
1450
|
|
|
859
1451
|
canvasPerf.panelRenders += 1;
|
|
860
1452
|
panel.ready = false;
|
|
861
1453
|
|
|
1454
|
+
// The host's frame box depends on whether this is a preview render, and mountIframeCanvas only
|
|
1455
|
+
// Learns that after it has awaited the resolved document — one await during which the iframe can
|
|
1456
|
+
// Post a contentHeight that the OUTGOING mode would answer. Declare it here, before the await,
|
|
1457
|
+
// Where it is already known: this is the mode whose surface was just built.
|
|
1458
|
+
adoptCanvasPreviewMode(canvas, canvasModeOfPane(surface.paneId) === "preview");
|
|
1459
|
+
|
|
862
1460
|
// Overrides (git-diff docs) mount with a null tab identity: their iframes must never route doc
|
|
863
1461
|
// Mutations anywhere. The real doc carries its tab id so edit/drop messages route to THAT tab.
|
|
864
1462
|
void timeSpanAsync(SPAN_MOUNT_CANVAS, () =>
|
|
@@ -868,23 +1466,297 @@ function renderCanvasIntoPanel(
|
|
|
868
1466
|
canvas,
|
|
869
1467
|
panel._width,
|
|
870
1468
|
docOverride ? null : (tab?.id ?? null),
|
|
1469
|
+
// The VIEW tab, which an override does not null out: a git-diff frame routes no mutations
|
|
1470
|
+
// But is still a view of this pane's document, and its docBase, layout toggle and mode are
|
|
1471
|
+
// That tab's. Explicit rather than left to the default, because this line already knows.
|
|
1472
|
+
tab,
|
|
871
1473
|
),
|
|
872
1474
|
)
|
|
873
1475
|
.then(() => {
|
|
874
|
-
if (gen ===
|
|
1476
|
+
if (gen === surface.renderGeneration) {
|
|
875
1477
|
// Mark the panel patchable once the real document is mounted (not a diff/preview override)
|
|
876
1478
|
// So classifyOps admits surgical patches; the iframe holds the render context, so this
|
|
877
1479
|
// Panel needs no parent-side render scope.
|
|
878
1480
|
panel.ready = !docOverride;
|
|
879
|
-
updateCanvas({ error: null, scope: null, status: "ready" });
|
|
880
|
-
|
|
1481
|
+
updateCanvas(tab, { error: null, scope: null, status: "ready" });
|
|
1482
|
+
// A successful render used to announce itself as "Iframe render OK" — a debug string
|
|
1483
|
+
// Shipped to end users and legible in a published docs screenshot. A render that worked is
|
|
1484
|
+
// The canvas you are looking at; it says so itself.
|
|
881
1485
|
}
|
|
882
1486
|
})
|
|
883
1487
|
.catch((error: unknown) => {
|
|
884
|
-
|
|
1488
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1489
|
+
notify.error("The canvas could not be mounted.", {
|
|
1490
|
+
detail,
|
|
1491
|
+
key: "canvas.mount",
|
|
1492
|
+
source: "Canvas",
|
|
1493
|
+
});
|
|
885
1494
|
});
|
|
886
1495
|
}
|
|
887
1496
|
|
|
1497
|
+
/*
|
|
1498
|
+
* How a pane's surface releases what THIS module created.
|
|
1499
|
+
*
|
|
1500
|
+
* Registered by injection rather than imported, because `canvas-surface.ts` sits below this file in
|
|
1501
|
+
* the graph and must stay there: it is imported by `store.ts`, the patcher and the iframe host.
|
|
1502
|
+
* Same idiom as `setIframePatchEscalation`. Without it a pane removed from the grid would drop its
|
|
1503
|
+
* record while its Monaco, its collab lock and its centering observer stayed alive.
|
|
1504
|
+
*/
|
|
1505
|
+
setSurfaceTeardown((surface) => {
|
|
1506
|
+
detachGridPanel(surface.paneId);
|
|
1507
|
+
detachLibraryPane(surface.paneId);
|
|
1508
|
+
detachEntryPane(surface.paneId);
|
|
1509
|
+
detachSettingsPane(surface.paneId);
|
|
1510
|
+
attachDocumentHeaderHost(surface.paneId, null);
|
|
1511
|
+
disposeSourceCollab(surface);
|
|
1512
|
+
disposeSourceEditor(surface);
|
|
1513
|
+
surface.centerObserver?.disconnect();
|
|
1514
|
+
surface.centerObserver = null;
|
|
1515
|
+
surface.panzoomWrap = null;
|
|
1516
|
+
_docHeaderRefs.delete(surface.paneId);
|
|
1517
|
+
});
|
|
1518
|
+
|
|
888
1519
|
export function renderOverlays() {
|
|
889
1520
|
overlaysPanel.render();
|
|
890
1521
|
}
|
|
1522
|
+
|
|
1523
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
1524
|
+
|
|
1525
|
+
/** The root identifier a merge-tag token binds to — `state`, `item` or `index`, or `""`. */
|
|
1526
|
+
function tokenRoot(token: string): string {
|
|
1527
|
+
return /^[A-Za-z_$][\w$]*/.exec(token)?.[0] ?? "";
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* Refuse a `${…}` token the open document cannot resolve, naming what it could have been.
|
|
1532
|
+
*
|
|
1533
|
+
* The same rule `data.expandRow` applies to a row name, for the same reason: an unresolvable token
|
|
1534
|
+
* renders as EMPTY TEXT, so inserting one silently produces a document that looks bound and is not
|
|
1535
|
+
* — and a capture of it documents a binding that never existed.
|
|
1536
|
+
*/
|
|
1537
|
+
function refuseUnresolvableToken(token: string): void {
|
|
1538
|
+
const tab = activeTab.value;
|
|
1539
|
+
if (!tab) {
|
|
1540
|
+
throw new RangeError(`command "insert.data" needs an open document; no tab is active`);
|
|
1541
|
+
}
|
|
1542
|
+
const root = tokenRoot(token);
|
|
1543
|
+
const state = (tab.doc.document.state ?? {}) as Record<string, unknown>;
|
|
1544
|
+
if (root === "state") {
|
|
1545
|
+
const name = token.split(".")[1] ?? "";
|
|
1546
|
+
if (!(name in state)) {
|
|
1547
|
+
const defined = Object.keys(state);
|
|
1548
|
+
throw new RangeError(
|
|
1549
|
+
`command "insert.data" argument "token": "${token}" names no state entry — this ` +
|
|
1550
|
+
`document defines: ${defined.length > 0 ? defined.join(", ") : "nothing"}`,
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
if (root === "item" || root === "index") {
|
|
1556
|
+
// The repeater scope exists only INSIDE a `map` template, and the caret's own doc path is what
|
|
1557
|
+
// Says whether it does — the same question `panels/block-action-bar.ts` asks before it offers
|
|
1558
|
+
// These tokens at all.
|
|
1559
|
+
const path = getEditSnapshot().snapshot?.path ?? primarySelection(tab.session.selection);
|
|
1560
|
+
if (!findEnclosingRepeater(tab.doc.document, path)) {
|
|
1561
|
+
throw new RangeError(
|
|
1562
|
+
`command "insert.data" argument "token": "${token}" is a repeater-scope token, and the ` +
|
|
1563
|
+
`caret is not inside a repeater template`,
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
throw new RangeError(
|
|
1569
|
+
`command "insert.data" argument "token": "${token}" is not an insertable token — a token ` +
|
|
1570
|
+
`reads "state.<name>", "item", "item.<field>" or "index"`,
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* The two verbs that address what the canvas pane is pointing AT — the element, and the caret.
|
|
1576
|
+
*
|
|
1577
|
+
* Both are defined beside the surface that DRAWS them rather than beside the fields that store
|
|
1578
|
+
* them: every consumer of `session.selection` — the overlay boxes, the Outline's selected row, the
|
|
1579
|
+
* Inspector, the block action bar — is downstream of this module's render, and both verbs must
|
|
1580
|
+
* guarantee that what they accept is something those surfaces can actually draw.
|
|
1581
|
+
*
|
|
1582
|
+
* `selection.set`: `path: []` is the document ROOT and is a legal selection (`selector-menu-shot`
|
|
1583
|
+
* uses it). `null` clears the selection. Anything else is resolved against the open document and
|
|
1584
|
+
* REFUSED when it addresses nothing — a stale path used to select a hole, and every panel
|
|
1585
|
+
* downstream then rendered its empty state while the shot recorded that as the truth.
|
|
1586
|
+
*
|
|
1587
|
+
* `insert.data`: the capability has shipped since the merge-tag menu landed and has been reachable
|
|
1588
|
+
* from that menu ALONE — `panels/block-action-bar.ts` posts an `insertData` intent straight to the
|
|
1589
|
+
* canvas bridge, so the palette, the keyboard, the AI and `__jxAutomation` could not do the one
|
|
1590
|
+
* thing the whole Insert-data affordance exists for. The screenshot manifest reached it by clicking
|
|
1591
|
+
* the toolbar button and then a row of the menu it opens, which is the shape §13.5 refuses on
|
|
1592
|
+
* principle: opening a menu to press an item names a CONTROL; the item is the command.
|
|
1593
|
+
*/
|
|
1594
|
+
export function selectionCommands(): AnyCommand[] {
|
|
1595
|
+
return [
|
|
1596
|
+
{
|
|
1597
|
+
args: argsSchema({
|
|
1598
|
+
path: pathProperty(
|
|
1599
|
+
"The document path to select — an array of keys and indexes. [] is the document " +
|
|
1600
|
+
"root; null clears the selection.",
|
|
1601
|
+
true,
|
|
1602
|
+
),
|
|
1603
|
+
}),
|
|
1604
|
+
category: "Selection",
|
|
1605
|
+
id: "selection.set",
|
|
1606
|
+
level: "document",
|
|
1607
|
+
menus: ["palette"],
|
|
1608
|
+
group: "2_navigate",
|
|
1609
|
+
requires: "an open document",
|
|
1610
|
+
when: (ctx) => ctx.document.open,
|
|
1611
|
+
aiTool: {
|
|
1612
|
+
description:
|
|
1613
|
+
"Select the element at a document path so the Inspector, the Outline and the canvas " +
|
|
1614
|
+
"overlay all address it. Pass null to clear the selection.",
|
|
1615
|
+
name: "select_node",
|
|
1616
|
+
},
|
|
1617
|
+
run: (_commandCtx, args) => {
|
|
1618
|
+
const path = nullablePathArg("selection.set", args, "path");
|
|
1619
|
+
const tab = activeTab.value;
|
|
1620
|
+
if (!tab) {
|
|
1621
|
+
throw new RangeError(`command "selection.set" needs an open document; no tab is active`);
|
|
1622
|
+
}
|
|
1623
|
+
if (path !== null && path.length > 0 && !getNodeAtPath(tab.doc.document, path)) {
|
|
1624
|
+
throw new RangeError(
|
|
1625
|
+
`command "selection.set" argument "path": [${path.join(", ")}] addresses no node in ` +
|
|
1626
|
+
`${tab.documentPath ?? "the open document"}`,
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
tab.session.selection = path === null ? [] : [path];
|
|
1630
|
+
},
|
|
1631
|
+
title: "Select Element",
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
args: argsSchema({
|
|
1635
|
+
paths: pathListProperty(
|
|
1636
|
+
"The document paths to select, in order — an array of paths. The LAST one becomes the " +
|
|
1637
|
+
"primary (what the Inspector edits); the first is the anchor a shift-range extends " +
|
|
1638
|
+
"from. [] selects nothing.",
|
|
1639
|
+
),
|
|
1640
|
+
}),
|
|
1641
|
+
category: "Selection",
|
|
1642
|
+
id: "selection.setPaths",
|
|
1643
|
+
level: "document",
|
|
1644
|
+
menus: ["palette"],
|
|
1645
|
+
group: "2_navigate",
|
|
1646
|
+
requires: "an open document",
|
|
1647
|
+
when: (ctx) => ctx.document.open,
|
|
1648
|
+
aiTool: {
|
|
1649
|
+
description:
|
|
1650
|
+
"Select several elements at once so one decision — a style paste, a delete, a duplicate " +
|
|
1651
|
+
"— applies to all of them as a single undoable step. Pass [] to select nothing.",
|
|
1652
|
+
name: "select_nodes",
|
|
1653
|
+
},
|
|
1654
|
+
/**
|
|
1655
|
+
* The idempotent SET for the whole selection, beside `selection.set`'s single path.
|
|
1656
|
+
*
|
|
1657
|
+
* There is deliberately no `selection.add` or `selection.toggle`: an accumulate verb names a
|
|
1658
|
+
* DELTA against state the caller cannot observe, which is the same objection that bans a
|
|
1659
|
+
* `toggle*` id without a `set*` beside it (§13). Ctrl-clicking twice is a gesture; the
|
|
1660
|
+
* command is always "the selection is now exactly these". `selection.set { path }` survives
|
|
1661
|
+
* unchanged because selecting one node is what almost every caller — every screenshot step,
|
|
1662
|
+
* and the assistant — actually does.
|
|
1663
|
+
*/
|
|
1664
|
+
run: (_commandCtx, args) => {
|
|
1665
|
+
const paths = pathListArg("selection.setPaths", args, "paths");
|
|
1666
|
+
const tab = activeTab.value;
|
|
1667
|
+
if (!tab) {
|
|
1668
|
+
throw new RangeError(
|
|
1669
|
+
`command "selection.setPaths" needs an open document; no tab is active`,
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
for (const path of paths) {
|
|
1673
|
+
if (path.length > 0 && !getNodeAtPath(tab.doc.document, path)) {
|
|
1674
|
+
throw new RangeError(
|
|
1675
|
+
`command "selection.setPaths" argument "paths": [${path.join(", ")}] addresses no ` +
|
|
1676
|
+
`node in ${tab.documentPath ?? "the open document"}`,
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
// Deduplicated, so running the same list twice lands on the same anchor and the same
|
|
1681
|
+
// Primary — the property that makes it photographable.
|
|
1682
|
+
tab.session.selection = uniquePaths(paths);
|
|
1683
|
+
},
|
|
1684
|
+
title: "Select Elements",
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
args: argsSchema({
|
|
1688
|
+
token: stringProperty(
|
|
1689
|
+
'The token to insert, without the ${…} wrapper — "state.<name>", "item", ' +
|
|
1690
|
+
'"item.<field>" or "index".',
|
|
1691
|
+
),
|
|
1692
|
+
}),
|
|
1693
|
+
category: "Insert",
|
|
1694
|
+
id: "insert.data",
|
|
1695
|
+
level: "selection",
|
|
1696
|
+
keyScope: "caret",
|
|
1697
|
+
// NOT `blockbar`: the bar's Insert-data control opens a PICKER, and a picker cannot be the
|
|
1698
|
+
// Rendering of a record that requires a token. The record is what the picker's rows run.
|
|
1699
|
+
menus: ["palette"],
|
|
1700
|
+
group: "5_data",
|
|
1701
|
+
undo: "document",
|
|
1702
|
+
requires: "a live text caret in the canvas",
|
|
1703
|
+
/* `inCanvas`, not `active`. `caret.active` is also true while focus is in a parent-realm text
|
|
1704
|
+
field (`commands/live-context.ts` folds both, because the SCOPE STACK wants them folded),
|
|
1705
|
+
and inserting a data token into the Inspector's href field is not a thing this verb can do
|
|
1706
|
+
— `postApplyFormat` would no-op against a frame with no session and the palette row would
|
|
1707
|
+
read as available. The `format.*` family is gated the same way, for the same reason. */
|
|
1708
|
+
when: (ctx) => ctx.caret.inCanvas,
|
|
1709
|
+
aiTool: {
|
|
1710
|
+
description:
|
|
1711
|
+
"Insert a live data placeholder at the text caret, binding that run of text to a state " +
|
|
1712
|
+
"entry or, inside a repeater template, to the current item.",
|
|
1713
|
+
name: "insert_data_token",
|
|
1714
|
+
},
|
|
1715
|
+
run: (_commandCtx, args) => {
|
|
1716
|
+
const token = stringArg("insert.data", args, "token");
|
|
1717
|
+
refuseUnresolvableToken(token);
|
|
1718
|
+
postApplyFormat({ command: "insertData", token });
|
|
1719
|
+
},
|
|
1720
|
+
title: "Insert Data",
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
category: "Insert",
|
|
1724
|
+
/**
|
|
1725
|
+
* The slash menu's door, and for two phases its ONLY working one.
|
|
1726
|
+
*
|
|
1727
|
+
* "/" at the caret is the gesture the docs describe and §10 documents, and it was recognised
|
|
1728
|
+
* in a `keydown` listener bound to the BLOCK — while the editing host is the canvas
|
|
1729
|
+
* container, so the listener never fired and typing "/" simply inserted a slash. The gesture
|
|
1730
|
+
* is fixed where it belongs (`canvas/iframe-inline-edit.ts` listens at the host), and this
|
|
1731
|
+
* record is the half that gesture never had: a name, so the palette, a rebound chord, the
|
|
1732
|
+
* automation runner and the screenshot manifest can all reach the same menu.
|
|
1733
|
+
*
|
|
1734
|
+
* Opened this way the menu has no "/" in the document to filter against, so the frame gives
|
|
1735
|
+
* it its own filter field and selecting a block deletes nothing — see `openSlashMenu`.
|
|
1736
|
+
*/
|
|
1737
|
+
id: "insert.openSlashMenu",
|
|
1738
|
+
level: "selection",
|
|
1739
|
+
keyScope: "caret",
|
|
1740
|
+
// No `blockbar`: the bar already carries the tag badge, which opens the same list for a
|
|
1741
|
+
// CONVERSION. A second control for "insert" beside it would be two buttons over one menu.
|
|
1742
|
+
menus: ["palette"],
|
|
1743
|
+
group: "5_data",
|
|
1744
|
+
undo: "document",
|
|
1745
|
+
requires: "a live text caret in the canvas",
|
|
1746
|
+
when: (ctx) => ctx.caret.inCanvas,
|
|
1747
|
+
run: () => {
|
|
1748
|
+
postOpenSlash();
|
|
1749
|
+
},
|
|
1750
|
+
title: "Insert Block…",
|
|
1751
|
+
},
|
|
1752
|
+
];
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* Register the canvas pane's addressing verbs. Called from the bootstrap.
|
|
1757
|
+
*
|
|
1758
|
+
* @param {CommandRegistry} registry
|
|
1759
|
+
*/
|
|
1760
|
+
export function registerSelectionSetCommand(registry: CommandRegistry): void {
|
|
1761
|
+
registry.registerAll(selectionCommands());
|
|
1762
|
+
}
|