@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
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Which files are project media, by extension — the classification alone, with nothing imported.
3
+ *
4
+ * `files/media-upload.ts` owns uploading and re-exports these; they live here so a module that only
5
+ * needs to ask "is this file media" (the collection matcher behind the Content workspace, which the
6
+ * canvas resolver reads too) does not pull the upload core, the platform and the reference index in
7
+ * behind the question.
8
+ *
9
+ * @docs studio/projects/media
10
+ */
11
+
12
+ /** Extensions rendered as a thumbnail (media picker, cards, file-tree icons). */
13
+ export const IMAGE_EXTENSIONS = new Set([
14
+ ".jpg",
15
+ ".jpeg",
16
+ ".png",
17
+ ".gif",
18
+ ".svg",
19
+ ".webp",
20
+ ".avif",
21
+ ".ico",
22
+ ]);
23
+
24
+ /** Extensions an upload places as a `<video>`. */
25
+ export const VIDEO_EXTENSIONS = new Set([".mp4", ".webm", ".mov", ".m4v"]);
26
+
27
+ /** Extensions an upload places as an `<audio>`. */
28
+ export const AUDIO_EXTENSIONS = new Set([".mp3", ".wav", ".ogg", ".m4a", ".flac"]);
29
+
30
+ /**
31
+ * Every extension Studio treats as project media: a superset of {@link IMAGE_EXTENSIONS} and of
32
+ * every extension classified as video or audio, so a file it classifies is one the media listing
33
+ * shows.
34
+ */
35
+ export const MEDIA_EXTENSIONS = new Set([
36
+ ...IMAGE_EXTENSIONS,
37
+ ...VIDEO_EXTENSIONS,
38
+ ...AUDIO_EXTENSIONS,
39
+ ".pdf",
40
+ ".woff",
41
+ ".woff2",
42
+ ".ttf",
43
+ ".otf",
44
+ ]);
45
+
46
+ /** Lowercased extension including the dot, or `""` when the name carries none. */
47
+ export function extensionOf(name: string): string {
48
+ const dot = name.lastIndexOf(".");
49
+ return dot > 0 ? name.slice(dot).toLowerCase() : "";
50
+ }
51
+
52
+ /** Whether a file extension (with or without the leading dot) is a previewable image. */
53
+ export function isImage(ext: string): boolean {
54
+ return IMAGE_EXTENSIONS.has(ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`);
55
+ }
@@ -3,15 +3,18 @@
3
3
  * Media upload core — the single path every upload surface goes through.
4
4
  *
5
5
  * Four surfaces feed it: the image field's Upload button (ui/media-picker), a file dropped on the
6
- * canvas (editor/file-drop-action), a file dropped on the Files tree (files/files), and the Manage
7
- * view's drop zone / Upload button (browse/browse). They differ only in how the destination
8
- * directory is chosen; naming, collision handling, the platform call, and cache invalidation are
9
- * shared here.
6
+ * canvas (editor/file-drop-action), a file dropped on the Files tree (files/files), and the Media
7
+ * workspace's drop zone and Upload button (media/media-library, and Add new › Media). They differ
8
+ * only in how the destination directory is chosen; naming, collision handling, the platform call,
9
+ * and cache invalidation are shared here.
10
10
  *
11
- * Destination (specs/site-architecture.md §9.1/§9.3): a document inside a content collection keeps
12
- * its media co-located in `content/<collection>/images/` — the entry references it file-relatively
13
- * and the collection loader rewrites it to the asset mount. Everything else goes to `public/`,
14
- * which is served from the site root.
11
+ * Destination (specs/site-architecture.md §9.1/§9.4, studio.md §9.3): a document inside a content
12
+ * collection keeps its media co-located in `content/<collection>/images/` — the entry references it
13
+ * file-relatively and the collection loader rewrites it to the asset mount. Everything else goes to
14
+ * {@link DEFAULT_MEDIA_DIR}, `public/media/`, which is served from the site root as `/media/`.
15
+ *
16
+ * {@link replaceAsset} is the one door that writes over an existing file, and it is asked for by
17
+ * name: the Media workspace's **Replace** (studio.md §9.3).
15
18
  *
16
19
  * @docs studio/projects/media
17
20
  */
@@ -19,37 +22,22 @@
19
22
  import { errorMessage } from "@jxsuite/schema/parse";
20
23
  import { getPlatform, hasPlatform } from "../platform";
21
24
  import { notify } from "../services/notify";
25
+ import { invalidateUsages } from "../services/references";
22
26
  import { activeTab } from "../workspace/workspace";
23
27
  import { forgetVersions, notedListing } from "./asset-versions";
28
+ import {
29
+ AUDIO_EXTENSIONS,
30
+ IMAGE_EXTENSIONS,
31
+ MEDIA_EXTENSIONS,
32
+ VIDEO_EXTENSIONS,
33
+ extensionOf,
34
+ } from "./media-types";
24
35
 
25
36
  // ─── File classification ─────────────────────────────────────────────────────
26
37
 
27
- /** Extensions rendered as a thumbnail (media picker, Manage cards, file-tree icons). */
28
- export const IMAGE_EXTENSIONS = new Set([
29
- ".jpg",
30
- ".jpeg",
31
- ".png",
32
- ".gif",
33
- ".svg",
34
- ".webp",
35
- ".avif",
36
- ".ico",
37
- ]);
38
-
39
- /** Every extension Studio treats as project media. Superset of {@link IMAGE_EXTENSIONS}. */
40
- export const MEDIA_EXTENSIONS = new Set([
41
- ...IMAGE_EXTENSIONS,
42
- ".mp4",
43
- ".webm",
44
- ".mp3",
45
- ".wav",
46
- ".ogg",
47
- ".pdf",
48
- ".woff",
49
- ".woff2",
50
- ".ttf",
51
- ".otf",
52
- ]);
38
+ /* The classification lives in `media-types.ts`, which imports nothing; it is re-exported here so
39
+ every existing caller of the upload core keeps one import. */
40
+ export { IMAGE_EXTENSIONS, MEDIA_EXTENSIONS, extensionOf, isImage } from "./media-types";
53
41
 
54
42
  /** A byte count as a person reads it, for a refusal that names the limit. */
55
43
  function formatBytes(bytes: number): string {
@@ -104,25 +92,11 @@ export interface UploadedAsset {
104
92
  kind: AssetKind;
105
93
  }
106
94
 
107
- /** Lowercased extension including the dot, or `""` when the name carries none. */
108
- export function extensionOf(name: string): string {
109
- const dot = name.lastIndexOf(".");
110
- return dot > 0 ? name.slice(dot).toLowerCase() : "";
111
- }
112
-
113
- /** Whether a file extension (with or without the leading dot) is a previewable image. */
114
- export function isImage(ext: string): boolean {
115
- return IMAGE_EXTENSIONS.has(ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`);
116
- }
117
-
118
95
  /** Whether a filename is project media. */
119
96
  export function isMediaFile(name: string): boolean {
120
97
  return MEDIA_EXTENSIONS.has(extensionOf(name));
121
98
  }
122
99
 
123
- const VIDEO_EXTENSIONS = new Set([".mp4", ".webm", ".mov", ".m4v"]);
124
- const AUDIO_EXTENSIONS = new Set([".mp3", ".wav", ".ogg", ".m4a", ".flac"]);
125
-
126
100
  /**
127
101
  * Classify a file by MIME type, falling back to its extension. A drop from the OS usually carries a
128
102
  * `type`, but it is empty for unrecognized extensions and for some Linux file managers.
@@ -151,18 +125,59 @@ export function mediaKind(file: { name: string; type?: string }): AssetKind {
151
125
  return "file";
152
126
  }
153
127
 
128
+ /** The font extensions, which a browser shows as a specimen rather than as a file. */
129
+ export const FONT_EXTENSIONS: ReadonlySet<string> = new Set([".woff", ".woff2", ".ttf", ".otf"]);
130
+
131
+ /**
132
+ * What a media file IS, as the Media workspace's filters name it: one of five kinds.
133
+ *
134
+ * Distinct from {@link AssetKind}, which answers "what element does this become" and so has no word
135
+ * for a font or a PDF (both become an `<a href>`). Decided by extension alone, because a file
136
+ * already on disk carries no MIME type: a font first, then the three a browser has an element for,
137
+ * and everything else (a PDF) is a document.
138
+ */
139
+ export type MediaFileKind = "image" | "video" | "audio" | "font" | "document";
140
+
141
+ /** The {@link MediaFileKind} of a file, by its name. */
142
+ export function mediaFileKind(name: string): MediaFileKind {
143
+ if (FONT_EXTENSIONS.has(extensionOf(name))) {
144
+ return "font";
145
+ }
146
+ const kind = mediaKind({ name });
147
+ return kind === "file" ? "document" : kind;
148
+ }
149
+
154
150
  // ─── Destination resolution ──────────────────────────────────────────────────
155
151
 
156
152
  /** Matches a path inside a content collection: `content/<collection>/…`. */
157
153
  const CONTENT_ENTRY = /^content\/([^/]+)\//;
158
154
 
155
+ /**
156
+ * Where media with no context lands: `public/media/`, referenced from the site root as `/media/…`.
157
+ *
158
+ * A default and not a rule (site-architecture.md §9.4): a file under any other `public/` path is
159
+ * published and listed exactly the same. Its one definition, so the Media workspace, Add new ›
160
+ * Media and the Library's All view cannot disagree about it.
161
+ */
162
+ export const DEFAULT_MEDIA_DIR = "public/media";
163
+
159
164
  /**
160
165
  * The directory an upload lands in when the caller doesn't name one. A document inside a content
161
- * collection gets `content/<collection>/images`; everything else gets `public`.
166
+ * collection gets `content/<collection>/images`; everything else gets {@link DEFAULT_MEDIA_DIR}.
162
167
  */
163
168
  export function uploadDirFor(documentPath: string | null): string {
164
169
  const match = documentPath ? CONTENT_ENTRY.exec(documentPath.replaceAll("\\", "/")) : null;
165
- return match ? `content/${match[1]}/images` : "public";
170
+ return match ? `content/${match[1]}/images` : DEFAULT_MEDIA_DIR;
171
+ }
172
+
173
+ /**
174
+ * Whether `dir` is a folder {@link uploadDirFor} answers: `public/media`, or a collection's
175
+ * `content/<collection>/images`. An upload creates these on its own, so they are the folders a
176
+ * media delete takes away again once it has emptied them (`media/media-library.ts`).
177
+ */
178
+ export function isUploadDestination(dir: string): boolean {
179
+ const normalized = dir.replaceAll("\\", "/");
180
+ return normalized === DEFAULT_MEDIA_DIR || /^content\/[^/]+\/images$/.test(normalized);
166
181
  }
167
182
 
168
183
  /**
@@ -223,8 +238,61 @@ export function setMediaChangedHandler(fn: ((dir: string) => void | Promise<void
223
238
  mediaChangedHandler = fn;
224
239
  }
225
240
 
241
+ /**
242
+ * Say that the media in `dir` changed by some door other than {@link uploadAssets} — a replace, or
243
+ * the Media workspace's delete — so every cache keyed on the listing is dropped the same way.
244
+ */
245
+ export async function announceMediaChanged(dir: string): Promise<void> {
246
+ await mediaChangedHandler?.(dir);
247
+ }
248
+
249
+ /** The directory part of a project path, `.` for a file at the root. */
250
+ function parentDir(path: string): string {
251
+ const normalized = path.replaceAll("\\", "/");
252
+ const slash = normalized.lastIndexOf("/");
253
+ return slash > 0 ? normalized.slice(0, slash) : ".";
254
+ }
255
+
226
256
  // ─── Upload ──────────────────────────────────────────────────────────────────
227
257
 
258
+ /**
259
+ * What a media surface may upload: any file the media listing shows (`media`), or only pictures
260
+ * (`image`, a picker for a field that holds one).
261
+ *
262
+ * A file input's `accept` narrows only the chooser; a drop carries whatever the OS hands over, and
263
+ * `image/*` admits types (`.heic`, `.bmp`) no listing names. A surface that selects what it just
264
+ * uploaded must not upload a file it will then fail to show.
265
+ */
266
+ export type UploadOnly = "media" | "image";
267
+
268
+ /** The files `only` admits; the rest are named in one warning rather than written unseen. */
269
+ function keepListable(files: File[], only: UploadOnly): File[] {
270
+ const admits = (file: File): boolean =>
271
+ only === "image" ? IMAGE_EXTENSIONS.has(extensionOf(file.name)) : isMediaFile(file.name);
272
+ const kept: File[] = [];
273
+ const skipped: string[] = [];
274
+ for (const file of files) {
275
+ if (admits(file)) {
276
+ kept.push(file);
277
+ } else {
278
+ skipped.push(file.name);
279
+ }
280
+ }
281
+ if (skipped.length > 0) {
282
+ notify.warn(
283
+ skipped.length === 1 ? `Skipped ${skipped[0]}` : `Skipped ${skipped.length} files`,
284
+ {
285
+ detail:
286
+ only === "image"
287
+ ? `Only images can be used here: ${skipped.join(", ")}.`
288
+ : `Studio lists images, video, audio, PDFs and fonts as media, so ${skipped.join(", ")} would not appear in it.`,
289
+ source: "Media",
290
+ },
291
+ );
292
+ }
293
+ return kept;
294
+ }
295
+
228
296
  /**
229
297
  * Upload files into the project and report what landed.
230
298
  *
@@ -235,9 +303,9 @@ export function setMediaChangedHandler(fn: ((dir: string) => void | Promise<void
235
303
  */
236
304
  export async function uploadAssets(
237
305
  files: Iterable<File>,
238
- opts: { dir?: string } = {},
306
+ opts: { dir?: string; only?: UploadOnly } = {},
239
307
  ): Promise<UploadedAsset[]> {
240
- const list = [...files];
308
+ const list = opts.only === undefined ? [...files] : keepListable([...files], opts.only);
241
309
  if (list.length === 0) {
242
310
  return [];
243
311
  }
@@ -297,3 +365,92 @@ export async function uploadAssets(
297
365
  }
298
366
  return uploaded;
299
367
  }
368
+
369
+ /** Extensions that name one format, so either may stand in for the other. */
370
+ const SAME_FORMAT: readonly ReadonlySet<string>[] = [
371
+ new Set([".jpg", ".jpeg"]),
372
+ new Set([".tif", ".tiff"]),
373
+ ];
374
+
375
+ /** Every extension that names the same format as `ext` (itself included). */
376
+ function formatSiblings(ext: string): ReadonlySet<string> {
377
+ return SAME_FORMAT.find((group) => group.has(ext)) ?? new Set([ext]);
378
+ }
379
+
380
+ /**
381
+ * The `accept` for a Replace of the file at `path`: its own format, and only that.
382
+ *
383
+ * The path is what every document names, and the server answers it with the type its EXTENSION
384
+ * implies, so bytes of another format under it are a broken picture on every page that uses it.
385
+ */
386
+ export function replaceAccept(path: string): string {
387
+ return [...formatSiblings(extensionOf(path))].join(",");
388
+ }
389
+
390
+ /** Whether `file` may replace the file at `path`: the same format, by extension. */
391
+ export function canReplaceWith(path: string, file: { name: string }): boolean {
392
+ return formatSiblings(extensionOf(path)).has(extensionOf(file.name));
393
+ }
394
+
395
+ /**
396
+ * Write `file`'s bytes over the existing media file at `path` — the Media workspace's **Replace**.
397
+ *
398
+ * The only overwrite in this module, and asked for by name (studio.md §9.3): every reference to
399
+ * `path` keeps resolving and now shows the new bytes. Nothing about the path tells a cache that the
400
+ * file changed, so this forgets the path's content version (§3.4) and announces the change, which
401
+ * drops the media listing, its metadata and the usage answers derived from it.
402
+ *
403
+ * A backend that reports a DIFFERENT path did not replace anything: it wrote a second file beside
404
+ * the first, and the references still name the old bytes. That is said rather than called success.
405
+ *
406
+ * @returns Whether the bytes now at `path` are the new ones.
407
+ */
408
+ export async function replaceAsset(path: string, file: File): Promise<boolean> {
409
+ const name = path.split("/").pop() ?? path;
410
+ /* An `accept` narrows only the chooser (and a backend's own picker may ignore it), so the format is
411
+ checked here too: same path, other format, is every reference broken at once. */
412
+ if (!canReplaceWith(path, file)) {
413
+ notify.error(`${file.name} cannot replace ${name}.`, {
414
+ detail: `${name} is ${extensionOf(path) || "a file with no extension"} and ${file.name} is ${extensionOf(file.name) || "a file with no extension"}. Every page that uses ${name} would get bytes its address does not describe; choose a file of the same type, or upload it as a new file.`,
415
+ path,
416
+ source: "Media",
417
+ });
418
+ return false;
419
+ }
420
+ const maxBytes = hasPlatform() ? getPlatform().assetCapabilities?.maxUploadBytes : undefined;
421
+ if (maxBytes !== undefined && file.size > maxBytes) {
422
+ notify.error(`${file.name} is too large.`, {
423
+ detail: `This backend accepts at most ${formatBytes(maxBytes)}; that file is ${formatBytes(file.size)}.`,
424
+ source: "Media",
425
+ });
426
+ return false;
427
+ }
428
+ let written: string;
429
+ try {
430
+ ({ path: written } = await getPlatform().uploadFile(path, file));
431
+ } catch (error) {
432
+ notify.error(`Could not replace ${name}.`, {
433
+ detail: errorMessage(error),
434
+ path,
435
+ source: "Media",
436
+ });
437
+ return false;
438
+ }
439
+ forgetVersions([path, written]);
440
+ // The answers were about the old bytes' place in the project; the place is the same, but a stale
441
+ // "Used on" is a list the reader acts on, so it is asked again rather than trusted.
442
+ invalidateUsages();
443
+ await announceMediaChanged(parentDir(path));
444
+ if (written !== path) {
445
+ notify.warn(`${name} was not replaced.`, {
446
+ detail: `The backend stored the new file as ${written}, so every reference still shows the old one.`,
447
+ path,
448
+ source: "Media",
449
+ // A Problem rather than a toast: two files now exist, and the reader has a decision to make.
450
+ tier: "problem",
451
+ });
452
+ return false;
453
+ }
454
+ notify.success(`Replaced ${name}`);
455
+ return true;
456
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The rules a new file's name is held to, with no dialog and no write attached.
3
+ *
4
+ * `createFileIn` (`files/files.ts`) used to keep these inside one closure over its prompt, which
5
+ * was right while the prompt was the only creation flow there was. Add new (`create/add-new.ts`,
6
+ * `specs/studio.md` §9.1.1) composes a name the same way and must refuse the same things — a blank
7
+ * name, a name that escapes the destination, a name that is already taken — and a second copy of
8
+ * those rules would disagree with the first the first time either was fixed. So the composing and
9
+ * the judging live here, DOM-free, and both flows call them; only the write, and what each flow
10
+ * does around it, differs.
11
+ *
12
+ * Nothing here lists a directory. The caller supplies the `taken` names it listed, because when
13
+ * that listing happens is the caller's concern: before the prompt opens for New File, and once per
14
+ * destination directory for Add new, whose destination moves as the reader types.
15
+ *
16
+ * @docs studio/interface
17
+ */
18
+
19
+ /**
20
+ * The picker row meaning "I will type the whole name myself".
21
+ *
22
+ * It is what keeps the format picker from being a cage. `New File…` is the only generic
23
+ * file-creation affordance in Studio and both backends create intermediate directories on write, so
24
+ * without this row `styles/main.css`, `public/robots.txt`, `.gitignore` and a `credits.txt` beside
25
+ * a collection's images all become uncreatable — and the picker would have taken away more than it
26
+ * gave. The picker stays the authority on the extension; this is one of its answers.
27
+ */
28
+ export const OTHER_FORMAT = "__other__";
29
+
30
+ /** How the extension of a new file is settled. */
31
+ export type FormatChoice =
32
+ /** Picker over every creatable format, plus Other…. The field is a NAME, taken verbatim. */
33
+ | { kind: "choose"; defaultExt?: string; docKind?: "page" | "component" | "content" }
34
+ /**
35
+ * Picker holding ONE format, plus Other… — the collection's own extension. The field is a NAME.
36
+ * `because` is the sentence a refusal quotes when Other… would smuggle a foreign document in.
37
+ */
38
+ | { kind: "locked"; ext: string; because: string }
39
+ /** No picker. The field is a DISPLAY NAME, slugified, and `ext` is appended. */
40
+ | { kind: "fixed"; ext: string };
41
+
42
+ /** A display name, reduced to a file stem. Lowercase by design: this is a slug. */
43
+ export function slugify(input: string): string {
44
+ return input
45
+ .trim()
46
+ .toLowerCase()
47
+ .replaceAll(/\s+/g, "-")
48
+ .replaceAll(/[^a-z\d-]/g, "");
49
+ }
50
+
51
+ /** The extension of a file name, including the dot, or `""`. */
52
+ export function extensionOfName(name: string): string {
53
+ const dot = name.lastIndexOf(".");
54
+ return dot > 0 ? name.slice(dot) : "";
55
+ }
56
+
57
+ /**
58
+ * The file name a field value and a pick compose to — three modes, not two.
59
+ *
60
+ * The verbatim STEM mode is not a convenience. The slugifier lowercases and strips everything
61
+ * outside `[a-z0-9-]`, which turns `[slug]` into `slug` — and eight shipped starters carry
62
+ * `pages/[slug].json`, `[sku].json`, `[...slug].json`. A picker that slugified would make a dynamic
63
+ * route uncreatable and would say nothing about it.
64
+ *
65
+ * @param input What the reader typed.
66
+ * @param picked The picker's value: an extension, {@link OTHER_FORMAT}, or `""` with no picker.
67
+ * @param choice How the extension is settled; absent takes the whole name verbatim.
68
+ */
69
+ export function composeNewFileName(input: string, picked: string, choice?: FormatChoice): string {
70
+ const trimmed = input.trim();
71
+ if (choice === undefined || picked === OTHER_FORMAT) {
72
+ return trimmed;
73
+ }
74
+ if (choice.kind === "fixed") {
75
+ const slug = slugify(trimmed);
76
+ return slug === "" ? "" : `${slug}${choice.ext}`;
77
+ }
78
+ return trimmed.toLowerCase().endsWith(picked.toLowerCase()) ? trimmed : `${trimmed}${picked}`;
79
+ }
80
+
81
+ /** What a name is judged against: where it is going, and what is already there. */
82
+ export interface NewFileNameCheck {
83
+ /** Destination directory, project-relative. `"."` is the project root. */
84
+ dir: string;
85
+ choice?: FormatChoice | undefined;
86
+ /** The destination's entries, LOWER-CASED: APFS and NTFS compare without case, and so does this. */
87
+ taken: ReadonlySet<string>;
88
+ /** Extensions that are documents, lower-cased (`knownDocumentExtensions()`). */
89
+ known?: ReadonlySet<string>;
90
+ /** The picker's rows, for naming a pick in a refusal. */
91
+ rows?: readonly { label: string; value: string }[] | null;
92
+ }
93
+
94
+ /**
95
+ * The refusal a name earns, or `""` when it is usable.
96
+ *
97
+ * In order: blank, a display name that slugifies to nothing, a path that starts or ends with a
98
+ * slash, a `..` segment, a subfolder where the field is a name rather than a whole file name, an
99
+ * extension that contradicts the pick, a document smuggled through Other… past a collection's lock,
100
+ * and finally a name already taken in the destination. The first that applies is the sentence.
101
+ *
102
+ * @param value What the reader typed.
103
+ * @param picked The picker's value, as {@link composeNewFileName} takes it.
104
+ * @param check The destination and what it already holds.
105
+ */
106
+ export function validateNewFileName(
107
+ value: string,
108
+ picked: string,
109
+ check: NewFileNameCheck,
110
+ ): string {
111
+ const { choice, dir, known = new Set<string>(), rows = null, taken } = check;
112
+ const trimmed = value.trim();
113
+ if (!trimmed) {
114
+ return choice?.kind === "fixed" ? "Enter a name." : "Enter a file name.";
115
+ }
116
+ if (choice?.kind === "fixed" && slugify(trimmed) === "") {
117
+ return "Enter at least one letter or number.";
118
+ }
119
+ const normalized = trimmed.replaceAll("\\", "/");
120
+ if (normalized.startsWith("/") || normalized.endsWith("/")) {
121
+ return "Enter a name, not a path that starts or ends with a slash.";
122
+ }
123
+ if (normalized.split("/").includes("..")) {
124
+ return "A name cannot step outside the destination folder.";
125
+ }
126
+ // A slash is the tree's ONLY way to make a directory — both backends create them on write — so
127
+ // It is allowed exactly where the field is a whole file name, and refused where it is a name.
128
+ if (normalized.includes("/") && choice !== undefined && picked !== OTHER_FORMAT) {
129
+ return `Pick Other… to create a file in a subfolder of ${dir === "." ? "the project root" : dir}.`;
130
+ }
131
+ /* A name whose extension MATCHES the pick is fine and is composed once, never doubled;
132
+ refusing every trailing extension would reject this dialog's own `untitled.json` default.
133
+ A name ending in an extension no format claims is a stem that happens to have a dot. */
134
+ const typedExt = extensionOfName(normalized);
135
+ if (
136
+ picked !== "" &&
137
+ picked !== OTHER_FORMAT &&
138
+ typedExt !== "" &&
139
+ typedExt.toLowerCase() !== picked.toLowerCase() &&
140
+ known.has(typedExt.toLowerCase())
141
+ ) {
142
+ const label = rows?.find((row) => row.value === picked)?.label ?? picked;
143
+ return `You picked ${label}; this name ends in ${typedExt}.`;
144
+ }
145
+ if (choice?.kind === "locked" && picked === OTHER_FORMAT) {
146
+ const foreign = typedExt !== "" && typedExt.toLowerCase() !== choice.ext.toLowerCase();
147
+ if (foreign && known.has(typedExt.toLowerCase())) {
148
+ return choice.because;
149
+ }
150
+ }
151
+ /* No emptiness check here: every way `composeNewFileName` can answer "" is already refused
152
+ above — a blank field by the first rule, a display name that slugifies to nothing by the
153
+ second. */
154
+ const candidate = composeNewFileName(value, picked, choice);
155
+ return taken.has(candidate.toLowerCase()) ? `${candidate} already exists in ${dir}/.` : "";
156
+ }
@@ -20,6 +20,7 @@ import {
20
20
  loadFormats,
21
21
  } from "../format/format-host";
22
22
  import { serializeJson } from "@jxsuite/schema/json-layout";
23
+ import { restoreBareRepeaters } from "../tabs/bare-repeaters";
23
24
  import type { Tab } from "../tabs/tab";
24
25
 
25
26
  /**
@@ -43,7 +44,11 @@ export async function serializeDocument(tab: Tab): Promise<string> {
43
44
  const fm = tab.doc.content.frontmatter;
44
45
  const doc = tab.doc.document;
45
46
  const fullDoc = { ...fm, ...doc, children: doc.children ?? [] };
46
- return formatSerialize(sourceFormat.name, fullDoc, { mode: "roundtrip" });
47
+ // The text the file was read as, so what the author did not edit keeps its own spelling.
48
+ return formatSerialize(sourceFormat.name, fullDoc, {
49
+ mode: "roundtrip",
50
+ ...(tab.doc.source !== null && { source: tab.doc.source }),
51
+ });
47
52
  }
48
53
  if (tab.doc.mode === "content") {
49
54
  const format = defaultContentFormat();
@@ -62,6 +67,11 @@ export async function serializeDocument(tab: Tab): Promise<string> {
62
67
  a blank line sits, which characters were escaped — and it is null for a document with no JSON
63
68
  source, where every object expands and every array is laid by fit: the formatter's own answer
64
69
  for stringified output. Either way the text ends with one newline, as every formatted file
65
- does. */
66
- return serializeJson(tab.doc.document, tab.doc.layout ?? null);
70
+ does. A repeater the file wrote as a node's whole `children` is written that way again
71
+ (`tabs/bare-repeaters.ts`), so the member form the editor works in never reaches the disk —
72
+ which is also what keeps the layout's pointers, taken from the file's own text, lined up. */
73
+ return serializeJson(
74
+ restoreBareRepeaters(tab.doc.document, tab.doc.bareRepeaters ?? null),
75
+ tab.doc.layout ?? null,
76
+ );
67
77
  }
@@ -284,6 +284,16 @@ export function documentExtensions(kind?: string): string[] {
284
284
  return [...exts];
285
285
  }
286
286
 
287
+ /**
288
+ * The extensions the build routes under `pages/`: `.json`, and every extension a page format claims
289
+ * (`discoverPages` in `@jxsuite/compiler` walks with the same set). Waits for the registry, so a
290
+ * Markdown page is never judged unroutable because the formats had not landed yet.
291
+ */
292
+ export async function routedPageExtensions(): Promise<string[]> {
293
+ await loadFormats();
294
+ return [".json", ...documentExtensions("page")];
295
+ }
296
+
287
297
  /** The default format for new content documents (first content-kind serializer). */
288
298
  export function defaultContentFormat(): StudioFormat | undefined {
289
299
  return _formats.find((f) => f.capabilities.serialize && f.documentKinds.includes("content"));
@@ -295,6 +305,11 @@ export interface FormatActionOptions {
295
305
  mode?: string;
296
306
  /** Whether the serializer writes a frontmatter block. */
297
307
  frontmatter?: boolean;
308
+ /**
309
+ * The text the document was read from: a roundtrip serializer keeps the spelling of whatever did
310
+ * not change (`@jxsuite/parser`'s `source` option).
311
+ */
312
+ source?: string;
298
313
  }
299
314
 
300
315
  /**
@@ -15,8 +15,8 @@
15
15
  * **This flow fails, and until now it failed silently.** `https://github.com/login/device/code`
16
16
  * sends no `Access-Control-Allow-Origin`, so a browser cannot call it: in Studio-in-a-browser the
17
17
  * very first request rejects with a bare `TypeError`. That rejection used to become a thrown
18
- * `Error` out of {@link authenticateGithub}, into `void createGithubRepository(…)` in
19
- * `panels/git-panel.ts`, and from there into an unhandled promise rejection — the user clicked
18
+ * `Error` out of {@link authenticateGithub}, into `void createGithubRepository(…)` in the Source
19
+ * Control panel of the time, and from there into an unhandled promise rejection — the user clicked
20
20
  * "Create GitHub repository" and the app did nothing at all, forever, with no message anywhere.
21
21
  *
22
22
  * Three failures now reach a surface, each as the kind of record §16 says it is:
@@ -175,7 +175,8 @@ export async function createGithubRepository({ projectName }: { projectName: str
175
175
  // Surfaced as a push failure describing the push.
176
176
  activity.log(errorMessage(error));
177
177
  activity.fail("The repository was created, but the remote could not be added.", {
178
- action: "panel.focus.git",
178
+ action: "view.setWorkspace",
179
+ actionArgs: { workspace: "publish" },
179
180
  });
180
181
  return false;
181
182
  }
@@ -191,8 +192,8 @@ export async function createGithubRepository({ projectName }: { projectName: str
191
192
  return false;
192
193
  }
193
194
 
194
- // Lazy import breaks the github-publish ↔ git-panel module cycle
195
- const { refreshGitStatus } = await import("../panels/git-panel");
195
+ // Lazy import breaks the github-publish ↔ source-control module cycle
196
+ const { refreshGitStatus } = await import("../publish/source-control");
196
197
  await refreshGitStatus();
197
198
  activity.done(repo.html_url);
198
199
  notify.success(`Repository created: ${repo.html_url}`, { key: "github.repo" });
@@ -144,9 +144,13 @@ export function createGridController(tab: Tab, source: GridSource): GridControll
144
144
 
145
145
  const reloadRows = async () => {
146
146
  const result = await source.rows(state.query);
147
+ /* Index BEFORE the rows are published. The index is not reactive, and assigning `state.rows`
148
+ runs the effects that read it at once: a record's entry form (`content/record-entry.ts`)
149
+ resolves its row key from the new rows and reads that row's cells through the baseline,
150
+ which with the index still empty drew every field blank until something else re-ran it. */
151
+ indexRows(result.rows);
147
152
  state.rows = result.rows;
148
153
  state.total = result.total;
149
- indexRows(result.rows);
150
154
  };
151
155
 
152
156
  /** Missing required cells block the save (marked as row/cell errors, like a failed commit). */