@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
@@ -1,48 +1,78 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Toolbar panel — extracted from studio.js renderToolbar(). Owns rendering of breadcrumbs, file
4
- * ops, feature toggles, and mode switcher.
3
+ * The Command Bar — region ① of UX-REDESIGN-PLAN §3.2, rendered FROM the registry.
4
+ *
5
+ * Every control in this band used to be hand-authored twice: once in `toolbarTemplate` and once in
6
+ * `minimalToolbarTemplate`, a retyped copy of the same buttons with `disabled` hard-coded on each
7
+ * one for the no-project case. That second copy is the canonical example of the defect §2 principle
8
+ * 1 names — a hand-maintained list of actions beside the definition site — and §2 principle 4 says
9
+ * enablement is a PREDICATE with a sentence, never a duplicated template. It is deleted. With no
10
+ * project open the same bar renders; the records' own `when` clauses empty it.
11
+ *
12
+ * What is left is four things, none of which decides what an action is called:
13
+ *
14
+ * - {@link tbCmd} renders one command: title, icon, `title="Save (⌘S)"` with the chord formatted for
15
+ * THIS platform by `keymap.format` (the predecessor hardcoded `⌘P` and showed it to Windows and
16
+ * Linux users), disabled state from `enablement`, and the `requires` sentence in the tooltip when
17
+ * it is off — so no control is ever permanently dead with no explanation.
18
+ * - The primary verb cluster is `forPlacement("commandbar/primary")`, capped at five by
19
+ * `scripts/check-chrome-budget.ts`; the ⬢ menu is `forPlacement("commandbar/overflow")`.
20
+ * - The **Command Center pill** (①a) is the app's address bar: `◈ project › document › selection`,
21
+ * right-aligned ⌘K, each segment opening the palette pre-scoped. It replaces the Open Project
22
+ * split button, the recents dropdown and the `Search files… ⌘P` trigger, and it gives Studio a
23
+ * persistent project name for the first time — today it renders only in the Files panel header,
24
+ * and the desktop titlebar is `titleBarStyle:"hidden"`.
25
+ * - The window controls, which are the one thing in here that is not an action.
26
+ *
27
+ * **Retired here, with a name, a chord and a residue** (§2 principle 9): Open Project + New Project
28
+ * + recents → the pill and `Project: Open Recent…`; `Manage` → `File: Browse Library`; `Publish` →
29
+ * the `Publish:` family; `Sync Project` → Source Control. The five-mode switcher leaves for the
30
+ * pane context bar (region ⑦) and is reachable meanwhile as `View: Set Canvas Mode` in the palette,
31
+ * which prompts for the mode from the record's own `args` enum.
5
32
  */
6
33
 
7
34
  import { html, render as litRender, nothing } from "lit-html";
8
- import { openPublishPanel } from "../publish/publish-panel";
9
- import { updateSession } from "../store";
10
- import {
11
- canRedo as tabCanRedo,
12
- canUndo as tabCanUndo,
13
- redo as tabRedo,
14
- undo as tabUndo,
15
- } from "../tabs/transact";
16
- import { collabState } from "../collab/collab-state";
17
35
  import { presenceChipsTemplate } from "../collab/presence-chips";
18
36
  import { effect, effectScope } from "../reactivity";
19
37
  import { activeTab } from "../workspace/workspace";
20
- import { applyPanelCollapse, view } from "../view";
21
- import { clearRecentProjects, getRecentProjects, removeRecentProject } from "../recent-projects";
38
+ import { primarySelection } from "../tabs/selection";
39
+ import { shell } from "../shell";
22
40
  import { openQuickSearch } from "./quick-search";
23
- import { getPlatform } from "../platform";
24
- import { refreshGitStatus } from "./git-panel";
25
- import { openBrowseModal } from "../browse/browse-modal";
26
- import { openNewProjectModal } from "../new-project/new-project-modal";
41
+ import { showPromptDialog } from "../ui/layers";
42
+ import { getPlatform, hasPlatform } from "../platform";
43
+ import { getPreviewNavigateHandler } from "../canvas/preview-navigate";
44
+ import { documentUrlPattern, dynamicRouteParams } from "../page-params";
45
+ import { getNodeAtPath, nodeLabel, projectState } from "../store";
46
+ import { activeRegistry } from "../commands/active-registry";
47
+ import { notify } from "../services/notify";
48
+ import type { Tab } from "../tabs/tab";
49
+ import type { SiteBuildResult } from "../types";
50
+ import type { CommandRegistry } from "../commands/registry";
27
51
  import type { EffectScope } from "@vue/reactivity";
28
52
  import type { TemplateResult } from "lit-html";
29
53
 
30
- interface ToolbarCtx {
31
- openProject: () => void;
54
+ /**
55
+ * What the Command Bar is handed at mount.
56
+ *
57
+ * HANDOFF: **nothing here is read any more** — every control in the band is a command, so the bar
58
+ * asks the registry rather than the bootstrap. The fields stay declared, and optional, so
59
+ * `studio.ts`'s `toolbarPanel.mount(toolbarEl, { … })` object literal keeps type-checking; deleting
60
+ * them is one edit in that file, which is another workstream's this wave.
61
+ */
62
+ export interface ToolbarCtx {
63
+ openProject?: () => void;
32
64
  openFile?: (path: string) => void;
33
- saveFile: () => void;
34
- getCanvasMode: () => string;
35
- setCanvasMode: (mode: string) => void;
36
- renderCanvas: () => void;
37
- safeRenderRightPanel: () => void;
38
- openRecentProject: (root: string) => Promise<void>;
39
- closeFunctionEditor: () => void;
65
+ saveFile?: () => void;
66
+ getCanvasMode?: () => string;
67
+ setCanvasMode?: (tab: Tab | null, mode: string) => void;
68
+ renderCanvas?: () => void;
69
+ safeRenderRightPanel?: () => void;
70
+ openRecentProject?: (root: string) => Promise<void>;
71
+ closeFunctionEditor?: () => void;
40
72
  }
41
73
 
42
74
  let _rootEl: HTMLElement | null = null;
43
75
 
44
- let _ctx: ToolbarCtx | null = null;
45
-
46
76
  /** Test override for the mac CSD layout — happy-dom forbids redefining navigator.platform. */
47
77
  let _isMacOverride: boolean | null = null;
48
78
 
@@ -58,535 +88,652 @@ function isMacPlatform(): boolean {
58
88
 
59
89
  let _scope: EffectScope | null = null;
60
90
 
61
- const toolbarIconMap = {
62
- "sp-icon-artboard": html`<sp-icon-artboard slot="icon"></sp-icon-artboard>`,
63
- "sp-icon-brush": html`<sp-icon-brush slot="icon"></sp-icon-brush>`,
64
- "sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
65
- "sp-icon-delete": html`<sp-icon-delete slot="icon"></sp-icon-delete>`,
66
- "sp-icon-document": html`<sp-icon-document slot="icon"></sp-icon-document>`,
67
- "sp-icon-duplicate": html`<sp-icon-duplicate slot="icon"></sp-icon-duplicate>`,
68
- "sp-icon-edit": html`<sp-icon-edit slot="icon"></sp-icon-edit>`,
69
- "sp-icon-folder-open": html`<sp-icon-folder-open slot="icon"></sp-icon-folder-open>`,
70
- "sp-icon-gears": html`<sp-icon-gears slot="icon"></sp-icon-gears>`,
71
- "sp-icon-preview": html`<sp-icon-preview slot="icon"></sp-icon-preview>`,
72
- "sp-icon-redo": html`<sp-icon-redo slot="icon"></sp-icon-redo>`,
73
- "sp-icon-save-floppy": html`<sp-icon-save-floppy slot="icon"></sp-icon-save-floppy>`,
74
- "sp-icon-undo": html`<sp-icon-undo slot="icon"></sp-icon-undo>`,
75
- "sp-icon-view-grid": html`<sp-icon-view-grid slot="icon"></sp-icon-view-grid>`,
76
- "sp-icon-view-list": html`<sp-icon-view-list slot="icon"></sp-icon-view-list>`,
77
- } as Record<string, TemplateResult>;
91
+ /**
92
+ * Icon key → Spectrum icon, for the `icon` a command record declares.
93
+ *
94
+ * The RECORD names the icon; this map only knows how to draw one. Keys are the record's vocabulary
95
+ * ("save", "undo"), not Spectrum tag names, so swapping the icon set is a change here and nowhere
96
+ * else — and a record that names an icon this bar cannot draw renders as a labelled button rather
97
+ * than as an empty one.
98
+ */
99
+ const COMMAND_ICONS: Readonly<Record<string, TemplateResult>> = {
100
+ browser: html`<sp-icon-export slot="icon"></sp-icon-export>`,
101
+ redo: html`<sp-icon-redo slot="icon"></sp-icon-redo>`,
102
+ save: html`<sp-icon-save-floppy slot="icon"></sp-icon-save-floppy>`,
103
+ undo: html`<sp-icon-undo slot="icon"></sp-icon-undo>`,
104
+ };
105
+
106
+ // ─── One command, one control ─────────────────────────────────────────────────
107
+
108
+ /** How {@link tbCmd} draws a record. Presentation only — never what the record means. */
109
+ export interface TbCmdOptions {
110
+ /** Icon-only, with the title as the accessible name. Used by the dock toggles. */
111
+ compact?: boolean;
112
+ /** Rendered pressed. The dock toggles are the only controls with an on-state. */
113
+ selected?: boolean;
114
+ /** Override the record's icon — a dock toggle's glyph depends on which way the dock is. */
115
+ icon?: TemplateResult;
116
+ }
78
117
 
79
118
  /**
80
- * @param {string} label
81
- * @param {() => void} onClick
82
- * @param {string} [iconTag]
119
+ * The tooltip a control shows: the action's name, plus its chord, or plus WHY it is off.
120
+ *
121
+ * One string, three sources, all from the record: `title`, `keymap.formatBinding` and `requires`.
122
+ * `tbBtnTpl(label, onClick, icon)` — the predecessor — could express none of them, which is why
123
+ * "Open in Browser" had to hand-build its own disabled variant with a bespoke reason string.
83
124
  */
84
- function tbBtnTpl(label: string, onClick: () => void, iconTag?: string) {
125
+ export function commandTooltip(registry: CommandRegistry, id: string): string {
126
+ const command = registry.get(id);
127
+ if (!command) {
128
+ return "";
129
+ }
130
+ const reason = registry.disabledReason(id);
131
+ if (reason) {
132
+ return `${command.title} — requires ${reason}`;
133
+ }
134
+ const chord = registry.keymap.formatBinding(id);
135
+ return chord ? `${command.title} (${chord})` : command.title;
136
+ }
137
+
138
+ /**
139
+ * Render one command as a Command Bar button, or `nothing` when its `when` hides it.
140
+ *
141
+ * This is the whole of §5.5's first row: label, icon, tooltip, chord, disabled state and disabled
142
+ * reason all come off the record, so the bar cannot disagree with the palette, the keymap or the
143
+ * agent about any of them.
144
+ */
145
+ export function tbCmd(registry: CommandRegistry, id: string, options: TbCmdOptions = {}) {
146
+ const command = registry.get(id);
147
+ if (!command || !registry.isVisible(id)) {
148
+ return nothing;
149
+ }
150
+ const enabled = registry.isEnabled(id);
151
+ const icon = options.icon ?? (command.icon ? COMMAND_ICONS[command.icon] : undefined);
85
152
  return html`
86
- <sp-action-button size="s" title=${label} @click=${onClick}>
87
- ${iconTag ? toolbarIconMap[iconTag] : nothing}<span class="tb-label">${label}</span>
153
+ <sp-action-button
154
+ size="s"
155
+ ?quiet=${options.compact === true}
156
+ ?selected=${options.selected === true}
157
+ title=${commandTooltip(registry, id)}
158
+ aria-label=${command.title}
159
+ ?disabled=${!enabled}
160
+ @click=${() => {
161
+ void registry.run(id);
162
+ }}
163
+ >
164
+ ${icon ?? nothing}
165
+ ${options.compact ? nothing : html`<span class="tb-label">${command.title}</span>`}
88
166
  </sp-action-button>
89
167
  `;
90
168
  }
91
169
 
170
+ // ─── View: Open in Browser ───────────────────────────────────────────────────
171
+
92
172
  /**
93
- * Mount the toolbar panel.
173
+ * Where `View: Open in Browser` would go — the site-relative PATH, not a URL — or the sentence
174
+ * explaining why it cannot go anywhere.
94
175
  *
95
- * @param {HTMLElement} rootEl
96
- * @param {ToolbarCtx} ctx — { navigateBack, closeFunctionEditor, openProject, openFile, saveFile,
97
- * parseMediaEntries, getCanvasMode, setCanvasMode, renderCanvas, safeRenderRightPanel }
176
+ * A path rather than a URL because the origin is not this command's to know: the built site is
177
+ * served by the backend, on a port of its own, and the build call is what names it.
98
178
  */
99
- export function mount(rootEl: HTMLElement, ctx: ToolbarCtx) {
100
- _rootEl = rootEl;
101
- _ctx = ctx;
102
- if (
103
- (globalThis as unknown as { __jxPlatform?: { windowControls?: unknown } }).__jxPlatform
104
- ?.windowControls
105
- ) {
106
- rootEl.classList.add("electrobun-webkit-app-region-drag");
179
+ export type BrowserTarget = { path: string } | { reason: string };
180
+
181
+ /**
182
+ * Resolve the active document to the ROUTE its built page will be published at.
183
+ *
184
+ * The path mirrors the compiler's own `documentUrlPattern` plus the project's `trailingSlash`
185
+ * setting, so it is the URL every link inside the built site already points at — which is exactly
186
+ * why the reader can then browse from it.
187
+ *
188
+ * Everything that is not a page resolves to a REASON rather than to nothing: a disabled control the
189
+ * user can hover is discoverable, an absent one is not.
190
+ */
191
+ export function openInBrowserTarget(tab: Tab | null): BrowserTarget {
192
+ const documentPath = tab?.documentPath?.replace(/^\.\//, "");
193
+ if (!documentPath) {
194
+ return { reason: "Open a page to view it in a browser." };
107
195
  }
108
- _scope = effectScope();
109
- _scope.run(() => {
110
- effect(() => {
111
- const tab = activeTab.value;
112
- if (tab) {
113
- // Read reactive properties to establish tracking
114
- void tab.doc.document;
115
- void tab.doc.dirty;
116
- void tab.doc.mode;
117
- void tab.session.selection;
118
- void tab.session.ui.canvasMode;
119
- void tab.session.ui.editingFunction;
120
- void tab.session.ui.featureToggles;
121
- void tab.session.ui.rightTab;
122
- void tab.session.ui.gitStatus;
123
- void tab.history.index;
124
- void tab.history.snapshots.length;
125
- void collabState(tab).status;
126
- void collabState(tab).peers.length;
127
- }
128
- render();
129
- });
130
- });
196
+ if (!projectState?.isSiteProject) {
197
+ return { reason: "This project does not build a site." };
198
+ }
199
+ if (!documentPath.startsWith("pages/")) {
200
+ return { reason: `Only pages have a route — ${documentPath} is not under pages/.` };
201
+ }
202
+ let route = documentUrlPattern(documentPath);
203
+ if (route.includes("*")) {
204
+ return { reason: "Catch-all routes match many pages — open a generated one instead." };
205
+ }
206
+ const params = dynamicRouteParams(documentPath);
207
+ if (params.length > 0) {
208
+ const chosen = (tab?.session.ui.previewParams ?? {}) as Record<string, string>;
209
+ const missing = params.filter((name) => !chosen[name]);
210
+ if (missing.length > 0) {
211
+ const names = missing.map((name) => `:${name}`).join(", ");
212
+ return { reason: `Pick a value for ${names} to open one of this route's pages.` };
213
+ }
214
+ route = route.replaceAll(/:(\w+)/g, (_m, name: string) => encodeURIComponent(chosen[name]!));
215
+ }
216
+ /* The page's URL, not its file's path.
217
+ This used to answer `${origin}/dist${route}/index.html` — the compiler's OUTPUT PATH — and the
218
+ browser then did exactly what a built page's own markup tells it to: a stylesheet at
219
+ `/components/demo.css` and a link to `/basics/counter` are ROOT-absolute, so from a `/dist/…`
220
+ URL the first 404s against the server root and the second leaves the site. Measured before the
221
+ fix: the page 200, its CSS 404, its first link 404. */
222
+ const trailingSlash = projectState.projectConfig?.build?.trailingSlash ?? "always";
223
+ return { path: route === "/" ? "/" : trailingSlash === "always" ? `${route}/` : route };
131
224
  }
132
225
 
133
- export function unmount() {
134
- _scope?.stop();
135
- _scope = null;
136
- _rootEl = null;
137
- _ctx = null;
226
+ /**
227
+ * Hand a URL to the user's real browser.
228
+ *
229
+ * Reuses the seam the desktop launchers already register for Preview link clicks
230
+ * (`canvas/preview-navigate.ts`), which routes through the OS rather than navigating a webview with
231
+ * no address bar; the browser build falls back to a new tab.
232
+ */
233
+ function openUrlExternally(url: string) {
234
+ const override = getPreviewNavigateHandler();
235
+ if (override) {
236
+ override(url);
237
+ return;
238
+ }
239
+ window.open(url, "_blank", "noopener,noreferrer");
138
240
  }
139
241
 
140
- export function render() {
141
- if (!_rootEl || !_ctx) {
242
+ /**
243
+ * Run `View: Open in Browser`, reporting the blocking reason when there is one.
244
+ *
245
+ * Exported as the implementation the bootstrap hands to the record — the ⌘⇧O chord is the record's
246
+ * `keybinding` now, so the bespoke `document.addEventListener("keydown", …)` this file used to
247
+ * install (with its own `isModalOpen()` guard, its own shift test and no way to be rebound) is
248
+ * deleted.
249
+ */
250
+ export async function runOpenInBrowser() {
251
+ const target = openInBrowserTarget(activeTab.value ?? null);
252
+ if (!("path" in target)) {
253
+ notify.warn(target.reason, { key: OPEN_IN_BROWSER, source: "Preview" });
254
+ return;
255
+ }
256
+ /* Build first, because the reader is about to see the OUTPUT and the author is looking at the
257
+ DOCUMENT. Without this they differ by however long it has been since anyone ran a build —
258
+ which for most projects is "always", since nothing in Studio had ever written the output at
259
+ all. A preview that quietly shows last week's page is worse than one that says it cannot open.
260
+
261
+ The build is also what NAMES the origin. The built site is served on a port of its own, because
262
+ the editor's own paths mean the project's sources while a built page means its output by the
263
+ same paths — so a backend that cannot build cannot preview either, and says so instead of
264
+ sending the reader to an address where half the site is the wrong file. */
265
+ const platform = hasPlatform() ? getPlatform() : null;
266
+ if (!platform?.buildSite) {
267
+ notify.warn("This backend cannot build a preview of the site.", {
268
+ key: OPEN_IN_BROWSER,
269
+ source: "Preview",
270
+ });
142
271
  return;
143
272
  }
273
+ notify.info("Building the site…", { key: OPEN_IN_BROWSER, source: "Preview" });
274
+ let result: SiteBuildResult;
144
275
  try {
145
- litRender(toolbarTemplate(), _rootEl);
276
+ result = await platform.buildSite();
146
277
  } catch (error) {
147
- console.error("toolbar render error:", error);
278
+ notify.error(`The site could not be built: ${errorText(error)}`, {
279
+ key: OPEN_IN_BROWSER,
280
+ source: "Preview",
281
+ });
282
+ return;
283
+ }
284
+ if (!result.url) {
285
+ notify.warn("The site was built, but this backend serves no preview of it.", {
286
+ key: OPEN_IN_BROWSER,
287
+ source: "Preview",
288
+ });
289
+ return;
290
+ }
291
+ if (result.errors.length > 0) {
292
+ /* Named, and the page still opens. A partial build produced pages, and the author can see the
293
+ one they asked for while reading what failed — which is the whole difference between a
294
+ preview and a build log. */
295
+ notify.warn(`The site built with ${result.errors.length} error(s): ${result.errors[0]}`, {
296
+ key: OPEN_IN_BROWSER,
297
+ source: "Preview",
298
+ });
299
+ } else {
300
+ notify.success(`Built ${result.routes} page(s).`, { key: OPEN_IN_BROWSER, source: "Preview" });
301
+ }
302
+ openUrlExternally(`${result.url}${target.path}`);
303
+ }
304
+
305
+ /** One notification key for the whole flow, so building → built → failed replaces rather than piles. */
306
+ const OPEN_IN_BROWSER = "view.openInBrowser";
307
+
308
+ /** The sentence in an unknown thrown value. */
309
+ function errorText(error: unknown): string {
310
+ return error instanceof Error ? error.message : String(error);
311
+ }
312
+
313
+ // ─── ①a The Command Center pill ──────────────────────────────────────────────
314
+
315
+ /** One segment of the address, and the palette mode it opens. */
316
+ interface PillSegment {
317
+ key: string;
318
+ label: string;
319
+ title: string;
320
+ onClick: () => void;
321
+ }
322
+
323
+ /** The document's label: its path without the project root, which is already segment one. */
324
+ export function documentSegmentLabel(tab: Tab | null): string {
325
+ const path = tab?.documentPath;
326
+ if (!path) {
327
+ return "No document";
328
+ }
329
+ const root = projectState?.projectRoot;
330
+ const trimmed = path.replace(/^\.\//, "");
331
+ return root && trimmed.startsWith(`${root}/`) ? trimmed.slice(root.length + 1) : trimmed;
332
+ }
333
+
334
+ /** The selection's label — the Outline's own `nodeLabel`, so the two never disagree. */
335
+ export function selectionSegmentLabel(tab: Tab | null): string {
336
+ if (shell.layoutSelection) {
337
+ return "layout";
338
+ }
339
+ const paths = tab?.session.selection ?? [];
340
+ const selection = primarySelection(paths);
341
+ if (!tab || !selection) {
342
+ return "";
343
+ }
344
+ // A batch is not a place, so the address bar names its SIZE rather than one of its members —
345
+ // Printing the primary's tag would say `section` while five other elements were also selected.
346
+ if (paths.length > 1) {
347
+ return `${paths.length} elements`;
348
+ }
349
+ return nodeLabel(getNodeAtPath(tab.doc.document, selection));
350
+ }
351
+
352
+ /**
353
+ * `◈ project › document › selection`, right-aligned ⌘K.
354
+ *
355
+ * Four facts about where you are, in non-collapsible chrome (§4.4), each one click-through to the
356
+ * surface that owns it. The empty space between the segments opens the mode picker, which is what
357
+ * makes the pill an address bar rather than three buttons.
358
+ */
359
+ function commandCenterTpl(registry: CommandRegistry | null) {
360
+ const tab = activeTab.value ?? null;
361
+ const chord = registry?.keymap.formatBinding("palette.open") ?? "";
362
+ const selection = selectionSegmentLabel(tab);
363
+ const segments: PillSegment[] = [
364
+ {
365
+ key: "project",
366
+ label: projectState?.name ?? "No project",
367
+ title: "Switch project — opens Project: Open Recent…",
368
+ onClick: () => openQuickSearch("projects"),
369
+ },
370
+ {
371
+ key: "document",
372
+ label: documentSegmentLabel(tab),
373
+ title: "Go to a file",
374
+ onClick: () => openQuickSearch("files"),
375
+ },
376
+ ];
377
+ if (selection) {
378
+ segments.push({
379
+ key: "selection",
380
+ label: selection,
381
+ title: "Go to an element in this document",
382
+ onClick: () => openQuickSearch("nodes"),
383
+ });
384
+ }
385
+ return html`
386
+ <div
387
+ class="tb-center"
388
+ role="group"
389
+ aria-label="Command Center"
390
+ @click=${() => openQuickSearch("picker")}
391
+ >
392
+ <span class="tb-center-mark" aria-hidden="true">◈</span>
393
+ ${segments.map(
394
+ (segment, index) => html`
395
+ ${index > 0 ? html`<span class="tb-center-sep" aria-hidden="true">›</span>` : nothing}
396
+ <button
397
+ class="tb-center-seg"
398
+ type="button"
399
+ title=${segment.title}
400
+ @click=${(e: Event) => {
401
+ e.stopPropagation();
402
+ segment.onClick();
403
+ }}
404
+ >
405
+ ${segment.label}
406
+ </button>
407
+ `,
408
+ )}
409
+ ${chord ? html`<kbd class="tb-center-chord">${chord}</kbd>` : nothing}
410
+ </div>
411
+ `;
412
+ }
413
+
414
+ // ─── ①b Layout tabs ──────────────────────────────────────────────────────────
415
+
416
+ /**
417
+ * `Write · Design · Build · Ship · +` — named arrangements, as plain-text tabs.
418
+ *
419
+ * The 80% of a workspace switcher that costs a segmented control and none of the re-architecture
420
+ * (§3.2 ①b). Each tab RUNS `view.setLayout`, so the click, the palette row and an agent all take
421
+ * the same path; double-clicking one renames it, and `+` saves whatever is on screen now.
422
+ *
423
+ * **A layout reconfigures; it never removes.** Nothing here hides a panel: every one stays on the
424
+ * rail, on its chord and in the palette after any layout is applied — §13 is explicit that
425
+ * workspaces which gate features hand the non-technical user the affordance they are least likely
426
+ * to reach for.
427
+ */
428
+ function layoutTabsTpl(registry: CommandRegistry) {
429
+ // `get` first, like {@link tbCmd}: a registry that has not been handed the shell's records yet
430
+ // (the skeleton the bar paints before the bootstrap composes them) has no verb to ask about.
431
+ if (!registry.get("view.setLayout") || !registry.isEnabled("view.setLayout")) {
432
+ return nothing;
433
+ }
434
+ return html`
435
+ <div class="tb-layouts" role="tablist" aria-label="Layouts">
436
+ ${shell.layouts.map(
437
+ (preset) => html`
438
+ <button
439
+ class=${preset.id === shell.layout ? "tb-layout active" : "tb-layout"}
440
+ type="button"
441
+ role="tab"
442
+ aria-selected=${preset.id === shell.layout ? "true" : "false"}
443
+ title=${`${preset.name} layout — double-click to rename`}
444
+ @click=${() => {
445
+ void registry.run("view.setLayout", { layout: preset.id });
446
+ }}
447
+ @dblclick=${() => {
448
+ void renameLayoutPrompt(registry, preset.id, preset.name);
449
+ }}
450
+ >
451
+ ${preset.name}
452
+ </button>
453
+ `,
454
+ )}
455
+ <button
456
+ class="tb-layout-add"
457
+ type="button"
458
+ title="Save the current arrangement as a layout"
459
+ aria-label="Save layout"
460
+ @click=${() => {
461
+ void saveLayoutPrompt(registry);
462
+ }}
463
+ >
464
+ +
465
+ </button>
466
+ </div>
467
+ `;
468
+ }
469
+
470
+ /** Ask for a name, then run the command. Exported for the same reason `runOpenInBrowser` is. */
471
+ export async function saveLayoutPrompt(registry: CommandRegistry): Promise<void> {
472
+ const name = await showPromptDialog("Save layout", {
473
+ confirmLabel: "Save",
474
+ message: "Remembers this project's Navigator panel, dock widths and Inspector tab.",
475
+ placeholder: "Layout name",
476
+ });
477
+ if (name) {
478
+ await registry.run("view.saveLayout", { name });
148
479
  }
149
480
  }
150
481
 
151
- async function handleNewProject() {
152
- const result = await openNewProjectModal();
153
- if (result && _ctx) {
154
- await _ctx.openRecentProject(result.root);
482
+ /** The double-click gesture, routed through the command so the rename has one implementation. */
483
+ export async function renameLayoutPrompt(
484
+ registry: CommandRegistry,
485
+ layout: string,
486
+ current: string,
487
+ ): Promise<void> {
488
+ const name = await showPromptDialog("Rename layout", { confirmLabel: "Rename", value: current });
489
+ if (name) {
490
+ await registry.run("view.renameLayout", { layout, name });
155
491
  }
156
492
  }
157
493
 
494
+ // ─── The ⬢ app menu (commandbar/overflow) ────────────────────────────────────
495
+
158
496
  /**
159
- * The chevron dropdown beside "Open Project": New Project, the recent-projects list (each with a
160
- * remove affordance), and a clear-all action. Shared by both the minimal and full toolbars.
497
+ * Everything that declared `commandbar/overflow` — the chrome's residue for retired controls.
161
498
  *
162
- * @param {ToolbarCtx} ctx
499
+ * Rendered as a menu of the records themselves, so a command that moves from the primary cluster to
500
+ * the overflow keeps its name, its chord and its gate, and the move is one edit to its `menus`.
163
501
  */
164
- function recentMenuTpl(ctx: ToolbarCtx) {
165
- const recentProjects = getRecentProjects();
502
+ function appMenuTpl(registry: CommandRegistry) {
503
+ const commands = registry.forPlacement("commandbar/overflow");
166
504
  return html`
167
505
  <overlay-trigger placement="bottom-start" triggered-by="click">
168
- <sp-action-button size="s" slot="trigger" title="Recent projects" class="tb-split-trigger">
169
- <sp-icon-chevron-down slot="icon"></sp-icon-chevron-down>
506
+ <sp-action-button size="s" quiet slot="trigger" title="Studio menu" aria-label="Studio menu">
507
+ <sp-icon-show-menu slot="icon"></sp-icon-show-menu>
170
508
  </sp-action-button>
171
509
  <sp-popover slot="click-content" tip>
172
510
  <sp-menu
173
511
  @change=${(e: Event) => {
174
- const val = (e.target as unknown as HTMLInputElement).value;
175
- if (val === "__new__") {
176
- void handleNewProject();
177
- } else if (val === "__clear__") {
178
- clearRecentProjects();
179
- render();
180
- } else {
181
- void ctx.openRecentProject(val);
512
+ const id = (e.target as unknown as HTMLInputElement).value;
513
+ if (registry.isEnabled(id)) {
514
+ void registry.run(id);
182
515
  }
183
516
  }}
184
517
  >
185
- <sp-menu-item value="__new__">New Project…</sp-menu-item>
186
- ${
187
- recentProjects.length > 0
188
- ? html`
189
- <sp-menu-divider></sp-menu-divider>
190
- ${recentProjects.map(
191
- (p) => html`
192
- <sp-menu-item value=${p.root} title=${p.root}>
193
- ${p.name}
194
- <sp-action-button
195
- slot="end"
196
- quiet
197
- size="s"
198
- title="Remove from recent"
199
- @click=${(e: Event) => {
200
- e.stopPropagation();
201
- removeRecentProject(p.root);
202
- render();
203
- }}
204
- >
205
- <sp-icon-close slot="icon"></sp-icon-close>
206
- </sp-action-button>
207
- </sp-menu-item>
208
- `,
209
- )}
210
- <sp-menu-divider></sp-menu-divider>
211
- <sp-menu-item value="__clear__">Clear recent projects</sp-menu-item>
212
- `
213
- : nothing
214
- }
518
+ ${commands.map(
519
+ (command) => html`
520
+ <sp-menu-item
521
+ value=${command.id}
522
+ ?disabled=${!registry.isEnabled(command.id)}
523
+ title=${commandTooltip(registry, command.id)}
524
+ >
525
+ ${command.title}
526
+ <span slot="value">${registry.keymap.formatBinding(command.id) ?? ""}</span>
527
+ </sp-menu-item>
528
+ `,
529
+ )}
215
530
  </sp-menu>
216
531
  </sp-popover>
217
532
  </overlay-trigger>
218
533
  `;
219
534
  }
220
535
 
221
- /** @param {ToolbarCtx} ctx */
222
- function minimalToolbarTemplate(ctx: ToolbarCtx) {
223
- const recentProjectsTpl = recentMenuTpl(ctx);
224
-
225
- const windowControls = (
226
- globalThis as unknown as {
227
- __jxPlatform?: {
228
- windowControls?: {
229
- minimize: () => void;
230
- maximize: () => void;
231
- close: () => void;
232
- };
233
- };
234
- }
235
- ).__jxPlatform?.windowControls;
236
- const csdTpl = windowControls
237
- ? html`
238
- <sp-action-group class="window-controls" size="s">
239
- <sp-action-button
240
- quiet
241
- size="s"
242
- title="Minimize"
243
- @click=${() => windowControls.minimize()}
244
- >
245
- <sp-icon-remove slot="icon"></sp-icon-remove>
246
- </sp-action-button>
247
- <sp-action-button
248
- quiet
249
- size="s"
250
- title="Maximize"
251
- @click=${() => windowControls.maximize()}
252
- >
253
- <sp-icon-rectangle slot="icon"></sp-icon-rectangle>
254
- </sp-action-button>
255
- <sp-action-button
256
- quiet
257
- size="s"
258
- title="Close"
259
- class="csd-close"
260
- @click=${() => windowControls.close()}
261
- >
262
- <sp-icon-close slot="icon"></sp-icon-close>
263
- </sp-action-button>
264
- </sp-action-group>
265
- `
266
- : nothing;
536
+ // ─── Window controls ──────────────────────────────────────────────────────────
267
537
 
268
- return html`
269
- <div class="tb-split-btn">
270
- <sp-action-button
271
- size="s"
272
- class="tb-split-main"
273
- title="Open Project"
274
- @click=${ctx.openProject}
275
- >
276
- ${toolbarIconMap["sp-icon-folder-open"]}<span class="tb-label">Open Project</span>
277
- </sp-action-button>
278
- ${recentProjectsTpl}
279
- </div>
280
- ${tbBtnTpl("Manage", openBrowseModal, "sp-icon-view-list")}
281
- <sp-action-button size="s" title="Save" disabled>
282
- ${toolbarIconMap["sp-icon-save-floppy"]}<span class="tb-label">Save</span>
283
- </sp-action-button>
284
- <sp-action-group compact size="s">
285
- <sp-action-button size="s" title="Undo" disabled>
286
- ${toolbarIconMap["sp-icon-undo"]}<span class="tb-label">Undo</span>
287
- </sp-action-button>
288
- <sp-action-button size="s" title="Redo" disabled>
289
- ${toolbarIconMap["sp-icon-redo"]}<span class="tb-label">Redo</span>
290
- </sp-action-button>
291
- </sp-action-group>
292
- <div class="tb-spacer"></div>
538
+ interface WindowControls {
539
+ minimize: () => void;
540
+ maximize: () => void;
541
+ close: () => void;
542
+ }
543
+
544
+ function windowControls(): WindowControls | undefined {
545
+ return (globalThis as unknown as { __jxPlatform?: { windowControls?: WindowControls } })
546
+ .__jxPlatform?.windowControls;
547
+ }
548
+
549
+ /** Client-side decorations. Mac puts them leading and close-first; everything else trailing. */
550
+ function csdTpl(controls: WindowControls, mac: boolean) {
551
+ const minimize = html`
293
552
  <sp-action-button
294
- class="tb-search-trigger"
295
- size="s"
296
553
  quiet
297
- title="Search files (⌘P)"
298
- @click=${openQuickSearch}
554
+ size="s"
555
+ title="Minimize"
556
+ class="csd-minimize"
557
+ @click=${() => controls.minimize()}
299
558
  >
300
- <sp-icon-search slot="icon"></sp-icon-search>
301
- <span class="tb-search-label">Search files… <kbd>⌘P</kbd></span>
559
+ <sp-icon-remove slot="icon"></sp-icon-remove>
302
560
  </sp-action-button>
303
- <div class="tb-spacer"></div>
304
- <sp-action-group selects="single" size="s" compact>
305
- ${modes.map(
306
- (m) => html`
307
- <sp-action-button size="s" title=${m.label} disabled ?selected=${m.key === "design"}>
308
- ${toolbarIconMap[m.iconTag]}<span class="tb-label">${m.label}</span>
309
- </sp-action-button>
310
- `,
311
- )}
312
- </sp-action-group>
561
+ `;
562
+ const maximize = html`
313
563
  <sp-action-button
314
564
  quiet
315
565
  size="s"
316
- title="Toggle Right Panel"
317
- @click=${() => {
318
- view.rightPanelCollapsed = !view.rightPanelCollapsed;
319
- applyPanelCollapse();
320
- render();
321
- }}
566
+ title="Maximize"
567
+ class="csd-maximize"
568
+ @click=${() => controls.maximize()}
322
569
  >
323
- ${
324
- view.rightPanelCollapsed
325
- ? html`<sp-icon-rail-right-open slot="icon"></sp-icon-rail-right-open>`
326
- : html`<sp-icon-rail-right-close slot="icon"></sp-icon-rail-right-close>`
327
- }
570
+ <sp-icon-rectangle slot="icon"></sp-icon-rectangle>
328
571
  </sp-action-button>
329
- ${chatToggleTpl()} ${csdTpl}
330
572
  `;
331
- }
332
-
333
- /** Toggle for the persistent AI chat sidebar (selected = open). Shared by both layouts. */
334
- function chatToggleTpl() {
335
- return html`
573
+ const close = html`
336
574
  <sp-action-button
337
575
  quiet
338
576
  size="s"
339
- title="Toggle Assistant"
340
- ?selected=${!view.chatPanelCollapsed}
341
- @click=${() => {
342
- view.chatPanelCollapsed = !view.chatPanelCollapsed;
343
- applyPanelCollapse();
344
- render();
345
- }}
577
+ title="Close"
578
+ class="csd-close"
579
+ @click=${() => controls.close()}
346
580
  >
347
- <sp-icon-chat slot="icon"></sp-icon-chat>
581
+ <sp-icon-close slot="icon"></sp-icon-close>
348
582
  </sp-action-button>
349
583
  `;
584
+ return mac
585
+ ? html`<sp-action-group class="window-controls mac" size="s">
586
+ ${close}${minimize}${maximize}
587
+ </sp-action-group>`
588
+ : html`<sp-action-group class="window-controls" size="s">
589
+ ${minimize}${maximize}${close}
590
+ </sp-action-group>`;
350
591
  }
351
592
 
352
- const modes = [
353
- { iconTag: "sp-icon-edit", key: "edit", label: "Edit" },
354
- { iconTag: "sp-icon-artboard", key: "design", label: "Design" },
355
- { iconTag: "sp-icon-view-grid", key: "grid", label: "Grid" },
356
- { iconTag: "sp-icon-code", key: "source", label: "Code" },
357
- { iconTag: "sp-icon-brush", key: "stylebook", label: "Stylebook" },
358
- ];
593
+ // ─── The three dock toggles ───────────────────────────────────────────────────
359
594
 
360
- function toolbarTemplate() {
361
- const tab = activeTab.value;
362
- if (!_ctx) {
363
- return html``;
364
- }
365
- const ctx = _ctx;
595
+ /* The window, with THIS dock's rail marked — one shipped glyph per region.
366
596
 
367
- if (!tab) {
368
- return minimalToolbarTemplate(ctx);
369
- }
597
+ The pair these replace was `rail-right-open`/`close` under `scaleX(-1)`, because the left-hand
598
+ pair looked absent. It is not: `rail-left`, `rail-right` and `rail-bottom` are all in the
599
+ workflow set, and they are a better vocabulary than an arrow besides. An arrow says which way
600
+ something will move and nothing about what or where, which is why the Bottom dock could never
601
+ join the set and carried a static `align-bottom` instead.
370
602
 
371
- const allowedModes = new Set(tab.capabilities.modes);
372
- const canUndo = tabCanUndo(tab);
373
- const canRedo = tabCanRedo(tab);
374
- const canSave = tab.doc.dirty;
375
-
376
- const S = {
377
- dirty: tab.doc.dirty,
378
- fileHandle: tab.fileHandle,
379
- mode: tab.doc.mode,
380
- selection: tab.session.selection,
381
- ui: tab.session.ui,
382
- };
383
- // Base mode, not the effective mode: the switcher keeps Edit/Design highlighted while the
384
- // Tab-bar preview toggle is on (preview is no longer a switchable mode).
385
- const { canvasMode } = tab.session.ui;
386
-
387
- const modeSwitcherTpl = html`
388
- <sp-action-group selects="single" size="s" compact>
389
- ${modes.map(
390
- (m) => html`
391
- <sp-action-button
392
- size="s"
393
- title=${m.label}
394
- ?selected=${canvasMode === m.key}
395
- ?disabled=${!allowedModes.has(m.key)}
396
- @click=${() => {
397
- if (canvasMode === m.key) {
398
- return;
399
- }
400
- if (!allowedModes.has(m.key)) {
401
- return;
402
- }
403
- if (S.ui.editingFunction && view.functionEditor) {
404
- view.functionEditor.dispose();
405
- view.functionEditor = null;
406
- }
407
- ctx.setCanvasMode(m.key);
408
- view.panX = 0;
409
- view.panY = 0;
410
- /** @type {{ editingFunction: null; rightTab?: string }} */
411
- const uiPatch: { editingFunction: null; rightTab?: string } = {
412
- editingFunction: null,
413
- };
414
- if (m.key === "stylebook") {
415
- uiPatch.rightTab = "style";
416
- }
417
- updateSession({ ui: uiPatch });
418
- ctx.renderCanvas();
419
- ctx.safeRenderRightPanel();
420
- }}
421
- >
422
- ${toolbarIconMap[m.iconTag]}<span class="tb-label">${m.label}</span>
423
- </sp-action-button>
424
- `,
425
- )}
426
- </sp-action-group>
603
+ The mirror was also worth deleting on its own terms. Spectrum's two right-hand glyphs are exact
604
+ mirror images OF EACH OTHER, so flipping the wrong member of the pair lands on the other
605
+ member's appearance — a real, crisp arrow pointing the wrong way, looking entirely deliberate.
606
+ It shipped crossed and nothing could see it. Three distinct glyphs cannot fail that way.
607
+
608
+ State stays where it already was and where it is already asserted: `?selected` on the button. */
609
+ const DOCK_ICONS = {
610
+ bottom: html`<sp-icon-rail-bottom slot="icon"></sp-icon-rail-bottom>`,
611
+ left: html`<sp-icon-rail-left slot="icon"></sp-icon-rail-left>`,
612
+ right: html`<sp-icon-rail-right slot="icon"></sp-icon-rail-right>`,
613
+ } as const;
614
+
615
+ /**
616
+ * ▤▥▦ — the three docks, each rendered from its own record.
617
+ *
618
+ * The glyph flips with the dock's state and `?selected` reports it, so the control says which way
619
+ * it will go; the NAME and the chord still come from the record, which is why ⌘B and this button
620
+ * cannot drift apart the way ⌘W and the tab strip's × did.
621
+ */
622
+ function dockTogglesTpl(registry: CommandRegistry) {
623
+ return html`
624
+ ${tbCmd(registry, "view.toggleNavigator", {
625
+ compact: true,
626
+ icon: DOCK_ICONS.left,
627
+ selected: !shell.docks.left.collapsed,
628
+ })}
629
+ ${tbCmd(registry, "view.toggleInspector", {
630
+ compact: true,
631
+ icon: DOCK_ICONS.right,
632
+ selected: !shell.docks.right.collapsed,
633
+ })}
634
+ ${tbCmd(registry, "view.toggleBottomDock", {
635
+ compact: true,
636
+ icon: DOCK_ICONS.bottom,
637
+ selected: !shell.docks.bottom.collapsed,
638
+ })}
427
639
  `;
640
+ }
428
641
 
429
- const windowControls = (
430
- globalThis as unknown as {
431
- __jxPlatform?: {
432
- windowControls?: {
433
- minimize: () => void;
434
- maximize: () => void;
435
- close: () => void;
436
- };
437
- };
438
- }
439
- ).__jxPlatform?.windowControls;
440
- const isMac = isMacPlatform();
441
- const csdTpl = windowControls
442
- ? isMac
443
- ? html`
444
- <sp-action-group class="window-controls mac" size="s">
445
- <sp-action-button
446
- quiet
447
- size="s"
448
- title="Close"
449
- class="csd-close"
450
- @click=${() => windowControls.close()}
451
- >
452
- <sp-icon-close slot="icon"></sp-icon-close>
453
- </sp-action-button>
454
- <sp-action-button
455
- quiet
456
- size="s"
457
- title="Minimize"
458
- class="csd-minimize"
459
- @click=${() => windowControls.minimize()}
460
- >
461
- <sp-icon-remove slot="icon"></sp-icon-remove>
462
- </sp-action-button>
463
- <sp-action-button
464
- quiet
465
- size="s"
466
- title="Maximize"
467
- class="csd-maximize"
468
- @click=${() => windowControls.maximize()}
469
- >
470
- <sp-icon-rectangle slot="icon"></sp-icon-rectangle>
471
- </sp-action-button>
472
- </sp-action-group>
473
- `
474
- : html`
475
- <sp-action-group class="window-controls" size="s">
476
- <sp-action-button
477
- quiet
478
- size="s"
479
- title="Minimize"
480
- class="csd-minimize"
481
- @click=${() => windowControls.minimize()}
482
- >
483
- <sp-icon-remove slot="icon"></sp-icon-remove>
484
- </sp-action-button>
485
- <sp-action-button
486
- quiet
487
- size="s"
488
- title="Maximize"
489
- class="csd-maximize"
490
- @click=${() => windowControls.maximize()}
491
- >
492
- <sp-icon-rectangle slot="icon"></sp-icon-rectangle>
493
- </sp-action-button>
494
- <sp-action-button
495
- quiet
496
- size="s"
497
- title="Close"
498
- class="csd-close"
499
- @click=${() => windowControls.close()}
500
- >
501
- <sp-icon-close slot="icon"></sp-icon-close>
502
- </sp-action-button>
503
- </sp-action-group>
504
- `
505
- : nothing;
506
-
507
- const recentProjectsTpl = recentMenuTpl(ctx);
642
+ // ─── Mount ────────────────────────────────────────────────────────────────────
643
+
644
+ /**
645
+ * Mount the Command Bar.
646
+ *
647
+ * @param rootEl The `#toolbar` host, stamped `commandbar` by `stampShellRegions()`.
648
+ * @param _ctx Ignored — see {@link ToolbarCtx}.
649
+ */
650
+ export function mount(rootEl: HTMLElement, _ctx: ToolbarCtx = {}) {
651
+ _rootEl = rootEl;
652
+ if (windowControls()) {
653
+ rootEl.classList.add("electrobun-webkit-app-region-drag");
654
+ }
655
+ _scope = effectScope();
656
+ _scope.run(() => {
657
+ effect(() => {
658
+ // Dock visibility, source control and the project are shell state, tracked here so the band
659
+ // Follows a flip made from anywhere — the automation runner, the New Project agent hand-off,
660
+ // The boot-time restore — not just this module's click handlers.
661
+ void shell.docks.left.collapsed;
662
+ void shell.docks.right.collapsed;
663
+ void shell.docks.bottom.collapsed;
664
+ void shell.git.status;
665
+ void shell.layoutSelection;
666
+ // The layout tabs are a rendering of the project's own record (§3.2 ①b), so the band
667
+ // Repaints when a layout is saved, renamed or deleted from anywhere.
668
+ void shell.layout;
669
+ void shell.layouts;
670
+ // The registry itself is reactive state: it is composed AFTER this mount runs, and reading it
671
+ // Here is what repaints the band from a skeleton into the real bar.
672
+ void activeRegistry();
673
+ const tab = activeTab.value;
674
+ if (tab) {
675
+ void tab.doc.document;
676
+ void tab.doc.dirty;
677
+ void tab.doc.mode;
678
+ // The whole SET, joined — a bare property read would not re-trigger when the selection
679
+ // Changes WITHIN the array, and §6.5's helpers always replace it but nothing enforces that.
680
+ void tab.session.selection.map((path) => path.join("/")).join("|");
681
+ void tab.session.ui.canvasMode;
682
+ // Open in Browser needs a value for every route param before it can resolve a page.
683
+ void tab.session.ui.previewParams;
684
+ void tab.history.index;
685
+ void tab.history.snapshots.length;
686
+ }
687
+ render();
688
+ });
689
+ });
690
+ }
691
+
692
+ export function unmount() {
693
+ _scope?.stop();
694
+ _scope = null;
695
+ _rootEl = null;
696
+ }
697
+
698
+ export function render() {
699
+ if (!_rootEl) {
700
+ return;
701
+ }
702
+ try {
703
+ litRender(toolbarTemplate(), _rootEl);
704
+ } catch (error) {
705
+ console.error("toolbar render error:", error);
706
+ }
707
+ }
508
708
 
709
+ /**
710
+ * The band.
711
+ *
712
+ * ONE template for every state. With no project open the records' `when` clauses empty the primary
713
+ * cluster and the pill reads "No project"; there is no second variant to keep in step.
714
+ */
715
+ function toolbarTemplate() {
716
+ const registry = activeRegistry();
717
+ const controls = windowControls();
718
+ const mac = isMacPlatform();
719
+ const csd = controls ? csdTpl(controls, mac) : nothing;
720
+ const tab = activeTab.value ?? null;
509
721
  return html`
510
- ${isMac ? csdTpl : nothing}
511
- <div class="tb-split-btn">
512
- <sp-action-button
513
- size="s"
514
- class="tb-split-main"
515
- title="Open Project"
516
- @click=${ctx.openProject}
517
- >
518
- ${toolbarIconMap["sp-icon-folder-open"]}<span class="tb-label">Open Project</span>
519
- </sp-action-button>
520
- ${recentProjectsTpl}
521
- </div>
522
- ${tbBtnTpl("Manage", openBrowseModal, "sp-icon-view-list")}
523
- ${tbBtnTpl("Publish", openPublishPanel)}
524
- <sp-action-button size="s" title="Save" ?disabled=${!canSave} @click=${ctx.saveFile}>
525
- ${toolbarIconMap["sp-icon-save-floppy"]}<span class="tb-label">Save</span>
526
- </sp-action-button>
527
- <sp-action-group compact size="s">
528
- <sp-action-button
529
- size="s"
530
- title="Undo"
531
- ?disabled=${!canUndo}
532
- @click=${() => tabUndo(activeTab.value!)}
533
- >
534
- ${toolbarIconMap["sp-icon-undo"]}<span class="tb-label">Undo</span>
535
- </sp-action-button>
536
- <sp-action-button
537
- size="s"
538
- title="Redo"
539
- ?disabled=${!canRedo}
540
- @click=${() => tabRedo(activeTab.value!)}
541
- >
542
- ${toolbarIconMap["sp-icon-redo"]}<span class="tb-label">Redo</span>
543
- </sp-action-button>
544
- </sp-action-group>
545
- ${presenceChipsTemplate(tab)}
722
+ ${mac ? csd : nothing} ${registry ? appMenuTpl(registry) : nothing}
723
+ ${registry ? layoutTabsTpl(registry) : nothing}
724
+ <div class="tb-spacer"></div>
725
+ ${commandCenterTpl(registry)}
546
726
  <div class="tb-spacer"></div>
547
- <sp-action-button
548
- class="tb-search-trigger"
549
- size="s"
550
- quiet
551
- title="Search files (⌘P)"
552
- @click=${openQuickSearch}
553
- >
554
- <sp-icon-search slot="icon"></sp-icon-search>
555
- <span class="tb-search-label">Search files… <kbd>⌘P</kbd></span>
556
- </sp-action-button>
557
727
  ${
558
- (activeTab.value?.session.ui.gitStatus?.behind ?? 0) > 0
559
- ? html`<sp-action-button
560
- size="s"
561
- title="Sync Project"
562
- @click=${async () => {
563
- await getPlatform().gitPull();
564
- await refreshGitStatus();
565
- }}
566
- >
567
- <sp-icon-download slot="icon"></sp-icon-download>
568
- <span class="tb-label">Sync Project</span>
569
- </sp-action-button>`
728
+ registry
729
+ ? html`<sp-action-group compact size="s">
730
+ ${registry
731
+ .forPlacement("commandbar/primary")
732
+ .map((command) => tbCmd(registry, command.id))}
733
+ </sp-action-group>`
570
734
  : nothing
571
735
  }
572
- <div class="tb-spacer"></div>
573
- ${modeSwitcherTpl}
574
- <sp-action-button
575
- quiet
576
- size="s"
577
- title="Toggle Right Panel"
578
- @click=${() => {
579
- view.rightPanelCollapsed = !view.rightPanelCollapsed;
580
- applyPanelCollapse();
581
- render();
582
- }}
583
- >
584
- ${
585
- view.rightPanelCollapsed
586
- ? html`<sp-icon-rail-right-open slot="icon"></sp-icon-rail-right-open>`
587
- : html`<sp-icon-rail-right-close slot="icon"></sp-icon-rail-right-close>`
588
- }
589
- </sp-action-button>
590
- ${chatToggleTpl()} ${isMac ? nothing : csdTpl}
736
+ ${tab ? presenceChipsTemplate(tab) : nothing} ${registry ? dockTogglesTpl(registry) : nothing}
737
+ ${mac ? nothing : csd}
591
738
  `;
592
739
  }