@jxsuite/studio 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/canvas.html +21 -0
- package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
- package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
- package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
- package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
- package/dist/chunks/{components-799jpvhm.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-799jpvhm.js.map → components-2bpqp4mq.js.map} +1 -1
- package/dist/chunks/content-source-34gnkbnn.js +38 -0
- package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
- package/dist/chunks/extension-sections-6mqydz35.js +301 -0
- package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
- package/dist/chunks/files-8kn321h1.js +70 -0
- package/dist/chunks/files-8kn321h1.js.map +9 -0
- package/dist/chunks/{format-host-8jxwts2p.js → format-host-mkwsmt8d.js} +3 -3
- package/dist/chunks/git-panel-tjbr191d.js +51 -0
- package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
- package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
- package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
- package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
- package/dist/chunks/layers-panel-87pb25km.js +58 -0
- package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
- package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
- package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
- package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
- package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
- package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
- package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
- package/dist/chunks/right-panel-zpdxd98t.js +37 -0
- package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
- package/dist/chunks/section-registry-zmckaxf4.js +30 -0
- package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
- package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
- package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
- package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
- package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
- package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
- package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
- package/dist/chunks/studio-41exgzhq.js +6690 -0
- package/dist/chunks/studio-41exgzhq.js.map +38 -0
- package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
- package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
- package/dist/chunks/studio-50k77cca.js +1787 -0
- package/dist/chunks/studio-50k77cca.js.map +18 -0
- package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
- package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
- package/dist/chunks/studio-6f0am8f2.js +53 -0
- package/dist/chunks/studio-6f0am8f2.js.map +10 -0
- package/dist/chunks/studio-6nmjc5qk.js +7002 -0
- package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
- package/dist/chunks/studio-7afd21nw.js +67 -0
- package/dist/chunks/studio-7afd21nw.js.map +10 -0
- package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
- package/dist/chunks/studio-7bmdvzne.js.map +10 -0
- package/dist/chunks/studio-8jpzqk5s.js +128 -0
- package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
- package/dist/chunks/studio-94m77tgk.js +1482 -0
- package/dist/chunks/studio-94m77tgk.js.map +13 -0
- package/dist/chunks/studio-9je6w5sq.js +1000 -0
- package/dist/chunks/studio-9je6w5sq.js.map +16 -0
- package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
- package/dist/chunks/{studio-gs6kqdt1.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-gs6kqdt1.js.map → studio-adn0z1k3.js.map} +3 -3
- package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
- package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
- package/dist/chunks/studio-bw5dz6km.js +608 -0
- package/dist/chunks/studio-bw5dz6km.js.map +21 -0
- package/dist/chunks/studio-c6s5getc.js +4232 -0
- package/dist/chunks/studio-c6s5getc.js.map +39 -0
- package/dist/chunks/studio-cjm7fdjf.js +451 -0
- package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
- package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
- package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
- package/dist/chunks/studio-dg7zyt5x.js +28 -0
- package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
- package/dist/chunks/studio-dzcyvvvz.js +166 -0
- package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
- package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
- package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
- package/dist/chunks/studio-f12h6177.js +411 -0
- package/dist/chunks/studio-f12h6177.js.map +13 -0
- package/dist/chunks/studio-hbp1s8m9.js +1817 -0
- package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
- package/dist/chunks/studio-k2sxjph5.js +15295 -0
- package/dist/chunks/studio-k2sxjph5.js.map +235 -0
- package/dist/chunks/studio-myje51t3.js +385 -0
- package/dist/chunks/studio-myje51t3.js.map +12 -0
- package/dist/chunks/studio-na6rwzhw.js +353 -0
- package/dist/chunks/studio-na6rwzhw.js.map +11 -0
- package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
- package/dist/chunks/studio-p471gg5n.js.map +14 -0
- package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
- package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
- package/dist/chunks/{studio-esrewxap.js → studio-s519zc99.js} +3 -3
- package/dist/chunks/studio-sas7mt3k.js +75 -0
- package/dist/chunks/studio-sas7mt3k.js.map +10 -0
- package/dist/chunks/studio-t5wqwzdh.js +74 -0
- package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
- package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
- package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
- package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
- package/dist/chunks/studio-tr20m8g8.js.map +9 -0
- package/dist/chunks/{studio-v7jkjqng.js → studio-v96qfnww.js} +5514 -4291
- package/dist/chunks/studio-v96qfnww.js.map +54 -0
- package/dist/chunks/studio-vcetc91k.js +659 -0
- package/dist/chunks/studio-vcetc91k.js.map +10 -0
- package/dist/chunks/{studio-jm0z592w.js → studio-vmymjefp.js} +2972 -4937
- package/dist/chunks/studio-vmymjefp.js.map +98 -0
- package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
- package/dist/chunks/studio-x9hmy5kr.js +390 -0
- package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
- package/dist/chunks/studio-xa0ce0n7.js +335 -0
- package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
- package/dist/chunks/studio-xm3dbxvn.js +378 -0
- package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
- package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
- package/dist/chunks/studio-zv37yrfa.js +1054 -0
- package/dist/chunks/studio-zv37yrfa.js.map +13 -0
- package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
- package/dist/chunks/ws-client-654ts66j.js.map +11 -0
- package/dist/iframe-entry.js +668 -230
- package/dist/studio.js +21080 -32585
- package/index.html +35 -4685
- package/package.json +17 -15
- package/src/about/about-modal.ts +42 -11
- package/src/browse/library-commands.ts +240 -0
- package/src/browse/library-layouts.ts +369 -0
- package/src/browse/library-model.ts +396 -0
- package/src/browse/library-pane.ts +1049 -0
- package/src/browse/library-preview.ts +252 -0
- package/src/browse/library-source.ts +150 -0
- package/src/canvas/canvas-helpers.ts +33 -9
- package/src/canvas/canvas-live-render.ts +45 -29
- package/src/canvas/canvas-patcher.ts +151 -31
- package/src/canvas/canvas-perf.ts +46 -7
- package/src/canvas/canvas-render.ts +1092 -220
- package/src/canvas/canvas-surface.ts +240 -0
- package/src/canvas/canvas-utils.ts +1166 -124
- package/src/canvas/iframe-drop.ts +2 -1
- package/src/canvas/iframe-entry.ts +241 -4
- package/src/canvas/iframe-host.ts +1226 -143
- package/src/canvas/iframe-inline-edit.ts +31 -0
- package/src/canvas/iframe-interaction.ts +76 -16
- package/src/canvas/iframe-keys.ts +107 -45
- package/src/canvas/iframe-overlay.ts +38 -1
- package/src/canvas/iframe-protocol.ts +137 -2
- package/src/canvas/iframe-render.ts +70 -2
- package/src/canvas/iframe-slash.ts +6 -0
- package/src/canvas/path-mapping.ts +56 -6
- package/src/canvas/surface-registry.ts +292 -0
- package/src/collab/collab-commands.ts +244 -0
- package/src/collab/collab-session.ts +198 -32
- package/src/collab/collab-state.ts +28 -2
- package/src/collab/presence-chips.ts +100 -9
- package/src/commands/active-registry.ts +42 -0
- package/src/commands/app-commands.ts +176 -0
- package/src/commands/budget.ts +158 -0
- package/src/commands/command-args.ts +253 -0
- package/src/commands/context.ts +387 -0
- package/src/commands/defaults.ts +580 -0
- package/src/commands/keymap.ts +571 -0
- package/src/commands/levels.ts +345 -0
- package/src/commands/live-context.ts +193 -0
- package/src/commands/reference.ts +227 -0
- package/src/commands/registry.ts +293 -0
- package/src/component-props.ts +3 -2
- package/src/content/draft-state.ts +106 -0
- package/src/content/entry-commands.ts +282 -0
- package/src/content/entry-editor.ts +327 -0
- package/src/content/entry-fields.ts +116 -0
- package/src/content/entry-model.ts +175 -0
- package/src/editor/canvas-context-menu.ts +17 -11
- package/src/editor/canvas-slash-bridge.ts +11 -4
- package/src/editor/context-menu.ts +804 -204
- package/src/editor/convert-to-component.ts +31 -10
- package/src/editor/convert-to-repeater.ts +3 -2
- package/src/editor/inline-edit-apply.ts +5 -5
- package/src/editor/inline-edit.ts +86 -50
- package/src/editor/insert-zone-action.ts +1 -1
- package/src/editor/shortcuts.ts +1043 -329
- package/src/editor/slash-menu.ts +1 -0
- package/src/files/elements.ts +166 -0
- package/src/files/file-ops.ts +178 -31
- package/src/files/files.ts +898 -268
- package/src/files/fs-events.ts +21 -0
- package/src/files/media-meta.ts +203 -0
- package/src/files/media-paths.ts +141 -0
- package/src/files/media-upload.ts +7 -3
- package/src/files/media-usage.ts +189 -0
- package/src/github/github-auth.ts +235 -33
- package/src/github/github-publish.ts +80 -21
- package/src/grid/grid-controller.ts +152 -12
- package/src/grid/grid-layout.ts +225 -20
- package/src/grid/grid-open.ts +120 -0
- package/src/grid/grid-panel.ts +554 -39
- package/src/grid/grid-source.ts +27 -1
- package/src/grid/redirects-grid.ts +599 -0
- package/src/grid/redirects.ts +398 -0
- package/src/grid/sources/content-source.ts +108 -2
- package/src/i18n/i18n-commands.ts +322 -0
- package/src/new-project/add-repo-modal.ts +8 -10
- package/src/new-project/import-tab.ts +7 -0
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +221 -245
- package/src/packages/jxsuite-update.ts +97 -48
- package/src/packages/pull-package-sync.ts +18 -9
- package/src/page-params.ts +2 -1
- package/src/panels/activity-bar.ts +152 -91
- package/src/panels/activity-panel.ts +481 -0
- package/src/panels/ai-chat/chat-markdown.ts +8 -3
- package/src/panels/ai-chat/chat-view.ts +245 -18
- package/src/panels/ai-chat/composer.ts +31 -5
- package/src/panels/ai-chat/sessions-view.ts +17 -10
- package/src/panels/ai-panel.ts +279 -53
- package/src/panels/block-action-bar.ts +1115 -224
- package/src/panels/bottom-dock.ts +337 -0
- package/src/panels/canvas-dnd-bridge.ts +37 -8
- package/src/panels/chat-panel.ts +29 -12
- package/src/panels/data-explorer.ts +254 -112
- package/src/panels/data-grid.ts +10 -4
- package/src/panels/dnd.ts +28 -51
- package/src/panels/editors.ts +434 -107
- package/src/panels/elements-panel.ts +66 -2
- package/src/panels/empty-state.ts +112 -0
- package/src/panels/events-panel.ts +596 -82
- package/src/panels/formula-workspace.ts +408 -218
- package/src/panels/frontmatter-fields.ts +51 -10
- package/src/panels/frontmatter-panel.ts +357 -79
- package/src/panels/git-panel.ts +375 -164
- package/src/panels/head-panel.ts +791 -85
- package/src/panels/i18n-panel.ts +592 -0
- package/src/panels/imports-panel.ts +95 -85
- package/src/panels/jump-bar.ts +584 -0
- package/src/panels/layers-panel.ts +1106 -328
- package/src/panels/left-panel.ts +109 -204
- package/src/panels/navigator-panels.ts +131 -0
- package/src/panels/overlays.ts +7 -2
- package/src/panels/pane-context.ts +1477 -0
- package/src/panels/pane-grid.ts +406 -0
- package/src/panels/panel-registry.ts +340 -0
- package/src/panels/panel-scheduler.ts +75 -0
- package/src/panels/problems-panel.ts +234 -0
- package/src/panels/properties-panel.ts +946 -1139
- package/src/panels/provenance.ts +258 -0
- package/src/panels/quick-search.ts +791 -122
- package/src/panels/right-panel.ts +228 -102
- package/src/panels/seo-modal.ts +395 -0
- package/src/panels/settings-pane.ts +203 -0
- package/src/panels/signals-panel.ts +354 -137
- package/src/panels/statement-editor.ts +41 -25
- package/src/panels/statusbar.ts +342 -106
- package/src/panels/style-panel.ts +974 -463
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-doc.ts +4 -2
- package/src/panels/stylebook-layers-panel.ts +3 -2
- package/src/panels/stylebook-panel.ts +51 -26
- package/src/panels/tab-strip.ts +1058 -78
- package/src/panels/target-line.ts +274 -0
- package/src/panels/toolbar.ts +631 -484
- package/src/panels/welcome-screen.ts +301 -198
- package/src/platform-errors.ts +26 -3
- package/src/platform.ts +96 -2
- package/src/platforms/cloud.ts +71 -8
- package/src/platforms/devserver.ts +92 -29
- package/src/publish/deploy-checklist.ts +383 -0
- package/src/publish/pages-service.ts +10 -1
- package/src/publish/publish-commands.ts +218 -0
- package/src/publish/publish-panel.ts +110 -16
- package/src/recent-projects.ts +3 -2
- package/src/services/a11y-report.ts +505 -0
- package/src/services/ai-models.ts +30 -1
- package/src/services/ai-project-tools.ts +29 -10
- package/src/services/ai-system-prompt.ts +53 -14
- package/src/services/ai-tools.ts +39 -3
- package/src/services/ai-writes.ts +154 -0
- package/src/services/announce.ts +97 -0
- package/src/services/automation.ts +424 -175
- package/src/services/cem-export.ts +9 -4
- package/src/services/clock.ts +61 -0
- package/src/services/document-assistant.ts +36 -8
- package/src/services/idle.ts +179 -0
- package/src/services/import-client.ts +26 -0
- package/src/services/monaco-buffer.ts +627 -0
- package/src/services/monaco-lazy.ts +36 -0
- package/src/services/notify.ts +271 -0
- package/src/services/profile.ts +155 -0
- package/src/services/references.ts +0 -0
- package/src/services/token-lint.ts +3 -2
- package/src/services/tool-executor.ts +53 -7
- package/src/services/trusted-types.ts +123 -0
- package/src/settings/contexts-section.ts +427 -0
- package/src/settings/contributed-section.ts +149 -11
- package/src/settings/css-vars-editor.ts +306 -349
- package/src/settings/defs-editor.ts +93 -34
- package/src/settings/dependencies-editor.ts +7 -7
- package/src/settings/extension-sections.ts +37 -5
- package/src/settings/general-settings.ts +204 -121
- package/src/settings/locales-section.ts +347 -0
- package/src/settings/preferences-accounts.ts +140 -0
- package/src/settings/preferences-dialog.ts +620 -0
- package/src/settings/preferences-keymap.ts +270 -0
- package/src/settings/project-sections.ts +260 -0
- package/src/settings/section-registry.ts +169 -0
- package/src/settings/settings-document.ts +385 -0
- package/src/shell.ts +1549 -0
- package/src/site-context.ts +45 -20
- package/src/state.ts +14 -275
- package/src/store.ts +88 -43
- package/src/studio.ts +790 -346
- package/src/style/live-preview.ts +36 -0
- package/src/style/project-styles.ts +349 -0
- package/src/style/token-ref.ts +123 -0
- package/src/tabs/patch-ops.ts +16 -4
- package/src/tabs/project-config.ts +416 -0
- package/src/tabs/selection.ts +323 -0
- package/src/tabs/tab.ts +182 -47
- package/src/tabs/transact.ts +209 -28
- package/src/types.ts +68 -13
- package/src/ui/ai-credentials-form.ts +39 -35
- package/src/ui/ai-managed-connect.ts +7 -1
- package/src/ui/dynamic-slot.ts +224 -81
- package/src/ui/expression-editor.ts +71 -63
- package/src/ui/field-input.ts +4 -6
- package/src/ui/field-row.ts +215 -38
- package/src/ui/form-controls.ts +194 -7
- package/src/ui/layers.ts +492 -69
- package/src/ui/media-picker.ts +115 -25
- package/src/ui/panel-resize.ts +109 -91
- package/src/ui/progress-modal.ts +131 -50
- package/src/ui/regions.ts +462 -0
- package/src/ui/schema-form.ts +229 -45
- package/src/ui/spectrum.ts +24 -8
- package/src/ui/timing.ts +49 -0
- package/src/ui/value-selector.ts +20 -0
- package/src/ui/value-source.ts +348 -0
- package/src/ui/virtual-window.ts +400 -0
- package/src/utils/edit-display.ts +3 -2
- package/src/utils/inherited-style.ts +67 -37
- package/src/view.ts +64 -78
- package/src/workspace/pane-derive.ts +1258 -0
- package/src/workspace/session.ts +317 -0
- package/src/workspace/workspace.ts +1345 -23
- package/styles/canvas.css +653 -0
- package/styles/forced-colors.css +106 -0
- package/styles/inspector.css +1454 -0
- package/styles/overlays.css +2704 -0
- package/styles/panels.css +1701 -0
- package/styles/shell.css +1423 -0
- package/styles/tokens.css +151 -0
- package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
- package/dist/chunks/extension-sections-qr3drkkw.js +0 -531
- package/dist/chunks/extension-sections-qr3drkkw.js.map +0 -12
- package/dist/chunks/files-j8a1t06k.js +0 -50
- package/dist/chunks/files-j8a1t06k.js.map +0 -9
- package/dist/chunks/git-panel-xtq5z81x.js +0 -28
- package/dist/chunks/git-panel-xtq5z81x.js.map +0 -9
- package/dist/chunks/layers-panel-ancn2wpq.js +0 -26
- package/dist/chunks/layers-panel-ancn2wpq.js.map +0 -9
- package/dist/chunks/settings-modal-17ttr96g.js +0 -29
- package/dist/chunks/settings-modal-17ttr96g.js.map +0 -9
- package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
- package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
- package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
- package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
- package/dist/chunks/studio-0xgs7xrs.js +0 -429
- package/dist/chunks/studio-0xgs7xrs.js.map +0 -13
- package/dist/chunks/studio-4d8hzcz5.js +0 -3427
- package/dist/chunks/studio-4d8hzcz5.js.map +0 -26
- package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-jm0z592w.js.map +0 -115
- package/dist/chunks/studio-m2t4mdg5.js +0 -1854
- package/dist/chunks/studio-m2t4mdg5.js.map +0 -18
- package/dist/chunks/studio-n4p11dfy.js +0 -853
- package/dist/chunks/studio-n4p11dfy.js.map +0 -12
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-sjh1wt1a.js +0 -3744
- package/dist/chunks/studio-sjh1wt1a.js.map +0 -22
- package/dist/chunks/studio-v7jkjqng.js.map +0 -46
- package/dist/chunks/studio-xq0t7n5t.js +0 -140
- package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
- package/dist/chunks/studio-xze4zrs1.js.map +0 -10
- package/dist/chunks/studio-yfcstdzy.js +0 -399
- package/dist/chunks/studio-yfcstdzy.js.map +0 -12
- package/dist/chunks/studio-zp7jncs8.js +0 -469
- package/dist/chunks/studio-zp7jncs8.js.map +0 -14
- package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
- package/src/browse/browse-modal.ts +0 -69
- package/src/browse/browse.ts +0 -904
- package/src/canvas/canvas-diff.ts +0 -216
- package/src/canvas/nested-site-style.ts +0 -58
- package/src/new-project/design-fields.ts +0 -264
- package/src/new-project/templates.ts +0 -61
- package/src/panels/tab-bar.ts +0 -548
- package/src/settings/settings-modal.ts +0 -203
- /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
- /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
- /package/dist/chunks/{format-host-8jxwts2p.js.map → format-host-mkwsmt8d.js.map} +0 -0
- /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
- /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
- /package/dist/chunks/{studio-esrewxap.js.map → studio-s519zc99.js.map} +0 -0
- /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
- /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
package/src/shell.ts
ADDED
|
@@ -0,0 +1,1549 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Shell — the reactive record of Jx Studio's UI _inputs_.
|
|
4
|
+
*
|
|
5
|
+
* Companion to `view.ts`, and the other half of the split that `view.ts` used to straddle:
|
|
6
|
+
*
|
|
7
|
+
* - `view` holds render **outputs** — Monaco instances, the panzoom wrap, observers, DOM refs and the
|
|
8
|
+
* per-render cleanup arrays. It stays a plain object, because wrapping a live editor instance and
|
|
9
|
+
* detached DOM nodes in a reactive proxy is an active hazard.
|
|
10
|
+
* - `shell` (here) holds UI **inputs** — which panel each dock shows, whether each dock is open and
|
|
11
|
+
* how wide, the focused region, the layout preset, and the project-level state (source control,
|
|
12
|
+
* the stylebook selection, the settings tab) that a _document_ must not own.
|
|
13
|
+
*
|
|
14
|
+
* Because this record is reactive, a surface tracks it by reading it: the rail, the left panel and
|
|
15
|
+
* the toolbar all repaint from the effects they already run. The stand-in that preceded this —
|
|
16
|
+
* `onPanelCollapse(fn)` plus an explicit `render()` in every toggle handler — is deleted.
|
|
17
|
+
*
|
|
18
|
+
* **Project-level state does not live on the focused tab.** Git status is a property of the
|
|
19
|
+
* project, not of whichever document happens to be open: sourcing it from `activeTab` meant the
|
|
20
|
+
* rail's Source Control badge vanished when the last tab closed, and two tabs could disagree about
|
|
21
|
+
* the branch. See specs/studio.md and the level × placement matrix.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { effect, effectScope, reactive } from "./reactivity";
|
|
25
|
+
import { applyStartupProfile } from "./services/profile";
|
|
26
|
+
import { stampShellRegions } from "./ui/regions";
|
|
27
|
+
import { workspace } from "./workspace/workspace";
|
|
28
|
+
import { captureSession, readSession } from "./workspace/session";
|
|
29
|
+
import type { PersistedSession } from "./workspace/session";
|
|
30
|
+
import {
|
|
31
|
+
argsSchema,
|
|
32
|
+
booleanArg,
|
|
33
|
+
booleanProperty,
|
|
34
|
+
enumArg,
|
|
35
|
+
enumProperty,
|
|
36
|
+
stringArg,
|
|
37
|
+
stringProperty,
|
|
38
|
+
} from "./commands/command-args";
|
|
39
|
+
import type { EffectScope } from "@vue/reactivity";
|
|
40
|
+
import type { LayoutHit } from "./canvas/iframe-protocol";
|
|
41
|
+
import type { GitBranchesResult, GitDiffState, GitStatusResult } from "./types";
|
|
42
|
+
import type { AnyCommand, CommandRegistry } from "./commands/registry";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The canvas selection when the author clicked LAYOUT chrome rather than page content — a header, a
|
|
46
|
+
* footer, anything contributed by the layout file. It is deliberately NOT a document selection: the
|
|
47
|
+
* node is not in the open page at all, so the properties panel shows the read-only layout panel
|
|
48
|
+
* (with "Open Layout →") instead of the element inspector. See {@link setLayoutSelection}.
|
|
49
|
+
*/
|
|
50
|
+
export type LayoutSelection = LayoutHit;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The three resizable docks around the pane grid: two flanking it, one under it.
|
|
54
|
+
*
|
|
55
|
+
* `"chat"` is not among them: the assistant is the Inspector's fourth TAB, not a dock, so it has no
|
|
56
|
+
* column, no size, no collapse flag and no resize handle of its own — which is the whole claim of
|
|
57
|
+
* plan §3.2 ⑨, that folding it in costs zero additional width. What selects it is
|
|
58
|
+
* {@link INSPECTOR_TAB_IDS}, the same mechanism that selects Content, Style and Logic.
|
|
59
|
+
*
|
|
60
|
+
* `"bottom"` is the one that arrives with P4.2. It is a dock like the other two — a collapse flag,
|
|
61
|
+
* a remembered size, a resize handle, a place in every {@link LayoutPreset} — and the reason
|
|
62
|
+
* `view.toggleBottomDock` had no idempotent setter for two phases is that it was not on this
|
|
63
|
+
* record. It sits under the PANE GRID rather than under the window (plan §3.2 ⑪), so opening it
|
|
64
|
+
* never narrows the Navigator or the Inspector.
|
|
65
|
+
*/
|
|
66
|
+
export type DockId = "left" | "right" | "bottom";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The ids of the panels the NAVIGATOR DOCK draws, in REGISTRATION order.
|
|
70
|
+
*
|
|
71
|
+
* This is the DECLARATION `view.setActivity`'s `args` enum is built from, and it is the reason a
|
|
72
|
+
* panel rename fails `scripts/check-shot-contract.ts` in the renaming PR naming both ids instead of
|
|
73
|
+
* silently photographing the wrong panel (plan §13.5). `shell.leftTab` stays a `string` because it
|
|
74
|
+
* is also read from persisted state written by an older build — the COMMAND is what refuses an
|
|
75
|
+
* undeclared id, at the one door a caller comes through, and {@link migratePanelId} is what turns a
|
|
76
|
+
* stale stored id into a current one before it gets there.
|
|
77
|
+
*
|
|
78
|
+
* **`"problems"` is not here, and it has no rail button either.** It is hosted in the Bottom dock,
|
|
79
|
+
* so `view.setBottomTab { tab: "problems" }` is the verb that shows it and this enum would be a
|
|
80
|
+
* second, wrong door. That verb is now the ONLY door: the status bar's warning count runs it, and
|
|
81
|
+
* `panel.focus.problems` no longer exists, because the roster it was generated from follows the
|
|
82
|
+
* rail and Problems is off the rail.
|
|
83
|
+
*
|
|
84
|
+
* The records themselves live in `panels/navigator-panels.ts`, one per owning module, and that
|
|
85
|
+
* file's registration order is this one — which is rail order within each level group, with a
|
|
86
|
+
* rail-less record sitting where its module registers it rather than at the end. Languages is the
|
|
87
|
+
* one that makes the difference visible: it is registered with the PROJECT group, beside the panels
|
|
88
|
+
* it belongs with in the palette, and it draws no rail button at all.
|
|
89
|
+
*
|
|
90
|
+
* This list is the same set written down in a module that imports no DOM, because
|
|
91
|
+
* `commands/app-commands.ts` must load the enum in a bare Bun process;
|
|
92
|
+
* `tests/panel-registry.test.ts` asserts the two agree, so the duplicate cannot drift.
|
|
93
|
+
*/
|
|
94
|
+
export const NAVIGATOR_PANEL_IDS = [
|
|
95
|
+
"files",
|
|
96
|
+
"search",
|
|
97
|
+
"git",
|
|
98
|
+
"i18n",
|
|
99
|
+
"layers",
|
|
100
|
+
"page",
|
|
101
|
+
"data",
|
|
102
|
+
"packages",
|
|
103
|
+
"insert",
|
|
104
|
+
] as const;
|
|
105
|
+
|
|
106
|
+
export type NavigatorPanelId = (typeof NAVIGATOR_PANEL_IDS)[number];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Panel ids a previous build persisted, and what they are called now.
|
|
110
|
+
*
|
|
111
|
+
* Three renames, each fixing a name that described a mechanism rather than a task: `blocks` was an
|
|
112
|
+
* insert palette, `head` was an HTML element, `imports` was the verb and not the thing. A stored
|
|
113
|
+
* value is the one place those old strings can still arrive from, so they are translated once, on
|
|
114
|
+
* read, and then never again — there is no alias in the registry and no branch in the rail.
|
|
115
|
+
*/
|
|
116
|
+
const RENAMED_PANEL_IDS: Readonly<Record<string, NavigatorPanelId>> = {
|
|
117
|
+
blocks: "insert",
|
|
118
|
+
head: "page",
|
|
119
|
+
imports: "packages",
|
|
120
|
+
/* Not a rename — a MERGE. The State editor is part of the Data panel now (plan §11.2), so a
|
|
121
|
+
stored `leftTab: "state"` names a real place and lands on it, rather than returning `null` and
|
|
122
|
+
booting into the default the way `problems` does. Defining a value and watching it resolve are
|
|
123
|
+
one task; they were two panels. */
|
|
124
|
+
state: "data",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** The panel the Navigator wakes up on when nothing usable is stored. */
|
|
128
|
+
export const DEFAULT_PANEL_ID: NavigatorPanelId = "layers";
|
|
129
|
+
|
|
130
|
+
/** Whether a string is a declared panel id. */
|
|
131
|
+
export function isNavigatorPanelId(value: unknown): value is NavigatorPanelId {
|
|
132
|
+
return typeof value === "string" && (NAVIGATOR_PANEL_IDS as readonly string[]).includes(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Narrow an id arriving from a `string`-typed roster, or throw naming where it came from.
|
|
137
|
+
*
|
|
138
|
+
* The lock on the door {@link NAVIGATOR_PANEL_IDS} describes, in one place so the rail and the ⌘1–8
|
|
139
|
+
* dispatcher cannot disagree about what an undeclared id means. Both receive their id from
|
|
140
|
+
* `PanelRecord.id`, which is deliberately a `string` — the registry hosts the Bottom dock's panels
|
|
141
|
+
* too, and their ids are {@link BOTTOM_TAB_IDS}.
|
|
142
|
+
*
|
|
143
|
+
* It throws rather than returning null because `tests/panel-registry.test.ts` asserts the enum and
|
|
144
|
+
* the registry agree, so a failure here means the two have drifted — and the symptom of the last
|
|
145
|
+
* drift was a control that silently did nothing.
|
|
146
|
+
*/
|
|
147
|
+
export function requireNavigatorPanelId(value: string, source: string): NavigatorPanelId {
|
|
148
|
+
if (!isNavigatorPanelId(value)) {
|
|
149
|
+
throw new Error(`${source}: "${value}" is not a declared Navigator panel id.`);
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Translate a stored panel id into a current one, or `null` when it names nothing.
|
|
156
|
+
*
|
|
157
|
+
* `null` rather than a default, so the caller decides: the boot path falls back to
|
|
158
|
+
* {@link DEFAULT_PANEL_ID}, and a test can tell "I stored junk" from "I stored `layers`".
|
|
159
|
+
*
|
|
160
|
+
* `"problems"` is the one id that returns `null` because it MOVED rather than because it was junk:
|
|
161
|
+
* a build before §7.2's correction could persist `leftTab: "problems"`, and the Navigator can no
|
|
162
|
+
* longer show it. There is no alias to add — Problems is not a Navigator panel under any name — so
|
|
163
|
+
* the shell boots into {@link DEFAULT_PANEL_ID} and the Bottom dock keeps its own `bottomTab`,
|
|
164
|
+
* which already defaults to Problems. This is exactly the wedge this function exists to prevent.
|
|
165
|
+
*/
|
|
166
|
+
export function migratePanelId(stored: unknown): NavigatorPanelId | null {
|
|
167
|
+
if (typeof stored !== "string") {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const renamed = RENAMED_PANEL_IDS[stored];
|
|
171
|
+
if (renamed) {
|
|
172
|
+
return renamed;
|
|
173
|
+
}
|
|
174
|
+
return isNavigatorPanelId(stored) ? stored : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The Inspector's tab ids, in strip order — the `value`s `panels/right-panel.ts` renders.
|
|
179
|
+
*
|
|
180
|
+
* Four now, and `"assistant"` is one of them: the chat is a tab of this dock, so the id space that
|
|
181
|
+
* addresses Content, Style and Logic addresses it too. The old arrangement needed a fourth verb
|
|
182
|
+
* (`view.setAssistant`) precisely because the assistant was somewhere else; it survives as the
|
|
183
|
+
* dock-toggle spelling of "show me the assistant", but it now writes the same field as the others.
|
|
184
|
+
*
|
|
185
|
+
* The ORDER is §3.2 ⑨'s — Content · Style · Logic · Assistant — which is the order ⌘⇧1–4 follow.
|
|
186
|
+
* The human titles live in `commands/defaults.ts`'s `INSPECTOR_TABS` (a module with no state and no
|
|
187
|
+
* DOM, so the CI checks can load it); `tests/right-panel.test.ts` asserts the two agree, the same
|
|
188
|
+
* way `tests/panel-registry.test.ts` does for the rail.
|
|
189
|
+
*/
|
|
190
|
+
export const INSPECTOR_TAB_IDS = ["properties", "style", "events", "assistant"] as const;
|
|
191
|
+
|
|
192
|
+
export type InspectorTabId = (typeof INSPECTOR_TAB_IDS)[number];
|
|
193
|
+
|
|
194
|
+
/** The tab the Inspector falls back to — the leftmost, Content. */
|
|
195
|
+
export const DEFAULT_INSPECTOR_TAB: InspectorTabId = "properties";
|
|
196
|
+
|
|
197
|
+
/** Whether a string is a declared Inspector tab id. */
|
|
198
|
+
export function isInspectorTabId(value: unknown): value is InspectorTabId {
|
|
199
|
+
return typeof value === "string" && (INSPECTOR_TAB_IDS as readonly string[]).includes(value);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The Bottom dock's tab ids, in strip order — plan §3.2 ⑪, at the documented cap of four.
|
|
204
|
+
*
|
|
205
|
+
* **Four, not five.** `scripts/check-chrome-budget.ts` caps a dock at four tabs, and §12's P4 entry
|
|
206
|
+
* resolves the fifth candidate explicitly: **Deploy folds into Activity**, because a deploy IS a
|
|
207
|
+
* long operation with a log, and giving it its own tab would have bought a fifth strip item to say
|
|
208
|
+
* what the fourth already says.
|
|
209
|
+
*
|
|
210
|
+
* Declared here, beside {@link NAVIGATOR_PANEL_IDS} and {@link INSPECTOR_TAB_IDS} and for the same
|
|
211
|
+
* reason: `view.setBottomTab`'s `args` enum is built from it, and `commands/app-commands.ts` must
|
|
212
|
+
* load that enum in a bare Bun process, which it could not do from a module that renders lit
|
|
213
|
+
* templates. `panels/bottom-dock.ts` builds its strip from this list and
|
|
214
|
+
* `tests/bottom-dock.test.ts` asserts the two agree, exactly as the rail's do.
|
|
215
|
+
*/
|
|
216
|
+
export const BOTTOM_TAB_IDS = ["problems", "logic", "activity"] as const;
|
|
217
|
+
|
|
218
|
+
export type BottomTabId = (typeof BOTTOM_TAB_IDS)[number];
|
|
219
|
+
|
|
220
|
+
/** The tab the Bottom dock opens on. Problems, because that is the one it opens itself for. */
|
|
221
|
+
export const DEFAULT_BOTTOM_TAB: BottomTabId = "problems";
|
|
222
|
+
|
|
223
|
+
/** Whether a string is a declared Bottom dock tab id. */
|
|
224
|
+
export function isBottomTabId(value: unknown): value is BottomTabId {
|
|
225
|
+
return typeof value === "string" && (BOTTOM_TAB_IDS as readonly string[]).includes(value);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The Studio chrome's Spectrum theme — the `color` attribute on `<sp-theme>`. */
|
|
229
|
+
export const CHROME_THEMES = ["light", "dark"] as const;
|
|
230
|
+
|
|
231
|
+
export type ChromeTheme = (typeof CHROME_THEMES)[number];
|
|
232
|
+
|
|
233
|
+
/** Which shell region currently owns keyboard focus (`focus.region` in the command context). */
|
|
234
|
+
export type FocusRegion = "rail" | "navigator" | "pane" | "inspector" | "dock" | "status";
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Open/closed plus size, per dock. Persisted together under one storage key.
|
|
238
|
+
*
|
|
239
|
+
* `size` and not `width`: the Bottom dock resizes on the other axis, so the field is measured along
|
|
240
|
+
* whichever axis its dock's handle drags — px across for the two side docks, px tall for the bottom
|
|
241
|
+
* one. {@link DOCK_CSS_VAR} names the custom property each one is projected onto, and that is where
|
|
242
|
+
* the axis is stated in a place CSS can read.
|
|
243
|
+
*/
|
|
244
|
+
export interface DockState {
|
|
245
|
+
collapsed: boolean;
|
|
246
|
+
size: number;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** One row of `git log`, as the Source Control panel's History tab renders it. */
|
|
250
|
+
export interface GitLogEntry {
|
|
251
|
+
hash: string;
|
|
252
|
+
message: string;
|
|
253
|
+
author: string;
|
|
254
|
+
date: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Source control — project level, one copy, independent of which document is focused. */
|
|
258
|
+
export interface ShellGit {
|
|
259
|
+
status: GitStatusResult | null;
|
|
260
|
+
branches: GitBranchesResult | null;
|
|
261
|
+
commitMessage: string;
|
|
262
|
+
loading: boolean;
|
|
263
|
+
error: string | null;
|
|
264
|
+
diffState: GitDiffState | null;
|
|
265
|
+
logEntries: GitLogEntry[] | null;
|
|
266
|
+
/** "changes" | "history" — the panel's sub-tab. */
|
|
267
|
+
subTab: string;
|
|
268
|
+
/** Epoch ms of the last successful refresh, or null when never refreshed. */
|
|
269
|
+
lastUpdated: number | null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The stylebook catalogue's selection and filters — project level, like the styles they edit. */
|
|
273
|
+
export interface ShellStylebook {
|
|
274
|
+
selection: string | null;
|
|
275
|
+
/** "elements" | "variables" */
|
|
276
|
+
tab: string;
|
|
277
|
+
filter: string;
|
|
278
|
+
customizedOnly: boolean;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* A named arrangement of the shell — plan §3.2 ①b.
|
|
283
|
+
*
|
|
284
|
+
* A layout is a TUPLE, not a mode: which Navigator panel is showing, whether each dock is open and
|
|
285
|
+
* how wide, and which Inspector tab is selected. Picking one reconfigures; it never removes. Every
|
|
286
|
+
* panel stays reachable by rail, chord and palette afterwards, because §13 rejects workspaces that
|
|
287
|
+
* gate features — telling a copy editor the Style panel "is in ⌘K" hands the non-technical user the
|
|
288
|
+
* one affordance they are least likely to reach for.
|
|
289
|
+
*
|
|
290
|
+
* The field this replaces (`shell.layout`, a string with a declared default and NO writer) was the
|
|
291
|
+
* exact failure `layoutSelection` had already been through: a record nothing sets, read by chrome
|
|
292
|
+
* that therefore cannot change. Wiring it was the alternative to deleting it, and these are the
|
|
293
|
+
* writers.
|
|
294
|
+
*
|
|
295
|
+
* **Editor kind is deliberately not in the tuple.** §3.2 ①b names it, but nothing in the shell can
|
|
296
|
+
* apply it: switching a pane's editor is `studio.ts`'s `setCanvasMode`, which this module does not
|
|
297
|
+
* have and cannot be handed without changing a bootstrap it does not own. A fourth field no writer
|
|
298
|
+
* could honour would recreate the defect this type exists to close, so the tuple is three fields
|
|
299
|
+
* and the fourth arrives with the pane record that can carry it.
|
|
300
|
+
*/
|
|
301
|
+
export interface LayoutPreset {
|
|
302
|
+
/** Stable key. Built-ins use their own lowercase name; a saved layout gets a slug of its. */
|
|
303
|
+
id: string;
|
|
304
|
+
/** What the Command Bar tab reads. Renamed by double-clicking it. */
|
|
305
|
+
name: string;
|
|
306
|
+
/** Which Navigator panel this arrangement shows. */
|
|
307
|
+
navigatorPanel: NavigatorPanelId;
|
|
308
|
+
/** Which Inspector tab this arrangement selects. */
|
|
309
|
+
inspectorTab: InspectorTabId;
|
|
310
|
+
/** Which Bottom dock tab this arrangement selects — read whether or not the dock is open. */
|
|
311
|
+
bottomTab: BottomTabId;
|
|
312
|
+
/** Dock visibility and sizes, all three of them. */
|
|
313
|
+
docks: Record<DockId, DockState>;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface ShellState {
|
|
317
|
+
/** Which panel the Navigator dock shows. One of {@link NAVIGATOR_PANEL_IDS}. */
|
|
318
|
+
leftTab: string;
|
|
319
|
+
/**
|
|
320
|
+
* Which tab the Bottom dock shows. One of {@link BOTTOM_TAB_IDS}.
|
|
321
|
+
*
|
|
322
|
+
* A `string` for the same reason `leftTab` is: it is also read back from persisted state, and the
|
|
323
|
+
* COMMAND (`view.setBottomTab`) is what refuses an undeclared id, at the one door callers use.
|
|
324
|
+
*/
|
|
325
|
+
bottomTab: string;
|
|
326
|
+
/** The Studio chrome's Spectrum theme. One of {@link CHROME_THEMES}. */
|
|
327
|
+
theme: ChromeTheme;
|
|
328
|
+
docks: Record<DockId, DockState>;
|
|
329
|
+
/**
|
|
330
|
+
* Where the splitter sits between two panes, as the primary's share of the grid — [0.2, 0.8].
|
|
331
|
+
*
|
|
332
|
+
* On `shell` rather than on `workspace` because it is pure LAYOUT: it names no tab, no document
|
|
333
|
+
* and no pane identity, and it is remembered with the dock widths because it is the same kind of
|
|
334
|
+
* thing. Read only when `workspace.panes.length > 1`, so a value restored with no split open is
|
|
335
|
+
* inert rather than wrong.
|
|
336
|
+
*/
|
|
337
|
+
paneSplit: number;
|
|
338
|
+
focusRegion: FocusRegion;
|
|
339
|
+
/** The active {@link LayoutPreset}'s id — the Command Bar tab drawn as selected. */
|
|
340
|
+
layout: string;
|
|
341
|
+
/** Every layout this project has, built-ins included. Persisted per project root. */
|
|
342
|
+
layouts: LayoutPreset[];
|
|
343
|
+
layoutSelection: LayoutSelection | null;
|
|
344
|
+
/** Which section Project Settings opens on. */
|
|
345
|
+
settingsTab: string;
|
|
346
|
+
stylebook: ShellStylebook;
|
|
347
|
+
git: ShellGit;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const DOCK_STORAGE_KEY = "jx-studio-panel-widths";
|
|
351
|
+
|
|
352
|
+
const THEME_STORAGE_KEY = "jx-studio-theme";
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The per-project record's key prefix — one namespaced record per project root (§4.4).
|
|
356
|
+
*
|
|
357
|
+
* Namespaced because a layout is an arrangement of THIS project's panels: "Ship" over a repo with
|
|
358
|
+
* no git remote is not the same tuple as "Ship" over one that has one, and two projects sharing a
|
|
359
|
+
* key means opening the second silently rewrites the first.
|
|
360
|
+
*
|
|
361
|
+
* Dock geometry stays in {@link DOCK_STORAGE_KEY}, which is read at module scope, before any
|
|
362
|
+
* project root exists. It is workspace state — `resetProjectShell()` has always kept it across a
|
|
363
|
+
* project switch for that reason — and moving it here would mean the shell could not lay itself out
|
|
364
|
+
* until a project opened.
|
|
365
|
+
*/
|
|
366
|
+
const PROJECT_STORAGE_PREFIX = "jx-studio-project::";
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The chrome theme Studio wakes up in.
|
|
370
|
+
*
|
|
371
|
+
* `index.html` hard-codes `color="dark"` on `<sp-theme>` so the first paint has a theme before this
|
|
372
|
+
* module evaluates; the default here has to agree with it or the shell flashes.
|
|
373
|
+
*/
|
|
374
|
+
const DEFAULT_THEME: ChromeTheme = "dark";
|
|
375
|
+
|
|
376
|
+
/** Whether a string is one of the declared {@link CHROME_THEMES}. */
|
|
377
|
+
function isChromeTheme(value: unknown): value is ChromeTheme {
|
|
378
|
+
return typeof value === "string" && (CHROME_THEMES as readonly string[]).includes(value);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Read the persisted theme, falling back to {@link DEFAULT_THEME} on absent/corrupt storage. */
|
|
382
|
+
function readPersistedTheme(): ChromeTheme {
|
|
383
|
+
try {
|
|
384
|
+
const stored = localStorage.getItem(THEME_STORAGE_KEY);
|
|
385
|
+
return isChromeTheme(stored) ? stored : DEFAULT_THEME;
|
|
386
|
+
} catch {
|
|
387
|
+
return DEFAULT_THEME;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export const DOCK_IDS: DockId[] = ["left", "right", "bottom"];
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* First-run docks.
|
|
395
|
+
*
|
|
396
|
+
* The two side docks open, because the shell's whole point is that it says where you are. The
|
|
397
|
+
* Bottom dock CLOSED, because it is the one dock whose contents are usually empty: a Problems list
|
|
398
|
+
* with nothing in it and an Activity log with nothing running would spend 220px of the canvas — the
|
|
399
|
+
* one region §3.2 says must never disappear — to say "nothing has gone wrong". ⌘J opens it, the
|
|
400
|
+
* rail badge and the status bar say when it has something to show, and nothing opens it for you.
|
|
401
|
+
*/
|
|
402
|
+
const DOCK_DEFAULTS: Record<DockId, DockState> = {
|
|
403
|
+
bottom: { collapsed: true, size: 220 },
|
|
404
|
+
left: { collapsed: false, size: 240 },
|
|
405
|
+
right: { collapsed: false, size: 280 },
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/** The size a dock returns to when its handle is double-clicked. */
|
|
409
|
+
export const DOCK_DEFAULT_SIZES: Record<DockId, number> = {
|
|
410
|
+
bottom: DOCK_DEFAULTS.bottom.size,
|
|
411
|
+
left: DOCK_DEFAULTS.left.size,
|
|
412
|
+
right: DOCK_DEFAULTS.right.size,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The custom property each dock's size is projected onto — and where its axis is written down.
|
|
417
|
+
*
|
|
418
|
+
* `--panel-w-*` feeds `grid-template-columns`, `--dock-h-bottom` feeds `grid-template-rows`. Both
|
|
419
|
+
* carry a declared fallback in `styles/tokens.css`, because an unset custom property inside a
|
|
420
|
+
* `grid-template-*` shorthand invalidates the WHOLE declaration at computed-value time and the
|
|
421
|
+
* shell would paint with no grid at all for the frame before the effect first runs.
|
|
422
|
+
*/
|
|
423
|
+
const DOCK_CSS_VAR: Record<DockId, string> = {
|
|
424
|
+
bottom: "--dock-h-bottom",
|
|
425
|
+
left: "--panel-w-left",
|
|
426
|
+
right: "--panel-w-right",
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const DOCK_CLASS: Record<DockId, string> = {
|
|
430
|
+
bottom: "bottom-collapsed",
|
|
431
|
+
left: "left-collapsed",
|
|
432
|
+
right: "right-collapsed",
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The persisted shell record. Absent keys keep the declared default.
|
|
437
|
+
*
|
|
438
|
+
* `leftTab` rides in the same record as the dock geometry because it is remembered for the same
|
|
439
|
+
* reason and by the same writer — one key, one shape, one `JSON.stringify`. P3.7 replaces the whole
|
|
440
|
+
* thing with a per-project session record; until then, adding a second localStorage key would
|
|
441
|
+
* recreate the two-writer bug this record was consolidated to fix.
|
|
442
|
+
*/
|
|
443
|
+
interface PersistedDocks {
|
|
444
|
+
left?: number;
|
|
445
|
+
right?: number;
|
|
446
|
+
bottom?: number;
|
|
447
|
+
leftCollapsed?: boolean;
|
|
448
|
+
rightCollapsed?: boolean;
|
|
449
|
+
bottomCollapsed?: boolean;
|
|
450
|
+
/** The Navigator panel last shown. Migrated through {@link migratePanelId} on read. */
|
|
451
|
+
leftTab?: string;
|
|
452
|
+
/** The Bottom dock tab last shown. Checked by {@link isBottomTabId} on read. */
|
|
453
|
+
bottomTab?: string;
|
|
454
|
+
/** The pane splitter's position. Clamped on READ — see {@link clampPaneSplit}. */
|
|
455
|
+
paneSplit?: number;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** An even split — what the grid opens on, and what a double-click on the splitter restores. */
|
|
459
|
+
export const DEFAULT_PANE_SPLIT = 0.5;
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Coerce a stored split into the supported range.
|
|
463
|
+
*
|
|
464
|
+
* Clamped here, at the READ, rather than at the drag site: the drag already refuses to leave the
|
|
465
|
+
* range, so the only way a bad value arrives is a corrupt or hand-edited record — and that is the
|
|
466
|
+
* one caller that has to be defended.
|
|
467
|
+
*/
|
|
468
|
+
function clampPaneSplit(value: unknown): number {
|
|
469
|
+
return typeof value === "number" && value >= 0.2 && value <= 0.8 ? value : DEFAULT_PANE_SPLIT;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Move the splitter. Clamped, and the one writer every control routes through. */
|
|
473
|
+
export function setPaneSplit(value: number): void {
|
|
474
|
+
shell.paneSplit = Math.min(0.8, Math.max(0.2, value));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Read the persisted dock record, tolerating absent/corrupt storage. */
|
|
478
|
+
function readPersistedDocks(): PersistedDocks {
|
|
479
|
+
try {
|
|
480
|
+
return JSON.parse(localStorage.getItem(DOCK_STORAGE_KEY) || "{}") as PersistedDocks;
|
|
481
|
+
} catch {
|
|
482
|
+
return {};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function freshGit(): ShellGit {
|
|
487
|
+
return {
|
|
488
|
+
branches: null,
|
|
489
|
+
commitMessage: "",
|
|
490
|
+
diffState: null,
|
|
491
|
+
error: null,
|
|
492
|
+
lastUpdated: null,
|
|
493
|
+
loading: false,
|
|
494
|
+
logEntries: null,
|
|
495
|
+
status: null,
|
|
496
|
+
subTab: "changes",
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function freshStylebook(): ShellStylebook {
|
|
501
|
+
return { customizedOnly: false, filter: "", selection: null, tab: "elements" };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The four layouts every project starts with — §3.2 ①b's `Write · Design · Build · Ship`.
|
|
506
|
+
*
|
|
507
|
+
* Each is a real arrangement of surfaces that already exist, not a mode: Write puts the file set
|
|
508
|
+
* beside the prose and the Inspector on Content; Design opens the Outline against the Style tab;
|
|
509
|
+
* Build pairs Data with Logic; Ship opens Source Control and gives the whole width to the diff by
|
|
510
|
+
* collapsing the Inspector — which stays one ⌘B away, because a layout never removes.
|
|
511
|
+
*
|
|
512
|
+
* Written as a factory so every project gets its own objects: they are mutable records (a saved
|
|
513
|
+
* layout overwrites the one with its name) and a shared array literal would leak one project's
|
|
514
|
+
* edits into the next.
|
|
515
|
+
*/
|
|
516
|
+
function builtInLayouts(): LayoutPreset[] {
|
|
517
|
+
return [
|
|
518
|
+
{
|
|
519
|
+
bottomTab: "problems",
|
|
520
|
+
docks: {
|
|
521
|
+
bottom: { collapsed: true, size: 220 },
|
|
522
|
+
left: { collapsed: false, size: 240 },
|
|
523
|
+
/* WRITE COLLAPSES THE INSPECTOR. §3.1's arithmetic is the whole point of this preset:
|
|
524
|
+
1600px − 56 rail − 240 navigator ≈ **1284px of page, 80%**, against Design's 974. It
|
|
525
|
+
shipped with the Inspector open at 280px, which is Design's number and makes Write a
|
|
526
|
+
Design with a different panel selected.
|
|
527
|
+
|
|
528
|
+
It does NOT re-open on selection. The plan's sentence reads "until something is
|
|
529
|
+
selected", and clicking a paragraph to put a caret in it IS a selection — a dock that
|
|
530
|
+
opened every time you clicked into prose would take the 80% back on the first keystroke.
|
|
531
|
+
`⌘⇧1`–`⌘⇧4` expand it (`focusInspectorTab` sets `collapsed: false` before it focuses), so
|
|
532
|
+
the Inspector is one chord away whenever it is wanted. */
|
|
533
|
+
right: { collapsed: true, size: 280 },
|
|
534
|
+
},
|
|
535
|
+
id: "write",
|
|
536
|
+
inspectorTab: "properties",
|
|
537
|
+
name: "Write",
|
|
538
|
+
navigatorPanel: "files",
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
bottomTab: "problems",
|
|
542
|
+
docks: {
|
|
543
|
+
bottom: { collapsed: true, size: 220 },
|
|
544
|
+
left: { collapsed: false, size: 240 },
|
|
545
|
+
right: { collapsed: false, size: 320 },
|
|
546
|
+
},
|
|
547
|
+
id: "design",
|
|
548
|
+
inspectorTab: "style",
|
|
549
|
+
name: "Design",
|
|
550
|
+
navigatorPanel: "layers",
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
bottomTab: "logic",
|
|
554
|
+
docks: {
|
|
555
|
+
bottom: { collapsed: true, size: 260 },
|
|
556
|
+
left: { collapsed: false, size: 280 },
|
|
557
|
+
right: { collapsed: false, size: 300 },
|
|
558
|
+
},
|
|
559
|
+
id: "build",
|
|
560
|
+
inspectorTab: "events",
|
|
561
|
+
name: "Build",
|
|
562
|
+
navigatorPanel: "data",
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
// The one built-in that OPENS the Bottom dock, on Activity: shipping is a long operation
|
|
566
|
+
// With a log, which is exactly what that tab is (§12 P4 — Deploy folds into Activity).
|
|
567
|
+
bottomTab: "activity",
|
|
568
|
+
docks: {
|
|
569
|
+
bottom: { collapsed: false, size: 240 },
|
|
570
|
+
left: { collapsed: false, size: 300 },
|
|
571
|
+
right: { collapsed: true, size: 280 },
|
|
572
|
+
},
|
|
573
|
+
id: "ship",
|
|
574
|
+
inspectorTab: "properties",
|
|
575
|
+
name: "Ship",
|
|
576
|
+
navigatorPanel: "git",
|
|
577
|
+
},
|
|
578
|
+
];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** The layout a project wakes up on, and the fallback when a stored id names nothing. */
|
|
582
|
+
export const DEFAULT_LAYOUT_ID = "design";
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Build the whole record — nested collections complete — before it is handed to `reactive()`.
|
|
586
|
+
*
|
|
587
|
+
* Assembling it this way is not style: writing into a nested plain object AFTER the proxy exists
|
|
588
|
+
* goes through the raw target and skips every effect depending on it.
|
|
589
|
+
*/
|
|
590
|
+
function createShellState(): ShellState {
|
|
591
|
+
const saved = readPersistedDocks();
|
|
592
|
+
const docks = {} as Record<DockId, DockState>;
|
|
593
|
+
for (const id of DOCK_IDS) {
|
|
594
|
+
const width = saved[id];
|
|
595
|
+
const collapsed = saved[`${id}Collapsed` as const];
|
|
596
|
+
docks[id] = {
|
|
597
|
+
// A stored `false` has to REOPEN a dock, so this cannot be the usual
|
|
598
|
+
// `if (saved.x) { collapse() }` shape — that silently pins a dock shut forever for
|
|
599
|
+
// Everyone who ever closed it once.
|
|
600
|
+
collapsed: typeof collapsed === "boolean" ? collapsed : DOCK_DEFAULTS[id].collapsed,
|
|
601
|
+
size: typeof width === "number" && width > 0 ? width : DOCK_DEFAULTS[id].size,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
bottomTab: isBottomTabId(saved.bottomTab) ? saved.bottomTab : DEFAULT_BOTTOM_TAB,
|
|
606
|
+
docks,
|
|
607
|
+
focusRegion: "pane",
|
|
608
|
+
paneSplit: clampPaneSplit(saved.paneSplit),
|
|
609
|
+
git: freshGit(),
|
|
610
|
+
layout: DEFAULT_LAYOUT_ID,
|
|
611
|
+
layoutSelection: null,
|
|
612
|
+
layouts: builtInLayouts(),
|
|
613
|
+
leftTab: migratePanelId(saved.leftTab) ?? DEFAULT_PANEL_ID,
|
|
614
|
+
settingsTab: "stylebook",
|
|
615
|
+
stylebook: freshStylebook(),
|
|
616
|
+
theme: readPersistedTheme(),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/*
|
|
621
|
+
* The startup profile is applied HERE, above the first read of persisted state.
|
|
622
|
+
*
|
|
623
|
+
* `?profile=fresh` has to mean "the app woke up with nothing", and this module's `createShellState()`
|
|
624
|
+
* is the earliest `localStorage.getItem` in the import graph — every other read is inside a function
|
|
625
|
+
* and therefore runs after module evaluation. Clearing storage a statement later would restore the
|
|
626
|
+
* dock widths from a profile that claims not to have any.
|
|
627
|
+
*/
|
|
628
|
+
applyStartupProfile();
|
|
629
|
+
|
|
630
|
+
/*
|
|
631
|
+
* The input is widened before `reactive()` sees it: `UnwrapNestedRefs<ShellState>` recurses through
|
|
632
|
+
* `LayoutHit` and the git result shapes deeply enough for tsc to give up (TS2589). The proxy is
|
|
633
|
+
* deep either way — only the type computation is short-circuited — and the declared annotation is
|
|
634
|
+
* what every consumer sees. Same reason `tabs/tab.ts` casts its own reactive tree.
|
|
635
|
+
*/
|
|
636
|
+
export const shell: ShellState = reactive(
|
|
637
|
+
createShellState() as unknown as Record<string, unknown>,
|
|
638
|
+
) as unknown as ShellState;
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Write the whole dock record in one shot.
|
|
642
|
+
*
|
|
643
|
+
* One writer, one shape. The predecessor had two: `persistWidths()` wrote a fresh `{left,right,…}`
|
|
644
|
+
* to this key while `applyPanelCollapse()` read-merge-wrote the collapse booleans into it, so
|
|
645
|
+
* dragging any handle wiped all of them.
|
|
646
|
+
*/
|
|
647
|
+
export function persistDocks(): void {
|
|
648
|
+
try {
|
|
649
|
+
localStorage.setItem(
|
|
650
|
+
DOCK_STORAGE_KEY,
|
|
651
|
+
JSON.stringify({
|
|
652
|
+
bottom: shell.docks.bottom.size,
|
|
653
|
+
bottomCollapsed: shell.docks.bottom.collapsed,
|
|
654
|
+
bottomTab: shell.bottomTab,
|
|
655
|
+
left: shell.docks.left.size,
|
|
656
|
+
leftCollapsed: shell.docks.left.collapsed,
|
|
657
|
+
leftTab: shell.leftTab,
|
|
658
|
+
paneSplit: shell.paneSplit,
|
|
659
|
+
right: shell.docks.right.size,
|
|
660
|
+
rightCollapsed: shell.docks.right.collapsed,
|
|
661
|
+
} satisfies PersistedDocks),
|
|
662
|
+
);
|
|
663
|
+
} catch {
|
|
664
|
+
// Storage full or unavailable — the layout is still applied, just not remembered.
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ─── Named layouts (§3.2 ①b) ──────────────────────────────────────────────────
|
|
669
|
+
|
|
670
|
+
/** What a project's namespaced record holds: its named layouts, and the session it was left in. */
|
|
671
|
+
interface PersistedProject {
|
|
672
|
+
layouts?: unknown;
|
|
673
|
+
activeLayout?: unknown;
|
|
674
|
+
/** §4.4's session — open documents per pane, the active one, and each one's view settings. */
|
|
675
|
+
session?: unknown;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** The project whose layouts are loaded, or `undefined` before the first sync. */
|
|
679
|
+
let _layoutRoot: string | null | undefined;
|
|
680
|
+
|
|
681
|
+
/** Whether a parsed value is a usable {@link LayoutPreset} — a stored record is untrusted input. */
|
|
682
|
+
function isLayoutPreset(value: unknown): value is LayoutPreset {
|
|
683
|
+
if (typeof value !== "object" || value === null) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
const preset = value as Partial<LayoutPreset>;
|
|
687
|
+
return (
|
|
688
|
+
typeof preset.id === "string" &&
|
|
689
|
+
preset.id !== "" &&
|
|
690
|
+
typeof preset.name === "string" &&
|
|
691
|
+
isNavigatorPanelId(preset.navigatorPanel) &&
|
|
692
|
+
isInspectorTabId(preset.inspectorTab) &&
|
|
693
|
+
isBottomTabId(preset.bottomTab) &&
|
|
694
|
+
DOCK_IDS.every((dock) => typeof preset.docks?.[dock]?.size === "number")
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** The raw record for `root`, or `{}` for absent, corrupt or hand-edited storage. */
|
|
699
|
+
function rawPersistedProject(root: string | null): PersistedProject {
|
|
700
|
+
if (!root) {
|
|
701
|
+
return {};
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
return JSON.parse(
|
|
705
|
+
localStorage.getItem(`${PROJECT_STORAGE_PREFIX}${root}`) || "{}",
|
|
706
|
+
) as PersistedProject;
|
|
707
|
+
} catch {
|
|
708
|
+
return {};
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Read a project's namespaced record, tolerating absent, corrupt and hand-edited storage. */
|
|
713
|
+
function readPersistedProject(root: string | null): {
|
|
714
|
+
layouts: LayoutPreset[];
|
|
715
|
+
activeLayout: string | null;
|
|
716
|
+
} {
|
|
717
|
+
const fresh = { activeLayout: null, layouts: builtInLayouts() };
|
|
718
|
+
if (!root) {
|
|
719
|
+
return fresh;
|
|
720
|
+
}
|
|
721
|
+
const parsed = rawPersistedProject(root);
|
|
722
|
+
const layouts = Array.isArray(parsed.layouts) ? parsed.layouts.filter(isLayoutPreset) : [];
|
|
723
|
+
if (layouts.length === 0) {
|
|
724
|
+
return fresh;
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
activeLayout: typeof parsed.activeLayout === "string" ? parsed.activeLayout : null,
|
|
728
|
+
layouts,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* The session `root` was last left in, or `null` when there is none to restore.
|
|
734
|
+
*
|
|
735
|
+
* Separate from {@link readPersistedProject} because the two are read at different moments: the
|
|
736
|
+
* layouts load reactively the instant the project root changes, and the session is read once, by
|
|
737
|
+
* the open sequence, at the point where it would otherwise open the home page.
|
|
738
|
+
*/
|
|
739
|
+
export function persistedSession(root: string | null): PersistedSession | null {
|
|
740
|
+
return readSession(rawPersistedProject(root).session);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* The project whose session has been READ, and may therefore be overwritten.
|
|
745
|
+
*
|
|
746
|
+
* Without this the feature ate itself. Opening a project sets `workspace.projectRoot`, which fires
|
|
747
|
+
* the persist effect below while the workspace is still empty — so `captureSession()` returned
|
|
748
|
+
* `{panes: []}` and wrote it over the record the open sequence was about to read, three lines
|
|
749
|
+
* later. The stored session was always destroyed a moment before it was wanted.
|
|
750
|
+
*
|
|
751
|
+
* So a session is only written for a root that has already been restored from. Until then the
|
|
752
|
+
* stored one is carried forward verbatim, and a project that fails halfway through opening leaves
|
|
753
|
+
* the record it could not use intact.
|
|
754
|
+
*/
|
|
755
|
+
let _sessionRoot: string | null = null;
|
|
756
|
+
|
|
757
|
+
/** Declare that `root`'s session has been read, so this window may now write over it. */
|
|
758
|
+
export function markSessionRestored(root: string | null): void {
|
|
759
|
+
_sessionRoot = root;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Write the project's namespaced record. One key, one writer, one shape.
|
|
764
|
+
*
|
|
765
|
+
* The dock record's two-writer bug (`persistWidths` vs `applyPanelCollapse`, both on
|
|
766
|
+
* {@link DOCK_STORAGE_KEY}) is the reason this is a single function that serialises the whole
|
|
767
|
+
* record rather than a merge at each call site.
|
|
768
|
+
*/
|
|
769
|
+
/**
|
|
770
|
+
* Write the session on the way out — the moment the whole feature exists for.
|
|
771
|
+
*
|
|
772
|
+
* The effect above tracks the session's SHAPE. The view settings ride along on the next shape
|
|
773
|
+
* change, so a zoom or a breakpoint chosen after the last tab was opened would otherwise be the one
|
|
774
|
+
* thing a relaunch forgot. `beforeunload` cannot await, and this does not need to: one synchronous
|
|
775
|
+
* `localStorage` write.
|
|
776
|
+
*/
|
|
777
|
+
export function flushSession(): void {
|
|
778
|
+
persistProjectShell();
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export function persistProjectShell(): void {
|
|
782
|
+
const root = workspace.projectRoot;
|
|
783
|
+
if (!root) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
localStorage.setItem(
|
|
788
|
+
`${PROJECT_STORAGE_PREFIX}${root}`,
|
|
789
|
+
JSON.stringify({
|
|
790
|
+
activeLayout: shell.layout,
|
|
791
|
+
layouts: shell.layouts,
|
|
792
|
+
// Only for a project whose session has been read — see {@link markSessionRestored}.
|
|
793
|
+
session: root === _sessionRoot ? captureSession() : rawPersistedProject(root).session,
|
|
794
|
+
} satisfies PersistedProject),
|
|
795
|
+
);
|
|
796
|
+
} catch {
|
|
797
|
+
// Storage full or unavailable — the arrangement is applied, just not remembered.
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Load the layouts belonging to `root`. Idempotent per root, so the effect can call it freely.
|
|
803
|
+
*
|
|
804
|
+
* Loading does NOT apply the active layout: a boot that re-applied it would overwrite the dock
|
|
805
|
+
* widths the user dragged since, which are persisted separately and are the more recent statement.
|
|
806
|
+
* The stored id says which tab is drawn as selected; clicking it is what re-applies the tuple.
|
|
807
|
+
*/
|
|
808
|
+
export function syncProjectLayouts(root: string | null): void {
|
|
809
|
+
if (root === _layoutRoot) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
_layoutRoot = root;
|
|
813
|
+
const { activeLayout, layouts } = readPersistedProject(root);
|
|
814
|
+
shell.layouts = layouts;
|
|
815
|
+
const has = (id: string | null) => id !== null && layouts.some((preset) => preset.id === id);
|
|
816
|
+
// The stored active layout wins, then the one already selected, then the default, then whatever
|
|
817
|
+
// The project actually has — a bar drawn with nothing selected says the app has lost your place.
|
|
818
|
+
shell.layout = has(activeLayout)
|
|
819
|
+
? activeLayout!
|
|
820
|
+
: has(shell.layout)
|
|
821
|
+
? shell.layout
|
|
822
|
+
: has(DEFAULT_LAYOUT_ID)
|
|
823
|
+
? DEFAULT_LAYOUT_ID
|
|
824
|
+
: (layouts[0]?.id ?? DEFAULT_LAYOUT_ID);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** The layout with this id, or `undefined`. */
|
|
828
|
+
export function layoutById(id: string): LayoutPreset | undefined {
|
|
829
|
+
return shell.layouts.find((preset) => preset.id === id);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** A stable id for a saved layout's name: `My Layout` → `my-layout`. */
|
|
833
|
+
export function layoutIdFor(name: string): string {
|
|
834
|
+
const slug = name
|
|
835
|
+
.trim()
|
|
836
|
+
.toLowerCase()
|
|
837
|
+
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
838
|
+
.replaceAll(/^-|-$/g, "");
|
|
839
|
+
return slug || "layout";
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** What applying or capturing a layout needs from the Inspector, which lives on the active tab. */
|
|
843
|
+
export interface LayoutDeps {
|
|
844
|
+
setInspectorTab: (tab: InspectorTabId) => void;
|
|
845
|
+
inspectorTab: () => InspectorTabId;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Adopt a layout: its panel, its docks, its Inspector tab — and remember that it is the active one.
|
|
850
|
+
*
|
|
851
|
+
* Throws on an unknown id rather than silently doing nothing, because the id space is the user's
|
|
852
|
+
* own (their saved layouts) and a typo in a command argument has to be reportable.
|
|
853
|
+
*/
|
|
854
|
+
export function applyLayout(id: string, deps: LayoutDeps): void {
|
|
855
|
+
const preset = layoutById(id);
|
|
856
|
+
if (!preset) {
|
|
857
|
+
throw new RangeError(
|
|
858
|
+
`no layout named "${id}" — this project has: ${shell.layouts.map((p) => p.id).join(", ")}`,
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
shell.layout = preset.id;
|
|
862
|
+
shell.leftTab = preset.navigatorPanel;
|
|
863
|
+
shell.bottomTab = preset.bottomTab;
|
|
864
|
+
for (const dock of DOCK_IDS) {
|
|
865
|
+
shell.docks[dock].collapsed = preset.docks[dock].collapsed;
|
|
866
|
+
shell.docks[dock].size = preset.docks[dock].size;
|
|
867
|
+
}
|
|
868
|
+
deps.setInspectorTab(preset.inspectorTab);
|
|
869
|
+
persistDocks();
|
|
870
|
+
persistProjectShell();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Save the current arrangement under `name`, and make it the active layout.
|
|
875
|
+
*
|
|
876
|
+
* Saving over an existing name overwrites it — that is what `+` on a layout you have just adjusted
|
|
877
|
+
* means, and it keeps the operation idempotent for a caller that cannot see the list.
|
|
878
|
+
*/
|
|
879
|
+
export function saveLayout(name: string, deps: LayoutDeps): LayoutPreset {
|
|
880
|
+
const trimmed = name.trim();
|
|
881
|
+
const id = layoutIdFor(trimmed);
|
|
882
|
+
const docks = {} as Record<DockId, DockState>;
|
|
883
|
+
for (const dock of DOCK_IDS) {
|
|
884
|
+
docks[dock] = { collapsed: shell.docks[dock].collapsed, size: shell.docks[dock].size };
|
|
885
|
+
}
|
|
886
|
+
const preset: LayoutPreset = {
|
|
887
|
+
bottomTab: isBottomTabId(shell.bottomTab) ? shell.bottomTab : DEFAULT_BOTTOM_TAB,
|
|
888
|
+
docks,
|
|
889
|
+
id,
|
|
890
|
+
inspectorTab: deps.inspectorTab(),
|
|
891
|
+
name: trimmed || id,
|
|
892
|
+
navigatorPanel: migratePanelId(shell.leftTab) ?? DEFAULT_PANEL_ID,
|
|
893
|
+
};
|
|
894
|
+
const existing = shell.layouts.findIndex((entry) => entry.id === id);
|
|
895
|
+
if (existing === -1) {
|
|
896
|
+
shell.layouts.push(preset);
|
|
897
|
+
} else {
|
|
898
|
+
shell.layouts.splice(existing, 1, preset);
|
|
899
|
+
}
|
|
900
|
+
shell.layout = id;
|
|
901
|
+
persistProjectShell();
|
|
902
|
+
return preset;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** Rename a layout in place. The id — and therefore every reference to it — is unchanged. */
|
|
906
|
+
export function renameLayout(id: string, name: string): void {
|
|
907
|
+
const preset = layoutById(id);
|
|
908
|
+
const trimmed = name.trim();
|
|
909
|
+
if (!preset || !trimmed) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
preset.name = trimmed;
|
|
913
|
+
persistProjectShell();
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Forget a layout. The last one cannot be deleted: an empty Command Bar with a `+` and no way to
|
|
918
|
+
* know what it does is worse than a layout you do not use.
|
|
919
|
+
*/
|
|
920
|
+
export function deleteLayout(id: string): void {
|
|
921
|
+
if (shell.layouts.length < 2) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const index = shell.layouts.findIndex((preset) => preset.id === id);
|
|
925
|
+
if (index === -1) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
shell.layouts.splice(index, 1);
|
|
929
|
+
if (shell.layout === id) {
|
|
930
|
+
shell.layout = shell.layouts[0]!.id;
|
|
931
|
+
}
|
|
932
|
+
persistProjectShell();
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Re-apply the active layout as declared — `View: Reset Layout`, always one action away (§3.2 ①b).
|
|
937
|
+
*
|
|
938
|
+
* This is the escape hatch that makes a layout safe to drift from: drag a dock, collapse the
|
|
939
|
+
* Inspector, open a different panel, and one command puts the arrangement back.
|
|
940
|
+
*/
|
|
941
|
+
export function resetLayout(deps: LayoutDeps): void {
|
|
942
|
+
applyLayout(shell.layout, deps);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Project the theme record onto `<sp-theme>`.
|
|
947
|
+
*
|
|
948
|
+
* The predecessor was a raw `document.querySelector("sp-theme")?.setAttribute(...)` inside the
|
|
949
|
+
* automation hook — a presentation poke no user could make and no surface could read back. It is a
|
|
950
|
+
* shell input like the docks, so it lives on the record and is applied by the same effect.
|
|
951
|
+
*/
|
|
952
|
+
export function applyChromeTheme(): void {
|
|
953
|
+
document.querySelector("sp-theme")?.setAttribute("color", shell.theme);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** Project the dock record onto the shell grid: collapse classes on #app, sizes as CSS vars. */
|
|
957
|
+
export function applyDockLayout(): void {
|
|
958
|
+
const root = document.documentElement;
|
|
959
|
+
for (const id of DOCK_IDS) {
|
|
960
|
+
root.style.setProperty(DOCK_CSS_VAR[id], `${shell.docks[id].size}px`);
|
|
961
|
+
}
|
|
962
|
+
const app = document.querySelector("#app");
|
|
963
|
+
if (!app) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
for (const id of DOCK_IDS) {
|
|
967
|
+
app.classList.toggle(DOCK_CLASS[id], shell.docks[id].collapsed);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
let _scope: EffectScope | null = null;
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* A surface that lives and dies with the shell but is drawn somewhere else.
|
|
975
|
+
*
|
|
976
|
+
* The Bottom dock is the first: it is a DOCK — projected onto the grid by this module's own record
|
|
977
|
+
* and effect — but it renders lit templates over the panel registry, and a direct import here would
|
|
978
|
+
* be a cycle (`shell → bottom-dock → shell`) that `oxlint`'s `import/no-cycle` refuses and that
|
|
979
|
+
* would drag the panel registry into every bare-Bun process that reads the command enums.
|
|
980
|
+
*
|
|
981
|
+
* So the dependency points one way: the surface's own module registers itself, and this module
|
|
982
|
+
* knows only that something wants to be mounted when the shell is.
|
|
983
|
+
*/
|
|
984
|
+
export interface ShellSurface {
|
|
985
|
+
mount: () => void;
|
|
986
|
+
unmount: () => void;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const _surfaces: ShellSurface[] = [];
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Attach a surface to the shell's lifecycle. Idempotent per object.
|
|
993
|
+
*
|
|
994
|
+
* Called at module scope by the surface itself, so importing it is enough — the same bargain
|
|
995
|
+
* `ui/panel-resize.ts` makes, minus its unguarded `document` read at import time.
|
|
996
|
+
*/
|
|
997
|
+
export function registerShellSurface(surface: ShellSurface): void {
|
|
998
|
+
if (!_surfaces.includes(surface)) {
|
|
999
|
+
_surfaces.push(surface);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** Forget every registered surface. Tests only. */
|
|
1004
|
+
export function resetShellSurfaces(): void {
|
|
1005
|
+
_surfaces.length = 0;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Start the one effect that keeps the DOM in step with the dock record. Idempotent.
|
|
1010
|
+
*
|
|
1011
|
+
* Nothing else applies the grid: a caller flips `shell.docks.right.collapsed` and the layout
|
|
1012
|
+
* follows, whether the flip came from a click, the automation runner or the boot-time restore.
|
|
1013
|
+
*/
|
|
1014
|
+
export function mountShell(): void {
|
|
1015
|
+
if (_scope) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
// The six shell hosts are bare `<div id>`s in index.html, so they cannot stamp their own region
|
|
1019
|
+
// The way a panel or an overlay slot does. One table, applied once the tree exists.
|
|
1020
|
+
stampShellRegions();
|
|
1021
|
+
// The Bottom dock mounts from here rather than from the bootstrap because it is a DOCK: it is
|
|
1022
|
+
// Projected onto the grid by the same record and the same effect as the other two, and a second
|
|
1023
|
+
// Mounting site would be a second place the shell's own layout is decided. Every surface's own
|
|
1024
|
+
// Mount is idempotent and inert when its host is absent — the desktop shell and the tests both
|
|
1025
|
+
// Boot partial trees.
|
|
1026
|
+
for (const surface of _surfaces) {
|
|
1027
|
+
surface.mount();
|
|
1028
|
+
}
|
|
1029
|
+
_scope = effectScope();
|
|
1030
|
+
_scope.run(() => {
|
|
1031
|
+
effect(() => {
|
|
1032
|
+
applyDockLayout();
|
|
1033
|
+
});
|
|
1034
|
+
effect(() => {
|
|
1035
|
+
applyChromeTheme();
|
|
1036
|
+
});
|
|
1037
|
+
// Layouts belong to a project, and the project root is reactive: opening one loads its record
|
|
1038
|
+
// With no boot-order hook to forget. `resetProjectShell()` runs while the OLD root is still
|
|
1039
|
+
// Set — it is the closing half of a switch — which is why the load cannot live there.
|
|
1040
|
+
effect(() => {
|
|
1041
|
+
syncProjectLayouts(workspace.projectRoot);
|
|
1042
|
+
});
|
|
1043
|
+
/*
|
|
1044
|
+
* §4.4's session, written whenever its SHAPE changes.
|
|
1045
|
+
*
|
|
1046
|
+
* The reads below are the tracking list: which panes exist, what is in each strip and in what
|
|
1047
|
+
* order, which tab is active, and where the keyboard is. Every one of them is a deliberate act
|
|
1048
|
+
* by the author, and there are few enough of them per session that a `localStorage` write each
|
|
1049
|
+
* time costs nothing measurable.
|
|
1050
|
+
*
|
|
1051
|
+
* Per-tab view settings are captured at write time rather than tracked. Zoom moves on every
|
|
1052
|
+
* wheel tick, and an effect that persisted the record sixty times a second to remember a scale
|
|
1053
|
+
* would be a worse bug than forgetting the scale — so a mode change or a new tab carries the
|
|
1054
|
+
* zoom along with it, and `flushSession()` on the way out catches the rest.
|
|
1055
|
+
*/
|
|
1056
|
+
effect(() => {
|
|
1057
|
+
const root = workspace.projectRoot;
|
|
1058
|
+
void workspace.activePaneId;
|
|
1059
|
+
for (const pane of workspace.panes) {
|
|
1060
|
+
void pane.id;
|
|
1061
|
+
void pane.activeTabId;
|
|
1062
|
+
void pane.tabOrder.join("\u0000");
|
|
1063
|
+
}
|
|
1064
|
+
if (root) {
|
|
1065
|
+
persistProjectShell();
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export function unmountShell(): void {
|
|
1072
|
+
_scope?.stop();
|
|
1073
|
+
_scope = null;
|
|
1074
|
+
for (const surface of _surfaces) {
|
|
1075
|
+
surface.unmount();
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Open or close a dock, and remember it.
|
|
1081
|
+
*
|
|
1082
|
+
* There is no cross-dock coupling left to write here. The predecessor had to keep "assistant open,
|
|
1083
|
+
* inspector collapsed" unreachable by hand, because the assistant was a fifth column sharing the
|
|
1084
|
+
* inspector's cell; a TAB cannot be in that state by construction — collapsing the Inspector dock
|
|
1085
|
+
* hides whichever of its four tabs was selected, assistant included.
|
|
1086
|
+
*/
|
|
1087
|
+
export function setDockCollapsed(dock: DockId, collapsed: boolean): void {
|
|
1088
|
+
if (shell.docks[dock].collapsed === collapsed) {
|
|
1089
|
+
persistDocks();
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
shell.docks[dock].collapsed = collapsed;
|
|
1093
|
+
persistDocks();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/** Flip a dock's open state, and remember it. */
|
|
1097
|
+
export function toggleDock(dock: DockId): void {
|
|
1098
|
+
setDockCollapsed(dock, !shell.docks[dock].collapsed);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** Resize a dock. Persisting is the caller's call — a drag persists once, on release. */
|
|
1102
|
+
export function setDockSize(dock: DockId, size: number): void {
|
|
1103
|
+
shell.docks[dock].size = size;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Reveal a Navigator panel: select it, make sure the dock is open, and remember both.
|
|
1108
|
+
*
|
|
1109
|
+
* `persistDocks()` is called unconditionally rather than relying on `setDockCollapsed` — that only
|
|
1110
|
+
* writes when the collapse flag actually changed, so re-picking a panel in an already-open dock
|
|
1111
|
+
* would have been forgotten.
|
|
1112
|
+
*
|
|
1113
|
+
* **The parameter is a {@link NavigatorPanelId}, not a `string`, and that is the whole guard.**
|
|
1114
|
+
* `shell.leftTab` stays a `string` because it is read back from state an older build persisted —
|
|
1115
|
+
* {@link migratePanelId} is what turns one of those into a current id. A LIVE caller is a different
|
|
1116
|
+
* question, and it was answered wrong: the Outline's empty state called this with `"blocks"` for
|
|
1117
|
+
* three phases after `blocks` became `insert`, so "Add an element" put the Navigator into
|
|
1118
|
+
* `unknownPanel`'s dead state (`panels/left-panel.ts`). The rename shipped a migration for the read
|
|
1119
|
+
* path and nothing for the write path, and no test could see it because a string is a string. This
|
|
1120
|
+
* signature makes that class of bug a type error at the call site.
|
|
1121
|
+
*/
|
|
1122
|
+
export function setActivityTab(tab: NavigatorPanelId): void {
|
|
1123
|
+
shell.leftTab = tab;
|
|
1124
|
+
setDockCollapsed("left", false);
|
|
1125
|
+
persistDocks();
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Reveal a Bottom dock tab: select it, make sure the dock is open, and remember both.
|
|
1130
|
+
*
|
|
1131
|
+
* The Navigator's {@link setActivityTab} shape, for the same reason — "show me the Activity log"
|
|
1132
|
+
* means the log is on screen when it returns, not that a tab is selected inside a closed dock.
|
|
1133
|
+
*/
|
|
1134
|
+
export function setBottomTab(tab: string): void {
|
|
1135
|
+
shell.bottomTab = tab;
|
|
1136
|
+
setDockCollapsed("bottom", false);
|
|
1137
|
+
persistDocks();
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/** Set the chrome theme, apply it, and remember it. */
|
|
1141
|
+
export function setChromeTheme(theme: ChromeTheme): void {
|
|
1142
|
+
if (shell.theme === theme) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
shell.theme = theme;
|
|
1146
|
+
try {
|
|
1147
|
+
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
|
1148
|
+
} catch {
|
|
1149
|
+
// Storage full or unavailable — the theme is still applied, just not remembered.
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Toggle-focus semantics for the rail: re-picking the panel that is already showing closes the dock
|
|
1155
|
+
* instead of reselecting it.
|
|
1156
|
+
*/
|
|
1157
|
+
export function toggleActivityTab(tab: NavigatorPanelId): void {
|
|
1158
|
+
if (tab === shell.leftTab && !shell.docks.left.collapsed) {
|
|
1159
|
+
setDockCollapsed("left", true);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
setActivityTab(tab);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Adopt (or clear) a layout-chrome selection reported by the canvas.
|
|
1167
|
+
*
|
|
1168
|
+
* The canvas host calls this on a `layoutHit`, and clears it (`null`) whenever an ordinary document
|
|
1169
|
+
* node is selected — the two are mutually exclusive, and only one panel can be right at a time. It
|
|
1170
|
+
* exists as a function rather than a bare assignment so the one writer has a name: the field spent
|
|
1171
|
+
* a long release cycle with a reader (the properties panel's layout panel) and no writer at all,
|
|
1172
|
+
* which is why clicking a header did nothing.
|
|
1173
|
+
*/
|
|
1174
|
+
export function setLayoutSelection(selection: LayoutSelection | null): void {
|
|
1175
|
+
shell.layoutSelection = selection;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Drop everything scoped to the project being closed.
|
|
1180
|
+
*
|
|
1181
|
+
* Source control, the stylebook selection and the settings tab all describe _a_ project; carrying
|
|
1182
|
+
* them into the next one showed the previous repository's branch, file count and "last updated"
|
|
1183
|
+
* time under the new project's name. Docks, the layout preset and the active rail panel are
|
|
1184
|
+
* deliberately kept — they describe the workspace, not the project.
|
|
1185
|
+
*/
|
|
1186
|
+
export function resetProjectShell(): void {
|
|
1187
|
+
Object.assign(shell.git, freshGit());
|
|
1188
|
+
Object.assign(shell.stylebook, freshStylebook());
|
|
1189
|
+
shell.settingsTab = "stylebook";
|
|
1190
|
+
shell.layoutSelection = null;
|
|
1191
|
+
// …and which project's session may be written, so the one being opened cannot be overwritten
|
|
1192
|
+
// Before it has been read.
|
|
1193
|
+
_sessionRoot = null;
|
|
1194
|
+
// Forget which project's layouts are loaded, so the next `syncProjectLayouts()` reloads even if
|
|
1195
|
+
// The same root is opened again. The docks and the active rail panel stay: they describe the
|
|
1196
|
+
// Workspace, and re-applying a layout on project open would discard the widths you just dragged.
|
|
1197
|
+
_layoutRoot = undefined;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
1201
|
+
|
|
1202
|
+
/** What the shell's view verbs need that this module does not own. */
|
|
1203
|
+
export interface ShellCommandDeps {
|
|
1204
|
+
/**
|
|
1205
|
+
* Show an Inspector tab. Writes the ACTIVE TAB's `session.ui.rightTab`, which is per-document
|
|
1206
|
+
* session state this module deliberately does not reach into (`shell` is workspace state, and the
|
|
1207
|
+
* split is the whole point of the file).
|
|
1208
|
+
*/
|
|
1209
|
+
setInspectorTab: (tab: InspectorTabId) => void;
|
|
1210
|
+
/**
|
|
1211
|
+
* Which Inspector tab is showing right now.
|
|
1212
|
+
*
|
|
1213
|
+
* Only `view.setAssistant { open: false }` needs it, and it needs it to stay idempotent: "the
|
|
1214
|
+
* assistant is not showing" is a state, not a delta, so the verb steps off the Assistant tab and
|
|
1215
|
+
* leaves any OTHER tab exactly where it was. Without the read it would have to guess, and a
|
|
1216
|
+
* screenshot that closed the assistant would silently also lose your Style tab.
|
|
1217
|
+
*/
|
|
1218
|
+
inspectorTab: () => InspectorTabId;
|
|
1219
|
+
/**
|
|
1220
|
+
* Whether a dock tab's panel record is currently available — its own `when` holding.
|
|
1221
|
+
*
|
|
1222
|
+
* INJECTED rather than imported for the reason the rest of this object exists: `shell` is
|
|
1223
|
+
* workspace state and must not reach into the panel registry, which reaches back. It is here
|
|
1224
|
+
* because `panel.focus.<id>` composes the record's own `when` and these two enum setters did not,
|
|
1225
|
+
* so one surface answered two ways. `view.setActivity { tab: "search" }` succeeded against a
|
|
1226
|
+
* panel registered `when: () => false`, wrote `shell.leftTab = "search"`, un-collapsed the dock
|
|
1227
|
+
* and PERSISTED it — after which the Navigator drew "No Navigator panel is registered as
|
|
1228
|
+
* 'search'", a sentence that is false: it is registered, it is gated off. Defaults to available
|
|
1229
|
+
* so a caller with no registry (the CI projection) is unaffected.
|
|
1230
|
+
*/
|
|
1231
|
+
panelAvailable?: (id: string) => boolean;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/** The Navigator dock hosts a panel — the precondition its own verbs share. */
|
|
1235
|
+
const projectOpen = (ctx: { project: { open: boolean } }) => ctx.project.open;
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* The shell's view verbs: idempotent SETTERS, never toggles.
|
|
1239
|
+
*
|
|
1240
|
+
* Plan §13.3 clause 3 is the whole shape of this list. `view.toggleAssistant` names a _delta_
|
|
1241
|
+
* against state the caller cannot observe, so flipping the assistant's default silently inverted 18
|
|
1242
|
+
* screenshots and an agent calling it is guessing; `view.setAssistant { open: false }` means the
|
|
1243
|
+
* same thing whichever way the default points. The rail's own click handler keeps its
|
|
1244
|
+
* toggle-to-collapse behaviour ({@link toggleActivityTab}) because a human CAN see which panel is
|
|
1245
|
+
* open — that is a gesture, not an API.
|
|
1246
|
+
*
|
|
1247
|
+
* The `args` enums are the highest-value declaration here: `view.setActivity`'s `tab` is exactly
|
|
1248
|
+
* {@link NAVIGATOR_PANEL_IDS}, so renaming a panel turns every stale manifest step red naming both
|
|
1249
|
+
* ids, in the PR that renamed it (plan §13.5).
|
|
1250
|
+
*
|
|
1251
|
+
* @param {ShellCommandDeps} deps
|
|
1252
|
+
* @returns {AnyCommand[]}
|
|
1253
|
+
*/
|
|
1254
|
+
export function shellViewCommands(deps: ShellCommandDeps): AnyCommand[] {
|
|
1255
|
+
return [
|
|
1256
|
+
{
|
|
1257
|
+
args: argsSchema({
|
|
1258
|
+
tab: enumProperty(NAVIGATOR_PANEL_IDS, "Which Navigator panel to show."),
|
|
1259
|
+
}),
|
|
1260
|
+
category: "View",
|
|
1261
|
+
id: "view.setActivity",
|
|
1262
|
+
level: "application",
|
|
1263
|
+
menus: ["commandbar/overflow", "palette"],
|
|
1264
|
+
group: "4_docks",
|
|
1265
|
+
requires: "an open project",
|
|
1266
|
+
when: projectOpen,
|
|
1267
|
+
aiTool: {
|
|
1268
|
+
description:
|
|
1269
|
+
"Show one of the Navigator panels (Files, Search, Source Control, Languages, Outline, " +
|
|
1270
|
+
"Page, Data, Packages, Insert) and open the Navigator dock if it is closed. Languages is " +
|
|
1271
|
+
"the translation-parity grid, and it exists only in a project that declares two or more " +
|
|
1272
|
+
"locales. Problems is a Bottom dock tab — use show_bottom_tab for it.",
|
|
1273
|
+
name: "show_navigator_panel",
|
|
1274
|
+
},
|
|
1275
|
+
run: (_ctx, args) => {
|
|
1276
|
+
const tab = enumArg("view.setActivity", args, "tab", NAVIGATOR_PANEL_IDS);
|
|
1277
|
+
// The record's own `when`, asked here because `enablement` cannot see an argument — the
|
|
1278
|
+
// Same shape `pane.derive` uses to refuse a preset the document cannot support.
|
|
1279
|
+
if (deps.panelAvailable && !deps.panelAvailable(tab)) {
|
|
1280
|
+
throw new RangeError(
|
|
1281
|
+
`command "view.setActivity" argument "tab": "${tab}" names a panel that is registered but not ` +
|
|
1282
|
+
`available right now, so showing it would persist a Navigator tab nothing can draw`,
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
setActivityTab(tab);
|
|
1286
|
+
},
|
|
1287
|
+
title: "Show Navigator Panel",
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
args: argsSchema({
|
|
1291
|
+
tab: enumProperty(INSPECTOR_TAB_IDS, "Which Inspector tab to show."),
|
|
1292
|
+
}),
|
|
1293
|
+
category: "View",
|
|
1294
|
+
id: "view.setRightTab",
|
|
1295
|
+
level: "document",
|
|
1296
|
+
menus: ["palette"],
|
|
1297
|
+
group: "4_docks",
|
|
1298
|
+
requires: "an open document",
|
|
1299
|
+
when: (ctx) => ctx.document.open,
|
|
1300
|
+
run: (_ctx, args) => {
|
|
1301
|
+
deps.setInspectorTab(enumArg("view.setRightTab", args, "tab", INSPECTOR_TAB_IDS));
|
|
1302
|
+
},
|
|
1303
|
+
title: "Show Inspector Tab",
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
args: argsSchema({
|
|
1307
|
+
open: booleanProperty("True to show the Navigator dock, false to collapse it."),
|
|
1308
|
+
}),
|
|
1309
|
+
category: "View",
|
|
1310
|
+
// The idempotent half of `toggleActivityTab`. A shot that wants the Navigator CLOSED used to
|
|
1311
|
+
// Say `view.toggleActivity { tab: "layers" }` — re-pick the open panel and the dock
|
|
1312
|
+
// Collapses — which only means "closed" if you already know it was open.
|
|
1313
|
+
id: "view.setNavigator",
|
|
1314
|
+
level: "application",
|
|
1315
|
+
menus: ["palette"],
|
|
1316
|
+
group: "4_docks",
|
|
1317
|
+
run: (_ctx, args) => {
|
|
1318
|
+
setDockCollapsed("left", !booleanArg("view.setNavigator", args, "open"));
|
|
1319
|
+
},
|
|
1320
|
+
title: "Show Navigator Dock",
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
args: argsSchema({
|
|
1324
|
+
open: booleanProperty("True to show the Inspector dock, false to collapse it."),
|
|
1325
|
+
}),
|
|
1326
|
+
category: "View",
|
|
1327
|
+
id: "view.setRightPanel",
|
|
1328
|
+
level: "application",
|
|
1329
|
+
menus: ["palette"],
|
|
1330
|
+
group: "4_docks",
|
|
1331
|
+
run: (_ctx, args) => {
|
|
1332
|
+
setDockCollapsed("right", !booleanArg("view.setRightPanel", args, "open"));
|
|
1333
|
+
},
|
|
1334
|
+
title: "Show Inspector Dock",
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
args: argsSchema({
|
|
1338
|
+
open: booleanProperty("True to show the Bottom dock, false to collapse it."),
|
|
1339
|
+
}),
|
|
1340
|
+
category: "View",
|
|
1341
|
+
// The idempotent half of `view.toggleBottomDock`, and the reason the toggle can exist at all:
|
|
1342
|
+
// `tests/app-commands.test.ts` fails any `toggle*` id with no `set*` beside it, and this one
|
|
1343
|
+
// Has been listed as HANDOFF debt since P2 because the dock was not on the `shell` record.
|
|
1344
|
+
// It is now, so this is a two-line `run` rather than the special case ⌘J used to need.
|
|
1345
|
+
id: "view.setBottomDock",
|
|
1346
|
+
level: "application",
|
|
1347
|
+
menus: ["palette"],
|
|
1348
|
+
group: "4_docks",
|
|
1349
|
+
run: (_ctx, args) => {
|
|
1350
|
+
setDockCollapsed("bottom", !booleanArg("view.setBottomDock", args, "open"));
|
|
1351
|
+
},
|
|
1352
|
+
title: "Show Bottom Dock",
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
// ⌘J. A gesture, not an API: a human presses it while looking at the dock, which is the
|
|
1356
|
+
// Exemption §13.3 clause 3 grants a toggle whose state the presser can SEE. A script says
|
|
1357
|
+
// `view.setBottomDock { open }` instead, and the test beside the registry enforces the pair.
|
|
1358
|
+
category: "View",
|
|
1359
|
+
id: "view.toggleBottomDock",
|
|
1360
|
+
keybinding: "mod+j",
|
|
1361
|
+
level: "application",
|
|
1362
|
+
menus: ["commandbar/overflow", "palette"],
|
|
1363
|
+
group: "4_docks",
|
|
1364
|
+
run: () => {
|
|
1365
|
+
toggleDock("bottom");
|
|
1366
|
+
},
|
|
1367
|
+
title: "Toggle Bottom Dock",
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
args: argsSchema({
|
|
1371
|
+
tab: enumProperty(BOTTOM_TAB_IDS, "Which Bottom dock tab to show."),
|
|
1372
|
+
}),
|
|
1373
|
+
category: "View",
|
|
1374
|
+
id: "view.setBottomTab",
|
|
1375
|
+
level: "application",
|
|
1376
|
+
menus: ["palette"],
|
|
1377
|
+
group: "4_docks",
|
|
1378
|
+
aiTool: {
|
|
1379
|
+
description:
|
|
1380
|
+
"Show one of the Bottom dock's tabs (Problems, Diff, Logic, Activity) and open the " +
|
|
1381
|
+
"Bottom dock if it is closed.",
|
|
1382
|
+
name: "show_bottom_tab",
|
|
1383
|
+
},
|
|
1384
|
+
run: (_ctx, args) => {
|
|
1385
|
+
const tab = enumArg("view.setBottomTab", args, "tab", BOTTOM_TAB_IDS);
|
|
1386
|
+
// The record's own `when`, asked here because `enablement` cannot see an argument — the
|
|
1387
|
+
// Same shape `pane.derive` uses to refuse a preset the document cannot support.
|
|
1388
|
+
if (deps.panelAvailable && !deps.panelAvailable(tab)) {
|
|
1389
|
+
throw new RangeError(
|
|
1390
|
+
`command "view.setBottomTab" argument "tab": "${tab}" names a panel that is registered but not ` +
|
|
1391
|
+
`available right now, so showing it would persist a Bottom dock tab nothing can draw`,
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
setBottomTab(tab);
|
|
1395
|
+
},
|
|
1396
|
+
title: "Show Bottom Dock Tab",
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
args: argsSchema({
|
|
1400
|
+
open: booleanProperty("True to show the Assistant tab, false to step off it."),
|
|
1401
|
+
}),
|
|
1402
|
+
category: "View",
|
|
1403
|
+
// Kept as a verb of its own even though the assistant is now an ordinary Inspector tab: it
|
|
1404
|
+
// Is the one tab with a dock toggle's meaning ("show me the assistant, wherever it lives"),
|
|
1405
|
+
// Three screenshot shots address it by this name, and `open` reads as a STATE where
|
|
1406
|
+
// `view.setRightTab { tab: "assistant" }` can only ever select.
|
|
1407
|
+
id: "view.setAssistant",
|
|
1408
|
+
level: "application",
|
|
1409
|
+
/* `never`, not `palette` — and the reason is a rule, not a preference.
|
|
1410
|
+
This and `inspector.focus.assistant` are both `View` + "Show Assistant", so the palette
|
|
1411
|
+
printed the SAME SENTENCE twice, in two level groups, with two different chords: the only
|
|
1412
|
+
duplicate title among 130 records. A palette row is a NAME, and two rows with one name is
|
|
1413
|
+
the defect the registry exists to prevent, arriving through the registry itself.
|
|
1414
|
+
`inspector.focus.assistant` is the row (it carries ⌘⇧4). This one keeps every other
|
|
1415
|
+
consumer — the `open` STATE that `setRightTab` cannot express, the layout presets, three
|
|
1416
|
+
screenshot steps, `⌘J`'s legacy branch — none of which needs a row to work. */
|
|
1417
|
+
menus: ["never"],
|
|
1418
|
+
group: "4_docks",
|
|
1419
|
+
run: (_ctx, args) => {
|
|
1420
|
+
if (booleanArg("view.setAssistant", args, "open")) {
|
|
1421
|
+
setDockCollapsed("right", false);
|
|
1422
|
+
deps.setInspectorTab("assistant");
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
if (deps.inspectorTab() === "assistant") {
|
|
1426
|
+
deps.setInspectorTab(DEFAULT_INSPECTOR_TAB);
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
title: "Show Assistant",
|
|
1430
|
+
},
|
|
1431
|
+
// ── Named layouts ────────────────────────────────────────────────────────
|
|
1432
|
+
// Five verbs, because a named arrangement you can create but not rename, delete or restore is
|
|
1433
|
+
// A trap. All five are SETTERS: `view.setLayout { layout: "ship" }` means the same thing twice
|
|
1434
|
+
// In a row, and `view.resetLayout` is the one action that puts a drifted arrangement back.
|
|
1435
|
+
{
|
|
1436
|
+
args: argsSchema({
|
|
1437
|
+
layout: stringProperty("The layout's id — one of this project's saved layouts."),
|
|
1438
|
+
}),
|
|
1439
|
+
category: "View",
|
|
1440
|
+
id: "view.setLayout",
|
|
1441
|
+
level: "application",
|
|
1442
|
+
menus: ["palette"],
|
|
1443
|
+
group: "4_layouts",
|
|
1444
|
+
requires: "an open project",
|
|
1445
|
+
when: projectOpen,
|
|
1446
|
+
aiTool: {
|
|
1447
|
+
description:
|
|
1448
|
+
"Adopt one of the project's named layouts (Write, Design, Build, Ship, or one the user " +
|
|
1449
|
+
"saved): its Navigator panel, its dock widths and visibility, and its Inspector tab.",
|
|
1450
|
+
name: "set_layout",
|
|
1451
|
+
},
|
|
1452
|
+
run: (_ctx, args) => {
|
|
1453
|
+
applyLayout(stringArg("view.setLayout", args, "layout"), deps);
|
|
1454
|
+
},
|
|
1455
|
+
title: "Switch Layout",
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
args: argsSchema({
|
|
1459
|
+
name: stringProperty("What to call this arrangement. An existing name is overwritten."),
|
|
1460
|
+
}),
|
|
1461
|
+
category: "View",
|
|
1462
|
+
id: "view.saveLayout",
|
|
1463
|
+
level: "application",
|
|
1464
|
+
menus: ["palette"],
|
|
1465
|
+
group: "4_layouts",
|
|
1466
|
+
requires: "an open project",
|
|
1467
|
+
when: projectOpen,
|
|
1468
|
+
run: (_ctx, args) => {
|
|
1469
|
+
saveLayout(stringArg("view.saveLayout", args, "name"), deps);
|
|
1470
|
+
},
|
|
1471
|
+
title: "Save Layout",
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
args: argsSchema({
|
|
1475
|
+
layout: stringProperty("The layout's id."),
|
|
1476
|
+
name: stringProperty("Its new name. The id, and every reference to it, is unchanged."),
|
|
1477
|
+
}),
|
|
1478
|
+
category: "View",
|
|
1479
|
+
id: "view.renameLayout",
|
|
1480
|
+
level: "application",
|
|
1481
|
+
menus: ["palette"],
|
|
1482
|
+
group: "4_layouts",
|
|
1483
|
+
requires: "an open project",
|
|
1484
|
+
when: projectOpen,
|
|
1485
|
+
run: (_ctx, args) => {
|
|
1486
|
+
renameLayout(
|
|
1487
|
+
stringArg("view.renameLayout", args, "layout"),
|
|
1488
|
+
stringArg("view.renameLayout", args, "name"),
|
|
1489
|
+
);
|
|
1490
|
+
},
|
|
1491
|
+
title: "Rename Layout",
|
|
1492
|
+
},
|
|
1493
|
+
{
|
|
1494
|
+
args: argsSchema({
|
|
1495
|
+
layout: stringProperty("The layout's id. The last remaining layout is kept."),
|
|
1496
|
+
}),
|
|
1497
|
+
category: "View",
|
|
1498
|
+
id: "view.deleteLayout",
|
|
1499
|
+
level: "application",
|
|
1500
|
+
menus: ["palette"],
|
|
1501
|
+
group: "4_layouts",
|
|
1502
|
+
requires: "an open project",
|
|
1503
|
+
when: projectOpen,
|
|
1504
|
+
run: (_ctx, args) => {
|
|
1505
|
+
deleteLayout(stringArg("view.deleteLayout", args, "layout"));
|
|
1506
|
+
},
|
|
1507
|
+
title: "Delete Layout",
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
// No `args`: this verb takes none. An empty schema would be a promise of parameters the
|
|
1511
|
+
// Palette would then prompt for and the record would ignore.
|
|
1512
|
+
category: "View",
|
|
1513
|
+
id: "view.resetLayout",
|
|
1514
|
+
level: "application",
|
|
1515
|
+
menus: ["palette"],
|
|
1516
|
+
group: "4_layouts",
|
|
1517
|
+
requires: "an open project",
|
|
1518
|
+
when: projectOpen,
|
|
1519
|
+
run: () => {
|
|
1520
|
+
resetLayout(deps);
|
|
1521
|
+
},
|
|
1522
|
+
title: "Reset Layout",
|
|
1523
|
+
},
|
|
1524
|
+
{
|
|
1525
|
+
args: argsSchema({
|
|
1526
|
+
color: enumProperty(CHROME_THEMES, "The Studio chrome's theme."),
|
|
1527
|
+
}),
|
|
1528
|
+
category: "View",
|
|
1529
|
+
id: "view.setTheme",
|
|
1530
|
+
level: "application",
|
|
1531
|
+
menus: ["palette"],
|
|
1532
|
+
group: "6_appearance",
|
|
1533
|
+
run: (_ctx, args) => {
|
|
1534
|
+
setChromeTheme(enumArg("view.setTheme", args, "color", CHROME_THEMES));
|
|
1535
|
+
},
|
|
1536
|
+
title: "Set Theme",
|
|
1537
|
+
},
|
|
1538
|
+
];
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Register the shell's view verbs. Called from the bootstrap, beside the state they write.
|
|
1543
|
+
*
|
|
1544
|
+
* @param {CommandRegistry} registry
|
|
1545
|
+
* @param {ShellCommandDeps} deps
|
|
1546
|
+
*/
|
|
1547
|
+
export function registerShellViewCommands(registry: CommandRegistry, deps: ShellCommandDeps): void {
|
|
1548
|
+
registry.registerAll(shellViewCommands(deps));
|
|
1549
|
+
}
|