@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,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility checks over the document an author is editing — ATAG 2.0 Part B.
|
|
3
|
+
*
|
|
4
|
+
* ATAG splits into two parts, and only one of them was answered here. Part A is "is the authoring
|
|
5
|
+
* tool itself accessible" — the live region, the dialog roles, the focus rings. **Part B is "does
|
|
6
|
+
* the tool help authors produce accessible content"**, and nothing in Studio did: an author could
|
|
7
|
+
* ship a page of unlabelled images and untitled links, and the editor said nothing at all.
|
|
8
|
+
*
|
|
9
|
+
* Three deliberate shapes, all copied from surfaces that already work here:
|
|
10
|
+
*
|
|
11
|
+
* - **No score.** `head-panel.ts` argues this for SEO and the argument transfers verbatim: a single
|
|
12
|
+
* figure out of a hundred aggregates unrelated facts into a verdict, and the verdict is what gets
|
|
13
|
+
* optimised. A named list of things that are wrong is the report.
|
|
14
|
+
* - **Each finding names its criterion, and where it can, a command that REPAIRS it.** That second
|
|
15
|
+
* half is B.3.2 rather than B.3.1 — checking is only half of what the standard asks, and a
|
|
16
|
+
* finding an author cannot act on from where they are told about it is a finding they will not
|
|
17
|
+
* act on.
|
|
18
|
+
* - **A check that could not run says so.** `redirects-grid.ts` files an explicit "this run could not
|
|
19
|
+
* check X" Problem rather than reporting a clean bill, because a report that silently passes what
|
|
20
|
+
* it could not inspect is worse than no report.
|
|
21
|
+
*
|
|
22
|
+
* **What is deliberately not here.** Colour contrast between computed colours, target size in
|
|
23
|
+
* rendered pixels, reading order, focus order, and anything else that depends on layout or the
|
|
24
|
+
* cascade. Those are properties of _built output_ in a browser, not of a document tree — checking
|
|
25
|
+
* them means running the page and axe-core over it, which is a different program with a different
|
|
26
|
+
* lifetime. `unavailableChecks()` names them so the absence is stated rather than implied.
|
|
27
|
+
*
|
|
28
|
+
* @docs studio/interface/problems-and-progress
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { activeTab } from "../workspace/workspace";
|
|
32
|
+
import { clearProblems, notify } from "./notify";
|
|
33
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
34
|
+
import type { JxElement } from "@jxsuite/schema/types";
|
|
35
|
+
|
|
36
|
+
/** WCAG success criteria these checks bind to, by the id ATAG B.3.1 asks a report to carry. */
|
|
37
|
+
export type A11yCriterion = "1.1.1" | "1.3.1" | "1.3.5" | "2.4.4" | "2.4.6" | "3.1.1" | "4.1.2";
|
|
38
|
+
|
|
39
|
+
/** One thing that is wrong with the document, and — where one exists — the command that fixes it. */
|
|
40
|
+
export interface A11yFinding {
|
|
41
|
+
/** Stable id, so a test and a Problem key name the same finding. */
|
|
42
|
+
id: string;
|
|
43
|
+
/** The WCAG criterion this violates. */
|
|
44
|
+
criterion: A11yCriterion;
|
|
45
|
+
/** One sentence, in the author's language, naming what is wrong. */
|
|
46
|
+
message: string;
|
|
47
|
+
/** What to do about it. */
|
|
48
|
+
detail: string;
|
|
49
|
+
/** How bad: an `error` must be fixed to be conformant, a `warn` is very probably wrong. */
|
|
50
|
+
severity: "error" | "warn";
|
|
51
|
+
/**
|
|
52
|
+
* A command that repairs it, when one exists (ATAG B.3.2).
|
|
53
|
+
*
|
|
54
|
+
* Most findings carry none today, and that is stated rather than papered over: "add alt text to
|
|
55
|
+
* this image" has no command, because the repair is an edit to one element and Studio has no verb
|
|
56
|
+
* for it yet. Naming a command that merely re-opens a panel would put a button on the finding
|
|
57
|
+
* that does not do what the button says.
|
|
58
|
+
*/
|
|
59
|
+
action?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Elements whose whole purpose is an image, and which therefore owe alternative text. */
|
|
63
|
+
const IMAGE_TAGS = new Set(["img", "area", "input-image"]);
|
|
64
|
+
|
|
65
|
+
/** Controls that owe an accessible name. */
|
|
66
|
+
const CONTROL_TAGS = new Set(["input", "select", "textarea", "button"]);
|
|
67
|
+
|
|
68
|
+
/** Input types that are not controls a person labels — a submit button names itself. */
|
|
69
|
+
const SELF_LABELLING_INPUTS = new Set(["button", "hidden", "image", "reset", "submit"]);
|
|
70
|
+
|
|
71
|
+
/** Link text that names no destination. WCAG 2.4.4's canonical failures. */
|
|
72
|
+
const VAGUE_LINK_TEXT = new Set([
|
|
73
|
+
"click here",
|
|
74
|
+
"here",
|
|
75
|
+
"read more",
|
|
76
|
+
"more",
|
|
77
|
+
"learn more",
|
|
78
|
+
"this",
|
|
79
|
+
"link",
|
|
80
|
+
"download",
|
|
81
|
+
"continue",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
/** The attribute bag, whatever shape the author wrote it in. */
|
|
85
|
+
function attrs(node: JxElement): Record<string, unknown> {
|
|
86
|
+
return (node.attributes ?? {}) as Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** An attribute's literal string value, or null when it is absent or a binding. */
|
|
90
|
+
function literal(node: JxElement, name: string): string | null {
|
|
91
|
+
const value = attrs(node)[name];
|
|
92
|
+
return typeof value === "string" ? value : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether an attribute is present at all, however it was written. */
|
|
96
|
+
function has(node: JxElement, name: string): boolean {
|
|
97
|
+
return name in attrs(node);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The element's own text, when it is a literal rather than a binding. */
|
|
101
|
+
function ownText(node: JxElement): string {
|
|
102
|
+
const parts: string[] = [];
|
|
103
|
+
if (typeof node.textContent === "string") {
|
|
104
|
+
parts.push(node.textContent);
|
|
105
|
+
}
|
|
106
|
+
const { children } = node;
|
|
107
|
+
if (Array.isArray(children)) {
|
|
108
|
+
for (const child of children) {
|
|
109
|
+
if (typeof child === "string") {
|
|
110
|
+
parts.push(child);
|
|
111
|
+
} else if (child && typeof child === "object") {
|
|
112
|
+
parts.push(ownText(child));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return parts.join(" ").trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Walk every element in the tree, including the ones inside `$switch` cases.
|
|
121
|
+
*
|
|
122
|
+
* @param {JxElement} node
|
|
123
|
+
* @yields {JxElement} Each element, in document order.
|
|
124
|
+
*/
|
|
125
|
+
function* walk(node: JxElement): Generator<JxElement> {
|
|
126
|
+
yield node;
|
|
127
|
+
const { children } = node;
|
|
128
|
+
if (Array.isArray(children)) {
|
|
129
|
+
for (const child of children) {
|
|
130
|
+
if (child && typeof child === "object") {
|
|
131
|
+
yield* walk(child);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const branch of Object.values(node.cases ?? {})) {
|
|
136
|
+
if (branch && typeof branch === "object") {
|
|
137
|
+
yield* walk(branch);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Whether the element carries a name any accessible-name computation would find. */
|
|
143
|
+
function hasAccessibleName(node: JxElement): boolean {
|
|
144
|
+
return (
|
|
145
|
+
literal(node, "aria-label") !== null ||
|
|
146
|
+
literal(node, "aria-labelledby") !== null ||
|
|
147
|
+
has(node, "aria-label") ||
|
|
148
|
+
has(node, "aria-labelledby") ||
|
|
149
|
+
(typeof node.title === "string" && node.title !== "") ||
|
|
150
|
+
ownText(node) !== ""
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Every finding in a document.
|
|
156
|
+
*
|
|
157
|
+
* @param {JxElement} doc
|
|
158
|
+
* @returns {A11yFinding[]} In document order, so an author reads them the way they read the page.
|
|
159
|
+
*/
|
|
160
|
+
export function checkDocument(doc: JxElement): A11yFinding[] {
|
|
161
|
+
const findings: A11yFinding[] = [];
|
|
162
|
+
const seenIds = new Map<string, number>();
|
|
163
|
+
let h1Count = 0;
|
|
164
|
+
let previousHeading = 0;
|
|
165
|
+
let index = 0;
|
|
166
|
+
|
|
167
|
+
for (const node of walk(doc)) {
|
|
168
|
+
index += 1;
|
|
169
|
+
const tag = typeof node.tagName === "string" ? node.tagName.toLowerCase() : "";
|
|
170
|
+
const where = tag === "" ? `element ${index}` : `<${tag}>`;
|
|
171
|
+
|
|
172
|
+
// ── 1.1.1 Non-text Content ────────────────────────────────────────────────
|
|
173
|
+
if (IMAGE_TAGS.has(tag)) {
|
|
174
|
+
const alt = literal(node, "alt");
|
|
175
|
+
if (!has(node, "alt")) {
|
|
176
|
+
findings.push({
|
|
177
|
+
criterion: "1.1.1",
|
|
178
|
+
detail:
|
|
179
|
+
"A screen reader falls back to reading the file name, which is rarely useful and " +
|
|
180
|
+
"sometimes misleading. An image that is purely decorative takes an EMPTY alt, which " +
|
|
181
|
+
"is a decision — the absent attribute is not.",
|
|
182
|
+
id: `img-no-alt:${index}`,
|
|
183
|
+
message: `${where} has no alt text.`,
|
|
184
|
+
severity: "error",
|
|
185
|
+
});
|
|
186
|
+
} else if (alt !== null && /^(image|photo|picture|graphic|icon)\b/i.test(alt.trim())) {
|
|
187
|
+
findings.push({
|
|
188
|
+
criterion: "1.1.1",
|
|
189
|
+
detail:
|
|
190
|
+
'A screen reader already announces that it is an image, so "image of a cat" is heard ' +
|
|
191
|
+
'as "image, image of a cat". Describe what it shows.',
|
|
192
|
+
id: `img-alt-redundant:${index}`,
|
|
193
|
+
message: `${where}'s alt text starts by saying it is an image.`,
|
|
194
|
+
severity: "warn",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── 1.3.1 Info and Relationships — heading structure ─────────────────────
|
|
200
|
+
const headingLevel = /^h([1-6])$/.exec(tag)?.[1];
|
|
201
|
+
if (headingLevel !== undefined) {
|
|
202
|
+
const level = Number(headingLevel);
|
|
203
|
+
if (level === 1) {
|
|
204
|
+
h1Count += 1;
|
|
205
|
+
if (h1Count === 2) {
|
|
206
|
+
findings.push({
|
|
207
|
+
criterion: "1.3.1",
|
|
208
|
+
detail:
|
|
209
|
+
"A page has one title, and readers who navigate by heading use the h1 to find it. " +
|
|
210
|
+
"Two make the outline ambiguous — demote the second to an h2.",
|
|
211
|
+
id: "multiple-h1",
|
|
212
|
+
message: "This page has more than one top-level heading.",
|
|
213
|
+
severity: "warn",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (previousHeading !== 0 && level > previousHeading + 1) {
|
|
218
|
+
findings.push({
|
|
219
|
+
criterion: "1.3.1",
|
|
220
|
+
detail:
|
|
221
|
+
`An h${previousHeading} is followed by an h${level}, so the outline has a hole in it. ` +
|
|
222
|
+
"Readers who navigate by heading use the levels as structure, not as sizes — style " +
|
|
223
|
+
"the heading rather than skipping a level to get a smaller one.",
|
|
224
|
+
id: `heading-skip:${index}`,
|
|
225
|
+
message: `A heading level is skipped: h${previousHeading} to h${level}.`,
|
|
226
|
+
severity: "warn",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
previousHeading = level;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── 4.1.2 Name, Role, Value — form controls ──────────────────────────────
|
|
233
|
+
if (CONTROL_TAGS.has(tag)) {
|
|
234
|
+
const type = literal(node, "type")?.toLowerCase() ?? "";
|
|
235
|
+
const selfLabelling = tag === "button" || SELF_LABELLING_INPUTS.has(type);
|
|
236
|
+
const labelled = hasAccessibleName(node) || has(node, "id");
|
|
237
|
+
if (!selfLabelling && !labelled) {
|
|
238
|
+
findings.push({
|
|
239
|
+
criterion: "4.1.2",
|
|
240
|
+
detail:
|
|
241
|
+
'A control with no name is announced as its type alone — "edit", "combo box" — ' +
|
|
242
|
+
"which tells a reader nothing about what to type. Give it an aria-label, or an id a " +
|
|
243
|
+
"<label> points at.",
|
|
244
|
+
id: `control-unnamed:${index}`,
|
|
245
|
+
message: `${where} has no label.`,
|
|
246
|
+
severity: "error",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// 1.3.5: a field asking for the user's own data should say which (autocomplete).
|
|
250
|
+
if (tag === "input" && !selfLabelling && !has(node, "autocomplete")) {
|
|
251
|
+
const name = `${literal(node, "name") ?? ""} ${literal(node, "id") ?? ""}`.toLowerCase();
|
|
252
|
+
if (/\b(email|name|tel|phone|address|postal|zip|country)\b/.test(name)) {
|
|
253
|
+
findings.push({
|
|
254
|
+
criterion: "1.3.5",
|
|
255
|
+
detail:
|
|
256
|
+
"An autocomplete token lets a browser fill the field from the person's own stored " +
|
|
257
|
+
"details, which matters most for people who find typing costly.",
|
|
258
|
+
id: `input-no-autocomplete:${index}`,
|
|
259
|
+
message: `${where} asks for personal data but names no autocomplete purpose.`,
|
|
260
|
+
severity: "warn",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── 2.4.4 Link Purpose ────────────────────────────────────────────────────
|
|
267
|
+
if (tag === "a") {
|
|
268
|
+
const text = ownText(node)
|
|
269
|
+
.toLowerCase()
|
|
270
|
+
.replaceAll(/[^a-z ]/g, "")
|
|
271
|
+
.trim();
|
|
272
|
+
if (!hasAccessibleName(node)) {
|
|
273
|
+
findings.push({
|
|
274
|
+
criterion: "4.1.2",
|
|
275
|
+
detail:
|
|
276
|
+
"A link with no text is announced as its URL, one character at a time in some " +
|
|
277
|
+
"readers. If the link is an icon, give it an aria-label.",
|
|
278
|
+
id: `link-unnamed:${index}`,
|
|
279
|
+
message: "A link has no text.",
|
|
280
|
+
severity: "error",
|
|
281
|
+
});
|
|
282
|
+
} else if (VAGUE_LINK_TEXT.has(text)) {
|
|
283
|
+
findings.push({
|
|
284
|
+
criterion: "2.4.4",
|
|
285
|
+
detail:
|
|
286
|
+
"Readers who navigate by pulling up a list of a page's links see the text alone, with " +
|
|
287
|
+
`no surrounding sentence — so a page of "${text}" is a list of identical entries.`,
|
|
288
|
+
id: `link-vague:${index}`,
|
|
289
|
+
message: `A link reads "${ownText(node)}", which names no destination.`,
|
|
290
|
+
severity: "warn",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (literal(node, "target") === "_blank" && !/new (tab|window)/i.test(ownText(node))) {
|
|
294
|
+
findings.push({
|
|
295
|
+
criterion: "2.4.6",
|
|
296
|
+
detail:
|
|
297
|
+
"A link that replaces the page without warning is disorienting for anyone not " +
|
|
298
|
+
"watching the whole viewport. Say so in the text, or in the aria-label.",
|
|
299
|
+
id: `link-new-window:${index}`,
|
|
300
|
+
message: "A link opens in a new tab without saying so.",
|
|
301
|
+
severity: "warn",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── 2.4.3 Focus Order — a positive tabindex reorders the whole page ──────
|
|
307
|
+
if (typeof node.tabIndex === "number" && node.tabIndex > 0) {
|
|
308
|
+
findings.push({
|
|
309
|
+
criterion: "1.3.1",
|
|
310
|
+
detail:
|
|
311
|
+
"A positive tabindex pulls the element out of document order and in front of every " +
|
|
312
|
+
"element with tabindex 0 — on the whole page, not just this component. Reorder the " +
|
|
313
|
+
"markup instead; 0 and -1 are the only values that compose.",
|
|
314
|
+
id: `positive-tabindex:${index}`,
|
|
315
|
+
message: `${where} has tabindex="${node.tabIndex}".`,
|
|
316
|
+
severity: "error",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── 4.1.1 Parsing — a duplicate id breaks every reference to it ──────────
|
|
321
|
+
if (typeof node.id === "string" && node.id !== "") {
|
|
322
|
+
const seen = seenIds.get(node.id);
|
|
323
|
+
if (seen !== undefined) {
|
|
324
|
+
findings.push({
|
|
325
|
+
criterion: "4.1.2",
|
|
326
|
+
detail:
|
|
327
|
+
"aria-labelledby, aria-controls and <label for> all resolve an id to the FIRST match, " +
|
|
328
|
+
"so a duplicate silently points half the references at the wrong element.",
|
|
329
|
+
id: `duplicate-id:${node.id}`,
|
|
330
|
+
message: `The id "${node.id}" is used more than once.`,
|
|
331
|
+
severity: "error",
|
|
332
|
+
});
|
|
333
|
+
} else {
|
|
334
|
+
seenIds.set(node.id, index);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── 1.4.2 Audio Control ──────────────────────────────────────────────────
|
|
339
|
+
if ((tag === "video" || tag === "audio") && has(node, "autoplay") && !has(node, "controls")) {
|
|
340
|
+
findings.push({
|
|
341
|
+
criterion: "1.3.1",
|
|
342
|
+
detail:
|
|
343
|
+
"Media that starts by itself and cannot be stopped talks over a screen reader, which " +
|
|
344
|
+
"leaves a reader no way to hear the page at all.",
|
|
345
|
+
id: `autoplay-no-controls:${index}`,
|
|
346
|
+
message: `${where} plays automatically with no controls.`,
|
|
347
|
+
severity: "error",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── 3.1.1 Language of Page ────────────────────────────────────────────────
|
|
353
|
+
if (typeof doc.lang !== "string" || doc.lang === "") {
|
|
354
|
+
findings.push({
|
|
355
|
+
criterion: "3.1.1",
|
|
356
|
+
detail:
|
|
357
|
+
"A screen reader picks its pronunciation rules from the page's language. Without one it " +
|
|
358
|
+
"uses the reader's own, so French text is read with English phonetics — intelligible to " +
|
|
359
|
+
"nobody.",
|
|
360
|
+
id: "no-lang",
|
|
361
|
+
message: "This document declares no language.",
|
|
362
|
+
severity: "warn",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return findings;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** A check this program cannot run, and the honest reason. */
|
|
370
|
+
export interface UnavailableCheck {
|
|
371
|
+
id: string;
|
|
372
|
+
message: string;
|
|
373
|
+
detail: string;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* What was NOT checked, and why — filed as a Problem rather than left implied.
|
|
378
|
+
*
|
|
379
|
+
* A report that silently passes what it could not inspect is worse than no report: the author reads
|
|
380
|
+
* "no problems" and takes it to mean the page is fine. Everything below is a property of rendered
|
|
381
|
+
* output, not of a document tree, and answering it means running the page in a browser with an
|
|
382
|
+
* engine like axe-core.
|
|
383
|
+
*
|
|
384
|
+
* @returns {UnavailableCheck[]}
|
|
385
|
+
*/
|
|
386
|
+
export function unavailableChecks(): UnavailableCheck[] {
|
|
387
|
+
return [
|
|
388
|
+
{
|
|
389
|
+
detail:
|
|
390
|
+
"Contrast is between the colours that actually paint, which depends on the cascade, on " +
|
|
391
|
+
"inherited values and on images behind text — none of which exist until the page is " +
|
|
392
|
+
"built and rendered. Checking the declared values alone would pass a black-on-black page " +
|
|
393
|
+
"whose colours came from a class.",
|
|
394
|
+
id: "contrast",
|
|
395
|
+
message: "Colour contrast was not checked.",
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
detail:
|
|
399
|
+
"Target size, focus order and reading order are all properties of the rendered layout. A " +
|
|
400
|
+
"document tree does not have a layout, so this run cannot see them.",
|
|
401
|
+
id: "layout",
|
|
402
|
+
message: "Target size and focus order were not checked.",
|
|
403
|
+
},
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The Problems `source` these findings carry, so a re-run replaces rather than stacks. */
|
|
408
|
+
export const A11Y_PROBLEM_SOURCE = "Accessibility";
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Re-file the accessibility Problems for a document: one per finding, plus one naming what this run
|
|
412
|
+
* could not check.
|
|
413
|
+
*
|
|
414
|
+
* Previous findings are cleared first, so a fixed page stops being listed without anyone having
|
|
415
|
+
* kept its record id — the same shape `reportRedirectProblems` uses, for the same reason.
|
|
416
|
+
*
|
|
417
|
+
* @param {JxElement} doc
|
|
418
|
+
* @param {string} [path] The document's path, so a Problem row can open the file it is about.
|
|
419
|
+
* @returns {number} How many Problems were filed, including the unchecked-coverage ones.
|
|
420
|
+
*/
|
|
421
|
+
export function reportA11yProblems(doc: JxElement, path?: string): number {
|
|
422
|
+
clearProblems((record) => record.source === A11Y_PROBLEM_SOURCE);
|
|
423
|
+
const findings = checkDocument(doc);
|
|
424
|
+
for (const finding of findings) {
|
|
425
|
+
notify(finding.severity, finding.message, {
|
|
426
|
+
detail: `WCAG ${finding.criterion} — ${finding.detail}`,
|
|
427
|
+
key: `a11y.${finding.id}`,
|
|
428
|
+
source: A11Y_PROBLEM_SOURCE,
|
|
429
|
+
tier: "problem",
|
|
430
|
+
...(finding.action === undefined ? {} : { action: finding.action }),
|
|
431
|
+
...(path === undefined ? {} : { path }),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
/*
|
|
435
|
+
* And then the honest half. A report that lists nothing reads as "this page is accessible", which
|
|
436
|
+
* is a claim this run cannot make: everything below is a property of rendered output. Saying so
|
|
437
|
+
* costs two rows and is the difference between a check and a reassurance.
|
|
438
|
+
*/
|
|
439
|
+
for (const check of unavailableChecks()) {
|
|
440
|
+
notify.info(check.message, {
|
|
441
|
+
action: "document.checkAccessibility",
|
|
442
|
+
detail: check.detail,
|
|
443
|
+
key: `a11y.unavailable.${check.id}`,
|
|
444
|
+
source: A11Y_PROBLEM_SOURCE,
|
|
445
|
+
tier: "problem",
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return findings.length + unavailableChecks().length;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* The command that runs the check.
|
|
453
|
+
*
|
|
454
|
+
* Registered like `document.openSeo` — same category, same level, same "an open document"
|
|
455
|
+
* requirement — because it is the same kind of thing: a report over the document in front of you,
|
|
456
|
+
* reached from the palette and from the assistant.
|
|
457
|
+
*
|
|
458
|
+
* @returns {AnyCommand[]}
|
|
459
|
+
*/
|
|
460
|
+
export function a11yCommands(): AnyCommand[] {
|
|
461
|
+
return [
|
|
462
|
+
{
|
|
463
|
+
aiTool: {
|
|
464
|
+
description:
|
|
465
|
+
"Check the open document for accessibility problems an author can fix — missing alt " +
|
|
466
|
+
"text, unlabelled controls, skipped heading levels, vague link text, duplicate ids — " +
|
|
467
|
+
"and file each as a Problem naming its WCAG criterion.",
|
|
468
|
+
name: "check_accessibility",
|
|
469
|
+
},
|
|
470
|
+
category: "Document",
|
|
471
|
+
group: "2_document",
|
|
472
|
+
id: "document.checkAccessibility",
|
|
473
|
+
level: "document",
|
|
474
|
+
menus: ["palette"],
|
|
475
|
+
requires: "an open document",
|
|
476
|
+
run: () => {
|
|
477
|
+
const tab = activeTab.value;
|
|
478
|
+
if (!tab) {
|
|
479
|
+
throw new RangeError(`command "document.checkAccessibility" needs an open document`);
|
|
480
|
+
}
|
|
481
|
+
const filed = reportA11yProblems(tab.doc.document, tab.documentPath ?? undefined);
|
|
482
|
+
notify.success(
|
|
483
|
+
filed === unavailableChecks().length
|
|
484
|
+
? "No accessibility problems found in this document."
|
|
485
|
+
: `${filed - unavailableChecks().length} accessibility problem(s) filed.`,
|
|
486
|
+
{ key: "a11y.run", source: A11Y_PROBLEM_SOURCE },
|
|
487
|
+
);
|
|
488
|
+
},
|
|
489
|
+
title: "Check Accessibility",
|
|
490
|
+
when: (ctx) => ctx.document.open,
|
|
491
|
+
},
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Register it.
|
|
497
|
+
*
|
|
498
|
+
* A registrar rather than only a factory: `appCommandSet()` and the running app are two different
|
|
499
|
+
* code paths, and a record in one and not the other is a command CI counts and the app cannot run.
|
|
500
|
+
*
|
|
501
|
+
* @param {CommandRegistry} registry
|
|
502
|
+
*/
|
|
503
|
+
export function registerA11yCommands(registry: CommandRegistry): void {
|
|
504
|
+
registry.registerAll(a11yCommands());
|
|
505
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import type { AiModelsResponse } from "@jxsuite/protocol";
|
|
15
15
|
import { getPlatform } from "../platform";
|
|
16
|
-
import { getBaseUrl, getOpenAiKey } from "./ai-settings";
|
|
16
|
+
import { getBaseUrl, getOpenAiKey, hasOpenAiKey } from "./ai-settings";
|
|
17
17
|
|
|
18
18
|
export interface AiModel {
|
|
19
19
|
id: string;
|
|
@@ -25,12 +25,56 @@ let proxyConfigured = false;
|
|
|
25
25
|
let proxyManaged = false;
|
|
26
26
|
let proxyDefaultModel = "";
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* One-shot capability probe shared by every credentials gate, plus the hosts to repaint when it
|
|
30
|
+
* settles. Managed platforms (cloud Workers AI) and env-keyed dev servers report their state from
|
|
31
|
+
* /models, so the gate cannot know which paths to offer until this has run once.
|
|
32
|
+
*/
|
|
33
|
+
let proxyProbe: Promise<void> | null = null;
|
|
34
|
+
const probeListeners = new Set<() => void>();
|
|
35
|
+
|
|
28
36
|
/** Drop the cached model list (call after credentials/endpoint changes). */
|
|
29
37
|
export function invalidateModelCache() {
|
|
30
38
|
cache = null;
|
|
31
39
|
proxyConfigured = false;
|
|
32
40
|
proxyManaged = false;
|
|
33
41
|
proxyDefaultModel = "";
|
|
42
|
+
/* The probe's result IS the three flags above. Clearing them while keeping the settled promise
|
|
43
|
+
would strand every gate on a permanent "unconfigured, unmanaged" reading — ensureProxyProbe
|
|
44
|
+
would no-op forever and the managed option would vanish until a full reload. */
|
|
45
|
+
proxyProbe = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run the capability probe once, repainting `onSettled` when it lands. Idempotent: concurrent and
|
|
50
|
+
* repeated callers share the single in-flight fetch, and every registered host is notified. Probe
|
|
51
|
+
* failure is not surfaced — an unreachable proxy simply leaves the gate showing the key form.
|
|
52
|
+
*
|
|
53
|
+
* @param {() => void} [onSettled] - Host re-render scheduler, registered for this and later probes.
|
|
54
|
+
*/
|
|
55
|
+
export function ensureProxyProbe(onSettled?: () => void) {
|
|
56
|
+
if (onSettled) {
|
|
57
|
+
probeListeners.add(onSettled);
|
|
58
|
+
}
|
|
59
|
+
proxyProbe ??= fetchAvailableModels({ force: true })
|
|
60
|
+
.catch(() => {
|
|
61
|
+
// Unreachable proxy — the key gate stays up.
|
|
62
|
+
})
|
|
63
|
+
.then(() => {
|
|
64
|
+
for (const notify of probeListeners) {
|
|
65
|
+
notify();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether the assistant can run at all: a key stored in this browser, or a backend that holds
|
|
72
|
+
* credentials itself (managed cloud platforms, env-keyed dev servers). Every credentials gate must
|
|
73
|
+
* use this rather than `hasOpenAiKey()` alone — gating on the local key locks managed-platform
|
|
74
|
+
* users out of features their own Cloudflare account is already paying for.
|
|
75
|
+
*/
|
|
76
|
+
export function hasAiCredentials(): boolean {
|
|
77
|
+
return hasOpenAiKey() || isProxyConfigured();
|
|
34
78
|
}
|
|
35
79
|
|
|
36
80
|
/**
|
|
@@ -52,6 +96,35 @@ export function getProxyDefaultModel(): string {
|
|
|
52
96
|
return proxyDefaultModel;
|
|
53
97
|
}
|
|
54
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The sibling of the chat route, replacing the last path segment and keeping everything else.
|
|
101
|
+
*
|
|
102
|
+
* Written as a URL rewrite rather than `chatUrl.replace(/\/chat$/, …)`, because the chat URL is not
|
|
103
|
+
* always a bare path: the desktop platform appends the loopback server's `?token=`, and a regex
|
|
104
|
+
* anchored on the end of the string silently stopped matching the moment it did — leaving the
|
|
105
|
+
* models request pointed at the chat endpoint.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} chatUrl - Absolute or root-relative, with or without a query
|
|
108
|
+
* @param {string} segment - The sibling route's last path segment
|
|
109
|
+
* @returns {string}
|
|
110
|
+
*/
|
|
111
|
+
export function siblingRoute(chatUrl: string, segment: string): string {
|
|
112
|
+
/*
|
|
113
|
+
* A fixed sentinel base rather than `location.href`. The document's own URL is not always a
|
|
114
|
+
* usable base — under the DOM test harness it is `about:blank`, which makes `new URL` throw and
|
|
115
|
+
* would silently return the chat URL unchanged, pointing the models request at the chat endpoint.
|
|
116
|
+
* The base is discarded below for a root-relative input, so its value cannot leak.
|
|
117
|
+
*/
|
|
118
|
+
let url: URL;
|
|
119
|
+
try {
|
|
120
|
+
url = new URL(chatUrl, "http://jx.invalid");
|
|
121
|
+
} catch {
|
|
122
|
+
return chatUrl; // Not a URL at all — leave a caller's own string alone.
|
|
123
|
+
}
|
|
124
|
+
url.pathname = url.pathname.replace(/[^/]*$/, segment);
|
|
125
|
+
return /^https?:/i.test(chatUrl) ? url.href : `${url.pathname}${url.search}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
55
128
|
/**
|
|
56
129
|
* Fetch the models available through the AI proxy. Returns the cached list when present unless
|
|
57
130
|
* `force` is set; throws on HTTP/network failure (callers surface the error).
|
|
@@ -68,7 +141,7 @@ export async function fetchAvailableModels(
|
|
|
68
141
|
}
|
|
69
142
|
const plat = getPlatform();
|
|
70
143
|
const chatUrl = await Promise.resolve(plat.aiChatUrl());
|
|
71
|
-
const modelsUrl = chatUrl
|
|
144
|
+
const modelsUrl = siblingRoute(chatUrl, "models");
|
|
72
145
|
|
|
73
146
|
const headers: Record<string, string> = {};
|
|
74
147
|
const apiKey = opts.apiKey || getOpenAiKey();
|
|
@@ -24,6 +24,7 @@ import type { Tab } from "../tabs/tab";
|
|
|
24
24
|
import { beginBatch, endBatch, isBatching } from "../tabs/transact";
|
|
25
25
|
import { translateValidationError } from "./ai-tools";
|
|
26
26
|
import { validateDoc, validateProjectConfig } from "./jx-validate";
|
|
27
|
+
import { recordWrite } from "./ai-writes";
|
|
27
28
|
import { flagHardcodedTokens, formatTokenHints } from "./token-lint";
|
|
28
29
|
|
|
29
30
|
/** Directories the file tools never descend into or report. */
|
|
@@ -102,10 +103,19 @@ export interface ProjectToolsCtx {
|
|
|
102
103
|
/** Fired after adoption is verified — the session store re-keys the live chat here. */
|
|
103
104
|
onProjectAdopted?: (root: string) => void;
|
|
104
105
|
/**
|
|
105
|
-
* Fired after a successful `project.json` write so
|
|
106
|
-
* config has passed the project-schema gate by then, which is what makes the type
|
|
106
|
+
* Fired after a successful `project.json` write so the configuration DOCUMENT holds what was
|
|
107
|
+
* written. The config has passed the project-schema gate by then, which is what makes the type
|
|
108
|
+
* honest.
|
|
109
|
+
*
|
|
110
|
+
* AWAITED, and the only thing that reconciles this write with the rest of the app:
|
|
111
|
+
* `tabs/project-config.ts` owns `project.json`, and a write that does not reach its document
|
|
112
|
+
* leaves that document holding the PREVIOUS configuration for the next settings edit to persist
|
|
113
|
+
* back over this one. It used to be fire-and-forget, and the write survived only because an open
|
|
114
|
+
* tab happened to be re-read from disk afterwards ({@link ProjectToolsCtx.reloadTab}); that
|
|
115
|
+
* re-read is gone, because a second parse of `project.json` is the very rival object the
|
|
116
|
+
* configuration document exists to prevent.
|
|
107
117
|
*/
|
|
108
|
-
onProjectConfigWritten?: (config: ProjectConfig) => void
|
|
118
|
+
onProjectConfigWritten?: (config: ProjectConfig) => void | Promise<void>;
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
/**
|
|
@@ -362,22 +372,31 @@ export function registerProjectTools(
|
|
|
362
372
|
|
|
363
373
|
try {
|
|
364
374
|
await getPlatform().writeFile(relPath, content);
|
|
375
|
+
/* Disk, not transaction: there is no undo behind this and there never was. Recorded as
|
|
376
|
+
`disk: true` so the panel can say so to the person holding ⌘Z — the caveat used to be
|
|
377
|
+
appended to the model-facing summary only (§7.4). */
|
|
378
|
+
recordWrite({ disk: true, ok: true, path: relPath, tool: "write_file" });
|
|
365
379
|
} catch (error) {
|
|
380
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
381
|
+
recordWrite({ disk: true, error: message, ok: false, path: relPath, tool: "write_file" });
|
|
366
382
|
return {
|
|
367
383
|
success: false,
|
|
368
|
-
error: `Failed to write "${relPath}": ${
|
|
384
|
+
error: `Failed to write "${relPath}": ${message}`,
|
|
369
385
|
};
|
|
370
386
|
}
|
|
371
387
|
|
|
388
|
+
/* `project.json`'s open tab IS the configuration document, so adopting the write refreshes
|
|
389
|
+
it — re-reading the file here would parse a SECOND configuration object and hand it to
|
|
390
|
+
that tab, which is the split the adoption just closed. Every other path is a plain file
|
|
391
|
+
and its tab has to be told from disk. */
|
|
372
392
|
if (projectConfig) {
|
|
373
|
-
onProjectConfigWritten?.(projectConfig);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
let summary = `Wrote "${relPath}" ${NOT_UNDOABLE}.`;
|
|
377
|
-
if (openTab) {
|
|
393
|
+
await onProjectConfigWritten?.(projectConfig);
|
|
394
|
+
} else if (openTab) {
|
|
378
395
|
await reloadTab(relPath);
|
|
379
|
-
summary = `Wrote "${relPath}" and refreshed its open editor tab ${NOT_UNDOABLE}.`;
|
|
380
396
|
}
|
|
397
|
+
|
|
398
|
+
const refreshed = openTab ? " and refreshed its open editor tab" : "";
|
|
399
|
+
const summary = `Wrote "${relPath}"${refreshed} ${NOT_UNDOABLE}.`;
|
|
381
400
|
return { success: true, summary: tokenHints ? `${summary}\n\n${tokenHints}` : summary };
|
|
382
401
|
},
|
|
383
402
|
}),
|