@velastack/cms 0.1.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 (327) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +644 -0
  3. package/dist/cms.d.ts +6 -0
  4. package/dist/cms.js +30 -0
  5. package/dist/components/admin-bar/admin-bar-internal.svelte +1697 -0
  6. package/dist/components/admin-bar/admin-bar-internal.svelte.d.ts +12 -0
  7. package/dist/components/admin-bar/admin-bar.css +5 -0
  8. package/dist/components/admin-bar/admin-bar.svelte +355 -0
  9. package/dist/components/admin-bar/admin-bar.svelte.d.ts +7 -0
  10. package/dist/components/admin-bar/click-outside.d.ts +8 -0
  11. package/dist/components/admin-bar/click-outside.js +17 -0
  12. package/dist/components/admin-bar/css-root.svelte +23 -0
  13. package/dist/components/admin-bar/css-root.svelte.d.ts +19 -0
  14. package/dist/components/admin-bar/delete-page-dialog.svelte +326 -0
  15. package/dist/components/admin-bar/delete-page-dialog.svelte.d.ts +32 -0
  16. package/dist/components/admin-bar/history-panel.svelte +264 -0
  17. package/dist/components/admin-bar/history-panel.svelte.d.ts +8 -0
  18. package/dist/components/admin-bar/icons/bold.svelte +13 -0
  19. package/dist/components/admin-bar/icons/bold.svelte.d.ts +3 -0
  20. package/dist/components/admin-bar/icons/check.svelte +11 -0
  21. package/dist/components/admin-bar/icons/check.svelte.d.ts +3 -0
  22. package/dist/components/admin-bar/icons/chevron-down.svelte +11 -0
  23. package/dist/components/admin-bar/icons/chevron-down.svelte.d.ts +3 -0
  24. package/dist/components/admin-bar/icons/chevron-right.svelte +11 -0
  25. package/dist/components/admin-bar/icons/chevron-right.svelte.d.ts +3 -0
  26. package/dist/components/admin-bar/icons/chevron-up.svelte +11 -0
  27. package/dist/components/admin-bar/icons/chevron-up.svelte.d.ts +3 -0
  28. package/dist/components/admin-bar/icons/code.svelte +14 -0
  29. package/dist/components/admin-bar/icons/code.svelte.d.ts +3 -0
  30. package/dist/components/admin-bar/icons/external-link.svelte +15 -0
  31. package/dist/components/admin-bar/icons/external-link.svelte.d.ts +3 -0
  32. package/dist/components/admin-bar/icons/file.svelte +19 -0
  33. package/dist/components/admin-bar/icons/file.svelte.d.ts +3 -0
  34. package/dist/components/admin-bar/icons/heading-2.svelte +16 -0
  35. package/dist/components/admin-bar/icons/heading-2.svelte.d.ts +3 -0
  36. package/dist/components/admin-bar/icons/heading-3.svelte +17 -0
  37. package/dist/components/admin-bar/icons/heading-3.svelte.d.ts +3 -0
  38. package/dist/components/admin-bar/icons/icon.svelte +41 -0
  39. package/dist/components/admin-bar/icons/icon.svelte.d.ts +21 -0
  40. package/dist/components/admin-bar/icons/images.svelte +16 -0
  41. package/dist/components/admin-bar/icons/images.svelte.d.ts +3 -0
  42. package/dist/components/admin-bar/icons/italic.svelte +15 -0
  43. package/dist/components/admin-bar/icons/italic.svelte.d.ts +3 -0
  44. package/dist/components/admin-bar/icons/link-2-off.svelte +16 -0
  45. package/dist/components/admin-bar/icons/link-2-off.svelte.d.ts +3 -0
  46. package/dist/components/admin-bar/icons/link.svelte +14 -0
  47. package/dist/components/admin-bar/icons/link.svelte.d.ts +3 -0
  48. package/dist/components/admin-bar/icons/list-ordered.svelte +18 -0
  49. package/dist/components/admin-bar/icons/list-ordered.svelte.d.ts +3 -0
  50. package/dist/components/admin-bar/icons/list.svelte +18 -0
  51. package/dist/components/admin-bar/icons/list.svelte.d.ts +3 -0
  52. package/dist/components/admin-bar/icons/loader-circle.svelte +11 -0
  53. package/dist/components/admin-bar/icons/loader-circle.svelte.d.ts +3 -0
  54. package/dist/components/admin-bar/icons/minus.svelte +11 -0
  55. package/dist/components/admin-bar/icons/minus.svelte.d.ts +3 -0
  56. package/dist/components/admin-bar/icons/quote.svelte +24 -0
  57. package/dist/components/admin-bar/icons/quote.svelte.d.ts +3 -0
  58. package/dist/components/admin-bar/icons/search.svelte +14 -0
  59. package/dist/components/admin-bar/icons/search.svelte.d.ts +3 -0
  60. package/dist/components/admin-bar/icons/trash-2.svelte +17 -0
  61. package/dist/components/admin-bar/icons/trash-2.svelte.d.ts +3 -0
  62. package/dist/components/admin-bar/icons/triangle-alert.svelte +15 -0
  63. package/dist/components/admin-bar/icons/triangle-alert.svelte.d.ts +3 -0
  64. package/dist/components/admin-bar/icons/types.d.ts +7 -0
  65. package/dist/components/admin-bar/icons/types.js +1 -0
  66. package/dist/components/admin-bar/icons/undo-2.svelte +14 -0
  67. package/dist/components/admin-bar/icons/undo-2.svelte.d.ts +3 -0
  68. package/dist/components/admin-bar/icons/upload.svelte +15 -0
  69. package/dist/components/admin-bar/icons/upload.svelte.d.ts +3 -0
  70. package/dist/components/admin-bar/icons/x.svelte +14 -0
  71. package/dist/components/admin-bar/icons/x.svelte.d.ts +3 -0
  72. package/dist/components/admin-bar/keyboard-shortcuts-dialog.svelte +96 -0
  73. package/dist/components/admin-bar/keyboard-shortcuts-dialog.svelte.d.ts +7 -0
  74. package/dist/components/admin-bar/locales-panel.svelte +137 -0
  75. package/dist/components/admin-bar/locales-panel.svelte.d.ts +9 -0
  76. package/dist/components/admin-bar/media-panel.svelte +282 -0
  77. package/dist/components/admin-bar/media-panel.svelte.d.ts +7 -0
  78. package/dist/components/admin-bar/new-page-chooser-dialog.svelte +56 -0
  79. package/dist/components/admin-bar/new-page-chooser-dialog.svelte.d.ts +10 -0
  80. package/dist/components/admin-bar/new-page-dialog.svelte +126 -0
  81. package/dist/components/admin-bar/new-page-dialog.svelte.d.ts +14 -0
  82. package/dist/components/admin-bar/page-config.d.ts +61 -0
  83. package/dist/components/admin-bar/page-config.js +24 -0
  84. package/dist/components/admin-bar/pages-panel.svelte +639 -0
  85. package/dist/components/admin-bar/pages-panel.svelte.d.ts +13 -0
  86. package/dist/components/admin-bar/panel-footer.svelte +21 -0
  87. package/dist/components/admin-bar/panel-footer.svelte.d.ts +8 -0
  88. package/dist/components/admin-bar/panel-header.svelte +29 -0
  89. package/dist/components/admin-bar/panel-header.svelte.d.ts +10 -0
  90. package/dist/components/admin-bar/panel.svelte +41 -0
  91. package/dist/components/admin-bar/panel.svelte.d.ts +10 -0
  92. package/dist/components/admin-bar/publish-dialog.svelte +387 -0
  93. package/dist/components/admin-bar/publish-dialog.svelte.d.ts +16 -0
  94. package/dist/components/admin-bar/resolve-route.d.ts +20 -0
  95. package/dist/components/admin-bar/resolve-route.js +50 -0
  96. package/dist/components/admin-bar/seo-panel.svelte +387 -0
  97. package/dist/components/admin-bar/seo-panel.svelte.d.ts +7 -0
  98. package/dist/components/admin-bar/share-preview-link-dialog.svelte +97 -0
  99. package/dist/components/admin-bar/share-preview-link-dialog.svelte.d.ts +9 -0
  100. package/dist/components/admin-bar/site-settings-panel.svelte +225 -0
  101. package/dist/components/admin-bar/site-settings-panel.svelte.d.ts +7 -0
  102. package/dist/components/admin-bar/status-pill.svelte +49 -0
  103. package/dist/components/admin-bar/status-pill.svelte.d.ts +11 -0
  104. package/dist/components/admin-bar/theme.svelte.d.ts +16 -0
  105. package/dist/components/admin-bar/theme.svelte.js +39 -0
  106. package/dist/components/admin-bar/ui/badge/badge.svelte +57 -0
  107. package/dist/components/admin-bar/ui/badge/badge.svelte.d.ts +16 -0
  108. package/dist/components/admin-bar/ui/badge/index.d.ts +4 -0
  109. package/dist/components/admin-bar/ui/badge/index.js +4 -0
  110. package/dist/components/admin-bar/ui/button/button.svelte +73 -0
  111. package/dist/components/admin-bar/ui/button/button.svelte.d.ts +16 -0
  112. package/dist/components/admin-bar/ui/button/index.d.ts +4 -0
  113. package/dist/components/admin-bar/ui/button/index.js +4 -0
  114. package/dist/components/admin-bar/ui/collapsible/collapsible-content.svelte +7 -0
  115. package/dist/components/admin-bar/ui/collapsible/collapsible-content.svelte.d.ts +4 -0
  116. package/dist/components/admin-bar/ui/collapsible/collapsible-trigger.svelte +7 -0
  117. package/dist/components/admin-bar/ui/collapsible/collapsible-trigger.svelte.d.ts +4 -0
  118. package/dist/components/admin-bar/ui/collapsible/collapsible.svelte +7 -0
  119. package/dist/components/admin-bar/ui/collapsible/collapsible.svelte.d.ts +4 -0
  120. package/dist/components/admin-bar/ui/collapsible/index.d.ts +4 -0
  121. package/dist/components/admin-bar/ui/collapsible/index.js +6 -0
  122. package/dist/components/admin-bar/ui/dialog/dialog-close.svelte +7 -0
  123. package/dist/components/admin-bar/ui/dialog/dialog-close.svelte.d.ts +4 -0
  124. package/dist/components/admin-bar/ui/dialog/dialog-content.svelte +46 -0
  125. package/dist/components/admin-bar/ui/dialog/dialog-content.svelte.d.ts +11 -0
  126. package/dist/components/admin-bar/ui/dialog/dialog-description.svelte +17 -0
  127. package/dist/components/admin-bar/ui/dialog/dialog-description.svelte.d.ts +4 -0
  128. package/dist/components/admin-bar/ui/dialog/dialog-footer.svelte +23 -0
  129. package/dist/components/admin-bar/ui/dialog/dialog-footer.svelte.d.ts +5 -0
  130. package/dist/components/admin-bar/ui/dialog/dialog-header.svelte +20 -0
  131. package/dist/components/admin-bar/ui/dialog/dialog-header.svelte.d.ts +5 -0
  132. package/dist/components/admin-bar/ui/dialog/dialog-overlay.svelte +20 -0
  133. package/dist/components/admin-bar/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
  134. package/dist/components/admin-bar/ui/dialog/dialog-portal.svelte +7 -0
  135. package/dist/components/admin-bar/ui/dialog/dialog-portal.svelte.d.ts +3 -0
  136. package/dist/components/admin-bar/ui/dialog/dialog-title.svelte +17 -0
  137. package/dist/components/admin-bar/ui/dialog/dialog-title.svelte.d.ts +4 -0
  138. package/dist/components/admin-bar/ui/dialog/dialog-trigger.svelte +7 -0
  139. package/dist/components/admin-bar/ui/dialog/dialog-trigger.svelte.d.ts +4 -0
  140. package/dist/components/admin-bar/ui/dialog/dialog.svelte +7 -0
  141. package/dist/components/admin-bar/ui/dialog/dialog.svelte.d.ts +3 -0
  142. package/dist/components/admin-bar/ui/dialog/index.d.ts +11 -0
  143. package/dist/components/admin-bar/ui/dialog/index.js +13 -0
  144. package/dist/components/admin-bar/ui/input/index.d.ts +3 -0
  145. package/dist/components/admin-bar/ui/input/index.js +3 -0
  146. package/dist/components/admin-bar/ui/input/input.svelte +24 -0
  147. package/dist/components/admin-bar/ui/input/input.svelte.d.ts +5 -0
  148. package/dist/components/admin-bar/ui/kbd/index.d.ts +4 -0
  149. package/dist/components/admin-bar/ui/kbd/index.js +6 -0
  150. package/dist/components/admin-bar/ui/kbd/kbd-group.svelte +20 -0
  151. package/dist/components/admin-bar/ui/kbd/kbd-group.svelte.d.ts +5 -0
  152. package/dist/components/admin-bar/ui/kbd/kbd-shortcut.svelte +52 -0
  153. package/dist/components/admin-bar/ui/kbd/kbd-shortcut.svelte.d.ts +8 -0
  154. package/dist/components/admin-bar/ui/kbd/kbd.svelte +27 -0
  155. package/dist/components/admin-bar/ui/kbd/kbd.svelte.d.ts +5 -0
  156. package/dist/components/admin-bar/ui/label/index.d.ts +3 -0
  157. package/dist/components/admin-bar/ui/label/index.js +3 -0
  158. package/dist/components/admin-bar/ui/label/label.svelte +20 -0
  159. package/dist/components/admin-bar/ui/label/label.svelte.d.ts +4 -0
  160. package/dist/components/admin-bar/ui/menubar/index.d.ts +18 -0
  161. package/dist/components/admin-bar/ui/menubar/index.js +20 -0
  162. package/dist/components/admin-bar/ui/menubar/menubar-checkbox-item.svelte +46 -0
  163. package/dist/components/admin-bar/ui/menubar/menubar-checkbox-item.svelte.d.ts +10 -0
  164. package/dist/components/admin-bar/ui/menubar/menubar-content.svelte +28 -0
  165. package/dist/components/admin-bar/ui/menubar/menubar-content.svelte.d.ts +4 -0
  166. package/dist/components/admin-bar/ui/menubar/menubar-group-heading.svelte +25 -0
  167. package/dist/components/admin-bar/ui/menubar/menubar-group-heading.svelte.d.ts +8 -0
  168. package/dist/components/admin-bar/ui/menubar/menubar-group.svelte +12 -0
  169. package/dist/components/admin-bar/ui/menubar/menubar-group.svelte.d.ts +7 -0
  170. package/dist/components/admin-bar/ui/menubar/menubar-item.svelte +27 -0
  171. package/dist/components/admin-bar/ui/menubar/menubar-item.svelte.d.ts +8 -0
  172. package/dist/components/admin-bar/ui/menubar/menubar-label.svelte +25 -0
  173. package/dist/components/admin-bar/ui/menubar/menubar-label.svelte.d.ts +8 -0
  174. package/dist/components/admin-bar/ui/menubar/menubar-menu.svelte +7 -0
  175. package/dist/components/admin-bar/ui/menubar/menubar-menu.svelte.d.ts +3 -0
  176. package/dist/components/admin-bar/ui/menubar/menubar-portal.svelte +7 -0
  177. package/dist/components/admin-bar/ui/menubar/menubar-portal.svelte.d.ts +3 -0
  178. package/dist/components/admin-bar/ui/menubar/menubar-radio-group.svelte +11 -0
  179. package/dist/components/admin-bar/ui/menubar/menubar-radio-group.svelte.d.ts +4 -0
  180. package/dist/components/admin-bar/ui/menubar/menubar-radio-item.svelte +37 -0
  181. package/dist/components/admin-bar/ui/menubar/menubar-radio-item.svelte.d.ts +8 -0
  182. package/dist/components/admin-bar/ui/menubar/menubar-separator.svelte +17 -0
  183. package/dist/components/admin-bar/ui/menubar/menubar-separator.svelte.d.ts +4 -0
  184. package/dist/components/admin-bar/ui/menubar/menubar-shortcut.svelte +23 -0
  185. package/dist/components/admin-bar/ui/menubar/menubar-shortcut.svelte.d.ts +5 -0
  186. package/dist/components/admin-bar/ui/menubar/menubar-sub-content.svelte +20 -0
  187. package/dist/components/admin-bar/ui/menubar/menubar-sub-content.svelte.d.ts +4 -0
  188. package/dist/components/admin-bar/ui/menubar/menubar-sub-trigger.svelte +29 -0
  189. package/dist/components/admin-bar/ui/menubar/menubar-sub-trigger.svelte.d.ts +8 -0
  190. package/dist/components/admin-bar/ui/menubar/menubar-sub.svelte +7 -0
  191. package/dist/components/admin-bar/ui/menubar/menubar-sub.svelte.d.ts +4 -0
  192. package/dist/components/admin-bar/ui/menubar/menubar-trigger.svelte +20 -0
  193. package/dist/components/admin-bar/ui/menubar/menubar-trigger.svelte.d.ts +4 -0
  194. package/dist/components/admin-bar/ui/menubar/menubar.svelte +20 -0
  195. package/dist/components/admin-bar/ui/menubar/menubar.svelte.d.ts +4 -0
  196. package/dist/components/admin-bar/ui/select/index.d.ts +12 -0
  197. package/dist/components/admin-bar/ui/select/index.js +14 -0
  198. package/dist/components/admin-bar/ui/select/select-content.svelte +45 -0
  199. package/dist/components/admin-bar/ui/select/select-content.svelte.d.ts +11 -0
  200. package/dist/components/admin-bar/ui/select/select-group-heading.svelte +21 -0
  201. package/dist/components/admin-bar/ui/select/select-group-heading.svelte.d.ts +10 -0
  202. package/dist/components/admin-bar/ui/select/select-group.svelte +17 -0
  203. package/dist/components/admin-bar/ui/select/select-group.svelte.d.ts +4 -0
  204. package/dist/components/admin-bar/ui/select/select-item.svelte +40 -0
  205. package/dist/components/admin-bar/ui/select/select-item.svelte.d.ts +5 -0
  206. package/dist/components/admin-bar/ui/select/select-label.svelte +20 -0
  207. package/dist/components/admin-bar/ui/select/select-label.svelte.d.ts +6 -0
  208. package/dist/components/admin-bar/ui/select/select-portal.svelte +7 -0
  209. package/dist/components/admin-bar/ui/select/select-portal.svelte.d.ts +3 -0
  210. package/dist/components/admin-bar/ui/select/select-scroll-down-button.svelte +23 -0
  211. package/dist/components/admin-bar/ui/select/select-scroll-down-button.svelte.d.ts +5 -0
  212. package/dist/components/admin-bar/ui/select/select-scroll-up-button.svelte +23 -0
  213. package/dist/components/admin-bar/ui/select/select-scroll-up-button.svelte.d.ts +5 -0
  214. package/dist/components/admin-bar/ui/select/select-separator.svelte +21 -0
  215. package/dist/components/admin-bar/ui/select/select-separator.svelte.d.ts +4 -0
  216. package/dist/components/admin-bar/ui/select/select-trigger.svelte +31 -0
  217. package/dist/components/admin-bar/ui/select/select-trigger.svelte.d.ts +8 -0
  218. package/dist/components/admin-bar/ui/select/select.svelte +11 -0
  219. package/dist/components/admin-bar/ui/select/select.svelte.d.ts +3 -0
  220. package/dist/components/admin-bar/ui/separator/index.d.ts +2 -0
  221. package/dist/components/admin-bar/ui/separator/index.js +4 -0
  222. package/dist/components/admin-bar/ui/separator/separator.svelte +21 -0
  223. package/dist/components/admin-bar/ui/separator/separator.svelte.d.ts +4 -0
  224. package/dist/components/admin-bar/ui/textarea/index.d.ts +3 -0
  225. package/dist/components/admin-bar/ui/textarea/index.js +3 -0
  226. package/dist/components/admin-bar/ui/textarea/textarea.svelte +21 -0
  227. package/dist/components/admin-bar/ui/textarea/textarea.svelte.d.ts +5 -0
  228. package/dist/components/admin-bar/user-avatar.svelte +29 -0
  229. package/dist/components/admin-bar/user-avatar.svelte.d.ts +7 -0
  230. package/dist/components/admin-bar/utils.d.ts +10 -0
  231. package/dist/components/admin-bar/utils.js +46 -0
  232. package/dist/components/admin-bar/variants.d.ts +20 -0
  233. package/dist/components/admin-bar/variants.js +14 -0
  234. package/dist/components/cms/cms-boolean.svelte +102 -0
  235. package/dist/components/cms/cms-boolean.svelte.d.ts +10 -0
  236. package/dist/components/cms/cms-date-time-editable.svelte +66 -0
  237. package/dist/components/cms/cms-date-time-editable.svelte.d.ts +11 -0
  238. package/dist/components/cms/cms-date-time.svelte +67 -0
  239. package/dist/components/cms/cms-date-time.svelte.d.ts +19 -0
  240. package/dist/components/cms/cms-entries.svelte +24 -0
  241. package/dist/components/cms/cms-entries.svelte.d.ts +31 -0
  242. package/dist/components/cms/cms-image-editable.svelte +439 -0
  243. package/dist/components/cms/cms-image-editable.svelte.d.ts +11 -0
  244. package/dist/components/cms/cms-image.svelte +78 -0
  245. package/dist/components/cms/cms-image.svelte.d.ts +18 -0
  246. package/dist/components/cms/cms-link-editable.svelte +429 -0
  247. package/dist/components/cms/cms-link-editable.svelte.d.ts +13 -0
  248. package/dist/components/cms/cms-link.svelte +92 -0
  249. package/dist/components/cms/cms-link.svelte.d.ts +25 -0
  250. package/dist/components/cms/cms-markdown-editable.svelte +74 -0
  251. package/dist/components/cms/cms-markdown-editable.svelte.d.ts +9 -0
  252. package/dist/components/cms/cms-markdown.svelte +56 -0
  253. package/dist/components/cms/cms-markdown.svelte.d.ts +10 -0
  254. package/dist/components/cms/cms-media-picker.svelte +150 -0
  255. package/dist/components/cms/cms-media-picker.svelte.d.ts +8 -0
  256. package/dist/components/cms/cms-number-editable.svelte +77 -0
  257. package/dist/components/cms/cms-number-editable.svelte.d.ts +13 -0
  258. package/dist/components/cms/cms-number.svelte +63 -0
  259. package/dist/components/cms/cms-number.svelte.d.ts +16 -0
  260. package/dist/components/cms/cms-repeater-editable.svelte +137 -0
  261. package/dist/components/cms/cms-repeater-editable.svelte.d.ts +31 -0
  262. package/dist/components/cms/cms-repeater.svelte +55 -0
  263. package/dist/components/cms/cms-repeater.svelte.d.ts +30 -0
  264. package/dist/components/cms/cms-rich-text-editable.svelte +101 -0
  265. package/dist/components/cms/cms-rich-text-editable.svelte.d.ts +9 -0
  266. package/dist/components/cms/cms-rich-text-toolbar.svelte +246 -0
  267. package/dist/components/cms/cms-rich-text-toolbar.svelte.d.ts +9 -0
  268. package/dist/components/cms/cms-rich-text.svelte +74 -0
  269. package/dist/components/cms/cms-rich-text.svelte.d.ts +10 -0
  270. package/dist/components/cms/cms-store.svelte.d.ts +294 -0
  271. package/dist/components/cms/cms-store.svelte.js +625 -0
  272. package/dist/components/cms/cms-text.svelte +131 -0
  273. package/dist/components/cms/cms-text.svelte.d.ts +11 -0
  274. package/dist/components/cms/index.d.ts +13 -0
  275. package/dist/components/cms/index.js +11 -0
  276. package/dist/components/cms/install-scope.svelte.d.ts +7 -0
  277. package/dist/components/cms/install-scope.svelte.js +33 -0
  278. package/dist/components/cms/locale-merge.d.ts +41 -0
  279. package/dist/components/cms/locale-merge.js +77 -0
  280. package/dist/components/cms/overlay-sync.d.ts +82 -0
  281. package/dist/components/cms/overlay-sync.js +76 -0
  282. package/dist/components/cms/path.d.ts +30 -0
  283. package/dist/components/cms/path.js +156 -0
  284. package/dist/components/cms/scope.d.ts +120 -0
  285. package/dist/components/cms/scope.js +23 -0
  286. package/dist/index.d.ts +27 -0
  287. package/dist/index.js +28 -0
  288. package/dist/srv/README.md +341 -0
  289. package/dist/srv/api-adapter.d.ts +20 -0
  290. package/dist/srv/api-adapter.js +178 -0
  291. package/dist/srv/cms.d.ts +62 -0
  292. package/dist/srv/cms.js +35 -0
  293. package/dist/srv/index.d.ts +5 -0
  294. package/dist/srv/index.js +4 -0
  295. package/dist/srv/load-cms.d.ts +105 -0
  296. package/dist/srv/load-cms.js +227 -0
  297. package/dist/srv/mock-adapter.d.ts +96 -0
  298. package/dist/srv/mock-adapter.js +171 -0
  299. package/dist/srv/types.d.ts +114 -0
  300. package/dist/srv/types.js +1 -0
  301. package/package.json +116 -0
  302. package/plugin/README.md +137 -0
  303. package/plugin/dist/adapter.d.ts +35 -0
  304. package/plugin/dist/adapter.js +346 -0
  305. package/plugin/dist/adapter.js.map +1 -0
  306. package/plugin/dist/build-state.d.ts +10 -0
  307. package/plugin/dist/build-state.js +26 -0
  308. package/plugin/dist/build-state.js.map +1 -0
  309. package/plugin/dist/fallback-shim.js +194 -0
  310. package/plugin/dist/index.d.ts +56 -0
  311. package/plugin/dist/index.js +396 -0
  312. package/plugin/dist/index.js.map +1 -0
  313. package/plugin/dist/manifest.d.ts +76 -0
  314. package/plugin/dist/manifest.js +367 -0
  315. package/plugin/dist/manifest.js.map +1 -0
  316. package/plugin/dist/media.d.ts +36 -0
  317. package/plugin/dist/media.js +153 -0
  318. package/plugin/dist/media.js.map +1 -0
  319. package/plugin/dist/parse-svelte.d.ts +35 -0
  320. package/plugin/dist/parse-svelte.js +120 -0
  321. package/plugin/dist/parse-svelte.js.map +1 -0
  322. package/plugin/dist/route-tree.d.ts +27 -0
  323. package/plugin/dist/route-tree.js +69 -0
  324. package/plugin/dist/route-tree.js.map +1 -0
  325. package/plugin/dist/transforms.d.ts +42 -0
  326. package/plugin/dist/transforms.js +95 -0
  327. package/plugin/dist/transforms.js.map +1 -0
@@ -0,0 +1,1697 @@
1
+ <script lang="ts">
2
+ import {
3
+ afterNavigate,
4
+ beforeNavigate,
5
+ goto,
6
+ invalidateAll,
7
+ replaceState
8
+ } from '$app/navigation';
9
+ import { page } from '$app/state';
10
+ import { pages } from 'virtual:vela-cms/pages';
11
+ import { cmsStore, type PageDeleteOutcome } from '../cms/cms-store.svelte.js';
12
+ import { deriveOverlayIntents } from '../cms/overlay-sync.js';
13
+ import type { CmsPayload, CmsScopeEntry } from '../cms/scope.js';
14
+ import CssRoot from './css-root.svelte';
15
+ import { adminBarTheme, type AdminBarTheme } from './theme.svelte.js';
16
+ import DeletePageDialog, {
17
+ type DeletePageMode,
18
+ type RedirectTarget
19
+ } from './delete-page-dialog.svelte';
20
+ import HistoryPanel from './history-panel.svelte';
21
+ import KeyboardShortcutsDialog from './keyboard-shortcuts-dialog.svelte';
22
+ import MediaPanel from './media-panel.svelte';
23
+ import NewPageChooserDialog from './new-page-chooser-dialog.svelte';
24
+ import NewPageDialog from './new-page-dialog.svelte';
25
+ import { isCreatable, type CmsCreatablePageConfigWithRouteId } from './page-config.js';
26
+ import PagesPanel from './pages-panel.svelte';
27
+ import PublishDialog from './publish-dialog.svelte';
28
+ import { resolveRouteOnlyParams, resolveRouteUrl } from './resolve-route.js';
29
+ import SharePreviewLinkDialog from './share-preview-link-dialog.svelte';
30
+ import StatusPill from './status-pill.svelte';
31
+ import { Button } from './ui/button/index.js';
32
+ import { KbdShortcut } from './ui/kbd/index.js';
33
+ import * as Menubar from './ui/menubar/index.js';
34
+ import UserAvatar from './user-avatar.svelte';
35
+
36
+ const menubarTriggerBase =
37
+ 'vela:bg-transparent vela:hover:bg-[var(--cms-bar-bg-hover)] vela:aria-expanded:bg-[var(--cms-bar-bg-hover)] vela:rounded-md vela:px-2 vela:py-1 vela:text-[13px] vela:font-medium vela:cursor-pointer';
38
+ const menubarContentClass =
39
+ 'vela:bg-bar-bg vela:text-bar-text vela:border vela:border-[var(--cms-bar-divider)] vela:shadow-[0_8px_24px_rgba(0,0,0,0.35)]';
40
+
41
+ // Editing dims the menubar — meta-actions are subordinated while editing.
42
+ const menubarTriggerClass = $derived(
43
+ cmsStore.isEditing
44
+ ? `${menubarTriggerBase} vela:text-bar-text-tertiary vela:hover:text-bar-text-secondary vela:aria-expanded:text-bar-text-secondary`
45
+ : `${menubarTriggerBase} vela:text-bar-text-secondary vela:hover:text-bar-text vela:aria-expanded:text-bar-text`
46
+ );
47
+
48
+ const menuContentClassExt = `${menubarContentClass} vela:min-w-56 vela:p-1.5`;
49
+ const menuItemClass =
50
+ 'vela:flex vela:items-center vela:gap-2 vela:px-2 vela:py-1.5 vela:rounded-md vela:text-[13px] vela:cursor-pointer vela:outline-none vela:focus:bg-[var(--cms-bar-bg-hover)] vela:focus:text-bar-text vela:data-disabled:opacity-40 vela:data-disabled:pointer-events-none';
51
+ const menuItemDestructiveClass =
52
+ 'vela:flex vela:items-center vela:gap-2 vela:px-2 vela:py-1.5 vela:rounded-md vela:text-[13px] vela:cursor-pointer vela:outline-none vela:text-[var(--cms-status-error-text-base)] vela:focus:bg-[var(--cms-status-error-bg-hover)] vela:focus:text-[var(--cms-status-error-text-hover)] vela:data-disabled:opacity-40 vela:data-disabled:pointer-events-none';
53
+ const menuLabelClass =
54
+ 'vela:px-2 vela:pt-2.5 vela:pb-1 vela:text-[10px] vela:font-semibold vela:uppercase vela:tracking-widest vela:text-bar-text-tertiary';
55
+ const menuShortcutClass = 'vela:ml-auto';
56
+ const menuSeparatorClass = 'vela:my-1 vela:h-px vela:bg-[var(--cms-bar-divider)]';
57
+ const menuCheckIndicatorClass =
58
+ 'vela:flex vela:items-center vela:gap-2 vela:px-2 vela:py-1.5 vela:pl-7 vela:rounded-md vela:text-[13px] vela:cursor-pointer vela:outline-none vela:focus:bg-[var(--cms-bar-bg-hover)] vela:focus:text-bar-text vela:data-disabled:opacity-40';
59
+ const avatarTriggerClass =
60
+ 'vela:bg-transparent vela:border-0 vela:p-0 vela:rounded-full vela:cursor-pointer vela:focus:outline-none vela:aria-expanded:ring-2 vela:aria-expanded:ring-[var(--cms-accent)] vela:aria-expanded:ring-offset-2 vela:aria-expanded:ring-offset-[var(--cms-bar-bg)]';
61
+
62
+ type Props = {
63
+ user: { id: string; name: string };
64
+ endpoint: string;
65
+ onClose: () => void;
66
+ onLogout: () => void;
67
+ };
68
+ let { user, endpoint, onClose, onLogout }: Props = $props();
69
+
70
+ const setThemePref = (next: AdminBarTheme) => adminBarTheme.setPref(next);
71
+
72
+ // Auto-discovered from `page.cms.ts` files via the Vite plugin's
73
+ // `virtual:vela-cms/pages` module — consumers don't pass these.
74
+ const creatablePages = $derived(Object.values(pages).filter(isCreatable));
75
+
76
+ let seoOpen = $state(false);
77
+ let historyOpen = $state(false);
78
+ let pagesOpen = $state(false);
79
+ let mediaOpen = $state(false);
80
+ let localesOpen = $state(false);
81
+ let publishOpen = $state(false);
82
+ let chooserOpen = $state(false);
83
+ let shareLinkOpen = $state(false);
84
+ let shortcutsOpen = $state(false);
85
+ let siteSettingsOpen = $state(false);
86
+ const anyPanelOpen = $derived(
87
+ seoOpen ||
88
+ historyOpen ||
89
+ pagesOpen ||
90
+ mediaOpen ||
91
+ localesOpen ||
92
+ publishOpen ||
93
+ chooserOpen ||
94
+ shareLinkOpen ||
95
+ shortcutsOpen ||
96
+ siteSettingsOpen
97
+ );
98
+ let publishing = $state(false);
99
+ let publishError = $state<string | null>(null);
100
+
101
+ let newDialog = $state<CmsCreatablePageConfigWithRouteId | null>(null);
102
+ let newPageError = $state<string | null>(null);
103
+ let newPageCreating = $state(false);
104
+
105
+ // Delete-page dialog. Shared between the in-panel "Delete" button and the
106
+ // `Page → Delete Page…` menu entry; pages-panel calls `requestDelete` via
107
+ // its `onRequestDelete` prop, the menu calls it directly.
108
+ type WirePageEntry = {
109
+ params: Record<string, string>;
110
+ isDraft?: boolean;
111
+ isDeletePending?: boolean;
112
+ redirectTo?: string;
113
+ gone?: boolean;
114
+ metadata?: Record<string, unknown>;
115
+ };
116
+ type WirePageRoute = { routeId: string; entries: WirePageEntry[] };
117
+ let deleteDialogOpen = $state(false);
118
+ let deleteTarget = $state<{
119
+ routeId: string;
120
+ params: Record<string, string>;
121
+ url: string;
122
+ isDraft: boolean;
123
+ } | null>(null);
124
+ let deleteRoutes = $state<WirePageRoute[]>([]);
125
+ let deleteError = $state<string | null>(null);
126
+ let deleting = $state(false);
127
+ // Source content captured when the dialog is opened in duplicate mode.
128
+ // `metadata` seeds the input field values; `tree` is the rest of the
129
+ // source's content (non-`metadata` branch) that gets copied to the new
130
+ // page after create succeeds.
131
+ let duplicateSource = $state<{
132
+ tree: Record<string, unknown>;
133
+ metadata: Record<string, unknown>;
134
+ } | null>(null);
135
+
136
+ // Only one panel/dialog open at a time. Every "open X" routes through here.
137
+ const closeAllPanels = () => {
138
+ seoOpen = false;
139
+ historyOpen = false;
140
+ pagesOpen = false;
141
+ mediaOpen = false;
142
+ localesOpen = false;
143
+ publishOpen = false;
144
+ chooserOpen = false;
145
+ shareLinkOpen = false;
146
+ shortcutsOpen = false;
147
+ siteSettingsOpen = false;
148
+ newDialog = null;
149
+ newPageError = null;
150
+ duplicateSource = null;
151
+ if (!deleting) {
152
+ deleteDialogOpen = false;
153
+ deleteTarget = null;
154
+ deleteError = null;
155
+ }
156
+ };
157
+
158
+ const counts = $derived(cmsStore.workingCopyCounts);
159
+ const previewKey = $derived(cmsStore.openRelease?.preview_key ?? null);
160
+
161
+ // `?version=<key>` opens a past published release in read-only snapshot
162
+ // view. Mutually exclusive with the draft `?preview=` flow: when set, we
163
+ // suppress draft overlays/auto-add and disable edit-mode entry.
164
+ const versionKey = $derived(page.url.searchParams.get('version'));
165
+ let versionRelease = $state<{ id: string; preview_key: string; name?: string } | null>(null);
166
+
167
+ // `?locale=` is the editor's preview override; it wins over the server-resolved
168
+ // `cms.locale` so locale switching works even when the consumer's `getLocale`
169
+ // doesn't honor the param (and on static-export sites where the server load
170
+ // can't be re-run). Falls back to `cms.locale` when no override is set.
171
+ const currentLocale = $derived(
172
+ page.url.searchParams.get('locale') ?? (page.data.cms as CmsPayload | undefined)?.locale ?? ''
173
+ );
174
+ const supportedLocales = $derived((page.data.cms as CmsPayload | undefined)?.locales ?? []);
175
+
176
+ // Sub-bar (DESIGN.md §2) hangs below the main bar in edit/pending modes,
177
+ // holding the contextual status pill + action buttons. Only the clean state
178
+ // pill remains in the main bar.
179
+ const subBarVisible = $derived(cmsStore.isEditing || counts.total > 0 || !!versionKey);
180
+
181
+ const previewUrl = $derived.by(() => {
182
+ if (versionKey) {
183
+ const url = new URL(page.url);
184
+ url.searchParams.delete('edit');
185
+ url.searchParams.delete('preview');
186
+ url.searchParams.set('version', versionKey);
187
+ return url.toString();
188
+ }
189
+ const key = previewKey;
190
+ if (!key) return '';
191
+ const url = new URL(page.url);
192
+ url.searchParams.delete('edit');
193
+ url.searchParams.set('preview', key);
194
+ return url.toString();
195
+ });
196
+
197
+ const currentScopes = (): CmsScopeEntry[] => {
198
+ const cms = page.data.cms as CmsPayload | undefined;
199
+ return cms ? Object.values(cms.scopes) : [];
200
+ };
201
+
202
+ const currentEntriesRouteIds = (): string[] => {
203
+ const cms = page.data.cms as CmsPayload | undefined;
204
+ return cms ? Object.keys(cms.entries ?? {}) : [];
205
+ };
206
+
207
+ const setPreviewParam = async (key: string | null, opts: { replace?: boolean } = {}) => {
208
+ const url = new URL(page.url);
209
+ const current = url.searchParams.get('preview');
210
+ url.searchParams.delete('edit');
211
+ if (key) {
212
+ if (current === key) return;
213
+ url.searchParams.set('preview', key);
214
+ } else {
215
+ if (!current) return;
216
+ url.searchParams.delete('preview');
217
+ }
218
+ const path = url.pathname + url.search + url.hash;
219
+ if (opts.replace) {
220
+ replaceState(path, page.state);
221
+ } else {
222
+ await goto(path, { keepFocus: true, noScroll: true });
223
+ }
224
+ };
225
+
226
+ const setVersionParam = async (key: string | null, opts: { replace?: boolean } = {}) => {
227
+ const url = new URL(page.url);
228
+ const current = url.searchParams.get('version');
229
+ url.searchParams.delete('edit');
230
+ url.searchParams.delete('preview');
231
+ if (key) {
232
+ if (current === key) return;
233
+ url.searchParams.set('version', key);
234
+ } else {
235
+ if (!current) return;
236
+ url.searchParams.delete('version');
237
+ }
238
+ const path = url.pathname + url.search + url.hash;
239
+ if (opts.replace) {
240
+ replaceState(path, page.state);
241
+ } else {
242
+ await goto(path, { keepFocus: true, noScroll: true });
243
+ // Force the server load to re-fetch. SvelteKit's URL tracking
244
+ // doesn't always pick up our `?version=` access (it lives behind
245
+ // the `loadCms` helper) — visible on Exit, where we'd otherwise
246
+ // keep showing the past release's `page.data.cms.docs`.
247
+ await invalidateAll();
248
+ }
249
+ };
250
+
251
+ // Set true while setLocaleParam(null) is mid-flight so beforeNavigate doesn't
252
+ // re-carry the old `?locale=` and undo the strip.
253
+ let clearingLocale = false;
254
+
255
+ // Override the locale for the editor preview. Pure client-side: the overlay
256
+ // effect re-fetches `${endpoint}/docs?locale=…` on the URL change, so this
257
+ // works whether or not the consumer's `getLocale(url)` honors `?locale=`
258
+ // (and on static-export sites with no server load to re-run). Like the
259
+ // other gating params, we strip the param when `null`.
260
+ const setLocaleParam = async (locale: string | null, opts: { replace?: boolean } = {}) => {
261
+ const url = new URL(page.url);
262
+ const current = url.searchParams.get('locale');
263
+ if (locale) {
264
+ if (current === locale) return;
265
+ url.searchParams.set('locale', locale);
266
+ } else {
267
+ if (!current) return;
268
+ url.searchParams.delete('locale');
269
+ }
270
+ const path = url.pathname + url.search + url.hash;
271
+ if (opts.replace) {
272
+ replaceState(path, page.state);
273
+ return;
274
+ }
275
+ if (!locale) {
276
+ clearingLocale = true;
277
+ try {
278
+ await goto(path, { keepFocus: true, noScroll: true });
279
+ } finally {
280
+ clearingLocale = false;
281
+ }
282
+ } else {
283
+ await goto(path, { keepFocus: true, noScroll: true });
284
+ }
285
+ };
286
+
287
+ // Set true while exitVersionView is mid-flight so beforeNavigate doesn't
288
+ // re-inject `?version=` and undo the exit.
289
+ let exitingVersion = false;
290
+
291
+ const exitVersionView = async () => {
292
+ exitingVersion = true;
293
+ try {
294
+ await setVersionParam(null);
295
+ } finally {
296
+ exitingVersion = false;
297
+ }
298
+ };
299
+
300
+ // Resolve the `?version=` URL param to its `PublishedRelease` (so we know
301
+ // the id for regenerate). Re-fetched whenever the version key changes.
302
+ const fetchVersionRelease = async (key: string) => {
303
+ const res = await fetch(`${endpoint}/release/history`, { credentials: 'include' });
304
+ if (!res.ok) {
305
+ versionRelease = null;
306
+ return;
307
+ }
308
+ const data = (await res.json()) as {
309
+ history: Array<{ id: string; preview_key: string; name?: string }>;
310
+ };
311
+ const match = data.history.find((r) => r.preview_key === key) ?? null;
312
+ versionRelease = match
313
+ ? { id: match.id, preview_key: match.preview_key, name: match.name }
314
+ : null;
315
+ };
316
+
317
+ // Push the URL-derived editor locale into the store so field components'
318
+ // `getValue`/`setValue` defaults route to the bucket the editor is actually
319
+ // previewing — even when the consumer's `getLocale(url)` doesn't honor the
320
+ // param. Without this, edits made in `?locale=es` would land in the `en`
321
+ // bucket and never appear.
322
+ $effect(() => {
323
+ cmsStore.setActiveLocale(currentLocale || null);
324
+ });
325
+
326
+ // Tracks whether `fetchOpenRelease` has resolved at least once this session.
327
+ // Until then, the overlay-sync effect must NOT strip `?preview=` from the
328
+ // URL — `cmsStore.openRelease` is null pre-fetch, but that's "unknown",
329
+ // not "no draft". Without this gate, a hard refresh on `?preview=<key>`
330
+ // would treat the param as stale and replaceState it away.
331
+ let openReleaseFetched = $state(false);
332
+
333
+ // On mount and on subsequent URL/store changes: derive what to do (URL sync,
334
+ // overlay loads, clears) as a list of pure intents, then dispatch.
335
+ // Decision logic lives in `overlay-sync.ts` and is unit-tested separately.
336
+ $effect(() => {
337
+ // Synchronous reads so Svelte tracks these as effect dependencies — reads
338
+ // inside the async IIFE below run on a microtask and aren't tracked.
339
+ const intents = deriveOverlayIntents({
340
+ versionKey,
341
+ locale: currentLocale,
342
+ pageLocale: (page.data.cms as CmsPayload | undefined)?.locale ?? currentLocale,
343
+ openReleaseKey: cmsStore.openRelease?.preview_key ?? null,
344
+ openReleaseFetched,
345
+ previewParam: page.url.searchParams.get('preview')
346
+ });
347
+
348
+ if (intents.length === 0) return;
349
+
350
+ void (async () => {
351
+ for (const intent of intents) {
352
+ switch (intent.kind) {
353
+ case 'fetch-open-release':
354
+ await cmsStore.fetchOpenRelease(endpoint);
355
+ openReleaseFetched = true;
356
+ break;
357
+ case 'fetch-version-release':
358
+ await fetchVersionRelease(intent.versionKey);
359
+ break;
360
+ case 'set-preview-param':
361
+ await setPreviewParam(intent.value, { replace: true });
362
+ break;
363
+ case 'load-version-overlay':
364
+ // Don't null versionRelease here — `fetch-version-release` ran
365
+ // just before and populated it. Only non-version paths null it.
366
+ await Promise.all([
367
+ cmsStore.loadAndApplyOverlay(endpoint, currentScopes(), null, {
368
+ reset: true,
369
+ versionKey: intent.versionKey,
370
+ locale: intent.locale
371
+ }),
372
+ cmsStore.loadAndApplyEntriesOverlay(endpoint, currentEntriesRouteIds(), null, {
373
+ reset: true,
374
+ versionKey: intent.versionKey,
375
+ locale: intent.locale
376
+ }),
377
+ cmsStore.loadAndApplySiteOverlay(endpoint, null, { versionKey: intent.versionKey })
378
+ ]);
379
+ break;
380
+ case 'load-overlay':
381
+ versionRelease = null;
382
+ await Promise.all([
383
+ cmsStore.loadAndApplyOverlay(endpoint, currentScopes(), intent.previewKey, {
384
+ reset: true,
385
+ locale: intent.locale
386
+ }),
387
+ cmsStore.loadAndApplyEntriesOverlay(
388
+ endpoint,
389
+ currentEntriesRouteIds(),
390
+ intent.previewKey,
391
+ { reset: true, locale: intent.locale }
392
+ ),
393
+ cmsStore.loadAndApplySiteOverlay(endpoint, intent.previewKey)
394
+ ]);
395
+ break;
396
+ case 'clear-overlay':
397
+ versionRelease = null;
398
+ cmsStore.clearOverlay();
399
+ break;
400
+ }
401
+ }
402
+ })();
403
+ });
404
+
405
+ // Carry the active gating params (`?version=` if in version-view, else
406
+ // `?preview=`, plus `?locale=` if set) into every internal navigation so
407
+ // the overlay and preview-locale survive link clicks (and any `goto()` in
408
+ // app code). We cancel the in-flight nav and re-issue with the params
409
+ // appended; already-correct nav targets pass through unchanged. Full-page
410
+ // unloads (`willUnload`) and external/hash-only navs bypass.
411
+ beforeNavigate((nav) => {
412
+ if (nav.type === 'leave' || nav.willUnload) return;
413
+ if (!nav.to) return;
414
+ if (nav.to.url.origin !== location.origin) return;
415
+
416
+ // While clearingLocale is true, the user intentionally stripped `?locale=`;
417
+ // don't re-carry the old value from `page.url` (which hasn't updated yet).
418
+ const localeParam = clearingLocale ? null : page.url.searchParams.get('locale');
419
+ const localeNeedsCarry = (target: URL) =>
420
+ !!localeParam && target.searchParams.get('locale') !== localeParam;
421
+ const carryLocaleOnto = (url: URL) => {
422
+ if (localeParam) url.searchParams.set('locale', localeParam);
423
+ };
424
+
425
+ if (versionKey) {
426
+ const targetVersion = nav.to.url.searchParams.get('version');
427
+ // Target carries its own `?version=` (same or different — e.g. clicking
428
+ // View on another release): respect it, don't clobber. Locale still
429
+ // rides along if missing on target.
430
+ if (targetVersion) {
431
+ if (!localeNeedsCarry(nav.to.url)) return;
432
+ nav.cancel();
433
+ const url = new URL(nav.to.url);
434
+ carryLocaleOnto(url);
435
+ void goto(url.pathname + url.search + url.hash, {
436
+ keepFocus: true,
437
+ noScroll: true
438
+ });
439
+ return;
440
+ }
441
+ // Target has no `?version=`. If we're exiting on purpose, let the nav
442
+ // proceed; otherwise it's a normal link click and we carry version over.
443
+ if (exitingVersion) return;
444
+ nav.cancel();
445
+ const url = new URL(nav.to.url);
446
+ url.searchParams.delete('preview');
447
+ url.searchParams.set('version', versionKey);
448
+ carryLocaleOnto(url);
449
+ void goto(url.pathname + url.search + url.hash, {
450
+ keepFocus: true,
451
+ noScroll: true
452
+ });
453
+ return;
454
+ }
455
+
456
+ // Target is jumping to a `?version=` URL (e.g. View button from history
457
+ // panel) — let it through unmodified except for locale carry.
458
+ if (nav.to.url.searchParams.get('version')) {
459
+ if (!localeNeedsCarry(nav.to.url)) return;
460
+ nav.cancel();
461
+ const url = new URL(nav.to.url);
462
+ carryLocaleOnto(url);
463
+ void goto(url.pathname + url.search + url.hash, {
464
+ keepFocus: true,
465
+ noScroll: true
466
+ });
467
+ return;
468
+ }
469
+
470
+ const key = cmsStore.openRelease?.preview_key ?? null;
471
+ const previewMatches = !key || nav.to.url.searchParams.get('preview') === key;
472
+ if (previewMatches && !localeNeedsCarry(nav.to.url)) return;
473
+
474
+ nav.cancel();
475
+ const url = new URL(nav.to.url);
476
+ if (key) url.searchParams.set('preview', key);
477
+ carryLocaleOnto(url);
478
+ void goto(url.pathname + url.search + url.hash, {
479
+ keepFocus: true,
480
+ noScroll: true
481
+ });
482
+ });
483
+
484
+ // Re-apply the overlay on every internal navigation. SvelteKit may strip
485
+ // the gating param when nav targets aren't preserve-params links, so we
486
+ // reapply it via replaceState too. Merge (not reset) so unchanged-layout
487
+ // overlays keep showing while new-page-scope fetches resolve.
488
+ afterNavigate(() => {
489
+ if (versionKey) {
490
+ const url = new URL(page.url);
491
+ if (url.searchParams.get('version') !== versionKey) {
492
+ url.searchParams.delete('preview');
493
+ url.searchParams.set('version', versionKey);
494
+ replaceState(url.pathname + url.search + url.hash, page.state);
495
+ }
496
+ // `reset` so a nav from one release to another wipes the prior
497
+ // snapshot's overlay before merging in the new one — otherwise
498
+ // stale fields from the previous release leak through on scopes
499
+ // the new release doesn't touch.
500
+ void cmsStore.loadAndApplyOverlay(endpoint, currentScopes(), null, {
501
+ reset: true,
502
+ versionKey,
503
+ locale: currentLocale
504
+ });
505
+ void cmsStore.loadAndApplyEntriesOverlay(endpoint, currentEntriesRouteIds(), null, {
506
+ reset: true,
507
+ versionKey,
508
+ locale: currentLocale
509
+ });
510
+ void cmsStore.loadAndApplySiteOverlay(endpoint, null, { versionKey });
511
+ return;
512
+ }
513
+
514
+ const key = cmsStore.openRelease?.preview_key ?? null;
515
+ if (!key) {
516
+ // No draft and no version → clear any overlay left over from a
517
+ // previous version-mode visit. Without this, exiting version view
518
+ // leaves the past release's content stuck on top of the freshly
519
+ // re-fetched `page.data.cms.docs`.
520
+ cmsStore.clearOverlay();
521
+ return;
522
+ }
523
+ const url = new URL(page.url);
524
+ if (url.searchParams.get('preview') !== key) {
525
+ url.searchParams.set('preview', key);
526
+ replaceState(url.pathname + url.search + url.hash, page.state);
527
+ }
528
+ void cmsStore.loadAndApplyOverlay(endpoint, currentScopes(), key, { locale: currentLocale });
529
+ void cmsStore.loadAndApplyEntriesOverlay(endpoint, currentEntriesRouteIds(), key, {
530
+ locale: currentLocale
531
+ });
532
+ void cmsStore.loadAndApplySiteOverlay(endpoint, key);
533
+ });
534
+
535
+ const onSave = async () => {
536
+ const result = await cmsStore.save(endpoint);
537
+ if (!result.ok) return;
538
+ cmsStore.isEditing = false;
539
+ seoOpen = false;
540
+ const newKey = result.release?.preview_key ?? null;
541
+ if (newKey) await setPreviewParam(newKey, { replace: true });
542
+ await Promise.all([
543
+ cmsStore.loadAndApplyOverlay(endpoint, currentScopes(), newKey, {
544
+ locale: currentLocale
545
+ }),
546
+ cmsStore.loadAndApplyEntriesOverlay(endpoint, currentEntriesRouteIds(), newKey, {
547
+ locale: currentLocale
548
+ }),
549
+ cmsStore.loadAndApplySiteOverlay(endpoint, newKey)
550
+ ]);
551
+ };
552
+
553
+ const onToggleEdit = () => {
554
+ // Past releases are read-only — no edit, no save, no publish flow.
555
+ if (versionKey) return;
556
+ if (cmsStore.isEditing) {
557
+ if (cmsStore.isDirty && !confirm('Discard unsaved changes?')) return;
558
+ cmsStore.clearDrafts();
559
+ cmsStore.isEditing = false;
560
+ seoOpen = false;
561
+ } else {
562
+ cmsStore.isEditing = true;
563
+ }
564
+ };
565
+
566
+ const onOpenPublish = () => {
567
+ if (counts.total === 0) return;
568
+ closeAllPanels();
569
+ publishError = null;
570
+ publishOpen = true;
571
+ };
572
+
573
+ const onOpenPages = () => {
574
+ closeAllPanels();
575
+ pagesOpen = true;
576
+ };
577
+
578
+ const onOpenMedia = () => {
579
+ closeAllPanels();
580
+ mediaOpen = true;
581
+ };
582
+
583
+ const onOpenLocales = () => {
584
+ closeAllPanels();
585
+ localesOpen = true;
586
+ };
587
+
588
+ const onOpenHistory = () => {
589
+ closeAllPanels();
590
+ historyOpen = true;
591
+ };
592
+
593
+ const onConfirmPublish = async (name: string | undefined) => {
594
+ publishing = true;
595
+ publishError = null;
596
+ try {
597
+ const body: { name?: string } = {};
598
+ if (name) body.name = name;
599
+ const res = await fetch(`${endpoint}/release/publish`, {
600
+ credentials: 'include',
601
+ method: 'POST',
602
+ headers: { 'content-type': 'application/json' },
603
+ body: JSON.stringify(body)
604
+ });
605
+ if (!res.ok) {
606
+ publishError = (await res.text()) || 'Could not publish release.';
607
+ return;
608
+ }
609
+ cmsStore.setOpenRelease(null);
610
+ publishOpen = false;
611
+ await setPreviewParam(null, { replace: true });
612
+ // Refetch `/docs` and `/pages` to pick up the just-published content
613
+ // as overlay. `page.data.cms.{docs,entries}` was loaded against the
614
+ // pre-publish published state and we can't re-run server load on
615
+ // static-export sites; the fresh overlays mask that staleness.
616
+ await Promise.all([
617
+ cmsStore.loadAndApplyOverlay(endpoint, currentScopes(), null, {
618
+ reset: true,
619
+ locale: currentLocale
620
+ }),
621
+ cmsStore.loadAndApplyEntriesOverlay(endpoint, currentEntriesRouteIds(), null, {
622
+ reset: true,
623
+ locale: currentLocale
624
+ }),
625
+ cmsStore.loadAndApplySiteOverlay(endpoint, null)
626
+ ]);
627
+ } finally {
628
+ publishing = false;
629
+ }
630
+ };
631
+
632
+ const refreshAfterReleaseChange = async () => {
633
+ const key = cmsStore.openRelease?.preview_key ?? null;
634
+ await setPreviewParam(key, { replace: true });
635
+ await Promise.all([
636
+ cmsStore.loadAndApplyOverlay(endpoint, currentScopes(), key, {
637
+ reset: true,
638
+ locale: currentLocale
639
+ }),
640
+ cmsStore.loadAndApplyEntriesOverlay(endpoint, currentEntriesRouteIds(), key, {
641
+ reset: true,
642
+ locale: currentLocale
643
+ }),
644
+ cmsStore.loadAndApplySiteOverlay(endpoint, key)
645
+ ]);
646
+ };
647
+
648
+ // View toggles + bar position. Local state — Highlight/DraftMarkers/Grid have
649
+ // no functional effect until step 7's editing affordances land; Preview-as is
650
+ // also visual-only for now. Position swaps the bar between top and bottom.
651
+ let highlightSlots = $state(true);
652
+ let showDraftMarkers = $state(true);
653
+ let showGrid = $state(false);
654
+ let previewAs = $state<'desktop' | 'tablet' | 'mobile' | 'signed-out'>('desktop');
655
+ let barPosition = $state<'top' | 'bottom'>('top');
656
+
657
+ // Items targeting the current page URL in the active editor locale. Used
658
+ // for "Discard changes on this page" and the visibility flag for that
659
+ // button. Cross-locale isn't intended here — the editor's intent is
660
+ // "discard what I'm currently editing", not "every locale's edits".
661
+ const currentPageItems = $derived(
662
+ (cmsStore.openRelease?.items ?? []).filter((item) => {
663
+ if (item.kind === 'layout' || item.kind === 'site') return false;
664
+ if (item.locale !== currentLocale) return false;
665
+ try {
666
+ return resolveRouteUrl(item.routeId, item.params) === page.url.pathname;
667
+ } catch {
668
+ return false;
669
+ }
670
+ })
671
+ );
672
+ const hasCurrentPageDrafts = $derived(currentPageItems.length > 0);
673
+
674
+ /**
675
+ * Current page identity + creatable config when it has owned params (i.e.
676
+ * is a parameterized template instance). Drives the `Page → Delete Page…`
677
+ * menu's enable state — static pages and unconfigured routes can't be
678
+ * deleted from the data layer alone.
679
+ */
680
+ const currentDeletableConfig = $derived.by((): CmsCreatablePageConfigWithRouteId | null => {
681
+ const cms = page.data.cms as CmsPayload | undefined;
682
+ const pp = cms?.page;
683
+ if (!pp) return null;
684
+ if (Object.keys(pp.params).length === 0) return null;
685
+ const config = pages[pp.routeId];
686
+ if (!config) return null;
687
+ return isCreatable(config) ? config : null;
688
+ });
689
+
690
+ const paramsEqual = (a: Record<string, string>, b: Record<string, string>): boolean => {
691
+ const aKeys = Object.keys(a);
692
+ if (aKeys.length !== Object.keys(b).length) return false;
693
+ for (const k of aKeys) if (a[k] !== b[k]) return false;
694
+ return true;
695
+ };
696
+
697
+ const isCurrentPage = (routeId: string, params: Record<string, string>): boolean => {
698
+ const cms = page.data.cms as CmsPayload | undefined;
699
+ const pp = cms?.page;
700
+ return !!pp && pp.routeId === routeId && paramsEqual(pp.params, params);
701
+ };
702
+
703
+ /**
704
+ * Selectable redirect targets for the delete dialog — every published,
705
+ * non-tombstoned, non-draft entry across all routes. Sourced from the
706
+ * cached `/pages` snapshot fetched by `requestDelete`.
707
+ */
708
+ const redirectTargets = $derived.by((): RedirectTarget[] => {
709
+ const out: RedirectTarget[] = [];
710
+ for (const r of deleteRoutes) {
711
+ for (const e of r.entries) {
712
+ if (e.isDraft || e.isDeletePending || e.redirectTo || e.gone) continue;
713
+ let url: string;
714
+ try {
715
+ url = resolveRouteUrl(r.routeId, e.params);
716
+ } catch {
717
+ continue;
718
+ }
719
+ out.push({ url, routeId: r.routeId, params: e.params });
720
+ }
721
+ }
722
+ out.sort((a, b) => a.url.localeCompare(b.url));
723
+ return out;
724
+ });
725
+
726
+ /**
727
+ * Map of source-url → terminal-redirect-url across published tombstones
728
+ * plus the working release. Lets the dialog auto-flatten `A → B → C` to
729
+ * `A → C` at submit time so we don't write redirect chains.
730
+ */
731
+ const redirectsByUrl = $derived.by((): Record<string, string> => {
732
+ const out: Record<string, string> = {};
733
+ for (const r of deleteRoutes) {
734
+ for (const e of r.entries) {
735
+ if (!e.redirectTo) continue;
736
+ try {
737
+ out[resolveRouteUrl(r.routeId, e.params)] = e.redirectTo;
738
+ } catch {
739
+ /* skip */
740
+ }
741
+ }
742
+ }
743
+ // Only consider release items in the active editor locale: redirect
744
+ // flattening is a runtime URL-routing concern, and at runtime the
745
+ // locale's tombstone is what serves the request. Mixing locales here
746
+ // would let an `es`-only staged redirect bend the chain shown to an
747
+ // `en` editor staging a different redirect.
748
+ for (const item of cmsStore.openRelease?.items ?? []) {
749
+ if (item.kind !== 'page-delete') continue;
750
+ if (item.outcome?.kind !== 'redirect') continue;
751
+ if (item.locale !== currentLocale) continue;
752
+ try {
753
+ out[resolveRouteUrl(item.routeId, item.params)] = item.outcome.to;
754
+ } catch {
755
+ /* skip */
756
+ }
757
+ }
758
+ return out;
759
+ });
760
+
761
+ /**
762
+ * Open the delete dialog targeting `(routeId, params)`. Both the
763
+ * `pages-panel` row button and the `Page → Delete Page…` menu route
764
+ * through here. Re-fetches `/pages` so target/chain data and the entry's
765
+ * draft flag are fresh.
766
+ */
767
+ const requestDelete = async (
768
+ routeId: string,
769
+ params: Record<string, string>,
770
+ isDraftHint = false
771
+ ) => {
772
+ let url: string;
773
+ try {
774
+ url = resolveRouteUrl(routeId, params);
775
+ } catch {
776
+ return;
777
+ }
778
+
779
+ let isDraft = isDraftHint;
780
+ try {
781
+ // `?locale=` so the published-state lookup matches what the editor
782
+ // is currently previewing — a row may be a published draft in `en`
783
+ // and not yet exist in `es`, and the delete dialog's flow must reflect
784
+ // the locale being acted on.
785
+ const res = await fetch(`${endpoint}/pages?locale=${encodeURIComponent(currentLocale)}`, {
786
+ credentials: 'include'
787
+ });
788
+ if (res.ok) {
789
+ const data = (await res.json()) as { routes: WirePageRoute[] };
790
+ deleteRoutes = data.routes;
791
+ const route = data.routes.find((r) => r.routeId === routeId);
792
+ const entry = route?.entries.find((e) => paramsEqual(e.params, params));
793
+ if (entry) isDraft = entry.isDraft ?? false;
794
+ }
795
+ } catch {
796
+ /* keep stale `deleteRoutes` and the caller's hint */
797
+ }
798
+
799
+ deleteTarget = { routeId, params, url, isDraft };
800
+ deleteError = null;
801
+ deleteDialogOpen = true;
802
+ };
803
+
804
+ const onDeletePage = () => {
805
+ const cms = page.data.cms as CmsPayload | undefined;
806
+ const pp = cms?.page;
807
+ if (!pp || !currentDeletableConfig) return;
808
+ closeAllPanels();
809
+ void requestDelete(pp.routeId, pp.params);
810
+ };
811
+
812
+ const onDeleteConfirm = async (mode: DeletePageMode, target?: string) => {
813
+ if (!deleteTarget) return;
814
+ const { routeId, params, isDraft } = deleteTarget;
815
+ const onCurrent = isCurrentPage(routeId, params);
816
+ deleting = true;
817
+ deleteError = null;
818
+ try {
819
+ let outcome: PageDeleteOutcome | undefined;
820
+ if (mode === 'gone') outcome = { kind: 'gone' };
821
+ else if (mode === 'redirect' && target) outcome = { kind: 'redirect', to: target };
822
+ const body: Record<string, unknown> = { routeId, locale: currentLocale, params };
823
+ // Drafts always hard-discard regardless of mode — the dialog gates
824
+ // gone/redirect off when isDraft, but defend in depth.
825
+ if (!isDraft && outcome) body.outcome = outcome;
826
+ const res = await fetch(`${endpoint}/pages`, {
827
+ method: 'DELETE',
828
+ credentials: 'include',
829
+ headers: { 'content-type': 'application/json' },
830
+ body: JSON.stringify(body)
831
+ });
832
+ if (!res.ok) {
833
+ deleteError = 'Could not delete page.';
834
+ return;
835
+ }
836
+ deleteDialogOpen = false;
837
+ deleteTarget = null;
838
+ await cmsStore.fetchOpenRelease(endpoint);
839
+ if (!onCurrent) await refreshAfterReleaseChange();
840
+ } finally {
841
+ deleting = false;
842
+ }
843
+ };
844
+
845
+ const onCopyPreviewLink = async () => {
846
+ if (!previewUrl) return;
847
+ try {
848
+ await navigator.clipboard.writeText(previewUrl);
849
+ } catch {
850
+ /* clipboard may be unavailable; swallow silently */
851
+ }
852
+ };
853
+
854
+ const onOpenInNewTab = () => {
855
+ const url = previewUrl || page.url.toString();
856
+ window.open(url, '_blank', 'noopener');
857
+ };
858
+
859
+ const onRegeneratePreviewKey = async () => {
860
+ if (versionKey && versionRelease) {
861
+ const res = await fetch(`${endpoint}/release/history/${versionRelease.id}/preview-key`, {
862
+ method: 'POST',
863
+ credentials: 'include'
864
+ });
865
+ if (!res.ok) return;
866
+ const data = (await res.json()) as { preview_key: string };
867
+ versionRelease = { ...versionRelease, preview_key: data.preview_key };
868
+ await setVersionParam(data.preview_key, { replace: true });
869
+ await Promise.all([
870
+ cmsStore.loadAndApplyOverlay(endpoint, currentScopes(), null, {
871
+ reset: true,
872
+ versionKey: data.preview_key,
873
+ locale: currentLocale
874
+ }),
875
+ cmsStore.loadAndApplyEntriesOverlay(endpoint, currentEntriesRouteIds(), null, {
876
+ reset: true,
877
+ versionKey: data.preview_key,
878
+ locale: currentLocale
879
+ })
880
+ ]);
881
+ return;
882
+ }
883
+
884
+ const res = await fetch(`${endpoint}/release/preview-key`, {
885
+ method: 'POST',
886
+ credentials: 'include'
887
+ });
888
+ if (!res.ok) return;
889
+ await cmsStore.fetchOpenRelease(endpoint);
890
+ await refreshAfterReleaseChange();
891
+ };
892
+
893
+ const onDiscardAllChanges = async () => {
894
+ if (!confirm('Discard all unpublished changes in your working copy?')) return;
895
+ const res = await fetch(`${endpoint}/release/discard`, {
896
+ method: 'POST',
897
+ credentials: 'include'
898
+ });
899
+ if (!res.ok) return;
900
+ cmsStore.setOpenRelease(null);
901
+ cmsStore.clearDrafts();
902
+ await refreshAfterReleaseChange();
903
+ };
904
+
905
+ const onDiscardCurrentPage = async () => {
906
+ if (currentPageItems.length === 0) return;
907
+ if (!confirm('Discard unpublished changes on this page?')) return;
908
+ for (const item of currentPageItems) {
909
+ if (item.kind !== 'page') continue;
910
+ await fetch(`${endpoint}/release/items`, {
911
+ method: 'DELETE',
912
+ credentials: 'include',
913
+ headers: { 'content-type': 'application/json' },
914
+ body: JSON.stringify({
915
+ kind: 'page',
916
+ routeId: item.routeId,
917
+ locale: item.locale,
918
+ params: item.params
919
+ })
920
+ });
921
+ }
922
+ await cmsStore.fetchOpenRelease(endpoint);
923
+ await refreshAfterReleaseChange();
924
+ cmsStore.isEditing = false;
925
+ };
926
+
927
+ // "Publish this page…" and "Review & publish…" open the same modal; the
928
+ // user filters down to the current page via the per-row checkboxes.
929
+ const onPublishThisPage = onOpenPublish;
930
+
931
+ const onNewFromTemplate = (config: CmsCreatablePageConfigWithRouteId) => {
932
+ openNewPageDialog(config);
933
+ };
934
+
935
+ // "New page…" always opens the chooser.
936
+ const onNewPage = () => {
937
+ if (creatablePages.length === 0) return;
938
+ closeAllPanels();
939
+ chooserOpen = true;
940
+ };
941
+
942
+ const onHideBar = () => onClose();
943
+ const onShareLink = () => {
944
+ closeAllPanels();
945
+ shareLinkOpen = true;
946
+ };
947
+
948
+ const onOpenSeo = () => {
949
+ closeAllPanels();
950
+ seoOpen = true;
951
+ };
952
+
953
+ // SEO edits are their own pending change — flush metadata drafts to the
954
+ // working copy without entering or exiting page edit mode.
955
+ const onSaveSeo = async () => {
956
+ const result = await cmsStore.save(endpoint);
957
+ if (!result.ok) return;
958
+ seoOpen = false;
959
+ const newKey = result.release?.preview_key ?? null;
960
+ if (newKey) await setPreviewParam(newKey, { replace: true });
961
+ await Promise.all([
962
+ cmsStore.loadAndApplyOverlay(endpoint, currentScopes(), newKey, {
963
+ locale: currentLocale
964
+ }),
965
+ cmsStore.loadAndApplyEntriesOverlay(endpoint, currentEntriesRouteIds(), newKey, {
966
+ locale: currentLocale
967
+ }),
968
+ cmsStore.loadAndApplySiteOverlay(endpoint, newKey)
969
+ ]);
970
+ };
971
+
972
+ const onOpenSiteSettings = () => {
973
+ closeAllPanels();
974
+ siteSettingsOpen = true;
975
+ };
976
+
977
+ // Mirrors `onSaveSeo`: flush the site-settings draft as part of the open
978
+ // release, refresh overlays so the just-saved values show via the
979
+ // preview-overlay path on the live page.
980
+ const onSaveSiteSettings = async () => {
981
+ const result = await cmsStore.save(endpoint);
982
+ if (!result.ok) return;
983
+ siteSettingsOpen = false;
984
+ const newKey = result.release?.preview_key ?? null;
985
+ if (newKey) await setPreviewParam(newKey, { replace: true });
986
+ await Promise.all([
987
+ cmsStore.loadAndApplyOverlay(endpoint, currentScopes(), newKey, {
988
+ locale: currentLocale
989
+ }),
990
+ cmsStore.loadAndApplyEntriesOverlay(endpoint, currentEntriesRouteIds(), newKey, {
991
+ locale: currentLocale
992
+ }),
993
+ cmsStore.loadAndApplySiteOverlay(endpoint, newKey)
994
+ ]);
995
+ };
996
+
997
+ const onShowKeyboardShortcuts = () => {
998
+ closeAllPanels();
999
+ shortcutsOpen = true;
1000
+ };
1001
+
1002
+ // Global keyboard shortcuts (DESIGN.md §9). Single-letter keys only fire
1003
+ // when no input is focused; modifier-keyed shortcuts always fire.
1004
+ $effect(() => {
1005
+ const isEditableTarget = (el: EventTarget | null): boolean => {
1006
+ if (!(el instanceof HTMLElement)) return false;
1007
+ const tag = el.tagName;
1008
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
1009
+ if (el.isContentEditable) return true;
1010
+ return false;
1011
+ };
1012
+
1013
+ const handler = (e: KeyboardEvent) => {
1014
+ const meta = e.metaKey || e.ctrlKey;
1015
+ const editable = isEditableTarget(e.target);
1016
+
1017
+ if (meta) {
1018
+ const k = e.key.toLowerCase();
1019
+ if (k === 's' && cmsStore.isEditing) {
1020
+ e.preventDefault();
1021
+ void onSave();
1022
+ } else if (k === 'e') {
1023
+ e.preventDefault();
1024
+ onToggleEdit();
1025
+ } else if (k === 'p') {
1026
+ e.preventDefault();
1027
+ onOpenPublish();
1028
+ } else if (k === 'k') {
1029
+ e.preventDefault();
1030
+ if (pagesOpen) pagesOpen = false;
1031
+ else onOpenPages();
1032
+ } else if (k === 'm') {
1033
+ e.preventDefault();
1034
+ if (mediaOpen) mediaOpen = false;
1035
+ else onOpenMedia();
1036
+ } else if (k === 'h') {
1037
+ e.preventDefault();
1038
+ if (historyOpen) historyOpen = false;
1039
+ else onOpenHistory();
1040
+ } else if (k === 'i') {
1041
+ e.preventDefault();
1042
+ if (seoOpen) seoOpen = false;
1043
+ else onOpenSeo();
1044
+ } else if (k === 'l' && supportedLocales.length > 1) {
1045
+ e.preventDefault();
1046
+ if (localesOpen) localesOpen = false;
1047
+ else onOpenLocales();
1048
+ } else if (k === 'n') {
1049
+ e.preventDefault();
1050
+ onNewPage();
1051
+ } else if (k === '.') {
1052
+ e.preventDefault();
1053
+ onClose();
1054
+ } else if (k === ',') {
1055
+ e.preventDefault();
1056
+ if (siteSettingsOpen) siteSettingsOpen = false;
1057
+ else onOpenSiteSettings();
1058
+ } else if (e.key === 'Enter') {
1059
+ e.preventDefault();
1060
+ onOpenInNewTab();
1061
+ }
1062
+ return;
1063
+ }
1064
+
1065
+ // Single-letter shortcuts gated on no input being focused.
1066
+ if (editable) return;
1067
+ if (e.key === 'Escape' && cmsStore.isEditing && !anyPanelOpen) {
1068
+ // When a panel is open, Escape closes the panel (each panel
1069
+ // owns its own listener); the bar should not also exit edit
1070
+ // mode out from under it.
1071
+ e.preventDefault();
1072
+ onToggleEdit();
1073
+ } else if (e.key === '/') {
1074
+ e.preventDefault();
1075
+ if (pagesOpen) pagesOpen = false;
1076
+ else onOpenPages();
1077
+ } else if (e.key === '?') {
1078
+ e.preventDefault();
1079
+ onShowKeyboardShortcuts();
1080
+ }
1081
+ };
1082
+ window.addEventListener('keydown', handler);
1083
+ return () => window.removeEventListener('keydown', handler);
1084
+ });
1085
+
1086
+ const openNewPageDialog = (config: CmsCreatablePageConfigWithRouteId) => {
1087
+ closeAllPanels();
1088
+ newDialog = config;
1089
+ newPageError = null;
1090
+ };
1091
+
1092
+ const onRequestDuplicate = async (
1093
+ config: CmsCreatablePageConfigWithRouteId,
1094
+ sourceParams: Record<string, string>
1095
+ ) => {
1096
+ const qs = new URLSearchParams({
1097
+ kind: 'page',
1098
+ routeId: config.routeId,
1099
+ params: JSON.stringify(sourceParams)
1100
+ });
1101
+ const previewKey = cmsStore.openRelease?.preview_key;
1102
+ if (previewKey) qs.set('preview', previewKey);
1103
+ const res = await fetch(`${endpoint}/docs?${qs}`, { credentials: 'include' });
1104
+ const sourceContents: Record<string, unknown> = res.ok
1105
+ ? ((await res.json()) as { contents: Record<string, unknown> }).contents
1106
+ : {};
1107
+ const { metadata: meta, ...rest } = sourceContents as { metadata?: unknown } & Record<
1108
+ string,
1109
+ unknown
1110
+ >;
1111
+ const sourceMetadata =
1112
+ meta && typeof meta === 'object' && !Array.isArray(meta)
1113
+ ? (meta as Record<string, unknown>)
1114
+ : {};
1115
+ closeAllPanels();
1116
+ duplicateSource = { tree: rest, metadata: sourceMetadata };
1117
+ newDialog = config;
1118
+ newPageError = null;
1119
+ };
1120
+
1121
+ const closeNewPageDialog = () => {
1122
+ if (newPageCreating) return;
1123
+ newDialog = null;
1124
+ newPageError = null;
1125
+ duplicateSource = null;
1126
+ };
1127
+
1128
+ const handleCreate = async (rawValues: Record<string, string>) => {
1129
+ const target = newDialog;
1130
+ if (!target) return;
1131
+ newPageCreating = true;
1132
+ newPageError = null;
1133
+ try {
1134
+ let transformed: { params: Record<string, string>; metadata?: Record<string, unknown> };
1135
+ try {
1136
+ transformed = target.transform(rawValues);
1137
+ } catch {
1138
+ newPageError = 'Could not derive params from input.';
1139
+ return;
1140
+ }
1141
+ const { params: newParams, metadata: transformMeta = {} } = transformed;
1142
+
1143
+ // Duplicating: keep the source's metadata and content as the base,
1144
+ // then let the transform's output (e.g. the new title) override on
1145
+ // matching keys. Without this merge a duplicated room would lose its
1146
+ // description, price, etc.
1147
+ const dup = duplicateSource;
1148
+ const metadata = dup ? { ...dup.metadata, ...transformMeta } : transformMeta;
1149
+
1150
+ const createRes = await fetch(`${endpoint}/pages`, {
1151
+ method: 'POST',
1152
+ credentials: 'include',
1153
+ headers: { 'content-type': 'application/json' },
1154
+ body: JSON.stringify({
1155
+ routeId: target.routeId,
1156
+ locale: currentLocale,
1157
+ params: newParams,
1158
+ metadata
1159
+ })
1160
+ });
1161
+ if (createRes.status === 409) {
1162
+ newPageError = 'A page with these values already exists.';
1163
+ return;
1164
+ }
1165
+ if (!createRes.ok) {
1166
+ newPageError = 'Could not create page.';
1167
+ return;
1168
+ }
1169
+
1170
+ if (dup && Object.keys(dup.tree).length > 0) {
1171
+ await fetch(`${endpoint}/release/items`, {
1172
+ credentials: 'include',
1173
+ method: 'POST',
1174
+ headers: { 'content-type': 'application/json' },
1175
+ body: JSON.stringify({
1176
+ items: [
1177
+ {
1178
+ kind: 'page',
1179
+ routeId: target.routeId,
1180
+ locale: currentLocale,
1181
+ params: newParams,
1182
+ tree: dup.tree
1183
+ }
1184
+ ]
1185
+ })
1186
+ });
1187
+ }
1188
+
1189
+ await cmsStore.fetchOpenRelease(endpoint);
1190
+ const key = cmsStore.openRelease?.preview_key ?? null;
1191
+ const url = new URL(resolveRouteUrl(target.routeId, newParams), page.url.origin);
1192
+ if (key) url.searchParams.set('preview', key);
1193
+ newDialog = null;
1194
+ duplicateSource = null;
1195
+ pagesOpen = false;
1196
+ cmsStore.isEditing = true;
1197
+ await goto(url, { keepFocus: true, noScroll: true });
1198
+ } finally {
1199
+ newPageCreating = false;
1200
+ }
1201
+ };
1202
+ </script>
1203
+
1204
+ <CssRoot>
1205
+ <div style:--cms-panel-top={subBarVisible && barPosition === 'top' ? '112px' : '72px'}>
1206
+ <!-- Shadow ghost: same shape and position as the bar pill but at a lower
1207
+ z-index than the sub-bar (9998) and panels (9998). The pill itself
1208
+ carries no shadow, so the bar's drop shadow is occluded by anything
1209
+ sitting underneath the bar instead of being painted over the top. -->
1210
+ <div
1211
+ aria-hidden="true"
1212
+ class="vela:fixed vela:left-1/2 vela:-translate-x-1/2 vela:z-[9997]
1213
+ vela:w-full vela:max-w-[560px] vela:mx-4 vela:sm:mx-auto
1214
+ vela:h-12 vela:rounded-full
1215
+ vela:shadow-[0_8px_24px_rgba(0,0,0,0.25)]
1216
+ {barPosition === 'bottom' ? 'vela:bottom-4' : 'vela:top-4'}"
1217
+ ></div>
1218
+
1219
+ <div
1220
+ class="vela:fixed vela:left-1/2 vela:-translate-x-1/2 vela:z-[9999]
1221
+ vela:flex vela:items-center vela:justify-between vela:gap-3
1222
+ vela:w-full vela:max-w-[560px] vela:mx-4 vela:sm:mx-auto
1223
+ vela:h-12 vela:px-3 vela:rounded-full
1224
+ vela:bg-bar-bg vela:text-bar-text
1225
+ {barPosition === 'bottom' ? 'vela:bottom-4' : 'vela:top-4'}"
1226
+ >
1227
+ <div class="vela:flex vela:items-center vela:gap-3">
1228
+ <span
1229
+ class="vela:text-[11px] vela:font-semibold vela:tracking-wider vela:uppercase
1230
+ vela:text-bar-text-tertiary"
1231
+ >
1232
+ CMS
1233
+ </span>
1234
+
1235
+ {#if !subBarVisible}
1236
+ <StatusPill variant="clean">All published</StatusPill>
1237
+ {/if}
1238
+ </div>
1239
+
1240
+ <div class="vela:flex vela:items-center vela:gap-2">
1241
+ <Menubar.Root
1242
+ class="vela:bg-transparent vela:border-0 vela:p-0 vela:gap-0.5 vela:h-auto vela:rounded-none"
1243
+ >
1244
+ <Menubar.Menu>
1245
+ <Menubar.Trigger class={menubarTriggerClass}>Page</Menubar.Trigger>
1246
+ <Menubar.Content class={menuContentClassExt} align="end">
1247
+ <Menubar.Item
1248
+ class={menuItemClass}
1249
+ onSelect={onToggleEdit}
1250
+ disabled={cmsStore.isEditing || !!versionKey}
1251
+ >
1252
+ Edit
1253
+ <KbdShortcut keys="⌘E" class={menuShortcutClass} />
1254
+ </Menubar.Item>
1255
+ <Menubar.Item class={menuItemClass} onSelect={onOpenSeo}>
1256
+ SEO &amp; Metadata
1257
+ <KbdShortcut keys="⌘I" class={menuShortcutClass} />
1258
+ </Menubar.Item>
1259
+ <Menubar.Separator class={menuSeparatorClass} />
1260
+ <Menubar.Item
1261
+ class={menuItemClass}
1262
+ onSelect={onCopyPreviewLink}
1263
+ disabled={!previewUrl}
1264
+ >
1265
+ Copy Preview Link
1266
+ </Menubar.Item>
1267
+ <Menubar.Item class={menuItemClass} onSelect={onOpenInNewTab}>
1268
+ Open In New Tab
1269
+ <KbdShortcut keys="⌘↵" class={menuShortcutClass} />
1270
+ </Menubar.Item>
1271
+ <Menubar.Separator class={menuSeparatorClass} />
1272
+ <Menubar.Item
1273
+ class={menuItemClass}
1274
+ onSelect={onDiscardCurrentPage}
1275
+ disabled={!hasCurrentPageDrafts}
1276
+ >
1277
+ Discard Draft…
1278
+ </Menubar.Item>
1279
+ <Menubar.Item
1280
+ class={menuItemClass}
1281
+ onSelect={onPublishThisPage}
1282
+ disabled={!hasCurrentPageDrafts}
1283
+ >
1284
+ Publish…
1285
+ <KbdShortcut keys="⌘P" class={menuShortcutClass} />
1286
+ </Menubar.Item>
1287
+ <Menubar.Separator class={menuSeparatorClass} />
1288
+ <Menubar.Item
1289
+ class={menuItemDestructiveClass}
1290
+ onSelect={onDeletePage}
1291
+ disabled={!currentDeletableConfig}
1292
+ >
1293
+ Delete Page…
1294
+ </Menubar.Item>
1295
+ </Menubar.Content>
1296
+ </Menubar.Menu>
1297
+
1298
+ <Menubar.Menu>
1299
+ <Menubar.Trigger class={menubarTriggerClass}>Site</Menubar.Trigger>
1300
+ <Menubar.Content class={menuContentClassExt} align="end">
1301
+ <Menubar.Item
1302
+ class={menuItemClass}
1303
+ onSelect={onNewPage}
1304
+ disabled={creatablePages.length === 0}
1305
+ >
1306
+ New Page…
1307
+ <KbdShortcut keys="⌘N" class={menuShortcutClass} />
1308
+ </Menubar.Item>
1309
+ <Menubar.Item class={menuItemClass} onSelect={onOpenPages}>
1310
+ All Pages
1311
+ <KbdShortcut keys="⌘K" class={menuShortcutClass} />
1312
+ </Menubar.Item>
1313
+ <Menubar.Item class={menuItemClass} onSelect={onOpenMedia}>
1314
+ Media Library
1315
+ <KbdShortcut keys="⌘M" class={menuShortcutClass} />
1316
+ </Menubar.Item>
1317
+ {#if supportedLocales.length > 1}
1318
+ <Menubar.Item class={menuItemClass} onSelect={onOpenLocales}>
1319
+ Locales
1320
+ <KbdShortcut keys="⌘L" class={menuShortcutClass} />
1321
+ </Menubar.Item>
1322
+ {/if}
1323
+
1324
+ <Menubar.Label class={menuLabelClass}>Working copy</Menubar.Label>
1325
+ <Menubar.Item
1326
+ class={menuItemClass}
1327
+ onSelect={onOpenPublish}
1328
+ disabled={counts.total === 0}
1329
+ >
1330
+ Review &amp; Publish…
1331
+ {#if counts.total > 0}
1332
+ <span class="vela:ml-auto vela:flex vela:items-center vela:gap-2">
1333
+ <span
1334
+ class="vela:inline-flex vela:items-center vela:justify-center
1335
+ vela:min-w-[18px] vela:h-[18px] vela:px-1 vela:rounded-full
1336
+ vela:text-[10px] vela:font-semibold
1337
+ vela:bg-[var(--cms-status-warn-bg)]
1338
+ vela:text-[var(--cms-status-warn-text)]">{counts.total}</span
1339
+ >
1340
+ <KbdShortcut keys="⌘P" />
1341
+ </span>
1342
+ {:else}
1343
+ <KbdShortcut keys="⌘P" class={menuShortcutClass} />
1344
+ {/if}
1345
+ </Menubar.Item>
1346
+ <Menubar.Item class={menuItemClass} onSelect={onShareLink} disabled={!previewUrl}>
1347
+ Share Preview Link…
1348
+ </Menubar.Item>
1349
+ <Menubar.Item
1350
+ class={menuItemDestructiveClass}
1351
+ onSelect={onDiscardAllChanges}
1352
+ disabled={counts.total === 0}
1353
+ >
1354
+ Discard All Changes…
1355
+ </Menubar.Item>
1356
+
1357
+ <Menubar.Label class={menuLabelClass}>History</Menubar.Label>
1358
+ <Menubar.Item class={menuItemClass} onSelect={onOpenHistory}>
1359
+ Recent Releases
1360
+ <KbdShortcut keys="⌘H" class={menuShortcutClass} />
1361
+ </Menubar.Item>
1362
+
1363
+ <Menubar.Label class={menuLabelClass}>Settings</Menubar.Label>
1364
+ <Menubar.Item class={menuItemClass} onSelect={onOpenSiteSettings}>
1365
+ Site Settings
1366
+ <KbdShortcut keys="⌘," class={menuShortcutClass} />
1367
+ </Menubar.Item>
1368
+ </Menubar.Content>
1369
+ </Menubar.Menu>
1370
+
1371
+ <Menubar.Menu>
1372
+ <Menubar.Trigger class={menubarTriggerClass}>View</Menubar.Trigger>
1373
+ <Menubar.Content class={menuContentClassExt} align="end">
1374
+ <Menubar.Label class={menuLabelClass}>Bar</Menubar.Label>
1375
+ <Menubar.Item class={menuItemClass} onSelect={onHideBar}>
1376
+ Hide Bar
1377
+ <KbdShortcut keys="⌘." class={menuShortcutClass} />
1378
+ </Menubar.Item>
1379
+ <Menubar.Sub>
1380
+ <Menubar.SubTrigger class={menuItemClass}>Position</Menubar.SubTrigger>
1381
+ <Menubar.SubContent class={menuContentClassExt}>
1382
+ <Menubar.Item
1383
+ class={menuCheckIndicatorClass}
1384
+ onSelect={() => (barPosition = 'top')}
1385
+ >
1386
+ <span class="vela:absolute vela:left-2">
1387
+ {barPosition === 'top' ? '✓' : ''}
1388
+ </span>
1389
+ Top
1390
+ </Menubar.Item>
1391
+ <Menubar.Item
1392
+ class={menuCheckIndicatorClass}
1393
+ onSelect={() => (barPosition = 'bottom')}
1394
+ >
1395
+ <span class="vela:absolute vela:left-2">
1396
+ {barPosition === 'bottom' ? '✓' : ''}
1397
+ </span>
1398
+ Bottom
1399
+ </Menubar.Item>
1400
+ </Menubar.SubContent>
1401
+ </Menubar.Sub>
1402
+
1403
+ <Menubar.Label class={menuLabelClass}>Page indicators</Menubar.Label>
1404
+ <Menubar.CheckboxItem class={menuCheckIndicatorClass} bind:checked={highlightSlots}>
1405
+ Highlight Editable Areas
1406
+ <KbdShortcut keys="⌘⇧E" class={menuShortcutClass} />
1407
+ </Menubar.CheckboxItem>
1408
+ <Menubar.CheckboxItem class={menuCheckIndicatorClass} bind:checked={showDraftMarkers}>
1409
+ Show Draft Markers On Links
1410
+ </Menubar.CheckboxItem>
1411
+
1412
+ <Menubar.Label class={menuLabelClass}>Preview As</Menubar.Label>
1413
+ <Menubar.RadioGroup bind:value={previewAs}>
1414
+ <Menubar.RadioItem class={menuCheckIndicatorClass} value="desktop">
1415
+ Desktop
1416
+ </Menubar.RadioItem>
1417
+ <Menubar.RadioItem class={menuCheckIndicatorClass} value="tablet">
1418
+ Tablet
1419
+ </Menubar.RadioItem>
1420
+ <Menubar.RadioItem class={menuCheckIndicatorClass} value="mobile">
1421
+ Mobile
1422
+ </Menubar.RadioItem>
1423
+ <Menubar.RadioItem class={menuCheckIndicatorClass} value="signed-out">
1424
+ Signed-out visitor
1425
+ </Menubar.RadioItem>
1426
+ </Menubar.RadioGroup>
1427
+
1428
+ <Menubar.Label class={menuLabelClass}>Appearance</Menubar.Label>
1429
+ <Menubar.Sub>
1430
+ <Menubar.SubTrigger class={menuItemClass}>Theme</Menubar.SubTrigger>
1431
+ <Menubar.SubContent class={menuContentClassExt}>
1432
+ <Menubar.Item
1433
+ class={menuCheckIndicatorClass}
1434
+ onSelect={() => setThemePref('system')}
1435
+ >
1436
+ <span class="vela:absolute vela:left-2">
1437
+ {adminBarTheme.pref === 'system' ? '✓' : ''}
1438
+ </span>
1439
+ System
1440
+ </Menubar.Item>
1441
+ <Menubar.Item
1442
+ class={menuCheckIndicatorClass}
1443
+ onSelect={() => setThemePref('light')}
1444
+ >
1445
+ <span class="vela:absolute vela:left-2">
1446
+ {adminBarTheme.pref === 'light' ? '✓' : ''}
1447
+ </span>
1448
+ Light
1449
+ </Menubar.Item>
1450
+ <Menubar.Item
1451
+ class={menuCheckIndicatorClass}
1452
+ onSelect={() => setThemePref('dark')}
1453
+ >
1454
+ <span class="vela:absolute vela:left-2">
1455
+ {adminBarTheme.pref === 'dark' ? '✓' : ''}
1456
+ </span>
1457
+ Dark
1458
+ </Menubar.Item>
1459
+ </Menubar.SubContent>
1460
+ </Menubar.Sub>
1461
+
1462
+ <Menubar.Separator class={menuSeparatorClass} />
1463
+ <Menubar.Item class={menuItemClass} onSelect={onShowKeyboardShortcuts}>
1464
+ Keyboard Shortcuts…
1465
+ <KbdShortcut keys="?" class={menuShortcutClass} />
1466
+ </Menubar.Item>
1467
+ </Menubar.Content>
1468
+ </Menubar.Menu>
1469
+
1470
+ <Menubar.Menu>
1471
+ <Menubar.Trigger class={avatarTriggerClass} aria-label="Account menu">
1472
+ <UserAvatar name={user.name} />
1473
+ </Menubar.Trigger>
1474
+ <Menubar.Content class={menuContentClassExt} align="end">
1475
+ <Menubar.Item class={menuItemClass} onSelect={onLogout}
1476
+ >Log Out {user.name}</Menubar.Item
1477
+ >
1478
+ </Menubar.Content>
1479
+ </Menubar.Menu>
1480
+ </Menubar.Root>
1481
+
1482
+ <Button
1483
+ variant="ghost"
1484
+ size="pill-icon"
1485
+ aria-label="Close"
1486
+ onclick={onClose}
1487
+ class="vela:h-6 vela:w-6"
1488
+ >
1489
+ ×
1490
+ </Button>
1491
+ </div>
1492
+ </div>
1493
+
1494
+ {#if subBarVisible}
1495
+ <!-- Sub-bar: bg starts behind the main bar (same top, taller height) so it
1496
+ reads as a footer/extension. Top corners are square; bottom matches
1497
+ the main bar's pill curvature. Bg is mostly transparent so the page
1498
+ shows through. The visible band is the 40px below the main bar. -->
1499
+ <div
1500
+ class="vela:fixed vela:left-1/2 vela:-translate-x-1/2 vela:z-[9998]
1501
+ vela:flex vela:justify-between vela:gap-3
1502
+ vela:w-full vela:max-w-[560px] vela:mx-4 vela:sm:mx-auto
1503
+ vela:h-16 vela:px-3
1504
+ vela:text-bar-text
1505
+ vela:bg-[var(--cms-sub-bar-bg)]
1506
+ vela:border vela:border-[var(--cms-sub-bar-border)]
1507
+ {barPosition === 'bottom'
1508
+ ? 'vela:bottom-10 vela:pt-2 vela:items-start vela:rounded-t-3xl'
1509
+ : 'vela:top-10 vela:pb-2 vela:items-end vela:rounded-b-3xl'}"
1510
+ >
1511
+ <div class="vela:flex vela:items-center">
1512
+ {#if versionKey}
1513
+ <StatusPill
1514
+ variant="edit"
1515
+ class="vela:h-5.5 vela:pl-2 vela:pr-2.5 vela:gap-1 vela:text-[11px]"
1516
+ >
1517
+ Viewing release {versionRelease?.name ?? versionRelease?.id.slice(0, 8) ?? ''}
1518
+ </StatusPill>
1519
+ {:else if cmsStore.isEditing}
1520
+ <StatusPill
1521
+ variant="edit"
1522
+ class="vela:h-5.5 vela:pl-2 vela:pr-2.5 vela:gap-1 vela:text-[11px]"
1523
+ >
1524
+ Editing
1525
+ </StatusPill>
1526
+ {:else}
1527
+ <StatusPill
1528
+ variant="warn"
1529
+ onclick={onOpenPages}
1530
+ class="vela:h-5.5 vela:pl-2 vela:pr-2.5 vela:gap-1 vela:text-[11px]"
1531
+ >
1532
+ {counts.total}
1533
+ {counts.total === 1 ? 'draft' : 'drafts'}
1534
+ </StatusPill>
1535
+ {/if}
1536
+ </div>
1537
+
1538
+ <div class="vela:flex vela:items-center vela:gap-1.5">
1539
+ {#if versionKey}
1540
+ <Button
1541
+ size="pill"
1542
+ onclick={exitVersionView}
1543
+ class="vela:h-5.5 vela:px-2.5 vela:text-[10px]"
1544
+ >
1545
+ Exit
1546
+ </Button>
1547
+ {:else if cmsStore.isEditing}
1548
+ <Button
1549
+ variant="ghost"
1550
+ size="pill"
1551
+ onclick={onToggleEdit}
1552
+ class="vela:h-5.5 vela:px-2.5 vela:text-[10px]"
1553
+ >
1554
+ Cancel
1555
+ </Button>
1556
+ <Button size="pill" onclick={onSave} class="vela:h-5.5 vela:px-2.5 vela:text-[10px]">
1557
+ Save
1558
+ </Button>
1559
+ {:else}
1560
+ <Button
1561
+ variant="ghost"
1562
+ size="pill"
1563
+ onclick={onDiscardAllChanges}
1564
+ class="vela:h-5.5 vela:px-2.5 vela:text-[10px]"
1565
+ >
1566
+ Discard all changes…
1567
+ </Button>
1568
+ <Button
1569
+ size="pill"
1570
+ onclick={onOpenPublish}
1571
+ class="vela:h-5.5 vela:px-2.5 vela:text-[10px]"
1572
+ >
1573
+ Publish…
1574
+ </Button>
1575
+ {/if}
1576
+ </div>
1577
+ </div>
1578
+ {/if}
1579
+
1580
+ {#if seoOpen}
1581
+ {#await import('./seo-panel.svelte') then { default: SeoPanel }}
1582
+ <SeoPanel onClose={() => (seoOpen = false)} onSave={onSaveSeo} />
1583
+ {/await}
1584
+ {/if}
1585
+
1586
+ {#if siteSettingsOpen}
1587
+ {#await import('./site-settings-panel.svelte') then { default: SiteSettingsPanel }}
1588
+ <SiteSettingsPanel onClose={() => (siteSettingsOpen = false)} onSave={onSaveSiteSettings} />
1589
+ {/await}
1590
+ {/if}
1591
+
1592
+ {#if historyOpen}
1593
+ <HistoryPanel
1594
+ {endpoint}
1595
+ onClose={() => (historyOpen = false)}
1596
+ onChanged={refreshAfterReleaseChange}
1597
+ />
1598
+ {/if}
1599
+
1600
+ {#if pagesOpen}
1601
+ <PagesPanel
1602
+ {endpoint}
1603
+ {creatablePages}
1604
+ onClose={() => (pagesOpen = false)}
1605
+ onChanged={refreshAfterReleaseChange}
1606
+ onRequestNew={openNewPageDialog}
1607
+ {onRequestDuplicate}
1608
+ onRequestDelete={(routeId, params, isDraft) => void requestDelete(routeId, params, isDraft)}
1609
+ />
1610
+ {/if}
1611
+
1612
+ {#if mediaOpen}
1613
+ <MediaPanel {endpoint} onClose={() => (mediaOpen = false)} />
1614
+ {/if}
1615
+
1616
+ {#if localesOpen}
1617
+ {#await import('./locales-panel.svelte') then { default: LocalesPanel }}
1618
+ <LocalesPanel
1619
+ {currentLocale}
1620
+ locales={supportedLocales}
1621
+ onClose={() => (localesOpen = false)}
1622
+ onSelectLocale={(locale) => {
1623
+ const target = locale === supportedLocales[0] ? null : locale;
1624
+ void setLocaleParam(target);
1625
+ }}
1626
+ />
1627
+ {/await}
1628
+ {/if}
1629
+
1630
+ {#if cmsStore.openRelease}
1631
+ <PublishDialog
1632
+ open={publishOpen}
1633
+ onOpenChange={(next) => (publishOpen = next)}
1634
+ items={cmsStore.openRelease.items}
1635
+ {publishing}
1636
+ error={publishError}
1637
+ {user}
1638
+ {endpoint}
1639
+ onConfirm={onConfirmPublish}
1640
+ />
1641
+ {/if}
1642
+
1643
+ {#if newDialog}
1644
+ <NewPageDialog
1645
+ open={!!newDialog}
1646
+ onOpenChange={(next) => {
1647
+ if (!next) closeNewPageDialog();
1648
+ }}
1649
+ config={newDialog}
1650
+ creating={newPageCreating}
1651
+ error={newPageError}
1652
+ onCreate={handleCreate}
1653
+ mode={duplicateSource ? 'duplicate' : 'new'}
1654
+ initialValues={duplicateSource
1655
+ ? Object.fromEntries(
1656
+ Object.entries(duplicateSource.metadata).filter(([, v]) => typeof v === 'string') as [
1657
+ string,
1658
+ string
1659
+ ][]
1660
+ )
1661
+ : {}}
1662
+ />
1663
+ {/if}
1664
+
1665
+ <DeletePageDialog
1666
+ open={deleteDialogOpen}
1667
+ onOpenChange={(next) => {
1668
+ if (!next && deleting) return;
1669
+ deleteDialogOpen = next;
1670
+ if (!next) deleteTarget = null;
1671
+ }}
1672
+ url={deleteTarget?.url ?? ''}
1673
+ isDraft={deleteTarget?.isDraft ?? false}
1674
+ targets={redirectTargets}
1675
+ {redirectsByUrl}
1676
+ {deleting}
1677
+ error={deleteError}
1678
+ onConfirm={onDeleteConfirm}
1679
+ />
1680
+
1681
+ <NewPageChooserDialog
1682
+ open={chooserOpen}
1683
+ onOpenChange={(next) => (chooserOpen = next)}
1684
+ {creatablePages}
1685
+ onSelect={onNewFromTemplate}
1686
+ />
1687
+
1688
+ <SharePreviewLinkDialog
1689
+ open={shareLinkOpen}
1690
+ onOpenChange={(next) => (shareLinkOpen = next)}
1691
+ {previewUrl}
1692
+ onRegenerate={onRegeneratePreviewKey}
1693
+ />
1694
+
1695
+ <KeyboardShortcutsDialog open={shortcutsOpen} onOpenChange={(next) => (shortcutsOpen = next)} />
1696
+ </div>
1697
+ </CssRoot>