@jxsuite/studio 6.1.2 → 8.0.1

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 (522) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/2020-5wtzce5g.js +4 -0
  3. package/dist/chunks/adapter-5tphg7xe.js +4 -0
  4. package/dist/chunks/add-new-flow-psym14sn.js +4 -0
  5. package/dist/chunks/cf-account-picker-haqazq5r.js +4 -0
  6. package/dist/chunks/cf-settings-h2wrh6hq.js +4 -0
  7. package/dist/chunks/{color-area-0fj8a4ga.js → color-area-6rbra5dy.js} +2 -2
  8. package/dist/chunks/{color-field-xch8kx2g.js → color-field-gy56c94g.js} +3 -3
  9. package/dist/chunks/color-m7r1vk54.js +4 -0
  10. package/dist/chunks/{color-slider-jxd8v350.js → color-slider-sqqdyf6c.js} +2 -2
  11. package/dist/chunks/{combobox-p0efwjke.js → combobox-72h32ejk.js} +3 -3
  12. package/dist/chunks/components-r22xca09.js +4 -0
  13. package/dist/chunks/content-source-mmppmcyg.js +4 -0
  14. package/dist/chunks/convert-to-repeater-he70fc2b.js +4 -0
  15. package/dist/chunks/editor-ccd9jwsa.js +4 -0
  16. package/dist/chunks/entry-commands-6qxz9e8p.js +4 -0
  17. package/dist/chunks/extension-sections-0egb88dm.js +4 -0
  18. package/dist/chunks/file-ops-qnab36ek.js +4 -0
  19. package/dist/chunks/files-rcn56qvm.js +4 -0
  20. package/dist/chunks/format-host-yxz0kjjs.js +4 -0
  21. package/dist/chunks/fs-events-d1qxq4z4.js +4 -0
  22. package/dist/chunks/grid-open-4z1g062t.js +4 -0
  23. package/dist/chunks/{grid-view-d2wq9ezg.js → grid-view-qg1b5eqx.js} +25 -25
  24. package/dist/chunks/{icons-pnfz478q.js → icons-63bjtbs7.js} +2 -2
  25. package/dist/chunks/{iframe-entry-jdcktez8.js → iframe-entry-2zggzajv.js} +4 -4
  26. package/dist/chunks/{iframe-entry-zap785cw.js → iframe-entry-76j8vwwx.js} +4 -4
  27. package/dist/chunks/iframe-entry-99nr5zfb.js +5 -0
  28. package/dist/chunks/{iframe-entry-x0ygf2yr.js → iframe-entry-pkgmcgp0.js} +4 -4
  29. package/dist/chunks/iframe-host-ssv7kyj0.js +4 -0
  30. package/dist/chunks/{index-zpjmzqfk.js → index-32eqzv1k.js} +3 -3
  31. package/dist/chunks/index-f0jn7nnp.js +4 -0
  32. package/dist/chunks/{javascript-m54mnwp6.js → javascript-pjp734q4.js} +3 -3
  33. package/dist/chunks/jsonMode-njkvsa02.js +13 -0
  34. package/dist/chunks/jx-validate-ttasrbh7.js +4 -0
  35. package/dist/chunks/layers-bgyr11tr.js +4 -0
  36. package/dist/chunks/layers-panel-7ez66zeq.js +4 -0
  37. package/dist/chunks/{listbox-e74qjyq6.js → listbox-c421cf8c.js} +2 -2
  38. package/dist/chunks/lit-html-v9gh687x.js +4 -0
  39. package/dist/chunks/media-library-sq468wts.js +4 -0
  40. package/dist/chunks/media-picker-shfe59pt.js +4 -0
  41. package/dist/chunks/{menu-n6yzswmy.js → menu-bdpzbfvs.js} +2 -2
  42. package/dist/chunks/{monaco-binding-7gx83x3r.js → monaco-binding-vq1hgnn0.js} +3 -3
  43. package/dist/chunks/monaco-buffer-3afgwvbp.js +4 -0
  44. package/dist/chunks/monaco-setup-xrd2cbkp.js +4 -0
  45. package/dist/chunks/notify-kzrjr3fv.js +4 -0
  46. package/dist/chunks/parse-v77xtwv7.js +4 -0
  47. package/dist/chunks/platform-7v4rtd7m.js +4 -0
  48. package/dist/chunks/popover-b89sfej7.js +4 -0
  49. package/dist/chunks/publish-panel-2z9gw8ee.js +4 -0
  50. package/dist/chunks/publish-quick-qx2455cj.js +4 -0
  51. package/dist/chunks/publish-workspace-wrc1dgwx.js +4 -0
  52. package/dist/chunks/quick-search-n0t0eqxb.js +4 -0
  53. package/dist/chunks/references-9547rg5s.js +4 -0
  54. package/dist/chunks/right-panel-wsgnr9pn.js +4 -0
  55. package/dist/chunks/source-control-kwktr9y6.js +4 -0
  56. package/dist/chunks/{split-n2gfpm5m.js → split-c9zfdgxf.js} +2 -2
  57. package/dist/chunks/studio-03ct6yfv.js +9 -0
  58. package/dist/chunks/studio-07rcfje3.js +5 -0
  59. package/dist/chunks/studio-3e54qh39.js +7 -0
  60. package/dist/chunks/studio-3exxhz5g.js +5 -0
  61. package/dist/chunks/studio-3q40e3rj.js +5 -0
  62. package/dist/chunks/studio-3qeh9t0h.js +6 -0
  63. package/dist/chunks/studio-3qqjeep5.js +6 -0
  64. package/dist/chunks/studio-43sd2mam.js +4 -0
  65. package/dist/chunks/studio-4hp8pkyt.js +5 -0
  66. package/dist/chunks/studio-58xg6j2q.js +9 -0
  67. package/dist/chunks/studio-5g0rprdt.js +7 -0
  68. package/dist/chunks/{studio-2j1cb95x.js → studio-6m380wp7.js} +4 -4
  69. package/dist/chunks/studio-80jyh1xh.js +4 -0
  70. package/dist/chunks/studio-83mk4bgg.js +9 -0
  71. package/dist/chunks/{studio-v0jzqdjm.js → studio-881z1xrx.js} +4 -4
  72. package/dist/chunks/studio-8m0y2qe7.js +5 -0
  73. package/dist/chunks/{studio-d81xfh1g.js → studio-8tr6cfrc.js} +4 -4
  74. package/dist/chunks/studio-8vcvfs7r.js +6 -0
  75. package/dist/chunks/studio-9tmebf0s.js +5 -0
  76. package/dist/chunks/studio-a2bvaptz.js +5 -0
  77. package/dist/chunks/studio-b7y5mr30.js +5 -0
  78. package/dist/chunks/studio-bavb31r0.js +5 -0
  79. package/dist/chunks/{studio-dgz8d0c1.js → studio-bbe4c90a.js} +4 -4
  80. package/dist/chunks/studio-c05wdnt3.js +5 -0
  81. package/dist/chunks/studio-ccv2svc6.js +5 -0
  82. package/dist/chunks/studio-d5grtb9e.js +16 -0
  83. package/dist/chunks/studio-df0baea9.js +5 -0
  84. package/dist/chunks/studio-dgk4dqtx.js +7 -0
  85. package/dist/chunks/studio-e1g64q43.js +5 -0
  86. package/dist/chunks/studio-ed1hp6zf.js +6 -0
  87. package/dist/chunks/studio-ed3xx40h.js +7 -0
  88. package/dist/chunks/studio-f14e1zdj.js +5 -0
  89. package/dist/chunks/studio-fbhxxc5f.js +5 -0
  90. package/dist/chunks/studio-fyh0jc96.js +5 -0
  91. package/dist/chunks/studio-gcxk20n7.js +5 -0
  92. package/dist/chunks/studio-jaaz8ce9.js +5 -0
  93. package/dist/chunks/{studio-90pq9km1.js → studio-jcnsdaep.js} +4 -4
  94. package/dist/chunks/studio-jsgf2q5k.js +7 -0
  95. package/dist/chunks/{studio-0q988f9w.js → studio-k3xxbagr.js} +3 -3
  96. package/dist/chunks/{studio-5ve6bbep.js → studio-k7evxqh4.js} +4 -4
  97. package/dist/chunks/studio-kt3pjkwd.js +5 -0
  98. package/dist/chunks/studio-m8g16bgz.js +5 -0
  99. package/dist/chunks/studio-mn0476ag.js +5 -0
  100. package/dist/chunks/studio-mx1smyq0.js +5 -0
  101. package/dist/chunks/studio-n0c5h6mf.js +5 -0
  102. package/dist/chunks/{studio-485046te.js → studio-nh6d342n.js} +7 -7
  103. package/dist/chunks/studio-nqxqf8p2.js +4 -0
  104. package/dist/chunks/studio-p4nrkvqr.js +5 -0
  105. package/dist/chunks/{studio-1fxkaaea.js → studio-pbxa5fpc.js} +4 -4
  106. package/dist/chunks/studio-pgmvdv7z.js +7 -0
  107. package/dist/chunks/studio-ph84avkf.js +5 -0
  108. package/dist/chunks/{studio-tq0rtfwj.js → studio-q0mvbaq6.js} +4 -4
  109. package/dist/chunks/studio-q0tccd43.js +5 -0
  110. package/dist/chunks/studio-q7rtx6zq.js +14 -0
  111. package/dist/chunks/{studio-a3g98wjr.js → studio-qgh13jtz.js} +4 -4
  112. package/dist/chunks/studio-qmn1xc24.js +5 -0
  113. package/dist/chunks/studio-qwdqwkj0.js +5 -0
  114. package/dist/chunks/studio-r5qy30vx.js +5 -0
  115. package/dist/chunks/studio-rj9eamf8.js +5 -0
  116. package/dist/chunks/studio-rptgsk96.js +5 -0
  117. package/dist/chunks/studio-rthyg42z.js +928 -0
  118. package/dist/chunks/studio-s19tp1xr.js +5 -0
  119. package/dist/chunks/studio-s32578pn.js +5 -0
  120. package/dist/chunks/studio-svb7yyve.js +5 -0
  121. package/dist/chunks/studio-syreb644.js +5 -0
  122. package/dist/chunks/studio-tf9jryb1.js +10 -0
  123. package/dist/chunks/studio-tfkznv39.js +5 -0
  124. package/dist/chunks/studio-tft6r105.js +5 -0
  125. package/dist/chunks/studio-txf2kysx.js +5 -0
  126. package/dist/chunks/studio-v8867ddv.js +12 -0
  127. package/dist/chunks/studio-vxq7fcvv.js +5 -0
  128. package/dist/chunks/studio-w4rz9hdk.js +5 -0
  129. package/dist/chunks/studio-wbcjbhy1.js +9 -0
  130. package/dist/chunks/studio-wc649wss.js +5 -0
  131. package/dist/chunks/studio-wys23mxz.js +5 -0
  132. package/dist/chunks/studio-x1x167jh.js +5 -0
  133. package/dist/chunks/studio-x9hsze61.js +5 -0
  134. package/dist/chunks/studio-y0n7yxea.js +9 -0
  135. package/dist/chunks/studio-y3ahhdpx.js +5 -0
  136. package/dist/chunks/studio-z97c48fk.js +5 -0
  137. package/dist/chunks/studio-zat62m0c.js +5 -0
  138. package/dist/chunks/studio-zcd2e7yz.js +5 -0
  139. package/dist/chunks/studio-zz9qe3rw.js +64 -0
  140. package/dist/chunks/toast-1gv0h53h.js +4 -0
  141. package/dist/chunks/{toast-host-ke5z4ng7.js → toast-host-1yg5ar8d.js} +3 -3
  142. package/dist/chunks/{tooltip-5v8ngwj2.js → tooltip-yypz28gq.js} +3 -3
  143. package/dist/chunks/{tree-mdq3v5cw.js → tree-9xa4hm7n.js} +2 -2
  144. package/dist/chunks/tsMode-epyfz1hv.js +14 -0
  145. package/dist/chunks/workspace-dz7nxfhv.js +4 -0
  146. package/dist/chunks/workspace-home-xsd7hkjw.js +4 -0
  147. package/dist/chunks/{ws-client-nze0k6mv.js → ws-client-bv552sfx.js} +3 -3
  148. package/dist/iframe-entry.js +20 -16
  149. package/dist/manifest.json +77 -63
  150. package/dist/studio.js +103 -104
  151. package/dist/workers/ts.worker.js +3 -3
  152. package/package.json +12 -11
  153. package/src/browse/component-examples.ts +272 -0
  154. package/src/browse/library-commands.ts +64 -104
  155. package/src/browse/library-layouts.ts +263 -49
  156. package/src/browse/library-model.ts +181 -133
  157. package/src/browse/library-pane.ts +738 -446
  158. package/src/browse/library-preview.ts +149 -19
  159. package/src/browse/library-source.ts +308 -41
  160. package/src/browse/preview-root.ts +78 -0
  161. package/src/browse/preview-style.ts +155 -0
  162. package/src/canvas/asset-refs.ts +16 -1
  163. package/src/canvas/canvas-live-render.ts +222 -30
  164. package/src/canvas/canvas-origin.ts +37 -12
  165. package/src/canvas/canvas-page-context.ts +112 -0
  166. package/src/canvas/canvas-render.ts +113 -107
  167. package/src/canvas/canvas-utils.ts +57 -12
  168. package/src/canvas/iframe-drop.ts +107 -0
  169. package/src/canvas/iframe-entry.ts +24 -12
  170. package/src/canvas/iframe-host.ts +134 -62
  171. package/src/canvas/iframe-keys.ts +11 -0
  172. package/src/canvas/iframe-patch.ts +8 -7
  173. package/src/canvas/iframe-protocol.ts +24 -5
  174. package/src/canvas/iframe-render.ts +48 -1
  175. package/src/canvas/iframe-subtree.ts +5 -2
  176. package/src/canvas/npm-modules.ts +22 -0
  177. package/src/canvas/preview-navigate.ts +16 -0
  178. package/src/canvas/repeater-sources.ts +200 -0
  179. package/src/canvas/stage-scrollbar.ts +5 -1
  180. package/src/canvas/surface-registry.ts +6 -0
  181. package/src/commands/app-commands.ts +16 -6
  182. package/src/commands/budget.ts +10 -8
  183. package/src/commands/context.ts +35 -10
  184. package/src/commands/defaults.ts +69 -45
  185. package/src/commands/keymap.ts +9 -0
  186. package/src/commands/levels.ts +11 -5
  187. package/src/commands/live-context.ts +2 -0
  188. package/src/commands/registry.ts +10 -1
  189. package/src/content/collection-match.ts +6 -5
  190. package/src/content/collection-pages.ts +412 -0
  191. package/src/content/content-collections.ts +270 -0
  192. package/src/content/entry-commands.ts +51 -60
  193. package/src/content/entry-editor.ts +111 -18
  194. package/src/content/entry-fields.ts +13 -1
  195. package/src/content/entry-inspector.ts +225 -0
  196. package/src/content/entry-model.ts +17 -3
  197. package/src/content/entry-open.ts +80 -0
  198. package/src/content/entry-summaries.ts +440 -0
  199. package/src/content/file-record-ids.ts +80 -0
  200. package/src/content/file-records.ts +125 -0
  201. package/src/content/page-summaries.ts +129 -0
  202. package/src/content/record-entry.ts +201 -0
  203. package/src/content/writing-banner.ts +177 -0
  204. package/src/create/add-new-commands.ts +185 -0
  205. package/src/create/add-new-flow.ts +1116 -0
  206. package/src/create/add-new.ts +1014 -0
  207. package/src/create/element-name.ts +121 -0
  208. package/src/editor/convert-to-component.ts +67 -43
  209. package/src/editor/file-drop-action.ts +9 -1
  210. package/src/editor/inline-edit-apply.ts +44 -0
  211. package/src/editor/inline-edit.ts +10 -0
  212. package/src/editor/insert-component.ts +91 -0
  213. package/src/editor/insert-zone-action.ts +13 -4
  214. package/src/editor/shortcuts.ts +44 -22
  215. package/src/editor/slash-menu.ts +51 -8
  216. package/src/files/components.ts +74 -10
  217. package/src/files/file-ops.ts +15 -5
  218. package/src/files/files.ts +164 -150
  219. package/src/files/fs-events.ts +18 -7
  220. package/src/files/media-meta.ts +95 -1
  221. package/src/files/media-types.ts +55 -0
  222. package/src/files/media-upload.ts +209 -52
  223. package/src/files/new-file-name.ts +156 -0
  224. package/src/files/serialize-document.ts +13 -3
  225. package/src/format/format-host.ts +15 -0
  226. package/src/github/github-auth.ts +2 -2
  227. package/src/github/github-publish.ts +4 -3
  228. package/src/grid/grid-controller.ts +5 -1
  229. package/src/grid/grid-open.ts +0 -27
  230. package/src/grid/grid-panel.ts +54 -2
  231. package/src/grid/grid-reveal.ts +34 -0
  232. package/src/grid/grid-source.ts +3 -25
  233. package/src/grid/grid-view.ts +31 -6
  234. package/src/grid/sources/content-source.ts +75 -7
  235. package/src/grid/tabulator-tables.d.ts +6 -0
  236. package/src/hosting/document.ts +2 -2
  237. package/src/i18n/i18n-commands.ts +2 -1
  238. package/src/media/media-commands.ts +181 -0
  239. package/src/media/media-library-model.ts +232 -0
  240. package/src/media/media-library.ts +1283 -0
  241. package/src/media/media-pane.ts +47 -19
  242. package/src/packages/pull-package-sync.ts +1 -1
  243. package/src/panels/activity-panel.ts +21 -5
  244. package/src/panels/ai-chat/chat-markdown.ts +8 -8
  245. package/src/panels/block-action-bar.ts +11 -11
  246. package/src/panels/canvas-dnd-bridge.ts +5 -2
  247. package/src/panels/chat-panel.ts +6 -6
  248. package/src/panels/collections-panel.ts +306 -0
  249. package/src/panels/component-preview.ts +35 -13
  250. package/src/panels/data-explorer.ts +1 -1
  251. package/src/panels/data-grid.ts +4 -0
  252. package/src/panels/dnd.ts +17 -30
  253. package/src/panels/elements-panel.ts +9 -2
  254. package/src/panels/empty-state.ts +2 -2
  255. package/src/panels/formula-workspace.ts +2 -3
  256. package/src/panels/frontmatter-fields.ts +9 -9
  257. package/src/panels/git-diff-open.ts +9 -5
  258. package/src/panels/head-panel.ts +369 -103
  259. package/src/panels/i18n-panel.ts +11 -10
  260. package/src/panels/imports-panel.ts +2 -0
  261. package/src/panels/jump-bar.ts +27 -2
  262. package/src/panels/layers-panel.ts +36 -30
  263. package/src/panels/left-panel.ts +22 -8
  264. package/src/panels/navigator-panels.ts +23 -11
  265. package/src/panels/pane-context.ts +111 -29
  266. package/src/panels/pane-grid.ts +6 -6
  267. package/src/panels/panel-registry.ts +14 -9
  268. package/src/panels/properties-panel.ts +7 -3
  269. package/src/panels/right-panel.ts +199 -43
  270. package/src/panels/seo-modal.ts +41 -108
  271. package/src/panels/settings-menu.ts +52 -50
  272. package/src/panels/settings-pane.ts +268 -162
  273. package/src/panels/signals-panel.ts +3 -4
  274. package/src/panels/site-model.ts +367 -0
  275. package/src/panels/site-panel.ts +638 -0
  276. package/src/panels/style-panel.ts +37 -3
  277. package/src/panels/stylebook-doc.ts +142 -10
  278. package/src/panels/stylebook-layers-panel.ts +39 -80
  279. package/src/panels/stylebook-panel.ts +47 -10
  280. package/src/panels/styles-panel.ts +327 -0
  281. package/src/panels/tab-strip.ts +109 -25
  282. package/src/platform.ts +1 -1
  283. package/src/platforms/cloud.ts +28 -1
  284. package/src/platforms/default-platform.ts +4 -4
  285. package/src/platforms/devserver.ts +51 -25
  286. package/src/publish/deploy-checklist.ts +12 -7
  287. package/src/publish/pages-service.ts +38 -14
  288. package/src/publish/publish-changes.ts +491 -0
  289. package/src/publish/publish-commands.ts +83 -18
  290. package/src/publish/publish-ids.ts +49 -0
  291. package/src/publish/publish-quick.ts +148 -0
  292. package/src/publish/publish-workspace.ts +926 -0
  293. package/src/publish/source-control.ts +907 -0
  294. package/src/services/automation.ts +16 -20
  295. package/src/services/cf-settings.ts +4 -4
  296. package/src/services/connection.ts +2 -2
  297. package/src/services/directory-picker.ts +2 -2
  298. package/src/services/references.ts +0 -0
  299. package/src/services/render-critic.ts +21 -2
  300. package/src/services/settings/definitions.ts +25 -14
  301. package/src/settings/contributed-section.ts +49 -10
  302. package/src/settings/css-vars-editor.ts +93 -27
  303. package/src/settings/developer-panels.ts +43 -0
  304. package/src/settings/extension-sections.ts +20 -2
  305. package/src/settings/general-settings.ts +91 -21
  306. package/src/settings/preferences-dialog.ts +8 -1
  307. package/src/settings/project-sections.ts +27 -10
  308. package/src/settings/schema-field-ui.ts +3 -1
  309. package/src/settings/section-registry.ts +116 -11
  310. package/src/settings/settings-dialog.ts +192 -0
  311. package/src/settings/settings-document.ts +182 -162
  312. package/src/shell.ts +467 -431
  313. package/src/site-context.ts +64 -10
  314. package/src/state.ts +28 -9
  315. package/src/studio.ts +194 -41
  316. package/src/style/project-styles.ts +34 -0
  317. package/src/style/stylebook-specimens.ts +89 -0
  318. package/src/style/stylebook-target.ts +26 -0
  319. package/src/style/tag-inheritance.ts +105 -0
  320. package/src/style/variables-focus.ts +61 -0
  321. package/src/style/variables-view.ts +235 -0
  322. package/src/surfaces/add-new.json +1482 -0
  323. package/src/surfaces/add-new.ts +325 -0
  324. package/src/surfaces/ai-chat.ts +2 -2
  325. package/src/surfaces/boot-failure.json +4 -3
  326. package/src/surfaces/bottom-dock.json +1 -1
  327. package/src/surfaces/canvas-stage.json +30 -19
  328. package/src/surfaces/canvas-stage.ts +5 -5
  329. package/src/surfaces/collections-panel.json +615 -0
  330. package/src/surfaces/collections-panel.ts +168 -0
  331. package/src/surfaces/commandbar.json +241 -96
  332. package/src/surfaces/commandbar.ts +134 -99
  333. package/src/surfaces/empty-state.json +1 -1
  334. package/src/surfaces/entry-editor.json +136 -0
  335. package/src/surfaces/entry-editor.ts +21 -0
  336. package/src/surfaces/entry-inspector.json +371 -0
  337. package/src/surfaces/entry-inspector.ts +143 -0
  338. package/src/surfaces/inspector-dock.json +162 -14
  339. package/src/surfaces/inspector-dock.ts +46 -19
  340. package/src/surfaces/library-pane.json +670 -437
  341. package/src/surfaces/library-pane.ts +94 -59
  342. package/src/surfaces/media-browser.json +62 -6
  343. package/src/surfaces/media-browser.ts +15 -1
  344. package/src/surfaces/media-field.json +1 -1
  345. package/src/surfaces/media-library.json +1586 -0
  346. package/src/surfaces/media-library.ts +282 -0
  347. package/src/surfaces/media-pick-dialog.json +71 -0
  348. package/src/surfaces/media-pick-dialog.ts +132 -0
  349. package/src/surfaces/menu.ts +18 -0
  350. package/src/surfaces/pane-context.json +144 -77
  351. package/src/surfaces/pane-context.ts +24 -0
  352. package/src/surfaces/panel-deploy-checklist.json +12 -6
  353. package/src/surfaces/panel-deploy-checklist.ts +3 -1
  354. package/src/surfaces/panel-page.json +72 -2
  355. package/src/surfaces/panel-page.ts +38 -7
  356. package/src/surfaces/panel-signals.json +1 -1
  357. package/src/surfaces/panel-signals.ts +5 -5
  358. package/src/surfaces/panel-stylebook-layers.json +71 -204
  359. package/src/surfaces/panel-stylebook-layers.ts +0 -21
  360. package/src/surfaces/preferences.json +120 -48
  361. package/src/surfaces/preferences.ts +8 -0
  362. package/src/surfaces/publish-quick.json +428 -0
  363. package/src/surfaces/publish-quick.ts +165 -0
  364. package/src/surfaces/publish-workspace.json +2892 -0
  365. package/src/surfaces/publish-workspace.ts +391 -0
  366. package/src/surfaces/rail.json +1 -159
  367. package/src/surfaces/rail.ts +18 -75
  368. package/src/surfaces/schema-form.json +10 -2
  369. package/src/surfaces/schema-form.ts +2 -0
  370. package/src/surfaces/seo.json +1 -1
  371. package/src/surfaces/settings-contributed.json +5 -5
  372. package/src/surfaces/settings-contributed.ts +18 -2
  373. package/src/surfaces/settings-css-vars.json +44 -36
  374. package/src/surfaces/settings-css-vars.ts +77 -4
  375. package/src/surfaces/settings-deploy.json +2 -15
  376. package/src/surfaces/settings-deploy.ts +1 -1
  377. package/src/surfaces/settings-dialog.json +51 -0
  378. package/src/surfaces/settings-dialog.ts +169 -0
  379. package/src/surfaces/settings-overview.json +3 -3
  380. package/src/surfaces/settings-overview.ts +9 -2
  381. package/src/surfaces/settings-pane.json +269 -46
  382. package/src/surfaces/settings-pane.ts +85 -24
  383. package/src/surfaces/shell.json +2 -1
  384. package/src/surfaces/site-panel.json +1029 -0
  385. package/src/surfaces/site-panel.ts +233 -0
  386. package/src/surfaces/slash-menu.json +63 -21
  387. package/src/surfaces/slash-menu.ts +23 -0
  388. package/src/surfaces/statusbar.ts +39 -15
  389. package/src/surfaces/styles-panel.json +735 -0
  390. package/src/surfaces/styles-panel.ts +195 -0
  391. package/src/surfaces/styles-variables.json +149 -0
  392. package/src/surfaces/styles-variables.ts +145 -0
  393. package/src/surfaces/tab-strip.json +4 -4
  394. package/src/surfaces/welcome.ts +1 -1
  395. package/src/surfaces/writing-banner.json +94 -0
  396. package/src/surfaces/writing-banner.ts +80 -0
  397. package/src/tabs/bare-repeaters.ts +101 -0
  398. package/src/tabs/tab.ts +47 -7
  399. package/src/types.ts +21 -26
  400. package/src/ui/form-controls.ts +47 -8
  401. package/src/ui/layers.ts +13 -0
  402. package/src/ui/media-picker.ts +284 -40
  403. package/src/ui/regions.ts +7 -6
  404. package/src/ui/schema-form.ts +259 -27
  405. package/src/ui/virtual-window.ts +26 -1
  406. package/src/utils/base64.ts +3 -3
  407. package/src/utils/edit-display.ts +322 -67
  408. package/src/utils/local-day.ts +18 -0
  409. package/src/workspace/pane-derive.ts +4 -4
  410. package/src/workspace/session.ts +28 -7
  411. package/src/workspace/tab-workspace.ts +137 -0
  412. package/src/workspace/workspace-back.ts +189 -0
  413. package/src/workspace/workspace-home.ts +133 -0
  414. package/src/workspace/workspace-ids.ts +129 -0
  415. package/src/workspace/workspace-switch.ts +372 -0
  416. package/src/workspace/workspace.ts +125 -11
  417. package/styles/shell-frame.css +30 -7
  418. package/styles/shell-frame.json +15 -3
  419. package/dist/chunks/2020-f2gkpy8d.js +0 -4
  420. package/dist/chunks/adapter-4912g339.js +0 -4
  421. package/dist/chunks/cf-account-picker-wccaenh6.js +0 -4
  422. package/dist/chunks/cf-settings-br6105tr.js +0 -4
  423. package/dist/chunks/color-28ksk2qm.js +0 -4
  424. package/dist/chunks/components-db68vsn7.js +0 -4
  425. package/dist/chunks/content-source-1449qesq.js +0 -4
  426. package/dist/chunks/convert-to-repeater-5h457egj.js +0 -4
  427. package/dist/chunks/editor-x9jn86a7.js +0 -4
  428. package/dist/chunks/entry-commands-may24cpt.js +0 -4
  429. package/dist/chunks/extension-sections-pvr14vxr.js +0 -4
  430. package/dist/chunks/file-ops-x9v54pcj.js +0 -4
  431. package/dist/chunks/files-j3knqf0s.js +0 -4
  432. package/dist/chunks/format-host-tyre2trb.js +0 -4
  433. package/dist/chunks/fs-events-64ma9htc.js +0 -4
  434. package/dist/chunks/git-panel-84ps5dex.js +0 -4
  435. package/dist/chunks/iframe-entry-fvbk89gm.js +0 -5
  436. package/dist/chunks/iframe-host-ck6vejt5.js +0 -4
  437. package/dist/chunks/index-b8xw3vy9.js +0 -4
  438. package/dist/chunks/jsonMode-w15cmrcm.js +0 -13
  439. package/dist/chunks/jx-validate-at3zn8wa.js +0 -4
  440. package/dist/chunks/layers-h4xp1pss.js +0 -4
  441. package/dist/chunks/layers-panel-m5hw1d7g.js +0 -4
  442. package/dist/chunks/lit-html-8t8t3dnr.js +0 -4
  443. package/dist/chunks/media-picker-2cweexc5.js +0 -4
  444. package/dist/chunks/menu-ynw13y0s.js +0 -4
  445. package/dist/chunks/monaco-buffer-qzxn567r.js +0 -4
  446. package/dist/chunks/monaco-setup-gxf6656g.js +0 -4
  447. package/dist/chunks/notify-cj5zbrs2.js +0 -4
  448. package/dist/chunks/parse-b7ddhr8m.js +0 -4
  449. package/dist/chunks/platform-3dfws9js.js +0 -4
  450. package/dist/chunks/popover-jt2gxmzs.js +0 -4
  451. package/dist/chunks/publish-panel-1gvxbj21.js +0 -4
  452. package/dist/chunks/quick-search-0dajrwqg.js +0 -4
  453. package/dist/chunks/references-49qgt404.js +0 -4
  454. package/dist/chunks/right-panel-jm1bwe53.js +0 -4
  455. package/dist/chunks/section-registry-6kwc4fky.js +0 -4
  456. package/dist/chunks/studio-0rt52vpq.js +0 -5
  457. package/dist/chunks/studio-18vgbhs5.js +0 -5
  458. package/dist/chunks/studio-24mccdrj.js +0 -5
  459. package/dist/chunks/studio-2eh507zk.js +0 -9
  460. package/dist/chunks/studio-2hcbccfv.js +0 -5
  461. package/dist/chunks/studio-2jbs3rxv.js +0 -5
  462. package/dist/chunks/studio-4h8k4dw9.js +0 -5
  463. package/dist/chunks/studio-5f6n18kx.js +0 -5
  464. package/dist/chunks/studio-5gthpky6.js +0 -5
  465. package/dist/chunks/studio-6ep3js6b.js +0 -9
  466. package/dist/chunks/studio-6wkmf3wz.js +0 -9
  467. package/dist/chunks/studio-841rf3cg.js +0 -5
  468. package/dist/chunks/studio-8gmwgheg.js +0 -5
  469. package/dist/chunks/studio-8qm0ep50.js +0 -5
  470. package/dist/chunks/studio-924p1tm1.js +0 -6
  471. package/dist/chunks/studio-a3shpz9y.js +0 -5
  472. package/dist/chunks/studio-amsbaaar.js +0 -6
  473. package/dist/chunks/studio-at5spjbc.js +0 -7
  474. package/dist/chunks/studio-b2xv945e.js +0 -5
  475. package/dist/chunks/studio-bn5nkafw.js +0 -5
  476. package/dist/chunks/studio-bw6q5xjf.js +0 -9
  477. package/dist/chunks/studio-c7q0t9x1.js +0 -5
  478. package/dist/chunks/studio-camnhc96.js +0 -6
  479. package/dist/chunks/studio-cffqvnzd.js +0 -5
  480. package/dist/chunks/studio-efvfrsf8.js +0 -5
  481. package/dist/chunks/studio-er30bpcx.js +0 -5
  482. package/dist/chunks/studio-ezscgr64.js +0 -5
  483. package/dist/chunks/studio-ffrrncvz.js +0 -5
  484. package/dist/chunks/studio-fpphgn8y.js +0 -10
  485. package/dist/chunks/studio-j1kvhdjk.js +0 -4
  486. package/dist/chunks/studio-j430n16g.js +0 -7
  487. package/dist/chunks/studio-jjkg84nj.js +0 -5
  488. package/dist/chunks/studio-m0q32ars.js +0 -4
  489. package/dist/chunks/studio-m6bdst11.js +0 -5
  490. package/dist/chunks/studio-nv2cptd4.js +0 -64
  491. package/dist/chunks/studio-nwfrc98n.js +0 -5
  492. package/dist/chunks/studio-p1wgvp7r.js +0 -5
  493. package/dist/chunks/studio-pag2wncm.js +0 -5
  494. package/dist/chunks/studio-qvanvzfd.js +0 -5
  495. package/dist/chunks/studio-qwa64kt7.js +0 -5
  496. package/dist/chunks/studio-rk1zc4x3.js +0 -928
  497. package/dist/chunks/studio-se2r3ptc.js +0 -5
  498. package/dist/chunks/studio-sg2a0dh0.js +0 -5
  499. package/dist/chunks/studio-tjzjhe68.js +0 -5
  500. package/dist/chunks/studio-v2w7q8r1.js +0 -7
  501. package/dist/chunks/studio-v5vjq1h0.js +0 -4
  502. package/dist/chunks/studio-v8aga3vp.js +0 -14
  503. package/dist/chunks/studio-vfcdrfmv.js +0 -5
  504. package/dist/chunks/studio-vmfkgdjf.js +0 -5
  505. package/dist/chunks/studio-wt1816jj.js +0 -5
  506. package/dist/chunks/studio-wz73q5kq.js +0 -4
  507. package/dist/chunks/studio-y04qtxvp.js +0 -7
  508. package/dist/chunks/studio-y21g8zzg.js +0 -5
  509. package/dist/chunks/studio-y64hrt8p.js +0 -12
  510. package/dist/chunks/studio-ynn90f58.js +0 -16
  511. package/dist/chunks/studio-yxz682sx.js +0 -5
  512. package/dist/chunks/studio-zp7snvkq.js +0 -5
  513. package/dist/chunks/studio-zq4z6tm6.js +0 -5
  514. package/dist/chunks/toast-c9fn075p.js +0 -4
  515. package/dist/chunks/tsMode-sf49ee9x.js +0 -14
  516. package/dist/chunks/workspace-jtfahpee.js +0 -4
  517. package/src/panels/frontmatter-panel.ts +0 -711
  518. package/src/panels/git-panel.ts +0 -1108
  519. package/src/surfaces/doc-header.json +0 -776
  520. package/src/surfaces/doc-header.ts +0 -222
  521. package/src/surfaces/git-panel.json +0 -1880
  522. package/src/surfaces/git-panel.ts +0 -264
package/src/shell.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  * per-render cleanup arrays. It stays a plain object, because wrapping a live editor instance and
9
9
  * detached DOM nodes in a reactive proxy is an active hazard.
10
10
  * - `shell` (here) holds UI **inputs** — which panel each dock shows, whether each dock is open and
11
- * how wide, the focused region, the layout preset, and the project-level state (source control,
12
- * the stylebook selection, the settings tab) that a _document_ must not own.
11
+ * how wide, the focused region, the workspace, and the project-level state (source control, the
12
+ * stylebook selection, the settings tab) that a _document_ must not own.
13
13
  *
14
14
  * Because this record is reactive, a surface tracks it by reading it: the rail, the left panel and
15
15
  * the toolbar all repaint from the effects they already run. The stand-in that preceded this —
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import { isBoolean, isJsonObject, isNumber, isString } from "@jxsuite/schema/guards";
25
- import { effect, effectScope, reactive } from "./reactivity";
25
+ import { effect, effectScope, pauseTracking, reactive, resetTracking } from "./reactivity";
26
26
  import { SETTINGS } from "./services/settings/definitions";
27
27
  import { readStoredSetting, setSetting } from "./services/settings/kernel";
28
28
  import { loadedMonaco } from "./services/monaco-lazy";
@@ -36,14 +36,22 @@ import {
36
36
  booleanProperty,
37
37
  enumArg,
38
38
  enumProperty,
39
- stringArg,
40
- stringProperty,
41
39
  } from "./commands/command-args";
42
40
  import type { EffectScope } from "@vue/reactivity";
43
41
  import type { JsonObject, JsonValue } from "@jxsuite/schema/types";
44
42
  import type { LayoutHit } from "./canvas/iframe-protocol";
45
43
  import type { GitBranchesResult, GitDiffState, GitLogEntry, GitStatusResult } from "./types";
46
44
  import type { AnyCommand, CommandRegistry } from "./commands/registry";
45
+ import {
46
+ DEFAULT_WORKSPACE,
47
+ isWorkspaceId,
48
+ WORKSPACE_IDS,
49
+ WORKSPACES,
50
+ } from "./workspace/workspace-ids";
51
+ import type { WorkspaceId } from "./workspace/workspace-ids";
52
+ import { DEFAULT_PUBLISH_DESTINATION, DEFAULT_PUBLISH_TAB } from "./publish/publish-ids";
53
+ import type { PublishDestination, PublishTab } from "./publish/publish-ids";
54
+ import type { PagesDeploymentInfo } from "./publish/pages-service";
47
55
 
48
56
  /**
49
57
  * The three resizable docks around the pane grid: two flanking it, one under it.
@@ -54,7 +62,7 @@ import type { AnyCommand, CommandRegistry } from "./commands/registry";
54
62
  * {@link INSPECTOR_TAB_IDS}, the same mechanism that selects Content, Style and Logic.
55
63
  *
56
64
  * `"bottom"` is the one the Bottom dock (§16.3) added. It is a dock like the other two — a collapse
57
- * flag, a remembered size, a resize handle, a place in every {@link LayoutPreset} — and the reason
65
+ * flag, a remembered size, a resize handle, a place in every workspace's frame — and the reason
58
66
  * `view.toggleBottomDock` had no idempotent setter for two phases is that it was not on this
59
67
  * record. It sits under the PANE GRID rather than under the window (§16.3), so opening it never
60
68
  * narrows the Navigator or the Inspector.
@@ -88,12 +96,13 @@ export type DockId = "left" | "right" | "bottom";
88
96
  * `tests/panel-registry.test.ts` asserts the two agree, so the duplicate cannot drift.
89
97
  */
90
98
  export const NAVIGATOR_PANEL_IDS = [
99
+ "site",
100
+ "collections",
101
+ "styles",
91
102
  "files",
92
103
  "search",
93
- "git",
94
104
  "i18n",
95
105
  "layers",
96
- "page",
97
106
  "data",
98
107
  "packages",
99
108
  "insert",
@@ -101,6 +110,32 @@ export const NAVIGATOR_PANEL_IDS = [
101
110
 
102
111
  export type NavigatorPanelId = (typeof NAVIGATOR_PANEL_IDS)[number];
103
112
 
113
+ /**
114
+ * The Navigator panels that exist only as one workspace's preset panel: no rail button, and no
115
+ * frame but that workspace's draws them. Collections is the Content workspace's Navigator (§22.5),
116
+ * and Styles the Styles workspace's (§7.1).
117
+ *
118
+ * The dock record is the WINDOW's, so `leftTab` is persisted whatever workspace wrote it; a window
119
+ * closed in Content stores `collections`. Every workspace's own frame replaces it on entry, but a
120
+ * window with no project open applies no workspace frame, and drew "Collections · project" over a
121
+ * rail that has no Collections button. {@link navigatorPanelOutsideWorkspace} is the guard both of
122
+ * those moments go through. `tests/panel-registry.test.ts` asserts this set is exactly the
123
+ * rail-less panels a preset pins, so it cannot drift from the records.
124
+ */
125
+ export const WORKSPACE_ONLY_PANELS: ReadonlySet<NavigatorPanelId> = new Set([
126
+ "collections",
127
+ "styles",
128
+ ]);
129
+
130
+ /**
131
+ * The Navigator panel to show when no workspace frame chooses one (boot, and no project open): the
132
+ * stored panel, unless it is {@link WORKSPACE_ONLY_PANELS workspace-only}, in which case the
133
+ * default workspace's own first panel.
134
+ */
135
+ export function navigatorPanelOutsideWorkspace(stored: NavigatorPanelId | null): NavigatorPanelId {
136
+ return stored && !WORKSPACE_ONLY_PANELS.has(stored) ? stored : DEFAULT_PANEL_ID;
137
+ }
138
+
104
139
  /**
105
140
  * Panel ids a previous build persisted, and what they are called now.
106
141
  *
@@ -111,8 +146,16 @@ export type NavigatorPanelId = (typeof NAVIGATOR_PANEL_IDS)[number];
111
146
  */
112
147
  const RENAMED_PANEL_IDS = new Map<string, NavigatorPanelId>([
113
148
  ["blocks", "insert"],
114
- ["head", "page"],
149
+ /* Not a rename either — a MOVE. The Page panel is the Inspector's first tab now (§6), so the
150
+ Navigator has no panel of that name; a stored `page` (or the `head` it was before that) reopens
151
+ the Navigator on the Site panel, the Design workspace's own default, rather than on nothing. */
152
+ ["head", "site"],
153
+ ["page", "site"],
115
154
  ["imports", "packages"],
155
+ /* A move out of the Navigator altogether: Source Control is the Publish workspace's Review
156
+ changes and Branches now (§5.5, §22.8), so the Navigator has no panel of that name. A stored
157
+ `git` reopens on the Site panel, the Design workspace's own default, rather than on nothing. */
158
+ ["git", "site"],
116
159
  /* Not a rename — a MERGE. The State editor is part of the Data panel now (§5.6), so a
117
160
  stored `leftTab: "state"` names a real place and lands on it, rather than returning `null` and
118
161
  booting into the default the way `problems` does. Defining a value and watching it resolve are
@@ -120,21 +163,19 @@ const RENAMED_PANEL_IDS = new Map<string, NavigatorPanelId>([
120
163
  ["state", "data"],
121
164
  ]);
122
165
 
123
- /** The panel the Navigator wakes up on when nothing usable is stored. */
124
- export const DEFAULT_PANEL_ID: NavigatorPanelId = "layers";
125
-
126
- /** Whether a string is a declared panel id. */
127
- export function isNavigatorPanelId(value: unknown): value is NavigatorPanelId {
128
- return typeof value === "string" && NAVIGATOR_PANEL_IDS.some((id) => id === value);
129
- }
166
+ /**
167
+ * The panel the Navigator wakes up on when nothing usable is stored: the Site panel, the project by
168
+ * route (§5.7), because a fresh project is opened to find a page rather than to edit one.
169
+ */
170
+ export const DEFAULT_PANEL_ID: NavigatorPanelId = "site";
130
171
 
131
172
  /**
132
173
  * Narrow an id arriving from a `string`-typed roster, or throw naming where it came from.
133
174
  *
134
- * The lock on the door {@link NAVIGATOR_PANEL_IDS} describes, in one place so the rail and the ⌘1–8
135
- * dispatcher cannot disagree about what an undeclared id means. Both receive their id from
136
- * `PanelRecord.id`, which is deliberately a `string` — the registry hosts the Bottom dock's panels
137
- * too, and their ids are {@link BOTTOM_TAB_IDS}.
175
+ * The lock on the door {@link NAVIGATOR_PANEL_IDS} describes, in one place so the rail and the
176
+ * `panel.focus.*` dispatcher cannot disagree about what an undeclared id means. Both receive their
177
+ * id from `PanelRecord.id`, which is deliberately a `string` — the registry hosts the Bottom dock's
178
+ * panels too, and their ids are {@link BOTTOM_TAB_IDS}.
138
179
  *
139
180
  * It throws rather than returning null because `tests/panel-registry.test.ts` asserts the enum and
140
181
  * the registry agree, so a failure here means the two have drifted — and the symptom of the last
@@ -174,24 +215,40 @@ export function migratePanelId(stored: JsonValue | undefined): NavigatorPanelId
174
215
  }
175
216
 
176
217
  /**
177
- * The Inspector's tab ids, in strip order — the `value`s `panels/right-panel.ts` renders.
178
- *
179
- * Four now, and `"assistant"` is one of them: the chat is a tab of this dock, so the id space that
180
- * addresses Content, Style and Logic addresses it too. The old arrangement needed a fourth verb
181
- * (`view.setAssistant`) precisely because the assistant was somewhere else; it survives as the
182
- * dock-toggle spelling of "show me the assistant", but it now writes the same field as the others.
183
- *
184
- * The ORDER is §6's — Content · Style · Logic · Assistant — which is the order ⌘⇧1–4 follow. The
185
- * human titles live in `commands/defaults.ts`'s `INSPECTOR_TABS` (a module with no state and no
186
- * DOM, so the CI checks can load it); `tests/right-panel.test.ts` asserts the two agree, the same
187
- * way `tests/panel-registry.test.ts` does for the rail.
218
+ * The Inspector's LEAF ids, in strip order — every view the dock can show, and the values
219
+ * `view.setRightTab` accepts (§6).
220
+ *
221
+ * The strip draws three tabs, **Page · Element · Assistant**, and five leaves sit under them:
222
+ * `page`, then Element's three views (`properties` = Content, `style` = Style, `events` = Logic)
223
+ * behind a sub-switch, then `assistant`. The leaves stay addressable one by one because they are
224
+ * what scripts, chords and shots name — `view.setRightTab { tab: "style" }` shows Element with
225
+ * Style chosen — and a strip of three that addressed only itself would make Style unreachable by
226
+ * name. `commands/defaults.ts`'s `INSPECTOR_TABS` carries the titles and the strip grouping (a
227
+ * module with no state and no DOM, so the CI checks can load it); `tests/right-panel.test.ts`
228
+ * asserts the two agree, the same way `tests/panel-registry.test.ts` does for the rail.
188
229
  */
189
- export const INSPECTOR_TAB_IDS = ["properties", "style", "events", "assistant"] as const;
230
+ export const INSPECTOR_TAB_IDS = ["page", "properties", "style", "events", "assistant"] as const;
190
231
 
191
232
  export type InspectorTabId = (typeof INSPECTOR_TAB_IDS)[number];
192
233
 
193
- /** The tab the Inspector falls back to — the leftmost, Content. */
194
- export const DEFAULT_INSPECTOR_TAB: InspectorTabId = "properties";
234
+ /**
235
+ * The leaf the Inspector falls back to — Page, the strip's first tab, which is what the dock shows
236
+ * when nothing is selected (§6: an undeclared stored id coerces to Page).
237
+ */
238
+ export const DEFAULT_INSPECTOR_TAB: InspectorTabId = "page";
239
+
240
+ /** The Element tab's three leaves, in sub-switch order. */
241
+ export const ELEMENT_TAB_IDS = ["properties", "style", "events"] as const;
242
+
243
+ export type ElementTabId = (typeof ELEMENT_TAB_IDS)[number];
244
+
245
+ /** The Element leaf the strip lands on when the selection first arrives and none was used yet. */
246
+ export const DEFAULT_ELEMENT_TAB: ElementTabId = "properties";
247
+
248
+ /** Whether a leaf is one of the Element tab's three views. */
249
+ export function isElementTabId(value: unknown): value is ElementTabId {
250
+ return typeof value === "string" && ELEMENT_TAB_IDS.some((id) => id === value);
251
+ }
195
252
 
196
253
  /** Whether a string is a declared Inspector tab id. */
197
254
  export function isInspectorTabId(value: unknown): value is InspectorTabId {
@@ -268,57 +325,149 @@ export interface ShellGit {
268
325
  */
269
326
  rev: number;
270
327
  logEntries: GitLogEntry[] | null;
271
- /** "changes" | "history" — the panel's sub-tab. */
272
- subTab: string;
273
328
  /** Epoch ms of the last successful refresh, or null when never refreshed. */
274
329
  lastUpdated: number | null;
275
330
  }
276
331
 
332
+ /**
333
+ * What the Publish workspace is showing (studio.md §22.8): which sub-tab, and which destination its
334
+ * Review changes and the quick publish dialog offer. Project level, like the repository both
335
+ * describe, and reset with it, so the next project opens on Review changes rather than on the last
336
+ * project's Branches.
337
+ */
338
+ export interface ShellPublish {
339
+ tab: PublishTab;
340
+ destination: PublishDestination;
341
+ /** What the host said about the connected project's deployments, and for which Pages project. */
342
+ deploys: {
343
+ state: "idle" | "loading" | "loaded" | "failed";
344
+ rows: PagesDeploymentInfo[];
345
+ error: string;
346
+ project: string;
347
+ };
348
+ /** The last build check, and the change set it was run against (`publish/publish-changes.ts`). */
349
+ build: { state: "idle" | "running" | "passed" | "failed"; text: string; for: string };
350
+ /** The pull request Send for review last opened; `number` 0 for none. */
351
+ review: { number: number; url: string };
352
+ }
353
+
277
354
  /** The stylebook catalogue's selection and filters — project level, like the styles they edit. */
278
355
  export interface ShellStylebook {
279
356
  selection: string | null;
280
- /** "elements" | "variables" */
281
- tab: string;
357
+ /**
358
+ * The custom property the Variables view is pointed at (`--accent`), chosen from the Outline's
359
+ * variable rows, or `null`. Which LIST the Outline draws is not stored here: it follows the view
360
+ * the pane is in (Elements or Variables), so the two can never disagree.
361
+ */
362
+ variable: string | null;
363
+ /**
364
+ * The view the Styles workspace's home is in: Variables (`tokens`, the first visit's view, which
365
+ * is the most common reason to come here) or Elements (`stylebook`). Written whenever
366
+ * `project.json` is shown in one of the two, so the next visit, and Back from Code, return to the
367
+ * view the author was last in. Code is never the home: it is a view opened beside it.
368
+ */
369
+ view: "stylebook" | "tokens";
282
370
  filter: string;
283
371
  customizedOnly: boolean;
284
372
  }
285
373
 
286
374
  /**
287
- * A named arrangement of the shell — a layout tab (`docs/studio/interface.md`, "Command Bar").
288
- *
289
- * A layout is a TUPLE, not a mode: which Navigator panel is showing, whether each dock is open and
290
- * how wide, and which Inspector tab is selected. Picking one reconfigures; it never removes. Every
291
- * panel stays reachable by rail, chord and palette afterwards, because workspaces that gate
292
- * features are refused (`docs/studio/interface.md`, "Command Bar") — telling a copy editor the
293
- * Style panel "is in ⌘K" hands the non-technical user the one affordance they are least likely to
294
- * reach for.
295
- *
296
- * The field this replaces (`shell.layout`, a string with a declared default and NO writer) was the
297
- * exact failure `layoutSelection` had already been through: a record nothing sets, read by chrome
298
- * that therefore cannot change. Wiring it was the alternative to deleting it, and these are the
299
- * writers.
300
- *
301
- * **Editor kind is deliberately not in the tuple.** The shell redesign's layout tuple named it, but
302
- * nothing in the shell can apply it: switching a pane's editor is `studio.ts`'s `setCanvasMode`,
303
- * which this module does not have and cannot be handed without changing a bootstrap it does not
304
- * own. A fourth field no writer could honour would recreate the defect this type exists to close,
305
- * so the tuple is three fields and the fourth arrives with the pane record that can carry it.
375
+ * What a workspace does to the frame when it is entered (`docs/studio/interface.md`, "Workspaces").
376
+ *
377
+ * A preset is applied on EVERY entry and is not a lock (`specs/studio.md` §22.3). Entering a
378
+ * workspace draws the rail only where the rail has something to offer (Design), and opens or
379
+ * collapses the Navigator and the Inspector for the work the workspace is for; the dock toggles
380
+ * keep working inside it, and the next entry applies the preset again. The one thing carried from
381
+ * visit to visit is which Navigator panel a workspace that names none was showing, so Design comes
382
+ * back on the panel it was left on rather than on Styles' Outline.
383
+ *
384
+ * It replaces the four named layouts (Write · Design · Build · Ship) that preceded it. A layout was
385
+ * a tuple of panels with no job attached; a workspace owns its tabs, so switching to one changes
386
+ * WHAT is on screen, not only where the docks are.
306
387
  */
307
- export interface LayoutPreset {
308
- /** Stable key. Built-ins use their own lowercase name; a saved layout gets a slug of its. */
309
- id: string;
310
- /** What the Command Bar tab reads. Renamed by double-clicking it. */
311
- name: string;
312
- /** Which Navigator panel this arrangement shows. */
313
- navigatorPanel: NavigatorPanelId;
314
- /** Which Inspector tab this arrangement selects. */
315
- inspectorTab: InspectorTabId;
316
- /** Which Bottom dock tab this arrangement selects — read whether or not the dock is open. */
317
- bottomTab: BottomTabId;
318
- /** Dock visibility and sizes, all three of them. */
319
- docks: Record<DockId, DockState>;
388
+ export interface WorkspacePreset {
389
+ id: WorkspaceId;
390
+ /** Whether the rail column is drawn at all. It has no toggle of its own, so this is a class. */
391
+ rail: boolean;
392
+ /**
393
+ * The Navigator on entry: open or collapsed, and on which panel. `panel` is applied on EVERY
394
+ * entry; `initial` is the panel a workspace that remembers its own opens on when it has nothing
395
+ * to remember yet — Design's Site panel (§22.3: "the Site panel, or the last Design panel").
396
+ */
397
+ navigator: { open: boolean; panel?: NavigatorPanelId; initial?: NavigatorPanelId };
398
+ /**
399
+ * The Inspector: open or collapsed on entry, and the tab it selects when one is named. The tab is
400
+ * per-document state, so naming it costs no other document anything.
401
+ *
402
+ * `whileWriting` draws it over a collapsed `open` while the active tab is a document written on
403
+ * the canvas ({@link WorkspaceDeps.writingOnCanvas}): Content's entries are edited on the canvas,
404
+ * where element selection and the Inspector apply, and its Library and forms are not; Styles'
405
+ * Elements view selects element defaults on its canvas, and its Variables and Code views do not.
406
+ */
407
+ inspector: { open: boolean; tab?: InspectorTabId; whileWriting?: boolean };
408
+ /**
409
+ * Whether each pane draws its document tab strip. Design and Content do: they are where several
410
+ * documents are open at once. Media, Styles and Publish draw their home surface alone, and what
411
+ * they open beside it (a media file, a comparison, `project.json` as Code) is left through the
412
+ * pane context bar's Back control (`workspace/workspace-back.ts`), so no strip is needed to get
413
+ * back. Projected as {@link TAB_STRIP_HIDDEN_CLASS} on `#app`.
414
+ */
415
+ tabStrip: boolean;
320
416
  }
321
417
 
418
+ /**
419
+ * The five presets.
420
+ *
421
+ * - **Design** draws the rail and both side docks: the canvas, its outline and its inspector.
422
+ * - **Content** opens the Navigator on the Collections panel and collapses the Inspector, drawing it
423
+ * while an entry is being written on the canvas.
424
+ * - **Media** and **Publish** collapse both side docks: each one's home is a surface that lays itself
425
+ * out (the media grid, the change list).
426
+ * - **Styles** opens the Navigator on the Styles panel (variables, element defaults and color
427
+ * schemes), and the Inspector on its Style tab only while the Elements view is on screen: that
428
+ * view selects an element default on a canvas, and the Inspector is where it is styled. Variables
429
+ * is edited in the view itself, and Code has no canvas, so both leave the pane its width.
430
+ *
431
+ * Only Design and Content draw a tab strip; the other three draw their home surface alone.
432
+ */
433
+ export const WORKSPACE_PRESETS: Readonly<Record<WorkspaceId, WorkspacePreset>> = {
434
+ content: {
435
+ id: "content",
436
+ inspector: { open: false, whileWriting: true },
437
+ navigator: { open: true, panel: "collections" },
438
+ rail: false,
439
+ tabStrip: true,
440
+ },
441
+ design: {
442
+ id: "design",
443
+ inspector: { open: true },
444
+ navigator: { initial: "site", open: true },
445
+ rail: true,
446
+ tabStrip: true,
447
+ },
448
+ media: {
449
+ id: "media",
450
+ inspector: { open: false },
451
+ navigator: { open: false },
452
+ rail: false,
453
+ tabStrip: false,
454
+ },
455
+ publish: {
456
+ id: "publish",
457
+ inspector: { open: false },
458
+ navigator: { open: false },
459
+ rail: false,
460
+ tabStrip: false,
461
+ },
462
+ styles: {
463
+ id: "styles",
464
+ inspector: { open: false, tab: "style", whileWriting: true },
465
+ navigator: { open: true, panel: "styles" },
466
+ rail: false,
467
+ tabStrip: false,
468
+ },
469
+ };
470
+
322
471
  export interface ShellState {
323
472
  /** Which panel the Navigator dock shows. One of {@link NAVIGATOR_PANEL_IDS}. */
324
473
  leftTab: string;
@@ -342,10 +491,11 @@ export interface ShellState {
342
491
  */
343
492
  paneSplit: number;
344
493
  focusRegion: FocusRegion;
345
- /** The active {@link LayoutPreset}'s id — the Command Bar tab drawn as selected. */
346
- layout: string;
347
- /** Every layout this project has, built-ins included. Persisted per project root. */
348
- layouts: LayoutPreset[];
494
+ /**
495
+ * The workspace on screen — the top bar's selected tab, and the filter every pane's tab strip
496
+ * applies. Written by {@link enterWorkspace} alone. Persisted per project root.
497
+ */
498
+ workspace: WorkspaceId;
349
499
  /**
350
500
  * Layout chrome the author clicked, or `null`: a header, a footer, anything contributed by the
351
501
  * layout file. It is deliberately NOT a document selection: the node is not in the open page at
@@ -353,10 +503,9 @@ export interface ShellState {
353
503
  * the element inspector. See {@link setLayoutSelection}.
354
504
  */
355
505
  layoutSelection: LayoutHit | null;
356
- /** Which section Project Settings opens on. */
357
- settingsTab: string;
358
506
  stylebook: ShellStylebook;
359
507
  git: ShellGit;
508
+ publish: ShellPublish;
360
509
  }
361
510
 
362
511
  export const DOCK_STORAGE_KEY = "jx-studio-panel-widths";
@@ -364,9 +513,8 @@ export const DOCK_STORAGE_KEY = "jx-studio-panel-widths";
364
513
  /**
365
514
  * The per-project record's key prefix — one namespaced record per project root (§14.8).
366
515
  *
367
- * Namespaced because a layout is an arrangement of THIS project's panels: "Ship" over a repo with
368
- * no git remote is not the same tuple as "Ship" over one that has one, and two projects sharing a
369
- * key means opening the second silently rewrites the first.
516
+ * Namespaced because a workspace and its session are THIS project's: two projects sharing a key
517
+ * means opening the second silently rewrites the first.
370
518
  *
371
519
  * Dock geometry stays in {@link DOCK_STORAGE_KEY}, which is read at module scope, before any
372
520
  * project root exists. It is workspace state — `resetProjectShell()` has always kept it across a
@@ -527,93 +675,31 @@ function freshGit(): ShellGit {
527
675
  logEntries: null,
528
676
  rev: 0,
529
677
  status: null,
530
- subTab: "changes",
531
678
  };
532
679
  }
533
680
 
534
- function freshStylebook(): ShellStylebook {
535
- return { customizedOnly: false, filter: "", selection: null, tab: "elements" };
681
+ function freshPublish(): ShellPublish {
682
+ return {
683
+ build: { for: "", state: "idle", text: "" },
684
+ deploys: { error: "", project: "", rows: [], state: "idle" },
685
+ destination: DEFAULT_PUBLISH_DESTINATION,
686
+ review: { number: 0, url: "" },
687
+ tab: DEFAULT_PUBLISH_TAB,
688
+ };
536
689
  }
537
690
 
538
- /**
539
- * The four layouts every project starts with — `Write · Design · Build · Ship`.
540
- *
541
- * Each is a real arrangement of surfaces that already exist, not a mode: Write puts the file set
542
- * beside the prose and the Inspector on Content; Design opens the Outline against the Style tab;
543
- * Build pairs Data with Logic; Ship opens Source Control and gives the whole width to the diff by
544
- * collapsing the Inspector — which stays one ⌘B away, because a layout never removes.
545
- *
546
- * Written as a factory so every project gets its own objects: they are mutable records (a saved
547
- * layout overwrites the one with its name) and a shared array literal would leak one project's
548
- * edits into the next.
549
- */
550
- function builtInLayouts(): LayoutPreset[] {
551
- return [
552
- {
553
- bottomTab: "problems",
554
- docks: {
555
- bottom: { collapsed: true, size: 220 },
556
- left: { collapsed: false, size: 240 },
557
- /* WRITE COLLAPSES THE INSPECTOR. The shell redesign's layout arithmetic is the whole point
558
- of this preset: 1600px − 56 rail − 240 navigator ≈ **1284px of page, 80%**, against
559
- Design's 974. It shipped with the Inspector open at 280px, which is Design's number and
560
- makes Write a Design with a different panel selected.
561
-
562
- It does NOT re-open on selection. The shell redesign's sentence read "until something is
563
- selected", and clicking a paragraph to put a caret in it IS a selection — a dock that
564
- opened every time you clicked into prose would take the 80% back on the first keystroke.
565
- `⌘⇧1`–`⌘⇧4` expand it (`focusInspectorTab` sets `collapsed: false` before it focuses), so
566
- the Inspector is one chord away whenever it is wanted. */
567
- right: { collapsed: true, size: 280 },
568
- },
569
- id: "write",
570
- inspectorTab: "properties",
571
- name: "Write",
572
- navigatorPanel: "files",
573
- },
574
- {
575
- bottomTab: "problems",
576
- docks: {
577
- bottom: { collapsed: true, size: 220 },
578
- left: { collapsed: false, size: 240 },
579
- right: { collapsed: false, size: 320 },
580
- },
581
- id: "design",
582
- inspectorTab: "style",
583
- name: "Design",
584
- navigatorPanel: "layers",
585
- },
586
- {
587
- bottomTab: "logic",
588
- docks: {
589
- bottom: { collapsed: true, size: 260 },
590
- left: { collapsed: false, size: 280 },
591
- right: { collapsed: false, size: 300 },
592
- },
593
- id: "build",
594
- inspectorTab: "events",
595
- name: "Build",
596
- navigatorPanel: "data",
597
- },
598
- {
599
- // The one built-in that OPENS the Bottom dock, on Activity: shipping is a long operation
600
- // With a log, which is exactly what that tab is (§16.4 — Deploy folds into Activity).
601
- bottomTab: "activity",
602
- docks: {
603
- bottom: { collapsed: false, size: 240 },
604
- left: { collapsed: false, size: 300 },
605
- right: { collapsed: true, size: 280 },
606
- },
607
- id: "ship",
608
- inspectorTab: "properties",
609
- name: "Ship",
610
- navigatorPanel: "git",
611
- },
612
- ];
613
- }
691
+ /** The view the Styles workspace opens on until the author picks another: Variables. */
692
+ export const DEFAULT_STYLES_HOME_VIEW: ShellStylebook["view"] = "tokens";
614
693
 
615
- /** The layout a project wakes up on, and the fallback when a stored id names nothing. */
616
- export const DEFAULT_LAYOUT_ID = "design";
694
+ function freshStylebook(): ShellStylebook {
695
+ return {
696
+ customizedOnly: false,
697
+ filter: "",
698
+ selection: null,
699
+ variable: null,
700
+ view: DEFAULT_STYLES_HOME_VIEW,
701
+ };
702
+ }
617
703
 
618
704
  /**
619
705
  * Build the whole record — nested collections complete — before it is handed to `reactive()`.
@@ -640,13 +726,12 @@ function createShellState(): ShellState {
640
726
  focusRegion: "pane",
641
727
  paneSplit: clampPaneSplit(saved.paneSplit),
642
728
  git: freshGit(),
643
- layout: DEFAULT_LAYOUT_ID,
729
+ publish: freshPublish(),
644
730
  layoutSelection: null,
645
- layouts: builtInLayouts(),
646
- leftTab: migratePanelId(saved.leftTab) ?? DEFAULT_PANEL_ID,
647
- settingsTab: "stylebook",
731
+ leftTab: navigatorPanelOutsideWorkspace(migratePanelId(saved.leftTab)),
648
732
  stylebook: freshStylebook(),
649
733
  theme: readPersistedTheme(),
734
+ workspace: DEFAULT_WORKSPACE,
650
735
  };
651
736
  }
652
737
 
@@ -660,6 +745,29 @@ function createShellState(): ShellState {
660
745
  */
661
746
  applyStartupProfile();
662
747
 
748
+ /**
749
+ * Storage keys a retired surface used to write, removed on boot so a stale preference never
750
+ * outlives the control that set it.
751
+ *
752
+ * `jx-studio-doc-header-expanded` was the Document Header card's fold-to-bar preference. Edit draws
753
+ * no card any more (studio.md §4.2), so nothing reads the key and nothing should be left holding
754
+ * it.
755
+ */
756
+ export const RETIRED_STORAGE_KEYS: readonly string[] = ["jx-studio-doc-header-expanded"];
757
+
758
+ /** Drop every {@link RETIRED_STORAGE_KEYS} entry, tolerating unavailable storage. Never throws. */
759
+ export function forgetRetiredStorage(): void {
760
+ try {
761
+ for (const key of RETIRED_STORAGE_KEYS) {
762
+ localStorage.removeItem(key);
763
+ }
764
+ } catch {
765
+ // Storage blocked or unavailable: there is nothing stored to forget.
766
+ }
767
+ }
768
+
769
+ forgetRetiredStorage();
770
+
663
771
  /*
664
772
  * SAFETY: `reactive()` returns a deep proxy carrying its target's own members, and ShellState holds
665
773
  * no refs for `UnwrapNestedRefs` to unwrap, so the proxy IS a ShellState. The type argument is
@@ -697,12 +805,17 @@ export function persistDocks(): void {
697
805
  }
698
806
  }
699
807
 
700
- // ─── Named layouts ────────────────────────────────────────────────────────────
808
+ // ─── The project's record: its workspace, and its session ────────────────────
701
809
 
702
- /** What a project's namespaced record holds: its named layouts, and the session it was left in. */
810
+ /**
811
+ * What a project's namespaced record holds: the workspace it was left in, and the session.
812
+ *
813
+ * A record written by a build that had named layouts also carries `layouts` and `activeLayout`.
814
+ * Neither is read, and the next write drops them: the layouts they name were replaced by the
815
+ * workspaces, and there is nothing in them a workspace could adopt.
816
+ */
703
817
  interface PersistedProject {
704
- layouts?: LayoutPreset[];
705
- activeLayout?: string;
818
+ workspace?: WorkspaceId;
706
819
  /**
707
820
  * §14.8's session — open documents per pane, the active one, and each one's view settings. A
708
821
  * session this boot has not read yet is carried over as the stored JSON it was.
@@ -710,31 +823,15 @@ interface PersistedProject {
710
823
  session?: PersistedSession | JsonValue | undefined;
711
824
  }
712
825
 
713
- /** The project whose layouts are loaded, or `undefined` before the first sync. */
714
- let _layoutRoot: string | null | undefined;
826
+ /** The project whose workspace is loaded, or `undefined` before the first sync. */
827
+ let _workspaceRoot: string | null | undefined;
715
828
 
716
- /** Whether a parsed value is a usable {@link LayoutPreset} — a stored record is untrusted input. */
717
- function isLayoutPreset(value: unknown): value is LayoutPreset {
718
- /* Arrays fail here where the old `typeof … === "object"` let them through, and the outcome is
719
- the same: an array carries no string `id`, so it was refused one line later. */
720
- if (!isJsonObject(value)) {
721
- return false;
722
- }
723
- const { docks } = value;
724
- return (
725
- isString(value.id) &&
726
- value.id !== "" &&
727
- isString(value.name) &&
728
- isNavigatorPanelId(value.navigatorPanel) &&
729
- isInspectorTabId(value.inspectorTab) &&
730
- isBottomTabId(value.bottomTab) &&
731
- isJsonObject(docks) &&
732
- DOCK_IDS.every((dock) => {
733
- const entry = docks[dock];
734
- return isJsonObject(entry) && isNumber(entry.size);
735
- })
736
- );
737
- }
829
+ /**
830
+ * The Navigator panel each workspace whose preset names none was last showing, in this window. Not
831
+ * persisted: it is a convenience between two visits, and the preset is the statement of what a
832
+ * workspace looks like on entry.
833
+ */
834
+ let _panels: Partial<Record<WorkspaceId, string>> = {};
738
835
 
739
836
  /** The raw record for `root`, or `{}` for absent, corrupt or hand-edited storage. */
740
837
  function rawPersistedProject(root: string | null): JsonObject {
@@ -751,36 +848,26 @@ function rawPersistedProject(root: string | null): JsonObject {
751
848
  }
752
849
  }
753
850
 
754
- /** A project's layouts as {@link readPersistedProject} recovers them. */
755
- interface ProjectLayouts {
756
- layouts: LayoutPreset[];
757
- activeLayout: string | null;
758
- }
759
-
760
- /** Read a project's namespaced record, tolerating absent, corrupt and hand-edited storage. */
761
- function readPersistedProject(root: string | null): ProjectLayouts {
762
- const fresh: ProjectLayouts = { activeLayout: null, layouts: builtInLayouts() };
763
- if (!root) {
764
- return fresh;
765
- }
766
- const parsed = rawPersistedProject(root);
767
- const layouts = Array.isArray(parsed.layouts)
768
- ? parsed.layouts.flatMap((entry) => (isLayoutPreset(entry) ? [entry] : []))
769
- : [];
770
- if (layouts.length === 0) {
771
- return fresh;
772
- }
773
- return {
774
- activeLayout: isString(parsed.activeLayout) ? parsed.activeLayout : null,
775
- layouts,
776
- };
851
+ /**
852
+ * The workspace `root` was last left in: a missing, stale or unknown id (a layout id from the build
853
+ * that had named layouts, a workspace a later build renamed, a hand edit) is
854
+ * {@link DEFAULT_WORKSPACE}. Never throws: a stale record must not be the reason a project cannot
855
+ * open.
856
+ *
857
+ * The open sequence asks for it again AFTER restoring the session (`files/files.ts`), because
858
+ * restoring activates tabs and activating a tab enters that tab's workspace: a project left in
859
+ * Media, whose home tab is not a document the session records, would otherwise reopen in Design.
860
+ */
861
+ export function persistedWorkspace(root: string | null): WorkspaceId {
862
+ const stored = rawPersistedProject(root).workspace;
863
+ return isWorkspaceId(stored) ? stored : DEFAULT_WORKSPACE;
777
864
  }
778
865
 
779
866
  /**
780
867
  * The session `root` was last left in, or `null` when there is none to restore.
781
868
  *
782
- * Separate from {@link readPersistedProject} because the two are read at different moments: the
783
- * layouts load reactively the instant the project root changes, and the session is read once, by
869
+ * Separate from {@link syncProjectWorkspace} because the two are read at different moments: the
870
+ * workspace loads reactively the instant the project root changes, and the session is read once, by
784
871
  * the open sequence, at the point where it would otherwise open the home page.
785
872
  */
786
873
  export function persistedSession(root: string | null): PersistedSession | null {
@@ -806,13 +893,6 @@ export function markSessionRestored(root: string | null): void {
806
893
  _sessionRoot = root;
807
894
  }
808
895
 
809
- /**
810
- * Write the project's namespaced record. One key, one writer, one shape.
811
- *
812
- * The dock record's two-writer bug (`persistWidths` vs `applyPanelCollapse`, both on
813
- * {@link DOCK_STORAGE_KEY}) is the reason this is a single function that serialises the whole
814
- * record rather than a merge at each call site.
815
- */
816
896
  /**
817
897
  * Write the session on the way out — the moment the whole feature exists for.
818
898
  *
@@ -825,6 +905,13 @@ export function flushSession(): void {
825
905
  persistProjectShell();
826
906
  }
827
907
 
908
+ /**
909
+ * Write the project's namespaced record. One key, one writer, one shape.
910
+ *
911
+ * The dock record's two-writer bug (`persistWidths` vs `applyPanelCollapse`, both on
912
+ * {@link DOCK_STORAGE_KEY}) is the reason this is a single function that serialises the whole
913
+ * record rather than a merge at each call site.
914
+ */
828
915
  export function persistProjectShell(): void {
829
916
  const root = workspace.projectRoot;
830
917
  if (!root) {
@@ -834,10 +921,9 @@ export function persistProjectShell(): void {
834
921
  localStorage.setItem(
835
922
  `${PROJECT_STORAGE_PREFIX}${root}`,
836
923
  JSON.stringify({
837
- activeLayout: shell.layout,
838
- layouts: shell.layouts,
839
924
  // Only for a project whose session has been read — see {@link markSessionRestored}.
840
925
  session: root === _sessionRoot ? captureSession() : rawPersistedProject(root).session,
926
+ workspace: shell.workspace,
841
927
  } satisfies PersistedProject),
842
928
  );
843
929
  } catch {
@@ -846,147 +932,99 @@ export function persistProjectShell(): void {
846
932
  }
847
933
 
848
934
  /**
849
- * Load the layouts belonging to `root`. Idempotent per root, so the effect can call it freely.
935
+ * Load the workspace belonging to `root`. Idempotent per root, so the effect can call it freely.
936
+ *
937
+ * The id is read by {@link persistedWorkspace}, so a stale one opens the project in
938
+ * {@link DEFAULT_WORKSPACE} and loading never throws.
850
939
  *
851
- * Loading does NOT apply the active layout: a boot that re-applied it would overwrite the dock
852
- * widths the user dragged since, which are persisted separately and are the more recent statement.
853
- * The stored id says which tab is drawn as selected; clicking it is what re-applies the tuple.
940
+ * Opening a project ENTERS its workspace, so the workspace's preset is applied here too. The dock
941
+ * record is the window's rather than the project's, and without this a project closed while Media
942
+ * had collapsed both side docks left the next project's Design frame collapsed. The rail follows
943
+ * `shell.workspace` on its own, through {@link applyDockLayout}. No preset is applied for `null`
944
+ * (no project open): there is no workspace on screen to lay out.
854
945
  */
855
- export function syncProjectLayouts(root: string | null): void {
856
- if (root === _layoutRoot) {
946
+ export function syncProjectWorkspace(root: string | null): void {
947
+ if (root === _workspaceRoot) {
857
948
  return;
858
949
  }
859
- _layoutRoot = root;
860
- const { activeLayout, layouts } = readPersistedProject(root);
861
- shell.layouts = layouts;
862
- const has = (id: string | null) => id !== null && layouts.some((preset) => preset.id === id);
863
- // The stored active layout wins, then the one already selected, then the default, then whatever
864
- // The project actually has — a bar drawn with nothing selected says the app has lost your place.
865
- shell.layout = has(activeLayout)
866
- ? activeLayout!
867
- : has(shell.layout)
868
- ? shell.layout
869
- : has(DEFAULT_LAYOUT_ID)
870
- ? DEFAULT_LAYOUT_ID
871
- : (layouts[0]?.id ?? DEFAULT_LAYOUT_ID);
872
- }
873
-
874
- /** The layout with this id, or `undefined`. */
875
- export function layoutById(id: string): LayoutPreset | undefined {
876
- return shell.layouts.find((preset) => preset.id === id);
877
- }
878
-
879
- /** A stable id for a saved layout's name: `My Layout` → `my-layout`. */
880
- export function layoutIdFor(name: string): string {
881
- const slug = name
882
- .trim()
883
- .toLowerCase()
884
- .replaceAll(/[^a-z0-9]+/g, "-")
885
- .replaceAll(/^-|-$/g, "");
886
- return slug || "layout";
950
+ _workspaceRoot = root;
951
+ _panels = {};
952
+ shell.workspace = persistedWorkspace(root);
953
+ if (root === null) {
954
+ /* No frame to apply, but the Navigator is still on screen beside the rail: a panel only the
955
+ closed project's workspace drew (Collections, left in Content) is not this window's to show. */
956
+ shell.leftTab = navigatorPanelOutsideWorkspace(migratePanelId(shell.leftTab));
957
+ return;
958
+ }
959
+ /* Untracked: this runs inside the shell's project-root effect, which must depend on the root
960
+ alone and not on the panel and docks the preset reads and writes. */
961
+ pauseTracking();
962
+ try {
963
+ applyFrame(shell.workspace);
964
+ persistDocks();
965
+ } finally {
966
+ resetTracking();
967
+ }
887
968
  }
888
969
 
889
- /** What applying or capturing a layout needs from the Inspector, which lives on the active tab. */
890
- export interface LayoutDeps {
970
+ /** What entering a workspace needs from the Inspector, whose tab lives on the active document. */
971
+ export interface WorkspaceDeps {
891
972
  setInspectorTab: (tab: InspectorTabId) => void;
892
- inspectorTab: () => InspectorTabId;
973
+ /**
974
+ * Whether the active tab is a document being written on the canvas, which is what draws a
975
+ * `whileWriting` Inspector ({@link WorkspacePreset.inspector}). Injected by
976
+ * `workspace/workspace-switch.ts`, which owns the rule that reads a tab; absent, it is `false`.
977
+ */
978
+ writingOnCanvas?: () => boolean;
893
979
  }
894
980
 
895
- /**
896
- * Adopt a layout: its panel, its docks, its Inspector tab — and remember that it is the active one.
897
- *
898
- * Throws on an unknown id rather than silently doing nothing, because the id space is the user's
899
- * own (their saved layouts) and a typo in a command argument has to be reportable.
900
- */
901
- export function applyLayout(id: string, deps: LayoutDeps): void {
902
- const preset = layoutById(id);
903
- if (!preset) {
904
- throw new RangeError(
905
- `no layout named "${id}" — this project has: ${shell.layouts.map((p) => p.id).join(", ")}`,
906
- );
907
- }
908
- shell.layout = preset.id;
909
- shell.leftTab = preset.navigatorPanel;
910
- shell.bottomTab = preset.bottomTab;
911
- for (const dock of DOCK_IDS) {
912
- shell.docks[dock].collapsed = preset.docks[dock].collapsed;
913
- shell.docks[dock].size = preset.docks[dock].size;
914
- }
915
- deps.setInspectorTab(preset.inspectorTab);
916
- persistDocks();
917
- persistProjectShell();
981
+ /** Whether a workspace's preset draws its Inspector, given whether a document is being written. */
982
+ export function presetInspectorOpen(id: WorkspaceId, writing: boolean): boolean {
983
+ const { inspector } = WORKSPACE_PRESETS[id];
984
+ return inspector.open || (inspector.whileWriting === true && writing);
918
985
  }
919
986
 
920
- /**
921
- * Save the current arrangement under `name`, and make it the active layout.
922
- *
923
- * Saving over an existing name overwrites it — that is what `+` on a layout you have just adjusted
924
- * means, and it keeps the operation idempotent for a caller that cannot see the list.
925
- */
926
- export function saveLayout(name: string, deps: LayoutDeps): LayoutPreset {
927
- const trimmed = name.trim();
928
- const id = layoutIdFor(trimmed);
929
- const docks = perDock((dock) => ({
930
- collapsed: shell.docks[dock].collapsed,
931
- size: shell.docks[dock].size,
932
- }));
933
- const preset: LayoutPreset = {
934
- bottomTab: isBottomTabId(shell.bottomTab) ? shell.bottomTab : DEFAULT_BOTTOM_TAB,
935
- docks,
936
- id,
937
- inspectorTab: deps.inspectorTab(),
938
- name: trimmed || id,
939
- navigatorPanel: migratePanelId(shell.leftTab) ?? DEFAULT_PANEL_ID,
940
- };
941
- const existing = shell.layouts.findIndex((entry) => entry.id === id);
942
- if (existing === -1) {
943
- shell.layouts.push(preset);
944
- } else {
945
- shell.layouts.splice(existing, 1, preset);
987
+ /** Lay a workspace's preset out: the Navigator's panel, both side docks and the Inspector's tab. */
988
+ function applyFrame(id: WorkspaceId, deps?: WorkspaceDeps): void {
989
+ const preset = WORKSPACE_PRESETS[id];
990
+ const panel = migratePanelId(
991
+ preset.navigator.panel ?? _panels[id] ?? preset.navigator.initial ?? shell.leftTab,
992
+ );
993
+ if (panel) {
994
+ shell.leftTab = panel;
946
995
  }
947
- shell.layout = id;
948
- persistProjectShell();
949
- return preset;
950
- }
951
-
952
- /** Rename a layout in place. The id — and therefore every reference to it — is unchanged. */
953
- export function renameLayout(id: string, name: string): void {
954
- const preset = layoutById(id);
955
- const trimmed = name.trim();
956
- if (!preset || !trimmed) {
957
- return;
996
+ shell.docks.left.collapsed = !preset.navigator.open;
997
+ shell.docks.right.collapsed = !presetInspectorOpen(id, deps?.writingOnCanvas?.() === true);
998
+ if (preset.inspector.tab && deps) {
999
+ deps.setInspectorTab(preset.inspector.tab);
958
1000
  }
959
- preset.name = trimmed;
960
- persistProjectShell();
961
1001
  }
962
1002
 
963
1003
  /**
964
- * Forget a layout. The last one cannot be deleted: an empty Command Bar with a `+` and no way to
965
- * know what it does is worse than a layout you do not use.
1004
+ * Enter a workspace: lay its frame out by its {@link WORKSPACE_PRESETS} preset, which every entry
1005
+ * applies afresh (`specs/studio.md` §22.3).
1006
+ *
1007
+ * This is the frame half of a switch and the one writer of `shell.workspace`. Which TAB is on
1008
+ * screen is the other half, and it is not this module's: `workspace/workspace-switch.ts` activates
1009
+ * the workspace's tab, and activating any tab enters that tab's workspace through here.
1010
+ *
1011
+ * Re-entering the workspace already showing changes nothing, so the verb is idempotent and a dock
1012
+ * the author just toggled is not put back.
1013
+ *
1014
+ * @returns Whether the workspace changed.
966
1015
  */
967
- export function deleteLayout(id: string): void {
968
- if (shell.layouts.length < 2) {
969
- return;
970
- }
971
- const index = shell.layouts.findIndex((preset) => preset.id === id);
972
- if (index === -1) {
973
- return;
1016
+ export function enterWorkspace(id: WorkspaceId, deps?: WorkspaceDeps): boolean {
1017
+ if (shell.workspace === id) {
1018
+ return false;
974
1019
  }
975
- shell.layouts.splice(index, 1);
976
- if (shell.layout === id) {
977
- shell.layout = shell.layouts[0]!.id;
1020
+ if (!WORKSPACE_PRESETS[shell.workspace].navigator.panel) {
1021
+ _panels[shell.workspace] = shell.leftTab;
978
1022
  }
1023
+ shell.workspace = id;
1024
+ applyFrame(id, deps);
1025
+ persistDocks();
979
1026
  persistProjectShell();
980
- }
981
-
982
- /**
983
- * Re-apply the active layout as declared — `View: Reset Layout`, always one action away.
984
- *
985
- * This is the escape hatch that makes a layout safe to drift from: drag a dock, collapse the
986
- * Inspector, open a different panel, and one command puts the arrangement back.
987
- */
988
- export function resetLayout(deps: LayoutDeps): void {
989
- applyLayout(shell.layout, deps);
1027
+ return true;
990
1028
  }
991
1029
 
992
1030
  /**
@@ -1029,7 +1067,28 @@ export function applyChromeTheme(): void {
1029
1067
  loadedMonaco()?.editor.setTheme(monacoTheme());
1030
1068
  }
1031
1069
 
1032
- /** Project the dock record onto the shell grid: collapse classes on #app, sizes as CSS vars. */
1070
+ /**
1071
+ * The `#app` class that takes the rail column out of the grid. `styles/shell-frame.json` owns what
1072
+ * it means; the rail has no toggle and no size, so it is a class and never a dock.
1073
+ */
1074
+ export const RAIL_HIDDEN_CLASS = "rail-hidden";
1075
+
1076
+ /**
1077
+ * The `#app` class that takes every pane's tab strip out of the grid, in a workspace whose preset
1078
+ * draws none ({@link WorkspacePreset.tabStrip}). `styles/shell-frame.json` owns what it means; the
1079
+ * strips stay mounted underneath, so returning to Design repaints nothing.
1080
+ */
1081
+ export const TAB_STRIP_HIDDEN_CLASS = "tabs-hidden";
1082
+
1083
+ /** Whether the workspace on screen draws pane tab strips. */
1084
+ export function workspaceDrawsTabStrip(id: WorkspaceId = shell.workspace): boolean {
1085
+ return WORKSPACE_PRESETS[id].tabStrip;
1086
+ }
1087
+
1088
+ /**
1089
+ * Project the dock record onto the shell grid: collapse classes on #app, sizes as CSS vars, and the
1090
+ * active workspace's rail and tab strips.
1091
+ */
1033
1092
  export function applyDockLayout(): void {
1034
1093
  const root = document.documentElement;
1035
1094
  for (const id of DOCK_IDS) {
@@ -1042,6 +1101,8 @@ export function applyDockLayout(): void {
1042
1101
  for (const id of DOCK_IDS) {
1043
1102
  app.classList.toggle(DOCK_CLASS[id], shell.docks[id].collapsed);
1044
1103
  }
1104
+ app.classList.toggle(RAIL_HIDDEN_CLASS, !WORKSPACE_PRESETS[shell.workspace].rail);
1105
+ app.classList.toggle(TAB_STRIP_HIDDEN_CLASS, !WORKSPACE_PRESETS[shell.workspace].tabStrip);
1045
1106
  }
1046
1107
 
1047
1108
  let _scope: EffectScope | null = null;
@@ -1109,11 +1170,11 @@ export function mountShell(): void {
1109
1170
  effect(() => {
1110
1171
  applyChromeTheme();
1111
1172
  });
1112
- // Layouts belong to a project, and the project root is reactive: opening one loads its record
1113
- // With no boot-order hook to forget. `resetProjectShell()` runs while the OLD root is still
1114
- // Set — it is the closing half of a switch — which is why the load cannot live there.
1173
+ // The workspace belongs to a project, and the project root is reactive: opening one loads its
1174
+ // Record with no boot-order hook to forget. `resetProjectShell()` runs while the OLD root is
1175
+ // Still set — it is the closing half of a switch — which is why the load cannot live there.
1115
1176
  effect(() => {
1116
- syncProjectLayouts(workspace.projectRoot);
1177
+ syncProjectWorkspace(workspace.projectRoot);
1117
1178
  });
1118
1179
  /*
1119
1180
  * §14.8's session, written whenever its SHAPE changes.
@@ -1251,21 +1312,21 @@ export function setLayoutSelection(selection: LayoutHit | null): void {
1251
1312
  *
1252
1313
  * Source control, the stylebook selection and the settings tab all describe _a_ project; carrying
1253
1314
  * them into the next one showed the previous repository's branch, file count and "last updated"
1254
- * time under the new project's name. Docks, the layout preset and the active rail panel are
1255
- * deliberately kept — they describe the workspace, not the project.
1315
+ * time under the new project's name. Docks and the active rail panel are deliberately kept: they
1316
+ * describe the window, not the project. The workspace is the project's, and the next
1317
+ * {@link syncProjectWorkspace} reads it from that project's own record.
1256
1318
  */
1257
1319
  export function resetProjectShell(): void {
1258
1320
  Object.assign(shell.git, freshGit());
1321
+ Object.assign(shell.publish, freshPublish());
1259
1322
  Object.assign(shell.stylebook, freshStylebook());
1260
- shell.settingsTab = "stylebook";
1261
1323
  shell.layoutSelection = null;
1262
1324
  // …and which project's session may be written, so the one being opened cannot be overwritten
1263
1325
  // Before it has been read.
1264
1326
  _sessionRoot = null;
1265
- // Forget which project's layouts are loaded, so the next `syncProjectLayouts()` reloads even if
1266
- // The same root is opened again. The docks and the active rail panel stay: they describe the
1267
- // Workspace, and re-applying a layout on project open would discard the widths you just dragged.
1268
- _layoutRoot = undefined;
1327
+ // Forget which project's workspace is loaded, so the next `syncProjectWorkspace()` reloads even
1328
+ // If the same root is opened again.
1329
+ _workspaceRoot = undefined;
1269
1330
  }
1270
1331
 
1271
1332
  // ─── Commands ─────────────────────────────────────────────────────────────────
@@ -1287,6 +1348,14 @@ export interface ShellCommandDeps {
1287
1348
  * screenshot that closed the assistant would silently also lose your Style tab.
1288
1349
  */
1289
1350
  inspectorTab: () => InspectorTabId;
1351
+ /**
1352
+ * The tab the strip rests on when nothing chose one: Page with nothing selected, otherwise the
1353
+ * Element leaf last used (`specs/studio.md` §6). `view.setAssistant { open: false }` steps onto
1354
+ * it, so closing the Assistant with a node selected shows that node rather than the document.
1355
+ * Injected because the selection and the remembered leaf are per-document session state; absent
1356
+ * (the CI projection), it is {@link DEFAULT_INSPECTOR_TAB}.
1357
+ */
1358
+ restingInspectorTab?: () => InspectorTabId;
1290
1359
  /**
1291
1360
  * Whether a dock tab's panel record is currently available — its own `when` holding.
1292
1361
  *
@@ -1300,6 +1369,24 @@ export interface ShellCommandDeps {
1300
1369
  * so a caller with no registry (the CI projection) is unaffected.
1301
1370
  */
1302
1371
  panelAvailable?: (id: string) => boolean;
1372
+ /**
1373
+ * Show a workspace: enter it AND put its tabs on screen (its most recent tab, or its home).
1374
+ *
1375
+ * Injected for the reason the Inspector writers are: opening a workspace's home reaches the
1376
+ * Library, the settings document and the pane model, none of which this module may import. Absent
1377
+ * (the CI projection, a test with no panes), the verb enters the workspace's frame and nothing
1378
+ * else, which is still the state the argument names.
1379
+ */
1380
+ showWorkspace?: (id: WorkspaceId) => void;
1381
+ }
1382
+
1383
+ /** Run {@link ShellCommandDeps.showWorkspace}, or enter the frame alone when none was handed in. */
1384
+ function showWorkspace(id: WorkspaceId, deps: ShellCommandDeps): void {
1385
+ if (deps.showWorkspace) {
1386
+ deps.showWorkspace(id);
1387
+ return;
1388
+ }
1389
+ enterWorkspace(id, deps);
1303
1390
  }
1304
1391
 
1305
1392
  /** The Navigator dock hosts a panel — the precondition its own verbs share. */
@@ -1476,7 +1563,7 @@ export function shellViewCommands(deps: ShellCommandDeps): AnyCommand[] {
1476
1563
  duplicate title among 130 records. A palette row is a NAME, and two rows with one name is
1477
1564
  the defect the registry exists to prevent, arriving through the registry itself.
1478
1565
  `inspector.focus.assistant` is the row (it carries ⌘⇧4). This one keeps every other
1479
- consumer — the `open` STATE that `setRightTab` cannot express, the layout presets, three
1566
+ consumer — the `open` STATE that `setRightTab` cannot express, the workspace presets, three
1480
1567
  screenshot steps, `⌘J`'s legacy branch — none of which needs a row to work. */
1481
1568
  menus: ["never"],
1482
1569
  group: "4_docks",
@@ -1487,100 +1574,49 @@ export function shellViewCommands(deps: ShellCommandDeps): AnyCommand[] {
1487
1574
  return;
1488
1575
  }
1489
1576
  if (deps.inspectorTab() === "assistant") {
1490
- deps.setInspectorTab(DEFAULT_INSPECTOR_TAB);
1577
+ deps.setInspectorTab(deps.restingInspectorTab?.() ?? DEFAULT_INSPECTOR_TAB);
1491
1578
  }
1492
1579
  },
1493
1580
  title: "Show Assistant",
1494
1581
  },
1495
- // ── Named layouts ────────────────────────────────────────────────────────
1496
- // Five verbs, because a named arrangement you can create but not rename, delete or restore is
1497
- // A trap. All five are SETTERS: `view.setLayout { layout: "ship" }` means the same thing twice
1498
- // In a row, and `view.resetLayout` is the one action that puts a drifted arrangement back.
1582
+ // ── Workspaces ───────────────────────────────────────────────────────────
1583
+ // One SETTER, and one chord record per workspace. A chord cannot carry an argument, so ⌘1–5
1584
+ // Are five records that each run the setter's own path rather than five implementations of it.
1499
1585
  {
1500
1586
  args: argsSchema({
1501
- layout: stringProperty("The layout's id — one of this project's saved layouts."),
1587
+ workspace: enumProperty(WORKSPACE_IDS, "Which workspace to show."),
1502
1588
  }),
1503
1589
  category: "View",
1504
- id: "view.setLayout",
1590
+ id: "view.setWorkspace",
1505
1591
  level: "application",
1506
- menus: ["palette"],
1507
- group: "4_layouts",
1592
+ /* `never`, not `palette`: the five `workspace.focus.*` rows below are the palette's way in,
1593
+ each with its chord, and an arguments prompt beside them would be the same action under a
1594
+ second name. This record is the scripted door and the one the top bar's tabs run. */
1595
+ menus: ["never"],
1596
+ group: "4_workspaces",
1508
1597
  requires: "an open project",
1509
1598
  when: projectOpen,
1510
1599
  /* No `aiTool`: application-level chrome, refused at registration
1511
1600
  (`studio-ui-guidelines.md` §12.4). */
1512
1601
  run: (_ctx, args) => {
1513
- applyLayout(stringArg("view.setLayout", args, "layout"), deps);
1602
+ showWorkspace(enumArg("view.setWorkspace", args, "workspace", WORKSPACE_IDS), deps);
1514
1603
  },
1515
- title: "Switch Layout",
1604
+ title: "Switch Workspace",
1516
1605
  },
1517
- {
1518
- args: argsSchema({
1519
- name: stringProperty("What to call this arrangement. An existing name is overwritten."),
1520
- }),
1606
+ ...WORKSPACES.map((entry, index): AnyCommand => ({
1521
1607
  category: "View",
1522
- id: "view.saveLayout",
1608
+ id: `workspace.focus.${entry.id}`,
1609
+ keybinding: `mod+${index + 1}`,
1523
1610
  level: "application",
1524
1611
  menus: ["palette"],
1525
- group: "4_layouts",
1526
- requires: "an open project",
1527
- when: projectOpen,
1528
- run: (_ctx, args) => {
1529
- saveLayout(stringArg("view.saveLayout", args, "name"), deps);
1530
- },
1531
- title: "Save Layout",
1532
- },
1533
- {
1534
- args: argsSchema({
1535
- layout: stringProperty("The layout's id."),
1536
- name: stringProperty("Its new name. The id, and every reference to it, is unchanged."),
1537
- }),
1538
- category: "View",
1539
- id: "view.renameLayout",
1540
- level: "application",
1541
- menus: ["palette"],
1542
- group: "4_layouts",
1543
- requires: "an open project",
1544
- when: projectOpen,
1545
- run: (_ctx, args) => {
1546
- renameLayout(
1547
- stringArg("view.renameLayout", args, "layout"),
1548
- stringArg("view.renameLayout", args, "name"),
1549
- );
1550
- },
1551
- title: "Rename Layout",
1552
- },
1553
- {
1554
- args: argsSchema({
1555
- layout: stringProperty("The layout's id. The last remaining layout is kept."),
1556
- }),
1557
- category: "View",
1558
- id: "view.deleteLayout",
1559
- level: "application",
1560
- menus: ["palette"],
1561
- group: "4_layouts",
1562
- requires: "an open project",
1563
- when: projectOpen,
1564
- run: (_ctx, args) => {
1565
- deleteLayout(stringArg("view.deleteLayout", args, "layout"));
1566
- },
1567
- title: "Delete Layout",
1568
- },
1569
- {
1570
- // No `args`: this verb takes none. An empty schema would be a promise of parameters the
1571
- // Palette would then prompt for and the record would ignore.
1572
- category: "View",
1573
- id: "view.resetLayout",
1574
- level: "application",
1575
- menus: ["palette"],
1576
- group: "4_layouts",
1612
+ group: "4_workspaces",
1577
1613
  requires: "an open project",
1578
1614
  when: projectOpen,
1579
1615
  run: () => {
1580
- resetLayout(deps);
1616
+ showWorkspace(entry.id, deps);
1581
1617
  },
1582
- title: "Reset Layout",
1583
- },
1618
+ title: `Go to ${entry.title}`,
1619
+ })),
1584
1620
  {
1585
1621
  args: argsSchema({
1586
1622
  color: enumProperty(CHROME_THEMES, "The Studio chrome's theme."),