@riducms/admin 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/THIRD_PARTY_NOTICES.md +60 -37
  2. package/package.json +27 -15
  3. package/src/app/admin-core-view-route.svelte +38 -53
  4. package/src/app/admin-extension-route.svelte +19 -0
  5. package/src/app/admin-layout.scss +175 -0
  6. package/src/app/admin-layout.svelte +144 -410
  7. package/src/app/admin-navigation-progress.svelte +11 -77
  8. package/src/app/admin-page-title.svelte +17 -85
  9. package/src/app/admin-page-title.ts +136 -0
  10. package/src/app/admin-route-error.svelte +24 -0
  11. package/src/app/admin-router-provider.svelte +34 -0
  12. package/src/app/admin-router-root.svelte +41 -0
  13. package/src/app/admin-routes.svelte +56 -19
  14. package/src/app/loading-boundary.svelte +3 -1
  15. package/src/app-meta.ts +13 -0
  16. package/src/app.css +138 -496
  17. package/src/app.svelte +60 -26
  18. package/src/components/brand/ridu-logo.svelte +19 -5
  19. package/src/components/data/list-table.scss +63 -0
  20. package/src/components/icons/LICENSE.md +24 -0
  21. package/src/components/icons/chevron.svelte +8 -0
  22. package/src/components/icons/plus.svelte +12 -0
  23. package/src/components/icons/search.svelte +12 -0
  24. package/src/components/icons/x.svelte +8 -0
  25. package/src/components/json-tree/json-tree-node.svelte +61 -67
  26. package/src/components/json-tree/json-viewer.scss +153 -0
  27. package/src/components/json-tree/json-viewer.svelte +57 -96
  28. package/src/components/ui/banner/banner.scss +28 -0
  29. package/src/components/ui/banner/banner.svelte +9 -8
  30. package/src/components/ui/banner/index.ts +1 -5
  31. package/src/components/ui/code-editor/code-editor.scss +20 -0
  32. package/src/components/ui/code-editor/code-editor.svelte +71 -0
  33. package/src/components/ui/code-editor/code-editor.ts +151 -0
  34. package/src/components/ui/code-editor/code-languages.ts +29 -0
  35. package/src/components/ui/collapsible-panel/collapsible-panel.scss +38 -0
  36. package/src/components/ui/collapsible-panel/collapsible-panel.svelte +13 -0
  37. package/src/components/ui/collapsible-panel/index.ts +1 -0
  38. package/src/components/ui/combobox/combobox.scss +163 -0
  39. package/src/components/ui/combobox/index.ts +22 -0
  40. package/src/components/ui/date-value-control/date-value-calendar.svelte +128 -16
  41. package/src/components/ui/date-value-control/date-value-control-loader.scss +26 -0
  42. package/src/components/ui/date-value-control/date-value-control-loader.svelte +35 -13
  43. package/src/components/ui/date-value-control/date-value-control.scss +223 -0
  44. package/src/components/ui/date-value-control/date-value-control.svelte +62 -44
  45. package/src/components/ui/drawer/drawer-depth.ts +8 -0
  46. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +2 -5
  47. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +2 -5
  48. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +2 -2
  49. package/src/components/ui/dropdown-menu/dropdown-menu.scss +62 -0
  50. package/src/components/ui/kbd/kbd.scss +17 -0
  51. package/src/components/ui/kbd/kbd.svelte +3 -10
  52. package/src/components/ui/navigation-drawer/navigation-drawer.scss +40 -0
  53. package/src/components/ui/navigation-drawer/navigation-drawer.svelte +38 -0
  54. package/src/components/ui/popover/popover-content.svelte +3 -5
  55. package/src/components/ui/popover/popover.scss +18 -0
  56. package/src/components/ui/radio-group/index.ts +0 -1
  57. package/src/components/ui/radio-group/radio-card-item.svelte +7 -12
  58. package/src/components/ui/radio-group/radio-group.svelte +2 -2
  59. package/src/components/ui/radio-group/radio.scss +45 -0
  60. package/src/components/ui/select/select-content.svelte +5 -6
  61. package/src/components/ui/select/select-item.svelte +8 -8
  62. package/src/components/ui/select/select-scroll-down-button.svelte +3 -5
  63. package/src/components/ui/select/select-scroll-up-button.svelte +3 -5
  64. package/src/components/ui/select/select-trigger.svelte +6 -7
  65. package/src/components/ui/select/select.scss +126 -0
  66. package/src/components/ui/sheet/sheet-content.svelte +6 -34
  67. package/src/components/ui/sheet/sheet-description.svelte +2 -2
  68. package/src/components/ui/sheet/sheet-overlay.svelte +2 -21
  69. package/src/components/ui/sheet/sheet-title.svelte +2 -2
  70. package/src/components/ui/sheet/sheet.scss +221 -0
  71. package/src/components/ui/skeleton/skeleton.scss +20 -0
  72. package/src/components/ui/skeleton/skeleton.svelte +4 -16
  73. package/src/components/ui/sonner/sonner.scss +121 -0
  74. package/src/components/ui/sonner/sonner.svelte +11 -10
  75. package/src/components/ui/status-indicator/status-indicator.scss +48 -0
  76. package/src/components/ui/status-indicator/status-indicator.svelte +14 -14
  77. package/src/components/ui/tabs/tabs-content.svelte +2 -2
  78. package/src/components/ui/tabs/tabs-list.svelte +7 -7
  79. package/src/components/ui/tabs/tabs-trigger.svelte +2 -8
  80. package/src/components/ui/tabs/tabs.scss +86 -0
  81. package/src/components/ui/tabs/tabs.svelte +2 -2
  82. package/src/components/ui/timezone-picker/timezone-picker.svelte +152 -0
  83. package/src/core/api/admin-client.ts +2 -2
  84. package/src/core/bootstrap/admin-bootstrap.ts +396 -0
  85. package/src/core/bootstrap/admin-prepared-state.ts +213 -0
  86. package/src/core/bootstrap/admin-route-modules.ts +66 -0
  87. package/src/core/clipboard/copy-feedback.svelte.ts +43 -0
  88. package/src/core/forms/embedded-fields.ts +4 -8
  89. package/src/core/forms/field-editor-authoring.ts +8 -0
  90. package/src/core/forms/field-issue-focus.ts +4 -4
  91. package/src/core/forms/field-occurrence.ts +4 -7
  92. package/src/core/forms/form-controller.svelte.ts +14 -21
  93. package/src/core/forms/form-draft-recovery.ts +17 -4
  94. package/src/core/forms/form-issue-correlation.ts +13 -13
  95. package/src/core/forms/form-path.ts +17 -0
  96. package/src/core/forms/form-schema.ts +12 -43
  97. package/src/core/forms/form-validation.ts +7 -46
  98. package/src/core/forms/plugin-field-authoring.ts +6 -2
  99. package/src/core/forms/plugin-field-write.ts +2 -6
  100. package/src/core/i18n/admin-i18n.svelte.ts +18 -0
  101. package/src/core/i18n/format-admin-date-time.ts +19 -0
  102. package/src/core/i18n/humanize-identifier.ts +5 -0
  103. package/src/core/i18n/time-zone-label.ts +20 -0
  104. package/src/core/loaders/admin-view-host.svelte +72 -0
  105. package/src/core/loaders/admin-view-loader.svelte.ts +90 -0
  106. package/src/core/notifications/validation-toast.svelte +2 -2
  107. package/src/core/plugins/field-registry.ts +7 -8
  108. package/src/core/routing/admin-paths.ts +53 -2
  109. package/src/core/routing/admin-scroll.svelte.ts +74 -0
  110. package/src/core/routing/admin-view-selection.ts +78 -0
  111. package/src/core/runtime/admin-runtime.svelte.ts +121 -124
  112. package/src/core/schema/field-default.ts +33 -0
  113. package/src/core/schema/field-localization.ts +15 -0
  114. package/src/core/schema/read-document-path.ts +11 -0
  115. package/src/features/account/account-menu.scss +37 -0
  116. package/src/features/account/account-menu.svelte +115 -0
  117. package/src/features/{auth → account}/account-navigation.svelte +16 -19
  118. package/src/features/{auth → account}/account-route.svelte +206 -49
  119. package/src/features/account/account-security-controller.svelte.ts +157 -0
  120. package/src/features/{auth → account}/account-security-route.svelte +140 -42
  121. package/src/features/api-reference/api-reference-code.svelte +64 -0
  122. package/src/features/api-reference/api-reference-content.svelte +258 -0
  123. package/src/features/api-reference/api-reference-drawer.scss +74 -0
  124. package/src/features/api-reference/api-reference-examples.ts +381 -0
  125. package/src/features/api-reference/api-reference-schema.ts +332 -0
  126. package/src/features/api-reference/api-reference.scss +190 -0
  127. package/src/features/api-reference/api-reference.svelte +74 -0
  128. package/src/features/auth/auth-brand.svelte +5 -3
  129. package/src/features/auth/auth-field.svelte +92 -0
  130. package/src/features/auth/auth-frame.svelte +5 -8
  131. package/src/features/auth/auth.scss +122 -0
  132. package/src/features/auth/create-first-user-controller.svelte.ts +20 -51
  133. package/src/features/auth/create-first-user-route.svelte +83 -122
  134. package/src/features/auth/forgot-password-route.svelte +31 -23
  135. package/src/features/auth/login-route.svelte +68 -116
  136. package/src/features/auth/logout-button.scss +34 -0
  137. package/src/features/auth/logout-button.svelte +52 -0
  138. package/src/features/auth/request-verification-route.svelte +27 -22
  139. package/src/features/auth/reset-password-route.svelte +108 -41
  140. package/src/features/auth/verify-email-route.svelte +49 -15
  141. package/src/features/bulk-edit/bulk-edit-fields.ts +23 -0
  142. package/src/features/bulk-edit/bulk-edit.scss +152 -0
  143. package/src/features/bulk-edit/field-picker.svelte +176 -0
  144. package/src/features/collections/bulk/bulk-actions.svelte +148 -0
  145. package/src/features/collections/bulk/bulk-confirmation.svelte +153 -0
  146. package/src/features/collections/bulk/bulk-editor-loader.svelte +95 -0
  147. package/src/features/collections/bulk/bulk-editor.svelte +186 -0
  148. package/src/features/collections/collection-list-cell-values.ts +144 -0
  149. package/src/features/collections/collection-list-controller.svelte.ts +268 -379
  150. package/src/features/collections/collection-list-preferences-controller.svelte.ts +54 -40
  151. package/src/features/collections/collection-list-query.ts +223 -0
  152. package/src/features/collections/collection-list-results.svelte +205 -329
  153. package/src/features/collections/collection-list-route.svelte +143 -884
  154. package/src/features/collections/collection-list.scss +261 -0
  155. package/src/features/collections/collection-list.svelte.ts +362 -0
  156. package/src/features/collections/controls/column-picker.svelte +110 -0
  157. package/src/features/collections/controls/column-pill.svelte +62 -0
  158. package/src/features/collections/controls/filter-builder.svelte +193 -0
  159. package/src/features/collections/controls/filter-row.svelte +142 -0
  160. package/src/features/collections/controls/list-controls.scss +196 -0
  161. package/src/features/collections/controls/list-options.svelte +203 -0
  162. package/src/features/collections/controls/list-pagination.scss +78 -0
  163. package/src/features/collections/controls/list-pagination.svelte +115 -0
  164. package/src/features/collections/controls/list-toolbar.svelte +132 -0
  165. package/src/features/collections/controls/sort-heading.scss +52 -0
  166. package/src/features/collections/controls/sort-heading.svelte +78 -0
  167. package/src/features/collections/list-columns.ts +43 -0
  168. package/src/features/collections/list-workspace.ts +155 -54
  169. package/src/features/dashboard/dashboard-route.svelte +82 -86
  170. package/src/features/dashboard/dashboard.scss +131 -0
  171. package/src/features/development/development-tools.svelte +20 -36
  172. package/src/features/documents/document-action-bar.svelte +438 -0
  173. package/src/features/documents/document-api-view-loader.svelte +17 -6
  174. package/src/features/documents/document-api-view.scss +120 -0
  175. package/src/features/documents/document-api-view.svelte +169 -454
  176. package/src/features/documents/document-controller.svelte.ts +526 -613
  177. package/src/features/documents/document-credentials.scss +37 -0
  178. package/src/features/documents/document-credentials.svelte +72 -0
  179. package/src/features/documents/{document-route-dirty.ts → document-credentials.ts} +25 -0
  180. package/src/features/documents/document-field-sections.scss +39 -0
  181. package/src/features/documents/document-field-sections.svelte +4 -13
  182. package/src/features/documents/document-loading.scss +24 -0
  183. package/src/features/documents/document-lock-controller.svelte.ts +187 -0
  184. package/src/features/documents/document-operation-dialogs.svelte +110 -0
  185. package/src/features/documents/document-route-loader.svelte +17 -5
  186. package/src/features/documents/document-route.svelte +305 -811
  187. package/src/features/documents/document-schedule.scss +127 -0
  188. package/src/features/documents/document-schedule.svelte +323 -0
  189. package/src/features/documents/document-title.ts +25 -0
  190. package/src/features/documents/document.scss +274 -0
  191. package/src/features/documents/live-preview-panel.scss +117 -0
  192. package/src/features/documents/live-preview-panel.svelte +167 -168
  193. package/src/features/documents/live-preview.ts +2 -10
  194. package/src/features/documents/upload-document-preview-loader.svelte +26 -23
  195. package/src/features/localization/content-locale-switcher.scss +43 -0
  196. package/src/features/localization/content-locale-switcher.svelte +24 -17
  197. package/src/features/navigation/admin-breadcrumbs.scss +56 -0
  198. package/src/features/navigation/admin-breadcrumbs.svelte +102 -37
  199. package/src/features/navigation/admin-command-menu.svelte +38 -15
  200. package/src/features/navigation/admin-navigation.scss +133 -0
  201. package/src/features/navigation/admin-navigation.svelte +169 -0
  202. package/src/features/navigation/breadcrumb-context.svelte.ts +15 -0
  203. package/src/features/navigation/navigation-preference.svelte.ts +70 -0
  204. package/src/features/reference-browser/reference-browser-editor.svelte +180 -0
  205. package/src/features/reference-browser/reference-browser-list.scss +113 -0
  206. package/src/features/reference-browser/reference-browser-list.svelte +370 -0
  207. package/src/features/reference-browser/reference-browser-loader.svelte +26 -8
  208. package/src/features/reference-browser/reference-browser-workflow.svelte.ts +335 -236
  209. package/src/features/reference-browser/reference-browser.scss +217 -0
  210. package/src/features/reference-browser/reference-browser.svelte +154 -797
  211. package/src/features/reference-browser/reference-collection-picker.svelte +116 -0
  212. package/src/features/reference-browser/reference-list-query.ts +35 -0
  213. package/src/features/uploads/bulk-upload-controller.svelte.ts +604 -0
  214. package/src/features/uploads/bulk-upload-edit-all.svelte +92 -0
  215. package/src/features/uploads/bulk-upload-editor.svelte +191 -0
  216. package/src/features/uploads/bulk-upload-file-picker.svelte +70 -0
  217. package/src/features/uploads/bulk-upload-remote.svelte +100 -0
  218. package/src/features/uploads/bulk-upload-route-loader.svelte +54 -0
  219. package/src/features/uploads/bulk-upload-route.svelte +172 -592
  220. package/src/features/uploads/bulk-upload-sidebar.svelte +125 -0
  221. package/src/features/uploads/bulk-upload.scss +526 -0
  222. package/src/features/uploads/bulk-upload.ts +105 -14
  223. package/src/features/uploads/crop-surface.ts +150 -0
  224. package/src/features/uploads/image-editor.svelte +231 -0
  225. package/src/features/uploads/rendition-preview.svelte +64 -0
  226. package/src/features/uploads/upload-control.svelte +340 -0
  227. package/src/features/uploads/upload-document-contracts.ts +30 -0
  228. package/src/features/uploads/upload-draft.svelte.ts +294 -0
  229. package/src/features/uploads/upload-metadata-fields.scss +32 -0
  230. package/src/features/uploads/upload-metadata-fields.svelte +8 -7
  231. package/src/features/uploads/upload-thumbnail.svelte +47 -0
  232. package/src/features/uploads/upload.scss +361 -0
  233. package/src/features/versions/version-comparison.svelte +361 -0
  234. package/src/features/versions/version-diff-fields.svelte +136 -0
  235. package/src/features/versions/version-diff.ts +296 -41
  236. package/src/features/versions/version-history-controller.svelte.ts +311 -0
  237. package/src/features/versions/version-history-route-loader.svelte +32 -0
  238. package/src/features/versions/version-history-route.svelte +157 -522
  239. package/src/features/versions/version-history.scss +444 -0
  240. package/src/features/versions/version-history.ts +45 -0
  241. package/src/features/versions/version-table.svelte +91 -0
  242. package/src/fields/embedded-schema-draft-editor.scss +48 -0
  243. package/src/fields/embedded-schema-draft-editor.svelte +24 -16
  244. package/src/fields/field-collapsible.svelte +14 -8
  245. package/src/fields/field-layout.scss +171 -0
  246. package/src/fields/field-layout.svelte +3 -5
  247. package/src/fields/field-renderer.svelte +17 -29
  248. package/src/fields/field-shell.svelte +1 -1
  249. package/src/fields/field-tab-layout.svelte +25 -19
  250. package/src/fields/join/join-field-controller.svelte.ts +513 -0
  251. package/src/fields/join/join-field-values.ts +111 -0
  252. package/src/fields/join/join-field.scss +189 -0
  253. package/src/fields/join/join-field.svelte +164 -332
  254. package/src/fields/live-validation-feedback.svelte +4 -3
  255. package/src/fields/local-field-editor.svelte +2 -2
  256. package/src/fields/nested/block-name-input.svelte +11 -12
  257. package/src/fields/nested/block-picker.scss +128 -0
  258. package/src/fields/nested/block-picker.svelte +81 -0
  259. package/src/fields/nested/block-placeholder.svg +2 -0
  260. package/src/fields/nested/nested-field.scss +331 -0
  261. package/src/fields/nested/nested-field.svelte +265 -245
  262. package/src/fields/nested/sortable-row.svelte +9 -7
  263. package/src/fields/plugin-field.svelte +4 -6
  264. package/src/fields/point/point-field.svelte +51 -38
  265. package/src/fields/primitive-list/primitive-list-field.svelte +63 -19
  266. package/src/fields/primitive-list/primitive-list.scss +109 -0
  267. package/src/fields/radio/radio-field.svelte +2 -1
  268. package/src/fields/relationship/relationship-field-controller.svelte.ts +81 -41
  269. package/src/fields/relationship/relationship-field.svelte +144 -175
  270. package/src/fields/relationship/relationship-lookup-controller.svelte.ts +58 -30
  271. package/src/fields/relationship/relationship-query.ts +2 -10
  272. package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +36 -62
  273. package/src/fields/relationship/relationship-quick-picker.svelte +178 -227
  274. package/src/fields/relationship/relationship-selection.svelte +120 -0
  275. package/src/fields/relationship/relationship-value.ts +46 -7
  276. package/src/fields/relationship/relationship.scss +182 -0
  277. package/src/fields/scalar/code-field.svelte +101 -0
  278. package/src/fields/scalar/date-control-value.ts +13 -0
  279. package/src/fields/scalar/scalar-field.svelte +25 -67
  280. package/src/fields/select/select-field.svelte +132 -94
  281. package/src/fields/text/slug-field.scss +28 -0
  282. package/src/fields/text/slug-field.svelte +19 -15
  283. package/src/fields/text/text-field.svelte +1 -2
  284. package/src/fields/ui/ui-field.svelte +7 -4
  285. package/src/fields/virtual/virtual-field.svelte +2 -5
  286. package/src/index.ts +1 -2
  287. package/src/routing.ts +2 -0
  288. package/src/app/deferred-redirect.svelte +0 -23
  289. package/src/components/code-block/code-block.svelte +0 -107
  290. package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +0 -50
  291. package/src/components/ui/bulk-action-bar/index.ts +0 -1
  292. package/src/components/ui/button/index.ts +0 -8
  293. package/src/components/ui/calendar/calendar-caption.svelte +0 -72
  294. package/src/components/ui/calendar/calendar-cell.svelte +0 -20
  295. package/src/components/ui/calendar/calendar-day.svelte +0 -27
  296. package/src/components/ui/calendar/calendar-grid-row.svelte +0 -13
  297. package/src/components/ui/calendar/calendar-grid.svelte +0 -17
  298. package/src/components/ui/calendar/calendar-head-cell.svelte +0 -20
  299. package/src/components/ui/calendar/calendar-header.svelte +0 -20
  300. package/src/components/ui/calendar/calendar-heading.svelte +0 -17
  301. package/src/components/ui/calendar/calendar-month-select.svelte +0 -45
  302. package/src/components/ui/calendar/calendar-month.svelte +0 -16
  303. package/src/components/ui/calendar/calendar-months.svelte +0 -20
  304. package/src/components/ui/calendar/calendar-nav.svelte +0 -20
  305. package/src/components/ui/calendar/calendar-next-button.svelte +0 -30
  306. package/src/components/ui/calendar/calendar-prev-button.svelte +0 -30
  307. package/src/components/ui/calendar/calendar-year-select.svelte +0 -44
  308. package/src/components/ui/calendar/calendar.svelte +0 -118
  309. package/src/components/ui/calendar/index.ts +0 -1
  310. package/src/components/ui/checkbox/checkbox.svelte +0 -35
  311. package/src/components/ui/checkbox/index.ts +0 -1
  312. package/src/components/ui/dialog/dialog-close.svelte +0 -11
  313. package/src/components/ui/dialog/dialog-content.svelte +0 -52
  314. package/src/components/ui/dialog/dialog-description.svelte +0 -20
  315. package/src/components/ui/dialog/dialog-footer.svelte +0 -20
  316. package/src/components/ui/dialog/dialog-header.svelte +0 -20
  317. package/src/components/ui/dialog/dialog-overlay.svelte +0 -20
  318. package/src/components/ui/dialog/dialog-title.svelte +0 -17
  319. package/src/components/ui/dialog/dialog-trigger.svelte +0 -11
  320. package/src/components/ui/dialog/dialog.svelte +0 -7
  321. package/src/components/ui/dialog/index.ts +0 -8
  322. package/src/components/ui/empty-state/empty-state.svelte +0 -34
  323. package/src/components/ui/empty-state/index.ts +0 -1
  324. package/src/components/ui/input/index.ts +0 -1
  325. package/src/components/ui/pagination/index.ts +0 -1
  326. package/src/components/ui/pagination/pagination.svelte +0 -88
  327. package/src/components/ui/radio-group/radio-group-item.svelte +0 -17
  328. package/src/components/ui/slider/index.ts +0 -3
  329. package/src/components/ui/slider/slider-content.svelte +0 -59
  330. package/src/components/ui/slider/slider.svelte +0 -35
  331. package/src/components/ui/table/index.ts +0 -6
  332. package/src/components/ui/table/table-body.svelte +0 -20
  333. package/src/components/ui/table/table-cell.svelte +0 -23
  334. package/src/components/ui/table/table-head.svelte +0 -23
  335. package/src/components/ui/table/table-header.svelte +0 -20
  336. package/src/components/ui/table/table-row.svelte +0 -23
  337. package/src/components/ui/table/table.svelte +0 -22
  338. package/src/components/ui/textarea/index.ts +0 -1
  339. package/src/components/ui/tooltip/index.ts +0 -4
  340. package/src/components/ui/tooltip/tooltip-content.svelte +0 -50
  341. package/src/components/ui/tooltip/tooltip-provider.svelte +0 -7
  342. package/src/components/ui/tooltip/tooltip-trigger.svelte +0 -7
  343. package/src/components/ui/tooltip/tooltip.svelte +0 -7
  344. package/src/features/auth/account-menu.svelte +0 -159
  345. package/src/features/auth/account-security-controller.svelte.ts +0 -119
  346. package/src/features/collections/collection-list-bulk-actions.svelte +0 -143
  347. package/src/features/collections/collection-list-bulk-editor.svelte +0 -157
  348. package/src/features/collections/collection-list-filter-popover.svelte +0 -238
  349. package/src/features/collections/collection-list-workspace-toolbar.svelte +0 -409
  350. package/src/features/collections/collection-table-skeleton.svelte +0 -44
  351. package/src/features/documents/document-heading.ts +0 -20
  352. package/src/features/documents/upload-document-preview.svelte +0 -525
@@ -0,0 +1,340 @@
1
+ <script lang="ts">
2
+ import { Dialog } from "bits-ui";
3
+ import { Button, Input, buttonVariants } from "@riducms/ui";
4
+ import { getAdminI18n } from "@riducms/plugin";
5
+ import { isRecord, type SchemaUploadSettings } from "@riducms/protocol";
6
+ import XIcon from "~icons/lucide/x";
7
+ import CopyIcon from "~icons/lucide/copy";
8
+ import FileIcon from "~icons/lucide/file";
9
+ import type { UploadDraft } from "@admin/features/uploads/upload-draft.svelte";
10
+ import RenditionPreview from "@admin/features/uploads/rendition-preview.svelte";
11
+ import UploadThumbnail from "@admin/features/uploads/upload-thumbnail.svelte";
12
+ import "@admin/features/uploads/upload.scss";
13
+
14
+ let {
15
+ draft,
16
+ settings,
17
+ editable = false,
18
+ }: { draft: UploadDraft; settings: SchemaUploadSettings; editable?: boolean } = $props();
19
+ const i18n = getAdminI18n();
20
+ let chooser: HTMLInputElement;
21
+ let urlMode = $state(false);
22
+ let remoteURL = $state("");
23
+ let dragging = $state(false);
24
+ let dialog = $state<"edit" | "sizes">("edit");
25
+ let dialogOpen = $state(false);
26
+ let copied = $state(false);
27
+ const sizes = $derived(
28
+ isRecord(draft.document?.sizes)
29
+ ? Object.entries(draft.document.sizes).filter(
30
+ (entry): entry is [string, Record<string, unknown>] => isRecord(entry[1])
31
+ )
32
+ : []
33
+ );
34
+ const renditions = $derived<[string, Record<string, unknown>][]>(
35
+ draft.document ? [[i18n.t("uploads:original"), draft.document], ...sizes] : sizes
36
+ );
37
+ const bytes = $derived(draft.file?.size ?? Number(draft.document?.filesize ?? 0));
38
+ const dimensions = $derived(
39
+ draft.source
40
+ ? `${Math.round(((draft.currentImage.cropWidth || 100) / 100) * draft.source.width)} × ${Math.round(((draft.currentImage.cropHeight || 100) / 100) * draft.source.height)}`
41
+ : `${draft.document?.width} × ${draft.document?.height}`
42
+ );
43
+
44
+ $effect(() => {
45
+ const editingImage = draft.editingImage;
46
+ if (!editable) {
47
+ dragging = false;
48
+ urlMode = false;
49
+ remoteURL = "";
50
+ if (editingImage || draft.busy) draft.endImageEdit();
51
+ if (dialog === "edit") dialogOpen = false;
52
+ }
53
+ if (dialog === "edit" && dialogOpen && !editingImage) dialogOpen = false;
54
+ });
55
+ $effect(() => () => draft.endImageEdit());
56
+
57
+ function closeImageEditor() {
58
+ dialogOpen = false;
59
+ draft.endImageEdit();
60
+ }
61
+
62
+ async function importURL() {
63
+ if (!editable || draft.busy) return;
64
+ await draft.fromURL(remoteURL, settings);
65
+ if (draft.file) {
66
+ urlMode = false;
67
+ remoteURL = "";
68
+ }
69
+ }
70
+
71
+ function choose(event: Event) {
72
+ if (!(event.currentTarget instanceof HTMLInputElement)) return;
73
+ const file = event.currentTarget.files?.[0];
74
+ if (editable && !draft.busy && file) draft.select(file, settings);
75
+ event.currentTarget.value = "";
76
+ }
77
+
78
+ function drop(event: DragEvent) {
79
+ event.preventDefault();
80
+ dragging = false;
81
+ const file = event.dataTransfer?.files[0];
82
+ if (editable && !draft.busy && file) draft.select(file, settings);
83
+ }
84
+
85
+ async function copyURL() {
86
+ try {
87
+ await navigator.clipboard.writeText(
88
+ new URL(String(draft.document?.url ?? ""), window.location.origin).href
89
+ );
90
+ copied = true;
91
+ } catch {
92
+ copied = false;
93
+ }
94
+ }
95
+ </script>
96
+
97
+ <!-- File drops are an alternative to the keyboard-accessible chooser below. -->
98
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
99
+ <div
100
+ role="region"
101
+ aria-label={i18n.t("uploads:assetPreview")}
102
+ class={["ridu-upload", { "ridu-upload--dragging": dragging }]}
103
+ ondragover={(event) => {
104
+ event.preventDefault();
105
+ if (editable) dragging = true;
106
+ }}
107
+ ondragleave={(event) => {
108
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) dragging = false;
109
+ }}
110
+ ondrop={drop}
111
+ >
112
+ <input
113
+ bind:this={chooser}
114
+ hidden
115
+ type="file"
116
+ accept={settings.mimeTypes.join(",")}
117
+ aria-label={i18n.t("uploads:chooseFile")}
118
+ tabindex="-1"
119
+ disabled={!editable || draft.busy}
120
+ onchange={choose}
121
+ />
122
+
123
+ {#if draft.present}
124
+ <div class="ridu-upload-thumbnail">
125
+ {#if draft.mimeType.startsWith("image/")}
126
+ <UploadThumbnail {draft} />
127
+ {:else}
128
+ <FileIcon />
129
+ {/if}
130
+ </div>
131
+ <div class="ridu-upload-details">
132
+ <div class="ridu-upload-name">
133
+ {#if draft.file}
134
+ <Input
135
+ aria-label={i18n.t("uploads:filename")}
136
+ disabled={!editable}
137
+ bind:value={() => draft.filename, draft.setFilename}
138
+ />
139
+ {:else}
140
+ <a href={String(draft.document?.url ?? "")} target="_blank" rel="noreferrer">
141
+ {draft.filename}
142
+ </a>
143
+ <Button
144
+ variant="ghost"
145
+ size="icon-xs"
146
+ aria-label={i18n.t(copied ? "general:copied" : "general:copy")}
147
+ onclick={copyURL}
148
+ >
149
+ <CopyIcon />
150
+ </Button>
151
+ {/if}
152
+ </div>
153
+ <p>
154
+ {i18n.formatNumber(bytes / 1024, {
155
+ maximumFractionDigits: 0,
156
+ })}KB
157
+ {#if draft.mimeType.startsWith("image/")}
158
+ · {dimensions}
159
+ {/if}
160
+ · {draft.mimeType}
161
+ </p>
162
+ <div class="ridu-upload-actions">
163
+ {#if !draft.file && sizes.length}
164
+ <Button
165
+ size="xs"
166
+ variant="secondary"
167
+ onclick={() => {
168
+ dialog = "sizes";
169
+ dialogOpen = true;
170
+ }}
171
+ >
172
+ {i18n.t("uploads:previewSizes")}
173
+ </Button>
174
+ {/if}
175
+ {#if editable && draft.editableImage}
176
+ <Button
177
+ size="xs"
178
+ variant="secondary"
179
+ disabled={draft.busy}
180
+ onclick={() => {
181
+ dialog = "edit";
182
+ dialogOpen = true;
183
+ draft.beginImageEdit();
184
+ }}
185
+ >
186
+ {i18n.t("uploads:editImage")}
187
+ </Button>
188
+ {/if}
189
+ </div>
190
+ </div>
191
+ {#if editable}
192
+ <Button
193
+ class="ridu-upload-remove"
194
+ size="icon-xs"
195
+ variant="ghost"
196
+ aria-label={i18n.t("uploads:removeFilename", { filename: draft.filename })}
197
+ disabled={draft.busy}
198
+ onclick={draft.remove}
199
+ >
200
+ <XIcon />
201
+ </Button>
202
+ {/if}
203
+ {:else if urlMode}
204
+ <div class="ridu-upload-url">
205
+ <Input
206
+ type="url"
207
+ aria-label={i18n.t("uploads:pasteURL")}
208
+ placeholder="https://"
209
+ bind:value={remoteURL}
210
+ disabled={!editable || draft.busy}
211
+ onkeydown={(event) => {
212
+ if (event.key === "Enter") {
213
+ event.preventDefault();
214
+ importURL();
215
+ }
216
+ }}
217
+ />
218
+ <Button
219
+ size="sm"
220
+ variant="secondary"
221
+ disabled={!editable || draft.busy || !remoteURL.trim()}
222
+ onclick={importURL}
223
+ >
224
+ {i18n.t(draft.busy ? "uploads:importing" : "uploads:addFile")}
225
+ </Button>
226
+ <Button
227
+ size="icon-sm"
228
+ variant="ghost"
229
+ aria-label={i18n.t("general:cancel")}
230
+ onclick={() => {
231
+ urlMode = false;
232
+ draft.cancelPending();
233
+ }}
234
+ >
235
+ <XIcon />
236
+ </Button>
237
+ </div>
238
+ {:else}
239
+ <div class="ridu-upload-choose">
240
+ <Button
241
+ size="sm"
242
+ variant="secondary"
243
+ disabled={!editable || draft.busy}
244
+ onclick={() => chooser.click()}
245
+ >
246
+ {i18n.t("uploads:chooseFile")}
247
+ </Button>
248
+ <Button
249
+ size="sm"
250
+ variant="secondary"
251
+ disabled={!editable || draft.busy}
252
+ onclick={() => {
253
+ urlMode = true;
254
+ }}
255
+ >
256
+ {i18n.t("uploads:pasteURL")}
257
+ </Button>
258
+ <span>{i18n.t("uploads:dropFile")}</span>
259
+ </div>
260
+ {/if}
261
+ </div>
262
+ {#if draft.error}
263
+ <p role="alert" class="ridu-upload-error">{draft.error}</p>
264
+ {/if}
265
+
266
+ <Dialog.Root
267
+ open={dialogOpen}
268
+ onOpenChange={(open) => {
269
+ if (!open) {
270
+ dialogOpen = false;
271
+ if (dialog === "edit") draft.endImageEdit();
272
+ else draft.cancelPending();
273
+ }
274
+ }}
275
+ >
276
+ <Dialog.Portal>
277
+ <Dialog.Overlay class="ridu-upload-drawer-overlay" />
278
+ <Dialog.Content class="ridu-upload-drawer">
279
+ <header class="ridu-upload-drawer-header">
280
+ <Dialog.Title level={2} class="ridu-upload-drawer-title">
281
+ {i18n.t(dialog === "edit" ? "uploads:editingFilename" : "uploads:sizesFor", {
282
+ filename: draft.filename,
283
+ })}
284
+ </Dialog.Title>
285
+ {#if dialog === "sizes"}
286
+ <Dialog.Close type="button" class={buttonVariants({ variant: "outline", size: "sm" })}>
287
+ {i18n.t("general:close")}
288
+ </Dialog.Close>
289
+ {/if}
290
+ </header>
291
+ <Dialog.Description class="ridu-upload-description">
292
+ {i18n.t(
293
+ dialog === "edit" ? "uploads:imageEditDescription" : "uploads:previewGeneratedSizes"
294
+ )}
295
+ </Dialog.Description>
296
+
297
+ {#if dialog === "edit"}
298
+ {#if draft.source}
299
+ {#await import("@admin/features/uploads/image-editor.svelte")}
300
+ <p class="ridu-upload-drawer-message">{i18n.t("uploads:loadingAssetPreview")}</p>
301
+ {:then { default: Editor }}
302
+ <Editor
303
+ source={draft.source}
304
+ initial={draft.currentImage}
305
+ oncancel={closeImageEditor}
306
+ onapply={(edit) => {
307
+ if (!editable) {
308
+ closeImageEditor();
309
+ return;
310
+ }
311
+ draft.applyImage(edit);
312
+ closeImageEditor();
313
+ }}
314
+ />
315
+ {:catch}
316
+ <div class="ridu-upload-drawer-message">
317
+ <p role="alert">{i18n.t("uploads:previewLoadFailed")}</p>
318
+ <Dialog.Close class={buttonVariants({ variant: "outline" })}>
319
+ {i18n.t("general:cancel")}
320
+ </Dialog.Close>
321
+ </div>
322
+ {/await}
323
+ {:else}
324
+ <div class="ridu-upload-drawer-message">
325
+ <p role={draft.error ? "alert" : "status"}>
326
+ {draft.error ?? i18n.t("uploads:loadingAssetPreview")}
327
+ </p>
328
+ <Button variant="outline" onclick={closeImageEditor}>
329
+ {i18n.t("general:cancel")}
330
+ </Button>
331
+ </div>
332
+ {/if}
333
+ {:else if dialog === "sizes"}
334
+ {#if draft.document}
335
+ <RenditionPreview {renditions} document={draft.document} />
336
+ {/if}
337
+ {/if}
338
+ </Dialog.Content>
339
+ </Dialog.Portal>
340
+ </Dialog.Root>
@@ -0,0 +1,30 @@
1
+ import type { UploadImageEdit } from "@riducms/protocol";
2
+ export type { UploadImageEdit } from "@riducms/protocol";
3
+
4
+ export const uploadImageEditFields = [
5
+ "focalX",
6
+ "focalY",
7
+ "cropX",
8
+ "cropY",
9
+ "cropWidth",
10
+ "cropHeight",
11
+ ] as const satisfies readonly (keyof UploadImageEdit)[];
12
+
13
+ export const uploadMetadataFields = [
14
+ "filename",
15
+ "mimeType",
16
+ "filesize",
17
+ "url",
18
+ "objectKey",
19
+ "width",
20
+ "height",
21
+ "sizes",
22
+ "source",
23
+ ...uploadImageEditFields,
24
+ ] as const;
25
+
26
+ const uploadMetadataFieldSet = new Set<string>(uploadMetadataFields);
27
+
28
+ export function isUploadMetadataField(name: string) {
29
+ return uploadMetadataFieldSet.has(name);
30
+ }
@@ -0,0 +1,294 @@
1
+ import type { AdminI18n } from "@riducms/plugin";
2
+ import type { AdminClient, AdminDocument } from "@admin/core/api/admin-client";
3
+ import { type SchemaUploadSettings, type UploadImageEdit } from "@riducms/protocol";
4
+
5
+ export interface UploadSource {
6
+ url: string;
7
+ width: number;
8
+ height: number;
9
+ }
10
+
11
+ export const originalImageEdit = (): UploadImageEdit => ({
12
+ focalX: 50,
13
+ focalY: 50,
14
+ cropX: 0,
15
+ cropY: 0,
16
+ cropWidth: 0,
17
+ cropHeight: 0,
18
+ });
19
+
20
+ export function documentImageEdit(document?: AdminDocument): UploadImageEdit {
21
+ const edit = originalImageEdit();
22
+ for (const field of Object.keys(edit) as (keyof UploadImageEdit)[]) {
23
+ const value = document?.[field];
24
+ if (typeof value === "number") edit[field] = value;
25
+ }
26
+ return edit;
27
+ }
28
+
29
+ /** The document owns this draft; an editor dialog works on a separate copy. */
30
+ export class UploadDraft {
31
+ #document = $state.raw<AdminDocument>();
32
+ #file = $state.raw<File>();
33
+ #filename = $state("");
34
+ #image = $state.raw<UploadImageEdit>();
35
+ #source = $state.raw<UploadSource>();
36
+ #removed = $state(false);
37
+ #busy = $state(false);
38
+ #editingImage = $state(false);
39
+ #error = $state<string>();
40
+ #request?: AbortController;
41
+ #generation = 0;
42
+ #revision = 0;
43
+
44
+ constructor(
45
+ private readonly client: AdminClient,
46
+ private readonly collection: () => string,
47
+ private readonly i18n: AdminI18n
48
+ ) {}
49
+
50
+ get revision() {
51
+ return this.#revision;
52
+ }
53
+
54
+ get document() {
55
+ return this.#document;
56
+ }
57
+
58
+ get file() {
59
+ return this.#file;
60
+ }
61
+
62
+ get filename() {
63
+ return this.#filename;
64
+ }
65
+
66
+ get image() {
67
+ return this.#image;
68
+ }
69
+
70
+ get source() {
71
+ return this.#source;
72
+ }
73
+
74
+ get busy() {
75
+ return this.#busy;
76
+ }
77
+
78
+ get editingImage() {
79
+ return this.#editingImage;
80
+ }
81
+
82
+ get error() {
83
+ return this.#error;
84
+ }
85
+
86
+ get removed() {
87
+ return this.#removed;
88
+ }
89
+
90
+ get present() {
91
+ return !this.#removed && (this.#file !== undefined || this.#document !== undefined);
92
+ }
93
+
94
+ get mimeType() {
95
+ return this.#file?.type ?? String(this.#document?.mimeType ?? "");
96
+ }
97
+
98
+ get editableImage() {
99
+ return this.mimeType === "image/jpeg" || this.mimeType === "image/png";
100
+ }
101
+
102
+ get previewURL() {
103
+ return this.#source?.url ?? String(this.#document?.url ?? "");
104
+ }
105
+
106
+ get currentImage() {
107
+ return this.#image ?? documentImageEdit(this.#file ? undefined : this.#document);
108
+ }
109
+
110
+ get dirty() {
111
+ return (
112
+ this.#file !== undefined ||
113
+ this.#removed ||
114
+ this.#image !== undefined ||
115
+ this.#filename !== String(this.#document?.filename ?? "")
116
+ );
117
+ }
118
+
119
+ setFilename = (name: string) => {
120
+ this.#revision++;
121
+ this.#filename = name;
122
+ };
123
+
124
+ reset = (document?: AdminDocument) => {
125
+ this.#revision++;
126
+ this.dispose();
127
+ this.#document = document;
128
+ this.#file = undefined;
129
+ this.#filename = String(document?.filename ?? "");
130
+ this.#image = undefined;
131
+ this.#removed = false;
132
+ this.#busy = false;
133
+ this.#error = undefined;
134
+ };
135
+
136
+ remove = () => {
137
+ this.#revision++;
138
+ this.dispose();
139
+ this.#file = undefined;
140
+ this.#image = undefined;
141
+ this.#filename = "";
142
+ this.#removed = this.#document !== undefined;
143
+ this.#error = undefined;
144
+ this.#busy = false;
145
+ };
146
+
147
+ select = async (file: File, settings: SchemaUploadSettings) => {
148
+ this.cancelPending();
149
+ const generation = this.#generation;
150
+ this.#busy = true;
151
+ this.#error = undefined;
152
+ try {
153
+ if (file.size > settings.maxFileSize) throw new Error(this.i18n.t("uploads:fileTooLarge"));
154
+ if (
155
+ !settings.mimeTypes.some(
156
+ (type) =>
157
+ type === file.type || (type.endsWith("/*") && file.type.startsWith(type.slice(0, -1)))
158
+ )
159
+ ) {
160
+ throw new Error(this.i18n.t("uploads:fileTypeDenied"));
161
+ }
162
+ const url = URL.createObjectURL(file);
163
+ let source: UploadSource;
164
+ try {
165
+ source = await imageSource(url, file.type);
166
+ } catch (cause) {
167
+ URL.revokeObjectURL(url);
168
+ throw cause;
169
+ }
170
+ if (generation !== this.#generation) {
171
+ URL.revokeObjectURL(url);
172
+ return;
173
+ }
174
+ if (this.#source) URL.revokeObjectURL(this.#source.url);
175
+ this.#revision++;
176
+ this.#source = source;
177
+ this.#file = file;
178
+ this.#filename = file.name;
179
+ this.#image = undefined;
180
+ this.#removed = false;
181
+ } catch (cause) {
182
+ if (generation === this.#generation)
183
+ this.#error =
184
+ cause instanceof Error ? cause.message : this.i18n.t("uploads:fileOpenFailed");
185
+ } finally {
186
+ if (generation === this.#generation) this.#busy = false;
187
+ }
188
+ };
189
+
190
+ fromURL = async (url: string, settings: SchemaUploadSettings) => {
191
+ this.#request?.abort();
192
+ const request = new AbortController();
193
+ this.#request = request;
194
+ this.#busy = true;
195
+ this.#error = undefined;
196
+ try {
197
+ const file = await this.client.previewUploadFromURL(this.collection(), url, {
198
+ id: this.#document?.id,
199
+ signal: request.signal,
200
+ });
201
+ if (!request.signal.aborted)
202
+ await this.select(new File([file.blob], file.filename, { type: file.blob.type }), settings);
203
+ } catch (cause) {
204
+ if (!request.signal.aborted)
205
+ this.#error =
206
+ cause instanceof Error ? cause.message : this.i18n.t("uploads:fileDownloadFailed");
207
+ } finally {
208
+ if (this.#request === request) {
209
+ this.#request = undefined;
210
+ this.#busy = false;
211
+ }
212
+ }
213
+ };
214
+
215
+ loadSource = async () => {
216
+ if (this.#source || !this.#document || !this.editableImage) return;
217
+ const request = new AbortController();
218
+ this.#request?.abort();
219
+ this.#request = request;
220
+ this.#busy = true;
221
+ this.#error = undefined;
222
+ try {
223
+ const blob = await this.client.readUploadSource(this.collection(), this.#document.id, {
224
+ signal: request.signal,
225
+ });
226
+ if (request.signal.aborted) return;
227
+ const url = URL.createObjectURL(blob);
228
+ let source: UploadSource;
229
+ try {
230
+ source = await imageSource(url, blob.type);
231
+ } catch (cause) {
232
+ URL.revokeObjectURL(url);
233
+ throw cause;
234
+ }
235
+ if (request.signal.aborted) {
236
+ URL.revokeObjectURL(url);
237
+ return;
238
+ }
239
+ this.#source = source;
240
+ } catch (cause) {
241
+ if (!request.signal.aborted)
242
+ this.#error =
243
+ cause instanceof Error ? cause.message : this.i18n.t("uploads:previewLoadFailed");
244
+ } finally {
245
+ if (this.#request === request) {
246
+ this.#request = undefined;
247
+ this.#busy = false;
248
+ }
249
+ }
250
+ };
251
+
252
+ beginImageEdit = () => {
253
+ if (!this.editableImage) return;
254
+ this.#editingImage = true;
255
+ return this.loadSource();
256
+ };
257
+
258
+ endImageEdit = () => {
259
+ this.#editingImage = false;
260
+ this.cancelPending();
261
+ };
262
+
263
+ applyImage = (edit: UploadImageEdit) => {
264
+ this.#revision++;
265
+ const original = documentImageEdit(this.#file ? undefined : this.#document);
266
+ this.#image = Object.keys(original).every(
267
+ (key) => original[key as keyof UploadImageEdit] === edit[key as keyof UploadImageEdit]
268
+ )
269
+ ? undefined
270
+ : { ...edit };
271
+ };
272
+
273
+ cancelPending = () => {
274
+ this.#generation++;
275
+ this.#request?.abort();
276
+ this.#request = undefined;
277
+ this.#busy = false;
278
+ };
279
+
280
+ dispose = () => {
281
+ this.#editingImage = false;
282
+ this.cancelPending();
283
+ if (this.#source) URL.revokeObjectURL(this.#source.url);
284
+ this.#source = undefined;
285
+ };
286
+ }
287
+
288
+ async function imageSource(url: string, mimeType: string): Promise<UploadSource> {
289
+ if (!mimeType.startsWith("image/")) return { url, width: 0, height: 0 };
290
+ const image = new Image();
291
+ image.src = url;
292
+ await image.decode();
293
+ return { url, width: image.naturalWidth, height: image.naturalHeight };
294
+ }
@@ -0,0 +1,32 @@
1
+ @use "@riducms/ui/styles.scss" as ui;
2
+
3
+ @layer ridu.components {
4
+ .ridu-upload-metadata {
5
+ display: grid;
6
+ grid-template-columns: repeat(2, minmax(0, 1fr));
7
+ gap: 20px;
8
+ }
9
+
10
+ .ridu-upload-metadata__field {
11
+ display: grid;
12
+ align-content: start;
13
+ gap: 5px;
14
+ min-inline-size: 0;
15
+ color: var(--ink-label);
16
+ font-size: var(--font-size-body);
17
+ }
18
+
19
+ .ridu-upload-metadata__select {
20
+ inline-size: 100%;
21
+ }
22
+
23
+ .ridu-upload-metadata__placeholder {
24
+ color: var(--ink-placeholder);
25
+ }
26
+
27
+ @media (max-width: ui.$small) {
28
+ .ridu-upload-metadata {
29
+ grid-template-columns: minmax(0, 1fr);
30
+ }
31
+ }
32
+ }