@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/ui/layers.ts
CHANGED
|
@@ -1,24 +1,55 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
* The overlay layers — the only sanctioned way to open a popover, modal, or
|
|
3
|
+
* The overlay layers — the only sanctioned way to open a popover, modal, dialog or toast in Studio.
|
|
4
4
|
*
|
|
5
|
-
* Everything renders into one of the
|
|
6
|
-
* #layer-modal, #layer-dialog), bound once at boot by initLayers(). Native browser
|
|
7
|
-
* (`prompt`, `confirm`, `alert`) are not permitted anywhere in Studio — use
|
|
8
|
-
* `showConfirmDialog`, or `showSaveDiscardDialog`. See studio-ui-guidelines.md
|
|
5
|
+
* Everything renders into one of the four fixed hosts declared in index.html (#layer-popover,
|
|
6
|
+
* #layer-modal, #layer-dialog, #layer-toast), bound once at boot by initLayers(). Native browser
|
|
7
|
+
* dialogs (`prompt`, `confirm`, `alert`) are not permitted anywhere in Studio — use
|
|
8
|
+
* `showPromptDialog`, `showConfirmDialog`, or `showSaveDiscardDialog`. See studio-ui-guidelines.md
|
|
9
|
+
* §8.7.
|
|
10
|
+
*
|
|
11
|
+
* The toast host is the fourth layer (plan §3.2 ④, §7.1). It is a rendering of
|
|
12
|
+
* `services/notify.ts`'s `toasts` array and owns exactly two things that array does not: the timer
|
|
13
|
+
* that retires a resting toast, and the transition account {@link overlayIdleBlockers} publishes.
|
|
9
14
|
*/
|
|
10
15
|
import { html, render as litRender, nothing } from "lit-html";
|
|
11
16
|
import { ref } from "lit-html/directives/ref.js";
|
|
17
|
+
import { repeat } from "lit-html/directives/repeat.js";
|
|
18
|
+
import { overlayRegion, REGION_ATTR } from "./regions";
|
|
19
|
+
import { dismiss, toasts } from "../services/notify";
|
|
20
|
+
import { activeRegistry } from "../commands/active-registry";
|
|
21
|
+
import { effect, effectScope } from "../reactivity";
|
|
22
|
+
import type { Notification, Severity } from "../services/notify";
|
|
23
|
+
import type { EffectScope } from "@vue/reactivity";
|
|
12
24
|
import type { TemplateResult } from "lit-html";
|
|
13
25
|
|
|
26
|
+
/** The four fixed layer hosts, by name. Also the `kind` half of every overlay region id. */
|
|
27
|
+
export type LayerKind = "popover" | "modal" | "dialog" | "toast";
|
|
28
|
+
|
|
14
29
|
let _popoverLayer: HTMLElement;
|
|
15
30
|
let _modalLayer: HTMLElement;
|
|
16
31
|
let _dialogLayer: HTMLElement;
|
|
32
|
+
let _toastLayer: HTMLElement;
|
|
33
|
+
|
|
34
|
+
/** The host a layer kind renders into, falling back to `<body>` before `initLayers()` has run. */
|
|
35
|
+
function layerHost(kind: LayerKind): HTMLElement {
|
|
36
|
+
const host =
|
|
37
|
+
kind === "popover"
|
|
38
|
+
? _popoverLayer
|
|
39
|
+
: kind === "modal"
|
|
40
|
+
? _modalLayer
|
|
41
|
+
: kind === "toast"
|
|
42
|
+
? _toastLayer
|
|
43
|
+
: _dialogLayer;
|
|
44
|
+
return host || document.body;
|
|
45
|
+
}
|
|
17
46
|
|
|
18
47
|
export function initLayers() {
|
|
19
48
|
_popoverLayer = document.querySelector("#layer-popover") as HTMLElement;
|
|
20
49
|
_modalLayer = document.querySelector("#layer-modal") as HTMLElement;
|
|
21
50
|
_dialogLayer = document.querySelector("#layer-dialog") as HTMLElement;
|
|
51
|
+
_toastLayer = document.querySelector("#layer-toast") as HTMLElement;
|
|
52
|
+
mountToastHost();
|
|
22
53
|
}
|
|
23
54
|
|
|
24
55
|
/** Anything in the modal/dialog layers that paints a viewport-wide underlay over the app. */
|
|
@@ -38,25 +69,33 @@ export function isModalOpen(): boolean {
|
|
|
38
69
|
return Boolean(_dialogLayer?.querySelector(UNDERLAID) || _modalLayer?.querySelector(UNDERLAID));
|
|
39
70
|
}
|
|
40
71
|
|
|
41
|
-
/** Focusable candidates in
|
|
72
|
+
/** Focusable candidates in an overlay body, in the order a keyboard user would reach them. */
|
|
42
73
|
const BODY_FOCUSABLE =
|
|
43
|
-
'input, textarea, select, button, sp-textfield, sp-button, sp-picker, sp-checkbox, [tabindex]:not([tabindex="-1"])';
|
|
74
|
+
'a[href], input, textarea, select, button, sp-textfield, sp-button, sp-action-button, sp-picker, sp-checkbox, sp-menu-item, [tabindex]:not([tabindex="-1"])';
|
|
75
|
+
|
|
76
|
+
/** The body's focusables that can actually take the caret right now. */
|
|
77
|
+
function focusablesIn(slot: HTMLElement): HTMLElement[] {
|
|
78
|
+
return [...slot.querySelectorAll<HTMLElement>(BODY_FOCUSABLE)].filter(
|
|
79
|
+
(el) => !el.hasAttribute("disabled") && el.getAttribute("aria-hidden") !== "true",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
44
82
|
|
|
45
83
|
/**
|
|
46
|
-
* Hand the keyboard to a freshly opened
|
|
84
|
+
* Hand the keyboard to a freshly opened overlay.
|
|
47
85
|
*
|
|
48
86
|
* `sp-dialog-wrapper` only throws focus into itself when an `<sp-overlay>` drives it. Opened
|
|
49
87
|
* directly through its `open` attribute — Studio's pattern, because this layer stack owns stacking
|
|
50
88
|
* rather than Spectrum's overlay system — NOTHING does, so focus stays on whatever sits behind the
|
|
51
|
-
* underlay: the
|
|
89
|
+
* underlay: the surface is unreachable by keyboard, <kbd>Escape</kbd> never reaches it, and
|
|
52
90
|
* keystrokes keep landing in the app the underlay is blocking.
|
|
53
91
|
*
|
|
54
|
-
* Prefers the first focusable in the
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* (
|
|
92
|
+
* Prefers the first focusable in the BODY (a bespoke form's opening field), else the wrapper's own
|
|
93
|
+
* cancel button — DialogWrapper renders cancel → secondary → confirm, so the first shadow button is
|
|
94
|
+
* the least destructive landing spot — else the slot itself, which carries `tabindex="-1"` so a
|
|
95
|
+
* body made only of static content (a progress spinner) still receives <kbd>Escape</kbd>. A body
|
|
96
|
+
* that already claimed focus ({@link showPromptDialog}'s field) is left alone.
|
|
58
97
|
*/
|
|
59
|
-
function
|
|
98
|
+
function focusOverlay(slot: HTMLElement): void {
|
|
60
99
|
// Deferred a frame: the wrapper's buttons live in a shadow root Spectrum renders asynchronously.
|
|
61
100
|
requestAnimationFrame(() => {
|
|
62
101
|
if (!slot.isConnected || slot.contains(document.activeElement)) {
|
|
@@ -64,17 +103,93 @@ function focusDialog(slot: HTMLElement): void {
|
|
|
64
103
|
}
|
|
65
104
|
const wrapper = slot.querySelector("sp-dialog-wrapper");
|
|
66
105
|
const target =
|
|
67
|
-
slot
|
|
68
|
-
|
|
69
|
-
null;
|
|
70
|
-
target?.focus();
|
|
106
|
+
focusablesIn(slot)[0] ?? wrapper?.shadowRoot?.querySelector<HTMLElement>("sp-button") ?? slot;
|
|
107
|
+
target.focus();
|
|
71
108
|
});
|
|
72
109
|
}
|
|
73
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Keep <kbd>Tab</kbd> inside the overlay: cycle through the body's focusables, wrapping at both
|
|
113
|
+
* ends. With no focusable body at all the caret stays on the slot — tabbing out of a surface the
|
|
114
|
+
* mouse cannot leave either would strand the keyboard behind the underlay.
|
|
115
|
+
*/
|
|
116
|
+
function trapTab(slot: HTMLElement, e: KeyboardEvent): void {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
const items = focusablesIn(slot);
|
|
119
|
+
if (items.length === 0) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const at = items.indexOf(document.activeElement as HTMLElement);
|
|
123
|
+
const next = e.shiftKey
|
|
124
|
+
? items[at <= 0 ? items.length - 1 : at - 1]
|
|
125
|
+
: items[at === -1 || at === items.length - 1 ? 0 : at + 1];
|
|
126
|
+
next?.focus();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** How an overlay slot behaves once it is up. */
|
|
130
|
+
interface OverlaySlotOptions {
|
|
131
|
+
/** Layer host the slot is appended to. */
|
|
132
|
+
layer: HTMLElement;
|
|
133
|
+
/** Which layer this is, for the slot's region id. */
|
|
134
|
+
kind: LayerKind;
|
|
135
|
+
/**
|
|
136
|
+
* Optional instance name, making the slot `overlay.<instance>:<id>` instead of the bare
|
|
137
|
+
* `overlay.<instance>`. A surface that can be open alongside another one of its kind wants this.
|
|
138
|
+
*/
|
|
139
|
+
regionId?: string | undefined;
|
|
140
|
+
/** Handle <kbd>Escape</kbd> pressed inside the slot; the callback owns `preventDefault`. */
|
|
141
|
+
onEscape?: (e: KeyboardEvent, slot: HTMLElement) => void;
|
|
142
|
+
/** Cycle <kbd>Tab</kbd> within the slot instead of letting it walk into the app behind. */
|
|
143
|
+
trapFocus?: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Open a slot in a layer with the full overlay keyboard contract: focus in on open, focus back to
|
|
148
|
+
* the opener on close, centralised <kbd>Escape</kbd>, and (optionally) a Tab trap.
|
|
149
|
+
*
|
|
150
|
+
* Both {@link showDialog} and {@link openModal} are thin wrappers over this — one contract, one
|
|
151
|
+
* implementation, so no surface can ship without the machinery.
|
|
152
|
+
*/
|
|
153
|
+
function openOverlaySlot(opts: OverlaySlotOptions): { slot: HTMLElement; release: () => void } {
|
|
154
|
+
const slot = document.createElement("div");
|
|
155
|
+
slot.style.pointerEvents = "auto";
|
|
156
|
+
slot.setAttribute(REGION_ATTR, overlayRegion(opts.kind, opts.regionId));
|
|
157
|
+
// Focusable as a last resort, so a body with no controls still owns the keyboard (focusOverlay).
|
|
158
|
+
slot.tabIndex = -1;
|
|
159
|
+
// The slot is a zero-height wrapper around fixed-position bodies, so its own focus ring would
|
|
160
|
+
// Paint as a stray line across the top of the layer.
|
|
161
|
+
slot.style.outline = "none";
|
|
162
|
+
opts.layer.append(slot);
|
|
163
|
+
// Whoever held focus before the overlay took it, so it can be handed back (a dialog opened from a
|
|
164
|
+
// Toolbar button returns the caret to that button, not to <body>).
|
|
165
|
+
const restoreTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
166
|
+
const onKeydown = (e: KeyboardEvent) => {
|
|
167
|
+
if (e.key === "Escape") {
|
|
168
|
+
opts.onEscape?.(e, slot);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (e.key === "Tab" && opts.trapFocus) {
|
|
172
|
+
trapTab(slot, e);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
slot.addEventListener("keydown", onKeydown);
|
|
176
|
+
return {
|
|
177
|
+
release() {
|
|
178
|
+
slot.removeEventListener("keydown", onKeydown);
|
|
179
|
+
litRender(nothing, slot);
|
|
180
|
+
slot.remove();
|
|
181
|
+
if (restoreTo?.isConnected) {
|
|
182
|
+
restoreTo.focus();
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
slot,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
74
189
|
/**
|
|
75
190
|
* Show an ephemeral dialog. Returns a Promise that resolves when the dialog is dismissed.
|
|
76
191
|
*
|
|
77
|
-
* Takes the keyboard on open ({@link
|
|
192
|
+
* Takes the keyboard on open ({@link focusOverlay}) and hands it back to the previously focused
|
|
78
193
|
* element on close. <kbd>Escape</kbd> dismisses by firing the wrapper's `close` event, so each
|
|
79
194
|
* helper's own `@close` binding decides what "dismissed" resolves to; a bespoke body with no
|
|
80
195
|
* `sp-dialog-wrapper` owns its own keys.
|
|
@@ -85,44 +200,61 @@ function focusDialog(slot: HTMLElement): void {
|
|
|
85
200
|
*/
|
|
86
201
|
export function showDialog<T>(
|
|
87
202
|
templateFn: (done: (value: T) => void) => TemplateResult,
|
|
203
|
+
opts: { region?: string; label?: string } = {},
|
|
88
204
|
): Promise<T> {
|
|
89
205
|
return new Promise((resolve) => {
|
|
90
|
-
const slot =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
206
|
+
const { release, slot } = openOverlaySlot({
|
|
207
|
+
kind: "dialog",
|
|
208
|
+
// `layerHost`, not the raw binding: it is the one that falls back to `<body>`, and reading
|
|
209
|
+
// The binding directly threw before `initLayers()` had run — which is any test that stands up
|
|
210
|
+
// A shell without the four layer hosts, and the boot window before layers are bound.
|
|
211
|
+
layer: layerHost("dialog"),
|
|
212
|
+
regionId: opts.region,
|
|
213
|
+
onEscape(e, host) {
|
|
214
|
+
const wrapper = host.querySelector("sp-dialog-wrapper");
|
|
215
|
+
if (!wrapper) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Stop it ALSO reaching the app behind (which clears the canvas selection on Escape).
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
wrapper.dispatchEvent(new Event("close", { bubbles: true }));
|
|
222
|
+
},
|
|
223
|
+
// No Tab trap: the wrapper's action buttons live in a shadow root a light-DOM cycle cannot
|
|
224
|
+
// Enumerate, so trapping here would strand the caret on the body and never reach Cancel.
|
|
225
|
+
});
|
|
226
|
+
/*
|
|
227
|
+
* The slot IS the dialog, so it says so.
|
|
228
|
+
*
|
|
229
|
+
* `aria-modal` is also the answer to the comment above: it tells assistive technology that
|
|
230
|
+
* everything outside this element is inert, which constrains a screen reader's virtual cursor —
|
|
231
|
+
* the thing a Tab trap cannot reach anyway, since the virtual cursor does not use Tab. So the
|
|
232
|
+
* caret still escapes into the shadow-root buttons, as it must, and a reader is no longer free
|
|
233
|
+
* to wander the page behind a modal that is covering it.
|
|
234
|
+
*/
|
|
235
|
+
slot.setAttribute("role", "dialog");
|
|
236
|
+
slot.setAttribute("aria-modal", "true");
|
|
96
237
|
let resolved = false;
|
|
97
|
-
const onKeydown = (e: KeyboardEvent) => {
|
|
98
|
-
if (e.key !== "Escape") {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const wrapper = slot.querySelector("sp-dialog-wrapper");
|
|
102
|
-
if (!wrapper) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
// Stop it ALSO reaching the app behind (which clears the canvas selection on Escape).
|
|
106
|
-
e.preventDefault();
|
|
107
|
-
e.stopPropagation();
|
|
108
|
-
wrapper.dispatchEvent(new Event("close", { bubbles: true }));
|
|
109
|
-
};
|
|
110
|
-
slot.addEventListener("keydown", onKeydown);
|
|
111
238
|
const done = (value: T) => {
|
|
112
239
|
if (resolved) {
|
|
113
240
|
return;
|
|
114
241
|
}
|
|
115
242
|
resolved = true;
|
|
116
|
-
|
|
117
|
-
litRender(nothing, slot);
|
|
118
|
-
slot.remove();
|
|
119
|
-
if (restoreTo?.isConnected) {
|
|
120
|
-
restoreTo.focus();
|
|
121
|
-
}
|
|
243
|
+
release();
|
|
122
244
|
resolve(value);
|
|
123
245
|
};
|
|
124
246
|
litRender(templateFn(done), slot);
|
|
125
|
-
|
|
247
|
+
/*
|
|
248
|
+
* The name, after render, because the usual source of one is the wrapper's own `headline` —
|
|
249
|
+
* which does not exist until the template has run. An explicit `label` wins; a dialog with
|
|
250
|
+
* neither is nameless, which is a defect in the caller rather than something to invent here.
|
|
251
|
+
*/
|
|
252
|
+
const headline =
|
|
253
|
+
opts.label ?? slot.querySelector("sp-dialog-wrapper")?.getAttribute("headline") ?? null;
|
|
254
|
+
if (headline !== null && headline !== "") {
|
|
255
|
+
slot.setAttribute("aria-label", headline);
|
|
256
|
+
}
|
|
257
|
+
focusOverlay(slot);
|
|
126
258
|
});
|
|
127
259
|
}
|
|
128
260
|
|
|
@@ -144,7 +276,9 @@ export function showConfirmDialog(
|
|
|
144
276
|
} = {},
|
|
145
277
|
) {
|
|
146
278
|
const { confirmLabel = "Confirm", cancelLabel = "Cancel", destructive = false } = opts;
|
|
147
|
-
|
|
279
|
+
// Explicit, because `done(true)` gives the generic nothing to infer from and it landed on
|
|
280
|
+
// `unknown` — which every caller happened to survive by using the answer in a truthy position.
|
|
281
|
+
return showDialog<boolean>(
|
|
148
282
|
(done) => html`
|
|
149
283
|
<sp-dialog-wrapper
|
|
150
284
|
open
|
|
@@ -361,20 +495,67 @@ export function showPromptDialog(
|
|
|
361
495
|
});
|
|
362
496
|
}
|
|
363
497
|
|
|
498
|
+
/** Options accepted by {@link openModal}. */
|
|
499
|
+
export interface ModalOptions {
|
|
500
|
+
/**
|
|
501
|
+
* Accessible name for the modal, applied as `aria-label` on the wrapper. Required: it is the only
|
|
502
|
+
* name assistive tech gets, and a per-modal opt-in would be forgotten.
|
|
503
|
+
*/
|
|
504
|
+
label: string;
|
|
505
|
+
/**
|
|
506
|
+
* Whether <kbd>Escape</kbd> dismisses. `false` for modals that must not vanish mid-flight (a
|
|
507
|
+
* running operation, a step that has to be confirmed).
|
|
508
|
+
*/
|
|
509
|
+
dismissible?: boolean;
|
|
510
|
+
/**
|
|
511
|
+
* What <kbd>Escape</kbd> runs. Defaults to the handle's own `close()`; pass the call site's close
|
|
512
|
+
* function when it keeps bookkeeping of its own (a module-level handle to clear).
|
|
513
|
+
*/
|
|
514
|
+
onDismiss?: () => void;
|
|
515
|
+
/**
|
|
516
|
+
* Instance name for this modal's region — `overlay.dialog:settings`.
|
|
517
|
+
*
|
|
518
|
+
* Optional because one modal at a time is the norm and `overlay.dialog` addresses it. A modal
|
|
519
|
+
* that can be open beside another, or that a command needs to move focus back into by name,
|
|
520
|
+
* declares one.
|
|
521
|
+
*/
|
|
522
|
+
region?: string;
|
|
523
|
+
}
|
|
524
|
+
|
|
364
525
|
/**
|
|
365
526
|
* Open a persistent modal. Returns a handle with update() and close() methods.
|
|
366
527
|
*
|
|
528
|
+
* The wrapper — not the body — owns the modal contract, so no surface can ship without it: the slot
|
|
529
|
+
* is the `role="dialog"` element, carries `aria-modal` and the caller's label, takes the keyboard
|
|
530
|
+
* on open, cycles <kbd>Tab</kbd> within itself, dismisses on <kbd>Escape</kbd>, and hands focus
|
|
531
|
+
* back to the opener on close. Bodies render content only.
|
|
532
|
+
*
|
|
367
533
|
* @param {import("lit-html").TemplateResult} template
|
|
534
|
+
* @param {ModalOptions} opts
|
|
368
535
|
*/
|
|
369
|
-
export function openModal(template: TemplateResult) {
|
|
370
|
-
const slot =
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
536
|
+
export function openModal(template: TemplateResult, opts: ModalOptions) {
|
|
537
|
+
const { release, slot } = openOverlaySlot({
|
|
538
|
+
kind: "modal",
|
|
539
|
+
layer: layerHost("modal"),
|
|
540
|
+
onEscape(e) {
|
|
541
|
+
if (opts.dismissible === false) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// Stop it ALSO reaching the app behind (which clears the canvas selection on Escape).
|
|
545
|
+
e.preventDefault();
|
|
546
|
+
e.stopPropagation();
|
|
547
|
+
(opts.onDismiss ?? handle.close)();
|
|
548
|
+
},
|
|
549
|
+
regionId: opts.region,
|
|
550
|
+
trapFocus: true,
|
|
551
|
+
});
|
|
552
|
+
slot.setAttribute("role", "dialog");
|
|
553
|
+
slot.setAttribute("aria-modal", "true");
|
|
554
|
+
slot.setAttribute("aria-label", opts.label);
|
|
555
|
+
|
|
556
|
+
const handle = {
|
|
375
557
|
close() {
|
|
376
|
-
|
|
377
|
-
slot.remove();
|
|
558
|
+
release();
|
|
378
559
|
},
|
|
379
560
|
host: slot,
|
|
380
561
|
/** @param {import("lit-html").TemplateResult} tpl */
|
|
@@ -382,6 +563,9 @@ export function openModal(template: TemplateResult) {
|
|
|
382
563
|
litRender(tpl, slot);
|
|
383
564
|
},
|
|
384
565
|
};
|
|
566
|
+
litRender(template, slot);
|
|
567
|
+
focusOverlay(slot);
|
|
568
|
+
return handle;
|
|
385
569
|
}
|
|
386
570
|
|
|
387
571
|
/**
|
|
@@ -391,7 +575,8 @@ export function openModal(template: TemplateResult) {
|
|
|
391
575
|
* @param {{
|
|
392
576
|
* dismissOnOutsideClick?: boolean;
|
|
393
577
|
* onDismiss?: () => void;
|
|
394
|
-
* layer?:
|
|
578
|
+
* layer?: LayerKind;
|
|
579
|
+
* region?: string;
|
|
395
580
|
* }} [opts]
|
|
396
581
|
*/
|
|
397
582
|
export function renderPopover(
|
|
@@ -399,14 +584,16 @@ export function renderPopover(
|
|
|
399
584
|
opts: {
|
|
400
585
|
dismissOnOutsideClick?: boolean;
|
|
401
586
|
onDismiss?: () => void;
|
|
402
|
-
layer?:
|
|
587
|
+
layer?: LayerKind;
|
|
588
|
+
/** Instance name for this popover's region — `overlay.menu:blockbar`. */
|
|
589
|
+
region?: string;
|
|
403
590
|
} = {},
|
|
404
591
|
) {
|
|
592
|
+
const kind = opts.layer ?? "popover";
|
|
405
593
|
const slot = document.createElement("div");
|
|
406
594
|
slot.style.pointerEvents = "auto";
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
target.append(slot);
|
|
595
|
+
slot.setAttribute(REGION_ATTR, overlayRegion(kind, opts.region));
|
|
596
|
+
layerHost(kind).append(slot);
|
|
410
597
|
litRender(template, slot);
|
|
411
598
|
|
|
412
599
|
let outsideClickHandler: ((e: MouseEvent) => void) | null = null;
|
|
@@ -428,6 +615,15 @@ export function renderPopover(
|
|
|
428
615
|
dismiss() {
|
|
429
616
|
if (outsideClickHandler) {
|
|
430
617
|
document.removeEventListener("mousedown", outsideClickHandler, true);
|
|
618
|
+
/* Disarm the PENDING arming too, not just the armed listener. The `addEventListener` above
|
|
619
|
+
is deferred a frame so the click that opened this popover cannot immediately close it —
|
|
620
|
+
so a popover dismissed within that frame (open the same menu twice in one frame, which a
|
|
621
|
+
double-click does) would otherwise be armed AFTER its own death: a document-wide capture
|
|
622
|
+
listener on a detached slot, never removed, that answers the next mousedown by calling
|
|
623
|
+
its owner's `onDismiss`. Owners null their handle field there, so the corpse's callback
|
|
624
|
+
cleared the pointer to the LIVE popover and stranded it on screen, un-dismissable. The
|
|
625
|
+
`if` in the rAF was always written for this; nothing had ever nulled the variable. */
|
|
626
|
+
outsideClickHandler = null;
|
|
431
627
|
}
|
|
432
628
|
litRender(nothing, slot);
|
|
433
629
|
slot.remove();
|
|
@@ -444,13 +640,39 @@ export function renderPopover(
|
|
|
444
640
|
const _namedSlots = new Map<string, HTMLElement>();
|
|
445
641
|
|
|
446
642
|
/**
|
|
447
|
-
* Get or create a named slot in a layer. Useful for persistent popovers like zoom indicator.
|
|
643
|
+
* Get or create a named slot in a layer. Useful for persistent popovers like the zoom indicator.
|
|
644
|
+
*
|
|
645
|
+
* The `${layer}:${id}` key this already builds IS the slot's region: it is stamped as
|
|
646
|
+
* `overlay.<instance>:<id>`, so naming a slot names its region and every persistent overlay becomes
|
|
647
|
+
* addressable for free. That also settles a latent ambiguity — two open popovers used to be
|
|
648
|
+
* indistinguishable to anything matching on `sp-popover[open]`, and each now answers to its own
|
|
649
|
+
* id.
|
|
448
650
|
*
|
|
449
|
-
* @param {
|
|
651
|
+
* @param {LayerKind} layer
|
|
450
652
|
* @param {string} id
|
|
451
653
|
* @returns {HTMLElement}
|
|
452
654
|
*/
|
|
453
|
-
|
|
655
|
+
/**
|
|
656
|
+
* The layer a transient popover must use to appear ABOVE the surface that opened it.
|
|
657
|
+
*
|
|
658
|
+
* The four layer hosts are sibling stacking contexts (`index.html`): popover 1000, modal 2000,
|
|
659
|
+
* dialog 3000, toast 4000. So a popover anchored to a control INSIDE a modal — the media picker's
|
|
660
|
+
* Browse button in Search appearance, say — renders into a layer that paints entirely beneath the
|
|
661
|
+
* modal body, and the author clicks Browse and sees nothing happen. Putting it in the modal's own
|
|
662
|
+
* layer makes it a later sibling of the modal body instead, which is exactly the relationship it
|
|
663
|
+
* should have: above the surface that opened it, below any dialog.
|
|
664
|
+
*
|
|
665
|
+
* @param {Element | null} anchor The control the popover is anchored to.
|
|
666
|
+
* @returns {LayerKind}
|
|
667
|
+
*/
|
|
668
|
+
export function popoverLayerFor(anchor: Element | null): LayerKind {
|
|
669
|
+
if (anchor?.closest("#layer-dialog")) {
|
|
670
|
+
return "dialog";
|
|
671
|
+
}
|
|
672
|
+
return anchor?.closest("#layer-modal") ? "modal" : "popover";
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export function getLayerSlot(layer: LayerKind, id: string) {
|
|
454
676
|
const key = `${layer}:${id}`;
|
|
455
677
|
let slot = _namedSlots.get(key);
|
|
456
678
|
if (slot && slot.parentElement) {
|
|
@@ -459,10 +681,8 @@ export function getLayerSlot(layer: "popover" | "modal" | "dialog", id: string)
|
|
|
459
681
|
|
|
460
682
|
slot = document.createElement("div");
|
|
461
683
|
slot.style.pointerEvents = "auto";
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
document.body;
|
|
465
|
-
target.append(slot);
|
|
684
|
+
slot.setAttribute(REGION_ATTR, overlayRegion(layer, id));
|
|
685
|
+
layerHost(layer).append(slot);
|
|
466
686
|
_namedSlots.set(key, slot);
|
|
467
687
|
return slot;
|
|
468
688
|
}
|
|
@@ -470,10 +690,10 @@ export function getLayerSlot(layer: "popover" | "modal" | "dialog", id: string)
|
|
|
470
690
|
/**
|
|
471
691
|
* Clear a named layer slot (remove from DOM and map).
|
|
472
692
|
*
|
|
473
|
-
* @param {
|
|
693
|
+
* @param {LayerKind} layer
|
|
474
694
|
* @param {string} id
|
|
475
695
|
*/
|
|
476
|
-
export function clearLayerSlot(layer:
|
|
696
|
+
export function clearLayerSlot(layer: LayerKind, id: string) {
|
|
477
697
|
const key = `${layer}:${id}`;
|
|
478
698
|
const slot = _namedSlots.get(key);
|
|
479
699
|
if (slot) {
|
|
@@ -482,3 +702,206 @@ export function clearLayerSlot(layer: "popover" | "modal" | "dialog", id: string
|
|
|
482
702
|
_namedSlots.delete(key);
|
|
483
703
|
}
|
|
484
704
|
}
|
|
705
|
+
|
|
706
|
+
// ─── The toast host — the fourth layer ───────────────────────────────────────
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* How long a toast is settling into place, in ms.
|
|
710
|
+
*
|
|
711
|
+
* Published to {@link overlayIdleBlockers} rather than kept private, because "the shell has stopped
|
|
712
|
+
* moving" is a question `services/idle.ts` answers on behalf of the screenshot runner and the
|
|
713
|
+
* verify skill, and a toast sliding in is exactly the half-painted frame a capture must not catch.
|
|
714
|
+
*/
|
|
715
|
+
export const TOAST_ENTER_MS = 180;
|
|
716
|
+
|
|
717
|
+
/** The severity glyphs. One character each: the toast's job is a line of text, not an illustration. */
|
|
718
|
+
const TOAST_ICON: Readonly<Record<Severity, string>> = {
|
|
719
|
+
error: "✕",
|
|
720
|
+
info: "ℹ",
|
|
721
|
+
success: "✓",
|
|
722
|
+
warn: "!",
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
let _toastScope: EffectScope | null = null;
|
|
726
|
+
/** Toast id → its retirement timer, so a re-render never schedules a second one. */
|
|
727
|
+
const _toastTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
728
|
+
/** Toast id → when it started settling in. Cleared by its own timer. */
|
|
729
|
+
const _toastEntering = new Set<string>();
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Whether the reader has asked for less movement.
|
|
733
|
+
*
|
|
734
|
+
* Read per call rather than cached: §13.3 clause 6 says the app must HONOUR this rather than have
|
|
735
|
+
* the runner inject a freeze stylesheet, which means the answer has to be able to change.
|
|
736
|
+
*/
|
|
737
|
+
function reducedMotion(): boolean {
|
|
738
|
+
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* **One of the three listed exceptions to §13.3 clause 6** — behaviour that differs under
|
|
743
|
+
* `?automation=1` beyond installing the hook, pinning the clock and selecting a profile. The other
|
|
744
|
+
* Two are both refusals to touch the project on disk: `packages/ensure-deps.ts` declines to run
|
|
745
|
+
* `bun install`, and `packages/jxsuite-update.ts` declines to prompt for a dependency update. This
|
|
746
|
+
* One is the only exception that changes what a picture SHOWS rather than what it avoids doing.
|
|
747
|
+
*
|
|
748
|
+
* A toast is the single surface in the app whose lifetime is a TIMER rather than a state. Every
|
|
749
|
+
* other surface a shot can photograph is there because the app is in a state, and it stays there
|
|
750
|
+
* until a command changes it; a toast retires itself between the step that raised it and the frame
|
|
751
|
+
* that captures it, so a shot of one is a race the manifest has no way to express. Holding it open
|
|
752
|
+
* is the smaller lie: the picture then shows a toast that a human reader would also have seen, for
|
|
753
|
+
* as long as they cared to look, instead of showing the toast that happened to still be there.
|
|
754
|
+
*
|
|
755
|
+
* The gate is re-derived from `location.search` rather than imported from `services/automation.ts`
|
|
756
|
+
* deliberately: §13.3 requires the scripting surface to be absent from the desktop and cloud
|
|
757
|
+
* bundles (`check-bundle-budget.ts`'s next assertion), and importing it from a module every layer
|
|
758
|
+
* pulls in would ship it everywhere. One line of duplication, on purpose.
|
|
759
|
+
*/
|
|
760
|
+
export function toastsAreHeld(): boolean {
|
|
761
|
+
try {
|
|
762
|
+
return new URLSearchParams(globalThis.location?.search ?? "").get("automation") === "1";
|
|
763
|
+
} catch {
|
|
764
|
+
// Intentionally ignored: no location (a bare test realm) means no automation.
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* What is still moving in the overlay layers, as `services/idle.ts` phrases it.
|
|
771
|
+
*
|
|
772
|
+
* Empty when every toast has settled — a RESTING toast is not a blocker, which is the property that
|
|
773
|
+
* lets {@link toastsAreHeld} hold one open forever without `probeIdle()` waiting forever with it.
|
|
774
|
+
*/
|
|
775
|
+
export function overlayIdleBlockers(): readonly string[] {
|
|
776
|
+
return [..._toastEntering].map((id) => `overlay: toast ${id} settling in`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** Take a toast away, cancelling any timer it still owns. */
|
|
780
|
+
function retireToast(id: string): void {
|
|
781
|
+
const timer = _toastTimers.get(id);
|
|
782
|
+
if (timer !== undefined) {
|
|
783
|
+
clearTimeout(timer);
|
|
784
|
+
_toastTimers.delete(id);
|
|
785
|
+
}
|
|
786
|
+
_toastEntering.delete(id);
|
|
787
|
+
dismiss(id);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Give a newly-arrived toast its timers: the settle window the idle account reads, and (unless
|
|
792
|
+
* held) the rest period after which it retires itself.
|
|
793
|
+
*/
|
|
794
|
+
function scheduleToast(record: Notification): void {
|
|
795
|
+
if (_toastTimers.has(record.id) || _toastEntering.has(record.id)) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const settle = reducedMotion() ? 0 : TOAST_ENTER_MS;
|
|
799
|
+
if (settle > 0) {
|
|
800
|
+
_toastEntering.add(record.id);
|
|
801
|
+
setTimeout(() => _toastEntering.delete(record.id), settle);
|
|
802
|
+
}
|
|
803
|
+
const rest = record.timeoutMs ?? 0;
|
|
804
|
+
if (rest <= 0 || toastsAreHeld()) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
_toastTimers.set(
|
|
808
|
+
record.id,
|
|
809
|
+
setTimeout(() => retireToast(record.id), rest),
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** The recovery button, or `nothing` when the record named no command or the command is hidden. */
|
|
814
|
+
function toastActionTpl(record: Notification) {
|
|
815
|
+
const registry = record.action === undefined ? null : activeRegistry();
|
|
816
|
+
const id = record.action;
|
|
817
|
+
if (!registry || id === undefined || !registry.get(id) || !registry.isVisible(id)) {
|
|
818
|
+
return nothing;
|
|
819
|
+
}
|
|
820
|
+
const command = registry.get(id)!;
|
|
821
|
+
const reason = registry.disabledReason(id);
|
|
822
|
+
return html`
|
|
823
|
+
<button
|
|
824
|
+
class="toast-action"
|
|
825
|
+
?disabled=${reason !== undefined}
|
|
826
|
+
title=${reason === undefined ? command.title : `${command.title} — requires ${reason}`}
|
|
827
|
+
@click=${() => {
|
|
828
|
+
retireToast(record.id);
|
|
829
|
+
void registry.run(id, record.actionArgs);
|
|
830
|
+
}}
|
|
831
|
+
>
|
|
832
|
+
${command.title}
|
|
833
|
+
</button>
|
|
834
|
+
`;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** One toast. `role="status"` lives on the HOST, so a stack of them is announced as one region. */
|
|
838
|
+
function toastTpl(record: Notification) {
|
|
839
|
+
return html`
|
|
840
|
+
<div class="toast toast--${record.severity}">
|
|
841
|
+
<span class="toast-icon" aria-hidden="true">${TOAST_ICON[record.severity]}</span>
|
|
842
|
+
<span class="toast-message">${record.message}</span>
|
|
843
|
+
${toastActionTpl(record)}
|
|
844
|
+
<button
|
|
845
|
+
class="toast-dismiss"
|
|
846
|
+
title="Dismiss"
|
|
847
|
+
aria-label="Dismiss notification"
|
|
848
|
+
@click=${() => retireToast(record.id)}
|
|
849
|
+
>
|
|
850
|
+
<span aria-hidden="true">×</span>
|
|
851
|
+
</button>
|
|
852
|
+
</div>
|
|
853
|
+
`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/** The whole stack, newest at the bottom — the reading order of a log, not of a menu. */
|
|
857
|
+
export function toastStackTemplate() {
|
|
858
|
+
return html`
|
|
859
|
+
<div class="toast-stack">
|
|
860
|
+
${repeat(
|
|
861
|
+
toasts,
|
|
862
|
+
(record) => record.id,
|
|
863
|
+
(record) => toastTpl(record),
|
|
864
|
+
)}
|
|
865
|
+
</div>
|
|
866
|
+
`;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Subscribe the toast layer to `notify`'s store.
|
|
871
|
+
*
|
|
872
|
+
* Called by {@link initLayers}, so no bootstrap has to remember it, and idempotent so a second call
|
|
873
|
+
* replaces the effect rather than stacking a second renderer on the same host.
|
|
874
|
+
*/
|
|
875
|
+
export function mountToastHost(): void {
|
|
876
|
+
unmountToastHost();
|
|
877
|
+
if (!_toastLayer) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
_toastScope = effectScope();
|
|
881
|
+
_toastScope.run(() => {
|
|
882
|
+
effect(() => {
|
|
883
|
+
// Tracked: the array itself (arrivals and retirements) and the registry holder, so a toast
|
|
884
|
+
// Raised before the bootstrap composed the registry grows its Retry button when it lands.
|
|
885
|
+
void toasts.length;
|
|
886
|
+
void activeRegistry();
|
|
887
|
+
for (const record of toasts) {
|
|
888
|
+
scheduleToast(record);
|
|
889
|
+
}
|
|
890
|
+
litRender(toastStackTemplate(), _toastLayer);
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** Release the effect and every pending timer. Tests and a window teardown both need this. */
|
|
896
|
+
export function unmountToastHost(): void {
|
|
897
|
+
_toastScope?.stop();
|
|
898
|
+
_toastScope = null;
|
|
899
|
+
for (const timer of _toastTimers.values()) {
|
|
900
|
+
clearTimeout(timer);
|
|
901
|
+
}
|
|
902
|
+
_toastTimers.clear();
|
|
903
|
+
_toastEntering.clear();
|
|
904
|
+
if (_toastLayer) {
|
|
905
|
+
litRender(nothing, _toastLayer);
|
|
906
|
+
}
|
|
907
|
+
}
|