@riducms/admin 0.2.3 → 0.4.0

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