@riducms/admin 0.2.2 → 0.3.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 (352) hide show
  1. package/THIRD_PARTY_NOTICES.md +60 -37
  2. package/package.json +27 -15
  3. package/src/app/admin-core-view-route.svelte +38 -53
  4. package/src/app/admin-extension-route.svelte +19 -0
  5. package/src/app/admin-layout.scss +175 -0
  6. package/src/app/admin-layout.svelte +144 -410
  7. package/src/app/admin-navigation-progress.svelte +11 -77
  8. package/src/app/admin-page-title.svelte +17 -85
  9. package/src/app/admin-page-title.ts +136 -0
  10. package/src/app/admin-route-error.svelte +24 -0
  11. package/src/app/admin-router-provider.svelte +34 -0
  12. package/src/app/admin-router-root.svelte +41 -0
  13. package/src/app/admin-routes.svelte +56 -19
  14. package/src/app/loading-boundary.svelte +3 -1
  15. package/src/app-meta.ts +13 -0
  16. package/src/app.css +138 -496
  17. package/src/app.svelte +60 -26
  18. package/src/components/brand/ridu-logo.svelte +19 -5
  19. package/src/components/data/list-table.scss +63 -0
  20. package/src/components/icons/LICENSE.md +24 -0
  21. package/src/components/icons/chevron.svelte +8 -0
  22. package/src/components/icons/plus.svelte +12 -0
  23. package/src/components/icons/search.svelte +12 -0
  24. package/src/components/icons/x.svelte +8 -0
  25. package/src/components/json-tree/json-tree-node.svelte +61 -67
  26. package/src/components/json-tree/json-viewer.scss +153 -0
  27. package/src/components/json-tree/json-viewer.svelte +57 -96
  28. package/src/components/ui/banner/banner.scss +28 -0
  29. package/src/components/ui/banner/banner.svelte +9 -8
  30. package/src/components/ui/banner/index.ts +1 -5
  31. package/src/components/ui/code-editor/code-editor.scss +20 -0
  32. package/src/components/ui/code-editor/code-editor.svelte +71 -0
  33. package/src/components/ui/code-editor/code-editor.ts +151 -0
  34. package/src/components/ui/code-editor/code-languages.ts +29 -0
  35. package/src/components/ui/collapsible-panel/collapsible-panel.scss +38 -0
  36. package/src/components/ui/collapsible-panel/collapsible-panel.svelte +13 -0
  37. package/src/components/ui/collapsible-panel/index.ts +1 -0
  38. package/src/components/ui/combobox/combobox.scss +163 -0
  39. package/src/components/ui/combobox/index.ts +22 -0
  40. package/src/components/ui/date-value-control/date-value-calendar.svelte +128 -16
  41. package/src/components/ui/date-value-control/date-value-control-loader.scss +26 -0
  42. package/src/components/ui/date-value-control/date-value-control-loader.svelte +35 -13
  43. package/src/components/ui/date-value-control/date-value-control.scss +223 -0
  44. package/src/components/ui/date-value-control/date-value-control.svelte +62 -44
  45. package/src/components/ui/drawer/drawer-depth.ts +8 -0
  46. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +2 -5
  47. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +2 -5
  48. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +2 -2
  49. package/src/components/ui/dropdown-menu/dropdown-menu.scss +62 -0
  50. package/src/components/ui/kbd/kbd.scss +17 -0
  51. package/src/components/ui/kbd/kbd.svelte +3 -10
  52. package/src/components/ui/navigation-drawer/navigation-drawer.scss +40 -0
  53. package/src/components/ui/navigation-drawer/navigation-drawer.svelte +38 -0
  54. package/src/components/ui/popover/popover-content.svelte +3 -5
  55. package/src/components/ui/popover/popover.scss +18 -0
  56. package/src/components/ui/radio-group/index.ts +0 -1
  57. package/src/components/ui/radio-group/radio-card-item.svelte +7 -12
  58. package/src/components/ui/radio-group/radio-group.svelte +2 -2
  59. package/src/components/ui/radio-group/radio.scss +45 -0
  60. package/src/components/ui/select/select-content.svelte +5 -6
  61. package/src/components/ui/select/select-item.svelte +8 -8
  62. package/src/components/ui/select/select-scroll-down-button.svelte +3 -5
  63. package/src/components/ui/select/select-scroll-up-button.svelte +3 -5
  64. package/src/components/ui/select/select-trigger.svelte +6 -7
  65. package/src/components/ui/select/select.scss +126 -0
  66. package/src/components/ui/sheet/sheet-content.svelte +6 -34
  67. package/src/components/ui/sheet/sheet-description.svelte +2 -2
  68. package/src/components/ui/sheet/sheet-overlay.svelte +2 -21
  69. package/src/components/ui/sheet/sheet-title.svelte +2 -2
  70. package/src/components/ui/sheet/sheet.scss +221 -0
  71. package/src/components/ui/skeleton/skeleton.scss +20 -0
  72. package/src/components/ui/skeleton/skeleton.svelte +4 -16
  73. package/src/components/ui/sonner/sonner.scss +121 -0
  74. package/src/components/ui/sonner/sonner.svelte +11 -10
  75. package/src/components/ui/status-indicator/status-indicator.scss +48 -0
  76. package/src/components/ui/status-indicator/status-indicator.svelte +14 -14
  77. package/src/components/ui/tabs/tabs-content.svelte +2 -2
  78. package/src/components/ui/tabs/tabs-list.svelte +7 -7
  79. package/src/components/ui/tabs/tabs-trigger.svelte +2 -8
  80. package/src/components/ui/tabs/tabs.scss +86 -0
  81. package/src/components/ui/tabs/tabs.svelte +2 -2
  82. package/src/components/ui/timezone-picker/timezone-picker.svelte +152 -0
  83. package/src/core/api/admin-client.ts +2 -2
  84. package/src/core/bootstrap/admin-bootstrap.ts +396 -0
  85. package/src/core/bootstrap/admin-prepared-state.ts +213 -0
  86. package/src/core/bootstrap/admin-route-modules.ts +66 -0
  87. package/src/core/clipboard/copy-feedback.svelte.ts +43 -0
  88. package/src/core/forms/embedded-fields.ts +4 -8
  89. package/src/core/forms/field-editor-authoring.ts +8 -0
  90. package/src/core/forms/field-issue-focus.ts +4 -4
  91. package/src/core/forms/field-occurrence.ts +4 -7
  92. package/src/core/forms/form-controller.svelte.ts +14 -21
  93. package/src/core/forms/form-draft-recovery.ts +17 -4
  94. package/src/core/forms/form-issue-correlation.ts +13 -13
  95. package/src/core/forms/form-path.ts +17 -0
  96. package/src/core/forms/form-schema.ts +12 -43
  97. package/src/core/forms/form-validation.ts +7 -46
  98. package/src/core/forms/plugin-field-authoring.ts +6 -2
  99. package/src/core/forms/plugin-field-write.ts +2 -6
  100. package/src/core/i18n/admin-i18n.svelte.ts +18 -0
  101. package/src/core/i18n/format-admin-date-time.ts +19 -0
  102. package/src/core/i18n/humanize-identifier.ts +5 -0
  103. package/src/core/i18n/time-zone-label.ts +20 -0
  104. package/src/core/loaders/admin-view-host.svelte +72 -0
  105. package/src/core/loaders/admin-view-loader.svelte.ts +90 -0
  106. package/src/core/notifications/validation-toast.svelte +2 -2
  107. package/src/core/plugins/field-registry.ts +7 -8
  108. package/src/core/routing/admin-paths.ts +53 -2
  109. package/src/core/routing/admin-scroll.svelte.ts +74 -0
  110. package/src/core/routing/admin-view-selection.ts +78 -0
  111. package/src/core/runtime/admin-runtime.svelte.ts +121 -124
  112. package/src/core/schema/field-default.ts +33 -0
  113. package/src/core/schema/field-localization.ts +15 -0
  114. package/src/core/schema/read-document-path.ts +11 -0
  115. package/src/features/account/account-menu.scss +37 -0
  116. package/src/features/account/account-menu.svelte +115 -0
  117. package/src/features/{auth → account}/account-navigation.svelte +16 -19
  118. package/src/features/{auth → account}/account-route.svelte +206 -49
  119. package/src/features/account/account-security-controller.svelte.ts +157 -0
  120. package/src/features/{auth → account}/account-security-route.svelte +140 -42
  121. package/src/features/api-reference/api-reference-code.svelte +64 -0
  122. package/src/features/api-reference/api-reference-content.svelte +258 -0
  123. package/src/features/api-reference/api-reference-drawer.scss +74 -0
  124. package/src/features/api-reference/api-reference-examples.ts +381 -0
  125. package/src/features/api-reference/api-reference-schema.ts +332 -0
  126. package/src/features/api-reference/api-reference.scss +190 -0
  127. package/src/features/api-reference/api-reference.svelte +74 -0
  128. package/src/features/auth/auth-brand.svelte +5 -3
  129. package/src/features/auth/auth-field.svelte +92 -0
  130. package/src/features/auth/auth-frame.svelte +5 -8
  131. package/src/features/auth/auth.scss +122 -0
  132. package/src/features/auth/create-first-user-controller.svelte.ts +20 -51
  133. package/src/features/auth/create-first-user-route.svelte +83 -122
  134. package/src/features/auth/forgot-password-route.svelte +31 -23
  135. package/src/features/auth/login-route.svelte +68 -116
  136. package/src/features/auth/logout-button.scss +34 -0
  137. package/src/features/auth/logout-button.svelte +52 -0
  138. package/src/features/auth/request-verification-route.svelte +27 -22
  139. package/src/features/auth/reset-password-route.svelte +108 -41
  140. package/src/features/auth/verify-email-route.svelte +49 -15
  141. package/src/features/bulk-edit/bulk-edit-fields.ts +23 -0
  142. package/src/features/bulk-edit/bulk-edit.scss +152 -0
  143. package/src/features/bulk-edit/field-picker.svelte +176 -0
  144. package/src/features/collections/bulk/bulk-actions.svelte +148 -0
  145. package/src/features/collections/bulk/bulk-confirmation.svelte +153 -0
  146. package/src/features/collections/bulk/bulk-editor-loader.svelte +95 -0
  147. package/src/features/collections/bulk/bulk-editor.svelte +186 -0
  148. package/src/features/collections/collection-list-cell-values.ts +144 -0
  149. package/src/features/collections/collection-list-controller.svelte.ts +268 -379
  150. package/src/features/collections/collection-list-preferences-controller.svelte.ts +54 -40
  151. package/src/features/collections/collection-list-query.ts +223 -0
  152. package/src/features/collections/collection-list-results.svelte +205 -329
  153. package/src/features/collections/collection-list-route.svelte +143 -884
  154. package/src/features/collections/collection-list.scss +261 -0
  155. package/src/features/collections/collection-list.svelte.ts +362 -0
  156. package/src/features/collections/controls/column-picker.svelte +110 -0
  157. package/src/features/collections/controls/column-pill.svelte +62 -0
  158. package/src/features/collections/controls/filter-builder.svelte +193 -0
  159. package/src/features/collections/controls/filter-row.svelte +142 -0
  160. package/src/features/collections/controls/list-controls.scss +196 -0
  161. package/src/features/collections/controls/list-options.svelte +203 -0
  162. package/src/features/collections/controls/list-pagination.scss +78 -0
  163. package/src/features/collections/controls/list-pagination.svelte +115 -0
  164. package/src/features/collections/controls/list-toolbar.svelte +132 -0
  165. package/src/features/collections/controls/sort-heading.scss +52 -0
  166. package/src/features/collections/controls/sort-heading.svelte +78 -0
  167. package/src/features/collections/list-columns.ts +43 -0
  168. package/src/features/collections/list-workspace.ts +155 -54
  169. package/src/features/dashboard/dashboard-route.svelte +82 -86
  170. package/src/features/dashboard/dashboard.scss +131 -0
  171. package/src/features/development/development-tools.svelte +20 -36
  172. package/src/features/documents/document-action-bar.svelte +438 -0
  173. package/src/features/documents/document-api-view-loader.svelte +17 -6
  174. package/src/features/documents/document-api-view.scss +120 -0
  175. package/src/features/documents/document-api-view.svelte +169 -454
  176. package/src/features/documents/document-controller.svelte.ts +526 -613
  177. package/src/features/documents/document-credentials.scss +37 -0
  178. package/src/features/documents/document-credentials.svelte +72 -0
  179. package/src/features/documents/{document-route-dirty.ts → document-credentials.ts} +25 -0
  180. package/src/features/documents/document-field-sections.scss +39 -0
  181. package/src/features/documents/document-field-sections.svelte +4 -13
  182. package/src/features/documents/document-loading.scss +24 -0
  183. package/src/features/documents/document-lock-controller.svelte.ts +187 -0
  184. package/src/features/documents/document-operation-dialogs.svelte +110 -0
  185. package/src/features/documents/document-route-loader.svelte +17 -5
  186. package/src/features/documents/document-route.svelte +305 -811
  187. package/src/features/documents/document-schedule.scss +127 -0
  188. package/src/features/documents/document-schedule.svelte +323 -0
  189. package/src/features/documents/document-title.ts +25 -0
  190. package/src/features/documents/document.scss +274 -0
  191. package/src/features/documents/live-preview-panel.scss +117 -0
  192. package/src/features/documents/live-preview-panel.svelte +167 -168
  193. package/src/features/documents/live-preview.ts +2 -10
  194. package/src/features/documents/upload-document-preview-loader.svelte +26 -23
  195. package/src/features/localization/content-locale-switcher.scss +43 -0
  196. package/src/features/localization/content-locale-switcher.svelte +24 -17
  197. package/src/features/navigation/admin-breadcrumbs.scss +56 -0
  198. package/src/features/navigation/admin-breadcrumbs.svelte +102 -37
  199. package/src/features/navigation/admin-command-menu.svelte +38 -15
  200. package/src/features/navigation/admin-navigation.scss +133 -0
  201. package/src/features/navigation/admin-navigation.svelte +169 -0
  202. package/src/features/navigation/breadcrumb-context.svelte.ts +15 -0
  203. package/src/features/navigation/navigation-preference.svelte.ts +70 -0
  204. package/src/features/reference-browser/reference-browser-editor.svelte +180 -0
  205. package/src/features/reference-browser/reference-browser-list.scss +113 -0
  206. package/src/features/reference-browser/reference-browser-list.svelte +370 -0
  207. package/src/features/reference-browser/reference-browser-loader.svelte +26 -8
  208. package/src/features/reference-browser/reference-browser-workflow.svelte.ts +335 -236
  209. package/src/features/reference-browser/reference-browser.scss +217 -0
  210. package/src/features/reference-browser/reference-browser.svelte +154 -797
  211. package/src/features/reference-browser/reference-collection-picker.svelte +116 -0
  212. package/src/features/reference-browser/reference-list-query.ts +35 -0
  213. package/src/features/uploads/bulk-upload-controller.svelte.ts +604 -0
  214. package/src/features/uploads/bulk-upload-edit-all.svelte +92 -0
  215. package/src/features/uploads/bulk-upload-editor.svelte +191 -0
  216. package/src/features/uploads/bulk-upload-file-picker.svelte +70 -0
  217. package/src/features/uploads/bulk-upload-remote.svelte +100 -0
  218. package/src/features/uploads/bulk-upload-route-loader.svelte +54 -0
  219. package/src/features/uploads/bulk-upload-route.svelte +172 -592
  220. package/src/features/uploads/bulk-upload-sidebar.svelte +125 -0
  221. package/src/features/uploads/bulk-upload.scss +526 -0
  222. package/src/features/uploads/bulk-upload.ts +105 -14
  223. package/src/features/uploads/crop-surface.ts +150 -0
  224. package/src/features/uploads/image-editor.svelte +231 -0
  225. package/src/features/uploads/rendition-preview.svelte +64 -0
  226. package/src/features/uploads/upload-control.svelte +340 -0
  227. package/src/features/uploads/upload-document-contracts.ts +30 -0
  228. package/src/features/uploads/upload-draft.svelte.ts +294 -0
  229. package/src/features/uploads/upload-metadata-fields.scss +32 -0
  230. package/src/features/uploads/upload-metadata-fields.svelte +8 -7
  231. package/src/features/uploads/upload-thumbnail.svelte +47 -0
  232. package/src/features/uploads/upload.scss +361 -0
  233. package/src/features/versions/version-comparison.svelte +361 -0
  234. package/src/features/versions/version-diff-fields.svelte +136 -0
  235. package/src/features/versions/version-diff.ts +296 -41
  236. package/src/features/versions/version-history-controller.svelte.ts +311 -0
  237. package/src/features/versions/version-history-route-loader.svelte +32 -0
  238. package/src/features/versions/version-history-route.svelte +157 -522
  239. package/src/features/versions/version-history.scss +444 -0
  240. package/src/features/versions/version-history.ts +45 -0
  241. package/src/features/versions/version-table.svelte +91 -0
  242. package/src/fields/embedded-schema-draft-editor.scss +48 -0
  243. package/src/fields/embedded-schema-draft-editor.svelte +24 -16
  244. package/src/fields/field-collapsible.svelte +14 -8
  245. package/src/fields/field-layout.scss +171 -0
  246. package/src/fields/field-layout.svelte +3 -5
  247. package/src/fields/field-renderer.svelte +17 -29
  248. package/src/fields/field-shell.svelte +1 -1
  249. package/src/fields/field-tab-layout.svelte +25 -19
  250. package/src/fields/join/join-field-controller.svelte.ts +513 -0
  251. package/src/fields/join/join-field-values.ts +111 -0
  252. package/src/fields/join/join-field.scss +189 -0
  253. package/src/fields/join/join-field.svelte +164 -332
  254. package/src/fields/live-validation-feedback.svelte +4 -3
  255. package/src/fields/local-field-editor.svelte +2 -2
  256. package/src/fields/nested/block-name-input.svelte +11 -12
  257. package/src/fields/nested/block-picker.scss +128 -0
  258. package/src/fields/nested/block-picker.svelte +81 -0
  259. package/src/fields/nested/block-placeholder.svg +2 -0
  260. package/src/fields/nested/nested-field.scss +331 -0
  261. package/src/fields/nested/nested-field.svelte +265 -245
  262. package/src/fields/nested/sortable-row.svelte +9 -7
  263. package/src/fields/plugin-field.svelte +4 -6
  264. package/src/fields/point/point-field.svelte +51 -38
  265. package/src/fields/primitive-list/primitive-list-field.svelte +63 -19
  266. package/src/fields/primitive-list/primitive-list.scss +109 -0
  267. package/src/fields/radio/radio-field.svelte +2 -1
  268. package/src/fields/relationship/relationship-field-controller.svelte.ts +81 -41
  269. package/src/fields/relationship/relationship-field.svelte +144 -175
  270. package/src/fields/relationship/relationship-lookup-controller.svelte.ts +58 -30
  271. package/src/fields/relationship/relationship-query.ts +2 -10
  272. package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +36 -62
  273. package/src/fields/relationship/relationship-quick-picker.svelte +178 -227
  274. package/src/fields/relationship/relationship-selection.svelte +120 -0
  275. package/src/fields/relationship/relationship-value.ts +46 -7
  276. package/src/fields/relationship/relationship.scss +182 -0
  277. package/src/fields/scalar/code-field.svelte +101 -0
  278. package/src/fields/scalar/date-control-value.ts +13 -0
  279. package/src/fields/scalar/scalar-field.svelte +25 -67
  280. package/src/fields/select/select-field.svelte +132 -94
  281. package/src/fields/text/slug-field.scss +28 -0
  282. package/src/fields/text/slug-field.svelte +19 -15
  283. package/src/fields/text/text-field.svelte +1 -2
  284. package/src/fields/ui/ui-field.svelte +7 -4
  285. package/src/fields/virtual/virtual-field.svelte +2 -5
  286. package/src/index.ts +1 -2
  287. package/src/routing.ts +2 -0
  288. package/src/app/deferred-redirect.svelte +0 -23
  289. package/src/components/code-block/code-block.svelte +0 -107
  290. package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +0 -50
  291. package/src/components/ui/bulk-action-bar/index.ts +0 -1
  292. package/src/components/ui/button/index.ts +0 -8
  293. package/src/components/ui/calendar/calendar-caption.svelte +0 -72
  294. package/src/components/ui/calendar/calendar-cell.svelte +0 -20
  295. package/src/components/ui/calendar/calendar-day.svelte +0 -27
  296. package/src/components/ui/calendar/calendar-grid-row.svelte +0 -13
  297. package/src/components/ui/calendar/calendar-grid.svelte +0 -17
  298. package/src/components/ui/calendar/calendar-head-cell.svelte +0 -20
  299. package/src/components/ui/calendar/calendar-header.svelte +0 -20
  300. package/src/components/ui/calendar/calendar-heading.svelte +0 -17
  301. package/src/components/ui/calendar/calendar-month-select.svelte +0 -45
  302. package/src/components/ui/calendar/calendar-month.svelte +0 -16
  303. package/src/components/ui/calendar/calendar-months.svelte +0 -20
  304. package/src/components/ui/calendar/calendar-nav.svelte +0 -20
  305. package/src/components/ui/calendar/calendar-next-button.svelte +0 -30
  306. package/src/components/ui/calendar/calendar-prev-button.svelte +0 -30
  307. package/src/components/ui/calendar/calendar-year-select.svelte +0 -44
  308. package/src/components/ui/calendar/calendar.svelte +0 -118
  309. package/src/components/ui/calendar/index.ts +0 -1
  310. package/src/components/ui/checkbox/checkbox.svelte +0 -35
  311. package/src/components/ui/checkbox/index.ts +0 -1
  312. package/src/components/ui/dialog/dialog-close.svelte +0 -11
  313. package/src/components/ui/dialog/dialog-content.svelte +0 -52
  314. package/src/components/ui/dialog/dialog-description.svelte +0 -20
  315. package/src/components/ui/dialog/dialog-footer.svelte +0 -20
  316. package/src/components/ui/dialog/dialog-header.svelte +0 -20
  317. package/src/components/ui/dialog/dialog-overlay.svelte +0 -20
  318. package/src/components/ui/dialog/dialog-title.svelte +0 -17
  319. package/src/components/ui/dialog/dialog-trigger.svelte +0 -11
  320. package/src/components/ui/dialog/dialog.svelte +0 -7
  321. package/src/components/ui/dialog/index.ts +0 -8
  322. package/src/components/ui/empty-state/empty-state.svelte +0 -34
  323. package/src/components/ui/empty-state/index.ts +0 -1
  324. package/src/components/ui/input/index.ts +0 -1
  325. package/src/components/ui/pagination/index.ts +0 -1
  326. package/src/components/ui/pagination/pagination.svelte +0 -88
  327. package/src/components/ui/radio-group/radio-group-item.svelte +0 -17
  328. package/src/components/ui/slider/index.ts +0 -3
  329. package/src/components/ui/slider/slider-content.svelte +0 -59
  330. package/src/components/ui/slider/slider.svelte +0 -35
  331. package/src/components/ui/table/index.ts +0 -6
  332. package/src/components/ui/table/table-body.svelte +0 -20
  333. package/src/components/ui/table/table-cell.svelte +0 -23
  334. package/src/components/ui/table/table-head.svelte +0 -23
  335. package/src/components/ui/table/table-header.svelte +0 -20
  336. package/src/components/ui/table/table-row.svelte +0 -23
  337. package/src/components/ui/table/table.svelte +0 -22
  338. package/src/components/ui/textarea/index.ts +0 -1
  339. package/src/components/ui/tooltip/index.ts +0 -4
  340. package/src/components/ui/tooltip/tooltip-content.svelte +0 -50
  341. package/src/components/ui/tooltip/tooltip-provider.svelte +0 -7
  342. package/src/components/ui/tooltip/tooltip-trigger.svelte +0 -7
  343. package/src/components/ui/tooltip/tooltip.svelte +0 -7
  344. package/src/features/auth/account-menu.svelte +0 -159
  345. package/src/features/auth/account-security-controller.svelte.ts +0 -119
  346. package/src/features/collections/collection-list-bulk-actions.svelte +0 -143
  347. package/src/features/collections/collection-list-bulk-editor.svelte +0 -157
  348. package/src/features/collections/collection-list-filter-popover.svelte +0 -238
  349. package/src/features/collections/collection-list-workspace-toolbar.svelte +0 -409
  350. package/src/features/collections/collection-table-skeleton.svelte +0 -44
  351. package/src/features/documents/document-heading.ts +0 -20
  352. package/src/features/documents/upload-document-preview.svelte +0 -525
@@ -1,52 +1,23 @@
1
- <script lang="ts" module>
2
- import { resolveBlockTypes } from "@riducms/protocol";
3
-
4
- import type { SchemaField } from "@riducms/protocol";
5
-
6
- function fieldUsesLocalization(field: SchemaField): boolean {
7
- if (field.localized === true) return true;
8
- if (field.nested?.fields.some(fieldUsesLocalization)) return true;
9
- return (
10
- resolveBlockTypes(field.blocks).some((block) => block.fields.some(fieldUsesLocalization)) ===
11
- true
12
- );
13
- }
14
-
15
- function withContentLocale(path: string, locale: string | undefined) {
16
- if (locale === undefined) return path;
17
- return `${path}?${new URLSearchParams({ locale }).toString()}`;
18
- }
19
- </script>
20
-
21
1
  <script lang="ts">
22
2
  import { focusFieldIssue } from "@admin/core/forms/field-issue-focus";
23
3
  import type { AdminDocumentExtensionHost } from "@riducms/plugin";
24
4
  import { Link, useBlocker, useLocation, useNavigate, useParams } from "@hvniel/svelte-router";
25
- import { tick, untrack } from "svelte";
26
- import CopyIcon from "~icons/lucide/copy";
27
- import DownloadIcon from "~icons/lucide/download";
28
- import FileUpIcon from "~icons/lucide/file-up";
29
- import Trash2Icon from "~icons/lucide/trash-2";
30
- import KeyRoundIcon from "~icons/lucide/key-round";
31
- import MonitorUpIcon from "~icons/lucide/monitor-up";
32
- import MoreVerticalIcon from "~icons/lucide/more-vertical";
5
+ import { tick } from "svelte";
6
+ import type { Attachment } from "svelte/attachments";
33
7
  import LockKeyholeIcon from "~icons/lucide/lock-keyhole";
34
8
 
35
9
  import { Banner } from "@admin/components/ui/banner";
36
- import { Button, buttonVariants } from "@admin/components/ui/button";
37
10
  import {
11
+ Button,
38
12
  Dialog,
39
13
  DialogContent,
40
14
  DialogDescription,
41
15
  DialogFooter,
42
16
  DialogHeader,
43
17
  DialogTitle,
44
- } from "@admin/components/ui/dialog";
45
- import { Popover, PopoverContent, PopoverTrigger } from "@admin/components/ui/popover";
46
- import { Select, SelectContent, SelectItem, SelectTrigger } from "@admin/components/ui/select";
18
+ } from "@riducms/ui";
47
19
  import { Skeleton } from "@admin/components/ui/skeleton";
48
20
  import {
49
- collectionPath,
50
21
  createDocumentAPIPath,
51
22
  createDocumentPath,
52
23
  documentIDFromAdminPath,
@@ -56,41 +27,54 @@
56
27
  globalAPIPath,
57
28
  globalPath,
58
29
  globalVersionsPath,
30
+ withContentLocale,
59
31
  } from "@admin/core/routing/admin-paths";
60
32
  import { getNotificationCenter } from "@admin/core/notifications/notification-center.svelte";
33
+ import { registerAdminScrollPage } from "@admin/core/routing/admin-scroll.svelte";
34
+ import { MediaQuery } from "svelte/reactivity";
61
35
  import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
36
+ import { getAdminBootstrapCoordinator } from "@admin/core/bootstrap/admin-bootstrap";
37
+ import { fieldsUseLocalization } from "@admin/core/schema/field-localization";
62
38
  import { DocumentController } from "@admin/features/documents/document-controller.svelte";
63
39
  import {
64
40
  documentRouteIsDirty,
41
+ validateNewUserCredentials,
65
42
  saveAuthCreateWithClearedCredentials,
66
43
  type AuthCreateCredentials,
67
- } from "@admin/features/documents/document-route-dirty";
44
+ } from "@admin/features/documents/document-credentials";
68
45
  import LivePreviewPanel from "@admin/features/documents/live-preview-panel.svelte";
69
46
  import UploadDocumentPreview from "@admin/features/documents/upload-document-preview-loader.svelte";
70
47
  import DocumentFieldSections from "@admin/features/documents/document-field-sections.svelte";
71
48
  import DocumentAPIView from "@admin/features/documents/document-api-view-loader.svelte";
72
49
 
50
+ import { getBreadcrumbs } from "@admin/features/navigation/breadcrumb-context.svelte";
51
+ import DocumentActionBar from "@admin/features/documents/document-action-bar.svelte";
52
+ import DocumentOperationDialogs from "@admin/features/documents/document-operation-dialogs.svelte";
53
+ import DocumentCredentials from "@admin/features/documents/document-credentials.svelte";
54
+
55
+ import "@admin/features/documents/document.scss";
56
+
73
57
  const runtime = getAdminRuntime();
58
+ const bootstrap = getAdminBootstrapCoordinator();
74
59
  const notifications = getNotificationCenter();
75
60
  const navigate = useNavigate();
76
- const params = $derived(useParams<"collection" | "document" | "global">());
77
- const location = $derived(useLocation());
78
- const globalResource = $derived(params.global !== undefined);
79
- const slug = $derived(params.global ?? params.collection ?? "");
61
+ const params = useParams<"collection" | "document" | "global">();
62
+ const location = useLocation();
63
+ const preparedRoute = $derived(
64
+ bootstrap.routeData(location.current.pathname, location.current.search)
65
+ );
66
+ const globalResource = $derived(params.current.global !== undefined);
67
+ const slug = $derived(params.current.global ?? params.current.collection ?? "");
80
68
  let routeDocumentID = $state(
81
- untrack(() =>
82
- params.document === undefined ? undefined : documentIDFromAdminPath(location.pathname)
83
- )
69
+ params.current.document === undefined
70
+ ? undefined
71
+ : documentIDFromAdminPath(location.current.pathname)
84
72
  );
85
- $effect(() => {
86
- if (params.document === undefined) {
87
- routeDocumentID = undefined;
88
- return;
89
- }
90
- const decoded = documentIDFromAdminPath(location.pathname);
91
- // A parent route can remain mounted for one render after navigation has
92
- // moved to the collection list. Retain its exact ID until unmount so the
93
- // document controller does not transiently enter create mode.
73
+ $effect.pre(() => {
74
+ if (params.current.document === undefined) return;
75
+ const decoded = documentIDFromAdminPath(location.current.pathname);
76
+ // Parent route params can clear one render before unmount. Retain the exact
77
+ // document owner so the outgoing editor cannot transiently enter create mode.
94
78
  if (decoded !== undefined) routeDocumentID = decoded;
95
79
  });
96
80
  const localization = $derived(runtime.manifest?.application.localization);
@@ -99,19 +83,16 @@
99
83
  (candidate) => candidate.slug === slug
100
84
  )
101
85
  );
102
- const localeEnabled = $derived(resourceSchema?.fields.some(fieldUsesLocalization) === true);
103
- const requestedLocale = $derived(new URLSearchParams(location.search).get("locale"));
104
- const activeLocale = $derived(
105
- localization?.locales.some((locale) => locale.code === requestedLocale)
106
- ? (requestedLocale ?? localization?.defaultLocale)
107
- : (runtime.contentLocale ?? localization?.defaultLocale)
108
- );
86
+ const localeEnabled = $derived(fieldsUseLocalization(resourceSchema?.fields ?? []));
87
+ const requestedLocale = $derived(new URLSearchParams(location.current.search).get("locale"));
88
+ const activeLocale = $derived(runtime.resolveContentLocale(requestedLocale));
109
89
  const activeLocaleConfig = $derived(
110
90
  localeEnabled ? localization?.locales.find((locale) => locale.code === activeLocale) : undefined
111
91
  );
112
92
  const routeDocumentView = $derived<"edit" | "api">(
113
- location.pathname.endsWith("/api") ? "api" : "edit"
93
+ location.current.pathname.endsWith("/api") ? "api" : "edit"
114
94
  );
95
+ let documentView = $derived<string>(routeDocumentView);
115
96
  const editPath = $derived(
116
97
  withContentLocale(
117
98
  globalResource
@@ -151,81 +132,30 @@
151
132
  get global() {
152
133
  return globalResource;
153
134
  },
154
- get view() {
155
- return routeDocumentView;
156
- },
157
135
  get locale() {
158
136
  return activeLocale;
159
137
  },
138
+ get editable() {
139
+ return routeDocumentView === "edit";
140
+ },
141
+ get prepared() {
142
+ if (preparedRoute?.kind === "collection-create") return preparedRoute.create;
143
+ if (
144
+ preparedRoute?.kind === "collection-document" ||
145
+ preparedRoute?.kind === "global-document" ||
146
+ preparedRoute?.kind === "collection-api" ||
147
+ preparedRoute?.kind === "global-api"
148
+ )
149
+ return preparedRoute.document;
150
+ return undefined;
151
+ },
160
152
  });
161
153
  const form = controller.form;
162
- const {
163
- assetFilename,
164
- assetURL,
165
- canDelete,
166
- canDuplicate,
167
- canEditImage,
168
- canPublish,
169
- canReadVersions,
170
- canSave,
171
- canTakeOverLock,
172
- canUnpublish,
173
- collection,
174
- collectionSlug,
175
- copiedAssetURL,
176
- creating,
177
- currentDocument,
178
- currentStatus,
179
- draftsCollection,
180
- documentFields,
181
- documentHeading,
182
- documentID,
183
- documentView,
184
- duplicateOperation,
185
- error,
186
- imageOperation,
187
- imageOutcomeUncertain,
188
- loading,
189
- lockedByAnotherEditor,
190
- hasUnsavedChanges,
191
- lockOperation,
192
- lockOwnerLabel,
193
- lockUpdatedAt,
194
- pendingRestore,
195
- saveOutcomeUncertain,
196
- selectedFile,
197
- uploadCollection,
198
- versionOperation,
199
- versionedCollection,
200
- versions,
201
- } = $derived(controller);
202
- const {
203
- changePublication,
204
- copyAssetURL,
205
- duplicate,
206
- documentDate,
207
- remove,
208
- requestRestore,
209
- restore,
210
- save,
211
- takeOverLock,
212
- updateUploadImage,
213
- versionDate,
214
- } = controller;
215
- const customDocumentActions = $derived(
216
- globalResource
217
- ? []
218
- : runtime.documentActions.filter(
219
- (action) =>
220
- (action.collection === undefined || action.collection === collectionSlug) &&
221
- (action.requires === undefined || form.access?.operations[action.requires] === true)
222
- )
223
- );
224
154
  const customDocumentViews = $derived(
225
- currentDocument === undefined
155
+ controller.currentDocument === undefined
226
156
  ? []
227
- : runtime.documentViews.filter(
228
- (view) => view.collection === undefined || view.collection === collectionSlug
157
+ : runtime.config.extensions.documentViews.filter(
158
+ (view) => view.collection === undefined || view.collection === controller.collectionSlug
229
159
  )
230
160
  );
231
161
  const customDocumentView = $derived(
@@ -235,65 +165,65 @@
235
165
  refresh: controller.refresh,
236
166
  notify: (tone, title, message) => notifications[tone]({ title, message }),
237
167
  };
238
- let unlockOperation = $state(false);
239
- let newUserPassword = $state("");
240
- let newUserPasswordConfirmation = $state("");
168
+ let credentials = $state<AuthCreateCredentials>({ password: "", passwordConfirmation: "" });
241
169
  let credentialIssue = $state<string>();
242
170
  let livePreviewOpen = $state(false);
243
- let moreActionsOpen = $state(false);
244
- let copyLocaleOperation = $state(false);
171
+ let viewport: HTMLElement | null = null;
172
+ let fieldsViewport: HTMLElement | null = null;
173
+ const attachViewport: Attachment<HTMLElement> = (element) => {
174
+ viewport = element;
175
+ return () => {
176
+ if (viewport === element) viewport = null;
177
+ };
178
+ };
179
+ const attachFieldsViewport: Attachment<HTMLElement> = (element) => {
180
+ fieldsViewport = element;
181
+ return () => {
182
+ if (fieldsViewport === element) fieldsViewport = null;
183
+ };
184
+ };
185
+ const splitPreview = new MediaQuery("(min-width: 1240px)");
186
+ registerAdminScrollPage({
187
+ ready: () => !controller.loading,
188
+ target: () =>
189
+ documentView !== "api" && livePreviewOpen && splitPreview.current ? fieldsViewport : viewport,
190
+ });
245
191
  const livePreviewRouteKey = $derived(
246
- `${globalResource ? "global" : "collection"}:${collectionSlug}:${documentID ?? collectionSlug}`
192
+ `${globalResource ? "global" : "collection"}:${controller.collectionSlug}:${controller.documentID ?? controller.collectionSlug}`
247
193
  );
248
194
  let previousLivePreviewRouteKey: string | undefined;
249
- let dismissedLock = $state<string>();
250
- const livePreview = $derived(collection?.admin.livePreview);
195
+ const livePreview = $derived(controller.collection?.admin.livePreview);
251
196
  $effect(() => {
252
- const state = location.state as { openLivePreview?: boolean } | null;
197
+ const state = location.current.state as { openLivePreview?: boolean } | null;
253
198
  if (routeDocumentView !== "edit" || state?.openLivePreview !== true) return;
254
199
  livePreviewOpen = true;
255
200
  // API and edit views have separate route instances. Consume the transient
256
201
  // intent on arrival so Back or reload cannot reopen a dismissed preview.
257
202
  const { openLivePreview: _, ...remainingState } = state;
258
- navigate(`${location.pathname}${location.search}${location.hash}`, {
203
+ navigate(`${location.current.pathname}${location.current.search}${location.current.hash}`, {
259
204
  replace: true,
260
205
  state: remainingState,
261
206
  });
262
207
  });
263
- const creatingAuthUser = $derived(
264
- creating && !globalResource && collection?.capabilities.auth === true
265
- );
266
208
  const dirty = $derived(
267
209
  documentRouteIsDirty({
268
- documentValuesDirty: hasUnsavedChanges,
269
- creatingAuthUser,
270
- password: newUserPassword,
271
- passwordConfirmation: newUserPasswordConfirmation,
210
+ documentValuesDirty: controller.hasUnsavedChanges,
211
+ creatingAuthUser: controller.creatingAuthUser,
212
+ password: credentials.password,
213
+ passwordConfirmation: credentials.passwordConfirmation,
272
214
  })
273
215
  );
274
216
  const navigationBlocker = useBlocker(() => dirty || form.submitting);
275
- const canForceUnlock = $derived(
276
- !globalResource &&
277
- !creating &&
278
- collection?.capabilities.auth === true &&
279
- (collection.authSettings?.maxLoginAttempts ?? 0) > 0 &&
280
- form.access?.operations.update === true
281
- );
282
- $effect(() => {
283
- if (activeLocale === undefined || requestedLocale === activeLocale) return;
284
- const next = new URLSearchParams(location.search);
285
- next.set("locale", activeLocale);
286
- navigate(`${location.pathname}?${next.toString()}${location.hash}`, { replace: true });
287
- });
217
+
288
218
  $effect(() => {
289
219
  return runtime.registerContentLocaleBlocker(
290
220
  () =>
291
221
  dirty ||
292
222
  form.submitting ||
293
- controller.versionOperation ||
294
- controller.imageOperation ||
223
+ controller.publicationOperation ||
224
+ controller.upload.busy ||
295
225
  controller.duplicateOperation ||
296
- copyLocaleOperation
226
+ controller.copyLocaleOperation
297
227
  );
298
228
  });
299
229
  $effect(() => {
@@ -322,127 +252,69 @@
322
252
  }
323
253
  });
324
254
 
325
- async function forceUnlock() {
326
- if (collection === undefined || currentDocument === undefined) return;
327
- unlockOperation = true;
328
- try {
329
- await runtime.client.forceUnlock(collection.slug, currentDocument.id);
330
- notifications.success({ title: runtime.i18n.t("documents:accountUnlocked") });
331
- } catch (cause) {
332
- notifications.error({
333
- title: runtime.i18n.t("documents:accountUnlockFailed"),
334
- message: cause instanceof Error ? cause.message : undefined,
335
- });
336
- } finally {
337
- unlockOperation = false;
338
- }
339
- }
340
-
341
255
  function toggleLivePreview() {
342
256
  if (!livePreviewOpen && routeDocumentView === "api") {
343
257
  navigate(editPath, { replace: true, state: { openLivePreview: true } });
344
258
  return;
345
259
  }
346
260
  livePreviewOpen = !livePreviewOpen;
347
- if (livePreviewOpen) {
348
- controller.documentView = "edit";
349
- }
261
+ if (livePreviewOpen) documentView = "edit";
350
262
  }
351
263
 
352
264
  function selectCustomDocumentView(view: string) {
353
- controller.documentView = view;
265
+ documentView = view;
354
266
  if (routeDocumentView === "api") navigate(editPath, { replace: true });
355
267
  }
356
268
 
357
- async function copyFromLocale(source: string) {
358
- if (
359
- activeLocale === undefined ||
360
- source === activeLocale ||
361
- currentDocument === undefined ||
362
- copyLocaleOperation
363
- )
364
- return;
365
- copyLocaleOperation = true;
366
- try {
367
- if (globalResource) {
368
- await runtime.client.copyGlobalLocale(
369
- slug,
370
- { from: source, to: activeLocale },
371
- { revision: currentDocument._revision }
372
- );
373
- } else {
374
- await runtime.client.copyLocale(
375
- slug,
376
- currentDocument.id,
377
- { from: source, to: activeLocale },
378
- { revision: currentDocument._revision }
379
- );
380
- }
381
- await controller.refresh();
382
- runtime.documentsChanged();
383
- notifications.success({
384
- title: runtime.i18n.t("documents:localeCopied"),
385
- message: runtime.i18n.t("documents:localeCopiedDescription", {
386
- source,
387
- destination: activeLocale,
388
- }),
389
- });
390
- } catch (cause) {
391
- notifications.error({
392
- title: runtime.i18n.t("documents:localeNotCopied"),
393
- message:
394
- cause instanceof Error ? cause.message : runtime.i18n.t("documents:localeCopyFailed"),
395
- });
396
- } finally {
397
- copyLocaleOperation = false;
398
- }
399
- }
400
-
401
- async function handleTakeover() {
402
- await takeOverLock();
403
- }
404
-
405
- const lockKey = $derived(
406
- lockedByAnotherEditor
407
- ? `${collectionSlug}:${controller.documentID ?? ""}:${lockOwnerLabel ?? ""}:${lockUpdatedAt ?? ""}`
408
- : undefined
409
- );
410
-
411
269
  async function submitDocument(event: Event, publish: boolean) {
412
270
  event.preventDefault();
413
- if (creatingAuthUser) {
414
- credentialIssue = validateNewUserPassword();
271
+ if (controller.creatingAuthUser) {
272
+ credentialIssue = validateNewUserCredentials(
273
+ credentials,
274
+ controller.collection?.authSettings,
275
+ runtime.i18n
276
+ );
415
277
  if (credentialIssue !== undefined) {
416
278
  document.getElementById("ridu-new-user-password")?.focus();
417
279
  return;
418
280
  }
419
281
  }
420
- const saved = creatingAuthUser
282
+ const saved = controller.creatingAuthUser
421
283
  ? await saveAuthCreateWithClearedCredentials(
422
284
  {
423
- password: newUserPassword,
424
- passwordConfirmation: newUserPasswordConfirmation,
285
+ password: credentials.password,
286
+ passwordConfirmation: credentials.passwordConfirmation,
425
287
  },
426
288
  replaceNewUserCredentials,
427
- (password) => save({ password, publish })
289
+ (password) => controller.save({ password, publish })
428
290
  )
429
- : await save({ publish });
291
+ : await controller.save({ publish });
430
292
  if (saved) {
431
293
  resetNewUserCredentials();
432
294
  return;
433
295
  }
434
- const issuePath = controller.revealFirstIssue();
296
+ const issuePath = form.issues[0]?.path;
435
297
  if (issuePath === undefined) return;
298
+ documentView = "edit";
436
299
  await tick();
437
- await focusFieldIssue(issuePath);
300
+ if (fieldsViewport) await focusFieldIssue(issuePath, fieldsViewport);
438
301
  }
439
302
 
440
- async function handleSave(event: Event) {
441
- await submitDocument(event, !creating && versionedCollection && currentStatus === "published");
303
+ function handleSave(event: Event) {
304
+ return submitDocument(
305
+ event,
306
+ !controller.creating &&
307
+ controller.versionedCollection &&
308
+ controller.currentStatus === "published"
309
+ );
442
310
  }
443
311
 
444
- async function handlePublish(event: Event) {
445
- await submitDocument(event, true);
312
+ function handlePublish(event: Event) {
313
+ if (!controller.creating && controller.currentStatus === "draft" && !dirty) {
314
+ event.preventDefault();
315
+ return controller.changePublication("published");
316
+ }
317
+ return submitDocument(event, true);
446
318
  }
447
319
 
448
320
  function cancelNavigation() {
@@ -456,512 +328,254 @@
456
328
  navigationBlocker.proceed();
457
329
  }
458
330
 
331
+ const breadcrumbs = getBreadcrumbs();
332
+ $effect(() => {
333
+ if (!breadcrumbs) return;
334
+ const entry = { pathname: location.current.pathname, label: controller.documentHeading };
335
+ breadcrumbs.document = entry;
336
+ return () => {
337
+ if (breadcrumbs.document === entry) breadcrumbs.document = undefined;
338
+ };
339
+ });
340
+
459
341
  function resetNewUserCredentials() {
460
342
  replaceNewUserCredentials({ password: "", passwordConfirmation: "" });
461
343
  }
462
344
 
463
- function replaceNewUserCredentials(credentials: AuthCreateCredentials) {
464
- newUserPassword = credentials.password;
465
- newUserPasswordConfirmation = credentials.passwordConfirmation;
345
+ function replaceNewUserCredentials(next: AuthCreateCredentials) {
346
+ credentials = next;
466
347
  credentialIssue = undefined;
467
348
  }
468
-
469
- function validateNewUserPassword() {
470
- if (!creatingAuthUser || collection?.authSettings === undefined) return undefined;
471
- if (newUserPassword === "") return runtime.i18n.t("documents:enterNewAccountPassword");
472
- if (Array.from(newUserPassword).length < collection.authSettings.passwordMinLength) {
473
- return runtime.i18n.t("documents:passwordTooShort", {
474
- minimum: runtime.i18n.formatNumber(collection.authSettings.passwordMinLength),
475
- });
476
- }
477
- if (
478
- new TextEncoder().encode(newUserPassword).length > collection.authSettings.passwordMaxBytes
479
- ) {
480
- return runtime.i18n.t("documents:passwordTooLong", {
481
- maximum: runtime.i18n.formatNumber(collection.authSettings.passwordMaxBytes),
482
- });
483
- }
484
- if (newUserPassword !== newUserPasswordConfirmation)
485
- return runtime.i18n.t("documents:passwordsDoNotMatch");
486
- return undefined;
487
- }
488
349
  </script>
489
350
 
490
351
  <section
491
- class="flex min-h-screen flex-col md:h-full md:min-h-0 md:overflow-x-hidden md:overflow-y-auto"
352
+ {@attach attachViewport}
353
+ data-slot="document-viewport"
354
+ class={["ridu-document", documentView === "api" && "ridu-document--api"]}
492
355
  >
493
- <header class="shrink-0 bg-background">
494
- <div
495
- class="flex min-h-[88px] flex-col justify-center gap-3 px-5 py-3 sm:px-8 lg:flex-row lg:items-center lg:justify-between lg:px-[60px]"
496
- >
356
+ <header class="ridu-document-header">
357
+ <div class="ridu-document-heading">
497
358
  <h1
498
- class="text-[30px] font-semibold leading-tight tracking-[-0.025em] text-foreground-strong"
499
- aria-busy={loading && currentDocument === undefined && !creating}
359
+ class="ridu-document-title"
360
+ aria-busy={controller.loading &&
361
+ controller.currentDocument === undefined &&
362
+ !controller.creating}
500
363
  >
501
- {documentHeading}
364
+ {controller.documentHeading}
502
365
  </h1>
503
- <nav
504
- class="flex min-w-0 shrink-0 items-center gap-5 overflow-x-auto lg:ms-auto"
505
- aria-label={runtime.i18n.t("documents:documentViews")}
506
- >
507
- <Link
508
- to={editPath}
509
- class={[
510
- "mt-2 flex h-8 items-center px-3 text-[12.5px] text-foreground-muted lg:mt-0",
511
- documentView === "edit" && "bg-control text-foreground-strong",
512
- ]}
513
- onclick={() => (controller.documentView = "edit")}
514
- >
515
- {runtime.i18n.t("documents:edit")}
516
- </Link>
517
- <Link
518
- to={apiPath}
519
- class={[
520
- "mt-2 flex h-8 items-center px-3 text-[12.5px] text-foreground-muted lg:mt-0",
521
- documentView === "api" && "bg-control text-foreground-strong",
522
- ]}
523
- >
524
- {runtime.i18n.t("documents:api")}
525
- </Link>
526
- {#each customDocumentViews as view (view.key)}<button
527
- type="button"
528
- class={[
529
- "mt-2 h-8 shrink-0 px-3 text-[12.5px] text-foreground-muted lg:mt-0",
530
- documentView === view.key && "bg-control text-foreground-strong",
531
- ]}
532
- onclick={() => selectCustomDocumentView(view.key)}
366
+ {#if !controller.creating || routeDocumentView === "api"}
367
+ <nav class="ridu-document-tabs" aria-label={runtime.i18n.t("documents:documentViews")}>
368
+ <Link
369
+ to={editPath}
370
+ class={["ridu-document-tab", documentView === "edit" && "ridu-document-tab--active"]}
371
+ onclick={() => (documentView = "edit")}
533
372
  >
534
- {view.labelKey === undefined ? view.label : runtime.i18n.t(view.labelKey)}
535
- </button>{/each}
536
- {#if !creating && versionedCollection && canReadVersions}<Link
537
- class="mt-2 flex h-8 items-center px-3 text-[12.5px] text-foreground-muted hover:text-foreground-strong lg:mt-0"
538
- to={versionHistoryPath}
373
+ {runtime.i18n.t("documents:edit")}
374
+ </Link>
375
+
376
+ {#each customDocumentViews as view (view.key)}
377
+ <button
378
+ type="button"
379
+ class={[
380
+ "ridu-document-tab",
381
+ documentView === view.key && "ridu-document-tab--active",
382
+ ]}
383
+ onclick={() => selectCustomDocumentView(view.key)}
384
+ >
385
+ {view.labelKey === undefined ? view.label : runtime.i18n.t(view.labelKey)}
386
+ </button>
387
+ {/each}
388
+ {#if !controller.creating && controller.versionedCollection && controller.canReadVersions}
389
+ <Link class="ridu-document-tab" to={versionHistoryPath}>
390
+ {runtime.i18n.t("documents:versions")}
391
+ </Link>
392
+ {/if}
393
+ <Link
394
+ to={apiPath}
395
+ class={["ridu-document-tab", documentView === "api" && "ridu-document-tab--active"]}
539
396
  >
540
- {runtime.i18n.t("documents:versions")}
541
- </Link>{/if}
542
- </nav>
397
+ {runtime.i18n.t("documents:api")}
398
+ </Link>
399
+ </nav>
400
+ {/if}
543
401
  </div>
402
+ {#if documentView !== "api" && controller.collection?.admin.description}
403
+ <p class="ridu-document-description">
404
+ {runtime.i18n.text(
405
+ controller.collection.admin.description,
406
+ controller.collection.admin.descriptionTranslations
407
+ )}
408
+ </p>
409
+ {/if}
544
410
  </header>
545
411
 
412
+ {#if documentView !== "api"}
413
+ <!-- Editor-owned overlays and extension actions must not survive a route or locale identity change. -->
414
+ {#key `${livePreviewRouteKey}:${activeLocale ?? ""}`}
415
+ <DocumentActionBar
416
+ {controller}
417
+ {extensionHost}
418
+ {localeEnabled}
419
+ {activeLocale}
420
+ {livePreviewOpen}
421
+ onTogglePreview={toggleLivePreview}
422
+ onSave={handleSave}
423
+ onPublish={handlePublish}
424
+ />
425
+ {/key}
426
+ {/if}
427
+
546
428
  <div
547
- class="relative sticky top-[-3.5rem] z-20 shrink-0 border-y border-control-border bg-background/96 backdrop-blur-md before:pointer-events-none before:absolute before:top-[-1px] before:end-[-1rem] before:h-px before:w-4 before:bg-control-border after:pointer-events-none after:absolute after:end-[-1rem] after:bottom-[-1px] after:h-px after:w-4 after:bg-control-border xl:top-0 xl:grid xl:grid-cols-1"
429
+ class={[
430
+ "ridu-document-body",
431
+ { "ridu-document-body--preview": documentView !== "api" && livePreviewOpen },
432
+ ]}
548
433
  >
549
434
  <div
550
- class="flex min-h-14 flex-wrap items-center gap-x-10 gap-y-1 px-5 py-2 text-[11.5px] text-foreground-faint sm:px-8 lg:px-[60px] xl:col-start-1 xl:row-start-1 xl:pe-[620px]"
551
- >
552
- {#if localeEnabled && activeLocale !== undefined && localization !== undefined && !creating && currentDocument !== undefined && localization.locales.length > 1}
553
- <div class="flex items-center gap-2.5">
554
- <Select
555
- type="single"
556
- value=""
557
- disabled={form.dirty || loading || form.submitting || copyLocaleOperation}
558
- onValueChange={copyFromLocale}
559
- >
560
- <SelectTrigger
561
- size="compact"
562
- class="min-w-28"
563
- aria-label={runtime.i18n.t("documents:copyFromLocale")}
564
- >
565
- <span>
566
- {copyLocaleOperation
567
- ? runtime.i18n.t("documents:copying")
568
- : runtime.i18n.t("documents:copyFrom")}
569
- </span>
570
- </SelectTrigger>
571
- <SelectContent>
572
- {#each localization.locales.filter((locale) => locale.code !== activeLocale) as locale (locale.code)}
573
- <SelectItem value={locale.code} label={locale.label}>
574
- <span>{locale.label}</span>
575
- <span class="ms-1 text-foreground-faint">{locale.code}</span>
576
- </SelectItem>
577
- {/each}
578
- </SelectContent>
579
- </Select>
580
- </div>
581
- {/if}
582
- <span>
583
- {runtime.i18n.t("documents:status")}:
584
- <strong class="font-semibold text-foreground-strong">
585
- {versionedCollection
586
- ? runtime.i18n.t(currentStatus === "draft" ? "documents:draft" : "documents:published")
587
- : creating
588
- ? runtime.i18n.t("documents:draft")
589
- : runtime.i18n.t("documents:saved")}
590
- </strong>
591
- </span>
592
- {#if !creating && currentDocument !== undefined}
593
- <span class="hidden sm:inline">
594
- {runtime.i18n.t("documents:lastModifiedLabel")}:
595
- <strong class="font-semibold text-foreground-strong">
596
- {documentDate(currentDocument.updatedAt)}
597
- </strong>
598
- </span>
599
- <span class="hidden md:inline">
600
- {runtime.i18n.t("documents:createdLabelPlain")}:
601
- <strong class="font-semibold text-foreground-strong">
602
- {documentDate(currentDocument.createdAt)}
603
- </strong>
604
- </span>
605
- {/if}
606
- </div>
607
-
608
- <div
609
- class="flex min-h-12 items-center gap-2 border-t border-control-border bg-background px-5 sm:px-8 lg:px-[60px] xl:col-start-1 xl:row-start-1 xl:min-h-14 xl:w-fit xl:justify-self-end xl:border-t-0 xl:pe-[60px] xl:ps-0"
435
+ {@attach attachFieldsViewport}
436
+ data-slot="document-fields-viewport"
437
+ class="ridu-document-editor"
610
438
  >
611
- {#if !creating && currentDocument !== undefined && livePreview !== undefined}
612
- <Button
613
- variant="outline"
614
- size="icon-sm"
615
- onclick={toggleLivePreview}
616
- aria-label={runtime.i18n.t("documents:livePreview")}
617
- aria-pressed={livePreviewOpen}
618
- >
619
- <MonitorUpIcon class="size-3.5" aria-hidden="true" />
620
- </Button>
621
- {/if}
622
- {#if canTakeOverLock}<Button
623
- variant="outline"
624
- size="sm"
625
- disabled={lockOperation}
626
- onclick={handleTakeover}
627
- >
628
- <LockKeyholeIcon class="size-3.5" aria-hidden="true" />{lockOperation
629
- ? runtime.i18n.t("documents:takingOver")
630
- : runtime.i18n.t("documents:takeOver")}
631
- </Button>{/if}
632
- {#if creating && draftsCollection && canPublish && !uploadCollection && !creatingAuthUser}
633
- <Button
634
- variant="outline"
635
- size="sm"
636
- disabled={!canSave}
637
- aria-busy={form.submitting}
638
- onclick={handleSave}
639
- >
640
- {runtime.i18n.t("documents:saveDraft")}
641
- </Button>
642
- <Button size="sm" disabled={!canSave} aria-busy={form.submitting} onclick={handlePublish}>
643
- {runtime.i18n.t("documents:publish")}
644
- </Button>
645
- {:else if creating && versionedCollection && !draftsCollection}
646
- <Button
647
- size="sm"
648
- disabled={!canSave || !canPublish}
649
- aria-busy={form.submitting}
650
- onclick={handlePublish}
651
- >
652
- {runtime.i18n.t("documents:publish")}
653
- </Button>
654
- {:else}
655
- <Button
656
- size="sm"
657
- disabled={!canSave ||
658
- (versionedCollection && currentStatus === "published" && !canPublish)}
659
- aria-busy={form.submitting}
660
- onclick={handleSave}
661
- >
662
- {creating && draftsCollection
663
- ? runtime.i18n.t("documents:saveDraft")
664
- : versionedCollection
665
- ? currentStatus === "published"
666
- ? runtime.i18n.t("documents:publishChanges")
667
- : runtime.i18n.t("documents:saveDraft")
668
- : runtime.i18n.t("documents:save")}
669
- </Button>
670
- {/if}
671
- {#if !creating && versionedCollection && currentStatus === "draft" && canPublish}
672
- <Button
673
- variant="outline"
674
- size="sm"
675
- disabled={versionOperation || form.dirty || form.submitting}
676
- onclick={() => changePublication("published")}
677
- >
678
- {runtime.i18n.t("documents:publish")}
679
- </Button>
680
- {/if}
681
- <div class="ms-auto flex items-center gap-2">
682
- {#if collection !== undefined && currentDocument !== undefined && !lockedByAnotherEditor}{#each customDocumentActions as action (action.key)}<action.component
683
- {collection}
684
- document={currentDocument}
685
- host={extensionHost}
686
- i18n={runtime.i18n}
687
- />{/each}{/if}
688
- {#if (!globalResource && !creating && canDuplicate) || (!globalResource && !creating && uploadCollection && assetURL !== undefined) || (!creating && versionedCollection && currentStatus === "published" && canUnpublish) || (!globalResource && !creating && canDelete) || canForceUnlock}
689
- <Popover bind:open={moreActionsOpen}>
690
- <PopoverTrigger
691
- class={buttonVariants({ variant: "outline", size: "sm", class: "size-9 p-0" })}
692
- aria-label={runtime.i18n.t("documents:moreActions")}
439
+ <div class="ridu-document-fields">
440
+ {#if controller.lock.error !== undefined}
441
+ <Banner class="ridu-document-notice" tone="destructive">
442
+ <span class="ridu-document-notice__message">{controller.lock.error}</span>
443
+ <Button
444
+ type="button"
445
+ variant="outline"
446
+ size="xs"
447
+ disabled={controller.lock.operation}
448
+ onclick={controller.lock.retry}
693
449
  >
694
- <MoreVerticalIcon class="size-4" aria-hidden="true" />
695
- </PopoverTrigger>
696
- <PopoverContent align="end" class="w-52 gap-1 p-1.5">
697
- {#if !globalResource && !creating && canDuplicate}<Button
698
- variant="ghost"
699
- class="w-full justify-start"
700
- disabled={duplicateOperation}
701
- onclick={() => {
702
- moreActionsOpen = false;
703
- duplicate();
704
- }}
705
- >
706
- <CopyIcon class="size-3.5" />{duplicateOperation
707
- ? runtime.i18n.t("documents:duplicating")
708
- : runtime.i18n.t("documents:duplicate")}
709
- </Button>{/if}
710
- {#if !globalResource && !creating && uploadCollection && assetURL !== undefined}<Button
711
- variant="ghost"
712
- class="w-full justify-start"
713
- href={assetURL}
714
- download={assetFilename}
715
- >
716
- <DownloadIcon class="size-3.5" />
717
- {runtime.i18n.t("documents:download")}
718
- </Button>{/if}
719
- {#if !creating && versionedCollection && currentStatus === "published" && canUnpublish}<Button
720
- variant="ghost"
721
- class="w-full justify-start"
722
- disabled={versionOperation || form.dirty || form.submitting}
723
- onclick={() => {
724
- moreActionsOpen = false;
725
- changePublication("draft");
726
- }}
727
- >
728
- {runtime.i18n.t("documents:unpublish")}
729
- </Button>{/if}
730
- {#if !globalResource && !creating && canDelete}<Button
731
- variant="ghost"
732
- class="w-full justify-start text-destructive hover:bg-destructive/7 hover:text-destructive"
733
- onclick={() => {
734
- moreActionsOpen = false;
735
- controller.deleteDialogOpen = true;
736
- }}
737
- >
738
- <Trash2Icon class="size-3.5" />
739
- {runtime.i18n.t(
740
- uploadCollection ? "documents:deleteAsset" : "documents:deleteDocument"
741
- )}
742
- </Button>{/if}
743
- {#if canForceUnlock}<Button
744
- variant="ghost"
745
- class="w-full justify-start"
746
- disabled={unlockOperation}
747
- onclick={() => {
748
- moreActionsOpen = false;
749
- forceUnlock();
750
- }}
751
- >
752
- <KeyRoundIcon class="size-3.5" aria-hidden="true" />{unlockOperation
753
- ? runtime.i18n.t("documents:unlocking")
754
- : runtime.i18n.t("documents:forceUnlock")}
755
- </Button>{/if}
756
- </PopoverContent>
757
- </Popover>
450
+ {runtime.i18n.t("general:retry")}
451
+ </Button>
452
+ </Banner>
758
453
  {/if}
759
- </div>
760
- </div>
761
- </div>
762
-
763
- <div
764
- class={[
765
- "flex min-h-0 flex-1 flex-col",
766
- livePreviewOpen && "min-[1240px]:flex-row min-[1240px]:overflow-hidden",
767
- ]}
768
- >
769
- <div class={["min-w-0 flex-1", livePreviewOpen && "min-[1240px]:overflow-y-auto"]}>
770
- <div class="w-full px-5 py-8 sm:px-8 sm:py-9 lg:px-[60px]">
771
- {#if lockedByAnotherEditor}
772
- <Banner class="mb-6" tone="warning">
773
- <LockKeyholeIcon class="mt-0.5 size-4 shrink-0" aria-hidden="true" />
454
+ {#if controller.lock.lockedByAnotherEditor}
455
+ <Banner class="ridu-document-notice" tone="warning">
456
+ <LockKeyholeIcon class="ridu-document-notice__icon" aria-hidden="true" />
774
457
  <span>
775
458
  {runtime.i18n.t("documents:lockedReadOnly", {
776
- owner: lockOwnerLabel ?? runtime.i18n.t("documents:anotherEditor"),
459
+ owner: controller.lock.ownerLabel ?? runtime.i18n.t("documents:anotherEditor"),
777
460
  })}
778
461
  </span>
779
462
  </Banner>
780
463
  {/if}
781
- {#if error !== undefined}
782
- <Banner class="mb-6" tone="destructive">
783
- <span class="size-2 shrink-0 rounded-full bg-destructive" aria-hidden="true"></span>
784
- {error}
785
- </Banner>
786
- {/if}
787
-
788
- {#if saveOutcomeUncertain}
789
- <Banner class="mb-6" tone="warning">
790
- {runtime.i18n.t("uploads:outcomeUnknownDescription")}
464
+ {#if documentView !== "api" && controller.error !== undefined}
465
+ <Banner class="ridu-document-notice" tone="destructive">
466
+ <span class="ridu-document-notice__error-marker" aria-hidden="true"></span>
467
+ {controller.error}
791
468
  </Banner>
792
469
  {/if}
793
470
 
794
- {#if imageOutcomeUncertain}
795
- <Banner class="mb-6" tone="warning">
796
- {runtime.i18n.t("uploads:imageOutcomeUnknownDescription")}
471
+ {#if controller.saveOutcomeUncertain}
472
+ <Banner class="ridu-document-notice" tone="warning">
473
+ {runtime.i18n.t(
474
+ controller.creating
475
+ ? "uploads:outcomeUnknownDescription"
476
+ : "uploads:saveOutcomeUnknownDescription"
477
+ )}
478
+ {#if !controller.creating}
479
+ <Button
480
+ variant="outline"
481
+ size="sm"
482
+ disabled={controller.loading}
483
+ onclick={controller.refresh}
484
+ >
485
+ {runtime.i18n.t("uploads:reloadSaved")}
486
+ </Button>
487
+ {/if}
797
488
  </Banner>
798
489
  {/if}
799
490
 
800
- {#if loading}
801
- <div class="grid gap-5" aria-label={runtime.i18n.t("documents:loadingDocument")}>
802
- <Skeleton class="h-12 w-2/3" />
803
- <Skeleton class="mt-5 h-10.5" />
804
- <Skeleton class="h-28" />
805
- </div>
806
- {:else if !creating && currentDocument === undefined}
807
- <!-- A failed read has no editable document; the recovery/error banner remains visible. -->
808
- {:else if documentView === "api"}
491
+ {#if documentView === "api"}
809
492
  <DocumentAPIView
810
493
  resourceSlug={slug}
811
- resourceLabel={collection === undefined
812
- ? runtime.i18n.t("documents:document")
813
- : runtime.i18n.text(
814
- collection.labels.singular,
815
- collection.labels.singularTranslations
816
- )}
817
494
  documentID={routeDocumentID}
818
495
  {globalResource}
496
+ contentLocale={activeLocale}
819
497
  fallbackValue={form.values}
498
+ prepared={preparedRoute?.kind === "collection-api" ||
499
+ preparedRoute?.kind === "global-api"
500
+ ? preparedRoute.document.document
501
+ : undefined}
820
502
  />
821
- {:else if customDocumentView !== undefined && collection !== undefined && currentDocument !== undefined}
503
+ {:else if controller.loading}
504
+ <div
505
+ class="ridu-document-loading"
506
+ data-ridu-loading-surface="document"
507
+ aria-label={runtime.i18n.t("documents:loadingDocument")}
508
+ >
509
+ <Skeleton class="ridu-document-loading__heading" />
510
+ <Skeleton class="ridu-document-loading__input" />
511
+ <Skeleton class="ridu-document-loading__textarea" />
512
+ </div>
513
+ {:else if !controller.creating && controller.currentDocument === undefined}
514
+ <!-- A failed read has no editable document; the recovery/error banner remains visible. -->
515
+ {:else if customDocumentView !== undefined && controller.collection !== undefined && controller.currentDocument !== undefined}
822
516
  <customDocumentView.component
823
- {collection}
824
- document={currentDocument}
517
+ collection={controller.collection}
518
+ document={controller.currentDocument}
825
519
  host={extensionHost}
826
520
  i18n={runtime.i18n}
827
521
  />
828
- {:else if uploadCollection && !creating && currentDocument !== undefined}
829
- {#key currentDocument.id}
830
- <UploadDocumentPreview
831
- document={currentDocument}
832
- editable={canEditImage}
833
- updating={imageOperation}
834
- onUpdate={updateUploadImage}
835
- />
836
- {/key}
837
-
838
- <form class="mt-8" dir={activeLocaleConfig?.rtl ? "rtl" : "ltr"} onsubmit={handleSave}>
839
- <fieldset class="contents" disabled={form.submitting || imageOperation}>
840
- <DocumentFieldSections fields={documentFields} {form} stacked={livePreviewOpen} />
841
- <button class="sr-only" type="submit">{runtime.i18n.t("documents:save")}</button>
842
- </fieldset>
843
- </form>
844
522
  {:else}
845
523
  <form
846
- class="grid gap-y-7"
524
+ class="ridu-document-form"
847
525
  dir={activeLocaleConfig?.rtl ? "rtl" : "ltr"}
526
+ novalidate
848
527
  onsubmit={handleSave}
849
528
  >
850
- <fieldset class="contents" disabled={form.submitting}>
851
- {#if creating && uploadCollection}
852
- <div class="grid gap-2">
853
- <div class="flex items-baseline justify-between gap-3">
854
- <span class="ridu-field-label">
855
- {runtime.i18n.t("uploads:file")}
856
- <span class="ridu-field-required" aria-hidden="true">*</span>
857
- </span>
858
- </div>
859
- <label
860
- class="grid min-h-42 cursor-pointer place-items-center rounded-[4px] border border-dashed border-control-border bg-control px-5 py-7 text-center transition-colors hover:border-primary/55 hover:bg-primary/[0.035]"
861
- >
862
- <input
863
- class="sr-only"
864
- type="file"
865
- accept={collection?.uploadSettings?.mimeTypes.join(",")}
866
- required
867
- disabled={form.access?.operations.create !== true}
868
- bind:files={controller.selectedFiles}
869
- />
870
- <span>
871
- <FileUpIcon class="mx-auto size-5 text-foreground-faint" />
872
- <span class="mt-3 block text-[13.5px] font-medium text-foreground-muted">
873
- {selectedFile?.name ?? runtime.i18n.t("uploads:chooseFileToUpload")}
874
- </span>
875
- <span class="mt-1 block text-[11.5px] text-foreground-faint">
876
- {runtime.i18n.t("uploads:maximumSize", {
877
- size: runtime.i18n.formatNumber(
878
- (collection?.uploadSettings?.maxFileSize ?? 0) / 1_048_576,
879
- { maximumFractionDigits: 0, style: "unit", unit: "megabyte" }
880
- ),
881
- })}
882
- </span>
883
- </span>
884
- </label>
885
- </div>
529
+ <fieldset class="ridu-document-form__fieldset" disabled={form.submitting}>
530
+ {#if controller.uploadCollection && controller.collection?.uploadSettings}
531
+ {#key JSON.stringify([globalResource, slug, routeDocumentID, activeLocale])}
532
+ <UploadDocumentPreview
533
+ draft={controller.upload}
534
+ settings={controller.collection.uploadSettings}
535
+ editable={controller.canEditUpload}
536
+ />
537
+ {/key}
886
538
  {/if}
887
539
 
888
- {#if creatingAuthUser}
889
- <fieldset
890
- class="grid gap-4 rounded-[4px] border border-control-border bg-control p-4.5"
891
- >
892
- <legend class="px-1 text-[13px] font-semibold text-foreground">
893
- {runtime.i18n.t("documents:credentials")}
894
- </legend>
895
- <p class="text-[11.5px] leading-5 text-foreground-faint">
896
- {runtime.i18n.t("documents:credentialsDescription")}
897
- </p>
898
- <div class="grid gap-4 sm:grid-cols-2">
899
- <label class="grid gap-2" for="ridu-new-user-password">
900
- <span class="ridu-field-label">
901
- {runtime.i18n.t("documents:password")}
902
- <span class="ridu-field-required" aria-hidden="true">*</span>
903
- </span>
904
- <input
905
- id="ridu-new-user-password"
906
- type="password"
907
- autocomplete="new-password"
908
- minlength={collection?.authSettings?.passwordMinLength}
909
- required
910
- aria-invalid={credentialIssue !== undefined}
911
- aria-describedby="ridu-new-user-password-help"
912
- bind:value={newUserPassword}
913
- oninput={() => (credentialIssue = undefined)}
914
- />
915
- </label>
916
- <label class="grid gap-2" for="ridu-new-user-password-confirmation">
917
- <span class="ridu-field-label">
918
- {runtime.i18n.t("documents:confirmPassword")}
919
- <span class="ridu-field-required" aria-hidden="true">*</span>
920
- </span>
921
- <input
922
- id="ridu-new-user-password-confirmation"
923
- type="password"
924
- autocomplete="new-password"
925
- required
926
- aria-invalid={credentialIssue !== undefined}
927
- aria-describedby="ridu-new-user-password-help"
928
- bind:value={newUserPasswordConfirmation}
929
- oninput={() => (credentialIssue = undefined)}
930
- />
931
- </label>
932
- </div>
933
- <p
934
- id="ridu-new-user-password-help"
935
- class={credentialIssue === undefined ? "ridu-field-help" : "ridu-field-error"}
936
- role={credentialIssue === undefined ? undefined : "alert"}
937
- >
938
- {credentialIssue ??
939
- runtime.i18n.t("documents:passwordMinimum", {
940
- minimum: runtime.i18n.formatNumber(
941
- collection?.authSettings?.passwordMinLength ?? 8
942
- ),
943
- })}
944
- </p>
945
- </fieldset>
540
+ {#if controller.creatingAuthUser}
541
+ <DocumentCredentials
542
+ bind:credentials
543
+ bind:issue={credentialIssue}
544
+ minimumLength={controller.collection?.authSettings?.passwordMinLength}
545
+ />
946
546
  {/if}
947
547
 
948
- <DocumentFieldSections fields={documentFields} {form} stacked={livePreviewOpen} />
949
- <button class="sr-only" type="submit">{runtime.i18n.t("documents:save")}</button>
548
+ <DocumentFieldSections
549
+ fields={controller.documentFields}
550
+ {form}
551
+ stacked={livePreviewOpen}
552
+ />
553
+ <button
554
+ class="ridu-document-form__submit"
555
+ type="submit"
556
+ aria-busy={form.submitting}
557
+ disabled={!controller.canSave ||
558
+ (!controller.creating &&
559
+ controller.versionedCollection &&
560
+ controller.currentStatus === "published" &&
561
+ !controller.canPublish)}
562
+ >
563
+ {runtime.i18n.t("documents:save")}
564
+ </button>
950
565
  </fieldset>
951
566
  </form>
952
567
  {/if}
953
568
  </div>
954
569
  </div>
955
570
 
956
- {#if livePreviewOpen && livePreview !== undefined && currentDocument !== undefined}
571
+ {#if documentView !== "api" && livePreviewOpen && livePreview !== undefined && controller.currentDocument !== undefined}
957
572
  {#key livePreviewRouteKey}
958
573
  <LivePreviewPanel
959
574
  preview={livePreview}
960
- collection={collectionSlug}
961
- documentID={documentID ?? collectionSlug}
575
+ collection={controller.collectionSlug}
576
+ documentID={controller.documentID ?? controller.collectionSlug}
962
577
  resource={globalResource ? "global" : "collection"}
963
578
  values={form.values}
964
- onclose={() => (livePreviewOpen = false)}
965
579
  />
966
580
  {/key}
967
581
  {/if}
@@ -974,15 +588,12 @@
974
588
  if (!open) cancelNavigation();
975
589
  }}
976
590
  >
977
- <DialogContent>
591
+ <DialogContent variant="confirmation">
978
592
  <DialogHeader>
979
- <p class="font-mono text-[10px] tracking-[0.13em] text-warning uppercase">
980
- {runtime.i18n.t("documents:unsavedChanges")}
981
- </p>
982
- <DialogTitle class="text-[28px] font-semibold leading-tight tracking-[-0.02em]">
593
+ <DialogTitle>
983
594
  {runtime.i18n.t("documents:leaveWithoutSavingQuestion")}
984
595
  </DialogTitle>
985
- <DialogDescription class="mt-1 text-[13.5px] leading-5 text-foreground-muted">
596
+ <DialogDescription>
986
597
  {runtime.i18n.t("documents:leaveWithoutSavingDescription")}
987
598
  </DialogDescription>
988
599
  </DialogHeader>
@@ -990,128 +601,11 @@
990
601
  <Button variant="outline" onclick={cancelNavigation}>
991
602
  {runtime.i18n.t("documents:keepEditing")}
992
603
  </Button>
993
- <Button variant="destructive" disabled={form.submitting} onclick={discardAndNavigate}>
604
+ <Button disabled={form.submitting} onclick={discardAndNavigate}>
994
605
  {runtime.i18n.t("documents:leaveWithoutSaving")}
995
606
  </Button>
996
607
  </DialogFooter>
997
608
  </DialogContent>
998
609
  </Dialog>
999
610
 
1000
- {#if !globalResource && canDelete}
1001
- {const deleteConfirmation = $derived(
1002
- collection?.capabilities.trash === true
1003
- ? {
1004
- title: runtime.i18n.t("documents:moveToTrashQuestion"),
1005
- description: runtime.i18n.t("documents:moveToTrashDescription", {
1006
- label: documentHeading,
1007
- }),
1008
- action: runtime.i18n.t("documents:moveToTrash"),
1009
- }
1010
- : {
1011
- title: runtime.i18n.t("documents:deleteQuestion"),
1012
- description: runtime.i18n.t("documents:deleteDescription", {
1013
- label: documentHeading,
1014
- }),
1015
- action: runtime.i18n.t("documents:deleteDocument"),
1016
- }
1017
- )}
1018
-
1019
- <Dialog bind:open={controller.deleteDialogOpen}>
1020
- <DialogContent>
1021
- <DialogHeader>
1022
- <p class="font-mono text-[10px] tracking-[0.13em] text-destructive uppercase">
1023
- {runtime.i18n.t("documents:destructiveAction")}
1024
- </p>
1025
- <DialogTitle class="text-[28px] font-semibold leading-tight tracking-[-0.02em]">
1026
- {deleteConfirmation.title}
1027
- </DialogTitle>
1028
- <DialogDescription class="mt-1 text-[13.5px] leading-5 text-foreground-muted">
1029
- {deleteConfirmation.description}
1030
- </DialogDescription>
1031
- </DialogHeader>
1032
- <DialogFooter>
1033
- <Button variant="outline" onclick={() => (controller.deleteDialogOpen = false)}>
1034
- {runtime.i18n.t("documents:cancel")}
1035
- </Button>
1036
- <Button variant="destructive" onclick={remove}>
1037
- {deleteConfirmation.action}
1038
- </Button>
1039
- </DialogFooter>
1040
- </DialogContent>
1041
- </Dialog>
1042
- {/if}
1043
-
1044
- <Dialog
1045
- open={lockKey !== undefined && dismissedLock !== lockKey}
1046
- onOpenChange={(open) => {
1047
- if (!open) dismissedLock = lockKey;
1048
- }}
1049
- >
1050
- <DialogContent>
1051
- <DialogHeader>
1052
- <p class="font-mono text-[10px] tracking-[0.13em] text-warning uppercase">
1053
- {runtime.i18n.t("documents:documentLocked")}
1054
- </p>
1055
- <DialogTitle class="text-[28px] font-semibold leading-tight tracking-[-0.02em]">
1056
- {runtime.i18n.t("documents:currentlyEditing", {
1057
- owner: lockOwnerLabel ?? runtime.i18n.t("documents:anotherEditor"),
1058
- })}
1059
- </DialogTitle>
1060
- <DialogDescription class="mt-1 text-[13.5px] leading-5 text-foreground-muted">
1061
- {lockUpdatedAt === undefined
1062
- ? runtime.i18n.t("documents:editingLeaseActive")
1063
- : runtime.i18n.t("documents:editedSince", { date: documentDate(lockUpdatedAt) })}
1064
- {canTakeOverLock
1065
- ? runtime.i18n.t("documents:lockOptionsWithTakeover")
1066
- : runtime.i18n.t("documents:lockOptionsReadOnly")}
1067
- </DialogDescription>
1068
- </DialogHeader>
1069
- <DialogFooter>
1070
- <Link class={buttonVariants({ variant: "ghost" })} to={collectionPath(collectionSlug)}>
1071
- {runtime.i18n.t("documents:goBack")}
1072
- </Link>
1073
- <Button variant="outline" onclick={() => (dismissedLock = lockKey)}>
1074
- {runtime.i18n.t("documents:viewReadOnly")}
1075
- </Button>
1076
- {#if canTakeOverLock}
1077
- <Button disabled={lockOperation} onclick={handleTakeover}>
1078
- {lockOperation
1079
- ? runtime.i18n.t("documents:takingOver")
1080
- : runtime.i18n.t("documents:takeOver")}
1081
- </Button>
1082
- {/if}
1083
- </DialogFooter>
1084
- </DialogContent>
1085
- </Dialog>
1086
-
1087
- <Dialog bind:open={controller.restoreDialogOpen}>
1088
- <DialogContent>
1089
- <DialogHeader>
1090
- <p class="font-mono text-[10px] tracking-[0.13em] text-warning uppercase">
1091
- {runtime.i18n.t("versions:restore")}
1092
- </p>
1093
- <DialogTitle class="text-[28px] font-semibold leading-tight tracking-[-0.02em]">
1094
- {runtime.i18n.t("versions:restoreRevisionQuestion", {
1095
- revision:
1096
- pendingRestore === undefined ? "—" : runtime.i18n.formatNumber(pendingRestore.Revision),
1097
- })}
1098
- </DialogTitle>
1099
- <DialogDescription class="mt-1 text-[13.5px] leading-5 text-foreground-muted">
1100
- {runtime.i18n.t("versions:restoreDescription")}
1101
- </DialogDescription>
1102
- </DialogHeader>
1103
- <DialogFooter>
1104
- <Button variant="outline" onclick={() => (controller.restoreDialogOpen = false)}>
1105
- {runtime.i18n.t("versions:cancel")}
1106
- </Button>
1107
- <Button
1108
- disabled={pendingRestore === undefined || versionOperation || !controller.canRestoreVersion}
1109
- onclick={() => pendingRestore !== undefined && restore(pendingRestore)}
1110
- >
1111
- {versionOperation
1112
- ? runtime.i18n.t("versions:restoring")
1113
- : runtime.i18n.t("versions:restore")}
1114
- </Button>
1115
- </DialogFooter>
1116
- </DialogContent>
1117
- </Dialog>
611
+ <DocumentOperationDialogs {controller} />