@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,8 +1,9 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * The Navigator's Page panel — everything the open document says about itself before anybody reads
4
- * its body: its frontmatter, the layout it is poured into, its title, description, viewport and
5
- * icon, its OpenGraph card, and the raw `$head` tags no structured control owns.
3
+ * The Page panel — the Inspector's first tab (§6) — everything the open document says about itself
4
+ * before anybody reads its body: its frontmatter, the layout it is poured into, its title,
5
+ * description, viewport and icon, its OpenGraph card, and the raw `$head` tags no structured
6
+ * control owns.
6
7
  *
7
8
  * This is the FLOW. The markup is `surfaces/panel-page.json` and the scope that feeds it is
8
9
  * `surfaces/panel-page.ts`; what stays here is every decision — what a `$head` entry means, which
@@ -13,27 +14,27 @@
13
14
  * read at submit time, which is a spelling a document does not have: a binding writes only when the
14
15
  * SCOPE moves, so every field's setter states what the control now holds before anything is decided
15
16
  * about it, and clearing one after a successful add is then a real change the runtime carries back.
16
- * `sync()` — not `renderLeftPanel()` — is what an echo calls: it assigns into the standing scope in
17
- * the same turn, where a Navigator repaint is a frame away and would rebuild far more than one
18
- * field.
17
+ * `sync()` — not a dock repaint — is what an echo calls: it assigns into the standing scope in the
18
+ * same turn, where a dock repaint is a frame away and would rebuild far more than one field.
19
19
  *
20
- * **What a frontmatter row looks like is `panels/frontmatter-fields.ts`'s answer.** The Document
21
- * Header card draws the same field set from the same schemas; the two surfaces deciding
22
- * independently that a `$ref` is a picker or that an array is a comma-separated line is how they
23
- * came to disagree about `title` in the first place.
20
+ * **What a frontmatter row looks like is `panels/frontmatter-fields.ts`'s answer.** The Page tab is
21
+ * the one surface that edits a page's frontmatter (the Document Header card that once drew the same
22
+ * field set above the canvas is gone, studio.md §4.2), and keeping the row vocabulary in its own
23
+ * module is what stopped two surfaces disagreeing about `title`.
24
24
  *
25
25
  * The other half of this module has no surface at all: the merged-`$head` preview model that
26
- * `panels/seo-modal.ts` and the Document Header card both read. It is pure, and pinned against
26
+ * `panels/seo-modal.ts` reads. It is pure, and pinned against
27
27
  * `packages/compiler/src/site/head-merger.ts` by `tests/head-panel.test.ts`.
28
28
  */
29
29
 
30
- import { nothing } from "lit-html";
31
- import { projectState, renderOnly } from "../store";
30
+ import { isString } from "@jxsuite/schema/guards";
31
+ import { withPageDescription } from "@jxsuite/site/head-merger";
32
+ import { projectState, renderOnly, updateSession } from "../store";
32
33
  import type { DirEntry, JsonValue } from "../types";
33
34
  import { activeTab } from "../workspace/workspace";
34
35
  import { activeRegistry } from "../commands/active-registry";
35
36
  import { clearProblems, notify } from "../services/notify";
36
- import { registerPanel } from "./panel-registry";
37
+ import type { NotifyOptions } from "../services/notify";
37
38
  import { mutateUpdateFrontmatter, transact, transactDoc } from "../tabs/transact";
38
39
  import { collectFmFields, projectFmField } from "./frontmatter-fields";
39
40
  import { isGoogleFontEntry, isGoogleFontPreconnect } from "../utils/google-fonts";
@@ -43,19 +44,28 @@ import { pageRoute } from "./tab-strip";
43
44
  import { LIVE_PREVIEW } from "../ui/timing";
44
45
  import { previewAssetSrc } from "../canvas/asset-refs";
45
46
  import { IMAGE_EXTENSIONS, extensionOf } from "../files/media-upload";
46
- import { renderPagePanelSurface } from "../surfaces/panel-page";
47
+ import { renderPagePanelSurface, unmountPagePanelSurface } from "../surfaces/panel-page";
48
+ import { emptyState } from "../surfaces/empty-state";
49
+ import type { EmptyStateSpec } from "./empty-state";
50
+ import { collectionForFile } from "../content/collection-match";
51
+ import {
52
+ isEntryTab,
53
+ renderEntryInspector,
54
+ unmountEntryInspector,
55
+ } from "../content/entry-inspector";
47
56
 
48
57
  import type { JxHeadEntry, JxMutableNode } from "@jxsuite/schema/types";
49
58
  import type { Tab } from "../tabs/tab";
50
59
  import type * as MediaPickerModule from "../ui/media-picker";
51
60
  import type {
52
- PageChoice,
53
61
  PagePanelActions,
54
62
  PagePanelView,
55
63
  PageRow,
56
64
  PageSection,
57
65
  PageTagRow,
58
66
  } from "../surfaces/panel-page";
67
+ import type { SurfaceOption } from "../ui/surface";
68
+ import type { SeoWarningView } from "../surfaces/seo";
59
69
 
60
70
  interface MetaField {
61
71
  label: string;
@@ -70,7 +80,14 @@ interface MetaField {
70
80
  /** @type {{ name: string; path: string }[] | null} */
71
81
  let layoutEntries: { name: string; path: string }[] | null = null;
72
82
 
73
- async function loadLayoutEntries() {
83
+ /**
84
+ * List `layouts/` now and keep the answer as the picker's cache.
85
+ *
86
+ * The one reading of which files are layouts and what each is called: the Page panel's picker and
87
+ * Add new (`create/add-new-flow.ts`) both read it, so they cannot name different templates. JSON
88
+ * only, because a layout is resolved as JSON.
89
+ */
90
+ export async function listLayoutEntries(): Promise<{ name: string; path: string }[]> {
74
91
  try {
75
92
  const platform = getPlatform();
76
93
  const listing = await platform.listDirectory("layouts");
@@ -86,8 +103,12 @@ async function loadLayoutEntries() {
86
103
  } catch {
87
104
  layoutEntries = [];
88
105
  }
89
- renderOnly("leftPanel");
90
- renderOnly("frontmatterPanel", "seoModal");
106
+ return layoutEntries;
107
+ }
108
+
109
+ async function loadLayoutEntries() {
110
+ await listLayoutEntries();
111
+ renderOnly("rightPanel", "seoModal");
91
112
  }
92
113
 
93
114
  /**
@@ -103,11 +124,10 @@ export function invalidateLayoutPickerCache() {
103
124
  /**
104
125
  * The layouts the picker offers, or `null` while the directory is still being listed.
105
126
  *
106
- * The listing BOTH surfaces draw their Layout picker from, handed over as DATA. The Page panel
107
- * (`src/surfaces/panel-page.json`) and the Document Header card (`src/surfaces/doc-header.json`)
108
- * are documents and neither can interpolate the other's markup — but a second LISTING would be a
109
- * second cache with a second lifetime, and {@link invalidateLayoutPickerCache} would then forget
110
- * only one of them. Asking starts the read; it repaints both surfaces when it lands.
127
+ * The listing the Page tab's Layout picker is drawn from, handed over as DATA: the Page panel
128
+ * (`src/surfaces/panel-page.json`) is a document and reads it as a projection. One listing is one
129
+ * cache with one lifetime, which {@link invalidateLayoutPickerCache} forgets. Asking starts the
130
+ * read; it repaints the Inspector when it lands.
111
131
  *
112
132
  * @returns {{ name: string; path: string }[] | null}
113
133
  */
@@ -141,19 +161,75 @@ export const OG_FIELDS: MetaField[] = [
141
161
  const MANAGED_META_KEYS = new Set([...PAGE_FIELDS, ...OG_FIELDS].map((f) => f.key));
142
162
 
143
163
  /**
144
- * Frontmatter keys the Document Header card owns with a dedicated control, so the generic field
145
- * list must not print them a second time.
164
+ * Frontmatter keys the Page tab owns with a dedicated control, so the generic field list must not
165
+ * print them a second time.
146
166
  *
147
- * **The one policy.** `frontmatter-panel.ts` used to pass an EMPTY reserved set while this module
148
- * passed `{title}`, so the same key rendered as a bare Obsidian-style property above the canvas and
149
- * as the Page panel's Title field at the same time, with two different commit paths. Merging the
150
- * two field sets was only safe once one of the two policies won, and it is this one: a document has
151
- * ONE title, and the surface that gives it a named row is the one that owns the key.
167
+ * **The one policy.** A document has ONE title, and the surface that gives it a named row is the
168
+ * one that owns the key: listed as a generic field as well, the same key rendered twice with two
169
+ * different commit paths.
152
170
  */
153
171
  export const RESERVED_FM_KEYS = new Set(["title"]);
154
172
 
173
+ /**
174
+ * The frontmatter keys a document's head controls own, so its generic field list leaves them out:
175
+ * {@link RESERVED_FM_KEYS}, plus `description` on a PAGE, whose description the build writes as its
176
+ * description meta (`withPageDescription` in `@jxsuite/site/head-merger`). Listed as a frontmatter
177
+ * field as well, a Markdown page showed two Description rows with two values (the frontmatter's and
178
+ * the head's empty one), and only one of them reaches the browser. A layout's or component's
179
+ * `description` is data the build does not read, so it stays an ordinary field.
180
+ *
181
+ * @param tab The document whose fields are listed.
182
+ */
183
+ export function reservedFmKeys(tab: Tab | null): ReadonlySet<string> {
184
+ return isPageDocument(tab) ? new Set([...RESERVED_FM_KEYS, "description"]) : RESERVED_FM_KEYS;
185
+ }
186
+
187
+ /**
188
+ * A page's description, from wherever the build reads it: a `<meta name="description">` in its own
189
+ * `$head` when it has one (stated last, it wins), else its `description` property.
190
+ *
191
+ * @param doc The head-bearing view of the document (`buildHeadDoc` for a Markdown page).
192
+ */
193
+ export function pageDescription(doc: JxMutableNode): string {
194
+ const meta = findMetaEntry(doc.$head ?? [], "name", "description");
195
+ if (meta) {
196
+ return String(meta.attributes?.content ?? "");
197
+ }
198
+ return isString(doc.description) ? doc.description : "";
199
+ }
200
+
201
+ /**
202
+ * Write a page's description where {@link pageDescription} reads it, so the one field never edits a
203
+ * value the build then overrules: the `description` property when that is where the page keeps it
204
+ * and its `$head` states none, else the head's meta (a page with neither gets the meta, as before).
205
+ * An empty value removes it.
206
+ *
207
+ * @param doc The head-bearing view of the document.
208
+ * @param value The new description, already trimmed.
209
+ */
210
+ export function setPageDescription(doc: JxMutableNode, value: string): void {
211
+ if (!findMetaEntry(doc.$head ?? [], "name", "description") && isString(doc.description)) {
212
+ if (value) {
213
+ doc.description = value;
214
+ } else {
215
+ delete doc.description;
216
+ }
217
+ return;
218
+ }
219
+ upsertMeta(doc, "name", "description", value);
220
+ }
221
+
155
222
  // ─── Helpers ─────────────────────────────────────────────────────────────
156
223
 
224
+ /**
225
+ * One slot of a `$head` array as it is read off a document.
226
+ *
227
+ * Wider than `JxHeadEntry` on purpose: a Markdown page's `$head` is its frontmatter, which reaches
228
+ * these helpers through {@link buildHeadDoc} without being validated, so an empty YAML list item
229
+ * arrives as `null`. Every reader here steps over such a slot rather than throwing on it.
230
+ */
231
+ type HeadSlot = JxHeadEntry | null | undefined;
232
+
157
233
  /**
158
234
  * Find a `$head` meta entry by attribute match.
159
235
  *
@@ -162,11 +238,8 @@ export const RESERVED_FM_KEYS = new Set(["title"]);
162
238
  * @param {string} key
163
239
  * @returns {JxHeadEntry | undefined}
164
240
  */
165
- export function findMetaEntry(head: JxHeadEntry[], attr: "name" | "property", key: string) {
166
- if (!head) {
167
- return;
168
- }
169
- return head.find((e: JxHeadEntry) => e?.tagName === "meta" && e?.attributes?.[attr] === key);
241
+ export function findMetaEntry(head: readonly HeadSlot[], attr: "name" | "property", key: string) {
242
+ return head.find((e): e is JxHeadEntry => e?.tagName === "meta" && e.attributes?.[attr] === key);
170
243
  }
171
244
 
172
245
  /**
@@ -176,11 +249,8 @@ export function findMetaEntry(head: JxHeadEntry[], attr: "name" | "property", ke
176
249
  * @param {string} rel
177
250
  * @returns {JxHeadEntry | undefined}
178
251
  */
179
- export function findLinkEntry(head: JxHeadEntry[], rel: string) {
180
- if (!head) {
181
- return;
182
- }
183
- return head.find((e: JxHeadEntry) => e?.tagName === "link" && e?.attributes?.rel === rel);
252
+ export function findLinkEntry(head: readonly HeadSlot[], rel: string) {
253
+ return head.find((e): e is JxHeadEntry => e?.tagName === "link" && e.attributes?.rel === rel);
184
254
  }
185
255
 
186
256
  /**
@@ -189,20 +259,20 @@ export function findLinkEntry(head: JxHeadEntry[], rel: string) {
189
259
  * @param {JxHeadEntry} entry
190
260
  * @returns {boolean}
191
261
  */
192
- export function isManagedEntry(entry: JxHeadEntry) {
262
+ export function isManagedEntry(entry: HeadSlot) {
193
263
  if (!entry?.tagName) {
194
264
  return false;
195
265
  }
196
266
  // Managed meta tags
197
267
  if (entry.tagName === "meta") {
198
- const name = String(entry?.attributes?.name ?? "");
199
- const prop = String(entry?.attributes?.property ?? "");
268
+ const name = String(entry.attributes?.name ?? "");
269
+ const prop = String(entry.attributes?.property ?? "");
200
270
  return (
201
271
  Boolean(name && MANAGED_META_KEYS.has(name)) || Boolean(prop && MANAGED_META_KEYS.has(prop))
202
272
  );
203
273
  }
204
274
  // Managed link: favicon
205
- if (entry.tagName === "link" && entry?.attributes?.rel === "icon") {
275
+ if (entry.tagName === "link" && entry.attributes?.rel === "icon") {
206
276
  return true;
207
277
  }
208
278
  return false;
@@ -226,7 +296,7 @@ export function upsertMeta(
226
296
  doc.$head = [];
227
297
  }
228
298
  const idx = doc.$head.findIndex(
229
- (e: JxHeadEntry) => e?.tagName === "meta" && e?.attributes?.[attr] === key,
299
+ (e: HeadSlot) => e?.tagName === "meta" && e.attributes?.[attr] === key,
230
300
  );
231
301
  if (content) {
232
302
  const entry = { attributes: { [attr]: key, content }, tagName: "meta" };
@@ -252,7 +322,7 @@ export function upsertLink(doc: JxMutableNode, rel: string, href: string) {
252
322
  doc.$head = [];
253
323
  }
254
324
  const idx = doc.$head.findIndex(
255
- (e: JxHeadEntry) => e?.tagName === "link" && e?.attributes?.rel === rel,
325
+ (e: HeadSlot) => e?.tagName === "link" && e.attributes?.rel === rel,
256
326
  );
257
327
  if (href) {
258
328
  const entry = { attributes: { href, rel }, tagName: "link" };
@@ -272,7 +342,7 @@ export function upsertLink(doc: JxMutableNode, rel: string, href: string) {
272
342
  * @param {JxHeadEntry} entry
273
343
  * @returns {string}
274
344
  */
275
- export function entryLabel(entry: JxHeadEntry) {
345
+ export function entryLabel(entry: HeadSlot) {
276
346
  if (!entry?.tagName) {
277
347
  return "unknown";
278
348
  }
@@ -301,7 +371,7 @@ export function entryLabel(entry: JxHeadEntry) {
301
371
  * @param {JxHeadEntry} entry
302
372
  * @returns {string}
303
373
  */
304
- export function entryValue(entry: JxHeadEntry) {
374
+ export function entryValue(entry: HeadSlot) {
305
375
  const a = entry?.attributes ?? {};
306
376
  return String(a.content ?? a.href ?? a.src ?? entry?.textContent ?? "");
307
377
  }
@@ -358,11 +428,15 @@ const NOT_SUPPLIED: ResolvedHeadField = { donor: null, source: "none", value: ""
358
428
  * Last rather than first, because the merger folds a layer into a keyed map in array order: a layer
359
429
  * that lists `description` twice emits the second one.
360
430
  */
361
- function metaContentIn(head: JxHeadEntry[], attr: "name" | "property", key: string): string | null {
431
+ function metaContentIn(
432
+ head: readonly HeadSlot[],
433
+ attr: "name" | "property",
434
+ key: string,
435
+ ): string | null {
362
436
  let found: string | null = null;
363
437
  for (const entry of head) {
364
- if (entry?.tagName === "meta" && entry?.attributes?.[attr] === key) {
365
- found = String(entry.attributes?.content ?? "");
438
+ if (entry?.tagName === "meta" && entry.attributes?.[attr] === key) {
439
+ found = String(entry.attributes.content ?? "");
366
440
  }
367
441
  }
368
442
  return found;
@@ -483,12 +557,12 @@ export interface SeoField extends ResolvedHeadField {
483
557
  * a verdict, and the verdict is what gets optimised (§16.6). A count beside a limit says the same
484
558
  * thing without pretending to rank it.
485
559
  */
486
- export const SEO_LIMITS: Readonly<Record<string, number>> = {
487
- description: 160,
488
- "og:description": 200,
489
- "og:title": 60,
490
- title: 60,
491
- };
560
+ export const SEO_LIMITS: ReadonlyMap<string, number> = new Map([
561
+ ["description", 160],
562
+ ["og:description", 200],
563
+ ["og:title", 60],
564
+ ["title", 60],
565
+ ]);
492
566
 
493
567
  /** Counts what a reader sees as one character. See {@link visibleLength}. */
494
568
  const GRAPHEME_SEGMENTER = new Intl.Segmenter("en-US", { granularity: "grapheme" });
@@ -518,15 +592,6 @@ export function visibleLength(value: string): number {
518
592
  return count;
519
593
  }
520
594
 
521
- /** A named thing that is wrong, or absent. Never summed — the list IS the report. */
522
- export interface SeoWarning {
523
- /** Stable id, so a test and a shot name the same warning. */
524
- id: string;
525
- /** The head key the warning is about, so the field row can carry it. */
526
- field: string;
527
- message: string;
528
- }
529
-
530
595
  /** The Problems `source` the SEO warnings carry, so a re-run replaces rather than stacks. */
531
596
  export const SEO_PROBLEM_SOURCE = "Search appearance";
532
597
 
@@ -548,13 +613,16 @@ export const SEO_PROBLEM_SOURCE = "Search appearance";
548
613
  export function reportSeoProblems(preview: SeoPreview, path?: string): number {
549
614
  clearProblems((record) => record.source === SEO_PROBLEM_SOURCE);
550
615
  for (const warning of preview.warnings) {
551
- notify.warn(warning.message, {
616
+ const opts: NotifyOptions = {
552
617
  action: "document.openSeo",
553
618
  key: `seo.${warning.id}`,
554
619
  source: SEO_PROBLEM_SOURCE,
555
620
  tier: "problem",
556
- ...(path === undefined ? {} : { path }),
557
- });
621
+ };
622
+ if (path !== undefined) {
623
+ opts.path = path;
624
+ }
625
+ notify.warn(warning.message, opts);
558
626
  }
559
627
  return preview.warnings.length;
560
628
  }
@@ -564,7 +632,7 @@ export interface SeoPreview {
564
632
  url: SeoUrl;
565
633
  /** In render order: `title`, `description`, then the four OpenGraph keys. */
566
634
  fields: SeoField[];
567
- warnings: SeoWarning[];
635
+ warnings: SeoWarningView[];
568
636
  }
569
637
 
570
638
  /** Look one field up by key — the previews read four of the six by name. */
@@ -574,14 +642,16 @@ export function seoField(preview: SeoPreview, key: string): SeoField {
574
642
  ...NOT_SUPPLIED,
575
643
  key,
576
644
  label: key,
577
- limit: SEO_LIMITS[key] ?? null,
645
+ limit: SEO_LIMITS.get(key) ?? null,
578
646
  }
579
647
  );
580
648
  }
581
649
 
582
650
  /** Whether any layer declares a `<title>` element, which the merger discards. */
583
651
  function hasTitleEntry(layers: HeadLayers): boolean {
584
- return [...layers.site, ...layers.layout, ...layers.page].some((e) => e?.tagName === "title");
652
+ return [...layers.site, ...layers.layout, ...layers.page].some(
653
+ (e: HeadSlot) => e?.tagName === "title",
654
+ );
585
655
  }
586
656
 
587
657
  /**
@@ -593,13 +663,13 @@ function hasTitleEntry(layers: HeadLayers): boolean {
593
663
  *
594
664
  * @param {SeoPreview} preview — `warnings` is ignored; this computes it.
595
665
  * @param {HeadLayers} layers
596
- * @returns {SeoWarning[]}
666
+ * @returns {SeoWarningView[]}
597
667
  */
598
668
  export function seoWarnings(
599
669
  preview: Omit<SeoPreview, "warnings">,
600
670
  layers: HeadLayers,
601
- ): SeoWarning[] {
602
- const warnings: SeoWarning[] = [];
671
+ ): SeoWarningView[] {
672
+ const warnings: SeoWarningView[] = [];
603
673
  const get = (key: string) => preview.fields.find((f) => f.key === key);
604
674
 
605
675
  const title = get("title");
@@ -671,6 +741,14 @@ export function seoWarnings(
671
741
  return warnings;
672
742
  }
673
743
 
744
+ /** What {@link buildSeoPreview} needs besides the layers: the page's own title, route and site. */
745
+ export interface SeoPreviewContext {
746
+ pageTitle: string;
747
+ route: string | null;
748
+ siteName?: string;
749
+ siteUrl?: string;
750
+ }
751
+
674
752
  /**
675
753
  * Build the whole preview from the three layers, the page title, the route and the site config.
676
754
  *
@@ -678,18 +756,15 @@ export function seoWarnings(
678
756
  * (resolving the layout document) happens in {@link layoutHeadEntries} before this is called.
679
757
  *
680
758
  * @param {HeadLayers} layers
681
- * @param {{ pageTitle: string; route: string | null; siteName?: string; siteUrl?: string }} ctx
759
+ * @param {SeoPreviewContext} ctx
682
760
  * @returns {SeoPreview}
683
761
  */
684
- export function buildSeoPreview(
685
- layers: HeadLayers,
686
- ctx: { pageTitle: string; route: string | null; siteName?: string; siteUrl?: string },
687
- ): SeoPreview {
762
+ export function buildSeoPreview(layers: HeadLayers, ctx: SeoPreviewContext): SeoPreview {
688
763
  const field = (key: string, label: string, resolved: ResolvedHeadField): SeoField => ({
689
764
  ...resolved,
690
765
  key,
691
766
  label,
692
- limit: SEO_LIMITS[key] ?? null,
767
+ limit: SEO_LIMITS.get(key) ?? null,
693
768
  });
694
769
 
695
770
  const fields: SeoField[] = [
@@ -741,11 +816,10 @@ async function loadLayoutHead(path: string): Promise<void> {
741
816
  _layoutHeadPending = null;
742
817
  _layoutHeadPath = path;
743
818
  _layoutHead = doc?.$head ?? [];
744
- // THREE surfaces show the merged head now, and the layout layer arrives asynchronously: the
745
- // Document Header card, and Search appearance, which is where the previews went. A modal left
746
- // Off this list renders the page's own head with the layout layer missing — which reads as
819
+ // Search appearance shows the merged head, and the layout layer arrives asynchronously. A modal
820
+ // Left off this list renders the page's own head with the layout layer missing — which reads as
747
821
  // "you have no description" on a page that inherits one.
748
- renderOnly("frontmatterPanel", "seoModal");
822
+ renderOnly("seoModal");
749
823
  }
750
824
 
751
825
  /**
@@ -765,6 +839,12 @@ export function layoutDisplayName(path: string): string {
765
839
  );
766
840
  }
767
841
 
842
+ /** A layout's `$head` entries, and the name to print for the layout. */
843
+ export interface LayoutHead {
844
+ entries: JxHeadEntry[];
845
+ name: string | null;
846
+ }
847
+
768
848
  /**
769
849
  * The effective layout's `$head`, and the name to print for it.
770
850
  *
@@ -774,15 +854,9 @@ export function layoutDisplayName(path: string): string {
774
854
  *
775
855
  * @param {Tab | null} tab — the tab whose document the entries are being resolved FOR.
776
856
  * @param {string | false} [docLayout]
777
- * @returns {{ entries: JxHeadEntry[]; name: string | null }}
857
+ * @returns {LayoutHead}
778
858
  */
779
- export function layoutHeadEntries(
780
- tab: Tab | null,
781
- docLayout?: string | false,
782
- ): {
783
- entries: JxHeadEntry[];
784
- name: string | null;
785
- } {
859
+ export function layoutHeadEntries(tab: Tab | null, docLayout?: string | false): LayoutHead {
786
860
  const path = isPageDocument(tab) ? getEffectiveLayoutPath(docLayout) : null;
787
861
  if (path === null) {
788
862
  return { entries: [], name: null };
@@ -807,19 +881,26 @@ export function seoPreviewFor(tab: Tab | null, doc: JxMutableNode): SeoPreview {
807
881
  const config = projectState?.projectConfig;
808
882
  const layout = layoutHeadEntries(tab, doc.$layout);
809
883
  const path = tab?.documentPath;
884
+ // A content document's head doc may carry a non-string title (see buildHeadDoc).
885
+ const ctx: SeoPreviewContext = {
886
+ pageTitle: isString(doc.title) ? doc.title : "",
887
+ route: path ? pageRoute(path) : null,
888
+ };
889
+ if (config?.name !== undefined) {
890
+ ctx.siteName = config.name;
891
+ }
892
+ if (config?.url !== undefined) {
893
+ ctx.siteUrl = config.url;
894
+ }
810
895
  return buildSeoPreview(
811
896
  {
812
897
  layout: layout.entries,
813
898
  layoutName: layout.name,
814
- page: doc.$head ?? [],
899
+ // The page level as the build merges it: its `description` property leads its own head.
900
+ page: withPageDescription(doc.$head ?? [], doc.description),
815
901
  site: config?.$head ?? [],
816
902
  },
817
- {
818
- pageTitle: typeof doc.title === "string" ? doc.title : "",
819
- route: path ? pageRoute(path) : null,
820
- ...(config?.name === undefined ? {} : { siteName: config.name }),
821
- ...(config?.url === undefined ? {} : { siteUrl: config.url }),
822
- },
903
+ ctx,
823
904
  );
824
905
  }
825
906
 
@@ -837,7 +918,7 @@ export function seoPreviewFor(tab: Tab | null, doc: JxMutableNode): SeoPreview {
837
918
  * component GAINED a header card because the focused tab was a page — and the Layout picker
838
919
  * appeared or vanished in the pane you were editing according to the document in the other one.
839
920
  *
840
- * A caller whose subject genuinely IS the focused document (the Navigator's Page panel) passes
921
+ * A caller whose subject genuinely IS the focused document (the Inspector's Page tab) passes
841
922
  * `activeTab.value`, where a reviewer can see it. The same bargain `setCanvasMode` made.
842
923
  *
843
924
  * @param {Tab | null} tab
@@ -853,14 +934,32 @@ export function isPageDocument(tab: Tab | null): boolean {
853
934
  }
854
935
 
855
936
  /** Overlay content-mode frontmatter title/`$head` onto the document the panel edits. */
856
- export function buildHeadDoc(doc: JxMutableNode, fm: Record<string, unknown>): JxMutableNode {
857
- const title = fm.title as string | undefined;
858
- const $head = fm.$head as JxHeadEntry[] | undefined;
859
- return {
860
- ...doc,
861
- ...(title === undefined ? {} : { title }),
862
- ...($head === undefined ? {} : { $head }),
863
- };
937
+ export function buildHeadDoc(
938
+ doc: JxMutableNode,
939
+ fm: Tab["doc"]["content"]["frontmatter"],
940
+ ): JxMutableNode {
941
+ const headDoc: JxMutableNode = { ...doc };
942
+ if (fm.title !== undefined) {
943
+ /* SAFETY: deliberately not narrowed — a YAML title can be a number, and it is overlaid as
944
+ written. Every reader of a head doc's `title` (titleRow here, seoPreviewFor) tests it with
945
+ isString before using it as text. */
946
+ headDoc.title = fm.title as string;
947
+ }
948
+ if (isString(fm.description)) {
949
+ // The page's description property, which the build writes as its description meta.
950
+ headDoc.description = fm.description;
951
+ }
952
+ if (isString(fm.$layout) || fm.$layout === false) {
953
+ // A Markdown page's layout override is its frontmatter `$layout`, which the build reads.
954
+ headDoc.$layout = fm.$layout;
955
+ }
956
+ if (fm.$head !== undefined) {
957
+ /* SAFETY: Studio's one writer of the frontmatter `$head` key is applyContentMutation below,
958
+ which stores a JxHeadEntry[] or deletes the key. A hand-written one is trusted exactly as
959
+ far as a JSON page's `doc.$head` is, and the readers step over null slots (HeadSlot). */
960
+ headDoc.$head = fm.$head as JxHeadEntry[];
961
+ }
962
+ return headDoc;
864
963
  }
865
964
 
866
965
  /**
@@ -870,13 +969,10 @@ export function buildHeadDoc(doc: JxMutableNode, fm: Record<string, unknown>): J
870
969
  * adapts one to the other in the module that owns both, instead of in the Navigator orchestrator
871
970
  * that owns neither.
872
971
  *
873
- * **`tab` is a parameter for the same reason every other frontmatter helper's is.** It resolved
874
- * `activeTab.value` itself, and the Document Header card calls it for the CONTENT branch of every
875
- * mutation it makes — Title, Clear title, the Layout picker — as does Search appearance, which is
876
- * where the head fields went and which takes its tab the same way. The card is drawn per pane, so
877
- * on a markdown page the card in one pane retitled the document in the other. The comment beside
878
- * the card's JSON branch has claimed since the pane grid landed that this was fixed; the fix
879
- * reached the JSON branch only, and this is the half that was left.
972
+ * **`tab` is a parameter for the same reason every other frontmatter helper's is.** Search
973
+ * appearance commits through it for the tab it was opened over, which need not be the focused
974
+ * pane's; resolving `activeTab.value` here once let an edit in one pane retitle the document in the
975
+ * other.
880
976
  *
881
977
  * @param {Tab | null} tab The document to commit into.
882
978
  * @param {() => void} rerender
@@ -890,19 +986,61 @@ export function applyContentMutation(
890
986
  if (!tab) {
891
987
  return;
892
988
  }
893
- const fmNow = (tab.doc.content?.frontmatter ?? {}) as Record<string, unknown>;
894
- const fmHead = fmNow.$head as JxHeadEntry[] | undefined;
895
- const tmp: JxMutableNode = {
896
- ...(typeof fmNow.title === "string" ? { title: fmNow.title } : {}),
897
- ...(fmHead ? { $head: [...fmHead] } : {}),
898
- };
899
- fn(tmp);
900
- if (tmp.title !== fmNow.title) {
901
- mutateUpdateFrontmatter(tab, "title", tmp.title as JsonValue);
989
+ const fmNow = tab.doc.content.frontmatter;
990
+ // The frontmatter `$head`, read through the one place that types it.
991
+ const fmHead = buildHeadDoc({}, fmNow).$head;
992
+ const tmp: JxMutableNode = {};
993
+ if (isString(fmNow.title)) {
994
+ tmp.title = fmNow.title;
995
+ }
996
+ if (isString(fmNow.description)) {
997
+ tmp.description = fmNow.description;
998
+ }
999
+ if (fmHead) {
1000
+ tmp.$head = [...fmHead];
1001
+ }
1002
+ if (isString(fmNow.$layout) || fmNow.$layout === false) {
1003
+ tmp.$layout = fmNow.$layout;
902
1004
  }
1005
+ fn(tmp);
903
1006
  const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
904
- // JxHeadEntry[] is JSON document content by construction.
905
- mutateUpdateFrontmatter(tab, "$head", newHead as JsonValue);
1007
+ const layoutChanged = tmp.$layout !== fmNow.$layout;
1008
+ const titleChanged = tmp.title !== fmNow.title;
1009
+ const descriptionChanged = isString(fmNow.description) && tmp.description !== fmNow.description;
1010
+ const headChanged = JSON.stringify(newHead) !== JSON.stringify(fmHead);
1011
+ /* The writes go through the transaction log, as every other frontmatter commit does. That is what
1012
+ records them for undo and swaps the document's root reference, which is the signal the canvas
1013
+ redraws on: a Layout pick on a Markdown page that only mutated the frontmatter in place was
1014
+ stored, and the page went on showing its old layout until something unrelated redrew it. A
1015
+ mutation that changed nothing records nothing, since an empty transaction is still an undo step. */
1016
+ if (layoutChanged || titleChanged || descriptionChanged || headChanged) {
1017
+ transactDoc(tab, (t) => {
1018
+ // The Layout picker writes `$layout`, and on a Markdown page that is a frontmatter key.
1019
+ if (layoutChanged) {
1020
+ mutateUpdateFrontmatter(
1021
+ t,
1022
+ "$layout",
1023
+ isString(tmp.$layout) || tmp.$layout === false ? tmp.$layout : undefined,
1024
+ );
1025
+ }
1026
+ if (titleChanged) {
1027
+ mutateUpdateFrontmatter(t, "title", tmp.title);
1028
+ }
1029
+ if (descriptionChanged) {
1030
+ mutateUpdateFrontmatter(
1031
+ t,
1032
+ "description",
1033
+ isString(tmp.description) ? tmp.description : undefined,
1034
+ );
1035
+ }
1036
+ if (headChanged) {
1037
+ /* SAFETY: every entry here either came from the frontmatter (JSON, from the format's split)
1038
+ or was built by a head editor out of string attributes; JxHeadEntry is wider than
1039
+ JsonValue only through its open index signature, which no head editor writes. */
1040
+ mutateUpdateFrontmatter(t, "$head", newHead as JsonValue | undefined);
1041
+ }
1042
+ });
1043
+ }
906
1044
  rerender();
907
1045
  }
908
1046
 
@@ -914,15 +1052,22 @@ interface RowCommit {
914
1052
  clear: () => void;
915
1053
  /** Commit what the control settled on — a string from every kind but the checkbox. */
916
1054
  commit: (raw: string | boolean) => void;
1055
+ /**
1056
+ * Whether this row's Browse lists only pictures (studio.md §9.3): the social image and the icon
1057
+ * always do, a frontmatter media field only when its schema says `format: "image"` — a video or a
1058
+ * PDF field browsing a picture-only list could never be filled from it.
1059
+ */
1060
+ images: boolean;
917
1061
  }
918
1062
 
919
- /** What the panel is drawn against. `left-panel.ts`'s context, reduced to what this reads. */
1063
+ /** What the panel is drawn against: the focused document, reduced to what this reads. */
920
1064
  export interface PagePanelContext {
921
1065
  /** The head-bearing view of the open document — `buildHeadDoc` for a content page. */
922
1066
  document: JxMutableNode;
923
1067
  /** How a change to that view is written back into whichever realm it belongs to. */
924
1068
  applyMutation: (fn: (doc: JxMutableNode) => void) => void;
925
- renderLeftPanel: () => void;
1069
+ /** Repaint the host the panel is drawn in — the Inspector's Page tab. */
1070
+ rerender: () => void;
926
1071
  }
927
1072
 
928
1073
  /** Where the surface is mounted, and the context it was last drawn against. */
@@ -943,6 +1088,22 @@ let _addValue = "";
943
1088
  /** This projection's rows, by key. Rebuilt with each projection. */
944
1089
  const _commits = new Map<string, RowCommit>();
945
1090
 
1091
+ /** This projection's row buttons ({@link PageRow.action}), by row key. Rebuilt with each projection. */
1092
+ const _rowActions = new Map<string, () => void>();
1093
+
1094
+ /**
1095
+ * The page's URL, derived from where its file sits under `pages/` (`pages/about.md` → `/about`), as
1096
+ * a readout: changing a URL is moving the file, which the file tree owns. `null` for anything that
1097
+ * is not a page.
1098
+ */
1099
+ function urlRow(tab: Tab | null): PageRow | null {
1100
+ const route = tab?.documentPath ? pageRoute(tab.documentPath) : null;
1101
+ if (route === null) {
1102
+ return null;
1103
+ }
1104
+ return { ...blankRow("__url", "url", "URL"), kind: "readout", value: route };
1105
+ }
1106
+
946
1107
  /**
947
1108
  * This projection's custom `$head` entries, by the key they were drawn with.
948
1109
  *
@@ -957,7 +1118,7 @@ const _entries = new Map<string, JxHeadEntry>();
957
1118
  const _pending = new Map<string, { commit: () => void; timer: ReturnType<typeof setTimeout> }>();
958
1119
 
959
1120
  /** The elements the add form offers, in the order a reader meets them. */
960
- const TAG_OPTIONS: PageChoice[] = [
1121
+ const TAG_OPTIONS: SurfaceOption[] = [
961
1122
  { label: "meta", value: "meta" },
962
1123
  { label: "link", value: "link" },
963
1124
  { label: "script", value: "script" },
@@ -966,8 +1127,10 @@ const TAG_OPTIONS: PageChoice[] = [
966
1127
  /** A blank row, so every kind carries every field the document's bindings read. */
967
1128
  function blankRow(key: string, prop: string, label: string): PageRow {
968
1129
  return {
1130
+ action: "",
969
1131
  checked: false,
970
1132
  clearLabel: `Clear ${prop}`,
1133
+ hasAction: false,
971
1134
  hasNote: false,
972
1135
  hasThumb: false,
973
1136
  isSet: false,
@@ -991,8 +1154,9 @@ function thumbFor(row: PageRow, value: string): void {
991
1154
 
992
1155
  /** The one head value you type while writing, in both realms. */
993
1156
  function titleRow(doc: JxMutableNode, apply: PagePanelContext["applyMutation"]): PageRow {
994
- const title = typeof doc.title === "string" ? doc.title : "";
1157
+ const title = isString(doc.title) ? doc.title : "";
995
1158
  _commits.set("title", {
1159
+ images: false,
996
1160
  clear: () =>
997
1161
  apply((d) => {
998
1162
  delete d.title;
@@ -1025,6 +1189,7 @@ function metaRow(
1025
1189
  const value = String(findMetaEntry(head, field.attr, field.key)?.attributes?.content ?? "");
1026
1190
  const key = `meta:${field.attr}:${field.key}`;
1027
1191
  _commits.set(key, {
1192
+ images: field.media === true,
1028
1193
  clear: () => apply((d) => upsertMeta(d, field.attr, field.key, "")),
1029
1194
  /* A media field is not trimmed: a path is committed exactly as it was chosen, and the browser
1030
1195
  and the upload both hand one back already clean. */
@@ -1047,10 +1212,31 @@ function metaRow(
1047
1212
  return row;
1048
1213
  }
1049
1214
 
1215
+ /**
1216
+ * The page's one Description: the value the build writes as its description meta, read and written
1217
+ * where it lives ({@link pageDescription}, {@link setPageDescription}).
1218
+ */
1219
+ function descriptionRow(doc: JxMutableNode, apply: PagePanelContext["applyMutation"]): PageRow {
1220
+ const value = pageDescription(doc);
1221
+ const key = "meta:name:description";
1222
+ _commits.set(key, {
1223
+ images: false,
1224
+ clear: () => apply((d) => setPageDescription(d, "")),
1225
+ commit: (raw) => apply((d) => setPageDescription(d, String(raw).trim())),
1226
+ });
1227
+ const row = blankRow(key, "description", "Description");
1228
+ row.isSet = Boolean(value);
1229
+ row.value = value;
1230
+ row.kind = "text";
1231
+ row.placeholder = "Description…";
1232
+ return row;
1233
+ }
1234
+
1050
1235
  /** The favicon, which is a `<link rel="icon">` rather than a `<meta>`. */
1051
1236
  function iconRow(head: JxHeadEntry[], apply: PagePanelContext["applyMutation"]): PageRow {
1052
1237
  const value = String(findLinkEntry(head, "icon")?.attributes?.href ?? "");
1053
1238
  _commits.set("link:icon", {
1239
+ images: true,
1054
1240
  clear: () => apply((d) => upsertLink(d, "icon", "")),
1055
1241
  commit: (raw) => apply((d) => upsertLink(d, "icon", String(raw))),
1056
1242
  });
@@ -1066,8 +1252,7 @@ function iconRow(head: JxHeadEntry[], apply: PagePanelContext["applyMutation"]):
1066
1252
  * The Layout picker, or `null` when this document takes none — a component has no layout, and a
1067
1253
  * page whose layouts directory is still being listed has nothing to offer yet.
1068
1254
  *
1069
- * The listing is {@link layoutPickerEntries}', which the Document Header card reads too: creating a
1070
- * layout invalidates one cache rather than two.
1255
+ * The listing is {@link layoutPickerEntries}', one cache that creating a layout invalidates.
1071
1256
  */
1072
1257
  function layoutSection(
1073
1258
  tab: Tab | null,
@@ -1085,6 +1270,7 @@ function layoutSection(
1085
1270
  const defaultPath = projectState?.projectConfig?.defaults?.layout;
1086
1271
  const defaultLabel = defaultPath ? layoutDisplayName(defaultPath) : "";
1087
1272
  _commits.set("__layout", {
1273
+ images: false,
1088
1274
  clear: () =>
1089
1275
  apply((d) => {
1090
1276
  delete d.$layout;
@@ -1112,6 +1298,17 @@ function layoutSection(
1112
1298
  ...entries.map((l) => ({ label: l.name, value: l.path })),
1113
1299
  ];
1114
1300
  row.value = current === false ? "__none__" : current || "__default__";
1301
+ // The template the page is drawn in, opened in Design: its own `$layout`, else the default.
1302
+ const effective = getEffectiveLayoutPath(current);
1303
+ if (effective) {
1304
+ row.action = "Edit template";
1305
+ row.hasAction = true;
1306
+ _rowActions.set("__layout", () => {
1307
+ void import("../files/files").then(({ openFileInTab }) =>
1308
+ openFileInTab(effective.replace(/^\.\//, "")),
1309
+ );
1310
+ });
1311
+ }
1115
1312
  return { hasSeo: false, key: "layout", rows: [row], title: "Layout" };
1116
1313
  }
1117
1314
 
@@ -1128,7 +1325,7 @@ function frontmatterSection(tab: Tab | null): PageSection | null {
1128
1325
  const { collection, fields, hasSchema, requiredFields } = collectFmFields(
1129
1326
  tab,
1130
1327
  projectState?.projectConfig,
1131
- RESERVED_FM_KEYS,
1328
+ reservedFmKeys(tab),
1132
1329
  );
1133
1330
  if (fields.length === 0 && !hasSchema) {
1134
1331
  return null;
@@ -1139,9 +1336,9 @@ function frontmatterSection(tab: Tab | null): PageSection | null {
1139
1336
  rerender: sync,
1140
1337
  });
1141
1338
  _commits.set(key, {
1339
+ images: row.kind === "media" && f.entry.format === "image",
1142
1340
  clear: () => transactDoc(tab, (t) => mutateUpdateFrontmatter(t, f.field)),
1143
- commit: (raw) =>
1144
- transactDoc(tab, (t) => mutateUpdateFrontmatter(t, f.field, parse(raw) as JsonValue)),
1341
+ commit: (raw) => transactDoc(tab, (t) => mutateUpdateFrontmatter(t, f.field, parse(raw))),
1145
1342
  });
1146
1343
  return Object.assign(blankRow(key, f.field, row.label), row, {
1147
1344
  clearLabel: `Clear ${f.field}`,
@@ -1158,27 +1355,30 @@ function frontmatterSection(tab: Tab | null): PageSection | null {
1158
1355
  /** The `$head` entries no structured control owns, and the keys a Remove hands back. */
1159
1356
  function customEntries(head: JxHeadEntry[]): PageTagRow[] {
1160
1357
  _entries.clear();
1161
- return head
1162
- .filter(
1163
- (e: JxHeadEntry) => !isManagedEntry(e) && !isGoogleFontEntry(e) && !isGoogleFontPreconnect(e),
1164
- )
1165
- .map((entry, index) => {
1166
- const key = `tag:${index}`;
1167
- _entries.set(key, entry);
1168
- const label = entryLabel(entry);
1169
- return { key, label, removeLabel: `Remove ${label}`, value: entryValue(entry) };
1170
- });
1358
+ const rows: PageTagRow[] = [];
1359
+ for (const entry of head) {
1360
+ if (isManagedEntry(entry) || isGoogleFontEntry(entry) || isGoogleFontPreconnect(entry)) {
1361
+ continue;
1362
+ }
1363
+ // The key counts the LISTED entries, as the index of the filtered list it replaces did.
1364
+ const key = `tag:${rows.length}`;
1365
+ _entries.set(key, entry);
1366
+ const label = entryLabel(entry);
1367
+ rows.push({ key, label, removeLabel: `Remove ${label}`, value: entryValue(entry) });
1368
+ }
1369
+ return rows;
1171
1370
  }
1172
1371
 
1173
1372
  /**
1174
1373
  * Everything the panel says about one document, and the commit table behind it.
1175
1374
  *
1176
- * `activeTab.value` is read HERE and nowhere below it: the Navigator's Page panel is an app-level
1375
+ * `activeTab.value` is read HERE and nowhere below it: the Inspector's Page tab is an app-level
1177
1376
  * surface that shows the focused document by definition, and spelling that out once at the top is
1178
1377
  * what lets every helper under it take a tab instead of asking for one.
1179
1378
  */
1180
1379
  function view(ctx: PagePanelContext): PagePanelView {
1181
1380
  _commits.clear();
1381
+ _rowActions.clear();
1182
1382
  const tab = activeTab.value;
1183
1383
  const doc = ctx.document;
1184
1384
  const head = doc.$head ?? [];
@@ -1192,13 +1392,19 @@ function view(ctx: PagePanelContext): PagePanelView {
1192
1392
  if (layout) {
1193
1393
  sections.push(layout);
1194
1394
  }
1395
+ const url = urlRow(tab);
1195
1396
  sections.push(
1196
1397
  {
1197
1398
  hasSeo: true,
1198
1399
  key: "page",
1199
1400
  rows: [
1200
1401
  titleRow(doc, ctx.applyMutation),
1201
- ...PAGE_FIELDS.map((field) => metaRow(field, head, ctx.applyMutation)),
1402
+ ...(url ? [url] : []),
1403
+ ...PAGE_FIELDS.map((field) =>
1404
+ field.key === "description"
1405
+ ? descriptionRow(doc, ctx.applyMutation)
1406
+ : metaRow(field, head, ctx.applyMutation),
1407
+ ),
1202
1408
  iconRow(head, ctx.applyMutation),
1203
1409
  ],
1204
1410
  title: "Page",
@@ -1290,7 +1496,7 @@ function addEntry(): void {
1290
1496
  }
1291
1497
  d.$head.push(entry);
1292
1498
  });
1293
- ctx.renderLeftPanel();
1499
+ ctx.rerender();
1294
1500
  }
1295
1501
 
1296
1502
  /** Take one custom `$head` entry away, by the entry the key was drawn for. */
@@ -1309,7 +1515,7 @@ function removeEntry(key: string): void {
1309
1515
  d.$head.splice(idx, 1);
1310
1516
  }
1311
1517
  });
1312
- ctx.renderLeftPanel();
1518
+ ctx.rerender();
1313
1519
  }
1314
1520
 
1315
1521
  /**
@@ -1342,9 +1548,13 @@ const ACTIONS: PagePanelActions = {
1342
1548
  flushPending(key);
1343
1549
  void mediaPicker().then((m) => {
1344
1550
  if (anchor instanceof HTMLElement) {
1345
- m.showMediaPickerPopover(anchor, (val: string) => {
1346
- _commits.get(key)?.commit(val);
1347
- });
1551
+ m.showMediaPickerPopover(
1552
+ anchor,
1553
+ (val: string) => {
1554
+ _commits.get(key)?.commit(val);
1555
+ },
1556
+ { images: _commits.get(key)?.images ?? true },
1557
+ );
1348
1558
  }
1349
1559
  });
1350
1560
  },
@@ -1377,11 +1587,14 @@ const ACTIONS: PagePanelActions = {
1377
1587
  sync();
1378
1588
  },
1379
1589
  openSeo: () => {
1380
- /* The COMMAND, not a local open() — the Document Header card offers the same door and neither
1381
- of them owns it, so the palette has it by name. */
1590
+ /* The COMMAND, not a local open(): no surface owns the capability, so the palette has it by
1591
+ name. */
1382
1592
  void activeRegistry()?.run("document.openSeo");
1383
1593
  },
1384
1594
  removeEntry,
1595
+ runRowAction: (key) => {
1596
+ _rowActions.get(key)?.();
1597
+ },
1385
1598
  setBoolean: (key, checked) => commitNow(key, checked),
1386
1599
  setChoice: (key, value) => commitNow(key, value),
1387
1600
  setNumber: (key, value) => commitNow(key, value),
@@ -1424,64 +1637,137 @@ export function renderPagePanel(host: HTMLElement, ctx: PagePanelContext): void
1424
1637
  sync();
1425
1638
  }
1426
1639
 
1640
+ // ─── The Inspector's Page tab ────────────────────────────────────────────
1641
+
1427
1642
  /**
1428
- * The Navigator's content area inside a panel body.
1643
+ * What the Page tab is called for a document (§6): Page under `pages/`, Template under `layouts/`,
1644
+ * Component under `components/`, Entry for a file a collection owns, Document for anything else.
1429
1645
  *
1430
- * `afterRender` is handed the `.panel-body`, and the panel's own content goes one level in — which
1431
- * is where the document must be mounted rather than beside it, because that child is lit's part and
1432
- * clearing it is how switching to another panel takes this surface down. A body drawn by the
1433
- * fallback path has no content area; mounting into the body itself is then still correct.
1646
+ * Path-based, like the rest of the site model: a file's role in a project is where it lives, and
1647
+ * `collectionForFile` is the one matcher that answers "is this an entry" (§22.1 uses it too).
1434
1648
  */
1435
- function contentArea(body: HTMLElement): HTMLElement {
1436
- return body.querySelector<HTMLElement>(".panel-content") ?? body;
1649
+ export function pageTabTitle(path: string | null | undefined): string {
1650
+ if (!path) {
1651
+ return "Document";
1652
+ }
1653
+ if (path.startsWith("pages/")) {
1654
+ return "Page";
1655
+ }
1656
+ if (path.startsWith("layouts/")) {
1657
+ return "Template";
1658
+ }
1659
+ if (path.startsWith("components/")) {
1660
+ return "Component";
1661
+ }
1662
+ const collection = collectionForFile(path);
1663
+ return collection && !collection.fileBacked ? "Entry" : "Document";
1437
1664
  }
1438
1665
 
1666
+ /** What the Component tab says: why there is no page form, and the one verb that reaches its own. */
1667
+ export const COMPONENT_TAB_MESSAGE =
1668
+ "A component has no page settings: its title, search and social details come from the page that uses it.";
1669
+
1670
+ /** The Component tab's empty state, for a component document's tab. */
1671
+ export function componentTabSpec(tab: Tab): EmptyStateSpec {
1672
+ return {
1673
+ actions: [
1674
+ {
1675
+ label: "Edit tag and props",
1676
+ run: () => {
1677
+ // The root is the component's definition; selecting it opens Element › Content on its
1678
+ // Tag name and "Component Defaults" (`panels/properties-panel.ts`).
1679
+ updateSession(tab, { selection: [[]], ui: { rightTab: "properties" } });
1680
+ },
1681
+ },
1682
+ ],
1683
+ detail: "Its tag name and the props it takes are set on its root element.",
1684
+ message: COMPONENT_TAB_MESSAGE,
1685
+ };
1686
+ }
1687
+
1688
+ /** The sentence the Page tab shows with no document open. */
1689
+ export const PAGE_TAB_EMPTY = "Open a page to edit its title, description and social preview.";
1690
+
1691
+ /** The Inspector's Page body host, handed over once by the dock. */
1692
+ let _tabHost: HTMLElement | null = null;
1693
+
1439
1694
  /**
1440
- * The Navigator no longer draws this panel with lit.
1441
- *
1442
- * The record below returns `nothing` and mounts its document in `afterRender`, so this is a stub:
1443
- * it survives only because `NavigatorPanelDeps` still declares the injection and `studio.ts` still
1444
- * passes it. Both go in the change that deletes this.
1695
+ * Take the Inspector's Page body host, or let it go (`null`, when the dock is taken down).
1445
1696
  *
1446
- * @deprecated The panel is `surfaces/panel-page.json`; call {@link renderPagePanel}.
1447
- * @returns {typeof nothing}
1697
+ * The Page panel moved here from the Navigator (§6): the same surface, re-hosted unchanged, drawn
1698
+ * against the focused document. `panels/right-panel.ts` hands the host over and calls
1699
+ * {@link renderPageTab} whenever the dock repaints.
1448
1700
  */
1449
- export function renderHeadTemplate(_against: PagePanelContext): typeof nothing {
1450
- return nothing;
1701
+ export function bindPageHost(host: HTMLElement | null): void {
1702
+ if (_tabHost && _tabHost !== host) {
1703
+ unmountEntryInspector(_tabHost);
1704
+ unmountPagePanelSurface(_tabHost);
1705
+ if (_host === _tabHost) {
1706
+ _host = null;
1707
+ _ctx = null;
1708
+ }
1709
+ }
1710
+ _tabHost = host;
1711
+ renderPageTab();
1451
1712
  }
1452
1713
 
1453
1714
  /**
1454
- * Contribute the Page panel.
1715
+ * Draw the Page tab against the focused document, or empty it when nothing is open.
1455
1716
  *
1456
- * `level: "document"` — title, description, social card and custom `$head` entries are the open
1457
- * document's. The id is `page` now: "head" named an HTML element, and §5.1's Document group calls
1458
- * the surface Page. The record is what keeps the surface addressable under one name.
1717
+ * Idempotent and cheap: a standing surface is only assigned to (`renderPagePanelSurface`), so the
1718
+ * dock can call this on every repaint without taking the caret out of a field.
1459
1719
  */
1460
- export function registerPagePanel(): void {
1461
- registerPanel({
1462
- id: "page",
1463
- title: "Page",
1464
- level: "document",
1465
- dock: "navigator",
1466
- icon: "file",
1467
- requiresDocument: "Open a page to edit its title, description and social preview.",
1468
- render: () => nothing,
1469
- afterRender: (ctx, host) => {
1470
- const doc = ctx.doc!;
1471
- const isContent = doc.mode === "content";
1472
- const fm = doc.content?.frontmatter ?? {};
1473
- renderPagePanel(contentArea(host), {
1474
- /* The Navigator's Page panel IS an app-level surface: it shows the focused document by
1475
- definition, so it spells that out at the call site instead of leaving it to a helper two
1476
- other surfaces share. */
1477
- applyMutation: isContent
1478
- ? (fn) => applyContentMutation(activeTab.value, ctx.rerender, fn)
1479
- : (fn) => {
1480
- transact(activeTab.value, fn);
1481
- },
1482
- document: isContent ? buildHeadDoc(doc.document, fm) : doc.document,
1483
- renderLeftPanel: ctx.rerender,
1484
- });
1485
- },
1720
+ export function renderPageTab(): void {
1721
+ const host = _tabHost;
1722
+ if (!host) {
1723
+ return;
1724
+ }
1725
+ const tab = activeTab.value;
1726
+ if (!tab || isEntryTab(tab)) {
1727
+ unmountPagePanelSurface(host);
1728
+ if (_host === host) {
1729
+ _host = null;
1730
+ _ctx = null;
1731
+ }
1732
+ /* A collection entry's tab is titled Entry and draws the entry's own settings — status,
1733
+ fields, page URL, file (§22.5) — rather than a page's title, head and SEO preview: the
1734
+ entry's page is its collection's template, and its head is that page's. */
1735
+ if (tab) {
1736
+ renderEntryInspector(host, tab);
1737
+ } else {
1738
+ unmountEntryInspector(host);
1739
+ }
1740
+ return;
1741
+ }
1742
+ unmountEntryInspector(host);
1743
+ /* A component is not a page: it has no title, no head and no social card of its own — the pages
1744
+ that use it do. Its own settings (its tag, the props it takes and their defaults) are its ROOT
1745
+ element's, edited in Element › Content, so the tab says that and goes there. */
1746
+ if (pageTabTitle(tab.documentPath) === "Component") {
1747
+ unmountPagePanelSurface(host);
1748
+ if (_host === host) {
1749
+ _host = null;
1750
+ _ctx = null;
1751
+ }
1752
+ // Assigned on every repaint; placed only when it is not already the tab's one child.
1753
+ const state = emptyState(host, componentTabSpec(tab));
1754
+ if (host.firstChild !== state || host.childNodes.length !== 1) {
1755
+ host.replaceChildren(state);
1756
+ }
1757
+ return;
1758
+ }
1759
+ const isContent = tab.doc.mode === "content";
1760
+ renderPagePanel(host, {
1761
+ /* The Page tab IS an app-level surface: it shows the focused document by definition, so it
1762
+ spells that out at the call site instead of leaving it to a helper two other surfaces share. */
1763
+ applyMutation: isContent
1764
+ ? (fn) => applyContentMutation(activeTab.value, renderPageTab, fn)
1765
+ : (fn) => {
1766
+ transact(activeTab.value, fn);
1767
+ },
1768
+ document: isContent
1769
+ ? buildHeadDoc(tab.doc.document, tab.doc.content.frontmatter)
1770
+ : tab.doc.document,
1771
+ rerender: renderPageTab,
1486
1772
  });
1487
1773
  }