@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,844 @@
1
+ <script lang="ts">
2
+ import type { FieldReferenceBrowserProps } from "@riducms/plugin";
3
+ import { tick } from "svelte";
4
+ import ArrowLeftIcon from "~icons/lucide/arrow-left";
5
+ import ChevronLeftIcon from "~icons/lucide/chevron-left";
6
+ import ChevronRightIcon from "~icons/lucide/chevron-right";
7
+ import FileIcon from "~icons/lucide/file";
8
+ import FileUpIcon from "~icons/lucide/file-up";
9
+ import PencilIcon from "~icons/lucide/pencil";
10
+ import PlusIcon from "~icons/lucide/plus";
11
+ import SearchIcon from "~icons/lucide/search";
12
+ import XIcon from "~icons/lucide/x";
13
+
14
+ import { Banner } from "@admin/components/ui/banner";
15
+ import { Button } from "@admin/components/ui/button";
16
+ import {
17
+ Dialog,
18
+ DialogContent,
19
+ DialogDescription,
20
+ DialogFooter,
21
+ DialogHeader,
22
+ DialogTitle,
23
+ } from "@admin/components/ui/dialog";
24
+ import { Input } from "@admin/components/ui/input";
25
+ import { RadioGroup, RadioGroupItem } from "@admin/components/ui/radio-group";
26
+ import JsonViewer from "@admin/components/json-tree/json-viewer.svelte";
27
+ import { Sheet, SheetContent, SheetDescription, SheetTitle } from "@admin/components/ui/sheet";
28
+ import { Skeleton } from "@admin/components/ui/skeleton";
29
+ import { getNotificationCenter } from "@admin/core/notifications/notification-center.svelte";
30
+ import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
31
+ import FieldLayout from "@admin/fields/field-layout.svelte";
32
+ import FieldMessages from "@admin/fields/field-messages.svelte";
33
+
34
+ import { ReferenceBrowserWorkflow } from "@admin/features/reference-browser/reference-browser-workflow.svelte";
35
+
36
+ let {
37
+ open = $bindable(false),
38
+ field,
39
+ collection,
40
+ hasMany,
41
+ selectedIDs,
42
+ readOnly = false,
43
+ initialDocument,
44
+ initialDocumentID,
45
+ optionFilter,
46
+ defaultValues,
47
+ allowCreate = true,
48
+ locale,
49
+ onCommit,
50
+ onClose,
51
+ }: FieldReferenceBrowserProps = $props();
52
+
53
+ const runtime = getAdminRuntime();
54
+ const notifications = getNotificationCenter();
55
+ const fieldLabel = $derived(runtime.i18n.text(field.admin.label, field.admin.labelTranslations));
56
+ const singularLabel = $derived(
57
+ runtime.i18n.text(collection.labels.singular, collection.labels.singularTranslations)
58
+ );
59
+ const pluralLabel = $derived(
60
+ runtime.i18n.text(collection.labels.plural, collection.labels.pluralTranslations)
61
+ );
62
+ const lowerSingularLabel = $derived(singularLabel.toLocaleLowerCase(runtime.i18n.language));
63
+ const lowerPluralLabel = $derived(pluralLabel.toLocaleLowerCase(runtime.i18n.language));
64
+ let contentElement = $state<HTMLElement | null>(null);
65
+ // The field mounts a fresh browser for each drawer session, so workflow inputs other than the
66
+ // controlled open state intentionally describe that session rather than later prop replacements.
67
+ // svelte-ignore state_referenced_locally
68
+ const controller = new ReferenceBrowserWorkflow({
69
+ runtime,
70
+ notifications,
71
+ field,
72
+ collection,
73
+ hasMany,
74
+ selectedIDs,
75
+ readOnly,
76
+ initialDocument,
77
+ initialDocumentID,
78
+ optionFilter,
79
+ defaultValues,
80
+ allowCreate,
81
+ locale,
82
+ onCommit,
83
+ onClose,
84
+ get open() {
85
+ return open;
86
+ },
87
+ setOpen(value) {
88
+ open = value;
89
+ },
90
+ });
91
+ const { editorFields, filters, form, headingField } = controller;
92
+ const {
93
+ canCommitSelection,
94
+ canCreateDocument,
95
+ canSave,
96
+ committing,
97
+ creating,
98
+ docs,
99
+ editorDocument,
100
+ editorError,
101
+ editorLoading,
102
+ editorTitle,
103
+ editorView,
104
+ error,
105
+ filterKey,
106
+ headingIssues,
107
+ pagination,
108
+ query,
109
+ rangeEnd,
110
+ rangeStart,
111
+ screen,
112
+ selectedFile,
113
+ status,
114
+ workingSelection,
115
+ } = $derived(controller);
116
+ const {
117
+ chooseFilter,
118
+ commitSelection,
119
+ discardChanges,
120
+ documentLabel,
121
+ documentSubtitle,
122
+ formatBytes,
123
+ handleSearch,
124
+ isImage,
125
+ mediaURL,
126
+ openKnownDocument,
127
+ openNewDocument,
128
+ requestBack,
129
+ requestOpenChange,
130
+ saveEditor,
131
+ toggleSelection,
132
+ } = controller;
133
+ const visibleSingleSelection = $derived(
134
+ hasMany || !docs.some((document) => document.id === workingSelection[0])
135
+ ? ""
136
+ : (workingSelection[0] ?? "")
137
+ );
138
+ const visibleResultIdentity = $derived(docs.map((document) => document.id).join("\u001f"));
139
+
140
+ async function handleSave(event: Event) {
141
+ event.preventDefault();
142
+ if (await saveEditor()) return;
143
+ if (controller.credentialIssue !== undefined) {
144
+ await tick();
145
+ contentElement?.querySelector<HTMLElement>("#ridu-reference-new-user-password")?.focus();
146
+ return;
147
+ }
148
+ const issue = form.issues[0];
149
+ if (issue === undefined) return;
150
+ await tick();
151
+ const segments = issue.path.split(".");
152
+ while (segments.length > 0) {
153
+ const container = contentElement?.querySelector<HTMLElement>(
154
+ `[data-field-path="${CSS.escape(segments.join("."))}"]`
155
+ );
156
+ if (container !== null && container !== undefined) {
157
+ container.scrollIntoView({ behavior: "smooth", block: "center" });
158
+ container
159
+ .querySelector<HTMLElement>("input, textarea, button, [tabindex]:not([tabindex='-1'])")
160
+ ?.focus({ preventScroll: true });
161
+ return;
162
+ }
163
+ segments.pop();
164
+ }
165
+ }
166
+ </script>
167
+
168
+ {#snippet uploadResults()}
169
+ {#each docs as document (document.id)}
170
+ <div class="group min-w-0">
171
+ <div
172
+ class={[
173
+ "relative aspect-[4/3] overflow-hidden rounded-[4px] border bg-control",
174
+ workingSelection.includes(document.id) ? "border-primary/60" : "border-control-border",
175
+ ]}
176
+ >
177
+ {#if isImage(document) && mediaURL(document) !== undefined}
178
+ <img
179
+ class="size-full object-cover"
180
+ src={mediaURL(document)}
181
+ alt={typeof document.alt === "string" ? document.alt : ""}
182
+ />
183
+ {:else}
184
+ <div class="grid size-full place-items-center">
185
+ <FileIcon class="size-6 text-foreground-faint" />
186
+ </div>
187
+ {/if}
188
+ {#if hasMany}
189
+ <button
190
+ type="button"
191
+ class="absolute inset-0 outline-none focus-visible:ring-2 focus-visible:ring-primary/70 focus-visible:ring-inset disabled:cursor-not-allowed"
192
+ role="checkbox"
193
+ aria-checked={workingSelection.includes(document.id)}
194
+ aria-label={runtime.i18n.t(
195
+ workingSelection.includes(document.id)
196
+ ? "reference:deselectLabel"
197
+ : "reference:selectLabel",
198
+ { label: documentLabel(document) }
199
+ )}
200
+ disabled={readOnly || committing}
201
+ onclick={() => toggleSelection(document.id)}
202
+ ></button>
203
+ {:else}
204
+ <RadioGroupItem
205
+ value={document.id}
206
+ class="absolute inset-0 rounded-none focus-visible:ring-primary/70 focus-visible:ring-inset"
207
+ aria-label={runtime.i18n.t("reference:selectLabel", {
208
+ label: documentLabel(document),
209
+ })}
210
+ />
211
+ {/if}
212
+ <span
213
+ class={[
214
+ "pointer-events-none absolute top-2 start-2 grid size-4 place-items-center rounded-[4px] border text-[9px] font-bold",
215
+ workingSelection.includes(document.id)
216
+ ? "border-primary bg-primary text-primary-foreground"
217
+ : "border-media-foreground/25 bg-media-overlay",
218
+ ]}
219
+ aria-hidden="true"
220
+ >
221
+ {workingSelection.includes(document.id) ? "✓" : ""}
222
+ </span>
223
+ <Button
224
+ variant="secondary"
225
+ size="icon-xs"
226
+ class="absolute end-2 bottom-2"
227
+ disabled={committing}
228
+ onclick={() => openKnownDocument(document)}
229
+ aria-label={runtime.i18n.t("reference:editLabel", {
230
+ label: documentLabel(document),
231
+ })}
232
+ >
233
+ <PencilIcon class="size-3" />
234
+ </Button>
235
+ </div>
236
+ <p class="mt-1.5 truncate text-[11.5px] text-foreground-muted">
237
+ {documentLabel(document)}
238
+ </p>
239
+ </div>
240
+ {/each}
241
+ {/snippet}
242
+
243
+ {#snippet documentResults()}
244
+ {#each docs as document (document.id)}
245
+ <div
246
+ class={[
247
+ "flex items-center gap-2 rounded-[9px] border px-2 py-1.5",
248
+ workingSelection.includes(document.id)
249
+ ? "border-primary/20 bg-primary/[0.055]"
250
+ : "border-transparent hover:bg-control",
251
+ ]}
252
+ >
253
+ {#if hasMany}
254
+ <button
255
+ type="button"
256
+ class="flex min-w-0 flex-1 items-center gap-3 rounded-[7px] px-1.5 py-1 text-start outline-none focus-visible:ring-2 focus-visible:ring-primary/60 disabled:cursor-not-allowed"
257
+ role="checkbox"
258
+ aria-checked={workingSelection.includes(document.id)}
259
+ disabled={readOnly || committing}
260
+ onclick={() => toggleSelection(document.id)}
261
+ >
262
+ {@render documentChoiceContent(document)}
263
+ </button>
264
+ {:else}
265
+ <RadioGroupItem
266
+ value={document.id}
267
+ class="flex min-w-0 flex-1 items-center gap-3 rounded-[7px] px-1.5 py-1 text-start"
268
+ >
269
+ {@render documentChoiceContent(document)}
270
+ </RadioGroupItem>
271
+ {/if}
272
+ <Button
273
+ variant="outline"
274
+ size="icon-sm"
275
+ disabled={committing}
276
+ onclick={() => openKnownDocument(document)}
277
+ aria-label={runtime.i18n.t("reference:editLabel", {
278
+ label: documentLabel(document),
279
+ })}
280
+ >
281
+ <PencilIcon class="size-3" />
282
+ </Button>
283
+ </div>
284
+ {/each}
285
+ {/snippet}
286
+
287
+ {#snippet documentChoiceContent(document: Record<string, unknown> & { id: string })}
288
+ <span
289
+ class={[
290
+ "grid size-4 shrink-0 place-items-center border text-[9px] font-bold",
291
+ hasMany ? "rounded-[4px]" : "rounded-full",
292
+ workingSelection.includes(document.id)
293
+ ? "border-primary bg-primary text-primary-foreground"
294
+ : "border-control-border-hover",
295
+ ]}
296
+ aria-hidden="true"
297
+ >
298
+ {workingSelection.includes(document.id) ? "✓" : ""}
299
+ </span>
300
+ <span
301
+ class="grid size-7 shrink-0 place-items-center rounded-full bg-control text-[9px] font-semibold text-foreground-muted"
302
+ >
303
+ {documentLabel(document).slice(0, 2).toLocaleUpperCase(runtime.i18n.language)}
304
+ </span>
305
+ <span class="min-w-0 flex-1">
306
+ <span class="block truncate text-[13.5px] text-foreground-strong">
307
+ {documentLabel(document)}
308
+ </span>
309
+ <span class="mt-0.5 block truncate text-[11.5px] text-foreground-faint">
310
+ {documentSubtitle(document)}
311
+ </span>
312
+ </span>
313
+ {/snippet}
314
+
315
+ <Sheet bind:open={() => open, requestOpenChange}>
316
+ <SheetContent
317
+ bind:ref={contentElement}
318
+ class="border-control-border bg-background-surface p-0"
319
+ style="width: min(760px, 96vw); max-width: 760px"
320
+ showCloseButton={false}
321
+ aria-label={screen === "list"
322
+ ? runtime.i18n.t("reference:selectLabel", { label: fieldLabel })
323
+ : editorTitle}
324
+ >
325
+ <header
326
+ class="flex min-h-16 shrink-0 items-center gap-3 border-b border-control-border px-4 sm:px-5"
327
+ >
328
+ {#if screen === "document"}
329
+ <Button
330
+ variant="outline"
331
+ size="icon-sm"
332
+ disabled={form.submitting}
333
+ onclick={requestBack}
334
+ aria-label={runtime.i18n.t("reference:backToResults")}
335
+ >
336
+ <ArrowLeftIcon class="size-3.5 rtl:rotate-180" />
337
+ </Button>
338
+ {/if}
339
+ <div class="min-w-0 flex-1">
340
+ <p class="font-mono truncate text-[9px] tracking-[0.13em] text-foreground-faint uppercase">
341
+ {pluralLabel} / {screen === "document"
342
+ ? runtime.i18n.t("reference:document")
343
+ : fieldLabel}
344
+ </p>
345
+ <SheetTitle class="font-serif mt-1 truncate text-[24px] leading-none text-foreground">
346
+ {screen === "list"
347
+ ? runtime.i18n.t(hasMany ? "reference:addLabel" : "reference:selectLabel", {
348
+ label: fieldLabel.toLocaleLowerCase(runtime.i18n.language),
349
+ })
350
+ : creating
351
+ ? runtime.i18n.t("reference:newLabel", { label: lowerSingularLabel })
352
+ : editorTitle}
353
+ </SheetTitle>
354
+ <SheetDescription class="sr-only">
355
+ {runtime.i18n.t("reference:browserDescription", { label: pluralLabel })}
356
+ </SheetDescription>
357
+ </div>
358
+ <Button
359
+ variant="ghost"
360
+ size="icon-sm"
361
+ disabled={committing || form.submitting}
362
+ onclick={() => requestOpenChange(false)}
363
+ aria-label={runtime.i18n.t("reference:closeBrowser")}
364
+ tooltip={runtime.i18n.t("reference:closeBrowser")}
365
+ >
366
+ <XIcon class="size-4" />
367
+ </Button>
368
+ </header>
369
+
370
+ {#if screen === "list"}
371
+ <div class="shrink-0 border-b border-control-border px-4 py-3.5 sm:px-5">
372
+ <div class="flex items-center gap-3">
373
+ <div class="relative min-w-0 flex-1">
374
+ <SearchIcon
375
+ class="pointer-events-none absolute top-1/2 start-3.5 size-3.5 -translate-y-1/2 text-foreground-faint"
376
+ aria-hidden="true"
377
+ />
378
+ <label class="sr-only" for="{field.id}-relationship-search">
379
+ {runtime.i18n.t("reference:search", { label: pluralLabel })}
380
+ </label>
381
+ <Input
382
+ id="{field.id}-relationship-search"
383
+ type="search"
384
+ disabled={committing}
385
+ class="h-9 bg-control ps-9"
386
+ value={query}
387
+ oninput={handleSearch}
388
+ placeholder={runtime.i18n.t("reference:search", { label: lowerPluralLabel })}
389
+ />
390
+ </div>
391
+ <p class="font-mono shrink-0 text-[9.5px] text-foreground-faint">
392
+ {runtime.i18n.formatNumber(pagination.totalDocs)}
393
+ {lowerPluralLabel}
394
+ </p>
395
+ </div>
396
+ {#if filters.length > 1}
397
+ <div
398
+ class="mt-3 flex flex-wrap gap-1.5"
399
+ aria-label={runtime.i18n.t("reference:collectionFilters")}
400
+ >
401
+ {#each filters as option (option.key)}
402
+ <Button
403
+ variant={filterKey === option.key ? "secondary" : "ghost"}
404
+ size="sm"
405
+ disabled={committing}
406
+ class="h-7 rounded-full px-3 text-[11.5px]"
407
+ onclick={() => chooseFilter(option.key)}
408
+ aria-pressed={filterKey === option.key}
409
+ >
410
+ {option.label}
411
+ </Button>
412
+ {/each}
413
+ </div>
414
+ {/if}
415
+ </div>
416
+
417
+ <div class="min-h-0 flex-1 overflow-y-auto px-4 py-3 sm:px-5">
418
+ {#if error !== undefined}
419
+ <Banner tone="destructive">{error}</Banner>
420
+ {/if}
421
+
422
+ {#if status === "loading" && docs.length === 0}
423
+ <div
424
+ class={collection.capabilities.upload
425
+ ? "grid grid-cols-2 gap-3 sm:grid-cols-3"
426
+ : "grid gap-2"}
427
+ aria-label={runtime.i18n.t("reference:loadingRelated")}
428
+ >
429
+ {#each Array(collection.capabilities.upload ? 6 : 5) as _, index (index)}
430
+ <Skeleton class={collection.capabilities.upload ? "aspect-[4/3]" : "h-15"} />
431
+ {/each}
432
+ </div>
433
+ {:else if docs.length === 0}
434
+ <div class="grid min-h-60 place-items-center px-5 text-center">
435
+ <div>
436
+ <p class="font-serif text-[23px] italic text-foreground-muted">
437
+ {runtime.i18n.t("reference:nothingMatches")}
438
+ </p>
439
+ <p class="mt-2 text-[13px] text-foreground-faint">
440
+ {runtime.i18n.t("reference:searchOrCreate", { label: lowerSingularLabel })}
441
+ </p>
442
+ </div>
443
+ </div>
444
+ {:else if collection.capabilities.upload}
445
+ {#if hasMany}
446
+ <div
447
+ class="grid grid-cols-2 gap-3 sm:grid-cols-3"
448
+ role="group"
449
+ aria-label={runtime.i18n.t("reference:results")}
450
+ >
451
+ {@render uploadResults()}
452
+ </div>
453
+ {:else}
454
+ {#key visibleResultIdentity}
455
+ <RadioGroup
456
+ class="grid grid-cols-2 gap-3 sm:grid-cols-3"
457
+ aria-label={runtime.i18n.t("reference:results")}
458
+ value={visibleSingleSelection}
459
+ readonly={readOnly || committing}
460
+ onValueChange={toggleSelection}
461
+ >
462
+ {@render uploadResults()}
463
+ </RadioGroup>
464
+ {/key}
465
+ {/if}
466
+ {:else}
467
+ {#if hasMany}
468
+ <div class="grid gap-1" role="group" aria-label={runtime.i18n.t("reference:results")}>
469
+ {@render documentResults()}
470
+ </div>
471
+ {:else}
472
+ {#key visibleResultIdentity}
473
+ <RadioGroup
474
+ class="grid gap-1"
475
+ aria-label={runtime.i18n.t("reference:results")}
476
+ value={visibleSingleSelection}
477
+ readonly={readOnly || committing}
478
+ onValueChange={toggleSelection}
479
+ >
480
+ {@render documentResults()}
481
+ </RadioGroup>
482
+ {/key}
483
+ {/if}
484
+ {/if}
485
+ </div>
486
+
487
+ <footer class="shrink-0 border-t border-control-border px-4 py-3.5 sm:px-5">
488
+ <div class="mb-3 flex items-center justify-between gap-3">
489
+ <p class="font-mono text-[10px] text-foreground-faint">
490
+ {runtime.i18n.t("reference:range", {
491
+ start: runtime.i18n.formatNumber(rangeStart),
492
+ end: runtime.i18n.formatNumber(rangeEnd),
493
+ total: runtime.i18n.formatNumber(pagination.totalDocs),
494
+ })}
495
+ </p>
496
+ {#if pagination.totalPages > 1}
497
+ <div class="flex items-center gap-1.5">
498
+ <Button
499
+ variant="outline"
500
+ size="icon-sm"
501
+ disabled={!pagination.hasPrevPage || status === "loading" || committing}
502
+ onclick={() => (controller.page -= 1)}
503
+ aria-label={runtime.i18n.t("reference:previousPage")}
504
+ >
505
+ <ChevronLeftIcon class="size-3.5 rtl:rotate-180" />
506
+ </Button>
507
+ <span class="font-mono min-w-10 text-center text-[10px] text-foreground-faint">
508
+ {runtime.i18n.formatNumber(pagination.page)} / {runtime.i18n.formatNumber(
509
+ pagination.totalPages
510
+ )}
511
+ </span>
512
+ <Button
513
+ variant="outline"
514
+ size="icon-sm"
515
+ disabled={!pagination.hasNextPage || status === "loading" || committing}
516
+ onclick={() => (controller.page += 1)}
517
+ aria-label={runtime.i18n.t("reference:nextPage")}
518
+ >
519
+ <ChevronRightIcon class="size-3.5 rtl:rotate-180" />
520
+ </Button>
521
+ </div>
522
+ {/if}
523
+ </div>
524
+ <div class="flex flex-wrap items-center gap-2">
525
+ {#if canCreateDocument}
526
+ <Button variant="outline" disabled={committing} onclick={openNewDocument}>
527
+ {#if collection.capabilities.upload}<FileUpIcon class="size-3.5" />{:else}<PlusIcon
528
+ class="size-3.5"
529
+ />{/if}
530
+ {collection.capabilities.upload
531
+ ? runtime.i18n.t("reference:uploadNew")
532
+ : runtime.i18n.t("reference:createLabel", { label: lowerSingularLabel })}
533
+ </Button>
534
+ {/if}
535
+ <span class="flex-1"></span>
536
+ <Button variant="outline" disabled={committing} onclick={() => requestOpenChange(false)}>
537
+ {runtime.i18n.t("reference:cancel")}
538
+ </Button>
539
+ {#if !readOnly}
540
+ <Button disabled={!canCommitSelection || committing} onclick={commitSelection}>
541
+ {committing
542
+ ? runtime.i18n.t("reference:applying")
543
+ : hasMany
544
+ ? runtime.i18n.t("reference:addSelected", {
545
+ count: runtime.i18n.formatNumber(workingSelection.length),
546
+ })
547
+ : runtime.i18n.t("reference:select")}
548
+ </Button>
549
+ {/if}
550
+ </div>
551
+ </footer>
552
+ {:else}
553
+ <div class="flex min-h-0 flex-1 flex-col">
554
+ <div
555
+ class="flex h-12 shrink-0 items-center gap-2 border-b border-control-border px-4 sm:px-5"
556
+ >
557
+ <div class="flex rounded-[3px] border border-control-border bg-control p-0.5">
558
+ <Button
559
+ variant="ghost"
560
+ size="sm"
561
+ class={[
562
+ "h-6 rounded-[6px] px-3 text-[11.5px]",
563
+ editorView === "edit" && "bg-background text-foreground",
564
+ ]}
565
+ onclick={() => (controller.editorView = "edit")}
566
+ >
567
+ {runtime.i18n.t("reference:edit")}
568
+ </Button>
569
+ <Button
570
+ variant="ghost"
571
+ size="sm"
572
+ class={[
573
+ "font-mono h-6 rounded-[6px] px-3 text-[10.5px]",
574
+ editorView === "api" && "bg-background text-foreground",
575
+ ]}
576
+ onclick={() => (controller.editorView = "api")}
577
+ >
578
+ {runtime.i18n.t("reference:api")}
579
+ </Button>
580
+ </div>
581
+ <span class="flex-1"></span>
582
+ <span class="font-mono text-[9.5px] text-foreground-faint">
583
+ {form.submitting
584
+ ? runtime.i18n.t("reference:saving")
585
+ : controller.editorDirty
586
+ ? runtime.i18n.t("reference:unsavedChanges")
587
+ : runtime.i18n.t("reference:saved")}
588
+ </span>
589
+ {#if !readOnly && editorView === "edit"}
590
+ <Button size="sm" disabled={!canSave} onclick={handleSave}>
591
+ {creating
592
+ ? runtime.i18n.t("reference:createLabel", { label: lowerSingularLabel })
593
+ : runtime.i18n.t("reference:saveLabel", { label: lowerSingularLabel })}
594
+ </Button>
595
+ {/if}
596
+ </div>
597
+
598
+ <div class="min-h-0 flex-1 overflow-y-auto">
599
+ {#if editorLoading}
600
+ <div class="mx-auto grid max-w-[600px] gap-5 px-6 py-8">
601
+ <Skeleton class="h-10 w-2/3" />
602
+ <Skeleton class="mt-5 h-10" />
603
+ <Skeleton class="h-28" />
604
+ </div>
605
+ {:else if editorView === "api"}
606
+ <div class="mx-auto max-w-[650px] px-6 py-8">
607
+ <p class="font-mono text-[9.5px] tracking-[0.14em] text-foreground-faint uppercase">
608
+ {runtime.i18n.t("reference:currentPayload")}
609
+ </p>
610
+ <JsonViewer class="mt-4" value={form.values} />
611
+ </div>
612
+ {:else}
613
+ <form class="mx-auto max-w-[620px] px-6 py-8" onsubmit={handleSave}>
614
+ {#if readOnly}
615
+ <Banner class="mb-6" tone="warning">
616
+ {runtime.i18n.t("reference:readOnlyDescription")}
617
+ </Banner>
618
+ {/if}
619
+ {#if editorError !== undefined}
620
+ <Banner class="mb-6" tone="destructive">{editorError}</Banner>
621
+ {/if}
622
+ {#if !readOnly && form.access !== undefined && !form.access.operations[creating ? "create" : "update"]}
623
+ <Banner class="mb-6" tone="warning">
624
+ {runtime.i18n.t(creating ? "reference:cannotCreate" : "reference:cannotUpdate", {
625
+ label: lowerSingularLabel,
626
+ })}
627
+ </Banner>
628
+ {/if}
629
+
630
+ {#if collection.capabilities.upload && editorDocument !== undefined}
631
+ <div
632
+ class="mb-7 overflow-hidden rounded-[4px] border border-control-border bg-control"
633
+ >
634
+ <div class="grid min-h-48 place-items-center bg-background-layer">
635
+ {#if isImage(editorDocument) && mediaURL(editorDocument) !== undefined}
636
+ <img
637
+ class="max-h-72 w-full object-contain"
638
+ src={mediaURL(editorDocument)}
639
+ alt={typeof editorDocument.alt === "string" ? editorDocument.alt : ""}
640
+ />
641
+ {:else}
642
+ <FileIcon class="size-9 text-foreground-faint" />
643
+ {/if}
644
+ </div>
645
+ <dl
646
+ class="grid grid-cols-[90px_1fr] gap-x-4 gap-y-2 border-t border-control-border px-4 py-3 text-[11.5px]"
647
+ >
648
+ <dt class="text-foreground-faint">{runtime.i18n.t("uploads:filename")}</dt>
649
+ <dd class="min-w-0 truncate text-foreground-muted">
650
+ {String(editorDocument.filename ?? "—")}
651
+ </dd>
652
+ <dt class="text-foreground-faint">{runtime.i18n.t("uploads:type")}</dt>
653
+ <dd class="font-mono text-[10.5px] text-foreground-muted">
654
+ {String(editorDocument.mimeType ?? "—")}
655
+ </dd>
656
+ <dt class="text-foreground-faint">{runtime.i18n.t("uploads:size")}</dt>
657
+ <dd class="font-mono text-[10.5px] text-foreground-muted">
658
+ {formatBytes(editorDocument.filesize) || "—"}
659
+ </dd>
660
+ <dt class="text-foreground-faint">
661
+ {runtime.i18n.t("uploads:dimensions")}
662
+ </dt>
663
+ <dd class="font-mono text-[10.5px] text-foreground-muted">
664
+ {typeof editorDocument.width === "number" &&
665
+ typeof editorDocument.height === "number"
666
+ ? `${runtime.i18n.formatNumber(editorDocument.width)} × ${runtime.i18n.formatNumber(editorDocument.height)}`
667
+ : "—"}
668
+ </dd>
669
+ </dl>
670
+ </div>
671
+ {/if}
672
+
673
+ {#if creating && collection.capabilities.upload}
674
+ <label
675
+ class="mb-7 grid min-h-44 cursor-pointer place-items-center rounded-[4px] border border-dashed border-control-border-hover bg-control px-6 py-8 text-center transition-colors hover:border-primary/45 hover:bg-primary/[0.025]"
676
+ >
677
+ <input
678
+ class="sr-only"
679
+ type="file"
680
+ accept={collection.uploadSettings?.mimeTypes.join(",")}
681
+ required
682
+ disabled={form.submitting || form.access?.operations.create !== true}
683
+ bind:files={controller.selectedFiles}
684
+ />
685
+ <span>
686
+ <FileUpIcon class="mx-auto size-6 text-foreground-faint" />
687
+ <span class="mt-3 block text-[13.5px] font-medium text-foreground-muted">
688
+ {selectedFile?.name ?? runtime.i18n.t("uploads:chooseFileToUpload")}
689
+ </span>
690
+ <span class="font-mono mt-1.5 block text-[9.5px] text-foreground-faint">
691
+ {runtime.i18n.formatList(collection.uploadSettings?.mimeTypes ?? [], {
692
+ style: "short",
693
+ type: "disjunction",
694
+ })}
695
+ </span>
696
+ </span>
697
+ </label>
698
+ {/if}
699
+
700
+ {#if controller.creatingAuthUser}
701
+ <fieldset
702
+ class="mb-7 grid gap-4 rounded-[4px] border border-control-border bg-control p-4.5"
703
+ >
704
+ <legend class="px-1 text-[13px] font-semibold text-foreground">
705
+ {runtime.i18n.t("reference:credentials")}
706
+ </legend>
707
+ <div class="grid gap-4 sm:grid-cols-2">
708
+ <label class="grid gap-2" for="ridu-reference-new-user-password">
709
+ <span class="ridu-field-label">{runtime.i18n.t("reference:password")}</span>
710
+ <input
711
+ id="ridu-reference-new-user-password"
712
+ type="password"
713
+ autocomplete="new-password"
714
+ required
715
+ disabled={form.submitting || form.access?.operations.create !== true}
716
+ aria-invalid={controller.credentialIssue !== undefined}
717
+ aria-describedby="ridu-reference-new-user-password-help"
718
+ bind:value={controller.newUserPassword}
719
+ oninput={() => (controller.credentialIssue = undefined)}
720
+ />
721
+ </label>
722
+ <label class="grid gap-2" for="ridu-reference-new-user-password-confirmation">
723
+ <span class="ridu-field-label">
724
+ {runtime.i18n.t("reference:confirmPassword")}
725
+ </span>
726
+ <input
727
+ id="ridu-reference-new-user-password-confirmation"
728
+ type="password"
729
+ autocomplete="new-password"
730
+ required
731
+ disabled={form.submitting || form.access?.operations.create !== true}
732
+ aria-invalid={controller.credentialIssue !== undefined}
733
+ aria-describedby="ridu-reference-new-user-password-help"
734
+ bind:value={controller.newUserPasswordConfirmation}
735
+ oninput={() => (controller.credentialIssue = undefined)}
736
+ />
737
+ </label>
738
+ </div>
739
+ <p
740
+ id="ridu-reference-new-user-password-help"
741
+ class={controller.credentialIssue === undefined
742
+ ? "ridu-field-help"
743
+ : "ridu-field-error"}
744
+ role={controller.credentialIssue === undefined ? undefined : "alert"}
745
+ >
746
+ {controller.credentialIssue ??
747
+ runtime.i18n.t("reference:passwordMinimum", {
748
+ minimum: runtime.i18n.formatNumber(
749
+ collection.authSettings?.passwordMinLength ?? 8
750
+ ),
751
+ })}
752
+ </p>
753
+ </fieldset>
754
+ {/if}
755
+
756
+ <p class="font-mono text-[9.5px] tracking-[0.13em] text-foreground-faint uppercase">
757
+ {creating
758
+ ? runtime.i18n.t("reference:newMetadata", { label: singularLabel })
759
+ : `${singularLabel} · ${editorDocument?.id ?? ""}`}
760
+ </p>
761
+ {#if headingField !== undefined}
762
+ <div data-field-path={headingField.path}>
763
+ <label class="sr-only" for={headingField.id}>
764
+ {runtime.i18n.text(
765
+ headingField.admin.label,
766
+ headingField.admin.labelTranslations
767
+ )}
768
+ </label>
769
+ <input
770
+ id={headingField.id}
771
+ class="font-serif mt-3 w-full border-x-0 border-t-0 border-b border-b-transparent bg-transparent p-0 pb-1 text-[34px] leading-[1.08] text-foreground caret-primary outline-none placeholder:text-foreground-soft aria-invalid:border-b-destructive/65"
772
+ value={String(form.get(headingField.path) ?? "")}
773
+ placeholder={runtime.i18n.t("reference:untitledLabel", {
774
+ label: lowerSingularLabel,
775
+ })}
776
+ required={headingField.required}
777
+ readonly={readOnly || form.submitting || !form.canWrite(headingField.path)}
778
+ aria-invalid={headingIssues.length > 0}
779
+ aria-describedby={headingIssues.length > 0
780
+ ? `${headingField.id}-message`
781
+ : undefined}
782
+ aria-errormessage={headingIssues.length > 0
783
+ ? `${headingField.id}-message`
784
+ : undefined}
785
+ oninput={(event) => form.set(headingField.path, event.currentTarget.value)}
786
+ />
787
+ <FieldMessages
788
+ id="{headingField.id}-message"
789
+ issues={headingIssues}
790
+ class="mt-2"
791
+ />
792
+ </div>
793
+ {/if}
794
+
795
+ <div class="mt-7">
796
+ <FieldLayout
797
+ fields={editorFields.map((candidate) =>
798
+ readOnly || form.submitting
799
+ ? { ...candidate, admin: { ...candidate.admin, readOnly: true } }
800
+ : candidate
801
+ )}
802
+ {form}
803
+ />
804
+ </div>
805
+
806
+ {#if !readOnly}
807
+ <div class="mt-8 flex justify-end">
808
+ <Button type="submit" disabled={!canSave}>
809
+ {creating
810
+ ? runtime.i18n.t("reference:createLabel", { label: lowerSingularLabel })
811
+ : runtime.i18n.t("reference:saveLabel", { label: lowerSingularLabel })}
812
+ </Button>
813
+ </div>
814
+ {/if}
815
+ </form>
816
+ {/if}
817
+ </div>
818
+ </div>
819
+ {/if}
820
+ </SheetContent>
821
+ </Sheet>
822
+
823
+ <Dialog bind:open={controller.confirmDiscard}>
824
+ <DialogContent showCloseButton={false}>
825
+ <DialogHeader>
826
+ <DialogTitle>{runtime.i18n.t("reference:discardChanges")}</DialogTitle>
827
+ <DialogDescription>
828
+ {runtime.i18n.t("reference:discardDescription")}
829
+ </DialogDescription>
830
+ </DialogHeader>
831
+ <DialogFooter>
832
+ <Button
833
+ variant="outline"
834
+ disabled={form.submitting}
835
+ onclick={() => (controller.confirmDiscard = false)}
836
+ >
837
+ {runtime.i18n.t("reference:keepEditing")}
838
+ </Button>
839
+ <Button variant="destructive" disabled={form.submitting} onclick={discardChanges}>
840
+ {runtime.i18n.t("reference:discard")}
841
+ </Button>
842
+ </DialogFooter>
843
+ </DialogContent>
844
+ </Dialog>