@jxsuite/studio 6.1.1 → 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 (785) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/2020-5wtzce5g.js +4 -0
  3. package/dist/chunks/{action-group-2mqby73c.js → action-group-4aan7g6g.js} +3 -3
  4. package/dist/chunks/adapter-5tphg7xe.js +4 -0
  5. package/dist/chunks/add-new-flow-psym14sn.js +4 -0
  6. package/dist/chunks/cf-account-picker-haqazq5r.js +4 -0
  7. package/dist/chunks/cf-settings-h2wrh6hq.js +4 -0
  8. package/dist/chunks/color-area-6rbra5dy.js +4 -0
  9. package/dist/chunks/color-field-gy56c94g.js +4 -0
  10. package/dist/chunks/color-m7r1vk54.js +4 -0
  11. package/dist/chunks/color-slider-sqqdyf6c.js +4 -0
  12. package/dist/chunks/combobox-72h32ejk.js +4 -0
  13. package/dist/chunks/components-r22xca09.js +4 -0
  14. package/dist/chunks/content-source-mmppmcyg.js +4 -0
  15. package/dist/chunks/convert-to-repeater-he70fc2b.js +4 -0
  16. package/dist/chunks/dialog-c4x05vyx.js +4 -0
  17. package/dist/chunks/editor-ccd9jwsa.js +4 -0
  18. package/dist/chunks/entry-commands-6qxz9e8p.js +4 -0
  19. package/dist/chunks/extension-sections-0egb88dm.js +4 -0
  20. package/dist/chunks/file-ops-qnab36ek.js +4 -0
  21. package/dist/chunks/files-rcn56qvm.js +4 -0
  22. package/dist/chunks/format-host-yxz0kjjs.js +4 -0
  23. package/dist/chunks/fs-events-d1qxq4z4.js +4 -0
  24. package/dist/chunks/grid-open-4z1g062t.js +4 -0
  25. package/dist/chunks/grid-view-qg1b5eqx.js +51 -0
  26. package/dist/chunks/icons-63bjtbs7.js +4 -0
  27. package/dist/chunks/iframe-entry-2zggzajv.js +5 -0
  28. package/dist/chunks/iframe-entry-76j8vwwx.js +5 -0
  29. package/dist/chunks/iframe-entry-99nr5zfb.js +5 -0
  30. package/dist/chunks/iframe-entry-pkgmcgp0.js +5 -0
  31. package/dist/chunks/iframe-host-ssv7kyj0.js +4 -0
  32. package/dist/chunks/{index-2veqmnhr.js → index-32eqzv1k.js} +3 -3
  33. package/dist/chunks/index-f0jn7nnp.js +4 -0
  34. package/dist/chunks/{javascript-8nbvs12s.js → javascript-pjp734q4.js} +3 -3
  35. package/dist/chunks/jsonMode-njkvsa02.js +13 -0
  36. package/dist/chunks/jx-validate-ttasrbh7.js +4 -0
  37. package/dist/chunks/layers-bgyr11tr.js +4 -0
  38. package/dist/chunks/layers-panel-7ez66zeq.js +4 -0
  39. package/dist/chunks/listbox-c421cf8c.js +4 -0
  40. package/dist/chunks/lit-html-v9gh687x.js +4 -0
  41. package/dist/chunks/media-library-sq468wts.js +4 -0
  42. package/dist/chunks/media-picker-shfe59pt.js +4 -0
  43. package/dist/chunks/menu-bdpzbfvs.js +4 -0
  44. package/dist/chunks/monaco-binding-vq1hgnn0.js +4 -0
  45. package/dist/chunks/monaco-buffer-3afgwvbp.js +4 -0
  46. package/dist/chunks/monaco-setup-xrd2cbkp.js +4 -0
  47. package/dist/chunks/notify-kzrjr3fv.js +4 -0
  48. package/dist/chunks/number-field-y62xq0ty.js +4 -0
  49. package/dist/chunks/parse-v77xtwv7.js +4 -0
  50. package/dist/chunks/platform-7v4rtd7m.js +4 -0
  51. package/dist/chunks/popover-b89sfej7.js +4 -0
  52. package/dist/chunks/publish-panel-2z9gw8ee.js +4 -0
  53. package/dist/chunks/publish-quick-qx2455cj.js +4 -0
  54. package/dist/chunks/publish-workspace-wrc1dgwx.js +4 -0
  55. package/dist/chunks/quick-search-n0t0eqxb.js +4 -0
  56. package/dist/chunks/references-9547rg5s.js +4 -0
  57. package/dist/chunks/right-panel-wsgnr9pn.js +4 -0
  58. package/dist/chunks/select-rzds841e.js +4 -0
  59. package/dist/chunks/source-control-kwktr9y6.js +4 -0
  60. package/dist/chunks/split-c9zfdgxf.js +4 -0
  61. package/dist/chunks/studio-03ct6yfv.js +9 -0
  62. package/dist/chunks/studio-07rcfje3.js +5 -0
  63. package/dist/chunks/studio-3e54qh39.js +7 -0
  64. package/dist/chunks/studio-3exxhz5g.js +5 -0
  65. package/dist/chunks/studio-3q40e3rj.js +5 -0
  66. package/dist/chunks/studio-3qeh9t0h.js +6 -0
  67. package/dist/chunks/studio-3qqjeep5.js +6 -0
  68. package/dist/chunks/studio-43sd2mam.js +4 -0
  69. package/dist/chunks/studio-4hp8pkyt.js +5 -0
  70. package/dist/chunks/studio-58xg6j2q.js +9 -0
  71. package/dist/chunks/studio-5g0rprdt.js +7 -0
  72. package/dist/chunks/studio-6m380wp7.js +5 -0
  73. package/dist/chunks/studio-80jyh1xh.js +4 -0
  74. package/dist/chunks/studio-83mk4bgg.js +9 -0
  75. package/dist/chunks/{studio-kggey1jt.js → studio-881z1xrx.js} +4 -4
  76. package/dist/chunks/studio-8m0y2qe7.js +5 -0
  77. package/dist/chunks/studio-8tr6cfrc.js +5 -0
  78. package/dist/chunks/studio-8vcvfs7r.js +6 -0
  79. package/dist/chunks/studio-9tmebf0s.js +5 -0
  80. package/dist/chunks/studio-a2bvaptz.js +5 -0
  81. package/dist/chunks/studio-b7y5mr30.js +5 -0
  82. package/dist/chunks/studio-bavb31r0.js +5 -0
  83. package/dist/chunks/studio-bbe4c90a.js +5 -0
  84. package/dist/chunks/studio-c05wdnt3.js +5 -0
  85. package/dist/chunks/studio-ccv2svc6.js +5 -0
  86. package/dist/chunks/studio-d5grtb9e.js +16 -0
  87. package/dist/chunks/studio-df0baea9.js +5 -0
  88. package/dist/chunks/studio-dgk4dqtx.js +7 -0
  89. package/dist/chunks/studio-e1g64q43.js +5 -0
  90. package/dist/chunks/studio-ed1hp6zf.js +6 -0
  91. package/dist/chunks/studio-ed3xx40h.js +7 -0
  92. package/dist/chunks/studio-f14e1zdj.js +5 -0
  93. package/dist/chunks/studio-fbhxxc5f.js +5 -0
  94. package/dist/chunks/studio-fyh0jc96.js +5 -0
  95. package/dist/chunks/studio-gcxk20n7.js +5 -0
  96. package/dist/chunks/studio-jaaz8ce9.js +5 -0
  97. package/dist/chunks/{studio-8grwxtqk.js → studio-jcnsdaep.js} +4 -4
  98. package/dist/chunks/studio-jsgf2q5k.js +7 -0
  99. package/dist/chunks/{studio-1q1sh2ax.js → studio-k3xxbagr.js} +3 -3
  100. package/dist/chunks/studio-k7evxqh4.js +5 -0
  101. package/dist/chunks/studio-kt3pjkwd.js +5 -0
  102. package/dist/chunks/studio-m8g16bgz.js +5 -0
  103. package/dist/chunks/studio-mn0476ag.js +5 -0
  104. package/dist/chunks/studio-mx1smyq0.js +5 -0
  105. package/dist/chunks/studio-n0c5h6mf.js +5 -0
  106. package/dist/chunks/{studio-rv11kfwa.js → studio-nh6d342n.js} +7 -7
  107. package/dist/chunks/studio-nqxqf8p2.js +4 -0
  108. package/dist/chunks/studio-p4nrkvqr.js +5 -0
  109. package/dist/chunks/{studio-h46vtzk8.js → studio-pbxa5fpc.js} +4 -4
  110. package/dist/chunks/studio-pgmvdv7z.js +7 -0
  111. package/dist/chunks/studio-ph84avkf.js +5 -0
  112. package/dist/chunks/{studio-wszjyh0j.js → studio-q0mvbaq6.js} +4 -4
  113. package/dist/chunks/studio-q0tccd43.js +5 -0
  114. package/dist/chunks/studio-q7rtx6zq.js +14 -0
  115. package/dist/chunks/{studio-pre0skq6.js → studio-qgh13jtz.js} +4 -4
  116. package/dist/chunks/studio-qmn1xc24.js +5 -0
  117. package/dist/chunks/studio-qwdqwkj0.js +5 -0
  118. package/dist/chunks/{studio-t6b6qe31.js → studio-r5qy30vx.js} +4 -4
  119. package/dist/chunks/studio-rj9eamf8.js +5 -0
  120. package/dist/chunks/studio-rptgsk96.js +5 -0
  121. package/dist/chunks/studio-rthyg42z.js +928 -0
  122. package/dist/chunks/studio-s19tp1xr.js +5 -0
  123. package/dist/chunks/studio-s32578pn.js +5 -0
  124. package/dist/chunks/studio-svb7yyve.js +5 -0
  125. package/dist/chunks/studio-syreb644.js +5 -0
  126. package/dist/chunks/studio-tf9jryb1.js +10 -0
  127. package/dist/chunks/studio-tfkznv39.js +5 -0
  128. package/dist/chunks/studio-tft6r105.js +5 -0
  129. package/dist/chunks/studio-txf2kysx.js +5 -0
  130. package/dist/chunks/studio-v8867ddv.js +12 -0
  131. package/dist/chunks/studio-vxq7fcvv.js +5 -0
  132. package/dist/chunks/studio-w4rz9hdk.js +5 -0
  133. package/dist/chunks/studio-wbcjbhy1.js +9 -0
  134. package/dist/chunks/studio-wc649wss.js +5 -0
  135. package/dist/chunks/studio-wys23mxz.js +5 -0
  136. package/dist/chunks/studio-x1x167jh.js +5 -0
  137. package/dist/chunks/studio-x9hsze61.js +5 -0
  138. package/dist/chunks/studio-y0n7yxea.js +9 -0
  139. package/dist/chunks/studio-y3ahhdpx.js +5 -0
  140. package/dist/chunks/studio-z97c48fk.js +5 -0
  141. package/dist/chunks/studio-zat62m0c.js +5 -0
  142. package/dist/chunks/studio-zcd2e7yz.js +5 -0
  143. package/dist/chunks/studio-zz9qe3rw.js +64 -0
  144. package/dist/chunks/swatch-group-ewh24n1f.js +4 -0
  145. package/dist/chunks/tabs-f5cw3tgp.js +4 -0
  146. package/dist/chunks/textfield-wewc8rq2.js +4 -0
  147. package/dist/chunks/toast-1gv0h53h.js +4 -0
  148. package/dist/chunks/toast-host-1yg5ar8d.js +4 -0
  149. package/dist/chunks/{toolbar-2nnzqhrj.js → toolbar-44cm5cta.js} +3 -3
  150. package/dist/chunks/tooltip-yypz28gq.js +4 -0
  151. package/dist/chunks/tree-9xa4hm7n.js +4 -0
  152. package/dist/chunks/tsMode-epyfz1hv.js +14 -0
  153. package/dist/chunks/workspace-dz7nxfhv.js +4 -0
  154. package/dist/chunks/workspace-home-xsd7hkjw.js +4 -0
  155. package/dist/chunks/ws-client-bv552sfx.js +4 -0
  156. package/dist/iframe-entry.js +20 -16
  157. package/dist/manifest.json +77 -63
  158. package/dist/studio.js +106 -107
  159. package/dist/workers/ts.worker.js +3 -3
  160. package/package.json +17 -16
  161. package/src/about/about-modal.ts +4 -3
  162. package/src/account/action-flow.ts +45 -38
  163. package/src/account-status.ts +7 -2
  164. package/src/browse/component-examples.ts +272 -0
  165. package/src/browse/library-commands.ts +64 -104
  166. package/src/browse/library-layouts.ts +270 -53
  167. package/src/browse/library-model.ts +194 -134
  168. package/src/browse/library-pane.ts +748 -440
  169. package/src/browse/library-preview.ts +161 -26
  170. package/src/browse/library-source.ts +308 -41
  171. package/src/browse/preview-root.ts +78 -0
  172. package/src/browse/preview-style.ts +155 -0
  173. package/src/canvas/asset-refs.ts +40 -10
  174. package/src/canvas/asset-resolve.ts +19 -23
  175. package/src/canvas/canvas-helpers.ts +7 -6
  176. package/src/canvas/canvas-live-render.ts +260 -77
  177. package/src/canvas/canvas-origin.ts +37 -12
  178. package/src/canvas/canvas-page-context.ts +112 -0
  179. package/src/canvas/canvas-patcher.ts +30 -30
  180. package/src/canvas/canvas-perf.ts +11 -5
  181. package/src/canvas/canvas-render.ts +175 -148
  182. package/src/canvas/canvas-surface.ts +5 -3
  183. package/src/canvas/canvas-utils.ts +147 -61
  184. package/src/canvas/dialog-path.ts +9 -4
  185. package/src/canvas/dialog-state.ts +1 -2
  186. package/src/canvas/diff-marks.ts +9 -16
  187. package/src/canvas/diff-toolbar.ts +8 -7
  188. package/src/canvas/edit-width-drag.ts +4 -1
  189. package/src/canvas/edit-width.ts +1 -3
  190. package/src/canvas/iframe-channel.ts +27 -13
  191. package/src/canvas/iframe-drop.ts +120 -12
  192. package/src/canvas/iframe-editable-root.ts +29 -16
  193. package/src/canvas/iframe-entry.ts +104 -75
  194. package/src/canvas/iframe-eval.ts +73 -49
  195. package/src/canvas/iframe-host.ts +269 -158
  196. package/src/canvas/iframe-inline-edit.ts +49 -30
  197. package/src/canvas/iframe-insert.ts +5 -4
  198. package/src/canvas/iframe-interaction.ts +30 -25
  199. package/src/canvas/iframe-keys.ts +11 -0
  200. package/src/canvas/iframe-overlay.ts +1 -1
  201. package/src/canvas/iframe-patch.ts +25 -21
  202. package/src/canvas/iframe-position.ts +12 -6
  203. package/src/canvas/iframe-protocol.ts +36 -21
  204. package/src/canvas/iframe-render.ts +84 -35
  205. package/src/canvas/iframe-slash.ts +24 -11
  206. package/src/canvas/iframe-subtree.ts +10 -3
  207. package/src/canvas/npm-modules.ts +22 -0
  208. package/src/canvas/path-mapping.ts +19 -12
  209. package/src/canvas/popover-path.ts +7 -3
  210. package/src/canvas/popover-state.ts +1 -2
  211. package/src/canvas/preview-navigate.ts +16 -0
  212. package/src/canvas/repeater-sources.ts +200 -0
  213. package/src/canvas/serialize-scope.ts +13 -5
  214. package/src/canvas/stage-scrollbar.ts +7 -2
  215. package/src/canvas/surface-registry.ts +27 -3
  216. package/src/collab/collab-commands.ts +7 -2
  217. package/src/collab/collab-session.ts +84 -45
  218. package/src/collab/collab-state.ts +4 -3
  219. package/src/collab/monaco-binding.ts +16 -13
  220. package/src/collab/monaco-cursors.ts +5 -2
  221. package/src/commands/app-commands.ts +16 -6
  222. package/src/commands/budget.ts +13 -11
  223. package/src/commands/command-args.ts +103 -71
  224. package/src/commands/context.ts +55 -28
  225. package/src/commands/defaults.ts +69 -45
  226. package/src/commands/keymap.ts +56 -43
  227. package/src/commands/levels.ts +14 -8
  228. package/src/commands/live-context.ts +9 -7
  229. package/src/commands/reference.ts +2 -1
  230. package/src/commands/registry.ts +22 -11
  231. package/src/commands/run-reported.ts +15 -10
  232. package/src/component-props.ts +46 -30
  233. package/src/content/collection-match.ts +11 -9
  234. package/src/content/collection-pages.ts +412 -0
  235. package/src/content/content-collections.ts +270 -0
  236. package/src/content/draft-state.ts +4 -3
  237. package/src/content/entry-commands.ts +54 -62
  238. package/src/content/entry-editor.ts +137 -32
  239. package/src/content/entry-fields.ts +25 -8
  240. package/src/content/entry-inspector.ts +225 -0
  241. package/src/content/entry-model.ts +38 -15
  242. package/src/content/entry-open.ts +80 -0
  243. package/src/content/entry-summaries.ts +440 -0
  244. package/src/content/file-record-ids.ts +80 -0
  245. package/src/content/file-records.ts +125 -0
  246. package/src/content/page-summaries.ts +129 -0
  247. package/src/content/record-entry.ts +201 -0
  248. package/src/content/writing-banner.ts +177 -0
  249. package/src/create/add-new-commands.ts +185 -0
  250. package/src/create/add-new-flow.ts +1116 -0
  251. package/src/create/add-new.ts +1014 -0
  252. package/src/create/element-name.ts +121 -0
  253. package/src/editor/canvas-context-menu.ts +1 -2
  254. package/src/editor/canvas-slash-bridge.ts +3 -6
  255. package/src/editor/content-slice.ts +16 -14
  256. package/src/editor/context-menu.ts +35 -22
  257. package/src/editor/convert-targets.ts +30 -35
  258. package/src/editor/convert-to-component.ts +78 -48
  259. package/src/editor/convert-to-repeater.ts +34 -22
  260. package/src/editor/editable-tags.ts +17 -2
  261. package/src/editor/file-drop-action.ts +29 -22
  262. package/src/editor/inline-edit-apply.ts +79 -17
  263. package/src/editor/inline-edit.ts +76 -50
  264. package/src/editor/inline-format.ts +59 -56
  265. package/src/editor/inline-link.ts +40 -15
  266. package/src/editor/insert-component.ts +91 -0
  267. package/src/editor/insert-zone-action.ts +14 -5
  268. package/src/editor/merge-tags.ts +58 -19
  269. package/src/editor/repeater-scope.ts +39 -28
  270. package/src/editor/shortcuts.ts +76 -42
  271. package/src/editor/slash-menu.ts +52 -14
  272. package/src/files/asset-versions.ts +4 -4
  273. package/src/files/components.ts +89 -20
  274. package/src/files/elements.ts +5 -6
  275. package/src/files/file-ops.ts +92 -41
  276. package/src/files/files.ts +248 -239
  277. package/src/files/fs-events.ts +18 -7
  278. package/src/files/media-meta.ts +97 -2
  279. package/src/files/media-paths.ts +3 -0
  280. package/src/files/media-types.ts +55 -0
  281. package/src/files/media-upload.ts +209 -52
  282. package/src/files/new-file-name.ts +156 -0
  283. package/src/files/serialize-document.ts +15 -5
  284. package/src/format/constraints.ts +1 -1
  285. package/src/format/convert-file.ts +27 -13
  286. package/src/format/format-choices.ts +2 -1
  287. package/src/format/format-host.ts +91 -50
  288. package/src/github/github-auth-status.ts +27 -3
  289. package/src/github/github-auth.ts +21 -11
  290. package/src/github/github-publish.ts +28 -23
  291. package/src/grid/cell-editors.ts +46 -31
  292. package/src/grid/cell-popovers.ts +5 -3
  293. package/src/grid/edit-buffer.ts +10 -9
  294. package/src/grid/grid-controller.ts +25 -23
  295. package/src/grid/grid-layout.ts +21 -5
  296. package/src/grid/grid-open.ts +3 -33
  297. package/src/grid/grid-panel.ts +65 -6
  298. package/src/grid/grid-reveal.ts +34 -0
  299. package/src/grid/grid-source.ts +9 -31
  300. package/src/grid/grid-view.ts +45 -23
  301. package/src/grid/redirects-grid.ts +14 -5
  302. package/src/grid/redirects.ts +16 -14
  303. package/src/grid/schema-columns.ts +25 -19
  304. package/src/grid/sources/connector-source.ts +47 -20
  305. package/src/grid/sources/content-source.ts +100 -31
  306. package/src/grid/sources/csv-file-source.ts +2 -2
  307. package/src/grid/tabulator-tables.d.ts +61 -34
  308. package/src/hosting/document.ts +2 -2
  309. package/src/hosting/stage.ts +7 -7
  310. package/src/i18n/i18n-commands.ts +3 -3
  311. package/src/media/media-commands.ts +181 -0
  312. package/src/media/media-library-model.ts +232 -0
  313. package/src/media/media-library.ts +1283 -0
  314. package/src/media/media-pane.ts +51 -20
  315. package/src/new-project/add-repo-modal.ts +20 -19
  316. package/src/new-project/import-tab.ts +10 -14
  317. package/src/new-project/location-fields.ts +2 -1
  318. package/src/new-project/new-project-modal.ts +12 -9
  319. package/src/packages/jxsuite-update.ts +4 -3
  320. package/src/packages/pull-package-sync.ts +26 -25
  321. package/src/page-params.ts +97 -68
  322. package/src/panels/activity-panel.ts +38 -14
  323. package/src/panels/ai-chat/attached-context.ts +13 -7
  324. package/src/panels/ai-chat/chat-markdown.ts +8 -8
  325. package/src/panels/ai-chat/chat-view.ts +80 -73
  326. package/src/panels/ai-chat/composer.ts +30 -33
  327. package/src/panels/ai-chat/sessions-view.ts +1 -2
  328. package/src/panels/ai-panel.ts +23 -20
  329. package/src/panels/block-action-bar.ts +57 -46
  330. package/src/panels/bottom-dock.ts +5 -3
  331. package/src/panels/canvas-dnd-bridge.ts +36 -26
  332. package/src/panels/chat-panel.ts +6 -6
  333. package/src/panels/collections-panel.ts +306 -0
  334. package/src/panels/component-preview.ts +35 -13
  335. package/src/panels/data-explorer.ts +63 -26
  336. package/src/panels/data-grid.ts +18 -11
  337. package/src/panels/dnd.ts +115 -138
  338. package/src/panels/editors.ts +52 -21
  339. package/src/panels/elements-panel.ts +19 -5
  340. package/src/panels/empty-state.ts +2 -2
  341. package/src/panels/events-panel.ts +84 -69
  342. package/src/panels/formula-workspace.ts +78 -54
  343. package/src/panels/frontmatter-fields.ts +33 -28
  344. package/src/panels/git-diff-open.ts +10 -6
  345. package/src/panels/head-panel.ts +488 -202
  346. package/src/panels/i18n-panel.ts +20 -12
  347. package/src/panels/imports-panel.ts +25 -47
  348. package/src/panels/jump-bar.ts +75 -31
  349. package/src/panels/layers-panel.ts +130 -101
  350. package/src/panels/left-panel.ts +25 -11
  351. package/src/panels/navigator-panels.ts +23 -11
  352. package/src/panels/pane-context.ts +140 -49
  353. package/src/panels/pane-grid.ts +55 -26
  354. package/src/panels/panel-registry.ts +36 -46
  355. package/src/panels/properties-panel.ts +182 -124
  356. package/src/panels/quick-search.ts +53 -31
  357. package/src/panels/right-panel.ts +201 -45
  358. package/src/panels/seo-modal.ts +54 -125
  359. package/src/panels/settings-menu.ts +63 -61
  360. package/src/panels/settings-pane.ts +268 -162
  361. package/src/panels/signals-panel.ts +156 -135
  362. package/src/panels/site-model.ts +367 -0
  363. package/src/panels/site-panel.ts +638 -0
  364. package/src/panels/statement-editor.ts +230 -140
  365. package/src/panels/style-panel.ts +253 -206
  366. package/src/panels/style-utils.ts +114 -51
  367. package/src/panels/stylebook-doc.ts +195 -60
  368. package/src/panels/stylebook-layers-panel.ts +48 -89
  369. package/src/panels/stylebook-panel.ts +60 -27
  370. package/src/panels/styles-panel.ts +327 -0
  371. package/src/panels/tab-drop.ts +27 -11
  372. package/src/panels/tab-strip.ts +143 -49
  373. package/src/platform-errors.ts +57 -43
  374. package/src/platform.ts +59 -31
  375. package/src/platforms/cloud.ts +373 -176
  376. package/src/platforms/default-platform.ts +14 -8
  377. package/src/platforms/devserver.ts +180 -142
  378. package/src/platforms/read-batcher.ts +16 -8
  379. package/src/project-list.ts +2 -1
  380. package/src/publish/deploy-checklist.ts +20 -9
  381. package/src/publish/pages-service.ts +66 -27
  382. package/src/publish/publish-changes.ts +491 -0
  383. package/src/publish/publish-commands.ts +88 -22
  384. package/src/publish/publish-ids.ts +49 -0
  385. package/src/publish/publish-panel.ts +10 -6
  386. package/src/publish/publish-quick.ts +148 -0
  387. package/src/publish/publish-workspace.ts +926 -0
  388. package/src/publish/source-control.ts +907 -0
  389. package/src/recent-projects.ts +15 -8
  390. package/src/resize-edges.ts +26 -36
  391. package/src/services/a11y-report.ts +26 -19
  392. package/src/services/agent-seed.ts +23 -11
  393. package/src/services/ai-ask.ts +9 -12
  394. package/src/services/ai-command-tools.ts +68 -62
  395. package/src/services/ai-import-tools.ts +80 -67
  396. package/src/services/ai-models.ts +17 -13
  397. package/src/services/ai-project-tools.ts +68 -32
  398. package/src/services/ai-session-store.ts +19 -10
  399. package/src/services/ai-system-prompt.ts +46 -20
  400. package/src/services/ai-tools.ts +71 -27
  401. package/src/services/ai-write-report.ts +20 -12
  402. package/src/services/ai-writes.ts +4 -4
  403. package/src/services/automation.ts +74 -51
  404. package/src/services/cem-export.ts +149 -58
  405. package/src/services/cf-settings.ts +4 -4
  406. package/src/services/clock.ts +6 -1
  407. package/src/services/code-services.ts +37 -31
  408. package/src/services/connection.ts +2 -2
  409. package/src/services/context-manager.ts +14 -10
  410. package/src/services/context-resolver.ts +20 -8
  411. package/src/services/data-service.ts +19 -15
  412. package/src/services/directory-picker.ts +20 -14
  413. package/src/services/document-assistant.ts +39 -19
  414. package/src/services/gated-registry.ts +10 -6
  415. package/src/services/harness/chat-reducer.ts +23 -1
  416. package/src/services/import-client.ts +91 -38
  417. package/src/services/import-seed.ts +8 -2
  418. package/src/services/jx-validate.ts +28 -31
  419. package/src/services/live-preview.ts +0 -0
  420. package/src/services/live-surfaces.ts +6 -3
  421. package/src/services/model-uri.ts +10 -2
  422. package/src/services/module-preloads.ts +12 -8
  423. package/src/services/monaco-buffer.ts +43 -24
  424. package/src/services/monaco-lazy.ts +7 -8
  425. package/src/services/monaco-setup.ts +23 -16
  426. package/src/services/notify.ts +44 -17
  427. package/src/services/popover-report.ts +25 -10
  428. package/src/services/preview-eval.ts +20 -8
  429. package/src/services/profile.ts +13 -6
  430. package/src/services/project-path.ts +13 -10
  431. package/src/services/references.ts +0 -0
  432. package/src/services/render-critic.ts +29 -9
  433. package/src/services/settings/definitions.ts +25 -14
  434. package/src/services/settings/kernel.ts +13 -4
  435. package/src/services/settings/write-queue.ts +9 -9
  436. package/src/services/token-lint.ts +12 -10
  437. package/src/services/tool-executor.ts +17 -5
  438. package/src/services/tool-outcomes.ts +44 -24
  439. package/src/services/trusted-types.ts +24 -7
  440. package/src/settings/contexts-section.ts +11 -8
  441. package/src/settings/contributed-section.ts +128 -70
  442. package/src/settings/css-vars-editor.ts +119 -32
  443. package/src/settings/defs-editor.ts +38 -35
  444. package/src/settings/developer-panels.ts +43 -0
  445. package/src/settings/extension-rows.ts +16 -8
  446. package/src/settings/extension-sections.ts +42 -14
  447. package/src/settings/extensions-section.ts +4 -1
  448. package/src/settings/general-settings.ts +101 -28
  449. package/src/settings/locales-section.ts +20 -12
  450. package/src/settings/preferences-dialog.ts +17 -7
  451. package/src/settings/preferences-keymap.ts +7 -5
  452. package/src/settings/preferences-sections.ts +11 -5
  453. package/src/settings/project-sections.ts +28 -11
  454. package/src/settings/schema-field-ui.ts +12 -1
  455. package/src/settings/section-registry.ts +116 -11
  456. package/src/settings/settings-dialog.ts +192 -0
  457. package/src/settings/settings-document.ts +186 -166
  458. package/src/shell/tree.ts +6 -5
  459. package/src/shell.ts +536 -477
  460. package/src/site-context.ts +101 -32
  461. package/src/state.ts +68 -57
  462. package/src/store.ts +54 -34
  463. package/src/studio.ts +219 -116
  464. package/src/style/live-preview.ts +1 -1
  465. package/src/style/project-styles.ts +40 -7
  466. package/src/style/stylebook-specimens.ts +89 -0
  467. package/src/style/stylebook-target.ts +26 -0
  468. package/src/style/tag-inheritance.ts +105 -0
  469. package/src/style/token-ref.ts +5 -4
  470. package/src/style/variables-focus.ts +61 -0
  471. package/src/style/variables-view.ts +235 -0
  472. package/src/surfaces/about.ts +18 -19
  473. package/src/surfaces/add-new.json +1482 -0
  474. package/src/surfaces/add-new.ts +325 -0
  475. package/src/surfaces/add-repo.ts +17 -19
  476. package/src/surfaces/ai-chat.ts +33 -38
  477. package/src/surfaces/ai-credentials-form.ts +6 -16
  478. package/src/surfaces/ai-managed-connect.ts +3 -4
  479. package/src/surfaces/ai-model-picker.ts +3 -5
  480. package/src/surfaces/block-action-bar.ts +22 -11
  481. package/src/surfaces/boot-failure.json +4 -3
  482. package/src/surfaces/boot-failure.ts +4 -5
  483. package/src/surfaces/bottom-dock.json +1 -1
  484. package/src/surfaces/bottom-dock.ts +9 -18
  485. package/src/surfaces/canvas-stage.json +30 -19
  486. package/src/surfaces/canvas-stage.ts +17 -16
  487. package/src/surfaces/cf-account-picker.ts +17 -18
  488. package/src/surfaces/collections-panel.json +615 -0
  489. package/src/surfaces/collections-panel.ts +168 -0
  490. package/src/surfaces/commandbar.json +241 -96
  491. package/src/surfaces/commandbar.ts +145 -105
  492. package/src/surfaces/convert-repeater.ts +17 -19
  493. package/src/surfaces/data-actions.ts +9 -7
  494. package/src/surfaces/dialog.ts +26 -28
  495. package/src/surfaces/diff-toolbar.ts +9 -7
  496. package/src/surfaces/empty-state.json +1 -1
  497. package/src/surfaces/empty-state.ts +7 -10
  498. package/src/surfaces/entry-editor.json +136 -0
  499. package/src/surfaces/entry-editor.ts +35 -11
  500. package/src/surfaces/entry-inspector.json +371 -0
  501. package/src/surfaces/entry-inspector.ts +143 -0
  502. package/src/surfaces/expression-editor.ts +11 -25
  503. package/src/surfaces/files-panel.ts +27 -14
  504. package/src/surfaces/formula-palette.ts +4 -5
  505. package/src/surfaces/github-auth.ts +16 -20
  506. package/src/surfaces/github-publish.ts +17 -22
  507. package/src/surfaces/grid-cell.ts +30 -31
  508. package/src/surfaces/grid-open.ts +10 -14
  509. package/src/surfaces/grid-panel.ts +13 -9
  510. package/src/surfaces/grid-replace.ts +13 -13
  511. package/src/surfaces/grid-views.ts +15 -18
  512. package/src/surfaces/inspector-dock.json +162 -14
  513. package/src/surfaces/inspector-dock.ts +59 -31
  514. package/src/surfaces/jump-bar.ts +5 -6
  515. package/src/surfaces/jxsuite-update.ts +16 -22
  516. package/src/surfaces/library-pane.json +670 -437
  517. package/src/surfaces/library-pane.ts +111 -77
  518. package/src/surfaces/logic-panel.ts +17 -30
  519. package/src/surfaces/logic-workspace.ts +13 -15
  520. package/src/surfaces/media-browser.json +62 -6
  521. package/src/surfaces/media-browser.ts +26 -11
  522. package/src/surfaces/media-field.json +1 -1
  523. package/src/surfaces/media-field.ts +4 -5
  524. package/src/surfaces/media-library.json +1586 -0
  525. package/src/surfaces/media-library.ts +282 -0
  526. package/src/surfaces/media-pane.ts +8 -7
  527. package/src/surfaces/media-pick-dialog.json +71 -0
  528. package/src/surfaces/media-pick-dialog.ts +132 -0
  529. package/src/surfaces/menu.ts +47 -23
  530. package/src/surfaces/navigator-dock.ts +11 -12
  531. package/src/surfaces/new-project.ts +27 -26
  532. package/src/surfaces/pane-context.json +144 -77
  533. package/src/surfaces/pane-context.ts +44 -25
  534. package/src/surfaces/pane-grid.ts +15 -10
  535. package/src/surfaces/panel-activity.ts +5 -6
  536. package/src/surfaces/panel-data.ts +3 -4
  537. package/src/surfaces/panel-deploy-checklist.json +12 -6
  538. package/src/surfaces/panel-deploy-checklist.ts +8 -7
  539. package/src/surfaces/panel-elements.ts +5 -7
  540. package/src/surfaces/panel-i18n.ts +5 -7
  541. package/src/surfaces/panel-imports.ts +6 -13
  542. package/src/surfaces/panel-outline.ts +25 -19
  543. package/src/surfaces/panel-page.json +72 -2
  544. package/src/surfaces/panel-page.ts +49 -25
  545. package/src/surfaces/panel-problems.ts +5 -7
  546. package/src/surfaces/panel-signals.json +1 -1
  547. package/src/surfaces/panel-signals.ts +25 -39
  548. package/src/surfaces/panel-stylebook-layers.json +71 -204
  549. package/src/surfaces/panel-stylebook-layers.ts +5 -29
  550. package/src/surfaces/preferences.json +120 -48
  551. package/src/surfaces/preferences.ts +29 -11
  552. package/src/surfaces/progress-modal.ts +15 -9
  553. package/src/surfaces/properties-panel.json +1 -1
  554. package/src/surfaces/properties-panel.ts +20 -42
  555. package/src/surfaces/publish-quick.json +428 -0
  556. package/src/surfaces/publish-quick.ts +165 -0
  557. package/src/surfaces/publish-workspace.json +2892 -0
  558. package/src/surfaces/publish-workspace.ts +391 -0
  559. package/src/surfaces/publish.ts +18 -18
  560. package/src/surfaces/push-plan.ts +7 -8
  561. package/src/surfaces/rail.json +1 -159
  562. package/src/surfaces/rail.ts +22 -79
  563. package/src/surfaces/reference-field.ts +11 -15
  564. package/src/surfaces/schema-builder.ts +16 -22
  565. package/src/surfaces/schema-form.json +10 -2
  566. package/src/surfaces/schema-form.ts +17 -25
  567. package/src/surfaces/secret-field.ts +9 -6
  568. package/src/surfaces/seo.json +1 -1
  569. package/src/surfaces/seo.ts +21 -15
  570. package/src/surfaces/settings-contexts.ts +9 -7
  571. package/src/surfaces/settings-contributed.json +5 -5
  572. package/src/surfaces/settings-contributed.ts +32 -12
  573. package/src/surfaces/settings-css-vars.json +44 -36
  574. package/src/surfaces/settings-css-vars.ts +82 -16
  575. package/src/surfaces/settings-defs.json +2 -2
  576. package/src/surfaces/settings-defs.ts +17 -21
  577. package/src/surfaces/settings-deploy.json +2 -15
  578. package/src/surfaces/settings-deploy.ts +9 -16
  579. package/src/surfaces/settings-dialog.json +51 -0
  580. package/src/surfaces/settings-dialog.ts +169 -0
  581. package/src/surfaces/settings-extensions.ts +5 -6
  582. package/src/surfaces/settings-head.ts +33 -39
  583. package/src/surfaces/settings-locales.ts +8 -20
  584. package/src/surfaces/settings-overview.json +3 -3
  585. package/src/surfaces/settings-overview.ts +14 -8
  586. package/src/surfaces/settings-packages.ts +8 -7
  587. package/src/surfaces/settings-pane.json +269 -46
  588. package/src/surfaces/settings-pane.ts +88 -36
  589. package/src/surfaces/settings-rawjson.ts +5 -6
  590. package/src/surfaces/shell.json +2 -1
  591. package/src/surfaces/shell.ts +1 -2
  592. package/src/surfaces/site-panel.json +1029 -0
  593. package/src/surfaces/site-panel.ts +233 -0
  594. package/src/surfaces/slash-menu.json +63 -21
  595. package/src/surfaces/slash-menu.ts +42 -9
  596. package/src/surfaces/statements.ts +13 -18
  597. package/src/surfaces/statusbar.ts +56 -29
  598. package/src/surfaces/style-panel.ts +18 -31
  599. package/src/surfaces/stylebook-chrome.ts +3 -5
  600. package/src/surfaces/styles-panel.json +735 -0
  601. package/src/surfaces/styles-panel.ts +195 -0
  602. package/src/surfaces/styles-variables.json +149 -0
  603. package/src/surfaces/styles-variables.ts +145 -0
  604. package/src/surfaces/tab-strip.json +4 -4
  605. package/src/surfaces/tab-strip.ts +14 -13
  606. package/src/surfaces/target-line.ts +6 -7
  607. package/src/surfaces/welcome.ts +6 -8
  608. package/src/surfaces/writing-banner.json +94 -0
  609. package/src/surfaces/writing-banner.ts +80 -0
  610. package/src/tabs/bare-repeaters.ts +101 -0
  611. package/src/tabs/patch-ops.ts +4 -3
  612. package/src/tabs/project-config.ts +32 -21
  613. package/src/tabs/selection.ts +36 -8
  614. package/src/tabs/tab.ts +99 -48
  615. package/src/tabs/transact.ts +98 -53
  616. package/src/types.ts +53 -43
  617. package/src/ui/ai-credentials-form.ts +10 -13
  618. package/src/ui/ai-model-picker.ts +3 -3
  619. package/src/ui/cf-account-picker.ts +7 -9
  620. package/src/ui/color-selector.ts +9 -5
  621. package/src/ui/dynamic-slot.ts +7 -8
  622. package/src/ui/expression-editor.ts +112 -105
  623. package/src/ui/form-controls.ts +104 -56
  624. package/src/ui/formula-catalog.ts +239 -230
  625. package/src/ui/formula-chips.ts +33 -18
  626. package/src/ui/layers.ts +57 -31
  627. package/src/ui/media-picker.ts +300 -47
  628. package/src/ui/popover-surface.ts +20 -12
  629. package/src/ui/progress-modal.ts +13 -8
  630. package/src/ui/regions.ts +14 -12
  631. package/src/ui/schema-form.ts +372 -97
  632. package/src/ui/surface.ts +71 -8
  633. package/src/ui/timing.ts +9 -0
  634. package/src/ui/value-source.ts +44 -31
  635. package/src/ui/virtual-window.ts +29 -3
  636. package/src/utils/base64.ts +6 -4
  637. package/src/utils/edit-display.ts +366 -111
  638. package/src/utils/element-selectors.ts +3 -2
  639. package/src/utils/google-fonts.ts +15 -14
  640. package/src/utils/inherited-style.ts +18 -20
  641. package/src/utils/link-target.ts +1 -1
  642. package/src/utils/local-day.ts +18 -0
  643. package/src/utils/preview-format.ts +25 -3
  644. package/src/utils/strip-events.ts +49 -22
  645. package/src/utils/studio-utils.ts +29 -14
  646. package/src/view.ts +2 -3
  647. package/src/workspace/pane-derive.ts +14 -16
  648. package/src/workspace/session.ts +81 -50
  649. package/src/workspace/tab-workspace.ts +137 -0
  650. package/src/workspace/workspace-back.ts +189 -0
  651. package/src/workspace/workspace-home.ts +133 -0
  652. package/src/workspace/workspace-ids.ts +129 -0
  653. package/src/workspace/workspace-switch.ts +372 -0
  654. package/src/workspace/workspace.ts +165 -47
  655. package/styles/shell-frame.css +30 -7
  656. package/styles/shell-frame.json +15 -3
  657. package/dist/chunks/2020-va2wwra4.js +0 -4
  658. package/dist/chunks/adapter-f93pafd7.js +0 -4
  659. package/dist/chunks/cf-account-picker-af1h381v.js +0 -4
  660. package/dist/chunks/cf-settings-125tdm0w.js +0 -4
  661. package/dist/chunks/color-area-jqhna8zy.js +0 -4
  662. package/dist/chunks/color-field-embq5ctg.js +0 -4
  663. package/dist/chunks/color-fpezvnkt.js +0 -4
  664. package/dist/chunks/color-slider-1qmw7wqy.js +0 -4
  665. package/dist/chunks/combobox-773hdzyj.js +0 -4
  666. package/dist/chunks/components-ax2f1fnn.js +0 -4
  667. package/dist/chunks/content-source-108aqtk1.js +0 -4
  668. package/dist/chunks/convert-to-repeater-ry5rmppr.js +0 -4
  669. package/dist/chunks/dialog-mtm9fq5t.js +0 -4
  670. package/dist/chunks/editor-pfgdmkb0.js +0 -4
  671. package/dist/chunks/entry-commands-t0k1q0kx.js +0 -4
  672. package/dist/chunks/extension-sections-6mwh2hf8.js +0 -4
  673. package/dist/chunks/file-ops-meej5mqf.js +0 -4
  674. package/dist/chunks/files-44vp8a2s.js +0 -4
  675. package/dist/chunks/format-host-7q1petb9.js +0 -4
  676. package/dist/chunks/fs-events-h2mwnxaz.js +0 -4
  677. package/dist/chunks/git-panel-zngbymvd.js +0 -4
  678. package/dist/chunks/grid-view-sx9vynsp.js +0 -51
  679. package/dist/chunks/icons-p44xhjf0.js +0 -4
  680. package/dist/chunks/iframe-entry-f7mqjwpm.js +0 -5
  681. package/dist/chunks/iframe-entry-haw7jkvz.js +0 -5
  682. package/dist/chunks/iframe-entry-z6qrytv3.js +0 -5
  683. package/dist/chunks/iframe-host-qk3ycq87.js +0 -4
  684. package/dist/chunks/index-wkyfjeqz.js +0 -4
  685. package/dist/chunks/jsonMode-s8kt9dsj.js +0 -13
  686. package/dist/chunks/jx-validate-psgd7vsx.js +0 -4
  687. package/dist/chunks/layers-panel-1kq2jyfk.js +0 -4
  688. package/dist/chunks/layers-xd92fg8d.js +0 -4
  689. package/dist/chunks/listbox-s8fkmnq8.js +0 -4
  690. package/dist/chunks/lit-html-ac4cwwr4.js +0 -4
  691. package/dist/chunks/media-picker-0svzkdyb.js +0 -4
  692. package/dist/chunks/menu-37d07z9m.js +0 -4
  693. package/dist/chunks/menu-5c55ea3d.js +0 -4
  694. package/dist/chunks/monaco-binding-z6r70pdt.js +0 -4
  695. package/dist/chunks/monaco-buffer-fz6gq5y7.js +0 -4
  696. package/dist/chunks/monaco-setup-q4axkgj7.js +0 -4
  697. package/dist/chunks/notify-0xgtn8q1.js +0 -4
  698. package/dist/chunks/number-field-gw7sq8rn.js +0 -4
  699. package/dist/chunks/parse-897fvqah.js +0 -4
  700. package/dist/chunks/platform-v1qctbe6.js +0 -4
  701. package/dist/chunks/popover-8htra4er.js +0 -4
  702. package/dist/chunks/publish-panel-ym1fmgq5.js +0 -4
  703. package/dist/chunks/quick-search-a54e6t5n.js +0 -4
  704. package/dist/chunks/references-gmqp82fs.js +0 -4
  705. package/dist/chunks/right-panel-v7t9awbk.js +0 -4
  706. package/dist/chunks/section-registry-vhyf3zsr.js +0 -4
  707. package/dist/chunks/select-n3v7gwas.js +0 -4
  708. package/dist/chunks/split-pff8f3ze.js +0 -4
  709. package/dist/chunks/studio-0bhx5hz6.js +0 -5
  710. package/dist/chunks/studio-0k1ddnyy.js +0 -6
  711. package/dist/chunks/studio-1b04pd0j.js +0 -64
  712. package/dist/chunks/studio-4dwbj6gb.js +0 -5
  713. package/dist/chunks/studio-4tanh828.js +0 -5
  714. package/dist/chunks/studio-55h4mstj.js +0 -4
  715. package/dist/chunks/studio-5tnvjkz4.js +0 -5
  716. package/dist/chunks/studio-60dqgb0c.js +0 -5
  717. package/dist/chunks/studio-61dp09qg.js +0 -9
  718. package/dist/chunks/studio-63epcrj2.js +0 -5
  719. package/dist/chunks/studio-74k29dy9.js +0 -5
  720. package/dist/chunks/studio-7vg6jcd3.js +0 -5
  721. package/dist/chunks/studio-7zx31knw.js +0 -9
  722. package/dist/chunks/studio-8bxk97km.js +0 -5
  723. package/dist/chunks/studio-933e30n4.js +0 -5
  724. package/dist/chunks/studio-98wnwhbg.js +0 -5
  725. package/dist/chunks/studio-9mccjwjh.js +0 -14
  726. package/dist/chunks/studio-a2ke1e4t.js +0 -5
  727. package/dist/chunks/studio-e3a2a6v3.js +0 -4
  728. package/dist/chunks/studio-e81q1vb9.js +0 -5
  729. package/dist/chunks/studio-epgrmqqm.js +0 -5
  730. package/dist/chunks/studio-ery42sqr.js +0 -4
  731. package/dist/chunks/studio-g1shz58g.js +0 -5
  732. package/dist/chunks/studio-gf0fz03c.js +0 -5
  733. package/dist/chunks/studio-gjtpe62q.js +0 -6
  734. package/dist/chunks/studio-gkskhg7m.js +0 -5
  735. package/dist/chunks/studio-gx4dnjzd.js +0 -5
  736. package/dist/chunks/studio-j03zga3y.js +0 -9
  737. package/dist/chunks/studio-jhpxeckz.js +0 -5
  738. package/dist/chunks/studio-k01r7ykp.js +0 -5
  739. package/dist/chunks/studio-kk7h30ne.js +0 -7
  740. package/dist/chunks/studio-kt1zqney.js +0 -5
  741. package/dist/chunks/studio-m3cwhvye.js +0 -5
  742. package/dist/chunks/studio-mcayt6hz.js +0 -5
  743. package/dist/chunks/studio-myjcghv3.js +0 -5
  744. package/dist/chunks/studio-n8bxwc3y.js +0 -4
  745. package/dist/chunks/studio-ngm8gvpb.js +0 -5
  746. package/dist/chunks/studio-nj5a01jy.js +0 -928
  747. package/dist/chunks/studio-pcgz8heb.js +0 -5
  748. package/dist/chunks/studio-qcsq0t15.js +0 -5
  749. package/dist/chunks/studio-qkstvve9.js +0 -5
  750. package/dist/chunks/studio-qv1pwd3c.js +0 -5
  751. package/dist/chunks/studio-rc6dvxjb.js +0 -7
  752. package/dist/chunks/studio-s8dmmz66.js +0 -5
  753. package/dist/chunks/studio-sp5xzbj4.js +0 -6
  754. package/dist/chunks/studio-szsjbj0g.js +0 -5
  755. package/dist/chunks/studio-t1xzawhr.js +0 -16
  756. package/dist/chunks/studio-tdcprbqb.js +0 -5
  757. package/dist/chunks/studio-vrmcjf9g.js +0 -7
  758. package/dist/chunks/studio-vyc0b4he.js +0 -10
  759. package/dist/chunks/studio-w0cgf6jg.js +0 -5
  760. package/dist/chunks/studio-w3hskzqk.js +0 -5
  761. package/dist/chunks/studio-xgrgcdyh.js +0 -9
  762. package/dist/chunks/studio-xz2yxfcb.js +0 -5
  763. package/dist/chunks/studio-y6n42tgs.js +0 -5
  764. package/dist/chunks/studio-yb560vxr.js +0 -5
  765. package/dist/chunks/studio-yn9f8vtx.js +0 -5
  766. package/dist/chunks/studio-yw05w8f0.js +0 -7
  767. package/dist/chunks/studio-z16sgavc.js +0 -5
  768. package/dist/chunks/studio-zey7zsdw.js +0 -12
  769. package/dist/chunks/studio-zyqn40w4.js +0 -5
  770. package/dist/chunks/swatch-group-8qh9a3bm.js +0 -4
  771. package/dist/chunks/tabs-jwthg40w.js +0 -4
  772. package/dist/chunks/textfield-spy3hc77.js +0 -4
  773. package/dist/chunks/toast-4d35zd64.js +0 -4
  774. package/dist/chunks/toast-host-8sjmfed3.js +0 -4
  775. package/dist/chunks/tooltip-fd6g9xy3.js +0 -4
  776. package/dist/chunks/tree-h19v1jfz.js +0 -4
  777. package/dist/chunks/tsMode-2frah6h1.js +0 -14
  778. package/dist/chunks/workspace-zt81g1ms.js +0 -4
  779. package/dist/chunks/ws-client-n2bh4hkf.js +0 -4
  780. package/src/panels/frontmatter-panel.ts +0 -701
  781. package/src/panels/git-panel.ts +0 -1120
  782. package/src/surfaces/doc-header.json +0 -776
  783. package/src/surfaces/doc-header.ts +0 -231
  784. package/src/surfaces/git-panel.json +0 -1880
  785. package/src/surfaces/git-panel.ts +0 -272
@@ -1,75 +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";
41
+ import type { PromptDialogOptions } from "../ui/layers";
38
42
  import { openMenu } from "../surfaces/menu";
39
- import { rectOf } from "../utils/geometry";
40
43
  import { getPlatform } from "../platform";
44
+ import { projectState, renderOnly } from "../store";
41
45
  import { invalidateUsages } from "../services/references";
42
46
  import { confirmFileDelete, renamePromptMessage } from "../files/file-ops";
43
- import { createFileIn, openFileInTab } from "../files/files";
44
- import { createEntry } from "../content/entry-commands";
45
- import { formatByExtension, formatSerialize } from "../format/format-host";
46
- import { entryCollections } from "../content/entry-model";
47
- import { uploadAccept, uploadAssets } from "../files/media-upload";
48
- 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";
49
78
  import {
50
- LIBRARY_CATEGORIES,
51
79
  LIBRARY_LAYOUTS,
52
80
  LIBRARY_LAYOUT_LABELS,
53
81
  PREVIEW_LAYOUTS,
54
82
  filterLibrary,
55
- libraryCategory,
56
83
  libraryLocales,
57
- uploadDirForCategory,
84
+ projectLocales,
58
85
  } from "./library-model";
59
- import { createLibrarySource, libraryColumns } from "./library-source";
86
+ import { createLibrarySource, entryFieldColumns, libraryColumns } from "./library-source";
60
87
  import { createPreviewCache, createPreviewObserver, previewFor } from "./library-preview";
88
+ import { mountPreview, shownPreview } from "./preview-root";
61
89
  import { computeWindow } from "../ui/virtual-window";
62
90
  import { paneRegion } from "../ui/regions";
63
- 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";
64
101
  import { mountLibrarySurface } from "../surfaces/library-pane";
65
102
  import type { ActivityHandle } from "../panels/activity-panel";
66
- import type { FormatChoice } from "../files/files";
103
+ import type { ContentCollection } from "../content/content-collections";
104
+ import type { CollectionPage } from "../content/collection-pages";
67
105
  import type { EffectScope } from "@vue/reactivity";
68
106
  import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
69
107
  import type { LibraryFile, LibraryLayout } from "./library-model";
108
+ import type { BoardField, EntryFacts, EntryFactsOf } from "./library-layouts";
70
109
  import type { LibrarySource } from "./library-source";
110
+ import type { CommandArgValues } from "../commands/command-args";
71
111
  import type {
72
112
  LibraryChip,
113
+ LibraryPageLine,
73
114
  LibrarySurfaceHandle,
74
115
  LibraryView,
75
116
  LibraryViewKind,
@@ -80,8 +121,8 @@ import type { CanvasSurface } from "../canvas/canvas-surface";
80
121
 
81
122
  // ─── View state ──────────────────────────────────────────────────────────────
82
123
 
83
- export interface LibraryViewState {
84
- category: string;
124
+ /** One collection's own view: how it is arranged, what it is filtered to. */
125
+ export interface CollectionViewState {
85
126
  layout: LibraryLayout;
86
127
  query: string;
87
128
  /**
@@ -91,30 +132,86 @@ export interface LibraryViewState {
91
132
  * matches what {@link LibraryFile.locale} carries.
92
133
  */
93
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>;
94
148
  /** Bumped to force a repaint from a non-reactive source (a scroll, a finished scan). */
95
149
  revision: number;
96
150
  /** A scan is running. Distinct from "scanned and empty", which is what the old view conflated. */
97
151
  loading: boolean;
98
152
  }
99
153
 
154
+ /** The view a collection starts in: Table, the shape both Content mockups open on. */
155
+ const DEFAULT_LAYOUT: LibraryLayout = "table";
156
+
100
157
  /**
101
- * The Library's view state, reactive and module-scoped.
158
+ * The Content home's state, reactive and module-scoped.
102
159
  *
103
- * Module-scoped rather than per-tab because there is one Library per window (its tab id carries no
104
- * category — see `grid-source.ts`), and because the commands that write it must be able to do so
105
- * whether or not the pane is currently mounted: `library.setCategory` run from the palette while
106
- * 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.
107
164
  */
108
- export const libraryView: LibraryViewState = reactive({
109
- category: "all",
110
- layout: "cards",
165
+ export const contentView: ContentViewState = reactive({
166
+ collection: PAGES_COLLECTION,
111
167
  loading: false,
112
- locale: "",
113
- query: "",
114
168
  revision: 0,
169
+ views: {},
115
170
  });
116
171
 
117
- /** 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}. */
118
215
  let source: LibrarySource | null = null;
119
216
 
120
217
  /**
@@ -129,33 +226,54 @@ let scanAttempted = false;
129
226
 
130
227
  /** The source, creating it on first use. */
131
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
+ }
132
241
  source ??= createLibrarySource();
133
242
  return source;
134
243
  }
135
244
 
136
- /** Repaint whatever is mounted. */
245
+ /** Repaint whatever is mounted, the Collections panel's counts included. */
137
246
  function bump() {
138
- libraryView.revision += 1;
247
+ contentView.revision += 1;
139
248
  }
140
249
 
141
- export function setLibraryCategory(key: string): void {
142
- 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);
143
254
  bump();
255
+ renderOnly("leftPanel");
144
256
  }
145
257
 
146
- export function setLibraryLayout(layout: LibraryLayout): void {
147
- libraryView.layout = layout;
258
+ export function setLibraryLayout(layout: LibraryLayout, key: string = shownCollectionKey()): void {
259
+ collectionView(key).layout = layout;
148
260
  bump();
149
261
  }
150
262
 
151
- export function setLibrarySearch(query: string): void {
152
- 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;
153
271
  bump();
154
272
  }
155
273
 
156
274
  /** Show one language, or `""` for all of them. A setter, so "" is a value and not an absence. */
157
- export function setLibraryLocale(locale: string): void {
158
- libraryView.locale = locale;
275
+ export function setLibraryLocale(locale: string, key: string = shownCollectionKey()): void {
276
+ collectionView(key).locale = locale;
159
277
  bump();
160
278
  }
161
279
 
@@ -163,7 +281,7 @@ export function setLibraryLocale(locale: string): void {
163
281
  * Drop the scan and the rendered previews.
164
282
  *
165
283
  * Called by every surface that changes the project's file set — an upload, a creation, a rename, a
166
- * 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.
167
285
  *
168
286
  * The SLOT table survives, and that is the difference between this and a teardown: clearing the
169
287
  * cache detaches every rendered preview from the box it was in, and those boxes are still on
@@ -179,13 +297,45 @@ export function invalidateLibrary(): void {
179
297
  bump();
180
298
  }
181
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
+
182
324
  // ─── Loading ─────────────────────────────────────────────────────────────────
183
325
 
184
326
  /** A scan slower than this earns an Activity row; a fast one must not litter the dock. */
185
327
  export const SCAN_ACTIVITY_DELAY_MS = 600;
186
328
 
187
329
  /** Deduplication key for the scan Problem, so a failing watcher is one row and not sixty. */
188
- 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
+ }
189
339
 
190
340
  /**
191
341
  * Run the scan and report it honestly.
@@ -198,13 +348,13 @@ const SCAN_PROBLEM_KEY = "library.scan";
198
348
  async function loadLibrary(): Promise<void> {
199
349
  const current = librarySource();
200
350
  scanAttempted = true;
201
- libraryView.loading = true;
351
+ contentView.loading = true;
202
352
  bump();
203
353
 
204
- const slow: { handle: ActivityHandle | null } = { handle: null };
354
+ const slow: SlowScanActivity = { handle: null };
205
355
  const timer = setTimeout(() => {
206
356
  slow.handle = beginActivity({
207
- source: "Library",
357
+ source: SOURCE,
208
358
  status: "Reading the project's directories…",
209
359
  title: "Scan project files",
210
360
  });
@@ -221,11 +371,11 @@ async function loadLibrary(): Promise<void> {
221
371
  // A Problem, not a toast: the list on screen is wrong until this is fixed, and the fix is a
222
372
  // Command the row can run.
223
373
  notify.error(summary, {
224
- action: "library.refresh",
374
+ action: "content.refresh",
225
375
  detail,
226
376
  key: SCAN_PROBLEM_KEY,
227
377
  path: failures[0]!.dir,
228
- source: "Library",
378
+ source: SOURCE,
229
379
  tier: "problem",
230
380
  });
231
381
  slow.handle?.setStatus(summary);
@@ -238,21 +388,23 @@ async function loadLibrary(): Promise<void> {
238
388
  slow.handle.fail(message);
239
389
  } else {
240
390
  notify.error(message, {
241
- action: "library.refresh",
391
+ action: "content.refresh",
242
392
  detail: errorMessage(error),
243
393
  key: SCAN_PROBLEM_KEY,
244
- source: "Library",
394
+ source: SOURCE,
245
395
  tier: "problem",
246
396
  });
247
397
  }
248
398
  } finally {
249
399
  clearTimeout(timer);
250
- libraryView.loading = false;
400
+ contentView.loading = false;
251
401
  bump();
402
+ // The Collections panel draws the counts this scan just produced.
403
+ renderOnly("leftPanel");
252
404
  }
253
405
  }
254
406
 
255
- /** Re-scan from scratch — the Retry behind the Problem, and the toolbar's Refresh. */
407
+ /** Re-scan from scratch — the Retry behind the Problem. */
256
408
  export async function refreshLibrary(): Promise<void> {
257
409
  source = null;
258
410
  scanAttempted = false;
@@ -265,7 +417,7 @@ export async function refreshLibrary(): Promise<void> {
265
417
  // ─── The mounted pane ────────────────────────────────────────────────────────
266
418
 
267
419
  interface ActiveLibraryPane {
268
- /** 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. */
269
421
  paneId: string;
270
422
  tabId: string;
271
423
  scope: EffectScope;
@@ -287,30 +439,26 @@ interface ActiveLibraryPane {
287
439
  * and a scroll through a long list would read each document several times over.
288
440
  */
289
441
  pendingPaths: Set<string>;
290
- /** Whether a drag is over the body. Held here so a dragover does not recompute the window. */
291
- dropActive: boolean;
292
442
  /** The coalesced post-repaint pass; see {@link scheduleSettle}. */
293
443
  settleTimer: ReturnType<typeof setTimeout> | null;
444
+ /** Stops listening for the collection-pages index. */
445
+ unlisten: () => void;
294
446
  }
295
447
 
296
448
  /**
297
- * The Library mounted in each pane, keyed by pane id.
449
+ * The home mounted in each pane, keyed by pane id.
298
450
  *
299
- * ONE instance per pane, not per window. It was a module-level `let active`, which is a fact about
300
- * the shell having had one stage — and the failure it produced the moment two are drawn is not
301
- * cosmetic: pane B mounting a Library called `detachLibraryPane()`, destroying pane A's
302
- * `IntersectionObserver`, preview cache and effect scope while pane A's DOM was still on screen.
303
- * `canvas-render.ts`'s `resetCanvasView` calls the detach UNCONDITIONALLY on every empty pane, so
304
- * 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.
305
453
  */
306
454
  const _active = new Map<string, ActiveLibraryPane>();
307
455
 
308
- /** The Library mounted in a pane, or null. */
456
+ /** The home mounted in a pane, or null. */
309
457
  function activeIn(paneId: string): ActiveLibraryPane | null {
310
458
  return _active.get(paneId) ?? null;
311
459
  }
312
460
 
313
- /** 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. */
314
462
  export function libraryPaneMounted(paneId: string, tab: Tab): boolean {
315
463
  const panel = activeIn(paneId);
316
464
  return (
@@ -318,7 +466,7 @@ export function libraryPaneMounted(paneId: string, tab: Tab): boolean {
318
466
  );
319
467
  }
320
468
 
321
- /** 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. */
322
470
  export function detachLibraryPane(paneId: string): void {
323
471
  const panel = _active.get(paneId);
324
472
  if (!panel) {
@@ -328,6 +476,7 @@ export function detachLibraryPane(paneId: string): void {
328
476
  clearTimeout(panel.settleTimer);
329
477
  panel.settleTimer = null;
330
478
  }
479
+ panel.unlisten();
331
480
  panel.observer.destroy();
332
481
  panel.cache.clear();
333
482
  panel.slots.clear();
@@ -371,8 +520,8 @@ async function fillPreview(panel: ActiveLibraryPane, path: string) {
371
520
  return;
372
521
  }
373
522
  const slot = panel.slots.get(path);
374
- if (slot?.isConnected && !slot.firstElementChild) {
375
- slot.append(rendered);
523
+ if (slot?.isConnected && !shownPreview(slot)) {
524
+ mountPreview(slot, rendered);
376
525
  }
377
526
  } finally {
378
527
  panel.pendingPaths.delete(path);
@@ -386,11 +535,11 @@ function tendSlot(panel: ActiveLibraryPane, slot: HTMLElement, path: string): vo
386
535
  // Drift to the tail of the LRU, get evicted, get detached, and be re-rendered on the next
387
536
  // Repaint: the 300-page measurement showed 560 reads for 300 documents until this line moved.
388
537
  const cached = panel.cache.get(path);
389
- if (slot.firstElementChild) {
538
+ if (shownPreview(slot)) {
390
539
  return;
391
540
  }
392
541
  if (cached) {
393
- slot.append(cached);
542
+ mountPreview(slot, cached);
394
543
  return;
395
544
  }
396
545
  if (panel.pendingPaths.has(path)) {
@@ -414,20 +563,11 @@ function previewSlot(panel: ActiveLibraryPane, slot: HTMLElement, path: string):
414
563
  /**
415
564
  * The pass that runs once the document has reconciled, coalesced onto a task of its own.
416
565
  *
417
- * It exists because a document repaint is not a moment this module is present at: writing the
418
- * projection returns immediately and the runtime's keyed arrays reconcile a microtask later, so
419
- * "the window has moved" is only true one task after the write. Two things have to happen then and
420
- * cannot happen before.
421
- *
422
- * The first is releasing observations. A card outside the new window is now detached, and an
423
- * observation of a detached node can never fire again: it is pure cost — the browser walks it on
424
- * every scroll frame — and it pins the card's whole subtree. The intersect callback cannot be the
425
- * only release path, because a card flicked past before it ever intersected is never reported.
426
- *
427
- * The second is tending the slots that ARE on screen. A row the reconciler kept keeps its preview
428
- * box, so nothing re-announces it — and an LRU eviction detaches the render that was inside it. So
429
- * every visible slot is touched here: the touch is what keeps it at the head of the LRU, and the
430
- * 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.
431
571
  */
432
572
  function scheduleSettle(panel: ActiveLibraryPane): void {
433
573
  if (panel.settleTimer !== null) {
@@ -439,7 +579,7 @@ function scheduleSettle(panel: ActiveLibraryPane): void {
439
579
  return;
440
580
  }
441
581
  panel.observer.releaseDetached();
442
- const previews = PREVIEW_LAYOUTS.has(libraryView.layout);
582
+ const previews = PREVIEW_LAYOUTS.has(collectionView().layout);
443
583
  for (const [path, slot] of panel.slots) {
444
584
  if (!slot.isConnected) {
445
585
  panel.slots.delete(path);
@@ -452,14 +592,34 @@ function scheduleSettle(panel: ActiveLibraryPane): void {
452
592
  }, 0);
453
593
  }
454
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
+
455
615
  // ─── Context menu ────────────────────────────────────────────────────────────
456
616
 
457
617
  /**
458
- * The menu the Library has up, whichever it is.
618
+ * The per-file menu the home has up, if any.
459
619
  *
460
- * One handle for both the per-file menu and the New menu, because only one of them can be open: the
461
- * kit's menu is a popover on the platform's own top layer, and opening a second while the first is
462
- * 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.
463
623
  */
464
624
  let _menu: MenuHandle | null = null;
465
625
 
@@ -477,16 +637,11 @@ function fileAt(path: string): LibraryFile | null {
477
637
  );
478
638
  }
479
639
 
480
- /**
481
- * The per-file menu, opened at the pointer.
482
- *
483
- * The rows are the `menu` surface's own projections, so the Library's Delete is drawn, keyed and
484
- * dismissed exactly as the element menu's is — including the edge clamping, which was a hand-rolled
485
- * `requestAnimationFrame` measurement here and is the kit's popover's job.
486
- */
640
+ /** The per-file menu, opened at the pointer. Region `content`. */
487
641
  function showLibraryContextMenu(path: string, x: number, y: number): void {
488
642
  const file = fileAt(path);
489
- 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) {
490
645
  return;
491
646
  }
492
647
  closeLibraryMenu();
@@ -512,10 +667,10 @@ function showLibraryContextMenu(path: string, x: number, y: number): void {
512
667
  }
513
668
  },
514
669
  origin: { x, y },
515
- region: "library",
670
+ region: "content",
516
671
  rows: [
517
672
  row("open", "Open", () => {
518
- void openFileInTab(file.path);
673
+ openLibraryFile(file);
519
674
  }),
520
675
  row(
521
676
  "rename",
@@ -540,57 +695,24 @@ function showLibraryContextMenu(path: string, x: number, y: number): void {
540
695
  });
541
696
  }
542
697
 
543
- /** The New menu, opened under its own button. A second press closes it: the button is a toggle. */
544
- function showLibraryNewMenu(opener: HTMLElement): void {
545
- if (_menu) {
546
- closeLibraryMenu();
547
- return;
548
- }
549
- const entries = libraryNewEntries();
550
- _menu = openMenu({
551
- label: "New",
552
- onClosed: (handle) => {
553
- if (_menu === handle) {
554
- _menu = null;
555
- }
556
- },
557
- opener,
558
- place: () => {
559
- const box = rectOf(opener);
560
- return { x: box.left, y: box.bottom + 4 };
561
- },
562
- region: "library-new",
563
- rows: entries.map((entry, index) => ({
564
- destructive: false,
565
- disabled: false,
566
- /* The document kinds and the collections are two different creation flows, so the boundary
567
- between them is drawn rather than left to be inferred from the folder names. */
568
- dividerAbove:
569
- entry.collection !== undefined && entries[index - 1]?.collection === undefined && index > 0,
570
- id: entry.key,
571
- title: `${entry.label} — ${entry.dir}/`,
572
- })),
573
- run: (id) => {
574
- void createLibraryEntry(id);
575
- },
576
- });
577
- }
578
-
579
698
  /**
580
- * 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.
581
700
  *
582
701
  * A rename here rewrites references exactly as one in the sidebar does, and two dialogs that
583
702
  * disagreed about that is the divergence the shared helper exists to prevent.
584
703
  */
585
704
  async function renameLibraryFile(file: LibraryFile) {
586
705
  const message = await renamePromptMessage(file.path);
587
- const newName = await showPromptDialog("Rename", {
706
+ const prompt: PromptDialogOptions = {
588
707
  confirmLabel: "Rename",
589
- ...(message === undefined ? {} : { message }),
590
708
  select: "stem",
591
709
  validate: (value) => (value.trim() ? "" : "Enter a file name."),
592
710
  value: file.name,
593
- });
711
+ };
712
+ if (message !== undefined) {
713
+ prompt.message = message;
714
+ }
715
+ const newName = await showPromptDialog("Rename", prompt);
594
716
  if (!newName || newName === file.name) {
595
717
  return;
596
718
  }
@@ -607,7 +729,7 @@ async function renameLibraryFile(file: LibraryFile) {
607
729
  notify.error(`Could not rename ${file.name}.`, {
608
730
  detail: errorMessage(error),
609
731
  path: file.path,
610
- source: "Library",
732
+ source: SOURCE,
611
733
  });
612
734
  }
613
735
  }
@@ -629,7 +751,7 @@ async function duplicateLibraryFile(file: LibraryFile) {
629
751
  notify.error(`Could not duplicate ${file.name}.`, {
630
752
  detail: errorMessage(error),
631
753
  path: file.path,
632
- source: "Library",
754
+ source: SOURCE,
633
755
  });
634
756
  }
635
757
  }
@@ -649,181 +771,95 @@ async function deleteLibraryFile(file: LibraryFile) {
649
771
  notify.error(`Could not delete ${file.name}.`, {
650
772
  detail: errorMessage(error),
651
773
  path: file.path,
652
- source: "Library",
774
+ source: SOURCE,
653
775
  });
654
776
  }
655
777
  }
656
778
 
657
- // ─── Creation ────────────────────────────────────────────────────────────────
658
-
659
- /** What the New menu offers: the three document kinds, plus one row per creatable collection. */
660
- export interface LibraryNewEntry {
661
- key: string;
662
- label: string;
663
- dir: string;
664
- /**
665
- * The content collection this row creates an entry IN, or undefined for a document kind.
666
- *
667
- * Present is the whole difference between the two creation paths below — a collection row is
668
- * `content/`'s job, because only that module knows the collection's extension and its schema.
669
- */
670
- collection?: string;
671
- }
779
+ // ─── The collection's page ───────────────────────────────────────────────────
672
780
 
673
- /**
674
- * The new-entity rows.
675
- *
676
- * Collections come from `content/entry-model.ts` rather than from `project.json` directly, so the
677
- * menu offers exactly the collections an entry can be created in. The predecessor read the raw
678
- * `content` map and derived a directory from the type NAME when `source` was absent, which produced
679
- * a row for a collection that has no entry directory at all and a row for a single-file (CSV)
680
- * catalogue, whose entries are rows and not files. Both created a file nothing would ever load.
681
- */
682
- export function libraryNewEntries(): LibraryNewEntry[] {
683
- const rows: LibraryNewEntry[] = [
684
- { dir: "pages", key: "page", label: "Page" },
685
- { dir: "layouts", key: "layout", label: "Layout" },
686
- { dir: "components", key: "component", label: "Component" },
687
- ];
688
- for (const collection of entryCollections()) {
689
- rows.push({
690
- collection: collection.name,
691
- dir: collection.dir,
692
- key: `collection:${collection.name}`,
693
- label: collection.name.charAt(0).toUpperCase() + collection.name.slice(1),
694
- });
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";
695
785
  }
696
- return rows;
786
+ const layout = page.layout ?? defaultLayout;
787
+ return layout ? `using ${templateDisplayName(layout)}` : "with no template";
697
788
  }
698
789
 
699
790
  /**
700
- * Create one entity and open it.
701
- *
702
- * Two paths, because a collection entry is not a blank document. A **collection** row goes through
703
- * `content/entry-commands.ts`'s `createEntry`, which supplies the collection's extension (so the
704
- * field asks for a display name and the file is actually matched by the collection it was created
705
- * in) and a body seeded from the collection's **schema defaults** — the entry is valid the moment
706
- * it exists, rather than a pile of absent required fields — and then opens it in the entry FORM,
707
- * which is the editor that collection's fields belong to. Both of those are the reason
708
- * `createEntry` is called rather than reimplemented; a second seeder here would drift from the one
709
- * the palette's `content.newEntry` uses within a release.
710
- *
711
- * A **document kind** gets the format PICKER, which is the same argument the verbatim name field
712
- * used to make: a page may be a `.md` as easily as a `.json`, so the extension is a real choice —
713
- * now offered rather than left to be typed from memory. A collection's extension is not a choice;
714
- * it is the collection's, and `createEntry` supplies it.
715
- *
716
- * **A layout is not a choice either, and that is not a policy.** Both readers of a layout parse it
717
- * as JSON and neither dispatches through the format registry — `site/layout-resolver.ts` throws and
718
- * `site-context.ts` returns null — so a `.md` layout is a file the build cannot load. There is no
719
- * `"layout"` document kind for a format to declare, so this is stated here rather than derived.
720
- *
721
- * A **component** keeps the picker: a format whose `$studio.documentMode.componentWhen` promotes a
722
- * frontmatter `tagName` (Markdown's does) can express one perfectly well. What it cannot do is
723
- * express one from a `newFileTemplate` that carries no `tagName`, so the seed below supplies it —
724
- * through the format's own serializer, because this module knows no format's syntax.
725
- */
726
- /** How each document kind's extension is settled. Anything else falls back to the full picker. */
727
- const FORMAT_CHOICE_BY_KIND: Record<string, FormatChoice> = {
728
- component: { defaultExt: ".json", docKind: "component", kind: "choose" },
729
- layout: { ext: ".json", kind: "fixed" },
730
- page: { defaultExt: ".json", docKind: "page", kind: "choose" },
731
- };
732
-
733
- /**
734
- * The body a new component starts as, when the reader picked a format rather than `.json`.
791
+ * The header line: where a collection's entries are published (§22.5).
735
792
  *
736
- * `.json` answers `undefined` and takes the shared blank document. A format answers with its OWN
737
- * serialization of `{ tagName }` — never a hand-written `---\ntagName: …\n---`, which would put one
738
- * format's syntax in the Library. Without the `tagName` the file satisfies neither
739
- * `$studio.documentMode.componentWhen` nor the build's tag registration, and is a component nothing
740
- * loads. A stem that is not a valid custom-element name (no hyphen) cannot carry one, so it falls
741
- * back to the format's own template and the author names the tag themselves.
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.
742
795
  */
743
- async function componentSeed(ext: string, fileName: string): Promise<string | undefined> {
744
- const format = formatByExtension(ext);
745
- if (!format?.capabilities.serialize) {
746
- return undefined;
747
- }
748
- const tagName = fileName.slice(0, fileName.length - ext.length);
749
- if (!/^[a-z][\d.a-z]*-[\d.a-z-]*$/.test(tagName)) {
750
- return undefined;
751
- }
752
- try {
753
- return await formatSerialize(
754
- format.name,
755
- { children: [], tagName },
756
- { frontmatter: true, mode: "roundtrip" },
757
- );
758
- } catch {
759
- // The serializer is the project's, so its failure is not a reason to refuse the creation —
760
- // The format's own template still produces a file the author can finish by hand.
761
- 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;
762
811
  }
763
- }
764
-
765
- export async function createLibraryEntry(key: string): Promise<string | null> {
766
- const entry = libraryNewEntries().find((row) => row.key === key);
767
- if (!entry) {
768
- return null;
812
+ if (page === undefined) {
813
+ return { ...hidden, state: "loading", text: "Finding where these entries are published…" };
769
814
  }
770
- const created =
771
- entry.collection === undefined
772
- ? await createFileIn({
773
- ...(entry.key === "component" ? { content: componentSeed } : {}),
774
- dir: entry.dir,
775
- format: FORMAT_CHOICE_BY_KIND[entry.key] ?? { defaultExt: ".json", kind: "choose" },
776
- source: "Library",
777
- suggestedName: "untitled",
778
- title: `New ${entry.label}`,
779
- })
780
- : await createEntry(entry.collection);
781
- if (created === null) {
782
- return null;
815
+ if (page === null) {
816
+ return { ...hidden, state: "none", text: "Entries have no page of their own" };
783
817
  }
784
- invalidateLibrary();
785
- await loadLibrary();
786
- // `createEntry` has already opened the entry form; opening the file again here would replace that
787
- // Editor with the generic one for the same tab.
788
- if (entry.collection === undefined) {
789
- void openFileInTab(created);
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`,
839
+ };
790
840
  }
791
- 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
+ };
792
848
  }
793
849
 
794
- // ─── Upload ──────────────────────────────────────────────────────────────────
795
-
796
- /**
797
- * Where an upload lands, and how the author was told.
798
- *
799
- * The active category's directory when it has one — printed on the drop zone, so the destination is
800
- * visible BEFORE the drop. "All" has none, and rather than falling back to a default nobody chose,
801
- * it asks. That fallback is exactly the surprise studio.md §16 exists to refuse: the file arrives,
802
- * the toast says it worked, and it is in a directory the author never picked.
803
- */
804
- export async function resolveUploadDir(): Promise<string | null> {
805
- const named = uploadDirForCategory(libraryView.category);
806
- if (named !== undefined) {
807
- 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);
808
856
  }
809
- const chosen = await showPromptDialog("Upload files", {
810
- confirmLabel: "Upload",
811
- message: "The All view has no folder of its own. Choose where these files land.",
812
- placeholder: "public",
813
- validate: (value) => (value.trim() ? "" : "Enter a folder."),
814
- value: "public",
815
- });
816
- return chosen === null ? null : chosen.trim().replace(/\/$/, "");
817
857
  }
818
858
 
819
- async function uploadIntoLibrary(files: FileList | File[]) {
820
- const dir = await resolveUploadDir();
821
- if (dir === null) {
822
- return;
823
- }
824
- await uploadAssets([...files], { dir });
825
- invalidateLibrary();
826
- 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);
827
863
  }
828
864
 
829
865
  // ─── The projection ──────────────────────────────────────────────────────────
@@ -834,25 +870,27 @@ function chip(key: string, label: string, active: boolean): LibraryChip {
834
870
  }
835
871
 
836
872
  /** The sentence a filter that matched nothing prints, naming both facets rather than one. */
837
- function noMatchMessage(): string {
838
- const category = libraryCategory(libraryView.category);
839
- // Both facets in one clause, so a reader who filtered twice is told about both rather than being
840
- // Sent to clear one and find the list still empty.
841
- const scopes = [
842
- category && category.key !== "all" ? category.label : "",
843
- libraryView.locale === "" ? "" : localeLabel(libraryView.locale),
844
- ].filter((scope) => scope !== "");
845
- const where = scopes.length === 0 ? "" : ` in ${scopes.join(" and ")}`;
846
- const term = libraryView.query.trim();
847
- 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}.`;
848
881
  }
849
882
 
850
- /** Which of the nine things the body draws right now. */
851
- 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 {
852
890
  if (matched > 0) {
853
- return libraryView.layout;
891
+ return layout;
854
892
  }
855
- if (libraryView.loading || !current.scanned()) {
893
+ if (contentView.loading || !current.scanned()) {
856
894
  return "loading";
857
895
  }
858
896
  if (current.failures().length > 0) {
@@ -861,11 +899,329 @@ function viewKind(current: LibrarySource, matched: number, total: number): Libra
861
899
  return total === 0 ? "empty" : "nomatch";
862
900
  }
863
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
+
864
1220
  // ─── Mount ───────────────────────────────────────────────────────────────────
865
1221
 
866
1222
  /**
867
- * Draw the Library into the pane. Re-entrant: a same-tab call while it is live is a no-op, because
868
- * 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).
869
1225
  */
870
1226
  export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
871
1227
  const { paneId, wrap: canvasWrap } = surface;
@@ -887,115 +1243,54 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
887
1243
  }
888
1244
  });
889
1245
 
890
- const view = (): LibraryView => {
891
- const current = librarySource();
892
- const { layout } = libraryView;
893
- const files = filterLibrary(current.files(), {
894
- category: libraryView.category,
895
- locale: libraryView.locale,
896
- query: libraryView.query,
897
- });
898
- const total = current.files().length;
899
- const kind = viewKind(current, files.length, total);
900
- const range = windowFor(panel, layout, files.length);
901
- const slice = files.slice(range.start, range.end);
902
- const failures = current.failures();
903
- const locales = libraryLocales(current.files());
904
- const destination = uploadDirForCategory(libraryView.category);
905
- /* Cards is the only layout that draws a live document render inside a card. A Media tile is a
906
- thumbnail of an ASSET — `library-layouts.ts` says why — so it asks for the glyph instead. */
907
- const live = kind === "cards";
908
- const calendar = kind === "calendar" ? calendarView(files) : { groups: [], truncated: "" };
909
-
910
- return {
911
- bannerState: failures.length > 0 ? "shown" : "hidden",
912
- bannerText:
913
- failures.length === 0
914
- ? ""
915
- : `This list is incomplete — ${failures.length} ${
916
- failures.length === 1 ? "directory" : "directories"
917
- } could not be read (${failures[0]!.dir}${failures.length > 1 ? ", …" : ""}).`,
918
- categories: LIBRARY_CATEGORIES.map((category) =>
919
- chip(category.key, category.label, libraryView.category === category.key),
920
- ),
921
- columns:
922
- kind === "table" ? libraryColumns().map((c) => ({ field: c.field, title: c.title })) : [],
923
- dropActive: panel?.dropActive ? "true" : "false",
924
- dropRegion: paneRegion(paneId, "library/dropZone"),
925
- emptyDetail:
926
- kind === "incomplete"
927
- ? `${failures.length} ${
928
- failures.length === 1 ? "directory" : "directories"
929
- } could not be read, so this is not the same as an empty project.`
930
- : kind === "nomatch"
931
- ? `${total} file(s) in the project.`
932
- : "",
933
- emptyMessage: kind === "nomatch" ? noMatchMessage() : "",
934
- groups: kind === "board" ? boardView(files) : calendar.groups,
935
- layouts: LIBRARY_LAYOUTS.map((name) =>
936
- chip(name, LIBRARY_LAYOUT_LABELS[name], libraryView.layout === name),
937
- ),
938
- localeOptions: [
939
- { label: "All languages", value: "all" },
940
- ...locales.map((tag) => ({ label: localeLabel(tag), value: tag })),
941
- ],
942
- /* The facet's options come from ALL scanned files, never from the filtered ones: a picker
943
- whose choices collapse to the choice just made cannot be used to make another. One locale —
944
- or none — means every file already agrees. */
945
- localeState: locales.length < 2 ? "hidden" : "shown",
946
- localeValue: libraryView.locale === "" ? "all" : libraryView.locale,
947
- pad: `padding-top:${range.padTop}px;padding-bottom:${range.padBottom}px`,
948
- padBottom: `height:${range.padBottom}px`,
949
- padTop: `height:${range.padTop}px`,
950
- query: libraryView.query,
951
- region: paneRegion(paneId, "library"),
952
- rows:
953
- kind === "cards" || kind === "media" || kind === "table"
954
- ? slice.map((file) => libraryRow(file, kind === "table" ? libraryColumns() : [], live))
955
- : [],
956
- truncated: calendar.truncated,
957
- truncatedState: calendar.truncated === "" ? "hidden" : "shown",
958
- uploadAccept: uploadAccept(),
959
- uploadHint:
960
- destination === undefined ? "Upload — asks for a folder" : `Upload into ${destination}/`,
961
- view: kind,
962
- };
963
- };
964
-
965
1246
  const draw = () => {
966
1247
  if (panel !== null && activeIn(paneId) === panel) {
967
- panel.surface.update(view());
1248
+ panel.surface.update(homeView(panel, paneId));
968
1249
  scheduleSettle(panel);
969
1250
  }
970
1251
  };
971
1252
 
972
- const mounted = mountLibrarySurface(canvasWrap, view(), {
1253
+ const mounted = mountLibrarySurface(canvasWrap, homeView(null, paneId), {
1254
+ addPage: () => {
1255
+ void addCollectionPageFor(shownCollection().key);
1256
+ },
973
1257
  contextMenu: (path, x, y) => showLibraryContextMenu(path, x, y),
974
- dragOut: () => {
975
- if (panel && panel.dropActive) {
976
- panel.dropActive = false;
977
- draw();
978
- }
1258
+ designPage: () => {
1259
+ designCollectionPage(shownCollection().key);
979
1260
  },
980
- dragOver: () => {
981
- if (panel && !panel.dropActive) {
982
- panel.dropActive = true;
983
- draw();
984
- }
1261
+ openFields: () => {
1262
+ void runActiveReported(
1263
+ "settings.open",
1264
+ { entry: shownCollection().key, section: "content" },
1265
+ SOURCE,
1266
+ );
985
1267
  },
986
- drop: (files) => {
987
- if (panel) {
988
- panel.dropActive = false;
989
- }
990
- draw();
991
- if (files?.length) {
992
- 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;
993
1275
  }
1276
+ void runActiveReported("collection.editInGrid", { name: collection.key }, SOURCE);
994
1277
  },
995
1278
  openFile: (path) => {
996
- 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);
997
1293
  },
998
- openNewMenu: (opener) => showLibraryNewMenu(opener),
999
1294
  previewSlot: (element, path) => {
1000
1295
  if (panel) {
1001
1296
  previewSlot(panel, element, path);
@@ -1003,7 +1298,6 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
1003
1298
  },
1004
1299
  reset: () => {
1005
1300
  setLibrarySearch("");
1006
- setLibraryCategory("all");
1007
1301
  setLibraryLocale("");
1008
1302
  },
1009
1303
  retry: () => {
@@ -1016,24 +1310,16 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
1016
1310
  }
1017
1311
  },
1018
1312
  search: (value) => setLibrarySearch(value),
1019
- setCategory: (key) => setLibraryCategory(key),
1313
+ /* SAFETY: the surface's view buttons pass `$map.item.key`, and the `layouts` state they map is
1314
+ built from LIBRARY_LAYOUTS in the projection above, so every key is a LibraryLayout. */
1020
1315
  setLayout: (key) => setLibraryLayout(key as LibraryLayout),
1316
+ setFilter: (value) => setLibraryFilter(value === "all" ? "" : value),
1021
1317
  setLocale: (value) => setLibraryLocale(value === "all" ? "" : value),
1022
- upload: (input) => {
1023
- const chosen = input.files;
1024
- // Cleared before the upload starts: a file input that keeps its value refuses the same file
1025
- // Twice, and the picker is this surface's own control rather than a node it found.
1026
- input.value = "";
1027
- if (chosen?.length) {
1028
- void uploadIntoLibrary(chosen);
1029
- }
1030
- },
1031
1318
  });
1032
1319
 
1033
1320
  const scope = effectScope();
1034
1321
  panel = {
1035
1322
  cache: createPreviewCache(),
1036
- dropActive: false,
1037
1323
  observer,
1038
1324
  paneId,
1039
1325
  pending: new WeakMap(),
@@ -1044,6 +1330,17 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
1044
1330
  slots: new Map(),
1045
1331
  surface: mounted,
1046
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
+ })(),
1047
1344
  wrap: canvasWrap,
1048
1345
  };
1049
1346
  _active.set(paneId, panel);
@@ -1054,18 +1351,29 @@ export function renderLibraryMode(surface: CanvasSurface, tab: Tab): void {
1054
1351
  return;
1055
1352
  }
1056
1353
  // Everything the pane draws from.
1057
- void libraryView.revision;
1058
- void libraryView.category;
1059
- void libraryView.layout;
1060
- void libraryView.query;
1061
- void libraryView.locale;
1062
- 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;
1063
1361
 
1064
1362
  draw();
1065
1363
 
1066
- if (!scanAttempted && !libraryView.loading) {
1364
+ if (!scanAttempted && !contentView.loading) {
1067
1365
  void loadLibrary();
1068
1366
  }
1069
1367
  });
1070
1368
  });
1071
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
+ }