@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.
Files changed (419) hide show
  1. package/canvas.html +21 -0
  2. package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
  3. package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
  4. package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
  5. package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
  6. package/dist/chunks/{components-0hzentwq.js → components-2bpqp4mq.js} +5 -7
  7. package/dist/chunks/{components-0hzentwq.js.map → components-2bpqp4mq.js.map} +1 -1
  8. package/dist/chunks/content-source-34gnkbnn.js +38 -0
  9. package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
  10. package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
  11. package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
  12. package/dist/chunks/extension-sections-6mqydz35.js +301 -0
  13. package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
  14. package/dist/chunks/files-8kn321h1.js +70 -0
  15. package/dist/chunks/files-8kn321h1.js.map +9 -0
  16. package/dist/chunks/{format-host-2c188n41.js → format-host-mkwsmt8d.js} +3 -3
  17. package/dist/chunks/git-panel-tjbr191d.js +51 -0
  18. package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
  19. package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
  20. package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
  21. package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
  22. package/dist/chunks/layers-panel-87pb25km.js +58 -0
  23. package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
  24. package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
  25. package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
  26. package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
  27. package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
  28. package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
  29. package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
  30. package/dist/chunks/right-panel-zpdxd98t.js +37 -0
  31. package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
  32. package/dist/chunks/section-registry-zmckaxf4.js +30 -0
  33. package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
  34. package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
  35. package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
  36. package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
  37. package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
  38. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
  39. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
  40. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
  41. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
  42. package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
  43. package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
  44. package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
  45. package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
  46. package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
  47. package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
  48. package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
  49. package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
  50. package/dist/chunks/studio-41exgzhq.js +6690 -0
  51. package/dist/chunks/studio-41exgzhq.js.map +38 -0
  52. package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
  53. package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
  54. package/dist/chunks/studio-50k77cca.js +1787 -0
  55. package/dist/chunks/studio-50k77cca.js.map +18 -0
  56. package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
  57. package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
  58. package/dist/chunks/studio-6f0am8f2.js +53 -0
  59. package/dist/chunks/studio-6f0am8f2.js.map +10 -0
  60. package/dist/chunks/studio-6nmjc5qk.js +7002 -0
  61. package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
  62. package/dist/chunks/studio-7afd21nw.js +67 -0
  63. package/dist/chunks/studio-7afd21nw.js.map +10 -0
  64. package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
  65. package/dist/chunks/studio-7bmdvzne.js.map +10 -0
  66. package/dist/chunks/studio-8jpzqk5s.js +128 -0
  67. package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
  68. package/dist/chunks/studio-94m77tgk.js +1482 -0
  69. package/dist/chunks/studio-94m77tgk.js.map +13 -0
  70. package/dist/chunks/studio-9je6w5sq.js +1000 -0
  71. package/dist/chunks/studio-9je6w5sq.js.map +16 -0
  72. package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
  73. package/dist/chunks/{studio-97qcgvdn.js → studio-adn0z1k3.js} +14 -8
  74. package/dist/chunks/{studio-97qcgvdn.js.map → studio-adn0z1k3.js.map} +3 -3
  75. package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
  76. package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
  77. package/dist/chunks/studio-bw5dz6km.js +608 -0
  78. package/dist/chunks/studio-bw5dz6km.js.map +21 -0
  79. package/dist/chunks/studio-c6s5getc.js +4232 -0
  80. package/dist/chunks/studio-c6s5getc.js.map +39 -0
  81. package/dist/chunks/studio-cjm7fdjf.js +451 -0
  82. package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
  83. package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
  84. package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
  85. package/dist/chunks/studio-dg7zyt5x.js +28 -0
  86. package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
  87. package/dist/chunks/studio-dzcyvvvz.js +166 -0
  88. package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
  89. package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
  90. package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
  91. package/dist/chunks/studio-f12h6177.js +411 -0
  92. package/dist/chunks/studio-f12h6177.js.map +13 -0
  93. package/dist/chunks/studio-hbp1s8m9.js +1817 -0
  94. package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
  95. package/dist/chunks/studio-k2sxjph5.js +15295 -0
  96. package/dist/chunks/studio-k2sxjph5.js.map +235 -0
  97. package/dist/chunks/studio-myje51t3.js +385 -0
  98. package/dist/chunks/studio-myje51t3.js.map +12 -0
  99. package/dist/chunks/studio-na6rwzhw.js +353 -0
  100. package/dist/chunks/studio-na6rwzhw.js.map +11 -0
  101. package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
  102. package/dist/chunks/studio-p471gg5n.js.map +14 -0
  103. package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
  104. package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
  105. package/dist/chunks/{studio-hhj92gcr.js → studio-s519zc99.js} +3 -3
  106. package/dist/chunks/studio-sas7mt3k.js +75 -0
  107. package/dist/chunks/studio-sas7mt3k.js.map +10 -0
  108. package/dist/chunks/studio-t5wqwzdh.js +74 -0
  109. package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
  110. package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
  111. package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
  112. package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
  113. package/dist/chunks/studio-tr20m8g8.js.map +9 -0
  114. package/dist/chunks/{studio-ygzav7z5.js → studio-v96qfnww.js} +5514 -4291
  115. package/dist/chunks/studio-v96qfnww.js.map +54 -0
  116. package/dist/chunks/studio-vcetc91k.js +659 -0
  117. package/dist/chunks/studio-vcetc91k.js.map +10 -0
  118. package/dist/chunks/{studio-n9zmpxjg.js → studio-vmymjefp.js} +2972 -4937
  119. package/dist/chunks/studio-vmymjefp.js.map +98 -0
  120. package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
  121. package/dist/chunks/studio-x9hmy5kr.js +390 -0
  122. package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
  123. package/dist/chunks/studio-xa0ce0n7.js +335 -0
  124. package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
  125. package/dist/chunks/studio-xm3dbxvn.js +378 -0
  126. package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
  127. package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
  128. package/dist/chunks/studio-zv37yrfa.js +1054 -0
  129. package/dist/chunks/studio-zv37yrfa.js.map +13 -0
  130. package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
  131. package/dist/chunks/ws-client-654ts66j.js.map +11 -0
  132. package/dist/iframe-entry.js +668 -230
  133. package/dist/studio.js +23711 -35185
  134. package/index.html +35 -4685
  135. package/package.json +17 -15
  136. package/src/about/about-modal.ts +42 -11
  137. package/src/browse/library-commands.ts +240 -0
  138. package/src/browse/library-layouts.ts +369 -0
  139. package/src/browse/library-model.ts +396 -0
  140. package/src/browse/library-pane.ts +1049 -0
  141. package/src/browse/library-preview.ts +252 -0
  142. package/src/browse/library-source.ts +150 -0
  143. package/src/canvas/canvas-helpers.ts +33 -9
  144. package/src/canvas/canvas-live-render.ts +45 -29
  145. package/src/canvas/canvas-patcher.ts +151 -31
  146. package/src/canvas/canvas-perf.ts +46 -7
  147. package/src/canvas/canvas-render.ts +1092 -220
  148. package/src/canvas/canvas-surface.ts +240 -0
  149. package/src/canvas/canvas-utils.ts +1166 -124
  150. package/src/canvas/iframe-drop.ts +2 -1
  151. package/src/canvas/iframe-entry.ts +241 -4
  152. package/src/canvas/iframe-host.ts +1226 -143
  153. package/src/canvas/iframe-inline-edit.ts +31 -0
  154. package/src/canvas/iframe-interaction.ts +76 -16
  155. package/src/canvas/iframe-keys.ts +107 -45
  156. package/src/canvas/iframe-overlay.ts +38 -1
  157. package/src/canvas/iframe-protocol.ts +137 -2
  158. package/src/canvas/iframe-render.ts +70 -2
  159. package/src/canvas/iframe-slash.ts +6 -0
  160. package/src/canvas/path-mapping.ts +56 -6
  161. package/src/canvas/surface-registry.ts +292 -0
  162. package/src/collab/collab-commands.ts +244 -0
  163. package/src/collab/collab-session.ts +198 -32
  164. package/src/collab/collab-state.ts +28 -2
  165. package/src/collab/presence-chips.ts +100 -9
  166. package/src/commands/active-registry.ts +42 -0
  167. package/src/commands/app-commands.ts +176 -0
  168. package/src/commands/budget.ts +158 -0
  169. package/src/commands/command-args.ts +253 -0
  170. package/src/commands/context.ts +387 -0
  171. package/src/commands/defaults.ts +580 -0
  172. package/src/commands/keymap.ts +571 -0
  173. package/src/commands/levels.ts +345 -0
  174. package/src/commands/live-context.ts +193 -0
  175. package/src/commands/reference.ts +227 -0
  176. package/src/commands/registry.ts +293 -0
  177. package/src/component-props.ts +3 -2
  178. package/src/content/draft-state.ts +106 -0
  179. package/src/content/entry-commands.ts +282 -0
  180. package/src/content/entry-editor.ts +327 -0
  181. package/src/content/entry-fields.ts +116 -0
  182. package/src/content/entry-model.ts +175 -0
  183. package/src/editor/canvas-context-menu.ts +17 -11
  184. package/src/editor/canvas-slash-bridge.ts +11 -4
  185. package/src/editor/context-menu.ts +804 -204
  186. package/src/editor/convert-to-component.ts +31 -10
  187. package/src/editor/convert-to-repeater.ts +3 -2
  188. package/src/editor/inline-edit-apply.ts +5 -5
  189. package/src/editor/inline-edit.ts +86 -50
  190. package/src/editor/insert-zone-action.ts +1 -1
  191. package/src/editor/shortcuts.ts +1043 -329
  192. package/src/editor/slash-menu.ts +1 -0
  193. package/src/files/elements.ts +166 -0
  194. package/src/files/file-ops.ts +178 -31
  195. package/src/files/files.ts +898 -268
  196. package/src/files/fs-events.ts +21 -0
  197. package/src/files/media-meta.ts +203 -0
  198. package/src/files/media-paths.ts +141 -0
  199. package/src/files/media-upload.ts +7 -3
  200. package/src/files/media-usage.ts +189 -0
  201. package/src/github/github-auth.ts +235 -33
  202. package/src/github/github-publish.ts +80 -21
  203. package/src/grid/grid-controller.ts +152 -12
  204. package/src/grid/grid-layout.ts +225 -20
  205. package/src/grid/grid-open.ts +120 -0
  206. package/src/grid/grid-panel.ts +554 -39
  207. package/src/grid/grid-source.ts +27 -1
  208. package/src/grid/redirects-grid.ts +599 -0
  209. package/src/grid/redirects.ts +398 -0
  210. package/src/grid/sources/content-source.ts +108 -2
  211. package/src/i18n/i18n-commands.ts +322 -0
  212. package/src/new-project/add-repo-modal.ts +8 -10
  213. package/src/new-project/import-tab.ts +34 -9
  214. package/src/new-project/location-fields.ts +9 -1
  215. package/src/new-project/new-project-modal.ts +270 -262
  216. package/src/packages/jxsuite-update.ts +97 -48
  217. package/src/packages/pull-package-sync.ts +18 -9
  218. package/src/page-params.ts +2 -1
  219. package/src/panels/activity-bar.ts +152 -91
  220. package/src/panels/activity-panel.ts +481 -0
  221. package/src/panels/ai-chat/chat-markdown.ts +8 -3
  222. package/src/panels/ai-chat/chat-view.ts +245 -18
  223. package/src/panels/ai-chat/composer.ts +31 -5
  224. package/src/panels/ai-chat/sessions-view.ts +17 -10
  225. package/src/panels/ai-panel.ts +279 -120
  226. package/src/panels/block-action-bar.ts +1115 -224
  227. package/src/panels/bottom-dock.ts +337 -0
  228. package/src/panels/canvas-dnd-bridge.ts +37 -8
  229. package/src/panels/chat-panel.ts +29 -12
  230. package/src/panels/data-explorer.ts +254 -112
  231. package/src/panels/data-grid.ts +10 -4
  232. package/src/panels/dnd.ts +28 -51
  233. package/src/panels/editors.ts +434 -107
  234. package/src/panels/elements-panel.ts +66 -2
  235. package/src/panels/empty-state.ts +112 -0
  236. package/src/panels/events-panel.ts +596 -82
  237. package/src/panels/formula-workspace.ts +408 -218
  238. package/src/panels/frontmatter-fields.ts +51 -10
  239. package/src/panels/frontmatter-panel.ts +357 -79
  240. package/src/panels/git-panel.ts +375 -164
  241. package/src/panels/head-panel.ts +791 -85
  242. package/src/panels/i18n-panel.ts +592 -0
  243. package/src/panels/imports-panel.ts +95 -85
  244. package/src/panels/jump-bar.ts +584 -0
  245. package/src/panels/layers-panel.ts +1106 -328
  246. package/src/panels/left-panel.ts +109 -204
  247. package/src/panels/navigator-panels.ts +131 -0
  248. package/src/panels/overlays.ts +7 -2
  249. package/src/panels/pane-context.ts +1477 -0
  250. package/src/panels/pane-grid.ts +406 -0
  251. package/src/panels/panel-registry.ts +340 -0
  252. package/src/panels/panel-scheduler.ts +75 -0
  253. package/src/panels/problems-panel.ts +234 -0
  254. package/src/panels/properties-panel.ts +946 -1139
  255. package/src/panels/provenance.ts +258 -0
  256. package/src/panels/quick-search.ts +791 -122
  257. package/src/panels/right-panel.ts +228 -102
  258. package/src/panels/seo-modal.ts +395 -0
  259. package/src/panels/settings-pane.ts +203 -0
  260. package/src/panels/signals-panel.ts +354 -137
  261. package/src/panels/statement-editor.ts +41 -25
  262. package/src/panels/statusbar.ts +342 -106
  263. package/src/panels/style-panel.ts +974 -463
  264. package/src/panels/style-utils.ts +3 -3
  265. package/src/panels/stylebook-doc.ts +4 -2
  266. package/src/panels/stylebook-layers-panel.ts +3 -2
  267. package/src/panels/stylebook-panel.ts +51 -26
  268. package/src/panels/tab-strip.ts +1058 -78
  269. package/src/panels/target-line.ts +274 -0
  270. package/src/panels/toolbar.ts +631 -484
  271. package/src/panels/welcome-screen.ts +301 -198
  272. package/src/platform-errors.ts +26 -3
  273. package/src/platform.ts +96 -2
  274. package/src/platforms/cloud.ts +71 -8
  275. package/src/platforms/devserver.ts +92 -29
  276. package/src/publish/deploy-checklist.ts +383 -0
  277. package/src/publish/pages-service.ts +10 -1
  278. package/src/publish/publish-commands.ts +218 -0
  279. package/src/publish/publish-panel.ts +110 -16
  280. package/src/recent-projects.ts +3 -2
  281. package/src/services/a11y-report.ts +505 -0
  282. package/src/services/ai-models.ts +75 -2
  283. package/src/services/ai-project-tools.ts +29 -10
  284. package/src/services/ai-system-prompt.ts +53 -14
  285. package/src/services/ai-tools.ts +39 -3
  286. package/src/services/ai-writes.ts +154 -0
  287. package/src/services/announce.ts +97 -0
  288. package/src/services/automation.ts +424 -175
  289. package/src/services/cem-export.ts +9 -4
  290. package/src/services/clock.ts +61 -0
  291. package/src/services/document-assistant.ts +36 -8
  292. package/src/services/idle.ts +179 -0
  293. package/src/services/import-client.ts +26 -0
  294. package/src/services/monaco-buffer.ts +627 -0
  295. package/src/services/monaco-lazy.ts +36 -0
  296. package/src/services/notify.ts +271 -0
  297. package/src/services/profile.ts +155 -0
  298. package/src/services/references.ts +0 -0
  299. package/src/services/token-lint.ts +3 -2
  300. package/src/services/tool-executor.ts +53 -7
  301. package/src/services/trusted-types.ts +123 -0
  302. package/src/settings/contexts-section.ts +427 -0
  303. package/src/settings/contributed-section.ts +149 -11
  304. package/src/settings/css-vars-editor.ts +306 -349
  305. package/src/settings/defs-editor.ts +93 -34
  306. package/src/settings/dependencies-editor.ts +7 -7
  307. package/src/settings/extension-sections.ts +37 -5
  308. package/src/settings/general-settings.ts +204 -121
  309. package/src/settings/locales-section.ts +347 -0
  310. package/src/settings/preferences-accounts.ts +140 -0
  311. package/src/settings/preferences-dialog.ts +620 -0
  312. package/src/settings/preferences-keymap.ts +270 -0
  313. package/src/settings/project-sections.ts +260 -0
  314. package/src/settings/section-registry.ts +169 -0
  315. package/src/settings/settings-document.ts +385 -0
  316. package/src/shell.ts +1549 -0
  317. package/src/site-context.ts +45 -20
  318. package/src/state.ts +14 -275
  319. package/src/store.ts +88 -43
  320. package/src/studio.ts +790 -346
  321. package/src/style/live-preview.ts +36 -0
  322. package/src/style/project-styles.ts +349 -0
  323. package/src/style/token-ref.ts +123 -0
  324. package/src/tabs/patch-ops.ts +16 -4
  325. package/src/tabs/project-config.ts +416 -0
  326. package/src/tabs/selection.ts +323 -0
  327. package/src/tabs/tab.ts +182 -47
  328. package/src/tabs/transact.ts +209 -28
  329. package/src/types.ts +68 -13
  330. package/src/ui/ai-credentials-form.ts +39 -35
  331. package/src/ui/ai-managed-connect.ts +113 -0
  332. package/src/ui/dynamic-slot.ts +224 -81
  333. package/src/ui/expression-editor.ts +71 -63
  334. package/src/ui/field-input.ts +4 -6
  335. package/src/ui/field-row.ts +215 -38
  336. package/src/ui/form-controls.ts +194 -7
  337. package/src/ui/layers.ts +492 -69
  338. package/src/ui/media-picker.ts +115 -25
  339. package/src/ui/panel-resize.ts +109 -91
  340. package/src/ui/progress-modal.ts +131 -50
  341. package/src/ui/regions.ts +462 -0
  342. package/src/ui/schema-form.ts +229 -45
  343. package/src/ui/spectrum.ts +24 -8
  344. package/src/ui/timing.ts +49 -0
  345. package/src/ui/value-selector.ts +20 -0
  346. package/src/ui/value-source.ts +348 -0
  347. package/src/ui/virtual-window.ts +400 -0
  348. package/src/utils/edit-display.ts +3 -2
  349. package/src/utils/inherited-style.ts +67 -37
  350. package/src/view.ts +64 -78
  351. package/src/workspace/pane-derive.ts +1258 -0
  352. package/src/workspace/session.ts +317 -0
  353. package/src/workspace/workspace.ts +1345 -23
  354. package/styles/canvas.css +653 -0
  355. package/styles/forced-colors.css +106 -0
  356. package/styles/inspector.css +1454 -0
  357. package/styles/overlays.css +2704 -0
  358. package/styles/panels.css +1701 -0
  359. package/styles/shell.css +1423 -0
  360. package/styles/tokens.css +151 -0
  361. package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
  362. package/dist/chunks/extension-sections-qct0gt8f.js +0 -531
  363. package/dist/chunks/extension-sections-qct0gt8f.js.map +0 -12
  364. package/dist/chunks/files-02zn5y24.js +0 -50
  365. package/dist/chunks/files-02zn5y24.js.map +0 -9
  366. package/dist/chunks/git-panel-mbb0y0hz.js +0 -28
  367. package/dist/chunks/git-panel-mbb0y0hz.js.map +0 -9
  368. package/dist/chunks/layers-panel-9qekmqk1.js +0 -26
  369. package/dist/chunks/layers-panel-9qekmqk1.js.map +0 -9
  370. package/dist/chunks/settings-modal-gj0hrah4.js +0 -29
  371. package/dist/chunks/settings-modal-gj0hrah4.js.map +0 -9
  372. package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
  373. package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
  374. package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
  375. package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
  376. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
  377. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
  378. package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
  379. package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
  380. package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
  381. package/dist/chunks/studio-568nx4t5.js +0 -469
  382. package/dist/chunks/studio-568nx4t5.js.map +0 -14
  383. package/dist/chunks/studio-9smfzqhc.js +0 -19
  384. package/dist/chunks/studio-9smfzqhc.js.map +0 -10
  385. package/dist/chunks/studio-asvf0j9e.js +0 -429
  386. package/dist/chunks/studio-asvf0j9e.js.map +0 -13
  387. package/dist/chunks/studio-d77qm3pk.js +0 -3427
  388. package/dist/chunks/studio-d77qm3pk.js.map +0 -26
  389. package/dist/chunks/studio-j6gqyjf4.js +0 -1854
  390. package/dist/chunks/studio-j6gqyjf4.js.map +0 -18
  391. package/dist/chunks/studio-n9zmpxjg.js.map +0 -115
  392. package/dist/chunks/studio-qkvt37c0.js.map +0 -9
  393. package/dist/chunks/studio-rq0vxb19.js +0 -3744
  394. package/dist/chunks/studio-rq0vxb19.js.map +0 -22
  395. package/dist/chunks/studio-wd6ewdvd.js +0 -853
  396. package/dist/chunks/studio-wd6ewdvd.js.map +0 -12
  397. package/dist/chunks/studio-xq0t7n5t.js +0 -140
  398. package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
  399. package/dist/chunks/studio-xze4zrs1.js.map +0 -10
  400. package/dist/chunks/studio-ygzav7z5.js.map +0 -46
  401. package/dist/chunks/studio-zmy7241e.js +0 -399
  402. package/dist/chunks/studio-zmy7241e.js.map +0 -12
  403. package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
  404. package/src/browse/browse-modal.ts +0 -69
  405. package/src/browse/browse.ts +0 -904
  406. package/src/canvas/canvas-diff.ts +0 -216
  407. package/src/canvas/nested-site-style.ts +0 -58
  408. package/src/new-project/design-fields.ts +0 -264
  409. package/src/new-project/templates.ts +0 -61
  410. package/src/panels/tab-bar.ts +0 -548
  411. package/src/settings/settings-modal.ts +0 -203
  412. /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
  413. /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
  414. /package/dist/chunks/{format-host-2c188n41.js.map → format-host-mkwsmt8d.js.map} +0 -0
  415. /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
  416. /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
  417. /package/dist/chunks/{studio-hhj92gcr.js.map → studio-s519zc99.js.map} +0 -0
  418. /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
  419. /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.replace(/\/chat$/, "/models");
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 workspace/project state stay in sync. The
106
- * config has passed the project-schema gate by then, which is what makes the type honest.
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}": ${error instanceof Error ? error.message : String(error)}`,
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
  }),