@jxsuite/studio 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/canvas.html +21 -0
- package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
- package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
- package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
- package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
- package/dist/chunks/{components-0hzentwq.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-0hzentwq.js.map → components-2bpqp4mq.js.map} +1 -1
- package/dist/chunks/content-source-34gnkbnn.js +38 -0
- package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
- package/dist/chunks/extension-sections-6mqydz35.js +301 -0
- package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
- package/dist/chunks/files-8kn321h1.js +70 -0
- package/dist/chunks/files-8kn321h1.js.map +9 -0
- package/dist/chunks/{format-host-2c188n41.js → format-host-mkwsmt8d.js} +3 -3
- package/dist/chunks/git-panel-tjbr191d.js +51 -0
- package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
- package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
- package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
- package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
- package/dist/chunks/layers-panel-87pb25km.js +58 -0
- package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
- package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
- package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
- package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
- package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
- package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
- package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
- package/dist/chunks/right-panel-zpdxd98t.js +37 -0
- package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
- package/dist/chunks/section-registry-zmckaxf4.js +30 -0
- package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
- package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
- package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
- package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
- package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
- package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
- package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
- package/dist/chunks/studio-41exgzhq.js +6690 -0
- package/dist/chunks/studio-41exgzhq.js.map +38 -0
- package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
- package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
- package/dist/chunks/studio-50k77cca.js +1787 -0
- package/dist/chunks/studio-50k77cca.js.map +18 -0
- package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
- package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
- package/dist/chunks/studio-6f0am8f2.js +53 -0
- package/dist/chunks/studio-6f0am8f2.js.map +10 -0
- package/dist/chunks/studio-6nmjc5qk.js +7002 -0
- package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
- package/dist/chunks/studio-7afd21nw.js +67 -0
- package/dist/chunks/studio-7afd21nw.js.map +10 -0
- package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
- package/dist/chunks/studio-7bmdvzne.js.map +10 -0
- package/dist/chunks/studio-8jpzqk5s.js +128 -0
- package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
- package/dist/chunks/studio-94m77tgk.js +1482 -0
- package/dist/chunks/studio-94m77tgk.js.map +13 -0
- package/dist/chunks/studio-9je6w5sq.js +1000 -0
- package/dist/chunks/studio-9je6w5sq.js.map +16 -0
- package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
- package/dist/chunks/{studio-97qcgvdn.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-97qcgvdn.js.map → studio-adn0z1k3.js.map} +3 -3
- package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
- package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
- package/dist/chunks/studio-bw5dz6km.js +608 -0
- package/dist/chunks/studio-bw5dz6km.js.map +21 -0
- package/dist/chunks/studio-c6s5getc.js +4232 -0
- package/dist/chunks/studio-c6s5getc.js.map +39 -0
- package/dist/chunks/studio-cjm7fdjf.js +451 -0
- package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
- package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
- package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
- package/dist/chunks/studio-dg7zyt5x.js +28 -0
- package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
- package/dist/chunks/studio-dzcyvvvz.js +166 -0
- package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
- package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
- package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
- package/dist/chunks/studio-f12h6177.js +411 -0
- package/dist/chunks/studio-f12h6177.js.map +13 -0
- package/dist/chunks/studio-hbp1s8m9.js +1817 -0
- package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
- package/dist/chunks/studio-k2sxjph5.js +15295 -0
- package/dist/chunks/studio-k2sxjph5.js.map +235 -0
- package/dist/chunks/studio-myje51t3.js +385 -0
- package/dist/chunks/studio-myje51t3.js.map +12 -0
- package/dist/chunks/studio-na6rwzhw.js +353 -0
- package/dist/chunks/studio-na6rwzhw.js.map +11 -0
- package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
- package/dist/chunks/studio-p471gg5n.js.map +14 -0
- package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
- package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
- package/dist/chunks/{studio-hhj92gcr.js → studio-s519zc99.js} +3 -3
- package/dist/chunks/studio-sas7mt3k.js +75 -0
- package/dist/chunks/studio-sas7mt3k.js.map +10 -0
- package/dist/chunks/studio-t5wqwzdh.js +74 -0
- package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
- package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
- package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
- package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
- package/dist/chunks/studio-tr20m8g8.js.map +9 -0
- package/dist/chunks/{studio-ygzav7z5.js → studio-v96qfnww.js} +5514 -4291
- package/dist/chunks/studio-v96qfnww.js.map +54 -0
- package/dist/chunks/studio-vcetc91k.js +659 -0
- package/dist/chunks/studio-vcetc91k.js.map +10 -0
- package/dist/chunks/{studio-n9zmpxjg.js → studio-vmymjefp.js} +2972 -4937
- package/dist/chunks/studio-vmymjefp.js.map +98 -0
- package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
- package/dist/chunks/studio-x9hmy5kr.js +390 -0
- package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
- package/dist/chunks/studio-xa0ce0n7.js +335 -0
- package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
- package/dist/chunks/studio-xm3dbxvn.js +378 -0
- package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
- package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
- package/dist/chunks/studio-zv37yrfa.js +1054 -0
- package/dist/chunks/studio-zv37yrfa.js.map +13 -0
- package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
- package/dist/chunks/ws-client-654ts66j.js.map +11 -0
- package/dist/iframe-entry.js +668 -230
- package/dist/studio.js +23711 -35185
- package/index.html +35 -4685
- package/package.json +17 -15
- package/src/about/about-modal.ts +42 -11
- package/src/browse/library-commands.ts +240 -0
- package/src/browse/library-layouts.ts +369 -0
- package/src/browse/library-model.ts +396 -0
- package/src/browse/library-pane.ts +1049 -0
- package/src/browse/library-preview.ts +252 -0
- package/src/browse/library-source.ts +150 -0
- package/src/canvas/canvas-helpers.ts +33 -9
- package/src/canvas/canvas-live-render.ts +45 -29
- package/src/canvas/canvas-patcher.ts +151 -31
- package/src/canvas/canvas-perf.ts +46 -7
- package/src/canvas/canvas-render.ts +1092 -220
- package/src/canvas/canvas-surface.ts +240 -0
- package/src/canvas/canvas-utils.ts +1166 -124
- package/src/canvas/iframe-drop.ts +2 -1
- package/src/canvas/iframe-entry.ts +241 -4
- package/src/canvas/iframe-host.ts +1226 -143
- package/src/canvas/iframe-inline-edit.ts +31 -0
- package/src/canvas/iframe-interaction.ts +76 -16
- package/src/canvas/iframe-keys.ts +107 -45
- package/src/canvas/iframe-overlay.ts +38 -1
- package/src/canvas/iframe-protocol.ts +137 -2
- package/src/canvas/iframe-render.ts +70 -2
- package/src/canvas/iframe-slash.ts +6 -0
- package/src/canvas/path-mapping.ts +56 -6
- package/src/canvas/surface-registry.ts +292 -0
- package/src/collab/collab-commands.ts +244 -0
- package/src/collab/collab-session.ts +198 -32
- package/src/collab/collab-state.ts +28 -2
- package/src/collab/presence-chips.ts +100 -9
- package/src/commands/active-registry.ts +42 -0
- package/src/commands/app-commands.ts +176 -0
- package/src/commands/budget.ts +158 -0
- package/src/commands/command-args.ts +253 -0
- package/src/commands/context.ts +387 -0
- package/src/commands/defaults.ts +580 -0
- package/src/commands/keymap.ts +571 -0
- package/src/commands/levels.ts +345 -0
- package/src/commands/live-context.ts +193 -0
- package/src/commands/reference.ts +227 -0
- package/src/commands/registry.ts +293 -0
- package/src/component-props.ts +3 -2
- package/src/content/draft-state.ts +106 -0
- package/src/content/entry-commands.ts +282 -0
- package/src/content/entry-editor.ts +327 -0
- package/src/content/entry-fields.ts +116 -0
- package/src/content/entry-model.ts +175 -0
- package/src/editor/canvas-context-menu.ts +17 -11
- package/src/editor/canvas-slash-bridge.ts +11 -4
- package/src/editor/context-menu.ts +804 -204
- package/src/editor/convert-to-component.ts +31 -10
- package/src/editor/convert-to-repeater.ts +3 -2
- package/src/editor/inline-edit-apply.ts +5 -5
- package/src/editor/inline-edit.ts +86 -50
- package/src/editor/insert-zone-action.ts +1 -1
- package/src/editor/shortcuts.ts +1043 -329
- package/src/editor/slash-menu.ts +1 -0
- package/src/files/elements.ts +166 -0
- package/src/files/file-ops.ts +178 -31
- package/src/files/files.ts +898 -268
- package/src/files/fs-events.ts +21 -0
- package/src/files/media-meta.ts +203 -0
- package/src/files/media-paths.ts +141 -0
- package/src/files/media-upload.ts +7 -3
- package/src/files/media-usage.ts +189 -0
- package/src/github/github-auth.ts +235 -33
- package/src/github/github-publish.ts +80 -21
- package/src/grid/grid-controller.ts +152 -12
- package/src/grid/grid-layout.ts +225 -20
- package/src/grid/grid-open.ts +120 -0
- package/src/grid/grid-panel.ts +554 -39
- package/src/grid/grid-source.ts +27 -1
- package/src/grid/redirects-grid.ts +599 -0
- package/src/grid/redirects.ts +398 -0
- package/src/grid/sources/content-source.ts +108 -2
- package/src/i18n/i18n-commands.ts +322 -0
- package/src/new-project/add-repo-modal.ts +8 -10
- package/src/new-project/import-tab.ts +34 -9
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +270 -262
- package/src/packages/jxsuite-update.ts +97 -48
- package/src/packages/pull-package-sync.ts +18 -9
- package/src/page-params.ts +2 -1
- package/src/panels/activity-bar.ts +152 -91
- package/src/panels/activity-panel.ts +481 -0
- package/src/panels/ai-chat/chat-markdown.ts +8 -3
- package/src/panels/ai-chat/chat-view.ts +245 -18
- package/src/panels/ai-chat/composer.ts +31 -5
- package/src/panels/ai-chat/sessions-view.ts +17 -10
- package/src/panels/ai-panel.ts +279 -120
- package/src/panels/block-action-bar.ts +1115 -224
- package/src/panels/bottom-dock.ts +337 -0
- package/src/panels/canvas-dnd-bridge.ts +37 -8
- package/src/panels/chat-panel.ts +29 -12
- package/src/panels/data-explorer.ts +254 -112
- package/src/panels/data-grid.ts +10 -4
- package/src/panels/dnd.ts +28 -51
- package/src/panels/editors.ts +434 -107
- package/src/panels/elements-panel.ts +66 -2
- package/src/panels/empty-state.ts +112 -0
- package/src/panels/events-panel.ts +596 -82
- package/src/panels/formula-workspace.ts +408 -218
- package/src/panels/frontmatter-fields.ts +51 -10
- package/src/panels/frontmatter-panel.ts +357 -79
- package/src/panels/git-panel.ts +375 -164
- package/src/panels/head-panel.ts +791 -85
- package/src/panels/i18n-panel.ts +592 -0
- package/src/panels/imports-panel.ts +95 -85
- package/src/panels/jump-bar.ts +584 -0
- package/src/panels/layers-panel.ts +1106 -328
- package/src/panels/left-panel.ts +109 -204
- package/src/panels/navigator-panels.ts +131 -0
- package/src/panels/overlays.ts +7 -2
- package/src/panels/pane-context.ts +1477 -0
- package/src/panels/pane-grid.ts +406 -0
- package/src/panels/panel-registry.ts +340 -0
- package/src/panels/panel-scheduler.ts +75 -0
- package/src/panels/problems-panel.ts +234 -0
- package/src/panels/properties-panel.ts +946 -1139
- package/src/panels/provenance.ts +258 -0
- package/src/panels/quick-search.ts +791 -122
- package/src/panels/right-panel.ts +228 -102
- package/src/panels/seo-modal.ts +395 -0
- package/src/panels/settings-pane.ts +203 -0
- package/src/panels/signals-panel.ts +354 -137
- package/src/panels/statement-editor.ts +41 -25
- package/src/panels/statusbar.ts +342 -106
- package/src/panels/style-panel.ts +974 -463
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-doc.ts +4 -2
- package/src/panels/stylebook-layers-panel.ts +3 -2
- package/src/panels/stylebook-panel.ts +51 -26
- package/src/panels/tab-strip.ts +1058 -78
- package/src/panels/target-line.ts +274 -0
- package/src/panels/toolbar.ts +631 -484
- package/src/panels/welcome-screen.ts +301 -198
- package/src/platform-errors.ts +26 -3
- package/src/platform.ts +96 -2
- package/src/platforms/cloud.ts +71 -8
- package/src/platforms/devserver.ts +92 -29
- package/src/publish/deploy-checklist.ts +383 -0
- package/src/publish/pages-service.ts +10 -1
- package/src/publish/publish-commands.ts +218 -0
- package/src/publish/publish-panel.ts +110 -16
- package/src/recent-projects.ts +3 -2
- package/src/services/a11y-report.ts +505 -0
- package/src/services/ai-models.ts +75 -2
- package/src/services/ai-project-tools.ts +29 -10
- package/src/services/ai-system-prompt.ts +53 -14
- package/src/services/ai-tools.ts +39 -3
- package/src/services/ai-writes.ts +154 -0
- package/src/services/announce.ts +97 -0
- package/src/services/automation.ts +424 -175
- package/src/services/cem-export.ts +9 -4
- package/src/services/clock.ts +61 -0
- package/src/services/document-assistant.ts +36 -8
- package/src/services/idle.ts +179 -0
- package/src/services/import-client.ts +26 -0
- package/src/services/monaco-buffer.ts +627 -0
- package/src/services/monaco-lazy.ts +36 -0
- package/src/services/notify.ts +271 -0
- package/src/services/profile.ts +155 -0
- package/src/services/references.ts +0 -0
- package/src/services/token-lint.ts +3 -2
- package/src/services/tool-executor.ts +53 -7
- package/src/services/trusted-types.ts +123 -0
- package/src/settings/contexts-section.ts +427 -0
- package/src/settings/contributed-section.ts +149 -11
- package/src/settings/css-vars-editor.ts +306 -349
- package/src/settings/defs-editor.ts +93 -34
- package/src/settings/dependencies-editor.ts +7 -7
- package/src/settings/extension-sections.ts +37 -5
- package/src/settings/general-settings.ts +204 -121
- package/src/settings/locales-section.ts +347 -0
- package/src/settings/preferences-accounts.ts +140 -0
- package/src/settings/preferences-dialog.ts +620 -0
- package/src/settings/preferences-keymap.ts +270 -0
- package/src/settings/project-sections.ts +260 -0
- package/src/settings/section-registry.ts +169 -0
- package/src/settings/settings-document.ts +385 -0
- package/src/shell.ts +1549 -0
- package/src/site-context.ts +45 -20
- package/src/state.ts +14 -275
- package/src/store.ts +88 -43
- package/src/studio.ts +790 -346
- package/src/style/live-preview.ts +36 -0
- package/src/style/project-styles.ts +349 -0
- package/src/style/token-ref.ts +123 -0
- package/src/tabs/patch-ops.ts +16 -4
- package/src/tabs/project-config.ts +416 -0
- package/src/tabs/selection.ts +323 -0
- package/src/tabs/tab.ts +182 -47
- package/src/tabs/transact.ts +209 -28
- package/src/types.ts +68 -13
- package/src/ui/ai-credentials-form.ts +39 -35
- package/src/ui/ai-managed-connect.ts +113 -0
- package/src/ui/dynamic-slot.ts +224 -81
- package/src/ui/expression-editor.ts +71 -63
- package/src/ui/field-input.ts +4 -6
- package/src/ui/field-row.ts +215 -38
- package/src/ui/form-controls.ts +194 -7
- package/src/ui/layers.ts +492 -69
- package/src/ui/media-picker.ts +115 -25
- package/src/ui/panel-resize.ts +109 -91
- package/src/ui/progress-modal.ts +131 -50
- package/src/ui/regions.ts +462 -0
- package/src/ui/schema-form.ts +229 -45
- package/src/ui/spectrum.ts +24 -8
- package/src/ui/timing.ts +49 -0
- package/src/ui/value-selector.ts +20 -0
- package/src/ui/value-source.ts +348 -0
- package/src/ui/virtual-window.ts +400 -0
- package/src/utils/edit-display.ts +3 -2
- package/src/utils/inherited-style.ts +67 -37
- package/src/view.ts +64 -78
- package/src/workspace/pane-derive.ts +1258 -0
- package/src/workspace/session.ts +317 -0
- package/src/workspace/workspace.ts +1345 -23
- package/styles/canvas.css +653 -0
- package/styles/forced-colors.css +106 -0
- package/styles/inspector.css +1454 -0
- package/styles/overlays.css +2704 -0
- package/styles/panels.css +1701 -0
- package/styles/shell.css +1423 -0
- package/styles/tokens.css +151 -0
- package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
- package/dist/chunks/extension-sections-qct0gt8f.js +0 -531
- package/dist/chunks/extension-sections-qct0gt8f.js.map +0 -12
- package/dist/chunks/files-02zn5y24.js +0 -50
- package/dist/chunks/files-02zn5y24.js.map +0 -9
- package/dist/chunks/git-panel-mbb0y0hz.js +0 -28
- package/dist/chunks/git-panel-mbb0y0hz.js.map +0 -9
- package/dist/chunks/layers-panel-9qekmqk1.js +0 -26
- package/dist/chunks/layers-panel-9qekmqk1.js.map +0 -9
- package/dist/chunks/settings-modal-gj0hrah4.js +0 -29
- package/dist/chunks/settings-modal-gj0hrah4.js.map +0 -9
- package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
- package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
- package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
- package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
- package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-568nx4t5.js +0 -469
- package/dist/chunks/studio-568nx4t5.js.map +0 -14
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-asvf0j9e.js +0 -429
- package/dist/chunks/studio-asvf0j9e.js.map +0 -13
- package/dist/chunks/studio-d77qm3pk.js +0 -3427
- package/dist/chunks/studio-d77qm3pk.js.map +0 -26
- package/dist/chunks/studio-j6gqyjf4.js +0 -1854
- package/dist/chunks/studio-j6gqyjf4.js.map +0 -18
- package/dist/chunks/studio-n9zmpxjg.js.map +0 -115
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-rq0vxb19.js +0 -3744
- package/dist/chunks/studio-rq0vxb19.js.map +0 -22
- package/dist/chunks/studio-wd6ewdvd.js +0 -853
- package/dist/chunks/studio-wd6ewdvd.js.map +0 -12
- package/dist/chunks/studio-xq0t7n5t.js +0 -140
- package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
- package/dist/chunks/studio-xze4zrs1.js.map +0 -10
- package/dist/chunks/studio-ygzav7z5.js.map +0 -46
- package/dist/chunks/studio-zmy7241e.js +0 -399
- package/dist/chunks/studio-zmy7241e.js.map +0 -12
- package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
- package/src/browse/browse-modal.ts +0 -69
- package/src/browse/browse.ts +0 -904
- package/src/canvas/canvas-diff.ts +0 -216
- package/src/canvas/nested-site-style.ts +0 -58
- package/src/new-project/design-fields.ts +0 -264
- package/src/new-project/templates.ts +0 -61
- package/src/panels/tab-bar.ts +0 -548
- package/src/settings/settings-modal.ts +0 -203
- /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
- /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
- /package/dist/chunks/{format-host-2c188n41.js.map → format-host-mkwsmt8d.js.map} +0 -0
- /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
- /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
- /package/dist/chunks/{studio-hhj92gcr.js.map → studio-s519zc99.js.map} +0 -0
- /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
- /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Collaborate:` command family — the entry point and the exit co-editing has never had (§7.4).
|
|
3
|
+
*
|
|
4
|
+
* Collaboration in Studio has been a thing that HAPPENS to a document: a session attaches because
|
|
5
|
+
* the platform said the file was shared, presence chips appear in the toolbar, and there is no verb
|
|
6
|
+
* anywhere in the app for starting it, inviting anybody, following someone, or stopping. The
|
|
7
|
+
* palette had no `Collaborate:` rows at all, and `CATEGORIES` has carried the name since P2 with
|
|
8
|
+
* nothing filed under it.
|
|
9
|
+
*
|
|
10
|
+
* Five records, each registered here — in the module that owns the machinery, per P2's rule — and
|
|
11
|
+
* composed by `commands/app-commands.ts`. Every one is idempotent or refuses with a `requires`
|
|
12
|
+
* sentence, and none of them is a `toggle*` without a `set*` beside it: `collab.setEnabled` is the
|
|
13
|
+
* setter, `collab.stop` is the same setter's `false` and exists because "stop sharing" is what a
|
|
14
|
+
* person actually looks for in a palette.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { collabState } from "./collab-state";
|
|
18
|
+
import { isCollabEnabled, setCollabEnabled } from "./collab-session";
|
|
19
|
+
import { notify } from "../services/notify";
|
|
20
|
+
import { activeTab } from "../workspace/workspace";
|
|
21
|
+
import { booleanArg, booleanProperty, stringProperty } from "../commands/command-args";
|
|
22
|
+
|
|
23
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
24
|
+
import type { Tab } from "../tabs/tab";
|
|
25
|
+
|
|
26
|
+
/** The tab a Collaborate verb acts on. */
|
|
27
|
+
function tab(): Tab | null {
|
|
28
|
+
return activeTab.value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True while the active tab has a live session — what Stop and Follow require. */
|
|
32
|
+
function sessionLive(): boolean {
|
|
33
|
+
const active = tab();
|
|
34
|
+
return Boolean(active && collabState(active).active);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A shareable reference to the current session.
|
|
39
|
+
*
|
|
40
|
+
* The document path IS the session key — `platform.collab(path)` is how every attach resolves — so
|
|
41
|
+
* the link is derived, not minted. That is the whole reason this command can exist without a server
|
|
42
|
+
* round trip, and the reason it cannot promise the recipient has access: it names a room, it does
|
|
43
|
+
* not grant one.
|
|
44
|
+
*
|
|
45
|
+
* @param {Tab} active
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function sessionLink(active: Tab): string {
|
|
49
|
+
const path = active.documentPath ?? "";
|
|
50
|
+
return `jx://collab/${path.replace(/^\.?\//, "")}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The Collaborate verbs.
|
|
55
|
+
*
|
|
56
|
+
* @returns {AnyCommand[]}
|
|
57
|
+
*/
|
|
58
|
+
export function collabCommands(): AnyCommand[] {
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
id: "collab.setEnabled",
|
|
62
|
+
title: "Collaborate: Share this document",
|
|
63
|
+
category: "Collaborate",
|
|
64
|
+
level: "document",
|
|
65
|
+
menus: ["palette"],
|
|
66
|
+
group: "1_session",
|
|
67
|
+
requires: "an open document",
|
|
68
|
+
when: () => Boolean(tab()?.documentPath),
|
|
69
|
+
args: {
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
properties: {
|
|
72
|
+
enabled: booleanProperty("True to attach a live session, false to leave it."),
|
|
73
|
+
},
|
|
74
|
+
required: ["enabled"],
|
|
75
|
+
type: "object",
|
|
76
|
+
},
|
|
77
|
+
run: (_ctx, args) => {
|
|
78
|
+
const enabled = booleanArg("collab.setEnabled", args, "enabled");
|
|
79
|
+
const active = tab();
|
|
80
|
+
if (!active) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (isCollabEnabled(active) === enabled) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
setCollabEnabled(active, enabled);
|
|
87
|
+
notify.info(
|
|
88
|
+
enabled
|
|
89
|
+
? "Connecting this document to its collaboration session…"
|
|
90
|
+
: "Left the collaboration session for this document.",
|
|
91
|
+
{ source: "Collaboration" },
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "collab.stop",
|
|
97
|
+
title: "Collaborate: Stop sharing",
|
|
98
|
+
category: "Collaborate",
|
|
99
|
+
level: "document",
|
|
100
|
+
menus: ["palette"],
|
|
101
|
+
group: "1_session",
|
|
102
|
+
requires: "a live collaboration session",
|
|
103
|
+
when: () => Boolean(tab()?.documentPath),
|
|
104
|
+
enablement: sessionLive,
|
|
105
|
+
run: () => {
|
|
106
|
+
/* The idempotent setter, said the way a person looks for it in a palette. `setEnabled
|
|
107
|
+
false` and this are one behaviour with two names, which is what stops "Stop" from being
|
|
108
|
+
a toggle with no setter beside it. */
|
|
109
|
+
const active = tab();
|
|
110
|
+
if (!active || !isCollabEnabled(active)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
setCollabEnabled(active, false);
|
|
114
|
+
notify.info("Left the collaboration session for this document.", {
|
|
115
|
+
source: "Collaboration",
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "collab.copyLink",
|
|
121
|
+
title: "Collaborate: Copy session link",
|
|
122
|
+
category: "Collaborate",
|
|
123
|
+
level: "document",
|
|
124
|
+
menus: ["palette"],
|
|
125
|
+
group: "2_invite",
|
|
126
|
+
requires: "a live collaboration session",
|
|
127
|
+
when: () => Boolean(tab()?.documentPath),
|
|
128
|
+
enablement: sessionLive,
|
|
129
|
+
run: async () => {
|
|
130
|
+
const active = tab();
|
|
131
|
+
if (!active) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const link = sessionLink(active);
|
|
135
|
+
try {
|
|
136
|
+
await navigator.clipboard.writeText(link);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
notify.error(
|
|
139
|
+
`Could not copy the session link — ${error instanceof Error ? error.message : String(error)}`,
|
|
140
|
+
{ detail: link, source: "Collaboration" },
|
|
141
|
+
);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
notify.success("Session link copied. It names the room; it does not grant access to it.", {
|
|
145
|
+
source: "Collaboration",
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "collab.follow",
|
|
151
|
+
title: "Collaborate: Follow a collaborator",
|
|
152
|
+
category: "Collaborate",
|
|
153
|
+
level: "document",
|
|
154
|
+
menus: ["palette"],
|
|
155
|
+
group: "3_presence",
|
|
156
|
+
requires: "another person in this session",
|
|
157
|
+
when: () => Boolean(tab()?.documentPath),
|
|
158
|
+
enablement: () => {
|
|
159
|
+
const active = tab();
|
|
160
|
+
return Boolean(active && collabState(active).peers.length > 0);
|
|
161
|
+
},
|
|
162
|
+
args: {
|
|
163
|
+
additionalProperties: false,
|
|
164
|
+
properties: {
|
|
165
|
+
login: stringProperty("The collaborator's login, as shown on their presence chip."),
|
|
166
|
+
},
|
|
167
|
+
required: [],
|
|
168
|
+
type: "object",
|
|
169
|
+
},
|
|
170
|
+
run: (_ctx, args) => {
|
|
171
|
+
const active = tab();
|
|
172
|
+
if (!active) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const login = (args as { login?: string } | undefined)?.login;
|
|
176
|
+
const { peers } = collabState(active);
|
|
177
|
+
const peer = login ? peers.find((p) => p.state.user.login === login) : peers[0];
|
|
178
|
+
if (!peer) {
|
|
179
|
+
notify.warn(
|
|
180
|
+
login ? `${login} is not in this session.` : "Nobody else is in this session.",
|
|
181
|
+
{ source: "Collaboration" },
|
|
182
|
+
);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const where = peer.state.focusedPath;
|
|
186
|
+
if (!where) {
|
|
187
|
+
notify.info(`${peer.state.user.login} is not in a document right now.`, {
|
|
188
|
+
source: "Collaboration",
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
notify.info(`${peer.state.user.login} is in ${where}.`, {
|
|
193
|
+
action: "file.open",
|
|
194
|
+
actionArgs: { path: where },
|
|
195
|
+
source: "Collaboration",
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: "collab.showStatus",
|
|
201
|
+
title: "Collaborate: What is happening in this document?",
|
|
202
|
+
category: "Collaborate",
|
|
203
|
+
level: "document",
|
|
204
|
+
menus: ["palette"],
|
|
205
|
+
group: "3_presence",
|
|
206
|
+
/* THE DOCUMENT PATH, like its four peers. `sessionLink` above says outright that the path IS
|
|
207
|
+
the session key, so a session cannot exist for a tab that has none — and this asked only
|
|
208
|
+
for a tab, so on an untitled document the palette showed four Collaborate rows missing and
|
|
209
|
+
this one present, reporting on a session that cannot be started. */
|
|
210
|
+
requires: "an open document",
|
|
211
|
+
when: () => Boolean(tab()?.documentPath),
|
|
212
|
+
run: () => {
|
|
213
|
+
const active = tab();
|
|
214
|
+
if (!active) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const state = collabState(active);
|
|
218
|
+
/* This is the honesty command: every one of these states used to be either invisible or
|
|
219
|
+
spelled "detached", and the freeze in particular looked exactly like a bug (§7.4). */
|
|
220
|
+
const lines = [
|
|
221
|
+
`Status: ${state.status}${state.attachError ? ` — ${state.attachError}` : ""}`,
|
|
222
|
+
`People here: ${state.peers.length === 0 ? "just you" : state.peers.map((p) => p.state.user.login).join(", ")}`,
|
|
223
|
+
state.readOnly
|
|
224
|
+
? "You have read access: your edits stay on this machine."
|
|
225
|
+
: "You can publish edits to this session.",
|
|
226
|
+
state.sourceCanonical
|
|
227
|
+
? "Someone holds the code view, so structural edits are paused. This is not an error."
|
|
228
|
+
: "Structural editing is available.",
|
|
229
|
+
"Undo only takes back your own edits, never a collaborator's.",
|
|
230
|
+
];
|
|
231
|
+
notify.info(lines[0]!, { detail: lines.join("\n"), source: "Collaboration" });
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Register the Collaborate family.
|
|
239
|
+
*
|
|
240
|
+
* @param {CommandRegistry} registry
|
|
241
|
+
*/
|
|
242
|
+
export function registerCollabCommands(registry: CommandRegistry): void {
|
|
243
|
+
registry.registerAll(collabCommands());
|
|
244
|
+
}
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
* evaluation defers until a collab session actually attaches.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { effect, onScopeDispose, toRaw } from "../reactivity";
|
|
23
|
+
import { effect, onScopeDispose, reactive, toRaw } from "../reactivity";
|
|
24
24
|
import { getPlatform } from "../platform";
|
|
25
25
|
import { jsonClone } from "../utils/studio-utils";
|
|
26
|
+
import { cloneSelection } from "../tabs/selection";
|
|
26
27
|
import {
|
|
27
28
|
applyExternalDocOps,
|
|
28
29
|
isBatching,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
setTransactObserver,
|
|
33
34
|
transactDoc,
|
|
34
35
|
} from "../tabs/transact";
|
|
36
|
+
import { PROJECT_CONFIG_PATH } from "../tabs/tab";
|
|
35
37
|
import type { TransactOrigin } from "../tabs/transact";
|
|
36
38
|
import type { TransactionRecord } from "../tabs/patch-ops";
|
|
37
39
|
import type { Tab } from "../tabs/tab";
|
|
@@ -40,6 +42,7 @@ import type { CollabHandle } from "@jxsuite/collab/provider";
|
|
|
40
42
|
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
41
43
|
import type * as CollabNS from "@jxsuite/collab";
|
|
42
44
|
import { collabState, registerCollabPath, unregisterCollabPath } from "./collab-state";
|
|
45
|
+
import { notify } from "../services/notify";
|
|
43
46
|
|
|
44
47
|
type CollabModule = typeof CollabNS;
|
|
45
48
|
|
|
@@ -116,13 +119,26 @@ interface ActiveSession {
|
|
|
116
119
|
applyingRemoteFrontmatter: boolean;
|
|
117
120
|
synced: boolean;
|
|
118
121
|
canWrite: boolean;
|
|
119
|
-
/** True while THIS client is in the code view (its structural freeze exempts itself). */
|
|
120
|
-
inSourceMode: boolean;
|
|
121
122
|
mirrorTimer: ReturnType<typeof setTimeout> | null;
|
|
122
123
|
/** When the shared source text was last brought up to date; bounds the debounce's max lag. */
|
|
123
124
|
lastMirrorAt?: number;
|
|
124
125
|
/** Debounce for the source reconciler's Y.Text → structure parse mirror. */
|
|
125
126
|
sourceParseTimer: ReturnType<typeof setTimeout> | null;
|
|
127
|
+
/**
|
|
128
|
+
* Which source surface currently speaks for this client's canonical lock.
|
|
129
|
+
*
|
|
130
|
+
* The lock is a room-level fact with no owner, and `leave()` reaches it from exactly one place —
|
|
131
|
+
* the cleanup {@link canvas/canvas-render.ts}'s `createSourceCollabBinding` returns. Toggling Code
|
|
132
|
+
* view off and on inside one round trip runs two mounts that overlap: the second `enter()` has
|
|
133
|
+
* already re-flipped the lock by the time the first mount's cleanup fires, and that cleanup
|
|
134
|
+
* released a lock the live mount was holding — leaving a co-edited buffer bound to a Y.Text that
|
|
135
|
+
* the room no longer treats as canonical, with the structure mirror free to serialize over it.
|
|
136
|
+
*
|
|
137
|
+
* A token, minted synchronously by each `enter()`, is the identity the release checks. Call
|
|
138
|
+
* order, not resolution order: `enter()` awaits a serialization, so the last mount to be ASKED
|
|
139
|
+
* must win regardless of which round trip returns first.
|
|
140
|
+
*/
|
|
141
|
+
sourceLockOwner: object | null;
|
|
126
142
|
disposers: (() => void)[];
|
|
127
143
|
}
|
|
128
144
|
|
|
@@ -132,6 +148,14 @@ interface TabRuntime {
|
|
|
132
148
|
generation: number;
|
|
133
149
|
session: ActiveSession | null;
|
|
134
150
|
attaching: Promise<void> | null;
|
|
151
|
+
/**
|
|
152
|
+
* The user asked to leave this document's session.
|
|
153
|
+
*
|
|
154
|
+
* Reactive, because the watcher effect that owns attach/detach has to re-run when it changes —
|
|
155
|
+
* which is also why leaving is a STATE rather than a teardown verb: the effect stays the single
|
|
156
|
+
* owner of the session lifecycle, and `Collaborate: Stop sharing` sets a flag it reads.
|
|
157
|
+
*/
|
|
158
|
+
optedOut: { value: boolean };
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
const runtimes = new WeakMap<Tab, TabRuntime>();
|
|
@@ -149,7 +173,13 @@ function runtimeFor(tab: Tab): TabRuntime {
|
|
|
149
173
|
const key = rawTab(tab);
|
|
150
174
|
let runtime = runtimes.get(key);
|
|
151
175
|
if (!runtime) {
|
|
152
|
-
runtime = {
|
|
176
|
+
runtime = {
|
|
177
|
+
attaching: null,
|
|
178
|
+
generation: 0,
|
|
179
|
+
optedOut: reactive({ value: false }),
|
|
180
|
+
session: null,
|
|
181
|
+
watcherInstalled: false,
|
|
182
|
+
};
|
|
153
183
|
runtimes.set(key, runtime);
|
|
154
184
|
}
|
|
155
185
|
return runtime;
|
|
@@ -166,11 +196,25 @@ function installGlobalHooks(): void {
|
|
|
166
196
|
hooksInstalled = true;
|
|
167
197
|
setTransactObserver(onTransact);
|
|
168
198
|
setBatchEndNotifier(onBatchEnd);
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
/* Soft-freeze structural editing while ANYONE holds source-canonical (remote origins pass — they
|
|
200
|
+
ARE the reconciler's mirror of the frozen representation).
|
|
201
|
+
**Including the client holding the lock, which used to be exempt** (`&& !session.inSourceMode`).
|
|
202
|
+
The exemption assumed a carrier that does not exist: a structural edit made by the lock holder
|
|
203
|
+
never reaches the shared `Y.Text`, because `scheduleMirror` returns early while canonical is
|
|
204
|
+
`"source"`, and nothing notices, because the source observer fires on TEXT changes only. So the
|
|
205
|
+
tree and the text diverged, held apart until `leave()` — which runs `sourceParseNow` before
|
|
206
|
+
releasing the lock, parses the UNCHANGED text back into a tree, and reverts the author's edit
|
|
207
|
+
under `MIRROR_ORIGIN`. It then arrives at every peer through `applyExternalDocOps` (origin
|
|
208
|
+
`"remote"`, which passes this gate by design), so the layer they deleted in the Outline
|
|
209
|
+
reappeared seconds later with no explanation — and they were the one client this toast was
|
|
210
|
+
never shown to.
|
|
211
|
+
Mirroring the other way while source is canonical is not the alternative: it is exactly the
|
|
212
|
+
round trip `services/monaco-buffer.ts`'s clause 5 exists to refuse. Clause 5's own reasoning
|
|
213
|
+
settles this one — if the CRDT owns that text and the tree is DERIVED from it, then nobody may
|
|
214
|
+
edit the tree directly, and "nobody" includes whoever is holding the pen. */
|
|
171
215
|
setTransactGate((tab) => {
|
|
172
216
|
const session = runtimeOf(tab)?.session;
|
|
173
|
-
if (session?.synced && collabState(tab).sourceCanonical
|
|
217
|
+
if (session?.synced && collabState(tab).sourceCanonical) {
|
|
174
218
|
_notify("Source editing in progress — structural edits are paused");
|
|
175
219
|
return "source-canonical";
|
|
176
220
|
}
|
|
@@ -424,10 +468,20 @@ export function collabSourceContext(tab: Tab): {
|
|
|
424
468
|
return null;
|
|
425
469
|
}
|
|
426
470
|
const { collab, handle } = session;
|
|
471
|
+
/* THIS CONTEXT'S CLAIM ON THE LOCK, and the reason a release needs one.
|
|
472
|
+
One context object is handed to one mount, and `leave()` is called by that mount's cleanup —
|
|
473
|
+
but the lock it releases is the SESSION's, shared by every mount this client makes. Toggling
|
|
474
|
+
Code view off and on inside one round trip overlaps two of them, and the first one's cleanup
|
|
475
|
+
handed back a lock the second one had just taken. The token is minted synchronously at
|
|
476
|
+
`enter()` so the ordering is the order the surfaces were asked for, not the order two
|
|
477
|
+
serializations happened to resolve in. */
|
|
478
|
+
let claim: object | null = null;
|
|
479
|
+
const holdsClaim = () => claim !== null && session.sourceLockOwner === claim;
|
|
427
480
|
return {
|
|
428
481
|
awareness: handle.awareness,
|
|
429
482
|
enter: async () => {
|
|
430
|
-
|
|
483
|
+
claim = {};
|
|
484
|
+
session.sourceLockOwner = claim;
|
|
431
485
|
const local = handle.awareness.getLocalState();
|
|
432
486
|
if (local) {
|
|
433
487
|
handle.awareness.setLocalState({ ...local, mode: "source" });
|
|
@@ -443,6 +497,11 @@ export function collabSourceContext(tab: Tab): {
|
|
|
443
497
|
serialized = null;
|
|
444
498
|
}
|
|
445
499
|
}
|
|
500
|
+
// Asked again across the await: a later mount owns the surface now, and seeding the shared
|
|
501
|
+
// Text from THIS mount's serialization would push a stale document at the room.
|
|
502
|
+
if (!holdsClaim()) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
446
505
|
collab.acquireSourceCanonical(
|
|
447
506
|
handle.doc,
|
|
448
507
|
serialized ?? collab.sourceText(handle.doc).toString(),
|
|
@@ -450,19 +509,44 @@ export function collabSourceContext(tab: Tab): {
|
|
|
450
509
|
);
|
|
451
510
|
},
|
|
452
511
|
leave: () => {
|
|
453
|
-
|
|
512
|
+
// Not this mount's lock to hand back, and not its awareness state to reset either — the live
|
|
513
|
+
// Mount is in source mode and says so.
|
|
514
|
+
if (!holdsClaim()) {
|
|
515
|
+
claim = null;
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
claim = null;
|
|
519
|
+
session.sourceLockOwner = null;
|
|
454
520
|
const local = handle.awareness.getLocalState();
|
|
455
521
|
if (local) {
|
|
456
522
|
handle.awareness.setLocalState({ ...local, mode: "structure" });
|
|
457
523
|
}
|
|
524
|
+
/* A client with no write access never reached `acquireSourceCanonical` above, so it holds
|
|
525
|
+
nothing to hand back — and `releaseSourceCanonical` is itself a write to shared state,
|
|
526
|
+
which the relay drops from a read-only socket anyway. The guard is right; it was never the
|
|
527
|
+
asymmetry. See below for the one that was. */
|
|
458
528
|
if (!session.canWrite) {
|
|
459
529
|
return;
|
|
460
530
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
531
|
+
/* AND A READ-ONLY VIEWER MUST NOT COUNT AS SOMEONE STILL HOLDING THE LOCK.
|
|
532
|
+
`otherSourceEditors` answers "who else has Code view open", per awareness, which is a
|
|
533
|
+
different question from the one asked here: who else could be the reason not to release.
|
|
534
|
+
A read-only guest publishes `mode: "source"` when they open Code view and then returns at
|
|
535
|
+
the guard above — they can neither acquire the lock nor hand it back. Counted, they made
|
|
536
|
+
the LAST WRITE-CAPABLE editor's departure release nothing, and their own departure release
|
|
537
|
+
nothing: `meta.canonical` stayed `"source"` for the whole room, forever, and with the
|
|
538
|
+
lock-holder exemption gone from the transact gate that is every client frozen out of every
|
|
539
|
+
structural edit, with only the keyed "structural edits are paused" toast to explain it.
|
|
540
|
+
The same `canWrite !== false` filter the reconciler election above uses, for the same
|
|
541
|
+
reason: a client that cannot write cannot be the one holding a write. */
|
|
542
|
+
const others = collab
|
|
543
|
+
.otherSourceEditors(handle.awareness, session.path, handle.awareness.clientID)
|
|
544
|
+
.filter((clientId) => {
|
|
545
|
+
const peer = handle.awareness.getStates().get(clientId) as
|
|
546
|
+
| { canWrite?: boolean }
|
|
547
|
+
| undefined;
|
|
548
|
+
return peer?.canWrite !== false;
|
|
549
|
+
});
|
|
466
550
|
if (others.length === 0) {
|
|
467
551
|
// Freshen the structure mirror once more, then hand the lock back.
|
|
468
552
|
void sourceParseNow(session).then(() => {
|
|
@@ -476,22 +560,43 @@ export function collabSourceContext(tab: Tab): {
|
|
|
476
560
|
};
|
|
477
561
|
}
|
|
478
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Whether this client is in `tab`'s session **without write access**.
|
|
565
|
+
*
|
|
566
|
+
* The single spelling of "this tab cannot be saved by me". A read-only client edits its local
|
|
567
|
+
* reactive document freely — nothing blocks structural editing, and `transactDoc` still marks the
|
|
568
|
+
* tab dirty — but `onTransact` gates BOTH `publishRecord` and `scheduleMirror` behind `canWrite`,
|
|
569
|
+
* so those edits exist in this browser and nowhere else: not in the Y-doc, not on the relay, not on
|
|
570
|
+
* disk. Every question that turns on "is this work recoverable from somewhere?" has to ask this
|
|
571
|
+
* one, which is why it is exported rather than re-derived per caller.
|
|
572
|
+
*/
|
|
573
|
+
export function collabReadOnly(tab: Tab): boolean {
|
|
574
|
+
const state = collabState(tab);
|
|
575
|
+
return state.active && state.readOnly;
|
|
576
|
+
}
|
|
577
|
+
|
|
479
578
|
/**
|
|
480
579
|
* Cmd+S for a collab tab: refresh the source mirror and ask the provider to persist now. Returns
|
|
481
|
-
* false when the tab has no active session (caller saves through the file path as usual)
|
|
580
|
+
* false when the tab has no active session (caller saves through the file path as usual), and false
|
|
581
|
+
* when this client cannot write to the session it does have.
|
|
582
|
+
*
|
|
583
|
+
* **The read-only `false` is the load-bearing one.** This used to skip `mirrorNow` for a read-only
|
|
584
|
+
* client and then flush and return `true` anyway — flushing a Y-doc that had received none of the
|
|
585
|
+
* edits, and reporting the result as a save. `saveFile` stamped "Saved just now" on it and the tab
|
|
586
|
+
* strip's Save button closed the tab on top of work that was still only in the browser. Answering
|
|
587
|
+
* `false` is the truth; `saveFile` is where the read-only tab is refused outright, so this never
|
|
588
|
+
* becomes a licence to write the file behind the room's back.
|
|
482
589
|
*/
|
|
483
590
|
export async function collabSave(tab: Tab): Promise<boolean> {
|
|
484
591
|
const session = runtimeOf(tab)?.session;
|
|
485
|
-
if (!session?.synced) {
|
|
592
|
+
if (!session?.synced || !session.canWrite) {
|
|
486
593
|
return false;
|
|
487
594
|
}
|
|
488
595
|
if (session.mirrorTimer) {
|
|
489
596
|
clearTimeout(session.mirrorTimer);
|
|
490
597
|
session.mirrorTimer = null;
|
|
491
598
|
}
|
|
492
|
-
|
|
493
|
-
await mirrorNow(session);
|
|
494
|
-
}
|
|
599
|
+
await mirrorNow(session);
|
|
495
600
|
await session.handle.flush();
|
|
496
601
|
return true;
|
|
497
602
|
}
|
|
@@ -513,12 +618,26 @@ const liveTabs = new Set<Tab>();
|
|
|
513
618
|
|
|
514
619
|
/**
|
|
515
620
|
* Idempotently wire collaboration for a tab. Installs a per-tab watcher that attaches a session for
|
|
516
|
-
* the tab's file
|
|
517
|
-
*
|
|
621
|
+
* the tab's file unless the author has opted out, and tears everything down when the tab's scope is
|
|
622
|
+
* disposed.
|
|
623
|
+
*
|
|
624
|
+
* The watcher used to carry a second condition — detach while the tab is "drilled into" a
|
|
625
|
+
* sub-document — guarding a `session.documentStack` that nothing could ever push onto. It never
|
|
626
|
+
* fired, and a tab holds one document now: drilling in opens a tab of its own, which gets its own
|
|
627
|
+
* watcher for its own file.
|
|
628
|
+
*
|
|
629
|
+
* **`project.json` is out of replication.** It is the one document whose edits arrive from surfaces
|
|
630
|
+
* that are not the canvas — every settings form, the imports panel, the deploy flow — and whose
|
|
631
|
+
* value the studio ITSELF reads to configure formats, extensions, schemas and the style cascade. A
|
|
632
|
+
* shared Y.Doc over it would mean a peer's extension list reconfiguring your editor mid-keystroke,
|
|
633
|
+
* and the source-canonical freeze would pause configuration edits that have nothing to do with
|
|
634
|
+
* text. Returning here is the whole gate: no session is attached, so `setHistoryDelegate` is never
|
|
635
|
+
* called for this tab and its history stays the local op log `tabs/project-config.ts` transacts
|
|
636
|
+
* on.
|
|
518
637
|
*/
|
|
519
638
|
export function ensureCollab(tab: Tab): void {
|
|
520
639
|
const platform = maybePlatform();
|
|
521
|
-
if (!platform?.collab || !tab.documentPath) {
|
|
640
|
+
if (!platform?.collab || !tab.documentPath || tab.documentPath === PROJECT_CONFIG_PATH) {
|
|
522
641
|
return;
|
|
523
642
|
}
|
|
524
643
|
const runtime = runtimeFor(tab);
|
|
@@ -530,8 +649,7 @@ export function ensureCollab(tab: Tab): void {
|
|
|
530
649
|
liveTabs.add(tab);
|
|
531
650
|
tab.scope.run(() => {
|
|
532
651
|
effect(() => {
|
|
533
|
-
|
|
534
|
-
if (drilled) {
|
|
652
|
+
if (runtime.optedOut.value) {
|
|
535
653
|
detachSession(tab);
|
|
536
654
|
} else {
|
|
537
655
|
void attachSession(tab);
|
|
@@ -544,6 +662,33 @@ export function ensureCollab(tab: Tab): void {
|
|
|
544
662
|
});
|
|
545
663
|
}
|
|
546
664
|
|
|
665
|
+
/**
|
|
666
|
+
* Join or leave this document's collaboration session — the idempotent setter behind `Collaborate:
|
|
667
|
+
* Share this document` and `Collaborate: Stop sharing` (§7.4).
|
|
668
|
+
*
|
|
669
|
+
* Leaving sets a flag the watcher effect reads rather than calling `detachSession` directly, so the
|
|
670
|
+
* effect remains the one owner of the session lifecycle and re-joining is the same call with the
|
|
671
|
+
* other value. Calling it twice with the same value does nothing, which is the property the
|
|
672
|
+
* `app-commands` pairing test checks for.
|
|
673
|
+
*
|
|
674
|
+
* @param {Tab} tab @param {boolean} enabled
|
|
675
|
+
*/
|
|
676
|
+
export function setCollabEnabled(tab: Tab, enabled: boolean): void {
|
|
677
|
+
const runtime = runtimeFor(tab);
|
|
678
|
+
if (runtime.optedOut.value === !enabled) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
runtime.optedOut.value = !enabled;
|
|
682
|
+
if (!runtime.watcherInstalled && enabled) {
|
|
683
|
+
ensureCollab(tab);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** True while the user has not opted this tab out of collaboration. */
|
|
688
|
+
export function isCollabEnabled(tab: Tab): boolean {
|
|
689
|
+
return runtimeOf(tab)?.optedOut.value !== true;
|
|
690
|
+
}
|
|
691
|
+
|
|
547
692
|
/** Re-key after a file rename: tear down and re-attach against the new path. */
|
|
548
693
|
export function rekeyCollab(tab: Tab): void {
|
|
549
694
|
const runtime = runtimeOf(tab);
|
|
@@ -563,15 +708,20 @@ async function attachSession(tab: Tab): Promise<void> {
|
|
|
563
708
|
const { documentPath: path } = tab;
|
|
564
709
|
const platform = maybePlatform();
|
|
565
710
|
if (!path || !platform?.collab) {
|
|
711
|
+
/* Not a failure and not a session: this build/platform has no collaboration to offer. Saying
|
|
712
|
+
so is the difference between "nobody else is here" and "something broke" (§7.4). */
|
|
713
|
+
collabState(tab).status = "unavailable";
|
|
566
714
|
return;
|
|
567
715
|
}
|
|
568
716
|
const { generation } = runtime;
|
|
569
717
|
const state = collabState(tab);
|
|
570
718
|
state.status = "connecting";
|
|
719
|
+
state.attachError = "";
|
|
571
720
|
const attempt = (async () => {
|
|
572
721
|
try {
|
|
573
722
|
const [collab, handle] = await Promise.all([loadCollab(), platform.collab!(path)]);
|
|
574
723
|
if (!handle) {
|
|
724
|
+
// The platform offers collaboration; this document is simply not shared. Solo, not broken.
|
|
575
725
|
state.status = "detached";
|
|
576
726
|
return;
|
|
577
727
|
}
|
|
@@ -598,10 +748,20 @@ async function attachSession(tab: Tab): Promise<void> {
|
|
|
598
748
|
state.active = true;
|
|
599
749
|
state.status = "synced";
|
|
600
750
|
state.readOnly = !runtime.session.canWrite;
|
|
751
|
+
state.attachError = "";
|
|
601
752
|
registerCollabPath(path);
|
|
602
|
-
} catch {
|
|
603
|
-
|
|
753
|
+
} catch (error) {
|
|
754
|
+
/* An attach that threw is a FAILURE, and it used to be reported as "detached" — the same
|
|
755
|
+
value a solo document carries. A relay that is down, a token that expired and a document
|
|
756
|
+
nobody shared were one indistinguishable state (§7.4). */
|
|
757
|
+
state.status = "failed";
|
|
604
758
|
state.active = false;
|
|
759
|
+
state.attachError = error instanceof Error ? error.message : String(error);
|
|
760
|
+
notify.warn(`Live collaboration is not available for "${path}" — ${state.attachError}`, {
|
|
761
|
+
key: `collab:${path}`,
|
|
762
|
+
path,
|
|
763
|
+
source: "Collaboration",
|
|
764
|
+
});
|
|
605
765
|
} finally {
|
|
606
766
|
runtime.attaching = null;
|
|
607
767
|
}
|
|
@@ -621,6 +781,7 @@ function detachSession(tab: Tab): void {
|
|
|
621
781
|
const state = collabState(tab);
|
|
622
782
|
state.active = false;
|
|
623
783
|
state.status = "detached";
|
|
784
|
+
state.attachError = "";
|
|
624
785
|
state.peers = [];
|
|
625
786
|
if (!session) {
|
|
626
787
|
return;
|
|
@@ -644,7 +805,7 @@ function detachSession(tab: Tab): void {
|
|
|
644
805
|
tab.history.snapshots = [
|
|
645
806
|
{
|
|
646
807
|
document: jsonClone(toRaw(tab.doc.document)) as Record<string, unknown>,
|
|
647
|
-
selection: tab.session.selection
|
|
808
|
+
selection: cloneSelection(tab.session.selection),
|
|
648
809
|
},
|
|
649
810
|
];
|
|
650
811
|
tab.history.index = 0;
|
|
@@ -667,10 +828,10 @@ function createSession(
|
|
|
667
828
|
collab,
|
|
668
829
|
disposers: [],
|
|
669
830
|
handle,
|
|
670
|
-
inSourceMode: false,
|
|
671
831
|
lastSeenRef: toRaw(tab.doc.document) as object,
|
|
672
832
|
mirrorTimer: null,
|
|
673
833
|
path,
|
|
834
|
+
sourceLockOwner: null,
|
|
674
835
|
sourceParseTimer: null,
|
|
675
836
|
synced: false,
|
|
676
837
|
tab,
|
|
@@ -742,11 +903,11 @@ function createSession(
|
|
|
742
903
|
);
|
|
743
904
|
session.undoManager = undoManager;
|
|
744
905
|
const onStackAdd = ({ stackItem }: { stackItem: { meta: Map<unknown, unknown> } }) => {
|
|
745
|
-
stackItem.meta.set("selection", tab.session.selection
|
|
906
|
+
stackItem.meta.set("selection", cloneSelection(tab.session.selection));
|
|
746
907
|
};
|
|
747
908
|
const onStackPop = ({ stackItem }: { stackItem: { meta: Map<unknown, unknown> } }) => {
|
|
748
|
-
const selection = stackItem.meta.get("selection") as (string | number)[] |
|
|
749
|
-
tab.session.selection = selection
|
|
909
|
+
const selection = stackItem.meta.get("selection") as (string | number)[][] | undefined;
|
|
910
|
+
tab.session.selection = cloneSelection(selection ?? []);
|
|
750
911
|
};
|
|
751
912
|
undoManager.on("stack-item-added", onStackAdd);
|
|
752
913
|
undoManager.on("stack-item-popped", onStackPop);
|
|
@@ -882,7 +1043,12 @@ function createSession(
|
|
|
882
1043
|
// FocusedPath, so per-doc boxes come free from the one project-level awareness state). The
|
|
883
1044
|
// Plain `selection` field is y-monaco's (in-buffer text cursors) — never write it here.
|
|
884
1045
|
effect(() => {
|
|
885
|
-
|
|
1046
|
+
// The whole selection SET crosses awareness, so a peer's canvas draws every node the author
|
|
1047
|
+
// Has selected. A selection of one publishes a one-entry list, which is the same box.
|
|
1048
|
+
// Empty stays `null` rather than `[]`: "this peer is not pointing at anything" is one fact
|
|
1049
|
+
// With one wire value, and every consumer already tests it by presence.
|
|
1050
|
+
const structuralSelection =
|
|
1051
|
+
tab.session.selection.length > 0 ? cloneSelection(tab.session.selection) : null;
|
|
886
1052
|
const local = handle.awareness.getLocalState();
|
|
887
1053
|
if (local) {
|
|
888
1054
|
handle.awareness.setLocalState({ ...local, structuralSelection });
|