@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,24 +1,16 @@
1
1
  <script lang="ts">
2
+ import { isSortable } from "@dnd-kit-svelte/svelte/sortable";
2
3
  import { resolveBlockTypes } from "@riducms/protocol";
3
4
 
4
5
  import type { SchemaField } from "@riducms/protocol";
5
- import { fieldControlARIA } from "@riducms/ui";
6
+ import { fieldControlARIA, Button, buttonVariants } from "@riducms/ui";
6
7
  import { DragDropProvider, type DragDropEvents } from "@dnd-kit-svelte/svelte";
7
8
  import ChevronDownIcon from "~icons/lucide/chevron-down";
8
9
  import CirclePlusIcon from "~icons/lucide/circle-plus";
9
10
  import EllipsisIcon from "~icons/lucide/ellipsis";
10
11
  import GripVerticalIcon from "~icons/lucide/grip-vertical";
11
- import LayoutTemplateIcon from "~icons/lucide/layout-template";
12
- import SearchIcon from "~icons/lucide/search";
13
12
 
14
- import { Button, buttonVariants } from "@admin/components/ui/button";
15
- import {
16
- Dialog,
17
- DialogContent,
18
- DialogDescription,
19
- DialogHeader,
20
- DialogTitle,
21
- } from "@admin/components/ui/dialog";
13
+ import BlockPicker from "@admin/fields/nested/block-picker.svelte";
22
14
  import {
23
15
  DropdownMenu,
24
16
  DropdownMenuContent,
@@ -26,8 +18,9 @@
26
18
  DropdownMenuSeparator,
27
19
  DropdownMenuTrigger,
28
20
  } from "@admin/components/ui/dropdown-menu";
29
- import { Input } from "@admin/components/ui/input";
21
+
30
22
  import type { FormController } from "@admin/core/forms/form-controller.svelte";
23
+ import { FieldBindingLifetime } from "@admin/core/forms/field-binding-lifetime";
31
24
  import { initialFormValues } from "@admin/core/forms/form-schema";
32
25
  import { immutableRowLabelSnapshot } from "@admin/core/plugins/row-label-registry";
33
26
  import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
@@ -42,6 +35,7 @@
42
35
  writeFieldClipboard,
43
36
  } from "@admin/fields/field-clipboard";
44
37
  import { scopeRepeatedRowField } from "@admin/fields/nested/scoped-field";
38
+ import "@admin/fields/nested/nested-field.scss";
45
39
  import SortableRow from "@admin/fields/nested/sortable-row.svelte";
46
40
  import BlockHeader from "@admin/fields/nested/block-header.svelte";
47
41
  import { blockHeaderValue, visibleBlockChild } from "@admin/fields/nested/block-header";
@@ -49,11 +43,11 @@
49
43
  let { field, form }: { field: SchemaField; form: FormController } = $props();
50
44
  const runtime = getAdminRuntime();
51
45
  const customRowLabel = $derived(runtime.rowLabels.resolve(field));
52
- const blockTypes = $derived(
53
- new Map(resolveBlockTypes(field.blocks).map((block) => [block.slug, block]))
54
- );
46
+ const blocks = $derived(resolveBlockTypes(field.blocks));
47
+ const blockTypes = $derived(new Map(blocks.map((block) => [block.slug, block])));
55
48
  const rows = $derived((form.get(field.path) as Record<string, unknown>[] | undefined) ?? []);
56
- const issues = $derived(form.issuesFor(field.path).filter((issue) => issue.path === field.path));
49
+ const allIssues = $derived(form.issuesFor(field.path));
50
+ const issues = $derived(allIssues.filter((issue) => issue.path === field.path));
57
51
  const controlARIA = $derived(
58
52
  fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
59
53
  );
@@ -64,31 +58,22 @@
64
58
  (field.type === "blocks" ? field.blocks?.maxRows : field.nested?.maxRows) ?? 0
65
59
  );
66
60
  const unknownRows = $derived(
67
- field.type === "blocks" &&
68
- rows.some(
69
- (row) => !resolveBlockTypes(field.blocks).some((block) => block.slug === row.blockType)
70
- )
61
+ field.type === "blocks" && rows.some((row) => !blockTypes.has(String(row.blockType)))
71
62
  );
72
63
  const readOnly = $derived(field.admin.readOnly === true || unknownRows);
73
64
  const editingBlocked = $derived(readOnly || form.editingBlocked);
74
65
  const canAdd = $derived(!editingBlocked && (maxRows === 0 || rows.length < maxRows));
75
- let collapsed = $state(new Set<string>());
66
+ // Only the row-error click handler reads this DOM binding.
67
+ // svelte-ignore non_reactive_update
68
+ let repeatedRoot: HTMLElement;
69
+ let collapsed = $state.raw(new Set<string>());
76
70
  let clipboardMessage = $state("");
77
71
  let blockPickerOpen = $state(false);
78
- let blockQuery = $state("");
79
72
  let insertAfter = $state<string>();
80
- const visibleBlockTypes = $derived(
81
- (resolveBlockTypes(field.blocks) ?? []).filter((block) =>
82
- block.labels.singular
83
- .toLocaleLowerCase(runtime.i18n.language)
84
- .includes(blockQuery.trim().toLocaleLowerCase(runtime.i18n.language))
85
- )
86
- );
87
73
 
88
74
  function changeBlockPickerOpen(open: boolean) {
89
75
  blockPickerOpen = open;
90
76
  if (!open) {
91
- blockQuery = "";
92
77
  insertAfter = undefined;
93
78
  }
94
79
  }
@@ -102,11 +87,28 @@
102
87
  }
103
88
 
104
89
  const inheritedFields = $derived((field.nested?.fields ?? []).map(inheritedField));
90
+ // A clipboard permission prompt may outlive the editor or its current field occurrence.
91
+ const pendingPastes = new Set<FieldBindingLifetime>();
92
+ $effect(() => () => {
93
+ for (const paste of pendingPastes) paste.destroy();
94
+ pendingPastes.clear();
95
+ });
96
+
97
+ function beginPaste(owner: FormController) {
98
+ const lifetime = new FieldBindingLifetime(
99
+ owner,
100
+ () => field,
101
+ () => runtime.manifestRevision
102
+ );
103
+ pendingPastes.add(lifetime);
104
+ lifetime.onDestroy(() => pendingPastes.delete(lifetime));
105
+ return lifetime;
106
+ }
105
107
 
106
108
  function createRow(blockType?: string) {
107
109
  const children =
108
110
  field.type === "blocks"
109
- ? (resolveBlockTypes(field.blocks).find((block) => block.slug === blockType)?.fields ?? [])
111
+ ? (blockTypes.get(blockType ?? "")?.fields ?? [])
110
112
  : (field.nested?.fields ?? []);
111
113
  return {
112
114
  ...initialFormValues(children),
@@ -130,7 +132,6 @@
130
132
  updateRows(next);
131
133
  insertAfter = undefined;
132
134
  blockPickerOpen = false;
133
- blockQuery = "";
134
135
  }
135
136
 
136
137
  function addBelow(index: number) {
@@ -152,40 +153,54 @@
152
153
 
153
154
  async function pasteField() {
154
155
  if (editingBlocked) return;
155
- const value = compatibleClipboardValue(await readFieldClipboard(), field, "field");
156
- if (value === undefined) {
157
- clipboardMessage = runtime.i18n.t("errors:clipboardField");
158
- return;
159
- }
160
- if ((field.type === "array" || field.type === "blocks") && !Array.isArray(value)) {
161
- clipboardMessage = runtime.i18n.t("errors:clipboardRows");
162
- return;
163
- }
164
- if (
165
- field.type === "group" &&
166
- (value === null || typeof value !== "object" || Array.isArray(value))
167
- ) {
168
- clipboardMessage = runtime.i18n.t("errors:clipboardGroup");
169
- return;
170
- }
171
- if (Array.isArray(value) && value.length < minRows) {
172
- clipboardMessage = runtime.i18n.t("errors:clipboardMinRows", { count: minRows });
173
- return;
174
- }
175
- if (Array.isArray(value) && maxRows > 0 && value.length > maxRows) {
176
- clipboardMessage = runtime.i18n.t("errors:clipboardMaxRows", { count: maxRows });
177
- return;
156
+ const owner = form;
157
+ const lifetime = beginPaste(owner);
158
+ try {
159
+ const payload = await readFieldClipboard();
160
+ if (form !== owner || lifetime.readOnly || editingBlocked) return;
161
+ const target = lifetime.schema;
162
+ const value = compatibleClipboardValue(payload, target, "field");
163
+ if (value === undefined) {
164
+ clipboardMessage = runtime.i18n.t("errors:clipboardField");
165
+ return;
166
+ }
167
+ if ((target.type === "array" || target.type === "blocks") && !Array.isArray(value)) {
168
+ clipboardMessage = runtime.i18n.t("errors:clipboardRows");
169
+ return;
170
+ }
171
+ if (
172
+ target.type === "group" &&
173
+ (value === null || typeof value !== "object" || Array.isArray(value))
174
+ ) {
175
+ clipboardMessage = runtime.i18n.t("errors:clipboardGroup");
176
+ return;
177
+ }
178
+ const minimum =
179
+ (target.type === "blocks" ? target.blocks?.minRows : target.nested?.minRows) ?? 0;
180
+ const maximum =
181
+ (target.type === "blocks" ? target.blocks?.maxRows : target.nested?.maxRows) ?? 0;
182
+ if (Array.isArray(value) && value.length < minimum) {
183
+ clipboardMessage = runtime.i18n.t("errors:clipboardMinRows", { count: minimum });
184
+ return;
185
+ }
186
+ if (Array.isArray(value) && maximum > 0 && value.length > maximum) {
187
+ clipboardMessage = runtime.i18n.t("errors:clipboardMaxRows", { count: maximum });
188
+ return;
189
+ }
190
+ const pasted = Array.isArray(value)
191
+ ? value.map((row) =>
192
+ row !== null && typeof row === "object" && !Array.isArray(row)
193
+ ? { ...row, _key: crypto.randomUUID() }
194
+ : row
195
+ )
196
+ : value;
197
+ if (Array.isArray(pasted)) owner.setRows(target.path, pasted as Record<string, unknown>[]);
198
+ else owner.set(target.path, pasted);
199
+ clipboardMessage = runtime.i18n.t("fields:pasted", { label: target.admin.label });
200
+ } finally {
201
+ pendingPastes.delete(lifetime);
202
+ lifetime.destroy();
178
203
  }
179
- const pasted = Array.isArray(value)
180
- ? value.map((row) =>
181
- row !== null && typeof row === "object" && !Array.isArray(row)
182
- ? { ...row, _key: crypto.randomUUID() }
183
- : row
184
- )
185
- : value;
186
- if (Array.isArray(pasted)) updateRows(pasted as Record<string, unknown>[]);
187
- else form.set(field.path, pasted);
188
- clipboardMessage = runtime.i18n.t("fields:pasted", { label: field.admin.label });
189
204
  }
190
205
 
191
206
  async function copyRow(index: number) {
@@ -197,24 +212,52 @@
197
212
 
198
213
  async function pasteRow(index: number) {
199
214
  if (editingBlocked || !canAdd) return;
200
- const value = compatibleClipboardValue(await readFieldClipboard(), field, "row");
201
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
202
- clipboardMessage = runtime.i18n.t("errors:clipboardRow");
203
- return;
204
- }
205
- const row = value as Record<string, unknown>;
206
- row._key = crypto.randomUUID();
207
- if (
208
- field.type === "blocks" &&
209
- !resolveBlockTypes(field.blocks).some((block) => block.slug === row.blockType)
210
- ) {
211
- clipboardMessage = runtime.i18n.t("errors:clipboardBlockType");
212
- return;
215
+ const anchor = rows[index];
216
+ if (anchor === undefined) return;
217
+ const owner = form;
218
+ const anchorKey = anchor._key;
219
+ const anchorVariant = anchor.blockType;
220
+ // Removing a row revokes this paste even if another row later reuses its serialized key.
221
+ const anchorMount = owner.rowMountKey(anchor);
222
+ const lifetime = beginPaste(owner);
223
+ try {
224
+ const payload = await readFieldClipboard();
225
+ if (form !== owner || lifetime.readOnly || editingBlocked) return;
226
+ const target = lifetime.schema;
227
+ const currentRows = (owner.get(target.path) as Record<string, unknown>[] | undefined) ?? [];
228
+ const currentIndex = currentRows.findIndex(
229
+ (row) =>
230
+ row._key === anchorKey &&
231
+ row.blockType === anchorVariant &&
232
+ owner.rowMountKey(row) === anchorMount
233
+ );
234
+ const maximum =
235
+ (target.type === "blocks" ? target.blocks?.maxRows : target.nested?.maxRows) ?? 0;
236
+ if (currentIndex < 0 || (maximum > 0 && currentRows.length >= maximum)) return;
237
+ const value = compatibleClipboardValue(payload, target, "row");
238
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
239
+ clipboardMessage = runtime.i18n.t("errors:clipboardRow");
240
+ return;
241
+ }
242
+ const row = value as Record<string, unknown>;
243
+ row._key = crypto.randomUUID();
244
+ if (
245
+ target.type === "blocks" &&
246
+ !resolveBlockTypes(target.blocks).some((block) => block.slug === row.blockType)
247
+ ) {
248
+ clipboardMessage = runtime.i18n.t("errors:clipboardBlockType");
249
+ return;
250
+ }
251
+ const next = [...currentRows];
252
+ next.splice(currentIndex + 1, 0, row);
253
+ owner.setRows(target.path, next);
254
+ clipboardMessage = runtime.i18n.t("fields:pasted", {
255
+ label: rowLabel(row, currentIndex + 1),
256
+ });
257
+ } finally {
258
+ pendingPastes.delete(lifetime);
259
+ lifetime.destroy();
213
260
  }
214
- const next = [...rows];
215
- next.splice(index + 1, 0, row);
216
- updateRows(next);
217
- clipboardMessage = runtime.i18n.t("fields:pasted", { label: rowLabel(row, index + 1) });
218
261
  }
219
262
 
220
263
  function updateRows(next: Record<string, unknown>[]) {
@@ -230,6 +273,10 @@
230
273
  updateRows(next);
231
274
  }
232
275
 
276
+ function errorCount(count: number) {
277
+ return runtime.i18n.t(count === 1 ? "fields:rowError" : "fields:rowErrors", { count });
278
+ }
279
+
233
280
  function rowLabel(row: Record<string, unknown>, index: number) {
234
281
  const block = blockTypes.get(String(row.blockType));
235
282
  if (field.type === "blocks" && block?.admin?.nameField !== undefined) {
@@ -240,16 +287,15 @@
240
287
  runtime.i18n.t("fields:untitled", { label: block.labels.singular })
241
288
  );
242
289
  }
243
- const key =
244
- field.type === "blocks"
245
- ? resolveBlockTypes(field.blocks).find((block) => block.slug === row.blockType)?.admin
246
- ?.rowLabel
247
- : field.nested?.rowLabel;
290
+ const key = field.type === "blocks" ? block?.admin?.rowLabel : field.nested?.rowLabel;
248
291
  const configured = key === undefined ? undefined : row[key];
249
292
  if (configured !== undefined && configured !== null && String(configured).trim() !== "") {
250
293
  return String(configured);
251
294
  }
252
- const number = runtime.i18n.formatNumber(index + 1);
295
+ const number = runtime.i18n.formatNumber(index + 1, {
296
+ minimumIntegerDigits: 2,
297
+ useGrouping: false,
298
+ });
253
299
  return field.nested?.rowLabels?.singular === undefined
254
300
  ? runtime.i18n.t("fields:rowNumber", { number })
255
301
  : runtime.i18n.t("fields:labeledRowNumber", {
@@ -260,8 +306,7 @@
260
306
 
261
307
  function visibleRowLabel(row: Record<string, unknown>, index: number) {
262
308
  if (field.type !== "blocks") return rowLabel(row, index);
263
- const key = resolveBlockTypes(field.blocks).find((block) => block.slug === row.blockType)?.admin
264
- ?.rowLabel;
309
+ const key = blockTypes.get(String(row.blockType))?.admin?.rowLabel;
265
310
  const configured = key === undefined ? undefined : row[key];
266
311
  return configured === undefined || configured === null || String(configured).trim() === ""
267
312
  ? runtime.i18n.t("fields:untitled", {
@@ -273,8 +318,8 @@
273
318
  function blockLabel(row: Record<string, unknown>) {
274
319
  if (field.type !== "blocks") return undefined;
275
320
  return (
276
- resolveBlockTypes(field.blocks).find((block) => block.slug === row.blockType)?.labels
277
- .singular ?? (typeof row.blockType === "string" ? row.blockType : undefined)
321
+ blockTypes.get(String(row.blockType))?.labels.singular ??
322
+ (typeof row.blockType === "string" ? row.blockType : undefined)
278
323
  );
279
324
  }
280
325
 
@@ -304,9 +349,7 @@
304
349
 
305
350
  function fieldsFor(row: Record<string, unknown>) {
306
351
  if (field.type !== "blocks") return field.nested?.fields ?? [];
307
- return (
308
- resolveBlockTypes(field.blocks).find((block) => block.slug === row.blockType)?.fields ?? []
309
- );
352
+ return blockTypes.get(String(row.blockType))?.fields ?? [];
310
353
  }
311
354
 
312
355
  function fieldsForRow(row: Record<string, unknown>, index: number) {
@@ -330,12 +373,11 @@
330
373
  }
331
374
 
332
375
  function reorder(event: Parameters<DragDropEvents["dragend"]>[0]) {
333
- if (editingBlocked) return;
334
- const source = String(event.operation.source?.id ?? "");
335
- const target = String(event.operation.target?.id ?? "");
336
- const from = rows.findIndex((row) => row._key === source);
337
- const to = rows.findIndex((row) => row._key === target);
338
- if (event.canceled || from < 0 || to < 0 || from === to) return;
376
+ const source = event.operation.source;
377
+ if (event.canceled || editingBlocked || !source || !isSortable(source)) return;
378
+ const from = source.sortable.initialIndex;
379
+ const to = source.sortable.index;
380
+ if (from === to) return;
339
381
  const next = [...rows];
340
382
  const [moved] = next.splice(from, 1);
341
383
  if (moved !== undefined) next.splice(to, 0, moved);
@@ -346,10 +388,10 @@
346
388
  {#snippet fieldActions()}
347
389
  <DropdownMenu>
348
390
  <DropdownMenuTrigger
349
- class={buttonVariants({ variant: "ghost", size: "icon-xs" })}
391
+ class={[buttonVariants({ variant: "ghost", size: "icon-xs" }), "ridu-nested-actions-trigger"]}
350
392
  aria-label={runtime.i18n.t("fields:openActions", { label: field.admin.label })}
351
393
  >
352
- <EllipsisIcon class="size-4" />
394
+ <EllipsisIcon />
353
395
  </DropdownMenuTrigger>
354
396
  <DropdownMenuContent>
355
397
  <DropdownMenuItem onSelect={copyField}>
@@ -368,67 +410,82 @@
368
410
  </div>
369
411
  {:else if field.type === "group"}
370
412
  <section
371
- class="grid gap-5"
413
+ class="ridu-group-field"
372
414
  data-field-path={field.path}
373
415
  data-invalid={issues.length > 0}
374
416
  aria-labelledby={`${field.id}-label`}
375
417
  {...controlARIA}
376
418
  >
377
419
  <FieldMessages controlID={field.id} {issues} description={field.admin.description}>
378
- <div class="flex items-start justify-between gap-3 border-b border-control-border pb-2.5">
379
- <div class="min-w-0">
380
- <h2 id={`${field.id}-label`} class="text-[17px] font-medium text-foreground">
381
- {field.admin.label}{#if field.required}<span
382
- class="ridu-field-required"
383
- aria-hidden="true"
384
- >
385
- *
386
- </span>{/if}
420
+ <div class="ridu-group-field__header">
421
+ <div class="ridu-group-field__heading">
422
+ <h2 id={`${field.id}-label`} class="ridu-group-field__title">
423
+ {field.admin.label}
424
+ {#if field.required}
425
+ <span class="ridu-field-required" aria-hidden="true">*</span>
426
+ {/if}
387
427
  </h2>
388
- {#if field.admin.readOnly}<span class="ridu-field-status mt-1">
428
+ {#if field.admin.readOnly}
429
+ <span class="ridu-group-field__status ridu-field-status">
389
430
  {runtime.i18n.t("fields:readOnly")}
390
- </span>{/if}
431
+ </span>
432
+ {/if}
391
433
  </div>
392
434
  {@render fieldActions()}
393
435
  </div>
394
436
  </FieldMessages>
395
437
  <FieldLayout fields={inheritedFields} {form} />
396
- {#if clipboardMessage !== ""}<p
397
- class="font-mono text-[10px] text-foreground-faint"
398
- aria-live="polite"
399
- >
438
+ {#if clipboardMessage !== ""}
439
+ <p class="ridu-nested-field__note" aria-live="polite">
400
440
  {clipboardMessage}
401
- </p>{/if}
441
+ </p>
442
+ {/if}
402
443
  </section>
403
444
  {:else}
404
445
  <section
405
- class="grid gap-3"
446
+ bind:this={repeatedRoot}
447
+ class="ridu-repeated-field"
406
448
  data-field-path={field.path}
407
- data-invalid={issues.length > 0}
449
+ data-invalid={allIssues.length > 0}
408
450
  aria-labelledby={`${field.id}-label`}
409
451
  {...controlARIA}
410
452
  >
411
453
  <FieldMessages controlID={field.id} {issues} description={field.admin.description}>
412
- <div class="flex flex-wrap items-end justify-between gap-3">
413
- <div class="min-w-0">
414
- <h2 id={`${field.id}-label`} class="text-[19px] leading-7 font-medium text-foreground">
415
- {field.admin.label}{#if field.required}<span
416
- class="ridu-field-required"
417
- aria-hidden="true"
418
- >
419
- *
420
- </span>{/if}
454
+ <div class="ridu-repeated-field__header">
455
+ <div class="ridu-repeated-field__heading-wrap">
456
+ <h2 id={`${field.id}-label`} class="ridu-repeated-field__heading">
457
+ {field.admin.label}
458
+ {#if field.required}
459
+ <span class="ridu-field-required" aria-hidden="true">*</span>
460
+ {/if}
461
+ {#if allIssues.length > 0}
462
+ <span class="ridu-error-count">
463
+ {errorCount(allIssues.length)}
464
+ </span>
465
+ {/if}
421
466
  </h2>
422
- {#if field.admin.readOnly}<span class="ridu-field-status mt-1">
467
+ {#if field.admin.readOnly}
468
+ <span class="ridu-repeated-field__status ridu-field-status">
423
469
  {runtime.i18n.t("fields:readOnly")}
424
- </span>{/if}
470
+ </span>
471
+ {/if}
425
472
  </div>
426
- <div class="flex items-center gap-1">
473
+ <div class="ridu-repeated-field__actions">
427
474
  {#if rows.length > 0}
428
- <Button size="xs" variant="ghost" class="px-1.5" onclick={collapseAll}>
475
+ <Button
476
+ size="xs"
477
+ variant="ghost"
478
+ class="ridu-repeated-field__header-action"
479
+ onclick={collapseAll}
480
+ >
429
481
  {runtime.i18n.t("fields:collapseAll")}
430
482
  </Button>
431
- <Button size="xs" variant="ghost" class="px-1.5" onclick={showAll}>
483
+ <Button
484
+ size="xs"
485
+ variant="ghost"
486
+ class="ridu-repeated-field__header-action"
487
+ onclick={showAll}
488
+ >
432
489
  {runtime.i18n.t("fields:showAll")}
433
490
  </Button>
434
491
  {/if}
@@ -436,48 +493,54 @@
436
493
  </div>
437
494
  </div>
438
495
  </FieldMessages>
439
- {#if unknownRows}<p role="alert" class="text-sm text-destructive">
496
+ {#if unknownRows}
497
+ <p role="alert" class="ridu-repeated-field__recovery">
440
498
  {runtime.i18n.t("errors:unknownBlockRecovery")}
441
- </p>{/if}
499
+ </p>
500
+ {/if}
442
501
  <DragDropProvider onDragEnd={reorder}>
443
- <div class="grid gap-3">
502
+ <div class="ridu-repeated-field__rows">
444
503
  {#each rows as row, index (form.rowMountKey(row))}
445
504
  {const rowKey = String(row._key)}
446
505
  {const rowCollapsed = $derived(collapsed.has(rowKey))}
447
506
  {const rowIssues = $derived(form.issuesFor(`${field.path}.${index}`))}
448
507
  {const rowSnapshot = $derived(rowLabelSnapshot(row, index))}
449
508
  {const block = $derived(blockTypes.get(String(row.blockType)))}
450
- <SortableRow id={String(row._key)} {index} disabled={editingBlocked}>
509
+ <SortableRow
510
+ id={String(row._key)}
511
+ {index}
512
+ disabled={editingBlocked}
513
+ invalid={rowIssues.length > 0}
514
+ >
451
515
  {#snippet children(sortable)}
452
- <div class="flex min-h-10 items-center justify-between gap-2 bg-control px-3 py-2">
516
+ <div class="ridu-repeated-row__header">
453
517
  <Button
454
518
  variant="ghost"
455
519
  size="icon-xs"
456
- class="shrink-0 cursor-grab text-foreground-faint hover:bg-background hover:text-foreground active:cursor-grabbing"
520
+ class="ridu-repeated-row__drag"
457
521
  aria-label={runtime.i18n.t("fields:drag", {
458
522
  label: rowLabel(row, index),
459
523
  })}
460
524
  disabled={editingBlocked}
461
525
  {@attach sortable.handleRef}
462
526
  >
463
- <GripVerticalIcon class="size-3.5 text-foreground-faint" />
527
+ <GripVerticalIcon />
464
528
  </Button>
465
- <div
466
- id={`${field.id}-${rowKey}-row-label`}
467
- class="flex min-w-0 flex-1 items-center gap-2 text-[12.5px] text-foreground-muted"
468
- >
469
- <span class="font-mono text-[10px] text-foreground-faint">
470
- {runtime.i18n.formatNumber(index + 1, {
471
- minimumIntegerDigits: 2,
472
- useGrouping: false,
473
- })}
474
- </span>
475
- {#if blockLabel(row) !== undefined}<span
476
- class="rounded-[2px] bg-background px-1.5 py-0.5 text-[11px] text-foreground"
477
- >
529
+ <div id={`${field.id}-${rowKey}-row-label`} class="ridu-repeated-row__heading">
530
+ {#if field.type === "blocks"}
531
+ <span class="ridu-repeated-row__number">
532
+ {runtime.i18n.formatNumber(index + 1, {
533
+ minimumIntegerDigits: 2,
534
+ useGrouping: false,
535
+ })}
536
+ </span>
537
+ {/if}
538
+ {#if blockLabel(row) !== undefined}
539
+ <span class="ridu-repeated-row__type">
478
540
  {blockLabel(row)}
479
- </span>{/if}
480
- <div class="min-w-0 flex-1">
541
+ </span>
542
+ {/if}
543
+ <div class="ridu-repeated-row__label">
481
544
  {#if block?.admin?.nameField !== undefined}
482
545
  <BlockHeader
483
546
  {block}
@@ -500,34 +563,42 @@
500
563
  : { config: customRowLabel.config }}
501
564
  />
502
565
  {/if}
566
+ {#if rowIssues.length > 0}
567
+ <button
568
+ type="button"
569
+ class="ridu-error-count"
570
+ aria-label={`${errorCount(rowIssues.length)}: ${rowIssues[0].message}`}
571
+ onclick={() =>
572
+ repeatedRoot && focusFieldIssue(rowIssues[0].path, repeatedRoot)}
573
+ >
574
+ {errorCount(rowIssues.length)}
575
+ </button>
576
+ {/if}
503
577
  </div>
504
578
  </div>
505
579
  <button
506
580
  type="button"
507
- class="group shrink-0 rounded-[3px] p-1 text-foreground-faint outline-none hover:bg-background hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/70"
581
+ class="ridu-repeated-row__toggle"
508
582
  onclick={() => toggleCollapsed(rowKey)}
509
583
  aria-label={runtime.i18n.t(rowCollapsed ? "fields:expand" : "fields:collapse")}
510
584
  aria-describedby={`${field.id}-${rowKey}-row-label`}
511
585
  aria-expanded={!rowCollapsed}
512
586
  aria-controls={`${field.id}-${rowKey}-content`}
513
587
  >
514
- <ChevronDownIcon
515
- class={[
516
- "ms-auto size-3.5 shrink-0 text-foreground-faint transition-transform duration-200 motion-reduce:transition-none",
517
- rowCollapsed &&
518
- (runtime.i18n.direction === "rtl" ? "rotate-90" : "-rotate-90"),
519
- ]}
520
- />
588
+ <ChevronDownIcon />
521
589
  </button>
522
- <div class="flex items-center gap-0.5">
590
+ <div class="ridu-repeated-row__actions">
523
591
  <DropdownMenu>
524
592
  <DropdownMenuTrigger
525
- class={buttonVariants({ variant: "ghost", size: "icon-xs" })}
593
+ class={[
594
+ buttonVariants({ variant: "ghost", size: "icon-xs" }),
595
+ "ridu-nested-actions-trigger",
596
+ ]}
526
597
  aria-label={runtime.i18n.t("fields:openActions", {
527
598
  label: rowLabel(row, index),
528
599
  })}
529
600
  >
530
- <EllipsisIcon class="size-4" />
601
+ <EllipsisIcon />
531
602
  </DropdownMenuTrigger>
532
603
  <DropdownMenuContent>
533
604
  <DropdownMenuItem
@@ -574,7 +645,7 @@
574
645
  </DropdownMenuItem>
575
646
  <DropdownMenuSeparator />
576
647
  <DropdownMenuItem
577
- class="text-destructive data-[highlighted]:text-destructive"
648
+ class="ridu-repeated-row__remove-action"
578
649
  disabled={editingBlocked ||
579
650
  rows.length <= Math.max(minRows, field.required ? 1 : 0)}
580
651
  onSelect={() =>
@@ -586,32 +657,22 @@
586
657
  </DropdownMenu>
587
658
  </div>
588
659
  </div>
589
- {#if rowIssues.length > 0}
590
- <button
591
- type="button"
592
- class="px-4 py-2 text-start text-sm text-destructive underline"
593
- onclick={() => focusFieldIssue(rowIssues[0].path)}
594
- >
595
- {runtime.i18n.t("fields:rowErrors", { count: rowIssues.length })}: {rowIssues[0]
596
- .message}
597
- </button>
598
- {/if}
599
660
  <div
600
661
  {@attach revealRow(rowKey)}
601
662
  data-field-path={`${field.path}.${index}`}
602
663
  id={`${field.id}-${rowKey}-content`}
603
- class={[
604
- "grid transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none",
605
- rowCollapsed
606
- ? "invisible grid-rows-[0fr] opacity-0"
607
- : "grid-rows-[1fr] opacity-100",
608
- ]}
664
+ class="ridu-repeated-row__content"
665
+ data-collapsed={rowCollapsed}
609
666
  aria-hidden={rowCollapsed}
610
667
  inert={rowCollapsed}
611
668
  >
612
- <div class="min-h-0 overflow-hidden">
613
- <div class="grid gap-5 p-4">
614
- {#if !rowCollapsed}<FieldLayout fields={fieldsForRow(row, index)} {form} />{/if}
669
+ <div class="ridu-repeated-row__content-clip">
670
+ <div class="ridu-repeated-row__fields">
671
+ {#if !rowCollapsed || form
672
+ .pendingEditIssues()
673
+ .some((issue) => issue.path.startsWith(`${field.path}.${index}.`))}
674
+ <FieldLayout fields={fieldsForRow(row, index)} {form} />
675
+ {/if}
615
676
  </div>
616
677
  </div>
617
678
  </div>
@@ -623,11 +684,11 @@
623
684
  <Button
624
685
  variant="ghost"
625
686
  size="sm"
626
- class="w-fit px-0 text-foreground-faint hover:bg-transparent hover:text-foreground"
687
+ class="ridu-repeated-field__add"
627
688
  disabled={editingBlocked || !canAdd}
628
689
  onclick={() => add()}
629
690
  >
630
- <CirclePlusIcon class="size-5" />
691
+ <CirclePlusIcon />
631
692
  {field.type === "blocks"
632
693
  ? runtime.i18n.t("fields:addBlock")
633
694
  : field.nested?.rowLabels?.singular === undefined
@@ -635,7 +696,7 @@
635
696
  : runtime.i18n.t("fields:add", { label: field.nested.rowLabels.singular })}
636
697
  </Button>
637
698
  {#if minRows > 0 || maxRows > 0}
638
- <p class="font-mono text-[10px] text-foreground-faint">
699
+ <p class="ridu-nested-field__note">
639
700
  {runtime.i18n.t("fields:rowLimits", {
640
701
  count: rows.length,
641
702
  minimum:
@@ -649,61 +710,20 @@
649
710
  })}
650
711
  </p>
651
712
  {/if}
652
- {#if clipboardMessage !== ""}<p
653
- class="font-mono text-[10px] text-foreground-faint"
654
- aria-live="polite"
655
- >
713
+ {#if clipboardMessage !== ""}
714
+ <p class="ridu-nested-field__note" aria-live="polite">
656
715
  {clipboardMessage}
657
- </p>{/if}
716
+ </p>
717
+ {/if}
658
718
  </section>
659
719
  {/if}
660
720
 
661
- {#if field.type === "blocks"}
662
- <Dialog bind:open={() => blockPickerOpen, changeBlockPickerOpen}>
663
- <DialogContent class="h-[calc(100vh-4rem)] sm:max-w-4xl">
664
- <DialogHeader>
665
- <DialogTitle class="text-[22px]">
666
- {runtime.i18n.t("fields:add", { label: field.admin.label })}
667
- </DialogTitle>
668
- <DialogDescription class="sr-only">
669
- {runtime.i18n.t("fields:chooseBlockFor", { label: field.admin.label })}
670
- </DialogDescription>
671
- </DialogHeader>
672
- <div class="relative">
673
- <SearchIcon
674
- class="pointer-events-none absolute top-1/2 start-3 size-4 -translate-y-1/2 text-foreground-faint"
675
- />
676
- <Input
677
- class="ps-9"
678
- placeholder={runtime.i18n.t("fields:searchBlock")}
679
- aria-label={runtime.i18n.t("fields:searchBlock")}
680
- bind:value={blockQuery}
681
- />
682
- </div>
683
- {#if visibleBlockTypes.length === 0}
684
- <p class="py-10 text-center text-[13px] text-foreground-muted">
685
- {runtime.i18n.t("fields:noBlocks")}
686
- </p>
687
- {:else}
688
- <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
689
- {#each visibleBlockTypes as block (block.slug)}
690
- <button
691
- type="button"
692
- class="group overflow-hidden rounded-[3px] border border-control-border bg-background text-start transition-colors hover:border-control-border-hover focus-visible:border-ring focus-visible:outline-none"
693
- onclick={() => add(block.slug)}
694
- >
695
- <span
696
- class="flex aspect-video items-center justify-center border-b border-control-border bg-control text-foreground-faint transition-colors group-hover:bg-control-hover group-hover:text-foreground-muted"
697
- >
698
- <LayoutTemplateIcon class="size-8" />
699
- </span>
700
- <span class="block px-3 py-2.5 text-[13px] font-medium text-foreground">
701
- {block.labels.singular}
702
- </span>
703
- </button>
704
- {/each}
705
- </div>
706
- {/if}
707
- </DialogContent>
708
- </Dialog>
721
+ {#if field.type === "blocks" && blockPickerOpen}
722
+ <BlockPicker
723
+ open={blockPickerOpen}
724
+ label={field.admin.label}
725
+ {blocks}
726
+ onOpenChange={changeBlockPickerOpen}
727
+ onSelect={add}
728
+ />
709
729
  {/if}