@jxsuite/studio 6.1.2 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -1,76 +1,116 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * The Library — an editor kind, drawn into the pane, over a {@link LibrarySource}.
3
+ * The Content home: one collection, browsed, in the Content workspace's home tab (`specs/studio.md`
4
+ * §9.1.2, §22.5).
4
5
  *
5
- * It replaces the Manage view, which was a full-screen MODAL over the whole shell. Everything that
6
- * was wrong with that follows from the modal: it could not be a tab, so it could not be one of two
7
- * panes, so it could not be open beside the page you were browsing FOR; it had no editor kind, so
8
- * the status bar and the pane context bar had nothing to say about it; and its own filter bar was
9
- * the only way to reach any of its state, so nothing it did was in the palette, on a chord, or
10
- * available to the assistant. `manage` was already in `commands/context.ts`'s `EDITOR_KIND_BY_MODE`
11
- * — the map just had nothing behind it.
6
+ * The Library this replaces answered "where is everything" with one tab of every project file in
7
+ * folder categories. That question has better answers now (pages, templates and components in the
8
+ * Site panel, media in the Media workspace), and what was left is the Library's real job: browsing
9
+ * and writing a COLLECTION. The home shows one at a time, the built-in **Pages** or a collection
10
+ * the project declares, chosen in the Collections panel (`panels/collections-panel.ts`) or by
11
+ * `content.showCollection`.
12
12
  *
13
13
  * The MARKUP is `surfaces/library-pane.json`, a Jx document over the kit; this module is the
14
- * projection and the decisions. Four things it is careful about:
14
+ * projection and the decisions. Five things it is careful about:
15
15
  *
16
16
  * 1. **It never says "No files found" for two different reasons.** A scan that could not read a
17
17
  * directory is INCOMPLETE, not empty; it raises a Problem carrying the directory and a Retry
18
- * (`library.refresh`), and the surface says so. A filter that matched nothing says which filter.
18
+ * (`content.refresh`), and the surface says so. A filter that matched nothing says which filter.
19
19
  * The four states are one word on the scope, so no two of them can be true at once.
20
20
  * 2. **The rendered item count is proportional to the viewport, not to the project.** See
21
- * `../ui/virtual-window.ts` and `library-preview.ts`; the acceptance case is 300 pages in
22
- * "All".
23
- * 3. **An upload has a named destination**, shown before the drop, asked for when the active category
24
- * does not name one.
25
- * 4. **Creation is somebody else's flow.** A page, layout or component is `files.ts`'s creation, so a
26
- * new page from the Library and a new file from the tree refuse a name that is already taken in
27
- * exactly the same way; a collection entry is `content/entry-commands.ts`'s `createEntry`, so it
28
- * arrives with the collection's extension and a body seeded from that collection's schema.
21
+ * `../ui/virtual-window.ts` and `library-preview.ts`; the acceptance case is 300 pages.
22
+ * 3. **Every collection keeps its own view.** Its view switch, search and language are its own
23
+ * ({@link contentView}`.views`), so Pages in Table and Posts in Cards is a state the home can be
24
+ * in, and coming back to a collection finds it as it was left.
25
+ * 4. **Where a collection is published is read from the pages.** The header line ("Each post has a
26
+ * page at `/blog/:slug` using Post") is derived from the page whose `$paths.contentType` names
27
+ * the collection (`content/collection-pages.ts`), never stored beside it.
28
+ * 5. **Creation is somebody else's flow.** **New <item>** runs `file.new` (Add new, §9.1.1) on the
29
+ * collection's kind, and **Add one** writes the collection's page exactly as Add new's own-page
30
+ * option does (`create/add-new-flow.ts` `addCollectionPage`).
29
31
  *
30
32
  * @docs studio/projects/browse
31
33
  */
32
34
 
33
35
  import { effect, effectScope, reactive } from "../reactivity";
34
36
  import { errorMessage } from "@jxsuite/schema/parse";
37
+ import { localeLabel, resolveI18n } from "@jxsuite/schema/locale";
35
38
  import { notify } from "../services/notify";
36
39
  import { beginActivity } from "../panels/activity-panel";
37
40
  import { showPromptDialog } from "../ui/layers";
38
41
  import type { PromptDialogOptions } from "../ui/layers";
39
42
  import { openMenu } from "../surfaces/menu";
40
- import { rectOf } from "../utils/geometry";
41
43
  import { getPlatform } from "../platform";
44
+ import { projectState, renderOnly } from "../store";
42
45
  import { invalidateUsages } from "../services/references";
43
46
  import { confirmFileDelete, renamePromptMessage } from "../files/file-ops";
44
- import { createFileIn, openFileInTab } from "../files/files";
45
- import { createEntry } from "../content/entry-commands";
46
- import { formatByExtension, formatSerialize } from "../format/format-host";
47
- import { entryCollections } from "../content/entry-model";
48
- import { uploadAccept, uploadAssets } from "../files/media-upload";
49
- import { localeLabel } from "@jxsuite/schema/locale";
47
+ import { openFileInTab } from "../files/files";
48
+ import { runActiveReported } from "../commands/run-reported";
49
+ import {
50
+ PAGES_COLLECTION,
51
+ collectionSourceFile,
52
+ declaredCollectionSchema,
53
+ contentCollection,
54
+ templateDisplayName,
55
+ } from "../content/content-collections";
56
+ import {
57
+ onCollectionPagesRead,
58
+ peekCollectionPage,
59
+ peekCollectionPages,
60
+ } from "../content/collection-pages";
61
+ import { onPageSummariesRead, peekPageSummaries } from "../content/page-summaries";
62
+ import { localeMarked, pageLabel, singularLabel } from "../panels/site-model";
63
+ import { compareRoutes, fileToRoute } from "@jxsuite/site/routes";
64
+ import {
65
+ dateFieldOf,
66
+ declaredDateField,
67
+ entryDay,
68
+ entryThumb,
69
+ onEntrySummariesRead,
70
+ peekEntrySummaries,
71
+ titleFieldOf,
72
+ } from "../content/entry-summaries";
73
+ import { entryCollection } from "../content/entry-model";
74
+ import { isString } from "@jxsuite/schema/guards";
75
+ import type { ContentTypeSchema } from "@jxsuite/schema/types";
76
+ import { openEntry } from "../content/entry-open";
77
+ import { openRecordEntry } from "../content/record-entry";
50
78
  import {
51
- LIBRARY_CATEGORIES,
52
79
  LIBRARY_LAYOUTS,
53
80
  LIBRARY_LAYOUT_LABELS,
54
81
  PREVIEW_LAYOUTS,
55
82
  filterLibrary,
56
- libraryCategory,
57
83
  libraryLocales,
58
- uploadDirForCategory,
84
+ projectLocales,
59
85
  } from "./library-model";
60
- import { createLibrarySource, libraryColumns } from "./library-source";
86
+ import { createLibrarySource, entryFieldColumns, libraryColumns } from "./library-source";
61
87
  import { createPreviewCache, createPreviewObserver, previewFor } from "./library-preview";
88
+ import { mountPreview, shownPreview } from "./preview-root";
62
89
  import { computeWindow } from "../ui/virtual-window";
63
90
  import { paneRegion } from "../ui/regions";
64
- import { LAYOUT_METRICS, boardView, calendarView, columnsAt, libraryRow } from "./library-layouts";
91
+ import {
92
+ LAYOUT_METRICS,
93
+ boardFieldOf,
94
+ boardView,
95
+ calendarView,
96
+ columnsAt,
97
+ entryFactsFrom,
98
+ fieldValues,
99
+ libraryRow,
100
+ } from "./library-layouts";
65
101
  import { mountLibrarySurface } from "../surfaces/library-pane";
66
102
  import type { ActivityHandle } from "../panels/activity-panel";
67
- import type { FormatChoice, NewFileRequest } from "../files/files";
103
+ import type { ContentCollection } from "../content/content-collections";
104
+ import type { CollectionPage } from "../content/collection-pages";
68
105
  import type { EffectScope } from "@vue/reactivity";
69
106
  import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
70
107
  import type { LibraryFile, LibraryLayout } from "./library-model";
108
+ import type { BoardField, EntryFacts, EntryFactsOf } from "./library-layouts";
71
109
  import type { LibrarySource } from "./library-source";
110
+ import type { CommandArgValues } from "../commands/command-args";
72
111
  import type {
73
112
  LibraryChip,
113
+ LibraryPageLine,
74
114
  LibrarySurfaceHandle,
75
115
  LibraryView,
76
116
  LibraryViewKind,
@@ -81,8 +121,8 @@ import type { CanvasSurface } from "../canvas/canvas-surface";
81
121
 
82
122
  // ─── View state ──────────────────────────────────────────────────────────────
83
123
 
84
- export interface LibraryViewState {
85
- category: string;
124
+ /** One collection's own view: how it is arranged, what it is filtered to. */
125
+ export interface CollectionViewState {
86
126
  layout: LibraryLayout;
87
127
  query: string;
88
128
  /**
@@ -92,30 +132,86 @@ export interface LibraryViewState {
92
132
  * matches what {@link LibraryFile.locale} carries.
93
133
  */
94
134
  locale: string;
135
+ /**
136
+ * The Filter: one value of the collection's filter field ({@link filterFieldOf}: its status, else
137
+ * the field its Board groups by), or "" for every entry. A value the field no longer has filters
138
+ * nothing, rather than hiding every entry behind a choice the picker cannot show.
139
+ */
140
+ filter: string;
141
+ }
142
+
143
+ export interface ContentViewState {
144
+ /** The collection the home shows: `pages`, or a declared collection's name. */
145
+ collection: string;
146
+ /** Each collection's own view, created the first time it is shown. */
147
+ views: Record<string, CollectionViewState>;
95
148
  /** Bumped to force a repaint from a non-reactive source (a scroll, a finished scan). */
96
149
  revision: number;
97
150
  /** A scan is running. Distinct from "scanned and empty", which is what the old view conflated. */
98
151
  loading: boolean;
99
152
  }
100
153
 
154
+ /** The view a collection starts in: Table, the shape both Content mockups open on. */
155
+ const DEFAULT_LAYOUT: LibraryLayout = "table";
156
+
101
157
  /**
102
- * The Library's view state, reactive and module-scoped.
158
+ * The Content home's state, reactive and module-scoped.
103
159
  *
104
- * Module-scoped rather than per-tab because there is one Library per window (its tab id carries no
105
- * category — see `grid-source.ts`), and because the commands that write it must be able to do so
106
- * whether or not the pane is currently mounted: `library.setCategory` run from the palette while
107
- * another tab is focused sets the state the Library opens into.
160
+ * Module-scoped rather than per-tab because there is one Content home per window, and because the
161
+ * commands that write it must be able to do so whether or not the home is mounted:
162
+ * `content.setLayout` run from the palette sets the view the home opens into. Per COLLECTION
163
+ * inside, so one collection's view never leaks into another's.
108
164
  */
109
- export const libraryView: LibraryViewState = reactive({
110
- category: "all",
111
- layout: "cards",
165
+ export const contentView: ContentViewState = reactive({
166
+ collection: PAGES_COLLECTION,
112
167
  loading: false,
113
- locale: "",
114
- query: "",
115
168
  revision: 0,
169
+ views: {},
116
170
  });
117
171
 
118
- /** The window's Library source. Built lazily; discarded by {@link invalidateLibrary}. */
172
+ /** The view state of a collection, created on first read. */
173
+ export function collectionView(key: string = shownCollectionKey()): CollectionViewState {
174
+ const existing = contentView.views[key];
175
+ if (existing) {
176
+ return existing;
177
+ }
178
+ const created: CollectionViewState = {
179
+ filter: "",
180
+ layout: DEFAULT_LAYOUT,
181
+ locale: "",
182
+ query: "",
183
+ };
184
+ contentView.views[key] = created;
185
+ // Read back through the reactive record, so a later write to the returned object is tracked.
186
+ return contentView.views[key] ?? created;
187
+ }
188
+
189
+ /** The collection the home shows, falling back to Pages when its name has gone from the project. */
190
+ export function shownCollection(): ContentCollection {
191
+ return (
192
+ contentCollection(contentView.collection) ?? {
193
+ fileBacked: false,
194
+ item: "page",
195
+ key: PAGES_COLLECTION,
196
+ kind: "pages",
197
+ title: "Pages",
198
+ }
199
+ );
200
+ }
201
+
202
+ /**
203
+ * The key of the collection the home actually shows.
204
+ *
205
+ * `contentView.collection` is what was last ASKED for, and it can outlive its collection (a rename
206
+ * or a removal in Settings › Content types). Every default that means "the shown collection" reads
207
+ * this instead, so the view switch, search, language picker and preview tending all act on the
208
+ * collection on screen rather than on a stale key.
209
+ */
210
+ export function shownCollectionKey(): string {
211
+ return shownCollection().key;
212
+ }
213
+
214
+ /** The window's scan source. Built lazily; discarded by {@link invalidateLibrary}. */
119
215
  let source: LibrarySource | null = null;
120
216
 
121
217
  /**
@@ -130,33 +226,54 @@ let scanAttempted = false;
130
226
 
131
227
  /** The source, creating it on first use. */
132
228
  export function librarySource(): LibrarySource {
229
+ /* A scan of a directory list the project has since outgrown is dropped here, on the next ask,
230
+ rather than by every door that can change the list: the project's directories loading after a
231
+ window restored into Content, or a collection being declared. Each of those repaints, and the
232
+ repaint is what asks. Without it the first answer, a walk of no directories, was cached as
233
+ "this project has no pages" until a reload. */
234
+ if (source?.stale() === true) {
235
+ source = null;
236
+ scanAttempted = false;
237
+ for (const panel of _active.values()) {
238
+ panel.cache.clear();
239
+ }
240
+ }
133
241
  source ??= createLibrarySource();
134
242
  return source;
135
243
  }
136
244
 
137
- /** Repaint whatever is mounted. */
245
+ /** Repaint whatever is mounted, the Collections panel's counts included. */
138
246
  function bump() {
139
- libraryView.revision += 1;
247
+ contentView.revision += 1;
140
248
  }
141
249
 
142
- export function setLibraryCategory(key: string): void {
143
- libraryView.category = key;
250
+ /** Show a collection in the home. The home itself is opened by `content.showCollection`. */
251
+ export function setContentCollection(key: string): void {
252
+ contentView.collection = key;
253
+ collectionView(key);
144
254
  bump();
255
+ renderOnly("leftPanel");
145
256
  }
146
257
 
147
- export function setLibraryLayout(layout: LibraryLayout): void {
148
- libraryView.layout = layout;
258
+ export function setLibraryLayout(layout: LibraryLayout, key: string = shownCollectionKey()): void {
259
+ collectionView(key).layout = layout;
149
260
  bump();
150
261
  }
151
262
 
152
- export function setLibrarySearch(query: string): void {
153
- libraryView.query = query;
263
+ export function setLibrarySearch(query: string, key: string = shownCollectionKey()): void {
264
+ collectionView(key).query = query;
265
+ bump();
266
+ }
267
+
268
+ /** Show the entries with one value of the filter field, or `""` for all of them. A setter. */
269
+ export function setLibraryFilter(value: string, key: string = shownCollectionKey()): void {
270
+ collectionView(key).filter = value;
154
271
  bump();
155
272
  }
156
273
 
157
274
  /** Show one language, or `""` for all of them. A setter, so "" is a value and not an absence. */
158
- export function setLibraryLocale(locale: string): void {
159
- libraryView.locale = locale;
275
+ export function setLibraryLocale(locale: string, key: string = shownCollectionKey()): void {
276
+ collectionView(key).locale = locale;
160
277
  bump();
161
278
  }
162
279
 
@@ -164,7 +281,7 @@ export function setLibraryLocale(locale: string): void {
164
281
  * Drop the scan and the rendered previews.
165
282
  *
166
283
  * Called by every surface that changes the project's file set — an upload, a creation, a rename, a
167
- * delete — so the Library is never showing a file that is gone.
284
+ * delete — so the home is never showing a file that is gone.
168
285
  *
169
286
  * The SLOT table survives, and that is the difference between this and a teardown: clearing the
170
287
  * cache detaches every rendered preview from the box it was in, and those boxes are still on
@@ -180,13 +297,45 @@ export function invalidateLibrary(): void {
180
297
  bump();
181
298
  }
182
299
 
300
+ /**
301
+ * How many files each collection holds, from the scan as it stands. Starts the scan when none has
302
+ * been attempted, so the Collections panel can ask before the home has ever been shown; the panel
303
+ * repaints when it lands.
304
+ *
305
+ * @returns Collection key → count, or `null` while no scan has finished.
306
+ */
307
+ export function collectionCounts(): Map<string, number> | null {
308
+ const current = librarySource();
309
+ if (!scanAttempted && !contentView.loading) {
310
+ void loadLibrary();
311
+ }
312
+ if (!current.scanned()) {
313
+ return null;
314
+ }
315
+ const counts = new Map<string, number>();
316
+ for (const file of current.files()) {
317
+ if (file.collection !== null) {
318
+ counts.set(file.collection, (counts.get(file.collection) ?? 0) + 1);
319
+ }
320
+ }
321
+ return counts;
322
+ }
323
+
183
324
  // ─── Loading ─────────────────────────────────────────────────────────────────
184
325
 
185
326
  /** A scan slower than this earns an Activity row; a fast one must not litter the dock. */
186
327
  export const SCAN_ACTIVITY_DELAY_MS = 600;
187
328
 
188
329
  /** Deduplication key for the scan Problem, so a failing watcher is one row and not sixty. */
189
- const SCAN_PROBLEM_KEY = "library.scan";
330
+ const SCAN_PROBLEM_KEY = "content.scan";
331
+
332
+ /** The `source` every Problem and toast this module files carries. */
333
+ const SOURCE = "Content";
334
+
335
+ /** The scan's Activity row, filled in only once the scan outlasts {@link SCAN_ACTIVITY_DELAY_MS}. */
336
+ interface SlowScanActivity {
337
+ handle: ActivityHandle | null;
338
+ }
190
339
 
191
340
  /**
192
341
  * Run the scan and report it honestly.
@@ -196,21 +345,16 @@ const SCAN_PROBLEM_KEY = "library.scan";
196
345
  * contract exists to end. The row is informational, and it only appears when the scan is slow
197
346
  * enough that its absence would look like a hang.
198
347
  */
199
- /** The scan's Activity row, filled in only once the scan outlasts {@link SCAN_ACTIVITY_DELAY_MS}. */
200
- interface SlowScanActivity {
201
- handle: ActivityHandle | null;
202
- }
203
-
204
348
  async function loadLibrary(): Promise<void> {
205
349
  const current = librarySource();
206
350
  scanAttempted = true;
207
- libraryView.loading = true;
351
+ contentView.loading = true;
208
352
  bump();
209
353
 
210
354
  const slow: SlowScanActivity = { handle: null };
211
355
  const timer = setTimeout(() => {
212
356
  slow.handle = beginActivity({
213
- source: "Library",
357
+ source: SOURCE,
214
358
  status: "Reading the project's directories…",
215
359
  title: "Scan project files",
216
360
  });
@@ -227,11 +371,11 @@ async function loadLibrary(): Promise<void> {
227
371
  // A Problem, not a toast: the list on screen is wrong until this is fixed, and the fix is a
228
372
  // Command the row can run.
229
373
  notify.error(summary, {
230
- action: "library.refresh",
374
+ action: "content.refresh",
231
375
  detail,
232
376
  key: SCAN_PROBLEM_KEY,
233
377
  path: failures[0]!.dir,
234
- source: "Library",
378
+ source: SOURCE,
235
379
  tier: "problem",
236
380
  });
237
381
  slow.handle?.setStatus(summary);
@@ -244,21 +388,23 @@ async function loadLibrary(): Promise<void> {
244
388
  slow.handle.fail(message);
245
389
  } else {
246
390
  notify.error(message, {
247
- action: "library.refresh",
391
+ action: "content.refresh",
248
392
  detail: errorMessage(error),
249
393
  key: SCAN_PROBLEM_KEY,
250
- source: "Library",
394
+ source: SOURCE,
251
395
  tier: "problem",
252
396
  });
253
397
  }
254
398
  } finally {
255
399
  clearTimeout(timer);
256
- libraryView.loading = false;
400
+ contentView.loading = false;
257
401
  bump();
402
+ // The Collections panel draws the counts this scan just produced.
403
+ renderOnly("leftPanel");
258
404
  }
259
405
  }
260
406
 
261
- /** Re-scan from scratch — the Retry behind the Problem, and the toolbar's Refresh. */
407
+ /** Re-scan from scratch — the Retry behind the Problem. */
262
408
  export async function refreshLibrary(): Promise<void> {
263
409
  source = null;
264
410
  scanAttempted = false;
@@ -271,7 +417,7 @@ export async function refreshLibrary(): Promise<void> {
271
417
  // ─── The mounted pane ────────────────────────────────────────────────────────
272
418
 
273
419
  interface ActiveLibraryPane {
274
- /** The pane whose stage this Library is drawn on. The map key, held on the record too. */
420
+ /** The pane whose stage this home is drawn on. The map key, held on the record too. */
275
421
  paneId: string;
276
422
  tabId: string;
277
423
  scope: EffectScope;
@@ -293,30 +439,26 @@ interface ActiveLibraryPane {
293
439
  * and a scroll through a long list would read each document several times over.
294
440
  */
295
441
  pendingPaths: Set<string>;
296
- /** Whether a drag is over the body. Held here so a dragover does not recompute the window. */
297
- dropActive: boolean;
298
442
  /** The coalesced post-repaint pass; see {@link scheduleSettle}. */
299
443
  settleTimer: ReturnType<typeof setTimeout> | null;
444
+ /** Stops listening for the collection-pages index. */
445
+ unlisten: () => void;
300
446
  }
301
447
 
302
448
  /**
303
- * The Library mounted in each pane, keyed by pane id.
449
+ * The home mounted in each pane, keyed by pane id.
304
450
  *
305
- * ONE instance per pane, not per window. It was a module-level `let active`, which is a fact about
306
- * the shell having had one stage — and the failure it produced the moment two are drawn is not
307
- * cosmetic: pane B mounting a Library called `detachLibraryPane()`, destroying pane A's
308
- * `IntersectionObserver`, preview cache and effect scope while pane A's DOM was still on screen.
309
- * `canvas-render.ts`'s `resetCanvasView` calls the detach UNCONDITIONALLY on every empty pane, so
310
- * it did not even take a second Library to do it.
451
+ * ONE instance per pane, not per window: pane B mounting the home must not destroy pane A's
452
+ * `IntersectionObserver`, preview cache and effect scope while pane A's DOM is still on screen.
311
453
  */
312
454
  const _active = new Map<string, ActiveLibraryPane>();
313
455
 
314
- /** The Library mounted in a pane, or null. */
456
+ /** The home mounted in a pane, or null. */
315
457
  function activeIn(paneId: string): ActiveLibraryPane | null {
316
458
  return _active.get(paneId) ?? null;
317
459
  }
318
460
 
319
- /** Whether the Library is live in this pane for this tab — the canvas-render fast-path guard. */
461
+ /** Whether the home is live in this pane for this tab — the canvas-render fast-path guard. */
320
462
  export function libraryPaneMounted(paneId: string, tab: Tab): boolean {
321
463
  const panel = activeIn(paneId);
322
464
  return (
@@ -324,7 +466,7 @@ export function libraryPaneMounted(paneId: string, tab: Tab): boolean {
324
466
  );
325
467
  }
326
468
 
327
- /** Tear one pane's Library down (mode change, tab switch, project close). Idempotent. */
469
+ /** Tear one pane's home down (mode change, tab switch, project close). Idempotent. */
328
470
  export function detachLibraryPane(paneId: string): void {
329
471
  const panel = _active.get(paneId);
330
472
  if (!panel) {
@@ -334,6 +476,7 @@ export function detachLibraryPane(paneId: string): void {
334
476
  clearTimeout(panel.settleTimer);
335
477
  panel.settleTimer = null;
336
478
  }
479
+ panel.unlisten();
337
480
  panel.observer.destroy();
338
481
  panel.cache.clear();
339
482
  panel.slots.clear();
@@ -377,8 +520,8 @@ async function fillPreview(panel: ActiveLibraryPane, path: string) {
377
520
  return;
378
521
  }
379
522
  const slot = panel.slots.get(path);
380
- if (slot?.isConnected && !slot.firstElementChild) {
381
- slot.append(rendered);
523
+ if (slot?.isConnected && !shownPreview(slot)) {
524
+ mountPreview(slot, rendered);
382
525
  }
383
526
  } finally {
384
527
  panel.pendingPaths.delete(path);
@@ -392,11 +535,11 @@ function tendSlot(panel: ActiveLibraryPane, slot: HTMLElement, path: string): vo
392
535
  // Drift to the tail of the LRU, get evicted, get detached, and be re-rendered on the next
393
536
  // Repaint: the 300-page measurement showed 560 reads for 300 documents until this line moved.
394
537
  const cached = panel.cache.get(path);
395
- if (slot.firstElementChild) {
538
+ if (shownPreview(slot)) {
396
539
  return;
397
540
  }
398
541
  if (cached) {
399
- slot.append(cached);
542
+ mountPreview(slot, cached);
400
543
  return;
401
544
  }
402
545
  if (panel.pendingPaths.has(path)) {
@@ -420,20 +563,11 @@ function previewSlot(panel: ActiveLibraryPane, slot: HTMLElement, path: string):
420
563
  /**
421
564
  * The pass that runs once the document has reconciled, coalesced onto a task of its own.
422
565
  *
423
- * It exists because a document repaint is not a moment this module is present at: writing the
424
- * projection returns immediately and the runtime's keyed arrays reconcile a microtask later, so
425
- * "the window has moved" is only true one task after the write. Two things have to happen then and
426
- * cannot happen before.
427
- *
428
- * The first is releasing observations. A card outside the new window is now detached, and an
429
- * observation of a detached node can never fire again: it is pure cost — the browser walks it on
430
- * every scroll frame — and it pins the card's whole subtree. The intersect callback cannot be the
431
- * only release path, because a card flicked past before it ever intersected is never reported.
432
- *
433
- * The second is tending the slots that ARE on screen. A row the reconciler kept keeps its preview
434
- * box, so nothing re-announces it — and an LRU eviction detaches the render that was inside it. So
435
- * every visible slot is touched here: the touch is what keeps it at the head of the LRU, and the
436
- * refill is what puts a preview back into a box eviction emptied.
566
+ * A document repaint is not a moment this module is present at: writing the projection returns
567
+ * immediately and the runtime's keyed arrays reconcile a microtask later. Two things happen then:
568
+ * observations of cards that left the window are released (an observation of a detached node can
569
+ * never fire again, and it pins the card's subtree), and every visible slot is tended, which keeps
570
+ * it at the head of the LRU and refills a box an eviction emptied.
437
571
  */
438
572
  function scheduleSettle(panel: ActiveLibraryPane): void {
439
573
  if (panel.settleTimer !== null) {
@@ -445,7 +579,7 @@ function scheduleSettle(panel: ActiveLibraryPane): void {
445
579
  return;
446
580
  }
447
581
  panel.observer.releaseDetached();
448
- const previews = PREVIEW_LAYOUTS.has(libraryView.layout);
582
+ const previews = PREVIEW_LAYOUTS.has(collectionView().layout);
449
583
  for (const [path, slot] of panel.slots) {
450
584
  if (!slot.isConnected) {
451
585
  panel.slots.delete(path);
@@ -458,14 +592,34 @@ function scheduleSettle(panel: ActiveLibraryPane): void {
458
592
  }, 0);
459
593
  }
460
594
 
595
+ // ─── Opening ─────────────────────────────────────────────────────────────────
596
+
597
+ /**
598
+ * Open a row: a page in Design, an entry where it is written (`content/entry-open.ts`).
599
+ *
600
+ * The same answer whichever collection drew the row, because the file says what it is: a page is
601
+ * Design's, and activating it enters Design (§22.1).
602
+ */
603
+ export function openLibraryFile(file: Pick<LibraryFile, "path" | "collection" | "record">): void {
604
+ if (file.record) {
605
+ /* A record of a file-backed collection opens in its entry form: that one row's fields, typed
606
+ by the collection's schema, kept in its file's grid (`content/record-entry.ts`). */
607
+ void openRecordEntry(file.record.source, file.record.row);
608
+ } else if (file.collection === null || file.collection === PAGES_COLLECTION) {
609
+ void openFileInTab(file.path);
610
+ } else {
611
+ void openEntry(file.path);
612
+ }
613
+ }
614
+
461
615
  // ─── Context menu ────────────────────────────────────────────────────────────
462
616
 
463
617
  /**
464
- * The menu the Library has up, whichever it is.
618
+ * The per-file menu the home has up, if any.
465
619
  *
466
- * One handle for both the per-file menu and the New menu, because only one of them can be open: the
467
- * kit's menu is a popover on the platform's own top layer, and opening a second while the first is
468
- * up is how two of them end up light-dismissing each other.
620
+ * One handle, because only one can be open: the kit's menu is a popover on the platform's own top
621
+ * layer, and opening a second while the first is up is how two of them end up light-dismissing each
622
+ * other.
469
623
  */
470
624
  let _menu: MenuHandle | null = null;
471
625
 
@@ -483,16 +637,11 @@ function fileAt(path: string): LibraryFile | null {
483
637
  );
484
638
  }
485
639
 
486
- /**
487
- * The per-file menu, opened at the pointer.
488
- *
489
- * The rows are the `menu` surface's own projections, so the Library's Delete is drawn, keyed and
490
- * dismissed exactly as the element menu's is — including the edge clamping, which was a hand-rolled
491
- * `requestAnimationFrame` measurement here and is the kit's popover's job.
492
- */
640
+ /** The per-file menu, opened at the pointer. Region `content`. */
493
641
  function showLibraryContextMenu(path: string, x: number, y: number): void {
494
642
  const file = fileAt(path);
495
- if (!file) {
643
+ // A record is a row of a file: renaming, duplicating or deleting "it" would act on the file.
644
+ if (!file || file.record) {
496
645
  return;
497
646
  }
498
647
  closeLibraryMenu();
@@ -518,10 +667,10 @@ function showLibraryContextMenu(path: string, x: number, y: number): void {
518
667
  }
519
668
  },
520
669
  origin: { x, y },
521
- region: "library",
670
+ region: "content",
522
671
  rows: [
523
672
  row("open", "Open", () => {
524
- void openFileInTab(file.path);
673
+ openLibraryFile(file);
525
674
  }),
526
675
  row(
527
676
  "rename",
@@ -546,44 +695,8 @@ function showLibraryContextMenu(path: string, x: number, y: number): void {
546
695
  });
547
696
  }
548
697
 
549
- /** The New menu, opened under its own button. A second press closes it: the button is a toggle. */
550
- function showLibraryNewMenu(opener: HTMLElement): void {
551
- if (_menu) {
552
- closeLibraryMenu();
553
- return;
554
- }
555
- const entries = libraryNewEntries();
556
- _menu = openMenu({
557
- label: "New",
558
- onClosed: (handle) => {
559
- if (_menu === handle) {
560
- _menu = null;
561
- }
562
- },
563
- opener,
564
- place: () => {
565
- const box = rectOf(opener);
566
- return { x: box.left, y: box.bottom + 4 };
567
- },
568
- region: "library-new",
569
- rows: entries.map((entry, index) => ({
570
- destructive: false,
571
- disabled: false,
572
- /* The document kinds and the collections are two different creation flows, so the boundary
573
- between them is drawn rather than left to be inferred from the folder names. */
574
- dividerAbove:
575
- entry.collection !== undefined && entries[index - 1]?.collection === undefined && index > 0,
576
- id: entry.key,
577
- title: `${entry.label} — ${entry.dir}/`,
578
- })),
579
- run: (id) => {
580
- void createLibraryEntry(id);
581
- },
582
- });
583
- }
584
-
585
698
  /**
586
- * The Library's rename dialog — the same copy, from the same helper, as the Files panel's.
699
+ * The rename dialog — the same copy, from the same helper, as the Files panel's.
587
700
  *
588
701
  * A rename here rewrites references exactly as one in the sidebar does, and two dialogs that
589
702
  * disagreed about that is the divergence the shared helper exists to prevent.
@@ -616,7 +729,7 @@ async function renameLibraryFile(file: LibraryFile) {
616
729
  notify.error(`Could not rename ${file.name}.`, {
617
730
  detail: errorMessage(error),
618
731
  path: file.path,
619
- source: "Library",
732
+ source: SOURCE,
620
733
  });
621
734
  }
622
735
  }
@@ -638,7 +751,7 @@ async function duplicateLibraryFile(file: LibraryFile) {
638
751
  notify.error(`Could not duplicate ${file.name}.`, {
639
752
  detail: errorMessage(error),
640
753
  path: file.path,
641
- source: "Library",
754
+ source: SOURCE,
642
755
  });
643
756
  }
644
757
  }
@@ -658,186 +771,95 @@ async function deleteLibraryFile(file: LibraryFile) {
658
771
  notify.error(`Could not delete ${file.name}.`, {
659
772
  detail: errorMessage(error),
660
773
  path: file.path,
661
- source: "Library",
774
+ source: SOURCE,
662
775
  });
663
776
  }
664
777
  }
665
778
 
666
- // ─── Creation ────────────────────────────────────────────────────────────────
779
+ // ─── The collection's page ───────────────────────────────────────────────────
667
780
 
668
- /** What the New menu offers: the three document kinds, plus one row per creatable collection. */
669
- export interface LibraryNewEntry {
670
- key: string;
671
- label: string;
672
- dir: string;
673
- /**
674
- * The content collection this row creates an entry IN, or undefined for a document kind.
675
- *
676
- * Present is the whole difference between the two creation paths below — a collection row is
677
- * `content/`'s job, because only that module knows the collection's extension and its schema.
678
- */
679
- collection?: string;
680
- }
681
-
682
- /**
683
- * The new-entity rows.
684
- *
685
- * Collections come from `content/entry-model.ts` rather than from `project.json` directly, so the
686
- * menu offers exactly the collections an entry can be created in. The predecessor read the raw
687
- * `content` map and derived a directory from the type NAME when `source` was absent, which produced
688
- * a row for a collection that has no entry directory at all and a row for a single-file (CSV)
689
- * catalogue, whose entries are rows and not files. Both created a file nothing would ever load.
690
- */
691
- export function libraryNewEntries(): LibraryNewEntry[] {
692
- const rows: LibraryNewEntry[] = [
693
- { dir: "pages", key: "page", label: "Page" },
694
- { dir: "layouts", key: "layout", label: "Layout" },
695
- { dir: "components", key: "component", label: "Component" },
696
- ];
697
- for (const collection of entryCollections()) {
698
- rows.push({
699
- collection: collection.name,
700
- dir: collection.dir,
701
- key: `collection:${collection.name}`,
702
- label: collection.name.charAt(0).toUpperCase() + collection.name.slice(1),
703
- });
781
+ /** The template a collection's page is drawn in, as the header line names it. */
782
+ function templateClause(page: CollectionPage, defaultLayout: string | null): string {
783
+ if (page.layout === false) {
784
+ return "with no template";
704
785
  }
705
- return rows;
786
+ const layout = page.layout ?? defaultLayout;
787
+ return layout ? `using ${templateDisplayName(layout)}` : "with no template";
706
788
  }
707
789
 
708
790
  /**
709
- * Create one entity and open it.
710
- *
711
- * Two paths, because a collection entry is not a blank document. A **collection** row goes through
712
- * `content/entry-commands.ts`'s `createEntry`, which supplies the collection's extension (so the
713
- * field asks for a display name and the file is actually matched by the collection it was created
714
- * in) and a body seeded from the collection's **schema defaults** — the entry is valid the moment
715
- * it exists, rather than a pile of absent required fields — and then opens it in the entry FORM,
716
- * which is the editor that collection's fields belong to. Both of those are the reason
717
- * `createEntry` is called rather than reimplemented; a second seeder here would drift from the one
718
- * the palette's `content.newEntry` uses within a release.
791
+ * The header line: where a collection's entries are published (§22.5).
719
792
  *
720
- * A **document kind** gets the format PICKER, which is the same argument the verbatim name field
721
- * used to make: a page may be a `.md` as easily as a `.json`, so the extension is a real choice —
722
- * now offered rather than left to be typed from memory. A collection's extension is not a choice;
723
- * it is the collection's, and `createEntry` supplies it.
724
- *
725
- * **A layout is not a choice either, and that is not a policy.** Both readers of a layout parse it
726
- * as JSON and neither dispatches through the format registry — `site/layout-resolver.ts` throws and
727
- * `site-context.ts` returns null — so a `.md` layout is a file the build cannot load. There is no
728
- * `"layout"` document kind for a format to declare, so this is stated here rather than derived.
729
- *
730
- * A **component** keeps the picker: a format whose `$studio.documentMode.componentWhen` promotes a
731
- * frontmatter `tagName` (Markdown's does) can express one perfectly well. What it cannot do is
732
- * express one from a `newFileTemplate` that carries no `tagName`, so the seed below supplies it —
733
- * through the format's own serializer, because this module knows no format's syntax.
793
+ * Pages have no line (a page is its own page), a collection whose index has not been read says
794
+ * nothing yet, and a collection with no page says so with the way to add one.
734
795
  */
735
- /** How each document kind's extension is settled. Anything else falls back to the full picker. */
736
- const FORMAT_CHOICE_BY_KIND = new Map<string, FormatChoice>([
737
- ["component", { defaultExt: ".json", docKind: "component", kind: "choose" }],
738
- ["layout", { ext: ".json", kind: "fixed" }],
739
- ["page", { defaultExt: ".json", docKind: "page", kind: "choose" }],
740
- ]);
741
-
742
- /**
743
- * The body a new component starts as, when the reader picked a format rather than `.json`.
744
- *
745
- * `.json` answers `undefined` and takes the shared blank document. A format answers with its OWN
746
- * serialization of `{ tagName }` — never a hand-written `---\ntagName: …\n---`, which would put one
747
- * format's syntax in the Library. Without the `tagName` the file satisfies neither
748
- * `$studio.documentMode.componentWhen` nor the build's tag registration, and is a component nothing
749
- * loads. A stem that is not a valid custom-element name (no hyphen) cannot carry one, so it falls
750
- * back to the format's own template and the author names the tag themselves.
751
- */
752
- async function componentSeed(ext: string, fileName: string): Promise<string | undefined> {
753
- const format = formatByExtension(ext);
754
- if (!format?.capabilities.serialize) {
755
- return undefined;
756
- }
757
- const tagName = fileName.slice(0, fileName.length - ext.length);
758
- if (!/^[a-z][\d.a-z]*-[\d.a-z-]*$/.test(tagName)) {
759
- return undefined;
796
+ export function pageLineFor(
797
+ collection: ContentCollection,
798
+ page: CollectionPage | null | undefined,
799
+ defaultLayout: string | null,
800
+ every: readonly CollectionPage[] = [],
801
+ ): LibraryPageLine {
802
+ const hidden: LibraryPageLine = {
803
+ designLabel: "",
804
+ pattern: "",
805
+ state: "hidden",
806
+ template: "",
807
+ text: "",
808
+ };
809
+ if (collection.kind === "pages") {
810
+ return hidden;
760
811
  }
761
- try {
762
- return await formatSerialize(
763
- format.name,
764
- { children: [], tagName },
765
- { frontmatter: true, mode: "roundtrip" },
766
- );
767
- } catch {
768
- // The serializer is the project's, so its failure is not a reason to refuse the creation —
769
- // The format's own template still produces a file the author can finish by hand.
770
- return undefined;
812
+ if (page === undefined) {
813
+ return { ...hidden, state: "loading", text: "Finding where these entries are published…" };
771
814
  }
772
- }
773
-
774
- export async function createLibraryEntry(key: string): Promise<string | null> {
775
- const entry = libraryNewEntries().find((row) => row.key === key);
776
- if (!entry) {
777
- return null;
815
+ if (page === null) {
816
+ return { ...hidden, state: "none", text: "Entries have no page of their own" };
778
817
  }
779
- let created: string | null;
780
- if (entry.collection === undefined) {
781
- const request: NewFileRequest = {
782
- dir: entry.dir,
783
- format: FORMAT_CHOICE_BY_KIND.get(entry.key) ?? { defaultExt: ".json", kind: "choose" },
784
- source: "Library",
785
- suggestedName: "untitled",
786
- title: `New ${entry.label}`,
818
+ /* Every language, when the view shows every language and the collection has a page in more than
819
+ one: naming only the default language's page under "All" says the others have none. Each is
820
+ named by its language, in the order the index holds them; one template clause when they share
821
+ a template, else each its own. */
822
+ // In the order the project declares its languages, the default first, as every language list is.
823
+ const order = projectLocales();
824
+ const rank = (locale: string | null) => {
825
+ const at = locale === null ? -1 : order.indexOf(locale);
826
+ return at === -1 ? order.length : at;
827
+ };
828
+ const localized = every
829
+ .filter((each) => each.locale !== null)
830
+ .toSorted((a, b) => rank(a.locale) - rank(b.locale));
831
+ if (localized.length > 1) {
832
+ const clauses = new Set(localized.map((each) => templateClause(each, defaultLayout)));
833
+ return {
834
+ designLabel: `Design the ${collection.item} page`,
835
+ pattern: localized.map((each) => `${each.pattern} (${each.locale})`).join(", "),
836
+ state: "page",
837
+ template: clauses.size === 1 ? [...clauses][0]! : "using a template per language",
838
+ text: `Each ${collection.item} has a page in each language at`,
787
839
  };
788
- if (entry.key === "component") {
789
- request.content = componentSeed;
790
- }
791
- created = await createFileIn(request);
792
- } else {
793
- created = await createEntry(entry.collection);
794
- }
795
- if (created === null) {
796
- return null;
797
840
  }
798
- invalidateLibrary();
799
- await loadLibrary();
800
- // `createEntry` has already opened the entry form; opening the file again here would replace that
801
- // Editor with the generic one for the same tab.
802
- if (entry.collection === undefined) {
803
- void openFileInTab(created);
804
- }
805
- return created;
841
+ return {
842
+ designLabel: `Design the ${collection.item} page`,
843
+ pattern: page.pattern,
844
+ state: "page",
845
+ template: templateClause(page, defaultLayout),
846
+ text: `Each ${collection.item} has a page at`,
847
+ };
806
848
  }
807
849
 
808
- // ─── Upload ──────────────────────────────────────────────────────────────────
809
-
810
- /**
811
- * Where an upload lands, and how the author was told.
812
- *
813
- * The active category's directory when it has one — printed on the drop zone, so the destination is
814
- * visible BEFORE the drop. "All" has none, and rather than falling back to a default nobody chose,
815
- * it asks. That fallback is exactly the surprise studio.md §16 exists to refuse: the file arrives,
816
- * the toast says it worked, and it is in a directory the author never picked.
817
- */
818
- export async function resolveUploadDir(): Promise<string | null> {
819
- const named = uploadDirForCategory(libraryView.category);
820
- if (named !== undefined) {
821
- return named;
850
+ /** Open the collection's page in Design: activating it enters Design (§22.1). */
851
+ function designCollectionPage(name: string): void {
852
+ // The page in the language the view is filtered to: the one the reader is looking at.
853
+ const page = peekCollectionPage(name, collectionView(name).locale);
854
+ if (page) {
855
+ void openFileInTab(page.path);
822
856
  }
823
- const chosen = await showPromptDialog("Upload files", {
824
- confirmLabel: "Upload",
825
- message: "The All view has no folder of its own. Choose where these files land.",
826
- placeholder: "public",
827
- validate: (value) => (value.trim() ? "" : "Enter a folder."),
828
- value: "public",
829
- });
830
- return chosen === null ? null : chosen.trim().replace(/\/$/, "");
831
857
  }
832
858
 
833
- async function uploadIntoLibrary(files: FileList | File[]) {
834
- const dir = await resolveUploadDir();
835
- if (dir === null) {
836
- return;
837
- }
838
- await uploadAssets([...files], { dir });
839
- invalidateLibrary();
840
- await loadLibrary();
859
+ /** Write the collection's page, as Add new's own-page option does, then show the new line. */
860
+ async function addCollectionPageFor(name: string): Promise<void> {
861
+ const { addCollectionPage } = await import("../create/add-new-flow");
862
+ await addCollectionPage(name);
841
863
  }
842
864
 
843
865
  // ─── The projection ──────────────────────────────────────────────────────────
@@ -848,25 +870,27 @@ function chip(key: string, label: string, active: boolean): LibraryChip {
848
870
  }
849
871
 
850
872
  /** The sentence a filter that matched nothing prints, naming both facets rather than one. */
851
- function noMatchMessage(): string {
852
- const category = libraryCategory(libraryView.category);
853
- // Both facets in one clause, so a reader who filtered twice is told about both rather than being
854
- // Sent to clear one and find the list still empty.
855
- const scopes = [
856
- category && category.key !== "all" ? category.label : "",
857
- libraryView.locale === "" ? "" : localeLabel(libraryView.locale),
858
- ].filter((scope) => scope !== "");
859
- const where = scopes.length === 0 ? "" : ` in ${scopes.join(" and ")}`;
860
- const term = libraryView.query.trim();
861
- return `No files match${term ? ` “${term}”` : ""}${where}.`;
873
+ function noMatchMessage(collection: ContentCollection, view: CollectionViewState): string {
874
+ const where = `${view.locale === "" ? "" : ` in ${localeLabel(view.locale)}`}${
875
+ view.filter === "" ? "" : ` filtered to ${view.filter}`
876
+ }`;
877
+ const term = view.query.trim();
878
+ return `No ${collection.kind === "pages" ? "pages" : collection.title.toLowerCase()} match${
879
+ term ? ` “${term}”` : ""
880
+ }${where}.`;
862
881
  }
863
882
 
864
- /** Which of the nine things the body draws right now. */
865
- function viewKind(current: LibrarySource, matched: number, total: number): LibraryViewKind {
883
+ /** Which of the eight things the body draws right now. */
884
+ function viewKind(
885
+ current: LibrarySource,
886
+ layout: LibraryLayout,
887
+ matched: number,
888
+ total: number,
889
+ ): LibraryViewKind {
866
890
  if (matched > 0) {
867
- return libraryView.layout;
891
+ return layout;
868
892
  }
869
- if (libraryView.loading || !current.scanned()) {
893
+ if (contentView.loading || !current.scanned()) {
870
894
  return "loading";
871
895
  }
872
896
  if (current.failures().length > 0) {
@@ -875,11 +899,329 @@ function viewKind(current: LibrarySource, matched: number, total: number): Libra
875
899
  return total === 0 ? "empty" : "nomatch";
876
900
  }
877
901
 
902
+ /**
903
+ * A collection's entry files, in scan order. The ONE list both the Content home and the Collections
904
+ * panel read entry summaries for: the summary cache keeps one list per collection, so two
905
+ * differently-ordered lists would each evict the other's read.
906
+ */
907
+ export function collectionEntryPaths(key: string): string[] {
908
+ return librarySource()
909
+ .files()
910
+ .filter((file) => file.collection === key)
911
+ .map((file) => file.path);
912
+ }
913
+
914
+ /**
915
+ * What each entry of a declared collection says about itself (title, status, date), read once
916
+ * through the Collections panel's summaries. Pages and a file-backed collection (whose entries are
917
+ * rows of one file) answer nothing, and so does every entry until the read lands.
918
+ */
919
+ function entryFactsFor(collection: ContentCollection, layout: LibraryLayout): EntryFactsOf {
920
+ if (collection.kind === "pages") {
921
+ return librarySource().scanned() ? pageFactsFor(layout === "table") : entryFactsFrom(new Map());
922
+ }
923
+ if (collection.fileBacked) {
924
+ /* A record's facts are its fields, read with the file by the scan; it is dated by its own date
925
+ field, as an entry file is, so the Calendar never falls back to the one file's mtime. */
926
+ const schema = collectionSchema(collection.key);
927
+ const records = librarySource()
928
+ .files()
929
+ .filter((file) => file.collection === collection.key && file.record);
930
+ return entryFactsFrom(
931
+ new Map(
932
+ records.map((file) => {
933
+ const { fields } = file.record!;
934
+ const dateField = dateFieldOf(schema, fields);
935
+ const date = dateField === null ? "" : entryDay(fields[dateField] ?? "");
936
+ // A record's picture is its image field, relative to the file that holds it.
937
+ const thumb = entryThumb(file.record!.source, fields, schema);
938
+ return [file.path, { date, fields, status: "", thumb, title: file.name }];
939
+ }),
940
+ ),
941
+ );
942
+ }
943
+ if (!librarySource().scanned()) {
944
+ return entryFactsFrom(new Map());
945
+ }
946
+ const summaries = peekEntrySummaries(collection.key, collectionEntryPaths(collection.key));
947
+ const byPath = new Map<string, EntryFacts>(
948
+ (summaries ?? []).map((entry) => [
949
+ entry.path,
950
+ {
951
+ date: entry.date,
952
+ fields: entry.fields,
953
+ status: entry.status,
954
+ thumb: entry.thumb,
955
+ title: entry.title,
956
+ },
957
+ ]),
958
+ );
959
+ return entryFactsFrom(byPath);
960
+ }
961
+
962
+ /**
963
+ * What each page says about itself in the Pages table: its name, as the Site panel gives it (Home,
964
+ * About, "Post page" for a collection's page), its template, and Draft for a page marked one.
965
+ *
966
+ * The name comes from the route, so it is there before any file is read; the template and the draft
967
+ * flag arrive with the page summaries. A page's Status reads Published only when some page in the
968
+ * project is a draft: with none, pages have no draft workflow, and a column of "Published" would be
969
+ * a state nobody defined. Only the Table draws a template or a status, so only the Table reads the
970
+ * pages: Cards, Calendar and Board name a page by its route and read nothing.
971
+ */
972
+ function pageFactsFor(readPages: boolean): EntryFactsOf {
973
+ const paths = collectionEntryPaths(PAGES_COLLECTION);
974
+ const summaries = readPages ? peekPageSummaries(paths) : null;
975
+ const declared = new Set(Object.keys(projectState?.projectConfig?.content ?? {}));
976
+ const fallback = defaultLayoutPath();
977
+ const { i18n } = resolveI18n(projectState?.projectConfig ?? {});
978
+ const anyDraft = [...(summaries?.values() ?? [])].some((page) => page.draft);
979
+ const byPath = new Map<string, EntryFacts>(
980
+ paths.map((path) => {
981
+ const summary = summaries?.get(path);
982
+ const collection = summary?.contentType ?? null;
983
+ const layout = summary === undefined ? undefined : (summary.layout ?? fallback ?? false);
984
+ return [
985
+ path,
986
+ {
987
+ date: "",
988
+ status: summary === undefined || !anyDraft ? "" : summary.draft ? "Draft" : "Published",
989
+ template:
990
+ layout === undefined ? "" : layout === false ? "None" : templateDisplayName(layout),
991
+ title:
992
+ collection !== null && declared.has(collection)
993
+ ? localeMarked(`${singularLabel(collection)} page`, path, i18n)
994
+ : pageLabel(fileToRoute(path), path, i18n),
995
+ },
996
+ ];
997
+ }),
998
+ );
999
+ return entryFactsFrom(byPath);
1000
+ }
1001
+
1002
+ /** Pages in the order the router matches them, the order the Site panel lists them in. */
1003
+ function inRouteOrder(files: readonly LibraryFile[]): readonly LibraryFile[] {
1004
+ return files
1005
+ .map((file) => ({ file, route: fileToRoute(file.path) }))
1006
+ .toSorted((a, b) => compareRoutes(a.route, b.route) || a.file.path.localeCompare(b.file.path))
1007
+ .map(({ file }) => file);
1008
+ }
1009
+
1010
+ /** A declared collection's schema, or null for one that declares none. */
1011
+ function collectionSchema(key: string): ContentTypeSchema | null {
1012
+ return entryCollection(key)?.schema ?? declaredCollectionSchema(key);
1013
+ }
1014
+
1015
+ /** The fields the Table names in a column of their own already: what an entry is called, its date. */
1016
+ function listedElsewhere(schema: ContentTypeSchema | null): ReadonlySet<string> {
1017
+ const skip = new Set<string>();
1018
+ const title = titleFieldOf(schema);
1019
+ const date = declaredDateField(schema);
1020
+ for (const name of [title, date]) {
1021
+ if (name !== null) {
1022
+ skip.add(name);
1023
+ }
1024
+ }
1025
+ return skip;
1026
+ }
1027
+
1028
+ /** Each field's declared string `enum`, the order a Board grouped by it draws its columns in. */
1029
+ function fieldEnums(schema: ContentTypeSchema | null): ReadonlyMap<string, readonly string[]> {
1030
+ const enums = new Map<string, readonly string[]>();
1031
+ for (const [name, field] of Object.entries(schema?.properties ?? {})) {
1032
+ if (Array.isArray(field.enum)) {
1033
+ enums.set(
1034
+ name,
1035
+ field.enum.filter((value) => isString(value)),
1036
+ );
1037
+ }
1038
+ }
1039
+ return enums;
1040
+ }
1041
+
1042
+ /** The field the Filter narrows a collection by, and the values it offers. */
1043
+ export interface FilterField {
1044
+ /** "Status", or the field's own title. */
1045
+ title: string;
1046
+ /** The values offered, in the order the Board stands its columns in. */
1047
+ values: readonly string[];
1048
+ /** Whether an entry with these facts has `value`. */
1049
+ matches: (facts: EntryFacts | undefined, value: string) => boolean;
1050
+ }
1051
+
1052
+ /**
1053
+ * What the Filter narrows by: an entry's **status** where the collection has a draft workflow, else
1054
+ * the field its Board groups by (`boardFieldOf`: the first field with an `enum`, else the first
1055
+ * whose values repeat; a list field by each of its items). The same choice the Board makes, so the
1056
+ * Filter offers exactly the columns the Board draws. `null` when nothing groups the entries.
1057
+ */
1058
+ export function filterFieldOf(
1059
+ facts: readonly (EntryFacts | undefined)[],
1060
+ field: BoardField | null,
1061
+ ): FilterField | null {
1062
+ const statuses = [...new Set(facts.flatMap((entry) => (entry?.status ? [entry.status] : [])))];
1063
+ if (statuses.length > 0) {
1064
+ return {
1065
+ matches: (entry, value) => (entry?.status ?? "") === value,
1066
+ title: "Status",
1067
+ values: statuses.toSorted((a, b) =>
1068
+ a === "Draft" ? -1 : b === "Draft" ? 1 : a.localeCompare(b),
1069
+ ),
1070
+ };
1071
+ }
1072
+ if (!field) {
1073
+ return null;
1074
+ }
1075
+ const held = new Set(
1076
+ facts.flatMap((entry) => fieldValues(entry?.fields?.[field.name], field.list)),
1077
+ );
1078
+ const values =
1079
+ field.order.length > 0
1080
+ ? [...field.order, ...[...held].filter((value) => !field.order.includes(value)).toSorted()]
1081
+ : [...held].toSorted((a, b) => a.localeCompare(b));
1082
+ return {
1083
+ matches: (entry, value) => fieldValues(entry?.fields?.[field.name], field.list).includes(value),
1084
+ title: field.title,
1085
+ values,
1086
+ };
1087
+ }
1088
+
1089
+ /** Everything the home draws in a pane, as one value. */
1090
+ function homeView(panel: ActiveLibraryPane | null, paneId: string): LibraryView {
1091
+ const current = librarySource();
1092
+ const collection = shownCollection();
1093
+ const view = collectionView(collection.key);
1094
+ const { layout } = view;
1095
+ const all = filterLibrary(current.files(), { collection: collection.key, query: "" });
1096
+ const factsOf = entryFactsFor(collection, layout);
1097
+ const facts = all.map((file) => factsOf(file));
1098
+ const schema = collection.kind === "collection" ? collectionSchema(collection.key) : null;
1099
+ const fieldColumns = entryFieldColumns(schema, listedElsewhere(schema));
1100
+ const boardField = boardFieldOf(fieldColumns, fieldEnums(schema), facts);
1101
+ // Pages are listed by route; the Filter is a collection's, over what its entries say.
1102
+ const filterField = collection.kind === "collection" ? filterFieldOf(facts, boardField) : null;
1103
+ const filterValue =
1104
+ filterField !== null && filterField.values.includes(view.filter) ? view.filter : "";
1105
+ const searched = filterLibrary(current.files(), {
1106
+ collection: collection.key,
1107
+ locale: view.locale,
1108
+ query: view.query,
1109
+ });
1110
+ const filtered =
1111
+ filterField === null || filterValue === ""
1112
+ ? searched
1113
+ : searched.filter((file) => filterField.matches(factsOf(file), filterValue));
1114
+ const files = collection.kind === "pages" ? inRouteOrder(filtered) : filtered;
1115
+ const total = all.length;
1116
+ const kind = viewKind(current, layout, files.length, total);
1117
+ const range = windowFor(panel, layout, files.length);
1118
+ const slice = files.slice(range.start, range.end);
1119
+ const failures = current.failures();
1120
+ const locales = libraryLocales(all);
1121
+ const columns = libraryColumns(collection.key, {
1122
+ // A date the schema declares is a column even before an entry fills it in.
1123
+ date: declaredDateField(schema) !== null || facts.some((entry) => (entry?.date ?? "") !== ""),
1124
+ fields: fieldColumns,
1125
+ locale: locales.length > 1,
1126
+ status: facts.some((entry) => (entry?.status ?? "") !== ""),
1127
+ });
1128
+ const live = kind === "cards";
1129
+ const calendar =
1130
+ kind === "calendar" ? calendarView(files, factsOf) : { groups: [], truncated: "" };
1131
+ const pages = collection.kind === "pages";
1132
+ const plural = pages ? "pages" : collection.title.toLowerCase();
1133
+
1134
+ return {
1135
+ bannerState: failures.length > 0 ? "shown" : "hidden",
1136
+ bannerText:
1137
+ failures.length === 0
1138
+ ? ""
1139
+ : `This list is incomplete — ${failures.length} ${
1140
+ failures.length === 1 ? "directory" : "directories"
1141
+ } could not be read (${failures[0]!.dir}${failures.length > 1 ? ", …" : ""}).`,
1142
+ columns: kind === "table" ? columns.map((c) => ({ field: c.field, title: c.title })) : [],
1143
+ emptyDetail:
1144
+ kind === "incomplete"
1145
+ ? `${failures.length} ${
1146
+ failures.length === 1 ? "directory" : "directories"
1147
+ } could not be read, so this is not the same as an empty collection.`
1148
+ : kind === "nomatch"
1149
+ ? `${total} ${total === 1 ? collection.item : plural} in all.`
1150
+ : kind === "empty"
1151
+ ? `New ${collection.item} adds the first one.`
1152
+ : "",
1153
+ emptyMessage:
1154
+ kind === "nomatch"
1155
+ ? noMatchMessage(collection, view)
1156
+ : kind === "empty"
1157
+ ? `No ${plural} yet.`
1158
+ : "",
1159
+ fieldsState: collection.kind === "collection" ? "shown" : "hidden",
1160
+ groups: kind === "board" ? boardView(files, factsOf, boardField) : calendar.groups,
1161
+ layouts: LIBRARY_LAYOUTS.map((name) =>
1162
+ chip(name, LIBRARY_LAYOUT_LABELS[name], layout === name),
1163
+ ),
1164
+ localeOptions: [
1165
+ { label: "All languages", value: "all" },
1166
+ ...locales.map((tag) => ({ label: localeLabel(tag), value: tag })),
1167
+ ],
1168
+ /* The facet's options come from ALL of the collection's files, never from the filtered ones: a
1169
+ picker whose choices collapse to the choice just made cannot be used to make another. One
1170
+ locale — or none — means every file already agrees. */
1171
+ filterOptions:
1172
+ filterField === null
1173
+ ? []
1174
+ : [
1175
+ { label: `Any ${filterField.title.toLowerCase()}`, value: "all" },
1176
+ ...filterField.values.map((value) => ({ label: value, value })),
1177
+ ],
1178
+ filterState: filterField !== null && filterField.values.length > 1 ? "shown" : "hidden",
1179
+ filterValue: filterValue === "" ? "all" : filterValue,
1180
+ localeState: locales.length < 2 ? "hidden" : "shown",
1181
+ localeValue: view.locale === "" ? "all" : view.locale,
1182
+ newLabel: `New ${collection.item}`,
1183
+ // A file-backed collection's entries are rows of its file, added in the file's grid.
1184
+ newState: collection.fileBacked ? "hidden" : "shown",
1185
+ /* ADDED to the grid's own padding rather than replacing it: a bare `padding-top:0px` took the
1186
+ gap under the header away, and the first row of cards sat with its top border under it. */
1187
+ pad: `padding-top:calc(var(--jx-space-4) + ${range.padTop}px);padding-bottom:calc(var(--jx-space-4) + ${range.padBottom}px)`,
1188
+ padBottom: `height:${range.padBottom}px`,
1189
+ padTop: `height:${range.padTop}px`,
1190
+ page: pageLineFor(
1191
+ collection,
1192
+ pages ? null : peekCollectionPage(collection.key, view.locale),
1193
+ defaultLayoutPath(),
1194
+ // Under every language, the page of each; filtered to one, that language's alone.
1195
+ pages || view.locale !== "" ? [] : (peekCollectionPages(collection.key) ?? []),
1196
+ ),
1197
+ query: view.query,
1198
+ region: paneRegion(paneId, "content"),
1199
+ rows:
1200
+ kind === "cards" || kind === "table"
1201
+ ? slice.map((file) =>
1202
+ libraryRow(file, kind === "table" ? columns : [], live, factsOf(file)),
1203
+ )
1204
+ : [],
1205
+ searchLabel: `Search ${plural}`,
1206
+ subtitle: pages ? "Every URL on your site." : "",
1207
+ title: collection.title,
1208
+ truncated: calendar.truncated,
1209
+ truncatedState: calendar.truncated === "" ? "hidden" : "shown",
1210
+ view: kind,
1211
+ };
1212
+ }
1213
+
1214
+ /** The project's default layout, which a page with no `$layout` of its own is drawn in. */
1215
+ function defaultLayoutPath(): string | null {
1216
+ // Read at call time: `projectState` is replaced wholesale on a project switch.
1217
+ return projectState?.projectConfig?.defaults?.layout ?? null;
1218
+ }
1219
+
878
1220
  // ─── Mount ───────────────────────────────────────────────────────────────────
879
1221
 
880
1222
  /**
881
- * Draw the Library into the pane. Re-entrant: a same-tab call while it is live is a no-op, because
882
- * the pane owns its own reactivity from here (the grid/settings/stylebook pattern).
1223
+ * Draw the Content home into the pane. Re-entrant: a same-tab call while it is live is a no-op,
1224
+ * because the pane owns its own reactivity from here (the grid/settings/stylebook pattern).
883
1225
  */
884
1226
  export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
885
1227
  const { paneId, wrap: canvasWrap } = surface;
@@ -901,115 +1243,54 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
901
1243
  }
902
1244
  });
903
1245
 
904
- const view = (): LibraryView => {
905
- const current = librarySource();
906
- const { layout } = libraryView;
907
- const files = filterLibrary(current.files(), {
908
- category: libraryView.category,
909
- locale: libraryView.locale,
910
- query: libraryView.query,
911
- });
912
- const total = current.files().length;
913
- const kind = viewKind(current, files.length, total);
914
- const range = windowFor(panel, layout, files.length);
915
- const slice = files.slice(range.start, range.end);
916
- const failures = current.failures();
917
- const locales = libraryLocales(current.files());
918
- const destination = uploadDirForCategory(libraryView.category);
919
- /* Cards is the only layout that draws a live document render inside a card. A Media tile is a
920
- thumbnail of an ASSET — `library-layouts.ts` says why — so it asks for the glyph instead. */
921
- const live = kind === "cards";
922
- const calendar = kind === "calendar" ? calendarView(files) : { groups: [], truncated: "" };
923
-
924
- return {
925
- bannerState: failures.length > 0 ? "shown" : "hidden",
926
- bannerText:
927
- failures.length === 0
928
- ? ""
929
- : `This list is incomplete — ${failures.length} ${
930
- failures.length === 1 ? "directory" : "directories"
931
- } could not be read (${failures[0]!.dir}${failures.length > 1 ? ", …" : ""}).`,
932
- categories: LIBRARY_CATEGORIES.map((category) =>
933
- chip(category.key, category.label, libraryView.category === category.key),
934
- ),
935
- columns:
936
- kind === "table" ? libraryColumns().map((c) => ({ field: c.field, title: c.title })) : [],
937
- dropActive: panel?.dropActive ? "true" : "false",
938
- dropRegion: paneRegion(paneId, "library/dropZone"),
939
- emptyDetail:
940
- kind === "incomplete"
941
- ? `${failures.length} ${
942
- failures.length === 1 ? "directory" : "directories"
943
- } could not be read, so this is not the same as an empty project.`
944
- : kind === "nomatch"
945
- ? `${total} file(s) in the project.`
946
- : "",
947
- emptyMessage: kind === "nomatch" ? noMatchMessage() : "",
948
- groups: kind === "board" ? boardView(files) : calendar.groups,
949
- layouts: LIBRARY_LAYOUTS.map((name) =>
950
- chip(name, LIBRARY_LAYOUT_LABELS[name], libraryView.layout === name),
951
- ),
952
- localeOptions: [
953
- { label: "All languages", value: "all" },
954
- ...locales.map((tag) => ({ label: localeLabel(tag), value: tag })),
955
- ],
956
- /* The facet's options come from ALL scanned files, never from the filtered ones: a picker
957
- whose choices collapse to the choice just made cannot be used to make another. One locale —
958
- or none — means every file already agrees. */
959
- localeState: locales.length < 2 ? "hidden" : "shown",
960
- localeValue: libraryView.locale === "" ? "all" : libraryView.locale,
961
- pad: `padding-top:${range.padTop}px;padding-bottom:${range.padBottom}px`,
962
- padBottom: `height:${range.padBottom}px`,
963
- padTop: `height:${range.padTop}px`,
964
- query: libraryView.query,
965
- region: paneRegion(paneId, "library"),
966
- rows:
967
- kind === "cards" || kind === "media" || kind === "table"
968
- ? slice.map((file) => libraryRow(file, kind === "table" ? libraryColumns() : [], live))
969
- : [],
970
- truncated: calendar.truncated,
971
- truncatedState: calendar.truncated === "" ? "hidden" : "shown",
972
- uploadAccept: uploadAccept(),
973
- uploadHint:
974
- destination === undefined ? "Upload — asks for a folder" : `Upload into ${destination}/`,
975
- view: kind,
976
- };
977
- };
978
-
979
1246
  const draw = () => {
980
1247
  if (panel !== null && activeIn(paneId) === panel) {
981
- panel.surface.update(view());
1248
+ panel.surface.update(homeView(panel, paneId));
982
1249
  scheduleSettle(panel);
983
1250
  }
984
1251
  };
985
1252
 
986
- const mounted = mountLibrarySurface(canvasWrap, view(), {
1253
+ const mounted = mountLibrarySurface(canvasWrap, homeView(null, paneId), {
1254
+ addPage: () => {
1255
+ void addCollectionPageFor(shownCollection().key);
1256
+ },
987
1257
  contextMenu: (path, x, y) => showLibraryContextMenu(path, x, y),
988
- dragOut: () => {
989
- if (panel && panel.dropActive) {
990
- panel.dropActive = false;
991
- draw();
992
- }
1258
+ designPage: () => {
1259
+ designCollectionPage(shownCollection().key);
993
1260
  },
994
- dragOver: () => {
995
- if (panel && !panel.dropActive) {
996
- panel.dropActive = true;
997
- draw();
998
- }
1261
+ openFields: () => {
1262
+ void runActiveReported(
1263
+ "settings.open",
1264
+ { entry: shownCollection().key, section: "content" },
1265
+ SOURCE,
1266
+ );
999
1267
  },
1000
- drop: (files) => {
1001
- if (panel) {
1002
- panel.dropActive = false;
1003
- }
1004
- draw();
1005
- if (files?.length) {
1006
- void uploadIntoLibrary(files);
1268
+ openGrid: () => {
1269
+ const collection = shownCollection();
1270
+ // A file-backed collection's grid is its file's own: `collection.editInGrid` grids folders.
1271
+ const file = collection.fileBacked ? collectionSourceFile(collection.key) : null;
1272
+ if (file) {
1273
+ void openFileInTab(file);
1274
+ return;
1007
1275
  }
1276
+ void runActiveReported("collection.editInGrid", { name: collection.key }, SOURCE);
1008
1277
  },
1009
1278
  openFile: (path) => {
1010
- void openFileInTab(path);
1279
+ openLibraryFile(fileAt(path) ?? { collection: shownCollectionKey(), path });
1280
+ },
1281
+ openNew: () => {
1282
+ const collection = shownCollection();
1283
+ // The language the view is filtered to is the one the reader is writing in.
1284
+ const { locale } = collectionView(collection.key);
1285
+ const args: CommandArgValues =
1286
+ collection.kind === "pages"
1287
+ ? { kind: "page" }
1288
+ : { collection: collection.key, kind: "entry" };
1289
+ if (locale !== "") {
1290
+ args.locale = locale;
1291
+ }
1292
+ void runActiveReported("file.new", args, SOURCE);
1011
1293
  },
1012
- openNewMenu: (opener) => showLibraryNewMenu(opener),
1013
1294
  previewSlot: (element, path) => {
1014
1295
  if (panel) {
1015
1296
  previewSlot(panel, element, path);
@@ -1017,7 +1298,6 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
1017
1298
  },
1018
1299
  reset: () => {
1019
1300
  setLibrarySearch("");
1020
- setLibraryCategory("all");
1021
1301
  setLibraryLocale("");
1022
1302
  },
1023
1303
  retry: () => {
@@ -1030,26 +1310,16 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
1030
1310
  }
1031
1311
  },
1032
1312
  search: (value) => setLibrarySearch(value),
1033
- setCategory: (key) => setLibraryCategory(key),
1034
- /* SAFETY: the surface's layout buttons pass `$map.item.key`, and the `layouts` state they map is
1313
+ /* SAFETY: the surface's view buttons pass `$map.item.key`, and the `layouts` state they map is
1035
1314
  built from LIBRARY_LAYOUTS in the projection above, so every key is a LibraryLayout. */
1036
1315
  setLayout: (key) => setLibraryLayout(key as LibraryLayout),
1316
+ setFilter: (value) => setLibraryFilter(value === "all" ? "" : value),
1037
1317
  setLocale: (value) => setLibraryLocale(value === "all" ? "" : value),
1038
- upload: (input) => {
1039
- const chosen = input.files;
1040
- // Cleared before the upload starts: a file input that keeps its value refuses the same file
1041
- // Twice, and the picker is this surface's own control rather than a node it found.
1042
- input.value = "";
1043
- if (chosen?.length) {
1044
- void uploadIntoLibrary(chosen);
1045
- }
1046
- },
1047
1318
  });
1048
1319
 
1049
1320
  const scope = effectScope();
1050
1321
  panel = {
1051
1322
  cache: createPreviewCache(),
1052
- dropActive: false,
1053
1323
  observer,
1054
1324
  paneId,
1055
1325
  pending: new WeakMap(),
@@ -1060,6 +1330,17 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
1060
1330
  slots: new Map(),
1061
1331
  surface: mounted,
1062
1332
  tabId: tab.id,
1333
+ unlisten: (() => {
1334
+ // The collection's page and its entries' titles are both read after the first paint.
1335
+ const stopPages = onCollectionPagesRead(() => bump());
1336
+ const stopEntries = onEntrySummariesRead(() => bump());
1337
+ const stopPageFacts = onPageSummariesRead(() => bump());
1338
+ return () => {
1339
+ stopPages();
1340
+ stopEntries();
1341
+ stopPageFacts();
1342
+ };
1343
+ })(),
1063
1344
  wrap: canvasWrap,
1064
1345
  };
1065
1346
  _active.set(paneId, panel);
@@ -1070,18 +1351,29 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
1070
1351
  return;
1071
1352
  }
1072
1353
  // Everything the pane draws from.
1073
- void libraryView.revision;
1074
- void libraryView.category;
1075
- void libraryView.layout;
1076
- void libraryView.query;
1077
- void libraryView.locale;
1078
- void libraryView.loading;
1354
+ void contentView.revision;
1355
+ void contentView.collection;
1356
+ void contentView.loading;
1357
+ const view = collectionView(shownCollectionKey());
1358
+ void view.layout;
1359
+ void view.query;
1360
+ void view.locale;
1079
1361
 
1080
1362
  draw();
1081
1363
 
1082
- if (!scanAttempted && !libraryView.loading) {
1364
+ if (!scanAttempted && !contentView.loading) {
1083
1365
  void loadLibrary();
1084
1366
  }
1085
1367
  });
1086
1368
  });
1087
1369
  }
1370
+
1371
+ /** Forget every collection's view and the scan — tests, and a project switch. */
1372
+ export function resetContentView(): void {
1373
+ contentView.collection = PAGES_COLLECTION;
1374
+ contentView.views = {};
1375
+ contentView.loading = false;
1376
+ source = null;
1377
+ scanAttempted = false;
1378
+ bump();
1379
+ }