@jxsuite/studio 6.1.2 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (522) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/2020-5wtzce5g.js +4 -0
  3. package/dist/chunks/adapter-5tphg7xe.js +4 -0
  4. package/dist/chunks/add-new-flow-psym14sn.js +4 -0
  5. package/dist/chunks/cf-account-picker-haqazq5r.js +4 -0
  6. package/dist/chunks/cf-settings-h2wrh6hq.js +4 -0
  7. package/dist/chunks/{color-area-0fj8a4ga.js → color-area-6rbra5dy.js} +2 -2
  8. package/dist/chunks/{color-field-xch8kx2g.js → color-field-gy56c94g.js} +3 -3
  9. package/dist/chunks/color-m7r1vk54.js +4 -0
  10. package/dist/chunks/{color-slider-jxd8v350.js → color-slider-sqqdyf6c.js} +2 -2
  11. package/dist/chunks/{combobox-p0efwjke.js → combobox-72h32ejk.js} +3 -3
  12. package/dist/chunks/components-r22xca09.js +4 -0
  13. package/dist/chunks/content-source-mmppmcyg.js +4 -0
  14. package/dist/chunks/convert-to-repeater-he70fc2b.js +4 -0
  15. package/dist/chunks/editor-ccd9jwsa.js +4 -0
  16. package/dist/chunks/entry-commands-6qxz9e8p.js +4 -0
  17. package/dist/chunks/extension-sections-0egb88dm.js +4 -0
  18. package/dist/chunks/file-ops-qnab36ek.js +4 -0
  19. package/dist/chunks/files-rcn56qvm.js +4 -0
  20. package/dist/chunks/format-host-yxz0kjjs.js +4 -0
  21. package/dist/chunks/fs-events-d1qxq4z4.js +4 -0
  22. package/dist/chunks/grid-open-4z1g062t.js +4 -0
  23. package/dist/chunks/{grid-view-d2wq9ezg.js → grid-view-qg1b5eqx.js} +25 -25
  24. package/dist/chunks/{icons-pnfz478q.js → icons-63bjtbs7.js} +2 -2
  25. package/dist/chunks/{iframe-entry-jdcktez8.js → iframe-entry-2zggzajv.js} +4 -4
  26. package/dist/chunks/{iframe-entry-zap785cw.js → iframe-entry-76j8vwwx.js} +4 -4
  27. package/dist/chunks/iframe-entry-99nr5zfb.js +5 -0
  28. package/dist/chunks/{iframe-entry-x0ygf2yr.js → iframe-entry-pkgmcgp0.js} +4 -4
  29. package/dist/chunks/iframe-host-ssv7kyj0.js +4 -0
  30. package/dist/chunks/{index-zpjmzqfk.js → index-32eqzv1k.js} +3 -3
  31. package/dist/chunks/index-f0jn7nnp.js +4 -0
  32. package/dist/chunks/{javascript-m54mnwp6.js → javascript-pjp734q4.js} +3 -3
  33. package/dist/chunks/jsonMode-njkvsa02.js +13 -0
  34. package/dist/chunks/jx-validate-ttasrbh7.js +4 -0
  35. package/dist/chunks/layers-bgyr11tr.js +4 -0
  36. package/dist/chunks/layers-panel-7ez66zeq.js +4 -0
  37. package/dist/chunks/{listbox-e74qjyq6.js → listbox-c421cf8c.js} +2 -2
  38. package/dist/chunks/lit-html-v9gh687x.js +4 -0
  39. package/dist/chunks/media-library-sq468wts.js +4 -0
  40. package/dist/chunks/media-picker-shfe59pt.js +4 -0
  41. package/dist/chunks/{menu-n6yzswmy.js → menu-bdpzbfvs.js} +2 -2
  42. package/dist/chunks/{monaco-binding-7gx83x3r.js → monaco-binding-vq1hgnn0.js} +3 -3
  43. package/dist/chunks/monaco-buffer-3afgwvbp.js +4 -0
  44. package/dist/chunks/monaco-setup-xrd2cbkp.js +4 -0
  45. package/dist/chunks/notify-kzrjr3fv.js +4 -0
  46. package/dist/chunks/parse-v77xtwv7.js +4 -0
  47. package/dist/chunks/platform-7v4rtd7m.js +4 -0
  48. package/dist/chunks/popover-b89sfej7.js +4 -0
  49. package/dist/chunks/publish-panel-2z9gw8ee.js +4 -0
  50. package/dist/chunks/publish-quick-qx2455cj.js +4 -0
  51. package/dist/chunks/publish-workspace-wrc1dgwx.js +4 -0
  52. package/dist/chunks/quick-search-n0t0eqxb.js +4 -0
  53. package/dist/chunks/references-9547rg5s.js +4 -0
  54. package/dist/chunks/right-panel-wsgnr9pn.js +4 -0
  55. package/dist/chunks/source-control-kwktr9y6.js +4 -0
  56. package/dist/chunks/{split-n2gfpm5m.js → split-c9zfdgxf.js} +2 -2
  57. package/dist/chunks/studio-03ct6yfv.js +9 -0
  58. package/dist/chunks/studio-07rcfje3.js +5 -0
  59. package/dist/chunks/studio-3e54qh39.js +7 -0
  60. package/dist/chunks/studio-3exxhz5g.js +5 -0
  61. package/dist/chunks/studio-3q40e3rj.js +5 -0
  62. package/dist/chunks/studio-3qeh9t0h.js +6 -0
  63. package/dist/chunks/studio-3qqjeep5.js +6 -0
  64. package/dist/chunks/studio-43sd2mam.js +4 -0
  65. package/dist/chunks/studio-4hp8pkyt.js +5 -0
  66. package/dist/chunks/studio-58xg6j2q.js +9 -0
  67. package/dist/chunks/studio-5g0rprdt.js +7 -0
  68. package/dist/chunks/{studio-2j1cb95x.js → studio-6m380wp7.js} +4 -4
  69. package/dist/chunks/studio-80jyh1xh.js +4 -0
  70. package/dist/chunks/studio-83mk4bgg.js +9 -0
  71. package/dist/chunks/{studio-v0jzqdjm.js → studio-881z1xrx.js} +4 -4
  72. package/dist/chunks/studio-8m0y2qe7.js +5 -0
  73. package/dist/chunks/{studio-d81xfh1g.js → studio-8tr6cfrc.js} +4 -4
  74. package/dist/chunks/studio-8vcvfs7r.js +6 -0
  75. package/dist/chunks/studio-9tmebf0s.js +5 -0
  76. package/dist/chunks/studio-a2bvaptz.js +5 -0
  77. package/dist/chunks/studio-b7y5mr30.js +5 -0
  78. package/dist/chunks/studio-bavb31r0.js +5 -0
  79. package/dist/chunks/{studio-dgz8d0c1.js → studio-bbe4c90a.js} +4 -4
  80. package/dist/chunks/studio-c05wdnt3.js +5 -0
  81. package/dist/chunks/studio-ccv2svc6.js +5 -0
  82. package/dist/chunks/studio-d5grtb9e.js +16 -0
  83. package/dist/chunks/studio-df0baea9.js +5 -0
  84. package/dist/chunks/studio-dgk4dqtx.js +7 -0
  85. package/dist/chunks/studio-e1g64q43.js +5 -0
  86. package/dist/chunks/studio-ed1hp6zf.js +6 -0
  87. package/dist/chunks/studio-ed3xx40h.js +7 -0
  88. package/dist/chunks/studio-f14e1zdj.js +5 -0
  89. package/dist/chunks/studio-fbhxxc5f.js +5 -0
  90. package/dist/chunks/studio-fyh0jc96.js +5 -0
  91. package/dist/chunks/studio-gcxk20n7.js +5 -0
  92. package/dist/chunks/studio-jaaz8ce9.js +5 -0
  93. package/dist/chunks/{studio-90pq9km1.js → studio-jcnsdaep.js} +4 -4
  94. package/dist/chunks/studio-jsgf2q5k.js +7 -0
  95. package/dist/chunks/{studio-0q988f9w.js → studio-k3xxbagr.js} +3 -3
  96. package/dist/chunks/{studio-5ve6bbep.js → studio-k7evxqh4.js} +4 -4
  97. package/dist/chunks/studio-kt3pjkwd.js +5 -0
  98. package/dist/chunks/studio-m8g16bgz.js +5 -0
  99. package/dist/chunks/studio-mn0476ag.js +5 -0
  100. package/dist/chunks/studio-mx1smyq0.js +5 -0
  101. package/dist/chunks/studio-n0c5h6mf.js +5 -0
  102. package/dist/chunks/{studio-485046te.js → studio-nh6d342n.js} +7 -7
  103. package/dist/chunks/studio-nqxqf8p2.js +4 -0
  104. package/dist/chunks/studio-p4nrkvqr.js +5 -0
  105. package/dist/chunks/{studio-1fxkaaea.js → studio-pbxa5fpc.js} +4 -4
  106. package/dist/chunks/studio-pgmvdv7z.js +7 -0
  107. package/dist/chunks/studio-ph84avkf.js +5 -0
  108. package/dist/chunks/{studio-tq0rtfwj.js → studio-q0mvbaq6.js} +4 -4
  109. package/dist/chunks/studio-q0tccd43.js +5 -0
  110. package/dist/chunks/studio-q7rtx6zq.js +14 -0
  111. package/dist/chunks/{studio-a3g98wjr.js → studio-qgh13jtz.js} +4 -4
  112. package/dist/chunks/studio-qmn1xc24.js +5 -0
  113. package/dist/chunks/studio-qwdqwkj0.js +5 -0
  114. package/dist/chunks/studio-r5qy30vx.js +5 -0
  115. package/dist/chunks/studio-rj9eamf8.js +5 -0
  116. package/dist/chunks/studio-rptgsk96.js +5 -0
  117. package/dist/chunks/studio-rthyg42z.js +928 -0
  118. package/dist/chunks/studio-s19tp1xr.js +5 -0
  119. package/dist/chunks/studio-s32578pn.js +5 -0
  120. package/dist/chunks/studio-svb7yyve.js +5 -0
  121. package/dist/chunks/studio-syreb644.js +5 -0
  122. package/dist/chunks/studio-tf9jryb1.js +10 -0
  123. package/dist/chunks/studio-tfkznv39.js +5 -0
  124. package/dist/chunks/studio-tft6r105.js +5 -0
  125. package/dist/chunks/studio-txf2kysx.js +5 -0
  126. package/dist/chunks/studio-v8867ddv.js +12 -0
  127. package/dist/chunks/studio-vxq7fcvv.js +5 -0
  128. package/dist/chunks/studio-w4rz9hdk.js +5 -0
  129. package/dist/chunks/studio-wbcjbhy1.js +9 -0
  130. package/dist/chunks/studio-wc649wss.js +5 -0
  131. package/dist/chunks/studio-wys23mxz.js +5 -0
  132. package/dist/chunks/studio-x1x167jh.js +5 -0
  133. package/dist/chunks/studio-x9hsze61.js +5 -0
  134. package/dist/chunks/studio-y0n7yxea.js +9 -0
  135. package/dist/chunks/studio-y3ahhdpx.js +5 -0
  136. package/dist/chunks/studio-z97c48fk.js +5 -0
  137. package/dist/chunks/studio-zat62m0c.js +5 -0
  138. package/dist/chunks/studio-zcd2e7yz.js +5 -0
  139. package/dist/chunks/studio-zz9qe3rw.js +64 -0
  140. package/dist/chunks/toast-1gv0h53h.js +4 -0
  141. package/dist/chunks/{toast-host-ke5z4ng7.js → toast-host-1yg5ar8d.js} +3 -3
  142. package/dist/chunks/{tooltip-5v8ngwj2.js → tooltip-yypz28gq.js} +3 -3
  143. package/dist/chunks/{tree-mdq3v5cw.js → tree-9xa4hm7n.js} +2 -2
  144. package/dist/chunks/tsMode-epyfz1hv.js +14 -0
  145. package/dist/chunks/workspace-dz7nxfhv.js +4 -0
  146. package/dist/chunks/workspace-home-xsd7hkjw.js +4 -0
  147. package/dist/chunks/{ws-client-nze0k6mv.js → ws-client-bv552sfx.js} +3 -3
  148. package/dist/iframe-entry.js +20 -16
  149. package/dist/manifest.json +77 -63
  150. package/dist/studio.js +103 -104
  151. package/dist/workers/ts.worker.js +3 -3
  152. package/package.json +12 -11
  153. package/src/browse/component-examples.ts +272 -0
  154. package/src/browse/library-commands.ts +64 -104
  155. package/src/browse/library-layouts.ts +263 -49
  156. package/src/browse/library-model.ts +181 -133
  157. package/src/browse/library-pane.ts +738 -446
  158. package/src/browse/library-preview.ts +149 -19
  159. package/src/browse/library-source.ts +308 -41
  160. package/src/browse/preview-root.ts +78 -0
  161. package/src/browse/preview-style.ts +155 -0
  162. package/src/canvas/asset-refs.ts +16 -1
  163. package/src/canvas/canvas-live-render.ts +222 -30
  164. package/src/canvas/canvas-origin.ts +37 -12
  165. package/src/canvas/canvas-page-context.ts +112 -0
  166. package/src/canvas/canvas-render.ts +113 -107
  167. package/src/canvas/canvas-utils.ts +57 -12
  168. package/src/canvas/iframe-drop.ts +107 -0
  169. package/src/canvas/iframe-entry.ts +24 -12
  170. package/src/canvas/iframe-host.ts +134 -62
  171. package/src/canvas/iframe-keys.ts +11 -0
  172. package/src/canvas/iframe-patch.ts +8 -7
  173. package/src/canvas/iframe-protocol.ts +24 -5
  174. package/src/canvas/iframe-render.ts +48 -1
  175. package/src/canvas/iframe-subtree.ts +5 -2
  176. package/src/canvas/npm-modules.ts +22 -0
  177. package/src/canvas/preview-navigate.ts +16 -0
  178. package/src/canvas/repeater-sources.ts +200 -0
  179. package/src/canvas/stage-scrollbar.ts +5 -1
  180. package/src/canvas/surface-registry.ts +6 -0
  181. package/src/commands/app-commands.ts +16 -6
  182. package/src/commands/budget.ts +10 -8
  183. package/src/commands/context.ts +35 -10
  184. package/src/commands/defaults.ts +69 -45
  185. package/src/commands/keymap.ts +9 -0
  186. package/src/commands/levels.ts +11 -5
  187. package/src/commands/live-context.ts +2 -0
  188. package/src/commands/registry.ts +10 -1
  189. package/src/content/collection-match.ts +6 -5
  190. package/src/content/collection-pages.ts +412 -0
  191. package/src/content/content-collections.ts +270 -0
  192. package/src/content/entry-commands.ts +51 -60
  193. package/src/content/entry-editor.ts +111 -18
  194. package/src/content/entry-fields.ts +13 -1
  195. package/src/content/entry-inspector.ts +225 -0
  196. package/src/content/entry-model.ts +17 -3
  197. package/src/content/entry-open.ts +80 -0
  198. package/src/content/entry-summaries.ts +440 -0
  199. package/src/content/file-record-ids.ts +80 -0
  200. package/src/content/file-records.ts +125 -0
  201. package/src/content/page-summaries.ts +129 -0
  202. package/src/content/record-entry.ts +201 -0
  203. package/src/content/writing-banner.ts +177 -0
  204. package/src/create/add-new-commands.ts +185 -0
  205. package/src/create/add-new-flow.ts +1116 -0
  206. package/src/create/add-new.ts +1014 -0
  207. package/src/create/element-name.ts +121 -0
  208. package/src/editor/convert-to-component.ts +67 -43
  209. package/src/editor/file-drop-action.ts +9 -1
  210. package/src/editor/inline-edit-apply.ts +44 -0
  211. package/src/editor/inline-edit.ts +10 -0
  212. package/src/editor/insert-component.ts +91 -0
  213. package/src/editor/insert-zone-action.ts +13 -4
  214. package/src/editor/shortcuts.ts +44 -22
  215. package/src/editor/slash-menu.ts +51 -8
  216. package/src/files/components.ts +74 -10
  217. package/src/files/file-ops.ts +15 -5
  218. package/src/files/files.ts +164 -150
  219. package/src/files/fs-events.ts +18 -7
  220. package/src/files/media-meta.ts +95 -1
  221. package/src/files/media-types.ts +55 -0
  222. package/src/files/media-upload.ts +209 -52
  223. package/src/files/new-file-name.ts +156 -0
  224. package/src/files/serialize-document.ts +13 -3
  225. package/src/format/format-host.ts +15 -0
  226. package/src/github/github-auth.ts +2 -2
  227. package/src/github/github-publish.ts +4 -3
  228. package/src/grid/grid-controller.ts +5 -1
  229. package/src/grid/grid-open.ts +0 -27
  230. package/src/grid/grid-panel.ts +54 -2
  231. package/src/grid/grid-reveal.ts +34 -0
  232. package/src/grid/grid-source.ts +3 -25
  233. package/src/grid/grid-view.ts +31 -6
  234. package/src/grid/sources/content-source.ts +75 -7
  235. package/src/grid/tabulator-tables.d.ts +6 -0
  236. package/src/hosting/document.ts +2 -2
  237. package/src/i18n/i18n-commands.ts +2 -1
  238. package/src/media/media-commands.ts +181 -0
  239. package/src/media/media-library-model.ts +232 -0
  240. package/src/media/media-library.ts +1283 -0
  241. package/src/media/media-pane.ts +47 -19
  242. package/src/packages/pull-package-sync.ts +1 -1
  243. package/src/panels/activity-panel.ts +21 -5
  244. package/src/panels/ai-chat/chat-markdown.ts +8 -8
  245. package/src/panels/block-action-bar.ts +11 -11
  246. package/src/panels/canvas-dnd-bridge.ts +5 -2
  247. package/src/panels/chat-panel.ts +6 -6
  248. package/src/panels/collections-panel.ts +306 -0
  249. package/src/panels/component-preview.ts +35 -13
  250. package/src/panels/data-explorer.ts +1 -1
  251. package/src/panels/data-grid.ts +4 -0
  252. package/src/panels/dnd.ts +17 -30
  253. package/src/panels/elements-panel.ts +9 -2
  254. package/src/panels/empty-state.ts +2 -2
  255. package/src/panels/formula-workspace.ts +2 -3
  256. package/src/panels/frontmatter-fields.ts +9 -9
  257. package/src/panels/git-diff-open.ts +9 -5
  258. package/src/panels/head-panel.ts +369 -103
  259. package/src/panels/i18n-panel.ts +11 -10
  260. package/src/panels/imports-panel.ts +2 -0
  261. package/src/panels/jump-bar.ts +27 -2
  262. package/src/panels/layers-panel.ts +36 -30
  263. package/src/panels/left-panel.ts +22 -8
  264. package/src/panels/navigator-panels.ts +23 -11
  265. package/src/panels/pane-context.ts +111 -29
  266. package/src/panels/pane-grid.ts +6 -6
  267. package/src/panels/panel-registry.ts +14 -9
  268. package/src/panels/properties-panel.ts +7 -3
  269. package/src/panels/right-panel.ts +199 -43
  270. package/src/panels/seo-modal.ts +41 -108
  271. package/src/panels/settings-menu.ts +52 -50
  272. package/src/panels/settings-pane.ts +268 -162
  273. package/src/panels/signals-panel.ts +3 -4
  274. package/src/panels/site-model.ts +367 -0
  275. package/src/panels/site-panel.ts +638 -0
  276. package/src/panels/style-panel.ts +37 -3
  277. package/src/panels/stylebook-doc.ts +142 -10
  278. package/src/panels/stylebook-layers-panel.ts +39 -80
  279. package/src/panels/stylebook-panel.ts +47 -10
  280. package/src/panels/styles-panel.ts +327 -0
  281. package/src/panels/tab-strip.ts +109 -25
  282. package/src/platform.ts +1 -1
  283. package/src/platforms/cloud.ts +28 -1
  284. package/src/platforms/default-platform.ts +4 -4
  285. package/src/platforms/devserver.ts +51 -25
  286. package/src/publish/deploy-checklist.ts +12 -7
  287. package/src/publish/pages-service.ts +38 -14
  288. package/src/publish/publish-changes.ts +491 -0
  289. package/src/publish/publish-commands.ts +83 -18
  290. package/src/publish/publish-ids.ts +49 -0
  291. package/src/publish/publish-quick.ts +148 -0
  292. package/src/publish/publish-workspace.ts +926 -0
  293. package/src/publish/source-control.ts +907 -0
  294. package/src/services/automation.ts +16 -20
  295. package/src/services/cf-settings.ts +4 -4
  296. package/src/services/connection.ts +2 -2
  297. package/src/services/directory-picker.ts +2 -2
  298. package/src/services/references.ts +0 -0
  299. package/src/services/render-critic.ts +21 -2
  300. package/src/services/settings/definitions.ts +25 -14
  301. package/src/settings/contributed-section.ts +49 -10
  302. package/src/settings/css-vars-editor.ts +93 -27
  303. package/src/settings/developer-panels.ts +43 -0
  304. package/src/settings/extension-sections.ts +20 -2
  305. package/src/settings/general-settings.ts +91 -21
  306. package/src/settings/preferences-dialog.ts +8 -1
  307. package/src/settings/project-sections.ts +27 -10
  308. package/src/settings/schema-field-ui.ts +3 -1
  309. package/src/settings/section-registry.ts +116 -11
  310. package/src/settings/settings-dialog.ts +192 -0
  311. package/src/settings/settings-document.ts +182 -162
  312. package/src/shell.ts +467 -431
  313. package/src/site-context.ts +64 -10
  314. package/src/state.ts +28 -9
  315. package/src/studio.ts +194 -41
  316. package/src/style/project-styles.ts +34 -0
  317. package/src/style/stylebook-specimens.ts +89 -0
  318. package/src/style/stylebook-target.ts +26 -0
  319. package/src/style/tag-inheritance.ts +105 -0
  320. package/src/style/variables-focus.ts +61 -0
  321. package/src/style/variables-view.ts +235 -0
  322. package/src/surfaces/add-new.json +1482 -0
  323. package/src/surfaces/add-new.ts +325 -0
  324. package/src/surfaces/ai-chat.ts +2 -2
  325. package/src/surfaces/boot-failure.json +4 -3
  326. package/src/surfaces/bottom-dock.json +1 -1
  327. package/src/surfaces/canvas-stage.json +30 -19
  328. package/src/surfaces/canvas-stage.ts +5 -5
  329. package/src/surfaces/collections-panel.json +615 -0
  330. package/src/surfaces/collections-panel.ts +168 -0
  331. package/src/surfaces/commandbar.json +241 -96
  332. package/src/surfaces/commandbar.ts +134 -99
  333. package/src/surfaces/empty-state.json +1 -1
  334. package/src/surfaces/entry-editor.json +136 -0
  335. package/src/surfaces/entry-editor.ts +21 -0
  336. package/src/surfaces/entry-inspector.json +371 -0
  337. package/src/surfaces/entry-inspector.ts +143 -0
  338. package/src/surfaces/inspector-dock.json +162 -14
  339. package/src/surfaces/inspector-dock.ts +46 -19
  340. package/src/surfaces/library-pane.json +670 -437
  341. package/src/surfaces/library-pane.ts +94 -59
  342. package/src/surfaces/media-browser.json +62 -6
  343. package/src/surfaces/media-browser.ts +15 -1
  344. package/src/surfaces/media-field.json +1 -1
  345. package/src/surfaces/media-library.json +1586 -0
  346. package/src/surfaces/media-library.ts +282 -0
  347. package/src/surfaces/media-pick-dialog.json +71 -0
  348. package/src/surfaces/media-pick-dialog.ts +132 -0
  349. package/src/surfaces/menu.ts +18 -0
  350. package/src/surfaces/pane-context.json +144 -77
  351. package/src/surfaces/pane-context.ts +24 -0
  352. package/src/surfaces/panel-deploy-checklist.json +12 -6
  353. package/src/surfaces/panel-deploy-checklist.ts +3 -1
  354. package/src/surfaces/panel-page.json +72 -2
  355. package/src/surfaces/panel-page.ts +38 -7
  356. package/src/surfaces/panel-signals.json +1 -1
  357. package/src/surfaces/panel-signals.ts +5 -5
  358. package/src/surfaces/panel-stylebook-layers.json +71 -204
  359. package/src/surfaces/panel-stylebook-layers.ts +0 -21
  360. package/src/surfaces/preferences.json +120 -48
  361. package/src/surfaces/preferences.ts +8 -0
  362. package/src/surfaces/publish-quick.json +428 -0
  363. package/src/surfaces/publish-quick.ts +165 -0
  364. package/src/surfaces/publish-workspace.json +2892 -0
  365. package/src/surfaces/publish-workspace.ts +391 -0
  366. package/src/surfaces/rail.json +1 -159
  367. package/src/surfaces/rail.ts +18 -75
  368. package/src/surfaces/schema-form.json +10 -2
  369. package/src/surfaces/schema-form.ts +2 -0
  370. package/src/surfaces/seo.json +1 -1
  371. package/src/surfaces/settings-contributed.json +5 -5
  372. package/src/surfaces/settings-contributed.ts +18 -2
  373. package/src/surfaces/settings-css-vars.json +44 -36
  374. package/src/surfaces/settings-css-vars.ts +77 -4
  375. package/src/surfaces/settings-deploy.json +2 -15
  376. package/src/surfaces/settings-deploy.ts +1 -1
  377. package/src/surfaces/settings-dialog.json +51 -0
  378. package/src/surfaces/settings-dialog.ts +169 -0
  379. package/src/surfaces/settings-overview.json +3 -3
  380. package/src/surfaces/settings-overview.ts +9 -2
  381. package/src/surfaces/settings-pane.json +269 -46
  382. package/src/surfaces/settings-pane.ts +85 -24
  383. package/src/surfaces/shell.json +2 -1
  384. package/src/surfaces/site-panel.json +1029 -0
  385. package/src/surfaces/site-panel.ts +233 -0
  386. package/src/surfaces/slash-menu.json +63 -21
  387. package/src/surfaces/slash-menu.ts +23 -0
  388. package/src/surfaces/statusbar.ts +39 -15
  389. package/src/surfaces/styles-panel.json +735 -0
  390. package/src/surfaces/styles-panel.ts +195 -0
  391. package/src/surfaces/styles-variables.json +149 -0
  392. package/src/surfaces/styles-variables.ts +145 -0
  393. package/src/surfaces/tab-strip.json +4 -4
  394. package/src/surfaces/welcome.ts +1 -1
  395. package/src/surfaces/writing-banner.json +94 -0
  396. package/src/surfaces/writing-banner.ts +80 -0
  397. package/src/tabs/bare-repeaters.ts +101 -0
  398. package/src/tabs/tab.ts +47 -7
  399. package/src/types.ts +21 -26
  400. package/src/ui/form-controls.ts +47 -8
  401. package/src/ui/layers.ts +13 -0
  402. package/src/ui/media-picker.ts +284 -40
  403. package/src/ui/regions.ts +7 -6
  404. package/src/ui/schema-form.ts +259 -27
  405. package/src/ui/virtual-window.ts +26 -1
  406. package/src/utils/base64.ts +3 -3
  407. package/src/utils/edit-display.ts +322 -67
  408. package/src/utils/local-day.ts +18 -0
  409. package/src/workspace/pane-derive.ts +4 -4
  410. package/src/workspace/session.ts +28 -7
  411. package/src/workspace/tab-workspace.ts +137 -0
  412. package/src/workspace/workspace-back.ts +189 -0
  413. package/src/workspace/workspace-home.ts +133 -0
  414. package/src/workspace/workspace-ids.ts +129 -0
  415. package/src/workspace/workspace-switch.ts +372 -0
  416. package/src/workspace/workspace.ts +125 -11
  417. package/styles/shell-frame.css +30 -7
  418. package/styles/shell-frame.json +15 -3
  419. package/dist/chunks/2020-f2gkpy8d.js +0 -4
  420. package/dist/chunks/adapter-4912g339.js +0 -4
  421. package/dist/chunks/cf-account-picker-wccaenh6.js +0 -4
  422. package/dist/chunks/cf-settings-br6105tr.js +0 -4
  423. package/dist/chunks/color-28ksk2qm.js +0 -4
  424. package/dist/chunks/components-db68vsn7.js +0 -4
  425. package/dist/chunks/content-source-1449qesq.js +0 -4
  426. package/dist/chunks/convert-to-repeater-5h457egj.js +0 -4
  427. package/dist/chunks/editor-x9jn86a7.js +0 -4
  428. package/dist/chunks/entry-commands-may24cpt.js +0 -4
  429. package/dist/chunks/extension-sections-pvr14vxr.js +0 -4
  430. package/dist/chunks/file-ops-x9v54pcj.js +0 -4
  431. package/dist/chunks/files-j3knqf0s.js +0 -4
  432. package/dist/chunks/format-host-tyre2trb.js +0 -4
  433. package/dist/chunks/fs-events-64ma9htc.js +0 -4
  434. package/dist/chunks/git-panel-84ps5dex.js +0 -4
  435. package/dist/chunks/iframe-entry-fvbk89gm.js +0 -5
  436. package/dist/chunks/iframe-host-ck6vejt5.js +0 -4
  437. package/dist/chunks/index-b8xw3vy9.js +0 -4
  438. package/dist/chunks/jsonMode-w15cmrcm.js +0 -13
  439. package/dist/chunks/jx-validate-at3zn8wa.js +0 -4
  440. package/dist/chunks/layers-h4xp1pss.js +0 -4
  441. package/dist/chunks/layers-panel-m5hw1d7g.js +0 -4
  442. package/dist/chunks/lit-html-8t8t3dnr.js +0 -4
  443. package/dist/chunks/media-picker-2cweexc5.js +0 -4
  444. package/dist/chunks/menu-ynw13y0s.js +0 -4
  445. package/dist/chunks/monaco-buffer-qzxn567r.js +0 -4
  446. package/dist/chunks/monaco-setup-gxf6656g.js +0 -4
  447. package/dist/chunks/notify-cj5zbrs2.js +0 -4
  448. package/dist/chunks/parse-b7ddhr8m.js +0 -4
  449. package/dist/chunks/platform-3dfws9js.js +0 -4
  450. package/dist/chunks/popover-jt2gxmzs.js +0 -4
  451. package/dist/chunks/publish-panel-1gvxbj21.js +0 -4
  452. package/dist/chunks/quick-search-0dajrwqg.js +0 -4
  453. package/dist/chunks/references-49qgt404.js +0 -4
  454. package/dist/chunks/right-panel-jm1bwe53.js +0 -4
  455. package/dist/chunks/section-registry-6kwc4fky.js +0 -4
  456. package/dist/chunks/studio-0rt52vpq.js +0 -5
  457. package/dist/chunks/studio-18vgbhs5.js +0 -5
  458. package/dist/chunks/studio-24mccdrj.js +0 -5
  459. package/dist/chunks/studio-2eh507zk.js +0 -9
  460. package/dist/chunks/studio-2hcbccfv.js +0 -5
  461. package/dist/chunks/studio-2jbs3rxv.js +0 -5
  462. package/dist/chunks/studio-4h8k4dw9.js +0 -5
  463. package/dist/chunks/studio-5f6n18kx.js +0 -5
  464. package/dist/chunks/studio-5gthpky6.js +0 -5
  465. package/dist/chunks/studio-6ep3js6b.js +0 -9
  466. package/dist/chunks/studio-6wkmf3wz.js +0 -9
  467. package/dist/chunks/studio-841rf3cg.js +0 -5
  468. package/dist/chunks/studio-8gmwgheg.js +0 -5
  469. package/dist/chunks/studio-8qm0ep50.js +0 -5
  470. package/dist/chunks/studio-924p1tm1.js +0 -6
  471. package/dist/chunks/studio-a3shpz9y.js +0 -5
  472. package/dist/chunks/studio-amsbaaar.js +0 -6
  473. package/dist/chunks/studio-at5spjbc.js +0 -7
  474. package/dist/chunks/studio-b2xv945e.js +0 -5
  475. package/dist/chunks/studio-bn5nkafw.js +0 -5
  476. package/dist/chunks/studio-bw6q5xjf.js +0 -9
  477. package/dist/chunks/studio-c7q0t9x1.js +0 -5
  478. package/dist/chunks/studio-camnhc96.js +0 -6
  479. package/dist/chunks/studio-cffqvnzd.js +0 -5
  480. package/dist/chunks/studio-efvfrsf8.js +0 -5
  481. package/dist/chunks/studio-er30bpcx.js +0 -5
  482. package/dist/chunks/studio-ezscgr64.js +0 -5
  483. package/dist/chunks/studio-ffrrncvz.js +0 -5
  484. package/dist/chunks/studio-fpphgn8y.js +0 -10
  485. package/dist/chunks/studio-j1kvhdjk.js +0 -4
  486. package/dist/chunks/studio-j430n16g.js +0 -7
  487. package/dist/chunks/studio-jjkg84nj.js +0 -5
  488. package/dist/chunks/studio-m0q32ars.js +0 -4
  489. package/dist/chunks/studio-m6bdst11.js +0 -5
  490. package/dist/chunks/studio-nv2cptd4.js +0 -64
  491. package/dist/chunks/studio-nwfrc98n.js +0 -5
  492. package/dist/chunks/studio-p1wgvp7r.js +0 -5
  493. package/dist/chunks/studio-pag2wncm.js +0 -5
  494. package/dist/chunks/studio-qvanvzfd.js +0 -5
  495. package/dist/chunks/studio-qwa64kt7.js +0 -5
  496. package/dist/chunks/studio-rk1zc4x3.js +0 -928
  497. package/dist/chunks/studio-se2r3ptc.js +0 -5
  498. package/dist/chunks/studio-sg2a0dh0.js +0 -5
  499. package/dist/chunks/studio-tjzjhe68.js +0 -5
  500. package/dist/chunks/studio-v2w7q8r1.js +0 -7
  501. package/dist/chunks/studio-v5vjq1h0.js +0 -4
  502. package/dist/chunks/studio-v8aga3vp.js +0 -14
  503. package/dist/chunks/studio-vfcdrfmv.js +0 -5
  504. package/dist/chunks/studio-vmfkgdjf.js +0 -5
  505. package/dist/chunks/studio-wt1816jj.js +0 -5
  506. package/dist/chunks/studio-wz73q5kq.js +0 -4
  507. package/dist/chunks/studio-y04qtxvp.js +0 -7
  508. package/dist/chunks/studio-y21g8zzg.js +0 -5
  509. package/dist/chunks/studio-y64hrt8p.js +0 -12
  510. package/dist/chunks/studio-ynn90f58.js +0 -16
  511. package/dist/chunks/studio-yxz682sx.js +0 -5
  512. package/dist/chunks/studio-zp7snvkq.js +0 -5
  513. package/dist/chunks/studio-zq4z6tm6.js +0 -5
  514. package/dist/chunks/toast-c9fn075p.js +0 -4
  515. package/dist/chunks/tsMode-sf49ee9x.js +0 -14
  516. package/dist/chunks/workspace-jtfahpee.js +0 -4
  517. package/src/panels/frontmatter-panel.ts +0 -711
  518. package/src/panels/git-panel.ts +0 -1108
  519. package/src/surfaces/doc-header.json +0 -776
  520. package/src/surfaces/doc-header.ts +0 -222
  521. package/src/surfaces/git-panel.json +0 -1880
  522. package/src/surfaces/git-panel.ts +0 -264
@@ -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,29 +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
30
  import { isString } from "@jxsuite/schema/guards";
31
- import { nothing } from "lit-html";
32
- import { projectState, renderOnly } from "../store";
31
+ import { withPageDescription } from "@jxsuite/site/head-merger";
32
+ import { projectState, renderOnly, updateSession } from "../store";
33
33
  import type { DirEntry, JsonValue } from "../types";
34
34
  import { activeTab } from "../workspace/workspace";
35
35
  import { activeRegistry } from "../commands/active-registry";
36
36
  import { clearProblems, notify } from "../services/notify";
37
37
  import type { NotifyOptions } from "../services/notify";
38
- import { registerPanel } from "./panel-registry";
39
38
  import { mutateUpdateFrontmatter, transact, transactDoc } from "../tabs/transact";
40
39
  import { collectFmFields, projectFmField } from "./frontmatter-fields";
41
40
  import { isGoogleFontEntry, isGoogleFontPreconnect } from "../utils/google-fonts";
@@ -45,7 +44,15 @@ import { pageRoute } from "./tab-strip";
45
44
  import { LIVE_PREVIEW } from "../ui/timing";
46
45
  import { previewAssetSrc } from "../canvas/asset-refs";
47
46
  import { IMAGE_EXTENSIONS, extensionOf } from "../files/media-upload";
48
- 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";
49
56
 
50
57
  import type { JxHeadEntry, JxMutableNode } from "@jxsuite/schema/types";
51
58
  import type { Tab } from "../tabs/tab";
@@ -73,7 +80,14 @@ interface MetaField {
73
80
  /** @type {{ name: string; path: string }[] | null} */
74
81
  let layoutEntries: { name: string; path: string }[] | null = null;
75
82
 
76
- 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 }[]> {
77
91
  try {
78
92
  const platform = getPlatform();
79
93
  const listing = await platform.listDirectory("layouts");
@@ -89,8 +103,12 @@ async function loadLayoutEntries() {
89
103
  } catch {
90
104
  layoutEntries = [];
91
105
  }
92
- renderOnly("leftPanel");
93
- renderOnly("frontmatterPanel", "seoModal");
106
+ return layoutEntries;
107
+ }
108
+
109
+ async function loadLayoutEntries() {
110
+ await listLayoutEntries();
111
+ renderOnly("rightPanel", "seoModal");
94
112
  }
95
113
 
96
114
  /**
@@ -106,11 +124,10 @@ export function invalidateLayoutPickerCache() {
106
124
  /**
107
125
  * The layouts the picker offers, or `null` while the directory is still being listed.
108
126
  *
109
- * The listing BOTH surfaces draw their Layout picker from, handed over as DATA. The Page panel
110
- * (`src/surfaces/panel-page.json`) and the Document Header card (`src/surfaces/doc-header.json`)
111
- * are documents and neither can interpolate the other's markup — but a second LISTING would be a
112
- * second cache with a second lifetime, and {@link invalidateLayoutPickerCache} would then forget
113
- * 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.
114
131
  *
115
132
  * @returns {{ name: string; path: string }[] | null}
116
133
  */
@@ -144,17 +161,64 @@ export const OG_FIELDS: MetaField[] = [
144
161
  const MANAGED_META_KEYS = new Set([...PAGE_FIELDS, ...OG_FIELDS].map((f) => f.key));
145
162
 
146
163
  /**
147
- * Frontmatter keys the Document Header card owns with a dedicated control, so the generic field
148
- * 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.
149
166
  *
150
- * **The one policy.** `frontmatter-panel.ts` used to pass an EMPTY reserved set while this module
151
- * passed `{title}`, so the same key rendered as a bare Obsidian-style property above the canvas and
152
- * as the Page panel's Title field at the same time, with two different commit paths. Merging the
153
- * two field sets was only safe once one of the two policies won, and it is this one: a document has
154
- * 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.
155
170
  */
156
171
  export const RESERVED_FM_KEYS = new Set(["title"]);
157
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
+
158
222
  // ─── Helpers ─────────────────────────────────────────────────────────────
159
223
 
160
224
  /**
@@ -752,11 +816,10 @@ async function loadLayoutHead(path: string): Promise<void> {
752
816
  _layoutHeadPending = null;
753
817
  _layoutHeadPath = path;
754
818
  _layoutHead = doc?.$head ?? [];
755
- // THREE surfaces show the merged head now, and the layout layer arrives asynchronously: the
756
- // Document Header card, and Search appearance, which is where the previews went. A modal left
757
- // 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
758
821
  // "you have no description" on a page that inherits one.
759
- renderOnly("frontmatterPanel", "seoModal");
822
+ renderOnly("seoModal");
760
823
  }
761
824
 
762
825
  /**
@@ -833,7 +896,8 @@ export function seoPreviewFor(tab: Tab | null, doc: JxMutableNode): SeoPreview {
833
896
  {
834
897
  layout: layout.entries,
835
898
  layoutName: layout.name,
836
- 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),
837
901
  site: config?.$head ?? [],
838
902
  },
839
903
  ctx,
@@ -854,7 +918,7 @@ export function seoPreviewFor(tab: Tab | null, doc: JxMutableNode): SeoPreview {
854
918
  * component GAINED a header card because the focused tab was a page — and the Layout picker
855
919
  * appeared or vanished in the pane you were editing according to the document in the other one.
856
920
  *
857
- * 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
858
922
  * `activeTab.value`, where a reviewer can see it. The same bargain `setCanvasMode` made.
859
923
  *
860
924
  * @param {Tab | null} tab
@@ -877,10 +941,18 @@ export function buildHeadDoc(
877
941
  const headDoc: JxMutableNode = { ...doc };
878
942
  if (fm.title !== undefined) {
879
943
  /* SAFETY: deliberately not narrowed — a YAML title can be a number, and it is overlaid as
880
- written. Every reader of a head doc's `title` (titleRow here and in frontmatter-panel.ts,
881
- seoPreviewFor) tests it with isString before using it as text. */
944
+ written. Every reader of a head doc's `title` (titleRow here, seoPreviewFor) tests it with
945
+ isString before using it as text. */
882
946
  headDoc.title = fm.title as string;
883
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
+ }
884
956
  if (fm.$head !== undefined) {
885
957
  /* SAFETY: Studio's one writer of the frontmatter `$head` key is applyContentMutation below,
886
958
  which stores a JxHeadEntry[] or deletes the key. A hand-written one is trusted exactly as
@@ -897,13 +969,10 @@ export function buildHeadDoc(
897
969
  * adapts one to the other in the module that owns both, instead of in the Navigator orchestrator
898
970
  * that owns neither.
899
971
  *
900
- * **`tab` is a parameter for the same reason every other frontmatter helper's is.** It resolved
901
- * `activeTab.value` itself, and the Document Header card calls it for the CONTENT branch of every
902
- * mutation it makes — Title, Clear title, the Layout picker — as does Search appearance, which is
903
- * where the head fields went and which takes its tab the same way. The card is drawn per pane, so
904
- * on a markdown page the card in one pane retitled the document in the other. The comment beside
905
- * the card's JSON branch has claimed since the pane grid landed that this was fixed; the fix
906
- * 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.
907
976
  *
908
977
  * @param {Tab | null} tab The document to commit into.
909
978
  * @param {() => void} rerender
@@ -924,18 +993,54 @@ export function applyContentMutation(
924
993
  if (isString(fmNow.title)) {
925
994
  tmp.title = fmNow.title;
926
995
  }
996
+ if (isString(fmNow.description)) {
997
+ tmp.description = fmNow.description;
998
+ }
927
999
  if (fmHead) {
928
1000
  tmp.$head = [...fmHead];
929
1001
  }
930
- fn(tmp);
931
- if (tmp.title !== fmNow.title) {
932
- mutateUpdateFrontmatter(tab, "title", tmp.title);
1002
+ if (isString(fmNow.$layout) || fmNow.$layout === false) {
1003
+ tmp.$layout = fmNow.$layout;
933
1004
  }
1005
+ fn(tmp);
934
1006
  const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
935
- /* SAFETY: every entry here either came from the frontmatter (JSON, from the format's split) or
936
- was built by a head editor out of string attributes; JxHeadEntry is wider than JsonValue only
937
- through its open index signature, which no head editor writes. */
938
- mutateUpdateFrontmatter(tab, "$head", newHead as JsonValue | undefined);
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
+ }
939
1044
  rerender();
940
1045
  }
941
1046
 
@@ -947,15 +1052,22 @@ interface RowCommit {
947
1052
  clear: () => void;
948
1053
  /** Commit what the control settled on — a string from every kind but the checkbox. */
949
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;
950
1061
  }
951
1062
 
952
- /** 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. */
953
1064
  export interface PagePanelContext {
954
1065
  /** The head-bearing view of the open document — `buildHeadDoc` for a content page. */
955
1066
  document: JxMutableNode;
956
1067
  /** How a change to that view is written back into whichever realm it belongs to. */
957
1068
  applyMutation: (fn: (doc: JxMutableNode) => void) => void;
958
- renderLeftPanel: () => void;
1069
+ /** Repaint the host the panel is drawn in — the Inspector's Page tab. */
1070
+ rerender: () => void;
959
1071
  }
960
1072
 
961
1073
  /** Where the surface is mounted, and the context it was last drawn against. */
@@ -976,6 +1088,22 @@ let _addValue = "";
976
1088
  /** This projection's rows, by key. Rebuilt with each projection. */
977
1089
  const _commits = new Map<string, RowCommit>();
978
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
+
979
1107
  /**
980
1108
  * This projection's custom `$head` entries, by the key they were drawn with.
981
1109
  *
@@ -999,8 +1127,10 @@ const TAG_OPTIONS: SurfaceOption[] = [
999
1127
  /** A blank row, so every kind carries every field the document's bindings read. */
1000
1128
  function blankRow(key: string, prop: string, label: string): PageRow {
1001
1129
  return {
1130
+ action: "",
1002
1131
  checked: false,
1003
1132
  clearLabel: `Clear ${prop}`,
1133
+ hasAction: false,
1004
1134
  hasNote: false,
1005
1135
  hasThumb: false,
1006
1136
  isSet: false,
@@ -1026,6 +1156,7 @@ function thumbFor(row: PageRow, value: string): void {
1026
1156
  function titleRow(doc: JxMutableNode, apply: PagePanelContext["applyMutation"]): PageRow {
1027
1157
  const title = isString(doc.title) ? doc.title : "";
1028
1158
  _commits.set("title", {
1159
+ images: false,
1029
1160
  clear: () =>
1030
1161
  apply((d) => {
1031
1162
  delete d.title;
@@ -1058,6 +1189,7 @@ function metaRow(
1058
1189
  const value = String(findMetaEntry(head, field.attr, field.key)?.attributes?.content ?? "");
1059
1190
  const key = `meta:${field.attr}:${field.key}`;
1060
1191
  _commits.set(key, {
1192
+ images: field.media === true,
1061
1193
  clear: () => apply((d) => upsertMeta(d, field.attr, field.key, "")),
1062
1194
  /* A media field is not trimmed: a path is committed exactly as it was chosen, and the browser
1063
1195
  and the upload both hand one back already clean. */
@@ -1080,10 +1212,31 @@ function metaRow(
1080
1212
  return row;
1081
1213
  }
1082
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
+
1083
1235
  /** The favicon, which is a `<link rel="icon">` rather than a `<meta>`. */
1084
1236
  function iconRow(head: JxHeadEntry[], apply: PagePanelContext["applyMutation"]): PageRow {
1085
1237
  const value = String(findLinkEntry(head, "icon")?.attributes?.href ?? "");
1086
1238
  _commits.set("link:icon", {
1239
+ images: true,
1087
1240
  clear: () => apply((d) => upsertLink(d, "icon", "")),
1088
1241
  commit: (raw) => apply((d) => upsertLink(d, "icon", String(raw))),
1089
1242
  });
@@ -1099,8 +1252,7 @@ function iconRow(head: JxHeadEntry[], apply: PagePanelContext["applyMutation"]):
1099
1252
  * The Layout picker, or `null` when this document takes none — a component has no layout, and a
1100
1253
  * page whose layouts directory is still being listed has nothing to offer yet.
1101
1254
  *
1102
- * The listing is {@link layoutPickerEntries}', which the Document Header card reads too: creating a
1103
- * layout invalidates one cache rather than two.
1255
+ * The listing is {@link layoutPickerEntries}', one cache that creating a layout invalidates.
1104
1256
  */
1105
1257
  function layoutSection(
1106
1258
  tab: Tab | null,
@@ -1118,6 +1270,7 @@ function layoutSection(
1118
1270
  const defaultPath = projectState?.projectConfig?.defaults?.layout;
1119
1271
  const defaultLabel = defaultPath ? layoutDisplayName(defaultPath) : "";
1120
1272
  _commits.set("__layout", {
1273
+ images: false,
1121
1274
  clear: () =>
1122
1275
  apply((d) => {
1123
1276
  delete d.$layout;
@@ -1145,6 +1298,17 @@ function layoutSection(
1145
1298
  ...entries.map((l) => ({ label: l.name, value: l.path })),
1146
1299
  ];
1147
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
+ }
1148
1312
  return { hasSeo: false, key: "layout", rows: [row], title: "Layout" };
1149
1313
  }
1150
1314
 
@@ -1161,7 +1325,7 @@ function frontmatterSection(tab: Tab | null): PageSection | null {
1161
1325
  const { collection, fields, hasSchema, requiredFields } = collectFmFields(
1162
1326
  tab,
1163
1327
  projectState?.projectConfig,
1164
- RESERVED_FM_KEYS,
1328
+ reservedFmKeys(tab),
1165
1329
  );
1166
1330
  if (fields.length === 0 && !hasSchema) {
1167
1331
  return null;
@@ -1172,6 +1336,7 @@ function frontmatterSection(tab: Tab | null): PageSection | null {
1172
1336
  rerender: sync,
1173
1337
  });
1174
1338
  _commits.set(key, {
1339
+ images: row.kind === "media" && f.entry.format === "image",
1175
1340
  clear: () => transactDoc(tab, (t) => mutateUpdateFrontmatter(t, f.field)),
1176
1341
  commit: (raw) => transactDoc(tab, (t) => mutateUpdateFrontmatter(t, f.field, parse(raw))),
1177
1342
  });
@@ -1207,12 +1372,13 @@ function customEntries(head: JxHeadEntry[]): PageTagRow[] {
1207
1372
  /**
1208
1373
  * Everything the panel says about one document, and the commit table behind it.
1209
1374
  *
1210
- * `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
1211
1376
  * surface that shows the focused document by definition, and spelling that out once at the top is
1212
1377
  * what lets every helper under it take a tab instead of asking for one.
1213
1378
  */
1214
1379
  function view(ctx: PagePanelContext): PagePanelView {
1215
1380
  _commits.clear();
1381
+ _rowActions.clear();
1216
1382
  const tab = activeTab.value;
1217
1383
  const doc = ctx.document;
1218
1384
  const head = doc.$head ?? [];
@@ -1226,13 +1392,19 @@ function view(ctx: PagePanelContext): PagePanelView {
1226
1392
  if (layout) {
1227
1393
  sections.push(layout);
1228
1394
  }
1395
+ const url = urlRow(tab);
1229
1396
  sections.push(
1230
1397
  {
1231
1398
  hasSeo: true,
1232
1399
  key: "page",
1233
1400
  rows: [
1234
1401
  titleRow(doc, ctx.applyMutation),
1235
- ...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
+ ),
1236
1408
  iconRow(head, ctx.applyMutation),
1237
1409
  ],
1238
1410
  title: "Page",
@@ -1324,7 +1496,7 @@ function addEntry(): void {
1324
1496
  }
1325
1497
  d.$head.push(entry);
1326
1498
  });
1327
- ctx.renderLeftPanel();
1499
+ ctx.rerender();
1328
1500
  }
1329
1501
 
1330
1502
  /** Take one custom `$head` entry away, by the entry the key was drawn for. */
@@ -1343,7 +1515,7 @@ function removeEntry(key: string): void {
1343
1515
  d.$head.splice(idx, 1);
1344
1516
  }
1345
1517
  });
1346
- ctx.renderLeftPanel();
1518
+ ctx.rerender();
1347
1519
  }
1348
1520
 
1349
1521
  /**
@@ -1376,9 +1548,13 @@ const ACTIONS: PagePanelActions = {
1376
1548
  flushPending(key);
1377
1549
  void mediaPicker().then((m) => {
1378
1550
  if (anchor instanceof HTMLElement) {
1379
- m.showMediaPickerPopover(anchor, (val: string) => {
1380
- _commits.get(key)?.commit(val);
1381
- });
1551
+ m.showMediaPickerPopover(
1552
+ anchor,
1553
+ (val: string) => {
1554
+ _commits.get(key)?.commit(val);
1555
+ },
1556
+ { images: _commits.get(key)?.images ?? true },
1557
+ );
1382
1558
  }
1383
1559
  });
1384
1560
  },
@@ -1411,11 +1587,14 @@ const ACTIONS: PagePanelActions = {
1411
1587
  sync();
1412
1588
  },
1413
1589
  openSeo: () => {
1414
- /* The COMMAND, not a local open() — the Document Header card offers the same door and neither
1415
- 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. */
1416
1592
  void activeRegistry()?.run("document.openSeo");
1417
1593
  },
1418
1594
  removeEntry,
1595
+ runRowAction: (key) => {
1596
+ _rowActions.get(key)?.();
1597
+ },
1419
1598
  setBoolean: (key, checked) => commitNow(key, checked),
1420
1599
  setChoice: (key, value) => commitNow(key, value),
1421
1600
  setNumber: (key, value) => commitNow(key, value),
@@ -1458,50 +1637,137 @@ export function renderPagePanel(host: HTMLElement, ctx: PagePanelContext): void
1458
1637
  sync();
1459
1638
  }
1460
1639
 
1640
+ // ─── The Inspector's Page tab ────────────────────────────────────────────
1641
+
1642
+ /**
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.
1645
+ *
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).
1648
+ */
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";
1664
+ }
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
+
1461
1694
  /**
1462
- * The Navigator's content area inside a panel body.
1695
+ * Take the Inspector's Page body host, or let it go (`null`, when the dock is taken down).
1463
1696
  *
1464
- * `afterRender` is handed the `.panel-body`, and the panel's own content goes one level in — which
1465
- * is where the document must be mounted rather than beside it, because that child is lit's part and
1466
- * clearing it is how switching to another panel takes this surface down. A body drawn by the
1467
- * fallback path has no content area; mounting into the body itself is then still correct.
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.
1468
1700
  */
1469
- function contentArea(body: HTMLElement): HTMLElement {
1470
- return body.querySelector<HTMLElement>(".panel-content") ?? body;
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();
1471
1712
  }
1472
1713
 
1473
1714
  /**
1474
- * Contribute the Page panel.
1715
+ * Draw the Page tab against the focused document, or empty it when nothing is open.
1475
1716
  *
1476
- * `level: "document"` — title, description, social card and custom `$head` entries are the open
1477
- * document's. The id is `page` now: "head" named an HTML element, and §5.1's Document group calls
1478
- * 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.
1479
1719
  */
1480
- export function registerPagePanel(): void {
1481
- registerPanel({
1482
- id: "page",
1483
- title: "Page",
1484
- level: "document",
1485
- dock: "navigator",
1486
- icon: "file",
1487
- requiresDocument: "Open a page to edit its title, description and social preview.",
1488
- render: () => nothing,
1489
- afterRender: (ctx, host) => {
1490
- const doc = ctx.doc!;
1491
- const isContent = doc.mode === "content";
1492
- const fm = doc.content?.frontmatter ?? {};
1493
- renderPagePanel(contentArea(host), {
1494
- /* The Navigator's Page panel IS an app-level surface: it shows the focused document by
1495
- definition, so it spells that out at the call site instead of leaving it to a helper two
1496
- other surfaces share. */
1497
- applyMutation: isContent
1498
- ? (fn) => applyContentMutation(activeTab.value, ctx.rerender, fn)
1499
- : (fn) => {
1500
- transact(activeTab.value, fn);
1501
- },
1502
- document: isContent ? buildHeadDoc(doc.document, fm) : doc.document,
1503
- renderLeftPanel: ctx.rerender,
1504
- });
1505
- },
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,
1506
1772
  });
1507
1773
  }