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