@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
@@ -80,6 +80,7 @@ import { runActiveReported, runReported } from "../commands/run-reported";
80
80
  import { collectionOfPath } from "../content/entry-model";
81
81
  import { confirmFileDelete, parseSourceForPath, renamePromptMessage } from "./file-ops";
82
82
  import { parseJsonDocument } from "@jxsuite/schema/json-layout";
83
+ import { normalizeDocument } from "../tabs/bare-repeaters";
83
84
  // The pure half of the differ: order-insensitive equality with no Yjs behind it.
84
85
  import { deepEqual } from "@jxsuite/collab/diff-core";
85
86
  import type { JsonLayout } from "@jxsuite/schema/json-layout";
@@ -94,14 +95,27 @@ import {
94
95
  refreshFormats,
95
96
  } from "../format/format-host";
96
97
  import { convertTargets, creationFormats, knownDocumentExtensions } from "../format/format-choices";
98
+ import {
99
+ composeNewFileName,
100
+ extensionOfName,
101
+ OTHER_FORMAT,
102
+ validateNewFileName,
103
+ } from "./new-file-name";
104
+ import type { FormatChoice, NewFileNameCheck } from "./new-file-name";
97
105
  import { collectionForDirectory } from "../content/collection-match";
98
- import { markSessionRestored, persistedSession, resetProjectShell, setActivityTab } from "../shell";
106
+ import {
107
+ markSessionRestored,
108
+ persistedSession,
109
+ persistedWorkspace,
110
+ resetProjectShell,
111
+ } from "../shell";
112
+ import type { WorkspaceId } from "../workspace/workspace-ids";
99
113
  import { restoreSession } from "../workspace/session";
100
- import { cleanupGitPanel } from "../panels/git-panel";
114
+ import { cleanupSourceControl } from "../publish/source-control";
101
115
  import { addRecentProject, trackRecentFile } from "../recent-projects";
102
116
  import { listWindow, revealListRow, watchListWindow } from "../ui/virtual-window";
103
117
  import type { JsonObject, JxMutableNode } from "@jxsuite/schema/types";
104
- import { isFunction, isString } from "@jxsuite/schema/guards";
118
+ import { isString } from "@jxsuite/schema/guards";
105
119
  import type { CommandArgValues } from "../commands/command-args";
106
120
  import type { FormatFrontmatter } from "../format/format-host";
107
121
  import type { ResolvedI18n } from "@jxsuite/schema/locale";
@@ -176,6 +190,29 @@ export async function loadProjectDirectories(): Promise<void> {
176
190
  projectState.projectDirs = found;
177
191
  }
178
192
 
193
+ /**
194
+ * Re-read the project's directories when a write created a conventional one the list lacks.
195
+ *
196
+ * The list is read as the project opens, so a project that had no `pages/` or `content/` then does
197
+ * not grow one in the list when Add new writes its first page or collection, and the Content scan
198
+ * walks only what the list names. Resolves `true` when the list was re-read, so the caller knows a
199
+ * repaint is due; `false` (and no listing at all) when every written path was already covered.
200
+ *
201
+ * @param paths Project-relative paths just written.
202
+ */
203
+ export async function refreshProjectDirectoriesFor(paths: readonly string[]): Promise<boolean> {
204
+ const known = new Set(projectState?.projectDirs);
205
+ const missing = paths.some((path) => {
206
+ const top = path.replaceAll("\\", "/").replace(/^\.\//, "").split("/")[0] ?? "";
207
+ return path.includes("/") && CONVENTIONAL_DIRS.has(top) && !known.has(top);
208
+ });
209
+ if (!missing) {
210
+ return false;
211
+ }
212
+ await loadProjectDirectories();
213
+ return true;
214
+ }
215
+
179
216
  /**
180
217
  * Whether the tree currently draws the files `.gitignore` masks.
181
218
  *
@@ -256,13 +293,19 @@ export async function loadProject() {
256
293
  * the window on the project it already had, and the caller announces the outcome — "Opening the
257
294
  * project…" over a dialog the user just dismissed is a report of something that did not happen.
258
295
  *
259
- * @param {{ renderLeftPanel: () => void }} ctx
296
+ * @param ctx.renderLeftPanel Repaint the Navigator.
297
+ * @param ctx.resetProjectCaches Drop the caches panels above this module keep for the project being
298
+ * left (the Site panel's index, the media listing). Injected for the reason `fs-events.ts` takes
299
+ * `invalidateDerivedCaches`: those panels import from the file layer, so reaching up is a cycle,
300
+ * and `studio.ts`'s `openRecentProject` runs the same reset.
260
301
  * @returns {Promise<boolean>} Whether the window changed project.
261
302
  */
262
303
  export async function openProject({
263
304
  renderLeftPanel,
305
+ resetProjectCaches,
264
306
  }: {
265
307
  renderLeftPanel: () => void;
308
+ resetProjectCaches?: () => void;
266
309
  }): Promise<boolean> {
267
310
  try {
268
311
  const platform = getPlatform();
@@ -312,10 +355,13 @@ export async function openProject({
312
355
  await loadComponentRegistry();
313
356
 
314
357
  // Source control, the stylebook selection and the settings tab describe the project being
315
- // Left behind. The poll timer goes with them; the panel re-arms it on its next render.
316
- cleanupGitPanel();
358
+ // Left behind. The poll timer goes with them; the Publish workspace re-arms it on its next mount.
359
+ cleanupSourceControl();
317
360
  resetProjectShell();
318
- setActivityTab("files");
361
+ resetProjectCaches?.();
362
+ /* The Navigator's panel is the workspace frame's, applied as the project opened (§22.3): Design
363
+ opens on the Site panel. Forcing Files here put a developer panel with no rail button on
364
+ screen and re-opened a Navigator the frame had hidden. */
319
365
  addRecentProject(requireProjectState().name, requireProjectState().projectRoot);
320
366
  renderLeftPanel();
321
367
  // The project's name is permanent state in the status bar's PROJECT field now.
@@ -424,6 +470,31 @@ export async function openLastSessionOrHome(): Promise<boolean> {
424
470
  // `projectState.projectRoot`, the recent-list entry. One reader of one field cannot disagree with
425
471
  // The writer; three callers passing three spellings silently restore nothing.
426
472
  const session = persistedSession(workspace.projectRoot);
473
+ /* The workspace it was left in, read BEFORE anything opens: every tab the restore activates
474
+ enters its own workspace, and that entry is persisted over the stored one. */
475
+ const left = persistedWorkspace(workspace.projectRoot);
476
+ const restored = await restoreSessionOrHome(session);
477
+ _showWorkspace?.(left);
478
+ return restored;
479
+ }
480
+
481
+ /**
482
+ * How a project open shows the workspace it was left in, once its documents are open. Installed by
483
+ * the bootstrap (`workspace/workspace-switch.ts`'s `switchWorkspace`), because the switch opens
484
+ * homes this module must not import; unset, the restore leaves the workspace wherever its tabs
485
+ * did.
486
+ */
487
+ let _showWorkspace: ((id: WorkspaceId) => void) | null = null;
488
+
489
+ /** Install (or with `null`, remove) what {@link openLastSessionOrHome} shows the workspace with. */
490
+ export function setSessionWorkspaceShow(show: ((id: WorkspaceId) => void) | null): void {
491
+ _showWorkspace = show;
492
+ }
493
+
494
+ /** The body of {@link openLastSessionOrHome}: the session, or the home page. */
495
+ async function restoreSessionOrHome(
496
+ session: ReturnType<typeof persistedSession>,
497
+ ): Promise<boolean> {
427
498
  // Read first, THEN allow writes: the persist effect fires the moment `workspace.projectRoot` is
428
499
  // Set, and an empty workspace captured at that instant would overwrite the very record this line
429
500
  // Reads. See `markSessionRestored`.
@@ -1698,29 +1769,6 @@ const BLANK_DOCUMENT = JSON.stringify(
1698
1769
  2,
1699
1770
  );
1700
1771
 
1701
- /**
1702
- * The picker row meaning "I will type the whole name myself".
1703
- *
1704
- * It is what keeps the format picker from being a cage. `New File…` is the only generic
1705
- * file-creation affordance in Studio and both backends create intermediate directories on write, so
1706
- * without this row `styles/main.css`, `public/robots.txt`, `.gitignore` and a `credits.txt` beside
1707
- * a collection's images all become uncreatable — and the picker would have taken away more than it
1708
- * gave. The picker stays the authority on the extension; this is one of its answers.
1709
- */
1710
- export const OTHER_FORMAT = "__other__";
1711
-
1712
- /** How the extension of a new file is settled. */
1713
- export type FormatChoice =
1714
- /** Picker over every creatable format, plus Other…. The field is a NAME, taken verbatim. */
1715
- | { kind: "choose"; defaultExt?: string; docKind?: "page" | "component" | "content" }
1716
- /**
1717
- * Picker holding ONE format, plus Other… — the collection's own extension. The field is a NAME.
1718
- * `because` is the sentence a refusal quotes when Other… would smuggle a foreign document in.
1719
- */
1720
- | { kind: "locked"; ext: string; because: string }
1721
- /** No picker. The field is a DISPLAY NAME, slugified, and `ext` is appended. */
1722
- | { kind: "fixed"; ext: string };
1723
-
1724
1772
  /**
1725
1773
  * One creation, named.
1726
1774
  *
@@ -1742,34 +1790,12 @@ export interface NewFileRequest {
1742
1790
  * (`i18n.createTranslation` composes `about.json` from the file it is translating).
1743
1791
  */
1744
1792
  format?: FormatChoice;
1745
- /**
1746
- * Body to write. A function is called with the extension the reader settled on and the composed
1747
- * file name, which is the only way a caller can seed a body whose FORMAT — or whose own tag name
1748
- * — it does not know until the dialog resolves. Defaults to the resolved format's
1749
- * `newFileTemplate`.
1750
- */
1751
- content?:
1752
- | string
1753
- | ((ext: string, fileName: string) => string | undefined | Promise<string | undefined>);
1793
+ /** Body to write. Defaults to the resolved format's `newFileTemplate`. */
1794
+ content?: string;
1754
1795
  /** Who is creating, for the Problem's `source` line. Defaults to "Files". */
1755
1796
  source?: string;
1756
1797
  }
1757
1798
 
1758
- /** A display name, reduced to a file stem. Lowercase by design: this is a slug. */
1759
- function slugify(input: string): string {
1760
- return input
1761
- .trim()
1762
- .toLowerCase()
1763
- .replaceAll(/\s+/g, "-")
1764
- .replaceAll(/[^a-z\d-]/g, "");
1765
- }
1766
-
1767
- /** The extension of a file name, including the dot, or `""`. */
1768
- function extensionOfName(name: string): string {
1769
- const dot = name.lastIndexOf(".");
1770
- return dot > 0 ? name.slice(dot) : "";
1771
- }
1772
-
1773
1799
  /** The two picker modes — the ones where the field asks for a NAME and the picker owns the suffix. */
1774
1800
  type PickerChoice = Extract<FormatChoice, { kind: "choose" } | { kind: "locked" }>;
1775
1801
 
@@ -1831,14 +1857,7 @@ export async function createFileIn(request: NewFileRequest): Promise<string | nu
1831
1857
  await loadFormats();
1832
1858
 
1833
1859
  // One listing, before the field opens: the names it must refuse are known while typing.
1834
- let taken = new Set<string>();
1835
- try {
1836
- const listing = await getPlatform().listDirectory(dir);
1837
- taken = new Set(listing.map((entry) => entry.name.toLowerCase()));
1838
- } catch {
1839
- // A directory that cannot be listed is usually one that does not exist yet — the write below
1840
- // Is the authority on whether that is a problem, and it reports with the real reason.
1841
- }
1860
+ const taken = await listTakenNames(dir);
1842
1861
 
1843
1862
  const picker = choice !== undefined && choice.kind !== "fixed" ? choice : null;
1844
1863
  const rows = picker === null ? null : formatRowsFor(picker);
@@ -1847,73 +1866,8 @@ export async function createFileIn(request: NewFileRequest): Promise<string | nu
1847
1866
  ? ""
1848
1867
  : initialPickFor(picker, rows, request.suggestedName ?? "");
1849
1868
 
1850
- /**
1851
- * The file name a field value and a pick compose to — three modes, not two.
1852
- *
1853
- * The verbatim STEM mode is not a convenience. The slugifier lowercases and strips everything
1854
- * outside `[a-z0-9-]`, which turns `[slug]` into `slug` — and eight shipped starters carry
1855
- * `pages/[slug].json`, `[sku].json`, `[...slug].json`. A picker that slugified would make a
1856
- * dynamic route uncreatable and would say nothing about it.
1857
- */
1858
- const fileNameFor = (input: string, picked: string) => {
1859
- const trimmed = input.trim();
1860
- if (choice === undefined || picked === OTHER_FORMAT) {
1861
- return trimmed;
1862
- }
1863
- if (choice.kind === "fixed") {
1864
- const slug = slugify(trimmed);
1865
- return slug === "" ? "" : `${slug}${choice.ext}`;
1866
- }
1867
- return trimmed.toLowerCase().endsWith(picked.toLowerCase()) ? trimmed : `${trimmed}${picked}`;
1868
- };
1869
-
1870
- const known = knownDocumentExtensions();
1871
-
1872
- const validate = (value: string, picked: string) => {
1873
- const trimmed = value.trim();
1874
- if (!trimmed) {
1875
- return choice?.kind === "fixed" ? "Enter a name." : "Enter a file name.";
1876
- }
1877
- if (choice?.kind === "fixed" && slugify(trimmed) === "") {
1878
- return "Enter at least one letter or number.";
1879
- }
1880
- const normalized = trimmed.replaceAll("\\", "/");
1881
- if (normalized.startsWith("/") || normalized.endsWith("/")) {
1882
- return "Enter a name, not a path that starts or ends with a slash.";
1883
- }
1884
- if (normalized.split("/").includes("..")) {
1885
- return "A name cannot step outside the destination folder.";
1886
- }
1887
- // A slash is the tree's ONLY way to make a directory — both backends create them on write — so
1888
- // It is allowed exactly where the field is a whole file name, and refused where it is a name.
1889
- if (normalized.includes("/") && choice !== undefined && picked !== OTHER_FORMAT) {
1890
- return `Pick Other… to create a file in a subfolder of ${dir === "." ? "the project root" : dir}.`;
1891
- }
1892
- /* A name whose extension MATCHES the pick is fine and is composed once, never doubled;
1893
- refusing every trailing extension would reject this dialog's own `untitled.json` default.
1894
- A name ending in an extension no format claims is a stem that happens to have a dot. */
1895
- const typedExt = extensionOfName(normalized);
1896
- if (
1897
- picked !== "" &&
1898
- picked !== OTHER_FORMAT &&
1899
- typedExt !== "" &&
1900
- typedExt.toLowerCase() !== picked.toLowerCase() &&
1901
- known.has(typedExt.toLowerCase())
1902
- ) {
1903
- const label = rows?.find((row) => row.value === picked)?.label ?? picked;
1904
- return `You picked ${label}; this name ends in ${typedExt}.`;
1905
- }
1906
- if (choice?.kind === "locked" && picked === OTHER_FORMAT) {
1907
- const foreign = typedExt !== "" && typedExt.toLowerCase() !== choice.ext.toLowerCase();
1908
- if (foreign && known.has(typedExt.toLowerCase())) {
1909
- return choice.because;
1910
- }
1911
- }
1912
- /* No emptiness check here: every way `fileNameFor` can answer "" is already refused above — a
1913
- blank field by the first rule, a display name that slugifies to nothing by the second. */
1914
- const candidate = fileNameFor(value, picked);
1915
- return taken.has(candidate.toLowerCase()) ? `${candidate} already exists in ${dir}/.` : "";
1916
- };
1869
+ const check: NewFileNameCheck = { choice, dir, known: knownDocumentExtensions(), rows, taken };
1870
+ const validate = (value: string, picked: string) => validateNewFileName(value, picked, check);
1917
1871
 
1918
1872
  let picked = initialPick;
1919
1873
  const promptOptions: PromptDialogOptions = {
@@ -1942,13 +1896,10 @@ export async function createFileIn(request: NewFileRequest): Promise<string | nu
1942
1896
  return null;
1943
1897
  }
1944
1898
 
1945
- const fileName = fileNameFor(entered, picked);
1899
+ const fileName = composeNewFileName(entered, picked, choice);
1946
1900
  const path = dir === "." ? fileName : `${dir}/${fileName}`;
1947
- markLocalMutation(path);
1948
1901
  const format = formatForPath(fileName);
1949
- const suppliedBody = isFunction(request.content)
1950
- ? await request.content(extensionOfName(fileName), fileName)
1951
- : request.content;
1902
+ const suppliedBody = request.content;
1952
1903
  /*
1953
1904
  * What a new file starts as, in precedence order.
1954
1905
  *
@@ -1968,16 +1919,59 @@ export async function createFileIn(request: NewFileRequest): Promise<string | nu
1968
1919
  ? BLANK_DOCUMENT
1969
1920
  : "{}\n"
1970
1921
  : "");
1922
+ return writeNewFile(path, content, { dir, source });
1923
+ }
1924
+
1925
+ /**
1926
+ * The names already in `dir`, lower-cased — what a new name there must not collide with.
1927
+ *
1928
+ * A directory that cannot be listed answers empty: it is usually one that does not exist yet, and
1929
+ * the write that follows is the authority on whether that is a problem, reporting with the real
1930
+ * reason.
1931
+ */
1932
+ export async function listTakenNames(dir: string): Promise<Set<string>> {
1933
+ try {
1934
+ const listing = await getPlatform().listDirectory(dir);
1935
+ return new Set(listing.map((entry) => entry.name.toLowerCase()));
1936
+ } catch {
1937
+ return new Set();
1938
+ }
1939
+ }
1940
+
1941
+ /**
1942
+ * Write one new file: the second half of every creation flow, after its name has been judged.
1943
+ *
1944
+ * The local mutation is marked first, so the watcher's echo of this write is not mistaken for an
1945
+ * edit made outside Studio. The destination is re-listed so the tree shows the file, the success is
1946
+ * a toast, and a failure is a Problem carrying the path, since the thing the author must do next is
1947
+ * about that path. It does NOT open the file: what a new file opens in is the caller's to say.
1948
+ *
1949
+ * @param path Project-relative path of the file to create.
1950
+ * @param content Its first bytes.
1951
+ * @param opts `dir` is re-listed afterwards (defaults to the path's parent); `source` names the
1952
+ * flow in the Problem; `quiet` skips the success toast for a write that is one step of a larger
1953
+ * creation that reports once for all of them.
1954
+ * @returns The path, or `null` when the write failed.
1955
+ */
1956
+ export async function writeNewFile(
1957
+ path: string,
1958
+ content: string,
1959
+ opts: { dir?: string; source?: string; quiet?: boolean } = {},
1960
+ ): Promise<string | null> {
1961
+ const dir = opts.dir ?? (path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : ".");
1962
+ markLocalMutation(path);
1971
1963
  try {
1972
1964
  await getPlatform().writeFile(path, content);
1973
1965
  await loadDirectory(dir);
1974
- notify.success(`Created ${path}`);
1966
+ if (opts.quiet !== true) {
1967
+ notify.success(`Created ${path}`);
1968
+ }
1975
1969
  return path;
1976
1970
  } catch (error) {
1977
1971
  notify.error(`Could not create ${path}.`, {
1978
1972
  detail: errorMessage(error),
1979
1973
  path,
1980
- source,
1974
+ source: opts.source ?? "Files",
1981
1975
  });
1982
1976
  return null;
1983
1977
  }
@@ -2241,6 +2235,13 @@ export interface OpenFileOpts {
2241
2235
  preview?: boolean;
2242
2236
  /** False leaves the keyboard where it is. Defaults to true. */
2243
2237
  focus?: boolean;
2238
+ /**
2239
+ * The canvas mode the tab is in when it becomes active, for an open that is not an edit: a
2240
+ * comparison opens straight into `git-diff`. Set BEFORE activation, because a tab's mode decides
2241
+ * which workspace it belongs to (`workspace/tab-workspace.ts`) and activation enters that
2242
+ * workspace; a mode set afterwards would visit the edit mode's workspace on the way.
2243
+ */
2244
+ canvasMode?: string;
2244
2245
  }
2245
2246
 
2246
2247
  /**
@@ -2293,6 +2294,9 @@ export async function openFileInTab(path: string, opts: OpenFileOpts = {}) {
2293
2294
  const follows = opts.focus !== false;
2294
2295
  for (const [id, tab] of workspace.tabs.entries()) {
2295
2296
  if (tab.documentPath === path) {
2297
+ if (opts.canvasMode !== undefined) {
2298
+ tab.session.ui.canvasMode = opts.canvasMode;
2299
+ }
2296
2300
  revealOpenTab(id, opts);
2297
2301
  // The tree's cursor answers "where is the author", so a side-open that deliberately left the
2298
2302
  // Keyboard behind must not move it.
@@ -2392,10 +2396,12 @@ export async function openFileInTab(path: string, opts: OpenFileOpts = {}) {
2392
2396
  document,
2393
2397
  ...(frontmatter != null && { frontmatter }),
2394
2398
  layout,
2399
+ source: format ? content : null,
2395
2400
  sourceFormat: format?.name ?? null,
2396
2401
  ...(opts.paneId !== undefined && { paneId: opts.paneId }),
2397
2402
  ...(opts.preview === true && { preview: true }),
2398
2403
  ...(opts.focus === false && { focus: false }),
2404
+ ...(opts.canvasMode !== undefined && { canvasMode: opts.canvasMode }),
2399
2405
  });
2400
2406
  if (follows) {
2401
2407
  requireProjectState().selectedPath = path;
@@ -2508,16 +2514,21 @@ export async function reloadFileInTab(path: string, opts: { onlyIfChanged?: bool
2508
2514
  }
2509
2515
  tab.doc.document = document;
2510
2516
  tab.doc.content.frontmatter = frontmatter;
2517
+ tab.doc.source = content;
2511
2518
  } else if (path.endsWith(".json")) {
2512
2519
  // The file changed under the tab, so its layout is whatever the new text says.
2513
2520
  const parsed = parseJsonDocument(content);
2514
2521
  tab.doc.layout = parsed.layout;
2515
- if (opts.onlyIfChanged && deepEqual(parsed.document, tab.doc.document)) {
2516
- return;
2517
- }
2518
2522
  /* SAFETY: this tab was opened on this `.json` path as a Jx document, and the file's schema
2519
2523
  Makes its top-level object a node — the object `parseJsonDocument` states it returns. */
2520
- tab.doc.document = parsed.document as JxMutableNode;
2524
+ const document = parsed.document as JxMutableNode;
2525
+ // The same member form the tab was opened in, so the comparison and every edit after it
2526
+ // Read one shape, and the record of what the file wrote is the new text's.
2527
+ tab.doc.bareRepeaters = normalizeDocument(document);
2528
+ if (opts.onlyIfChanged && deepEqual(document, tab.doc.document)) {
2529
+ return;
2530
+ }
2531
+ tab.doc.document = document;
2521
2532
  }
2522
2533
  tab.doc.dirty = false;
2523
2534
  } catch (error) {
@@ -2548,6 +2559,10 @@ export function registerFilesPanel(): void {
2548
2559
  level: "project",
2549
2560
  dock: "navigator",
2550
2561
  icon: "folder",
2562
+ /* A developer panel (§5.1): the Site panel lists the project by what its files ARE, so the tree
2563
+ of where they are stored is on the Design rail only with **Show developer panels** on. With it
2564
+ off the panel is still `view.setActivity { tab: "files" }` and a palette row away. */
2565
+ rail: "developer",
2551
2566
  // The body is a document, so lit draws nothing and the mount happens against the painted DOM.
2552
2567
  // `afterRender` runs on every repaint; {@link mountFilesPanel} is idempotent.
2553
2568
  render: () => nothing,
@@ -2603,16 +2618,15 @@ let _lastValues: FilesPanelValues | null = null;
2603
2618
  * Draw the panel — mounting the document the first time, and re-projecting into it every time
2604
2619
  * after.
2605
2620
  *
2606
- * The document goes into `.panel-content`, not into the `.panel-body` this is handed, for the
2607
- * reason `panels/git-panel.ts` states: only one of them is the node lit renders this panel's body
2608
- * into, and appending to the other would leave the panel drawn under whatever the Navigator paints
2609
- * next.
2621
+ * The document goes into `.panel-content`, not into the `.panel-body` this is handed, because only
2622
+ * one of them is the node lit renders this panel's body into, and appending to the other would
2623
+ * leave the panel drawn under whatever the Navigator paints next.
2610
2624
  *
2611
- * The repaint stays the NAVIGATOR'S. Source Control owns an `effect()` because its inputs are the
2612
- * reactive `shell.git` record; the tree's inputs are a `Map` of directory listings, a `Set` of
2613
- * expanded paths and a roaming setting, none of which is reactive — so `ctx.rerender` is what this
2614
- * surface has always been redrawn by, and it stays that way rather than growing a second scheduler
2615
- * that could disagree with it.
2625
+ * The repaint stays the NAVIGATOR'S. The Publish workspace owns an `effect()` because its inputs
2626
+ * are the reactive `shell.git` record; the tree's inputs are a `Map` of directory listings, a `Set`
2627
+ * of expanded paths and a roaming setting, none of which is reactive — so `ctx.rerender` is what
2628
+ * this surface has always been redrawn by, and it stays that way rather than growing a second
2629
+ * scheduler that could disagree with it.
2616
2630
  *
2617
2631
  * @param host - The painted `.panel-body`
2618
2632
  * @param rerender - The Navigator's repaint
@@ -2647,7 +2661,7 @@ export function mountFilesPanel(host: HTMLElement, rerender: () => void): void {
2647
2661
  * Take the panel down: the document, the drag registrations, the scroll watch and the row handles.
2648
2662
  *
2649
2663
  * Exported because a project close has to reach it — the tree it is showing belongs to the project
2650
- * being left behind, exactly as `cleanupGitPanel` is for the working tree.
2664
+ * being left behind, exactly as `cleanupSourceControl` is for the working tree.
2651
2665
  */
2652
2666
  export function unmountFilesPanel(): void {
2653
2667
  dismissFileContextMenu();
@@ -237,11 +237,11 @@ export async function relistLoadedDirs(
237
237
  }
238
238
 
239
239
  /**
240
- * Modes only a tab whose `document` is a placeholder offers: the media viewer, a CSV grid, the
241
- * Library. Spelled out as strings, not imported, because each constant's module drags its whole
242
- * editor in behind it.
240
+ * Modes only a tab whose `document` is a placeholder offers: the media viewer and a CSV grid.
241
+ * Spelled out as strings, not imported, because each constant's module drags its whole editor in
242
+ * behind it. A workspace home has no file at all, so it never reaches the re-read.
243
243
  */
244
- const STUB_DOCUMENT_MODES = new Set(["media", "grid", "manage"]);
244
+ const STUB_DOCUMENT_MODES = new Set(["media", "grid"]);
245
245
 
246
246
  /**
247
247
  * Whether a tab's `document` is a placeholder rather than its parsed file. Such a tab reads its
@@ -293,8 +293,16 @@ export interface FsSyncContext {
293
293
  * and this module is imported BY the file layer those panels sit on top of — reaching up would be
294
294
  * a cycle. The bootstrap owns the list because the bootstrap is where the panels are already in
295
295
  * scope.
296
+ *
297
+ * Handed the project-relative paths the batch touched, so a cache that only one directory can
298
+ * change (the media listing, the component registry) can ignore the rest. `undefined` means the
299
+ * stream had a gap and any path may have changed.
300
+ *
301
+ * `structural` says whether the batch added, removed or renamed anything, as opposed to only
302
+ * rewriting files that already existed: a cache of the file SET can ignore a batch of plain
303
+ * `change` events, which is every save. It is `true` after a gap.
296
304
  */
297
- invalidateDerivedCaches?: () => void;
305
+ invalidateDerivedCaches?: (paths?: readonly string[], structural?: boolean) => void;
298
306
  /**
299
307
  * After a reconnect, the clean, non-collab open tabs whose files may have changed while the
300
308
  * stream was down. The handler re-reads them; it should leave a tab whose content did not change
@@ -363,7 +371,7 @@ export function startFsSync(ctx: FsSyncContext): () => void {
363
371
  );
364
372
  if (reason === "reconnect") {
365
373
  invalidateUsages();
366
- ctx.invalidateDerivedCaches?.();
374
+ ctx.invalidateDerivedCaches?.(undefined, true);
367
375
  await Promise.all([relist, reloadIgnoreCache()]);
368
376
  } else {
369
377
  await relist;
@@ -413,7 +421,10 @@ export function startFsSync(ctx: FsSyncContext): () => void {
413
421
  // Removed layout's `$head` to the open page, a `$paths` enumeration listing entries that are
414
422
  // Gone.
415
423
  invalidateUsages();
416
- ctx.invalidateDerivedCaches?.();
424
+ ctx.invalidateDerivedCaches?.(
425
+ events.map((event) => event.path),
426
+ events.some((event) => event.type !== "change"),
427
+ );
417
428
  // Content versions are derived state of the same kind, and Studio's own writes change bytes:
418
429
  // A version kept past its echo would let an immutable cache answer with the old file.
419
430
  forgetEventVersions(events);
@@ -11,6 +11,12 @@
11
11
  * renders anyway. {@link recordImageSize} is how that surface hands the number back, and until
12
12
  * one does, the dimensions are `null` and the summary simply omits them. Downloading the file a
13
13
  * second time to measure it would be a network request per thumbnail for a line of text.
14
+ * - **An SVG's dimensions are its own**, read from the file: `width`/`height` on the root element, or
15
+ * its `viewBox` when either is missing ({@link svgIntrinsicSize}). A vector image has no pixels
16
+ * to decode, and an `<img>` of one that declares no size reports the replaced element's default
17
+ * object size (150 × 150 in Chromium), a number about the browser rather than the file. So
18
+ * {@link recordImageSize} ignores SVGs, and {@link loadMediaMeta} reads the text instead, which
19
+ * for a vector is a few hundred bytes the thumbnail already fetched.
14
20
  *
15
21
  * A missing number is `null` everywhere, never `0`. `0 × 0` and `0 B` are things a real file can
16
22
  * almost be, so they must not double as "we did not find out" — the same rule
@@ -63,6 +69,81 @@ const inFlight = new Map<string, Promise<MediaMeta>>();
63
69
  */
64
70
  const measured = new Map<string, { width: number; height: number }>();
65
71
 
72
+ /**
73
+ * SVG paths whose own size has been read (or found unreadable), so {@link loadMediaMeta} reads each
74
+ * file once rather than on every paint that finds no dimensions: most SVGs declare none at all.
75
+ */
76
+ const svgRead = new Set<string>();
77
+
78
+ function isSvg(path: string): boolean {
79
+ return extensionOf(path) === ".svg";
80
+ }
81
+
82
+ /** A `width`/`height` attribute in user units (`64`, `64px`), or null for anything relative. */
83
+ function svgLength(value: string | null): number | null {
84
+ const match = value === null ? null : /^\s*(\d+(?:\.\d+)?)\s*(?:px)?\s*$/.exec(value);
85
+ const length = match ? Number(match[1]) : Number.NaN;
86
+ return length > 0 ? length : null;
87
+ }
88
+
89
+ /**
90
+ * The size an SVG document declares for itself, or null when it declares none (the intrinsic
91
+ * dimensions of an outermost `<svg>` element, SVG 2 §8.2): `width` and `height` where both are
92
+ * absolute lengths; one of them with the `viewBox`'s aspect ratio supplying the other; or the
93
+ * `viewBox`'s own width and height when neither is given. A percentage or a font-relative length is
94
+ * a size relative to whatever embeds the file, which a file on its own does not have.
95
+ */
96
+ export function svgIntrinsicSize(text: string): { width: number; height: number } | null {
97
+ const root = new DOMParser().parseFromString(text, "image/svg+xml").documentElement;
98
+ if (root.localName !== "svg") {
99
+ return null;
100
+ }
101
+ const width = svgLength(root.getAttribute("width"));
102
+ const height = svgLength(root.getAttribute("height"));
103
+ const box = (root.getAttribute("viewBox") ?? "")
104
+ .trim()
105
+ .split(/[\s,]+/)
106
+ .map(Number);
107
+ const boxWidth = box[2] ?? Number.NaN;
108
+ const boxHeight = box[3] ?? Number.NaN;
109
+ const hasBox = box.length === 4 && boxWidth > 0 && boxHeight > 0;
110
+ let size: { width: number; height: number } | null = null;
111
+ if (width !== null && height !== null) {
112
+ size = { height, width };
113
+ } else if (hasBox && width !== null) {
114
+ size = { height: (width * boxHeight) / boxWidth, width };
115
+ } else if (hasBox && height !== null) {
116
+ size = { height, width: (height * boxWidth) / boxHeight };
117
+ } else if (hasBox) {
118
+ size = { height: boxHeight, width: boxWidth };
119
+ }
120
+ return size && { height: Math.round(size.height), width: Math.round(size.width) };
121
+ }
122
+
123
+ /** Read an SVG's declared size into {@link measured}, once per path. */
124
+ async function readSvgSize(path: string): Promise<void> {
125
+ svgRead.add(path);
126
+ try {
127
+ const size = svgIntrinsicSize(await getPlatform().readFile(path));
128
+ if (size && size.width > 0 && size.height > 0) {
129
+ measured.set(path, size);
130
+ }
131
+ } catch {
132
+ // Unreadable: the dimensions stay unknown, which the summary already says by omitting them.
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Whether {@link loadMediaMeta} has nothing left to learn about a file: its record is settled and,
138
+ * for an SVG, its own declared size has been read. A surface asks this, rather than whether
139
+ * {@link peekMediaMeta} is null, before starting a load: a listing seeds every SVG's record without
140
+ * the dimensions only the file itself can give.
141
+ */
142
+ export function mediaMetaSettled(path: string): boolean {
143
+ const key = normalizeProjectPath(path);
144
+ return settled.has(key) && (!isSvg(key) || svgRead.has(key));
145
+ }
146
+
66
147
  /** Build a meta record from a path and whatever the listing knew about it. */
67
148
  function metaFrom(path: string, entry: DirEntry | undefined): MediaMeta {
68
149
  const name = baseName(path);
@@ -118,7 +199,7 @@ export function seedMediaMeta(entries: Iterable<DirEntry>): void {
118
199
  export async function loadMediaMeta(path: string): Promise<MediaMeta> {
119
200
  const key = normalizeProjectPath(path);
120
201
  const done = settled.get(key);
121
- if (done) {
202
+ if (done && mediaMetaSettled(key)) {
122
203
  return done;
123
204
  }
124
205
  const running = inFlight.get(key);
@@ -126,6 +207,14 @@ export async function loadMediaMeta(path: string): Promise<MediaMeta> {
126
207
  return running;
127
208
  }
128
209
  const pending = (async (): Promise<MediaMeta> => {
210
+ if (isSvg(key) && !svgRead.has(key)) {
211
+ await readSvgSize(key);
212
+ }
213
+ if (done) {
214
+ // Seeded by a listing already: only the SVG's own size was missing.
215
+ const dims = measured.get(key);
216
+ return { ...done, height: dims?.height ?? null, width: dims?.width ?? null };
217
+ }
129
218
  let entry: DirEntry | undefined;
130
219
  try {
131
220
  const listing = await notedListing(() => getPlatform().listDirectory(dirName(key)));
@@ -154,6 +243,10 @@ export function recordImageSize(path: string, width: number, height: number): bo
154
243
  return false; // A broken or still-decoding image reports 0 — that is not a measurement.
155
244
  }
156
245
  const key = normalizeProjectPath(path);
246
+ if (isSvg(key)) {
247
+ // The decode of a vector measures the browser's default object size, not the file.
248
+ return false;
249
+ }
157
250
  const known = measured.get(key);
158
251
  if (known && known.width === width && known.height === height) {
159
252
  return false;
@@ -170,6 +263,7 @@ export function recordImageSize(path: string, width: number, height: number): bo
170
263
  export function invalidateMediaMeta(): void {
171
264
  settled.clear();
172
265
  measured.clear();
266
+ svgRead.clear();
173
267
  }
174
268
 
175
269
  /** `84 KB`, `1.2 MB`, `903 B` — one significant fraction, because this is a caption. */