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